diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_1_Improvement_Claude_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_1_Improvement_Claude_v1.md new file mode 100644 index 00000000..9a3d6dcb --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_1_Improvement_Claude_v1.md @@ -0,0 +1,258 @@ +# Stage 1 Part 1 개선 전략 보고서 — 두 비효율 리포트 비교 분석 (Claude v1) + +- 비교 대상 문서: + - `Part_1_Inefficiency_Report_Codex_v1.md` (이하 **Codex 리포트**) + - `Part_1_Inefficiency_Report_Claude_v1.md` (이하 **Claude 리포트**) +- 분석 대상 작업: `Stage_1_Part_1_v2.yml`의 `Task_B1_quality_gate_evidence_indexed`(**B1 게이트**), `Task_B2_quality_gate_event_candidates`(**B2 게이트**) 및 그 주변 DAG +- 선별 기준: **최소의 노력**(LLM 추론은 꼭 필요한 곳에만 필요한 수준으로, 100% 결정적 작업은 Python 코드화, 토큰 경제성 최대화)으로 **최대의 효과**(최고 품질 유지, 토큰 경제성 증대, 에이전트 속도 최대화) +- 참고: Claude 리포트 §7.2(대안 B)는 지시에 따라 본 비교·선별에서 제외하였다. + +--- + +## 0. 결론 요약 + +1. 두 리포트는 **서로 독립적으로 동일한 핵심 진단과 동일한 해법 방향에 도달했다.** 진단 — 비효율의 본체는 "fan-out 결과를 모아서 넘기는 것" 자체가 아니라, 그 fan-in을 LLM 컨텍스트로 수행(합계 약 500KB 입력)하고 통합 결과 전체(약 141~146KB)를 LLM 출력 토큰으로 재작성하게 하며, 이 대형 LLM 호출 2개를 `reasoning: high`로 파이프라인 꼬리에서 직렬 실행하는 구조다. 해법 — 두 게이트를 **결정적(Python) reducer로 전환**하고, 결정적 검사로 좁혀진 의미론적 예외가 있을 때만 소형 LLM 판정을 조건부 실행한다(예외 0건이면 LLM 0회). +2. 독립적으로 수렴한 이 합의 사항들은 신뢰도가 가장 높으므로 **전부 채택**한다. 두 리포트가 갈리는 지점은 5개이며, 본 보고서 §3에서 각각 판정했다. 핵심 판정: 예외 판정기는 단일 조건부 task(Claude안)에 Codex의 micro-pack 상한을 내장, `evidence_indexed.json`의 중복 배열은 제거(Claude안)하되 Codex의 호환성 우려를 fixture 검증 항목으로 흡수, audit/handoff 확정은 기존 SHA writer 무수정 유지 + 초소형 GC 신설(Claude안), 잔존 LLM reasoning은 기본 low + Codex의 3개 기준 충족 시에만 high escalation. +3. 실행 결과가 실증한 품질 결함은 합산 5건이다: **E-019 항목 통째 소실**(양 리포트 공통 발견), **candidate 필드 16종 무단 탈락**(Claude 고유), **B1 최종본 동일 배열 2중 수록**(Claude 고유 — Codex는 입력측 중복으로 실측), **dedup 3건 미기록**(Claude 고유), **audit `source_refs` shape 불일치로 handoff에서 ref 전량 소실 + `review_gate_count` 4≠1 오계수**(Codex 고유). 5건 모두 LLM-writer 구조의 산물이며, 결정적 reducer 전환으로 **구조적으로 불가능**해진다. +4. 이번 7/10 실행에서 게이트의 LLM 추론이 추가로 발견한 결함은 **0건**이었다(Claude 리포트 실증 — 유일 finding은 mapper self-warning의 릴레이 1건). 즉 개정 구조였다면 이번 실행의 게이트 구간은 LLM 0회 또는 초소형 1회로 끝났다. 이것이 "정상 경로 LLM 호출 2→0"이라는 개정 목표의 실증 근거다. +5. 예상 효과: 게이트 구간 LLM 입력 ~500KB → 0~수 KB, LLM 출력 ~146KB → 0~1KB, 게이트 구간 벽시계 수 분 → 수십 초(code 2회 + 조건부 소형 LLM ≤1회), Part 1 $2.09 중 게이트 2호출 몫 사실상 제거. 하류(Part 2/3/4)가 소비하는 파일 경로·스키마·sidecar 계약은 완전 동일하므로 **downstream DAG 변경 0건**. + +--- + +## 1. 두 리포트의 공통 진단 — 합의 사항 (전부 채택) + +두 리포트가 독립적으로 일치한 항목. 교차 검증이 완료된 것과 같으므로 추가 검토 없이 개정의 확정 전제로 삼는다. + +| # | 합의 내용 | Codex 근거 | Claude 근거 | +|---|---|---|---| +| C-1 | 게이트의 **논리적 기능과 최종 산출물 3종은 삭제 불가.** `evidence_indexed.json`, `evidence_event_candidates.json`, `stage1_part1_soft_gate_handoff.json`은 Part 2/3/4가 실제로 읽는다 | §6 (Part 2 Stage A, Part 3 LES0, Part 4 FL0 직접 소비) | §6.1 (grep 실증: P2 5개소·P3 4개소·P4 3개소 등) | +| C-2 | **비효율의 본체 3중 구조**: ① fan-in을 LLM 컨텍스트로 수행 ② 통합 결과 전체를 LLM 출력으로 재작성 ③ 대형 호출 2개의 직렬 연쇄 + reasoning high | §3 (B2 게이트 최소 입력 308,269B 실측), §2.1 | §3 (증폭 ①②③), §2.1 | +| C-3 | 검증 항목의 **대부분은 결정적 대체 가능.** completeness·형식·유일성·집합 멤버십·exact dedup·정렬·병합·ID 확정·직렬화·count 집계는 100% Python | §5.1, §5.2 (작업별 판정표) | §4 (10+1개 항목 중 7개 완전 대체, 4개는 "후보 추출(결정적)→판정(LLM)" 분해) | +| C-4 | **E-019 항목 통째 소실** (30 part → 29 items, 후보 74→71) — 그런데 completeness check는 PASS. final writer와 validator를 한 LLM에 맡긴 보존성 위반 | §4.1 (기대/실제 대조표, E-019 후보 3건 명시) | §5 Q-1 (읽기는 했으나 재작성에서 소실; Part 4 `_lien_extinction_notice_scan`·Part 2 notice 도메인 실질 피해 가능성) | +| C-5 | 해법 = **결정적 reducer + 조건부 예외 판정.** 예외 0건이면 LLM task 자체를 실행하지 않음 | §7.1 (deterministic by default 원칙), §7.2 | §7.1 (Part 3 LES1/LES2에서 이미 검증된 샌드위치 패턴 이식) | +| C-6 | **B2 게이트 입력 다이어트**: 참조 무결성에 필요한 것은 finalized E-ID 집합(~30개 문자열, ~0.5KB)뿐인데 91.8KB 전문을 읽음 → compact manifest로 대체 | §3.4, §6, §7.2 B | §2.1, §6.2-2 | +| C-7 | **동일 ordinal B1→B2 mapper streaming은 효율적 — 유지** | §2.1, §8 | §2.1 (DAG 도해) | +| C-8 | **audit의 count·severity·source_refs는 LLM 자유 생성 금지** — 실제 배열 길이와 named object 스키마로 결정적 생성 | §4.2, §7.2 D, §10 | §6.2-6, §7.1 GC | +| C-9 | **모델 일괄 하향은 답이 아님.** 두 게이트는 이미 flash-lite — 개선 수단은 정상 경로의 LLM 호출 수를 2→0으로 만드는 것 | §9 | §4 핵심 관찰, §6.2-5 | +| C-10 | planner(`evidence_shard_plan.json`) 전문 로드 불필요 — 게이트에 필요한 것은 `expected_ordinals`/`shard_count` 수준의 소수 키 | §3.4 (shards/dynamic_fanout 27,165B×2 중복 실측) | §1 표, §6.2-3 | + +--- + +## 2. 각 리포트의 고유 기여 — 통합안에 반영할 항목 + +### 2.1 Codex 리포트 고유 기여 (채택) + +| # | 내용 | 반영처 | +|---|---|---| +| X-1 | **handoff에서 source ref 전량 소실 결함** (§4.2): B2 audit이 `source_refs`를 배열로 출력 → 결정적 writer가 기대하는 named object와 불일치 → 최종 sidecar의 review item ref가 전부 빈 배열. `review_gate_count=4` vs 실제 finding 1건 오계수 동반 | GC의 named object 스키마 강제 + count=len() 규칙 (§4 GC) | +| X-2 | **보존성 invariant의 정식화** (§5.2): `set(final evidence_index) == planner expected`, `len(items) == shard_count`, 모든 input part는 final item 또는 명시적 BLOCK finding에 정확히 1회 대응. dedup 시에도 source evidence item과 provenance는 보존 — 스키마가 표현 못 하면 후보 보존, semantic merge는 Stage 2로 | GA-2의 write 전·후 검증 (§4 GA-2) | +| X-3 | **핫픽스 7종** (§10): 위 invariant + "dedup group 없는데 후보 수 감소 → BLOCK", "ref shape 불일치 시 `HANDOFF_SOURCE_REF_SHAPE_INVALID` 기록" 등 전부 Python 구현 | §6 로드맵 — 별도 선행 구현하지 않고 GA/GC에 1회만 구현 (중복 작업 회피) | +| X-4 | **micro-pack 상한** (§7.2 C): pack당 evidence item ≤2, event candidate ≤8, source excerpt ≤1,500자, exception type 1종만, full planner/final/meeting/evidence 금지, 출력은 `exception_id, decision, severity, source_refs, concise_basis`만 | GB 입력·출력 규칙 (§4 GB) | +| X-5 | **reasoning escalation 3기준** (§7.2 C): ① 서로 다른 source가 같은 법률행위의 날짜·당사자·목적물을 실질 충돌 표시 ② authority drift가 source trace 유효성을 좌우 ③ chronology 판단이 hard block 여부를 좌우 — 이때만 high 허용 | GB 모델 설정 (§5) | +| X-6 | **tool-level allowlist에 의한 authority drift 구조적 차단** (§5.1): B1 mapper의 localdocs 접근을 assigned shard + 명시된 meeting slice로 제한하면 전역 raw 재독해 검사를 표본/예외 감사로 강등 가능 | §6 로드맵 Phase 2 (런타임 지원 여부 확인 후) | +| X-7 | **sub-task별 telemetry 추가 후 v.6 fixture로 A/B 실행** 권고 (§13) — 게이트 외 구간(mapper 등)이 런타임 증가에 기여했을 가능성을 계측으로 규명 | §7 검증 계획 (telemetry 추가) | + +### 2.2 Claude 리포트 고유 기여 (채택) + +| # | 내용 | 반영처 | +|---|---|---| +| Y-1 | **Q-2: candidate 필드 16종 무단 탈락** (§5): YAML의 pass-through 지시에도 28필드 중 12필드만 잔존. 탈락분에 `fraction_ref`·`legal_effect_candidate`·`defense_candidate` 등 v1/v2에서 하류 연계 위해 일부러 강화한 필드 포함 — **복원 필요.** 결정적 조립(dict 복사)이면 필드 소실이 구조적으로 불가능 | GA-2 pass-through 규칙 + fixture 검증 "candidate 28필드 보존" (§4, §7) | +| Y-2 | **Q-3: B1 최종본 2중 수록** (§5): `items`와 `evidence_indexed` 키가 바이트 동일한 30개 배열 — 91.8KB가 parts 합 54.3KB보다 커진 원인. 스키마 문구의 양자택일 모호성을 LLM이 "둘 다"로 해석 | §3 판정 D-1 (단일 배열 확정) | +| Y-3 | **Q-4: `dedup_findings: null`** (§5): 후보 3건 감소가 기록 없는 소실로 남음 | GA-2 dedup 기록 의무화 (§4) | +| Y-4 | **"이번 실행에서 LLM 추론의 추가 발견 0건" 실증** (§4): 전 항목 PASS, 유일 finding은 mapper self-warning 릴레이 → 개정 구조였다면 게이트 LLM 0회 또는 초소형 1회 | §0 결론 4, 개정 효과 추정의 실증 근거 | +| Y-5 | **audit 2종의 하류 소비 0건 grep 실증** (§6.1): `B1_*.json`/`B2_*.json`은 Part 1 내부(B2 게이트, SHA writer)만 소비 → **내부 인터페이스로 자유 재정의 가능.** SHA writer가 읽는 필드 4종만 유지하면 충분 | GA/GC audit 스키마 설계 자유도 (§4) | +| Y-6 | **B2→B1 직렬 의존의 실질 사유 분해** (§2.1): (a) 참조 무결성 — E-ID 목록만 필요 (b) review finding 승계 — audit 일부 필드만 필요. 의존 자체는 정당, 전달 표면만 수십 배 과대 | §4 개정 DAG (의존 유지, 전달물 축소) | +| Y-7 | **기존 파이프라인 자산 재사용 구현 디테일** (§7.1): Part 3 LES2의 budget_gate(항목 수·문자 수 상한) 재사용, `COMMON_CACHE_PREFIX_STAGE_1` 블록은 신규 LLM task(GB)에만 부착, SKILL.md의 code-executor 보일러플레이트(localdocs MCP httpx, `{{__user_hash__}}`)를 GA/GC에 그대로 사용 | §4 task 명세, 구현 노력 최소화 | +| Y-8 | **fixture 기반 검증 계획과 기대값** (§7.3): 7/10 실행물 재사용, 2-run 바이트 동일성(결정론) 검증, 의심 항목 주입 테스트 | §7 검증 계획 | +| Y-9 | **mapper self-warning 승격 판정 경로는 실제 작동 중인 유일한 실질 산출** (§6.3): review finding → sidecar → Part 2 승계 체인 보존 필수 | GB의 승격 판정 역할, §5 삭제 금지 목록 | + +--- + +## 3. 상충 지점 판정 + +두 리포트가 갈리는 5개 지점. 판정 기준은 선별 기준 그대로 — 최소 노력·최대 효과·품질 무손실. + +### D-1. `evidence_indexed.json`의 중복 배열 — **제거 (Claude안 채택, Codex 우려는 검증 항목으로 흡수)** + +- Codex: 내부 parser들이 `items` 우선 소비이므로 제거 가능성 높으나, 외부 Stage 2/구버전 소비자 호환성 검증 전 제거는 비권장. 1차 개정에서는 alias 유지하되 B2 게이트 입력에서만 배제 (§6). +- Claude: 하류 3개 Part 전부 `items` 기준 소비를 grep으로 실증 — 중복 배열 1개 제거 가능 (§6.1). +- **판정**: v3에서 `items` 단일 배열로 확정한다. 근거 — ① Claude 리포트가 소비 지점을 실증했고 ② 결정적 writer 전환 후에도 중복은 파일 크기(및 하류 컴파일러 read량)를 2배로 유지하므로 토큰 경제성 원칙에 반하며 ③ Q-3의 원인인 스키마 모호성("`items` 또는 v.2 호환 구조")을 문구 차원에서 해소해야 재발이 막힌다. Codex의 호환성 우려는 기각이 아니라 **fixture 검증 항목으로 전환**한다: 개정 산출물로 Part 2 Stage A·Part 3 LES0·Part 4 FL0 파서를 실제 통과시켜 확인하고, v.6 외부(Stage 2 등)에 `evidence_indexed` 키를 읽는 소비자가 없는지 1회 grep으로 확정한 뒤 제거를 확정한다. + +### D-2. 예외 판정기 형태 — **단일 조건부 task (Claude안) + Codex micro-pack 상한 내장** + +- Codex: `Task_B12_quality_exception_adjudicator_*`를 dynamic fan-out으로 신설, exception cluster별 개별 LLM task (§7.2 C). +- Claude: 단일 `Task_B1B2_gate_llm_adjudicator`(GB)에 의심 항목 compact 목록 일괄 입력, budget_gate로 상한 (§7.1). +- **판정**: 단일 조건부 GB를 채택한다. 근거 — 실증 예외 건수가 0~1건 수준(Y-4)인데 cluster별 fan-out은 DAG 복잡도와 task 스폰 오버헤드만 추가한다(최소 노력 위반). 다만 Codex의 pack 상한(X-4)을 GB의 입력 구성 규칙으로 그대로 내장해 "단일 호출이 비대해지는" 위험을 차단하고, 의심 항목이 budget_gate 상한을 초과하는 드문 경우에만 fan-out으로 escalation하는 조항을 남긴다. Codex의 "exception 0건이면 LLM task를 생성하지 않음" 원칙은 양안 공통이므로 그대로 유지. + +### D-3. audit/handoff 확정 주체 — **기존 SHA writer 무수정 + 초소형 GC 신설 (Claude안 채택)** + +- Codex: 기존 `Task_B2_SHA256_soft_gate_handoff_writer`를 확장해 audit 2종 작성까지 통합 (§7.2 D). +- Claude: GB 판정 병합·audit 확정은 GC(GA 내 또는 초소형 code-executor)가 수행, 검증된 SHA writer는 무수정 연결 (§7.1). +- **판정**: GC 신설을 채택한다. 근거 — SHA writer는 이번 실행에서 유일하게 결함 없이 작동한 결정적 구성요소이고 Part 2가 소비하는 sidecar 계약의 생산자다. 이를 무수정으로 두면 회귀 위험이 0이 된다. Codex가 D안으로 달성하려던 목표(audit과 handoff의 count·source_refs 불일치 차단)는 GC가 audit을 결정적으로 확정하고 SHA writer가 그것을 그대로 파생하는 구조로 동일하게 달성된다(X-1 반영: named object 스키마 강제, count는 `len()`으로만). SHA writer로의 통합(task 1개 절감)은 v3 안정화 후 선택적 후속 단순화로 남긴다. + +### D-4. 잔존 LLM reasoning 수준 — **기본 low, 유형별 medium, Codex 3기준 충족 시에만 high** + +- Codex: exception 판정 기본 `reasoning=medium, verbosity=low`, 3개 기준 충족 시 high escalation (§7.2 C). +- Claude: 잔존 국소 판정은 low로 충분, 필요시 medium (§6.2-5). +- **판정**: 절충한다. self-warning 승격 분류·coverage 사유 서술 같은 경량 판정은 low, 날짜·법률행위 충돌 판정 등은 medium, Codex의 3기준(X-5)에 해당하는 경우에만 high. 근거 — 집계·분류 작업에 high reasoning이 기여한 증거가 이번 실행에 없고(Y-4), 반대로 법률적 모호성의 최고 난도 케이스에 대한 안전판(escalation)은 품질 무손실 원칙상 남겨야 한다. + +### D-5. authority drift 검사 방식 — **1차: 의심신호 spot-check (Claude안), 2차: tool allowlist (Codex안)** + +- Codex: mapper localdocs를 tool-level allowlist로 제한하거나 `source_support_manifest`를 추가해 구조적으로 차단 → 전역 재독해 검사를 표본/예외 감사로 강등 (§5.1). +- Claude: 상시 검사 삭제, 결정적 통합기가 낸 의심 신호(예: doc_type상 있어야 할 key_dates/key_amounts 공백, mapper self-warning 존재) 문서에 한해 해당 shard 원문만 GB 입력에 첨부 (§6.2-4). +- **판정**: 1차 개정(v3)에서는 Claude안을 채택한다 — GA-1의 결정적 검사에 신호 규칙 몇 줄을 추가하는 것으로 끝나 추가 노력이 거의 0이다. Codex의 tool allowlist는 더 근본적인 차단이므로 **런타임(Liti-agent)이 task별 localdocs 접근 제한을 지원하는지 확인 후 Phase 2로** 적용한다. `source_support_manifest`는 mapper 30개의 출력량을 늘려 mapper 구간 토큰을 증가시키므로(토큰 경제성 상충) allowlist가 불가능한 경우의 대안으로만 보류한다. + +--- + +## 4. 최종 권고 아키텍처 (통합안) + +### 4.1 개정 DAG + +```text +Task_Evidence_shard_planner (Python, 기존 유지) + | + +-- Task_B1_map_doc_001..030 (LLM, 기존 유지) + | | same-ordinal streaming (유지 — C-7) + | v + | Task_B2_map_events_001..030 (LLM, 기존 유지) + | | + | all B1 parts all B2 parts + v | +GA-1 Task_B1_gate_merge_deterministic | + (code-executor) | + - evidence_indexed.json (items 단일) | + - finalized_evidence_id_manifest | + - B1 precheck + suspects | + | | | + | +---------------------------+ + | | + | v + | GA-2 Task_B2_gate_merge_deterministic (code-executor) + | - evidence_event_candidates.json (전 필드 pass-through) + | - conservation 검사 (write 전·후) + | - B2 precheck + suspects + | | + | +-------------+--------------+ + | | suspects = 0 | suspects > 0 + | v v + | (GB 미실행, GB Task_B1B2_gate_llm_adjudicator + | "no suspects" 기록) (LLM flash-lite, 조건부, micro-pack) + | | | + | +-------------+--------------+ + | v + +----------------> GC Task_B12_gate_audit_finalizer (code-executor, 초소형) + - B1/B2 audit 확정 (count=len, named source_refs) + | + v + Task_B2_SHA256_soft_gate_handoff_writer (기존 Python, 무수정) + | + v + OUT +``` + +하류에 보이는 것: `evidence_indexed.json`(중복 배열 제거 외 item 스키마 동일), `evidence_event_candidates.json`(경로·envelope·스키마 동일 + Q-2 탈락 필드 복원으로 오히려 하류 v2 필드 정상 공급), `quality_gates/*.json`(SHA writer 소비 필드 동일), sidecar 완전 동일. **Part 2/3/4 YAML 변경 0건.** + +### 4.2 Task별 명세 + +**GA-1 `Task_B1_gate_merge_deterministic`** (code-executor / Python) + +- 입력: `evidence_shard_plan.json`(코드 파싱이므로 로드 무해 — C-10), `evidence_indexed_parts/E-*.json` 30개. ~~`evidence_all.json` 전문, `client_meeting.md` 전문~~ 상시 로드 삭제 (D-5). +- 수행: expected/received/stale set 비교 → part 단일 item·스키마·`E-{ordinal:03d}` 형식 검사 → ordinal·proposed ID·source pointer·content hash 검사 → stable sort로 전 item 보존 병합 → final `evidence_index` 확정 → exact duplicate(doc_uid·content hash·정규화 title) 및 component coverage 규칙 검사 → **의심 신호 규칙**으로 duplicate-alias 후보쌍·coverage 의심건·authority-drift 의심건 추출 → mapper self-warning 전량 `review_candidates[]`로 승계 (Y-9). +- 출력: `evidence_indexed.json`(**`items` 단일 배열** — D-1), `finalized_evidence_id_manifest.json`(~0.5KB — C-6), B1 precheck/suspects(audit skeleton). + +**GA-2 `Task_B2_gate_merge_deterministic`** (code-executor / Python) + +- 입력: `evidence_event_candidate_parts/E-*.json` 30개, `finalized_evidence_id_manifest.json`, GA-1 suspects. ~~full `evidence_indexed.json`, full B1 audit, full planner 중복 객체~~ 제거 (C-6, Y-6). +- 수행: expected/received/stale set 비교 → schema invariant(empty candidates + null zero_event_reason 포함) → 참조 무결성(manifest 집합 멤버십) → candidate ID 형식·유일성 → `identity_signature` exact dedup(canonical 우선순위 규칙은 YAML에 이미 결정적으로 명문화되어 있으므로 그대로 코드화, **`dedup_findings[]` 기록 의무** — Y-3) → EVT-ID finalize → **candidate 전 필드 dict 복사 pass-through** (Y-1: 항목·필드 소실이 구조적으로 불가능) → 날짜 역전 스캔·REQUIRED_EVENT_SUBKINDS 공백 검사로 chronology/coverage 의심건 추출. +- **보존성 invariant (write 전·후 각 1회 — X-2, X-3):** + +```text +set(final.items[*].evidence_index) == set(planner.expected_evidence_ids) +len(final.items) == planner.shard_count +sum(input part candidates) == sum(final candidates) + len(dedup_findings) +dedup group 없이 candidate 수 감소 → BLOCK +모든 input part는 final item 또는 명시적 BLOCK finding에 정확히 1회 대응 +candidate 필드 수 보존 (dict copy + count 재검증) +``` + +- 출력: `evidence_event_candidates.json`, B2 precheck/suspects, (GA-1 suspects와 합산한) `B12_exception_pack.json`. + +**GB `Task_B1B2_gate_llm_adjudicator`** (LLM, **조건부** — suspects 0건이면 미실행) + +- 입력: 의심 항목 compact 목록만. **micro-pack 상한 (X-4):** pack당 evidence item ≤2, event candidate ≤8, source excerpt ≤1,500자, exception type 1종. authority-drift 의심 문서에 한해 해당 shard 원문만 첨부 (D-5). full planner/final/meeting/evidence 금지. Part 3 LES2의 budget_gate 방식 재사용 (Y-7). 상한 초과 시에만 fan-out escalation (D-2). +- 출력: 항목별 `{exception_id, decision, severity, issue_type, source_refs, concise_basis}`만 (수백 토큰). +- `COMMON_CACHE_PREFIX_STAGE_1` 블록은 이 신규 task에 부착, 기존 블록 무수정 (Y-7). + +**GC `Task_B12_gate_audit_finalizer`** (code-executor / Python, 초소형) + +- GA precheck + GB 판정을 병합해 `quality_gates/B1_evidence_indexed_gate.json`·`B2_event_candidates_gate.json` 확정. audit 2종은 하류 소비 0건이 실증되었으므로(Y-5) SHA writer가 읽는 인터페이스 필드(`hard_gate_findings[]`, `review_findings[]`, `review_gate_summary`, `handoff_export_summary`)와 결정적 체크 결과로 국한해 재정의. +- **모든 count는 실제 배열 길이에서만 계산, `source_refs`는 named object(`evidence_indexes`/`event_candidate_ids`/`meeting_clause_refs`/`doc_titles`) 스키마 강제, shape 불일치는 schema failure** (X-1, C-8). 서사적 자유서술 필드(`stage2_risk_notes[]` 등 소비자 없는 부분)는 결정적 생성 범위로 국한. + +**SHA writer** (기존 Python task): **무수정** (D-3). + +--- + +## 5. 모델·토큰 경제성 판단 + +| 작업군 | 실행 주체 | 모델/설정 | +|---|---|---| +| completeness, 형식, 집합, exact dedup, 정렬, 병합, ID 확정, 직렬화, count, digest | Python (GA-1/GA-2/GC) | LLM 없음 — 정상 경로 게이트 LLM 호출 **2 → 0** (C-9) | +| self-warning 승격 분류, coverage 누락 사유 서술 | GB | `gemini-3.1-flash-lite`, reasoning **low**, verbosity low | +| 모호한 alias·chronology·법률행위 충돌 판정 | GB | 동일 모델, reasoning **medium** | +| Codex 3기준(X-5) 해당 케이스 | GB | 동일 모델, reasoning **high**로 제한 escalation (D-4) | +| B1/B2 mapper 60개 | 기존 유지 | 본 개정 범위 외 (v.6 런타임 증가 원인의 나머지 후보 — X-7의 telemetry로 후속 계측) | + +예상 효과(추정 명시): 게이트 구간 LLM 입력 ~500KB → 0~수 KB, 출력 ~146KB → 0~1KB, 벽시계 수 분 → 수십 초, Q-1~Q-4 및 X-1 결함 구조적 차단, 하류 계약 무변경. 이번 7/10 실행과 동일 입력이었다면 GB는 self-warning 승격 1건 판정의 초소형 호출(또는 규칙 매핑으로 0회)이었다 (Y-4). + +### 삭제·유지·조건부 최종 판정 (통합) + +| 구분 | 항목 | +|---|---| +| **삭제 (정상 경로에서 제거)** | 게이트 2개의 full-context LLM merge·JSON 재작성 역할 / B1의 `evidence_all.json`·`client_meeting.md` 상시 전문 로드 / B2의 `evidence_indexed.json` 전문 로드·B1 audit 전문 import / LLM 생성 count·severity·ID sequence·schema envelope / `evidence_indexed.json` 중복 배열(D-1, fixture 검증 조건부) / `llm_reasoning: high` 상시 적용 | +| **유지 (변경 금지)** | 최종 산출물 3종의 경로·envelope·스키마 / 단일 writer 원칙과 fan-in 지점 / B2→B1 의존(전달물만 manifest로 축소) / HARD·REVIEW 분류와 BLOCK 의미론(rerun_target_task 라우팅 포함) / mapper self-warning 승격 경로 / SHA256 digest_guard sidecar와 그 결정적 writer / 동일 ordinal streaming | +| **조건부 (예외 시에만 LLM)** | suspected alias 판정 / 모호한 authority drift / relation key 불명확 chronology / 문서 의미 해석이 필요한 legal-effect coverage | + +--- + +## 6. 이행 로드맵 + +**Phase 1 — v3 본 개정 (핵심, 즉시 착수)** + +1. GA-1/GA-2/GB/GC를 반영한 `Stage_1_Part_1_v3.yml` 작성. task_procedure: `all Task_B1_map_doc_*`→GA-1, `all Task_B2_map_events_*`+GA-1→GA-2→GB(조건부)→GC→SHA writer(무수정). code-executor 보일러플레이트는 SKILL.md 기존 패턴 재사용 (Y-7). +2. Codex 핫픽스 7종(X-3)은 **별도 선행 작업으로 이중 구현하지 않는다** — 전부 GA-2 write 전·후 검증과 GC 스키마 강제로 1회만 구현된다(최소 노력). 단, v3 반영이 지연될 경우에 한해 임시 방편으로 기존 SHA writer에 conservation set-equality 검사만 추가해 E-019류 소실의 하류 전파를 차단한다. +3. `evidence_indexed` 중복 키 제거 전 v.6 외부 소비자 grep 1회 확정 (D-1). + +**Phase 2 — 구조적 강화 (v3 안정화 후 선택)** + +4. 런타임의 task별 localdocs allowlist 지원 확인 → B1 mapper 접근 제한으로 authority drift 구조 차단 (X-6). 불가 시에만 `source_support_manifest` 검토. +5. GC를 SHA writer로 통합해 task 1개 절감 (D-3의 후속 단순화). +6. sub-task별 telemetry(시간·토큰·read bytes·LLM call 수) 추가 후 v.5/v.6/v3 A/B 계측 — mapper 구간 등 게이트 외 원인 규명 (X-7). + +--- + +## 7. 검증 계획 (fixture 기대값) + +7/10 실행물(`Results_July_10_3_15pm`)을 회귀 fixture로 사용한다 (양 리포트 공통 + Claude §7.3 구체화). + +1. **결정적 통합기 로컬 검증**: 이번 실행의 30개 part를 입력으로 GA-1/GA-2 실행 → + - `evidence_indexed.json`: 30 items, 단일 배열(중복 제거 외 item-level 동등) + - `evidence_event_candidates.json`: **30 items / 후보 74건 / E-019 3건(`lien_extinction_notice_dispatch`, `ongoing_physical_possession`, `lien_violation_candidate`) 보존** / candidate **28필드 전량 보존** / exact duplicate signature group 0개 확인 + - 2-run 바이트 동일성(결정론 확인) + - GC 산출 audit: count == 실제 배열 길이, `source_refs` named object, sidecar 입력 인터페이스 필드 존재 +2. **의심 항목 주입 테스트**: 고의 결측 part, 중복 signature, 날짜 역전, candidate 필드 삭제 → GB 트리거·BLOCK 경로·conservation BLOCK 확인. +3. **하류 무영향 검증**: 개정 산출물로 Part 2 Stage A·Part 3 LES0·Part 4 FL0 파서 통과 확인(특히 D-1의 단일 배열). +4. **실측 재실행**: v3로 Part 1 실행 → 시간·비용을 v.5/v.6과 비교. 목표: 정상 사례 게이트 LLM 호출 0회, 보존성 오류 0, audit count/source_refs 스키마 오류 0. + +--- + +## 8. 채택하지 않은 권고와 사유 + +| 권고 | 출처 | 미채택 사유 | +|---|---|---| +| exception cluster별 dynamic fan-out adjudicator | Codex §7.2 C | 실증 예외 0~1건 규모에 과설계 — 단일 조건부 GB + budget 초과 시 escalation으로 대체 (D-2) | +| SHA writer 확장으로 audit 작성 통합 | Codex §7.2 D | 유일하게 무결함으로 작동한 구성요소의 무수정 유지가 회귀 위험 최소 — GC로 동일 목표 달성, Phase 2 옵션으로 보류 (D-3) | +| `evidence_indexed` alias 1차 유지 | Codex §6 | grep 실증(Y-5 방법론)으로 하류가 `items`만 소비 — 제거하되 호환성 우려는 fixture 검증으로 흡수 (D-1) | +| `source_support_manifest` 즉시 도입 | Codex §5.1 | mapper 30개 출력 토큰 증가로 토큰 경제성 상충 — allowlist 불가 시의 대안으로만 보류 (D-5) | +| adjudicator 기본 reasoning=medium | Codex §7.2 C | 경량 판정에는 low로 충분(실증 근거 Y-4) — 유형별 차등 + 3기준 high escalation으로 절충 (D-4) | +| 게이트 통째 삭제 | — | 양 리포트 공히 불가 판정 (C-1) — 검토 대상에서 제외 | +| 모델 일괄 하향 | — | 이미 flash-lite — 효과 제한적, 호출 자체 제거가 우선 (C-9) | diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_1_Improvement_Codex_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_1_Improvement_Codex_v1.md new file mode 100644 index 00000000..8588cf6b --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_1_Improvement_Codex_v1.md @@ -0,0 +1,632 @@ +# Stage 1 Part 1 효율화 통합 개정 전략 + +## 0. 문서 목적과 최종 결론 + +이 문서는 다음 두 분석서를 비교·대조하여 Stage 1 Part 1 개정에 실제로 반영할 최적 전략을 확정한다. + +- `Part_1_Inefficiency_Report_Codex_v1.md` +- `Part_1_Inefficiency_Report_Claude_v1.md` + +두 문서의 공통 결론은 타당하다. 현재 병목은 fan-in 자체가 아니라 **모든 part와 선행 final을 대형 LLM context에 적재하고, LLM이 final JSON 전체를 다시 작성하게 하는 구조**다. fan-in과 단일 writer 원칙은 유지하되, 병합·정렬·ID 확정·스키마·집합 검증·count·serialization은 Python으로 이전하고 법률적·의미론적 예외만 조건부 LLM에 보내야 한다. + +최종 채택안은 다음과 같다. + +1. 기존 `Task_B1_quality_gate_evidence_indexed`와 `Task_B2_quality_gate_event_candidates`의 **task name과 DAG 위치는 유지**하되 실행유형을 LLM에서 Python deterministic reducer로 변경한다. +2. 두 reducer는 final authority file을 무손실로 작성하고, 판단이 필요한 항목만 하나의 `B1B2_exception_pack`으로 분리한다. +3. exception이 0건이면 LLM을 호출하지 않는다. exception이 있을 때만 bounded micro-pack 단위의 `Task_B1B2_gate_llm_adjudicator_*`를 실행한다. +4. 별도 Python `Task_B1B2_gate_finalize_audit`가 precheck와 adjudication decision을 병합하여 기존 B1/B2 audit schema를 작성한다. +5. 기존 `Task_B2_SHA256_soft_gate_handoff_writer`는 **수정하지 않고 그대로 유지**한다. 이는 변경 범위와 회귀 위험을 줄이는 데 Claude안이 Codex 초안보다 우수한 부분이다. +6. `evidence_indexed.json`, `evidence_event_candidates.json`, B1/B2 audit file, `stage1_part1_soft_gate_handoff.json`의 경로와 downstream 계약은 유지한다. +7. `evidence_indexed.json`의 duplicate alias 제거는 1차 개정에서 보류한다. Python 전환 후에는 토큰 비용이 아니며, 외부 소비자 호환성 확인 없이 제거할 실익보다 위험이 크다. +8. exact `identity_signature` 중복은 Python으로 탐지하되 Stage 1에서 candidate나 source item을 파괴적으로 삭제하지 않는다. canonical group은 audit에 기록하고 semantic merge는 provenance를 보존할 수 있는 downstream 단계로 넘긴다. + +## 1. 두 분석서 비교 + +### 1.1 공통으로 채택할 결론 + +| 쟁점 | Codex | Claude | 통합 판정 | +|---|---|---|---| +| fan-in 자체 | 필요 | 필요 | 유지 | +| 병목 본체 | full-context LLM read/write | 3중 증폭: full read, full rewrite, serial high reasoning | Claude의 3중 설명을 채택 | +| deterministic 검사 | Python 전환 | 7개 완전 대체, 4개 hybrid | 전면 채택 | +| LLM 역할 | semantic exception만 | 조건부 국소 판정만 | 전면 채택 | +| B1/B2 final file | 삭제 불가 | 삭제 불가 | 경로·schema 유지 | +| soft-gate handoff | 삭제 불가 | 삭제 불가 | 기존 SHA writer 유지 | +| E-019 소실 | Critical | Q-1 중대 | 즉시 hotfix 및 회귀 fixture화 | +| audit count/source refs | deterministic 생성 | SHA 소비 필드만 유지 | named schema와 실제 array count 강제 | +| 모델 최적화 | flash-lite medium, low verbosity | flash-lite low | medium 기본, 단순 relay만 low | +| 검증 방식 | conservation invariant | 30/74 fixture, fault injection, 2-run determinism | 합쳐서 채택 | + +### 1.2 Codex 문서에서 반드시 채택할 내용 + +1. 실측 fan-in byte와 duplicate payload 분석 +2. `E-019` 및 candidate 3건 소실에 대한 conservation audit +3. B2 audit의 `source_refs` shape 오류와 review count 불일치 +4. B2가 full `evidence_indexed.json` 대신 finalized evidence ID set만 읽어야 한다는 제안 +5. B1 mapper 접근범위 allowlist와 `source_support_manifest`에 의한 authority drift 구조적 예방 +6. write 전·후 item/candidate 보존성 invariant +7. 정상 경로의 LLM 호출을 2회에서 0회로 만드는 우선순위 +8. sub-task별 runtime/token/read-byte telemetry 도입 + +### 1.3 Claude 문서에서 반드시 채택할 내용 + +1. fan-in 비효율을 `LLM 입력`, `LLM 전체 재작성`, `직렬 high reasoning`의 3중 증폭으로 분리한 설명 +2. 10+1개 검증항목을 deterministic과 hybrid로 분해한 표 +3. final candidate의 field pass-through 위반을 독립적인 중대 결함으로 식별한 점 +4. B1/B2 audit은 Part 2~4가 직접 소비하지 않고 SHA writer만 소비한다는 내부 인터페이스 분석 +5. Part 3/4의 `deterministic propose -> exception adjudication -> deterministic finalize` 패턴을 Part 1에 이식하는 제안 +6. GC를 별도 Python task로 두고 기존 SHA writer를 무수정 유지하는 분리 +7. 현재 30-part fixture, 의도적 결함 주입, 실제 실행 재측정을 포함한 이행계획 + +### 1.4 수정하여 채택하거나 보류할 내용 + +| 제안 | 최종 처리 | 이유 | +|---|---|---| +| `items`/`evidence_indexed` duplicate alias 즉시 제거 | 보류 | v.6 내부는 `items`를 우선 소비하지만 외부 Stage 2·구버전 소비자까지 검증되지 않음 | +| exact signature candidate 자동 삭제 | 수정 채택 | exact grouping은 Python, destructive dedup은 금지. 복수 증거의 동일 사건 입증력을 보존해야 함 | +| 모든 exception을 reasoning low로 처리 | 수정 채택 | self-warning relay는 low, 법률적 chronology·authority 판정은 medium, hard provenance 판단만 high escalation | +| 기존 SHA writer에 audit finalize 책임 추가 | 미채택 | 기존 검증된 digest/handoff writer의 blast radius가 커짐. 별도 GC가 더 안전함 | +| full planner를 새 compact file로 즉시 대체 | 부분 채택 | Python reducer가 full plan을 읽는 것은 토큰 비용이 0. LLM에는 전달하지 않고 GA output에 필요한 compact fields만 남기면 충분 | +| authority drift 상시 full raw 재검사 삭제 | 조건부 채택 | mapper tool allowlist와 source support가 선행되어야 품질 저하 없이 제거 가능 | + +## 2. 실증 결함을 개정의 최우선 요구사항으로 반영 + +### 2.1 E-019 보존성 실패 + +현재 fixture의 결정론적 사실은 다음과 같다. + +```text +planner expected evidence IDs: 30 +B2 part files: 30 +input event candidates: 74 +final event items: 29 +final event candidates: 71 +missing item: E-019 +missing candidates: 3 +exact duplicate signatures: 0 groups +``` + +따라서 개정의 첫 번째 acceptance condition은 단순 PASS 문구가 아니라 다음 machine assertion이어야 한다. + +```text +set(final.items[*].evidence_index) == set(plan.expected_evidence_ids) +len(final.items) == plan.shard_count +input_candidate_count == final_candidate_count + audited_non_destructive_alias_count +``` + +Stage 1에서는 duplicate signature group을 표시하더라도 candidate를 삭제하지 않으므로 1차 개정의 기본식은 `input_candidate_count == final_candidate_count`다. + +### 2.2 candidate field pass-through 실패 + +Claude 문서의 Q-2는 실질적으로 옳지만 field 수는 더 정밀하게 표현할 필요가 있다. + +- mapper candidate union: 28개 key +- final candidate union: 13개 key +- `candidate_id_proposed -> candidate_id`, `source_evidence_index_proposed -> source_evidence_index` rename을 반영한 후 final 전체에서 완전히 사라진 key: 15개 +- retained candidate 71건을 개별 비교하면, **non-empty 값이 있었는데 해당 candidate final에서 사라진 field name은 17종**이다. + +실제 non-empty loss가 확인된 field에는 다음이 포함된다. + +```text +actio_relevance_candidates +amount +commercial_successor_candidates +current_state_candidate +defense_candidate +event_subkind +fraction_ref +legal_effect_candidate +legal_keywords +location +meeting_clause_refs +object_spec +participants +succession_candidate +support_locators +time_precision +time_text +``` + +따라서 Python reducer는 candidate를 새 object로 요약 재구성하면 안 된다. 다음 두 key rename과 `mapper_meta` 제외 외에는 deep-copy pass-through를 원칙으로 한다. + +```text +candidate_id_proposed -> candidate_id +source_evidence_index_proposed -> source_evidence_index +all other candidate keys -> exact pass-through +``` + +### 2.3 audit 및 handoff source ref 실패 + +B2 audit은 `source_refs`를 문자열 배열로 출력했지만 SHA writer는 named object를 기대했다. 이 때문에 E-010/E-011/E-012/E-028가 최종 handoff에서 소실됐다. 또한 `review_findings` 1건을 `review_gate_count=4`로 잘못 계산했다. + +GC는 다음 schema를 강제하고 count를 직접 계산해야 한다. + +```json +{ + "source_refs": { + "evidence_indexes": [], + "event_candidate_ids": [], + "meeting_clause_refs": [], + "doc_titles": [] + } +} +``` + +`review_gate_count`, `b1_review_count`, `b2_review_count`는 LLM 출력값을 신뢰하지 않고 최종 array를 기준으로 Python이 계산한다. + +## 3. 최종 개정 아키텍처 + +### 3.1 설계 원칙 + +```text +기계적으로 판정 가능한 것: Python이 판정·작성 +법률적 의미가 모호한 것: exception으로 격리 +exception이 없는 정상 경로: LLM 호출 0회 +exception이 있는 경로: 작은 micro-pack만 LLM 판정 +LLM은 final artifact 수정권 없음 +final artifact와 audit: Python single writer +``` + +### 3.2 개정 DAG + +```text +Task_Evidence_shard_planner + | + v + Task_B1_map_doc_* --------------------------------+ + | same ordinal | + v | + Task_B2_map_events_* | + | | + | all B2 parts all B1 parts + | | + | v + | Task_B1_quality_gate_evidence_indexed + | [기존 이름 유지, Python GA-1] + | - evidence_indexed.json + | - B1 precheck/review candidates + | - finalized E-ID manifest + | | + +------------------------------------------+ + | + v + Task_B2_quality_gate_event_candidates + [기존 이름 유지, Python GA-2] + - evidence_event_candidates.json + - B2 precheck/review candidates + - combined exception routing manifest + | + +-----------+------------+ + | exception_count = 0 | exception_count > 0 + v v + empty decision artifact Task_B1B2_gate_llm_adjudicator_* + [bounded dynamic fan-out] + | | + +-----------+------------+ + v + Task_B1B2_gate_finalize_audit [Python GC] + - B1 gate audit + - B2 gate audit + | + v + Task_B2_SHA256_soft_gate_handoff_writer + [기존 구현 무수정] + | + v + OUT +``` + +### 3.3 기존 task name 유지의 이유 + +- planner barrier와 `task_procedure` 변경량을 줄인다. +- 기존 로그·운영 대시보드·rerun target 이름의 연속성을 보존한다. +- Part 2~4는 task name이 아니라 file contract를 소비하므로 내부 실행유형 변경은 downstream에 비가시적이다. +- 새로 필요한 이름은 adjudicator wildcard와 GC 두 개뿐이다. + +## 4. Task별 개정 명세 + +### 4.1 GA-1: `Task_B1_quality_gate_evidence_indexed` + +실행유형: + +```yaml +mcp: code-executor +language: python +``` + +입력: + +- `evidence_shard_plan.json` +- `evidence_indexed_parts/E-*.json` +- 해당 exception에서만 `evidence_shards/E-xxx.json` + +상시 입력에서 제거: + +- full `evidence_all.json` +- full `client_meeting.md` + +주요 처리: + +1. planner status, expected ordinal, stale part, file set 검사 +2. part마다 실질 item이 정확히 1개인지 검사 +3. `evidence_index_proposed`, ordinal, source pointer, hash 정합 검사 +4. item 전체를 deep copy하고 `evidence_index`만 deterministic finalize +5. stable ordinal sort +6. exact duplicate는 hash/doc UID/title fingerprint로 group만 작성 +7. semantic flag별 component presence rule 실행 +8. suspicious alias/component/authority item만 exception candidate로 생성 +9. `evidence_indexed.json` 작성 +10. `B1_precheck.json`과 finalized E-ID list 작성 + +1차 개정에서는 `items`와 legacy `evidence_indexed` alias를 모두 유지한다. 단 B2 reducer와 LLM adjudicator는 legacy alias를 읽지 않는다. + +authority drift 비용을 줄이기 위한 보강: + +- mapper localdocs를 assigned shard와 허용된 meeting slice로 제한 +- part에 `source_support_manifest` additive field 추가 +- source ordinal/hash/locator mismatch는 Python hard finding +- 의미가 불명확한 경우에만 해당 shard를 exception micro-pack에 포함 + +### 4.2 GA-2: `Task_B2_quality_gate_event_candidates` + +실행유형: + +```yaml +mcp: code-executor +language: python +``` + +입력: + +- `evidence_shard_plan.json` +- `evidence_event_candidate_parts/E-*.json` +- `B1_precheck.json.finalized_evidence_indexes` +- `B1_precheck.json.review_candidates` + +제거할 입력: + +- full `evidence_indexed.json` +- final B1 audit + +주요 처리: + +1. part count, expected/received ID set, stale part 검사 +2. `event_candidates`/`zero_event_reason` invariant 검사 +3. `source_evidence_index_proposed` membership 검사 +4. proposed candidate ID format·ordinal·seq uniqueness 검사 +5. candidate object deep-copy 후 두 ID key만 rename +6. 모든 optional component field를 그대로 보존 +7. exact signature group 탐지 및 audit candidate 생성 +8. candidate나 evidence item은 삭제하지 않음 +9. parseable date와 명시적 group key에 한해 chronology rule 실행 +10. semantic flag/component와 REQUIRED_EVENT_SUBKINDS의 coverage rule 실행 +11. ambiguous chronology/coverage만 exception 생성 +12. write 전·후 conservation invariant 실행 +13. `evidence_event_candidates.json`과 `B2_precheck.json` 작성 + +### 4.3 GB: `Task_B1B2_gate_llm_adjudicator_*` + +역할은 final writer가 아니라 **분류 전용 adjudicator**다. + +허용 입력: + +- 하나의 exception type +- 관련 evidence 최대 2건 +- 관련 candidate 최대 8건 +- source excerpt 총 1,500자 이하 +- deterministic precheck와 rule violation 요약 + +금지 입력: + +- full planner +- full evidence/index/event final +- full meeting +- 전체 B1/B2 part +- 관련 없는 exception + +허용 출력: + +```json +{ + "exception_id": "P1GEX-0001", + "decision": "PASS|REVIEW|HARD_WARNING|BLOCK", + "issue_type": "...", + "source_refs": { + "evidence_indexes": [], + "event_candidate_ids": [], + "meeting_clause_refs": [], + "doc_titles": [] + }, + "concise_basis": "...", + "rerun_target_task": null +} +``` + +금지 권한: + +- final item/candidate 생성·삭제·수정 +- ID 재부여 +- dedup 실행 +- audit count 계산 +- final JSON serialization + +### 4.4 GC: `Task_B1B2_gate_finalize_audit` + +Python single writer로 다음을 수행한다. + +1. B1/B2 deterministic precheck와 GB decision 병합 +2. mechanical BLOCK을 LLM이 downgrade하지 못하도록 precedence 적용 +3. source ref named schema와 membership 검증 +4. `hard_gate_findings[]`, `review_findings[]`, severity, count 계산 +5. `quality_gates/B1_evidence_indexed_gate.json` 작성 +6. `quality_gates/B2_event_candidates_gate.json` 작성 +7. SHA writer가 요구하는 기존 필드 보존 + +기존 SHA writer는 top-level finding과 `checks[*]` 내부 finding을 모두 수집한다. 따라서 GC는 동일 finding을 두 위치에 중복 기록하지 않는다. canonical finding은 top-level 배열에 한 번만 두고, 각 check에는 `finding_ids[]`만 참조시키거나 finding 배열을 비워 둔다. `review_items` 40건 상한을 넘는 경우에는 조용히 절단하지 않고 overflow count와 `BLOCK_REVIEW` routing을 audit에 남긴다. + +기존 SHA writer는 GC가 작성한 두 audit과 두 final artifact를 읽어 digest와 soft-gate handoff를 기존 방식대로 생성한다. + +## 5. 검증항목별 실행유형 확정 + +| 검증 | Python 처리 | LLM 처리 | +|---|---|---| +| B1 shard completeness | 전부 | 없음 | +| B1 E-ID determinism | 전부 | 없음 | +| B1 duplicate alias | exact/fuzzy 후보 추출 | 후보쌍의 실질 동일성만 | +| B1 component coverage | required field rule | 문서 의미상 required 여부가 모호할 때만 | +| B1 authority drift | access/hash/locator 위반 | 의심 source와 part의 의미 비교만 | +| B2 schema invariant | 전부 | 없음 | +| B2 event completeness | 전부 | 없음 | +| B2 evidence reference integrity | 전부 | 없음 | +| B2 candidate ID/signature | format/set/group 전부 | semantic 동일성 판단이 꼭 필요할 때만 | +| B2 chronological ordering | date parse/reversal 후보 | relation group·법적 순서가 모호할 때만 | +| B2 legal-effect coverage | flag/subkind rule | 누락인지 문서 비해당인지 모호할 때만 | +| severity/count/audit schema | 전부 | 없음 | + +## 6. Exception routing decision tree + +```text +GA mechanical validation + | + +-- parse/schema/provenance/set failure? + | | + | +-- YES -> deterministic BLOCK + | | exception LLM 호출 금지 + | | + | +-- NO + | + +-- semantic suspect 존재? + | + +-- NO -> exception_count=0 + | empty decision artifact + | GC로 직행 + | + +-- YES + | + +-- 단순 mapper self-warning relay? + | -> deterministic mapping 또는 reasoning=low + | + +-- alias/component/coverage ambiguity? + | -> reasoning=medium + | + +-- authority/chronology가 BLOCK 여부 좌우? + -> reasoning=high escalation 허용 +``` + +Batch 정책: + +- exception 1~8건: exception type별 한 번의 compact 호출 +- 9건 이상: type·asset/claim group별 micro-pack 분할 +- micro-pack 최대 8건, 병렬 실행 `max_concurrency=4` +- 동일 exception을 여러 pack에 중복 수록 금지 +- total character budget 초과 항목은 자동 truncate하지 않고 `BLOCK_REVIEW` queue로 분리 +- GC는 `B1B2_exception_manifest.json`을 항상 기다리고, manifest가 선언한 generated adjudicator task 전체를 barrier로 기다린다. generated task가 0개이면 빈 decision artifact를 정상 완료로 간주한다. + +정상 fixture에서는 deterministic mapper self-warning을 규칙으로 relay할 수 있으면 GB 0회, 의미 판정이 필요하면 초소형 1회가 목표다. + +## 7. IO 및 writer 계약 + +### 7.1 내부 신규 artifact + +| 파일 | Writer | Reader | 목적 | +|---|---|---|---| +| `stage1_tmp/quality_gate/B1_precheck.json` | GA-1 | GA-2, GC | E-ID list, mechanical findings, B1 review candidates | +| `stage1_tmp/quality_gate/B2_precheck.json` | GA-2 | GC | conservation result, mechanical findings, B2 review candidates | +| `stage1_tmp/quality_gate/B1B2_exception_manifest.json` | GA-2 | GB router, GC | exception count, micro-pack descriptors, zero-exception status | +| `stage1_tmp/quality_gate/B1B2_exception_decisions/*.json` | GB | GC | 국소 판정 결과 | + +### 7.2 유지할 외부 artifact + +| 파일 | Writer | 계약 | +|---|---|---| +| `evidence_indexed.json` | GA-1 | 기존 envelope·item schema 유지 | +| `evidence_event_candidates.json` | GA-2 | `evidence_event_candidates.v1`, 전 field pass-through | +| `quality_gates/B1_evidence_indexed_gate.json` | GC | SHA writer 소비 필드 유지 | +| `quality_gates/B2_event_candidates_gate.json` | GC | SHA writer 소비 필드 유지 | +| `quality_gates/stage1_part1_soft_gate_handoff.json` | 기존 SHA writer | schema v1, digest 4종, hard/review summary 유지 | + +단일 writer 경계: + +```text +GA-1 only -> evidence_indexed.json +GA-2 only -> evidence_event_candidates.json +GC only -> B1/B2 audit files +SHA only -> stage1_part1_soft_gate_handoff.json +``` + +## 8. 삭제·유지·보류 판정 + +### 8.1 즉시 삭제 또는 정상 경로에서 제거 + +- B1/B2 gate의 full-context LLM read +- LLM의 final JSON 전체 재작성 +- LLM의 ID finalize·sort·merge·count·serialization +- B1의 full `evidence_all.json`·meeting 상시 재독해 +- B2의 full `evidence_indexed.json` read +- B2의 B1 final audit read +- exact set/regex/signature 검사를 위한 LLM reasoning +- `simple concat 금지, 반드시 LLM 검증` 규칙 + +마지막 규칙은 다음으로 교체한다. + +```text +deterministic merge는 허용하되, conservation·schema·writer-boundary gate를 모두 통과해야 한다. +semantic exception만 LLM 판정 대상으로 한다. +``` + +### 8.2 반드시 유지 + +- B1/B2 fan-in과 single writer +- 동일 ordinal B1→B2 mapper streaming +- final 두 authority file +- hard/review 분리 및 BLOCK 의미론 +- mapper self-warning 승계 경로 +- B1→B2 finalized E-ID 의존 +- SHA256 digest와 soft-gate handoff +- rerun target routing + +### 8.3 1차 개정에서 보류 + +- `evidence_indexed` legacy alias 제거 +- planner의 duplicate `shards`/`dynamic_fanout` 구조 개편 +- cross-document candidate destructive dedup +- B1/B2 reducer를 하나의 combined task로 합치기 +- mapper 모델 변경 + +이 항목들은 Python 전환의 핵심 효과와 무관하거나 호환성 위험이 더 크다. + +## 9. 모델 및 토큰 경제성 전략 + +현재 gate 모델은 이미 `gemini-3.1-flash-lite`이므로 더 약한 모델로 일괄 교체하는 것보다 호출을 없애는 것이 우선이다. + +권장값: + +```yaml +llm_provider: google +llm_model: gemini-3.1-flash-lite +llm_reasoning: medium +llm_verbosity: low +``` + +단순 self-warning enum mapping은 Python으로 처리한다. LLM이 필요하더라도 low reasoning 별도 profile을 사용할 수 있다. high reasoning은 provenance나 chronology의 hard-block 여부가 실제로 걸린 exception에만 허용한다. + +토큰 절감 원칙: + +1. common prefix는 GB에만 한 번 적용 +2. final JSON을 LLM 출력으로 생성하지 않음 +3. full plan/final/meeting/evidence를 GB에 전달하지 않음 +4. concise basis 200자 이하 +5. source excerpt total budget 1,500자 +6. zero-exception이면 model spawn 자체를 생략 + +## 10. 구현 순서 + +### Phase 0: 보존성 hotfix + +현행 v.6에 먼저 다음 Python post-write 검사를 추가한다. + +1. final evidence/event item set equality +2. candidate count conservation +3. candidate deep field pass-through +4. dedup group과 감소 수의 일치 +5. named `source_refs` schema +6. audit count와 실제 array length 일치 + +불일치 시 `PASS`를 금지하고 `BLOCK`한다. + +### Phase 1: GA-1/GA-2 구현 + +1. 기존 두 gate task의 이름을 유지한 채 `code-executor`로 교체 +2. final 두 파일을 deep-copy 기반으로 작성 +3. precheck와 combined exception manifest 생성 +4. 현행 fixture로 deterministic output 검증 + +### Phase 2: GB/GC 연결 + +1. dynamic zero-or-more adjudicator fan-out 추가 +2. strict decision JSON schema 적용 +3. GC single writer로 B1/B2 audit 생성 +4. 기존 SHA writer 무수정 연결 + +### Phase 3: downstream 및 성능 검증 + +1. Part 2 Stage A normalization 실행 +2. Part 3 LES0 input pack compiler 실행 +3. Part 4 FL0 fact source pack compiler 실행 +4. Part 1 wall time, model calls, tokens, read bytes 비교 +5. v.6과 artifact-level semantic diff 수행 + +## 11. 검증 계획 + +### 11.1 현재 fixture 회귀시험 + +```text +evidence_indexed items == 30 +event items == 30 +event candidates == 74 +E-019 retained == true +E-019 candidate count == 3 +candidate non-ID fields deep-pass-through == true +review source refs E-010/E-011/E-012/E-028 retained == true +``` + +### 11.2 fault injection + +| 주입 결함 | 기대 결과 | +|---|---| +| B1 part 1개 삭제 | deterministic BLOCK, LLM 0회 | +| stale ordinal 추가 | deterministic BLOCK, LLM 0회 | +| invalid E-ID reference | deterministic BLOCK, LLM 0회 | +| empty candidates + null reason | deterministic BLOCK, LLM 0회 | +| exact signature group | 전 candidate 보존 + audit group 기록 | +| parseable date 역전 | exception 생성 또는 명시적 deterministic violation | +| ambiguous alias pair | GB 호출, source pair만 입력 | +| malformed source_refs | GC schema failure | +| exception 0건 | GB spawn 0회, GC 직행 | +| exception budget 초과 | micro-pack 분할 또는 BLOCK_REVIEW | + +### 11.3 결정론 검증 + +- 같은 입력으로 final 두 artifact를 2회 생성했을 때 byte-identical +- audit은 `executed_at` 제외 후 semantic-identical +- array order는 planner ordinal과 candidate seq에 의해 고정 +- 모든 count는 실제 array에서 재계산 + +### 11.4 성능 telemetry + +- task별 wall time +- input/output token +- LLM call count +- exception count 및 pack bytes +- localdocs read file count/bytes +- Python reducer duration +- cache hit +- final artifact bytes + +성공 KPI: + +```text +정상 gate LLM calls: 2 -> 0 +exception gate input: full global context -> bounded micro-pack +final conservation errors: 0 +field pass-through errors: 0 +audit source/count errors: 0 +Part 2~4 contract regressions: 0 +``` + +## 12. 최종 권고 + +가장 적합한 개정은 Claude의 `GA-1 -> GA-2 -> GB -> GC -> SHA writer` 분리 구조를 뼈대로 사용하되, Codex 문서의 보존성·source ref·compact dependency·authority isolation 요구사항을 결합하는 것이다. + +다만 변경량을 최소화하기 위해 GA-1/GA-2는 새 이름으로 추가하지 않고 기존 B1/B2 quality gate task를 Python으로 전환한다. 기존 SHA writer는 건드리지 않으며, audit finalize만 GC로 분리한다. final artifact는 Python deep-copy로 생성하고 LLM은 어떠한 수정 권한도 갖지 않는다. + +시행 우선순위는 다음과 같다. + +1. E-019/item/candidate/field conservation hotfix +2. B1/B2 deterministic reducer 전환 +3. conditional bounded exception adjudicator +4. deterministic audit finalizer +5. 기존 SHA writer 및 downstream 계약 회귀시험 +6. runtime·token A/B 측정 + +이 경로가 가장 적은 위험과 작업으로 최고 품질, 토큰 경제성, 실행속도를 동시에 달성한다. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_1_Improvement_Strategy_Claude_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_1_Improvement_Strategy_Claude_v1.md new file mode 100644 index 00000000..569d9166 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_1_Improvement_Strategy_Claude_v1.md @@ -0,0 +1,455 @@ +# Stage 1 Part 1 개정 작업 명세서 (Part_1_Improvement_Strategy_Claude_v1) + +- **개정 대상**: `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_1_v2.yml` (3,404행) +- **개정 산출물**: `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_1_v3.yml` (신규 파일 — 원본 v2는 절대 덮어쓰지 않는다) +- **전략 근거**: `YAML_Prompts/1. Stage_1/v.6/Part_1_Improvement_Claude_v1.md` (이하 **개선전략서**) +- **본 문서의 성격**: 개정 실행자(LLM)가 본 문서만 읽고 v3 YAML을 100% 작성할 수 있도록, 편집 대상 anchor·신규 task 명세·데이터 계약·검증 기준을 빠짐없이 기술한 작업 명세서다. 본 문서와 v2 원본이 충돌하면 본 문서가 우선한다. 본 문서가 침묵하는 모든 사항은 **v2 원본을 그대로 보존**한다. + +--- + +## 0. 개정 목표와 판단 배경 (요약) + +### 0.1 무엇을 바꾸는가 + +v2의 두 LLM quality gate(`Task_B1_quality_gate_evidence_indexed`, `Task_B2_quality_gate_event_candidates`)는 합계 약 500KB를 LLM 컨텍스트로 읽고 약 146KB의 최종 JSON을 LLM 출력 토큰으로 재작성하며, `reasoning=high`의 대형 호출 2개가 파이프라인 꼬리에서 직렬 실행된다. 2026-07-10 실측에서 이 구조는 (Q-1) E-019 항목 통째 소실, (Q-2) candidate 필드 16종 무단 탈락, (Q-3) B1 최종본 동일 배열 2중 수록, (Q-4) dedup 미기록, (X-1) audit `source_refs` shape 불일치로 handoff에서 ref 전량 소실이라는 5건의 실증 결함을 만들었다. 반면 LLM 추론이 추가로 발견한 결함은 0건이었다. + +개정은 개선전략서의 확정 아키텍처를 따른다: **두 게이트를 결정적(Python code-executor) reducer로 전환**하고(GA-1, GA-2), 결정적 검사로 좁혀진 의미론적 의심 항목이 있을 때만 소형 LLM 판정(GB)을 실행하며, audit 확정은 초소형 결정적 finalizer(GC)가 수행하고, 검증된 기존 `Task_B2_SHA256_soft_gate_handoff_writer`는 **무수정**으로 연결한다. + +### 0.2 변호사 관점의 불변 원칙 (법률 품질 보존) + +1. **증거 보존성이 최우선이다.** 소멸청구 통지서(E-019)처럼 유치권 소멸·통지 도달 lifecycle을 좌우하는 증거가 무단 소실되면 Part 4 `_lien_extinction_notice_scan`과 Part 2 notice 도메인이 오염된다. 병합·직렬화를 기계에 맡기는 1차 목적이 바로 이 보존성이다. +2. **법률적 불확실성은 review 큐로, 기계적 실패만 BLOCK으로.** v2의 HARD/REVIEW 분류 철학(suspected alias, coverage concern, chronology ambiguity는 review-only, 자동 BLOCK 금지)을 그대로 계승한다. 결정적 코드는 severity를 임의로 상향하지 않는다. +3. **canonical 결정의 법률적 우선순위 유지.** dedup canonical 규칙 (i) raw event > derived legal-effect event (ii) higher confidence (iii) more support_locators (iv) earlier ordinal, alias canonical 규칙 (i) 작은 ordinal (ii) 더 명시적 title (iii) 더 구체적 key_dates/key_amounts는 v2에 이미 결정적으로 명문화된 법률 판단의 산물이므로 코드로 그대로 이식한다. +4. **mapper self-warning의 승격 경로 보존.** "상계 항변 등 Stage 2 검토 필요" 류의 mapper self-warning → review finding → sidecar → Part 2 승계 체인은 이번 실행에서 실제 작동한 유일한 실질 산출이며, 반드시 유지한다. + +### 0.3 개선전략서 대비 본 명세서의 구체화 결정 2건 (근거 포함) + +| 결정 | 개선전략서 표기 | 본 명세서 확정안 | 근거 | +|---|---|---|---| +| N-1. GA-1/GA-2의 task 이름 | `Task_B1_gate_merge_deterministic`, `Task_B2_gate_merge_deterministic` (예시 명명) | **기존 이름 유지**: `Task_B1_quality_gate_evidence_indexed`, `Task_B2_quality_gate_event_candidates` (실행 유형만 LLM → code-executor로 치환) | v2의 planner 코드(약 856~904행)는 plan JSON 내부에 두 게이트의 task 이름을 문자열로 내장하고("next": "Task_B1_quality_gate_evidence_indexed" 등), B1/B2 mapper 프롬프트(1026행, 1621행 등)도 이름을 참조한다. 이름을 유지하면 planner·mapper를 **한 글자도 수정하지 않아도 되어** 편집 표면과 회귀 위험이 최소화된다. 개선전략서의 "최소 변경·하류 무영향" 원칙이 예시 명명보다 우선한다 | +| N-2. GB 조건부 실행 방식 | "suspects 0건이면 미실행" | **정적 task + empty-pack 즉시 종료** (기본): GB는 항상 스케줄되지만 첫 동작이 exception pack 1개 파일(수백 byte)만 읽는 것이고, `exception_count==0`이면 즉시 `"NO_SUSPECTS"` 출력 후 종료한다 | v2에서 확인되는 0-instance fan-out 메커니즘(planner의 `fanout_payload=[]`)은 mapper 미생성 시 후속 barrier(`all Task_B12_*`)가 정상 resolve되는지 v2 YAML만으로 보증할 수 없다. 정적 체인은 런타임 가정이 0이며, empty-pack 조기 종료 호출의 비용은 cache prefix + 소형 입력으로 무시 가능 수준이다. 런타임에서 0-instance wildcard barrier resolve가 실증되면 후속 버전에서 dynamic fan-out으로 전환할 수 있다(§9 Phase 2) | + +--- + +## 1. 절대 불변 계약 (개정 실행자가 위반하면 안 되는 것) + +아래 항목은 Part 2/3/4 및 기존 결정적 구성요소가 소비하는 인터페이스다. **v3에서 바이트 하나도 바꾸지 않는다.** + +1. **최종 산출물 경로**: `evidence_indexed.json`, `evidence_event_candidates.json`, `quality_gates/B1_evidence_indexed_gate.json`, `quality_gates/B2_event_candidates_gate.json`, `quality_gates/stage1_part1_soft_gate_handoff.json`. +2. **`evidence_event_candidates.json` envelope**: top-level object, `schema_version: "evidence_event_candidates.v1"`, `items` 배열. 각 item은 `evidence_index`, `title`, `doc_type`, `source_pointer`, 그리고 non-empty `event_candidates` 또는 non-null `zero_event_reason`. +3. **`evidence_indexed.json` envelope**: top-level object, `schema_contract_version`, `items` 배열. 각 item은 v.2 item schema(`evidence_index`, `doc_uid`, `title`, `title_normalized`, `doc_type`, `key_*`, `source_pointer`, optional component fields). ※ `evidence_indexed` alias 배열의 처리만 §3 STEP 0-3의 조건부 결정을 따른다. +4. **`Task_B2_SHA256_soft_gate_handoff_writer`**: task 정의 전체(v2 2950~3363행)를 **무수정** 복사한다. 이 task의 `INPUT_PATHS` 4개 경로, sidecar 스키마, digest 로직, review item 변환 로직은 이번 실행에서 유일하게 무결함으로 작동한 구성요소다. +5. **audit 파일이 SHA writer에 제공해야 하는 필드** (writer 코드가 실제로 읽는 필드 — v2 3183~3310행에서 실증): + - top-level 및 `checks[]` 내부의 `hard_gate_findings[]`, `review_findings[]` + - `overall_severity`, `downstream_progression`, `stage2_auto_progression_allowed`, `block_reasons[]` + - 각 review finding의 `issue_type`(또는 `check_id`/`source_check`), `severity`, `domain_hints`, `target_stage_hints`, `target_task_hints`, `review_instruction`, `candidate_use_policy`(허용값: `prioritize_if_supported` | `preserve_as_needs_review` | `do_not_generate_without_support`), `concise_basis`, `gate_finding_id`, `lawyer_workflow_role` + - **`source_refs`는 반드시 named object** `{"evidence_indexes": [], "event_candidate_ids": [], "meeting_clause_refs": [], "doc_titles": []}` — writer의 `source_refs()` 함수는 dict가 아니면 nested ref를 전부 무시한다(7/10 실행의 X-1 결함이 정확히 이 지점에서 발생). 배열형 `source_refs` 생성은 스키마 위반으로 금지한다. + - review finding은 writer가 40개에서 절단하므로 GC도 최대 40개로 제한한다. +6. **task 이름**: `Task_A_client_goal`, `Task_Evidence_shard_planner`, `Task_B1_map_doc_*`, `Task_B2_map_events_*`, `Task_B1_quality_gate_evidence_indexed`, `Task_B2_quality_gate_event_candidates`, `Task_B2_SHA256_soft_gate_handoff_writer` 전부 유지. +7. **수정 금지 task**: `Task_A_client_goal`, `Task_Evidence_shard_planner`, `Task_B1_map_doc_*`, `Task_B2_map_events_*`, `Task_B2_SHA256_soft_gate_handoff_writer` — 이 5개 task 정의는 v2에서 v3로 **그대로 복사**한다. +8. **동일 ordinal streaming**: `Task_B2_map_events_*`의 `wait_until: ["Task_B1_map_doc_{same_ordinal}"]` 구조 유지. +9. **상태 메시지 문자열**: `"B1 quality gate BLOCK 발생: downstream 진행 금지"`, `"B2 quality gate BLOCK 발생: downstream 진행 금지"`, `"증거문서 authority catalog finalize 완료 (severity=)"`, `"증거문서 사건행위 후보 finalize 완료 (severity=)"` — GA-1/GA-2의 stdout이 동일 문자열을 출력한다(orchestrator 호환). + +--- + +## 2. 개정 범위 총괄표 + +| # | v2 task (행 범위) | v3 처분 | 실행 유형 변화 | +|---|---|---|---| +| 1 | `Task_A_client_goal` (25~620) | 무수정 복사 | LLM 유지 | +| 2 | `Task_Evidence_shard_planner` (622~945) | 무수정 복사 | code-executor 유지 | +| 3 | `Task_B1_map_doc_*` (946~1539) | 무수정 복사 | LLM 유지 | +| 4 | `Task_B2_map_events_*` (1540~2204) | 무수정 복사 | LLM 유지 | +| 5 | `Task_B1_quality_gate_evidence_indexed` (2205~2546) | **전면 치환** → GA-1 결정적 reducer (§4) | LLM → **code-executor** | +| 6 | `Task_B2_quality_gate_event_candidates` (2547~2949) | **전면 치환** → GA-2 결정적 reducer (§5) | LLM → **code-executor** | +| 7 | `Task_B12_gate_llm_adjudicator` | **신설** → GB 조건부 국소 판정 (§6) | LLM (flash-lite, low) | +| 8 | `Task_B12_gate_audit_finalizer` | **신설** → GC audit 확정 (§7) | code-executor | +| 9 | `Task_B2_SHA256_soft_gate_handoff_writer` (2950~3363) | 무수정 복사 | code-executor 유지 | +| 10 | `task_procedure` (3364~3404) | **부분 개정** (§8) | — | + +행 번호는 v2 원본 기준의 위치 참조이며, anchor는 `- task_name:` 라인이다. 치환 범위는 해당 `- task_name:` 라인부터 다음 `- task_name:` 라인 직전까지다. + +--- + +## 3. STEP 0 — 사전 확인 (v3 작성 전 필수) + +1. **STEP 0-1**: `Stage_1_Part_1_v2.yml`을 읽고 §2 총괄표의 8개 task anchor와 `task_procedure` anchor가 모두 존재함을 확인한다. 하나라도 없으면 작업을 중단하고 보고한다. +2. **STEP 0-2**: 결과 fixture `v.6/Results_July_10_3_15pm/`에 `evidence_shard_plan.json`, `evidence_indexed_parts/`(30개), `evidence_event_candidate_parts/`(30개)가 존재함을 확인한다(§10 검증에 사용). +3. **STEP 0-3 (alias 제거 확정)**: v.6 폴더의 `Stage_1_Part_2_v2.yml`, `Stage_1_Part_3_v1.yml`, `Stage_1_Part_4_v2.yml` 및 접근 가능한 Stage 2 명세 전체에서 정규식 `evidence_indexed"?\s*[\]\[:]`로 **`items`가 아닌 `evidence_indexed` 키를 파싱하는 소비자**를 검색한다. + - 소비자 0건(기대값): `evidence_indexed.json`은 v3에서 `schema_contract_version: "evidence_indexed.v3"` + **`items` 단일 배열**로 확정한다(중복 alias 제거 — Q-3 해소). + - 소비자 1건 이상: alias를 유지하되(`items`와 동일 배열 참조로 Python이 생성 — LLM 토큰 비용 0) 발견 지점을 개정 보고서에 기록한다. + +--- + +## 4. STEP 1 — `Task_B1_quality_gate_evidence_indexed` 치환 명세 (GA-1) + +v2 2205~2546행 블록 전체를 삭제하고 아래 명세의 code-executor task로 대체한다. + +### 4.1 task 엔트리 골격 + +```yaml + - task_name: Task_B1_quality_gate_evidence_indexed + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 240 + code: | + # §4.3의 기능 명세를 구현한 Python 스크립트 +``` + +- `preflight_files`, `llm_provider/llm_model/llm_reasoning/llm_verbosity`, `cache_control`, `use_tools`, `prompts` 필드는 **모두 제거**한다(code-executor task는 v2의 `Task_Evidence_shard_planner`/`Task_B2_SHA256_soft_gate_handoff_writer` 엔트리 형식을 따른다). + +### 4.2 MCP 보일러플레이트 (강제) + +Python 코드의 MCP 통신부는 v2 `Task_B2_SHA256_soft_gate_handoff_writer`(2959~3131행)의 다음 요소를 **그대로 복사**한다: `LOCALDOCS_URL`, `MCP_HEADERS`, `CLIENT`, `MSG_ID`, `_mid`, `_init`, `_parse`, `_call`, `_unwrap`, `read_raw`, `parse_json`, `write_doc`, `as_list`, `as_str_list`, `clip`. `_init`의 `clientInfo.name`만 `"task-b1-quality-gate-evidence-indexed"`로 바꾸고 `user_id: "{{__user_hash__}}"`, `workspace_id: "{{__workspace_hash__}}"` placeholder는 유지한다. 파일 목록 조회는 planner(726행)와 동일하게 `list_docs`를 `_call`로 호출한다. + +### 4.3 기능 명세 (main 로직 — 순서 강제) + +**입력** (read_docs): ① `evidence_shard_plan.json` ② `evidence_indexed_parts/E-*.json` 전체(plan의 `expected_ordinals` 기준으로 경로 구성). **v2가 읽던 `evidence_all.json` 전문과 `client_meeting.md` 전문은 읽지 않는다** — 단, §4.4 R-DRIFT 의심 신호가 발생한 ordinal에 한해 해당 shard 원문(`evidence_shards/E-{ordinal}.json` — plan의 `shards[*].shard_doc_path` 값 사용) 1건만 추가로 읽어 발췌를 exception pack에 첨부한다. + +1. **입력 고정**: plan에서 `expected_ordinals`, `expected_evidence_ids`, `shard_count`, `barriers.B1_all_parts_barrier.require_files`, `stale_part_policy.detected_stale_parts`를 파싱한다. +2. **completeness (v2 CHECK_1의 결정적 이식)**: `detected_stale_parts` 비어있지 않음 → BLOCK. expected와 received ordinal의 set 차이 존재(누락 또는 초과) → BLOCK. 각 part 파싱 실패·빈 파일 → BLOCK. +3. **index determinism (CHECK_2)**: 각 part의 `evidence_index_proposed`가 정규식 `^E-\d{3}$`이고 자기 ordinal과 일치하지 않으면 → BLOCK. +4. **exact duplicate alias (CHECK_3의 결정적 부분)**: `doc_uid` 완전 일치 또는 `title_normalized` 완전 일치 그룹을 추출한다. 확정 중복은 canonical 규칙(§0.2-3)으로 1개만 남기고 `dropped_aliases`에 ordinal·사유를 기록한다. **유사(비완전일치) 의심쌍**(공백·조사 제거 후 일치, 또는 `key_dates`+`key_amounts`+`key_parties` 3종 동시 일치인데 title 상이)은 삭제하지 않고 suspect로 등록한다(§4.4 R-ALIAS). +5. **component coverage (CHECK_4의 결정적 이식)**: v2 CHECK_4의 10개 규칙을 `doc_type(또는 semantic flag) → 필수 non-empty 필드` rule table로 코드화한다(예: `registry_doc`이고 `registry_row_components` empty → coverage suspect). 규칙 위반은 suspect로 등록한다(§4.4 R-COVERAGE) — 결정적 코드가 severity를 직접 확정하지 않고 GB가 판정한다(원문에 실제로 해당 component가 "보이는지"는 의미론적 판단이기 때문). +6. **authority drift (CHECK_5의 강등)**: 상시 전역 검사를 삭제한다. §4.4 R-DRIFT 신호 발생 시에만 suspect 등록 + 해당 shard 발췌 첨부. +7. **finalize & merge**: ordinal 오름차순 stable sort → `evidence_index = evidence_index_proposed`(검증 통과 시) 확정 → item dict를 **그대로 복사**(전 필드 pass-through)하되 `evidence_index_proposed` 키를 `evidence_index`로 교체하고 `mapper_meta`는 item에서 제외(v2 CONSOLIDATION_RULES 계승, mapper_meta는 precheck에 기록). +8. **envelope 작성**: §3 STEP 0-3 결정에 따라 `{"schema_contract_version": "evidence_indexed.v3", "items": [...]}` (+조건부 alias). `json.dumps(ensure_ascii=False, indent=2)`로 직렬화하여 `evidence_indexed.json` 저장. +9. **write 전·후 보존성 검사**: `len(items) == shard_count`, `set(evidence_index) == set(expected_evidence_ids) - dropped_aliases의 제거분`, 모든 part가 items 또는 `dropped_aliases`에 정확히 1회 대응. 위반 시 파일을 쓰지 않고 BLOCK. +10. **mapper self-warning 승계**: 각 part의 self-warning/review 필드(`mapper_meta` 내 warning 포함)를 전량 `review_candidates[]`로 수집한다(승격 판정은 GB). +11. **중간 산출물 저장**: `stage1_tmp/quality_gate/finalized_evidence_id_manifest.json`(§4.5-A), `stage1_tmp/quality_gate/B1_precheck.json`(§4.5-B). +12. **stdout**: BLOCK 발생 시 `"B1 quality gate BLOCK 발생: downstream 진행 금지"` + status JSON, 정상 시 `"증거문서 authority catalog finalize 완료 (severity=)"` + status JSON. 여기서 overall_severity는 precheck 기준 잠정값(BLOCK 또는 PASS — review성 판정은 GB/GC 확정 전이므로 suspect 존재 시 `PENDING_ADJUDICATION`을 status JSON에 병기). +13. **FAILURE_POLICY 계승**: 입력 파일 누락·파싱 불가·stale 감지 시 final 파일을 쓰지 않고 오류 보고 후 종료. tool 오류는 1회 재시도. + +### 4.4 의심 신호 규칙 (suspect rules — GA-1) + +| ID | 신호 | pack 첨부물 | +|---|---|---| +| R-ALIAS | doc_uid/title 비완전일치 유사쌍 (§4.3-4) | 두 item의 `title`, `title_normalized`, `key_dates`, `key_amounts`, `key_parties`만 | +| R-COVERAGE | doc_type rule table 위반 (§4.3-5) | 해당 item의 `doc_type`, 비어 있는 component 필드명, item의 key_* 필드 | +| R-DRIFT | (a) part의 key_dates·key_amounts가 모두 empty인데 doc_type이 `registry_doc`/`claim_doc`/`notice_doc` 계열, (b) `source_pointer`의 ordinal 표기와 자기 ordinal 불일치, (c) mapper self-warning에 drift/출처 관련 문구 존재 | 해당 shard 원문 발췌 ≤1,500자 + part의 관련 필드 | + +### 4.5 중간 산출물 스키마 + +**A. `stage1_tmp/quality_gate/finalized_evidence_id_manifest.json`** + +```json +{ + "schema_version": "finalized_evidence_id_manifest.v1", + "source_task": "Task_B1_quality_gate_evidence_indexed", + "shard_count": 30, + "finalized_evidence_indexes": ["E-001", "E-002", "..."], + "dropped_aliases": [] +} +``` + +**B. `stage1_tmp/quality_gate/B1_precheck.json`** + +```json +{ + "schema_version": "B1_precheck.v1", + "deterministic_checks": [ + {"check_id": "shard_completeness", "severity": "PASS|BLOCK", "detail": "", "missing_ordinals": []}, + {"check_id": "evidence_index_determinism", "severity": "PASS|BLOCK", "detail": "", "violations": []}, + {"check_id": "duplicate_document_alias", "severity": "PASS|BLOCK", "detail": "", "duplicate_groups": [], "canonical_choices": []}, + {"check_id": "document_component_coverage", "severity": "PASS", "detail": "", "missing_components": []}, + {"check_id": "authority_drift_detection", "severity": "PASS", "detail": "", "drift_findings": []} + ], + "hard_gate_findings": [], + "review_candidates": [], + "suspects": [], + "dropped_aliases": [], + "conservation": {"input_parts": 30, "final_items": 30, "dropped": 0}, + "block": false +} +``` + +- `deterministic_checks[*].check_id`는 v2 B1 audit의 5개 check_id 문자열과 **완전히 동일**해야 한다(GC가 그대로 audit `checks[]`로 옮긴다). coverage/drift check는 결정적 단계에서 severity를 `PASS`로 두고 suspect만 등록한다 — 최종 severity는 GB 판정 후 GC가 기입한다. +- `suspects[*]`: `{"exception_id": "B1-SUS-001", "rule_id": "R-ALIAS|R-COVERAGE|R-DRIFT", "check_id": "<대응 check_id>", "ordinals": [], "payload": {...}, "excerpt": "≤1500자", "escalation_flag": false}`. + +--- + +## 5. STEP 2 — `Task_B2_quality_gate_event_candidates` 치환 명세 (GA-2) + +v2 2547~2949행 블록 전체를 삭제하고 code-executor task로 대체한다. 엔트리 골격·보일러플레이트는 §4.1~4.2와 동일하다(`clientInfo.name`: `"task-b2-quality-gate-event-candidates"`, timeout 240). + +### 5.1 기능 명세 (순서 강제) + +**입력** (read_docs): ① `evidence_shard_plan.json` ② `evidence_event_candidate_parts/E-*.json` 전체 ③ `stage1_tmp/quality_gate/finalized_evidence_id_manifest.json` ④ `stage1_tmp/quality_gate/B1_precheck.json`(suspects 병합용). **v2가 읽던 `evidence_indexed.json` 전문(91.8KB)과 `quality_gates/B1_evidence_indexed_gate.json`은 읽지 않는다** — 참조 무결성은 manifest의 E-ID 집합(~0.5KB)으로, B1 review 승계는 GC가 B1_precheck로 수행한다. + +1. **입력 고정**: plan에서 `expected_ordinals`, `expected_evidence_ids`, `shard_count`, `barriers.B2_all_parts_barrier.require_files`, `stale_part_policy` 파싱. manifest에서 `finalized_evidence_indexes`를 set으로 고정. +2. **schema invariant gate (v2 PART_ITEM_SCHEMA_INVARIANT_GATE의 완전 이식 — 6개 invariant 전부)**: 각 part는 실질 item 정확히 1개 / `evidence_index_proposed`(또는 `evidence_index`)가 expected ID 및 manifest와 일치 / non-empty `event_candidates` XOR non-null `zero_event_reason` 규칙(`[]`+null 결합 → BLOCK) / candidate는 `candidate_id_proposed`·`source_evidence_index_proposed`·`identity_signature` 보유. 위반마다 `schema_invariant_findings[]`에 `{ordinal, part_path, reason, severity: "BLOCK", rerun_target_task: "Task_B2_map_events_{ordinal}"}` 기록(v2 2880행 형식 그대로). +3. **completeness (CHECK_1)**: stale/누락/초과 ordinal → BLOCK. +4. **reference integrity (CHECK_2)**: 각 candidate의 `source_evidence_index_proposed` ∉ `finalized_evidence_indexes` → BLOCK. +5. **candidate ID (CHECK_3 결정적 부분)**: `candidate_id_proposed` 정규식 `^EVT-\d{3}-\d{2}$` + ordinal 일치 + 같은 ordinal 내 seq 유일성 → 위반 시 BLOCK. cross-document `identity_signature` **완전 일치** 그룹은 canonical 규칙(§0.2-3)으로 dedup하고 **`dedup_findings[]`에 의무 기록**(Q-4 해소). dropped candidate의 원본 필드는 `dedup_findings[*].dropped_candidate`에 보존한다(증거 보존성). +6. **chronology (CHECK_4의 결정적 부분)**: v2 CHECK_4에 명문화된 4개 chain 순서 규칙(underlying_claim_arising < first_demand < demand_notice_dispatch <= demand_notice_arrival < debt_acknowledgement / lien chain / 경매 chain / 사해행위 chain)을 rule table로 코드화한다. **양쪽 날짜가 모두 ISO-parse 가능한 경우에만** 역전을 검사하고, 역전 발견 → suspect(R-CHRONO, `escalation_flag: true`). 날짜 null·불완전은 suspect(R-CHRONO, escalation_flag false). 코드가 직접 BLOCK하지 않는다(시간 모순의 법적 중대성 판정은 GB). +7. **coverage (CHECK_5)**: v2 CHECK_5의 REQUIRED_EVENT_SUBKINDS 9개 규칙을 `트리거 신호(doc_type/기존 candidate subkind 존재) → 기대 subkind set` rule table로 코드화 → 위반은 suspect(R-EVTCOV). +8. **finalize & merge**: ordinal 오름차순 정렬 → `candidate_id = candidate_id_proposed`, `source_evidence_index = source_evidence_index_proposed` 확정(키 교체) → **candidate dict 전 필드 그대로 복사**(Q-2 구조적 해소 — 어떤 필드도 선별·요약하지 않는다) → item 레벨도 전 필드 복사, `mapper_meta`만 제외. +9. **envelope 작성**: `{"schema_version": "evidence_event_candidates.v1", "items": [...]}` 저장. +10. **보존성 invariant (write 전·후 각 1회 — 위반 시 파일 미작성 + BLOCK)**: + +```text +set(items[*].evidence_index) == set(expected_evidence_ids) +len(items) == shard_count +sum(part candidate 수) == sum(final candidate 수) + len(dedup_findings) +dedup_findings가 비어 있는데 candidate 수 감소 → BLOCK +candidate별 필드 수: final == input (키 교체 2건 외 증감 0) +모든 part는 items 또는 schema_invariant_findings에 정확히 1회 대응 +``` + +11. **exception pack 통합**: B1_precheck의 `suspects[]` + `review_candidates[]`와 GA-2 자체 suspects를 병합하여 `stage1_tmp/quality_gate/B12_exception_pack.json`(§5.2) 저장. **micro-pack 상한 강제**: 항목당 evidence item ≤2, candidate ≤8, 발췌 ≤1,500자, exception type 1종. 상한 초과분은 절단하지 않고 `overflow_items[]`로 이월 기록한다(silent cap 금지). +12. **중간 산출물**: `stage1_tmp/quality_gate/B2_precheck.json`(§4.5-B와 동형, check_id는 v2 B2 audit의 5개 + `schema_invariant_findings` 배열, `dedup_findings[]` 포함). +13. **stdout**: §1-9의 B2 메시지 문자열 + status JSON(`exception_count` 포함). + +### 5.2 `stage1_tmp/quality_gate/B12_exception_pack.json` 스키마 + +```json +{ + "schema_version": "B12_exception_pack.v1", + "exception_count": 0, + "exceptions": [ + { + "exception_id": "B1-SUS-001 | B2-SUS-001 | B1-RC-001(review_candidate)", + "source_gate": "B1|B2", + "rule_id": "R-ALIAS|R-COVERAGE|R-DRIFT|R-CHRONO|R-EVTCOV|R-SELFWARN", + "check_id": "", + "ordinals": ["019"], + "evidence_indexes": ["E-019"], + "candidate_ids": [], + "payload": {}, + "excerpt": "≤1500자", + "escalation_flag": false + } + ], + "overflow_items": [], + "budget": {"max_items_per_exception": 2, "max_candidates": 8, "max_excerpt_chars": 1500} +} +``` + +`exception_count == len(exceptions)`를 코드로 강제한다. mapper self-warning은 rule_id `R-SELFWARN`으로 전량 포함한다(이번 7/10 실행 기준이라면 pack에는 R-SELFWARN 1건만 존재했을 것). + +--- + +## 6. STEP 3 — `Task_B12_gate_llm_adjudicator` 신설 명세 (GB) + +`Task_B2_quality_gate_event_candidates` 엔트리 뒤, `Task_B12_gate_audit_finalizer` 앞에 신설한다. + +### 6.1 task 엔트리 골격 + +```yaml + - task_name: Task_B12_gate_llm_adjudicator + preflight_files: + - stage1_tmp/quality_gate/B12_exception_pack.json + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: low + llm_verbosity: low + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + ... + ... + ... + ... +``` + +- ``과 ``는 v2 2223~2275행의 원문을 **바이트 동일하게 복사**한다(캐시 적중 유지). 기존 task들의 해당 블록은 수정하지 않는다. +- `llm_reasoning: low` 확정 근거: 실증상 판정 대상의 대부분이 self-warning 승격 분류이고(7/10 실행 기준 발견 0건), 고난도 케이스는 아래 ESCALATE 경로로 인간 검토에 회부한다. 런타임이 호출별 reasoning 변경을 지원하지 않으므로 high 상시 적용은 금지한다. + +### 6.2 STATIC_BLOCK 필수 내용 (프롬프트 작성 지시) + +1. **MISSION**: "B1/B2 결정적 reducer가 추출한 의심 항목만 국소 판정하는 예외 심판기다. 최종 파일을 쓰지 않고, 병합하지 않고, 새 사실을 창작하지 않는다. 판정 결과는 `stage1_tmp/quality_gate/B12_adjudication_decisions.json`에만 쓴다." +2. **EXECUTION_SEQUENCE**: + - ① `read_docs`로 `stage1_tmp/quality_gate/B12_exception_pack.json` **1개 파일만** 읽는다. + - ② `exception_count == 0`이면: `write_file`로 `{"schema_version":"B12_adjudication_decisions.v1","decisions":[],"no_suspects":true}`를 저장하고 `"NO_SUSPECTS"`만 출력 후 즉시 종료한다. 다른 어떤 파일도 읽지 않는다. + - ③ `exception_count > 0`이면: 각 exception을 pack에 포함된 payload·excerpt **만으로** 판정한다. 추가 read_docs 금지(발췌는 GA가 이미 pack에 내장). + - ④ 판정 decision enum: `DISMISS`(문제 없음) | `SOFT_WARNING_REVIEW` | `HARD_WARNING_REVIEW` | `CONFIRM_BLOCK`(provenance 오염·source trace 상실·final identity collision 등 v2 hard gate 승격 요건 충족 시에만) | `ESCALATE_TO_REVIEW`(`escalation_flag: true`이고 pack 정보만으로 확정 불가 — 날짜·당사자·목적물의 실질 충돌, drift가 source trace 유효성을 좌우, chronology가 block 여부를 좌우하는 경우). + - ⑤ `write_file`로 decisions 파일 저장, 건수 요약만 출력 후 종료. +3. **출력 item 스키마** (필드 고정, 자유 서술 금지): + +```json +{ + "exception_id": "", + "decision": "DISMISS|SOFT_WARNING_REVIEW|HARD_WARNING_REVIEW|CONFIRM_BLOCK|ESCALATE_TO_REVIEW", + "severity": "PASS|SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "", + "source_refs": {"evidence_indexes": [], "event_candidate_ids": [], "meeting_clause_refs": [], "doc_titles": []}, + "concise_basis": "≤120자 한국어", + "candidate_use_policy": "prioritize_if_supported|preserve_as_needs_review|do_not_generate_without_support" +} +``` + +4. **ABSOLUTE_FORBIDDEN**: final 파일(`evidence_indexed.json`, `evidence_event_candidates.json`) 및 `quality_gates/*` 작성·재작성 / pack 외 파일 read / mapper part 수정 / 새 candidate·사실 창작 / `source_refs`를 배열로 출력(반드시 named object) / decision 없는 exception 방치(모든 exception_id에 정확히 1개 decision). +5. **판정 법률 기준**(STATIC_BLOCK에 포함): v2 HARD_REVIEW_GATE_CLASSIFICATION(2337~2343행, 2685~2690행)의 분류 원칙을 요약 이식한다 — "법률적 불확실성은 review, 기계적/출처/무결성 실패만 BLOCK. review는 사실 확정이 아니다." + +--- + +## 7. STEP 4 — `Task_B12_gate_audit_finalizer` 신설 명세 (GC) + +GB 뒤, SHA writer 앞에 신설한다. code-executor task(골격 §4.1, `clientInfo.name`: `"task-b12-gate-audit-finalizer"`, timeout 120). + +### 7.1 기능 명세 + +**입력**: `stage1_tmp/quality_gate/B1_precheck.json`, `stage1_tmp/quality_gate/B2_precheck.json`, `stage1_tmp/quality_gate/B12_adjudication_decisions.json`, `stage1_tmp/quality_gate/B12_exception_pack.json`. + +**출력**: `quality_gates/B1_evidence_indexed_gate.json`, `quality_gates/B2_event_candidates_gate.json`. + +1. **checks[] 재구성**: precheck의 `deterministic_checks`를 audit `checks[]`로 옮기되, GB 판정이 있는 check는 severity를 갱신한다: 해당 check_id에 매핑된 decisions 중 최고 severity(BLOCK > HARD_WARNING > SOFT_WARNING > PASS, `DISMISS`→PASS, `ESCALATE_TO_REVIEW`→HARD_WARNING review). 각 check의 detail 배열 필드(`missing_ordinals`, `violations`, `duplicate_groups`, `canonical_choices`, `missing_components`, `drift_findings`, `format_violations`, `dedup_findings`, `ordering_violations`, `missing_subkinds_by_ordinal`)는 precheck·decisions에서 결정적으로 채운다. +2. **audit 스키마**: v2의 AUDIT_REPORT_SCHEMA(B1: 2419~2480행, B2: 2811~2876행)의 **모든 키를 동일하게** 생성한다. `gate_id`, `executed_at`(UTC ISO-8601, 코드 생성), `input_parts_count`, `expected_ordinals`, `planner_source`, `planner_shard_count`, `received_ordinals`, `checks`, `overall_severity`, `block_reasons`, `warning_reasons`, `hard_gate_findings`, `review_findings`, `rerun_targets`, `stage2_risk_notes`, `review_gate_summary`, `handoff_export_summary`, `downstream_progression`, `stage2_auto_progression_allowed`, B1의 `dropped_aliases`, B2의 `schema_invariant_findings`·`dedup_findings`, `schema_contract_version`(`"B1_quality_gate.v3"`/`"B2_quality_gate.v3"`). +3. **결정적 집계 규칙 (자유 생성 절대 금지)**: + - `hard_gate_findings[]` = precheck BLOCK findings + `CONFIRM_BLOCK` decisions. 각 항목에 `gate_finding_id`(`B1-HARD-001` 형식) 부여. + - `review_findings[]` = `SOFT_WARNING_REVIEW`/`HARD_WARNING_REVIEW`/`ESCALATE_TO_REVIEW` decisions + (GB no_suspects 시) 빈 배열. 각 finding은 §1-5의 SHA writer 소비 필드를 모두 포함하며 `source_refs`는 named object 그대로 전달. 최대 40개. + - `review_gate_summary.review_gate_count` = `len(review_findings)` (**다른 계산 금지** — X-1 오계수 재발 차단). `b1_review_count`/`b2_review_count`는 source_gate별 len. + - `overall_severity` = checks·hard_gate_findings의 최고 severity. `overall_severity=="BLOCK"` ⇔ `hard_gate_findings`(B2는 +`schema_invariant_findings`) 1개 이상. + - `downstream_progression`/`stage2_auto_progression_allowed`: v2 추가 제약(2482~2494행, 2878~2889행)의 대응 규칙을 코드로 그대로 구현. + - `handoff_export_summary`: `is_soft_handoff_writer: false`, `handoff_writer_task: "Task_B2_SHA256_soft_gate_handoff_writer"`, `handoff_path: "quality_gates/stage1_part1_soft_gate_handoff.json"`, `exported_review_count: len(review_findings)`, `handoff_status_candidate`: BLOCKED/READY_WITH_REVIEW/READY_NO_REVIEW (v2 2707~2708행 규칙). + - `block_reasons`/`warning_reasons`: findings의 한국어 mirror (clip 120자). + - `stage2_risk_notes[]`: review_findings의 `concise_basis`만 복사(자유 서술 신설 금지). +4. **stdout**: `{"status": "GATE_AUDITS_FINALIZED", "b1_overall": "...", "b2_overall": "...", "review_count": N, "hard_count": N}` + BLOCK 존재 시 해당 게이트의 §1-9 BLOCK 메시지 문자열 재출력. + +--- + +## 8. STEP 5 — `task_procedure` 개정 + +v2 3364~3404행을 아래로 교체한다. **변경은 굵게 표시한 3개 노드의 edge뿐이며 나머지는 v2 그대로다.** + +```yaml + task_procedure: + IN: + nexts: ["Task_A_client_goal", "Task_Evidence_shard_planner"] + wait_until: [] + + Task_A_client_goal: + nexts: [] + wait_until: ["IN"] + + Task_Evidence_shard_planner: + nexts: ["Task_B1_map_doc_*"] + wait_until: ["IN"] + + Task_B1_map_doc_*: + nexts: + - "Task_B2_map_events_{same_ordinal}" + - "Task_B1_quality_gate_evidence_indexed" + wait_until: ["Task_Evidence_shard_planner"] + + Task_B2_map_events_*: + nexts: ["Task_B2_quality_gate_event_candidates"] + wait_until: ["Task_B1_map_doc_{same_ordinal}"] + + Task_B1_quality_gate_evidence_indexed: + nexts: ["Task_B2_quality_gate_event_candidates"] + wait_until: ["all Task_B1_map_doc_*"] + + Task_B2_quality_gate_event_candidates: + nexts: ["Task_B12_gate_llm_adjudicator"] + wait_until: ["all Task_B2_map_events_*", "Task_B1_quality_gate_evidence_indexed"] + + Task_B12_gate_llm_adjudicator: + nexts: ["Task_B12_gate_audit_finalizer"] + wait_until: ["Task_B2_quality_gate_event_candidates"] + + Task_B12_gate_audit_finalizer: + nexts: ["Task_B2_SHA256_soft_gate_handoff_writer"] + wait_until: ["Task_B12_gate_llm_adjudicator"] + + Task_B2_SHA256_soft_gate_handoff_writer: + nexts: ["OUT"] + wait_until: ["Task_B12_gate_audit_finalizer"] + + OUT: + nexts: [] + wait_until: ["Task_B2_SHA256_soft_gate_handoff_writer"] + + prevs: [] + nexts: [] +``` + +주의: SHA writer는 이제 audit 2종이 GC에 의해 확정된 **후** 실행되므로 `wait_until`이 `Task_B12_gate_audit_finalizer`로 바뀐다. SHA writer의 task 정의 자체(INPUT_PATHS 포함)는 무수정이다 — 읽는 파일 4종의 경로가 동일하기 때문이다. + +--- + +## 9. 단일 writer 책임표 및 BLOCK 전파 (v3 확정) + +| 파일 | 단일 writer | +|---|---| +| `evidence_indexed.json` | GA-1 (`Task_B1_quality_gate_evidence_indexed`) | +| `evidence_event_candidates.json` | GA-2 (`Task_B2_quality_gate_event_candidates`) | +| `stage1_tmp/quality_gate/finalized_evidence_id_manifest.json`, `B1_precheck.json` | GA-1 | +| `stage1_tmp/quality_gate/B2_precheck.json`, `B12_exception_pack.json` | GA-2 | +| `stage1_tmp/quality_gate/B12_adjudication_decisions.json` | GB | +| `quality_gates/B1_evidence_indexed_gate.json`, `B2_event_candidates_gate.json` | GC | +| `quality_gates/stage1_part1_soft_gate_handoff.json` | SHA writer (기존, 무수정) | + +BLOCK 전파: GA 단계의 기계적 BLOCK(stale/누락/파싱/형식/참조/보존성 위반)은 precheck에 기록되고 GB를 거치지 않고 GC가 BLOCK audit을 확정 → SHA writer가 `handoff_status: "BLOCKED"` sidecar 생성 → Part 2 Stage A가 차단. GB의 `CONFIRM_BLOCK`도 동일 경로다. v2와 마찬가지로 check-level BLOCK 시에도 final 파일은 작성하되(입력 자체 결손·보존성 위반 시에만 미작성) stdout에 BLOCK을 명시한다. + +Phase 2 후속 과제(본 개정 범위 외, v3 안정화 후): ① 런타임의 0-instance wildcard barrier resolve 실증 시 GB를 GA-2 stdout `dynamic_fanout` 기반 조건부 spawn으로 전환(진정한 LLM 0회) ② B1 mapper localdocs tool-level allowlist에 의한 authority drift 구조 차단 ③ GC의 SHA writer 통합으로 task 1개 절감 ④ sub-task별 telemetry 추가 후 v.5/v.6/v3 A/B 계측. + +--- + +## 10. 수용 기준 (Definition of Done — 전 항목 충족 시에만 개정 완료) + +### 10.1 정적 검사 + +1. `Stage_1_Part_1_v3.yml`이 유효한 YAML로 파싱된다. +2. task 수 9개(§2 표의 1~9), `task_procedure` 노드가 §8과 일치한다. +3. §1의 수정 금지 task 5개가 v2와 diff 0이다(공백 포함). +4. GA-1/GA-2/GC 엔트리에 `llm_*`/`prompts` 필드가 없고, GB 엔트리에 `mcp: code-executor`가 없다. +5. GB 프롬프트의 COMMON_CACHE_PREFIX_STAGE_1·ROLE_PREFIX가 v2 원문과 바이트 동일하다. +6. v3 전체에서 `simple concat으로 작동(반드시 LLM 검증 수행)` 문구가 게이트 task에 존재하지 않는다(해당 FORBIDDEN 규칙은 결정적 전환으로 폐기). + +### 10.2 fixture 회귀 (7/10 실행물 `Results_July_10_3_15pm` 입력) + +GA-1/GA-2 코드를 fixture의 30개 B1 part·30개 B2 part·plan으로 로컬 실행하여: + +```text +evidence_indexed.json: items == 30, (STEP 0-3 기본 결정 시) 중복 alias 배열 부재 +evidence_event_candidates.json: items == 30, 후보 총 74건 +E-019 항목 존재 + 후보 3건(lien_extinction_notice_dispatch, ongoing_physical_possession, lien_violation_candidate) 보존 +candidate 필드: input 대비 증감 0 (candidate_id_proposed→candidate_id, source_evidence_index_proposed→source_evidence_index 키 교체 2건 제외) +identity_signature 완전일치 dedup group == 0, dedup_findings == [] +2-run 결과: 최종 파일 2종 바이트 동일 (audit의 executed_at 필드만 비교 제외) +B12_exception_pack: R-SELFWARN 1건(상계 항변 self-warning) 외 GA 규칙상 발생분만 존재 +``` + +### 10.3 주입 테스트 (fixture 사본 변조) + +1. part 1개 삭제 → GA completeness BLOCK + rerun 대상 기록. +2. 서로 다른 ordinal에 동일 `identity_signature` 주입 → dedup 1건 + `dedup_findings` 기록 + 후보 합계 보존식 성립. +3. `event_candidates: []` + `zero_event_reason: null` 주입 → `schema_invariant_findings` BLOCK + `rerun_target_task: "Task_B2_map_events_{ordinal}"`. +4. 명시 날짜 역전 주입 → R-CHRONO suspect 생성(자동 BLOCK 아님) → GB 판정 경로 확인. +5. candidate 필드 1개 삭제 주입 → 보존성 invariant(필드 수) BLOCK. +6. 존재하지 않는 `source_evidence_index_proposed` 주입 → reference integrity BLOCK. + +### 10.4 하류 무영향 검증 + +1. GC 산출 audit 2종을 SHA writer에 입력하여 sidecar가 생성되고, review item의 `source_refs` 4개 키에 값이 정상 매핑됨을 확인한다(X-1 재발 검사). +2. 개정 산출 최종 파일 2종 + sidecar로 Part 2 Stage A(`Task_C_BO_Stage_A_input_normalization`), Part 3 `Task_LES0_input_pack_compiler`, Part 4 `Task_FL0_fact_source_pack_compiler`의 입력 파싱이 통과함을 확인한다(특히 alias 제거 결정 시). +3. `stage1_tmp/` 신규 파일들이 Part 2/3/4의 어떤 입력 계약과도 충돌하지 않음을 확인한다(신규 디렉토리이므로 기본 통과). + +### 10.5 실측 + +v3로 Part 1 전체 재실행 → 게이트 구간(GA-1→GA-2→GB→GC) LLM 호출이 suspects 유무에 따라 0~1회인지, Part 1 총 시간·비용이 v.6(8분 45초 / $2.09) 대비 감소했는지 기록한다. + +--- + +## 11. 금지 사항 최종 목록 (개정 실행자 대상) + +1. v2 원본 파일 덮어쓰기·이동·삭제 금지 (v3는 신규 파일). +2. §1 불변 계약의 파일 경로·envelope 키·enum·상태 문자열 변경 금지. +3. Part 2/3/4 YAML 수정 금지 (본 개정의 성립 조건이 "하류 무영향"이다). +4. GA/GC 코드에서 count·severity·ID를 입력 데이터 외 근거로 생성 금지 (모든 count는 `len()`, 모든 ID는 검증된 propose 값 또는 형식 규칙). +5. GA-2에서 candidate 필드 선별·요약·정규화 금지 (dict 복사 + 지정된 키 교체 2건만). +6. GB에 exception pack 외 입력 제공 금지, GB의 final 파일 write 금지. +7. dedup 시 dropped candidate의 원본 데이터 파기 금지 (`dedup_findings[*].dropped_candidate`에 보존 — semantic merge는 Stage 2 소관). +8. 의심 항목의 silent 절단 금지 (`overflow_items[]` 기록 의무). +9. `source_refs`의 배열형 생성 금지 (named object 4키 고정). +10. 본 명세서에 없는 "개선"의 임의 추가 금지 — 범위 밖 아이디어는 개정 보고서에 제안으로만 기록한다. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_1_Improvement_Strategy_Codex_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_1_Improvement_Strategy_Codex_v1.md new file mode 100644 index 00000000..3e3e40ba --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_1_Improvement_Strategy_Codex_v1.md @@ -0,0 +1,1200 @@ +# Stage 1 Part 1 v2 개정 작업 명세서 + +## 0. 문서의 지위와 실행 명령 + +이 문서는 `Part_1_Improvement_Claude_v1.md`의 최종 권고안을 실제 YAML 개정 작업으로 변환한 실행 명세서다. 구현자는 이 문서를 참고자료가 아니라 **완료 조건이 있는 작업 지시서**로 취급한다. + +### 0.1 입력, 출력, 작업 경계 + +| 구분 | 경로 | 처리 원칙 | +|---|---|---| +| 개정 원본 | `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_1_v2.yml` | 읽기 전용. 직접 덮어쓰지 않는다. | +| 설계 근거 | `YAML_Prompts/1. Stage_1/v.6/Part_1_Improvement_Claude_v1.md` | GA-1, GA-2, GB, GC 구조와 검증 원칙을 적용한다. | +| 회귀 fixture | `YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm` | 개정 결과 검증에 사용한다. | +| 개정 결과 | `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_1_v3.yml` | `v2` 전체를 복제한 뒤 이 명세서가 허용한 부분만 수정한다. | + +다음 파일은 이번 작업에서 수정하지 않는다. + +- `Stage_1_Part_2_v2.yml` +- `Stage_1_Part_3_v1.yml` +- `Stage_1_Part_4_v2.yml` +- `Results_July_10_3_15pm` 아래의 모든 실행 결과물 + +### 0.2 최종 목표 + +기존의 대형 LLM 품질 게이트 2개를 다음 구조로 교체한다. + +```text +all B1 mapper parts + | + v +GA-1 Task_B1_gate_merge_deterministic [Python] + | \ + | +--> B1 precheck + compact suspects + v +all B2 mapper parts + finalized evidence ID manifest + | + v +GA-2 Task_B2_gate_merge_deterministic [Python] + | + +--> suspects == 0 --> GB instance 0개 + | + +--> suspects > 0 --> GB instance 통상 1개 + | budget 초과 때만 제한적 분할 + v +GB Task_B1B2_gate_llm_adjudicator_* [조건부 LLM] + | + v +GC Task_B12_gate_audit_finalizer [Python] + | + v +Task_B2_SHA256_soft_gate_handoff_writer [기존 Python, 무수정] + | + v +OUT +``` + +정상 경로에서 게이트 LLM 호출 수는 반드시 `2 -> 0`이 되어야 한다. 의미 판정이 필요한 예외가 있을 때만 compact exception pack을 대상으로 LLM을 실행한다. + +--- + +## 1. 절대 준수 원칙 + +### 1.1 품질 및 법률 안전 원칙 + +1. 증거 문서, event candidate, 출처 locator 또는 식별자를 묵시적으로 삭제하지 않는다. +2. LLM은 최종 `evidence_indexed.json` 또는 `evidence_event_candidates.json`을 작성하거나 수정할 권한이 없다. +3. LLM은 결정적 검사 결과가 만든 `exception_id`만 판정한다. 새 사실, 날짜, 금액, 당사자, ID 또는 source ref를 생성할 수 없다. +4. 기계적으로 판정 가능한 completeness, parsing, 형식, 집합 동등성, 정렬, ID 확정, count, serialization, SHA-256은 전부 Python에서 수행한다. +5. `BLOCK`은 데이터 무결성 또는 출처 추적이 깨진 경우에만 사용한다. 법률적 모호성은 원칙적으로 `HARD_WARNING`, `SOFT_WARNING` 또는 `BLOCK_REVIEW`로 분리한다. +6. mapper part에 존재하는 self-warning/self-error는 누락하지 않고 review candidate 또는 hard candidate로 승계한다. +7. downstream이 소비하는 최종 파일 경로와 핵심 envelope 계약은 유지한다. + +### 1.2 단일 writer 권한 + +| 산출물 | 유일한 writer | +|---|---| +| `evidence_indexed.json` | `Task_B1_gate_merge_deterministic` | +| `finalized_evidence_id_manifest.json` | `Task_B1_gate_merge_deterministic` | +| `evidence_event_candidates.json` | `Task_B2_gate_merge_deterministic` | +| `quality_gates/B12_exception_pack.json` | `Task_B2_gate_merge_deterministic` | +| `quality_gates/B12_adjudication_parts/PACK-*.json` | 해당 `Task_B1B2_gate_llm_adjudicator_*` 인스턴스 | +| `quality_gates/B1_evidence_indexed_gate.json` | `Task_B12_gate_audit_finalizer` | +| `quality_gates/B2_event_candidates_gate.json` | `Task_B12_gate_audit_finalizer` | +| `quality_gates/stage1_part1_soft_gate_handoff.json` | 기존 `Task_B2_SHA256_soft_gate_handoff_writer` | + +한 산출물을 둘 이상의 task가 쓸 수 있게 만드는 개정은 금지한다. + +### 1.3 변경 금지 영역 + +다음 블록은 명시된 문자열 치환 외에는 수정하지 않는다. + +- `Task_A_client_goal` +- B1/B2 mapper의 extraction prompt와 출력 schema +- same-ordinal `Task_B1_map_doc_* -> Task_B2_map_events_{same_ordinal}` streaming edge +- MCP server 설정과 인증 정보 +- `Task_B2_SHA256_soft_gate_handoff_writer`의 task block 전체 + +SHA writer는 공백과 주석을 포함하여 원본과 동일하게 유지한다. 개정 전후 해당 task block의 SHA-256을 비교해 동일성을 검증한다. + +--- + +## 2. 구현 전에 확정할 해석 + +### 2.1 `evidence_indexed` alias 제거 + +개정 결과의 `evidence_indexed.json`은 다음처럼 `items` 배열 하나만 가진다. + +```json +{ + "schema_contract_version": "evidence_indexed.v3", + "items": [] +} +``` + +동일 배열을 `evidence_indexed` 키에 다시 복제하지 않는다. 현재 v.6 Part 2는 `items`를 우선 파싱하고, Part 3은 `items`를 순회하며, Part 4는 object 내부 배열을 재귀 순회하므로 `items` 단일 배열과 호환된다. + +개정자는 YAML 작성 전에 저장소 전체에서 다음 소비 형태가 없는지 다시 확인한다. + +```text +obj["evidence_indexed"] +obj.get("evidence_indexed")만 사용하고 items fallback이 없는 코드 +``` + +그와 같은 active downstream consumer가 발견되면 해당 소비자를 이번 작업에서 수정하지 말고 개정을 중단하여 보고한다. 발견되지 않으면 alias를 제거한다. + +### 2.2 cross-document duplicate 처리 + +Claude 보고서의 "canonical 1개만 남기기"는 현재 item schema와 결합할 경우 비정상적인 `zero_event_reason`을 만들거나 독립 증거의 출처를 Stage 2에서 소실시킬 수 있다. 따라서 다음과 같이 법률상 증거 보존에 맞게 구체화한다. + +1. 서로 다른 evidence item에 속한 candidate가 같은 `identity_signature`를 가져도 **물리적으로 삭제하지 않는다**. +2. Python은 duplicate group과 canonical candidate를 결정적으로 계산해 `dedup_findings[]`에 기록한다. +3. canonical 우선순위는 기존 YAML을 그대로 사용한다. + - raw event 우선 + - higher confidence 우선 + - 더 많은 `support_locators` 우선 + - 더 이른 ordinal 우선 +4. 서로 다른 문서가 같은 행위를 뒷받침하는 경우 이는 중복 데이터인 동시에 별개의 증거 provenance이므로 모든 candidate를 final에 보존한다. +5. 동일 part 안에서 candidate 전체가 바이트 수준으로 중복된 경우에도 1차 개정에서는 삭제하지 않고 finding만 남긴다. 파괴적 dedup은 downstream의 multi-source 표현 schema가 마련된 뒤 별도 개정으로 수행한다. + +따라서 v3의 보존식은 다음과 같다. + +```text +sum(input part candidates) == sum(final candidates) +``` + +`dedup_findings[]`는 삭제 수량이 아니라 duplicate group 수와 canonical 선택을 기록한다. candidate 수가 1개라도 줄면 `CANDIDATE_CONSERVATION_FAILURE`로 `BLOCK`한다. + +### 2.3 조건부 LLM의 실제 구현 방식 + +현재 v.6 YAML에서 검증된 조건부 실행 수단은 planner가 출력하는 `dynamic_fanout`과 wildcard task다. 지원 여부가 확인되지 않은 `run_if`, `skip_if`, `when` 같은 새 YAML 키를 임의로 발명하지 않는다. + +따라서 GB는 이름상 wildcard task로 구현하되, 의미상 단일 조건부 adjudicator다. + +- 예외 0건: GA-2가 `dynamic_fanout: []`를 출력한다. GB 인스턴스는 0개다. +- budget 이내 예외: GA-2가 1개 pack descriptor를 출력한다. GB 인스턴스는 1개다. +- pack 상한 초과: exception type별로 분리하되 아래 budget 내에서만 여러 인스턴스를 만든다. +- 전체 pack 수가 상한을 넘으면 나머지를 LLM에 보내지 않고 `BUDGET_OVERFLOW_BLOCK_REVIEW`로 GC에 전달한다. + +Liti-agent 런타임이 하나의 workflow에서 두 번째 `dynamic_fanout` 생산자를 지원하지 않는다면 정상 경로 LLM 0회 목표를 달성할 수 없다. 이 경우 무조건 실행되는 GB로 타협하지 말고 구현을 중단하여 runtime capability 문제를 보고한다. + +### 2.4 reasoning 설정 + +GB는 예외 시에만 실행되고 입력이 극도로 작으므로 v3에서는 task 복잡도를 늘리지 않기 위해 다음 하나의 설정으로 고정한다. + +```yaml +llm_provider: google +llm_model: gemini-3.1-flash-lite +llm_reasoning: medium +llm_verbosity: low +max_iterations: 2 +``` + +고난도 기준에 해당하여 medium으로도 안전하게 결정할 수 없으면 LLM은 억지로 결론을 내리지 않고 `BLOCK_REVIEW`를 반환한다. 자동 high escalation은 runtime의 item별 model setting 지원이 확인된 뒤 Phase 2에서 추가한다. + +--- + +## 3. 개정 대상 요약 + +| 순서 | 현재 위치 또는 anchor | 개정 작업 | +|---|---|---| +| 1 | 파일 전체 | `v2`를 `Stage_1_Part_1_v3.yml`로 복제하고 `Agent.version`을 `v3.0`으로 변경한다. | +| 2 | `Task_Evidence_shard_planner`의 `barriers` | 기존 B1/B2 quality gate 이름을 GA-1/GA-2 이름으로 교체한다. | +| 3 | `Task_B1_quality_gate_evidence_indexed` 전체 block | 삭제하고 Python GA-1 block을 삽입한다. | +| 4 | `Task_B2_quality_gate_event_candidates` 전체 block | 삭제하고 Python GA-2 block을 삽입한다. | +| 5 | GA-2 다음, SHA writer 앞 | 조건부 GB wildcard task와 Python GC task를 삽입한다. | +| 6 | `Task_B2_SHA256_soft_gate_handoff_writer` | 수정하지 않는다. | +| 7 | 최하단 `task_procedure` | 새 task명과 0-or-N GB barrier를 반영해 전면 교체한다. | +| 8 | 전체 YAML | old gate 참조, 중복 task명, 끊어진 DAG, unauthorized writer를 검사한다. | + +삭제 대상 task명은 정확히 다음 2개다. + +```text +Task_B1_quality_gate_evidence_indexed +Task_B2_quality_gate_event_candidates +``` + +추가 대상 task명은 정확히 다음 4개다. + +```text +Task_B1_gate_merge_deterministic +Task_B2_gate_merge_deterministic +Task_B1B2_gate_llm_adjudicator_* +Task_B12_gate_audit_finalizer +``` + +--- + +## 4. planner 수정 명세 + +`Task_Evidence_shard_planner`의 Python 코드는 shard 생성, stale part 검사, streaming edge를 그대로 유지한다. `plan["barriers"]` 안의 task 이름만 다음처럼 수정한다. + +```python +"barriers": { + "B1_all_parts_barrier": { + "wait_for_tasks": b1_waits, + "require_files": b1_files, + "next": "Task_B1_gate_merge_deterministic" + }, + "B2_all_parts_barrier": { + "wait_for_tasks": b2_waits, + "require_files": b2_files, + "plus": ["Task_B1_gate_merge_deterministic"], + "next": "Task_B2_gate_merge_deterministic" + } +} +``` + +planner의 `dynamic_fanout`, `fanout_meta`, `streaming_edges`, stale part 정책과 출력 형식은 수정하지 않는다. + +--- + +## 5. GA-1 작업 명세 + +### 5.1 task header + +```yaml +- task_name: Task_B1_gate_merge_deterministic + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + # deterministic GA-1 implementation +``` + +LLM 관련 키와 prompt block을 두지 않는다. + +### 5.2 입력과 출력 + +입력: + +- `evidence_shard_plan.json` +- `evidence_indexed_parts/E-*.json` + +상시 입력에서 제거: + +- `evidence_all.json` +- `client_meeting.md` + +출력: + +- `evidence_indexed.json` +- `finalized_evidence_id_manifest.json` +- `quality_gates/B1_gate_precheck.json` + +### 5.3 필수 Python helper + +planner와 SHA writer에서 이미 사용하는 localdocs MCP `httpx` 패턴을 복제해 다음 helper를 구현한다. + +```text +_init() +_call(tool_name, arguments) +_unwrap(response, name) +read_json(path) +list_doc_names(path, pattern) +write_doc(path, text) +as_list(value) +extract_single_item(part_object, part_path) +normalize_ordinal(value) +severity_max(values) +stable_json(object) +sha256_text(text) +``` + +`stable_json`은 `ensure_ascii=False`, `indent=2`를 사용하고 배열 순서를 변경하지 않는다. final artifact에는 현재 시각, random 값 또는 실행별 UUID를 넣지 않는다. + +### 5.4 실행 순서 + +1. plan을 strict JSON으로 파싱한다. +2. `planner_status == "READY"`, `mapper_execution_allowed == true`인지 검사한다. +3. `expected_ordinals`, `expected_evidence_ids`, `shard_count`, stale part 목록을 읽는다. +4. `evidence_indexed_parts/E-*.json`의 실제 파일명을 수집한다. +5. expected, received, missing, unexpected, stale 집합을 계산한다. +6. 각 expected part를 ordinal 순서로 strict parse한다. +7. part는 단일 object 또는 1-item envelope만 허용한다. 실질 item이 0개 또는 2개 이상이면 `BLOCK`한다. +8. 다음 필드를 검사한다. + - `evidence_index_proposed` + - `doc_uid` + - `title` + - `title_normalized` + - `doc_type` + - `source_pointer` +9. proposed ID가 plan의 해당 ID와 정확히 같은지 검사한다. +10. `source_pointer.ordinal`과 part filename ordinal이 같은지 검사한다. +11. plan shard의 `doc_uid_candidate`, `title_candidate`, `content_sha256`와 part metadata를 비교 가능한 범위에서 검증한다. +12. mapper object를 `copy.deepcopy`한 뒤 허용된 projection만 수행한다. +13. ordinal 오름차순으로 `items`를 구성한다. +14. duplicate alias, component coverage, authority drift suspect를 결정적 규칙으로 추출한다. +15. write 전 보존성 검사를 수행한다. +16. `evidence_indexed.json`, manifest, precheck를 순서대로 쓴다. +17. 메모리에 있는 serialized final을 다시 strict parse하여 write 후 논리 검사를 한 번 더 수행한다. + +### 5.5 허용된 B1 projection + +part item의 모든 키와 값은 기본적으로 보존한다. 다음 변환만 허용한다. + +```python +final_item = copy.deepcopy(part_item) +final_item["evidence_index"] = final_item.pop("evidence_index_proposed") +final_item.pop("mapper_meta", None) +``` + +allowlist 방식으로 final item을 새로 조립하지 않는다. 이 방식은 향후 mapper schema에 추가된 component field를 다시 소실시킬 수 있기 때문이다. + +### 5.6 B1 결정적 검사 + +| 검사 | BLOCK 조건 | suspect/review 조건 | +|---|---|---| +| shard completeness | missing, unexpected, stale, parse failure | 없음 | +| ID determinism | proposed ID, ordinal, source pointer 불일치 | 없음 | +| identity collision | 같은 `doc_uid`가 상충하는 title/hash를 가짐 | alias 가능성이 있으나 확정 불가 | +| component coverage | 없음. schema 자체가 깨지면 BLOCK | doc type별 기대 component가 비었음 | +| authority drift | 다른 ordinal의 고유 ID/hash가 직접 주입됨 | mapper self-warning 또는 drift 의심 신호 | + +component coverage suspect 규칙은 기존 B1 gate prompt의 named 규칙을 Python 조건으로 옮긴다. 예: + +- `registry_doc`인데 `registry_row_components`가 비어 있음 +- `schedule_doc`인데 `schedule_items`가 비어 있음 +- `claim_doc`인데 직접 표시된 component self-warning이 존재함 +- notice 계열인데 dispatch/arrival component가 모두 비어 있고 mapper warning이 존재함 + +Python이 raw 원문을 재해석하지 않는다. 구조화 part의 `doc_type`, component emptiness, mapper warning과 schema field만 사용해 suspect를 만든다. + +### 5.7 B1 final schema + +```json +{ + "schema_contract_version": "evidence_indexed.v3", + "items": [ + { + "evidence_index": "E-001" + } + ] +} +``` + +`items` 내부는 B1 mapper part의 모든 필드를 보존하되 `evidence_index_proposed`를 `evidence_index`로 rename하고 `mapper_meta`만 제거한다. + +### 5.8 finalized manifest schema + +```json +{ + "schema_version": "finalized_evidence_id_manifest.v1", + "status": "READY|BLOCKED", + "source_artifact": "evidence_indexed.json", + "shard_count": 30, + "expected_evidence_ids": ["E-001"], + "finalized_evidence_ids": ["E-001"], + "missing_evidence_ids": [], + "unexpected_evidence_ids": [], + "evidence_id_set_sha256": "64 lowercase hex chars" +} +``` + +`evidence_id_set_sha256`는 정렬된 ID 배열을 compact JSON으로 직렬화한 UTF-8 bytes에 대해 계산한다. + +### 5.9 B1 precheck schema + +```json +{ + "schema_version": "B1_gate_precheck.v1", + "status": "READY|BLOCKED|READY_WITH_SUSPECTS", + "expected_ordinals": [], + "received_ordinals": [], + "missing_ordinals": [], + "unexpected_ordinals": [], + "stale_parts": [], + "parse_failures": [], + "hard_gate_findings": [], + "review_candidates": [], + "checks": [], + "metrics": { + "input_parts_count": 0, + "final_items_count": 0, + "input_bytes": 0, + "output_bytes": 0, + "elapsed_ms": 0 + } +} +``` + +--- + +## 6. GA-2 작업 명세 + +### 6.1 task header + +```yaml +- task_name: Task_B2_gate_merge_deterministic + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + # deterministic GA-2 implementation +``` + +### 6.2 입력과 출력 + +입력: + +- `evidence_shard_plan.json` +- `evidence_event_candidate_parts/E-*.json` +- `finalized_evidence_id_manifest.json` +- `quality_gates/B1_gate_precheck.json` + +상시 입력에서 제거: + +- full `evidence_indexed.json` +- 기존 B1 final audit +- `evidence_all.json` +- `client_meeting.md` + +출력 파일: + +- `evidence_event_candidates.json` +- `quality_gates/B2_gate_precheck.json` +- `quality_gates/B12_exception_pack.json` + +stdout: + +```json +{ + "dynamic_fanout": [], + "exception_status": "READY_NO_EXCEPTIONS|READY|BLOCKED", + "exception_pack_count": 0 +} +``` + +### 6.3 실행 순서 + +1. plan, manifest, B1 precheck를 strict parse한다. +2. B1이 BLOCKED여도 GC와 sidecar가 BLOCK 상태를 기록할 수 있도록 GA-2는 종료하지 말고 자신의 구조 검사를 계속한다. +3. expected, received, missing, unexpected, stale set을 계산한다. +4. 각 part를 ordinal 순서로 읽고 실질 item 1개를 추출한다. +5. 다음 schema invariant를 검사한다. + - item은 non-empty `event_candidates` 또는 허용된 non-null `zero_event_reason`을 가짐 + - candidate가 있으면 object임 + - `candidate_id_proposed`, `source_evidence_index_proposed`, `identity_signature` 존재 + - proposed candidate ID 형식이 `EVT-{ordinal:03d}-{seq:02d}`임 + - source evidence ID가 자기 item과 manifest 양쪽에 존재함 +6. candidate와 item을 `copy.deepcopy`한다. +7. 허용된 rename과 `mapper_meta` 제거만 수행한다. +8. duplicate signature group과 canonical candidate를 계산하되 candidate는 삭제하지 않는다. +9. chronology와 legal-effect coverage의 결정적 suspect를 추출한다. +10. write 전 보존성 검사를 수행한다. +11. final event file을 직렬화한다. +12. 직렬화 결과를 다시 parse하여 write 후 보존성 검사를 수행한다. +13. B1/B2 suspects를 exception type별 compact pack으로 만든다. +14. exception pack을 쓰고 `dynamic_fanout` descriptor를 stdout으로 출력한다. + +### 6.4 허용된 B2 projection + +item 수준: + +```python +final_item = copy.deepcopy(part_item) +final_item["evidence_index"] = final_item.pop("evidence_index_proposed") +final_item.pop("mapper_meta", None) +``` + +candidate 수준: + +```python +final_candidate = copy.deepcopy(input_candidate) +final_candidate["candidate_id"] = final_candidate.pop("candidate_id_proposed") +final_candidate["source_evidence_index"] = final_candidate.pop("source_evidence_index_proposed") +``` + +그 밖의 candidate key는 삭제, rename, flatten 또는 allowlist projection하지 않는다. 다음 필드들은 특히 보존 여부를 검사한다. + +```text +event_subkind +participants +object_spec +fraction_ref +amount +time_text +time_precision +location +legal_keywords +support_locators +current_state_candidate +succession_candidate +legal_effect_candidate +defense_candidate +commercial_successor_candidates +meeting_clause_refs +actio_relevance_candidates +``` + +### 6.5 보존성 invariant + +write 전과 직렬화 후에 모두 다음을 검사한다. + +```text +set(final.items[*].evidence_index) == set(manifest.finalized_evidence_ids) +len(final.items) == planner.shard_count +sum(input part candidate count) == sum(final candidate count) +모든 expected part는 final item에 정확히 1회 대응 +모든 input candidate는 final candidate에 정확히 1회 대응 +모든 final source_evidence_index는 manifest 집합의 원소 +모든 final candidate_id는 형식에 맞고 전체에서 unique +``` + +candidate key 보존 검사는 각 candidate별로 한다. + +```text +expected_final_keys += input_candidate.keys +- {candidate_id_proposed, source_evidence_index_proposed} ++ {candidate_id, source_evidence_index} + +actual_final_keys == expected_final_keys +``` + +값도 rename 대상 두 필드를 제외하면 deep equality여야 한다. 위반 시 `CANDIDATE_FIELD_CONSERVATION_FAILURE`로 BLOCK한다. + +### 6.6 chronology suspect 규칙 + +Python은 날짜를 안전하게 parse할 수 있고 동일 relation key가 구조적으로 확인되는 경우에만 순서 비교를 한다. relation key가 없거나 날짜가 null이면 법적 결론을 내리지 않고 suspect를 만든다. + +검사 chain: + +- `underlying_claim_arising < first_demand < demand_notice_dispatch <= demand_notice_arrival < debt_acknowledgement` +- `lien_extinction_notice_dispatch <= lien_extinction_notice_receipt < lien_extinction_effect_candidate` +- `secured_debt_creation < mortgage_setting_by_fraction < auction_application_after_extinguishment < auction_sale_and_registry_transfer` +- `preserved_claim_arising < fraudulent_transfer < encumbrance_release_after_act` + +명백한 동일 relation의 날짜 역전은 `chronology_conflict` exception을 만든다. relation 연결이 불확실한 경우 `chronology_ambiguity`를 만든다. + +### 6.7 legal-effect coverage suspect 규칙 + +기존 B2 gate의 9개 coverage 규칙을 삭제하지 않고 deterministic trigger로 변환한다. trigger는 B1 item의 `doc_type`, component 존재 여부, B2 event_subkind 집합과 mapper warning만 사용한다. raw 문서를 다시 읽지 않는다. + +coverage가 비어 있다는 사실만으로 새 event를 만들거나 BLOCK하지 않는다. `legal_effect_coverage` exception으로 GB 또는 후속 review에 보낸다. + +### 6.8 final event schema + +```json +{ + "schema_version": "evidence_event_candidates.v1", + "items": [ + { + "evidence_index": "E-001", + "event_candidates": [] + } + ] +} +``` + +### 6.9 B2 precheck schema + +```json +{ + "schema_version": "B2_gate_precheck.v1", + "status": "READY|BLOCKED|READY_WITH_SUSPECTS", + "upstream_b1_status": "READY|BLOCKED|READY_WITH_SUSPECTS", + "expected_ordinals": [], + "received_ordinals": [], + "missing_ordinals": [], + "unexpected_ordinals": [], + "stale_parts": [], + "schema_invariant_findings": [], + "hard_gate_findings": [], + "review_candidates": [], + "dedup_findings": [], + "conservation": { + "expected_items": 0, + "final_items": 0, + "input_candidates": 0, + "final_candidates": 0, + "candidate_field_failures": [] + }, + "checks": [], + "metrics": { + "input_bytes": 0, + "output_bytes": 0, + "elapsed_ms": 0 + } +} +``` + +--- + +## 7. exception pack과 조건부 GB 명세 + +### 7.1 exception type + +허용 type은 다음으로 닫는다. + +```text +suspected_duplicate_alias +component_coverage_concern +authority_drift_suspect +mapper_self_warning +chronology_conflict +chronology_ambiguity +legal_effect_coverage_concern +``` + +기계적 BLOCK finding은 GB에 보내지 않는다. GA precheck에서 확정하고 GC가 그대로 반영한다. + +### 7.2 micro-pack budget + +각 pack은 다음 상한을 모두 만족해야 한다. + +| 항목 | 상한 | +|---|---:| +| exception type | 1종 | +| evidence item | 2개 | +| event candidate | 8개 | +| source excerpt 총길이 | 1,500자 | +| serialized pack | 6,000자 | +| 정상 pack 수 | 1개 | +| 분할 escalation 포함 최대 pack 수 | 8개 | + +금지 입력: + +- full planner +- full `evidence_indexed.json` +- full `evidence_event_candidates.json` +- full `evidence_all.json` +- full `client_meeting.md` +- 관련 없는 ordinal의 shard 또는 candidate + +상한을 넘는 항목은 자르면서 의미를 바꾸지 않는다. 최소 context로도 6,000자를 넘거나 총 pack이 8개를 넘으면 `BUDGET_OVERFLOW_BLOCK_REVIEW`로 남기고 LLM에 보내지 않는다. + +### 7.3 exception pack schema + +```json +{ + "schema_version": "stage1_part1_gate_exception_pack.v1", + "status": "READY_NO_EXCEPTIONS|READY|BLOCKED", + "exception_count": 0, + "pack_count": 0, + "budget_gate": { + "max_evidence_items_per_pack": 2, + "max_event_candidates_per_pack": 8, + "max_excerpt_chars_per_pack": 1500, + "max_serialized_chars_per_pack": 6000, + "max_pack_count": 8, + "overflow_detected": false + }, + "packs": [ + { + "pack_id": "PACK-001", + "output_path": "quality_gates/B12_adjudication_parts/PACK-001.json", + "exception_type": "chronology_ambiguity", + "exception_ids": ["B12-EXC-0001"], + "items": [] + } + ], + "auto_blocked_review_items": [] +} +``` + +GA-2 stdout의 `dynamic_fanout`은 `packs`의 각 원소를 그대로 사용한다. `status == READY_NO_EXCEPTIONS`이면 반드시 빈 배열이다. + +### 7.4 GB task header + +```yaml +- task_name: Task_B1B2_gate_llm_adjudicator_* + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: medium + llm_verbosity: low + max_iterations: 2 + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs +``` + +GB prompt에는 기존 `COMMON_CACHE_PREFIX_STAGE_1`을 붙이되, 다음 authority boundary를 그 뒤에 명시한다. + +1. `{{item.*}}`로 지정된 pack 하나만 읽는다. +2. pack에 없는 exception을 판정하지 않는다. +3. `evidence_all.json`, final files, meeting 원문을 추가로 읽지 않는다. +4. candidate 또는 evidence item을 수정하지 않는다. +5. 새 ID와 source ref를 만들지 않는다. +6. 출력 file 하나 외에는 어떤 파일도 쓰지 않는다. +7. chain-of-thought를 출력하지 않는다. +8. 근거가 부족하면 반드시 `BLOCK_REVIEW`를 사용한다. + +### 7.5 GB 허용 decision + +```text +PASS_NO_ACTION +REVIEW_SOFT +REVIEW_HARD +BLOCK_SOURCE_INTEGRITY +BLOCK_REVIEW +``` + +`BLOCK_SOURCE_INTEGRITY`는 pack 안의 정보만으로 출처 오염 또는 ID 충돌이 명백한 경우에만 허용한다. 법리상 애매함은 `BLOCK_REVIEW`이지 Stage 1 전체 BLOCK이 아니다. + +### 7.6 GB output schema + +각 인스턴스는 `{{item.output_path}}`에 정확히 다음 object를 쓴다. + +```json +{ + "schema_version": "stage1_part1_gate_adjudication_part.v1", + "pack_id": "PACK-001", + "status": "READY|READY_WITH_BLOCK_REVIEW|FAILED", + "decisions": [ + { + "exception_id": "B12-EXC-0001", + "exception_type": "chronology_ambiguity", + "decision": "PASS_NO_ACTION|REVIEW_SOFT|REVIEW_HARD|BLOCK_SOURCE_INTEGRITY|BLOCK_REVIEW", + "severity": "PASS|SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "chronology_ambiguity", + "source_refs": { + "evidence_indexes": [], + "event_candidate_ids": [], + "meeting_clause_refs": [], + "doc_titles": [] + }, + "candidate_use_policy": "prioritize_if_supported|preserve_as_needs_review|do_not_generate_without_support", + "review_instruction": "120자 이내", + "concise_basis": "240자 이내" + } + ] +} +``` + +각 expected exception ID는 정확히 1회 결정되어야 한다. pack에 없는 source ref가 output에 추가되면 GC가 `ADJUDICATION_SOURCE_REF_OUT_OF_SCOPE`로 BLOCK한다. + +--- + +## 8. GC 작업 명세 + +### 8.1 task header + +```yaml +- task_name: Task_B12_gate_audit_finalizer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + # deterministic audit finalizer +``` + +### 8.2 입력 + +- `quality_gates/B1_gate_precheck.json` +- `quality_gates/B2_gate_precheck.json` +- `quality_gates/B12_exception_pack.json` +- `quality_gates/B12_adjudication_parts/PACK-*.json`, 단 expected pack이 있을 때만 + +GC는 final evidence/event file을 수정하지 않는다. 필요하면 존재와 parse 가능성만 확인한다. + +### 8.3 실행 순서 + +1. precheck 2종과 exception pack을 strict parse한다. +2. expected pack ID와 실제 adjudication part ID의 set equality를 검사한다. +3. 예외 0건이면 adjudication part가 0개인지 확인한다. +4. 모든 exception ID가 정확히 1회 판정되었는지 검사한다. +5. decision의 enum, severity, source ref scope를 검사한다. +6. GA hard finding과 GB `BLOCK_SOURCE_INTEGRITY`를 hard findings로 병합한다. +7. `REVIEW_SOFT`, `REVIEW_HARD`, `BLOCK_REVIEW`, budget overflow를 review findings로 병합한다. +8. B1 origin과 B2 origin에 따라 두 audit에 finding을 배분한다. +9. count는 배열의 실제 `len()`으로만 계산한다. +10. severity rank `BLOCK > HARD_WARNING > SOFT_WARNING > PASS`로 overall을 계산한다. +11. audit 2종을 쓴다. +12. audit serialized object를 다시 parse해 schema와 count를 검증한다. + +### 8.4 finding 공통 schema + +```json +{ + "gate_finding_id": "B1-REVIEW-001", + "source_check": "component_coverage", + "severity": "SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "component_coverage_concern", + "domain_hints": [], + "target_stage_hints": [], + "target_task_hints": [], + "source_refs": { + "evidence_indexes": [], + "event_candidate_ids": [], + "meeting_clause_refs": [], + "doc_titles": [] + }, + "review_instruction": "짧은 지시", + "candidate_use_policy": "preserve_as_needs_review", + "concise_basis": "짧은 근거" +} +``` + +`source_refs`를 배열 또는 자유형 object로 두지 않는다. 위 네 named array를 모두 포함한다. + +### 8.5 audit 작성 규칙 + +두 audit은 기존 경로를 유지하고 다음 필드를 반드시 가진다. + +```text +gate_id +checks +overall_severity +block_reasons +warning_reasons +hard_gate_findings +review_findings +rerun_targets +stage2_risk_notes +review_gate_summary +handoff_export_summary +downstream_progression +stage2_auto_progression_allowed +schema_contract_version +``` + +중요 규칙: + +1. `hard_gate_findings`와 `review_findings`는 top-level 한 곳에만 둔다. +2. `checks[]` 안에 같은 finding 배열을 복제하지 않는다. 기존 SHA writer는 top-level과 nested checks를 모두 읽으므로 복제하면 sidecar count가 증가한다. +3. `review_gate_summary.review_gate_count == len(review_findings)`이어야 한다. +4. `handoff_export_summary`의 exported count도 실제 배열 길이와 같아야 한다. +5. `overall_severity == BLOCK`이면 `stage2_auto_progression_allowed == false`와 `downstream_progression == BLOCKED`를 동시에 만족한다. +6. hard finding이 없고 review만 있으면 Stage 2 자동 진행은 허용한다. +7. review finding이 40개를 초과하면 기존 SHA writer의 40개 cap에서 조용히 잘릴 수 있으므로 `REVIEW_QUEUE_OVERFLOW` hard finding을 추가하고 전체 진행을 BLOCK한다. +8. `block_reasons`와 `warning_reasons`는 finding의 짧은 mirror일 뿐 별도 판단 근거가 아니다. + +### 8.6 B1 audit 최소 schema + +```json +{ + "gate_id": "B1_evidence_indexed", + "checks": [], + "overall_severity": "PASS|SOFT_WARNING|HARD_WARNING|BLOCK", + "block_reasons": [], + "warning_reasons": [], + "hard_gate_findings": [], + "review_findings": [], + "rerun_targets": [], + "stage2_risk_notes": [], + "review_gate_summary": { + "review_gate_count": 0, + "highest_review_severity": "PASS|SOFT_WARNING|HARD_WARNING" + }, + "handoff_export_summary": { + "is_soft_handoff_writer": false, + "handoff_writer_task": "Task_B2_SHA256_soft_gate_handoff_writer", + "exportable_review_count": 0 + }, + "downstream_progression": "ALLOWED|ALLOWED_WITH_WARNING|BLOCKED", + "stage2_auto_progression_allowed": true, + "schema_contract_version": "B1_quality_gate.v4" +} +``` + +### 8.7 B2 audit 최소 schema + +```json +{ + "gate_id": "B2_event_candidates", + "schema_invariant_findings": [], + "checks": [], + "overall_severity": "PASS|SOFT_WARNING|HARD_WARNING|BLOCK", + "block_reasons": [], + "warning_reasons": [], + "hard_gate_findings": [], + "review_findings": [], + "rerun_targets": [], + "stage2_risk_notes": [], + "review_gate_summary": { + "review_gate_count": 0, + "highest_review_severity": "PASS|SOFT_WARNING|HARD_WARNING", + "b1_review_count": 0, + "b2_review_count": 0 + }, + "handoff_export_summary": { + "is_soft_handoff_writer": false, + "handoff_writer_task": "Task_B2_SHA256_soft_gate_handoff_writer", + "handoff_path": "quality_gates/stage1_part1_soft_gate_handoff.json", + "exported_review_count": 0, + "handoff_status_candidate": "READY_WITH_REVIEW|READY_NO_REVIEW|BLOCKED" + }, + "downstream_progression": "ALLOWED|ALLOWED_WITH_WARNING|BLOCKED", + "stage2_auto_progression_allowed": true, + "schema_contract_version": "B2_quality_gate.v4" +} +``` + +--- + +## 9. 최종 DAG 명세 + +최하단 `task_procedure`는 다음 논리와 정확히 같아야 한다. + +```yaml +task_procedure: + IN: + nexts: ["Task_A_client_goal", "Task_Evidence_shard_planner"] + wait_until: [] + + Task_A_client_goal: + nexts: [] + wait_until: ["IN"] + + Task_Evidence_shard_planner: + nexts: ["Task_B1_map_doc_*"] + wait_until: ["IN"] + + Task_B1_map_doc_*: + nexts: + - "Task_B2_map_events_{same_ordinal}" + - "Task_B1_gate_merge_deterministic" + wait_until: ["Task_Evidence_shard_planner"] + + Task_B2_map_events_*: + nexts: ["Task_B2_gate_merge_deterministic"] + wait_until: ["Task_B1_map_doc_{same_ordinal}"] + + Task_B1_gate_merge_deterministic: + nexts: ["Task_B2_gate_merge_deterministic"] + wait_until: ["all Task_B1_map_doc_*"] + + Task_B2_gate_merge_deterministic: + nexts: + - "Task_B1B2_gate_llm_adjudicator_*" + - "Task_B12_gate_audit_finalizer" + wait_until: + - "all Task_B2_map_events_*" + - "Task_B1_gate_merge_deterministic" + + Task_B1B2_gate_llm_adjudicator_*: + nexts: ["Task_B12_gate_audit_finalizer"] + wait_until: ["Task_B2_gate_merge_deterministic"] + + Task_B12_gate_audit_finalizer: + nexts: ["Task_B2_SHA256_soft_gate_handoff_writer"] + wait_until: + - "Task_B2_gate_merge_deterministic" + - "all Task_B1B2_gate_llm_adjudicator_*" + + Task_B2_SHA256_soft_gate_handoff_writer: + nexts: ["OUT"] + wait_until: ["Task_B12_gate_audit_finalizer"] + + OUT: + nexts: [] + wait_until: ["Task_B2_SHA256_soft_gate_handoff_writer"] +``` + +0개 wildcard instance에 대한 `all Task_B1B2_gate_llm_adjudicator_*` barrier가 즉시 충족되는지 작은 smoke workflow로 먼저 확인한다. 충족되지 않으면 production YAML을 완성했다고 판단하지 않는다. + +--- + +## 10. 실패 처리와 상태 전파 + +### 10.1 GA-1 BLOCK + +GA-1은 누락 part 또는 parse failure가 있어도 다음을 가능한 범위에서 작성한다. + +- available item만 담은 `evidence_indexed.json` +- `status=BLOCKED` manifest +- hard finding을 담은 B1 precheck + +누락 데이터를 생성해 expected count를 맞추지 않는다. + +### 10.2 GA-2 BLOCK + +GA-2는 schema-valid part만 final에 반영하고 invalid part를 조용히 보정하지 않는다. B2 precheck에 ordinal, path, reason, `rerun_target_task`를 기록한다. exception fanout은 빈 배열로 두고 GC까지 진행하여 BLOCK sidecar를 만들게 한다. + +### 10.3 GB 실패 또는 누락 + +expected pack의 output이 누락되거나 schema가 틀리면 GC는 `ADJUDICATION_PART_MISSING_OR_INVALID` hard finding을 생성한다. 미판정 exception을 PASS로 간주하지 않는다. + +### 10.4 GC 또는 SHA writer 실패 + +audit 또는 sidecar를 쓸 수 없으면 workflow는 성공으로 종료할 수 없다. 채팅의 완료 메시지만으로 성공을 판단하지 않고 파일 존재, strict JSON parse, schema invariant를 확인한다. + +--- + +## 11. 구현 순서 + +### Phase 0. 원본 보존과 사전 검사 + +1. `Stage_1_Part_1_v2.yml`을 `Stage_1_Part_1_v3.yml`로 복제한다. +2. source와 target의 최초 hash가 같은지 기록한다. +3. downstream alias consumer search를 수행한다. +4. SHA writer task block의 원본 hash를 기록한다. +5. 0-instance wildcard barrier smoke test를 수행한다. + +### Phase 1. 결정적 경로 구현 + +1. planner barrier task명 4곳을 수정한다. +2. 기존 B1 LLM gate block을 GA-1 Python block으로 교체한다. +3. 기존 B2 LLM gate block을 GA-2 Python block으로 교체한다. +4. fixture part로 GA-1과 GA-2를 독립 실행해 final 파일을 검증한다. + +### Phase 2. 예외 경로 구현 + +1. GB wildcard task를 추가한다. +2. exception 0건에서 GB instance가 생성되지 않는지 확인한다. +3. 의심 항목 1건에서 GB instance가 정확히 1개 생성되는지 확인한다. +4. budget overflow에서 pack 분할과 auto-block review가 작동하는지 확인한다. + +### Phase 3. 감사와 handoff 연결 + +1. GC를 추가한다. +2. audit count와 source ref schema를 검증한다. +3. 기존 SHA writer를 byte-for-byte 유지한 채 GC 뒤에 연결한다. +4. 최하단 `task_procedure`를 교체한다. + +### Phase 4. 통합 회귀 검증 + +1. YAML parse와 DAG 검사를 수행한다. +2. fixture 전체를 실행한다. +3. Part 2, Part 3, Part 4 parser smoke test를 수행한다. +4. runtime, LLM call count, read bytes, output bytes를 기존 결과와 비교한다. + +--- + +## 12. 필수 회귀 테스트 + +### 12.1 7/10 fixture 기대값 + +| 검사항목 | 기대값 | +|---|---:| +| planner shard count | 30 | +| `evidence_indexed.json.items` | 30 | +| `evidence_indexed` 중복 alias | 없음 | +| B2 final items | 30 | +| input candidates | 74 | +| final candidates | 74 | +| E-019 candidates | 3 | +| E-019 subkinds | `lien_extinction_notice_dispatch`, `ongoing_physical_possession`, `lien_violation_candidate` | +| candidate field loss | 0 | +| missing/unexpected ordinal | 0 | +| normal-path GB LLM calls | 0 | +| audit source_refs shape error | 0 | +| audit/sidecar count mismatch | 0 | + +fixture에 존재하는 이미 분류된 mapper self-warning의 단순 승계는 Python auto-decision으로 처리한다. 따라서 7/10 fixture에서는 GB를 실행하지 않는다. 별도의 ambiguity 주입 테스트에서만 GB 1회 실행을 검증한다. + +### 12.2 결정론 테스트 + +같은 input part로 GA-1과 GA-2를 2회 실행한다. + +다음 두 파일은 바이트 단위로 같아야 한다. + +- `evidence_indexed.json` +- `evidence_event_candidates.json` + +audit의 `executed_at` 또는 elapsed telemetry는 실행마다 달라도 되지만 final fact artifact에는 실행 시각을 넣지 않는다. + +### 12.3 fault injection + +각 테스트는 하나의 오류만 주입한다. + +| 주입 오류 | 기대 결과 | +|---|---| +| E-019 part 제거 | missing ordinal BLOCK, sidecar BLOCKED | +| unexpected E-031 part 추가 | unexpected/stale BLOCK | +| candidate field 하나 삭제 | field conservation BLOCK | +| 잘못된 source evidence ID | reference integrity BLOCK | +| `event_candidates=[]`, `zero_event_reason=null` | schema invariant BLOCK + rerun target | +| duplicate identity signature | candidate 모두 보존 + deterministic duplicate finding | +| 날짜 역전 | compact chronology exception 생성 | +| adjudication output에 pack 밖 source ref 추가 | GC BLOCK | +| review finding 41건 | REVIEW_QUEUE_OVERFLOW BLOCK | + +### 12.4 downstream smoke test + +개정 산출물로 다음 consumer를 실제 실행 또는 최소 parser 단위로 통과시킨다. + +- Part 2 evidence authority map compiler +- Part 3 LES0 input pack compiler +- Part 4 FL0 deterministic compiler + +`items` 단일 배열 제거로 parser가 실패하면 v3를 완료 처리하지 않는다. + +--- + +## 13. 정적 검증 명령과 판정 기준 + +구현 완료 후 최소한 다음 검사를 수행한다. + +### 13.1 YAML parse + +설치된 YAML parser로 `Stage_1_Part_1_v3.yml`을 strict parse한다. parse error가 하나라도 있으면 실패다. + +### 13.2 task명 검사 + +다음 old task명은 정의와 DAG에서 0건이어야 한다. + +```text +Task_B1_quality_gate_evidence_indexed +Task_B2_quality_gate_event_candidates +``` + +다음 new task명은 각각 정의 1건이어야 한다. + +```text +Task_B1_gate_merge_deterministic +Task_B2_gate_merge_deterministic +Task_B1B2_gate_llm_adjudicator_* +Task_B12_gate_audit_finalizer +``` + +### 13.3 writer 검사 + +파일별 `write_file` 호출을 검색하여 제1.2절의 single-writer 표와 일치하는지 확인한다. + +### 13.4 read diet 검사 + +GB prompt 또는 preflight에 다음 full artifact가 포함되면 실패다. + +```text +evidence_all.json +client_meeting.md +evidence_indexed.json +evidence_event_candidates.json +evidence_shard_plan.json +``` + +GA code가 구조 검사를 위해 plan을 읽는 것은 허용한다. GB가 full plan을 읽는 것은 금지한다. + +### 13.5 SHA writer 무수정 검사 + +`Task_B2_SHA256_soft_gate_handoff_writer` block의 source/target SHA-256이 같아야 한다. + +### 13.6 비밀정보 검사 + +기존 MCP 인증 정보는 원본에서 복제될 수 있으나 새로운 토큰, key 또는 자격증명을 추가하지 않는다. 작업 보고서나 로그에 기존 secret 값을 출력하지 않는다. + +--- + +## 14. 완료 정의 + +다음 조건을 모두 만족해야 작업을 완료했다고 보고한다. + +- [ ] `Stage_1_Part_1_v3.yml`이 생성되었고 v2 원본은 변경되지 않았다. +- [ ] YAML이 strict parse된다. +- [ ] planner barrier와 최하단 DAG가 새 task명으로 일치한다. +- [ ] 기존 대형 B1/B2 LLM gate 2개가 완전히 제거되었다. +- [ ] GA-1, GA-2, GC는 Python code-executor다. +- [ ] GB는 compact exception만 읽고 정상 경로에서 instance 0개다. +- [ ] 최종 file writer 권한이 단일하다. +- [ ] SHA writer block은 원본과 동일하다. +- [ ] fixture에서 30 evidence items, 30 event items, 74 candidates가 보존된다. +- [ ] E-019와 그 candidate 3건이 보존된다. +- [ ] candidate key/value loss가 0건이다. +- [ ] audit count는 실제 배열 길이와 일치한다. +- [ ] 모든 `source_refs`가 named object schema를 따른다. +- [ ] sidecar가 정상적으로 생성되고 digest가 64자리 lowercase SHA-256이다. +- [ ] Part 2/3/4 parser smoke test가 통과한다. +- [ ] 정상 경로의 gate LLM 호출 수가 0회임을 runtime telemetry로 확인했다. + +하나라도 충족하지 못하면 "부분 완료" 또는 "개정 완료"라고 표현하지 말고, 실패한 조건과 관련 task/path를 명시한다. + +--- + +## 15. 구현자 최종 보고 형식 + +개정 작업을 수행한 LLM은 마지막 보고에 다음만 포함한다. + +1. 생성한 YAML 경로 +2. 변경한 task 목록 +3. 실행한 검증과 통과/실패 결과 +4. fixture 보존 count +5. 정상 경로 LLM 호출 수 +6. runtime capability 또는 downstream 호환성 관련 잔여 blocker + +분석만 수행하고 YAML을 생성하지 않았거나, YAML만 만들고 fixture 검증을 하지 않았다면 이를 명확히 구분한다. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_1_Inefficiency_Report_Claude_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_1_Inefficiency_Report_Claude_v1.md new file mode 100644 index 00000000..2f2254c1 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_1_Inefficiency_Report_Claude_v1.md @@ -0,0 +1,189 @@ +# Part 1 비효율 분석 및 개정 전략 보고서 (Claude v1) + +- 대상: `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_1_v2.yml`의 + `Task_B1_quality_gate_evidence_indexed` (이하 **B1 게이트**), + `Task_B2_quality_gate_event_candidates` (이하 **B2 게이트**) +- 근거 자료: v.6 YAML 4종 전체 + 실행 결과 `v.6/Results_July_10_3_15pm/` 실측 + Part 2/3/4 하류 소비 지점 grep 검증 +- 실행 실적: Part 1 = 8분 45초 / $2.09 (v.5 대비 악화, Part 1이 4개 Part 중 최장 실행) + +--- + +## 0. 결론 요약 + +1. **비효율의 본체는 "fan-out 결과를 모아서 넘기는 것" 자체가 아니라, 그 fan-in을 LLM 컨텍스트로 수행하고 통합 결과 전체를 LLM 출력 토큰으로 손으로 재작성하게 만든 구조다.** 두 게이트는 합쳐서 약 500KB를 LLM 입력으로 읽고, 약 141KB의 최종 JSON을 LLM 출력 토큰으로 재생성하며, 이 두 개의 대형 LLM 호출이 파이프라인 꼬리에서 **직렬**로 실행된다. +2. 두 게이트가 수행하는 10개 검증 항목 중 대부분은 결정적(Python) 검사로 완전 대체 가능하며, 실제 실행 결과에서 LLM 추론이 추가로 발견한 것은 **0건**이다(전 항목 PASS, 유일한 finding은 mapper가 이미 남긴 self-warning의 릴레이 1건). +3. 오히려 LLM-writer 구조가 **실증된 품질 결함 4건**을 만들었다: (Q-1) E-019 항목 통째 무단 누락(후보 3건 소실, completeness check는 PASS로 오판), (Q-2) candidate 필드 16종 무단 탈락(YAML의 pass-through 지시 위반), (Q-3) B1 최종본에 동일 배열 2중 수록(출력 2배), (Q-4) dedup 3건 미기록(`dedup_findings: null`). +4. 개정 방향: Part 3(LES1/LES2)·Part 4에서 이미 검증한 **"결정적 propose/조립 → 조건부 국소 LLM 판정 → 결정적 finalize" 샌드위치 패턴**을 두 게이트에 이식한다. 게이트 구간 예상 효과: 수 분 → 수십 초, 게이트 LLM 비용 사실상 제거, Q-1~Q-4 원천 차단. + +--- + +## 1. 실측 데이터 (Results_July_10_3_15pm 기준) + +| 파일/디렉토리 | 크기 | 비고 | +|---|---|---| +| `evidence_shard_plan.json` | 84,582 B | 두 게이트가 전문을 읽음. 게이트에 필요한 것은 `expected_ordinals`/`shard_count`뿐 | +| `evidence_indexed_parts/E-*.json` (30개) | 54,289 B | B1 mapper fan-out 산출물 | +| `evidence_event_candidate_parts/E-*.json` (30개) | 129,294 B | B2 mapper fan-out 산출물, 후보 총 74건 | +| `evidence_indexed.json` (B1 게이트가 LLM 출력으로 재작성) | **91,841 B** | parts 합(54KB)보다 큼 — 동일 배열이 `items`와 `evidence_indexed` 키로 2중 수록(§5 Q-3) | +| `evidence_event_candidates.json` (B2 게이트가 LLM 출력으로 재작성) | 49,327 B | 29 items / 후보 71건 — **E-019 항목·후보 3건 소실**(§5 Q-1, Q-2) | +| `quality_gates/B1_evidence_indexed_gate.json` | 2,552 B | 전 체크 PASS | +| `quality_gates/B2_event_candidates_gate.json` | 3,017 B | 전 체크 PASS, review finding 1건(mapper self-warning 릴레이) | +| `quality_gates/stage1_part1_soft_gate_handoff.json` | 2,773 B | 결정적 SHA writer 산출(READY_WITH_REVIEW) | + +게이트별 LLM 입출력 규모(추정 포함): + +- **B1 게이트 입력**: plan 84.6KB + B1 parts 30개 54.3KB + `evidence_all.json` 전문 + `client_meeting.md` 전문 (원문 2종은 결과 폴더 외 입력물, 합계 수십~수백 KB 추정). 출력: 91.8KB + audit 2.6KB ≈ **약 94KB를 출력 토큰으로 생성**. +- **B2 게이트 입력**: plan 84.6KB + B2 parts 30개 129.3KB + `evidence_indexed.json` 91.8KB + B1 audit 2.6KB ≈ **약 308KB**. 출력: 49.3KB + audit 3.0KB ≈ **약 52KB를 출력 토큰으로 생성**. +- 둘 다 `gemini-3.1-flash-lite` + `llm_reasoning: high` + localdocs 툴 루프(list → 다중 read → 메모리 검증 → 대형 write ×2). 출력 토큰 생성은 순차적이므로, 합계 약 14만 6천 바이트(대략 4만~5만 출력 토큰 수준)의 재작성 + high reasoning의 사고 토큰이 게이트 구간 벽시계 시간의 지배항이다. + +--- + +## 2. 두 게이트의 실행 방식 (질문 6에 대한 답) + +### 2.1 DAG 상 위치와 직렬화 + +``` +Task_Evidence_shard_planner + ├─ Task_B1_map_doc_001..030 (fan-out, max_concurrency 12) + │ └─ (same_ordinal) Task_B2_map_events_001..030 (fan-out, max_concurrency 12) + ├─ all Task_B1_map_doc_* ──▶ Task_B1_quality_gate_evidence_indexed [LLM, 대형] + └─ all Task_B2_map_events_* ─┐ + Task_B1_quality_gate ─┴▶ Task_B2_quality_gate_event_candidates [LLM, 대형] + └▶ Task_B2_SHA256_soft_gate_handoff_writer [code-executor, 결정적] ─▶ OUT +``` + +- B2 게이트는 `all Task_B2_map_events_*` **그리고** B1 게이트 완료를 함께 대기한다. B1 게이트는 B2 mapper들과 병렬로 돌 수 있으나, 꼬리에서 **B1 게이트 → B2 게이트 → SHA writer**의 3단 직렬 체인이 형성되고, 그중 앞 2단이 각각 위 §1 규모의 대형 LLM 호출이다. +- B2 게이트가 B1 게이트를 대기하는 실질적 이유는 두 가지다: (a) `evidence_indexed.json`(B1 final)을 읽어 후보의 `evidence_index` 참조 무결성을 검증, (b) B1 audit의 review finding을 가져와 soft handoff 통합 소스로 보존. **(a)에 필요한 정보는 finalized E-ID 목록(30줄)뿐인데 91.8KB 전문을 읽고, (b)는 2.6KB audit의 극히 일부 필드만 쓴다.** 즉 직렬 의존 자체는 정당하나, 의존을 만족시키는 데 전달되는 데이터 표면이 필요량의 수십 배다. + +### 2.2 게이트 내부 절차 (공통 패턴) + +두 게이트 모두 동일한 5단 패턴이다: + +1. `list_docs`/`read_docs`로 plan + **모든** part 파일 + (B1: 원문 `evidence_all.json`·`client_meeting.md` / B2: B1 final 전문 + B1 audit)을 LLM 컨텍스트에 적재. +2. 5개 검증 항목을 "LLM 추론으로" 수행 (B1: shard completeness, index determinism, duplicate alias, component coverage, authority drift / B2: completeness, evidence reference integrity, candidate_id collision, chronological ordering, legal-effect coverage + schema invariant 사전검사). +3. E-###/EVT-### ID finalize, identity_signature dedup. +4. 최종 envelope JSON의 "정확한 serialized string"을 메모리에서 생성. +5. `write_file`로 최종 파일 + audit report 저장. + +`ABSOLUTE_FORBIDDEN`에 "simple concat으로 작동(반드시 LLM 검증 수행)"이 명시되어 있어, 기계적 병합조차 LLM이 전량 손으로 재타이핑하도록 강제된다. 이것이 §1의 출력량을 만든 직접 원인이다. + +--- + +## 3. 질문 7에 대한 답: "모아서 한 번에 넘기는" 것이 런타임을 증폭시키는가? + +**부분적으로 그렇고, 더 정확히는 3중 증폭 구조다.** + +- **fan-in 자체는 필요하다.** 단일 writer 원칙(최종 파일을 한 task만 작성)과 cross-document 검증(ID 충돌, 중복 서명, 참조 무결성)은 본질적으로 전 part를 한 곳에 모아야 가능하다. 따라서 "모아서 넘기는 설계"를 없애는 것이 답이 아니다. +- **증폭 ①: fan-in을 LLM 컨텍스트로 수행.** 30개 part + plan 전문 + 원문(B1) + 선행 final 전문(B2)을 전부 LLM 입력으로 밀어 넣는다. 필요 필드만 추리면 입력의 대부분은 불필요하다(§2.1, §6). +- **증폭 ②: 통합 결과 전체를 LLM 출력으로 재작성.** 게이트가 최종 파일의 단독 writer이므로, 병합·정렬·ID부여라는 기계적 작업의 결과물 141KB를 LLM이 토큰 단위로 다시 생성한다. LLM 호출에서 출력 토큰은 입력 토큰보다 수 배 느리고 비싸며, 이 재작성이 벽시계 시간의 지배항이다. **게다가 재작성은 무손실이 아니었다(§5).** +- **증폭 ③: 대형 LLM 호출 2개의 직렬 연쇄 + reasoning high.** B1 게이트 → B2 게이트가 순차이고, 둘 다 `llm_reasoning: high`로 사고 토큰까지 최대로 쓴다. 집계·병합 작업에 high reasoning이 기여한 증거는 실행 결과에 없다(발견 0건). + +요약하면: *모아서 넘기는 것*이 아니라 *모은 것을 LLM에게 읽히고, LLM에게 다시 쓰게 하는 것*이 비효율의 본체다. + +--- + +## 4. 10개 검증 항목의 성격 분류 — 무엇이 정말 LLM을 필요로 하는가 + +| # | 검증 항목 | 성격 | 결정적 대체 | 실행 결과 | +|---|---|---|---|---| +| B1-1 | shard completeness (expected vs received) | 집합 비교 | **완전 대체** (set diff) | PASS | +| B1-2 | evidence_index determinism (E-### 형식·유일성) | 정규식+유일성 | **완전 대체** | PASS | +| B1-3 | duplicate alias (제목/별칭 중복 의심) | 문자열 유사 + 판단 | 후보쌍 추출은 결정적(정규화 일치·n-gram), **판정만 LLM 잔존 가능** | PASS(발견 0) | +| B1-4 | component coverage (구성요소 누락) | mapper self-warning 승계 + 판단 | 규칙 기반 1차(필수 키 존재) + **의심건만 LLM** | PASS | +| B1-5 | authority drift (part 내용이 원문과 괴리) | 원문 대조 추론 | 상시 수행 불가(원문 필요). **조건부 spot-check로 강등** 권고(§6.3) | PASS(발견 0) | +| B2-0 | schema invariant (empty candidates + null zero_event_reason) | 키 존재 검사 | **완전 대체** | 위반 0 | +| B2-1 | event candidate completeness | 집합 비교 | **완전 대체** | PASS | +| B2-2 | evidence reference integrity (B1 E-ID 존재) | 집합 멤버십 | **완전 대체** (E-ID 목록만 필요) | PASS | +| B2-3 | candidate_id collision + identity_signature dedup | 형식·유일성·exact-match | **완전 대체** (canonical 우선순위 규칙도 이미 결정적으로 명문화됨: raw>derived, confidence, locators 수, ordinal) | PASS | +| B2-4 | chronological ordering (이행기·시효·도달일 시간 정합) | 날짜 비교 + 법률 판단 | 위반 후보 추출은 결정적(날짜 역전 스캔), **판정만 LLM 잔존 가능** | PASS(발견 0) | +| B2-5 | legal-effect coverage (payment/notice/auction/lien 등 누락) | 분포 검사 + 판단 | REQUIRED_EVENT_SUBKINDS 존재 검사는 결정적, **누락 의심 사유 서술만 LLM 잔존 가능** | PASS | + +**핵심 관찰**: 10+1개 항목 중 7개는 100% 결정적 대체가 가능하고, 나머지 4개도 "의심 후보 추출(결정적) → 판정(LLM)"으로 분해하면 LLM 입력이 O(전체 데이터)에서 O(의심 항목)로 줄어든다. 이는 Part 3 LES1→LES2에서 이미 적용해 검증한 것과 동일한 구조다(LES2 판정 universe = 예외 항목만). 그리고 이번 실행에서 의심 항목은 사실상 0건이었다 — 즉 개정 구조였다면 **게이트 구간의 LLM 호출이 0회 또는 초소형 1회**로 끝났을 실행이다. + +또한 유일한 review finding(`review_findings_in_mapper`, SOFT_WARNING, "상계 주장 등 일부 항변 사항은 Stage 2에서 검토 필요", refs E-010/E-011/E-012/E-028)은 mapper part가 이미 남긴 self-warning의 **분류·릴레이**였다. 이 승격 판정이야말로 국소 LLM 1회(또는 규칙 매핑)로 충분한 작업이다. + +--- + +## 5. 실행 결과가 실증한 품질 결함 — LLM-writer 구조의 직접 비용 + +경량화 논거이기 이전에, 현행 구조가 **정확성 자체를 훼손**하고 있음이 이번 실행물에서 확인된다. + +- **Q-1 (중대): E-019 항목 통째 누락.** `evidence_event_candidate_parts/E-019.json`(제목 "소멸청구 통지서", 후보 3건, zero_event 아님)이 최종 `evidence_event_candidates.json`에서 항목째 사라졌다(30 part → 29 items, 후보 74 → 71). 그런데 같은 게이트의 completeness check는 `received_ordinals`에 001~030 전부를 기록하고 PASS를 선언했다 — **읽기는 했으나 재작성에서 빠뜨린 것**으로, LLM 재생성 writer의 전형적 실패 양식이다. 소멸청구 통지서는 Part 4 `_lien_extinction_notice_scan`과 Part 2 notice 도메인이 소비해야 할 법적 중대 자료라 실질 피해 가능성이 있다. +- **Q-2 (중대): pass-through 위반 — candidate 필드 16종 무단 탈락.** YAML은 명시적으로 "mapper part의 모든 component field(participants, current_state_candidate, succession_candidate, legal_effect_candidate 등)는 그대로 보존한다"고 지시하지만, 최종본 candidate에는 28개 필드 중 12개만 남았다. 탈락: `amount`, `location`, `time_text`, `time_precision`, `event_end_date`, `fraction_ref`, `legal_effect_candidate`, `legal_keywords`, `defense_candidate`, `succession_candidate`, `commercial_successor_candidates`, `actio_relevance_candidates`, `current_state_candidate`, `source_fact_candidate`, `meeting_clause_refs`, `support_locators`. `fraction_ref`(지분 이전 추적)·`legal_effect_candidate`·`defense_candidate`는 v1/v2 개정에서 하류 연계를 위해 일부러 추가·강화한 필드들이다. +- **Q-3: B1 최종본 2중 수록.** `evidence_indexed.json`의 `items`와 `evidence_indexed` 키가 **바이트 동일한 30개 배열**을 각각 담고 있다(91.8KB, parts 합 54.3KB보다 커진 이유). 스키마 문구의 "top-level `items` 또는 v.2 호환 구조"라는 양자택일 모호성을 LLM이 "둘 다"로 해석한 결과로 보이며, 출력 토큰(=시간·비용)을 정확히 2배로 만든다. +- **Q-4: dedup 미기록.** 후보 3건 감소가 dedup이었다면 audit의 `dedup_findings`에 기록해야 하나 `null`이다(Q-1과 결합 시, 감소분은 기록 없는 소실이다). + +**함의**: "simple concat 금지, 반드시 LLM 검증"이라는 FORBIDDEN 규칙은 품질을 지키기 위한 것이었지만, 실측 결과는 반대로 **결정적 병합이라면 원천적으로 불가능한 4종의 오류**를 만들었다. 병합·보존·직렬화는 기계가, 판단은 LLM이 해야 한다는 파이프라인 전반의 설계 원칙("결정적 propose → LLM 예외 판정 → 결정적 finalize")과 정확히 합치하는 증거다. + +--- + +## 6. 삭제·축소 안전성 판정 (질문 5에 대한 답 — 하류 연계 근거) + +### 6.1 절대 보존해야 할 인터페이스 (하류 소비 실증) + +| 산출물 | 소비자 (grep 실증) | 보존 범위 | +|---|---|---| +| `evidence_indexed.json` | Part 2(5개소)·Part 3(4개소)·Part 4(3개소) | 파일 경로·envelope·item 스키마(`evidence_index`, `title`, `doc_type`, `source_pointer`, key_* 등) 전부 보존. 단 **중복 배열 1개는 제거 가능**(하류는 `items` 기준 소비) | +| `evidence_event_candidates.json` | Part 2(5개소)·Part 3(3개소)·Part 4(3개소) | `schema_version="evidence_event_candidates.v1"` envelope + item/candidate 전 필드 보존(오히려 Q-2 탈락분 **복원** 필요) | +| `quality_gates/stage1_part1_soft_gate_handoff.json` | Part 2 Stage_A(`PART1_SOFT_GATE_HANDOFF_FILE`, `_compact_part1_soft_gate_handoff`가 스키마 버전·digest_guard·review 요약을 검증·승계) | sidecar 스키마 v1 + `digest_guard` 4종 SHA256 + hard/review 요약 구조 완전 보존 | +| `quality_gates/B1_*.json`, `B2_*.json` (audit) | **Part 2/3/4 소비 0건.** Part 1 내부에서만 소비(B2 게이트가 B1 audit 읽기, SHA writer가 두 audit 읽어 sidecar 생성) | 파일은 유지하되 **내부 인터페이스로 재정의 가능** — SHA writer가 읽는 필드(`hard_gate_findings[]`, `review_findings[]`, `review_gate_summary`, `handoff_export_summary`)와 감사 가치가 있는 결정적 체크 결과만 있으면 충분 | + +### 6.2 삭제해도 안전한 작업 (근거 포함) + +1. **최종 파일의 LLM 재작성 전체.** 병합·정렬·ID finalize·dedup·직렬화는 §4에서 본 대로 전부 결정적 규칙으로 명문화되어 있다(canonical 우선순위까지 YAML에 이미 결정적 규칙으로 적혀 있음). code-executor로 옮기면 산출물 스키마는 동일하고 Q-1~Q-4가 구조적으로 불가능해진다. 하류는 파일 내용 스키마만 보므로 writer의 구현 주체 변경은 하류 비가시적이다. +2. **B2 게이트의 `evidence_indexed.json` 전문 로드.** 참조 무결성 검증에 필요한 것은 finalized E-ID 집합(≈30개 문자열)뿐. B1 결정적 writer가 audit(또는 초소형 사이드 파일)에 `finalized_evidence_indexes: ["E-001",...]`를 남기면 91.8KB → 약 0.5KB로 축소된다. +3. **두 게이트의 `evidence_shard_plan.json` 전문 로드.** 필요한 것은 `expected_ordinals`·`shard_count`·`stale_part_policy`. 결정적 코드는 json 파싱 후 해당 키만 쓰므로 "로드"는 무해해지지만(코드 실행이므로 토큰 비용 0), LLM 잔존 판정 호출에는 절대 전달하지 않는다. +4. **B1 게이트의 `evidence_all.json`·`client_meeting.md` 상시 전문 로드 (authority drift 상시 검사).** 실행 실적상 발견 0건, mapper가 이미 원문(shard)을 직접 읽고 part를 생성하며 self-warning 채널을 가진다. 하류에도 이 검사 결과를 소비하는 지점이 없다. → **상시 검사 삭제, 조건부 spot-check로 강등**: 결정적 통합기가 의심 신호(예: part의 key_dates/key_amounts가 비었는데 doc_type상 있어야 하는 유형, mapper self-warning 존재)를 낸 문서에 한해, 해당 shard 원문만 국소 LLM 판정 입력에 첨부한다. +5. **`llm_reasoning: high`.** 잔존하는 국소 판정 호출은 low(필요시 medium)로 충분하다. 집계 작업에 high가 기여한 증거 없음. +6. **audit report의 서사적 확장 필드 중 소비자 없는 부분** (`stage2_risk_notes[]` 등 자유서술): sidecar와 Part 2가 소비하지 않음. 유지 비용이 낮아 보이나 LLM이 생성하는 한 출력 토큰이므로, 결정적 생성으로 전환하면서 "결정적 체크 결과 + 승격 판정 결과"로 국한한다. + +### 6.3 삭제하면 안 되는 것 + +- **단일 writer 원칙과 fan-in 지점 자체** (필요 — §3). +- **B2→B1 의존** (참조 무결성·review 통합에 필요 — 단 전달물은 §6.2-2처럼 축소). +- **mapper self-warning의 승격 판정 경로** (이번 실행의 유일한 실질 산출 — review finding → sidecar → Part 2 승계 체인은 실제로 작동 중). +- **`digest_guard` SHA256 sidecar와 그 결정적 writer** (이미 결정적이고 Part 2가 검증에 사용). +- **HARD/REVIEW 분류 규칙과 BLOCK 의미론** (rerun_target_task 등 재실행 라우팅 포함). + +--- + +## 7. 개정 전략 + +### 7.1 권장안 A — 게이트 분해: "결정적 통합기 + 조건부 국소 판정" (Part 3/4 검증 패턴 이식) + +``` +[현행] all B1 parts ─▶ B1 LLM 게이트(읽기 전부·재작성 전부) ─▶ B2 LLM 게이트(읽기 전부·재작성 전부) ─▶ SHA writer +[개정] all B1 parts ─▶ GA-1 결정적 통합기(B1) ─┐ + all B2 parts ─▶ GA-2 결정적 통합기(B2) ─┴▶ GB 조건부 국소 LLM 판정(의심항목만, 0건이면 skip) ─▶ GC 결정적 finalize/audit ─▶ SHA writer +``` + +- **GA-1 `Task_B1_gate_merge_deterministic`** (code-executor): plan 파싱 → 전 part 로드 → B1-1/B1-2 결정적 검사 → duplicate-alias 후보쌍·component-coverage 의심건·authority-drift 의심건을 **결정적 신호 규칙**으로 추출 → E-### finalize → `evidence_indexed.json` 조립·저장(**`items` 단일 배열**, Q-3 해소) → audit skeleton(결정적 체크 결과 + `finalized_evidence_indexes[]` + `suspect_items[]`) 저장. mapper self-warning은 전량 `review_candidates[]`로 승계. +- **GA-2 `Task_B2_gate_merge_deterministic`** (code-executor): GA-1 완료 대기(E-ID 목록만 사용) + 전 B2 part → schema invariant/completeness/참조 무결성/ID collision 결정적 검사 → identity_signature exact dedup(우선순위 규칙 코드화, `dedup_findings[]` 의무 기록 — Q-4 해소) → EVT-ID finalize → **candidate 전 필드 pass-through**로 `evidence_event_candidates.json` 조립·저장(Q-1·Q-2 구조적 해소: 조립이 `dict` 복사이므로 항목·필드 소실 불가) → 날짜 역전·REQUIRED_EVENT_SUBKINDS 공백 등 의심건 추출. +- **GB `Task_B1B2_gate_llm_adjudicator`** (LLM, flash-lite, reasoning **low**, 조건부): GA-1/GA-2가 추출한 의심 항목 + mapper self-warning의 **compact 목록만** 입력(항목당 발췌 수백 자, authority-drift 의심 문서에 한해 해당 shard 원문 첨부). 출력은 항목별 `{finding_id, severity, issue_type, concise_basis, source_refs}` 판정만(수백 토큰). **의심 항목 0건이면 이 task는 호출 없이 통과**(GA 산출 audit에 "no suspects" 기록). Part 3 LES2의 budget_gate 방식(항목 수·문자 수 상한)을 그대로 재사용한다. +- **GC (GA 내 또는 초소형 후속 code-executor)**: GB 판정을 audit의 `hard_gate_findings[]`/`review_findings[]`/`review_gate_summary`/`handoff_export_summary`로 병합(SHA writer가 읽는 인터페이스 동일 유지) → 기존 `Task_B2_SHA256_soft_gate_handoff_writer` 무수정 연결. + +**하류 영향 없음 논증**: 최종 2개 파일의 스키마·경로·envelope 동일(+Q-2 복원으로 오히려 하류 v2 필드가 정상 공급됨), audit 파일의 SHA-writer 소비 필드 동일, sidecar 완전 동일. COMMON_CACHE_PREFIX_STAGE_1 블록은 GB(신규 LLM task)에 그대로 부착하고 기존 블록은 수정하지 않는다. SKILL.md의 code-executor 보일러플레이트(localdocs MCP httpx 패턴, `{{__user_hash__}}`)를 GA/GC에 그대로 사용한다. + +**예상 효과(추정 명시)**: 게이트 구간 LLM 입력 ~500KB → 0~수 KB, LLM 출력 ~146KB → 0~1KB. 게이트 구간 벽시계 수 분 → code 2회(수 초~수십 초) + 조건부 LLM ≤1회. Part 1 $2.09 중 게이트 2호출이 차지하던 몫(입출력 규모상 mapper 60호출 대비 최대 단일 호출 2건)이 사실상 제거된다. 이번 실행과 동일 입력이었다면 GB는 self-warning 승격 1건만 판정하는 초소형 호출(또는 규칙 매핑으로 0회)이었다. + +### 7.2 대안 B — 구조 유지 최소 개정 (권장하지 않음, 비교용) + +현행 LLM 게이트 골격을 유지한 채: (i) `llm_reasoning: high → low`, (ii) B2 입력에서 `evidence_indexed.json` 전문 제거(B1 audit에 E-ID 목록 추가), (iii) B1 산출 envelope의 중복 배열 금지 문구를 단일 형태로 확정, (iv) authority drift를 "self-warning 있는 문서만 해당 shard 대조"로 축소, (v) pass-through 위반 시 BLOCK을 POST_WRITE_VERIFICATION에 추가(필드 수 count 검증). +→ 입력은 절반 이하로 줄지만 **출력 재작성 병목(약 96KB: 중복 제거된 B1 46KB + B2 50KB)은 그대로 남고**, Q-1/Q-2류(재작성 소실)는 지시문 강화로 완화될 뿐 구조적으로 차단되지 않는다. 실행 시간 개선 폭도 제한적일 것으로 추정된다. 안 A가 파이프라인의 기존 설계 원칙과도 정합하므로 A를 권장한다. + +### 7.3 이행 순서 및 검증 계획 + +1. GA-1/GA-2/GB/GC를 v.6 YAML에 반영한 `Stage_1_Part_1_v3.yml` 작성(task_procedure 갱신: `all Task_B1_map_doc_*`→GA-1, `all Task_B2_map_events_*`+GA-1→GA-2→GB(조건부)→GC→SHA writer). +2. 결정적 통합기 fixture 검증: 이번 실행의 30개 part를 입력으로 GA-1/GA-2를 로컬 실행 → (a) `evidence_indexed.json`이 현행 대비 중복 배열 제거 외 item-level 동등, (b) `evidence_event_candidates.json`에 **E-019 3건 포함 74후보(중복 제외 시 dedup_findings 기록과 정확 일치)** + candidate 28필드 보존, (c) 2-run 바이트 동일성(결정론), (d) sidecar 입력 인터페이스 필드 존재. +3. 의심 항목 주입 테스트(고의 결측 part, 중복 signature, 날짜 역전)로 GB 트리거·BLOCK 경로 확인. +4. 실 실행으로 Part 1 시간·비용 재측정 후 v.5/v.6과 비교. + +--- + +## 부록 A. 근거 실측 로그 요약 + +- v.6 4개 YAML은 v.5 산출본과 byte-identical (diff 확인). +- 게이트 audit 실측: B1 checks 5종 전부 PASS(`received_ordinals` 30/30), B2 checks 전부 PASS + `schema_invariant_findings: []`, `overall_severity: PASS`, `hard_gate_findings: []`, review 1건(SOFT_WARNING, mapper self-warning 릴레이, refs E-010/E-011/E-012/E-028), `downstream_progression: ALLOWED_WITH_WARNING`. +- sidecar 실측: `handoff_status: READY_WITH_REVIEW`, `stage2_auto_progression_allowed: true`, digest_guard SHA256 4종 정상. +- 최종본 대조: `evidence_indexed.json` — `items` 30 == `evidence_indexed` 30 (JSON 정규화 후 동일); `evidence_event_candidates.json` — items 29(E-019 부재), 후보 71/74, zero-event 1건(E-016, REFERENCE_ONLY_DOC)은 정상 처리, `dedup_findings: null`. +- 하류 참조 건수: evidence_indexed.json → P2:5 / P3:4 / P4:3, evidence_event_candidates.json → P2:5 / P3:3 / P4:3, `soft_gate_handoff` → P2 Stage_A 소비 확인, audit 2종 → P2/P3/P4 참조 0건. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_1_Inefficiency_Report_Codex_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_1_Inefficiency_Report_Codex_v1.md new file mode 100644 index 00000000..0c76372b --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_1_Inefficiency_Report_Codex_v1.md @@ -0,0 +1,556 @@ +# Stage 1 Part 1 B1/B2 Quality Gate 비효율 및 최적화 보고서 + +## 0. 결론 요약 + +검토 대상 중 사용자가 `B2_quality_gate_event_indexed`라고 지칭한 첫 작업의 YAML상 실제 이름은 `Task_B1_quality_gate_evidence_indexed`이다. 두 번째 작업의 실제 이름은 `Task_B2_quality_gate_event_candidates`이다. 본 보고서는 이 두 작업을 각각 **B1 gate**, **B2 gate**로 부른다. + +최종 결론은 다음과 같다. + +1. 두 gate의 **논리적 기능과 최종 산출물은 삭제하면 안 된다.** `evidence_indexed.json`과 `evidence_event_candidates.json`은 Part 2, Part 3, Part 4가 실제로 읽는 downstream authority file이다. +2. 그러나 두 gate에서 수행하는 작업 대부분은 LLM 추론이 아니라 Python으로 완전히 결정할 수 있는 검증·정렬·병합·ID 확정·스키마 검사다. **정상 경로의 full-context LLM 실행은 제거할 수 있다.** +3. 현재는 B1 gate와 B2 gate가 각각 모든 mapper part를 한 번에 다시 읽는 global fan-in 구조다. 특히 B2 gate는 30개 B2 part 외에 84,582-byte planner, 91,841-byte B1 final, B1 audit까지 다시 읽는다. 이것이 사용자가 의심한 “앞 작업의 결과를 한 번에 모아 후행 작업에 넘기는 비효율”의 정확한 형태다. +4. 다만 각 mapper의 출력이 orchestrator 대화문에 모두 삽입되는 것은 아니다. part 파일을 localdocs로 다시 읽는 방식이다. 병목은 **대화 context 누적**이 아니라 **barrier 이후 대량 파일 재독해 + high-reasoning LLM의 전역 병합·검증**이다. +5. 실제 2026-07-10 결과에서 B2 gate는 30개 part가 모두 반영됐다고 `PASS`했지만, 최종 파일은 29개 item만 포함한다. `E-019` 전체와 그 안의 유치권 관련 event candidate 3개가 소실됐다. 따라서 현재 구조는 느릴 뿐 아니라 deterministic reducer보다 merge 품질도 낮다. +6. 최적 경로는 두 gate를 **deterministic reducer**로 전환하고, Python이 생성한 작은 exception pack에 실제 법률적·의미론적 모호성이 있을 때만 단일 LLM adjudicator를 실행하는 것이다. +7. 현재 gate 모델은 이미 `gemini-3.1-flash-lite`다. 주된 개선수단은 더 약한 모델로 바꾸는 것이 아니라 **LLM 호출 자체와 입력량을 제거하는 것**이다. 예외 추론이 발생할 때만 같은 모델을 `reasoning=medium`, `verbosity=low`로 사용한다. + +## 1. 검토 범위와 근거 + +### 1.1 검토한 YAML + +| Part | 파일 | 핵심 downstream 역할 | +|---|---|---| +| Part 1 | `Stage_1_Part_1_v2.yml` | client goal, evidence index, event candidates, Part 1 gate handoff 생성 | +| Part 2 | `Stage_1_Part_2_v2.yml` | Part 1 authority file을 정규화하여 BO 50건 및 3개 signal file 생성 | +| Part 3 | `Stage_1_Part_3_v1.yml` | BO·signals·evidence/event를 이용해 legal effect structure 생성 | +| Part 4 | `Stage_1_Part_4_v2.yml` | BO·legal effect·evidence/event를 이용해 Fact Ledger 생성 | + +네 YAML의 task/model/DAG와 실제 downstream parser를 함께 확인하였다. Part 2의 `Task_C_BO_Stage_A_input_normalization`, Part 3의 `Task_LES0_input_pack_compiler`, Part 4의 `Task_FL0_fact_source_pack_compiler`가 모두 Part 1의 final authority file을 직접 소비한다. + +### 1.2 검토한 2026-07-10 실행 결과 + +`Results_July_10_3_15pm`에는 다음 주요 결과가 존재한다. + +- Part 1: 30개 evidence shard, 30개 B1 part, 30개 B2 part, `evidence_indexed.json`, `evidence_event_candidates.json`, B1/B2 gate report, soft-gate handoff +- Part 2: `BO.json` 50건, `actio_case_signals.json` 5건, `case_liability_signals.json` 50건, `legal_effect_signals.json` 50건 +- Part 3: `legal_effect_structures.json` 및 LES intermediate/exception artifacts +- Part 4: `Fact_Ledger_base.json` 50건 및 Fact Ledger intermediate/report artifacts + +Part 1 전체 런타임 8분 45초와 비용 $2.09만 제공되었고 sub-task별 timing/token log는 없다. 그러므로 특정 gate가 정확히 몇 초 또는 몇 달러를 소비했다고 단정할 수는 없다. 아래 평가는 YAML의 실제 fan-in, 모델 설정, 파일 크기와 결과 보존성에 근거한 구조적 평가다. + +또한 v.5와 v.6 Part 1은 planner, 30-way B1/B2 mapper fan-out, B1/B2 gate, SHA256 handoff writer라는 기본 topology와 gate 모델이 같다. 따라서 v.6의 전체 런타임 증가를 두 gate의 “신설” 탓으로 돌릴 수는 없다. v.6의 확대된 schema·prompt·출력량, 실제 입력 사건의 복잡도, 모델 응답시간도 함께 계측해야 한다. + +## 2. 현재 Part 1 실행 구조 + +```text + +----------------------+ +IN ---------------------------------> | Task_A_client_goal | + | +----------------------+ + | + +--> Task_Evidence_shard_planner (Python) + | + | dynamic_fanout = 30 + v + +--------------------------+ + | B1_map_doc_001 ... _030 | LLM, max_concurrency=12 + +--------------------------+ + | same-ordinal streaming edge + v + +----------------------------+ + | B2_map_events_001 ... _030 | LLM, max_concurrency=12 + +----------------------------+ + | + | all B2 parts | all B1 parts + | v + | +------------------------------+ + | | B1 quality gate | + | | all B1 parts + planner | + | | + evidence_all + meeting | + | | -> evidence_indexed.json | + | +------------------------------+ + | | + +------------------------------+ + barrier + | + v + +----------------------------------+ + | B2 quality gate | + | all B2 parts + planner | + | + evidence_indexed + B1 audit | + | -> event final + B2 audit | + +----------------------------------+ + | + v + B2_SHA256_soft_gate_handoff_writer (Python) + | + v + OUT +``` + +### 2.1 중요한 실행 특성 + +- B2 mapper는 모든 B1이 끝날 때까지 기다리지 않는다. 동일 ordinal의 B1 part가 생기면 해당 B2 mapper가 시작하는 streaming 구조다. 이 부분은 효율적이며 유지해야 한다. +- B1 gate는 `all Task_B1_map_doc_*` barrier 뒤에서 실행된다. +- B2 gate는 `all Task_B2_map_events_*`와 B1 gate 완료를 모두 기다린다. +- 따라서 두 gate는 Part 1 critical path의 직렬 말단에 있다. +- mapper 60개가 모두 LLM이고, 두 gate도 `gemini-3.1-flash-lite`, `reasoning=high`, `verbosity=medium`이다. + +## 3. 실측 fan-in과 중복량 + +### 3.1 산출물 cardinality 및 크기 + +| 입력/산출물 | 파일 수 | 실제 byte | 비고 | +|---|---:|---:|---| +| `evidence_shards/E-*.json` | 30 | 76,682 | mapper별 raw shard | +| `evidence_indexed_parts/E-*.json` | 30 | 54,289 | B1 mapper 결과 | +| `evidence_event_candidate_parts/E-*.json` | 30 | 129,294 | B2 mapper 결과 | +| `evidence_shard_plan.json` | 1 | 84,582 | 두 gate가 전부 읽음 | +| `evidence_indexed.json` | 1 | 91,841 | B2 gate 및 Part 2~4가 읽음 | +| `evidence_event_candidates.json` | 1 | 49,327 | Part 2~4가 읽음 | +| B1 gate report | 1 | 2,552 | handoff source | +| B2 gate report | 1 | 3,017 | handoff source | + +### 3.2 B1 gate 최소 입력량 + +B1 gate가 읽는 구조화 파일만 계산해도 다음과 같다. + +```text +evidence_shard_plan.json 84,582 bytes +B1 mapper parts total 54,289 bytes + ------- +known lower bound 138,871 bytes +``` + +여기에 `evidence_all.json` 전체와 `client_meeting.md` 전체, task prompt, MCP wrapper가 추가된다. B1 gate는 mapper가 이미 읽고 구조화한 raw evidence를 authority drift와 alias 판단을 위해 다시 읽는다. + +### 3.3 B2 gate 최소 입력량 + +```text +evidence_shard_plan.json 84,582 bytes +B2 mapper parts total 129,294 bytes +evidence_indexed.json 91,841 bytes +B1 gate report 2,552 bytes + ------- +known input total 308,269 bytes +``` + +여기에 B2 task prompt와 MCP wrapper가 추가된다. 이 전체를 한 LLM 호출이 읽고, schema 검사·병합·정렬·ID 확정·법률적 검토·JSON serialization까지 동시에 수행한다. + +### 3.4 구조 내부의 중복 + +1. `evidence_shard_plan.json`의 `shards`와 `dynamic_fanout`은 exact same array다. compact JSON 기준 각 27,165 bytes가 중복된다. +2. `evidence_indexed.json`의 `items`와 `evidence_indexed` alias도 exact same 30-item array다. compact JSON 기준 각 31,118 bytes가 중복된다. +3. B2 gate는 evidence reference membership 검사를 위해 `E-001`~`E-030`의 set만 있으면 되는데, B1 final의 모든 component와 위 duplicate alias까지 읽는다. +4. B2 gate는 B1 review finding을 최종 sidecar로 직접 쓰지 않으면서 B1 audit 전체를 읽는다. 실제 통합은 뒤의 deterministic handoff writer가 다시 수행한다. + +즉 “앞 작업의 개별 결과를 모두 모아 넘기는가?”에 대한 답은 **그렇다**이다. 다만 B2 gate가 모든 B1 part를 직접 받는 것은 아니고, 모든 B1 part가 합쳐진 full `evidence_indexed.json`을 중복 alias까지 포함해 받는다. 동시에 모든 B2 part 30개를 별도로 받는다. + +## 4. 실제 결과에서 확인된 품질 결함 + +### 4.1 Critical: B2 final에서 E-019 전체 소실 + +결정론적으로 비교한 결과는 다음과 같다. + +| 항목 | 기대값 | 실제값 | +|---|---:|---:| +| planner expected evidence IDs | 30 | 30 | +| B2 part files | 30 | 30 | +| B2 part candidate 합계 | 74 | 74 | +| final document items | 30 | 29 | +| final candidates | 74 | 71 | +| 누락 evidence | 없음 | `E-019` | + +`E-019` part에는 다음 3개 후보가 존재한다. + +- `lien_extinction_notice_dispatch` +- `ongoing_physical_possession` +- `lien_violation_candidate` + +30개 part 전체의 `identity_signature`를 비교해도 exact duplicate group은 0개였다. 따라서 `E-019` 제거는 B2의 명시된 cross-document dedup 규칙으로 설명되지 않는다. + +그런데 B2 audit은 다음을 동시에 기록했다. + +- `input_parts_count = 30` +- `event_candidate_completeness = PASS` +- `overall_severity = PASS` +- `downstream_progression = ALLOWED_WITH_WARNING` + +이는 final writer와 validator를 한 LLM에게 동시에 맡긴 결과로 발생한 보존성 위반이다. Part 2의 Stage A는 `evidence_event_candidates.json` 자체가 제공하는 item만 파싱하며 planner의 30개 expected ID와 final item set을 다시 대조하지 않는다. digest handoff도 “잘못 생성된 final file이 이후 바뀌지 않았음”만 보장하므로 이 누락은 정상 입력처럼 downstream에 전달된다. + +### 4.2 High: B2 review source ref가 handoff에서 소실 + +B2 audit의 `review_findings`는 `source_refs`를 다음처럼 배열로 출력했다. + +```json +"source_refs": ["E-010", "E-012", "E-011", "E-028"] +``` + +그러나 deterministic handoff writer는 다음 named object를 기대한다. + +```json +"source_refs": { + "evidence_indexes": [], + "event_candidate_ids": [], + "meeting_clause_refs": [], + "doc_titles": [] +} +``` + +그 결과 최종 `stage1_part1_soft_gate_handoff.json`의 해당 review item은 모든 source ref가 빈 배열이 되었다. B2 audit은 `review_gate_count=4`라고 적었지만 실제 `review_findings`는 1개이고, deterministic writer가 산출한 review item도 1개다. LLM이 evidence ref 4개를 finding 4개로 잘못 계수한 것으로 보인다. + +이 결함도 gate audit schema를 LLM 자유 생성에 맡기면 안 된다는 근거다. + +## 5. 두 gate의 작업별 필요성 판정 + +### 5.1 B1 gate + +| 현재 작업 | 필요한가 | 최적 실행유형 | 판단 | +|---|---|---|---| +| part completeness/stale 검사 | 필수 | Python | 파일 set과 expected ID 비교 | +| `E-{ordinal:03d}` 형식·순서 검사 | 필수 | Python | regex와 ordinal equality | +| canonical ID 확정·정렬·병합 | 필수 | Python | stable sort와 pure transform | +| final envelope/alias 생성 | 필수 | Python | serialization 작업 | +| exact duplicate 판정 | 필수 | Python | `doc_uid`, planner content hash, normalized title | +| suspected alias 판정 | 조건부 | exception LLM | hash는 다르지만 title/fact fingerprint가 유사한 pair만 | +| component coverage | 필수 | 우선 Python | semantic flag별 required field 존재 검사 | +| component 의미가 모호한 경우 | 조건부 | exception LLM | 해당 item과 짧은 source excerpt만 | +| clear authority drift | 필수 | 구조적 차단/Python | source ordinal/hash/allowlist 위반 | +| ambiguous authority drift | 조건부 | exception LLM | 의심 part와 해당 shard만 제공 | +| severity·audit count 계산 | 필수 | Python | enum priority와 array length | + +결론적으로 B1의 5개 check 중 1, 2와 final writing은 완전한 deterministic 작업이다. 3, 4, 5도 먼저 Python으로 candidate/exception을 좁힐 수 있다. 모든 30개 part, `evidence_all.json`, meeting 전체를 매 실행마다 LLM에 다시 읽힐 필요가 없다. + +authority drift를 품질 저하 없이 경량화하려면 다음 중 하나를 함께 적용한다. + +1. B1 mapper의 localdocs read를 assigned shard와 명시된 meeting slice로 tool-level allowlist하여 다른 evidence 접근을 구조적으로 금지한다. +2. B1 part에 additive `source_support_manifest`를 두어 주요 field group의 source locator·source hash·unsupported field path를 남긴다. +3. Python reducer는 ordinal/hash/locator 불일치가 있는 part만 exception으로 올린다. + +tool isolation이 확실하다면 전역 raw 재독해를 통한 authority drift LLM 검사는 상시 작업이 아니라 표본 감사 또는 예외 작업으로 낮출 수 있다. + +### 5.2 B2 gate + +| 현재 작업 | 필요한가 | 최적 실행유형 | 판단 | +|---|---|---|---| +| part schema invariant | 필수 | Python | 현재 규칙 전부 구조 검사 | +| event part completeness | 필수 | Python | expected ID set equality | +| evidence reference integrity | 필수 | Python | compact finalized ID set membership | +| candidate ID format/uniqueness | 필수 | Python | regex/set 검사 | +| exact `identity_signature` collision | 필수 | Python | group-by로 탐지 | +| final ID 확정·정렬·병합 | 필수 | Python | 보존성 invariant 포함 | +| 날짜 형식·명시적 chain ordering | 필수 | Python | 같은 group key의 parseable date 비교 | +| 관계 group이 불명확한 chronology | 조건부 | exception LLM | 해당 chain만 micro-pack으로 제공 | +| named event coverage | 필수 | 우선 Python | semantic flag/component와 subkind set의 rule table | +| 문서 의미가 불명확한 coverage | 조건부 | exception LLM | 해당 evidence 1건만 제공 | +| audit severity/count/source ref | 필수 | Python | 자유 생성 금지 | + +B2의 schema invariant, completeness, reference, candidate ID, serialization은 LLM에서 삭제해야 한다. chronological ordering과 legal-effect event coverage 자체는 삭제하면 안 되지만, **정형 규칙은 Python**, 의미론적 예외만 LLM으로 분리해야 한다. + +cross-document duplicate를 처리할 때 source item 전체를 삭제해서는 안 된다. 최소 보존 invariant는 다음과 같다. + +```text +set(final.items[*].evidence_index) == set(planner.expected_evidence_ids) +len(final.items) == planner.shard_count +모든 input part는 final item 또는 명시적 BLOCK finding에 정확히 1회 대응 +``` + +candidate를 dedup하더라도 source evidence item과 provenance는 남겨야 한다. 현재 schema가 “모든 candidate가 dedup된 item”을 표현할 수 없다면 Stage 1에서는 후보를 보존하고, semantic merge는 Stage 2로 미루는 편이 안전하다. + +## 6. Downstream 연계성과 삭제 안전성 + +| 파일/기능 | 직접 소비자 | 삭제 가능 여부 | 개정 원칙 | +|---|---|---|---| +| `evidence_indexed.json` | Part 2 Stage A, Part 3 LES0, Part 4 FL0 | 삭제 불가 | final schema 유지 | +| `evidence_event_candidates.json` | Part 2 Stage A, Part 3 LES0, Part 4 FL0 | 삭제 불가 | 1:1 evidence 보존 invariant 추가 | +| `B1_evidence_indexed_gate.json` | Part 1 handoff writer | 파일은 유지 권장 | Python이 생성한 compact audit로 대체 | +| `B2_event_candidates_gate.json` | Part 1 handoff writer | 파일은 유지 권장 | named source refs와 deterministic count 강제 | +| `stage1_part1_soft_gate_handoff.json` | Part 2 Stage A | 삭제 불가 | digest + review queue 계약 유지 | +| full `evidence_all.json` B1 gate read | B1 gate 내부 | 상시 read 삭제 가능 | exception item에 해당 shard만 읽기 | +| full `client_meeting.md` B1 gate read | B1 gate 내부 | 삭제 가능 | mapper/source manifest에서 필요한 alias cue만 전달 | +| full `evidence_indexed.json` B2 gate read | B2 gate 내부 | 삭제 가능 | `finalized_evidence_id_set` compact manifest 사용 | +| B1 audit의 B2 LLM import | B2 gate 내부 | 삭제 가능 | 최종 deterministic handoff writer가 통합 | +| LLM의 merge/serialization/count | 두 gate 내부 | 삭제해야 함 | Python single writer로 전환 | +| chronology/coverage 검증 자체 | B2 gate 내부 | 삭제 불가 | rule engine + exception adjudication | + +Part 2~4는 task name이 아니라 final file contract를 소비한다. 따라서 gate 내부 구현을 Python으로 바꾸거나 B1/B2 exception adjudication을 합쳐도, 세 final JSON과 sidecar의 schema를 유지하면 downstream DAG는 변경할 필요가 없다. + +`evidence_indexed.json`의 duplicate alias는 v.6 내부 parser들이 `items`를 우선 사용하므로 내부적으로는 제거 가능성이 높다. 다만 외부 Stage 2 또는 구버전 소비자 호환성 검증 없이 바로 제거하는 것은 권하지 않는다. 1차 개정에서는 final alias를 유지하되 **B2 gate 입력으로 full file을 사용하지 않는 것**만으로도 token 중복을 제거한다. + +## 7. 권고 개정안: 최소 변경, 최대 효과 + +### 7.1 설계 원칙 + +```text +deterministic by default + | + +-- hard mechanical failure -> BLOCK, LLM 호출 없음 + | + +-- no semantic exception -> final write, LLM 호출 없음 + | + +-- semantic exception only -> compact micro-pack -> LLM adjudication +``` + +### 7.2 기존 task를 최대한 재사용하는 개정 + +#### A. `Task_B1_quality_gate_evidence_indexed` + +실행유형을 LLM에서 `code-executor/Python`으로 변경한다. + +입력: + +- compact gate manifest +- `evidence_indexed_parts/E-*.json` +- 필요 시에만 해당 `evidence_shards/E-xxx.json` + +출력: + +- `evidence_indexed.json` +- `stage1_tmp/quality_gate/B1_precheck.json` +- `stage1_tmp/quality_gate/B1_exception_items.json` +- `stage1_tmp/quality_gate/finalized_evidence_id_manifest.json` + +Python이 수행할 일: + +1. expected/received/stale set 비교 +2. part 단일 item 및 schema 검사 +3. ordinal·proposed ID·source pointer·content hash 검사 +4. stable order로 모든 item 보존 +5. final `evidence_index` 확정 +6. exact duplicate 및 component coverage rule 검사 +7. ambiguous pair/item만 exception 생성 +8. final JSON serialization + +#### B. `Task_B2_quality_gate_event_candidates` + +실행유형을 LLM에서 `code-executor/Python`으로 변경한다. + +입력: + +- compact gate manifest +- `evidence_event_candidate_parts/E-*.json` +- `finalized_evidence_id_manifest.json` +- `B1_exception_items.json` + +제거할 입력: + +- full `evidence_indexed.json` +- full `B1_evidence_indexed_gate.json` +- duplicate-heavy full planner object + +출력: + +- `evidence_event_candidates.json` +- `stage1_tmp/quality_gate/B2_precheck.json` +- `stage1_tmp/quality_gate/B12_exception_pack.json` + +Python이 수행할 일: + +1. expected/received/stale set 비교 +2. 모든 part item의 invariant 검사 +3. candidate ID/reference/signature/date parsing 검사 +4. stable merge와 candidate ID 확정 +5. final evidence item set equality를 write 전·후 모두 검사 +6. rule-table chronology/coverage 검사 +7. B1/B2 semantic exception만 bounded micro-pack으로 생성 +8. final JSON serialization + +#### C. `Task_B12_quality_exception_adjudicator_*` 신설 + +dynamic fan-out을 사용하여 exception cluster가 0개면 LLM task를 생성하지 않는다. exception이 있으면 cluster별 작은 pack만 제공한다. + +권장 제한: + +- 한 micro-pack당 evidence item 최대 2개 +- event candidate 최대 8개 +- source excerpt 최대 1,500자 +- exception type은 하나만 포함 +- full planner, full final JSON, full meeting, full evidence 금지 +- 출력은 `exception_id`, `decision`, `severity`, `source_refs`, `concise_basis`만 허용 + +LLM 설정: + +```yaml +llm_provider: google +llm_model: gemini-3.1-flash-lite +llm_reasoning: medium +llm_verbosity: low +``` + +다음 경우에만 `reasoning=high` escalation을 허용한다. + +- 서로 다른 source가 같은 법률행위의 날짜·당사자·목적물을 실질적으로 충돌하게 표시 +- authority drift 여부가 source trace의 유효성을 좌우 +- chronology 판단 결과에 따라 hard block 여부가 달라짐 + +#### D. `Task_B2_SHA256_soft_gate_handoff_writer` 확장 + +기존 Python writer에 다음 책임을 추가한다. + +1. B1/B2 precheck와 exception decision 병합 +2. `quality_gates/B1_evidence_indexed_gate.json` 단일 작성 +3. `quality_gates/B2_event_candidates_gate.json` 단일 작성 +4. 모든 count를 실제 array length에서 계산 +5. `source_refs` named object 강제 +6. final file conservation invariant 재검증 +7. 기존 SHA256 digest와 soft-gate handoff 작성 + +이렇게 하면 gate audit과 handoff를 동일한 deterministic writer가 생성하여 현재의 count·source-ref 불일치를 차단할 수 있다. + +## 8. 개정 DAG + +```text +Task_Evidence_shard_planner + | + v + B1_map_doc_* ------------------------------+ + | same ordinal | + v | + B2_map_events_* | + | | + | all B2 parts all B1 parts + | v + | B1 deterministic reducer + | - evidence_indexed final + | - B1 precheck/exceptions + | - evidence ID manifest + | | + +----------------------------------+ + | + v + B2 deterministic reducer + - event final + - conservation gate + - B12 exception pack + | + +----------+-----------+ + | exception_count = 0 | exception_count > 0 + v v + empty decision file B12 exception LLM fan-out + | | + +----------+-----------+ + v + deterministic final audit + SHA256 handoff writer + | + v + OUT +``` + +이 개정은 동일 ordinal B1→B2 streaming을 보존한다. Part 2~4가 보는 final file name과 schema도 보존한다. + +## 9. 모델 경량화 판단 + +두 기존 gate는 이미 flash-lite 계열을 사용하므로 단순 모델 하향의 기대효과는 제한적이다. 더 중요한 사실은 high reasoning 모델이 필요 없는 작업을 아예 LLM에 주고 있다는 점이다. + +| 작업군 | 권장 실행 | +|---|---| +| file completeness, schema, regex, set equality, sort, merge, digest, count | Python only | +| exact hash/signature duplicate | Python only | +| explicit date chain with relation key | Python only | +| ambiguous alias/authority/chronology/coverage | flash-lite, medium reasoning, low verbosity | +| hard legal ambiguity 중 source trace에 중대한 영향 | 동일 모델 high reasoning으로 제한 escalation | + +즉 모델을 일괄 하향하지 말고, **정상 사례의 LLM 호출 수를 2회에서 0회로 만드는 것**이 우선이다. + +## 10. 즉시 적용할 핫픽스 + +최적화 리팩터링 전이라도 다음 항목은 즉시 보강해야 한다. + +1. B2 write 전 `expected_evidence_ids == final evidence IDs` set equality를 검사한다. +2. `len(final.items) == planner.shard_count`를 강제한다. +3. `sum(input part candidate count)`와 `sum(final candidate count) + explicit dedup count`가 일치해야 한다. +4. dedup group이 없는데 candidate 수가 줄면 `BLOCK`한다. +5. `review_gate_summary.review_gate_count`는 `len(review_findings)`로만 계산한다. +6. `source_refs`가 named object가 아니면 schema failure로 처리한다. +7. handoff writer는 review item의 source ref가 모두 비어 있는데 원 gate finding에 ref 문자열이 있으면 조용히 통과시키지 말고 `HANDOFF_SOURCE_REF_SHAPE_INVALID`를 기록한다. + +이 7개는 모두 Python으로 구현해야 한다. + +## 11. 삭제·유지 최종 판정 + +### 삭제 또는 정상 경로에서 제거 + +- B1/B2 full-context LLM merge 및 JSON writer 역할 +- B1 gate의 full `client_meeting.md` 재독해 +- B1 gate의 full `evidence_all.json` 상시 재독해 +- B2 gate의 full `evidence_indexed.json` 재독해 +- B2 gate의 B1 audit import +- LLM이 만드는 count, timestamp, ID sequence, schema envelope, severity aggregate +- exact duplicate·regex·set equality를 위한 LLM 추론 + +### 반드시 유지 + +- B1/B2 logical gate 자체 +- final evidence/event authority file +- completeness, provenance, schema, reference integrity +- chronology와 legal-effect coverage의 검증 목적 +- hard/review gate 분리 +- SHA256 handoff와 Part 2 blocking contract +- 동일 ordinal mapper streaming + +### 예외 시에만 유지 + +- suspected alias adjudication +- ambiguous authority drift +- relation key가 불명확한 chronology +- 문서 의미 해석이 필요한 event coverage + +## 12. 검증 및 성능 측정 기준 + +### 12.1 품질 acceptance gate + +```text +final evidence item count == expected shard count +final event item count == expected shard count +final evidence ID set == planner expected ID set +every candidate source_evidence_index in finalized evidence ID set +input candidate count == final candidate count + explicitly audited dedup count +every review item has schema-valid named source_refs +audit summary counts == actual array lengths +no downstream file name/schema regression +``` + +현재 7월 10일 fixture를 회귀시험으로 사용하면 최소 기대값은 다음과 같다. + +```text +evidence_indexed items: 30 +evidence_event_candidates items: 30 +input event candidates: 74 +E-019 retained: true +E-019 candidate count: 3 +exact duplicate identity_signature groups: 0 +``` + +### 12.2 성능 계측 + +다음 sub-task별 telemetry를 추가해야 실제 개선효과를 판단할 수 있다. + +- wall time +- provider/model +- input/output token +- localdocs read bytes 및 파일 수 +- exception count +- LLM call count +- cache hit 여부 +- final artifact bytes +- deterministic validation duration + +목표 KPI는 특정 초 단위 예측보다 다음 비율로 두는 것이 안전하다. + +- 정상 사례 B1/B2 quality gate LLM 호출: 2 → 0 +- exception 사례 LLM 입력: full global input이 아니라 해당 exception micro-pack만 +- B2 gate read volume: 308KB 이상의 full fan-in → B2 parts + compact manifest 중심 +- final 보존성 오류: 0 +- audit count/source ref schema 오류: 0 + +## 13. 최종 권고 + +우선순위는 다음과 같다. + +1. `E-019`와 같은 item 소실을 차단하는 conservation hotfix를 먼저 적용한다. +2. B1/B2 gate의 merge·validation·audit aggregation을 Python으로 이전한다. +3. planner와 B1 final 대신 compact gate manifest를 gate 입력으로 사용한다. +4. B1/B2의 의미론적 예외를 하나의 bounded exception pipeline으로 합친다. +5. exception이 0이면 LLM task를 spawn하지 않는다. +6. Part 1 sub-task별 runtime/token/read-byte telemetry를 추가한 뒤 v.6 fixture로 A/B 실행한다. + +두 gate를 통째로 삭제하는 것은 부적절하다. 반대로 현 상태처럼 모든 기계적 검증과 final writing을 high-reasoning LLM에 유지하는 것도 비용·속도·정확성 측면에서 정당화되지 않는다. 가장 안전하고 효율적인 개정은 **gate 책임은 유지하되 정상 경로는 deterministic하게 만들고, 법률적 모호성만 작게 잘라 LLM으로 보내는 것**이다. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_2_Improvement_Claude_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_2_Improvement_Claude_v1.md new file mode 100644 index 00000000..f34491b7 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_2_Improvement_Claude_v1.md @@ -0,0 +1,187 @@ +# Stage 1 Part 2 개선 전략 보고서 — 두 비효율 리포트 비교 분석 (Claude v1) + +- 비교 대상 문서: + - `Part_2_Inefficiency_Report_Claude_v1.md` (이하 **Claude 리포트**) + - `Part_2_Inefficiency_Report_Codex_v1.md` (이하 **Codex 리포트**) +- 분석 대상: `Stage_1_Part_2_v2.yml` (7,462행, **20개 task** = Python 14 + LLM 6) — Task_C_BO 파이프라인 +- 선별 기준: **최소의 노력**(LLM 추론은 꼭 필요한 곳에만 필요한 수준으로, 100% 결정적 작업은 Python 코드화, 토큰 경제성 최대화)으로 **최대의 효과**(최고 품질 유지, 토큰 경제성 증대, 에이전트 속도 최대화) +- 집중 비교 항목(how_to_read): ① 비효율 판정 항목 ② 실행구조·병목 지점 ③ 삭제/축소 안전성(하류 연계 근거) ④ 권장 개정 전략 + +--- + +## 0. 결론 요약 + +1. 두 리포트는 Part 2의 성격 진단에서 완전히 일치한다: **구조(결정적 컴파일 + 도메인별 국소 LLM)는 이미 올바르며, 비효율은 LLM 호출의 입력·출력·모델이 전부 최대치로 설정된 것과 전후 Python task의 과잉 분할에 있다.** 두 리포트가 독립적으로 수렴한 4대 축 — ⓐ PostB_2 예외 판정기의 입력 증폭·상시 실행, ⓑ 워커 출력의 null/스캐폴드 낭비, ⓒ pro/high 일괄 배정, ⓓ Python task 통합 여지 — 는 그대로 개정 확정 항목으로 채택한다. +2. 계측의 정밀도는 Codex 리포트가 우위다: exception pack 재계산(6건, 18KB vs full 130KB = **7.16배 증폭**), sparse 환산 실측(candidate **35% 감소** + envelope 25.3%), 도메인 라우팅 중복 배수(event 2.56×, evidence 2.80×), **task 계수 20개(정확)**. Claude 리포트의 task 계수 "17개"는 오기이며 본 보고서에서 **20개로 교정**한다(원본 grep 재확인 완료). 반면 Claude 리포트는 prefix SHA 실측(워커 5개 동일·adjudicator 상이), domain_payload의 하류 `.get()` 소비 실증(null 생략 안전성의 직접 근거), publisher strict parser ↔ pro 재호출 결합 위험(F-9) 등 Codex에 없는 근거를 보탠다. +3. 가장 중요한 전략적 진전은 Codex의 **"결정적 defer 정책"**이다. Claude 리포트의 R-1은 "빈 pack이면 즉시 종료"까지였으나, Codex §7.8은 near-duplicate(`KEEP_SEPARATE`+cluster id), meeting-only(`MEETING_ONLY_EVIDENCE_GAP` warning), PriorAct 불명(null+review code) 등을 **결정적 정책으로 처리해 pack 자체를 비운다** — 7/10 fixture의 예외 6건 전원이 이 정책으로 처리 가능하므로 **fixture 기준 PostB LLM 호출 목표 0회**가 성립한다. 이를 채택하되, 실행 골격은 Part 1 v3에서 검증한 GB 패턴(정적 task + 빈 pack 즉시 종료)으로 구현해 런타임 조건부 스폰 불확실성을 피한다. +4. 상충 지점 중 가장 중대한 것은 **모델 배치**다: B3 하향(양측 합의)과 B4/B5 pro 유지(양측 합의)는 확정하되, **B1은 Claude가 유지·Codex가 하향, B2는 Claude가 하향 후보·Codex가 유지로 정반대 판정**을 냈다. 두 독립 분석이 상반된 도메인은 불확실성이 크다는 신호이므로, 1차 개정에서는 B1·B2 모두 pro를 유지하고 fixture A/B 실측으로만 하향을 결정한다(품질 우선 원칙). +5. 통합 권고안의 기대 효과(추정): 상시 pro/high 호출 6 → 3~5(A/B 결과에 따라), PostB LLM 입력 약 130KB → 0~18KB, 워커 출력 약 절반, task 20 → 9~10, 비용 $4.28 → **$2.50 이하 목표**, 런타임 7:40 → **5분 이내 목표**. 하류 계약 4파일(BO.json + signal 3종)의 경로·스키마는 완전 보존. + +--- + +## 1. 공통 진단 — 합의 사항 (전부 채택) + +두 리포트가 독립적으로 일치한 항목. 교차 검증 완료로 간주하고 개정의 확정 전제로 삼는다. + +| # | 합의 내용 | Claude 근거 | Codex 근거 | +|---|---|---|---| +| C-1 | **하류 계약 4파일 불변** — `BO.json`(bh# 유일성·provenance·domain_payload 포함), signal 3종(schema_version·status·배열 키). Part 2 중간 산출물(seed 파일, publisher/join/ledger/compiled stdout)은 Part 3/4 직접 입력이 아니므로 자유 통합 가능 | §5 (P3:5/8/7/6, P4:3/4/5/4개소 grep) | §2 (외부 불변식 표 + 중간 파일 비소비 확인) | +| C-2 | **PostB_2 = 최대 단일 비효율.** 필요 입력은 exception_pack뿐인데 PostB_1 stdout 전문을 pro/high에 주입(프롬프트 자체가 런타임 제약을 자인), 예외 0건이어도 상시 실행 | F-1 (5091~5092행 자인 문구, 추정 200KB+) | P0-2 (**재계산 실측: pack 18,124B vs full 129,772B = 7.16×**, 예외 6건) | +| C-3 | **워커 출력의 스캐폴드 낭비** — null/[]/{} default와 반복 envelope를 pro 출력 토큰으로 재생성. sparse 출력 + Python expander로 전환 | F-2 (domain_payload leaf 54% null/empty, transport_refs·slice_guard 각 ~10KB) | P1-3 + §4.3 (**sparse 환산: candidate 94,830→61,670B = 35%↓**, envelope 25.3%) | +| C-4 | **pro/high 일괄 배정은 과잉, 그러나 일괄 하향 금지** — 도메인 차등 배치 + verbosity low. B4(사해행위)·B5(상속·통지·유치권)는 pro 유지, B3(토지·가액)는 하향 후보 | F-3, §3 판정표 | P0-1, §7.6 배치표 | +| C-5 | **B0→워커 5×5 barrier 불필요** — 각 워커는 자기 slice만 소비 → one-to-one edge | F-6 | P1-1 | +| C-6 | **publisher + domain_join + PostB_1 → 단일 결정적 reducer** (write→re-read 왕복 제거 포함) | F-5, R-4 | P1-4, §7.7 R0 | +| C-7 | **PostB_3 + PostB_4 → 단일 compile+gate+writer** | R-4 | §7.10 F0 | +| C-8 | **Middle ×3 → 단일 signal writer** (BO 1회 read로 3파일 생성, 기존 3파일·스키마 유지) | F-5, R-4 | P1-5, §7.11 S0 | +| C-9 | **Stage A context의 반복 주입** (Claude: 12개 task 실측 / Codex: 구조적 data amplification 판정) → 파일화 + compact manifest stdout | F-8 | P0-3, §7.3 | +| C-10 | **adjudicator의 COMMON_CACHE_PREFIX 불일치** — 워커 5개는 바이트 동일, adjudicator만 커스텀 → canonical prefix로 통일 | F-7 (SHA 실측 `adbdf43c…` vs `7207d2da…`) | P2-1 | +| C-11 | **이번 실행에서 adjudicator 실효 0** — seed 50 → BO 50, merge/drop 0 (LLM 판정이 아무것도 바꾸지 않음) | §1.2 | §4.5 (1:1 대응 + core 값 불변 대조) | +| C-12 | **도메인 5분할 자체는 유지** — 단일 거대 LLM 호출로의 통합은 prompt 비대화·실패 blast radius 확대로 금지 | §5 (품질 장치 유지 판정) | §10 | +| C-13 | Reason/PriorAct 품질 결함 — Reason 50건 동일 boilerplate, PriorAct 50/50 null (finalization 선언 대비 미이행) | §1.3-4 (F-9) | §4.5 | + +--- + +## 2. 각 리포트의 고유 기여 — 통합안에 반영 + +### 2.1 Codex 리포트 고유 기여 (채택) + +| # | 내용 | 반영처 | +|---|---|---| +| X-1 | **라우팅 중복 실측과 3등급 라우팅** (§4.2, §7.4): B2·B5가 event/evidence universe 전체를 재적재(중복 배수 event 2.56×·evidence 2.80×·meeting 2.05×). broad keyword 대신 event_subkind·doc_type·named component 우선, `primary/support/review_only` 3등급으로 slice 구성(primary만 full record), `ROUTING_BROADNESS_WARNING` 기록. **워커 입력 측 다이어트의 핵심** — Claude 리포트는 출력 측만 계측했다 | §4 R-2(입력), A0 계약 | +| X-2 | **결정적 defer 정책** (§7.8): exact dup은 provenance union 무손실일 때만 merge, near-dup은 `KEEP_SEPARATE`+cluster id, meeting-only는 warning 보존, Stage 2 법리 필요건은 `legal_theory_required`로 LLM 호출 금지, universe 밖 ref는 BLOCK. fixture 예외 6건 전원 결정적 처리 → **PostB LLM 0회 목표의 근거** | §4 R-1 | +| X-3 | **조건부 판정의 4중 발동 조건** (§7.9): ①writer가 단일 값 선택 필수 ②source-backed 후보 2개 이상 ③보존·defer 불가 ④compact payload로 충분 — 전부 충족 시에만 LLM. "판정 남발 방지"의 명문 기준 | §4 R-1 | +| X-4 | **품질 결함 추가 발견** (§4.5): source evidence가 빈 BO 2건(meeting-only assertion의 BO화 — 4억원 변제 관련), **13개 domain review가 BO·signal 어디에도 구조적으로 전달되지 않음**(비싼 pro 출력이 하류 미소비) | §4 R-6 (review handoff 신설) | +| X-5 | **review compact handoff 계약** (P1-6): review_id·severity·closed-enum issue_type·source refs 3종·downstream_owner만 구조 보존, 긴 서술은 LLM이 쓰지 않음 → `quality_gates/stage1_part2_review_handoff.json` 신설 | §4 R-6 | +| X-6 | **Stage A + B0 ×5 → A0 통합 compiler** (§7.3): slice를 파일로 저장하고 stdout에는 경로 manifest만. B0 코드 5중 복제(P1-2: 1,162행/79KB) 제거와 Stage A 반복 주입(C-9) 근본 해소를 겸함 | §4 R-3 (조건부 채택 — §3 D-2) | +| X-7 | Reason/PriorAct 생성 정책 (§7.10): 고정 enum/짧은 template, 명시적 prior link 존재 시에만 PriorAct/ReasonRefs 기입, Stage 1의 법적 인과 확정 금지 | §4 R-5 | +| X-8 | Stage-level 미사용 기본 모델(`openai/gpt-4o-2024-08-06`) 선언 삭제 (P2-2) | §4 R-7 | +| X-9 | **검증 불변식의 정밀화** (§9.2): BO 50건을 절대 불변식으로 두지 않되 candidate conservation·provenance union 위반 감소 금지, B4 actio·B5 notice/lien named fields 무손실, 13개 review 보존, meeting-only BO 2건 자동 삭제 금지, LES0/FL0 smoke test | §6 검증 계획 | +| X-10 | **task 계수 교정**: 정확한 현재 task 수는 20(Python 14 + LLM 6)이며 Claude 리포트의 17은 오기 (원본 재grep으로 확정) | 본 문서 전체 | + +### 2.2 Claude 리포트 고유 기여 (채택) + +| # | 내용 | 반영처 | +|---|---|---| +| Y-1 | **null 생략의 하류 안전성 실증**: Part 3 `_domain_payload()`(304~308행), Part 4 `domain_payload_compact`(312~314행)가 `.get()` 접근으로 compact 소비 → sparse 출력 계약(C-3)이 하류 무영향임을 grep으로 입증. Codex 리포트에는 이 안전성 근거가 없다 | §4 R-2의 안전 근거 | +| Y-2 | **prefix SHA 실측**: 워커 5개 `adbdf43c…` 바이트 동일(stripped 내용은 Part 1 canonical `6e01f72f…`와 동일), adjudicator `7207d2da…` 상이 — C-10의 정량 근거이자 통일 시 회귀 기준값 | §4 R-1/R-7 | +| Y-3 | **publisher strict parser ↔ pro 재호출 결합 위험** (F-9): `OUTPUT_SURFACE_HARD_RULE` 위반 1회가 max_iterations=2의 pro 전체 재호출을 유발할 수 있음 → SKILL.md §6.7 관용 파서(결정적 fence salvage)를 R0 파싱에 적용. Codex 미포착 | §4 R-4 | +| Y-4 | **BO 내부 중복의 정량 실측**: `BO_ID==id` 50/50, `EvidenceTitles==Evidence[*].source_title` 49/50, `core_field_base.amount==amount.value` 50/50 — 단 스키마 계약이므로 Stage 2 소비 확정 전 삭제 금지(Part 1 alias 판정과 동일한 조건부 처리) | §4 R-5 비고 | +| Y-5 | **Part 1 v3와의 패턴 일관성 논거**: PostB_2 개정을 Part 1 v3 GB(정적 task + pack 파일 + 빈 pack 즉시 종료 + flash-lite)와 동일 골격으로 구현하면 파이프라인 전체의 예외 처리 패턴이 단일화되어 유지보수 비용이 준다 | §4 R-1 실행 방식 | + +--- + +## 3. 상충 지점 판정 + +### D-1. 워커 모델 배치 — **합의 도메인만 확정, 상반 도메인은 pro 유지 + A/B** + +| 도메인 | Claude 판정 | Codex 판정 | 본 보고서 확정 | +|---|---|---|---| +| B1 금전채권·상호속용 | **pro 유지** (청구원인 구조의 법률 추론 실질) | **Flash 하향** (provisional seed, 최종 법리 아님) | **1차 pro 유지 + fixture A/B** | +| B2 담보·등기 | flash A/B 후보 (등기 row 추출 성격) | **pro 유지** (지분·담보·변제충당 chain의 높은 오류비용) | **1차 pro 유지 + fixture A/B** | +| B3 토지·가액 | flash A/B 후보 | Flash 하향 (medium) | **A/B 통과 시 Flash 확정** (양측 합의 유일 도메인) | +| B4 사해행위 | pro 유지 | pro 유지 | **pro 유지 확정** | +| B5 상속·통지·유치권·항변 | pro 유지 (+2분할 검토) | pro 유지 | **pro 유지 확정** | + +판정 근거: 서로 독립인 두 분석이 B1·B2에서 **정반대 결론**을 냈다는 사실 자체가 해당 도메인의 난이도 판단이 표면 신호만으로 확정될 수 없음을 보여준다. 선별 기준의 "최고 품질 유지"가 우선하므로, 상반 판정 도메인의 하향은 전부 fixture A/B 실측(§6-3)을 통과한 경우로 한정한다. 하향 대상 모델명(Codex 표기 `gemini-3.5-flash`)은 실행 환경의 가용 모델 목록 확인 후 확정하며, 미가용 시 flash-preview/flash-lite 계열로 대체한다. verbosity는 5개 전부 low로 즉시 변경(합의). + +### D-2. Stage A + B0 ×5 통합 — **Codex A0안을 조건부 채택 (채널 검증 후), 실패 시 Claude 최소안** + +- Claude: 워커 입력 채널이 stdout 템플릿(중첩 필드 접근 불가 런타임 제약)이므로 B0 5분리는 정당 → edge만 1:1 수정. +- Codex: A0 1개로 통합, slice 5개를 **파일**로 저장, stdout에는 경로 manifest만. +- **판정**: Codex안이 우월하다 — task 6→1, B0 코드 5중 복제 제거(X-6), Stage A 반복 주입 근본 해소(C-9)를 동시에 달성하며, 파일화된 slice는 워커의 `preflight_files` 정적 경로로 주입 가능해 중첩 접근 제약을 우회한다. 단 **워커 입력 채널이 템플릿→preflight 인라인으로 바뀌는 것은 실행 방식 변화**이므로, 소형 테스트(slice 파일 1개를 preflight_files로 받는 워커 1개 실행)로 채널 안정성을 확인한 후 적용한다. 검증 실패 시 fallback: Claude 최소안(B0 5개 유지 + 공통 코드 config table화 + edge 1:1)으로도 X-7·C-5는 해소된다. + +### D-3. PostB LLM의 목표 수준 — **정책은 Codex, 실행 골격은 Claude(Part 1 v3 GB)** + +- Claude: pack 파일 + flash-lite + 빈 pack 즉시 종료 (호출 자체는 스케줄됨). +- Codex: 결정적 defer 정책으로 pack을 비워 fixture 기준 **0회**, non-deferrable만 조건부 실행. +- **판정**: 융합한다. pack 생성 정책은 Codex §7.8(결정적 defer 표)을 채택해 pack이 실질적으로 비워지게 하고, 실행 골격은 Part 1 v3 GB와 동일한 **정적 task + 빈 pack 즉시 종료**로 구현한다(0-instance 조건부 스폰의 런타임 불확실성 회피 — Part 1 개정 N-2와 동일 논거). 결과적으로 fixture 기준 LLM 실질 작동 0회(빈 pack 조기 종료의 잔여 비용은 소형 flash-lite 1회분)이며, 예외가 실재하는 사건에서만 X-3의 4중 조건을 통과한 항목이 판정된다. + +### D-4. adjudicator 모델 — flash 계열 합의, **flash-lite로 통일** + +Claude flash-lite(low~medium) vs Codex gemini-3.5-flash(medium). bounded selection + closed enum이라는 작업 성격 판정은 동일하다. Part 1 v3 GB가 flash-lite로 이미 확정되어 있으므로 파이프라인 일관성을 위해 flash-lite 계열로 통일하고, pack이 비지 않는 사건에서 판정 품질 문제가 관찰되면 flash로 상향한다. + +### D-5. B5 2분할 (Claude 고유 제안) — **보류, Phase 2 이후 별도 실측** + +Claude 리포트는 최장 워커 B5(19건/62.9KB)의 succession_notice / lien_asset_defense 2분할을 제안했으나 Codex는 제안하지 않았다. sparse 출력 계약(C-3)만으로 B5 출력이 ~40KB대로 줄어들 것으로 추정되므로, 1차 개정에서는 분할하지 않고 개정 후 실측에서 B5가 여전히 wall-clock 지배 항목일 때만 분할한다(도메인 수 증가는 라우팅·프롬프트 유지보수 비용을 동반). + +--- + +## 4. 최종 권고안 (R-1 ~ R-7) + +### 권고 DAG (Codex §7.1 기반 + D-2/D-3 판정 반영) + +```text +IN + v +A0 context + domain-slice compiler [Python] ← Stage A + B0×5 통합 (D-2 조건부; fallback: B0 5개 유지+edge 1:1) + | 파일: stage_a_context / source_universe_manifest / domain_slices/B1~B5 (3등급 라우팅) + | stdout: 경로 manifest + digest만 + +--> B1 [pro*] --+ + +--> B2 [pro*] --+ * B1·B2는 A/B 후 결정, B3는 A/B 통과 시 flash, + +--> B3 [pro*] --+ B4·B5 pro 확정. 전원 verbosity low + sparse 출력 계약 + +--> B4 [pro ] --+ + +--> B5 [pro ] --+ + v +R0 seed reducer + schema expander + exception planner [Python] ← publisher+domain_join+PostB_1 통합 + | sparse→full 확장, 결정적 defer 정책(Codex §7.8), 관용 파서(Y-3) + | 파일: postb_seed_ledger / stage1_part2_exception_pack / stage1_part2_review_handoff + v +R1 exception adjudicator [flash-lite, 정적 task] ← pack 1파일만 read, 빈 pack 즉시 종료 (Part 1 v3 GB 골격) + v +F0 final BO compiler + gate + writer [Python] ← PostB_3+PostB_4 통합, Reason/PriorAct 정책(X-7) + v +S0 signal bundle writer [Python] ← Middle×3 통합, 기존 3파일·스키마 유지 + v +OUT +``` + +| # | 권고 | 출처 판정 | +|---|---|---| +| R-1 | PostB_2 전환: 결정적 defer 정책(X-2) + 4중 발동 조건(X-3) + pack 파일 입력 + flash-lite + 빈 pack 즉시 종료 + canonical prefix 통일 | C-2, D-3, D-4, Y-2, Y-5 | +| R-2 | 워커 입출력 다이어트: 입력 = 3등급 라우팅 slice(X-1, primary만 full record), 출력 = sparse seed delta 계약(C-3, transport_refs·slice_guard 제거, null/빈값 미출력 — 하류 안전성 Y-1) + R0 expander, verbosity low | C-3, C-4, X-1, Y-1 | +| R-3 | A0 통합 compiler (조건부 D-2) — slice 파일화 + stdout manifest, B0 코드 config table화. 채널 검증 실패 시 fallback: B0 5개 유지 + edge 1:1(C-5)만 | X-6, C-9, C-5 | +| R-4 | Python task 통합: R0(=publisher+join+PostB_1, 관용 파서 포함), F0(=PostB_3+4), S0(=Middle×3) → task 20 → 9~10 | C-6, C-7, C-8, Y-3 | +| R-5 | Reason/PriorAct 정책(X-7): 고정 enum/짧은 template, prior link 실재 시에만 기입. BO 내부 중복 필드(Y-4)는 Stage 2 소비 grep 확정 전 유지 | C-13, X-7, Y-4 | +| R-6 | review compact handoff 신설(X-5): 13건 review의 issue_type·source refs·downstream_owner를 `quality_gates/stage1_part2_review_handoff.json`으로 구조 보존, LLM의 긴 서술 생성 금지 | X-4, X-5 | +| R-7 | 위생 항목: 모든 LLM task의 COMMON_CACHE_PREFIX 바이트 통일(role overlay는 prefix 밖), Stage-level 미사용 모델 선언 삭제(X-8) | C-10, Y-2, X-8 | + +### 이행 순서 + +1. **Phase 1 (구조 무변경, 즉시)**: R-1 + edge 1:1(C-5) + verbosity low + R0 전신인 PostB_1에 defer 정책·pack 파일화만 선반영 + 관용 파서(Y-3) + R-7. +2. **Phase 2 (구조 통합, `Stage_1_Part_2_v3.yml`)**: R-2(sparse 계약) + R-3(A0, 채널 검증 후) + R-4(R0/F0/S0) + R-5 + R-6. +3. **Phase 3 (모델 A/B)**: B3 → flash 확정 시 반영, B1·B2는 A/B 결과로만 결정(D-1). B5 분할은 개정 후 실측에서 여전히 지배 항목일 때만(D-5). + +--- + +## 5. 삭제·유지·조건부 최종 판정 (통합) + +| 구분 | 항목 | +|---|---| +| **삭제 (정상 경로에서 제거)** | PostB_2의 full ledger 입력·상시 pro 실행 / 워커 출력의 transport_candidate_refs·slice_guard 에코·null 스캐폴드 / Stage A stdout의 12개소 원문 주입 / B0 코드 5중 복제(A0 채택 시) / 5×5 barrier / Stage-level 미사용 모델 선언 / LLM의 긴 review 서술 생성 | +| **유지 (변경 금지)** | BO.json·signal 3종의 경로·envelope·스키마(bh# 유일성, provenance, extensions.domain_payload 필드 자체) / 도메인 5분할과 워커의 법률 추론 역할 / Stage A universe 가드·PostB_4 게이트 의미론(F0로 계승) / BLOCK_REVIEW 라우팅 / meeting-only BO 2건(자동 삭제 금지, warning 보존) / BO 내부 중복 필드(Stage 2 소비 확정 전) | +| **조건부** | B1·B2·B3 모델 하향(fixture A/B 통과 시) / A0 통합(preflight 채널 검증 통과 시) / B5 2분할(개정 후에도 wall-clock 지배 시) / non-deferrable 예외의 LLM 판정(X-3 4중 조건 충족 시에만) | + +--- + +## 6. 검증 계획 (fixture = Results_July_10_3_15pm) + +1. **품질 불변식 (Codex §9.2 채택 + Claude 보강)**: source universe(evidence 30·event 71·meeting clause 41) 보존, 모든 BO source ref ⊂ Part 1 universe, 모든 candidate ref가 {최종 BO, provenance-union 병합, BLOCK/review}중 정확히 1곳으로 추적, B4 actio·B5 notice/lien/possession named fields 무손실, 13개 review의 issue_type·source refs가 review handoff에 보존, meeting-only BO 2건 보존, **candidate conservation: seed 후보 수 == BO 수 + 명시 병합 수 + 명시 BLOCK 수** (1차 구현 기준값: 50→50). +2. **sparse 왕복 검증 (Claude 방식)**: 7/10 seed 5종을 sparse로 축약 → R0 expander로 확장 → 원본과 **의미 동등**(false/0 등 유의미 값 보존, null/빈값만 복원) 확인 + 2-run 바이트 결정론. +3. **하류 무영향**: 개정 BO.json·signal 3종으로 Part 3 LES0·Part 4 FL0 파서 통과(특히 domain_payload null-생략본의 compact 소비). +4. **defer 정책 회귀**: fixture 예외 6건(schema risk 2, near-dup cluster 4)이 전부 결정적 처리되어 exception pack이 비고, R1이 즉시 종료함을 확인. 주입 테스트로 non-deferrable 예외(동일 사실에 상충하는 source-backed 값 2개)를 만들어 R1 발동 경로 확인. +5. **모델 A/B (D-1)**: 고정 slice 입력으로 B1/B2/B3의 pro vs flash 산출을 후보 수·source ref 집합·domain_review_queue 동등성 + 변호사 육안 대조로 판정. +6. **성능 합격 기준 (Codex §9.3 채택)**: task ≤10, PostB LLM 입력 ≤20KB(pack만), fixture PostB LLM 실질 0회, compact 도메인 출력 ≤70KB, 런타임 ≤5분, 비용 ≤$2.50 — 동일 조건 3회 중앙값, **품질 불변식 위반 시 성능 달성해도 실패**. + +--- + +## 7. 채택하지 않은 권고와 사유 + +| 권고 | 출처 | 미채택/보류 사유 | +|---|---|---| +| B1 즉시 Flash 하향 | Codex §7.6 | Claude와 정반대 판정(청구원인 구조의 법률 추론 실질) — A/B 실측 전 하향 보류 (D-1) | +| B2 flash A/B 우선 후보 | Claude §3 | Codex와 정반대 판정(담보·변제충당 오류비용) — 동일하게 A/B로만 결정 (D-1) | +| B0 5개 유지 (템플릿 채널 전제) | Claude §2 | Codex A0안이 파일+preflight 채널로 제약을 우회하며 3중 효과(task·중복코드·주입) — 채널 검증 조건부로 A0 우선 (D-2) | +| 빈 pack에도 상시 LLM 스케줄만으로 충분 | Claude R-1 | Codex defer 정책이 pack 자체를 비워 한 단계 더 나감 — 정책 채택, 골격만 유지 (D-3) | +| B5 즉시 2분할 | Claude R-3 | sparse 계약의 출력 감소 효과를 먼저 실측 — 지배 항목 잔존 시에만 (D-5) | +| task 수 "17→10" 기준 계수 | Claude §6.6 | **오기 교정**: 정확한 계수는 20(Python 14+LLM 6) → 9~10 (X-10) | +| BO_ID==id 등 중복 필드 즉시 정리 | (양측 관찰) | 스키마 계약 — Stage 2 소비자 확정 전 변경 금지 (Part 1 alias 판정과 동일 원칙) | diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_2_Improvement_Codex_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_2_Improvement_Codex_v1.md new file mode 100644 index 00000000..bac87538 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_2_Improvement_Codex_v1.md @@ -0,0 +1,638 @@ +# Stage 1 Part 2 통합 개정 전략 + +작성일: 2026-07-20 +개정 대상: `Stage_1_Part_2_v2.yml` +비교 대상: + +- `Part_2_Inefficiency_Report_Claude_v1.md` +- `Part_2_Inefficiency_Report_Codex_v1.md` + +실측 기준: `Results_July_10_3_15pm` + +## 0. 최종 판정 + +두 보고서의 핵심 진단은 일치한다. Part 2의 법률 도메인 분리 자체가 문제가 아니라, 그 앞뒤의 데이터 운반 방식과 모델 배치가 과도하다. + +가장 효과가 큰 개정은 다음 네 가지다. + +1. PostB LLM에 full seed ledger를 주입하지 않고, Python이 만든 compact exception pack만 동적 fan-out으로 전달한다. +2. 5개 domain worker는 유지하되 sparse seed delta만 출력하게 하고, schema default와 반복 metadata는 Python이 복원한다. +3. Stage A/B0 및 후속 Python chain을 compiler, reducer, final writer, signal writer의 네 task로 통합한다. +4. 법률 난도에 따라 모델을 차등 배치하되, 모델 하향은 fixture A/B와 법률 필드 불변식 통과 후 확정한다. + +권장 target DAG는 상시 task 9개와 compact adjudication pack 수만큼의 동적 LLM task로 구성한다. + +```text +상시 task = A0 1 + domain LLM 5 + R0 1 + F0 1 + S0 1 = 9 +동적 task = R1 adjudicator pack N개 +정상적인 no-exception 경로 = 9개 +exception pack 1개 경로 = 10개 +``` + +외부 계약인 `BO.json`, `actio_case_signals.json`, `case_liability_signals.json`, `legal_effect_signals.json`의 경로·envelope·필수 필드는 변경하지 않는다. 따라서 Part 3, Part 4 및 이후 Stage 2가 Part 2 내부 task 재편 때문에 깨져서는 안 된다. + +질문의 method 2에 적힌 “Stage 2 작업의 효율성”은 전체 goal과 산출물명에 비추어 “Stage 1 Part 2를 효율화하면서 Stage 2 downstream 효율도 훼손하지 않는 것”으로 해석한다. + +## 1. 비교의 신뢰 기준 + +### 1.1 live YAML로 확정한 사실 + +`Stage_1_Part_2_v2.yml`에는 실제로 20개 task가 있다. + +| 구간 | task 수 | 실행 유형 | +|---|---:|---| +| Stage A normalization | 1 | Python | +| Stage B0 router | 5 | Python | +| Stage B domain worker | 5 | LLM | +| publisher + domain_join | 2 | Python | +| PostB | 4 | Python 3 + LLM 1 | +| signal writer | 3 | Python | +| 합계 | 20 | Python 14 + LLM 6 | + +Claude 보고서의 “17개 task, code 11개”는 live YAML과 맞지 않는다. Codex 보고서의 “20개 task, Python 14개, LLM 6개”를 기준값으로 채택한다. + +### 1.2 수치 차이의 해석 + +두 보고서의 worker output 크기 수치는 측정 표현이 다르므로 직접 충돌하지 않는다. + +| 수치 | 의미 | 판정 | +|---|---|---| +| Claude: 206,681 bytes | 디스크에 저장된 pretty JSON 5개 파일의 합 | 실제 artifact 전송·저장 규모 파악에 유효 | +| Codex: 126,955 bytes | 같은 객체를 compact JSON으로 직렬화한 크기 | token surface와 구조별 중복 분석에 유효 | +| Codex: candidate 94,830 -> sparse 61,670 bytes | null·빈 값 제거 시 candidate 부분 감소량 | sparse output 설계 근거로 채택 | + +PostB 입력도 마찬가지다. Claude의 약 200KB 이상은 raw/pretty representation 추정이고, Codex의 재계산값은 compact full object 129,772 bytes 대 compact exception pack 18,124 bytes다. 개정 효과 산정에는 같은 직렬화 조건을 사용한 `7.16x` 증폭값을 채택한다. + +### 1.3 Part 1 기준 파일 차이 + +Claude 보고서는 `Stage_1_Part_1_Claude_v3.yml`, Codex 보고서는 `Stage_1_Part_1_Codex_v3.yml`을 참조했다. 현재 Part 2 개정에서는 최신 Part 1 기준을 `Stage_1_Part_1_Codex_v3.yml`로 둔다. + +해당 파일은 다음 실행 패턴이 실제 YAML 문법으로 구현되어 있다. + +```text +Python reducer + -> dynamic_fanout: []이면 adjudicator 0회 + -> dynamic_fanout: [pack...]이면 adjudicator_* N회 + -> deterministic finalizer가 all adjudicator_*를 대기 +``` + +따라서 Part 2의 “예외가 없으면 LLM 0회”는 단순 아이디어가 아니라 현행 Liti-agent 명세에서 재사용 가능한 패턴이다. + +## 2. 두 보고서의 공통 결론 + +### 2.1 비효율 판정 + +| 공통 판정 | 실측 또는 구조 근거 | 통합 전략 반영 | +|---|---|---| +| PostB_2 full-ledger 주입이 최대 단일 token 낭비 | compact exception만 필요하지만 seed 50건 전체 전달 | compact pack 전용 동적 adjudicator로 교체 | +| LLM 6개가 모두 Pro/high인 것은 과잉 | PostB는 bounded selection, 일부 worker는 provisional extraction/grouping | worker별 모델 차등화, PostB Flash-lite | +| worker output이 지나치게 큼 | null/empty scaffold, transport refs, slice guard 반복 | sparse delta 출력 + Python schema expander | +| B0 -> worker all-to-all wait가 불필요 | 각 worker는 자기 B0 slice만 소비 | 즉시 one-to-one edge 또는 A0 통합 | +| Python 후처리 chain이 잘게 쪼개짐 | write/re-read, full object 재직렬화, container/MCP 반복 | R0/F0/S0로 통합 | +| signal writer 3개가 동일 입력을 반복 소비 | BO와 upstream을 각 task가 다시 읽음 | 단일 S0가 기존 파일 3개 작성 | +| 5개 법률 domain을 한 거대 LLM으로 합치면 안 됨 | 도메인 법리·payload·failure boundary가 다름 | domain worker 5개 유지 | +| 외부 4개 산출물 계약은 불변이어야 함 | Part 3/4 직접 소비, Stage 2 handoff 연계 | 내부 구조만 변경, 외부 schema 회귀 검사 | + +### 2.2 병목 위치 + +```text +현재 critical path + +Stage A + -> B0 5개 전역 barrier + -> B1~B5 Pro/high 병렬 구간 + -> publisher + -> domain_join + -> PostB_1 + -> PostB_2 Pro/high + full ledger + -> PostB_3 + -> PostB_4 + -> signal writer 3개 +``` + +병목 우선순위는 다음과 같다. + +1. **P0:** PostB_2의 full-object 입력과 상시 Pro/high 호출 +2. **P0:** worker 5개의 full schema 출력 및 반복 envelope +3. **P0:** 모든 domain에 동일한 최고 모델 배치 +4. **P1:** publisher -> join -> ledger -> compiler -> writer 직렬 chain +5. **P1:** Stage A/B0 반복 parse와 5×5 대기 barrier +6. **P1:** signal writer 3개의 중복 입력·파일 read +7. **P2:** 좁은 common prefix와 adjudicator prefix 불일치 +8. **P2:** 사용되지 않는 stage-level 기본 모델과 generic Reason prose + +## 3. 차이점에 대한 채택 판정 + +### 3.1 Stage A와 B0의 통합 + +**Claude:** B0 분리는 nested stdout 접근 제약 때문에 정당하므로 유지하고 edge만 one-to-one으로 수정한다. +**Codex:** Stage A와 B0 5개를 A0 Python compiler 하나로 통합하여 artifact 5개를 쓴다. + +**판정: Codex안을 최종 구조로 채택하되 Claude안을 hotfix로 사용한다.** + +한 Python task가 domain slice 5개를 파일로 쓰고 각 worker가 자기 파일만 읽으면 nested stdout 제약을 우회할 수 있다. 현행 Part 1도 planner가 artifact와 `dynamic_fanout` descriptor를 함께 발행하는 방식을 사용하므로 실행 가능하다. + +다만 개정 초기에는 다음 순서가 안전하다. + +1. 먼저 B0 -> worker edge를 one-to-one으로 고쳐 전역 barrier를 제거한다. +2. 회귀 검사를 통과한 뒤 Stage A와 B0를 A0로 통합한다. + +### 3.2 worker 모델 배치 + +**Claude:** B1/B4/B5는 Pro 유지, B2/B3만 Flash A/B 후보. +**Codex:** B1/B3는 Flash, B2/B4/B5는 Pro. + +**판정: Codex의 target 배치를 채택하되 모든 하향은 A/B gate를 통과해야 한다.** + +| Worker | target 모델 | 이유 | release 조건 | +|---|---|---|---| +| B1 Money/Successor | Flash, reasoning high | Stage 1 provisional grouping이며 최종 청구원인 법리 확정이 아님 | candidate/source/review 불변식 통과 전에는 Pro 유지 | +| B2 Secured/Registry | Pro, reasoning high | 지분, 담보, 등기 순위, 변제충당 chain의 오류비용이 높음 | 1차 개정에서 하향 금지 | +| B3 Land/Valuation | Flash, reasoning medium | 기간·점유·가액 source grouping 비중이 큼 | A/B 동등성 통과 | +| B4 Actio | Pro, reasoning high | 피보전채권, 무자력, 담보공제, 가액배상 구조 보존 필요 | Pro 유지 | +| B5 Succession/Notice/Lien/Asset/Defense | Pro, reasoning high | 상속, 통지, 유치권, possession two-track이 결합된 고위험 domain | Pro 유지 | +| R1 exception adjudicator | Flash-lite, reasoning medium | closed enum에 의한 compact exception 판정 | compact pack 외 입력 금지 | + +Claude가 B2를 추출형으로 본 점은 일부 타당하나, 현재 B2 slice가 event/evidence universe 전체를 포함하고 registry·fraction·payment chain을 동시에 다룬다는 실측상 1차 하향은 위험하다. routing이 좁아지고 다수 fixture가 확보된 후 별도 A/B 대상으로 검토한다. + +### 3.3 B5 분할 + +**Claude:** B5를 succession/notice와 lien/asset/defense로 분할하는 옵션을 제시한다. +**Codex:** B5를 유지하고 입력 narrowing과 sparse output을 우선한다. + +**판정: 현재 개정에서는 B5 분할을 기각한다.** + +이유: + +- LLM 호출과 router surface가 각각 하나씩 늘어난다. +- succession은 notice sender·party standing과, notice는 lien extinction과 연결될 수 있어 완전히 독립적이지 않다. +- B5가 실제 wall-clock 최장 worker인지 확인할 task-level latency 로그가 없다. +- broad routing과 full schema 출력부터 제거하면 분할 없이도 입력·출력이 크게 감소한다. + +B5 분할은 sparse/narrowing 이후에도 3회 실행 중앙값에서 B5가 worker 병렬 구간의 40% 이상을 차지할 때만 재검토한다. + +### 3.4 PostB adjudicator의 존치 방식 + +**Claude:** task는 삭제하지 말고 compact pack + Flash-lite + empty pack 조기 종료로 경량화한다. +**Codex:** deterministic defer 규칙으로 대부분 처리하고 non-deferrable exception만 동적 LLM으로 보낸다. + +**판정: 두 안을 결합한다. adjudication capability는 유지하되 상시 task는 제거한다.** + +```text +R0 Python + |- auto-resolvable/deferable -> deterministic decision + |- non-deferrable -> dynamic_fanout pack + `- no pack -> R1 LLM 0회 + +R1 adjudicator_* N개 + `- supplied values 중 closed decision만 반환 +``` + +이는 “예외 판정 기능 삭제”가 아니다. 기능을 실제 예외가 있을 때만 생성되는 dynamic task로 바꾸는 것이다. + +### 3.5 sparse output 적용 범위 + +**Claude:** Part 3/4가 `.get()`으로 소비하므로 null/empty key 생략이 안전하다고 본다. +**Codex:** LLM은 sparse delta만 출력하고 Python이 canonical schema를 복원한다. + +**판정: Codex 경계를 채택한다.** + +null/empty 생략은 **worker의 내부 중간 산출물에만** 적용한다. F0가 `BO.json`을 쓰기 전 현재 canonical schema와 default를 복원한다. 이 방식은 output token을 줄이면서 Stage 2의 미확인 consumer가 key 존재에 의존할 위험을 제거한다. + +다음 값은 sparse 처리 시에도 제거하면 안 된다. + +- `false` +- `0` +- 빈 문자열 자체가 명시적 source 값인 경우 +- 명시적으로 확인된 `null`과 단순 미추출을 구별하는 schema field +- source provenance와 named legal-structure key + +### 3.6 parser 관용화 + +Claude의 `parse_llm_json` deterministic salvage 제안은 채택한다. Markdown fence나 앞뒤 잡음 제거는 LLM 재호출 사유가 아니다. + +단, 관용화는 syntax wrapper에만 적용한다. + +- 허용: code fence 제거, 단일 JSON object 추출, UTF-8 BOM 제거 +- 금지: 누락 사실 추정, enum 자동 의미변경, source ref 창작, candidate 자동 삭제 + +## 4. downstream 삭제·축소 안정성 + +### 4.1 반드시 보존할 외부 계약 + +| 파일 | 보존 항목 | 이유 | +|---|---|---| +| `BO.json` | 파일 경로, array envelope, 연속·유일 `bh#`, provenance, evidence, `extensions.domain_payload` | Part 3/4 및 이후 Stage 2의 핵심 사실·BO 입력 | +| `actio_case_signals.json` | schema version, status, `actio_case_signals[]` | 사해행위 structure routing | +| `case_liability_signals.json` | schema version, status, `case_liability_signals[]` | 책임·당사자 routing | +| `legal_effect_signals.json` | schema version, status, `bo_legal_effect_routes[]` | legal effect structure routing | + +다음 법률 payload는 회귀 검사의 별도 보호 대상이다. + +- B2: registry row, fraction, mortgage, payment allocation 관련 named 값 +- B4: preserved claim, transfer, encumbrance, valuation, cap, defense +- B5: succession, notice lifecycle, lien state, possession two-track, asset alias, defense + +### 4.2 내부에서 삭제·통합 가능한 것 + +| 현재 구조 | 개정 판정 | 안전 근거 | +|---|---|---| +| B0 task 5개 | A0로 통합 | worker별 slice artifact를 동일 의미로 생성 가능 | +| `transport_candidate_refs` LLM 출력 | 삭제 | provenance에서 Python이 무손실 재구성 가능 | +| full `slice_guard` LLM echo | 삭제 | A0 manifest/digest로 Python 검증 가능 | +| null/empty schema scaffold | LLM 출력에서 삭제 | R0/F0가 canonical default 복원 | +| publisher | R0에 통합 | 외부 consumer 없음, seed artifact write 유지 가능 | +| domain_join | R0에 통합 | 외부 consumer 없음, same-memory join 가능 | +| PostB_1 | R0에 통합 | ledger artifact와 exception pack을 R0가 생성 | +| PostB_3 + PostB_4 | F0로 통합 | 연속된 deterministic compile/gate/write | +| signal writer 3개 | S0로 통합 | BO를 한 번 읽고 기존 3개 파일 작성 가능 | +| generic `Reason` prose 생성 | fixed template/code로 축소 | 현 fixture 50건이 동일 boilerplate | +| stage-level 미사용 model | 삭제 | 모든 LLM task가 자체 provider/model 지정 | + +### 4.3 삭제하거나 약화하면 안 되는 것 + +- 5개 domain별 법률 추론 경계 +- candidate와 source provenance conservation +- source universe membership 검사 +- Stage B seed 감사 artifact 자체 +- exact duplicate의 provenance union 검사 +- non-deferrable exception의 adjudication capability +- F0의 pre-write/post-write gate와 유일 writer 원칙 +- meeting-only assertion 2건의 보존 및 evidence-gap warning +- 기존 13개 domain review의 issue type과 source refs +- Part 3 LES0 및 Part 4 FL0이 소비하는 외부 key + +BO 수 50은 fixture 기준값이지 보편적 법칙은 아니다. 최종 안전성은 단순 count보다 다음 conservation 식으로 판단한다. + +```text +모든 input candidate_ref + = final BO에 반영된 candidate_ref + + 무손실 merge의 absorbed candidate_ref + + BLOCK/blocked-review candidate_ref +``` + +## 5. 권장 개정 DAG + +```text +IN + | + v +A0 Context + Domain Slice Compiler [Python] + | writes stage context, source manifest, B1~B5 slices + | + +--> B1 Money/Successor [Flash*] + +--> B2 Secured/Registry [Pro] + +--> B3 Land/Valuation [Flash*] + +--> B4 Actio [Pro] + +--> B5 Succession/Notice/Lien/Asset/Defense [Pro] + | * switch only after A/B acceptance + | \ | / + v v v v +R0 Seed Reducer + Schema Expander + Exception Planner [Python] + | writes seed ledger, review handoff, compact packs + | + +--> dynamic R1 Adjudicator_* [Flash-lite, 0..N] + | compact pack only + | closed decisions only + | / + v v +F0 Final BO Compiler + Gate + Single Writer [Python] + | writes BO.json once, rereads and audits + v +S0 Signal Bundle Writer [Python] + | writes the existing three signal files + v +OUT +``` + +## 6. task별 개정 명세 + +### 6.1 A0 Context + Domain Slice Compiler + +입력: + +- `client_meeting.md` +- `evidence_event_candidates.json` +- `evidence_indexed.json` +- `Default_Agent/Juristic_Act.md` +- `quality_gates/stage1_part1_soft_gate_handoff.json` + +결정적 작업: + +1. 현 Stage A와 동일한 source map, digest, acceptance gate를 생성한다. +2. 현 B0의 5개 domain config를 하나의 table로 옮긴다. +3. source를 `primary`, `support`, `review_only`로 분류한다. +4. primary에는 필요한 compact record를, support에는 ref와 최소 구조값을 넣는다. +5. broad keyword 단독 match는 primary 승격 사유로 사용하지 않는다. +6. B5의 activation reason을 succession, notice, lien, possession, asset, defense별로 기록한다. +7. 각 slice의 byte 수와 global universe 대비 비율을 기록한다. + +권장 artifact: + +```text +stage1_tmp/task_c_bo/stage_a_context.json +stage1_tmp/task_c_bo/source_universe_manifest.json +stage1_tmp/task_c_bo/domain_slices/B1.json +stage1_tmp/task_c_bo/domain_slices/B2.json +stage1_tmp/task_c_bo/domain_slices/B3.json +stage1_tmp/task_c_bo/domain_slices/B4.json +stage1_tmp/task_c_bo/domain_slices/B5.json +``` + +A0 stdout은 full context가 아니라 path, digest, count만 담은 manifest로 제한한다. + +### 6.2 B1~B5 domain worker + +각 worker는 자기 slice 하나만 읽는다. 다른 B0/B worker의 완료를 기다리지 않는다. + +LLM output은 다음으로 제한한다. + +- domain id와 status +- candidate ref +- BO classification과 짧은 Action +- exact source refs +- 실제 값이 채워진 domain payload delta +- closed enum review code와 해당 source refs + +LLM이 출력하지 않을 항목: + +- global source universe 재에코 +- full `slice_guard` +- `transport_candidate_refs` +- 항상 null인 `Reason`, `PriorAct`, `ReasonRefs` +- schema를 맞추기 위한 빈 dict/list/null +- 긴 review prose +- Python이 계산 가능한 count, ordering, digest, ID + +모든 worker의 `llm_verbosity`는 `low`로 한다. common cache prefix는 바이트 동일하게 두고, domain role/schema는 prefix 뒤 overlay로 둔다. + +### 6.3 R0 Seed Reducer + Exception Planner + +R0는 publisher, domain_join, PostB_1을 흡수한다. + +작업 순서: + +1. 5개 worker output을 strict parse한다. +2. fence/BOM 같은 wrapper만 deterministic salvage한다. +3. domain id와 source membership을 검사한다. +4. sparse delta를 canonical seed schema로 확장한다. +5. seed 5개 감사 artifact를 유지한다. +6. candidate/source index와 review code index를 만든다. +7. exact duplicate, near duplicate, schema risk, link ambiguity를 계산한다. +8. full seed ledger를 artifact로 저장한다. +9. deterministic exception policy를 적용한다. +10. non-deferrable 항목만 size-bounded pack으로 만들어 `dynamic_fanout`에 넣는다. + +LLM에 보내지 않을 항목: + +| 항목 | deterministic 처리 | +|---|---| +| exact duplicate | 모든 substantive field가 같고 provenance union이 무손실일 때만 merge | +| near duplicate | `KEEP_SEPARATE` + shared cluster id + review | +| meeting-only candidate | 보존 + `MEETING_ONLY_EVIDENCE_GAP` | +| optional prior link 미확정 | null 유지 + review code | +| Stage 2 법리 확정 필요 | `legal_theory_required`로 defer | +| source universe 밖 ref | BLOCK | +| 필수 type/enum 오류 | deterministic mapper 재검사 또는 BLOCK | + +권장 artifact: + +```text +stage1_tmp/task_c_bo/postb_seed_ledger.json +quality_gates/stage1_part2_review_handoff.json +quality_gates/stage1_part2_exception_pack.json +``` + +`stage1_part2_review_handoff.json`은 기존 13개 review의 긴 prose를 보존할 필요가 없다. 다음 compact schema면 충분하다. + +```json +{ + "review_id": "B2:R001", + "severity": "SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "closed_enum", + "candidate_refs": [], + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Part3|Part4|Stage2|human_review" +} +``` + +이 파일을 Stage 2 handoff에 추가할지는 Stage 2 consumer 개정 시 별도로 결정한다. 이번 Part 2 개정에서는 감사 artifact로 보존하되 기존 4개 외부 산출물 계약을 변경하지 않는다. + +### 6.4 R1 Dynamic Exception Adjudicator + +R1은 `Task_C_BO_PostB_2_single_exception_adjudicator`의 상시 단일 task를 대체한다. + +실행 조건: + +1. F0가 진행하려면 하나의 값 또는 처리를 선택해야 한다. +2. 둘 이상의 source-backed 후보가 존재한다. +3. 둘 다 보존하거나 Stage 2로 defer할 수 없다. +4. compact record만으로 판정할 수 있다. + +허용 decision은 closed enum으로 제한한다. + +```text +KEEP_SEPARATE +MERGE_PROVENANCE_UNION +SPLIT +DROP_UNSUPPORTED +BLOCK_REVIEW +``` + +R1은 raw evidence, full Stage A context, full seed ledger를 읽지 않는다. compact pack이 부족하면 추론으로 채우지 않고 `BLOCK_REVIEW`를 반환한다. + +pack 제한 예시: + +- candidate 8건 이하 +- source excerpt 총 1,500자 이하 +- serialized pack 20KB 이하 +- pack id와 output path 고정 + +### 6.5 F0 Final BO Compiler + Gate + Writer + +F0는 PostB_3과 PostB_4를 통합한다. + +1. seed ledger digest를 확인한다. +2. `dynamic_fanout` pack과 adjudication decision의 coverage를 검사한다. +3. candidate conservation을 검사한다. +4. deterministic ordering과 `bh#`를 부여한다. +5. sparse domain payload를 현재 BO canonical schema로 확장한다. +6. Evidence, EvidenceTitles, provenance, source index를 결정적으로 구성한다. +7. 명시적 prior link가 없으면 `PriorAct`와 `ReasonRefs`를 임의 생성하지 않는다. +8. `Reason`은 fixed code/template로만 구성한다. +9. pre-write schema/reference gate를 통과한 경우에만 `BO.json`을 한 번 쓴다. +10. 다시 읽어 JSON parse, count, ID, SHA-256, source membership을 검증한다. + +F0만 `BO.json` writer 권한을 가진다. + +### 6.6 S0 Signal Bundle Writer + +S0는 BO를 한 번 읽고 다음 파일 세 개를 메모리에서 projection하여 각각 쓴다. + +- `actio_case_signals.json` +- `case_liability_signals.json` +- `legal_effect_signals.json` + +기존 파일별 schema version, status 규칙, array key를 그대로 유지한다. 각 파일 write 후 다시 읽어 count와 source BO id subset을 검사한다. + +## 7. LLM과 Python의 경계 + +| 작업 | 실행 주체 | 이유 | +|---|---|---| +| input parse, digest, source membership | Python | 100% 결정적 | +| routing rule 적용과 slice 생성 | Python | config 기반 결정적 | +| 법률 domain별 provisional grouping | LLM | 문맥적 법률 추론 필요 | +| null/default/schema expansion | Python | 100% 결정적 | +| ordering, ID, count, serialization | Python | 100% 결정적 | +| exact duplicate 검사 | Python | 100% 결정적 | +| near duplicate 보존·review 부여 | Python | Stage 1 보수 정책으로 결정 가능 | +| meeting-only evidence gap | Python | source type으로 결정 가능 | +| non-deferrable 충돌 판정 | compact LLM | 실제 의미 선택이 필요한 예외만 | +| final BO gate/write/reread | Python | fail-closed 결정 작업 | +| signal projection | Python | 기존 BO의 결정적 projection | + +## 8. 최소 작업으로 이행하는 순서 + +### Phase 1. 고효율 hotfix + +기존 task 구조를 크게 바꾸기 전에 다음을 적용한다. + +1. B0 -> worker의 `wait_until`을 자기 B0 하나로 줄인다. +2. worker `llm_verbosity`를 모두 low로 바꾼다. +3. 모든 LLM의 common cache prefix를 바이트 동일하게 만든다. +4. task별 role/schema는 common prefix 밖으로 이동한다. +5. PostB_1이 compact pack과 `dynamic_fanout`을 생성하게 한다. +6. PostB_2를 Flash-lite `adjudicator_*` map task로 교체한다. +7. parser에 wrapper-only deterministic salvage를 적용한다. + +이 단계만으로 가장 큰 token 낭비와 불필요한 barrier를 제거할 수 있다. + +### Phase 2. structural consolidation + +1. Stage A와 B0 5개를 A0로 통합한다. +2. worker output을 sparse delta로 변경한다. +3. publisher, domain_join, PostB_1을 R0로 통합한다. +4. PostB_3과 PostB_4를 F0로 통합한다. +5. signal writer 3개를 S0로 통합한다. +6. 미사용 stage-level model 선언을 삭제한다. + +완료 시 20개 task는 상시 9개와 동적 R1 N개가 된다. + +### Phase 3. model A/B + +모델 교체는 구조 개정 후 고정된 slice와 output contract를 기준으로 시행한다. + +1. B3를 Flash/medium으로 A/B한다. +2. B1을 Flash/high로 A/B한다. +3. B2는 Pro를 유지한다. +4. B4/B5는 Pro를 유지한다. +5. R1은 Flash-lite/medium으로 고정한다. + +B1/B3 A/B는 다음을 모두 만족해야 통과한다. + +- candidate ref 집합 동등 +- source ref 집합 동등 +- protected named field의 값 동등 +- review issue type과 source refs 동등 +- unsupported 값의 hallucination 0 +- lawyer spot review 통과 + +## 9. 기각하거나 유보할 제안 + +| 제안 | 판정 | 이유 | +|---|---|---| +| domain LLM 5개를 하나로 통합 | 기각 | prompt와 failure blast radius 증가, 법률 경계 훼손 | +| B5 즉시 2분할 | 유보 | 호출 수 증가, cross-domain 결속 가능성, latency 근거 부족 | +| B2 즉시 Flash 하향 | 기각 | registry/fraction/payment chain의 높은 오류비용 | +| PostB adjudication capability 완전 삭제 | 기각 | 실제 non-deferrable 충돌에 필요 | +| empty exception에도 LLM 호출 | 기각 | Part 1 dynamic fan-out 패턴으로 0회 구현 가능 | +| final BO의 null/empty key 즉시 삭제 | 기각 | Stage 2 consumer 범위 미확정, 외부 contract 위험 | +| BO alias/중복 field 즉시 제거 | 유보 | Stage 2 전체 소비자 확인 전 삭제 위험 | +| review prose 전량 유지 | 기각 | 비용 대비 downstream 소비 없음, compact code로 충분 | +| source 공유를 막는 배타 routing | 기각 | 동일 사실이 복수 법률 domain에 필요할 수 있음 | + +## 10. 검증 계획 + +### 10.1 정적 검증 + +- YAML parser 통과 +- task name 중복 0 +- unreachable task 0 +- old task reference 0 +- `dynamic_fanout=[]`에서 R1 인스턴스 0개 +- embedded Python 전부 syntax compile +- Python code가 `YAML_Prompts/1. Stage_1/SKILL.md` 제약 준수 +- 모든 LLM common cache prefix hash 동일 +- 모델별 role overlay가 prefix 밖에 위치 +- `BO.json` writer 정확히 1개 +- signal 파일별 writer 정확히 1개 + +### 10.2 7/10 fixture 품질 불변식 + +1. evidence index 30개와 event candidate 71개의 source universe를 보존한다. +2. 모든 final BO source ref는 Part 1 universe의 subset이어야 한다. +3. input candidate conservation 식을 만족한다. +4. 1차 회귀 기준으로 seed 50건과 BO 50건을 보존한다. +5. 기존 review 13건의 issue type과 source refs를 compact handoff에 보존한다. +6. meeting-only BO 2건을 삭제하지 않는다. +7. B4 protected actio fields를 보존한다. +8. B5 notice/lien/possession/asset named fields를 보존한다. +9. `BO.json` 및 signal 3종의 기존 envelope와 필수 key를 보존한다. +10. Part 3 LES0과 Part 4 FL0 smoke test를 통과한다. + +### 10.3 예외 주입 검사 + +fixture 복제본에 다음을 주입한다. + +- exact duplicate 1쌍 +- near duplicate 1쌍 +- source universe 밖 ref 1건 +- conflicting amount/date 1건 +- empty exception case +- oversized exception pack case + +기대 결과: + +| 주입 | 기대 처리 | +|---|---| +| exact duplicate | provenance-union 조건 통과 시 deterministic merge | +| near duplicate | KEEP_SEPARATE + review, R1 호출 없음 | +| outside-universe ref | BLOCK | +| non-deferrable conflict | compact R1 pack 생성 | +| empty exception | R1 인스턴스 0개 | +| oversized pack | split 또는 BLOCK_REVIEW, full-ledger fallback 금지 | + +### 10.4 성능 합격 기준 + +동일 provider 가격·부하 조건에서 3회 이상 실행한 중앙값으로 판정한다. + +| 지표 | 현재 | 1차 합격 | target | +|---|---:|---:|---:| +| 상시 task | 20 | 10 이하 | 9 | +| 상시 Pro/high LLM | 6 | 4 이하 | 3 | +| no-exception PostB LLM | 1 | 0 | 0 | +| PostB LLM input | full ledger | 20KB 이하 pack | 필요한 pack만 | +| worker candidate compact output | 94,830 bytes | 75,000 bytes 이하 | 약 62,000 bytes | +| runtime | 7분 40초 | 5분 30초 이하 | 5분 이하 | +| cost | $4.28 | $3.00 이하 | $2.50 이하 | + +품질 불변식 하나라도 위반하면 성능 수치를 만족해도 개정 실패로 판정한다. + +## 11. 최종 개정 명령 + +Part 2 v3 작성자는 다음 우선순위를 지켜야 한다. + +1. 외부 BO/signal 계약과 법률 named field를 먼저 고정한다. +2. PostB full-ledger LLM 입력을 가장 먼저 제거한다. +3. 결정적 작업을 Python으로 이동한다. +4. worker의 법률 추론 경계는 유지한다. +5. sparse 표현은 내부 worker output에만 사용한다. +6. 비결정적이며 finalization에 필수인 compact exception만 LLM에 보낸다. +7. 모델 하향은 B3, B1 순서로 A/B gate를 통과한 뒤 적용한다. +8. B5 분할과 외부 schema 축소는 이번 개정 범위에서 제외한다. + +한 문장으로 요약하면 다음과 같다. + +> **Part 2의 5개 법률 domain worker는 유지하되, 입력은 narrow slice, 출력은 sparse delta로 줄이고, 전후 처리는 네 개 Python compiler/reducer/writer로 통합하며, PostB LLM은 동적으로 생성되는 non-deferrable compact exception에만 허용한다.** diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_2_Improvement_Strategy_Claude_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_2_Improvement_Strategy_Claude_v1.md new file mode 100644 index 00000000..e6583b2e --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_2_Improvement_Strategy_Claude_v1.md @@ -0,0 +1,304 @@ +# Stage 1 Part 2 개정 작업 명세서 (Part_2_Improvement_Strategy_Claude_v1) + +- **개정 대상**: `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_2_v2.yml` (7,462행, 20 task = Python 14 + LLM 6) +- **개정 산출물**: `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_2_Claude_v3.yml` (신규 파일 — 원본 v2는 절대 덮어쓰지 않는다) +- **전략 근거**: `YAML_Prompts/1. Stage_1/v.6/Part_2_Improvement_Claude_v1.md` (이하 **개선전략서**; R-1~R-7, D-1~D-5 판정 포함) +- **본 문서의 성격**: 개정 실행자(LLM)가 본 문서만 읽고 v3 YAML을 100% 작성할 수 있도록 anchor·신규 task 명세·데이터 계약·검증 기준을 기술한 작업 명세서다. 본 문서와 v2 원본이 충돌하면 본 문서가 우선한다. 본 문서가 침묵하는 사항은 v2 원본의 의미론을 보존한다. Part 1 개정에서 검증된 규율(SKILL.md 준수, fixture 회귀, 불변 계약 diff-0)을 동일하게 적용한다. + +--- + +## 0. 개정 목표와 확정 결정 + +### 0.1 무엇을 바꾸는가 (개선전략서 R-1~R-7 요약) + +Part 2의 구조(결정적 컴파일 + 도메인별 국소 LLM)는 유지하고, **LLM 호출의 입력·출력·모델과 전후 Python task 개수를 다이어트**한다: ① PostB_2를 결정적 defer 정책 + compact pack + flash-lite + 빈 pack 즉시 종료(Part 1 v3 GB 골격)로 전환, ② 워커 5개의 입력을 3등급 라우팅 slice로·출력을 sparse seed delta로 축소, ③ Python 14개를 4개(A0/R0/F0/S0)로 통합, ④ 도메인 review 13건의 compact handoff 신설, ⑤ 프리픽스 통일 및 위생 항목. 하류 계약 4파일(BO.json + signal 3종)은 완전 보존한다. + +### 0.2 변호사 관점의 불변 원칙 + +1. **provenance 보존이 최우선**: 모든 seed candidate는 최종 BO 1건, provenance-union 병합, 또는 명시적 BLOCK/review 중 정확히 1곳으로 추적되어야 한다. near-duplicate는 Stage 1에서 병합하지 않고 `KEEP_SEPARATE` + cluster id로 보존한다(증거 소실 방지 — Part 1 개정에서 확립한 원칙과 동일). +2. **meeting-only BO 2건(무증거 변제 주장 등)은 자동 삭제 금지** — `MEETING_ONLY_EVIDENCE_GAP` warning과 함께 보존하여 Stage 2 검토에 회부한다. +3. **법리 확정의 Stage 경계 준수**: `legal_theory_required` 항목은 LLM 판정 대상이 아니라 Stage 2 회부 대상이다. Reason/PriorAct는 Stage 1이 임의 확정하지 않으며 고정 template/enum + 실재 prior link 시에만 기입한다. +4. **B4(사해행위)·B5(상속·통지·유치권) named fields 무손실**: preserved claim, transfer, encumbrance, valuation, cap, defense / notice lifecycle, lien state, possession two-track, asset alias 정보는 sparse화 과정에서도 값이 있으면 반드시 보존된다. + +### 0.3 본 명세서의 확정 결정 (개선전략서의 조건부 항목 구체화) + +| # | 결정 | 내용과 근거 | +|---|---|---| +| N-1 | **task 명명** | 워커 5개는 기존 이름 유지(`Task_C_BO_Stage_B_B1_Money_Successor` 등 — 프롬프트만 개정). 신설: `Task_C_BO_A0_context_and_domain_slice_compiler`(code), `Task_C_BO_R0_seed_reducer_and_exception_planner`(code), `Task_C_BO_R1_exception_adjudicator`(LLM), `Task_C_BO_F0_final_bo_compiler_gate_writer`(code), `Task_C_BO_S0_signal_bundle_writer`(code). Part 3/4는 Part 2 task 이름을 참조하지 않으므로(파일 계약만 소비) 신설 명명은 하류 무영향 | +| N-2 | **워커 입력 채널 (D-2)** | **Plan A(기본)**: A0가 slice 5개를 파일로 저장, 각 워커는 `preflight_files`에 자기 slice 파일 1개만 whitelist(`preflight: true`)하여 인라인 수신. **STEP 0-2의 채널 검증 통과가 전제**다. **Plan B(fallback)**: 검증 실패 시 Stage A + B0 ×5 구조 유지(B0 코드는 config table 공통화, edge만 1:1) + `{{prev.B0}}` 템플릿 채널 유지. 두 Plan 모두 아래 STEP들에 병기한다 | +| N-3 | **모델 동결 (D-1)** | v3 파일에서 워커 5개는 `gemini-3.1-pro-preview`/`reasoning: high` **유지**, `llm_verbosity: medium → low`만 변경. B1/B2/B3 하향은 v3 배포 후 §9-5의 A/B 절차로만 결정한다(두 리포트의 상반 판정 도메인은 실측 없이 하향 금지). R1은 `gemini-3.1-flash-lite`/`reasoning: low`/`verbosity: low` | +| N-4 | **R1 실행 골격** | Part 1 v3 GB와 동일: **정적 task + pack 파일 1개만 read + 빈 pack 즉시 종료**. 조건부 스폰(dynamic fan-out) 사용 금지(0-instance barrier의 런타임 불확실성 — Part 1 N-2와 동일 논거) | +| N-5 | **Stage-level 미사용 모델 선언 (R-7/X-8)** | v2 9~10행의 `llm_provider: openai`/`llm_model: gpt-4o-2024-08-06`은 **삭제를 시도**하되, STEP 0-4의 등록 smoke test 실패 시 유지한다(백엔드 스키마의 필수 여부 미확인 + Part 1 v3도 보유 중 — 실패해도 실행 비용 영향 없음) | +| N-6 | **bh# 안정성** | F0는 v2의 정렬·부여 규칙을 그대로 계승한다: PostB_1의 `deterministic_sort_key`(BehaviorTime null 여부→BehaviorTime→domain_order→BOType→ActionType→JuristicActLabel→Action→candidate_ref) 순 정렬 후 `bh{idx}` 연속 부여(v2 5443·5466행과 동일). 동일 입력에서 동일 bh# 산출을 보장한다 | + +--- + +## 1. 절대 불변 계약 (위반 금지) + +1. **최종 산출물 4파일의 경로·스키마**: `BO.json`(JSON array, 연속·유일 `bh#`, item 키: BO_ID·id·BOType·ActionType·JuristicAct·Action·Reason·PriorAct·ReasonRefs·Legal_Keywords·core_field_base·amount·EvidenceTitles·Evidence·source_evidence_indexes·provenance·downstream_seed_refs·extensions), `actio_case_signals.json`(`schema_version`·`status`·`actio_case_signals[]`), `case_liability_signals.json`(동형, `case_liability_signals[]`), `legal_effect_signals.json`(동형, `bo_legal_effect_routes[]`). +2. **BO item의 내부 중복 필드 유지**: `BO_ID==id`, `EvidenceTitles`, `core_field_base.amount` 등은 v.2 스키마 계약이므로 Stage 2 소비 확정 전 삭제 금지(개선전략서 Y-4). +3. **`extensions.domain_payload` 필드 자체 유지** — 단 null/[]/{} 값 키의 생략은 허용(Part 3 304~308행, Part 4 312~314행의 `.get()` compact 소비 실증). +4. **Part 1 v3 산출물 소비 계약 유지**: A0의 입력은 `client_meeting.md`, `evidence_event_candidates.json`, `evidence_indexed.json`, `Default_Agent/Juristic_Act.md`, `quality_gates/stage1_part1_soft_gate_handoff.json` — v2 Stage A와 동일(`evidence_all.json` 등 forbidden 목록도 계승). +5. **Part 1/3/4 YAML 수정 금지.** +6. **워커 5개의 도메인 분할·AUTHORITY_BOUNDARY·DOMAIN_SCOPE·법률 규칙 본문 유지**: 프롬프트 개정은 입력 채널(§6.1)·출력 계약(§6.2)·verbosity에 한정하며, 각 도메인의 법률 지시(예: B1의 이율 literal 보존 규칙, B4의 actio 구조, B5의 notice lifecycle)는 자구 그대로 보존한다. +7. **COMMON_CACHE_PREFIX_STAGE_1**: 워커 5개의 현행 블록(바이트 동일, SHA `adbdf43c…`)을 canonical로 삼아 **v3의 LLM task 6개 전부에 바이트 동일하게** 배치한다(R1 포함). role/도메인별 내용은 prefix 밖 블록에 둔다. +8. **상태 문자열·enum**: `READY`/`READY_WITH_REVIEW`/`FAILED`, `KEEP_SEPARATE`/`MERGE`/`SPLIT`/`DROP`/`BLOCK_REVIEW`, severity enum 등 v2의 값 문자열을 유지한다. + +--- + +## 2. 현행 v2 anchor 지도 (편집 좌표) + +anchor는 ` - task_name:` 라인이며, 범위는 해당 라인부터 다음 anchor 직전까지다. + +| # | task (v2 행 범위) | v3 처분 | +|---|---|---| +| 1 | `Task_C_BO_Stage_A_input_normalization` (24~1015) | **A0로 흡수** (Plan B: 파일 출력형으로 개정 유지) | +| 2~6 | `Stage_B0_input_*` ×5 (1016~1343 / 1344~1564 / 1565~1779 / 1780~2004 / 2005~2227) | **A0로 흡수** (Plan B: config table 공통화하여 유지) | +| 7~11 | `Stage_B_B1~B5` (2228~2494 / 2495~2767 / 2768~3043 / 3044~3314 / 3315~3622) | **유지 + 프롬프트 개정** (§6) | +| 12 | `Stage_B_worker_output_publisher` (3623~3962) | **R0로 흡수** | +| 13 | `Stage_B_domain_join` (3963~4585) | **R0로 흡수** | +| 14 | `PostB_1_seed_ledger_compiler` (4586~5045) | **R0로 흡수** | +| 15 | `PostB_2_single_exception_adjudicator` (5046~5140) | **R1로 대체** (§8) | +| 16 | `PostB_3_final_bo_compiler` (5141~5556) | **F0로 흡수** | +| 17 | `PostB_4_final_gate_and_writer` (5557~5904) | **F0로 흡수** | +| 18~20 | `Middle_Input_*` ×3 (5905~6525 / 6526~6958 / 6959~7283) | **S0로 흡수** | +| — | `task_procedure` (7284~7462) | **전면 교체** (§10) | + +Plan A의 v3 task 구성: A0 → B1~B5 → R0 → R1 → F0 → S0 = **10개**. Plan B: Stage A' + B0 ×5 + B1~B5 + R0 + R1 + F0 + S0 = 14개. + +**코드 이식 원칙**: 흡수되는 Python task의 로직은 폐기가 아니라 **함수 단위 이식**이다. Stage A의 map 빌더들(meeting_clause_map·event_candidate_map·evidence_authority_map·taxonomy·hazard·`_compact_part1_soft_gate_handoff`), B0의 `DOMAIN_CONFIG`·필드 사영 목록(MEETING_FIELDS/EVENT_FIELDS/EVIDENCE_FIELDS), publisher의 가드 검증, domain_join의 사영·인덱스, PostB_1의 sort key·exception 규칙, PostB_3의 컴파일·Reason 규칙, PostB_4의 게이트·검증, Middle 3종의 signal 빌더를 각각 A0/R0/F0/S0로 옮기고, 중복 보일러플레이트만 1벌로 줄인다. + +--- + +## 3. STEP 0 — 사전 검증 (v3 작성 전 필수) + +1. **STEP 0-1**: v2에서 §2의 20개 anchor + `task_procedure`(7284행) 존재 확인. 불일치 시 중단·보고. +2. **STEP 0-2 (Plan A/B 결정 게이트)**: 소형 채널 테스트 — slice 형식의 JSON 파일 1개를 localdocs에 쓰고, `preflight: true` + `preflight_files: [그 파일]`인 미니 LLM task(flash-lite)가 파일 내용을 프롬프트에서 수신·인용할 수 있는지 1회 실행으로 확인한다. **통과 → Plan A, 실패 → Plan B.** 결과를 개정 보고서에 기록한다. +3. **STEP 0-3**: fixture(`Results_July_10_3_15pm`)에 Part 1 산출물 5종(A0 입력)과 도메인 seed 5종·BO.json·signal 3종이 존재함을 확인한다(§11 회귀에 사용). +4. **STEP 0-4**: stage-level 모델 선언(9~10행) 삭제본으로 YAML parse + 백엔드 등록 smoke test(`upload-agent`) 시도. 실패 시 선언 유지(N-5). +5. **STEP 0-5**: R1에 사용할 canonical prefix를 v2 2242~2284행(B1 워커의 COMMON_CACHE_PREFIX_STAGE_1 블록)에서 바이트 추출해 둔다. + +--- + +## 4. 신규 데이터 계약 (파일 7종) + +| 파일 | writer | 소비자 | 핵심 스키마 | +|---|---|---|---| +| `stage1_tmp/task_c_bo/stage_a_context.json` | A0 | R0, F0, S0 (파일 read) | v2 `stage_a_context` 객체와 동일 구조(schema_version `task_c_bo_stage_a_context.v1` 유지) | +| `stage1_tmp/task_c_bo/source_universe_manifest.json` | A0 | R0, F0, S0 | `{schema_version, input_digests_sha256, evidence_index_set[], event_candidate_ids[], meeting_clause_ids[], counts}` | +| `stage1_tmp/task_c_bo/domain_slices/B{1..5}.json` (Plan A) | A0 | 워커 (preflight) | v2 B0 slice 구조(`stage_b_domain_slice`, schema_version `task_c_bo_stage_b_domain_slice.v1`, READ_ONLY 필드 목록 동일) + **3등급 라우팅 확장**: `selected_*`는 `primary`만 full record, `support_refs[]`·`review_only_refs[]`는 id+1줄 요약, `routing_audit`에 `primary_ids/support_ids/review_only_ids/activation_reasons/source_count/slice_bytes/broadness_ratio` | +| `stage1_tmp/task_c_bo/postb_seed_ledger.json` | R0 | F0, S0 | v2 `postb_seed_ledger` stdout 객체와 동일 구조(ledger_candidates는 **expander로 full schema 확장 후** 수록, deterministic_sort_key 포함) | +| `quality_gates/stage1_part2_exception_pack.json` | R0 | R1 | `{schema_version: "stage1_part2_exception_pack.v1", has_exceptions, exception_count, exceptions[], budget}` — exception 항목: `{exception_id, exception_type: "canonical|field_conflict|link_ambiguity|semantic_risk", candidate_refs[], compact_payload(판정에 필요한 필드만), allowed_decisions[], escalation_flag}`. **X-3의 4중 조건을 통과한 항목만 수록** | +| `stage1_tmp/task_c_bo/postb_adjudication_decisions.json` | R1 | F0 | `{schema_version: "task_c_bo_postb_exception_adjudication.v1", status: "READY|READY_NO_EXCEPTIONS", exception_count, canonical_decisions[], field_decisions[], link_decisions[], semantic_gate_decisions[], blocked_review_items[]}` — v2 PostB_2 출력 스키마 유지(F0의 `_adjudication` 소비 로직 이식 호환) | +| `quality_gates/stage1_part2_review_handoff.json` | R0 (F0가 최종 status 갱신) | Stage 2 / 인간 검토 | `{schema_version: "stage1_part2_review_handoff.v1", review_items[]}` — 항목: `{review_id, source_domain, severity: "SOFT_WARNING|HARD_WARNING", issue_type(closed enum), source_event_candidate_ids[], source_evidence_indexes[], source_meeting_clause_ids[], downstream_owner: "C0|C1|C2|C3|C5|D|E|Stage2", template_note(고정 템플릿 1줄)}` — 워커의 domain_review_queue 13건 + defer 정책이 생성한 review code를 전량 수록. **자유 서술 금지** | + +stdout 규약(SKILL.md §3.3): A0/R0/F0/S0 모두 **단일 JSON 라인**만 출력하며 full context/ledger를 stdout에 넣지 않는다(경로 manifest + digest + count + `message` 필드). + +--- + +## 5. STEP 1 — A0 명세 (`Task_C_BO_A0_context_and_domain_slice_compiler`, Plan A) + +code-executor 엔트리(v2 Stage A 형식: language python, requirements httpx, network agent-network, timeout 240). MCP 보일러플레이트는 v2 Stage A(43~145행)를 이식하고 `clientInfo.name`만 `"task-c-bo-a0-context-and-domain-slice-compiler"`로 변경. SKILL.md 필수사항(user/workspace hash, raw_decode fallback) 유지. + +1. v2 Stage A의 정규화 로직 전체 이식(입력 5종 read → meeting/event/evidence map·taxonomy·hazard·digest·part1 handoff compact — 24~1015행의 함수군 그대로). +2. `stage_a_context.json`·`source_universe_manifest.json` 파일 write (stdout 발행 대신 — C-9 해소). +3. v2 B0 ×5의 `DOMAIN_CONFIG`(도메인별 terms/sources/hazard_keys/taxonomy_axes — 1016~2227행)를 **config table 1개**로 통합하고 공통 라우팅 함수 1벌로 5개 slice를 생성한다(X-7 해소). +4. **3등급 라우팅(X-1)**: (a) `event_subkind`·`doc_type`·`legal_effect_candidate`·named component·registry/fraction/notice/lien key 매칭 → `primary`, (b) broad term 단독 매칭 또는 source-ref closure 유입 → `support`(id+title+1줄), (c) 잔여 참조 → `review_only`(id만). broad term(`등기`·`변제`·`항변`·`점유`·`목적물` 등)은 단독으로 primary 승격 불가. slice가 universe의 60%를 넘으면 `ROUTING_BROADNESS_WARNING`을 routing_audit에 기록(자동 실패 금지). +5. `domain_slices/B{1..5}.json` write. 각 slice에 v2 B0 출력과 동일한 가드 필드(`router_status`, `domain_id`, `source_stage_a_*`, `stage_b_worker_contract`)를 유지한다(워커의 ABSOLUTE_SYSTEM_RULES 검증 호환). +6. stdout: `{"status":"READY","message":"...","stage_a_path":...,"source_manifest_path":...,"domain_slice_paths":{...},"input_digests_sha256":{...},"routing_summary":{도메인별 primary/support count}}` 1줄. +7. FAILURE_POLICY: 입력 누락·파싱 불가 시 파일 미작성, FAILED JSON 1줄 + raise (v2 Stage A와 동일). + +**Plan B(STEP 0-2 실패 시)**: Stage A는 위 1·2·6·7만 적용(파일 출력형으로 개정), B0 ×5는 유지하되 5중 복제 코드를 공통 함수+config로 재작성하고 3등급 라우팅(4)을 적용하며, stdout은 자기 slice 1개만 발행한다. edge는 §10 Plan B대로 1:1. + +--- + +## 6. STEP 2 — 워커 5종 프롬프트 개정 (B1~B5 공통) + +각 워커의 task 엔트리·법률 본문(AUTHORITY_BOUNDARY/DOMAIN_SCOPE/WORKFLOW의 도메인 규칙)은 유지하고 다음만 변경한다. + +### 6.1 입력 채널 + +- **Plan A**: `preflight: false` → **`preflight: true`** + `preflight_files: [stage1_tmp/task_c_bo/domain_slices/B{n}.json]`(자기 slice 1개만). 프롬프트의 `{{prev.…}}` 블록을 "preflight로 제공된 slice 파일의 `stage_b_domain_slice` 객체만 사용하라"는 지시로 교체. ABSOLUTE_SYSTEM_RULES의 검증 항목(schema_version·router_status·domain_id·worker_contract)은 유지. +- **Plan B**: 현행 `{{prev.Task_C_BO_Stage_B0_input_*}}` 채널 유지. +- 3등급 slice 소비 규칙 추가: "`support_refs`/`review_only_refs`는 존재 확인·ref 인용에만 사용하고 사실 인용의 근거는 primary record로 한정한다." + +### 6.2 sparse seed delta 출력 계약 (OUTPUT_CONTRACT 교체) + +**emit 규칙** (신설, 5개 워커 공통 문구): +- 값이 null/[]/{}/빈 문자열인 키는 **출력하지 않는다**(의미 있는 false·0은 유지). R0가 canonical schema로 확장하며, 키 부재는 null로 해석된다. +- **출력 금지 목록**: `transport_candidate_refs`(R0가 provenance에서 재구성), `slice_guard`의 source_universe 전체 에코(→ `source_stage_a_created_at_utc`와 `input_digests_summary`(digest들의 결합 SHA 1개)만 유지), `Reason`, `PriorAct`, `ReasonRefs`, `downstream_seed_refs`, 긴 review `description`. +- `domain_review_queue` 항목은 §4 review_handoff 스키마의 필드만 출력한다(closed enum issue_type + source refs 3종 + downstream_owner; 서술은 `template_note` 생략 — R0가 고정 템플릿 부여). +- OUTPUT_SURFACE_HARD_RULE(단일 raw JSON)은 유지. +- `llm_verbosity: medium → low` (5개 전부). +- 스키마 예시 블록은 sparse 예시로 교체하되, **candidate의 실질 필드(BOType·ActionType·JuristicAct·Action·core_field_base·amount·source refs·domain_payload의 유의미 값)와 B4/B5 named fields는 그대로 요구**한다(§0.2-4). + +### 6.3 프리픽스 + +COMMON_CACHE_PREFIX_STAGE_1 블록은 현행 바이트 그대로 유지(5개 동일성 보존 — 회귀 기준 SHA `adbdf43c…`는 들여쓰기 포함 기준이므로 블록을 편집하지 않는 한 자동 충족). + +--- + +## 7. STEP 3 — R0 명세 (`Task_C_BO_R0_seed_reducer_and_exception_planner`) + +code-executor(timeout 240). publisher(3623~3962)+domain_join(3963~4585)+PostB_1(4586~5045)의 로직을 이식·통합한다. + +1. **입력**: 워커 5개의 stdout(`{{prev.Task_C_BO_Stage_B_B1_Money_Successor.json_output}}` 등 5개 — r''' raw string으로 주입, SKILL §6.1) + `stage_a_context.json`·`source_universe_manifest.json` 파일 read. +2. **관용 파서(Y-3)**: SKILL §6.7 `parse_llm_json` 패턴으로 워커 출력의 코드펜스·잡음을 결정적으로 salvage한 후 strict 검증. `OUTPUT_SURFACE_HARD_RULE` 위반은 salvage 성공 시 warning 기록으로 강등(pro 재호출 차단). +3. **가드 검증**(publisher 이식): domain_id·schema_version·worker_contract·digest 대조. +4. **seed 파일 5종 write**(감사 아티팩트 유지 — 기존 경로 `stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/*.json`에 **expander 적용 후의 full schema로** 저장, 기존 파일 형식 호환). +5. **schema expander**: sparse candidate → canonical full schema 확장(§6.2 금지 목록의 default 복원: Reason=null, PriorAct=null, ReasonRefs=[], downstream_seed_refs=4-key null 객체, transport_candidate_refs 재구성, domain_payload 미출력 키의 null 복원). **expander는 schema default만 채우고 사실을 창작하지 않는다.** +6. **membership·invariant 검사**(domain_join+PostB_1 이식): source ref ⊆ universe(위반 → BLOCK), candidate_ref 형식·유일성, prop_id 매핑. +7. **결정적 defer 정책(X-2 — LLM에 보내지 않는 예외)**: + +| 예외 | 결정적 처리 | +|---|---| +| exact duplicate (정규화 candidate 동일) | provenance union이 무손실일 때만 canonical merge + `EXACT_DUPLICATE_MERGE` 기록 | +| near duplicate (shared source refs, 비동일) | `KEEP_SEPARATE` + `near_dup_cluster_id` + review code | +| meeting-only candidate (source evidence 공백) | 보존 + `MEETING_ONLY_EVIDENCE_GAP` review code | +| PriorAct/선행관계 불명 | null 유지 + review code | +| Stage 2 법리 확정 필요(`legal_theory_required`) | review handoff 회부, LLM 금지 | +| source ref가 universe 밖 | BLOCK (hard) | +| 필수 schema type/enum 오류 | BLOCK 또는 해당 워커 rerun 지시 기록 | + +8. **exception pack 생성(X-3)**: 다음 4중 조건을 **모두** 충족하는 항목만 pack에 수록 — ① F0가 진행하려면 단일 값 선택이 필수 ② source-backed 후보 2개 이상 ③ 보존·defer 불가 ④ compact payload로 판정 충분. 항목당 compact_payload는 판정 필요 필드만(후보당 ≤2KB), full ledger·Stage A 참조 금지. +9. **review handoff 생성**: 워커 review 13건 + defer 정책 review code를 §4 스키마로 전량 수록(고정 template_note 부여). +10. **ledger 파일 write** + **conservation invariant**(write 전·후): `len(seed candidates) == len(ledger_candidates) + len(EXACT_DUPLICATE_MERGE 흡수분)`, 모든 candidate_ref의 1:1 추적성, 정렬 안정성(N-6 sort key). +11. **stdout 1줄**: `{"status","message","ledger_path","exception_pack_path","review_handoff_path","has_exceptions","exception_count","candidate_counts":{입력/최종/병합/BLOCK}}`. + +--- + +## 8. STEP 4 — R1 명세 (`Task_C_BO_R1_exception_adjudicator`) + +Part 1 v3의 `Task_B12_gate_llm_adjudicator`와 동일 골격. LLM task 엔트리: + +```yaml + - task_name: Task_C_BO_R1_exception_adjudicator + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: low + llm_verbosity: low + max_iterations: 1 + use_tools: [localdocs] + cache_control: {mode: auto, ttl: 20m} + preflight_files: + - quality_gates/stage1_part2_exception_pack.json +``` + +프롬프트 구성: STEP 0-5에서 추출한 canonical COMMON_CACHE_PREFIX_STAGE_1(바이트 동일) + 신규 STATIC_BLOCK: +1. MISSION: "R0가 추출한 non-deferrable compact exception만 판정한다. 파일 병합·BO 작성·사실 창작 금지." +2. EXECUTION: ① pack 1파일만 read ② `has_exceptions == false`면 `{"postb_exception_adjudication": {…, "status": "READY_NO_EXCEPTIONS", "exception_count": 0, 전 배열 []}}`를 decisions 파일에 write 후 `"NO_EXCEPTIONS"` 출력·즉시 종료 ③ 예외 존재 시 compact_payload만으로 판정(추가 read 금지) ④ decision은 `allowed_decisions` 내 값 또는 `BLOCK_REVIEW`만 ⑤ decisions 파일 write(§4 스키마 — v2 PostB_2 출력 스키마와 동일) ⑥ 건수 요약 1줄 출력·종료. +3. ABSOLUTE_FORBIDDEN: pack 외 파일 read / BO.json·ledger·handoff 작성 / 새 candidate_ref·bh#·사실 창작 / pack에 없는 exception_id / markdown fence. +4. 판정 기준(v2 PostB_2의 Allowed judgment types·Required rules 이식): canonical(KEEP_SEPARATE/MERGE/SPLIT/DROP/BLOCK_REVIEW), field conflict(제공값 중 선택), link ambiguity(나열된 ref/NO_LINK/BLOCK_REVIEW), semantic risk(PASS/WARNING/BLOCK_REVIEW). 판정 불충분 시 항상 BLOCK_REVIEW(확신 없는 확정 금지). + +--- + +## 9. STEP 5 — F0 명세 (`Task_C_BO_F0_final_bo_compiler_gate_writer`) + +code-executor(timeout 240). PostB_3(5141~5556)+PostB_4(5557~5904) 로직 이식·통합. + +1. 입력: `postb_seed_ledger.json`·`postb_adjudication_decisions.json`·`stage_a_context.json`(또는 manifest) 파일 read. `{{prev}}` 대형 주입 제거. +2. adjudication 검증(v2 `_adjudication` 이식): schema_version·status 검사, `blocked_review_items` 존재 시 FAILED_NEEDS_REVIEW. +3. 결정 적용(v2 `_decision_sets`·`_field_decision_map`·`_link_decision_map` 이식): EXACT_DUPLICATE_MERGE(R0 결정)+R1 canonical/field/link 결정 반영. +4. **bh# 부여(N-6)**: ledger 정렬 순서에서 dropped/merged 제외 후 `bh{idx}` 연속 부여 — v2 5443·5466행 로직 그대로. +5. **Reason/PriorAct 정책(R-5/X-7)**: v2 5511행의 2분기 template을 계승하되 enum화 — `ReasonRefs` 실재 시 "ReasonRefs에 기재된 선행 BO와 source evidence/event chain으로 연결됨", 부재 시 "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO". 자유 서술 생성 금지. PriorAct는 명시적 prior link 결정이 있을 때만 기입. +6. 게이트(v2 PostB_4 이식): Stage A freshness, bo_items 비어있지 않음, pre-gate blockers 공백, item별 스키마·provenance ⊆ universe·downstream_seed_refs 키 검증, **conservation**(seed→BO 추적성: BO 수 + 병합 흡수 수 + BLOCK 수 == seed 후보 수). +7. `BO.json` 단일 write → reread → JSON parse·bh# 연속성·count·SHA-256 검증(v2와 동일). +8. review handoff의 최종 status 갱신(BLOCK_REVIEW 항목 반영). +9. stdout 1줄: `{"status","message","bo_count","merged","blocked","gates":[…]}`. + +--- + +## 10. STEP 6 — S0 명세 + task_procedure + +### S0 (`Task_C_BO_S0_signal_bundle_writer`) + +code-executor(timeout 180). Middle ×3(5905~7283)의 signal 빌더 함수를 이식·통합: `BO.json` **1회** read + `source_universe_manifest.json`(+필요 시 ledger compact index) → 메모리에서 3개 projection 생성 → `actio_case_signals.json`·`case_liability_signals.json`·`legal_effect_signals.json` write(각 파일의 schema_version·status 규칙·검증 로직은 v2의 `_validate` 이식). v2가 B4 stdout을 참조하던 부분은 ledger의 B4 도메인 candidate(전 필드 보존됨)로 대체한다. stdout 1줄. + +### task_procedure (Plan A — 전면 교체) + +```yaml + task_procedure: + IN: + nexts: [Task_C_BO_A0_context_and_domain_slice_compiler] + wait_until: [] + Task_C_BO_A0_context_and_domain_slice_compiler: + nexts: + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + wait_until: [IN] + Task_C_BO_Stage_B_B1_Money_Successor: + nexts: [Task_C_BO_R0_seed_reducer_and_exception_planner] + wait_until: [Task_C_BO_A0_context_and_domain_slice_compiler] + # B2~B5 동일 형식 (각각 A0만 대기) + Task_C_BO_R0_seed_reducer_and_exception_planner: + nexts: [Task_C_BO_R1_exception_adjudicator] + wait_until: + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + Task_C_BO_R1_exception_adjudicator: + nexts: [Task_C_BO_F0_final_bo_compiler_gate_writer] + wait_until: [Task_C_BO_R0_seed_reducer_and_exception_planner] + Task_C_BO_F0_final_bo_compiler_gate_writer: + nexts: [Task_C_BO_S0_signal_bundle_writer] + wait_until: [Task_C_BO_R1_exception_adjudicator] + Task_C_BO_S0_signal_bundle_writer: + nexts: [OUT] + wait_until: [Task_C_BO_F0_final_bo_compiler_gate_writer] + OUT: + nexts: [] + wait_until: [Task_C_BO_S0_signal_bundle_writer] +``` + +**Plan B**: `IN → Stage_A' → B0_i ×5(각각 Stage_A'만 대기) → 워커 i(자기 B0_i만 대기 — 1:1) → R0(워커 5개 대기) → R1 → F0 → S0 → OUT`. + +--- + +## 11. 수용 기준 (Definition of Done) + +### 11.1 정적 검사 + +1. `Stage_1_Part_2_Claude_v3.yml` YAML parse 성공, task 수 10(Plan A)/14(Plan B), task_procedure가 §10과 일치, unreachable task 0, 구 task 참조(`{{prev.Task_C_BO_Stage_B0_…}}` 등 폐기 채널) 잔존 0. +2. 모든 embedded Python `ast.parse` 통과, SKILL.md 준수(full boilerplate, `{{__user_hash__}}`/`{{__workspace_hash__}}`, raw_decode fallback, **stdout 단일 JSON 라인**, `r'''…'''` 템플릿 주입). +3. `BO.json` writer 정확히 1개(F0), signal 3파일 writer 정확히 1개(S0), review handoff writer R0(+F0 status 갱신). +4. LLM task 6개의 COMMON_CACHE_PREFIX_STAGE_1 바이트 동일(기준: v2 워커 블록). +5. 워커 5개의 llm_model/reasoning은 v2와 동일(pro-preview/high — N-3), verbosity만 low. R1은 flash-lite/low. +6. 워커의 도메인 법률 본문(AUTHORITY_BOUNDARY·DOMAIN_SCOPE·도메인 규칙)이 v2 대비 자구 보존(입출력 계약·채널 변경분 제외). + +### 11.2 fixture 회귀 (7/10 실행물 입력, 코드 로컬 실행) + +1. **A0**: slice 5종 생성, 각 slice의 primary/support 분리로 B2·B5의 event/evidence 전체 재적재(중복 배수 2.56×/2.80×)가 유의미하게 감소, universe(evidence 30·event 71·meeting 41) 보존. +2. **sparse 왕복**: 7/10 seed 5종을 §6.2 규칙으로 sparse화 → R0 expander로 확장 → 원본과 의미 동등(유의미 값 보존, default만 복원) + **2-run 바이트 결정론**. +3. **R0 defer**: fixture 예외 6건(schema risk 2 = meeting-only, near-dup cluster 4)이 전부 결정적 처리되어 `has_exceptions=false`, R1이 NO_EXCEPTIONS 조기 종료. review handoff에 워커 review 13건 + defer review code 전량 수록(source refs 보존). +4. **F0**: BO 50건, bh1~bh50 연속·유일, **v2 산출 BO.json과 item-level 의미 동등**(Reason 2분기 template 포함), conservation 성립(50 = 50 + 0 + 0), meeting-only BO 2건 보존, B4/B5 named fields 무손실. +5. **S0**: signal 3종 5/50/50 items, schema_version·status 규칙 유지. +6. **주입 테스트**: ① universe 밖 source ref → R0 BLOCK ② exact duplicate 주입 → provenance union merge + conservation 성립 ③ non-deferrable field conflict 주입(동일 사실에 상충하는 source-backed 값 2개) → pack 수록·R1 발동 경로 ④ 워커 출력에 코드펜스 주입 → R0 관용 파서 salvage 성공(재호출 없음). +7. **하류 무영향**: 개정 BO.json·signal 3종으로 Part 3 LES0·Part 4 FL0 파서 통과(domain_payload null-생략본 포함). + +### 11.3 성능 합격 기준 (실 재실행, 동일 조건 3회 중앙값) + +task ≤10(Plan A), R1 입력 = pack만(≤20KB), fixture에서 R1 실질 작동 0회, 워커 compact 출력 합계 ≤70KB 목표, 런타임 ≤5분, 비용 ≤$2.50. **§11.2 품질 불변식 위반 시 성능 달성해도 실패.** + +### 11.4 모델 A/B (v3 배포 후, D-1) + +고정 slice 입력으로 B1/B2/B3의 pro vs flash 계열 산출을 후보 수·source ref 집합·review queue 동등성 + 변호사 육안 대조로 판정 → 통과 도메인만 v3.1에서 하향. B4/B5는 대상 아님. **B5 2분할(개선전략서 D-5)**은 v3 실측에서 B5가 여전히 워커 구간 wall-clock 지배 항목일 때만 별도 개정으로 검토한다(v3 범위 밖). + +--- + +## 12. 금지 사항 최종 목록 + +1. v2 원본 덮어쓰기·이동·삭제 금지 (v3는 신규 파일). +2. §1 불변 계약(4파일 스키마, BO 내부 중복 필드, domain_payload 필드, 워커 법률 본문, enum 문자열) 변경 금지. +3. Part 1/3/4 YAML 수정 금지. +4. 워커 모델·reasoning 하향 금지(v3 시점 — A/B는 §11.4 절차로만). +5. R1에 pack 외 입력 제공 금지, R1의 파일 병합·BO 작성 금지, expander의 사실 창작 금지. +6. near-duplicate 자동 병합 금지(provenance union 무손실 exact dup 제외), meeting-only BO 자동 삭제 금지. +7. review 항목의 silent drop 금지(13건 + 신규 code 전량 handoff 수록). +8. LLM의 긴 review 서술·Reason 자유 서술 생성 금지. +9. count·ID·순서를 입력 데이터 외 근거로 생성 금지(bh#는 N-6 규칙, count는 len()). +10. 본 명세서에 없는 "개선"의 임의 추가 금지 — 범위 밖 아이디어는 개정 보고서에 제안으로만 기록. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_2_Improvement_Strategy_Codex_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_2_Improvement_Strategy_Codex_v1.md new file mode 100644 index 00000000..63e6d87e --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_2_Improvement_Strategy_Codex_v1.md @@ -0,0 +1,1433 @@ +# Stage 1 Part 2 v2 개정 작업 명세서 + +## 0. 문서의 지위와 실행 명령 + +이 문서는 `Part_2_Improvement_Codex_v1.md`의 통합 권고를 실제 YAML 개정 작업으로 변환한 **완료 조건이 있는 구현 명세서**다. 구현자는 이 문서를 참고자료나 선택지 목록으로 취급하지 않는다. 아래의 “유지”, “삭제”, “교체”, “금지”, “반드시” 지시를 그대로 실행한다. + +### 0.1 입력, 출력, 작업 경계 + +| 구분 | 경로 | 처리 원칙 | +|---|---|---| +| 개정 원본 | `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_2_v2.yml` | 읽기 전용. 직접 덮어쓰지 않는다. | +| 설계 근거 | `YAML_Prompts/1. Stage_1/v.6/Part_2_Improvement_Codex_v1.md` | 9+N 구조, sparse worker, compact PostB 원칙을 적용한다. | +| Python 제약 | `YAML_Prompts/1. Stage_1/SKILL.md` | code-executor, localdocs, template, wildcard 제약을 모두 적용한다. | +| canonical prefix | `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_1_Codex_v3.yml` | ``을 바이트 동일하게 복제한다. | +| 회귀 fixture | `YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm` | 개정 전 기준값과 개정 후 결과를 비교한다. | +| 개정 결과 | `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_2_Codex_v3.yml` | v2 전체를 복제한 뒤 이 명세서가 허용한 부분만 수정한다. | + +다음 파일은 이번 작업에서 수정하지 않는다. + +- `Stage_1_Part_1_Codex_v3.yml` +- `Stage_1_Part_3_v1.yml` +- `Stage_1_Part_4_v2.yml` +- `Results_July_10_3_15pm` 아래의 모든 파일 +- `YAML_Prompts/1. Stage_1/SKILL.md` + +### 0.2 최종 목표 + +현재 Part 2의 20개 task를 다음 10개 task definition으로 교체한다. + +```text +Task_C_BO_A0_context_domain_slice_compiler [Python] + +Task_C_BO_Stage_B_B1_Money_Successor [LLM] +Task_C_BO_Stage_B_B2_Secured_Registry [LLM] +Task_C_BO_Stage_B_B3_Land_Valuation [LLM] +Task_C_BO_Stage_B_B4_Actio [LLM] +Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense [LLM] + +Task_C_BO_R0_seed_reducer_exception_planner [Python] +Task_C_BO_R1_exception_adjudicator_* [0..N LLM] +Task_C_BO_F0_final_bo_gate_writer [Python] +Task_C_BO_S0_signal_bundle_writer [Python] +``` + +runtime task 수는 다음과 같아야 한다. + +```text +no-exception 경로: 9개 +exception pack N개 경로: 9 + N개 +``` + +현재의 상시 PostB LLM은 제거한다. R0가 실제로 비결정적이고 finalization에 필수인 compact exception을 만들었을 때만 R1 wildcard 인스턴스를 생성한다. + +### 0.3 목표 DAG + +```text +IN + | + v +A0 context + domain-slice compiler [Python] + | + +--> B1 Money/Successor [LLM] + +--> B2 Secured/Registry [LLM] + +--> B3 Land/Valuation [LLM] + +--> B4 Actio [LLM] + +--> B5 Succession/Notice/Lien/Asset/Defense [LLM] + | \ | / + v v v v +R0 reducer + schema expander + exception planner [Python] + | + +--> dynamic R1 adjudicator_* [0..N LLM] + | / + v v +F0 final BO compiler + gate + single writer [Python] + | + v +S0 signal bundle writer [Python] + | + v +OUT +``` + +## 1. 절대 준수 원칙 + +### 1.1 법률·품질 안전 원칙 + +1. evidence index, event candidate, meeting clause, source locator, candidate ref를 묵시적으로 삭제하지 않는다. +2. Stage 1은 최종 소장 법리나 승패 판단을 확정하지 않는다. +3. LLM은 assigned domain slice 또는 assigned exception pack 밖의 사실을 사용할 수 없다. +4. 미확정 법리는 `legal_theory_required`, 추출 가능하지만 불명확한 사실은 `review_required` 또는 closed review code로 보존한다. +5. meeting-only assertion은 자동 삭제하지 않는다. `MEETING_ONLY_EVIDENCE_GAP` warning을 붙여 보존한다. +6. near duplicate는 `KEEP_SEPARATE`와 cluster id를 부여한다. +7. exact duplicate는 substantive field가 같고 provenance union이 무손실인 경우에만 Python이 병합할 수 있다. +8. source universe 밖의 ref는 자동 보정하지 않고 BLOCK한다. +9. review prose는 최소화하고 issue type, severity, source refs, downstream owner를 구조화한다. +10. 최종 외부 파일의 경로, envelope, 필수 schema와 named legal payload를 유지한다. + +### 1.2 deterministic 작업 원칙 + +다음 작업은 반드시 Python이 수행한다. + +- 파일 존재 확인과 strict JSON parse +- input digest와 run fingerprint +- source universe와 subset 검사 +- domain routing rule 적용 +- count, ordering, ID 부여 +- sparse delta의 schema default 복원 +- transport metadata와 candidate ref index 구성 +- exact duplicate 및 near-duplicate cluster 계산 +- candidate conservation +- final `bh#` 부여 +- Evidence/EvidenceTitles 구성 +- serialization과 SHA-256 +- BO/signal schema 검증 +- file write 후 reread 검증 + +LLM은 domain별 provisional BO seed 추론과 non-deferrable compact exception 판정에만 사용한다. + +### 1.3 단일 writer 권한 + +| 산출물 | 유일한 writer | +|---|---| +| `stage1_tmp/task_c_bo/stage_a_context.json` | A0 | +| `stage1_tmp/task_c_bo/source_universe_manifest.json` | A0 | +| `stage1_tmp/task_c_bo/domain_slices/*.json` | A0 | +| `stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/*.json` | R0 | +| `stage1_tmp/task_c_bo/postb_seed_ledger.json` | R0 | +| `quality_gates/stage1_part2_review_handoff.json` | R0 | +| `quality_gates/stage1_part2_exception_pack.json` | R0 | +| `quality_gates/stage1_part2_adjudication_parts/PACK-*.json` | 해당 R1 인스턴스 | +| `stage1_tmp/task_c_bo/postb_compiled_bo_bundle.json` | F0 | +| `quality_gates/stage1_part2_bo_gate.json` | F0 | +| `BO.json` | F0 | +| `actio_case_signals.json` | S0 | +| `case_liability_signals.json` | S0 | +| `legal_effect_signals.json` | S0 | + +한 파일을 둘 이상의 task가 쓰게 만드는 구현은 금지한다. + +### 1.4 외부 계약 불변 + +다음 네 파일은 Part 3, Part 4 및 이후 Stage 2와의 계약이다. + +```text +BO.json +actio_case_signals.json +case_liability_signals.json +legal_effect_signals.json +``` + +내부 worker output은 sparse로 바꿀 수 있지만 F0와 S0가 쓰는 외부 파일에서는 현재 v2의 canonical key를 복원한다. Stage 2 전체 consumer가 확인되지 않은 상태에서 BO alias, null key 또는 중복 key를 제거하지 않는다. + +### 1.5 MCP와 보안 블록 + +`tools.mcpServers`의 server type, URL, description, 기존 인증 header는 변경하지 않는다. 새 API key, token 또는 credential을 추가하지 않는다. 기존 인증 값을 보고서나 로그에 출력하지 않는다. + +### 1.6 금지된 개정 + +- B1-B5를 하나의 LLM task로 합치지 않는다. +- B5를 이번 개정에서 분할하지 않는다. +- B2를 Flash로 하향하지 않는다. +- R1을 예외가 없어도 실행하지 않는다. +- 검증되지 않은 `run_if`, `skip_if`, `when` 키를 발명하지 않는다. +- full seed ledger를 LLM prompt, preflight 또는 `item`에 넣지 않는다. +- LLM이 `BO.json`이나 signal을 쓰게 하지 않는다. +- 최종 BO schema를 sparse로 만들지 않는다. +- placeholder, 생략 주석, “기존 코드 동일” 같은 축약을 최종 YAML에 남기지 않는다. + +## 2. 원본 구조와 개정 매핑 + +### 2.1 원본 task 수 + +`Stage_1_Part_2_v2.yml`에는 Python 14개와 LLM 6개, 합계 20개 task가 있다. + +### 2.2 정확한 삭제·유지·대체 표 + +| 현재 task | 처리 | 새 책임 위치 | +|---|---|---| +| `Task_C_BO_Stage_A_input_normalization` | 삭제·흡수 | A0 | +| `Task_C_BO_Stage_B0_input_money_successor` | 삭제·흡수 | A0 | +| `Task_C_BO_Stage_B0_input_secured_registry` | 삭제·흡수 | A0 | +| `Task_C_BO_Stage_B0_input_land_valuation` | 삭제·흡수 | A0 | +| `Task_C_BO_Stage_B0_input_actio` | 삭제·흡수 | A0 | +| `Task_C_BO_Stage_B0_input_succession_notice_lien_asset_defense` | 삭제·흡수 | A0 | +| `Task_C_BO_Stage_B_B1_Money_Successor` | 이름 유지·prompt/model 수정 | 동일 task | +| `Task_C_BO_Stage_B_B2_Secured_Registry` | 이름 유지·prompt/output 수정 | 동일 task | +| `Task_C_BO_Stage_B_B3_Land_Valuation` | 이름 유지·prompt/model 수정 | 동일 task | +| `Task_C_BO_Stage_B_B4_Actio` | 이름 유지·prompt/output 수정 | 동일 task | +| `Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense` | 이름 유지·prompt/output 수정 | 동일 task | +| `Task_C_BO_Stage_B_worker_output_publisher` | 삭제·흡수 | R0 | +| `Task_C_BO_Stage_B_domain_join` | 삭제·흡수 | R0 | +| `Task_C_BO_PostB_1_seed_ledger_compiler` | 삭제·흡수 | R0 | +| `Task_C_BO_PostB_2_single_exception_adjudicator` | 삭제 | R1 wildcard | +| `Task_C_BO_PostB_3_final_bo_compiler` | 삭제·흡수 | F0 | +| `Task_C_BO_PostB_4_final_gate_and_writer` | 삭제·흡수 | F0 | +| `Task_C_BO_Middle_Input_actio_case_signals` | 삭제·흡수 | S0 | +| `Task_C_BO_Middle_Input_case_liability_signals` | 삭제·흡수 | S0 | +| `Task_C_BO_Middle_Input_legal_effect_signals` | 삭제·흡수 | S0 | + +### 2.3 새 task 정의 + +```text +Task_C_BO_A0_context_domain_slice_compiler +Task_C_BO_R0_seed_reducer_exception_planner +Task_C_BO_R1_exception_adjudicator_* +Task_C_BO_F0_final_bo_gate_writer +Task_C_BO_S0_signal_bundle_writer +``` + +### 2.4 Agent header 수정 + +1. `Agent.version`을 `v.3`으로 변경한다. +2. Stage-level `llm_provider`와 `llm_model`은 삭제한다. +3. `tools.mcpServers` 블록은 변경하지 않는다. + +## 3. canonical common cache prefix + +### 3.1 source와 기대 hash + +`Stage_1_Part_1_Codex_v3.yml`의 첫 `` 시작 태그부터 종료 태그까지를 복제한다. + +```text +bytes: 3977 +sha256: 5feea0882eac0a1431b377fa20a396e7c009fe9a441da6dca7050e3a24618c50 +``` + +### 3.2 적용 대상 + +다음 6개 LLM task definition의 prefix가 바이트 동일해야 한다. + +```text +Task_C_BO_Stage_B_B1_Money_Successor +Task_C_BO_Stage_B_B2_Secured_Registry +Task_C_BO_Stage_B_B3_Land_Valuation +Task_C_BO_Stage_B_B4_Actio +Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense +Task_C_BO_R1_exception_adjudicator_* +``` + +task별 role, path, input, schema는 common prefix 종료 뒤에 둔다. 최종 YAML에서 prefix block은 6개, unique hash는 1개여야 한다. + +## 4. 모든 Python task의 공통 구현 제약 + +A0, R0, F0, S0는 `mcp: code-executor`, `tool_name: run_code`, `language: python`, `requirements: httpx`, `network: agent-network`를 사용한다. + +### 4.1 localdocs full boilerplate + +네 task 모두 `SKILL.md` 제5절의 full boilerplate를 포함한다. `initialize.params.clientInfo`에는 반드시 다음을 넣는다. + +```python +"user_id": "{{__user_hash__}}", +"workspace_id": "{{__workspace_hash__}}", +``` + +### 4.2 read_docs parsing + +`read_json` helper는 JSON-RPC/SSE, `result.content[].text`, localdocs `results[]`, JSON 문자열을 순차 unwrap하고 `json.loads` 실패 시 `json.JSONDecoder().raw_decode(raw.strip())` fallback을 사용한다. 허용되지 않은 prose tail은 실패다. + +### 4.3 template 삽입 + +R0의 worker output template는 raw triple string으로 삽입한다. + +```python +B1_RAW = r"""{{prev.Task_C_BO_Stage_B_B1_Money_Successor.json_output}}""" +``` + +### 4.4 LLM JSON wrapper salvage + +R0는 BOM, 공백, 단일 JSON code fence, `success/output`, `type/text` wrapper만 제거할 수 있다. 누락 candidate, enum, source ref 또는 사실은 자동 수정하지 않는다. + +### 4.5 code 완전성 + +최종 YAML에 `pass`, `TODO`, `TBD`, `omitted`, `same as v2`, placeholder를 남기지 않는다. 기존 helper를 이관할 때 함수 본문 전체를 삽입한다. + +### 4.6 실패 표면 + +Python task 실패 시 compact FAILED status를 stdout에 한 번 출력하고 final artifact를 쓰지 않으며 non-zero exit 또는 `raise`로 runtime에 실패를 전달한다. 오류를 출력한 뒤 `sys.exit(0)`으로 성공 처리하지 않는다. + +## 5. A0 작업 명세 + +### 5.1 task header + +```yaml +- task_name: Task_C_BO_A0_context_domain_slice_compiler + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 +``` + +### 5.2 이관 source + +A0는 기존 Stage A 전체와 B0 5개의 router helper/DOMAIN_CONFIG를 흡수한다. 중복 helper는 1벌로 만들고 domain 차이는 `DOMAIN_CONFIGS` table로 옮긴다. terms, allowed BO types, meeting/event sources, hazard keys, taxonomy axes는 원본 값을 빠짐없이 복사한다. + +### 5.3 입력 + +```text +client_meeting.md +evidence_event_candidates.json +evidence_indexed.json +Default_Agent/Juristic_Act.md +quality_gates/stage1_part1_soft_gate_handoff.json # optional +``` + +필수 네 파일은 한 localdocs 세션에서 읽는다. `BO.json`과 signal 파일은 읽지 않는다. + +### 5.4 실행 순서 + +1. 필수 파일 존재 여부를 검사한다. +2. 각 입력을 한 번 읽고 SHA-256을 계산한다. +3. 현재 Stage A와 동일한 meeting/event/evidence/taxonomy map과 hazard seed를 만든다. +4. Part 1 soft gate가 있으면 digest와 source count를 대조한다. +5. `stage_a_context`를 만든다. +6. 5개 config를 loop로 순회한다. +7. 현재 B0와 같은 selected source ID 집합을 먼저 계산한다. +8. source를 primary/support/review_only로 tiering한다. +9. slice 5개를 모두 메모리에서 만들고 검증한다. +10. 7개 artifact를 쓴 뒤 reread 검증한다. +11. stdout에는 path/digest/count manifest만 출력한다. + +### 5.5 routing tier 원칙 + +1차 v3에서는 기존 B0가 선택한 source ID를 삭제하지 않는다. + +| tier | 조건 | payload | +|---|---|---| +| `primary` | 구조화 field/event/component 또는 복수 독립 field가 domain을 직접 지지 | compact full record | +| `support` | primary의 evidence/meeting/asset/property/source-ref closure | ID와 핵심 날짜·당사자·금액·요약 | +| `review_only` | broad term 하나만 match하거나 가능성만 존재 | ID, match reason, source type | + +`등기`, `변제`, `항변`, `점유`, `목적물`, `사용`, `통지`, `매매`, `가액` 하나만으로 primary에 올리지 않는다. ref는 제거하지 않고 tier만 낮춘다. + +### 5.6 artifact + +```text +stage1_tmp/task_c_bo/stage_a_context.json +stage1_tmp/task_c_bo/source_universe_manifest.json +stage1_tmp/task_c_bo/domain_slices/B1_Money_Successor.json +stage1_tmp/task_c_bo/domain_slices/B2_Secured_Registry.json +stage1_tmp/task_c_bo/domain_slices/B3_Land_Valuation.json +stage1_tmp/task_c_bo/domain_slices/B4_Actio.json +stage1_tmp/task_c_bo/domain_slices/B5_Succession_Notice_Lien_Asset_Defense.json +``` + +### 5.7 source universe manifest schema + +```json +{ + "schema_version": "task_c_bo_source_universe_manifest.v1", + "status": "READY", + "run_fingerprint": "sha256", + "input_digests_sha256": {}, + "source_universe": { + "meeting_clause_ids": [], + "event_candidate_ids": [], + "evidence_indexes": [] + }, + "counts": {}, + "domain_slice_digests_sha256": {} +} +``` + +`run_fingerprint`는 정렬된 input digest canonical JSON의 SHA-256이다. 실행 시각은 넣지 않는다. + +### 5.8 domain slice schema + +worker guard 변경을 최소화하기 위해 root와 `task_c_bo_stage_b_domain_slice.v1`을 유지한다. + +```json +{ + "stage_b_domain_slice": { + "schema_version": "task_c_bo_stage_b_domain_slice.v1", + "router_status": "READY", + "domain_id": "B1_Money_Successor", + "domain_label": "string", + "allowed_legal_effect_bo_types": [], + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": "ISO-8601", + "run_fingerprint": "sha256", + "input_digests_sha256": {}, + "stage_b_worker_contract": {}, + "source_universe": {}, + "selected_meeting_clauses": [], + "selected_event_candidates": [], + "selected_evidence_authority": [], + "juristic_act_taxonomy_subset": {}, + "hazard_subset": {}, + "part1_review_items_relevant": [], + "shared_or_ambiguous_items": [], + "routing_audit": { + "primary_ids": [], + "support_ids": [], + "review_only_ids": [], + "activation_reasons": [], + "source_count": {}, + "slice_bytes": 0, + "broadness_ratio": 0.0 + } + } +} +``` + +### 5.9 A0 stdout + +```json +{ + "a0_manifest": { + "schema_version": "task_c_bo_a0_manifest.v1", + "status": "READY", + "run_fingerprint": "sha256", + "stage_a_context_path": "stage1_tmp/task_c_bo/stage_a_context.json", + "source_manifest_path": "stage1_tmp/task_c_bo/source_universe_manifest.json", + "domain_slice_paths": {}, + "input_digests_sha256": {}, + "counts": {} + } +} +``` + +full context나 slice를 stdout에 넣지 않는다. + +## 6. B1-B5 worker 개정 명세 + +### 6.1 유지할 domain 법률 내용 + +각 worker의 TASK_META, AUTHORITY_BOUNDARY, DOMAIN_SCOPE, WORKFLOW, domain payload named field, review 조건, Stage 2 defer 규칙을 유지한다. B1 agreed rate, B2 fraction/registry/mortgage, B3 possession/valuation, B4 actio balance sheet/remedy/cap, B5 notice/lien/possession/asset alias 규칙을 삭제하지 않는다. + +### 6.2 입력 방식 + +기존 `{{prev.Task_C_BO_Stage_B0_input_*}}` inline 입력을 제거한다. 각 worker는 `use_tools: [localdocs]`, `preflight: false`로 설정하고 assigned path 하나만 `read_docs`로 한 번 읽는다. + +| Worker | assigned path | +|---|---| +| B1 | `stage1_tmp/task_c_bo/domain_slices/B1_Money_Successor.json` | +| B2 | `stage1_tmp/task_c_bo/domain_slices/B2_Secured_Registry.json` | +| B3 | `stage1_tmp/task_c_bo/domain_slices/B3_Land_Valuation.json` | +| B4 | `stage1_tmp/task_c_bo/domain_slices/B4_Actio.json` | +| B5 | `stage1_tmp/task_c_bo/domain_slices/B5_Succession_Notice_Lien_Asset_Defense.json` | + +`list_docs`, 다른 path read, `write_file`, `write_binary_file`은 금지한다. 응답으로 raw JSON object 하나만 반환한다. + +### 6.3 model 설정 + +| Worker | provider/model | reasoning | verbosity | iterations | TTL | +|---|---|---|---|---:|---| +| B1 | `google/gemini-3.5-flash` | high | low | 2 | 1h | +| B2 | `google/gemini-3.1-pro-preview` | high | low | 2 | 1h | +| B3 | `google/gemini-3.5-flash` | medium | low | 2 | 1h | +| B4 | `google/gemini-3.1-pro-preview` | high | low | 2 | 1h | +| B5 | `google/gemini-3.1-pro-preview` | high | low | 2 | 1h | + +B1/B3는 A/B gate를 통과해야 확정한다. 실패하면 해당 worker만 Pro/high/low로 되돌리고 검증한다. + +### 6.4 sparse output root + +```json +{ + "stage_b_domain_bo_seed_delta": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed_delta.v1", + "domain_id": "B1_Money_Successor", + "status": "READY|READY_WITH_REVIEW|FAILED", + "run_fingerprint": "sha256", + "slice_digest_sha256": "sha256", + "bo_seed_candidates": [], + "domain_review_queue": [] + } +} +``` + +### 6.5 sparse candidate schema + +필수 key: + +```json +{ + "candidate_ref": "B1:001", + "BOType": "event|state", + "ActionType": "existing enum", + "Action": "short neutral draft", + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "domain-specific tag" + } + } +} +``` + +실제 값이 있을 때만 `JuristicAct`, `Legal_Keywords`, `core_field_base`, `amount`, `downstream_seed_refs`, domain payload optional field를 출력한다. + +다음은 LLM이 출력하지 않는다. + +```text +transport_metadata +transport_candidate_refs +Reason +PriorAct +ReasonRefs +Evidence +EvidenceTitles +BO_ID +id +빈 dict/list/null scaffold +``` + +`false`, `0`, source가 명시한 빈 문자열은 단순 empty로 삭제하지 않는다. + +### 6.6 candidate ref + +worker 배열 순서에 따라 `B1:001`, `B2:001` 형식으로 연속 부여한다. 중복, gap, wrong prefix는 R0에서 FAILED이며 R0가 임의 보정하지 않는다. + +### 6.7 compact review schema + +```json +{ + "review_id": "B1:R001", + "severity": "SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "candidate_refs": [], + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Part3|Part4|Stage2|human_review" +} +``` + +긴 `description`은 출력하지 않는다. + +### 6.8 domain payload 보호 목록 + +| Domain | 보호 field | +|---|---| +| B1 | `money_claim_details`, `payment_history`, `demand_or_notice_hints`, `admission_hints`, `successor_liability_hints`, `successor_link_uncertainty` | +| B2 | `secured_debt_chain`, `payment_allocation`, `registry_rows`, `fractional_effect_events`, `mortgage_extinguishment_hints`, `auction_steps`, `dividend_distribution_hints`, `derivative_invalidity_hints` | +| B3 | `use_gain_facts`, `valuation_object`, `valuation_rows`, `valuation_matrix`, `possession_state`, `delivery_or_possession_start_hints`, `unregistered_building_hints`, `liability_overlap_hints` | +| B4 | `preserved_claim_bundle`, `fraudulent_transfer_facts`, `encumbrance_timeline`, `valuation_matrix`, `balance_sheet`, `remedy_mode_hints`, `value_compensation_cap_hints`, `defense_rebuttal` | +| B5 | `succession_resolution`, `general_succession_status`, `asset_specific_succession_resolution`, `notice_lifecycle`, `lien_state`, `possession_state`, `asset_alias`, `defense_rebuttal`, `simultaneous_performance_hints`, `setoff_or_payment_defense_hints` | + +특히 B1 agreed rate는 source literal만 사용하고, B5 `received_date`를 창작하지 않으며, `legal_sender_after_resolution.party`를 Stage 1에서 확정하지 않는다. possession facts와 legal-cause assertions를 분리하고 lien id를 결정적으로 부여한다. + +## 7. R0 작업 명세 + +### 7.1 task header + +```yaml +- task_name: Task_C_BO_R0_seed_reducer_exception_planner + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 +``` + +### 7.2 이관 source + +R0는 다음 기존 code의 책임을 흡수한다. + +```text +Task_C_BO_Stage_B_worker_output_publisher +Task_C_BO_Stage_B_domain_join +Task_C_BO_PostB_1_seed_ledger_compiler +``` + +중복 parser/MCP/session helper는 1벌로 통합하되 다음 검사를 모두 보존한다. + +- worker JSON parse와 domain/schema/status 검사 +- source/slice guard와 candidate ref 검사 +- seed artifact write +- global candidate/source index +- review queue routing +- forbidden key 제거 감사 +- source membership 검사 +- deterministic sort key +- duplicate, near-duplicate, schema risk, link ambiguity 검사 + +### 7.3 입력 + +template input: + +```text +5개 worker `.json_output` +``` + +localdocs input: + +```text +stage1_tmp/task_c_bo/stage_a_context.json +stage1_tmp/task_c_bo/source_universe_manifest.json +``` + +### 7.4 실행 순서 + +1. A0 manifest와 stage context를 읽고 run fingerprint를 검사한다. +2. 5개 worker output을 wrapper-only salvage 후 strict parse한다. +3. delta schema, domain id, status, fingerprint, slice digest를 검사한다. +4. candidate ref의 prefix, sequence, uniqueness를 검사한다. +5. 모든 source ref가 global 및 해당 slice universe의 subset인지 검사한다. +6. sparse candidate를 domain별 canonical seed schema로 확장한다. +7. 5개 seed artifact를 현재 경로에 쓴다. +8. global seed ledger와 source index를 만든다. +9. review queue를 compact handoff로 정규화한다. +10. deterministic decision을 적용한다. +11. non-deferrable exception만 micro-pack으로 만든다. +12. full artifact를 쓴 뒤 reread 검증한다. +13. stdout에는 compact manifest와 `dynamic_fanout`만 출력한다. + +### 7.5 schema expander + +R0는 v2의 각 worker ``에서 복제한 domain별 `DOMAIN_PAYLOAD_DEFAULTS`를 가진다. + +deep merge 규칙: + +1. dict는 key별 재귀 merge한다. +2. list는 worker가 제공하면 그대로 보존한다. +3. 누락 optional key만 default로 채운다. +4. `false`, `0`, 명시적 null을 truthiness 검사로 덮어쓰지 않는다. +5. schema에 없는 key는 audit에 기록하고 BLOCK 또는 명시된 extension 정책으로 처리한다. + +R0가 복원할 공통 seed field: + +```text +Reason = null +PriorAct = null +ReasonRefs = [] +Legal_Keywords = [] if omitted +core_field_base = canonical full key set +amount = canonical seed amount object or null +downstream_seed_refs = canonical full key set with null defaults +extensions.domain_payload = domain default deep-merged with delta +transport_metadata = A0 manifest에서 Python이 구성 +transport_candidate_refs = provenance에서 Python이 구성 +``` + +### 7.6 seed artifact 호환성 + +R0는 다음 기존 경로를 유지한다. + +```text +stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B1_Money_Successor.json +stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B2_Secured_Registry.json +stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B3_Land_Valuation.json +stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B4_Actio.json +stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B5_Succession_Notice_Lien_Asset_Defense.json +``` + +artifact root는 `stage_b_domain_bo_seed_output`, schema는 `task_c_bo_stage_b_domain_bo_seed.v2`를 유지한다. + +### 7.7 deterministic exception policy + +| exception | R0 처리 | R1 | +|---|---|---| +| exact duplicate | substantive equality와 무손실 provenance union일 때 merge | 없음 | +| near duplicate | KEEP_SEPARATE + cluster id + review | 없음 | +| meeting-only | candidate 보존 + evidence-gap warning | 없음 | +| optional prior link 불명 | null + review | 없음 | +| Stage 2 법리 판단 | `legal_theory_required` | 없음 | +| source universe 밖 ref | BLOCK | 없음 | +| 필수 type/enum 오류 | BLOCK | 없음 | +| wrapper-only JSON 오류 | deterministic salvage | 없음 | +| 복수 source-backed 값 중 final 하나가 필수 | compact pack | 있음 | +| merge/split/drop 없이는 finalization 불가 | compact pack | 있음 | + +### 7.8 non-deferrable 조건 + +다음을 모두 만족할 때만 R1 pack을 만든다. + +1. F0가 진행하려면 하나의 값 또는 처리를 선택해야 한다. +2. 둘 이상의 source-backed candidate/value가 존재한다. +3. 둘 다 보존하거나 Stage 2로 defer할 수 없다. +4. compact payload만으로 판정할 수 있다. + +### 7.9 micro-pack budget + +```text +max candidates per pack: 8 +max exception items per pack: 4 +max excerpt characters per pack: 1500 +max serialized UTF-8 bytes per pack: 20000 +max pack count: 8 +``` + +상한 초과 시 exception type으로 분할한다. 8개를 넘는 나머지는 full-ledger fallback 없이 `BUDGET_OVERFLOW_BLOCK_REVIEW`로 보존한다. + +### 7.10 fan-out descriptor + +```json +{ + "pack_id": "P2-PACK-001", + "exception_type": "field_conflict|link_ambiguity|canonicalization_conflict", + "exception_ids": ["P2-EXC-001"], + "items": [], + "output_path": "quality_gates/stage1_part2_adjudication_parts/PACK-001.json" +} +``` + +각 compact item: + +```json +{ + "exception_id": "P2-EXC-001", + "exception_type": "field_conflict", + "candidate_refs": [], + "conflicting_field": "ActionType", + "allowed_values": [], + "compact_candidates": [ + { + "candidate_ref": "B2:001", + "source_domain": "B2_Secured_Registry", + "relevant_fields": {}, + "source_refs": { + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [] + } + } + ], + "allowed_decisions": [] +} +``` + +full extensions, Stage A, evidence map 또는 unrelated candidate를 넣지 않는다. + +### 7.11 seed ledger artifact + +경로: `stage1_tmp/task_c_bo/postb_seed_ledger.json` + +```json +{ + "postb_seed_ledger": { + "schema_version": "task_c_bo_postb_seed_ledger.v2", + "status": "READY|READY_WITH_REVIEW|BLOCKED|FAILED", + "run_fingerprint": "sha256", + "source_universe": {}, + "ledger_candidates": [], + "candidate_index": {}, + "domain_review_queue": [], + "deterministic_decisions": [], + "exception_summary": {}, + "audit_trace": {}, + "conservation": {} + } +} +``` + +full ledger는 artifact로만 저장한다. R0 stdout에 `ledger_candidates`를 넣지 않는다. + +### 7.12 review handoff + +경로: `quality_gates/stage1_part2_review_handoff.json` + +```json +{ + "schema_version": "stage1_part2_review_handoff.v1", + "status": "READY|READY_WITH_REVIEW|BLOCKED", + "run_fingerprint": "sha256", + "review_count": 0, + "items": [] +} +``` + +이번 개정에서는 이 파일을 Stage 2 외부 handoff에 강제로 추가하지 않는다. + +### 7.13 R0 stdout + +```json +{ + "r0_manifest": { + "schema_version": "task_c_bo_r0_manifest.v1", + "status": "READY|READY_WITH_REVIEW|BLOCKED|FAILED", + "run_fingerprint": "sha256", + "seed_ledger_path": "stage1_tmp/task_c_bo/postb_seed_ledger.json", + "review_handoff_path": "quality_gates/stage1_part2_review_handoff.json", + "exception_pack_path": "quality_gates/stage1_part2_exception_pack.json", + "candidate_count": 0, + "review_count": 0, + "pack_count": 0 + }, + "dynamic_fanout": [] +} +``` + +## 8. R1 조건부 adjudicator 명세 + +### 8.1 task header + +```yaml +- task_name: Task_C_BO_R1_exception_adjudicator_* + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: medium + llm_verbosity: low + max_iterations: 2 + max_concurrency: 2 + cache_control: + mode: auto + ttl: 1h + use_tools: + - localdocs +``` + +### 8.2 입력 경계 + +R1 prompt에는 `{{item.pack_id}}`, `{{item.exception_type}}`, `{{item.exception_ids}}`, `{{item.items}}`, `{{item.output_path}}`만 넣는다. + +R1은 다음을 읽지 않는다. + +```text +client_meeting.md +evidence_indexed.json +evidence_event_candidates.json +stage_a_context.json +source_universe_manifest.json +postb_seed_ledger.json +BO.json +domain seed 5개 전체 +``` + +### 8.3 허용 decision + +```text +KEEP_SEPARATE +MERGE_PROVENANCE_UNION +SPLIT +DROP_UNSUPPORTED +SELECT_VALUE +SELECT_LINK +NO_LINK +BLOCK_REVIEW +``` + +pack의 `allowed_decisions` subset만 사용할 수 있다. + +### 8.4 안전 규칙 + +1. pack 밖 exception id, candidate ref, source ref를 만들지 않는다. +2. `SELECT_VALUE`는 `allowed_values`에 있는 값만 선택한다. +3. compact 정보가 부족하면 `BLOCK_REVIEW`를 사용한다. +4. 새 사실, 날짜, 금액, 당사자, BO ID 또는 법률 결론을 생성하지 않는다. +5. chain-of-thought를 출력하지 않는다. + +### 8.5 output schema + +```json +{ + "schema_version": "stage1_part2_exception_adjudication_part.v1", + "pack_id": "P2-PACK-001", + "status": "READY|READY_WITH_BLOCK_REVIEW|FAILED", + "decisions": [ + { + "exception_id": "P2-EXC-001", + "exception_type": "field_conflict", + "decision": "SELECT_VALUE|BLOCK_REVIEW", + "affected_candidate_refs": [], + "canonical_candidate_ref": null, + "selected_fields": {}, + "selected_link_refs": [], + "source_refs": { + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [] + }, + "concise_basis": "240 characters or fewer" + } + ] +} +``` + +R1은 `write_file`로 assigned output path 하나만 쓰고 다른 read/write를 하지 않는다. write 후 응답은 `{{item.pack_id}} adjudication written`만 출력한다. + +### 8.6 0-instance 경로 + +R0가 `dynamic_fanout: []`를 출력하면 R1 인스턴스는 0개다. empty pack 확인용 LLM 호출을 만들지 않는다. + +## 9. F0 작업 명세 + +### 9.1 task header + +```yaml +- task_name: Task_C_BO_F0_final_bo_gate_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 +``` + +### 9.2 이관 source + +F0는 `Task_C_BO_PostB_3_final_bo_compiler`와 `Task_C_BO_PostB_4_final_gate_and_writer`를 흡수한다. compile logic과 pre/post-write gate를 한 task에서 순차 실행한다. + +### 9.3 입력 + +```text +stage1_tmp/task_c_bo/stage_a_context.json +stage1_tmp/task_c_bo/source_universe_manifest.json +stage1_tmp/task_c_bo/postb_seed_ledger.json +quality_gates/stage1_part2_exception_pack.json +quality_gates/stage1_part2_adjudication_parts/PACK-*.json +``` + +R0 compact stdout은 `r"""{{prev.Task_C_BO_R0_seed_reducer_exception_planner}}"""`로 받아 expected pack을 확인한다. + +### 9.4 실행 순서 + +1. A0/R0 fingerprint와 digest를 대조한다. +2. R0 status를 확인한다. +3. expected R1 pack과 decision file을 1:1 대조한다. +4. decision의 exception/candidate/source subset을 검사한다. +5. missing, duplicate, foreign decision은 BLOCK한다. +6. deterministic decision과 valid R1만 ledger에 적용한다. +7. candidate conservation을 검사한다. +8. current deterministic sort와 `bh1..bhN`을 적용한다. +9. current Evidence/EvidenceTitles/provenance를 구성한다. +10. canonical BO schema를 복원한다. +11. pre-write schema/reference gate를 실행한다. +12. 통과 시에만 `BO.json`을 한 번 쓴다. +13. reread하여 JSON, count, ID, source, digest를 검사한다. +14. compiled bundle과 gate artifact를 쓴다. +15. compact write report만 stdout에 출력한다. + +### 9.5 candidate conservation + +```text +모든 input candidate_ref + = final BO에 반영된 candidate_ref + + 무손실 merge에 흡수된 candidate_ref + + BLOCK/blocked-review candidate_ref +``` + +drop은 R1의 `DROP_UNSUPPORTED`가 있고 source-backed support가 없을 때만 허용한다. + +### 9.6 final BO schema + +각 BO는 다음 top-level key를 유지한다. + +```text +BO_ID, id, BOType, ActionType, JuristicAct, Action, Reason, +PriorAct, ReasonRefs, Legal_Keywords, core_field_base, amount, +EvidenceTitles, Evidence, source_evidence_indexes, provenance, +downstream_seed_refs, extensions +``` + +`core_field_base` exact key: + +```text +Performer, PerformerType, Action_proposal, Subject, Object, +BehaviorTime, TimeText, TimePrecision, StatementType, Perspective +``` + +`downstream_seed_refs` exact key: + +```text +claim_group_seed_refs_proposed +canonical_theory_graph_seed_ref_proposed +legal_effect_structure_seed_ref_proposed +``` + +Evidence item exact key: + +```text +evidence_index, source_title, priority_class, relevant_content, +authentication_status, corroboration, selection_basis +``` + +### 9.7 Reason/PriorAct 정책 + +- 명시적 source-backed prior link가 없으면 `PriorAct = null`, `ReasonRefs = []`다. +- Stage 1이 법적 인과관계를 창작하지 않는다. +- `Reason`은 non-empty fixed template 또는 code 기반 문구다. +- generic prose를 LLM으로 생성하지 않는다. + +### 9.8 gate artifact + +경로: `quality_gates/stage1_part2_bo_gate.json` + +```json +{ + "schema_version": "stage1_part2_bo_gate.v1", + "status": "PASS|BLOCKED|FAILED", + "run_fingerprint": "sha256", + "bo_count": 0, + "candidate_conservation": {}, + "source_membership": {}, + "adjudication_coverage": {}, + "schema_gate": {}, + "post_write": { + "file_exists": true, + "json_parse_pass": true, + "sha256": "sha256" + }, + "findings": [] +} +``` + +F0 gate가 PASS가 아니면 S0는 signal을 쓰지 않는다. + +## 10. S0 작업 명세 + +### 10.1 task header + +```yaml +- task_name: Task_C_BO_S0_signal_bundle_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 +``` + +### 10.2 이관 source + +S0는 기존 actio, liability, legal-effect signal writer의 projection과 validator를 흡수한다. 중복 MCP/session/parser/BO read helper는 1벌로 통합하고 각 signal의 enum, projection, status, validator는 유지한다. + +### 10.3 입력 + +```text +BO.json +quality_gates/stage1_part2_bo_gate.json +stage1_tmp/task_c_bo/source_universe_manifest.json +stage1_tmp/task_c_bo/postb_seed_ledger.json +stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B4_Actio.json +``` + +BO와 각 보조 artifact는 한 번만 읽는다. + +### 10.4 실행 순서 + +1. F0 gate PASS, fingerprint, BO digest를 확인한다. +2. BO를 한 번 읽는다. +3. 세 signal payload를 메모리에서 각각 생성·검증한다. +4. 모두 valid일 때 write phase를 시작한다. +5. 기존 파일 경로에 한 번씩 쓴다. +6. 세 파일을 reread하여 schema, count, related BO subset을 검사한다. +7. compact bundle report를 stdout에 출력한다. + +### 10.5 외부 signal schema + +`actio_case_signals.json`: + +```text +top-level: schema_version, status, actio_case_signals +item: signal_id, actio_scope, suspicion_level, suspicion_reasons, +related_bo_ids, related_evidence_indexes, target_property_candidates, +preserved_claim_candidates, actio_role_hints +``` + +`case_liability_signals.json`: + +```text +top-level: schema_version, status, case_liability_signals +item: liability_candidate_type, related_bo_ids, related_evidence_indexes, +party_roles, claim_group_id, liability_group_id, linked_structure_ids +``` + +`legal_effect_signals.json`: + +```text +top-level: schema_version, status, bo_legal_effect_routes +item: bo_id, candidate_structure_types, linked_structure_ids, +related_evidence_indexes +``` + +### 10.6 S0 stdout + +```json +{ + "signal_bundle_write_report": { + "schema_version": "stage1_part2_signal_bundle_write_report.v1", + "status": "PASS|FAILED", + "run_fingerprint": "sha256", + "files": { + "actio_case_signals.json": {"count": 0, "status": "READY", "sha256": "sha256"}, + "case_liability_signals.json": {"count": 0, "status": "READY", "sha256": "sha256"}, + "legal_effect_signals.json": {"count": 0, "status": "READY", "sha256": "sha256"} + } + } +} +``` + +## 11. 최종 task_procedure + +최하단 `task_procedure`를 전면 교체한다. + +```yaml +task_procedure: + IN: + nexts: [Task_C_BO_A0_context_domain_slice_compiler] + wait_until: [] + + Task_C_BO_A0_context_domain_slice_compiler: + nexts: + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + wait_until: [IN] + + Task_C_BO_Stage_B_B1_Money_Successor: + nexts: [Task_C_BO_R0_seed_reducer_exception_planner] + wait_until: [Task_C_BO_A0_context_domain_slice_compiler] + Task_C_BO_Stage_B_B2_Secured_Registry: + nexts: [Task_C_BO_R0_seed_reducer_exception_planner] + wait_until: [Task_C_BO_A0_context_domain_slice_compiler] + Task_C_BO_Stage_B_B3_Land_Valuation: + nexts: [Task_C_BO_R0_seed_reducer_exception_planner] + wait_until: [Task_C_BO_A0_context_domain_slice_compiler] + Task_C_BO_Stage_B_B4_Actio: + nexts: [Task_C_BO_R0_seed_reducer_exception_planner] + wait_until: [Task_C_BO_A0_context_domain_slice_compiler] + Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense: + nexts: [Task_C_BO_R0_seed_reducer_exception_planner] + wait_until: [Task_C_BO_A0_context_domain_slice_compiler] + + Task_C_BO_R0_seed_reducer_exception_planner: + nexts: + - Task_C_BO_R1_exception_adjudicator_* + - Task_C_BO_F0_final_bo_gate_writer + wait_until: + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + + Task_C_BO_R1_exception_adjudicator_*: + nexts: [Task_C_BO_F0_final_bo_gate_writer] + wait_until: [Task_C_BO_R0_seed_reducer_exception_planner] + + Task_C_BO_F0_final_bo_gate_writer: + nexts: [Task_C_BO_S0_signal_bundle_writer] + wait_until: + - Task_C_BO_R0_seed_reducer_exception_planner + - "all Task_C_BO_R1_exception_adjudicator_*" + + Task_C_BO_S0_signal_bundle_writer: + nexts: [OUT] + wait_until: [Task_C_BO_F0_final_bo_gate_writer] + + OUT: + nexts: [] + wait_until: [Task_C_BO_S0_signal_bundle_writer] +``` + +`prevs`와 `nexts`는 기존 Stage 구조에 맞춰 유지한다. + +## 12. failure와 status 전파 + +### 12.1 A0/worker/R0 + +- A0 input/freshness 실패 시 worker가 stale slice를 사용하지 못하게 한다. +- worker 하나라도 FAILED/malformed/wrong domain이면 R0 BLOCK이다. +- R0가 READY/READY_WITH_REVIEW가 아니면 F0는 BO를 쓰지 않는다. +- 다른 4개 worker 결과만으로 BO를 만들지 않는다. + +### 12.2 R1/F0 + +- expected R1 output 누락, schema 오류, pack 밖 ref는 F0 BLOCK이다. +- 미판정 exception을 PASS로 간주하지 않는다. +- pre-write 실패 시 `BO.json`을 덮어쓰지 않는다. +- post-write reread 실패 시 gate는 FAILED다. + +### 12.3 S0 + +- 같은 fingerprint의 F0 PASS gate가 없으면 signal을 쓰지 않는다. +- 세 payload를 모두 validate하기 전에 write하지 않는다. +- 하나라도 post-write validation 실패 시 bundle은 FAILED다. + +## 13. 구현 순서 + +### Phase 0. 원본 보존 + +1. v2를 `Stage_1_Part_2_Codex_v3.yml`로 복제한다. +2. 복제 직후 source/target SHA-256이 같은지 기록한다. +3. v2 task 20개와 writer path를 inventory한다. +4. Part 3/4 BO/signal consumer key를 다시 검색한다. +5. Part 1 canonical prefix hash를 기록한다. + +### Phase 1. A0 + +1. Stage A code를 A0로 이관한다. +2. B0 config/helper를 table-driven router로 통합한다. +3. fixture에서 기존 B0와 새 A0 selected source ID를 비교한다. +4. artifact read/write/reread를 검증한다. + +### Phase 2. worker + +1. common prefix를 canonical block으로 교체한다. +2. assigned localdocs read policy를 적용한다. +3. output을 sparse delta로 교체한다. +4. domain 법률 규칙과 protected field를 유지한다. +5. verbosity/model을 적용하고 rollback 준비를 한다. + +### Phase 3. R0/R1 + +1. publisher, join, ledger 책임을 R0로 이관한다. +2. schema expander와 deterministic policy를 추가한다. +3. full artifact와 compact stdout 경계를 검사한다. +4. R1 wildcard를 추가하고 0/1/overflow pack을 검증한다. + +### Phase 4. F0/S0/DAG + +1. PostB_3/4를 F0로 통합한다. +2. signal writer 3개를 S0로 통합한다. +3. old task 정의와 참조를 제거한다. +4. `task_procedure`를 교체한다. + +### Phase 5. 검증 + +1. YAML parse, Python compile, prefix hash, writer audit를 실행한다. +2. fixture 전체를 실행한다. +3. B1/B3 A/B gate를 적용한다. +4. Part 3 LES0, Part 4 FL0 smoke test를 실행한다. +5. runtime/cost를 3회 중앙값으로 비교한다. + +## 14. 정적 검증 + +### 14.1 YAML과 task + +- strict YAML parse 성공 +- task definition 정확히 10개 +- unreachable task 0 +- old task 정의와 DAG 참조 0 +- 새 task 5개와 기존 worker 5개 각각 정의 1건 + +### 14.2 old task 0건 목록 + +```text +Task_C_BO_Stage_A_input_normalization +Task_C_BO_Stage_B0_input_money_successor +Task_C_BO_Stage_B0_input_secured_registry +Task_C_BO_Stage_B0_input_land_valuation +Task_C_BO_Stage_B0_input_actio +Task_C_BO_Stage_B0_input_succession_notice_lien_asset_defense +Task_C_BO_Stage_B_worker_output_publisher +Task_C_BO_Stage_B_domain_join +Task_C_BO_PostB_1_seed_ledger_compiler +Task_C_BO_PostB_2_single_exception_adjudicator +Task_C_BO_PostB_3_final_bo_compiler +Task_C_BO_PostB_4_final_gate_and_writer +Task_C_BO_Middle_Input_actio_case_signals +Task_C_BO_Middle_Input_case_liability_signals +Task_C_BO_Middle_Input_legal_effect_signals +``` + +### 14.3 Python compile/SKILL 검사 + +YAML parser로 A0/R0/F0/S0의 `parameters.code`를 추출해 `python -m py_compile`한다. + +- localdocs full boilerplate 포함 +- user/workspace hash 포함 +- raw triple-string template 사용 +- raw_decode fallback 포함 +- wrapper-only fence salvage 포함 +- wildcard/dynamic_fanout 규약 일치 +- `all Task_C_BO_R1_exception_adjudicator_*` barrier 사용 +- R1 `max_concurrency` 지정 + +### 14.4 common prefix + +- block count 6 +- bytes 각각 3977 +- SHA-256 각각 `5feea0882eac0a1431b377fa20a396e7c009fe9a441da6dca7050e3a24618c50` +- unique hash count 1 + +### 14.5 writer/read diet + +writer 수: + +```text +BO.json: 1 +actio_case_signals.json: 1 +case_liability_signals.json: 1 +legal_effect_signals.json: 1 +``` + +R1 input에 `client_meeting.md`, `evidence_indexed.json`, `evidence_event_candidates.json`, `postb_seed_ledger.json`, `BO.json`이 있으면 실패다. R0 stdout에 `ledger_candidates`가 있어도 실패다. + +### 14.6 placeholder와 secret + +- `TODO`, `TBD`, `omitted`, `same as`, placeholder 0건 +- 새 credential 0건 +- 기존 credential의 로그/보고서 출력 0건 + +## 15. fixture와 A/B 회귀 테스트 + +### 15.1 7/10 기준값 + +| 검사항목 | 기준값 | +|---|---:| +| evidence indexes | 30 | +| event candidates | 71 | +| B1/B2/B3/B4/B5 seed | 13 / 7 / 6 / 5 / 19 | +| 전체 seed | 50 | +| domain review | 13 | +| BO | 50 | +| actio/liability/legal-effect signals | 5 / 50 / 50 | +| normal-path R1 calls | 0 | + +1차 release에서는 위 count를 유지한다. 이후 사건은 candidate conservation으로 판단한다. + +### 15.2 source와 법률 field + +1. 모든 BO evidence/event/meeting ref는 Part 1 universe의 subset이어야 한다. +2. meeting-only BO 2건을 삭제하지 않는다. +3. candidate 50개가 final/absorbed/blocked 중 하나로 추적돼야 한다. +4. B1 agreed rate와 refs를 보존한다. +5. B2 registry/fraction/mortgage/payment chain을 보존한다. +6. B3 possession/use-gain/valuation을 보존한다. +7. B4 preserved claim/transfer/encumbrance/valuation/balance sheet/cap/defense를 보존한다. +8. B5 succession/notice/receipt/lien/possession/asset alias를 보존한다. + +### 15.3 review + +기존 13개 review의 description 문자열은 같을 필요가 없다. issue type, severity 이상의 보수성, candidate/source refs, downstream owner는 같아야 한다. + +### 15.4 B1/B3 A/B gate + +동일 A0 slice로 control Pro와 target Flash를 비교한다. + +통과 조건: + +- candidate count/source ref 집합 동등 +- normalized BOType/ActionType 동등 +- protected named field 동등 +- review issue/source 동등 +- unsupported hallucination 0 +- 대한민국 민사소송 실무 관점의 lawyer spot review 통과 + +실패한 worker만 Pro로 되돌린다. 검증 기준을 낮추지 않는다. + +### 15.5 결정론 + +같은 worker delta 5개로 R0/F0/S0를 두 번 실행한다. `BO.json`과 signal 3개는 바이트 동일해야 한다. gate telemetry의 실행 시각은 달라도 된다. + +## 16. fault injection + +| 주입 | 기대 결과 | +|---|---| +| worker JSON code fence | R0 salvage, 재호출 없음 | +| worker prose tail | R0 BLOCK | +| candidate ref gap/duplicate | R0 BLOCK | +| source universe 밖 ref | R0 BLOCK | +| exact duplicate | 무손실 union 또는 BLOCK_REVIEW | +| near duplicate | KEEP_SEPARATE + review, R1 0회 | +| meeting-only | 보존 + warning, R1 0회 | +| non-deferrable field conflict | compact R1 pack 1개 | +| empty exception | R1 instance 0개 | +| oversized exception | 분할 또는 overflow BLOCK_REVIEW | +| R1 foreign id/source | F0 BLOCK | +| R1 output 누락 | F0 BLOCK | +| BO schema key 누락 | F0 pre-write BLOCK | +| stale BO | fingerprint mismatch로 S0 BLOCK | + +## 17. downstream smoke test + +다음을 실제 실행하거나 최소 parser 단위로 통과시킨다. + +1. Part 3 `Stage_1_Part_3_v1.yml`의 LES0 input compiler +2. Part 4 `Stage_1_Part_4_v2.yml`의 FL0 deterministic compiler +3. 존재하는 경우 Stage 2 BO/signal schema validator + +external regression은 Part 2 v3에서 수정한다. consumer를 임의 수정하지 않는다. 정말 consumer 개정이 필요한 경우 별도 blocker로 보고한다. + +## 18. 성능 검증 + +task별 elapsed time, input/output bytes, model/reasoning/cache, token, artifact bytes, pack count를 기록한다. 동일 조건 3회 이상 중앙값으로 판정한다. + +| 지표 | v2 | 1차 합격 | target | +|---|---:|---:|---:| +| task definition | 20 | 10 | 10 | +| no-exception runtime task | 20 | 10 이하 | 9 | +| 상시 Pro/high LLM | 6 | 4 이하 | 3 | +| no-exception PostB LLM | 1 | 0 | 0 | +| PostB LLM input | full ledger | 20KB 이하 | 필요한 pack만 | +| runtime | 7분 40초 | 5분 30초 이하 | 5분 이하 | +| cost | $4.28 | $3.00 이하 | $2.50 이하 | + +품질 불변식 하나라도 위반하면 성능을 달성해도 실패다. + +## 19. 완료 정의 + +- [ ] `Stage_1_Part_2_Codex_v3.yml`이 생성되었다. +- [ ] v2 원본은 변경되지 않았다. +- [ ] Agent version은 v.3이다. +- [ ] stage-level 미사용 model이 제거되었다. +- [ ] task definition은 정확히 10개다. +- [ ] old task명/참조가 0건이다. +- [ ] no-exception runtime task는 9개다. +- [ ] R1은 compact dynamic exception만 읽는다. +- [ ] empty exception에서 R1 호출은 0회다. +- [ ] Python 4개가 SKILL.md와 py_compile을 통과한다. +- [ ] common prefix 6개가 canonical hash와 같다. +- [ ] final writer 권한이 단일하다. +- [ ] fixture seed/BO/signal count가 보존된다. +- [ ] candidate/source conservation을 통과한다. +- [ ] protected legal field와 review issue/source가 보존된다. +- [ ] Part 3 LES0과 Part 4 FL0 smoke test가 통과한다. +- [ ] runtime/cost 3회 중앙값을 기록했다. + +하나라도 충족하지 못하면 완료라고 표현하지 않는다. 실패 condition, task, artifact와 보수적 fallback을 명시한다. + +## 20. 구현자 최종 보고 형식 + +1. 생성한 YAML 경로 +2. source YAML 무변경 확인 +3. 삭제·유지·추가 task 목록 +4. task definition/runtime task 수 +5. common prefix count/hash +6. Python SKILL/compile 결과 +7. writer audit 결과 +8. fixture count/conservation 결과 +9. B1/B3 A/B 결과와 최종 model +10. Part 3/4 smoke 결과 +11. 3회 runtime/cost 중앙값 +12. 잔여 blocker 또는 fallback + +분석만 했거나 fixture/runtime 검증을 하지 않았다면 그 상태를 정확히 구분한다. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_2_Inefficiency_Report_Claude_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_2_Inefficiency_Report_Claude_v1.md new file mode 100644 index 00000000..13428061 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_2_Inefficiency_Report_Claude_v1.md @@ -0,0 +1,201 @@ +# Part 2 비효율 분석 및 개정 전략 보고서 (Claude v1) + +- 대상: `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_2_v2.yml` (7,462행, 17개 task) — Task_C_BO 파이프라인 +- 근거 자료: v.6 YAML 4종(Part 1은 `Stage_1_Part_1_Claude_v3.yml` 기준) 전체 + `v.6/Results_July_10_3_15pm/` 실측(BO.json, 도메인 seed 5종, signal 3종) + Part 3/4 하류 소비 지점 grep 검증 +- 실행 실적: Part 2 = 7분 40초 / **$4.28 (4개 Part 중 최고 비용, Stage 1 전체 $6.95의 62%)** + +--- + +## 0. 결론 요약 + +1. **Part 2는 Part 1(v2)과 달리 이미 "결정적 컴파일 + 국소 LLM" 원칙으로 설계되어 있다.** 17개 task 중 11개가 code-executor이고, 병합·ID 확정·게이트·signal 생성은 전부 Python이다. 따라서 Part 1식 "LLM writer 전면 치환" 여지는 없다. 비효율은 다른 곳에 있다: **① 6개 LLM 호출 전부가 `gemini-3.1-pro-preview` + `reasoning: high`로 일괄 상향**되어 있고, ② 그중 1개(PostB_2 예외 판정기)는 **필요 입력의 수십 배(전체 ledger)를 주입받으면서 항상 실행**되며, ③ 워커 5개의 출력 206,681B 중 상당분이 **null/empty 스캐폴드(leaf의 54%)와 이중 배선 필드**라는 점이다. +2. **최대 단일 비효율은 PostB_2다.** 프롬프트 스스로 "Full upstream object is provided only because some runtimes do not support stable nested field access. You must read only `postb_seed_ledger.exception_pack`"이라고 적어 놓고, 50개 후보 전체 payload가 담긴 PostB_1 stdout(추정 200KB+)을 pro-preview high 호출에 통째로 주입한다. 이것은 Part 1 개정에서 제거한 "full fan-in을 LLM 컨텍스트로 수행" 문제의 재현이며, 해법도 동일하다 — **Part 1 v3의 GB 패턴(compact exception pack 파일 + flash-lite + 빈 팩 즉시 종료) 이식**. 이번 7/10 실행에서 seed 50건 → BO 50건으로 **merge/drop 0건**이었다는 실측은, 이 사건에서 pro-preview 판정기가 실질 기여 없이 최대 비용 경로로 실행되었음을 보여준다. +3. **워커 5개(B1~B5)는 Part 2의 정당한 비용 중심이다.** 도메인별 BO seed 생성은 Stage 1에서 가장 법률 추론 밀도가 높은 작업(금전채권·상호속용, 담보·등기, 사해행위, 상속·유치권)이므로 모델 일괄 하향은 권고하지 않는다. 대신 **출력 계약 슬리밍(null 생략, 이중 배선 제거)과 verbosity 하향으로 출력 토큰을 줄이고**, 추출 성격이 강한 B2/B3에 한해 fixture A/B로 하향을 검증한다. +4. **wall-clock의 숨은 소비자는 직렬 code task 12개다.** publisher→domain_join→PostB_1→…→Middle×3이 각각 컨테이너 부팅 + MCP 왕복을 반복하고, 같은 50개 후보를 5회 재직렬화하며, publisher가 쓴 파일을 domain_join이 즉시 재독해한다. 통합으로 17→10 task로 줄일 수 있다. +5. 예상 효과(추정 명시): LLM 입력 약 200KB 제거(PostB_2), LLM 출력 30~45% 감소(워커 슬리밍), 비용 $4.28 → $2.5~3.0 수준, 런타임 7:40 → 5분 내외. 하류 계약(BO.json, signal 3종의 경로·스키마)은 완전 보존. + +--- + +## 1. 실측 데이터 (Results_July_10_3_15pm 기준) + +### 1.1 task 구성과 모델 + +| 구간 | task | 유형 | 모델 설정 | +|---|---|---|---| +| Stage A | `Task_C_BO_Stage_A_input_normalization` | code | — (stdout으로 stage_a_context 발행, 파일 미작성) | +| B0 라우터 ×5 | `Stage_B0_input_{money_successor, secured_registry, land_valuation, actio, succession_notice_lien_asset_defense}` | code | — (Stage A stdout을 도메인 slice로 절단) | +| **Stage B 워커 ×5** | `Stage_B_B1~B5` | **LLM** | **gemini-3.1-pro-preview, reasoning=high**, verbosity=medium, max_iterations=2, use_tools=[], cache ttl 1h | +| 게시·조인 | `worker_output_publisher`, `domain_join` | code | — | +| PostB | `PostB_1_seed_ledger_compiler` | code | — | +| **예외 판정** | `PostB_2_single_exception_adjudicator` | **LLM** | **gemini-3.1-pro-preview, reasoning=high**, verbosity=low, max_iterations=1 | +| 최종 | `PostB_3_final_bo_compiler`, `PostB_4_final_gate_and_writer` | code | — (PostB_4가 BO.json 단일 writer + 게이트) | +| Middle ×3 | `Middle_Input_{actio_case, case_liability, legal_effect}_signals` | code | — (signal 3종 파일 writer) | + +### 1.2 산출물 실측 + +| 산출물 | 크기 | 실측 내용 | +|---|---|---| +| 도메인 seed 5종 (`stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/`) | 합계 **206,681 B** | B1: 13건/49,793B · B2: 7건/38,901B · B3: 6건/29,322B · B4: 5건/25,747B · **B5: 19건/62,918B** — 5개 전부 `READY_WITH_REVIEW`, review 합계 13건 | +| `BO.json` | 175,126 B | **50 items (seed 50건 → BO 50건, merge/drop 0)**, BO_ID 유일성 통과 | +| `actio_case_signals.json` | 9,990 B | 5 items | +| `case_liability_signals.json` | 15,565 B | 50 items | +| `legal_effect_signals.json` | 19,093 B | 50 items | + +### 1.3 출력물 내부의 중복·공백 실측 (결정론적 대조) + +1. seed 후보들의 `extensions.domain_payload` leaf 값의 **54%가 null/[]/{}/빈 문자열**(BO.json 기준 782/1,451) — 출력 스키마가 도메인 공통 30여 키를 전 후보에 강제한 결과이며, 워커가 pro-preview 출력 토큰으로 이 스캐폴드를 매번 재타이핑한다. +2. `transport_candidate_refs[]`(파일당 1.0~3.6KB)는 각 후보의 `provenance`와 동일한 source id 3종을 이중 배선한다. +3. `transport_metadata.slice_guard`(파일당 1.3~3.3KB)는 Stage A digest·universe를 워커 출력에 재에코한다 — 가드 검증은 publisher(code)가 수행 가능하므로 LLM 출력에 실릴 필요가 없다. +4. BO.json 내부: `BO_ID == id` (50/50 동일), `EvidenceTitles == Evidence[*].source_title` (49/50 동일), `core_field_base.amount == amount.value` (50/50 동일), `Reason`은 **50건 전부 동일한 보일러플레이트 1종**("source evidence 및 event candidate에 의해 독립적으로 확인되는 BO"). `extensions.domain_payload`가 파일 바이트의 29%를 차지한다. +5. Stage A의 stdout(stage_a_context — meeting/event/evidence map 전체 포함)은 `{{prev.…}}` 템플릿으로 **12개 task에 원문 주입**된다(B0 ×5, publisher, domain_join, PostB_1, PostB_3, PostB_4, Middle ×3). LLM 토큰은 아니지만 転送·재파싱이 12회 반복된다. + +--- + +## 2. 실행 구조와 병목 (DAG 분석) + +```text +Stage_A(code) ──> B0 ×5(code) ══all-to-all══> 워커 B1~B5 ×5(LLM pro/high, 병렬) + │ (.json_output 템플릿) + v + publisher(code, seed 5파일 write) ─> domain_join(code, 방금 쓴 5파일 재독해) + │ + v + PostB_1(code, ledger 전체를 stdout으로) ─> PostB_2(LLM pro/high, ledger 전문 주입) ─> PostB_3(code) ─> PostB_4(code, BO.json write+gate) + │ + Middle ×3(code, 각각 BO.json 재독해) ─> OUT +``` + +- **critical path의 LLM 구간은 2개**: 워커 병렬 구간(최장 워커 = B5, 출력 62.9KB + high 사고 토큰)과 PostB_2. 나머지는 12개의 직렬/준직렬 code task로, 각각 컨테이너 기동과 MCP 세션 초기화를 반복한다. +- **B0→워커 all-to-all 대기**: 각 워커는 자기 slice 하나(`{{prev.Stage_B0_input_<자기도메인>}}`)만 소비하는데 `wait_until`은 B0 5개 전부다. 가장 느린 B0가 모든 워커의 시작을 지연시킨다. +- **write→즉시 re-read 왕복**: publisher가 seed 5파일을 쓰고 domain_join이 같은 파일을 다시 읽는다(감사 목적 명시). 목적은 정당하나 두 task를 합치면 왕복 없이 동일 감사가 가능하다. +- 워커의 입력 채널이 stdout 템플릿(중첩 필드 접근 불가 런타임 제약) 때문에 B0가 5개로 분리된 것은 **정당한 설계**다(비효율 아님). + +--- + +## 3. LLM 6호출의 필요성 판정 + +| 호출 | 실질 작업 | 현재 설정 | 판정 | +|---|---|---|---| +| B1_Money_Successor | 금전채권·변제기·채무승인·상호속용 seed 생성 (13건) | pro-preview/high | **유지** — 청구원인 구조의 법률 추론 실질. 출력 슬리밍만 적용 | +| B2_Secured_Registry | 담보·등기·경매 seed (7건) | pro-preview/high | 유지 기본, **flash 계열 A/B 후보** (등기 row 추출 성격 강함) | +| B3_Land_Valuation | 토지·점유·차임·감정 seed (6건) | pro-preview/high | 유지 기본, **flash 계열 A/B 후보** (추출 성격 최강, 후보 최소) | +| B4_Actio | 사해행위·가액배상 seed (5건) | pro-preview/high | **유지** — 피보전채권·사해성 판단은 최고 난도 | +| B5_Succession_Notice_Lien_Asset_Defense | 상속·통지·유치권·항변 seed (19건) | pro-preview/high | **유지 + 분할 검토** — 후보 19건/출력 62.9KB로 최장 워커. 상속·통지 vs 유치권·항변은 법리적으로 독립이므로 2분할 병렬화 가능 | +| PostB_2 adjudicator | exception pack 항목의 **제한된 선택 판정**(KEEP_SEPARATE/MERGE/SPLIT/DROP/BLOCK_REVIEW, 제공된 값 중 선택만 허용) | pro-preview/high + **ledger 전문 주입** | **강등 + 입력 교체** — bounded selection은 Part 1 v3 GB와 동일 성격. flash-lite/low~medium + pack 전용 입력으로 전환. 이번 실행 실측: 50→50 무변경 | + +모델 판정 원칙은 Part 1 개정과 일관되게 한다: **일괄 하향이 아니라, 법률 추론이 실질인 곳(워커 B1/B4/B5)은 유지하고, 기계적 선택 판정(PostB_2)과 추출 성격 도메인(B2/B3)만 하향 또는 A/B 검증**한다. + +--- + +## 4. 비효율 판정 목록 (F-1 ~ F-9) + +| # | 심각도 | 비효율 | 실측 근거 | +|---|---|---|---| +| **F-1** | **중대** | PostB_2가 필요 입력(exception_pack, 추정 수 KB)의 수십 배인 **PostB_1 stdout 전문(ledger_candidates 50건 full payload 포함, 추정 200KB+)**을 pro-preview high로 읽음. 예외 0건이어도 항상 동일 규모로 실행 | 프롬프트 5091~5092행의 자기 인정 문구, ledger는 seed 206KB의 재직렬화본, 이번 실행 merge/drop 0 | +| **F-2** | **중대** | 워커 출력 206,681B 중 null/empty 스캐폴드(payload leaf 54%) + `transport_candidate_refs` 이중 배선(~10KB) + `slice_guard` 에코(~10KB)를 pro-preview **출력 토큰**으로 재생성 | §1.3-1~3 | +| **F-3** | **중대** | LLM 6호출 전부 pro-preview + reasoning high 일괄 적용 — PostB_2는 bounded selection, B2/B3는 추출 성격 | §3 판정표 | +| F-4 | 중 | 직렬 code task 12개의 기동·MCP 왕복 반복 + 동일 50건 후보의 5회 재직렬화(seed→join→ledger→compiled→BO) | §2 | +| F-5 | 중 | publisher가 쓴 seed 5파일을 domain_join이 즉시 재독해(왕복), Middle ×3가 BO.json(175KB)을 각각 재독해 | §2, 6145/6752/7173행 | +| F-6 | 중 | B0→워커 all-to-all wait (워커는 자기 B0 1개만 필요) | task_procedure 7343~7387행 | +| F-7 | 중 | **PostB_2가 워커 5개와 다른 커스텀 COMMON_CACHE_PREFIX 사용** — 같은 pro-preview 모델인데 프리픽스 캐시 공유 포기 (워커 5개는 바이트 동일, 내용은 Part 1 canonical과 동일하나 들여쓰기 상이) | prefix SHA 실측: 워커 5개 `adbdf43c…` 동일, PostB_2 `7207d2da…` 상이 | +| F-8 | 하 | Stage A stdout이 12개 task에 원문 주입(전송·재파싱 12회) — LLM 비용은 없음 | §1.3-5 | +| F-9 | 하 | BO.json 내부 중복(BO_ID==id, EvidenceTitles==Evidence.source_title 49/50, core_field_base.amount==amount.value 50/50, Reason 동일 문자열 50회) + `OUTPUT_SURFACE_HARD_RULE` 위반 시 publisher strict parser가 pro 재호출(max_iterations 2)을 유발할 수 있는 취약 결합 | §1.3-4 | + +**Part 1과의 대조로 명확해지는 점**: Part 1 v2의 병목이 "LLM이 병합·재작성까지 수행"이었다면, Part 2의 병목은 "**LLM 호출의 입력·출력·모델이 전부 최대치로 설정**"된 것이다. 구조는 이미 올바르므로 개정은 호출의 다이어트에 집중한다. + +--- + +## 5. 삭제·축소 안전성 판정 (하류 연계 근거) + +| 산출물/구조 | 소비자 (grep 실증) | 판정 | +|---|---|---| +| `BO.json` (경로·item 스키마) | Part 3(5개소) · Part 4(3개소) | **불변** — 스키마 키 삭제 금지 | +| `actio_case_signals.json` / `case_liability_signals.json` / `legal_effect_signals.json` | Part 3(8/7/6개소) · Part 4(4/5/4개소) | **불변** | +| `extensions.domain_payload` | Part 3 `_domain_payload()`(304~308행), Part 4 `domain_payload_compact`(312~314행)가 **`.get()` 접근으로 compact 소비** | 필드 유지. 단 **null/empty 키 생략은 안전** (소비자가 키 부재를 기본값 처리) — F-2 슬리밍의 근거 | +| 도메인 5분할 구조, Stage A universe 가드, PostB_4 사전·사후 게이트, seed 파일 감사 체계 | Part 2 내부 품질 장치 | **유지** — 이 장치들 덕분에 Part 2에는 Part 1의 E-019류 소실 결함이 없다 | +| `transport_candidate_refs`, `slice_guard` 에코 | Part 3/4 소비 0건 (publisher/join 내부 검증용) | **LLM 출력에서 제거 가능** — publisher(code)가 `bo_seed_candidates[*].provenance`에서 결정적으로 재구성·검증 | +| BO_ID==id 등 내부 중복 필드 | v.2 BO 스키마 계약 (Stage 2 소비 범위 미확정) | **1차 개정에서는 유지** — Part 1 alias 판정과 동일하게, Stage 2 소비자 grep 확정 전 스키마 변경 금지 | +| PostB_2 task 자체 | 예외 판정 경로 (BLOCK_REVIEW 라우팅 포함) | **삭제 불가, 경량화만** — 예외가 실재하는 사건에서 병합·충돌 판정은 필요하다 | + +--- + +## 6. 개정 전략 (권장안) + +### 6.1 R-1: PostB_2를 Part 1 v3 GB 패턴으로 전환 (F-1, F-3, F-7 해소) + +```text +[현행] PostB_1(code) ──ledger 전문 stdout──> PostB_2(pro/high, 항상 실행) ──> PostB_3 +[개정] PostB_1(code) ──ledger stdout──────────────────────────────────────> PostB_3 + └─ write: stage1_tmp/task_c_bo/postb_exception_pack.json (compact, 예외 항목+발췌만) + │ + v + PostB_2' (flash-lite, reasoning=low~medium, use_tools: [localdocs], preflight_files: pack 1개) + - pack 파일 1개만 read. `has_exceptions == false`면 빈 decisions 저장 후 즉시 종료 + - 출력: stage1_tmp/task_c_bo/postb_adjudication_decisions.json + - PostB_3는 decisions 파일을 read (STAGE_A/LEDGER 템플릿 유지) +``` + +- 근거: PostB_2의 판정은 "제공된 값 중 선택"으로 제한된 bounded selection이며, 이는 Part 1 v3에서 flash-lite/low로 확정한 GB와 동일한 작업 성격이다. 확신 없는 케이스는 이미 `BLOCK_REVIEW` enum으로 인간 검토 라우팅이 존재한다. +- 프리픽스는 워커 5개와 동일한 canonical COMMON_CACHE_PREFIX로 통일한다(F-7). +- 기대 효과: LLM 입력 ~200KB 제거, 예외 0건 사건(이번 실행이 그 사례)에서 사실상 0비용 통과. + +### 6.2 R-2: 워커 출력 계약 슬리밍 (F-2 해소) + +1. **null/empty 생략 계약**: OUTPUT_CONTRACT에 "값이 null/[]/{}인 키는 emit하지 않는다. 하류는 키 부재를 null로 해석한다"를 명시한다(Part 3/4의 `.get()` 소비 실증으로 안전). 스키마 예시는 전체 키를 보여주되 emit 규칙을 분리 기술한다. +2. **`transport_candidate_refs` 제거**: publisher(code)가 `bo_seed_candidates[*].candidate_ref + provenance`에서 결정적으로 재구성한다. +3. **`slice_guard` 에코 축소**: 워커는 `source_stage_a_created_at_utc`와 `input_digests_sha256`의 SHA 1개(요약 digest)만 반향하고, 전체 가드 대조는 publisher가 B0 slice(stdout 보존)와 직접 수행한다. +4. `llm_verbosity: medium → low` (JSON 산출 task에 medium 서술 여지가 불필요). +- 기대 효과: 워커 출력 30~45% 감소(206KB → 약 115~145KB) = pro-preview 출력 토큰·사고 토큰·벽시계 동시 감소. **candidate 실질 내용(법률 추론 결과)은 감소 폭에 포함되지 않는다.** + +### 6.3 R-3: 모델 차등화 A/B (F-3 해소, 품질 가드 포함) + +- PostB_2': flash-lite 확정(R-1). +- B2_Secured_Registry, B3_Land_Valuation: 7/10 fixture의 B0 slice를 고정 입력으로 `gemini-3.1-flash-preview`(또는 flash-lite/high) A/B 실행 → seed 후보의 (a) 후보 수·source ref 집합 동일성, (b) domain_review_queue 실질 동등성, (c) 변호사 육안 대조를 통과하면 하향 확정. **B1/B4/B5는 pro 유지**(법률 추론 실질). +- B5 분할 옵션: `B5a_Succession_Notice`(상속·통지)와 `B5b_Lien_Asset_Defense`(유치권·목적물·항변)로 2분할 — 법리적으로 독립 도메인이므로 결속 훼손 없음. 최장 워커 출력 62.9KB가 절반으로 나뉘어 워커 병렬 구간의 wall-clock 상한이 내려간다. B0 라우터 1개 추가 필요(터무니없는 비용 아님, code task). + +### 6.4 R-4: code task 통합 및 DAG 수정 (F-4, F-5, F-6, F-8 해소) + +| 통합 | 내용 | +|---|---| +| publisher + domain_join + PostB_1 → `Stage_B_join_and_ledger_compiler` 1개 | 워커 5개 stdout을 직접 받아 (a) 가드 검증·seed 5파일 write(감사 아티팩트 유지) (b) join (c) ledger 컴파일 + exception pack 파일 write까지 한 번에. write→re-read 왕복 제거 | +| PostB_3 + PostB_4 → `PostB_final_compile_gate_writer` 1개 | 컴파일과 게이트·write는 연속 결정 작업 | +| Middle ×3 → `Middle_signals_writer` 1개 | BO.json 1회 read로 signal 3파일 write (병렬 이득 없음 — 모두 수초짜리 code) | +| B0→워커 edge 수정 | 각 워커 `wait_until: [자기 B0 1개]`로 축소 | + +- task 수 17 → 10, 직렬 code 구간의 기동·MCP 세션 왕복 약 7회분 제거. Stage A stdout 주입처도 12 → 7로 감소(F-8 부분 해소). + +### 6.5 R-5: publisher 파서 관용화 (F-9 해소) + +- SKILL.md §6.7의 `parse_llm_json` 패턴(코드펜스·잡음 결정적 salvage)을 통합 join task의 seed 파싱에 적용한다. `OUTPUT_SURFACE_HARD_RULE`은 유지하되, 규칙 위반 1회가 pro-preview 전체 재호출(max_iterations 2)로 이어지는 결합을 끊는다 — fence 제거는 기계 작업이지 LLM 재실행 사유가 아니다. + +### 6.6 기대 효과 종합 (추정 명시) + +| 항목 | 현행 | 개정 후 추정 | +|---|---|---| +| LLM 호출 | 6회 전부 pro/high | 정상 경로 5회(pro 워커) + flash-lite 소형 1회(예외 있을 때만 실질 작동) | +| PostB_2 입력 | ledger 전문 ~200KB+ | pack 수 KB (예외 0이면 즉시 종료) | +| 워커 출력 | 206.7KB | 약 115~145KB | +| task 수 / 직렬 code 구간 | 17 / 12개 | 10 / 5개 | +| 비용 | $4.28 | **$2.5~3.0 추정** (B2/B3 하향 확정 시 추가 하락) | +| 런타임 | 7:40 | **5분 내외 추정** (B5 분할 시 워커 구간 추가 단축) | + +--- + +## 7. 이행 순서 및 검증 계획 + +1. **R-1 + R-5 먼저** (구조 변경 없이 최대 효과·최소 위험): PostB_1에 pack 파일 write 추가, PostB_2 교체, join 파서 관용화. fixture 회귀 — BO.json 50 items 바이트 수준 동등(Reason 등 결정적 필드 동일), 예외 0 경로에서 adjudicator 조기 종료 확인. +2. **R-2 + R-4**: 출력 계약 개정 + task 통합 → `Stage_1_Part_2_v3.yml`. fixture 회귀 — seed 후보 수 50±0(내용 동등성은 candidate_ref·source ref 집합으로 판정), BO.json이 Part 3 `LES0`/Part 4 `FL0` 파서를 통과, signal 3종 5/50/50 유지, `domain_payload` null-생략본에 대한 Part 3/4 compact 소비 정상 동작 확인. +3. **R-3 A/B**: 고정 B0 slice로 B2/B3 모델 비교 → 통과 시 하향 반영. B5 분할은 별도 실측 후 결정. +4. 재실행 계측: sub-task별 시간·토큰 telemetry(Part 1 개정과 동일 항목)를 붙여 v.6 대비 비용·런타임을 A/B 기록한다. +5. 회귀 불변 조건: `BO.json`·signal 3종의 파일 경로/envelope/스키마 키, PostB_4 게이트 의미론, `BLOCK_REVIEW` 라우팅, Stage A universe 가드. + +--- + +## 부록 A. 근거 실측 로그 요약 + +- task 17개 중 LLM 6개(전부 gemini-3.1-pro-preview, reasoning=high), code 11개. +- 도메인 seed: B1 13/49,793B · B2 7/38,901B · B3 6/29,322B · B4 5/25,747B · B5 19/62,918B (합 50건/206,681B, 전부 READY_WITH_REVIEW, review 13건). +- BO.json: 50 items/175,126B, BO_ID 유일, seed→BO 50→50 (merge/drop 0), `Reason` 1종 보일러플레이트 ×50, `BO_ID==id` 50/50, `EvidenceTitles==Evidence[*].source_title` 49/50, `core_field_base.amount==amount.value` 50/50, domain_payload 바이트 29%·leaf null/empty 54%(782/1,451). +- COMMON_CACHE_PREFIX: 워커 5개 바이트 동일(sha `adbdf43c…`, stripped 내용은 Part 1 canonical `6e01f72f…`와 동일), PostB_2는 커스텀 프리픽스(sha `7207d2da…`). +- 하류 소비: BO.json → P3:5/P4:3, actio/case_liability/legal_effect signals → P3:8/7/6, P4:4/5/4. domain_payload는 P3 304~308행, P4 312~314행에서 `.get()` compact 소비. +- PostB_2 프롬프트 5091~5092행: "Full upstream object is provided only because some runtimes do not support stable nested field access. You must read only `postb_seed_ledger.exception_pack`." diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_2_Inefficiency_Report_Codex_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_2_Inefficiency_Report_Codex_v1.md new file mode 100644 index 00000000..9d10cd93 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_2_Inefficiency_Report_Codex_v1.md @@ -0,0 +1,688 @@ +# Stage 1 Part 2 비효율성 분석 및 최적화 전략 + +작성일: 2026-07-20 +분석 대상: `Stage_1_Part_2_v2.yml` +실측 fixture: `Results_July_10_3_15pm` + +## 0. 결론 + +Part 2의 7분 40초, $4.28이라는 비용은 단일 원인보다 다음 세 구조가 결합한 결과로 판단된다. + +1. 5개 Stage B domain worker와 PostB adjudicator를 모두 `gemini-3.1-pro-preview/high`로 실행한다. +2. 약 50개 seed 전체가 든 PostB_1 객체를 예외 6건만 판정하는 LLM에 통째로 전달한다. +3. 같은 Stage A context를 5개 B0 router와 여러 후속 Python task에 반복 삽입하고, publisher, domain join, seed ledger, final compiler, final writer, 3개 signal writer를 별도 code-executor 호출로 직렬 연결한다. + +최적 해법은 Stage B의 법률 도메인 분리는 유지하되 다음과 같이 실행 surface를 축소하는 것이다. + +- Stage A normalization과 5개 B0 router를 Python task 1개로 통합한다. +- 5개 LLM worker는 유지하되 B1/B3에는 Flash, B2/B4/B5에는 Pro를 배치한다. +- LLM은 완성 schema가 아니라 sparse seed delta만 출력하고, null/빈 배열/default 채움은 Python이 수행한다. +- publisher, domain_join, PostB_1을 하나의 deterministic reducer로 통합한다. +- near-duplicate는 Stage 1에서 억지로 병합하지 않고 `KEEP_SEPARATE + review`로 결정한다. +- 최종 BO 작성에 반드시 필요한 비결정적 예외만 compact pack으로 조건부 LLM에 보낸다. +- PostB final compiler와 final gate/writer를 단일 Python writer로 통합한다. +- 3개 signal writer를 하나의 Python task로 통합하되 기존 3개 파일은 그대로 작성한다. + +이 구조는 현재 20개 task를 통상 9개, 비결정적 예외가 있을 때 10개 안팎으로 줄인다. 외부 계약인 `BO.json`, `actio_case_signals.json`, `case_liability_signals.json`, `legal_effect_signals.json`은 변경하지 않는다. + +## 1. 분석 범위와 전제 + +### 1.1 읽은 작업 명세 + +Stage 1 전체 연결을 다음 파일로 확인했다. + +| Part | 기준 파일 | 역할 | +|---|---|---| +| Part 1 | `Stage_1_Part_1_Codex_v3.yml` | client goal, evidence index, event candidates, Part 1 gate handoff | +| Part 2 | `Stage_1_Part_2_v2.yml` | BO와 3개 signal 생성 | +| Part 3 | `Stage_1_Part_3_v1.yml` | legal effect structures 생성 | +| Part 4 | `Stage_1_Part_4_v2.yml` | Fact Ledger 생성 및 sealed gate 적용 | + +질문의 method 1에는 첫 문장에서 `Codex_v3`를 지정한 뒤 목록에서 `Claude_v3`라고 적은 불일치가 있다. 최신 Part 1 작업 명세라는 문맥, 직전 개정 이력 및 명시적 지시를 우선하여 `Stage_1_Part_1_Codex_v3.yml`을 기준으로 분석했다. + +### 1.2 판정 한계 + +- 전체 Part 2 런타임과 비용은 제공됐지만 task별 latency, input/output token, cache hit 로그는 결과 폴더에 없다. +- 따라서 특정 task가 정확히 몇 달러를 소비했다고 배분하지 않는다. +- 파일 크기, task/model 설정, DAG, 실제 산출물 수량 및 동일 규칙의 재계산 결과는 실측값이다. +- 성능 절감률은 목표와 검증 기준으로 제시하며 보장값으로 표현하지 않는다. + +## 2. Stage 1 전체 handoff + +```text +Part 1 Codex v3 + client_goal.json + evidence_indexed.json + evidence_event_candidates.json + Part 1 quality gates / soft-gate handoff + | + v +Part 2 v2 + BO.json + actio_case_signals.json + case_liability_signals.json + legal_effect_signals.json + | + v +Part 3 v1 + legal_effect_structures.json + | + v +Part 4 v2 + Fact_Ledger_base.json +``` + +Part 3의 LES0과 Part 4의 FL0은 Part 2의 `BO.json`과 3개 signal을 직접 읽는다. 반면 Part 2의 다음 중간 파일은 Part 3/4의 직접 입력이 아니다. + +- `stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/*.json` +- Stage B publisher stdout +- Stage B domain_join stdout +- PostB seed ledger stdout +- PostB compiled bundle stdout + +따라서 Part 2 내부 task와 중간 artifact는 자유롭게 통합할 수 있다. 단, 다음 외부 불변식은 유지해야 한다. + +| 외부 파일 | 유지할 핵심 계약 | +|---|---| +| `BO.json` | JSON array, 연속·유일 `bh#`, provenance, evidence, `extensions.domain_payload` | +| `actio_case_signals.json` | `schema_version`, `status`, `actio_case_signals[]` | +| `case_liability_signals.json` | `schema_version`, `status`, `case_liability_signals[]` | +| `legal_effect_signals.json` | `schema_version`, `status`, `bo_legal_effect_routes[]` | + +## 3. 현재 Part 2 구조 + +### 3.1 현재 DAG + +```text + +--> B0-B1 --+ + +--> B0-B2 --+ +IN -> Stage A normalize +--> B0-B3 --+-- 5x5 barrier --> 5 Pro/high workers + +--> B0-B4 --+ | + +--> B0-B5 --+ v + worker output publisher + | + v + domain_join + | + v + PostB_1 seed ledger compiler + | + v + PostB_2 Pro/high exception adjudicator + | + v + PostB_3 final BO compiler + | + v + PostB_4 final gate + BO writer + / | \ + v v v + actio signal liability legal effect + \ | / + OUT +``` + +각 worker는 자기 B0 slice만 prompt에 삽입한다. 즉, 5개 B0 결과 모두를 한 worker prompt에 합치는 직접적인 token fan-in은 없다. 그러나 모든 worker가 모든 B0 task 완료를 기다리므로 불필요한 전역 barrier가 존재한다. + +### 3.2 task와 모델 + +| 구간 | task 수 | 실행 유형 | 모델/특징 | +|---|---:|---|---| +| Stage A | 1 | Python | 4개 주요 입력 normalize | +| Stage B0 | 5 | Python | 같은 Stage A 객체를 각 task가 다시 parse·route | +| Stage B | 5 | LLM | 모두 Gemini 3.1 Pro, reasoning high | +| publisher/domain join | 2 | Python | 5개 output 저장 후 재읽기·결합 | +| PostB | 4 | Python 3 + LLM 1 | adjudicator도 Gemini 3.1 Pro/high | +| Middle Input | 3 | Python | 같은 대형 upstream 4종을 각 task에 반복 삽입 | +| 합계 | 20 | Python 14 + LLM 6 | 모든 LLM이 Pro/high | + +정적 규모는 다음과 같다. + +| 항목 | 실측 | +|---|---:| +| Part 2 YAML | 7,462행, 446,306 bytes | +| Python task | 14개 | +| embedded Python | 5,630행, 287,323 bytes | +| B0 Python만 | 5개, 1,162행, 79,102 bytes | +| LLM task | 6개 | +| LLM 정적 prompt | 1,400행, 78,944 bytes | + +## 4. 실행 결과 계측 + +### 4.1 Part 2 주요 산출물 + +| 파일 | 크기 | 수량/상태 | +|---|---:|---| +| `BO.json` | 175,126 bytes | BO 50건 | +| `actio_case_signals.json` | 9,990 bytes | signal 5건 | +| `case_liability_signals.json` | 15,565 bytes | signal 50건 | +| `legal_effect_signals.json` | 19,093 bytes | route 50건 | +| B1 domain seed | 49,793 bytes | seed 13, review 3 | +| B2 domain seed | 38,901 bytes | seed 7, review 3 | +| B3 domain seed | 29,322 bytes | seed 6, review 2 | +| B4 domain seed | 25,747 bytes | seed 5, review 3 | +| B5 domain seed | 62,918 bytes | seed 19, review 2 | + +5개 domain worker 모두 `READY_WITH_REVIEW`를 반환했다. domain review item은 총 13건이다. + +### 4.2 source routing 중복 + +worker output의 `slice_guard.source_universe` 기준 결과는 다음과 같다. + +| Domain | meeting | event | evidence | seed | +|---|---:|---:|---:|---:| +| B1 Money/Successor | 14 | 17 | 11 | 13 | +| B2 Secured/Registry | 15 | 71 | 30 | 7 | +| B3 Land/Valuation | 11 | 12 | 8 | 6 | +| B4 Actio | 3 | 11 | 5 | 5 | +| B5 Succession/Notice/Lien/Asset/Defense | 41 | 71 | 30 | 19 | + +전체 universe는 event 71개, evidence 30개, meeting clause 41개다. B2와 B5는 event/evidence universe 전체를 다시 싣는다. 도메인 간 합산/유일 수량의 중복 배수는 다음과 같다. + +- meeting: `84 / 41 = 2.05x` +- event: `182 / 71 = 2.56x` +- evidence: `84 / 30 = 2.80x` + +B2의 `변제`, `등기`, `말소`와 B5의 `점유`, `인도`, `목적물`, `항변`, `party_assertion` 등 넓은 keyword가 source-ref closure와 결합해 과다 routing을 만든다. 공유 source 자체는 법률상 필요할 수 있으므로 단순 배타 routing은 금지해야 한다. 다만 `직접 관련`, `참조 관련`, `검토 후보`의 3등급으로 분리하지 않고 모두 full authority record로 넘기는 현재 방식은 비경제적이다. + +### 4.3 LLM output 비대화 + +5개 domain seed 파일을 compact JSON으로 환산하면 전체 126,955 bytes다. + +- 실제 seed candidate 본체: 94,830 bytes +- transport metadata: 9,841 bytes +- transport candidate refs: 9,614 bytes +- review queue: 6,159 bytes +- handoff guard: 5,350 bytes + +candidate 이외 반복 envelope가 전체의 약 25.3%다. 또한 candidate에서 `null`, `{}`, `[]` default를 제거한 sparse 표현을 실측하면 다음과 같다. + +| Domain | 현재 candidate bytes | sparse bytes | 감소율 | +|---|---:|---:|---:| +| B1 | 24,910 | 12,701 | 49.0% | +| B2 | 16,862 | 10,621 | 37.0% | +| B3 | 13,411 | 9,944 | 25.9% | +| B4 | 11,855 | 8,233 | 30.6% | +| B5 | 27,792 | 20,171 | 27.4% | +| 합계 | 94,830 | 61,670 | 35.0% | + +LLM이 full target schema를 매번 채울 이유가 없다. false/0처럼 의미 있는 값은 유지하고, null·빈 배열·빈 객체만 Python expander가 보충하면 candidate 출력만 약 35% 줄일 수 있다. 반복 envelope까지 줄이면 domain LLM output surface는 현재의 절반 안팎을 목표로 할 수 있다. + +### 4.4 PostB adjudicator 입력 증폭 + +PostB_2 prompt는 “nested field access가 안정적이지 않다”는 이유로 `postb_seed_ledger.exception_pack`만 필요하면서 PostB_1 전체 stdout을 삽입한다. + +7/10 fixture의 seed 50건에 PostB_1과 동일한 규칙을 적용한 재계산 결과: + +- schema risk: 2건 +- link ambiguity: 0건 +- exact duplicate bucket: 0건 +- near-duplicate cluster: 4개, candidate 8건 +- 총 exception item: 6건 + +같은 규칙으로 만든 대략적 JSON 크기: + +- exception pack: 18,124 bytes +- full PostB_1 object: 129,772 bytes +- 불필요한 LLM input 증폭: 약 7.16배 + +더구나 현재 PostB_2는 `has_exceptions=false`여도 Pro/high LLM을 호출해 `READY_NO_EXCEPTIONS`를 출력하게 되어 있다. + +### 4.5 실제 BO 품질 신호 + +최종 BO 50건과 domain seed 50건을 `source_domain + Action + BOType + ActionType`로 대조한 결과 50건 모두 1:1 대응했다. + +- merge, split, drop으로 core candidate가 달라진 흔적: 0건 +- `PriorAct == null`: 50/50 +- `Reason`: 50건 모두 사실상 generic template +- source evidence가 빈 BO: 2건 + - 4억원 변제·채무 미특정 사실 2개로, meeting-only assertion을 BO화한 경우다. + +이는 현재 PostB Pro/high adjudicator가 적어도 이 fixture에서는 seed 수와 핵심 값을 바꾸지 않았음을 의미한다. near-duplicate를 합치지 않은 것은 Stage 1 provenance 보존 관점에서 타당하다. 반면 `PriorAct`, `Reason`, `ReasonRefs`를 finalization 대상으로 선언했으나 실제로 유의미하게 채우지 못한 점은 품질 결함이다. + +또한 13개 domain review의 상세 내용은 임시 domain seed 파일에는 있으나 최종 BO와 3개 signal에는 review id·source refs 형태로 직접 전달되지 않는다. signal은 `READY_WITH_REVIEW` status만 표시한다. 비싼 LLM이 생성한 상세 review prose가 downstream에서 구조적으로 소비되지 않는 셈이다. + +## 5. 비효율성 판정 + +### P0-1. 모든 법률 worker에 Pro/high를 일률 배정 + +**판정:** 과잉 배정. + +Stage B worker는 최종 청구원인이나 법률효과를 확정하지 않는다. prompt도 명시적으로 provisional seed 생성, null 보존, Stage 2 법리 확정을 요구한다. 따라서 모든 domain에 최고 모델을 쓰는 것은 역할 경계와 맞지 않는다. + +다만 B2의 담보·등기·변제충당, B4의 사해행위·가액배상, B5의 상속·통지·유치권은 source grouping 자체가 복잡하다. 이 세 domain을 즉시 Flash-lite로 내리는 것은 권고하지 않는다. + +### P0-2. PostB LLM에 full seed ledger 전달 + +**판정:** 가장 명확한 token 비경제성. + +LLM은 예외 6건만 판정하지만 약 7배 큰 full object를 읽는다. full ledger는 artifact로 저장하고 LLM에는 exception pack만 전달해야 한다. 예외 0건에는 호출 자체가 없어야 한다. + +### P0-3. Stage A context의 반복 전송 + +**판정:** 구조적 data amplification. + +Stage A output 전체가 다음에 반복 삽입된다. + +- 5개 B0 router +- worker output publisher +- domain_join +- PostB_1 +- PostB_3 +- PostB_4 +- 3개 signal writer + +모든 task가 full context를 필요로 하지 않는다. freshness와 source-universe 검증에는 digest와 compact manifest면 충분한 경우가 많다. + +### P1-1. B0 5×5 barrier + +**판정:** prompt fan-in은 아니지만 DAG barrier가 불필요하다. + +각 worker는 자기 B0 output만 읽는다. 그런데 각 B0 task가 5개 worker 모두를 next로 지정하고, 각 worker는 B0 다섯 개를 모두 기다린다. 최소 수정은 one-to-one edge다. + +```text +B0-B1 -> Worker-B1 +B0-B2 -> Worker-B2 +B0-B3 -> Worker-B3 +B0-B4 -> Worker-B4 +B0-B5 -> Worker-B5 +``` + +권장 구조에서는 B0 전체를 하나의 router compiler로 통합하므로 이 barrier 자체가 사라진다. + +### P1-2. B0 코드 5중 복제 + +**판정:** 유지보수와 실행 모두 비효율적. + +5개 B0 task는 공통 parser, matcher, projection, taxonomy/hazard subset, Part 1 review routing 코드를 반복한다. 차이는 주로 `DOMAIN_CONFIG`다. config table과 공통 함수 1벌로 통합할 수 있다. + +### P1-3. LLM이 default-filled full schema를 작성 + +**판정:** output token 낭비이며 schema 오류 가능성도 높인다. + +`Reason:null`, `PriorAct:null`, 빈 `ReasonRefs`, 빈 downstream refs, 여러 빈 domain payload 필드는 전부 결정적 default다. Python이 채우는 편이 더 빠르고 정확하다. + +### P1-4. publisher -> domain_join -> PostB_1의 연속 재직렬화 + +**판정:** 세 task를 하나의 deterministic reducer로 합칠 수 있다. + +현재 publisher가 5개 LLM JSON을 검증·저장하고, domain_join이 다시 5개 파일을 읽어 project하며, PostB_1이 다시 seed ledger와 exception pack을 만든다. downstream은 이 세 stdout을 직접 소비하지 않는다. + +### P1-5. signal writer 3개가 같은 대형 upstream을 반복 소비 + +**판정:** 단일 Python writer로 통합 가능. + +세 task 모두 다음을 각각 삽입한다. + +- Stage A output +- PostB seed ledger +- PostB compiled bundle +- B4 worker output + +동일한 BO/source index를 한 번 읽고 메모리에서 3개 signal을 생성한 뒤 파일 3개를 쓰면 된다. + +### P1-6. review prose 생성 비용과 handoff 불일치 + +**판정:** 현재 형태의 긴 `description` 생성은 비경제적. + +review가 downstream 사실·법리 검토에 중요하다면 compact structured handoff로 보존해야 한다. 보존하지 않는다면 LLM이 긴 설명을 쓰게 해서는 안 된다. + +권장 최소 필드: + +```json +{ + "review_id": "B2:R001", + "severity": "SOFT_WARNING|HARD_WARNING", + "issue_type": "closed enum", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "C0|C2|C5|D|Stage2" +} +``` + +설명문은 fixed template로 Python이 생성하거나 downstream UI에서 렌더링한다. + +### P2-1. 공통 cache prefix 범위가 좁고 adjudicator prefix가 다름 + +5개 worker의 common prefix hash는 동일하지만 공유되는 부분은 약 3.4KB에 그친다. output contract와 공통 authority rules의 상당 부분이 task별 block에서 반복된다. adjudicator는 다른 common prefix를 사용한다. + +공통 rule과 compact seed-delta contract를 dynamic input보다 앞에 두고, 모든 LLM task의 ``을 바이트 동일하게 유지한다. task별 domain schema와 role은 prefix 밖 overlay로 둔다. 모델이 다르면 cache가 모델별로 나뉠 수 있으므로 Flash group과 Pro group 각각의 hit를 계측해야 한다. + +### P2-2. 사용되지 않는 Stage-level 기본 모델 + +Stage에 `openai/gpt-4o-2024-08-06`가 선언돼 있지만 6개 LLM task 모두 자체 provider/model을 지정한다. 실행비용의 직접 원인은 아니나 오독과 fallback 위험을 없애기 위해 삭제하는 편이 낫다. + +## 6. task별 유지·통합·삭제 판정 + +| 현재 task | 판정 | 개정 방향 | +|---|---|---| +| Stage A input normalization | 유지·통합 | B0 routing까지 한 Python task에서 수행 | +| B0 input 5개 | 삭제 | domain config table 기반 단일 router로 대체 | +| B1 worker | 유지·경량화 | Flash, sparse output | +| B2 worker | 유지 | Pro, input narrowing, sparse output | +| B3 worker | 유지·경량화 | Flash, sparse output | +| B4 worker | 유지 | Pro, actio named fields 보존 | +| B5 worker | 유지 | Pro, notice/lien/possession/asset named fields 보존 | +| worker output publisher | 삭제·통합 | reducer에 writer/validation 흡수 | +| domain_join | 삭제·통합 | reducer에 projection/index 흡수 | +| PostB_1 seed ledger compiler | 삭제·통합 | reducer가 ledger artifact와 exception pack 생성 | +| PostB_2 adjudicator | 조건부화 | non-deferrable compact exception만 Flash 판정 | +| PostB_3 final compiler | 통합 | final gate/writer와 단일 writer 구성 | +| PostB_4 final gate/writer | 유지·통합 | BO의 유일 writer 유지 | +| 3개 Middle Input task | 삭제·통합 | signal bundle writer 1개로 대체 | + +## 7. 권장 최적화 구조 + +### 7.1 권장 DAG + +```text +IN + | + v +A0 context + domain-slice compiler [Python] + | writes Stage A artifact, global manifest, 5 narrow slices + | + +--> B1 Money/Successor worker [Flash] + +--> B2 Secured/Registry worker [Pro] + +--> B3 Land/Valuation worker [Flash] + +--> B4 Actio worker [Pro] + +--> B5 Succession/Notice/Lien/Asset/Defense worker [Pro] + \ | / + \ | / + v v v +R0 seed reducer + schema expander + exception planner [Python] + | writes full seed ledger + compact review handoff + | + +--> no non-deferrable exception ----------------------+ + | | + +--> compact exception pack -> R1 adjudicator_* [Flash]| + v +F0 final BO compiler + gate + single writer [Python] + | + v +S0 signal bundle writer [Python] + | writes the existing three signal files + v +OUT +``` + +### 7.2 권장 task 수 + +| 구분 | 현재 | 권장 | +|---|---:|---:| +| Python task | 14 | 4 | +| 상시 LLM task | 6 | 5 | +| 조건부 LLM | 0 | 0 또는 제한적 1~N | +| 총 task | 20 | 통상 9, 예외 시 약 10 | + +### 7.3 A0 compiler 계약 + +입력: + +- `client_meeting.md` +- `evidence_event_candidates.json` +- `evidence_indexed.json` +- `Default_Agent/Juristic_Act.md` +- `quality_gates/stage1_part1_soft_gate_handoff.json` + +작업: + +1. 현재 Stage A normalization과 동일한 source map 및 digest를 만든다. +2. global source universe를 별도 manifest 1개에만 저장한다. +3. 5개 domain config를 loop로 실행한다. +4. routing을 `primary`, `support`, `review_only`로 구분한다. +5. LLM slice에는 primary full compact record와 support ref/minimal summary만 넣는다. +6. domain별 source count와 max-byte budget을 검사한다. + +출력 artifact 예시: + +```text +stage1_tmp/task_c_bo/stage_a_context.json +stage1_tmp/task_c_bo/source_universe_manifest.json +stage1_tmp/task_c_bo/domain_slices/B1.json +stage1_tmp/task_c_bo/domain_slices/B2.json +stage1_tmp/task_c_bo/domain_slices/B3.json +stage1_tmp/task_c_bo/domain_slices/B4.json +stage1_tmp/task_c_bo/domain_slices/B5.json +``` + +stdout에는 full context를 넣지 않고 다음 manifest만 둔다. + +```json +{ + "status": "READY", + "stage_a_path": "stage1_tmp/task_c_bo/stage_a_context.json", + "source_manifest_path": "stage1_tmp/task_c_bo/source_universe_manifest.json", + "domain_slice_paths": {"B1": "...", "B2": "...", "B3": "...", "B4": "...", "B5": "..."}, + "input_digests_sha256": {} +} +``` + +### 7.4 routing 개선 원칙 + +1. broad Korean noun 하나만으로 primary routing하지 않는다. +2. `event_kind`, `event_subkind`, `doc_type`, `legal_effect_candidate`, named component, registry/fraction/notice/lien key를 keyword보다 우선한다. +3. `등기`, `변제`, `항변`, `점유`, `목적물` 같은 broad term은 보조 신호로만 쓴다. +4. source-ref closure로 따라온 항목은 `support` 등급으로 표시한다. +5. B5가 모든 사건의 catch-all이 되지 않도록 succession, notice, lien, possession, asset_alias, defense의 activation reason을 각각 기록한다. +6. source를 다른 domain에서 제거하지 않는다. 중복 허용과 full-payload 중복은 구분한다. +7. 한 domain slice가 전체 event/evidence universe의 일정 비율을 넘으면 자동 실패시키지 말고 `ROUTING_BROADNESS_WARNING`을 기록한다. + +권장 audit 필드: + +```json +{ + "primary_ids": [], + "support_ids": [], + "review_only_ids": [], + "activation_reasons": [], + "source_count": {}, + "slice_bytes": 0, + "broadness_ratio": 0.0 +} +``` + +### 7.5 LLM sparse output 계약 + +LLM은 다음만 출력한다. + +- domain id와 status +- candidate ref +- 의미 있는 BO 분류값 +- 짧은 Action +- source refs +- 채워진 domain payload 값 +- compact review code + +다음은 출력하지 않는다. + +- 반복 input digest와 global source universe +- 별도 `transport_candidate_refs` +- 항상 null인 `Reason`, `PriorAct` +- 항상 빈 `ReasonRefs` +- 항상 빈 downstream seed refs +- schema를 맞추기 위한 빈 dict/list/null field +- 긴 review description + +R0 Python이 canonical schema로 확장한다. expander는 누락을 임의 사실로 채우지 않고 schema default만 채운다. + +### 7.6 모델 배치 + +| Task | 권장 모델 | reasoning | verbosity | 근거 | +|---|---|---|---|---| +| B1 Money/Successor | `gemini-3.5-flash` | high | low | source grouping과 provisional seed, 최종 법리 판단 아님 | +| B2 Secured/Registry | `gemini-3.1-pro-preview` | high | low | 지분·담보·변제충당·등기 chain의 높은 오류비용 | +| B3 Land/Valuation | `gemini-3.5-flash` | medium | low | 상태/가액 source grouping 중심 | +| B4 Actio | `gemini-3.1-pro-preview` | high | low | 피보전채권·무자력·담보공제·가액배상 구조가 핵심 | +| B5 Succession/Notice/Lien/Asset/Defense | `gemini-3.1-pro-preview` | high | low | 여러 고위험 법률 구조가 결합됨 | +| conditional exception adjudicator | `gemini-3.5-flash` | medium | low | closed decision enum, compact candidate만 사용 | + +B1/B3를 Flash-lite까지 낮추는 것은 1차 개정에서 권고하지 않는다. 먼저 Flash로 fixture 동등성을 검증한 후에만 추가 하향한다. + +### 7.7 R0 deterministic reducer + +R0는 다음을 한 번에 수행한다. + +1. 5개 LLM JSON strict parse +2. domain id, source membership, candidate ref 검사 +3. sparse candidate를 full schema로 확장 +4. global candidate index와 source index 생성 +5. exact duplicate, near duplicate, schema risk, link ambiguity 검사 +6. review code 정규화 +7. full seed ledger artifact 저장 +8. compact exception pack 생성 +9. adjudicator fan-out descriptor 또는 no-exception marker 출력 + +권장 artifact: + +```text +stage1_tmp/task_c_bo/postb_seed_ledger.json +quality_gates/stage1_part2_review_handoff.json +quality_gates/stage1_part2_exception_pack.json +``` + +stdout에는 full ledger를 넣지 않는다. + +### 7.8 LLM에 보내지 않을 예외 + +다음은 deterministic policy로 처리한다. + +| 예외 | 처리 | +|---|---| +| exact byte/schema duplicate | provenance union이 무손실일 때만 canonical merge | +| near duplicate | `KEEP_SEPARATE`, shared cluster id와 review code 부여 | +| meeting-only candidate | 보존, `MEETING_ONLY_EVIDENCE_GAP` warning | +| optional PriorAct 불명 | null 유지, review code | +| Stage 2 법리 확정 필요 | `legal_theory_required`, LLM 호출 금지 | +| source ref가 universe 밖 | BLOCK | +| 필수 schema type/enum 오류 | BLOCK 또는 mapper rerun | + +7/10 fixture의 4개 near cluster와 meeting-only schema risk 2건은 위 정책으로 모두 결정적 처리할 수 있다. 따라서 이 정책 기준 fixture의 PostB LLM 호출 목표는 0회다. + +### 7.9 조건부 adjudicator에 보낼 예외 + +다음 조건을 모두 충족할 때만 LLM을 실행한다. + +1. final BO writer가 진행하려면 하나의 값을 선택해야 한다. +2. 둘 이상의 source-backed candidate가 존재한다. +3. 두 값을 모두 보존하거나 Stage 2로 defer할 수 없다. +4. compact payload가 판정에 충분하다. + +LLM이 불충분하다고 판단하면 `BLOCK_REVIEW`를 반환한다. full Stage A, raw evidence, full seed ledger를 읽게 해서는 안 된다. + +### 7.10 F0 단일 BO writer + +PostB_3과 PostB_4를 통합한다. + +F0는 다음 순서로 실행한다. + +1. seed ledger artifact read + digest 확인 +2. conditional adjudication part가 있으면 exception id coverage 확인 +3. candidate conservation 확인 +4. deterministic ordering과 `bh#` 부여 +5. source evidence object 구성 +6. `Reason`, `PriorAct`, `ReasonRefs` 정책 적용 +7. BO schema 및 reference gate +8. `BO.json` 1회 write +9. reread, JSON parse, SHA-256 및 count 검증 + +`Reason`은 generic prose를 길게 생성하지 않는다. source chain이 없으면 fixed enum/code와 짧은 template을 사용하고, 명시적 prior link가 있을 때만 `PriorAct`/`ReasonRefs`를 채운다. 최종 법적 인과관계를 Stage 1이 임의 확정하지 않는다. + +### 7.11 S0 signal bundle writer + +세 signal task를 하나로 통합한다. + +입력: + +- `BO.json` +- source universe manifest +- 필요한 경우 seed ledger의 compact index + +출력: + +- `actio_case_signals.json` +- `case_liability_signals.json` +- `legal_effect_signals.json` + +각 파일의 기존 schema와 status 규칙을 유지한다. BO를 한 번 읽고 3개 projection을 메모리에서 만든다. + +## 8. 최소 개정 우선순위 + +전체 구조 개정 전에 가장 적은 수정으로 효과를 확인하려면 다음 순서가 적절하다. + +### Phase 1: 즉시 적용 + +1. B0와 worker의 5×5 edge를 one-to-one으로 수정한다. +2. B1/B3 모델을 Flash로, 모든 worker verbosity를 low로 변경한다. +3. PostB_1이 full ledger를 artifact로 저장하고 stdout에는 exception pack만 반환하게 한다. +4. near duplicate와 meeting-only risk를 deterministic defer 대상으로 바꾼다. +5. PostB adjudicator를 조건부 Flash task로 변경한다. +6. 모든 LLM의 common prefix를 동일한 full prefix로 교체하고 role overlay를 분리한다. + +### Phase 2: 구조 통합 + +1. Stage A와 5개 B0를 A0 compiler로 통합한다. +2. worker output을 sparse delta schema로 변경한다. +3. publisher, domain_join, PostB_1을 R0 reducer로 통합한다. +4. PostB_3/4를 F0로 통합한다. +5. 3개 signal task를 S0로 통합한다. + +Phase 1 결과로 비용·시간이 충분히 줄어도 Phase 2의 full-object handoff와 중복 Python 유지보수 문제는 남는다. 최종 v3에는 Phase 2까지 반영하는 것을 권고한다. + +## 9. 회귀 및 성능 검증 + +### 9.1 정적 검증 + +- YAML parse 성공 +- task name 중복 0 +- DAG unreachable task 0 +- old task reference 0 +- 모든 embedded Python syntax compile +- localdocs full boilerplate 및 user/workspace hash 전달 +- `BO.json` writer 정확히 1개 +- 각 signal 파일 writer 정확히 1개 +- 모든 LLM common cache prefix hash 동일 +- stage-level 미사용 model 제거 + +### 9.2 7/10 fixture 품질 불변식 + +1. evidence index 30개와 event candidate 71개의 source universe를 보존한다. +2. 모든 최종 BO source ref는 Part 1 universe의 subset이어야 한다. +3. 모든 Stage B candidate ref는 다음 중 하나로 추적돼야 한다. + - 최종 BO 1건 + - provenance-union merge의 absorbed ref + - BLOCK/blocked review item +4. B4 actio payload의 preserved claim, transfer, encumbrance, valuation, cap, defense 정보가 소실되지 않는다. +5. B5의 notice lifecycle, lien state, possession two-track, asset alias named fields가 소실되지 않는다. +6. 13개 기존 review item의 issue type과 source refs가 compact handoff 또는 BO review flag로 보존돼야 한다. +7. meeting-only BO 2건은 자동 삭제하지 않는다. +8. Part 3 LES0 및 Part 4 FL0 smoke test를 통과한다. + +BO 수 50 자체를 절대 불변식으로 두지는 않는다. 다만 candidate conservation과 provenance union을 만족하지 않는 감소는 금지한다. 1차 구현에서는 안전을 위해 50건 보존을 기준값으로 삼는 것이 좋다. + +### 9.3 성능 합격 기준 + +| 지표 | 현재 | 1차 목표 | +|---|---:|---:| +| Part 2 총 task | 20 | 10 이하 | +| 상시 Pro/high LLM | 6 | 3 이하 | +| PostB LLM input | 약 130KB 재구성값 | exception pack만, 20KB 안팎 | +| 7/10 fixture PostB LLM | 1회 | 0회 목표 | +| compact domain output | 126,955 bytes | 70,000 bytes 이하 목표 | +| runtime | 7분 40초 | 5분 이내 1차 목표 | +| cost | $4.28 | $2.50 이하 1차 목표 | + +runtime과 cost 목표는 동일 provider 가격·부하 조건에서 3회 이상 반복 실행한 중앙값으로 판정한다. 품질 불변식을 하나라도 위반하면 성능 목표를 달성해도 실패다. + +## 10. 최종 권고 + +Part 2의 도메인별 법률 추론 자체를 하나의 거대 LLM 호출로 합치는 것은 권고하지 않는다. 금전채권, 담보·등기, 토지·가액, 사해행위, 상속·통지·유치권의 판단 경계가 달라 prompt가 더 커지고 실패 blast radius가 커진다. + +반대로 현재처럼 모든 전후 처리와 schema 채움을 task별로 잘게 분리하고, 모든 도메인에 Pro/high를 쓰는 것도 적절하지 않다. + +가장 효율적인 개정은 다음 한 문장으로 요약된다. + +> **도메인 LLM 5개는 유지하되 입력과 출력을 절반 수준으로 줄이고 모델을 차등 배치하며, 그 앞뒤의 14개 Python task를 4개 deterministic compiler/reducer/writer로 통합하고, LLM 예외 판정은 non-deferrable compact exception에만 허용한다.** + +이 방식이 현재 Part 2의 법률적 provenance, named domain payload, Part 3/4 handoff를 보존하면서 runtime과 비용을 동시에 줄이는 최적 경로다. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_3_Improvement_Claude_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_3_Improvement_Claude_v1.md new file mode 100644 index 00000000..78e50c92 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_3_Improvement_Claude_v1.md @@ -0,0 +1,155 @@ +# Stage 1 Part 3 개정 전략 선별 보고서 (Claude v1) + +- 비교 대상: `Part_3_Inefficiency_Report_Claude_v1.md` ↔ `Part_3_Inefficiency_Report_Codex_v1.md` +- 목적: 두 분석의 교차 검증·상충 판정을 거쳐 Part 3 개정에 가장 적합한 전략을 선별 +- 선별 기준: 최소의 노력(LLM 추론은 꼭 필요한 곳에 필요한 수준만, 100% 결정론 작업은 Python화, 토큰 경제성 최대화)으로 최대의 효과(최고 품질 유지, 토큰 경제성 증대, 에이전트 속도 최대화) +- 작성 관점: 20년 이상 경력의 대한민국 최고 수준 변호사 + 세계적 수준의 AI 아키텍트 + +## 0. 총평 + +두 보고서는 서로 다른 방법으로 같은 결론에 도달했다. Claude 보고서는 Part 3 전체 코드를 7/10 실행물로 **바이트 일치 재실행**하여 데이터 흐름과 LLM 판정의 실효성을 실측했고, Codex 보고서는 YAML 계약 추적과 산출물 계측으로 **예외가 과잉 생성되는 원인 메커니즘**을 해부했다. 핵심 진단 8개 항목이 독립적으로 일치하므로 사실 기반은 매우 견고하다. 차이는 처방의 결에 있다: Claude는 구조 통합(4→3 task)과 결정론 defer로 "전송·호출 자체의 소거"를, Codex는 타입 어휘 정규화와 상태 모델 교정으로 "예외 발생 원인의 소거"를 겨냥한다. 이 둘은 경합하지 않고 **정확히 상호보완**이며, 본 보고서는 두 축을 모두 채택한 통합 골격을 확정한다. 한편 공통 캐시 프리픽스에 대해서는 두 보고서의 판정이 상충하는데, 3-way 바이트 재실측 결과 **양쪽 모두 부분적으로 부정확**했음을 확인하고 §3에서 공개 정정한다. + +## 1. 비교 분석 + +### 1.1 비효율 판정 항목 비교 + +**양측 공통 판정 (독립 교차 검증 — 신뢰도 최상).** 다음 8개는 두 보고서가 서로 다른 방법으로 동일하게 확인했으므로 개정의 확정 근거로 삼는다. + +| # | 공통 판정 | Claude 근거 | Codex 근거 | +|---|---|---|---| +| 1 | LES2가 예외 유무와 무관하게 상시 실행 | P3-6 | §1-1 | +| 2 | 예외 과잉 생성 → BO 50건 전부 예외화 (47건) | P3-7 | §5.1 | +| 3 | LES2 판정 30건 중 28건 BLOCK_REVIEW (93%) | P3-7 실측 | §5.2 실측 | +| 4 | budget_overflow 17건이 LLM도 못 보고 자동 차단 | P3-8 | §5.4 | +| 5 | 성공 판정이 review_required를 해제하지 못함 → 50/50 포화, 큐 78건 | P3-9 | §5.3 | +| 6 | input pack 대형 물질화 + 미사용 필드(bo_by_id·signals_by_bo_id·events·client_goal) | P3-1·P3-2 (55% 미사용 실측) | §5.5·§5.6 (동일 필드 판정) | +| 7 | seed bundle 중복(진단·type_index_seed·내장 exception pack) + 별도 pack 파일 소비자 0 | P3-4 | §5.7 | +| 8 | 실패 시 `sys.exit(0)` — 실패의 정상종료 위장 | P3-14 | §5.10 | + +양측의 크기 계측이 수 KB 단위로 다른 것(예: bo_by_id 82.7KB vs 89.0KB)은 직렬화 기준 차이일 뿐 판정은 동일하다. + +**Claude 보고서 고유 발견 (Codex 미포착).** + +- **LES3 `SEED_RAW` 주입의 구조적 사장(P3-5)**: LES1 stdout의 루트 키에 `legal_structure_seed_bundle`이 존재하지 않아 LES3의 stdout 파싱이 **100% 실패**하고 매 실행 422KB 파일을 재읽는다. Codex는 LES0→LES1의 이중 채널만 지적했고 LES3의 dead injection은 포착하지 못했다. 32.6KB 주입 제거 + 파일 읽기 일원화의 직접 근거. +- **LLM 부가가치 0의 최종 산출물 수준 실증**: 구조물 50건 전부의 대표 타입이 결정론 우선순위 규칙(priority-first)과 100% 일치하고 split 0건임을 실측 — Codex의 "비차단 판정 2건뿐"(과정 증거)을 넘어, **LES2를 결정론 기본값으로 대체해도 최종 파일이 동일**하다는 결과 증거다. 결정론 defer의 무손실성을 보증하는 가장 강한 근거. +- **`evidence_all.json`(87KB) 기여 0의 실험적 확정**: 포함/제외 재실행 비교로 evidence authority 30건 전부 동일함을 증명. Codex는 같은 제거를 "coverage 검증 후 조건부"로 남겼는데, Claude 실측이 그 조건을 이미 충족시켰으므로 **무조건 제거로 격상**한다. +- 바이트 일치 재현 방법론: 중간 파일 3종(416,115/422,236/52,982B)의 재현 일치로 모든 수치의 신뢰성을 담보. + +**Codex 보고서 고유 발견 (Claude 미포착 — 채택 가치 높음).** + +- **타입 어휘 오염의 정량 해부(§5.1)**: `candidate_structure_types`에 canonical 법률구조 family 외에 `event`/`state`, 행위 분류(법률행위 등), 세부 사건명(변제·상계 등)까지 승격되어 출현 263회·distinct 69종에 이르고, **canonical enum만 남기면 복수 role BO가 50→14로 감소**한다는 실측. Claude 보고서가 "합집합 과다"로 뭉뚱그린 원인을 어휘 수준에서 해부해, 예외 발생 자체를 상류에서 줄이는 처방(closed enum)의 근거를 만들었다. 법률적 논거도 타당하다: 하나의 사실이 대여금·승계책임·사해행위에 동시 연결되는 것은 모순이 아니라 대체적·예비적 청구를 보전하는 정상적 다중 라우팅이다. +- **multi-routing의 최종 인덱스 미반영(§5.3.1)**: LES3가 `by_bo_id`·`by_candidate_structure_type` 인덱스에 대표 타입 하나만 넣어, PRESERVE_MULTI_ROUTING 판정이 나와도 전체 canonical 타입이 인덱스로 전달되지 않는다. 코드 재확인 결과 사실이다(by_type은 `structure["candidate_structure_type"]`만 키로 사용). LLM 판정의 downstream 효용을 더 낮추는 실재 품질 결함. +- **asset ref-first 결정 규칙(§5.8)**: 자산 동일성은 fuzzy 라벨 유사도가 아니라 `REG-*`/`SCH-*` 등기행·별지목록 ref가 우선한다는 원칙. 실측 asset 예외 6건의 다수가 `SCH-016-01 ↔ 성수동 256 대` 같은 ref-라벨 쌍이므로, ref 우선 규칙만으로 예외가 소멸한다. 법률상으로도 권리객체 오병합을 막는 더 안전한 규칙. +- **`SPLIT_BATCH_THEN_BLOCK`의 명칭-실행 불일치(§5.4)**: phase 2가 DAG에 없어 실제로는 TRUNCATE_THEN_BLOCK이며, 사건이 복잡할수록 검토가 줄어드는 역설을 명시. +- **`pending_exception_ids` 상태 모델 설계(§7.1 P0)**: review 신호 복원을 boolean 해제가 아니라 exception ID 단위 상태 재계산(`expected = resolved ∪ blocked ∪ deterministic_review` coverage 등식)으로 설계 — Claude R-5보다 엄밀하다. + +**상충 판정 1건 — 공통 캐시 프리픽스.** Claude 보고서는 "Part 1/2 canonical과 바이트 동일(58f2b72d) — 유지 자산", Codex 보고서는 "Part 1·2 v3(3,385B, 6e01f72f)와 불일치 — 교체 필요"로 정반대다. 3-way 바이트 재실측 결과: + +| 파일 | 프리픽스 블록 | SHA-256 앞 12자리 | +|---|---:|---| +| Part 3 v1 (LES2) / Part 4 v2 | 3,829B (들여쓰기 포함 원형) | 58f2b72d6c26 | +| Part 2 Claude v3 (워커 5 + R1) | 3,829B | 58f2b72d6c26 | +| Part 1 Claude v3 / Part 1 Codex v3 | 3,385B (들여쓰기 제거형) | 6e01f72f1803 | + +즉 **두 보고서 모두 반쪽만 봤다**: Claude는 Part 2 라인만 대조해 "동일"로, Codex는 Part 1 계열만 대조해 "불일치"로 판정했다. 실상은 배포본 자체가 Part 1(3,385B) ↔ Part 2·3·4(3,829B)로 이원화되어 있다. 판정은 §2 X-11에서 확정한다(요지: 캐시 실익은 같은 모델 간에만 발생하므로, Part 3 L1은 직전 실행되는 같은 모델의 Part 2 R1 블록(3,829B)과 정합시키는 것이 적중 확률이 가장 높고, Part 1 GB의 3,829B 정렬은 별도 1줄 후속 작업으로 제안). + +### 1.2 실행구조·병목 비교 + +완전 직렬 4-task 체인이라는 구조 인식은 동일하다. 병목 규명은 상호보완적이다: Claude는 **전송 병목**을 바이트 흐름으로 계량했고(pack 3중 물질화 930KB, LES1 run_code 페이로드 297KB, LES3의 422KB 상시 재읽기, stdout 총 290KB), Codex는 **LLM 병목의 원인**을 계량했다(어휘 오염 → 50/50 예외 → 30건 monolithic 프롬프트 — 단일 대형 프롬프트의 항목 간 주의력 경쟁·JSON 누락 위험 지적 포함). Codex가 스스로 인정했듯 task별 타임스탬프가 없어 구간별 소요는 양쪽 다 추정이지만, $0 비용·3:40 실측에서 LLM 구간(reasoning high·30건 판정·28건 차단 사유 서술)과 대형 페이로드 왕복이 지배 요인이라는 결론은 공유된다. 종합하면 병목 제거는 두 축을 모두 요구한다: **(a) 예외를 애초에 만들지 않는 상류 정규화(Codex 축), (b) 그래도 남는 전송·호출의 구조적 소거(Claude 축).** + +### 1.3 삭제/축소 안전성 비교 (downstream 근거) + +최종 계약에 대한 판정은 완전히 일치한다: `legal_effect_structures.json`의 스키마·status·5-way index는 Part 4 FL0가 직접 검증하고 FL2는 LES 모호성을 재판정할 수 없으므로 **불변 계약 + review 신호 약화 금지**다. 중간 아티팩트 삭제 판정도 필드 단위로 일치한다(§1.1 공통 6·7). 양측 판정을 병합한 안전성 표는 다음과 같다. + +| 대상 | 판정 | 근거 (양측 합산) | +|---|---|---| +| 최종 파일명·스키마·5-way index | 변경 금지 (additive 확장만 허용) | Part 4 validator 직접 검증 (양측 일치) | +| BO/evidence conservation 게이트 | 유지·강화 | 구조 누락은 Fact Ledger 누락으로 전파 (Codex §6) | +| unresolved 예외·review 신호 | 삭제 금지, **재계산으로 정밀화** | FL2 재판정 불가 (양측); coverage 등식으로 봉인 (Codex P0) | +| pack의 bo_by_id·signals_by_bo_id·events·client_goal | 삭제 확정 | 소비 0 실측 (양측 독립 확인) | +| `evidence_all.json` read | **무조건 삭제로 격상** | Codex "조건부" + Claude 포함/제외 실험 기여 0 → 조건 충족 | +| `evidence_event_candidates.json`·`client_goal.json` read | 삭제 확정 | related_event_candidates까지 미소비 (Codex §5.5, Claude 소비표와 정합) | +| seed bundle의 진단·type_index_seed·내장 pack | 삭제/외부화 확정 | LES3 참조 0회 (양측); exception pack은 단일 authoritative 파일화 | +| LES3 SEED_RAW 주입 | 삭제 확정 | 100% 사장 실증 (Claude 고유) | + +### 1.4 권장 개정 전략 비교와 판정 + +| 쟁점 | Claude 제안 | Codex 제안 | 판정 | +|---|---|---|---| +| task 골격 | LES0+LES1 병합 → 3-task (L0→L1→L2) | 4-task 유지, 병합은 2차 보류 (§7.7) | **병합 채택.** Part 2 개정에서 publisher+domain_join+PostB_1→R0 3,000행 병합을 함수 단위 이식 + 골든 회귀로 이미 실증했고, LES0·LES1은 소비자가 파이프 내부뿐인 순수 결정론 체인이라 위험이 더 낮다. 병합 시에만 pack 3중 물질화(930KB)가 통째로 소멸한다. Codex의 책임 경계 우려는 코드 내부 함수 경계(컴파일러부/시드부)로 대응 | +| LLM 형태 | 단일 조건부 태스크 (Part 2 R1 골격) | wildcard fan-out (pack당 3~4건, 병렬 2) | **단일 조건부 태스크 채택.** 양측 모두 개정 후 예외 실질 0회를 기대하는 상황에서 fan-out 기계장치는 복잡도 과잉이다. Codex 자신의 pack 한도(항목 3~4·12KB)는 잔여 예외의 pack 구성 규칙으로 흡수하고, 사건 다양화로 예외가 상시 다건 재발하면 그때 wildcard로 확장한다(D-1) | +| 예외 축소 방법 | 보존적 defer 기본값 (PRESERVE/SEPARATE) + 4중 조건 pack | canonical enum 정규화 + ref-first 자산 규칙 + 진짜 비가역성만 LLM | **양측 병행 채택.** Codex 축이 예외 발생을 상류에서 줄이고(50→14 복수 role), Claude 축이 잔여를 결정론으로 처리해(대표타입 100% 일치 실증) LLM행을 0으로 만든다. 순서: 정규화 먼저, defer는 그 뒤 안전망 | +| review 모델 | 판정 반영해 review_required 해소 | pending_exception_ids + ID 단위 재계산 + coverage 등식 | **Codex 설계 채택** (Claude 방향과 동일하되 더 엄밀) | +| LES2 모델/사양 | flash-lite/low (R1 정합) + BLOCK_REVIEW 안전판 | flash-lite, reasoning medium, verbosity low | **flash-lite/low 채택.** 잔여 예외는 판정 불충분 시 BLOCK_REVIEW로 인간 검토에 안전하게 떨어지므로 low로 충분하며 Part 1 GB·Part 2 R1과 사양 문법이 통일된다. 비용은 이미 $0이므로 이 선택의 목적은 속도·일관성 | +| 프리픽스 | 현행 유지 (부분 오판) | 3,385B로 교체 (부분 오판) | **§3 정정 후 X-11로 확정**: L1은 Part 2 R1과 동일한 3,829B 블록(같은 모델·직전 실행·TTL 내 적중 가능). Part 1 GB의 3,829B 정렬은 별도 후속(D-2) | +| multi-routing 인덱스 반영 | (미포착) | named field + 전 타입 인덱싱 (§5.3.1) | **채택 (additive 한정).** 필수 필드 추가·기존 키 변경 없이 additive로만 — Part 4 validator는 필수 키만 검사하므로 양립 | + +## 2. 선별된 개정 전략 (최소 노력 / 최대 효과) + +### 2.1 개정 골격 — 4 task → 3 task + +```text +IN → L0_structure_seed_reducer (Python; LES0+LES1 병합. pack 비물질화·canonical 정규화· + | ref-first 자산 규칙·defer 정책·exception pack/slim seed 파일 write) + ↓ + L1_exception_adjudicator (LLM 조건부; gemini-3.1-flash-lite/low/low, preflight=pack 1파일. + | has_exceptions=false → no-exception 판정 파일 write 후 즉시 종료. + ↓ 실측 근거상 실질 0회) + L2_final_structure_index_writer (Python; SEED_RAW 제거, pending_exception_ids 재계산, + | multi-routing additive 인덱싱, conservation 게이트, 단독 writer) + ↓ + OUT +``` + +Part 1 v3(GA→GB→GC)·Part 2 v3(A0→워커→R0→R1→F0→S0)와 동일한 "결정론 reducer → 조건부 adjudicator → 결정론 writer" 문법으로 Stage 1 전체가 통일된다. + +### 2.2 채택 작업 목록 + +| ID | 작업 | 출처 | 핵심 근거 | 기대 효과 | +|---|---|---|---|---| +| X-1 | **canonical routing enum 정규화**: `candidate_structure_types`를 Part 4가 role로 해석하는 canonical 10종으로 제한하고, 비-routing 라벨(event/state·행위분류·세부사건명)은 `nonrouting_labels`로 보존(삭제 아님) | Codex P2 | 출현 263/distinct 69 → 복수 role BO 50→14 | 예외 발생 원인 소거 | +| X-2 | **multi-routing 보존 기본값**: canonical 복수 타입은 전부 보존 + 대표는 고정 priority — 복수 타입 자체는 예외 아님 | 양측 | 대표타입 50/50 결정론 일치 실증 (Claude) | type 예외 24건 → 0 | +| X-3 | **asset ref-first 동일성 규칙**: 동일 `REG-*`/`SCH-*` ref → 동일 클러스터 확정, Part 2 `asset_alias` ref-라벨 연결 활용, ref 상이 → 클러스터 분리+번들 보존, ref 없는 자유문구 → SEPARATE + review (임의 병합 금지) | Codex §5.8 + Claude 보존 기본값 | asset 예외 6건 다수가 ref-라벨 쌍 | asset 예외 6건 → ~0 | +| X-4 | **4중 조건 pack 수록 한정** (Part 2 X-3 이식): ①writer가 단일 값 선택 필수 ②source-backed 후보 2+ ③보존·defer 불가 ④compact 근거로 판정 충분 — 충족 항목만 LLM행. budget cap은 잔여가 소수이므로 사실상 소멸(초과 시 자동차단이 아니라 pack 분할을 기본으로 명세) | Claude + Codex §5.4 | LES2가 93%를 "근거 불충분" 차단 → ④ 사전 적용이 곧 그 판정 | LLM 호출 실질 0회, 17건 임의 절단 소멸 | +| X-5 | **pending_exception_ids 상태 모델**: seed의 boolean 대신 예외 ID 목록. L2가 resolved/blocked/split/auto_review를 ID별 재계산, `expected = resolved ∪ blocked ∪ deterministic_review` coverage 등식 강제, 미지 ID·중복 판정·후보 밖 값 hard fail | Codex P0 | 50/50 포화·큐 78건 실측 | review 신호 변별력 복원 — 변호사가 봐야 할 것만 큐에 | +| X-6 | **multi-routing의 최종 인덱스 반영 (additive)**: 구조물에 `candidate_structure_types[]` named field 추가, `by_candidate_structure_type`·`by_bo_id`를 전체 canonical 타입으로 생성. 기존 키·형태 유지 | Codex §5.3.1 | 보존 판정이 인덱스에서 소실되는 실결함 | Part 4 라우팅 정보 무손실 | +| X-7 | **L0 통합 + 입력·채널 다이어트**: LES0+LES1 병합, input pack 비물질화(파일·stdout·주입 3채널 소멸), 입력 read 8→5 (`evidence_all`·`client_goal`·`evidence_event_candidates` 제거 — 기여 0/소비 0 실측), stdout은 manifest 1줄 | Claude R-1·R-2 + Codex P1 | pack 3중 물질화 930KB·미사용 55% | 전송 최대 절감 + task/세션 1개 감소 | +| X-8 | **아티팩트 단일화**: exception pack을 유일한 authoritative 파일로(seed 내장 제거), slim seed bundle에는 `exception_pack_ref{path, sha256, expected_exception_ids}`만. 진단·type_index_seed 제거(감사 필요분은 소형 audit 파일) | 양측 + Codex §7.6 ref 설계 | seed 422KB 중 ~80KB 미소비 + pack 이중 저장 | seed bundle 422KB → 250KB 이하 | +| X-9 | **L2 dead code 제거**: SEED_RAW 주입 삭제, seed는 파일 읽기로 일원화. L1 출력 파싱에 §6.7 관용 파서(코드펜스 salvage — max_iterations 재호출 방지) | Claude P3-5·P3-15 | 주입 100% 사장 실증 | 32.6KB 낭비 제거 + 재호출 차단 | +| X-10 | **SKILL 정합 일괄**: read_docs raw_decode fallback, 실패 시 FAILED 1줄 + 비정상 종료(성공 exit 위장 금지), 검증 실패 시 기존 final file overwrite 금지, `r"""…"""` 주입, stdout 전 task 단일 JSON 라인 | 양측 | Part 1/2 v3 확립 기준 | 강건성·오류 전파 차단 | +| X-11 | **프리픽스**: L1에 Part 2 Claude v3 R1과 바이트 동일한 3,829B 블록 채택 (같은 flash-lite 모델·직전 Part 실행·TTL 내 캐시 적중 가능성 최대) | §3 정정 결과 | 3-way 실측 | 유일하게 캐시 실익 있는 선택 | + +### 2.3 기각·유보 항목 + +| ID | 항목 | 판정 근거 | +|---|---|---| +| N-1 | LES2 wildcard fan-out (Codex P4) | 기대 예외 0회 하에서 기계장치 과잉. 단일 조건부 태스크로 시작, pack 구성 한도(3~4건·12KB)는 X-4에 흡수 | +| N-2 | LES0·LES1 병합 보류 (Codex §7.7) | Part 2에서 동일 규모 병합의 방법론(함수 단위 이식+골든 회귀)이 이미 실증됨. 보류 시 pack 3중 물질화가 잔존해 최대 절감 항목을 놓침 | +| N-3 | 프리픽스 3,385B 교체 (Codex §5.9) | 배포 라인 이원화를 간과한 판정 — §3 정정. 단 Codex가 제기한 "교차 Part 캐시 정합" 문제의식 자체는 타당하며 X-11·D-2로 수용 | +| N-4 | reasoning medium (Codex §7.5) | BLOCK_REVIEW 안전판이 있으므로 low로 통일 (GB·R1 문법 정합). 판정 품질 우려는 골든 회귀와 D-1 모니터링으로 통제 | + +### 2.4 후속·범위 밖 제안 + +- **D-1**: 사건 유형 다양화 시 예외 발생률 모니터링 — 상시 다건 재발이 확인되면 그때 wildcard fan-out(Codex P4 설계 재사용)으로 확장. +- **D-2**: Part 1 v3의 GB 프리픽스를 3,829B 블록으로 정렬하는 1줄 수정(flash-lite 3형제 GB·R1·L1의 캐시 공유 완성) — Part 3 개정 범위 밖이므로 별도 작업으로. +- **D-3**: issue cluster가 7/10에서 BO당 1개(50:50)로 퇴화한 문제 — 클러스터링 키 고도화는 효율이 아닌 기능 개선 주제로 이관. + +### 2.5 검증 기준 (개정 작업 명세서에 반영할 DoD 골자) + +정적: YAML parse·task 3개·DAG 정합, embedded Python ast 전부 통과, SKILL 정합(boilerplate·hash·raw_decode·단일 JSON stdout·raw string), L1 프리픽스가 Part 2 R1과 바이트 동일, final writer 단일성. + +동적(7/10 fixture 골든 회귀): ① BO 50건이 final `by_bo_id`에 100% 존재 + evidence conservation ② 최종 파일이 Part 4 FL0 validator 무수정 통과 ③ 개정 전 인정된 canonical routing type 누락 0 (X-1의 nonrouting 라벨은 `nonrouting_labels`에서 전량 추적 가능) ④ coverage 등식 성립 ⑤ 단순 multi-routing만으로 review 생성 0 — 축소된 review 항목 전량이 audit에서 추적 가능 ⑥ 예외 실질 0회 → L1 즉시 종료 경로 ⑦ 2-run 바이트 결정론 ⑧ 주입 테스트(진짜 비가역 예외 주입 → pack 수록·L1 발동·BLOCK_REVIEW 시 L2 중단). + +성능 목표(실 재실행, cold/warm 구분 3회 median — Codex §11.3 방법론 채택): 중간 파일 891KB → 300KB 이하, stdout 290KB → ~1KB, LLM 실행 상시 1회 → 조건부 0회, 런타임 3:40 → 40% 이상 단축(목표 1분대), 비용 $0 유지. + +## 3. 정정 사항 (양 보고서의 부정확 지점 공개) + +**Claude 보고서 정정**: "LES2 프리픽스가 Part 1/2 canonical과 바이트 동일(58f2b72d)"이라는 판정은 **Part 2 Claude v3 기준으로만 참**이다. Part 1 Claude v3(및 Part 1 Codex v3)는 들여쓰기 제거형 3,385B(6e01f72f)를 사용하므로 Part 1 라인과는 불일치한다. "유지 자산" 결론은 X-11과 같이 조건부로 수정한다. + +**Codex 보고서 정정**: "Part 1·2 v3와 불일치하므로 3,385B 블록으로 교체해야 한다"는 판정은 **Part 2 Claude v3가 3,829B를 사용한다는 사실을 간과**했다. 3,385B로 교체하면 직전 실행 Part(Part 2)의 같은 모델 태스크(R1)와 어긋나 캐시 실익이 없다. 캐시는 모델 단위로 격리되므로, 실익이 있는 유일한 정합 대상은 같은 flash-lite 계열이며 그중 시간적으로 가장 가까운 것은 Part 2 R1(ttl 20m 내 도달)이다. + +이 상충은 배포본 자체의 이원화(Part 1: 3,385B ↔ Part 2·3·4: 3,829B)에서 비롯되었으며, 근본 해소는 D-2(Part 1 정렬)로 제안한다. + +## 4. 결론 + +채택 전략은 "**Codex의 원인 소거 + Claude의 구조 소거**"의 병행이다. canonical enum 정규화(X-1)와 ref-first 자산 규칙(X-3)이 예외의 발생 자체를 상류에서 제거하고, 보존적 defer(X-2·X-4)가 잔여를 결정론으로 처리하며 — 이 무손실성은 대표타입 50/50 일치라는 최종 산출물 실측으로 이미 보증되어 있다 — L0 통합(X-7)과 아티팩트 단일화(X-8·X-9)가 930KB의 유령 전송을 소거한다. pending_exception_ids 상태 모델(X-5)과 multi-routing 인덱싱(X-6)은 효율화를 넘어 현행의 실재 품질 결함(review 포화, 라우팅 정보 소실)을 바로잡는다. 최종 산출물 계약과 review 신호는 Part 4의 재판정 불가 원칙에 따라 additive 확장 외에는 손대지 않는다. 다음 단계는 본 선별 결과를 LLM이 100% 수행 가능한 개정 작업 명세서(`Part_3_Improvement_Strategy_Claude_v1.md`)로 구체화하는 것이다. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_3_Improvement_Codex_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_3_Improvement_Codex_v1.md new file mode 100644 index 00000000..a38a7b8c --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_3_Improvement_Codex_v1.md @@ -0,0 +1,521 @@ +# Stage 1 Part 3 최적 개정 전략 + +- 작성일: 2026-07-20 +- 비교 대상: + - `Part_3_Inefficiency_Report_Claude_v1.md` + - `Part_3_Inefficiency_Report_Codex_v1.md` +- 개정 대상: `Stage_1_Part_3_v1.yml` +- 최우선 기준: 최소 변경으로 법률적 정보 보존, LLM 호출 최소화, 토큰·전송량 절감, 실행속도 향상 + +## 1. 최종 판정 + +두 보고서는 핵심 원인에 관하여 실질적으로 일치한다. Part 3의 주된 비효율은 모델 성능보다 다음 구조에서 발생한다. + +1. LES1이 정상적인 multi-routing까지 대량의 예외로 만든다. +2. LES2는 예외 유무와 무관하게 항상 실행되며 최대 30건을 한 번에 판정한다. +3. LES2가 해결한 예외도 LES3에서 `review_required` 해제로 연결되지 않는다. +4. LES0 input pack과 LES1 seed/exception이 파일, stdout, template에 중복 물질화된다. +5. Part 4가 쓰지 않는 중간 필드와 Part 3 계산에 기여하지 않는 원문 입력을 계속 운반한다. + +7/10 실행물에서는 merge 후 예외 47건 중 30건이 LES2로 전달되고, 17건이 budget overflow로 차단되었다. LES2가 받은 30건 중 28건은 다시 `BLOCK_REVIEW`가 되었으며, 최종 구조 50건 전부가 review 대상으로 남았다. 따라서 **LES2 모델만 낮추는 조치는 핵심 해결책이 아니다.** + +가장 적합한 1차 개정은 다음 한 경로다. + +> LES0과 LES1의 책임 경계는 유지하되 payload를 축소하고, LES1에서 canonical multi-routing과 ref 기반 자산 처리를 결정적으로 수행하며, 진짜 비가역적 예외만 wildcard LES2에 보내고, LES3가 exception ID별 최종 review 상태를 다시 계산한다. + +이 경로는 현행 Part 4 계약을 유지하면서 가장 적은 구조 변경으로 가장 큰 효과를 얻는다. + +## 2. 두 보고서 비교·채택 결과 + +### 2.1 공통 결론: 전부 채택 + +| 공통 판정 | Claude | Codex | 최종 채택 | +|---|---|---|---| +| LES0 pack의 파일/stdout/template 중복 | 지적 | 지적 | stdout manifest화, payload 단일화 | +| LES1 미사용 input 필드 | 지적 | 지적 | 삭제 | +| `evidence_all.json`의 Part 3 기여 없음 | byte 동일 재현 | 조건부 삭제 | coverage gate와 함께 read 제거 | +| seed bundle의 미소비·중복 필드 | 지적 | 지적 | authoritative exception pack 한 곳만 유지 | +| LES2 무조건 실행 | 지적 | 지적 | 조건부 wildcard로 전환 | +| type 예외 과다 | 47건 실측 | 69종/263회 원인 분석 | canonical enum + multi-routing 기본 보존 | +| budget 30건 초과 17건 auto-block | 지적 | 지적 | hard cap 절단 폐기, micro-pack fan-out | +| LES2 판정 효용 부족 | 28/30 block | 동일 | true exception만 LLM 사용 | +| review queue 50/50 포화 | 지적 | 지적 | `pending_exception_ids` 기반 재계산 | +| Python failure가 `sys.exit(0)` | 지적 | 지적 | FAILED 출력 후 raise/non-zero | +| Part 4 final 계약 보존 | 지적 | 지적 | file/schema/5개 index 불변 | + +### 2.2 차이점과 최종 선택 + +#### A. LES0·LES1 즉시 병합 여부 + +- Claude: 즉시 `L0_structure_seed_reducer`로 병합한다. +- Codex: 1차 개정에서는 유지·축소하고, 계측 후 2차 병합한다. + +**최종 선택: 즉시 병합하지 않는다.** + +거짓 예외 제거와 LES2 무호출 fast path가 전체 runtime에서 가장 큰 효과를 낸다. LES0·LES1 병합은 MCP 세션 1회와 중간 read/write를 더 줄일 수 있지만, source pack compilation과 legal routing seed 생성의 책임을 한 70KB 이상 코드에 결합한다. 최소 작업이라는 본 요청의 기준상 1차 개정에서는 payload 축소만 시행한다. 개정 후 LES0→LES1이 Part 3 runtime의 15% 이상이면 2차로 병합한다. + +#### B. LES2 조건부 실행 방식 + +- Claude: 하나의 조건부 adjudicator가 exception pack을 preflight하고 no-exception이면 종료한다. +- Codex: `dynamic_fanout: []`를 사용하는 wildcard adjudicator로 만든다. + +**최종 선택: wildcard `Task_LES2_exception_adjudicator_*`를 사용한다.** + +LLM task 내부의 no-exception 분기는 provider 요청 자체를 없애지 못할 수 있다. 반면 Part 1·2 v3에서 이미 사용하는 wildcard 문법은 `dynamic_fanout: []`일 때 LLM instance를 0개로 만든다. 남은 예외도 독립 micro-pack으로 병렬 처리할 수 있다. + +#### C. LES2 모델 하향 여부 + +- Claude: 조건부 0회화를 우선하고 모델 하향은 A/B 검증 후 적용한다. +- Codex: `gemini-3.1-flash-lite`, medium reasoning, low verbosity를 권고한다. + +**최종 선택: flash-lite/medium/low를 기본값으로 적용하되 동일 fixture 회귀를 통과시킨다.** + +잔여 LES2는 raw evidence를 읽거나 법률상 최종 결론을 내리는 task가 아니다. 제공된 후보와 closed enum 사이에서 routing decision만 내린다. Part 1·2의 예외 adjudicator와 동일한 모델 수준이면 충분하다. 회귀에서 decision coverage 또는 blocked rate가 악화될 때만 해당 pack 유형을 `gemini-3.5-flash`로 되돌린다. + +#### D. common cache prefix 판정 + +Claude 보고서는 Part 3 prefix가 upstream canonical과 같다고 보았고, Codex 보고서는 Part 1·2 Codex v3와 다르다고 보았다. 실제 파일 hash는 다음과 같다. + +| 파일 | bytes | SHA-256 | +|---|---:|---| +| Part 1 Claude/Codex v3 | 3,385 | `6e01f72f1803767b6bb16b5aef6cf34760e6dc1b293c316c75e990d09ad5fe63` | +| Part 2 Codex v3 | 3,385 | `6e01f72f1803767b6bb16b5aef6cf34760e6dc1b293c316c75e990d09ad5fe63` | +| Part 2 Claude v3 | 3,829 | `58f2b72d6c2650739c45d85cc57899150434db86a97bf19cd5a59d92d11dfbb5` | +| Part 3 v1 | 3,829 | `58f2b72d6c2650739c45d85cc57899150434db86a97bf19cd5a59d92d11dfbb5` | + +현재 연계 기준인 Part 1·2 Codex v3에 맞춰 Part 3은 **3,385-byte `6e01...fe63` block을 그대로 복사**한다. 내용이 비슷한 것이 아니라 byte-identical해야 한다. 향후 Part 2 Claude v3를 production으로 선택한다면 Part 2까지 한꺼번에 같은 canonical block으로 통일해야 한다. + +#### E. multi-routing의 final 반영 범위 + +Claude는 deterministic `PRESERVE_MULTI_ROUTING`을 채택했고, Codex는 현행 LES3가 복수 type을 `routing_audit`에만 남기고 final index에는 대표 type 하나만 반영하는 문제까지 확인했다. + +**최종 선택: Codex의 보강안을 채택한다.** + +모든 canonical type을 final structure의 `candidate_structure_types[]`, `by_bo_id.candidate_structure_types`, `by_candidate_structure_type`에 반영한다. Part 4가 요구하는 5개 index의 이름과 object 형태는 바꾸지 않는다. 이 보강이 없으면 `PRESERVE_MULTI_ROUTING`이 선언에 그치고 downstream 전달은 불완전하다. + +#### F. asset ambiguity 처리 + +- Claude: 기본값을 `SEPARATE_CLUSTER + review`로 둔다. +- Codex: `REG-*`/`SCH-*`와 Part 2 `asset_alias`를 우선하여 결정적으로 동일성을 해소한다. + +**최종 선택: 두 안을 결합한다.** + +정확한 ref 연결은 결정적으로 병합하고, 서로 다른 ref는 별도 cluster와 multi-asset bundle로 보존한다. ref 없는 자유문구는 임의 병합하지 않고 `SEPARATE_CLUSTER + review`를 기본값으로 한다. 이 방식이 등기·별지목록상 권리객체를 잘못 합치는 위험을 가장 낮춘다. + +## 3. 선별된 필수 개정 작업 + +| 우선순위 | 개정 묶음 | 노력 | 기대 효과 | 채택 | +|---|---|---:|---:|---| +| P0 | review 상태 재계산 + exception exact coverage | 중 | 품질 매우 큼, 무효 LLM 판정 제거 | 즉시 | +| P0 | canonical multi-routing + ref-first asset 처리 | 중 | 예외 47건의 대부분 제거 | 즉시 | +| P1 | LES0 5-file diet + compact stdout/artifact | 낮음~중 | 전송·parse·MCP I/O 대폭 축소 | 즉시 | +| P1 | LES2 conditional wildcard micro-pack | 중 | no-exception LLM 0회, 잔여 예외 병렬화 | 즉시 | +| P1 | seed/exception 중복 제거 | 낮음 | 중간 파일·재읽기 축소 | 즉시 | +| P2 | Python/SKILL 강건성 정비 | 낮음 | 재시도·silent failure 방지 | 즉시 | +| P3 | LES0+LES1 물리적 병합 | 높음 | Python task 1회 추가 절감 | 계측 후 보류 | + +### 3.1 최우선: review 상태 모델 수정 + +현행 seed의 단일 `review_required` boolean을 다음 구조로 바꾼다. + +```json +{ + "pending_exception_ids": [], + "deterministic_review_codes": [], + "resolved_exception_ids": [], + "blocked_exception_ids": [] +} +``` + +역할은 다음과 같이 분리한다. + +- LES1: `pending_exception_ids`와 deterministic review code만 생성 +- LES2: 자신에게 배정된 exception ID의 decision만 생성 +- LES3: expected/decision/defer/block 집합을 대조하여 최종 상태 계산 +- LES3 final `review_required`: unresolved, `BLOCK_REVIEW`, review-sensitive split, deterministic defer인 경우에만 `true` + +다음 conservation을 hard gate로 둔다. + +```text +expected_exception_ids + == resolved_exception_ids + ∪ deterministic_resolution_ids + ∪ blocked_exception_ids + ∪ deferred_review_exception_ids +``` + +집합은 서로 중복되면 안 된다. 성공적으로 해결된 exception만 있었던 BO는 review queue에서 제거한다. + +### 3.2 canonical structure type과 deterministic multi-routing + +Part 4가 실제 module/role에 사용하는 type만 routing enum으로 인정한다. + +```text +money_claim +commercial_successor +secured_debt +registry_invalidity +land_use_gain +valuation +fraudulent_transfer +preserved_claim +succession_notice_lien_asset_defense +general_legal_effect +``` + +다음 값은 법률적 의미가 있더라도 routing type으로 쓰지 않는다. + +- `event`, `state` +- `법률행위`, `사실행위`, `준법률행위`, `소송행위` +- `변제`, `상계`, `상속포기`, `소유권이전` 등 세부 사건·행위 label +- Part 4 mapping에 없는 임시 transport tag + +이 값들은 삭제하지 않고 `nonrouting_labels[]` 또는 signal audit에 보존한다. + +복수 canonical type은 다음과 같이 처리한다. + +1. 모든 type을 보존한다. +2. 대표 type은 고정 priority로 선택한다. +3. 모든 type에 대응하는 `legal_effect_roles`를 생성한다. +4. final `by_bo_id`와 `by_candidate_structure_type`에 전부 반영한다. +5. 단순 복수 role이라는 이유로 exception을 만들지 않는다. + +type exception은 다음 4개 조건을 모두 충족할 때만 생성한다. + +1. LES3가 단일값 또는 split을 선택해야 final write가 가능하다. +2. source-backed 후보가 2개 이상이다. +3. 전체 보존 또는 Stage 2 defer가 불가능하다. +4. 제공할 compact context만으로 안전한 LLM 판정 가능성이 있다. + +하나라도 충족하지 않으면 LLM에 보내지 않는다. 정보 보존이 가능하면 `PRESERVE_MULTI_ROUTING`, 불충분하면 식별 가능한 deterministic review로 남긴다. + +### 3.3 ref-first asset identity + +LES0에서 다음 deterministic map을 만든다. + +```text +asset_alias_by_ref[REG-*|SCH-*] + -> canonical_label_candidate + -> evidence_index + -> registry/schedule metadata +``` + +Part 2의 `extensions.domain_payload.asset_alias`에 있는 `schedule_refs`와 `registry_refs`도 같은 map에 결합한다. + +판정 순서는 다음과 같다. + +1. 동일 `REG-*`/`SCH-*`: 같은 asset cluster +2. alias map이 ref와 자유문구를 명시적으로 연결: 같은 cluster +3. 서로 다른 ref: 별도 cluster +4. 하나의 BO가 여러 ref를 포함: 각 cluster를 보존하고 multi-asset bundle 생성 +5. ref 없는 자유문구만 존재: `SEPARATE_CLUSTER + deterministic review` +6. 상충하는 source-backed ref 때문에 merge 여부가 권리효과를 바꾸고 defer할 수 없는 경우만 LES2 exception + +fuzzy 문자열 유사도만으로 자산을 병합하지 않는다. + +### 3.4 LES0 input diet와 compact channel + +Part 3 필수 read를 다음 5개로 제한한다. + +```text +BO.json +evidence_indexed.json +actio_case_signals.json +case_liability_signals.json +legal_effect_signals.json +``` + +Part 3 read에서 제거한다. + +- `client_goal.json`: LES1/LES3가 사용하지 않음 +- `evidence_all.json`: Claude의 byte-identical 재실행에서 evidence authority 기여 0 +- `evidence_event_candidates.json`: `related_event_candidates`가 LES1/LES3에서 사용되지 않음 + +단, 제거 후 다음 gate를 통과해야 한다. + +```text +input BO ids == candidate BO ids == seed BO ids +input evidence refs contains all seed evidence refs +every REG/SCH ref used by a seed resolves to evidence_indexed or BO asset_alias +``` + +LES0 input pack에는 BO별 compact candidate만 저장한다. 다음 중복 필드는 제거한다. + +- top-level `bo_by_id` +- top-level `signals_by_bo_id` +- `events_by_evidence_index` +- `client_goal_compact` +- `related_event_candidates` + +stdout은 payload가 아니라 다음 manifest 한 줄만 반환한다. + +```json +{ + "les0_manifest": { + "schema_version": "stage1_legal_structure_input_manifest.v2", + "status": "READY", + "artifact_path": "stage1_tmp/legal_effect_structure_input_pack.json", + "run_fingerprint": "sha256:...", + "bo_count": 50, + "candidate_count": 50 + } +} +``` + +LES1은 manifest의 path/hash를 검증한 뒤 파일을 한 번 읽는다. 정상 경로와 fallback 경로를 중복 운영하지 않는다. + +### 3.5 seed/exception artifact 단일화 + +LES1 산출물은 역할별로 다음과 같이 나눈다. + +| Artifact | 포함 내용 | 제외 내용 | +|---|---|---| +| slim seed bundle | structure seeds, issue map, asset map, evidence index seed, pending IDs | full diagnostics, final type index, embedded exception pack | +| exception pack | exception items, audit/defer rows, pack plan, fingerprint | full seed/BO/signal | +| LES1 stdout | manifest + `dynamic_fanout` | seed/exception 전체 본문 | + +seed bundle에서 제거한다. + +- `candidate_structure_type_index_seed`: LES3가 final structure에서 재생성 +- full `ambiguity_diagnostics`: exception pack audit로 이전 +- embedded `legal_structure_exception_pack`: 별도 pack을 authoritative source로 사용 +- LES3가 사용하지 않는 full `multi_asset_bundles`: 필요한 bundle ref/member만 seed item 또는 exception context에 남김 + +LES3는 seed와 exception pack의 path, schema, SHA-256, `run_fingerprint` 일치를 확인한다. + +### 3.6 LES2를 conditional wildcard micro-adjudicator로 전환 + +task 이름은 다음과 같이 바꾼다. + +```text +Task_LES2_exception_adjudicator_* +``` + +LES1 stdout은 실제 LLM 대상이 있을 때만 `dynamic_fanout`에 pack을 넣는다. + +```json +{ + "les1_manifest": { + "status": "READY", + "seed_path": "stage1_tmp/legal_effect_structure_seed_bundle.json", + "exception_pack_path": "stage1_tmp/legal_effect_structure_exception_pack.json", + "pack_count": 0, + "run_fingerprint": "sha256:..." + }, + "dynamic_fanout": [] +} +``` + +pack 규칙은 다음과 같다. + +| 항목 | 값 | +|---|---:| +| items per pack | 최대 3~4 | +| serialized bytes | 최대 12 KB | +| candidate values per item | 최대 8 | +| `max_concurrency` | 2 | +| model | `gemini-3.1-flash-lite` | +| reasoning | `medium` | +| verbosity | `low` | +| max iterations | 2 | +| tools | `localdocs`만 허용 | +| preflight | `false` | +| cache TTL | `1h` | + +각 wildcard instance는 자신의 `item`만 읽고 다음 한 파일만 쓴다. + +```text +stage1_tmp/legal_effect_structure_adjudication_parts/PACK-###.json +``` + +허용 decision은 exception type별 closed enum으로 제한하고, selected value와 source ref는 input 후보의 부분집합이어야 한다. 출력 본문은 파일로 저장하고 stdout은 `pack_id`, status, output path, decision count만 반환한다. + +### 3.7 LES3 final gate/writer 강화 + +LES3는 다음 입력만 읽는다. + +- slim seed bundle +- authoritative exception pack +- 존재하는 모든 adjudication part + +DAG에서 다음을 기다린다. + +```text +Task_LES1_deterministic_structure_seed_builder +all Task_LES2_exception_adjudicator_* +``` + +LES3 수행 순서는 다음과 같다. + +1. 세 artifact의 schema/fingerprint/hash 검증 +2. expected exception과 decision/defer/block exact coverage 검증 +3. unknown·duplicate exception ID 및 후보 밖 value 차단 +4. 최종 review 상태 재계산 +5. 모든 canonical type을 final structure와 type index에 투영 +6. BO/evidence/issue/asset/type index conservation 검증 +7. `legal_effect_structures.json` 단독 write +8. write 후 재읽기와 schema/coverage 재검증 + +LES1 stdout을 `SEED_RAW`로 주입하는 dead path는 삭제한다. + +### 3.8 Python/SKILL 강건성 정비 + +LES0, LES1, LES3에 공통 적용한다. + +- localdocs direct MCP의 `user_id`/`workspace_id` hash 전달 유지 +- `read_docs` JSON parse에 `JSONDecoder.raw_decode` fallback 적용 +- template 삽입이 남는 경우 raw string 사용 +- FAILED compact JSON 출력 후 `raise` 또는 non-zero 종료 +- 코드펜스가 포함된 adjudication JSON은 보수적으로 salvage하고 warning 기록 +- final writer 검증 실패 시 기존 final file overwrite 금지 +- stdout은 한 줄 compact JSON으로 제한 + +## 4. 최종 권장 DAG + +```text +IN + | + v +LES0 compact input compiler (Python) + | - 5 files only + | - compact artifact + manifest + v +LES1 deterministic seed/exception planner (Python) + |\ + | +-- true LLM exceptions --> LES2_exception_adjudicator_* (0..N, max 2 parallel) + | | + | +-- decision part files + | + +-- no exception: dynamic_fanout = [] + | + +------------------------------+ + v + LES3 final gate/writer (Python) + - waits LES1 + all LES2_* + - exact coverage/review recomputation + - writes final file only + | + v + OUT +``` + +정의 task 수는 LES0, LES1, LES2 wildcard template, LES3의 4개를 유지한다. 정상 no-exception 실행에서는 wildcard instance가 생성되지 않으므로 실제 실행 task는 Python 3개다. + +## 5. 삭제·유지·보류 최종 판정 + +### 5.1 즉시 삭제 또는 축소 + +| 대상 | 판정 | 이유 | +|---|---|---| +| Part 3의 `client_goal.json` read | 삭제 | downstream seed/writer 미사용 | +| Part 3의 `evidence_all.json` read | 삭제 + coverage gate | 실측 기여 0, structured evidence가 대체 | +| Part 3의 `evidence_event_candidates.json` read | 삭제 | 관련 event projection 미사용 | +| LES0 full-pack stdout | 삭제 | file artifact와 중복 | +| top-level `bo_by_id`/`signals_by_bo_id` | 삭제 | 미사용 또는 candidate 내부 중복 | +| `candidate_structure_type_index_seed` | 삭제 | LES3 재생성 가능 | +| seed 내 full diagnostics/exception pack | 삭제 | authoritative exception pack으로 통합 | +| LES3 `SEED_RAW` 주입 | 삭제 | 현행 root가 달라 항상 fallback | +| 30건 monolithic LES2 | 삭제 | wildcard micro-pack으로 대체 | +| 성공 후에도 남는 seed review boolean | 삭제 | pending/resolved 상태로 대체 | + +### 5.2 반드시 유지·강화 + +| 대상 | 판정 | 이유 | +|---|---|---| +| `legal_effect_structures.json` file/schema | 유지 | Part 4 직접 소비 | +| 5개 `structure_index` | 유지·내용 강화 | BO/Fact Ledger join surface | +| BO/evidence coverage | 강화 | 누락 전파 방지 | +| issue/asset/type exception audit | 유지 | Part 4 재판단 금지 | +| source IDs와 REG/SCH refs | 원형 유지 | 법률상 객체·근거 추적 | +| LES3 단독 writer | 유지 | 중복·경합 방지 | +| LLM `BLOCK_REVIEW` 경로 | 유지 | 불충분 근거에서 추론 강행 방지 | + +### 5.3 1차 개정에서 보류 + +| 대상 | 보류 이유 | 재검토 조건 | +|---|---|---| +| LES0+LES1 물리적 병합 | 책임 경계와 대형 코드 변경 위험 | 개정 후 해당 구간이 runtime 15% 이상 | +| LES2 완전 삭제 | 다른 사건에서 진짜 identity/split 예외 가능 | 다수 fixture에서 0건이 장기 확인될 때 | +| issue clustering 기능 고도화 | 효율화 범위를 넘어서는 법률 모델 변경 | 별도 정확도 과제로 수행 | +| Part 4 구조 개정 | 현행 계약으로 충분 | 회귀에서 multi-routing 소비 누락 발견 시 | + +## 6. 구현 순서 + +### Phase 1: 품질 결함 제거 + +1. canonical routing enum과 nonrouting label 분리 +2. deterministic multi-routing 및 ref-first asset 규칙 적용 +3. seed를 `pending_exception_ids` 기반으로 변경 +4. LES3 review 재계산과 all-type index 생성 + +### Phase 2: 데이터 이동 축소 + +1. LES0 read를 5개로 축소 +2. compact candidate pack과 manifest stdout 적용 +3. slim seed/authoritative exception pack 분리 +4. LES3 dead `SEED_RAW` 제거 + +### Phase 3: LLM fast path + +1. LES2를 wildcard template으로 변경 +2. micro-pack size/count gate 적용 +3. flash-lite/medium/low 및 canonical cache prefix 적용 +4. LES3의 all-wildcard coverage 검증 연결 + +### Phase 4: 강건성·회귀 + +1. raw_decode, non-zero failure, tolerant JSON parser 적용 +2. YAML parse 및 모든 embedded Python compile +3. 7/10 fixture golden regression +4. cold/warm 3회 runtime·token·cache 계측 + +## 7. 검증 및 합격 기준 + +### 7.1 정적 검증 + +- YAML parse 성공 +- dangling task/DAG reference 0 +- embedded Python compile 성공 +- direct MCP user/workspace context 누락 0 +- 모든 LLM wildcard의 common prefix SHA-256이 `6e01...fe63` +- LES1의 `dynamic_fanout`과 LES3의 `all Task_LES2_exception_adjudicator_*` 연결 확인 +- `legal_effect_structures.json` writer가 LES3 하나뿐임 + +### 7.2 기능·법률 품질 검증 + +| 검증 항목 | 합격 기준 | +|---|---| +| BO conservation | source BO IDs = seed BO IDs = final `by_bo_id` IDs | +| evidence conservation | 모든 seed evidence ref가 final evidence index에 존재 | +| asset ref conservation | 모든 REG/SCH ref가 원형 유지되고 미지 ref 0 | +| canonical type conservation | source-backed canonical type 누락 0 | +| multi-routing | 모든 canonical type이 structure와 2개 type 관련 index에 반영 | +| exception coverage | expected = resolved + deterministic + deferred + blocked | +| review precision | 해결된 예외만 있는 구조는 review에서 제거 | +| unresolved safety | 불충분·상충 예외는 식별 가능한 ID로 review 유지 | +| Part 4 호환성 | 현행 Part 4 FL0 validator 무수정 통과 | +| 결정론 | 동일 입력 2회 실행 결과 byte-identical | + +### 7.3 성능 합격 기준 + +| 지표 | 현행 | 목표 | +|---|---:|---:| +| input pack file | 416,115 B | 200 KB 이하 | +| seed bundle file | 422,236 B | 250 KB 이하 | +| LES0 stdout | 257 KB 수준 | 2 KB 이하 | +| LES1 stdout | 32.6 KB 수준 | no-exception 4 KB 이하 | +| LLM 호출 | 항상 1회 | true exception pack 수만큼, fixture 목표 0회 | +| auto-block by item cap | 17건 | 0건 | +| blanket structure review | 50/50 | 0건; 실제 review 사유만 유지 | +| Part 3 runtime | 3분 40초 | 3회 median 40% 이상 단축 | +| 비용 | `$0` | 증가 금지 | + +성능 결과는 task별 elapsed time, input/output token, cache hit, pack count, serialized bytes를 함께 기록한다. 전체 runtime만 측정하면 다음 병목을 다시 찾을 수 없다. + +## 8. 최종 실행 지침 + +1차 개정에서는 **task 병합보다 false exception 제거와 true zero-LLM fast path를 우선**한다. 이 두 작업이 현행 3분 40초 중 가장 큰 LLM·review 낭비를 제거한다. + +구조유형은 하나를 억지로 고르는 대상이 아니라 source-backed canonical type을 모두 보존하는 routing surface로 취급한다. 자산은 ref를 우선하고, 동일성이 불확실하면 병합보다 분리를 선택한다. 이는 처리속도를 높이면서 청구원인·항변·담보·사해행위 구조가 Stage 2에서 누락되는 위험도 줄인다. + +최종 개정 성공의 기준은 단순히 LES2 호출 수가 줄어드는 것이 아니다. `legal_effect_structures.json`의 외부 계약과 BO/evidence/type 보존을 유지하면서, LLM이 실제로 필요한 예외만 보고, 최종 review queue가 변호사가 실제로 검토할 항목만 남기는 상태가 되어야 한다. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_3_Improvement_Strategy_Claude_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_3_Improvement_Strategy_Claude_v1.md new file mode 100644 index 00000000..67dab900 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_3_Improvement_Strategy_Claude_v1.md @@ -0,0 +1,206 @@ +# Stage 1 Part 3 개정 작업 명세서 (Claude v1) + +- 개정 대상: `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_3_v1.yml` (2,639행, task 4개) +- 산출물: `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_3_Claude_v3.yml` (신규 파일 — v1 원본은 수정·이동·삭제 금지) +- 근거 문서: `Part_3_Improvement_Claude_v1.md` (채택 전략 X-1~X-11, 기각 N-1~N-4, 후속 D-1~D-3) +- 작성 관점: 20년 이상 경력의 대한민국 최고 수준 변호사 + 세계적 수준의 AI 아키텍트. 본 명세서는 LLM이 읽고 추가 판단 없이 100% 수행할 수 있도록 작성되었다. 명세에 없는 "개선"의 임의 추가는 금지한다(§12-9). + +--- + +## 0. 개정 목표와 원칙 + +1. **골격 전환**: `LES0→LES1→LES2(상시 LLM)→LES3` 4-task 직렬 체인을 `L0(결정론 reducer)→L1(조건부 LLM adjudicator)→L2(결정론 final writer)` 3-task로 재편한다. Part 1 v3(GA→GB→GC)·Part 2 v3(R0→R1→F0)와 동일한 문법이다. +2. **예외의 상류 소거**: canonical routing enum 정규화(X-1)와 ref-first 자산 규칙(X-3)으로 예외 발생 자체를 제거하고, 잔여는 보존적 defer(X-2)와 4중 조건 pack(X-4)으로 결정론 처리한다. 7/10 fixture 기준 LLM 호출 실질 0회가 목표이며, 이 무손실성은 "대표 타입 50/50 결정론 일치·split 0" 실측으로 이미 보증되어 있다. +3. **전송의 구조 소거**: input pack 3중 물질화(파일 416KB + stdout 257KB + 코드 주입 257KB)와 LES3의 사장된 SEED_RAW 주입(32.6KB)을 제거하고, stdout은 전 task 1줄 요약 JSON으로 제한한다. +4. **품질 결함 교정**: pending_exception_ids 상태 모델(X-5)로 review 신호 변별력을 복원하고, multi-routing을 최종 인덱스에 additive로 반영한다(X-6). +5. **불변 계약 준수**: `legal_effect_structures.json`의 파일명·schema_version·필수 필드·5-way index·status enum은 변경 금지(additive 확장만 허용). Part 4 FL0 validator가 무수정 통과해야 한다. +6. **SKILL.md 100% 준수**: 모든 Python task는 `YAML_Prompts/1. Stage_1/SKILL.md`의 제약(localdocs 보일러플레이트, `{{__user_hash__}}`/`{{__workspace_hash__}}`, read_docs raw_decode fallback, stdout 단일 JSON 라인, `r"""…"""` 템플릿 주입)을 준수한다. +7. **프리픽스(X-11)**: L1의 `COMMON_CACHE_PREFIX_STAGE_1`은 **현행 Part 3 LES2 블록을 바이트 그대로 유지**한다. 이 블록(3,829B, sha256 앞 12자리 `58f2b72d6c26`)은 Part 2 Claude v3의 R1과 바이트 동일하며, 같은 flash-lite 모델로 직전 Part에서 실행되는 R1과의 캐시 정합이 유일한 실익이다. 들여쓰기 제거·재정렬 금지. (Part 1 GB의 3,829B 정렬은 D-2 — 본 개정 범위 밖.) + +--- + +## 1. 불변 계약 (변경 금지 목록) + +1. **최종 파일** `legal_effect_structures.json`: 파일명, `schema_version: "stage1_legal_effect_structures.v1"`, `status ∈ {READY, READY_WITH_REVIEW}`, `legal_effect_structures[]`의 필수 필드 13종(structure_id, candidate_structure_type, structure_label, source_bo_ids, issue_cluster_ids, asset_cluster_ids, evidence_indexes, linked_claim_group_ids, linked_liability_group_ids, linked_actio_signal_ids, legal_effect_roles, basis, review_required), `structure_index`의 5개 섹션(by_bo_id, by_issue_cluster_id, by_asset_cluster_id, by_evidence_index, by_candidate_structure_type), `quality_gate`(bo_coverage·evidence_coverage·review_queue). **키 삭제·이름 변경·타입 변경 금지. 신규 키 추가(additive)만 허용.** +2. **ID 규칙**: `REG-*`/`SCH-*`/BO ID(bh#)/evidence index(E-###)/claim·liability group id/signal id는 절대 renumber·창작 금지. `LES-###`/`IC-###`/`AC-###`는 L2가 현행과 동일한 결정론 순서로 부여한다. +3. **LLM 판정 계약**: 결정 enum(현행 LES2 OUTPUT_CONTRACT의 exception_type별 enum과 BLOCK_REVIEW), "후보 밖 값 생성 금지", "불충분 시 BLOCK_REVIEW" 원칙 유지. +4. **review 신호**: unresolved/blocked/split 예외는 review_queue에서 삭제 금지. 축소(해소)되는 항목은 전량 audit 파일에서 추적 가능해야 한다. +5. Part 1/2/4 YAML은 수정하지 않는다. + +--- + +## 2. 현행 구조 매핑 (개정 대상 앵커) + +| 현행 task (v1 시작 행) | 처분 | +|---|---| +| `Task_LES0_input_pack_compiler` (53행) | **L0로 흡수** — 컴파일 함수군 이식, pack 물질화 폐지 | +| `Task_LES1_deterministic_structure_seed_builder` (724행) | **L0로 흡수** — seed/예외 생성 로직 이식 + X-1/X-2/X-3/X-4/X-5 개정 | +| `Task_LES2_single_cluster_exception_adjudicator` (1665행) | **L1로 대체** — 조건부·pack 파일 입력·flash-lite/low. 프리픽스 블록(1679~1721행)은 바이트 유지 | +| `Task_LES3_final_structure_index_writer` (1894행) | **L2로 개정** — SEED_RAW 제거, 상태 재계산, additive 인덱싱 | +| `task_procedure` (21행) | **전면 교체** (§10) | + +**코드 이식 원칙**: 흡수되는 로직은 폐기가 아니라 **함수 단위 이식**이다. LES0의 `_compact_bo`·`_actio_signals_by_bo`·`_case_signals_by_bo`·`_legal_routes_by_bo`·`_collect_evidence_authority`(evidence_all 인자 제거판)·`_normalize_structure_type`·`_asset_kind`·`_asset_strings`·`_extract_asset_candidates`·`_dedupe_assets`, LES1의 `_score_issue`·`_hard_issue_keys`·`_issue_decision`·`_asset_decisions`(X-3 개정)·`_structure_type_decision`(X-2 개정)·`_merge_exceptions`·`_add_issue_seed`·`_add_asset_seeds`·`_add_evidence_index`·`_norm_text`·`_asset_norm`·`_legal_unit_tokens`, LES3의 `_validate_decisions`·`_validate_decision_enum`·`_validate_decision_subsets`·`_issue_ids_for_item`·`_asset_ids_for_item`·`_type_plan_for_item`·`_build_structures`·`_build_structure_index`·`_quality_gate`·`_validate_payload`를 각각 L0/L2로 옮기고, 중복 MCP 보일러플레이트만 1벌로 줄인다. 개정 지점은 본 명세서가 명시한 곳으로 한정한다. + +--- + +## 3. STEP 0 — 사전 검증 (v3 작성 전 필수) + +1. **STEP 0-1**: v1에서 §2의 앵커 4개 + `task_procedure`(21행) 존재 확인. 불일치 시 중단·보고. +2. **STEP 0-2**: fixture(`Results_July_10_3_15pm`)에 L0 입력 5종(`BO.json`, `evidence_indexed.json`, `actio_case_signals.json`, `case_liability_signals.json`, `legal_effect_signals.json`)과 골든 산출물(`legal_effect_structures.json`, sha256 앞 12자리 `021b5d8f8ec2`) 및 중간 실행물 3종(input pack 416,115B / seed bundle 422,236B / exception pack 52,982B)이 존재함을 확인한다(§11 회귀에 사용). +3. **STEP 0-3**: L1 프리픽스 canonical을 v1의 1679~1721행 `…` 블록에서 바이트 추출하고, Part 2 Claude v3의 R1 블록과 바이트 동일함을 확인한다(불일치 시 중단·보고 — X-11 전제 붕괴). +4. **STEP 0-4**: preflight 파일 채널(Plan A) 가용성은 Part 2 v3 배포와 공유되는 전제조건이다. Part 2 v3의 STEP 0-2 채널 테스트가 이미 통과되어 있으면 재검 불요. 미확인 상태면 동일한 소형 테스트(파일 1개 write → `preflight: true` + `preflight_files`로 flash-lite 미니 task가 내용을 인용하는지 1회 확인)를 수행한다. **실패 시 Plan B**: L1은 `preflight: false`로 두고 L0 stdout에 exception pack 요약(`has_exceptions`, `exceptions[]`)을 포함시켜 `{{prev.Task_L0_structure_seed_reducer.json_output}}` 템플릿으로 주입한다(예외 0이면 요약이 수십 바이트에 불과). + +--- + +## 4. 신규 데이터 계약 (파일 5종) + +| 파일 | writer | 소비자 | 핵심 스키마 | +|---|---|---|---| +| `stage1_tmp/legal_effect_structure_seed_bundle.json` | L0 | L2 | `{legal_structure_seed_bundle: {schema_version: "stage1_legal_structure_seed_bundle.v2", status, structure_seed_items[], issue_cluster_seed_map, asset_cluster_seed_map, evidence_index_seed, multi_asset_bundles[], exception_pack_ref: {path, sha256, expected_exception_ids[]}}}` — **v1의 `ambiguity_diagnostics`·`candidate_structure_type_index_seed`·내장 exception pack은 수록 금지** | +| `stage1_tmp/legal_effect_structure_exception_pack.json` | L0 | L1 (preflight) | Part 2 pack 형식으로 교체: `{schema_version: "stage1_legal_structure_exception_pack.v2", has_exceptions, exception_count, exceptions[], budget}` — exception 항목은 v1 필드(exception_id `LEX-###`, exception_type, source_bo_ids, candidate_values, basis_fields, score_table→score_summary 상위 3, conflict_flags, allowed_decisions, merged_from_count, compact_context) 유지. **X-4 4중 조건 통과 항목만 수록** | +| `stage1_tmp/legal_effect_structure_adjudication.json` | L1 | L2 | `{legal_structure_exception_adjudication: {…}}` — v1 LES2 OUTPUT_CONTRACT의 루트·필드·enum 그대로(스키마 버전 `stage1_legal_structure_exception_adjudication.v1` 유지). L2의 `_validate_decisions` 이식 호환 | +| `stage1_tmp/legal_effect_structure_audit.json` | L0 (L2가 review 추적 필드 추가) | 인간 감사 | `{schema_version: "stage1_legal_structure_audit.v1", nonrouting_labels_by_bo_id, deterministic_resolutions[], merge_groups[], review_reduction_trace[]}` — 소형(목표 40KB 이하). §1-4의 추적성 요건을 이 파일이 담보 | +| `legal_effect_structures.json` | L2 (유일 writer) | Part 4 FL0 | **§1-1 불변 + additive**: 구조물에 `candidate_structure_types[]`·`pending_exception_state` 추가, `structure_index.by_candidate_structure_type`을 전체 canonical 타입으로 생성(§9-4) | + +폐지: input pack 파일(`stage1_tmp/legal_effect_structure_input_pack.json`)은 쓰지 않는다. + +stdout 규약(SKILL §3.3): L0/L2는 **단일 JSON 라인**만 출력한다(경로·sha·count·status·has_exceptions). pack·bundle 내용을 stdout에 넣지 않는다. + +--- + +## 5. STEP 1 — L0 명세 (`Task_L0_structure_seed_reducer`) + +code-executor 엔트리(language python, requirements httpx, network agent-network, timeout 180). MCP 보일러플레이트는 v1 LES1(747~888행)을 1벌만 이식하고 `clientInfo.name`을 `"stage-1-legal-effect-l0-structure-seed-reducer"`로 변경. SKILL 필수사항(user/workspace hash, **read_docs raw_decode fallback — v1의 strict `json.loads` 2곳을 SKILL §6.4 GOOD 패턴으로 교체**) 적용. + +1. **입력 read 5종**: `BO.json`, `evidence_indexed.json`, `actio_case_signals.json`, `case_liability_signals.json`, `legal_effect_signals.json`. **`evidence_all.json`·`client_goal.json`·`evidence_event_candidates.json`은 읽지 않는다**(기여 0/소비 0 실측). `_collect_evidence_authority`는 evidence_all 인자를 제거하고 evidence_indexed + BO.Evidence 2원으로 동작(포함/제외 산출 동일 실측이 근거). `_require_signal_root` 검증 3종 유지. +2. **컴파일(LES0 이식)**: `_compact_bo`·signal 매핑 3종·evidence authority·candidate_inputs 구성을 메모리 내 변수로만 생성한다. **pack 파일 write·stdout 발행·중간 dict의 bo_by_id/signals_by_bo_id/events_by_evidence_index/client_goal_compact 구성은 전부 제거**(미사용 실측). `_compact_bo`의 반환 필드 중 candidate_inputs가 실제 소비하는 필드(keyword_fields 구성원, provenance.source_domain, downstream_seed_refs, domain_payload 소비 키, source_evidence_indexes, Evidence excerpt)는 유지한다. +3. **X-1 canonical enum 정규화**: `CANONICAL_STRUCTURE_TYPES = [money_claim, commercial_successor, secured_debt, registry_invalidity, land_use_gain, valuation, fraudulent_transfer, preserved_claim, actio_remedy_or_cap, succession_notice_lien_asset_defense]` + fallback `general_legal_effect`를 closed enum으로 선언한다. `_candidate_structure_types`는 기존 소스(legal_effect_signals·case_liability·actio_scope 매핑·domain tag·source_domain)를 유지하되, `_normalize_structure_type` 결과가 **enum에 속하는 값만** `candidate_structure_types`에 넣고, 속하지 않는 값(`event`/`state`, 행위 분류, 세부 사건명 등)은 삭제하지 말고 `nonrouting_labels`로 분리 보존한다(audit 파일 §4). v1의 "정규화 실패 문자열 그대로 반환"(474행) 경로를 제거한다. +4. **X-2 multi-routing 결정론**: `_structure_type_decision`을 개정한다 — canonical 타입이 복수여도 **예외를 생성하지 않는다**. 전 타입 보존 + `STRUCTURE_TYPE_PRIORITY`(v1 757~769행 순서 유지)로 대표 선정 + 전 타입의 downstream role 부여. type 예외는 §5-6의 4중 조건을 통과하는 경우(현행 스키마에서 상호 배타 필드 충돌 또는 source-backed split 필요성이 구체적 근거로 존재)로만 한정한다. v1의 "role 2종 이상이면 예외"(1172~1185행) 규칙을 삭제한다. +5. **X-3 asset ref-first 규칙**: `_asset_decisions`를 개정한다 — (a) 동일 `REG-*`/`SCH-*` ref 공유 → 동일 클러스터 확정(현행 keyed 그룹핑 유지). (b) **BO `domain_payload.asset_alias`의 `schedule_refs`/`registry_refs` ↔ `alias_strings`/`canonical_label_candidate` 연결을 alias map으로 구축**하여, unkeyed 라벨이 alias로 keyed ref와 연결되면 동일 클러스터로 결정론 병합한다(예외 아님 — 7/10 asset 예외 6건의 해소 경로). (c) 하나의 BO에 서로 다른 ref 복수 → 클러스터 분리 + multi_asset_bundle 보존(현행 로직 유지, 예외 생성만 제거). (d) ref 없는 자유문구 간 동일성 불명 → **`SEPARATE_CLUSTER` 보존 + review code**(LLM 금지, fuzzy 병합 금지). asset 예외는 4중 조건 통과 시로만 한정. +6. **X-4 exception pack 수록 4중 조건**: ① L2가 진행하려면 단일 값 선택이 필수 ② source-backed 후보 2개 이상 ③ 보존·defer로 처리 불가 ④ compact payload(항목당 ≤2KB)로 판정 충분. 전부 충족하는 항목만 pack에 수록한다. **budget 자동 차단(`budget_overflow`) 로직을 제거한다** — 수록 항목이 방어 상한(30건 또는 직렬화 40KB)을 넘으면 초과분을 자동 차단이 아니라 `deterministic_review`로 라우팅하고 exception_id를 유지한 채 audit·review 추적에 명시 항목으로 남긴다(임의 절단·추적성 공백 금지). `_merge_exceptions`(서명 병합)와 `LEX-###` 부여는 이식 유지. +7. **X-5 상태 모델**: structure_seed_items에서 `review_required` boolean 대신 **`pending_exception_ids: []`**(해당 seed에 걸린 LEX id 목록)와 `deterministic_review_codes: []`(defer 정책이 남긴 review 사유)를 기록한다. issue/asset/evidence seed map의 `review_required`도 동일 원칙으로 대체한다. +8. **issue 결정(현행 유지)**: `_issue_decision`의 hard-key 우선·score 규칙은 7/10에서 예외 0건이므로 로직 유지. `auto_blocked`(후보 전무) 항목은 `deterministic_review_codes: ["missing_candidate_values"]`로 기록. +9. **출력**: §4의 seed bundle(v2, slim)·exception pack(v2)·audit 파일 3종 write + stdout 1줄: `{"status", "message", "seed_bundle_path", "exception_pack_path", "audit_path", "seed_count", "has_exceptions", "exception_count", "deterministic_resolution_count", "nonrouting_label_count"}`. +10. **FAILURE_POLICY**: 입력 누락·파싱 불가 시 파일 미작성, FAILED 단일 JSON 라인 출력 후 **raise**(`sys.exit(0)` 금지 — P3-14 교정). + +--- + +## 6. STEP 2 — L1 명세 (`Task_L1_exception_adjudicator`) + +LLM task 엔트리: + +```yaml + - task_name: Task_L1_exception_adjudicator + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: low + llm_verbosity: low + max_iterations: 1 + use_tools: [localdocs] + cache_control: {mode: auto, ttl: 20m} + preflight: true + preflight_files: + - stage1_tmp/legal_effect_structure_exception_pack.json +``` + +프롬프트 구성: STEP 0-3에서 추출한 canonical `COMMON_CACHE_PREFIX_STAGE_1`(3,829B, **바이트 그대로** — X-11) + 신규 STATIC_BLOCK(Part 2 v3 R1 골격): + +1. MISSION: "결정론 reducer(L0)가 non-deferrable로 판정한 compact exception만 판정한다. 파일 병합·최종 파일 작성·사실 창작 금지." +2. EXECUTION: ① preflight로 제공된 pack의 `has_exceptions` 확인 ② **false이면 no-exception 판정 객체**(`{"legal_structure_exception_adjudication": {"schema_version": "stage1_legal_structure_exception_adjudication.v1", "status": "READY_NO_EXCEPTIONS", "decisions": [], "blocked_review_items": [], "adjudication_gate": {...}}}`)를 `write_file(overwrite=true)`로 `stage1_tmp/legal_effect_structure_adjudication.json`에 저장하고 `"NO_EXCEPTIONS"`만 출력 후 즉시 종료(terminate) ③ true이면 각 exception을 `compact_context`만으로 판정(추가 read 금지) ④ 판정 규칙·enum·필드 규칙은 v1 LES2의 DECISION_RULES·OUTPUT_CONTRACT(1789~1892행)를 이식(exception_type별 enum 제한, 후보 밖 값 금지, subset 규칙, 불충분 시 BLOCK_REVIEW) ⑤ 결과를 위 경로에 write ⑥ `"L1 예외 판정 완료 (decisions=<건수>)"`만 출력 후 종료. +3. ABSOLUTE_FORBIDDEN: pack 외 파일 read / `legal_effect_structures.json`·seed bundle·audit 작성 / 새 BO·evidence·structure·cluster ID·사실 창작 / pack에 없는 exception_id / markdown fence. +4. FAILURE_POLICY: pack 부재·파싱 불가 시 판정 파일을 쓰지 않고 `FAILED: <사유>`만 출력. tool 오류는 1회만 재시도. + +Plan B(STEP 0-4 실패 시): `preflight: false`, `{{prev.Task_L0_structure_seed_reducer.json_output}}` 주입으로 대체하고 L0 stdout에 pack 요약을 포함시킨다(§3-4). + +--- + +## 7. STEP 3 — L2 명세 (`Task_L2_final_structure_index_writer`) + +code-executor(timeout 180). v1 LES3(1894~2638행) 이식 + 다음 개정: + +1. **입력 채널 정리(X-9)**: `SEED_RAW`·`ADJUDICATION_RAW` 템플릿 주입을 **제거**한다(SEED_RAW는 100% 사장 실증). 입력은 파일 read 3종: seed bundle(v2) + exception pack(v2, `exception_pack_ref.sha256` 대조) + adjudication 파일. adjudication 파일 내용 파싱에는 SKILL §6.7 관용 파서(코드펜스 salvage 후 strict 검증)를 적용한다. +2. **판정 검증(이식+개정)**: `_expected_exceptions`는 seed bundle 내장 pack 대신 **exception pack 파일**에서 읽는다. `_validate_decisions`의 커버리지·enum·subset·중복·미지 ID hard fail은 이식 유지하되, expected 집합과의 등식을 X-5 모델로 확장한다: `expected_exception_ids == resolved_ids ∪ blocked_ids` (L0의 deterministic_review 항목은 pack에 없으므로 별도 집합). +3. **X-5 review 상태 재계산**: 구조물별 `review_required`를 다음으로만 산정한다 — 해당 seed의 `pending_exception_ids` 중 **blocked**(BLOCK_REVIEW 판정 또는 blocked_review_items)가 존재, 또는 `deterministic_review_codes` 존재, 또는 split 구조. **비차단 판정으로 해소된 예외는 review를 발생시키지 않는다.** review_queue에는 (a) blocked exception(식별 항목), (b) deterministic_review 항목(식별 항목 — v1에서 누락되던 자동차단 추적성 복원), (c) review_required 구조만 남긴다. 해소된 항목은 audit 파일의 `review_reduction_trace[]`에 {exception_id, 해소 방법, 근거}로 전량 기록한다(§1-4). +4. **X-6 multi-routing additive 인덱싱**: 구조물 top-level에 `candidate_structure_types[]`(routing_audit에만 있던 전 타입)를 추가하고, `structure_index.by_candidate_structure_type`을 **전체 canonical 타입 기준으로** 생성한다(구조가 보유한 모든 타입에 대해 해당 타입 키 아래 등록 — 기존 대표 타입 항목의 필드 구조는 유지). `by_bo_id` row의 `candidate_structure_types`도 전 타입 합집합으로 채운다. 기존 키·오브젝트 형태는 변경하지 않는다(additive). +5. **빌더·게이트 이식 유지**: `_build_structures`(LES-/IC-/AC- 부여 순서 동일), `_build_structure_index`(5-way), `_quality_gate`(BO/evidence coverage), `_validate_payload`(교차 참조 검증 — additive 필드 검증 추가), write 전 검증 → write → 재읽기 검증 순서 유지. **검증 실패 시 기존 final file을 overwrite하지 않는다**(검증을 write 앞에 두는 현행 순서 유지가 곧 이 보장). +6. **stdout 1줄**: `{"status", "message", "written_file", "payload_status", "structure_count", "review_queue_count", "resolved_exception_count", "blocked_exception_count"}`. FAILURE_POLICY는 §5-10과 동일(FAILED 1줄 + raise). + +--- + +## 8. 모델·프리픽스 규칙 요약 + +| task | 유형 | 모델/사양 | 프리픽스 | +|---|---|---|---| +| L0 | code-executor | — | — | +| L1 | LLM 조건부 | gemini-3.1-flash-lite / low / low / max_iterations 1 | 현행 LES2 블록 바이트 유지 (= Part 2 R1과 동일, 58f2b72d) | +| L2 | code-executor | — | — | + +v1 헤더에 stage-level LLM 선언은 없으므로 관련 조치 불요. LES2의 `gemini-3.5-flash/high/medium/2` → L1 사양으로 교체하는 것은 N-4 판정(BLOCK_REVIEW 안전판 전제)에 따른 것이며, 잔여 예외의 판정 품질 우려는 §11 주입 테스트와 D-1 모니터링으로 통제한다. + +--- + +## 9. task_procedure (전면 교체) + +```yaml + task_procedure: + IN: + nexts: [Task_L0_structure_seed_reducer] + wait_until: [] + Task_L0_structure_seed_reducer: + nexts: [Task_L1_exception_adjudicator] + wait_until: [IN] + Task_L1_exception_adjudicator: + nexts: [Task_L2_final_structure_index_writer] + wait_until: [Task_L0_structure_seed_reducer] + Task_L2_final_structure_index_writer: + nexts: [OUT] + wait_until: [Task_L1_exception_adjudicator] + OUT: + nexts: [] + wait_until: [Task_L2_final_structure_index_writer] +``` + +--- + +## 10. 수용 기준 (Definition of Done) + +### 10.1 정적 검사 + +1. `Stage_1_Part_3_Claude_v3.yml` YAML parse 성공, task 수 3, task_procedure가 §9와 일치, unreachable task 0, 폐기 참조(`SEED_RAW`, `ADJUDICATION_RAW`, `legal_effect_structure_input_pack`, `{{prev.Task_LES0…}}`, `{{prev.Task_LES1…}}`, `budget_overflow`) 잔존 0. +2. embedded Python 전부 `ast.parse` 통과, SKILL 준수(boilerplate·hash placeholder·raw_decode fallback·stdout 단일 JSON 라인·`r"""…"""`·실패 시 raise). +3. `legal_effect_structures.json` writer 정확히 1개(L2), exception pack·seed bundle·audit writer 정확히 1개(L0), adjudication writer 1개(L1). +4. L1 프리픽스가 Part 2 Claude v3 R1 블록과 **바이트 동일**. +5. L1 사양 §8 일치(flash-lite/low/low/1, preflight pack 1파일). + +### 10.2 fixture 회귀 (7/10 실행물 입력, 코드 로컬 실행) + +1. **L0**: 입력 5종만 read, pack 파일 미생성, seed 50건, `has_exceptions=false`(X-1~X-4 적용 결과 — 7/10의 47건 예외가 전부 결정론 해소), nonrouting_labels에 비-canonical 라벨 전량 보존, seed bundle ≤250KB. +2. **L1 경로**: has_exceptions=false → no-exception 판정 파일 + 즉시 종료 시뮬레이션. +3. **L2 골든 동등**: 최종 `legal_effect_structures.json`이 현행 골든(021b5d8f8ec2) 대비 — (a) 구조 50건·`LES-###` 순서·**대표 candidate_structure_type 50건 완전 일치** (b) source_bo_ids/evidence_indexes/linked_* 필드 동일 (c) IC/AC 매핑의 결정론 동등 (d) additive 필드(candidate_structure_types 등)와 review 재계산 차이만 허용 — review 축소분은 audit `review_reduction_trace`에서 전량 추적 가능 (e) BO 50건 by_bo_id 100% + evidence coverage PASS (f) **Part 4 FL0 validator 무수정 통과**(로컬 실행) (g) canonical 타입 보존: 개정 전 최종 산출의 canonical routing 타입 집합이 개정 후 candidate_structure_types 합집합의 부분집합. +4. **coverage 등식**: `expected_exception_ids == resolved ∪ blocked` + deterministic_review 전량 식별 항목화. 단순 multi-routing만으로 생성된 review 0건. +5. **2-run 바이트 결정론** (L0/L2 산출 전 파일). +6. **주입 테스트**: ① 진짜 비가역 type 충돌(상호 배타 필드가 실제 충돌하는 합성 케이스) 주입 → pack 수록·L1 발동 경로 ② ref 상충 자산 주입 → 결정론 분리 보존(pack 미수록) ③ L1 BLOCK_REVIEW 시뮬레이션 → L2 review_queue에 식별 항목 반영 ④ adjudication 파일 부재/코드펜스 → L2가 관용 파서로 salvage 또는 FAILED(최종 파일 미작성) ⑤ 커버리지 등식 위반(누락 판정) → L2 hard fail. +7. **성능 실측(사전 추정)**: 중간 파일 합계·stdout 합계를 §10.3 목표와 대조해 기록. + +### 10.3 성능 합격 기준 (실 재실행, cold/warm 구분 3회 median) + +중간 파일 891KB → 300KB 이하, stdout 290KB → 2KB 이하, LLM 실행 상시 1회 → 조건부(정상 fixture 0회), 런타임 3:40 → 40% 이상 단축, 표시 비용 $0 유지. task별 elapsed·token·cache hit 기록 권장. **§10.2 품질 불변식 위반 시 성능 달성해도 실패.** + +--- + +## 11. 개정 보고서 기재 의무 + +v3 저장 시 다음을 최종 보고에 명시한다: (1) 7/10 기준 예외 47→0의 해소 내역(X-1 정규화로 소거된 건수 / X-3 alias 병합 건수 / defer 보존 건수), (2) review_queue 78→N의 축소 내역과 audit 추적 경로, (3) Plan A/B 중 적용 채널, (4) D-1(예외 발생률 모니터링)·D-2(Part 1 GB 프리픽스 정렬)·D-3(issue cluster 고도화)의 후속 이관. + +## 12. 금지 사항 최종 목록 + +1. v1 원본 덮어쓰기·이동·삭제 금지 (v3는 신규 파일). +2. §1 불변 계약(최종 파일 스키마·5-way index·ID 규칙·판정 enum) 변경 금지 — additive만 허용. +3. Part 1/2/4 YAML 수정 금지. +4. canonical 타입을 하나만 남기는 축소 금지 — 모든 타당한 multi-routing 타입 보존. +5. 자산의 fuzzy 문자열 병합 금지 — ref/alias 근거 없는 병합은 SEPARATE 보존. +6. unresolved/blocked 예외의 review 삭제 금지, 해소 항목의 무추적 삭제 금지(audit 필수). +7. L1에 pack 외 입력 제공 금지, L1의 최종 파일·seed·audit 작성 금지, 후보 밖 값 생성 금지. +8. nonrouting 라벨의 파괴적 삭제 금지(audit 보존 필수). +9. 본 명세서에 없는 "개선"의 임의 추가 금지 — 범위 밖 아이디어는 개정 보고서에 제안으로만 기록. +10. 실패의 `sys.exit(0)` 위장 금지, 검증 실패 상태에서 최종 파일 overwrite 금지. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_3_Improvement_Strategy_Codex_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_3_Improvement_Strategy_Codex_v1.md new file mode 100644 index 00000000..0b16f8a9 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_3_Improvement_Strategy_Codex_v1.md @@ -0,0 +1,1548 @@ +# Stage 1 Part 3 v1 개정 작업 명세서 + +## 0. 문서의 지위와 실행 명령 + +이 문서는 `Part_3_Improvement_Codex_v1.md`의 통합 권고를 실제 YAML 개정 작업으로 변환한 **완료 조건이 있는 구현 명세서**다. 구현자는 이 문서를 참고자료, 선택지 또는 의사코드 모음으로 취급하지 않는다. 아래에서 “유지”, “삭제”, “교체”, “금지”, “반드시”로 표현한 지시를 그대로 실행한다. + +### 0.1 입력, 출력, 작업 경계 + +| 구분 | 경로 | 처리 원칙 | +|---|---|---| +| 개정 원본 | `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_3_v1.yml` | 읽기 전용. 직접 덮어쓰지 않는다. | +| 설계 근거 | `YAML_Prompts/1. Stage_1/v.6/Part_3_Improvement_Codex_v1.md` | canonical routing, ref-first asset, conditional LES2, compact artifact 원칙을 적용한다. | +| Python 제약 | `YAML_Prompts/1. Stage_1/SKILL.md` | direct MCP, JSON parsing, template, wildcard, failure 제약을 모두 적용한다. | +| canonical prefix | `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_1_Codex_v3.yml` | ``을 바이트 동일하게 복제한다. | +| upstream 계약 | `Stage_1_Part_1_Codex_v3.yml`, `Stage_1_Part_2_Codex_v3.yml` | BO, evidence, signal schema와 ref를 보존한다. | +| downstream 계약 | `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_4_v2.yml` | final file과 5개 index 계약을 변경하지 않는다. | +| 회귀 fixture | `YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm` | 개정 전 기준값과 의미·성능을 비교한다. | +| 개정 결과 | `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_3_Codex_v2.yml` | 원본 전체를 복제한 뒤 이 명세서가 허용한 부분만 수정한다. | + +다음 파일은 이번 작업에서 수정하지 않는다. + +- `Stage_1_Part_3_v1.yml` +- `Stage_1_Part_1_Codex_v3.yml` +- `Stage_1_Part_2_Codex_v3.yml` +- `Stage_1_Part_4_v2.yml` +- `Results_July_10_3_15pm` 아래의 모든 파일 +- `YAML_Prompts/1. Stage_1/SKILL.md` +- 기존 Claude/Codex 분석·전략 문서 + +### 0.2 최종 목표 + +현재 4개 task definition의 책임 경계는 유지하되 LES2를 wildcard template으로 교체한다. + +```text +Task_LES0_input_pack_compiler [Python] +Task_LES1_deterministic_structure_seed_builder [Python] +Task_LES2_exception_adjudicator_* [0..N LLM] +Task_LES3_final_structure_index_writer [Python] +``` + +runtime task 수는 다음과 같아야 한다. + +```text +no-exception 경로: 3개 Python task +exception pack N개 경로: 3개 Python task + N개 LLM task +``` + +개정의 직접 목표는 다음과 같다. + +1. 단순 multi-routing을 type exception으로 만들지 않는다. +2. `REG-*`/`SCH-*`/Part 2 asset alias로 해결 가능한 자산 동일성을 Python이 처리한다. +3. true exception이 없으면 LES2 LLM instance를 하나도 생성하지 않는다. +4. true exception은 최대 3~4건, 12KB 이하 micro-pack으로만 전달한다. +5. 해결된 exception은 final review queue에서 제거한다. +6. Part 3 read를 8개에서 5개 파일로 줄인다. +7. full input pack과 seed/exception 중복을 stdout/template에 싣지 않는다. +8. final `legal_effect_structures.json`의 기존 외부 계약을 보존한다. + +### 0.3 목표 DAG + +```text +IN + | + v +Task_LES0_input_pack_compiler [Python] + | compact input artifact + one-line manifest + v +Task_LES1_deterministic_structure_seed_builder [Python] + |\ + | +--> Task_LES2_exception_adjudicator_* [0..N LLM] + | each instance reads one micro-pack + | each instance writes one decision part + | + +------ dynamic_fanout=[] when no true exception + | + +------------------------------------+ + v +Task_LES3_final_structure_index_writer [Python] + | waits LES1 + all LES2_* + | exact coverage + review recomputation + | final single writer + v +OUT +``` + +## 1. 절대 준수 원칙 + +### 1.1 법률·품질 안전 원칙 + +1. Stage 1은 최종 승패, 청구 인용 여부 또는 확정 법률효과를 판단하지 않는다. +2. source-backed canonical type은 하나만 선택해 버리지 않고 모두 보존한다. +3. 복수 canonical type은 원칙적으로 모순이 아니라 downstream multi-routing이다. +4. 자산은 fuzzy 문자열보다 `REG-*`, `SCH-*`, 명시적 `asset_alias`를 우선한다. +5. 서로 다른 ref를 임의로 같은 자산으로 병합하지 않는다. +6. 근거가 부족한 자산 자유문구는 `SEPARATE_CLUSTER`와 review code로 보존한다. +7. BO ID, evidence index, claim/liability/signal ID, REG/SCH ref를 renumber하지 않는다. +8. LLM은 assigned micro-pack 밖의 파일, 사실, 후보 또는 ID를 사용할 수 없다. +9. unresolved exception은 Part 4가 재판단하지 않으므로 Part 3에서 식별 가능한 상태로 봉인한다. +10. 성능 개선 때문에 BO/evidence/type coverage를 낮추지 않는다. + +### 1.2 deterministic 작업 원칙 + +다음 작업은 반드시 Python이 수행한다. + +- 파일 read와 schema/status 검증 +- strict JSON parse 및 `raw_decode` fallback +- canonical JSON serialization과 SHA-256 +- run fingerprint 생성·검증 +- BO/evidence/source ref universe와 subset 검사 +- signal을 BO별 compact projection으로 변환 +- canonical type normalization +- source domain fallback type 부여 +- representative type priority 선택 +- 모든 canonical type의 multi-routing 보존 +- REG/SCH/asset alias map 생성 +- issue/asset/type seed 및 stable ID 생성 +- exact ref identity와 conservative separation +- exception 필요조건 판정 +- micro-pack grouping, byte/count 검사, dynamic fanout 생성 +- exception/decision exact coverage 검사 +- final structure/index/review queue 생성 +- 최종 serialization, write, reread 검증 + +LLM은 Python으로 보존·분리·defer할 수 없고 finalization에 실제로 필요한 compact exception만 판정한다. + +### 1.3 단일 writer 권한 + +| 산출물 | 유일한 writer | +|---|---| +| `stage1_tmp/legal_effect_structure_input_pack.json` | LES0 | +| `stage1_tmp/legal_effect_structure_seed_bundle.json` | LES1 | +| `stage1_tmp/legal_effect_structure_exception_pack.json` | LES1 | +| `stage1_tmp/legal_effect_structure_adjudication_parts/PACK-*.json` | 해당 LES2 wildcard instance | +| `legal_effect_structures.json` | LES3 | + +한 파일을 둘 이상의 task가 쓰게 만들지 않는다. LES2는 final file과 seed/exception pack을 수정할 수 없다. + +### 1.4 외부 계약 불변 + +다음 final 계약은 변경하지 않는다. + +```text +file: legal_effect_structures.json +schema_version: stage1_legal_effect_structures.v1 +status: READY | READY_WITH_REVIEW +root keys: + schema_version + status + legal_effect_structures + structure_index + quality_gate +``` + +`structure_index`의 다음 5개 named object를 모두 유지한다. + +```text +by_bo_id +by_issue_cluster_id +by_asset_cluster_id +by_evidence_index +by_candidate_structure_type +``` + +기존 필드를 삭제하거나 이름을 바꾸지 않는다. `candidate_structure_types[]`와 quality-gate audit 필드는 additive하게 추가할 수 있다. + +### 1.5 MCP와 보안 + +1. `tools.mcpServers`의 기존 server type, URL, description, 인증 header는 변경하지 않는다. +2. 새 API key, token, credential을 추가하지 않는다. +3. 기존 인증 값을 stdout, report 또는 exception에 출력하지 않는다. +4. Python direct MCP initialize의 `clientInfo`에 `{{__user_hash__}}`, `{{__workspace_hash__}}`를 반드시 포함한다. +5. LES2에는 `localdocs`만 허용하고 다른 tool을 추가하지 않는다. + +### 1.6 금지된 개정 + +- LES0과 LES1을 1차 개정에서 합치지 않는다. +- LES2를 완전히 삭제하지 않는다. +- 검증되지 않은 `run_if`, `skip_if`, `when` 키를 만들지 않는다. +- 단일 상시 LES2 task를 유지하지 않는다. +- full BO, full signal, full seed bundle을 LLM prompt나 wildcard `item`에 넣지 않는다. +- 30건 global cap으로 뒤의 exception을 자동 절단하지 않는다. +- unknown type label을 canonical type으로 그대로 승격하지 않는다. +- fuzzy label만으로 서로 다른 자산을 병합하지 않는다. +- LLM이 새 BO/evidence/structure/cluster/ref ID를 만들게 하지 않는다. +- `legal_effect_structures.json`을 LES3 이외 task가 쓰게 하지 않는다. +- placeholder, 생략 주석, `기존 코드 동일`, 의사코드를 최종 YAML의 Python 또는 prompt에 남기지 않는다. +- Python 실패 후 `sys.exit(0)`을 사용하지 않는다. + +## 2. 원본 구조와 정확한 개정 매핑 + +### 2.1 Agent header + +1. `Agent.name`은 유지한다. +2. `Agent.description`은 유지한다. +3. `Agent.version`을 `v2.0`으로 변경한다. +4. Stage name과 description은 유지한다. +5. `tools.mcpServers` 블록은 byte-level 수정을 피하고 그대로 유지한다. +6. Stage-level LLM provider/model을 새로 추가하지 않는다. + +### 2.2 task 정의 매핑 + +| 현재 task | 처리 | 새 책임 | +|---|---|---| +| `Task_LES0_input_pack_compiler` | 이름 유지·code 전면 개정 | 5-file compact source compiler | +| `Task_LES1_deterministic_structure_seed_builder` | 이름 유지·code 전면 개정 | deterministic seed, review, exception planner | +| `Task_LES2_single_cluster_exception_adjudicator` | 삭제 | wildcard LES2로 대체 | +| `Task_LES2_exception_adjudicator_*` | 신규 | true micro-exception만 판정 | +| `Task_LES3_final_structure_index_writer` | 이름 유지·code 전면 개정 | decision reduce, review recompute, final writer | + +task definition 수는 정확히 4개다. + +### 2.3 내부 schema version + +내부 artifact는 의미가 바뀌므로 v2 schema를 사용한다. + +```text +stage1_legal_structure_input_pack.v2 +stage1_legal_structure_seed_bundle.v2 +stage1_legal_structure_exception_pack.v2 +stage1_legal_structure_adjudication_part.v2 +stage1_legal_structure_input_manifest.v2 +stage1_legal_structure_les1_manifest.v2 +``` + +final schema만 `stage1_legal_effect_structures.v1`을 유지한다. + +## 3. canonical common cache prefix + +### 3.1 source와 기대 hash + +`Stage_1_Part_1_Codex_v3.yml`의 첫 `` 시작 태그부터 종료 태그까지를 그대로 복제한다. + +```text +bytes: 3385 +sha256: 6e01f72f1803767b6bb16b5aef6cf34760e6dc1b293c316c75e990d09ad5fe63 +``` + +공백, 줄바꿈, indentation, 태그 순서를 변경하지 않는다. + +### 3.2 적용 대상 + +`Task_LES2_exception_adjudicator_*` prompt의 맨 앞에 이 prefix를 둔다. static task block, `{{item.*}}`, output schema는 종료 태그 뒤에 둔다. + +현재 Part 3 v1의 3,829-byte `58f2...bb5` prefix는 제거한다. + +### 3.3 cache 설정 + +```yaml +cache_control: + mode: auto + ttl: 1h +``` + +## 4. 모든 Python task의 공통 구현 제약 + +### 4.1 task header + +LES0, LES1, LES3는 다음을 사용한다. + +```yaml +mcp: code-executor +tool_name: run_code +parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 +``` + +### 4.2 localdocs boilerplate + +각 Python task는 독립 실행 가능해야 하므로 다음을 자체 포함한다. + +- `httpx.Client(timeout=60)` +- MCP initialize +- initialized notification +- monotonic message ID +- `read_docs` +- `write_file` +- SSE와 plain JSON response parser +- tool error surface +- user/workspace hash 전달 + +다른 task의 Python 함수가 runtime에 공유된다고 가정하지 않는다. + +### 4.3 read_docs JSON parser + +모든 JSON read는 다음 순서를 사용한다. + +1. MCP envelope를 해제한다. +2. content가 object/list면 그대로 사용한다. +3. string이면 `json.loads`를 시도한다. +4. `JSONDecodeError`이면 `JSONDecoder.raw_decode(raw.strip())`로 첫 JSON object를 읽는다. +5. 둘 다 실패하면 파일명과 앞 300자만 포함한 `RuntimeError`를 발생시킨다. + +strict `json.loads`만 사용하는 기존 코드는 전부 교체한다. + +### 4.4 canonical serialization과 hash + +공통 canonical 함수는 다음 의미를 가져야 한다. + +```python +json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) +``` + +SHA-256은 canonical UTF-8 bytes에 계산한다. `run_fingerprint`는 다음 object의 hash다. + +```json +{ + "contract": "stage1_legal_structure_run.v2", + "input_digests": { + "BO.json": "sha256:...", + "evidence_indexed.json": "sha256:...", + "actio_case_signals.json": "sha256:...", + "case_liability_signals.json": "sha256:...", + "legal_effect_signals.json": "sha256:..." + } +} +``` + +LES0, LES1, exception pack, adjudication part, LES3 quality gate가 같은 fingerprint를 사용한다. + +### 4.5 stdout + +Python stdout은 한 줄 compact JSON만 출력한다. artifact 본문, traceback, source text 또는 full candidate를 출력하지 않는다. + +### 4.6 failure + +각 Python task의 outermost failure는 다음 순서를 따른다. + +1. task-specific FAILED manifest 한 줄 출력 +2. 가능하면 target final file을 쓰지 않음 +3. bare `sys.exit(0)` 금지 +4. exception re-raise 또는 non-zero 종료 + +### 4.7 code 완전성 + +최종 YAML의 `parameters.code`는 실행 가능한 완전한 Python이어야 한다. helper, validator, schema expander, write/reread, main, failure path를 생략하지 않는다. + +## 5. canonical type과 asset 규칙 + +### 5.1 canonical type enum + +다음 10개 값만 `candidate_structure_types`에 들어갈 수 있다. + +```text +money_claim +commercial_successor +secured_debt +registry_invalidity +land_use_gain +valuation +fraudulent_transfer +preserved_claim +succession_notice_lien_asset_defense +general_legal_effect +``` + +### 5.2 representative priority + +대표 type의 고정 우선순위는 다음과 같다. + +```text +money_claim +commercial_successor +secured_debt +registry_invalidity +land_use_gain +valuation +fraudulent_transfer +preserved_claim +succession_notice_lien_asset_defense +general_legal_effect +``` + +입력 순서나 LLM 문장 순서로 대표 type을 정하지 않는다. + +### 5.3 source domain fallback + +signal과 tag에서 canonical type이 하나도 나오지 않더라도 source domain으로 최소 하나를 부여한다. + +| source domain | fallback type | +|---|---| +| `B1_Money_Successor` | `money_claim` | +| `B2_Secured_Registry` | `secured_debt` | +| `B3_Land_Valuation` | `land_use_gain` | +| `B4_Actio` | `fraudulent_transfer` | +| `B5_Succession_Notice_Lien_Asset_Defense` | `succession_notice_lien_asset_defense` | +| unknown/missing | `general_legal_effect` | + +### 5.4 alias normalization + +먼저 exact canonical enum 값은 그대로 통과시킨다. 그 외 값은 아래 규칙으로만 normalize한다. 한 source label이 둘 이상의 명시 규칙에 해당하면 결과 type을 모두 보존한다. + +| source label family | canonical result | +|---|---| +| money, loan, debt payment, unpaid claim | `money_claim` | +| successor, business transfer, continued trade name | `commercial_successor` | +| secured debt, mortgage setting, collateral | `secured_debt` | +| registry invalidity, auction invalidity, cancellation, derivative invalidity | `registry_invalidity` | +| mortgage extinguishment or derivative mortgage invalidity | `secured_debt` + `registry_invalidity` | +| B3의 possession/use gain/rent/delivery | `land_use_gain` | +| valuation/appraisal/value-compensation calculation | `valuation` | +| fraudulent act, beneficiary/transferee, insolvency, remedy/cap/actio asset target | `fraudulent_transfer` | +| preserved claim | `preserved_claim` | +| B5의 succession/notice/lien/possession/defense | `succession_notice_lien_asset_defense` | + +최소한 다음 exact alias를 코드 상수 map에 넣는다. + +```text +auction_invalidity -> registry_invalidity +derivative_bank_mortgage_invalidity -> secured_debt + registry_invalidity +mortgage_extinguishment -> secured_debt + registry_invalidity +actio_remedy_or_cap -> fraudulent_transfer +value_compensation -> fraudulent_transfer + valuation +insolvency_check -> fraudulent_transfer +actio_asset_target -> fraudulent_transfer +preserved_claim -> preserved_claim +``` + +`possession`처럼 domain에 따라 기능이 달라지는 label은 source domain 없이 단독 매핑하지 않는다. 정규화되지 않은 label은 `nonrouting_labels[]`에 보존하고 canonical enum에 넣지 않는다. + +### 5.5 downstream role map + +```text +money_claim -> claim_chain_ref +commercial_successor -> claim_chain_ref +secured_debt -> linked_structures +registry_invalidity -> linked_structures +land_use_gain -> legal_calculation_object +valuation -> legal_calculation_object +fraudulent_transfer -> actio_roles +preserved_claim -> claim_chain_ref +succession_notice_lien_asset_defense -> linked_structures +general_legal_effect -> legal_effect_roles +``` + +복수 type이면 모든 role을 dedupe하여 보존한다. + +### 5.6 asset identity priority + +1. 동일 `REG-*`/`SCH-*` ref는 같은 cluster다. +2. explicit `asset_alias`가 ref와 label을 연결하면 같은 cluster다. +3. 서로 다른 ref는 다른 cluster다. +4. 하나의 BO가 여러 ref를 가지면 multi-asset bundle을 만든다. +5. ref 없는 자유문구는 동일하다고 확정하지 않는다. +6. fuzzy match는 review 힌트일 뿐 merge authority가 아니다. + +## 6. LES0 작업 명세 + +### 6.1 task header + +```yaml +task_name: Task_LES0_input_pack_compiler +mcp: code-executor +tool_name: run_code +parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 +``` + +### 6.2 입력 + +정확히 다음 5개 파일만 읽는다. + +```text +BO.json +evidence_indexed.json +actio_case_signals.json +case_liability_signals.json +legal_effect_signals.json +``` + +다음 문자열은 LES0 code의 `INPUT_FILES`와 read 호출에 존재하면 안 된다. + +```text +client_goal.json +evidence_all.json +evidence_event_candidates.json +``` + +### 6.3 입력 검증 + +1. `BO.json` root는 array여야 한다. +2. 모든 BO에 `BO_ID` 또는 `id`가 있어야 한다. +3. canonical BO ID는 `BO_ID`를 우선하고 없으면 `id`를 사용한다. +4. BO ID는 중복되면 hard fail한다. +5. evidence root의 `evidence_indexed` 또는 `items`는 array여야 한다. +6. evidence index는 중복되면 hard fail한다. +7. 3개 signal은 기존 v3 output schema/status와 array key를 검증한다. +8. signal의 BO ref가 source BO universe 밖이면 hard fail한다. + +### 6.4 compact evidence authority + +각 evidence row에서 다음만 보존한다. + +```json +{ + "evidence_index": "E-001", + "source_title": "string|null", + "doc_type": "string|null", + "source_pointer": {}, + "priority_class": "string|null", + "authentication_status": "string|null" +} +``` + +full content와 긴 excerpt는 넣지 않는다. BO `Evidence`가 더 구체적인 priority/authentication을 제공하면 non-null 값을 병합할 수 있다. + +### 6.5 asset alias map + +`evidence_indexed.schedule_items[]`에서 다음을 생성한다. + +```json +{ + "SCH-016-01": { + "ref_type": "schedule", + "canonical_label_candidate": "string|null", + "evidence_index": "E-016", + "usable_in_relief": "yes|no|null" + } +} +``` + +registry component에서는 최소한 다음을 보존한다. + +```json +{ + "REG-013-01": { + "ref_type": "registry_row", + "canonical_label_candidate": null, + "evidence_index": "E-013", + "section": "string|null", + "row_seq": "string|number|null" + } +} +``` + +Part 2 BO `extensions.domain_payload.asset_alias`의 `schedule_refs`/`registry_refs`와 label도 읽어 같은 map에 보강한다. 서로 다른 non-null canonical label이 같은 ref에 붙으면 자동 병합하지 않고 `input_review_items`에 `ASSET_ALIAS_LABEL_CONFLICT`를 기록한다. + +자유문구를 schedule ref에 연결할 수 있는 조건은 다음으로 제한한다. + +1. Part 2 `asset_alias`가 ref와 label을 같은 object에서 명시한 경우 +2. 자유문구의 공백·구두점을 제거한 문자열이 schedule `canonical_label_candidate`의 연속 substring이고 길이가 4자 이상인 경우 +3. 같은 BO/evidence 범위 안에서 정확히 하나의 schedule ref만 위 조건을 만족하는 경우 + +둘 이상이 매칭되거나 조건을 충족하지 못하면 ref를 추정하지 않는다. edit distance, embedding, LLM 또는 임의 fuzzy threshold를 사용하지 않는다. + +### 6.6 compact candidate record + +BO마다 정확히 한 record를 만든다. + +```json +{ + "bo_id": "bh1", + "source_domain": "B1_Money_Successor", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [], + "canonical_theory_graph_seed_refs": [], + "linked_structure_ids": [] + }, + "canonical_structure_types": [], + "nonrouting_labels": [], + "asset_candidates": [ + { + "asset_label": "string", + "asset_kind": "real_property|security_right|claim|cash_or_deposit|business_asset|movable|unknown", + "asset_ref_keys": [], + "source_code": "string", + "evidence_indexes": [] + } + ], + "evidence_indexes": [], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [], + "linked_actio_signal_ids": [], + "issue_basis": { + "legal_keywords": [], + "action": "string|null", + "reason": "string|null", + "juristic_act_label": "string|null", + "action_type": "string|null", + "bo_type": "string|null" + }, + "basis_codes": [] +} +``` + +full BO object와 full signal row를 record에 넣지 않는다. + +### 6.7 input pack schema + +LES0은 `stage1_tmp/legal_effect_structure_input_pack.json`에 다음을 쓴다. + +```json +{ + "legal_structure_input_pack": { + "schema_version": "stage1_legal_structure_input_pack.v2", + "status": "READY|READY_WITH_REVIEW|FAILED", + "run_fingerprint": "sha256:...", + "input_digests": {}, + "source_counts": { + "bo": 0, + "evidence": 0, + "actio_signals": 0, + "case_liability_signals": 0, + "legal_effect_signals": 0 + }, + "evidence_authority_by_index": {}, + "asset_alias_by_ref": {}, + "candidate_records_by_bo_id": {}, + "input_review_items": [] + } +} +``` + +top-level `bo_by_id`, `signals_by_bo_id`, `events_by_evidence_index`, `client_goal_compact`, `related_event_candidates`는 만들지 않는다. + +### 6.8 LES0 stdout + +```json +{ + "les0_manifest": { + "schema_version": "stage1_legal_structure_input_manifest.v2", + "status": "READY|READY_WITH_REVIEW|FAILED", + "artifact_path": "stage1_tmp/legal_effect_structure_input_pack.json", + "run_fingerprint": "sha256:...", + "bo_count": 0, + "candidate_count": 0, + "review_count": 0 + } +} +``` + +LES0 stdout에 `legal_structure_input_pack` 본문을 넣지 않는다. + +## 7. LES1 작업 명세 + +### 7.1 task header + +```yaml +task_name: Task_LES1_deterministic_structure_seed_builder +mcp: code-executor +tool_name: run_code +parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 +``` + +### 7.2 입력 경계 + +LES1은 `{{prev.Task_LES0_input_pack_compiler}}` 전체를 Python code에 삽입하지 않는다. LES0 manifest에서 path, fingerprint, count만 읽거나 고정 path를 읽고 manifest와 대조한다. + +`LES0_RAW = r'''...'''`에 full pack이 들어가는 기존 구조를 삭제한다. manifest가 필요하면 compact 문자열만 raw literal로 삽입한다. + +### 7.3 issue seed 규칙 + +1. hard issue key는 다음 우선순위를 유지한다. + +```text +claim_group_ids +liability_group_ids +canonical_theory_graph_seed_refs +linked_structure_ids +``` + +2. 하나의 hard key이면 deterministic issue cluster를 만든다. +3. 여러 hard key가 있으면 모든 source-backed key를 보존할 수 있는 경우 복수 issue cluster ID를 부여한다. +4. hard key가 없으면 현재 keyword score 규칙을 사용할 수 있다. +5. score margin이 충분하면 deterministic family를 선택한다. +6. score가 불충분하면 `general_legal_effect` issue와 `ISSUE_FAMILY_LOW_MARGIN` review를 남긴다. +7. 보존 가능한 복수 issue를 단일값 선택 문제로 만들어 LLM에 보내지 않는다. + +### 7.4 type seed 규칙 + +각 candidate record에 대하여 다음을 수행한다. + +1. `canonical_structure_types`를 canonical enum으로 재검증한다. +2. 빈 배열이면 source domain fallback을 적용한다. +3. 모든 type을 보존한다. +4. representative는 고정 priority 첫 값으로 정한다. +5. 모든 downstream role을 생성한다. +6. 복수 type이라는 이유만으로 exception을 만들지 않는다. +7. unknown/nonrouting label은 type decision에 사용하지 않는다. + +### 7.5 asset seed 규칙 + +1. 같은 ref를 가진 candidate는 같은 cluster key를 사용한다. +2. alias map이 label을 ref에 연결하면 keyed candidate로 승격한다. +3. 서로 다른 ref는 별도 cluster다. +4. 여러 cluster가 한 BO에 있으면 bundle ref를 만든다. +5. ref 없는 candidate는 normalized label과 kind로 별도 cluster를 만든다. +6. 자유문구 유사성만으로 merge하지 않는다. +7. ref 없는 2개 이상의 후보는 `ASSET_IDENTITY_DEFERRED_SEPARATE` review code를 남긴다. + +### 7.6 true exception 4중 조건 + +LLM exception은 다음을 모두 충족해야 한다. + +1. LES3가 단일 선택 또는 split을 해야만 final write가 안전하게 가능하다. +2. source-backed candidate가 2개 이상이다. +3. 전체 보존, conservative separation 또는 Stage 2 defer가 불가능하다. +4. 12KB 이하 compact context만으로 안전한 판정 가능성이 있다. + +하나라도 충족하지 않으면 LLM exception을 만들지 않는다. 대신 deterministic result 또는 식별 가능한 review item을 만든다. + +### 7.7 stable ID + +입력 BO는 현재와 동일하게 문자열 오름차순으로 처리한다. ID는 deterministic encounter order로 부여한다. + +```text +seed_id: LESS-### +issue_cluster_id: IC-### +asset_cluster_id: AC-### +exception_id: LEX-### +review_id: LRV-### +bundle_ref: BUNDLE-### +pack_id: LES-PACK-### +``` + +동일 입력 2회에서 모든 ID와 배열 순서가 같아야 한다. + +### 7.8 seed item schema + +```json +{ + "seed_id": "LESS-001", + "source_bo_ids": [], + "candidate_structure_type": "money_claim", + "candidate_structure_types": [], + "issue_cluster_ids": [], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [], + "linked_actio_signal_ids": [], + "legal_effect_roles": [], + "basis_codes": [], + "confidence": "HIGH|MEDIUM|LOW", + "pending_exception_ids": [], + "deterministic_review_ids": [] +} +``` + +`review_required` boolean은 seed item에서 제거한다. + +### 7.9 deterministic review schema + +```json +{ + "review_id": "LRV-001", + "review_code": "ASSET_ALIAS_LABEL_CONFLICT|ASSET_IDENTITY_DEFERRED_SEPARATE|ISSUE_FAMILY_LOW_MARGIN|MISSING_EVIDENCE_AUTHORITY|PACK_CONTEXT_TOO_LARGE", + "severity": "WARNING|HARD_WARNING", + "source_bo_ids": [], + "evidence_indexes": [], + "related_ref_keys": [], + "reason_code": "closed code", + "downstream_owner": "STAGE_2|HUMAN_REVIEW" +} +``` + +긴 prose를 넣지 않는다. + +### 7.10 exception item schema + +```json +{ + "exception_id": "LEX-001", + "exception_type": "ambiguous_issue_cluster|ambiguous_asset_cluster|ambiguous_candidate_structure_type", + "source_bo_ids": [], + "candidate_values": [], + "basis_codes": [], + "score_summary": [], + "conflict_codes": [], + "allowed_decisions": [], + "compact_context": {} +} +``` + +`compact_context`에 full BO, full signal, full seed map 또는 raw evidence를 넣지 않는다. + +### 7.11 허용 decision + +| exception type | 허용 decision | +|---|---| +| `ambiguous_issue_cluster` | `USE_EXISTING_HARD_KEY`, `PRESERVE_MULTI_ISSUE`, `SPLIT_ISSUE_CLUSTER`, `BLOCK_REVIEW` | +| `ambiguous_asset_cluster` | `SAME_CLUSTER`, `SEPARATE_CLUSTER`, `MULTI_ASSET_BUNDLE`, `BLOCK_REVIEW` | +| `ambiguous_candidate_structure_type` | `SELECT_REPRESENTATIVE_TYPE`, `PRESERVE_MULTI_ROUTING`, `SPLIT_STRUCTURE_SEED`, `BLOCK_REVIEW` | + +### 7.12 micro-pack budget + +```text +MAX_EXCEPTION_ITEMS_PER_PACK = 4 +TARGET_EXCEPTION_ITEMS_PER_PACK = 3 +MAX_EXCEPTION_PACK_BYTES = 12000 +MAX_EXCEPTION_CANDIDATE_VALUES = 8 +MAX_EXCEPTION_PACKS = 8 +MAX_CONCURRENCY = 2 +``` + +위 이름과 값으로 LES1 Python code 상단에 상수를 선언한다. 동일 exception type끼리 우선 pack한다. 단일 item이 12KB를 넘으면 LLM에 보내지 않고 `PACK_CONTEXT_TOO_LARGE` deterministic review로 전환한다. 8 pack을 초과하면 뒤를 조용히 절단하지 않고 LES1 status를 `BLOCKED`로 기록한 뒤 non-zero 실패한다. + +### 7.13 fan-out descriptor + +각 `dynamic_fanout` item은 다음 schema를 따른다. + +```json +{ + "pack_id": "LES-PACK-001", + "exception_type": "ambiguous_asset_cluster", + "exception_ids": ["LEX-001"], + "items": [], + "output_path": "stage1_tmp/legal_effect_structure_adjudication_parts/PACK-001.json", + "run_fingerprint": "sha256:..." +} +``` + +serialized fan-out item이 12KB를 넘으면 안 된다. + +### 7.14 slim seed bundle schema + +`stage1_tmp/legal_effect_structure_seed_bundle.json`: + +```json +{ + "legal_structure_seed_bundle": { + "schema_version": "stage1_legal_structure_seed_bundle.v2", + "status": "READY|READY_WITH_REVIEW|BLOCKED", + "run_fingerprint": "sha256:...", + "source_input_pack": { + "path": "stage1_tmp/legal_effect_structure_input_pack.json", + "schema_version": "stage1_legal_structure_input_pack.v2", + "sha256": "sha256:..." + }, + "structure_seed_items": [], + "issue_cluster_seed_map": {}, + "asset_cluster_seed_map": {}, + "evidence_index_seed": {}, + "deterministic_review_items": [], + "exception_pack_ref": { + "path": "stage1_tmp/legal_effect_structure_exception_pack.json", + "schema_version": "stage1_legal_structure_exception_pack.v2", + "sha256": "sha256:...", + "expected_exception_ids": [] + } + } +} +``` + +다음을 넣지 않는다. + +- `candidate_structure_type_index_seed` +- full `ambiguity_diagnostics` +- embedded `legal_structure_exception_pack` +- LES3가 소비하지 않는 full bundle diagnostic + +### 7.15 exception pack schema + +`stage1_tmp/legal_effect_structure_exception_pack.json`: + +```json +{ + "legal_structure_exception_pack": { + "schema_version": "stage1_legal_structure_exception_pack.v2", + "status": "READY|READY_NO_EXCEPTIONS|READY_WITH_REVIEW|BLOCKED", + "run_fingerprint": "sha256:...", + "expected_exception_ids": [], + "exceptions": [], + "deterministic_review_items": [], + "pack_count": 0, + "packs": [] + } +} +``` + +`exceptions[].exception_id` 집합과 `expected_exception_ids`는 정확히 같아야 한다. `packs[].exception_ids`의 union도 같은 집합이어야 한다. + +### 7.16 LES1 stdout + +```json +{ + "les1_manifest": { + "schema_version": "stage1_legal_structure_les1_manifest.v2", + "status": "READY|READY_NO_EXCEPTIONS|READY_WITH_REVIEW|BLOCKED|FAILED", + "seed_path": "stage1_tmp/legal_effect_structure_seed_bundle.json", + "exception_pack_path": "stage1_tmp/legal_effect_structure_exception_pack.json", + "run_fingerprint": "sha256:...", + "seed_count": 0, + "exception_count": 0, + "review_count": 0, + "pack_count": 0 + }, + "dynamic_fanout": [] +} +``` + +true exception이 없으면 `dynamic_fanout`은 반드시 빈 배열이다. + +## 8. LES2 wildcard 작업 명세 + +### 8.1 task header + +```yaml +task_name: Task_LES2_exception_adjudicator_* +llm_provider: google +llm_model: gemini-3.1-flash-lite +llm_reasoning: medium +llm_verbosity: low +max_iterations: 2 +max_concurrency: 2 +use_tools: +- localdocs +cache_control: + mode: auto + ttl: 1h +preflight: false +``` + +### 8.2 입력 경계 + +prompt에는 canonical common prefix 뒤에 다음 `item` 필드만 넣는다. + +```text +{{item.pack_id}} +{{item.exception_type}} +{{item.exception_ids}} +{{item.items}} +{{item.output_path}} +{{item.run_fingerprint}} +``` + +`{{prev.Task_LES1_deterministic_structure_seed_builder}}` 전체를 넣지 않는다. LES2는 BO, evidence, signal, input pack, seed bundle을 read하거나 요청할 수 없다. + +### 8.3 LLM 권한 + +LES2는 다음만 할 수 있다. + +1. input exception을 하나씩 읽는다. +2. 해당 exception type의 allowed decision 중 하나를 선택한다. +3. selected value/ref가 input candidate의 부분집합인지 지킨다. +4. 불충분하면 `BLOCK_REVIEW`를 선택한다. +5. 자신의 output path 한 곳에 decision part를 쓴다. +6. compact manifest만 stdout으로 반환한다. + +새 사실, 새 ID, 새 candidate, 새 source ref, 법률상 최종 결론을 만들 수 없다. + +### 8.4 decision schema + +```json +{ + "exception_id": "LEX-001", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [], + "decision": "closed enum", + "selected_value": null, + "selected_values": [], + "preserve_candidate_values": [], + "split_plan": [], + "review_required": false, + "reason_code": "HARD_KEY_CONTROLS|SOURCE_REF_IDENTITY|CONSERVATIVE_SEPARATION|PRESERVE_ALL_ROUTING|SPLIT_REQUIRED|INSUFFICIENT_COMPACT_CONTEXT", + "basis_codes_used": [], + "conflict_codes_acknowledged": [] +} +``` + +`BLOCK_REVIEW`이면 `review_required`는 true다. split은 review-sensitive이므로 true다. 그 외 성공 decision은 false다. + +### 8.5 adjudication part schema + +각 instance는 `{{item.output_path}}`에 다음을 쓴다. + +```json +{ + "schema_version": "stage1_legal_structure_adjudication_part.v2", + "status": "READY|READY_WITH_BLOCKS|FAILED", + "pack_id": "LES-PACK-001", + "run_fingerprint": "sha256:...", + "exception_type": "ambiguous_asset_cluster", + "input_exception_ids": [], + "decisions": [], + "coverage": { + "expected": 0, + "actual": 0, + "missing_exception_ids": [], + "unknown_exception_ids": [], + "coverage_pass": true + } +} +``` + +write가 성공한 뒤 stdout에는 다음만 반환한다. + +```json +{ + "pack_id": "LES-PACK-001", + "status": "READY|READY_WITH_BLOCKS|FAILED", + "output_path": "stage1_tmp/legal_effect_structure_adjudication_parts/PACK-001.json", + "decision_count": 0 +} +``` + +### 8.6 0-instance 경로 + +LES1의 `dynamic_fanout`이 빈 배열이면 LES2 instance가 생성되지 않아야 한다. no-exception JSON을 만들기 위해 LLM을 호출하지 않는다. LES3가 exception pack의 `pack_count: 0`과 빈 expected ID를 검증하여 진행한다. + +## 9. LES3 작업 명세 + +### 9.1 task header + +```yaml +task_name: Task_LES3_final_structure_index_writer +mcp: code-executor +tool_name: run_code +parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 +``` + +### 9.2 입력 + +정확히 다음 artifact를 읽는다. + +```text +stage1_tmp/legal_effect_structure_seed_bundle.json +stage1_tmp/legal_effect_structure_exception_pack.json +stage1_tmp/legal_effect_structure_adjudication_parts/PACK-*.json # packs가 있을 때만 +``` + +기존 `SEED_RAW`, `ADJUDICATION_RAW`, 단일 LES2 stdout parse를 삭제한다. + +### 9.3 artifact 검증 + +1. seed/exception schema가 v2인지 확인한다. +2. status가 허용 enum인지 확인한다. +3. run fingerprint가 모두 같은지 확인한다. +4. seed의 exception-pack path/hash가 실제 파일과 일치하는지 확인한다. +5. exception pack의 pack count와 실제 decision part 수가 일치하는지 확인한다. +6. 각 part의 pack ID, fingerprint, input exception IDs가 descriptor와 일치하는지 확인한다. +7. decision ID union이 expected exception ID와 정확히 같은지 확인한다. +8. duplicate/unknown/missing decision은 hard fail한다. +9. selected/preserved/split value가 input 후보의 부분집합인지 확인한다. +10. decision enum이 exception type에 허용되는지 확인한다. + +### 9.4 review 재계산 + +final review는 다음만 포함한다. + +- deterministic review item +- `BLOCK_REVIEW` decision +- review-sensitive split +- LLM이 명시적으로 `BLOCK_REVIEW`로 봉인한 법률적 미해결 item + +성공적으로 해결된 `SAME_CLUSTER`, `SEPARATE_CLUSTER`, `MULTI_ASSET_BUNDLE`, `PRESERVE_MULTI_ROUTING`, `SELECT_REPRESENTATIVE_TYPE`만 있는 구조는 review에서 제거한다. + +여기서 법률적 미해결과 기계적 누락을 구분한다. compact context가 불충분하여 LLM이 유효한 `BLOCK_REVIEW` decision을 낸 경우는 review queue에 보존한다. 반대로 expected exception ID에 대응하는 decision 자체가 없거나, part가 누락되거나, ID/hash/schema가 맞지 않는 경우는 review로 강등하지 않고 hard fail한다. + +review queue item은 반드시 `review_id` 또는 `exception_id`를 가져야 한다. 포괄적인 이유 없는 blanket review를 만들지 않는다. + +### 9.5 final structure schema + +기존 필드를 모두 유지하고 `candidate_structure_types`를 named field로 추가한다. + +```json +{ + "structure_id": "LES-001", + "candidate_structure_type": "money_claim", + "candidate_structure_types": ["money_claim", "commercial_successor"], + "structure_label": "string", + "source_bo_ids": [], + "issue_cluster_ids": [], + "asset_cluster_ids": [], + "evidence_indexes": [], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [], + "linked_actio_signal_ids": [], + "legal_effect_roles": [], + "basis": { + "primary_fields": [], + "source_signal_files": [], + "source_evidence_indexes": [] + }, + "review_required": false, + "routing_audit": { + "source_seed_id": "LESS-001", + "candidate_structure_types": [], + "multi_asset_bundle_refs": [], + "resolved_exception_ids": [], + "blocked_exception_ids": [], + "deterministic_review_ids": [] + } +} +``` + +### 9.6 index 생성 규칙 + +1. `by_bo_id.candidate_structure_types`에는 structure의 모든 canonical type을 넣는다. +2. `by_candidate_structure_type`에는 모든 canonical type별 row를 만든다. +3. representative type만 type index에 넣는 기존 코드를 교체한다. +4. issue/asset/evidence index는 기존 named shape를 유지한다. +5. `candidate_structure_types`는 제5.2절 priority 순서를 유지한다. 그 외 ID/ref 배열은 deterministic dedupe 후 문자열 오름차순으로 정렬한다. +6. split 구조가 생기면 source BO coverage가 유지되는지 확인한다. + +### 9.7 quality gate + +기존 BO/evidence coverage를 유지하고 다음을 additive하게 추가한다. + +```json +{ + "bo_coverage": {}, + "evidence_coverage": {}, + "exception_coverage": { + "expected_exception_count": 0, + "resolved_exception_count": 0, + "blocked_exception_count": 0, + "missing_exception_ids": [], + "coverage_pass": true + }, + "run_fingerprint_gate": { + "match": true + }, + "review_summary": { + "structure_review_count": 0, + "deterministic_review_count": 0, + "blocked_exception_count": 0 + }, + "review_queue": [] +} +``` + +### 9.8 final status + +```text +review_queue empty -> READY +review_queue non-empty -> READY_WITH_REVIEW +coverage/hash/schema failure -> final file 미작성 + task failure +``` + +### 9.9 final writer + +1. payload를 메모리에서 완성한다. +2. 모든 schema/index/conservation을 검증한다. +3. `legal_effect_structures.json`을 한 번 쓴다. +4. 다시 읽어 schema, status, structure count, 5개 index, coverage를 검증한다. +5. compact manifest 한 줄을 stdout으로 출력한다. +6. 검증 실패 시 성공 상태를 출력하지 않는다. + +## 10. 최종 task_procedure + +다음 mapping을 정확히 사용한다. + +```yaml +task_procedure: + IN: + nexts: + - Task_LES0_input_pack_compiler + wait_until: [] + + Task_LES0_input_pack_compiler: + nexts: + - Task_LES1_deterministic_structure_seed_builder + wait_until: + - IN + + Task_LES1_deterministic_structure_seed_builder: + nexts: + - Task_LES2_exception_adjudicator_* + - Task_LES3_final_structure_index_writer + wait_until: + - Task_LES0_input_pack_compiler + + Task_LES2_exception_adjudicator_*: + nexts: + - Task_LES3_final_structure_index_writer + wait_until: + - Task_LES1_deterministic_structure_seed_builder + + Task_LES3_final_structure_index_writer: + nexts: + - OUT + wait_until: + - Task_LES1_deterministic_structure_seed_builder + - all Task_LES2_exception_adjudicator_* + + OUT: + nexts: [] + wait_until: + - Task_LES3_final_structure_index_writer +``` + +`Task_LES2_single_cluster_exception_adjudicator` 문자열은 최종 YAML에 0건이어야 한다. + +## 11. failure와 status 전파 + +### 11.1 LES0 + +- input missing/schema mismatch/duplicate BO/ref universe 위반: `FAILED` manifest 후 raise +- review 가능한 alias conflict: `READY_WITH_REVIEW` +- FAILED이면 LES1이 진행해도 즉시 block하고 artifact를 final로 사용하지 않음 + +### 11.2 LES1 + +- input hash/schema/count 불일치: `FAILED` 후 raise +- true exception 없음: `READY_NO_EXCEPTIONS`, `dynamic_fanout: []` +- deterministic review만 있음: `READY_WITH_REVIEW`, `dynamic_fanout: []` +- 8 pack 초과 또는 conservation 실패: `BLOCKED` 후 raise +- exception 뒤쪽을 조용히 drop/auto-block하지 않음 + +### 11.3 LES2 + +- 모든 input exception을 정확히 한 번 결정 +- 불충분하면 decision을 생략하지 말고 `BLOCK_REVIEW` +- write 실패 또는 coverage 실패: `FAILED` +- 다른 pack/file 수정 금지 + +### 11.4 LES3 + +- missing part/hash mismatch/unknown decision/schema failure: final 미작성 + raise +- blocked decision은 final review로 보존 가능 +- expected decision 누락은 review로 강등하지 않고 hard fail +- post-write reread 실패도 hard fail + +## 12. 구현 순서 + +### Phase 0. 원본 보존과 사전 검사 + +1. 원본 SHA-256 기록 +2. 원본을 `Stage_1_Part_3_Codex_v2.yml`로 복제 +3. 원본과 대상의 초기 byte equality 확인 +4. Ruby/Psych로 원본 parse +5. task명, model, writer, input file 기준값 기록 + +### Phase 1. header, constants, Python 공통 helper + +1. Agent version 변경 +2. 3개 Python task에 robust MCP/read/hash/failure helper 적용 +3. canonical type/role/alias constants 정의 +4. 기존 `sys.exit(0)` 제거 + +### Phase 2. LES0 + +1. input 5개로 축소 +2. source schema 검증 +3. evidence authority/asset alias map 생성 +4. compact candidate record 생성 +5. v2 input pack write/reread +6. one-line manifest stdout + +### Phase 3. LES1 + +1. full stdout injection 제거 +2. deterministic issue/type/asset seed 구현 +3. pending/review 상태 구현 +4. 4중 exception gate 구현 +5. slim seed/exception pack write +6. micro-pack/dynamic fanout 생성 + +### Phase 4. LES2와 DAG + +1. 단일 LES2 삭제 +2. wildcard LES2 추가 +3. canonical prefix 적용 +4. localdocs decision-part writer prompt 구현 +5. task_procedure를 제10장과 동일하게 변경 + +### Phase 5. LES3 + +1. dead stdout injection 제거 +2. artifact/part reducer 구현 +3. exact exception coverage 구현 +4. review 재계산 구현 +5. all-type final/index projection 구현 +6. final writer/reread gate 구현 + +### Phase 6. 정적·fixture·성능 검증 + +1. 제13장 정적 검증 +2. 제14장 fixture 회귀 +3. 제15장 fault injection +4. 제16장 downstream smoke +5. 제17장 성능 측정 + +## 13. 정적 검증 명령과 판정 기준 + +### 13.1 YAML parse + +```bash +ruby -ryaml -e 'YAML.load_file(ARGV[0]); puts "YAML_OK"' \ + 'YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_3_Codex_v2.yml' +``` + +### 13.2 task명과 DAG + +```bash +ruby -ryaml -e ' +d=YAML.load_file(ARGV[0]); s=d["Agent"]["Stages"].first +names=s["tasks"].map{|t|t["task_name"]} +puts names +abort("task count") unless names.length==4 +expected=%w[ +Task_LES0_input_pack_compiler +Task_LES1_deterministic_structure_seed_builder +Task_LES2_exception_adjudicator_* +Task_LES3_final_structure_index_writer] +abort("task names") unless names==expected +' 'YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_3_Codex_v2.yml' +``` + +YAML의 모든 `nexts`/`wait_until` reference가 task definition 또는 `IN`/`OUT`/`all wildcard`에 대응하는지 별도 검사한다. + +### 13.3 Python compile + +Ruby로 3개 `parameters.code`를 임시 파일로 추출한 뒤 `python3 -m py_compile`을 수행한다. 정확히 3개가 성공해야 한다. 임시 파일은 workspace 밖 `/tmp`를 사용한다. + +### 13.4 Python/SKILL 검사 + +다음을 확인한다. + +```text +Python task count: 3 +{{__user_hash__}} count: 3 이상 +{{__workspace_hash__}} count: 3 이상 +raw_decode helper: 각 Python task 1개 이상 +sys.exit(0): 0건 +FAILED 후 raise/non-zero: 3개 task 모두 +template payload raw string 위반: 0건 +placeholder/ellipsis: 0건 +``` + +### 13.5 common prefix + +LES2 wildcard의 prefix를 추출하여 다음을 확인한다. + +```text +count: 1 +bytes: 3385 +sha256: 6e01f72f1803767b6bb16b5aef6cf34760e6dc1b293c316c75e990d09ad5fe63 +``` + +### 13.6 read diet + +LES0 code에서 다음은 각각 1회 이상 존재해야 한다. + +```text +BO.json +evidence_indexed.json +actio_case_signals.json +case_liability_signals.json +legal_effect_signals.json +``` + +다음은 LES0 `INPUT_FILES`/read path에서 0건이어야 한다. + +```text +client_goal.json +evidence_all.json +evidence_event_candidates.json +``` + +LES2 prompt의 금지 설명에 파일명이 등장하는 것은 read diet 위반으로 세지 않는다. + +### 13.7 writer audit + +```text +legal_effect_structure_input_pack.json writer: LES0 1개 +legal_effect_structure_seed_bundle.json writer: LES1 1개 +legal_effect_structure_exception_pack.json writer: LES1 1개 +adjudication parts writer: LES2 wildcard 1개 template +legal_effect_structures.json writer: LES3 1개 +``` + +### 13.8 비밀정보·무관 변경 + +- 원본 tools header가 보존되었는지 확인 +- 새 credential literal 0건 +- 원본과 대상 diff가 Agent.version, task_procedure, 4개 task body/header에만 존재하는지 확인 +- 다른 파일 변경 0건 + +## 14. fixture 회귀 테스트 + +### 14.1 기준값 + +7/10 fixture의 개정 전 기준은 다음과 같다. + +```text +BO: 50 +evidence: 30 +final structures: 50 +final schema: stage1_legal_effect_structures.v1 +structure index kinds: 5 +old merged exceptions: 47 +old LES2 input: 30 +old budget auto-block: 17 +old blocked decisions: 28 +old structure review: 50/50 +old total review queue: 78 +``` + +### 14.2 구조·coverage + +1. source BO ID set = seed BO ID set = final `by_bo_id` key set +2. candidate/seed가 참조한 evidence ID set이 final `by_evidence_index`에 모두 반영 +3. final structure 수는 BO coverage를 잃지 않음 +4. 모든 final structure가 1개 이상의 source BO를 가짐 +5. 5개 index가 object이고 모든 structure/ref를 역참조 가능 +6. Part 4의 현행 schema validator를 무수정 통과 + +### 14.3 canonical type + +1. final type은 canonical enum 밖 값 0건 +2. `event`, `state`, 행위 분류가 candidate type에 존재하지 않음 +3. source-backed canonical multi-routing 누락 0건 +4. structure top-level type 배열과 routing audit type 배열 일치 +5. `by_bo_id`와 `by_candidate_structure_type`이 모든 canonical type을 반영 + +### 14.4 asset + +1. 동일 REG/SCH ref가 서로 다른 cluster로 분리되지 않음 +2. 서로 다른 ref가 fuzzy label 때문에 병합되지 않음 +3. SCH ref와 explicit canonical label 연결 보존 +4. multi-asset BO의 모든 asset cluster와 bundle ref 보존 +5. unresolved free-text identity는 review ID로 추적 가능 + +### 14.5 exception과 review + +1. 단순 multi-routing으로 생성된 exception 0건 +2. expected exception exact coverage pass +3. budget 순서로 drop된 exception 0건 +4. 해결된 exception만 가진 구조는 review false +5. deterministic review와 blocked exception은 ID로 추적 가능 +6. 50/50 blanket review 제거 +7. fixture의 목표 LES2 instance는 0개다. 0이 아니면 각 item이 4중 조건을 충족하는 근거를 audit로 제시한다. + +### 14.6 결정론 + +동일 fixture로 Python 경로를 2회 실행한다. 다음 파일은 byte-identical이어야 한다. + +```text +stage1_tmp/legal_effect_structure_input_pack.json +stage1_tmp/legal_effect_structure_seed_bundle.json +stage1_tmp/legal_effect_structure_exception_pack.json +legal_effect_structures.json # LLM instance 0인 경우 +``` + +## 15. fault injection + +| 주입 | 기대 결과 | +|---|---| +| BO ID 중복 | LES0 FAILED, final 미작성 | +| signal이 미지 BO ID 참조 | LES0 FAILED | +| evidence index 중복 | LES0 FAILED | +| 같은 asset ref에 상충 label | deterministic review, 묵시 병합 금지 | +| exception item 12KB 초과 | LES1 review로 defer, LLM 미전달 | +| 8 pack 초과 | LES1 BLOCKED + raise, silent truncate 0 | +| decision part 누락 | LES3 FAILED, final 미작성 | +| decision에 미지 exception ID | LES3 FAILED | +| selected value가 후보 밖 | LES3 FAILED | +| part fingerprint mismatch | LES3 FAILED | +| LLM JSON code fence | parser salvage + warning 또는 명시적 FAILED, 무한 재호출 금지 | +| final write 후 reread mismatch | LES3 FAILED | + +## 16. downstream smoke test + +`Stage_1_Part_4_v2.yml`의 FL0 validator 및 compact projection 기준으로 개정 final을 검사한다. + +통과 조건: + +1. `schema_version == stage1_legal_effect_structures.v1` +2. status가 `READY` 또는 `READY_WITH_REVIEW` +3. `legal_effect_structures`가 non-empty array +4. 5개 structure index가 object +5. 모든 `by_bo_id.structure_ids`가 실제 structure를 참조 +6. 모든 structure의 BO/evidence/issue/asset/type ref가 index에서 역참조 가능 +7. additive `candidate_structure_types` 때문에 Part 4 parser가 실패하지 않음 +8. Part 4가 LES ambiguity를 재판단하지 않아도 unresolved item이 review queue에 남음 + +## 17. 성능 검증 + +동일 환경에서 cold/warm을 구분하여 최소 3회 실행하고 median을 기록한다. + +| 지표 | v1 실측 | 1차 합격 | 목표 | +|---|---:|---:|---:| +| task definition | 4 | 4 | 4 | +| no-exception runtime task | 4 | 3 | 3 | +| Part 3 input read | 8 files | 5 files | 5 files | +| input pack | 416,115 B | 220 KB 이하 | 200 KB 이하 | +| seed bundle | 422,236 B | 280 KB 이하 | 250 KB 이하 | +| LES0 stdout | 약 257 KB | 4 KB 이하 | 2 KB 이하 | +| LES1 stdout | 약 32.6 KB | 8 KB 이하 | 4 KB 이하 | +| monolithic LES2 | 1 | 0 | 0 | +| budget auto-block | 17 | 0 | 0 | +| runtime | 3분 40초 | 2분 12초 이하 | 2분 이하 | +| 비용 | `$0` | 증가 금지 | `$0` 또는 동등 수준 | + +품질 invariant 하나라도 위반하면 성능을 달성해도 실패다. + +## 18. 완료 정의 + +- [ ] `Stage_1_Part_3_Codex_v2.yml`이 생성되었다. +- [ ] 원본 `Stage_1_Part_3_v1.yml`은 변경되지 않았다. +- [ ] Agent version은 `v2.0`이다. +- [ ] task definition은 정확히 4개다. +- [ ] 단일 LES2 task명과 참조는 0건이다. +- [ ] wildcard LES2와 `all Task_LES2_exception_adjudicator_*`가 연결되었다. +- [ ] no-exception runtime LLM은 0개다. +- [ ] LES0 read는 정확히 5개 input으로 제한되었다. +- [ ] canonical type enum 밖 final type은 0건이다. +- [ ] multi-routing type이 final structure와 index에 모두 반영된다. +- [ ] seed `review_required` boolean이 pending/review ID 구조로 교체되었다. +- [ ] exception exact coverage와 review 재계산이 구현되었다. +- [ ] full input/seed/exception payload stdout 주입이 제거되었다. +- [ ] seed/exception 중복 저장이 제거되었다. +- [ ] Python 3개가 SKILL과 compile을 통과한다. +- [ ] common prefix는 3,385 bytes, `6e01...fe63`이다. +- [ ] `legal_effect_structures.json`의 유일 writer는 LES3다. +- [ ] final v1 schema와 5개 index가 유지된다. +- [ ] fixture BO/evidence/type/ref conservation을 통과한다. +- [ ] Part 4 smoke test를 통과한다. +- [ ] 동일 입력 2회 결정론을 통과한다. +- [ ] 3회 runtime/cost median을 기록한다. + +하나라도 충족하지 못하면 완료라고 표현하지 않는다. 실패한 task, artifact, invariant와 보수적 fallback을 명시한다. + +## 19. 구현자 최종 보고 형식 + +1. 생성한 YAML 경로 +2. source YAML 무변경과 SHA-256 +3. 유지·삭제·교체 task 목록 +4. task definition과 no-exception runtime task 수 +5. LES0 read file 목록 +6. common prefix count/bytes/hash +7. Python SKILL/compile 결과 +8. writer audit 결과 +9. fixture BO/evidence/type/asset conservation 결과 +10. exception pack/LLM instance/review queue 수 +11. Part 4 smoke 결과 +12. 2-run 결정론 결과 +13. 3회 runtime/cost median +14. 잔여 blocker 또는 fallback + +YAML 생성만 하고 fixture/runtime을 실행하지 않았다면, “정적 검증 완료”, “fixture 미실행”, “runtime 미측정”을 구분하여 보고한다. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_3_Inefficiency_Report_Claude_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_3_Inefficiency_Report_Claude_v1.md new file mode 100644 index 00000000..7d148547 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_3_Inefficiency_Report_Claude_v1.md @@ -0,0 +1,132 @@ +# Stage 1 Part 3 비효율성 분석 보고서 (Claude v1) + +- 분석 대상: `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_3_v1.yml` (2,638행, task 4개) +- 실행 실측: 런타임 3분 40초 / 비용 $0 (7/10 실행, `Results_July_10_3_15pm`) +- 참조 명세: Part 1 `Stage_1_Part_1_Claude_v3.yml`, Part 2 `Stage_1_Part_2_Claude_v3.yml`(개정 완료본), Part 4 `Stage_1_Part_4_v2.yml` +- 작성 관점: 20년 이상 경력의 대한민국 최고 수준 변호사 + 세계적 수준의 AI 아키텍트 + +## 0. 분석 방법 — 실행물 기반 실측 + +본 보고서의 모든 정량 수치는 추정이 아니라 **7/10 실행물로 Part 3 전체 코드를 로컬 재실행하여 얻은 실측**이다. 재실행 결과가 실제 실행물과 바이트 단위로 일치함을 먼저 확인하였다: LES0가 쓴 `legal_effect_structure_input_pack.json` 416,115B, LES1이 쓴 `legal_effect_structure_seed_bundle.json` 422,236B와 `legal_effect_structure_exception_pack.json` 52,982B가 모두 실제 실행물과 동일 바이트로 재현되었다. 따라서 아래의 stdout 크기·예외 건수·미사용 필드 판정은 실제 7/10 실행에서 일어난 일 그 자체다. + +## 1. Part 3 실행 구조와 데이터 흐름 실측 + +Part 3는 완전 직렬 4-task 체인이다: `IN → LES0(input pack 컴파일, Python) → LES1(결정론 seed/예외 생성, Python) → LES2(예외 판정, LLM: gemini-3.5-flash/high/medium, max_iterations 2) → LES3(최종 writer, Python) → OUT`. 병렬 구간이 전혀 없으므로 DAG 관점의 이득은 0이고, 효율은 오로지 각 단계의 페이로드와 LLM 소비가 결정한다. + +7/10 실행에서 실측된 데이터 흐름은 다음과 같다. + +| 구간 | 채널 | 실측 크기 | +|---|---|---| +| LES0 입력 | localdocs read ×8 (client_goal, BO, evidence_indexed, **evidence_all 87,436B**, event_candidates, signal 3종) | 합계 약 340KB | +| LES0 → 파일 | `legal_effect_structure_input_pack.json` (indent=2) | 416,115B | +| LES0 → stdout | **pack 전체를 compact JSON으로 재출력** | 257,367B | +| LES0 stdout → LES1 | `{{prev.…}}` 템플릿으로 **LES1 코드 안에 재주입** | 257,367B (run_code 페이로드 약 297KB) | +| LES1 → 파일 | seed bundle 422,236B + exception pack 52,982B | 475,218B | +| LES1 → stdout | 요약 + adjudicator_input 전체 | 32,617B | +| LES1 stdout → LES2 | 프롬프트 주입 (정적 블록 14,154B 포함 프롬프트 총 46,771B) | 32,617B | +| LES1 stdout → LES3 | `SEED_RAW` 템플릿 주입 — **아래 P3-5: 100% 사장** | 32,617B | +| LES3 입력 | seed bundle **파일 재읽기**(항상) + LES2 stdout | 422,236B + α | +| LES3 → 파일 | `legal_effect_structures.json` (최종, write 후 재읽기 검증) | 254,991B | + +즉 최종 산출물 255KB를 만들기 위해 중간 아티팩트 891KB가 파일로 쓰이고, 290KB가 stdout으로, 322KB가 코드/프롬프트 주입으로 이동한다. 그리고 이 모든 것의 유일한 실질 소비자는 파이프라인 내부의 바로 다음 task 하나씩이다. + +## 2. 비효율 판정 항목 + +### 2.1 토큰·전송 비경제 + +**P3-1. input pack의 3중 물질화.** LES0는 같은 내용을 (1) 파일 416KB, (2) stdout 257KB, (3) LES1 코드 주입 257KB로 세 번 실어 나른다. stdout과 템플릿 주입은 백엔드 컨텍스트/로그와 run_code 요청에 그대로 누적된다. Part 2 v2에서 지적되어 v3(A0)가 "파일 write + stdout 1줄 요약"으로 해소한 것과 동일한 유형(C-9)이며, Part 3에는 아직 그대로 남아 있다. + +**P3-2. pack 내용의 55%가 유일 소비자(LES1)에게 미사용.** pack의 compact 크기 약 232KB 중 LES1 코드가 실제로 참조하는 키는 `candidate_inputs_by_bo_id`(98.5KB)와 `evidence_authority_by_index`(5.3KB)뿐이다. `bo_by_id` 82.7KB는 변수 할당 후 한 번도 참조되지 않고, `signals_by_bo_id` 42.2KB는 `candidate_inputs_by_bo_id` 안에 각 BO의 `signals`로 **완전히 중복 재수록**되어 있으며, `events_by_evidence_index` 2.9KB와 `client_goal_compact` 0.6KB도 참조 0회다. 미사용 합계 약 128KB(55%)가 세 채널(P3-1)로 각각 운반된다. + +**P3-3. `evidence_all.json`(87KB) 읽기의 기여가 실측 0.** LES0의 evidence authority 수집기를 evidence_all 포함/제외로 각각 재실행해 비교한 결과, 30개 evidence index 전부에서 산출이 완전히 동일했다. `evidence_indexed.json`과 `BO.json.Evidence`가 이미 전 항목을 커버하기 때문이다. Part 3에서 가장 큰 입력 파일 하나가 순수 낭비다. + +**P3-4. seed bundle의 이중 저장·미소비 필드.** LES1이 쓰는 seed bundle 422KB 중 LES3가 읽지 않는 필드가 약 80KB다: `ambiguity_diagnostics` 34.9KB, `candidate_structure_type_index_seed` 43.4KB, `multi_asset_bundles` 2.0KB(모두 LES3 참조 0회 — LES3는 by_type 인덱스를 구조물에서 재계산한다). 또한 exception pack이 seed bundle **내부에 32.6KB로 내장**되고 **별도 파일로 53KB 재저장**되는데, 별도 파일을 읽는 소비자는 Part 3/Part 4 어디에도 없다(LES3는 내장본을 사용). input pack 파일 역시 정상 경로에서는 아무도 읽지 않는다(LES1의 템플릿 실패 시 fallback 전용). + +**P3-5. LES3의 `SEED_RAW` 주입은 구조적으로 사장(死藏)되어 있다.** LES3는 `SEED_RAW = r'''{{prev.Task_LES1_…}}'''`로 LES1 stdout 32.6KB를 주입받아 seed bundle로 파싱을 시도하고, 실패 시 파일을 읽는 설계다. 그런데 LES1 stdout의 루트 키는 `legal_structure_les0_seed_generation`과 `legal_structure_exception_adjudicator_input`뿐이고 `legal_structure_seed_bundle`이 **없다**. 따라서 이 파싱은 100% 실패하고 LES3는 **매 실행 항상** 422KB 파일을 다시 읽는다. 의도한 최적화가 한 번도 작동한 적 없는 dead code이며, 32.6KB 주입은 순수 낭비다. + +### 2.2 LLM 추론 비효율 — "판정이 산출물을 바꾸지 못했다" + +**P3-6. LES2는 예외 유무와 무관하게 무조건 실행된다.** 조건부 실행·조기 종료 구조(Part 1 v3 GB, Part 2 v3 R1의 "pack만 preflight, 예외 없으면 즉시 종료" 패턴)가 없다. 입력도 파일이 아니라 LES1 stdout 전체(32.6KB)를 프롬프트에 싣는 방식이고, 이 중 첫 465B(les0_seed_generation 요약)는 계약상 불필요하다. + +**P3-7. 예외 생성 과다 → LLM 판정의 실질 무효화 (핵심 실측).** 7/10 실행에서: + +| 단계 | 실측 | +|---|---| +| LES1이 생성한 예외 (병합 후) | **47건** (BO 50건 대비 94% — 사실상 전 항목이 "예외") | +| 그중 LLM에 전달 | 30건 (budget cap) — type 24 + asset 6 | +| LES2 판정 결과 | **28건(93%)이 BLOCK_REVIEW 회신** (insufficient_compact_record 24 + conflicting_allowed_decisions 4) | +| 실질 판정 | ≤2건 | +| 최종 산출물에 미친 영향 | **0** — 구조물 50건 전부의 대표 타입이 결정론 우선순위 규칙(priority-first)과 100% 일치, split 0건, multi-routing 전건 보존 | + +즉 LES2를 통째로 제거하고 "PRESERVE_MULTI_ROUTING + 우선순위 대표 타입"이라는 결정론 기본값을 적용했어도 `legal_effect_structures.json`은 **동일하게** 나왔다. 원인은 두 가지다. 첫째, `_candidate_structure_types`가 signal·domain tag·source_domain 매핑을 전부 합집합하므로 거의 모든 BO가 복수 타입을 갖게 되고, 복수 타입이 서로 다른 downstream role로 매핑되면 기계적으로 예외가 된다(과다 생성). 둘째, 판정에 필요한 근거를 1.2KB compact 레코드로 제한했기 때문에 LLM 스스로 "근거 불충분"을 93% 선언한다. 예외로 보낼 필요조건(Part 2 개정에서 정립한 X-3 4중 조건: 단일 값 선택이 필수 + 후보 2개 이상 + 보존·defer 불가 + compact로 판정 충분)을 적용하면 이 47건 중 pack에 남을 항목은 사실상 0건이다. + +**P3-8. budget_overflow에 의한 임의 절단.** 자동 차단 17건의 사유는 전부 `budget_overflow`(max_llm_items=30 초과)다. 즉 예외 47건 중 어떤 17건이 LLM 판정도 받지 못하고 조용히 review로 빠지는지가 정렬 순서(유형 → BO 수 → id)라는 임의 기준으로 결정된다. 판정 대상 선별이 법률적 중요도가 아니라 큐 순서로 정해지는 구조다(과다 생성의 2차 피해). + +**P3-9. 판정 결과가 review 신호에 반영되지 않아 리뷰 큐가 포화된다.** LES1은 예외가 하나라도 걸린 seed에 `review_required=true`를 찍는데, LES2가 그 예외를 확신 있게 판정해도 이 플래그를 해소하는 로직이 없다. 결과: **구조물 50건 전부 review_required=true, review_queue 78건**(structure_review_required 50 + blocked_exception 28). 변별력 0인 리뷰 신호는 인간 검토자와 Stage 2 모두에게 무의미하며, LLM이 수행한 판정 노동의 가치도 소거된다. 또한 자동 차단 17건은 최종 review_queue에 식별 가능한 항목으로 남지 않고(감사 필드에만 존재) 구조물 플래그로만 뭉개져 추적성 공백이 있다. + +**P3-10. 사양 과잉(부차적).** enum 선택형 라우팅 판정에 `llm_reasoning: high` + `llm_verbosity: medium` + `max_iterations: 2`가 설정되어 있다. 비용은 실측 $0(flash 계열)이므로 금전 문제는 아니나, 30건 판정 + 28건 블록 사유 서술 출력은 3:40 런타임 중 LLM 구간을 불필요하게 늘린다. 한편 LES2의 `COMMON_CACHE_PREFIX_STAGE_1`은 Part 1/2 canonical과 **바이트 동일**(sha256 앞 8자리 58f2b72d)임을 확인했다 — 이 부분은 이미 정합하며 유지하면 된다. + +### 2.3 DAG·실행 구조 비효율 + +**P3-11. 순수 결정론 작업의 3-task 분할.** LES0·LES1·LES3는 전부 결정론 Python인데 별개 run_code task로 분리되어, MCP 세션 초기화 3회, localdocs 왕복 다수(입력 8 read + 중간 파일 write 3/재읽기 2), run_code 왕복 3회가 발생한다. LES0→LES1은 다른 소비자가 전혀 없는 pack을 사이에 두고 갈라져 있을 뿐이므로 합치면 pack 자체(파일 416KB + stdout 257KB + 주입 257KB)가 사라진다. 완전 직렬 체인이라 분리로 얻는 병렬성 이득도 없다. + +**P3-12. run_code 페이로드 비대.** LES1 요청은 코드 약 40KB + 주입 257KB ≈ 297KB다. 파일 채널로 바꾸면 코드만 남는다. + +### 2.4 강건성·SKILL 정합 (Part 1/2 v3에서 확립한 기준 대비) + +**P3-13.** 세 Python task 모두 read_docs 응답을 strict `json.loads`로만 파싱한다(SKILL.md가 금지한 "Extra data 취약" 패턴 — raw_decode fallback 부재). Part 1/2 v3에서는 전 task에 fallback을 적용했다. + +**P3-14.** 실패 시 `sys.exit(0)` — 실패가 정상 종료로 위장되어 백엔드가 FAILED를 태스크 실패로 감지하지 못할 수 있다(Part 1/2 v3는 FAILED JSON 출력 + raise). + +**P3-15.** LES3의 LES2 출력 파서는 코드펜스 salvage가 없고 오히려 tail 검사(````` 잔여물)로 실패하므로, LLM이 펜스를 붙이면 max_iterations 2에 의한 **재호출**이 유발된다. Part 2 v3 R0의 §6.7 관용 파서(salvage 후 warning 강등)와 대조된다. + +### 2.5 유지해야 할 강점 (실측 확인) + +공통 캐시 프리픽스의 바이트 정합(P3-10), 최종 산출물의 5-way 인덱스·커버리지 게이트·write 후 재읽기 검증, 예외 서명 병합(merged_from 9건 절감), LES3의 결정 enum·부분집합 검증은 견실하며 개정에서 그대로 보존할 자산이다. + +## 3. 효율화 방안 — 최소 작업 / 최대 효과 + +개정 원칙은 Part 1/2 개정에서 검증된 문법의 이식이다: **결정론이 전부 처리하고, LLM은 결정론이 처리할 수 없는 소수 예외만 조건부로 본다. 채널은 파일로 단일화하고 stdout은 1줄 요약만 낸다.** 최종 산출물 `legal_effect_structures.json`의 스키마·인덱스 5종·Part 4 소비 필드(FL0의 `_compact_structure`가 읽는 13개 필드 + `by_bo_id`/`by_issue_cluster_id`/`by_asset_cluster_id` 인덱스 + status)는 불변 계약으로 고정한다. + +**R-1. task 통합: 4 → 3 (LES0+LES1 → L0).** LES0와 LES1을 단일 code task `L0_structure_seed_reducer`로 합친다. input pack은 파일·stdout·주입 어디에도 물질화하지 않고 메모리 내 변수로만 존재시킨다. 이것만으로 P3-1·P3-2·P3-11·P3-12가 한꺼번에 소멸한다(pack 관련 전송 930KB 제거, run_code 1회·MCP 세션 1회 절감). L0의 산출은 (a) slim seed bundle 파일, (b) exception pack 파일(조건부 LLM용, Part 2 형식), (c) stdout 1줄 요약뿐이다. + +**R-2. 입력 다이어트.** L0 입력에서 `evidence_all.json` 읽기를 제거한다(P3-3 — 기여 0 실측이므로 downstream 위험이 없다). seed bundle에서 LES3 미소비 필드(진단·type_index_seed·bundles)는 제거하거나 별도 소형 audit 파일로 이전한다(P3-4). exception pack은 단독 파일 1곳에만 저장하고 seed bundle 내장을 제거한다 — L2(현 LES3)의 `_expected_exceptions`가 파일을 읽도록 1줄 변경. 예상 seed bundle: 422KB → 약 250KB 이하. + +**R-3. 결정론 defer 정책 도입으로 LLM 실질 0회화 (Part 2 X-2/X-3 이식).** 실측이 근거다: 판정 47건 중 최종 산출을 바꾼 것이 0건이므로, 아래 기본값은 7/10 사건에서 **무손실**이 보장된다. + +| 예외 유형 | 결정론 기본값 (보존적) | 근거 | +|---|---|---| +| ambiguous_candidate_structure_type (24건) | `PRESERVE_MULTI_ROUTING` + 우선순위표 대표 타입 | 실측상 LLM 결과와 100% 동일. multi-routing 보존은 정보 무손실이며 Part 4는 전 타입을 소비 | +| ambiguous_asset_cluster (6건) | `SEPARATE_CLUSTER` 보존 + review code | 암묵 병합 금지 원칙(Part 2 near-dup KEEP_SEPARATE와 동형). 병합은 인간/후속 단계 결정 | +| ambiguous_issue_cluster (0건) | 현행 hard-key/score 규칙 유지 | 7/10에서 이미 결정론이 전건 처리 | + +pack 수록은 X-3 4중 조건(①최종 writer가 단일 값을 선택해야만 진행 가능 ②source-backed 후보 2개 이상 ③보존·defer 불가 ④compact 근거로 판정 충분)을 전부 충족하는 항목으로 한정한다. LES2가 93%를 "근거 불충분"으로 되돌려 보낸 실측이 보여주듯, ④를 사전 적용하면 보낼 것이 거의 없다. 이로써 budget_overflow 임의 절단(P3-8)도 소멸한다(잔여 예외가 cap보다 훨씬 적어짐). + +**R-4. LES2 → L1 조건부 어드주디케이터로 전환.** Part 1 v3 GB / Part 2 v3 R1과 동일 골격: `preflight_files: [exception pack]`만 입력, `has_exceptions=false`면 no-exception 판정 파일을 쓰고 "NO_EXCEPTIONS" 출력 후 즉시 종료. 프리픽스는 현행 canonical 블록(바이트 동일) 유지. `llm_verbosity`는 low로(enum 선택 출력), 모델·reasoning은 실질 0회화가 전제이므로 현행 유지를 기본으로 하되 flash-lite/low 하향은 통제된 A/B로만 검토(N-3 원칙 준용). max_iterations 재호출 의존 대신 L2에 §6.7 관용 파서를 이식한다(P3-15). + +**R-5. L2(최종 writer) 정비.** 사장된 `SEED_RAW` 주입 제거(P3-5 — 파일 읽기로 일원화), 판정·defer 결과를 review 신호에 반영: 결정론/LLM 판정이 확정한 항목은 `review_required`를 해소하고, BLOCK_REVIEW·defer-review·자동 차단만 review_queue에 **식별 가능한 항목으로** 남긴다(P3-9). 이는 효율화이자 품질 회복이다 — 리뷰 큐가 "변호사가 실제로 봐야 할 것"만 담게 된다. 최종 파일 스키마는 불변. + +**R-6. SKILL 정합 일괄 적용.** read_docs raw_decode fallback(P3-13), 실패 시 FAILED 1줄 출력 + raise(P3-14), stdout 전 task 1줄 JSON(P3-1의 stdout 측면), `r"""…"""` 템플릿 주입 유지. + +**검증 계획(개정 시).** 7/10 fixture로 골든 회귀: L0→(L1)→L2 재실행 → `legal_effect_structures.json`이 현행 산출과 의미 동등(구조 50건, 대표 타입/IC/AC id 매핑, 커버리지 게이트 PASS, Part 4 FL0 파서 통과)해야 하며, review_queue는 축소가 **의도된 개선**이므로 "축소 전 항목의 전량이 audit에 추적 가능"을 통과 기준으로 삼는다. 2-run 바이트 결정론 포함. + +## 4. 기대 효과 정량 요약 + +| 지표 | 현행 (실측) | 개정 후 (추정) | +|---|---|---| +| task 수 | 4 (직렬) | 3 (L1은 실질 0회 조기 종료) | +| LLM 호출 | 1회 무조건, 프롬프트 46.8KB, 판정 30건 | 예외 없으면 즉시 종료(수백 토큰), 있어도 pack만 | +| stdout 총량 | 약 290KB | 약 1KB (요약 3줄) | +| run_code 페이로드 | 약 340KB (LES1 297KB 포함) | 약 90KB (코드만) | +| 중간 파일 IO | 891KB write + 422KB re-read | 약 250KB write (slim bundle + pack) | +| 입력 read | 8파일 (evidence_all 87KB 포함) | 7파일 | +| 런타임 | 3분 40초 | 1분 내외 예상 (LLM 구간 소거 + 전송 1/4) | +| 비용 | $0 | $0 (유지) | +| 최종 산출 | 255KB, review 50/50 포화 | 스키마 동일, review 신호 변별력 복원 | + +## 5. 잔여 리스크와 열린 질문 + +첫째, LES2를 "실질 0회"로 만드는 근거는 7/10 사건 1건의 실측이다. 사건 유형이 달라지면(예: 자산 동일성 다툼이 실제 쟁점인 사건) asset 예외 중 일부는 진짜 판정 가치가 있을 수 있으므로, L1을 **제거하지 않고 조건부로 남기는** 본 설계가 안전하다. 둘째, R-5의 review 신호 복원은 후속 소비자(Stage 2·인간 검토)의 기대를 바꾸므로, 축소된 큐가 감사 파일에서 전량 추적 가능해야 한다(검증 계획에 반영). 셋째, issue cluster가 7/10에서 BO당 1개(50:50)로 퇴화한 것은 클러스터링 키 설계의 개선 여지를 시사하나, 이는 효율이 아닌 기능 고도화 주제이므로 본 개정 범위 밖 제안으로만 남긴다. + +--- +*본 보고서의 수치는 전량 7/10 실행물과 로컬 바이트-일치 재실행에서 실측되었다. 다음 단계는 본 보고서를 기반으로 한 개정 전략 수립(Codex 보고서와의 비교 포함)이다.* diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_3_Inefficiency_Report_Codex_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_3_Inefficiency_Report_Codex_v1.md new file mode 100644 index 00000000..0ee99a71 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_3_Inefficiency_Report_Codex_v1.md @@ -0,0 +1,634 @@ +# Stage 1 Part 3 비효율성 분석 및 최적화 보고서 + +- 작성일: 2026-07-20 +- 분석 대상: `Stage_1_Part_3_v1.yml` +- 연계 기준: 최적화된 Part 1·2와 현행 Part 4의 입출력 계약 +- 평가 관점: 대한민국 민사소송 실무와 대규모 LLM 파이프라인 아키텍처를 함께 고려한 엄격 평가 + +## 1. 결론 요약 + +현행 Part 3은 `BO.json`과 3개 signal을 `legal_effect_structures.json`으로 전환하고 Part 4에 필요한 5개 index를 생성한다는 **외부 작업 목표는 달성**한다. 그러나 내부 실행 방식은 다음 이유로 효율성과 결과 품질 모두에서 개정이 필요하다. + +1. LES2가 예외 유무와 무관하게 항상 한 번 실행된다. +2. 법률구조 유형이 아닌 `event`, `state`, 행위유형, 세부 사건 태그까지 `candidate_structure_types`로 승격하여 50개 BO 전부를 구조유형 예외로 만든다. +3. 하나의 BO가 여러 downstream 역할을 갖는 정상적인 multi-routing까지 LLM 판단 대상으로 보낸다. +4. 실측상 LES2 입력 30건 중 28건이 `BLOCK_REVIEW`로 끝났고, 예산 초과 17건은 LES2에 들어가지도 못했다. +5. LES3는 LES2가 예외를 해결해도 LES1의 `review_required: true`를 해제하지 않아, 최종 50개 구조 전부가 review queue에 남았다. +6. LES0은 LES1이 쓰지 않는 원문·상위 집합을 포함한 416,115 byte input pack을 파일로 쓰고 동일 내용을 stdout으로도 전송한다. +7. LES1 seed bundle은 진단·예외·재생성 가능한 index를 중복 보존하여 422,236 byte까지 커진다. +8. Part 1·2 v3의 common cache prefix와 Part 3의 prefix가 바이트 단위로 달라 upstream cache를 공유할 수 없다. + +따라서 최적 개정 방향은 **“결정적 정규화로 예외 자체를 먼저 제거하고, 남은 비가역적 모호성만 조건부 micro-pack LLM으로 판정하며, LES3가 해결 상태를 다시 계산하도록 하는 것”**이다. 현재 fixture에서는 LES2 호출을 30건에서 원칙적으로 0건까지 줄일 수 있는 구조가 타당하다. 다만 실제 0건 달성 여부는 개정 후 동일 fixture 재실행으로 검증해야 한다. + +## 2. 분석 범위와 근거 + +### 2.1 읽은 작업 명세서 + +| Part | 기준 파일 | 확인한 핵심 | +|---|---|---| +| Part 1 | `Stage_1_Part_1_Codex_v3.yml` | shard map, 결정적 B1/B2 gate, 조건부 wildcard adjudicator, 최종 audit/writer | +| Part 2 | `Stage_1_Part_2_Codex_v3.yml` | 5개 domain worker, 결정적 R0, 조건부 `R1_*`, F0 단독 writer, signal writer | +| Part 3 | `Stage_1_Part_3_v1.yml` | LES0 → LES1 → 단일 LES2 → LES3 구조 및 모든 Python/LLM 계약 | +| Part 4 | `Stage_1_Part_4_v2.yml` | `legal_effect_structures.json` 소비 계약, Fact Ledger 생성, LES2 재판단 금지 규칙 | + +### 2.2 실행 결과 근거 + +`Results_July_10_3_15pm`의 Part 3 관련 산출물 전체를 계측하였다. + +| 산출물 | 크기 | 핵심 수치 | +|---|---:|---| +| `BO.json` | 175,126 B | BO 50건 | +| `evidence_indexed.json` | 91,841 B | evidence 30건 및 registry/schedule component | +| `actio_case_signals.json` | 9,990 B | Part 3 입력 signal | +| `case_liability_signals.json` | 15,565 B | Part 3 입력 signal | +| `legal_effect_signals.json` | 19,093 B | Part 3 입력 signal | +| `legal_effect_structure_input_pack.json` | 416,115 B | LES0 중간팩 | +| `legal_effect_structure_seed_bundle.json` | 422,236 B | LES1 seed 및 중복 진단 | +| `legal_effect_structure_exception_pack.json` | 52,982 B | LES2 예외 30건 | +| `legal_effect_structures.json` | 254,991 B | 최종 구조 50건, review queue 78건 | + +제공된 Part 3 전체 실행시간은 3분 40초, 표시 비용은 `$0`이다. 다만 결과 폴더에는 task별 timestamp, provider token usage, cache hit 여부가 없으므로 LES0·LES1·LES2·LES3별 정확한 소요시간을 사후 배분할 수는 없다. 아래 병목 판정은 YAML 구조, 실제 pack 크기, 예외 수와 최종 판정 결과에 근거한다. `$0` 표시는 계산량이나 wall-clock 지연이 없다는 의미가 아니다. + +## 3. Stage 1 전체 흐름에서 Part 3의 위치 + +최적화된 Part 1·2는 이미 다음 패턴을 사용한다. + +```text +deterministic planner/reducer + | + +---- no exception ------------------+ + | | + +---- dynamic_fanout -> LLM_* --------+--> deterministic final writer +``` + +반면 Part 3은 예외가 없어도 단일 LES2를 반드시 통과한다. + +```text +Part 1 v3 Part 2 v3 +evidence + events -----> BO + 3 signals + | + v + Part 3 v1 (현행) + +-------------------------------+ + | | + v | + LES0 input compiler | + | | + v | + LES1 seed builder | + | | + v | + LES2 single LLM, 항상 실행 | + | | + v | + LES3 final writer | + +-------------------------------+ + | + v + Part 4 Fact Ledger +``` + +Part 3만 최적화된 upstream의 조건부 adjudication 패턴을 따르지 않는 것이 첫 번째 구조적 불일치다. + +## 4. 현행 Part 3 작업 구조 + +| Task | 실행 유형 | 주요 입력 | 주요 출력 | 현재 역할 | +|---|---|---|---|---| +| `Task_LES0_input_pack_compiler` | Python | client goal, BO, evidence 3종, signals 3종 | `legal_effect_structure_input_pack.json` 및 전체 pack stdout | 8개 파일을 하나의 중간팩으로 통합 | +| `Task_LES1_deterministic_structure_seed_builder` | Python | LES0 stdout 또는 input pack 파일 | seed bundle, exception pack, LES2 compact input | issue/asset/type seed 및 예외 생성 | +| `Task_LES2_single_cluster_exception_adjudicator` | LLM | LES1의 adjudicator input | 단일 adjudication JSON | 최대 30개 예외를 한 호출에서 판정 | +| `Task_LES3_final_structure_index_writer` | Python | seed bundle 및 LES2 응답 | `legal_effect_structures.json` | 최종 구조와 5개 index 생성 | + +현행 DAG는 완전 직렬이다. + +```text +IN + | + v +LES0 (Python, 대형 input pack 작성 + 전체 stdout) + | + v +LES1 (Python, seed/exception 작성 + 30건 LLM pack stdout) + | + v +LES2 (Gemini 3.5 Flash, high/medium, 항상 1회) + | + v +LES3 (Python, final writer) + | + v +OUT +``` + +## 5. 실측으로 확인된 핵심 비효율 + +### 5.1 가장 큰 병목: 정상적인 multi-routing을 예외로 오인 + +LES0은 다음 값을 모두 `candidate_structure_types`에 합친다. + +- `legal_effect_signals.candidate_structure_types` +- `case_liability_signals.liability_candidate_type` +- `actio_case_signals.actio_scope`의 일부 매핑값 +- `domain_legal_effect_tag`, `legal_effect_tags` +- BO source domain의 coarse type + +그 후 알려진 값으로 정규화되지 않은 문자열도 그대로 반환한다. 실제 input pack에는 다음이 함께 들어갔다. + +- 법률구조 family: `money_claim`, `secured_debt`, `registry_invalidity`, `fraudulent_transfer` 등 +- transport/state label: `event`, `state` +- 행위 분류: `법률행위(legal_acts)`, `사실행위(factual_acts)`, `준법률행위(quasi-legal_acts)` +- 세부 사건명: `변제`, `상계`, `소유권이전`, `임의경매`, `상속포기` 등 + +실측치는 다음과 같다. + +| 항목 | 수치 | +|---|---:| +| candidate type 전체 출현 수 | 263 | +| distinct type 수 | 69 | +| 현행 role 규칙상 복수 downstream role로 판정된 BO | 50 / 50 | +| Part 4가 명시적으로 아는 canonical type만 남겼을 때 복수 role BO | 14 / 50 | + +LES1은 type들이 둘 이상의 downstream role로 매핑되면 곧바로 `ambiguous_candidate_structure_type`을 만든다. 이 규칙 때문에 모든 BO가 한 번씩 예외가 되었다. + +법률적으로도 이 판정 방식은 적절하지 않다. 하나의 사실이 대여금 청구, 승계책임, 담보관계, 사해행위 또는 가액산정에 동시에 연결되는 것은 모순이 아니라 **대체적·예비적 청구와 법률요건 사실을 보전하기 위한 정상적인 다중 라우팅**이다. Stage 1에서 하나만 고르는 행위는 오히려 downstream 법리 검토 범위를 부당하게 축소할 수 있다. + +**판정:** 여러 canonical type의 공존 자체는 LLM 예외가 아니다. canonical type 전체를 보존하고, 대표 type만 고정 priority로 정하면 된다. + +### 5.2 LES2의 실질적 판정 효용이 매우 낮음 + +LES1 결과는 다음과 같다. + +| 예외 처리 단계 | 수치 | +|---|---:| +| raw type 예외 | 50 | +| asset 예외 | 6 | +| merge 후 대표 예외 | 47 | +| LES2에 전달 | 30 | +| budget 초과로 auto-block | 17 | +| LES2 결과 중 `BLOCK_REVIEW` | 28 | +| LES2가 비차단 판정한 건 | 2 | + +최종 `legal_effect_structures.json`의 review queue는 다음과 같다. + +| review 유형 | 건수 | +|---|---:| +| `structure_review_required` | 50 | +| `blocked_exception` | 28 | +| 합계 | 78 | + +특히 type 예외 24건은 LES2에서 전부 차단되었다. 고성능 LLM이 처리한 예외 중 실제로 해소된 것은 asset 예외 2건뿐이다. 이 호출은 runtime을 사용하면서도 review queue를 거의 줄이지 못했다. + +### 5.3 성공한 LLM 판정도 review 상태를 해제하지 못함 + +LES1은 `issue_exc`, `asset_excs`, `type_exc` 중 하나라도 있으면 seed의 `review_required`를 `true`로 고정한다. LES3는 최종 구조를 만들 때 다음 의미의 OR 연산만 한다. + +```text +final review_required + = seed.review_required + OR blocked BO + OR split structure +``` + +LES2가 `SAME_CLUSTER`, `SEPARATE_CLUSTER`, `PRESERVE_MULTI_ROUTING` 등으로 예외를 성공적으로 해결한 경우에도 `seed.review_required`를 지우는 경로가 없다. 따라서 LES2의 성공 판정이 final review workload를 감소시키지 않는다. + +**판정:** 이는 단순 성능 문제가 아니라 판정 상태 모델의 결함이다. seed에는 `review_required` 대신 `pending_exception_ids`를 두고, LES3가 unresolved/blocked/split 예외만으로 최종 review 상태를 다시 계산해야 한다. + +### 5.3.1 `PRESERVE_MULTI_ROUTING`도 final index에 충분히 반영되지 않음 + +LES3는 type decision의 복수 `candidate_structure_types`를 구조 내부 `routing_audit`에는 남기지만, `structure_index.by_bo_id`와 `structure_index.by_candidate_structure_type`에는 대표 `candidate_structure_type` 하나만 넣는다. Part 4의 compact structure도 대표 type 하나를 중심으로 읽는다. + +따라서 LES2가 어렵게 `PRESERVE_MULTI_ROUTING`을 선택해도 모든 canonical routing type이 final index를 통해 전달된다는 보장이 없다. 이는 LES2 판단의 downstream 효용을 더 낮춘다. + +**개정 원칙:** LES3는 대표 type과 별개로 `candidate_structure_types[]` 전체를 final structure의 named field로 보존하고, `by_bo_id` 및 `by_candidate_structure_type`을 모든 canonical type에 대해 생성해야 한다. 기존 5개 index의 key 이름과 object 형태는 유지하므로 Part 4 계약과 양립한다. + +### 5.4 30건 hard cap과 실제로 구현되지 않은 phase 2 + +LES1의 예산은 다음과 같다. + +```text +max_llm_items = 30 +max_candidate_values_per_item = 8 +max_chars_per_item = 1200 +overflow_policy = SPLIT_BATCH_THEN_BLOCK +``` + +그러나 실제 DAG에는 split batch phase 2가 없다. 30건을 넘는 17건은 `budget_overflow`로 auto-block되고, 재판정 task가 생성되지 않는다. 명칭은 `SPLIT_BATCH_THEN_BLOCK`이지만 실행은 사실상 `TRUNCATE_THEN_BLOCK`이다. + +이 방식은 다음 문제를 함께 만든다. + +- 사건이 복잡할수록 LLM 검토를 더 받는 것이 아니라 더 많은 항목이 자동 차단된다. +- auto-block 항목은 exception pack에서 빠지고, final에는 개별 exception ID가 없는 포괄 review 상태만 남을 수 있다. +- 단일 30건 prompt가 길어져 독립 항목 간 주의력 경쟁과 JSON 누락 위험이 증가한다. + +### 5.5 LES0의 과도한 read와 대형 중간팩 + +LES0은 8개 파일을 각각 MCP로 읽는다. 그러나 LES1의 실제 소비 관계를 추적하면 다음과 같다. + +| input pack 필드 | compact JSON 크기 | LES1 실제 소비 | 판정 | +|---|---:|---|---| +| `bo_by_id` | 89,045 B | 변수에 할당만 하고 사용하지 않음 | 삭제 | +| `candidate_inputs_by_bo_id` | 102,148 B | 사용 | 유지·축소 | +| `evidence_authority_by_index` | 5,384 B | 사용 | 최소 필드 유지 | +| `signals_by_bo_id` | 42,199 B | 사용하지 않음; candidate에 이미 중복 | 삭제 | +| `events_by_evidence_index` | 2,669 B | 사용하지 않음 | 삭제 | +| `client_goal_compact` | 793 B | 사용하지 않음 | 삭제 | + +`candidate_inputs_by_bo_id.related_event_candidates`도 LES1에서 읽지 않는다. 따라서 Part 3 관점에서 `client_goal.json`과 `evidence_event_candidates.json`의 read는 제거할 수 있다. + +`evidence_all.json`은 evidence excerpt 보강 목적으로만 쓰인다. Part 3은 merits fact extraction을 다시 하는 단계가 아니며 최종 구조에는 evidence index가 핵심이다. 다음 조건을 충족하면 Part 3 read 대상에서 제거할 수 있다. + +1. `evidence_indexed.json`이 evidence ID, title/source pointer, registry/schedule component를 제공한다. +2. BO가 자신의 `source_evidence_indexes`를 보존한다. +3. Part 4가 필요한 원문/excerpt는 Part 4 source pack에서 독립적으로 읽는다. + +현행 Results 폴더에도 `evidence_all.json`은 보존되어 있지 않다. 다만 실제 운영 workspace에서만 존재하는 원문이 있으므로, 제거 전 fixture에서 evidence coverage conservation을 반드시 확인해야 한다. + +### 5.6 대형 stdout과 파일 재읽기의 중복 + +LES0은 416,115 B input pack을 파일로 저장한 뒤 같은 pack 전체를 stdout으로 출력한다. LES1은 stdout parse에 실패하면 동일 파일을 다시 읽는다. 이 구조는 다음 비용을 만든다. + +- localdocs write +- orchestration stdout 저장·전달 +- 다음 task의 template 확장 및 JSON parse +- fallback 시 localdocs read + +Python task 간 전달에 원문 전체가 필요하지 않다. stdout은 schema, status, path, hash, count만 포함하는 manifest로 제한하고, 실제 payload는 하나의 authoritative artifact에서 읽어야 한다. + +### 5.7 seed bundle 내부 중복 + +실측 seed bundle의 compact component 크기는 다음과 같다. + +| component | compact JSON 크기 | LES3 소비 여부 | +|---|---:|---| +| `structure_seed_items` | 75,377 B | 사용 | +| `issue_cluster_seed_map` | 21,162 B | 사용 | +| `asset_cluster_seed_map` | 19,430 B | 사용 | +| `evidence_index_seed` | 15,363 B | 사용 | +| `multi_asset_bundles` | 2,484 B | final writer 직접 사용 안 함 | +| `candidate_structure_type_index_seed` | 41,367 B | 사용 안 함; LES3가 final index 재생성 | +| `ambiguity_diagnostics` | 34,864 B | 사용 안 함 | +| 내장 `legal_structure_exception_pack` | 32,629 B | expected exception 검증에 사용 | + +동일 exception pack은 별도 52,982 B 파일로도 저장된다. 즉 진단 배열, seed 내장 pack, 별도 pack에 같은 예외 데이터가 반복된다. + +**판정:** 별도 exception pack을 authoritative artifact로 유지하고 seed에는 `exception_pack_path`, hash, expected exception IDs만 둔다. `candidate_structure_type_index_seed`는 삭제하고 LES3가 final structures로부터 재생성한다. + +### 5.8 자산 alias/ref를 충분히 이용하지 않아 발생하는 false exception + +실측 asset 예외 6건 중 다수는 다음 조합이다. + +- `SCH-016-01` ↔ `성수동 256 대` +- `SCH-016-04` ↔ `평택시 ... 서정빌라 101호` +- `SCH-016-05` ↔ `흑석동 201호` +- 복수 부동산을 함께 적은 BO + +`evidence_indexed.json`에는 `SCH-016-01` 등의 `schedule_ref`와 `canonical_label_candidate`가 이미 존재한다. 최적화된 Part 2 v3도 `asset_alias`에 `schedule_refs`/`registry_refs`를 보존하도록 명시한다. 그런데 현행 LES1은 keyed candidate와 unkeyed label을 별도 그룹으로 만든 후, 두 그룹이면 다시 LLM 예외를 만든다. + +법률상 자산 동일성은 fuzzy 문자열 유사도보다 등기행·별지목록 ref가 우선한다. 다음 우선순위로 결정해야 한다. + +1. 같은 `REG-*` 또는 `SCH-*` ref: 동일 cluster +2. Part 2 `asset_alias`가 ref와 label을 명시적으로 연결: 동일 cluster +3. 하나의 BO가 서로 다른 ref를 명시: 별도 cluster를 보존하고 multi-asset bundle 생성 +4. ref 없는 자유문구만 있고 동일성이 불명확: 임의 병합하지 않고 `SEPARATE_CLUSTER + review` 또는 실제 비가역성이 있을 때만 LLM + +이 규칙은 LLM보다 빠르고, 권리객체의 잘못된 병합을 방지하므로 법률적으로도 더 안전하다. + +### 5.9 모델과 cache 설정의 과잉 + +현행 LES2는 다음 설정이다. + +```text +model = gemini-3.5-flash +reasoning = high +verbosity = medium +max_iterations = 2 +``` + +그러나 LES2가 허용하는 작업은 제공된 후보 중 enum 하나를 선택하거나 `BLOCK_REVIEW`를 반환하는 좁은 routing 판정이다. raw evidence, 외부 tool, 새로운 법률사실 생성은 모두 금지되어 있다. 이 작업에 `high` reasoning과 `medium` verbosity는 과도하다. + +common prefix도 최적화된 Part 1·2와 다르다. + +| 파일군 | prefix bytes | SHA-256 | +|---|---:|---| +| Part 1 Codex v3 / Part 2 Codex v3 | 3,385 | `6e01f72f1803767b6bb16b5aef6cf34760e6dc1b293c316c75e990d09ad5fe63` | +| Part 3 v1 / Part 4 v2 | 3,829 | `58f2b72d6c2650739c45d85cc57899150434db86a97bf19cd5a59d92d11dfbb5` | + +문구가 유사해도 들여쓰기와 내용이 달라 byte-identical cache prefix가 아니다. Part 3은 Part 1·2 v3의 3,385-byte canonical block을 그대로 사용해야 한다. + +### 5.10 실패를 성공 exit로 보고하는 문제 + +LES0, LES1, LES3는 exception을 잡아 `FAILED` JSON을 출력한 뒤 `sys.exit(0)`으로 종료한다. 이 경우 orchestration이 task 성공으로 오인하여 후속 task를 실행할 수 있다. 이는 재실행·디버깅 비용과 잘못된 최종 산출물 위험을 높인다. + +실패 시 compact FAILED manifest를 출력하되 반드시 non-zero exit 또는 exception re-raise를 사용해야 한다. writer는 검증 실패 시 기존 final file을 overwrite해서는 안 된다. + +## 6. Downstream 계약과 삭제 안전성 + +Part 4는 `legal_effect_structures.json`에 대해 다음을 강제한다. + +- `schema_version == "stage1_legal_effect_structures.v1"` +- `status in {READY, READY_WITH_REVIEW}` +- `legal_effect_structures[]` +- `structure_index.by_bo_id` +- `structure_index.by_issue_cluster_id` +- `structure_index.by_asset_cluster_id` +- `structure_index.by_evidence_index` +- `structure_index.by_candidate_structure_type` + +또한 Part 4 FL2는 LES2의 issue/asset/type ambiguity를 다시 판정하지 못한다. 그러므로 Part 3 최적화 시 **final contract와 unresolved review 신호는 절대 약화하면 안 된다.** + +| 변경 대상 | 삭제/변경 안전성 | 근거 | +|---|---|---| +| final file name 및 schema | 변경 금지 | Part 4가 직접 검증 | +| final 5개 index | 변경 금지 | Part 4 source pack과 Fact Ledger join key | +| BO/evidence conservation gate | 유지·강화 | 구조 누락 시 Fact Ledger 누락으로 전파 | +| `client_goal_compact` in LES0 pack | 삭제 가능 | LES1/LES3 미사용, Part 4는 별도 입력 사용 | +| `events_by_evidence_index` 및 `related_event_candidates` | 삭제 가능 | LES1/LES3 미사용 | +| top-level `bo_by_id` | 삭제 가능 | LES1에서 할당만 하고 사용하지 않음 | +| top-level `signals_by_bo_id` | 삭제 가능 | candidate record에 필요한 ID/type가 이미 투영됨 | +| `candidate_structure_type_index_seed` | 삭제 가능 | LES3가 final structures에서 index 재생성 | +| seed 내 진단/exception 중복 | 삭제 가능 | 하나의 authoritative exception pack과 hash로 대체 | +| raw `evidence_all.json` Part 3 read | 조건부 삭제 | evidence ID/source conservation 검증 후 제거 | +| unresolved exception 및 review queue | 삭제 금지 | Part 4가 LES ambiguity를 재판단할 수 없음 | + +## 7. 권장 개정 전략 + +### 7.1 P0: 정확성 결함을 먼저 수정 + +1. seed의 `review_required` boolean을 `pending_exception_ids`로 바꾼다. +2. LES3가 `resolved`, `blocked`, `split`, `auto_review` 상태를 exception ID별로 계산한다. +3. 최종 `review_required`는 unresolved/blocked/split인 경우에만 `true`로 한다. +4. `expected_exception_ids == decision_ids ∪ deterministic_resolution_ids ∪ blocked_ids`를 강제한다. +5. 중복 decision, 미지 exception ID, 후보 밖 selected value를 hard fail한다. +6. BO ID와 evidence ID conservation을 final write 전후 모두 검증한다. +7. Python task 실패는 non-zero로 종료한다. + +### 7.2 P1: LES0 input diet + +Part 3의 필수 read를 원칙적으로 다음 5개로 줄인다. + +```text +BO.json +evidence_indexed.json +actio_case_signals.json +case_liability_signals.json +legal_effect_signals.json +``` + +LES0의 authoritative payload는 BO별 한 개의 compact record로 제한한다. + +```json +{ + "bo_id": "bh24", + "hard_issue_keys": {}, + "canonical_structure_types": [], + "nonrouting_labels": [], + "asset_candidates": [], + "evidence_indexes": [], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [], + "linked_actio_signal_ids": [], + "basis_codes": [] +} +``` + +full BO와 full signal row를 중복 보존하지 않는다. stdout에는 다음만 출력한다. + +```json +{ + "les0_manifest": { + "status": "READY", + "artifact_path": "stage1_tmp/legal_effect_structure_input_pack.json", + "run_fingerprint": "sha256:...", + "bo_count": 50, + "candidate_count": 50 + } +} +``` + +### 7.3 P2: 구조유형을 closed canonical routing enum으로 제한 + +Part 4가 실제로 module/role로 해석하는 type을 canonical enum으로 삼는다. + +```text +money_claim +commercial_successor +secured_debt +registry_invalidity +land_use_gain +valuation +fraudulent_transfer +preserved_claim +succession_notice_lien_asset_defense +general_legal_effect +``` + +세부 법률효과·행위·상태 label은 삭제하지 말고 `nonrouting_labels` 또는 signal audit에 보존한다. 다만 `candidate_structure_types`와 예외 판정에는 넣지 않는다. + +canonical type이 여러 개이면 다음을 결정적으로 수행한다. + +1. 모든 type을 `candidate_structure_types`에 보존한다. +2. `representative_candidate_structure_type`은 고정 priority로 선택한다. +3. 모든 type으로 `legal_effect_roles`와 `by_candidate_structure_type` index를 만든다. +4. 복수 type 자체만으로 exception을 만들지 않는다. + +LLM type 예외는 다음 경우로 한정한다. + +- 서로 다른 type을 하나의 structure에 보존하면 final schema의 상호 배타 필드가 충돌하는 경우 +- source-backed split 여부가 downstream 권리·청구 누락에 직접 영향을 주는 경우 +- deterministic priority로도 대표값을 정할 수 없고 대표값이 실제 writer 동작을 변경하는 경우 + +현행처럼 단순히 downstream role 수가 2개 이상이라는 이유로 예외를 만들면 안 된다. + +### 7.4 P3: asset ref 우선의 결정적 동일성 규칙 + +LES0에서 `evidence_indexed.schedule_items`와 registry component로 다음 map을 만든다. + +```text +asset_alias_by_ref[REG-* or SCH-*] + -> canonical_label_candidate + -> evidence_index + -> usable_in_relief / section / row metadata +``` + +Part 2 v3의 `asset_alias.schedule_refs`/`registry_refs`와 결합하여 동일 ref는 같은 cluster로 묶는다. 여러 다른 ref가 존재하면 임의 병합하지 않고 별도 cluster와 bundle을 보존한다. + +LLM asset 예외는 **상충하는 source-backed ref가 같은 법률상 자산을 가리킨다고 주장하면서, merge 여부가 실제 권리효과를 바꾸는 경우**로만 제한한다. 단순 자유문구 차이는 보수적 분리와 review flag로 처리할 수 있다. + +### 7.5 P4: LES2를 조건부 wildcard micro-adjudicator로 전환 + +Part 1·2 v3에서 이미 검증한 구조를 그대로 이식한다. + +```text +Task_LES1_deterministic_structure_seed_builder + outputs: + les1_manifest + dynamic_fanout: [] or [PACK-001, PACK-002, ...] + +Task_LES2_exception_adjudicator_* + one compact pack per instance + writes one decision part file + +Task_LES3_final_structure_index_writer + waits: + LES1 + all Task_LES2_exception_adjudicator_* +``` + +권장 pack 한도는 다음과 같다. + +| 항목 | 권장값 | +|---|---:| +| exception items per pack | 최대 3~4 | +| serialized pack size | 최대 12 KB | +| candidate values per item | 최대 8 | +| `max_concurrency` | 2 | +| model | `gemini-3.1-flash-lite` | +| reasoning | `medium` | +| verbosity | `low` | +| max iterations | 2 | +| tools | `[]` | +| preflight | `false` | + +예외가 없으면 wildcard instance는 0개이고 LES3가 `READY_NO_EXCEPTIONS` 경로로 즉시 진행한다. 예외가 있더라도 독립 pack을 최대 2개씩 병렬 처리한다. + +Part 3의 모든 LES2 wildcard instance는 Part 1·2 v3와 **완전히 동일한 3,385-byte common cache prefix**를 사용해야 한다. `cache_control.ttl`은 순차 Stage 1 전체 시간을 고려하여 `1h`로 둔다. + +### 7.6 P5: 중간 artifact 단일화 + +다음 원칙을 적용한다. + +- input pack: compact BO candidate records만 저장 +- seed bundle: LES3가 실제 사용하는 seed/map만 저장 +- exception pack: 예외 상세의 유일한 authoritative source +- decision parts: wildcard pack별 독립 파일 +- stdout: path, hash, count, status, dynamic fanout만 출력 +- final file: LES3만 작성 + +seed bundle에서 제거할 항목은 다음과 같다. + +- `candidate_structure_type_index_seed` +- 전체 `ambiguity_diagnostics` +- 내장 full `legal_structure_exception_pack` + +대신 다음만 둔다. + +```json +{ + "exception_pack_ref": { + "path": "stage1_tmp/legal_effect_structure_exception_pack.json", + "sha256": "...", + "expected_exception_ids": [] + } +} +``` + +### 7.7 LES0·LES1 병합은 2차 최적화로 보류 + +LES0과 LES1은 모두 Python이며, 병합하면 한 번의 task/session과 input pack write/read를 없앨 수 있다. 그러나 첫 개정에서 두 작업을 합치면 source compilation과 legal routing seed 생성의 책임 경계가 한 코드에 결합된다. + +가장 적은 변경으로 가장 큰 효과를 얻는 1차 개정은 다음이다. + +1. LES0 payload 축소 및 stdout manifest화 +2. LES1 false exception 제거 +3. LES2 conditional wildcard화 +4. LES3 review 상태 재계산 + +개정 후 계측에서 LES0→LES1 I/O가 전체 Part 3 runtime의 15% 이상이면 2차로 `LES01_compact_seed_exception_planner` 병합을 시행한다. + +## 8. 권장 개정 DAG + +```text + +----------------------------+ + | no true exception | + | dynamic_fanout = [] | + v | +IN --> LES0 compact compiler --> LES1 deterministic reducer -------+--> LES3 final gate/writer --> OUT + | | ^ + | | true exceptions | + | v | + | +-------------------------+ | + | | LES2_adjudicator_001 |-- decision ----+ + | | LES2_adjudicator_002 |-- decision ----+ + | | ... max_concurrency=2 | | + | +-------------------------+ | + | all + +-- compact artifact + hash -----------------------------+ +``` + +## 9. Task별 개정 명세 요약 + +| Task | 유지/변경 | 핵심 개정 | +|---|---|---| +| LES0 | 유지·축소 | 5개 필수 파일만 read, compact candidate record, ref alias map, manifest stdout | +| LES1 | 유지·핵심 개정 | canonical type closed enum, multi-routing deterministic, ref-first asset clustering, pending exception ID, dynamic packs | +| LES2 | 단일 task 삭제 후 wildcard로 대체 | true exception만 micro-pack 판정, lite model, medium/low, 병렬 2개 | +| LES3 | 유지·강화 | all wildcard wait, exact coverage, resolved review 해제, hash/conservation, final 단독 writer | + +## 10. 품질 보존 규칙 + +최적화는 단순 삭제가 아니라 다음 법률·데이터 보존 규칙을 전제로 한다. + +1. canonical type을 하나만 남기지 말고 모든 타당한 multi-routing type을 보존한다. +2. 상충하는 자산 ref를 fuzzy match로 병합하지 않는다. +3. `REG-*`, `SCH-*`, BO ID, evidence ID, claim/liability/signal ID를 renumber하지 않는다. +4. unresolved 예외는 review queue에 남긴다. +5. Part 4가 LES ambiguity를 재판단하지 않으므로 Part 3에서 exception coverage를 봉인한다. +6. final 5개 index와 source BO/evidence coverage를 현행과 동일하거나 더 엄격하게 검증한다. +7. LLM은 후보 밖 값을 만들지 못하고, 불충분하면 `BLOCK_REVIEW`만 반환한다. + +## 11. 개정 후 검증 기준 + +### 11.1 정적 검증 + +- YAML parse 성공 +- 모든 task name과 DAG reference 일치 +- Python code 전부 compile 성공 +- localdocs direct MCP boilerplate에 user/workspace hash 포함 +- read parser의 `raw_decode` fallback 포함 +- 실패 경로 non-zero exit +- 모든 LLM task의 common prefix hash 일치 +- wildcard parent의 `dynamic_fanout` 및 LES3의 `all Task_LES2_*` wait 확인 + +### 11.2 동일 fixture 회귀 검증 + +| 검증 항목 | 통과 기준 | +|---|---| +| BO conservation | 입력 BO 50건이 final `by_bo_id`에 100% 존재 | +| evidence conservation | seed evidence ID가 final index에 100% 연결 | +| final schema | Part 4의 현행 validator 무수정 통과 | +| canonical type 보존 | 개정 전 인정된 canonical routing type 누락 0 | +| exception coverage | expected = resolved + blocked + deterministic review | +| blanket review 제거 | 단순 multi-routing만으로 review 생성 금지 | +| writer authority | LES3 외 `legal_effect_structures.json` write 0건 | + +### 11.3 성능 목표 + +아래는 구현 후 측정할 acceptance target이며 현재 달성 실측치가 아니다. + +| 지표 | 현행 | 1차 목표 | +|---|---:|---:| +| input pack | 416,115 B | 200 KB 이하 | +| seed bundle | 422,236 B | 250 KB 이하 | +| LES2 전달 예외 | 30 + auto-block 17 | 정상 fixture 0, 예외 발생 시 pack당 3~4 | +| LLM task 실행 | 항상 1회 | 조건부 0..N | +| structure review | 50 / 50 | 실제 unresolved/split만 | +| Part 3 runtime | 3분 40초 | 3회 median 기준 40% 이상 단축 | +| Part 3 표시 비용 | `$0` | 증가 금지 | + +runtime은 cold/warm cache를 구분하여 최소 3회씩 측정하고 median을 사용한다. task별 elapsed time, prompt/input/output token, cache hit, pack count도 함께 기록해야 다음 최적화가 가능하다. + +## 12. 최종 권고 + +Part 3의 최우선 과제는 더 빠른 모델로 단일 LES2를 그대로 돌리는 것이 아니다. **LES1이 만들어내는 거짓 예외를 제거하고, 해결된 판정이 review 상태에 실제 반영되도록 상태 모델을 바로잡는 것**이 먼저다. + +권장 적용 순서는 다음과 같다. + +1. LES3의 `pending_exception_ids` 기반 review 재계산 +2. canonical structure type과 deterministic multi-routing 도입 +3. schedule/registry ref 기반 asset 정규화 +4. LES0 read/payload/stdout 축소 +5. LES2 conditional wildcard micro-pack 전환 및 lite model 적용 +6. seed/exception artifact 중복 제거 +7. 동일 fixture 회귀·성능 측정 후 필요할 때만 LES0·LES1 병합 + +이 순서를 따르면 Part 4의 외부 계약과 법률상 보수적 검토 원칙을 유지하면서, 현행 30건 monolithic LLM 호출과 17건 예산 차단, 50건 blanket review를 함께 제거할 수 있다. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_4_Improvement_Claude_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_4_Improvement_Claude_v1.md new file mode 100644 index 00000000..85eb0252 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_4_Improvement_Claude_v1.md @@ -0,0 +1,135 @@ +# Stage 1 Part 4 개정 전략 선별 보고서 (Claude v1) + +- 비교 대상: `Part_4_Inefficiency_Report_Claude_v1.md` ↔ `Part_4_Inefficiency_Report_Codex_v1.md` +- 목적: 두 분석의 교차 검증·상충 판정을 거쳐 Part 4 개정에 가장 적합한 전략을 선별 +- 선별 기준: 최소의 노력(LLM 추론은 꼭 필요한 곳에 필요한 수준만, 100% 결정론 작업은 Python화, 토큰 경제성 최대화)으로 최대의 효과(최고 품질 유지, 토큰 경제성 증대, 에이전트 속도 최대화) +- 작성 관점: 20년 이상 경력의 대한민국 최고 수준 변호사 + 세계적 수준의 AI 아키텍트 + +## 0. 총평 + +Part 4에서는 두 보고서의 수렴도가 시리즈 중 가장 높다. 핵심 실증 — **FL2(pro-preview/high, $0.58 전액)가 42건을 판정했으나 최종 `Fact_Ledger_base.json`이 한 행도 바뀌지 않았다** — 에 양측이 서로 다른 방법으로 독립 도달했다(Claude: 42건 전량 KEEP_AS_IS 가상 판정을 FL3에 투입한 대조 실험으로 50행 완전 동일 재현 / Codex: 후보본·최종본 전후 비교로 변경 행 0·필드 0 확인). 예외 42건의 발생 원인(cardinality 오분류), pack 602KB의 중복 구조, 미소비 필드 목록까지 수치 단위로 일치하므로 사실 기반은 확정이다. 차이는 이번에도 처방의 결이다: Claude는 구조 소거(FL0+FL1 병합, pack 비물질화)와 근거 실질화(빈 excerpt 교정)를, Codex는 FL3 게이트의 실질 결함 3건(경계 위반 patch 적용, 비엄밀 coverage, blocked 상태의 READY 위장)과 upstream review 승계를 잡았다. 본 보고서는 Codex의 게이트·승계 발견을 코드 수준에서 재검증해 전량 채택하고, 프리픽스 상충은 Part 3 비교에서 확립한 3-way 실측 판정을 재적용해 정리한다. + +## 1. 비교 분석 + +### 1.1 비효율 판정 항목 비교 + +**양측 공통 판정 (독립 교차 검증 — 확정 사실).** + +| # | 공통 판정 | Claude | Codex | +|---|---|---|---| +| 1 | 예외 42건(role 40 + weak 2) = 후보 50행의 84% — cardinality 오분류가 원인 | P4-1 | §6.1 | +| 2 | **FL2 실질 효과 0** (최종본 무변경) — 두 방법으로 독립 실증 | P4-3 대조 실험 | §7.3 전후 비교 | +| 3 | FL2 무조건 실행 + pro-preview/high 과대 사양 | P4-4 | §7.1·7.2 | +| 4 | 예외 pack 147KB의 3중 이동(파일 내장·stdout 재출력·프롬프트 주입) + 항목 비대 | P4-2·P4-5 | §6.3 | +| 5 | pack 602KB: bo/signal/evidence의 전역+BO별 이중 수록, event 2계열·client_goal·index 4종 미소비 | P4-6 (소비 추적) | §5.1·5.2 (수치 일치) | +| 6 | `evidence_all.json` 의존 문제 | P4-7 (기여 0 실측) | §4.2 (재현성 관점) | +| 7 | 최종 스키마·possession/notice/lien 게이트 3종·FL3 골격은 보존 자산 | §2.5 | §15.3 | + +**Claude 보고서 고유 발견.** + +- **빈 증거 발췌(P4-9)**: FL0의 excerpt 수집 키가 `evidence_indexed.json`의 실제 필드(`key_facts`/`key_dates`/`key_amounts`)와 불일치하여 **evidence authority 30건 전부 excerpt=None**. credibility/proof_strength가 제목 문자열에만 의존하고, FL2에 제공된 `related_evidence_excerpts`가 빈 껍데기였다 — FL2 무효과의 근인 중 하나이자, 조건부 판정 체계에서 잔여 예외가 실제로 발생할 미래 사건을 위해 반드시 교정해야 할 품질 결함. Codex 미포착. +- `datetime.now()` 3곳(FL0/FL1/FL3)에 의한 2-run 바이트 결정론 파괴 — 골든 회귀 검증력 저하. Codex 미언급. +- read_docs **envelope 계층** strict `json.loads`(3 task) — SKILL Extra-data 취약. Codex 미언급. +- FL1 재현 바이트 일치(candidate 50행·예외 42건 완전 재현) 방법론 — 모든 판정의 신뢰 기반. + +**Codex 보고서 고유 발견 (코드 재검증 결과 전량 사실 — 채택 가치 최상).** + +- **경계 위반 patch의 실제 적용(§9.2)**: `_apply_patch`가 `allowed_output_fields` 밖 필드에 warning을 남긴 뒤 **그대로 `row[key] = value`를 실행**함을 코드로 재확인했다. LLM의 권한 경계 위반이 경고와 함께 최종 파일에 반영되는 실재 결함이다. 7/10에서는 발화하지 않았지만(전량 KEEP_AS_IS), 잔여 예외가 실재할 사건에서 봉인이 뚫리는 구멍이다. +- **비엄밀 coverage(§9.1)**: `expected ⊆ decided ∪ blocked`만 검사 — 미지 ID·중복 decision·양쪽 동시 수록·예외 0건에서의 임의 decision을 차단하지 못함을 재확인. Part 2/3 개정에서 확립한 coverage 등식이 Part 4에는 없다. +- **blocked 상태의 READY 위장(§9.3)**: FL3 stdout status가 blocked review 유무와 무관하게 `READY` 고정임을 재확인. 최종 파일이 상태 없는 배열이므로 이 stdout이 유일한 상태 신호인데 변별력이 없다. +- **upstream review 미승계(§8)**: Part 3의 review 상태를 FL1이 코드·플래그로, FL3가 report로 승계하지 않는다. Part 3 Claude_v2 개정으로 review가 실질 항목만 남게 된 지금, "실질 review의 결정론 승계"는 LLM 0회로 품질을 올리는 정확한 처방이다. +- **weak_derived_fact의 언어 편향(§6.2)**: 실제 2건("물품대금 지급 최고" 10자, "목적물 특정" 6자 — 재확인)은 법률상 유의미한 사실 표제다. 문자열 길이는 한국어 법률 표제의 완결성을 대변하지 못한다. 결정적 재작성 fallback + review 처방 채택. +- 회귀 fixture 12종 설계(§16.3) — 검증 계획에 통합 채택. + +### 1.2 실행구조·병목 비교 + +구조 인식(직렬 4-task, FL2 상시 실행이 비용·런타임 지배, 중간 파일 1,020KB)은 완전히 일치한다. Codex의 병목 한 줄 요약 — "모델 자체보다, 정상 다중값을 대량 예외로 오분류하고 예외 0건에도 FL2를 반드시 실행하는 직렬 구조" — 은 Claude 실측과 정확히 합치하며 본 보고서의 공식 병목 정의로 채택한다. I/O 계량은 Codex(§5.3: localdocs 19회·MCP 세션 3개)가 더 세밀하고, 페이로드 계량은 양측 일치(FL2 프롬프트 ≈170KB = prefix 3.8K + overlay 9.7K + 주입 147~156KB)한다. + +### 1.3 삭제/축소 안전성 비교 (downstream 근거) + +`Fact_Ledger_base.json`의 27필드 닫힌 스키마와 행 순서·`F-###` 규칙은 Stage 2 핵심 handoff 계약으로 양측 모두 불변 판정이다. 양측 판정을 병합한 안전성 표: + +| 대상 | 판정 | 근거 (양측 합산) | +|---|---|---| +| 최종 파일 스키마·writer report | 변경 금지 (status 규칙 정밀화는 additive) | Stage 2 소비 계약 (양측) | +| possession/notice/lien 게이트 3종 | 유지 (경량화 금지) | 법률상 sealed 게이트 (양측) | +| cardinality 기반 role/claim 예외 | 삭제 확정 | false positive 실증 + 최종 스키마가 원래 복수값 보존형 (양측) | +| 길이 기반 weak_derived_fact 예외 | 삭제 확정 → 결정적 재작성 fallback + review | 한국어 표제 실례 2건 (Codex) | +| pack의 중복·사장 필드 (signal global, event 2계열, client_goal, index 4종, bo 이중 수록) | 삭제 확정 | 소비 0 추적 (양측 일치) | +| `evidence_all.json` read | **제거 확정** | Claude 포함/제외 실험 기여 0 (30/30 동일) — Codex의 optional화 제안은 실측으로 상회. 부재·제거 사실은 manifest에 기록(Codex 재현성 지적 수용) | +| FL2 단일 필수 task | 조건부로 대체 확정 | 무효과 실증 (양측) | +| FL0·FL1·FL3 골격 | 삭제 금지, 병합·강화만 | provenance join·결정론 게이트·단독 writer (양측) | +| 중간 파일 파일명 | 유지 (내부 payload만 축소) | Stage 2·감사 도구 소비 미확인 (Codex §10 신중론 수용) | + +### 1.4 권장 개정 전략 비교와 판정 + +| 쟁점 | Claude | Codex | 판정 | +|---|---|---|---| +| task 골격 | FL0+FL1 병합 → 3-task | 병합은 P2 보류, 4-task 유지 | **병합 채택.** Part 2(3,000행)·Part 3(LES0+LES1)에서 동일 병합을 골든 회귀로 2회 실증했고, 602KB pack의 3중 물질화는 병합 시에만 통째로 소멸한다. Codex의 회귀 위험 우려는 확립된 방법론(함수 단위 이식 + 바이트 재현 확인 + 골든 회귀)으로 통제 | +| LLM 형태 | 단일 조건부 (R1/L1 골격) | wildcard fan-out (shard 4건/25KB, flash-lite/medium) | **단일 조건부 채택.** 양측 모두 정상 경로 0회를 기대하는 상황에서 fan-out은 과잉 장치(Part 3 비교 N-1과 동일 판정). Codex의 shard 상한은 pack 구성 규칙(항목 ≤2KB·유형별 최소 context)으로 흡수 | +| 모델 사양 | flash-lite/**low** | flash-lite/**medium** | **low 채택.** BLOCK_REVIEW 안전판이 있어 판정 불충분은 인간 검토로 안전하게 낙하하며, GB·R1·L1과 문법 통일(Part 3 비교 N-4와 동일 판정) | +| 프리픽스 | 현행 유지 (= R1·L1과 바이트 동일) | 3,385B로 교체 | **현행 유지 확정.** 재실측: FL2의 3,829B(58f2b72d)는 Part 2 Claude v3 R1·Part 3 Claude v2 L1과 **바이트 동일**. Codex의 교체 주장은 Part 3 비교(§3 정정)에서 판정한 것과 동일하게 자사 v3 라인(3,385B) 기준의 오인이다. Claude 배포 라인에서 3,385B로 바꾸면 오히려 직전 Part의 같은 모델 태스크와 어긋나 캐시 실익이 사라진다. Part 1 GB 정렬은 D-2로 별도 | +| FL3 게이트 | (미포착) | exact coverage 등식·patch hard reject·status 규칙 | **Codex 설계 전량 채택** (코드 재검증 완료 — 실재 결함 3건) | +| upstream review 승계 | (미포착) | LES review의 결정론 승계 | **채택** — Part 3 Claude_v2의 실질 review·audit 체계와 접속 | +| 품질 교정 | excerpt 키 교정 (빈 발췌 30/30) | (미포착) | **채택** — 잔여 예외의 판정 근거 실질화 전제 조건 | + +## 2. 선별된 개정 전략 (최소 노력 / 최대 효과) + +### 2.1 개정 골격 — 4 task → 3 task + +```text +IN → F0_fact_ledger_reducer (Python; FL0+FL1 병합. pack 비물질화, 입력 9→6 + | (evidence_all·event_candidates·client_goal 제거), + | excerpt 키 교정, 예외 3분법(pass/deterministic review/ + ↓ true exception), LES review 결정론 승계, + F1_exception_adjudicator slim candidate bundle + exception pack 파일 write) + | (LLM 조건부; flash-lite/low/low/1, preflight=pack. + ↓ has_exceptions=false → 즉시 종료. 실측 기대 0회) + F2_final_gate_and_writer (Python; exact coverage 등식, patch hard reject, + | status 3단 규칙, 게이트 3종 유지, 단독 writer) + ↓ + OUT +``` + +### 2.2 채택 작업 목록 + +| ID | 작업 | 출처 | 핵심 근거 | +|---|---|---|---| +| X-1 | **cardinality 예외 폐지**: role/module 복수 = 정상 다중 라우팅(최종 스키마 자체가 배열 보존형). 명시적 상호배타 conflict가 있을 때만 true exception | 양측 | 40건 false positive + KEEP_AS_IS 무손실 실증 | +| X-2 | **weak_derived_fact 교정**: 길이 트리거 삭제. 빈 값은 BO action/object/date로 결정적 재작성, 실패 시 review code | Codex §6.2 | 한국어 표제 실례 2건 | +| X-3 | **true exception 4중 조건 한정** (Part 2 X-3 이식): 단일 값 선택 필수 + source-backed 상충 근거 2+ + 보존·defer 불가 + compact 판정 충분. 항목당 ≤2KB·유형별 최소 context(전 유형 공통의 비대 envelope 폐지). legal_theory 계열은 LLM 금지·Stage 2 회부 | Claude + Codex §13 | 실측 기준 pack 0건 기대 | +| X-4 | **F0 통합 + pack 비물질화 + 입력 다이어트**: FL0+FL1 병합, fact_source_pack 파일 폐지(602KB 소멸), 입력 9→6 (`evidence_all`·`evidence_event_candidates`·`client_goal` 제거 — 기여 0/소비 0 실측; 제거 사실 manifest 기록), slim candidate bundle에는 후보 50행 + F2 검증용 ID 집합만(예외 pack 내장 제거 — 단독 파일화) | Claude R-1 + Codex §12 | 중복·사장 ~430KB 소거 | +| X-5 | **F1 조건부 전환**: flash-lite/low/low/max_iterations 1, `preflight_files: [exception pack]`, 무예외 시 no-exception 판정 파일 write 후 즉시 종료. SPLIT_FACT enum 삭제(실제 autosplit 미수행 — Codex §14.3), 프리픽스는 현행 블록 바이트 유지 | 양측 + §1.4 판정 | $0.58 → 0, LLM 구간 소거 | +| X-6 | **F2 게이트 강화(전량 Codex 설계)**: ① `expected == decided ∪ blocked` + `decided ∩ blocked = ∅` + 중복·extra ID 0 + count 정합 ② patch는 `allowed_output_fields ∩ FINAL_FIELD_SET` 밖이면 **적용 거부 + BLOCK**(warning 후 적용 금지) ③ status 3단 규칙(무결 READY / 검토 존재 READY_WITH_REVIEW / 위반 FAILED + overwrite 금지) | Codex §9·§15 | 실재 결함 3건 코드 확인 | +| X-7 | **LES review 결정론 승계**: Part 3 v2의 실질 review·blocked·audit 항목을 관련 Fact의 review code + `must_consider` + writer report로 승계, blocked 존재 시 READY 광고 금지 | Codex §8 | LLM 0회로 품질 상승 | +| X-8 | **증거 발췌 실질화**: excerpt 수집 키를 `key_facts`/`key_dates`/`key_amounts`로 교정 — credibility 판정과 잔여 예외 판정 근거의 회복 | Claude P4-9 | 30/30 빈 발췌 실측 | +| X-9 | **SKILL 정합 일괄**: 실패 시 FAILED 1줄 + raise(`sys.exit(0)` 폐지), envelope 계층 raw_decode, `datetime.now` 제거(2-run 바이트 결정론 확보), F2의 F1 출력 파싱에 §6.7 관용 파서 | Claude P4-11 | Part 1~3 확립 기준 | +| X-10 | **변별 신호 보강(additive 한정)**: must_consider/legal_centrality 필드·의미는 유지하되 변별 정보는 `skeleton_validation_codes`로 보강 | Claude P4-10 | Stage 2 계약 보호 | + +### 2.3 기각·유보 항목 + +| ID | 항목 | 판정 근거 | +|---|---|---| +| N-1 | 프리픽스 3,385B 교체 (Codex §7.4) | 재실측 기각: FL2는 이미 Claude 배포 라인(R1·L1)과 바이트 동일. 교체 시 캐시 실익 상실. Stage 1 프리픽스 완전 통일은 D-2(Part 1 GB 1건 정렬)가 정도(正道) | +| N-2 | wildcard fan-out (Codex §11·14) | 기대 0회에 과잉 장치. shard 한도는 X-3에 흡수. 재발 시 확장(D-1) | +| N-3 | FL0+FL1 병합 보류 (Codex P2) | 2회 실증된 방법론 + 602KB 비물질화 효과가 병합에 결부 → 1차 포함 | +| N-4 | reasoning medium (Codex §14.2) | BLOCK_REVIEW 안전판 전제 low로 통일 (GB·R1·L1 문법) | +| N-5 | evidence_all optional 유지 (Codex §4.2) | 기여 0 실측으로 제거 확정. 재현성 문제의식은 manifest 기록으로 수용 | + +### 2.4 후속·범위 밖 + +- **D-1**: 사건 다양화 시 true exception 발생률 모니터링 — 상시 다건화되면 wildcard 확장(Codex 설계 재사용). +- **D-2**: Part 1 GB 프리픽스의 3,829B 정렬(1줄) — flash-lite 4형제(GB·R1·L1·F1) 캐시 통일의 마지막 퍼즐. +- **D-3**: 중간 파일의 debug-mode 조건부 생성(Codex P2-3) — Stage 2·감사 도구 소비 확인 후. + +### 2.5 검증 기준 (개정 작업 명세서 DoD 골자) + +정적: task 3·DAG·ast·SKILL(hash/raw_decode/단일 stdout/raise/raw string)·F1 프리픽스가 R1·L1과 바이트 동일·writer 단일성·폐기 참조 0. 동적(7/10 골든): ① F0 has_exceptions=false + 예외 42건의 해소 내역 audit 추적 ② 최종 `Fact_Ledger_base.json` 50행이 현행과 의미 동등(KEEP_AS_IS 실증으로 사실상 동일 기대 — 타임스탬프 제거 반영) ③ writer report 게이트 결과 동일 + LES review 승계 항목 추가 확인 ④ 2-run 바이트 결정론 ⑤ 주입 테스트(Codex §16.3 fixture 12종 통합: true conflict → pack·F1 발동 / 임의 extra decision → F2 실패 / out-of-scope patch → 거부·차단 / blocked → READY_WITH_REVIEW / BO·ref conservation / notice·possession 게이트 유지) ⑥ Stage 2 소비 필드 무변경. 성능(실 재실행 3회 median): 중간 파일 1,020KB→150KB, stdout 157KB→1KB, LLM 조건부 0회, 비용 $0.58→≈0, 런타임 3:06→90초 이하(no-exception p95, Codex §16.2 채택). + +## 3. 정정 사항 + +**프리픽스 상충의 재확인**: Codex 보고서는 Part 3 비교 때와 동일하게 "Part 1~3 v3가 3,385B이므로 Part 4도 교체"를 주장하나, 이는 Codex 자기 배포 라인 기준이다. Claude 배포 라인 실측(본 세션): Part 2 Claude v3 R1 = Part 3 Claude v2 L1 = Part 4 v2 FL2 = **3,829B(58f2b72d) 바이트 동일** — 즉 Claude 라인에서는 Part 4가 이미 정합 상태이고, 유일한 이탈은 Part 1 v3(3,385B)이다. 따라서 Part 4 개정에서 프리픽스는 손대지 않고, D-2로 Part 1을 정렬하는 것이 Stage 1 통일의 올바른 경로다. 한편 Claude 보고서도 Codex가 잡은 FL3의 실질 결함 3건(patch 적용·coverage·status)을 놓쳤음을 기록한다 — §2.2 X-6으로 전량 수용했다. + +## 4. 결론 + +채택 전략은 "**양측 공통의 원인 소거(cardinality 예외 폐지) + Claude의 구조 소거(F0 통합·pack 비물질화·근거 실질화) + Codex의 봉인 강화(exact coverage·patch hard reject·status 규칙·review 승계)**"다. FL2 무효과의 이중 실증 덕분에 이 개정의 무손실성은 시리즈 중 가장 강하게 보증되어 있으며, 7/10 기준 LLM 0회·비용 $0로 현행 최종 파일을 그대로 재현하면서 FL3의 봉인 구멍 3개를 막아 품질은 오히려 올라간다. Part 4 개정이 완료되면 Stage 1 전체가 "결정론 reducer → 조건부 flash-lite 판정 → 결정론 writer" 단일 문법으로 통일된다. 다음 단계는 본 선별 결과를 LLM이 100% 수행 가능한 개정 작업 명세서(`Part_4_Improvement_Strategy_Claude_v1.md`)로 구체화하는 것이다. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_4_Improvement_Codex_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_4_Improvement_Codex_v1.md new file mode 100644 index 00000000..8b4e391e --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_4_Improvement_Codex_v1.md @@ -0,0 +1,563 @@ +# Stage 1 Part 4 최적 개정 방안 + +## 1. 문서 목적 + +이 문서는 다음 두 분석서를 비교·검증하여 `Stage_1_Part_4_v2.yml`의 최적 개정 방향을 확정한다. + +- `Part_4_Inefficiency_Report_Claude_v1.md` +- `Part_4_Inefficiency_Report_Codex_v1.md` + +선별 기준은 다음과 같다. + +1. 사실·증거·법률효과 후보와 Stage 2 handoff 품질을 훼손하지 않는다. +2. 100% 결정 가능한 작업은 Python으로 처리한다. +3. LLM은 명시적 근거가 충돌하여 하나의 Stage 1 투영을 선택해야 하는 경우에만 실행한다. +4. 가장 적은 YAML·Python 변경으로 비용과 런타임의 가장 큰 병목부터 제거한다. +5. 중간 파일 삭제나 최종 스키마 변경은 downstream 소비 확인 없이 시행하지 않는다. + +현재 연계 기준은 다음 Codex 개정본으로 고정한다. + +- `Stage_1_Part_1_Codex_v3.yml` +- `Stage_1_Part_2_Codex_v3.yml` +- `Stage_1_Part_3_Codex_v2.yml` +- 개정 대상: `Stage_1_Part_4_v2.yml` + +Claude 보고서가 참조한 Claude branch의 prefix·모델 설정은 비교 근거로만 사용하고, 실제 Part 4 개정은 위 Codex branch의 계약과 일치시킨다. + +## 2. 통합 결론 + +두 보고서의 핵심 결론은 일치한다. + +> Part 4의 주 병목은 FL2 모델보다 FL1이 정상적인 다중 라우팅을 대량의 예외로 오분류하고, FL2를 고사양 단일 LLM task로 무조건 실행하는 구조다. + +7월 10일 fixture에서 확인된 핵심 수치는 다음과 같다. + +| 항목 | 실측값 | 판정 | +|---|---:|---| +| Fact 후보 | 50행 | 정상 | +| 예외 | 42건 | 과잉 | +| 예외가 연결된 Fact | 40행, 80% | exception path가 main path로 변질 | +| `ambiguous_legal_role` | 40건 | 정상 복수값의 false positive | +| `weak_derived_fact` | 2건 | 문자열 길이 heuristic 오류 | +| exception pack | 146,998B, minified | LLM 입력 과대 | +| FL1 stdout | 약 156,444B | pack 재출력 비효율 | +| FL2 | Gemini Pro/high, 필수 1회 | 과대 사양·무조건 실행 | +| 후보 대비 최종 변경 | 행 0, 필드 0 | LLM 한계효용 0 | +| `fact_source_pack.json` | 602,059B | 중복·사장 데이터 과다 | +| candidate bundle | 418,076B | 예외 내장으로 비대 | +| 최종 `Fact_Ledger_base.json` | 128,855B | 보존해야 할 handoff | + +따라서 1차 개정의 중심은 **FL2 정상 경로 0회화**다. FL0+FL1 통합은 장기적으로 유효하지만, LLM 병목 제거보다 수정 범위가 크므로 1차 필수 개정에서 제외하고 재측정 후 조건부로 시행한다. + +## 3. 두 보고서의 공통 판정 + +### 3.1 공통 비효율 판정 + +두 보고서는 다음 항목에 사실상 합의한다. + +1. `ambiguous_legal_role`은 다중 role/module 개수만으로 생성되는 false exception이다. +2. `weak_derived_fact`의 글자 수 기준은 한국어 법률 사실표제에 부적절하다. +3. FL2가 42건을 판정했지만 최종 후보를 변경하지 않았다. +4. FL2는 예외 0건에도 실행되는 완전 직렬 병목이다. +5. `gemini-3.1-pro-preview/high`는 제한된 projection adjudication에 과대 사양이다. +6. 동일 exception pack이 candidate 파일, stdout, LLM prompt에 중복 물질화된다. +7. FL0 source pack은 전역 map과 BO별 payload를 중복 저장한다. +8. `client_goal_compact`, event map, 일부 structure index는 source pack 이후 소비되지 않는다. +9. FL1의 possession/notice/lien deterministic gate와 FL3 sole-writer 구조는 유지해야 한다. +10. `Fact_Ledger_base.json`의 27개 필드, 행 순서, Fact ID 규칙은 Stage 2 계약이므로 보존해야 한다. +11. LLM은 조건부 경량 task로 남겨 진짜 projection conflict만 처리해야 한다. + +### 3.2 공통 권고 중 즉시 채택할 내용 + +- cardinality-only exception 폐지 +- 문자열 길이 기반 weak-fact exception 폐지 +- FL2를 조건부 wildcard adjudicator로 변경 +- 정상 사건에서 wildcard 인스턴스 0개 +- exception pack을 파일 또는 shard로 저장하고 stdout에는 전체 pack을 출력하지 않음 +- FL2 입력을 유형별 최소 context로 축소 +- 최종 writer의 닫힌 스키마·참조 검증 유지 +- 7월 10일 fixture에 대한 골든 회귀 +- Stage 2 공개 handoff 파일명과 최종 schema 불변 + +## 4. 차이점 비교와 채택 판정 + +### 4.1 비교표 + +| 쟁점 | Claude 보고서 | Codex 보고서 | 통합 판정 | +|---|---|---|---| +| FL0+FL1 | 즉시 통합, task 4→3 | 우선 유지·축소, 병목 잔존 시 통합 | **2차 조건부 채택** | +| FL2 실행 방식 | 경량 conditional adjudicator | 0-instance wildcard | **Codex 방식 채택** | +| FL2 모델 | flash-lite/low/max 1 | flash-lite/medium/max 2 | **medium/max 2 채택** | +| common prefix | Claude branch prefix 유지 주장 | Codex Part 1~3 prefix와 불일치 지적 | **현재 Codex prefix로 통일** | +| evidence excerpt | 30/30 비어 있음, key 보강 | 주로 pack·예외 구조 분석 | **Claude 발견 채택** | +| priority signal | `must_consider`·critical 50/50 포화 | upstream review 승계 결함 강조 | **둘 다 채택, 단계 분리** | +| upstream LES review | 상세 언급 약함 | deterministic 승계 필수 | **Codex 제안 P0 채택** | +| FL3 coverage | 현행 강점으로 평가 | subset 검사·extra ID 허용 결함 | **Codex exact gate 채택** | +| patch whitelist | 정책 유지 | 허용 밖 field도 warning 후 적용 결함 | **Codex hard reject 채택** | +| blocked status | 안전판 유지 | blocked인데 READY 가능 | **Codex fail-closed 채택** | +| Python SKILL | parser·exit·timestamp 결함 지적 | 주 분석 범위 아님 | **Claude 발견 채택** | +| `evidence_all` | 제거 | archive에 부재, 제거 또는 optional | **필수 의존 제거 채택** | +| 내부 파일 물리 삭제 | source pack 소멸 | downstream 확인 전 보류 | **1차 삭제 보류** | + +### 4.2 FL0+FL1 즉시 통합을 보류하는 이유 + +통합은 최종 목표로 타당하다. source pack 602KB의 write/read와 MCP session 하나를 제거할 수 있기 때문이다. 그러나 즉시 통합하려면 다음을 동시에 바꿔야 한다. + +- 두 Python 코드 블록과 helper namespace 병합 +- source pack schema 또는 파일 폐기 +- candidate bundle에 FL3 validation context 이관 +- FL3 read contract 변경 +- intermediate audit 및 재현 계약 변경 +- downstream 또는 운영 도구의 중간 파일 소비 여부 확인 + +반면 FL1 exception 조건과 DAG만 고치면 7월 10일 fixture의 42개 LLM 판정을 모두 없앨 수 있다. 비용과 런타임의 가장 큰 원인을 훨씬 작은 변경으로 제거할 수 있으므로, 1차 개정에서는 FL0와 FL1의 task 경계를 유지한다. + +다음 조건을 충족하지 못할 때만 2차로 통합한다. + +- no-exception Part 4 p95 runtime 90초 이하 +- source pack 330KB 이하 +- deterministic MCP I/O가 전체 runtime의 40% 미만 + +### 4.3 FL2 모델 수준 판정 + +Claude의 `low/max_iterations 1`은 비용 면에서 더 작지만, 실제로 남길 true exception은 명시적 상충 근거를 비교하는 작업이다. 현재 Codex Part 1~3의 exception adjudicator가 사용하는 `flash-lite/medium/max_iterations 2`와 통일하는 편이 다음 이유에서 낫다. + +- rare exception에 필요한 최소 비교 추론 확보 +- JSON schema 오류 시 한 번의 복구 여지 +- Stage 1 전체 model policy 통일 +- 정상 경로에서는 task가 0개이므로 비용 차이가 사실상 없음 + +따라서 Part 4 FL2는 `gemini-3.1-flash-lite`, `reasoning: medium`, `max_iterations: 2`로 고정한다. Pro escalation은 두지 않는다. 해결 불가능한 항목은 `BLOCK_REVIEW`가 맞다. + +### 4.4 common cache prefix 판정 + +현재 Codex Part 1~3의 LLM task는 다음 prefix를 공유한다. + +- bytes: 3,385 +- SHA-256: `6e01f72f1803767b6bb16b5aef6cf34760e6dc1b293c316c75e990d09ad5fe63` + +Part 4 v2는 3,829B, SHA-256 `58f2b72d6c2650739c45d85cc57899150434db86a97bf19cd5a59d92d11dfbb5`를 사용한다. Claude 보고서의 canonical 판단은 Claude branch 기준이므로 현재 Codex branch에는 적용하지 않는다. + +Part 4 개정본은 Part 1~3의 3,385B prefix를 바이트 단위로 복제해야 한다. + +### 4.5 수치 오류 정정 + +Claude 보고서의 `Fact_Ledger_base.json` 175,126B 표기는 실제로 `BO.json`의 크기다. 7월 10일 최종 Fact Ledger의 실제 크기는 128,855B다. + +또한 `42/50=84%`는 예외 건수를 Fact 수로 나눈 값이다. 예외와 연결된 Fact는 40행이므로 영향 행 비율은 80%다. 2개 Fact가 복수 예외를 가진다. + +이 오류는 Claude 보고서의 주 결론에는 영향을 주지 않지만, 개정 후 정량 비교의 baseline은 위 정정값을 사용해야 한다. + +## 5. 추가로 채택할 품질 개선 + +### 5.1 evidence excerpt 스키마 불일치 + +실제 source pack의 evidence authority 30건은 다음 상태였다. + +- excerpt가 비어 있는 record: 30/30 +- title 존재: 30/30 +- priority 존재: 0/30 +- authentication 존재: 0/30 + +FL0는 `excerpt`, `summary`, `content`, `text`, `body`만 찾지만 `evidence_indexed.json`은 주로 `key_facts`, `key_dates`, `key_amounts`를 제공한다. 그 결과 evidence score와 rare FL2 exception에 핵심 사실이 전달되지 않는다. + +개정 시 다음 compact field를 결정적으로 생성한다. + +```json +{ + "evidence_index": "E-001", + "title": "string", + "key_facts": [], + "key_dates": [], + "key_amounts": [], + "doc_type": "string", + "source_pointer": {}, + "authentication": null +} +``` + +긴 raw text는 source pack에 넣지 않는다. exception별로 필요한 `key_facts`만 상한을 두고 전달한다. + +### 5.2 priority signal 포화 + +최종 50행은 전부 다음 값을 가진다. + +- `must_consider: true` +- `legal_centrality: critical` + +`must_consider`는 non-drop 안전장치일 수 있으므로 1차 개정에서 false로 낮추지 않는다. 대신 다음처럼 처리한다. + +1. upstream review·blocking 여부를 별도 code와 writer report에 명시한다. +2. `legal_centrality` 산정 근거를 결정적 code로 기록한다. +3. Stage 2 소비 검증 후 enum 값의 변별 기준을 조정한다. + +즉, 포화 문제는 인정하지만 보존 안전성보다 앞세워 즉시 값을 낮추지는 않는다. + +### 5.3 Part 3 review 승계 + +Part 4는 Part 3의 다음 정보를 LLM 없이 승계해야 한다. + +- root `READY_WITH_REVIEW` +- `structure.review_required` +- `quality_gate.review_queue[]` +- `source_bo_ids` +- `review_code` +- `downstream_owner` +- blocked exception 정보 + +관련 Fact에는 최소 다음을 적용한다. + +- `LES_REVIEW_REQUIRED` 또는 구체적 review code +- `must_consider: true` +- writer report의 affected Fact/BO 목록 +- blocking review가 있으면 final writer status `READY_WITH_REVIEW` +- 필요한 경우 `BLOCK_FINAL_DRAFTING` + +Part 4 FL2는 LES 법리나 structure type을 재판정하지 않는다. + +### 5.4 Python SKILL 및 결정성 + +Claude 보고서의 다음 지적은 채택한다. + +1. FL0·FL1·FL3의 실패 경로에서 `sys.exit(0)`을 제거한다. +2. 실패는 non-zero exit 또는 exception으로 agent에 전달한다. +3. `read_docs` outer/inner JSON 모두 `json.loads` 후 `raw_decode` fallback을 사용한다. +4. LLM output parser는 raw JSON, text envelope, code fence를 처리한다. +5. intermediate semantic artifact에서 불필요한 `created_at_utc`를 제거한다. +6. 시간정보가 필요한 operational report는 semantic equality 검사에서 분리한다. +7. `run_fingerprint`로 동일 입력 여부를 검증한다. +8. `{{__user_hash__}}`, `{{__workspace_hash__}}` 전달을 유지한다. + +## 6. 최종 선별 전략 + +### 6.1 1차 필수 개정 P0 + +가장 작은 변경으로 가장 큰 효과를 내는 작업이다. + +1. FL1의 `ambiguous_legal_role` cardinality trigger를 삭제한다. +2. `ambiguous_claim_chain`도 배열 cardinality만으로 예외화하지 않는다. +3. `weak_derived_fact`의 문자 수 trigger를 삭제한다. +4. 빈 `derived_fact`는 deterministic template로 보충한다. +5. FL2를 `Task_FL2_fact_exception_adjudicator_*` wildcard로 변경한다. +6. FL1이 true exception 0건이면 `dynamic_fanout: []`를 출력한다. +7. FL3가 FL1과 `all Task_FL2_*`를 기다리도록 DAG를 바꾼다. +8. FL2 모델을 flash-lite/medium/max 2로 변경한다. +9. current Codex common prefix를 바이트 동일하게 적용한다. +10. FL3 exact coverage, patch whitelist, blocked-status gate를 강화한다. +11. Part 3 review queue를 Fact와 writer report에 deterministic하게 승계한다. +12. failure exit와 JSON parser를 SKILL에 맞게 수정한다. + +### 6.2 같은 릴리스의 P1 + +P0 이후에도 변경 위험이 낮고 효과가 큰 작업이다. + +1. exception pack을 candidate bundle과 분리한다. +2. stdout은 전체 pack 대신 `dynamic_fanout` manifest만 출력한다. +3. exception을 유형별 shard로 저장한다. +4. shard당 최대 4건, dynamic payload 25KB 이하를 강제한다. +5. `evidence_all.json` 필수 의존을 제거한다. +6. `client_goal.json`과 `evidence_event_candidates.json`을 Part 4 입력에서 제거한다. +7. 단, 이 파일들 자체는 Stage 1 handoff에서 삭제하지 않는다. +8. evidence compact를 실제 `key_facts/key_dates/key_amounts`에 맞춘다. +9. source pack의 전역 map/BO별 사본 중 하나만 유지한다. +10. FL0에서 다섯 structure index를 검증하되 source pack에는 `by_bo_id`와 필요한 ID set만 기록한다. + +### 6.3 재측정 후 조건부 P2 + +다음은 1차 성능검증 후에만 시행한다. + +1. FL0와 FL1을 단일 deterministic compiler로 통합한다. +2. `fact_source_pack.json` 생성을 debug mode로 제한한다. +3. candidate bundle에 FL3용 최소 validation context를 내장한다. +4. FL3의 대형 source pack 재읽기를 제거한다. +5. `legal_centrality` 값의 변별 기준을 변경한다. +6. internal projection 파일의 물리적 삭제 여부를 Stage 2 consumer audit 후 결정한다. + +## 7. 개정 DAG + +### 7.1 1차 권고 DAG + +```text +IN + | + v +FL0 slim source compiler [Python] + | + v +FL1 candidate + exception planner [Python] + | \ + | \ true exception shard 1..N + | v + | FL2 exception adjudicator_* [LLM, 0..N] + | | + +----+ + v +FL3 exact final gate + sole writer [Python] + | + v +OUT +``` + +정상 경로는 다음과 같다. + +```text +FL0 -> FL1(dynamic_fanout=[]) -> FL3 +``` + +진짜 예외가 있을 때만 다음 경로가 열린다. + +```text +FL0 -> FL1 -> FL2_0 ... FL2_N -> FL3 +``` + +### 7.2 task procedure 계약 + +```text +FL0.nexts = [FL1] +FL1.nexts = [FL2_*, FL3] +FL2_*.nexts = [FL3] +FL3.wait_until = [FL1, all FL2_*] +``` + +FL1 stdout root는 `dynamic_fanout`을 포함해야 한다. 각 item은 full exception이 아니라 shard path, shard ID, expected exception IDs, run fingerprint만 포함한다. + +## 8. 작업별 개정 계약 + +### 8.1 FL0 + +**유지할 역할** + +- upstream schema/status 검증 +- BO·structure·evidence reference 검증 +- `structure_index.by_bo_id` 검증 +- compact source map 작성 +- Part 3 review-by-BO 작성 + +**삭제·축소할 내용** + +- `client_goal_compact` +- 사용되지 않는 event map 두 벌 +- BO·signal·evidence의 global/per-BO 이중 저장 +- 검증 후 소비되지 않는 네 structure index의 재직렬화 +- `evidence_all.json` 필수 read + +**주의사항** + +`evidence_event_candidates.json`을 Part 4가 읽지 않더라도 Part 1 결과물과 Stage 1 handoff에서는 유지한다. Part 4의 입력 축소와 전역 산출물 삭제는 다른 문제다. + +### 8.2 FL1 + +FL1은 문제를 세 경로로 나눈다. + +| 경로 | 조건 | 처리 | +|---|---|---| +| deterministic pass | 정상 복수값, unique projection | 그대로 보존 | +| deterministic review | source 누락, legal theory 필요, upstream review | code·gate로 전달 | +| true LLM exception | 동일 scalar projection에 source-backed 후보 2개 이상이 실제 충돌 | FL2 shard | + +true exception은 다음 네 조건을 모두 만족해야 한다. + +1. 하나의 Stage 1 output field를 선택해야 한다. +2. 명시적 source-backed 후보가 둘 이상이다. +3. 배열 보존이나 downstream defer로 해결할 수 없다. +4. compact context만으로 안전하게 비교할 수 있다. + +하나라도 불충족하면 LLM에 보내지 않는다. + +### 8.3 FL2 + +**설정** + +- provider: `google` +- model: `gemini-3.1-flash-lite` +- reasoning: `medium` +- max_iterations: `2` +- tools: none +- common prefix: current Codex canonical prefix +- prompt input: 해당 shard만 + +**허용 decision** + +- `KEEP_AS_IS` +- `PATCH` +- `BLOCK_REVIEW` + +현재 FL3가 autosplit하지 않으므로 `SPLIT_FACT`는 제거한다. + +**금지 사항** + +- Part 3 structure/type 재판정 +- raw evidence 탐색 +- 새 Fact·BO·evidence·structure ID 생성 +- legal theory 확정 +- allowed field 밖 patch + +### 8.4 FL3 + +다음 hard gate를 모두 구현한다. + +1. `expected_exception_ids == decision_ids ∪ blocked_ids` +2. `decision_ids ∩ blocked_ids == ∅` +3. duplicate·extra·missing exception ID 0건 +4. decision의 fact ID, BO ID, exception type이 원본과 일치 +5. `patch.keys ⊆ allowed_output_fields ∩ FINAL_FIELD_SET` +6. 허용 밖 patch는 warning 후 적용하지 않고 hard reject +7. expected BO ID set과 final source BO ID set 보존 +8. Fact ID uniqueness와 deterministic ordering +9. evidence/structure ref가 authoritative set의 부분집합 +10. FL2 `FAILED`면 final overwrite 금지 +11. unresolved blocked review가 있으면 `READY` 금지 +12. Part 3 blocked review를 writer report와 drafting gate에 승계 + +status는 다음과 같이 고정한다. + +| 조건 | status | final write | +|---|---|---| +| 모든 gate 통과, block 없음 | `READY` | 허용 | +| sealed ledger는 가능하나 review/block 존재 | `READY_WITH_REVIEW` | 허용 + drafting gate | +| schema·coverage·conservation 위반 | `FAILED` | overwrite 금지 | + +## 9. 삭제·축소 안전성 + +### 9.1 즉시 안전 + +- false exception trigger 삭제 +- FL2의 정상 경로 0회화 +- exception pack stdout 제거 +- source pack 내부 중복 필드 제거 +- Part 4의 `client_goal` read 제거 +- Part 4의 event-candidate read 제거 +- `evidence_all` 필수 의존 제거 +- Pro 모델 제거 +- `SPLIT_FACT` enum 제거 + +이 작업은 최종 Stage 2 handoff 파일을 삭제하지 않고 Part 4 내부 read·intermediate 계약만 축소한다. + +### 9.2 반드시 유지 + +- `Fact_Ledger_base.json` +- FINAL_FIELDS 27종과 닫힌 schema +- `source_bo_id`, evidence ref, linked structure ref +- Fact ordering과 `F-###` 규칙 +- possession/legal-cause conflict gate +- notice receipt drafting gate +- lien extinction notice review gate +- Part 3 `structure_index.by_bo_id` 검증 +- final sole-writer 원칙 + +### 9.3 downstream 확인 전 보류 + +- `fact_source_pack.json` 파일 자체 삭제 +- `Fact_Ledger_base_candidate.json` 파일 자체 삭제 +- writer report 삭제 또는 이름 변경 +- `must_consider` true를 false로 낮추는 의미 변경 +- `legal_centrality` enum 값 재분류 +- Stage 1의 `client_goal.json`, `evidence_event_candidates.json` 산출 중단 +- 최종 Fact Ledger 필드 추가·삭제 + +두 보고서는 최종 Fact Ledger의 Stage 2 소비를 확인했지만, 모든 Stage 2·감사 도구의 internal projection 파일 소비 지도를 완결하지는 않았다. 따라서 물리 파일 삭제는 별도 consumer audit 후 결정한다. + +## 10. 구현 순서 + +### 단계 A: 비용 병목 제거 + +1. FL1 exception trigger 수정 +2. FL2 wildcard DAG 적용 +3. flash-lite/medium과 common prefix 적용 +4. no-exception direct path 구현 + +### 단계 B: 안전성 강화 + +1. FL3 exact coverage +2. patch hard whitelist +3. blocked/failed status 규칙 +4. upstream LES review 승계 +5. BO/evidence/structure conservation gate + +### 단계 C: 입력·토큰 축소 + +1. exception pack 별도 파일·shard +2. stdout manifest화 +3. evidence compact key 수정 +4. FL0 dead field·중복 제거 +5. 불필요한 Part 4 input read 제거 + +### 단계 D: Python 계약 정비 + +1. non-zero failure +2. outer/inner raw-decode fallback +3. code-fence tolerant result parser +4. semantic timestamp 제거와 run fingerprint +5. SKILL checklist 검증 + +### 단계 E: 재측정 + +P0·P1 적용 후 runtime profile을 수집한다. no-exception p95가 90초를 넘을 때만 FL0+FL1 통합을 시행한다. + +## 11. 검증 계획 + +### 11.1 정적 검증 + +- YAML parse 성공 +- 모든 task_procedure 노드가 실제 task 또는 wildcard와 일치 +- wildcard parent stdout에 `dynamic_fanout` 존재 +- Python code compile 성공 +- 모든 code-executor가 user/workspace hash 전달 +- 모든 Python read parser가 raw-decode fallback 보유 +- 모든 LLM task의 common prefix hash 동일 +- FL2 모델·reasoning·iterations 일치 +- 최종 writer 1개만 `Fact_Ledger_base.json` 작성 + +### 11.2 7월 10일 골든 회귀 + +기대 결과는 다음과 같다. + +- candidate Fact 50행 보존 +- `ambiguous_legal_role` false exception 40건 제거 +- `weak_derived_fact` false exception 2건 제거 +- true FL2 shard 0개 +- LLM 호출 0회 +- 최종 Fact 50행 의미 동등 +- BO ID·evidence ref·structure ref 보존 +- existing notice weak review 2건 보존 +- Part 3 review 승계는 기존보다 강화 + +### 11.3 주입 fixture + +1. multi-role non-conflict: 배열 보존, FL2 미실행 +2. true scalar projection conflict: shard 생성, FL2 실행 +3. empty derived fact: deterministic fallback 또는 review +4. missing calculation basis: hallucination 없이 review +5. upstream blocked LES review: `READY_WITH_REVIEW` +6. extra decision ID: FL3 failure +7. duplicate decision ID: FL3 failure +8. out-of-scope patch: 미적용 + failure/block +9. missing BO: conservation failure +10. unknown evidence/structure ref: failure +11. code-fenced LLM JSON: parser 성공 +12. malformed LLM JSON: retry 후 fail-closed +13. notice receipt missing: drafting block +14. possession/legal-cause conflict: hard warning + +### 11.4 정량 합격 기준 + +| 지표 | baseline | 합격 기준 | +|---|---:|---:| +| 7월 10일 false exception | 42 | 0 | +| 정상 경로 FL2 task | 1 | 0 | +| 정상 경로 LLM 비용 | $0.58 포함 | $0 | +| source pack | 602,059B | 330KB 이하 | +| candidate bundle | 418,076B | 200KB 이하 | +| FL1 stdout | 약 156,444B | 1KB 이하 권고 | +| Part 4 common prefix 종류 | current Codex와 불일치 | Stage 1 전체 1종 | +| Part 4 runtime | 3분 6초 | no-exception p95 90초 이하 | +| out-of-scope patch 적용 | 가능 | 0 | +| final Fact count | 50 | 50 | + +## 12. 최종 권고 + +Part 4의 최적 개정은 전면 통합이 아니라 **가짜 예외 제거 → FL2 wildcard 0회 경로 → FL3 fail-closed 강화 → source pack 축소** 순서다. + +두 보고서가 공통으로 증명한 가장 중요한 사실은 고사양 LLM이 42건을 판정하고도 최종 Fact를 바꾸지 않았다는 점이다. 따라서 먼저 이 호출을 없애야 한다. 동시에 Codex 보고서가 확인한 FL3 경계 결함과 Part 3 review 승계 누락을 고쳐야 비용 절감이 품질 저하로 이어지지 않는다. Claude 보고서의 evidence-key mismatch와 Python SKILL 결함은 같은 릴리스에서 함께 교정한다. + +FL0+FL1 통합은 좋은 2차 최적화지만 1차 필수 작업은 아니다. P0·P1만으로도 정상 사건의 Part 4 LLM 비용을 0으로 만들고, 토큰·중간 파일 크기를 크게 줄이며, 현재보다 강한 법률·데이터 안전 게이트를 확보할 수 있다. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_4_Improvement_Strategy_Claude_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_4_Improvement_Strategy_Claude_v1.md new file mode 100644 index 00000000..1ca5da2a --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_4_Improvement_Strategy_Claude_v1.md @@ -0,0 +1,207 @@ +# Stage 1 Part 4 개정 작업 명세서 (Claude v1) + +- 개정 대상: `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_4_v2.yml` (2,027행, task 4개) +- 산출물: `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_4_Claude_v3.yml` (신규 파일 — v2 원본은 수정·이동·삭제 금지) +- 근거 문서: `Part_4_Improvement_Claude_v1.md` (채택 전략 X-1~X-10, 기각 N-1~N-5, 후속 D-1~D-3) +- 작성 관점: 20년 이상 경력의 대한민국 최고 수준 변호사 + 세계적 수준의 AI 아키텍트. 본 명세서는 LLM이 읽고 추가 판단 없이 100% 수행할 수 있도록 작성되었다. 명세에 없는 "개선"의 임의 추가는 금지한다(§12-10). + +--- + +## 0. 개정 목표와 원칙 + +1. **골격 전환**: `FL0→FL1→FL2(상시 pro-preview LLM)→FL3` 4-task 직렬 체인을 `F0(결정론 reducer)→F1(조건부 flash-lite adjudicator)→F2(결정론 gate/writer)` 3-task로 재편한다. Part 1 v3(GA→GB→GC)·Part 2 v3(R0→R1→F0)·Part 3 v2(L0→L1→L2)와 동일 문법이며, 이로써 Stage 1 전체가 단일 문법으로 통일된다. +2. **예외의 상류 소거**: cardinality 기반 가짜 예외(role/module 복수 = 정상 다중 라우팅, X-1)와 문자열 길이 기반 예외(X-2)를 폐지하고, true exception은 4중 조건(X-3)을 통과하는 항목으로 한정한다. 7/10 fixture 기준 LLM 호출 실질 0회가 목표이며, 무손실성은 "42건 전량 KEEP_AS_IS 대조 실험 = 최종 50행 완전 동일"이라는 이중 실증(Claude 대조 실험 + Codex 전후 비교)으로 보증되어 있다. +3. **전송의 구조 소거**: `fact_source_pack.json` 602KB(내부 중복·사장 필드 포함)를 물질화하지 않고, 예외 pack의 3중 이동(candidate 내장 + stdout 재출력 + 프롬프트 주입)을 단독 파일 + preflight 1회로 단일화한다. +4. **봉인 강화(품질 개선)**: F2에 exact coverage 등식, 경계 위반 patch의 hard reject(경고 후 적용 금지), status 3단 규칙을 도입하고, Part 3의 실질 review 상태를 결정론으로 승계한다(X-6·X-7). +5. **근거 실질화**: 증거 발췌 수집 키를 `evidence_indexed.json`의 실제 필드로 교정한다(X-8 — 현행은 30/30 빈 발췌). +6. **불변 계약 준수**: `Fact_Ledger_base.json`(FINAL_FIELDS 27종 닫힌 스키마, `F-###` 연속 규칙, 최상위 배열 형식)과 writer report 파일은 Stage 2 소비 계약으로 변경 금지(additive 확장만 허용). +7. **SKILL.md 100% 준수** + **프리픽스(N-1 판정)**: F1의 `COMMON_CACHE_PREFIX_STAGE_1`은 **현행 FL2 블록을 바이트 그대로 유지**한다. 이 블록(3,829B, sha256 앞 12자리 `58f2b72d6c26`)은 Part 2 Claude v3 R1·Part 3 Claude v2 L1과 바이트 동일함이 실측되어 있다. 들여쓰기 제거·재정렬 금지. (Part 1 GB의 정렬은 D-2 — 본 개정 범위 밖.) + +--- + +## 1. 불변 계약 (변경 금지 목록) + +1. **최종 파일** `Fact_Ledger_base.json`: 최상위 배열, 행당 FINAL_FIELDS 27종(fact_id, source_bo_id, type, date, parties, object_spec, amount, action, evidence_refs, credibility, legal_centrality, proof_strength, legal_effect_roles, claim_chain_ref, linked_structures, must_not_drop_in_claim_types, must_consider, state_context, money_claim_effect, commercial_successor_effect, actio_roles, linked_actio_structures, downstream_module_candidates, skeleton_validation_codes, derived_fact, derivation_basis, legal_calculation_object) 전 필드 존재 + 임의 필드 금지, `F-###` 연속·유일, 행 순서는 source_bo_id 정렬 기반의 현행 결정론 순서. enum(credibility/legal_centrality/proof_strength) 유지. **키 삭제·이름 변경·타입 변경 금지.** 변별 정보 보강은 `skeleton_validation_codes` 값 추가로만(additive, X-10). +2. **writer report** `stage1_tmp/fact_ledger/fact_ledger_writer_report.json`: 파일명·기존 필드 유지, 신규 필드(status, LES 승계 기록)는 additive. +3. **결정론 게이트 3종 보존**: `_possession_conflict_scan`·`_notice_receipt_gate_scan`·`_lien_extinction_notice_scan`과 그 review code 6종(POSSESSION_CAUSE_CONFLICT, LEGAL_CAUSE_STATE_UNKNOWN_REVIEW, NOTICE_RECEIPT_UNVERIFIED, NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW, LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW, BLOCK_FINAL_DRAFTING_NOTICE_RECEIPT_MISSING)은 자구 수준 이식(경량화 금지). +4. **ID 규칙**: BO ID·evidence index·structure/cluster ID·claim/liability group id renumber·창작 금지. `F-###`은 F2가 현행 규칙(정렬 순서 연속 부여)으로 부여. +5. **LLM 판정 계약**: 후보 밖 값 금지, 불충분 시 BLOCK_REVIEW, "LES2 모호성 재판정 금지" 경계(v2 FL2의 PROHIBITED_EXCEPTION_INTERPRETATION 취지) 유지. +6. Part 1/2/3 YAML은 수정하지 않는다. + +--- + +## 2. 현행 구조 매핑 (개정 대상 앵커) + +| 현행 task (v2 시작 행) | 처분 | +|---|---| +| `Task_FL0_fact_source_pack_compiler` (53행) | **F0로 흡수** — 컴파일 함수군 이식(발췌 키 교정), pack 물질화 폐지 | +| `Task_FL1_deterministic_fact_ledger_candidate_builder` (617행) | **F0로 흡수** — row builder·게이트 3종 이식 + X-1/X-2/X-3/X-7 개정 | +| `Task_FL2_single_fact_exception_adjudicator` (1333행) | **F1로 대체** — 조건부·pack 파일 preflight·flash-lite/low. 프리픽스 블록(1347~1389행)은 바이트 유지 | +| `Task_FL3_final_fact_ledger_gate_and_writer` (1563행) | **F2로 개정** — X-6 봉인 강화, 채널 정리 | +| `task_procedure` (21행) | **전면 교체** (§9) | + +**코드 이식 원칙**: 흡수 로직은 함수 단위 이식이다. FL0의 `_bo_items`·`_bo_id`·`_compact_bo`·`_validate_legal_effect_structures`·`_compact_structure`·`_build_evidence_authority`(**발췌 키 교정판** — §5-2)·`_build_signal_hints_by_bo_id`·`_compact_signal`·`_collect_bo_refs`·`_linked_by_bo`, FL1의 `_score_evidence`·`_legal_roles`·`_modules`·`_linked_structures`·`_parties`·`_derived_fact`·`_basis`·`_claim_chain_ref`·`_legal_calculation_object`·`_build_row`·`_possession_conflict_scan`·`_notice_receipt_gate_scan`·`_lien_extinction_notice_scan`·`_validate_candidate_bundle`(개정 스키마 반영), FL3의 `_apply_patch`(X-6 개정)·`_exception_allowed_fields`·`_validate_adjudication_coverage`(X-6 개정)·`_linked_ids_from_source`(입력원 변경)·`_validate_refs`·`_normalize_final_row`·게이트/report 조립부를 각각 F0/F2로 옮기고, 중복 MCP 보일러플레이트는 1벌로 줄인다. 개정 지점은 본 명세서가 명시한 곳으로 한정한다. + +--- + +## 3. STEP 0 — 사전 검증 (v3 작성 전 필수) + +1. **STEP 0-1**: v2에서 §2의 앵커 4개 + `task_procedure`(21행) 존재 확인. 불일치 시 중단·보고. +2. **STEP 0-2**: fixture(`Results_July_10_3_15pm`)에 F0 입력 6종(`BO.json`, `legal_effect_structures.json`, `actio_case_signals.json`, `case_liability_signals.json`, `legal_effect_signals.json`, `evidence_indexed.json`)과 골든 산출물(`Fact_Ledger_base.json`, sha256 앞 12자리 `b589ff166a7d`, 50행) 및 중간 실행물(`fact_source_pack.json` 602,059B / `Fact_Ledger_base_candidate.json` 418,076B / `fact_ledger_writer_report.json` 452B)이 존재함을 확인한다(§10 회귀에 사용). +3. **STEP 0-3**: F1 프리픽스 canonical을 v2의 1347~1389행 블록에서 바이트 추출하고, Part 2 Claude v3 R1·Part 3 Claude v2 L1 블록과 바이트 동일(3,829B, `58f2b72d6c26`)함을 확인한다. 불일치 시 중단·보고. +4. **STEP 0-4**: preflight 파일 채널(Plan A)은 Part 2/3 개정과 공유되는 배포 전제조건이다. 기확인 상태면 재검 불요. **Plan B**: 실패 시 F1은 `preflight: false`로 두고 F0 stdout에 exception pack 요약을 포함시켜 `{{prev.Task_F0_fact_ledger_reducer.json_output}}` 템플릿으로 주입한다(예외 0이면 요약 수십 바이트). + +--- + +## 4. 신규 데이터 계약 (파일 5종) + +| 파일 | writer | 소비자 | 핵심 스키마 | +|---|---|---|---| +| `stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json` | F0 | F2 | `{fact_ledger_candidate_bundle: {schema_version: "stage1_fact_ledger_candidate_bundle.v2", status, candidate_items[](FINAL_FIELDS 27종 완전형), ref_manifest: {bo_ids[], structure_ids[], evidence_ids[]}, les_review_inheritance: {by_fact_id: {…review codes/원천}}, exception_pack_ref: {path, sha256, expected_exception_ids[]}, compile_gate}}` — **v2의 exception_pack 내장·created_at_utc는 수록 금지**. `ref_manifest`는 F2 참조 검증용(602KB pack 재읽기 대체) | +| `stage1_tmp/fact_ledger/fact_exception_pack.json` | F0 | F1 (preflight) | `{schema_version: "stage1_fact_ledger_exception_pack.v2", has_exceptions, exception_count, exceptions[], budget}` — 항목: `{exception_id "FX-###", exception_type, fact_id, source_bo_id, severity, trigger_reason, candidate_fact_row_subset, compact_context(유형별 최소 — §5-6), allowed_output_fields, allowed_decisions, conflict_flags}`. **X-3 4중 조건 통과 항목만, 항목당 ≤2KB** | +| `stage1_tmp/fact_ledger/fact_adjudication.json` | F1 | F2 | `{fact_exception_adjudication: {…}}` — v2 FL2 OUTPUT_CONTRACT 이식하되 decision enum은 `PATCH|KEEP_AS_IS|BLOCK_REVIEW`(**SPLIT_FACT 삭제** — X-5), schema_version `stage1_fact_exception_adjudication.v1` 유지 | +| `Fact_Ledger_base.json` | F2 (유일 writer) | Stage 2 | **§1-1 불변** | +| `stage1_tmp/fact_ledger/fact_ledger_writer_report.json` | F2 | Stage 2/감사 | 기존 필드 + additive: `status`(§7-5 3단 규칙), `les_review_inheritance[]`, `deterministic_review_items[]`. **created_at_utc 제거**(2-run 결정론 — X-9) | + +폐지: `stage1_tmp/fact_ledger/fact_source_pack.json`은 쓰지 않는다. + +stdout 규약(SKILL §3.3): F0/F2는 단일 JSON 라인만 출력한다(경로·sha·count·status·has_exceptions). pack·bundle 내용을 stdout에 넣지 않는다(v2 FL1의 147KB 예외팩 stdout 재출력 폐지). + +--- + +## 5. STEP 1 — F0 명세 (`Task_F0_fact_ledger_reducer`) + +code-executor(language python, requirements httpx, network agent-network, timeout 180). MCP 보일러플레이트는 v2 FL1(약 700~790행)을 1벌 이식하고 `clientInfo.name`을 `"stage-1-fact-ledger-f0-reducer"`로 변경. **envelope 계층에도 raw_decode fallback 적용**(v2는 inner에만 존재 — X-9). + +1. **입력 read 6종**: §3-2의 6파일. **`evidence_all.json`·`evidence_event_candidates.json`·`client_goal.json`은 읽지 않는다**(기여 0/소비 0 실측 — X-4). stdout manifest에 `"removed_inputs": ["evidence_all.json", "evidence_event_candidates.json", "client_goal.json"]`을 1회 기록한다(재현성 고지). `_validate_legal_effect_structures`의 스키마·인덱스·참조 검증은 전량 이식 유지한다(검증은 메모리에서 수행하고 pack에 재직렬화하지 않는다). +2. **X-8 발췌 실질화**: `_build_evidence_authority`를 개정한다 — excerpt 수집을 `record.get("key_facts")`(리스트 → " / " 결합) 우선, 보조로 `key_dates`·`key_amounts` 결합 요약을 사용하고 기존 키(`excerpt/summary/content/text/body`)는 fallback으로 유지한다. `_score_evidence`는 무변경 이식(입력 텍스트가 실질화되므로 판정 근거가 회복된다). +3. **후보 50행 생성**: FL1의 `_build_row`와 스캔 게이트 3종을 이식하되, 입력을 pack 파일이 아니라 메모리 구조(§5-1에서 구축한 bo_by_id·structure_by_id·structure_index.by_bo_id·signal_hints·evidence_authority)로 직접 연결한다. `bo_fact_inputs` 중간 표현은 메모리 내에서만 구성하며 어떤 채널에도 물질화하지 않는다. +4. **X-1/X-2 예외 생성 교정**: `_exceptions_for_row`에서 (a) `ambiguous_legal_role`(roles>2 & modules>1)과 `ambiguous_claim_chain`(그룹 합>2) trigger를 **삭제**한다 — 복수 role/module/그룹은 배열형 최종 스키마에 그대로 보존되는 정상 상태다. (b) `weak_derived_fact`의 길이(<12자) trigger를 삭제한다 — `derived_fact`가 빈 경우는 현행 결정적 재작성(`_derived_fact`)이 이미 처리하며, 재작성 후에도 비면 LLM행이 아니라 `EMPTY_DERIVED_FACT` review code로 남긴다(현행 코드 유지). +5. **X-3 잔여 유형의 4중 조건 한정**: `legal_calculation_required`·`unresolved_asset_cluster_projection`·`unresolved_structure_role_projection`은 ① F2가 진행하려면 단일 값 선택 필수 ② **source-backed 상충 후보 2개 이상이 compact 레코드에 실재** ③ 보존·defer 불가 ④ compact로 판정 충분 — 을 전부 충족할 때만 pack에 수록한다. 단순 누락(amount 부재 등)은 후보 2개가 없으므로 ② 불충족 → `CALCULATION_BASIS_INCOMPLETE_REVIEW` 등 deterministic review code로 전환한다. legal_theory 계열 표지는 무조건 LLM 금지·Stage 2 회부. +6. **예외 항목 compact 계약**: 유형별 최소 context만 수록(전 유형 공통의 비대 envelope 폐지 — v2의 related_bo_compact 전체·structure 8종·signal 전체·excerpt 8종 내장 금지). 항목당 직렬화 ≤2,000자. 방어 상한(30건 또는 40KB) 초과분은 자동 차단이 아니라 exception_id를 유지한 채 deterministic review로 라우팅하고 audit 추적 항목으로 남긴다. +7. **X-7 LES review 결정론 승계**: `legal_effect_structures.json`에서 (a) 구조별 `review_required=true`, (b) `pending_exception_state.blocked` 비공백(Part 3 v2 additive 필드 — 부재 시 무시), (c) `quality_gate.review_queue`의 실질 항목을 수집하여, 관련 fact 행에 `LES_REVIEW_REQUIRED`(또는 더 구체적 코드) 부여 + `must_consider=true` + candidate bundle의 `les_review_inheritance`에 {fact_id, source_bo_id, 원천 항목} 기록. LLM에 보내지 않는다. +8. **출력**: §4의 slim candidate bundle·exception pack 파일 write(+pack sha를 bundle의 `exception_pack_ref`에 기록) + stdout 1줄: `{"status","message","candidate_path","exception_pack_path","fact_count","has_exceptions","exception_count","deterministic_review_count","les_inheritance_count","removed_inputs"}`. +9. **FAILURE_POLICY**: 입력 누락·파싱 불가 시 파일 미작성, FAILED 단일 JSON 라인 출력 후 **raise**(`sys.exit(0)` 금지). `datetime` 사용 금지(타임스탬프 필드 자체를 산출물에서 제거 — X-9). + +--- + +## 6. STEP 2 — F1 명세 (`Task_F1_fact_exception_adjudicator`) + +LLM task 엔트리: + +```yaml + - task_name: Task_F1_fact_exception_adjudicator + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: low + llm_verbosity: low + max_iterations: 1 + use_tools: [localdocs] + cache_control: {mode: auto, ttl: 20m} + preflight: true + preflight_files: + - stage1_tmp/fact_ledger/fact_exception_pack.json +``` + +프롬프트 구성: STEP 0-3에서 추출한 canonical `COMMON_CACHE_PREFIX_STAGE_1`(3,829B **바이트 그대로**) + 신규 STATIC_BLOCK(Part 3 v2 L1 골격): + +1. MISSION: "결정론 reducer(F0)가 non-deferrable로 판정한 compact 투영 예외만 판정한다. 파일 병합·최종 파일 작성·사실 창작·LES 모호성 재판정 금지." +2. EXECUTION: ① preflight pack의 `has_exceptions` 확인 ② **false이면 no-exception 판정 객체**(`{"fact_exception_adjudication": {"schema_version": "stage1_fact_exception_adjudication.v1", "status": "READY_NO_EXCEPTIONS", "decisions": [], "blocked_review_items": [], "adjudication_gate": {...}}}`)를 `write_file(overwrite=true)`로 `stage1_tmp/fact_ledger/fact_adjudication.json`에 저장하고 `"NO_EXCEPTIONS"`만 출력 후 즉시 종료(terminate) ③ true이면 각 항목을 compact_context만으로 판정(추가 read 금지) ④ 판정 규칙·필드 규칙은 v2 FL2의 DECISION_RULES·OUTPUT_CONTRACT(1471~1561행)를 이식하되 decision enum은 `PATCH|KEEP_AS_IS|BLOCK_REVIEW`로 축소(X-5), **"필요 필드가 allowed 밖이어도 포함 가능" 완화 조항(v2 1477행)은 삭제** — patch는 `allowed_output_fields` 안에서만(F2가 hard reject하므로 계약 일치) ⑤ 결과 write ⑥ `"F1 예외 판정 완료 (decisions=<건수>)"`만 출력 후 종료. +3. ABSOLUTE_FORBIDDEN·PROHIBITED_EXCEPTION_INTERPRETATION: v2 FL2의 해당 블록(LES 재판정 금지, 원본 파일 read 금지, ID·사실 창작 금지, 파일 작성은 판정 파일 1개만, markdown fence 금지)을 이식. +4. FAILURE_POLICY: pack 부재·파싱 불가 시 판정 파일 미작성, `FAILED: <사유>`만 출력. tool 오류 1회만 재시도. + +Plan B(STEP 0-4 실패 시): `preflight: false` + `{{prev.Task_F0_fact_ledger_reducer.json_output}}` 주입(§3-4). + +--- + +## 7. STEP 3 — F2 명세 (`Task_F2_final_fact_ledger_gate_and_writer`) + +code-executor(timeout 180). v2 FL3(1563~2027행) 이식 + 다음 개정: + +1. **채널 정리**: `ADJUDICATION_RAW` 템플릿 주입 제거 → 파일 read 2종(candidate bundle + adjudication 파일; adjudication 파싱에 §6.7 관용 파서 — 코드펜스 salvage 후 strict 검증). exception pack은 bundle의 `exception_pack_ref.sha256` 대조 후 파일 read. **`fact_source_pack.json` 재읽기 폐지** — 참조 검증 ID 집합은 bundle의 `ref_manifest`에서 취한다(`_linked_ids_from_source` 대체). +2. **X-6① exact coverage 등식**: `_validate_adjudication_coverage`를 강화한다 — `expected_exception_ids == decided ∪ blocked`(등식), `decided ∩ blocked == ∅`, 중복 decision·미지 ID·예외 0건에서의 임의 decision hard fail, `adjudication_gate.input_exception_count`·decision_count와 실제 배열 길이 정합 검사, decision의 fact_id/source_bo_id/exception_type이 원 예외와 일치하는지 검사. +3. **X-6② patch hard reject**: `_apply_patch`를 개정한다 — patch key가 `allowed_output_fields ∩ FINAL_FIELD_SET` 밖이면 **해당 decision 전체를 거부**하고 그 fact를 BLOCK 처리(`BLOCK_REVIEW_PRESENT` code + must_consider=true + report 기재). **warning 후 적용(v2 1835~1842행) 금지.** `SPLIT_FACT` 분기는 enum 삭제에 따라 미지 decision으로 처리(hard fail). +4. **적용·검증·게이트**: KEEP_AS_IS/BLOCK/PATCH 적용, `_normalize_final_row`·`_validate_refs`(ref_manifest 기준)·닫힌 스키마 검증·게이트 3종·`gate_review_code_summary`는 이식 유지. X-7 승계 항목을 report의 `les_review_inheritance`로 기재. +5. **X-6③ status 3단 규칙**: stdout·report status를 — 게이트 전부 통과 + blocked 0 + 승계 review 0 → `READY` / blocked 또는 deterministic·LES review 존재(ledger 봉인은 가능) → `READY_WITH_REVIEW`(+ drafting gate 기록) / 스키마·coverage·보존·patch 위반 → `FAILED` + **기존 최종 파일 overwrite 금지**(검증을 write 앞에 두는 순서 유지) — 로 산정한다. v2의 "일률 READY"(2003행) 폐지. +6. **conservation**: 후보 BO 집합 == 최종 행 source_bo_id 집합, `F-###` 연속·유일 검증 유지. write → 재읽기 검증 유지. +7. **stdout 1줄**: `{"status","message","written_file","report_file","fact_count","blocked_review_count","resolved_exception_count","deterministic_review_count","les_inheritance_count","drafting_gate_count"}`. FAILURE_POLICY는 §5-9와 동일(FAILED 1줄 + raise, datetime 금지). + +--- + +## 8. 모델·프리픽스 규칙 요약 + +| task | 유형 | 모델/사양 | 프리픽스 | +|---|---|---|---| +| F0 | code-executor | — | — | +| F1 | LLM 조건부 | gemini-3.1-flash-lite / low / low / max_iterations 1 | 현행 FL2 블록 바이트 유지 (= Part 2 R1 = Part 3 L1, 58f2b72d) | +| F2 | code-executor | — | — | + +v2의 FL2 사양(pro-preview/high/medium/2)을 위로 교체하는 근거는 무효과 이중 실증 + BLOCK_REVIEW 안전판이며(개선보고서 §1.4), pro 계열 escalation 경로는 두지 않는다(판정 불가는 인간 검토가 정답 — Codex §14.2 원칙 채택). + +--- + +## 9. task_procedure (전면 교체) + +```yaml + task_procedure: + IN: + nexts: [Task_F0_fact_ledger_reducer] + wait_until: [] + Task_F0_fact_ledger_reducer: + nexts: [Task_F1_fact_exception_adjudicator] + wait_until: [IN] + Task_F1_fact_exception_adjudicator: + nexts: [Task_F2_final_fact_ledger_gate_and_writer] + wait_until: [Task_F0_fact_ledger_reducer] + Task_F2_final_fact_ledger_gate_and_writer: + nexts: [OUT] + wait_until: [Task_F1_fact_exception_adjudicator] + OUT: + nexts: [] + wait_until: [Task_F2_final_fact_ledger_gate_and_writer] +``` + +--- + +## 10. 수용 기준 (Definition of Done) + +### 10.1 정적 검사 + +1. `Stage_1_Part_4_Claude_v3.yml` YAML parse 성공, task 수 3, task_procedure §9 일치, unreachable 0, 폐기 참조(`fact_source_pack`, `ADJUDICATION_RAW`, `{{prev.Task_FL1…}}`, `{{prev.Task_FL2…}}`, `SPLIT_FACT`, `bo_fact_inputs`) 잔존 0. +2. embedded Python 전부 `ast.parse` 통과, SKILL 준수(boilerplate·`{{__user_hash__}}`/`{{__workspace_hash__}}`·envelope 포함 raw_decode·stdout 단일 JSON 라인·`r"""…"""`·실패 raise·`datetime` 부재). +3. `Fact_Ledger_base.json` writer 정확히 1개(F2), candidate/pack writer 1개(F0), adjudication writer 1개(F1), report writer 1개(F2). +4. F1 프리픽스가 Part 2 Claude v3 R1·Part 3 Claude v2 L1 블록과 **바이트 동일**. +5. F1 사양 §8 일치(flash-lite/low/low/1, preflight pack 1파일). +6. 게이트 3종·review code 6종(§1-3) 자구 보존. + +### 10.2 fixture 회귀 (7/10 실행물 입력, 코드 로컬 실행) + +1. **F0**: 입력 6종만 read, pack 파일 미생성, 후보 50행이 v2 FL1 후보와 필드 동등(candidate_items 27필드 — 발췌 실질화에 따른 credibility/proof_strength 변화는 허용하되 변화 내역 기록), `has_exceptions=false`(42건 전량 결정론 해소: X-1로 40건, X-2로 2건 — 해소 내역이 stdout count와 audit 추적으로 확인 가능), candidate bundle ≤200KB. +2. **F1 경로**: 무예외 → no-exception 판정 파일 + 즉시 종료 시뮬레이션. +3. **F2 골든 동등**: 최종 `Fact_Ledger_base.json`이 현행 골든(`b589ff166a7d`, 50행)과 — (a) 행 수·`F-###` 순서·source_bo_id 시퀀스 동일 (b) 27필드 값 동등(KEEP_AS_IS 이중 실증에 따라 사실상 동일 기대; 허용 편차는 X-8 발췌 실질화가 credibility/proof_strength/derivation에 미치는 문서화된 개선 + X-7 승계로 추가되는 review code·must_consider뿐이며 전량 diff 목록화) (c) writer report의 게이트 결과(NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW 2건 등) 보존 + les_review_inheritance 추가 확인. +4. **2-run 바이트 결정론** (candidate·pack·최종·report 4파일 — 타임스탬프 제거로 성립). +5. **주입 테스트**: ① true 투영 충돌 합성(동일 필드에 source-backed 상충 후보 2개) → pack 수록·F1 발동 경로·비차단 판정 반영 ② 임의 extra/중복 decision → F2 hard fail ③ allowed 밖 patch → 거부 + BLOCK 처리(적용 0) ④ blocked 존재 → status `READY_WITH_REVIEW` + drafting gate ⑤ coverage 등식 위반(판정 누락) → F2 hard fail ⑥ adjudication 코드펜스 → 관용 파서 salvage / 부재 → FAILED + 최종 파일 미작성 ⑦ BO conservation 위반 합성 → hard fail ⑧ notice/possession/lien 게이트 회귀(현행 코드 6종 발화 케이스 유지) ⑨ LES review 주입(Part 3 산출에 review_required/blocked 합성) → fact code·report 승계. +6. **Stage 2 계약**: 최종 파일 27필드·배열 형식·enum 무변경 확인. + +### 10.3 성능 합격 기준 (실 재실행, cold/warm 구분 3회 median) + +중간 파일 1,020KB → 200KB 이하, stdout 157KB → 2KB 이하, LLM 실행 상시 1회(pro/high) → 조건부(정상 fixture 0회, flash-lite), 비용 $0.58 → ≈$0, 런타임 3:06 → no-exception 경로 90초 이하 목표. **§10.2 품질 불변식 위반 시 성능 달성해도 실패.** + +--- + +## 11. 개정 보고서 기재 의무 + +v3 저장 시 최종 보고에 명시한다: (1) 예외 42건의 해소 내역(X-1 40건 / X-2 2건)과 audit 추적 경로, (2) 골든 대비 diff 전량 목록(발췌 실질화·LES 승계로 인한 문서화된 개선 포함), (3) Plan A/B 중 적용 채널, (4) D-1(예외 발생률 모니터링)·D-2(Part 1 GB 프리픽스 3,829B 정렬 — Stage 1 flash-lite 4형제 캐시 통일의 마지막 퍼즐)·D-3(중간 파일 debug-mode화)의 후속 이관. + +## 12. 금지 사항 최종 목록 + +1. v2 원본 덮어쓰기·이동·삭제 금지 (v3는 신규 파일). +2. §1 불변 계약(최종 파일 27필드 스키마·행 규칙·게이트 3종·review code 6종) 변경 금지 — additive만 허용. +3. Part 1/2/3 YAML 수정 금지. +4. 복수 role/module/claim group의 축소 선택 강제 금지 — 배열 보존이 기본값. +5. F1에 pack 외 입력 제공 금지, F1의 최종 파일·candidate·report 작성 금지, 후보 밖 값·allowed 밖 patch 생성 금지. +6. 경계 위반 patch의 "경고 후 적용" 금지 — 거부·차단만 허용. +7. blocked·deterministic review·LES 승계 review의 silent drop 금지(전량 report·audit 추적). +8. LLM의 긴 서술·사실 창작 금지, pro 계열 escalation 금지. +9. 실패의 `sys.exit(0)` 위장 금지, 검증 실패 상태의 최종 파일 overwrite 금지, `datetime` 사용 금지. +10. 본 명세서에 없는 "개선"의 임의 추가 금지 — 범위 밖 아이디어는 개정 보고서에 제안으로만 기록. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_4_Improvement_Strategy_Codex_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_4_Improvement_Strategy_Codex_v1.md new file mode 100644 index 00000000..0e6a5e63 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_4_Improvement_Strategy_Codex_v1.md @@ -0,0 +1,1881 @@ +# Stage 1 Part 4 v2 개정 작업 명세서 + +## 0. 문서의 지위와 실행 명령 + +이 문서는 `Part_4_Improvement_Codex_v1.md`의 통합 결론을 실제 YAML 개정 작업으로 변환한 **완료 조건이 있는 구현 명세서**다. 구현자는 이 문서를 참고자료나 선택지로 취급하지 않는다. 이 문서에서 “유지”, “삭제”, “교체”, “금지”, “반드시”로 표현한 지시를 그대로 수행한다. + +이번 개정은 P0와 P1을 적용한다. P2인 FL0+FL1 통합, intermediate 파일의 물리적 폐기, `must_consider`·`legal_centrality` 의미 변경은 수행하지 않는다. + +### 0.1 입력, 출력, 작업 경계 + +| 구분 | 경로 | 처리 원칙 | +|---|---|---| +| 개정 원본 | `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_4_v2.yml` | 읽기 전용. 직접 덮어쓰지 않는다. | +| 설계 근거 | `YAML_Prompts/1. Stage_1/v.6/Part_4_Improvement_Codex_v1.md` | false exception 제거, conditional FL2, FL3 fail-closed, compact artifact 원칙을 적용한다. | +| Python 제약 | `YAML_Prompts/1. Stage_1/SKILL.md` | direct MCP, JSON parser, wildcard, template, failure 제약을 모두 적용한다. | +| canonical prefix | `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_1_Codex_v3.yml` | 첫 `` 블록을 바이트 동일하게 복제한다. | +| upstream 계약 | `Stage_1_Part_1_Codex_v3.yml`, `Stage_1_Part_2_Codex_v3.yml`, `Stage_1_Part_3_Codex_v2.yml` | evidence, BO, signals, legal-effect structure와 review 계약을 보존한다. | +| downstream 계약 | Stage 2의 `Fact_Ledger_base.json` 소비 | 최종 파일명, top-level array, 27개 필드와 ID/ref 의미를 변경하지 않는다. | +| 회귀 fixture | `YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm` | 개정 전 기준값과 의미·성능을 비교한다. | +| 권장 개정 결과 | `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_4_Codex_v3.yml` | 원본 전체를 복제한 뒤 이 명세가 허용한 부분만 수정한다. | + +다음 파일은 이번 YAML 구현 작업에서 수정하지 않는다. + +- `Stage_1_Part_4_v2.yml` +- `Stage_1_Part_1_Codex_v3.yml` +- `Stage_1_Part_2_Codex_v3.yml` +- `Stage_1_Part_3_Codex_v2.yml` +- `Results_July_10_3_15pm` 아래의 모든 파일 +- `YAML_Prompts/1. Stage_1/SKILL.md` +- 기존 Claude/Codex 분석·전략 문서 + +### 0.2 최종 목표 + +task definition은 정확히 4개를 유지한다. 단, 단일 FL2를 wildcard template으로 교체한다. + +```text +Task_FL0_fact_source_pack_compiler [Python] +Task_FL1_deterministic_fact_ledger_candidate_builder [Python] +Task_FL2_fact_exception_adjudicator_* [0..N LLM] +Task_FL3_final_fact_ledger_gate_and_writer [Python] +``` + +runtime task 수는 다음과 같아야 한다. + +```text +no-exception 경로: Python task 3개, LLM task 0개 +exception pack N개 경로: Python task 3개 + LLM task N개 +``` + +직접 목표는 다음과 같다. + +1. 다중 `legal_effect_roles[]`와 다중 module route를 정상 상태로 보존한다. +2. 문자열 길이만으로 `weak_derived_fact`를 만들지 않는다. +3. 7월 10일 fixture의 false exception 42건을 0건으로 만든다. +4. true exception이 없으면 FL2 instance를 하나도 생성하지 않는다. +5. true exception은 source-backed compact micro-pack으로만 전달한다. +6. FL2 모델을 flash-lite/medium으로 낮춘다. +7. Part 3 review queue를 deterministic하게 Fact와 writer report에 승계한다. +8. FL3 coverage·patch·blocked·reference gate를 exact/fail-closed로 만든다. +9. Part 4 input read를 9개에서 6개로 줄인다. +10. source pack의 중복·사장 필드를 제거한다. +11. exception pack 전체를 stdout과 prompt에 중복 출력하지 않는다. +12. final `Fact_Ledger_base.json`의 외부 계약을 보존한다. + +### 0.3 목표 DAG + +```text +IN + | + v +Task_FL0_fact_source_pack_compiler [Python] + | six-file compact source artifact + v +Task_FL1_deterministic_fact_ledger_candidate_builder [Python] + |\ + | +--> Task_FL2_fact_exception_adjudicator_* [0..N LLM] + | one micro-pack per instance + | one adjudication part per instance + | + +------ dynamic_fanout=[] when no true exception + | + +------------------------------------+ + v +Task_FL3_final_fact_ledger_gate_and_writer [Python] + | waits FL1 + all FL2_* + | exact conservation, coverage, patch, review gates + | sole final writer + v +OUT +``` + +## 1. 절대 준수 원칙 + +### 1.1 법률·품질 안전 원칙 + +1. Stage 1은 최종 승패, 청구 인용 여부 또는 확정 법률효과를 판단하지 않는다. +2. 하나의 사실이 복수 청구·법률효과·downstream module에 관련되면 배열을 보존한다. +3. 복수 route의 존재만으로 “ambiguous”라고 판단하지 않는다. +4. source-backed 후보가 충돌하지 않으면 LLM을 호출하지 않는다. +5. amount, date, party, asset, legal role 또는 source ref를 LLM이 창조하게 하지 않는다. +6. compact context가 불충분하면 `BLOCK_REVIEW`를 사용한다. +7. legal theory 확정이 필요한 문제는 `STAGE_2` 또는 `HUMAN_REVIEW`로 넘긴다. +8. Part 3의 blocked/review 상태를 제거하거나 정상으로 승격하지 않는다. +9. meeting-only 또는 간접 evidence를 direct documentary proof로 승격하지 않는다. +10. `must_not_drop_in_claim_types[]`와 `must_consider`의 non-drop 기능을 약화하지 않는다. + +### 1.2 deterministic 작업 원칙 + +다음은 전부 Python이 수행한다. + +- JSON schema/status 검증 +- canonical serialization과 SHA-256 +- input manifest와 run fingerprint +- BO/structure/evidence ref universe 생성 +- ID 정렬·중복 제거·coverage +- source pack join과 compact projection +- Fact candidate 27개 필드 작성 +- empty derived fact fallback +- credibility/proof score의 닫힌 규칙 +- Part 3 review-by-BO 승계 +- possession/legal-cause conflict scan +- notice receipt gate +- lien extinction notice scan +- true-exception 4중 조건 검사 +- micro-pack budget과 stable ID +- exception/adjudication exact coverage +- allowed patch whitelist +- final normalization·ordering·single write + +LLM은 true exception의 닫힌 decision만 선택한다. + +### 1.3 단일 writer 권한 + +| artifact | 유일 writer | +|---|---| +| `stage1_tmp/fact_ledger/fact_source_pack.json` | FL0 | +| `stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json` | FL1 | +| `stage1_tmp/fact_ledger/fact_exception_manifest.json` | FL1 | +| `stage1_tmp/fact_ledger/fact_exception_input_parts/PACK-*.json` | FL1 | +| `stage1_tmp/fact_ledger/fact_exception_adjudication_parts/PACK-*.json` | 해당 FL2 instance | +| `Fact_Ledger_base.json` | FL3 | +| `stage1_tmp/fact_ledger/fact_ledger_writer_report.json` | FL3 | + +다른 task는 위 writer의 파일을 수정하지 않는다. + +### 1.4 외부 계약 불변 + +다음 최종 계약은 변경하지 않는다. + +```text +file: Fact_Ledger_base.json +root: JSON array +row count rule: candidate conservation 결과 +fact_id rule: F-###, deterministic order +``` + +각 row의 필드는 정확히 다음 27개다. + +```text +fact_id +source_bo_id +type +date +parties +object_spec +amount +action +evidence_refs +credibility +legal_centrality +proof_strength +legal_effect_roles +claim_chain_ref +linked_structures +must_not_drop_in_claim_types +must_consider +state_context +money_claim_effect +commercial_successor_effect +actio_roles +linked_actio_structures +downstream_module_candidates +skeleton_validation_codes +derived_fact +derivation_basis +legal_calculation_object +``` + +필드 삭제, 이름 변경, 임의 필드 추가를 금지한다. `skeleton_validation_codes[]`와 `derivation_basis`에는 additive review 정보만 추가할 수 있다. + +### 1.5 MCP와 보안 + +1. `tools.mcpServers`의 기존 type, URL, description, header를 그대로 유지한다. +2. 새 API key, token, credential을 추가하지 않는다. +3. 기존 인증 값을 stdout, artifact 또는 report에 출력하지 않는다. +4. 모든 Python direct MCP initialize의 `clientInfo`에 `{{__user_hash__}}`, `{{__workspace_hash__}}`를 포함한다. +5. FL2에는 `localdocs`만 허용한다. +6. FL2의 localdocs 권한은 지정된 adjudication output path에 대한 `write_file` 1회로 제한한다. +7. FL2는 `read_docs`, `list_docs`, 다른 파일 write를 호출하지 않는다. + +### 1.6 금지된 개정 + +- FL0와 FL1을 이번 v3에서 합치지 않는다. +- FL2를 완전히 삭제하지 않는다. +- 단일 상시 FL2 task를 유지하지 않는다. +- 검증되지 않은 `run_if`, `skip_if`, `when` 키를 만들지 않는다. +- full BO, full structure index, full signal 또는 raw evidence를 wildcard item에 넣지 않는다. +- exception을 global cap 뒤에서 조용히 drop하지 않는다. +- `evidence_all.json`, `client_goal.json`, `evidence_event_candidates.json`을 Part 4에서 읽지 않는다. +- 위 세 파일 자체를 Stage 1 handoff에서 삭제하지 않는다. +- `must_consider`를 이번 개정에서 false로 낮추지 않는다. +- `legal_centrality`의 현재 enum 산정 의미를 이번 개정에서 바꾸지 않는다. +- `fact_source_pack.json`, candidate, writer report 파일 자체를 폐기하지 않는다. +- `SPLIT_FACT`를 FL2 decision에 남기지 않는다. +- 허용 범위 밖 patch를 warning만 남기고 적용하지 않는다. +- LLM이 새 Fact/BO/evidence/structure/ref ID를 만들게 하지 않는다. +- placeholder, 생략 주석, `기존 코드 동일`, 의사코드를 최종 YAML code/prompt에 남기지 않는다. +- Python 실패 후 `sys.exit(0)`을 사용하지 않는다. + +## 2. 원본 구조와 정확한 개정 매핑 + +### 2.1 Agent header + +1. `Agent.name`을 유지한다. +2. `Agent.description`을 유지한다. +3. `Agent.version`을 `v2.0`으로 변경한다. +4. Stage name과 description을 유지한다. +5. `tools.mcpServers` 블록은 byte-level 수정을 피하고 그대로 유지한다. +6. Stage-level LLM provider/model을 추가하지 않는다. + +### 2.2 task 정의 매핑 + +| 현재 task | 처리 | 새 책임 | +|---|---|---| +| `Task_FL0_fact_source_pack_compiler` | 이름 유지·code 전면 개정 | 6-file compact source compiler, upstream review map | +| `Task_FL1_deterministic_fact_ledger_candidate_builder` | 이름 유지·code 전면 개정 | deterministic Fact candidate, review, exception planner | +| `Task_FL2_single_fact_exception_adjudicator` | 삭제 | wildcard FL2로 대체 | +| `Task_FL2_fact_exception_adjudicator_*` | 신규 | true micro-exception만 판정 | +| `Task_FL3_final_fact_ledger_gate_and_writer` | 이름 유지·code 전면 개정 | exact decision reduce, review/status gate, final writer | + +task definition 수는 정확히 4개다. + +### 2.3 내부 schema version + +내부 artifact의 의미가 바뀌므로 다음 version을 사용한다. + +```text +stage1_fact_source_pack.v3 +stage1_fact_source_pack_manifest.v2 +stage1_fact_ledger_candidate_bundle.v2 +stage1_fact_ledger_fl1_manifest.v2 +stage1_fact_exception_manifest.v2 +stage1_fact_exception_input_part.v2 +stage1_fact_exception_adjudication_part.v2 +stage1_fact_ledger_writer_report.v2 +stage1_fact_ledger_final_writer.v2 +``` + +최종 `Fact_Ledger_base.json`은 top-level array이므로 새 root schema field를 추가하지 않는다. + +## 3. canonical common cache prefix + +### 3.1 source와 기대 hash + +`Stage_1_Part_1_Codex_v3.yml`의 첫 `` 시작 태그부터 종료 태그까지 그대로 복제한다. + +```text +bytes: 3385 +sha256: 6e01f72f1803767b6bb16b5aef6cf34760e6dc1b293c316c75e990d09ad5fe63 +``` + +공백, 줄바꿈, indentation, 문장 또는 태그 순서를 바꾸지 않는다. + +### 3.2 적용 대상 + +`Task_FL2_fact_exception_adjudicator_*` prompt의 맨 앞에 prefix를 둔다. static task block과 `{{item.*}}`는 종료 태그 뒤에 둔다. + +Part 4 v2의 3,829B `58f2...bb5` prefix는 제거한다. + +### 3.3 LLM 설정 + +```yaml +llm_provider: google +llm_model: gemini-3.1-flash-lite +llm_reasoning: medium +llm_verbosity: low +max_iterations: 2 +max_concurrency: 2 +cache_control: + mode: auto + ttl: 1h +use_tools: +- localdocs +preflight: false +``` + +Pro/high escalation을 추가하지 않는다. + +## 4. 모든 Python task의 공통 구현 제약 + +### 4.1 task header + +FL0, FL1, FL3는 다음을 사용한다. + +```yaml +mcp: code-executor +tool_name: run_code +parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 +``` + +### 4.2 localdocs full boilerplate + +각 Python task는 독립 실행 가능해야 하므로 다음을 자체 포함한다. + +- `httpx.Client(timeout=60)` +- MCP initialize와 initialized notification +- monotonic message ID +- SSE와 plain JSON RPC response parser +- `read_docs`, `write_file` +- user/workspace hash +- tool error surface +- post-write reread validator + +다른 task의 함수가 runtime에 공유된다고 가정하지 않는다. + +### 4.3 JSON parser + +모든 JSON read는 다음 순서를 따른다. + +1. MCP response에서 마지막 유효 SSE `data:` object 또는 plain JSON object를 얻는다. +2. localdocs envelope를 해제한다. +3. inner content가 object/list면 그대로 사용한다. +4. string이면 BOM과 앞뒤 공백을 제거하고 `json.loads`를 시도한다. +5. `JSONDecodeError`이면 `JSONDecoder.raw_decode`로 첫 JSON 값을 읽는다. +6. 허용하지 않은 trailing text가 있으면 실패한다. +7. 실패 메시지에는 파일명과 앞 300자만 넣고 전체 source를 노출하지 않는다. + +outer envelope와 inner document 모두 fallback을 가져야 한다. + +### 4.4 canonical serialization과 hash + +canonical 함수의 의미는 다음과 같다. + +```python +json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) +``` + +SHA-256은 canonical UTF-8 bytes에 계산한다. + +`run_fingerprint`는 다음 object의 hash다. + +```json +{ + "contract": "stage1_fact_ledger_run.v3", + "input_digests": { + "BO.json": "sha256:...", + "legal_effect_structures.json": "sha256:...", + "actio_case_signals.json": "sha256:...", + "case_liability_signals.json": "sha256:...", + "legal_effect_signals.json": "sha256:...", + "evidence_indexed.json": "sha256:..." + } +} +``` + +FL0, FL1, exception manifest, input parts, adjudication parts와 FL3 report가 같은 fingerprint를 사용한다. + +### 4.5 결정적 ordering + +- BO: `source_bo_id` 문자열 오름차순 +- Fact: BO 순서에 따라 `F-001`부터 +- structure/evidence/ref/code: 중복 제거 후 문자열 오름차순 +- review: `source_bo_id`, review code, source ID 순 +- exception: `source_bo_id`, exception type, target field 순 +- pack: exception type 우선, exception ID 순 +- JSON map의 artifact write: `sort_keys=True` canonical hash, 사람이 읽는 파일은 indent 2 허용 + +동일 입력 2회에서 timestamp를 제외한 모든 semantic artifact가 바이트 동일해야 한다. source pack, candidate, exception manifest/part에는 timestamp를 넣지 않는다. + +### 4.6 stdout + +Python stdout은 한 줄 compact JSON만 출력한다. artifact 본문, traceback, full candidate, full exception 또는 source text를 출력하지 않는다. + +### 4.7 failure + +각 Python task의 outermost failure는 다음 순서를 따른다. + +1. task-specific `FAILED` manifest 한 줄을 stdout으로 출력한다. +2. final target은 쓰지 않거나 기존 파일을 overwrite하지 않는다. +3. bare `sys.exit(0)`을 사용하지 않는다. +4. exception을 re-raise하거나 non-zero로 종료한다. + +### 4.8 code 완전성 + +최종 YAML `parameters.code`에는 실행 가능한 완전한 Python을 넣는다. helper, validator, writer, reread, `main()`, failure path를 생략하지 않는다. + +## 5. 안정 ID, enum과 최종 row 규칙 + +### 5.1 stable ID + +```text +fact_id: F-### +review_id: FRV-### +exception_id: FX-### +pack_id: FL-PACK-### +``` + +입력 순서가 달라도 canonical BO ordering이 같으면 ID가 같아야 한다. + +### 5.2 final enum + +다음 enum을 유지한다. + +```text +credibility: high | medium | low +legal_centrality: low | medium | critical +proof_strength: low | medium | high | documentary | meeting_only_core +``` + +### 5.3 skeleton review code + +기존 code를 유지하고 다음 code를 additive하게 허용한다. + +```text +EMPTY_DERIVED_FACT +EMPTY_DERIVED_FACT_AFTER_FALLBACK +NO_EVIDENCE_REFS +NO_LINKED_LEGAL_EFFECT_STRUCTURE +LES_REVIEW_REQUIRED +LES_DETERMINISTIC_REVIEW +LES_BLOCKED_EXCEPTION_REVIEW +LES_REVIEW_SENSITIVE_SPLIT +LEGAL_CAUSE_STATE_UNKNOWN_REVIEW +POSSESSION_CAUSE_CONFLICT +NOTICE_RECEIPT_UNVERIFIED +NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW +LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW +CALCULATION_BASIS_MISSING_STAGE2 +BLOCK_REVIEW_PRESENT +FL2_REVIEW_REQUIRED +``` + +임의 prose를 code 배열에 넣지 않는다. + +### 5.4 현재 priority 의미 보존 + +이번 v3에서는 다음을 바꾸지 않는다. + +- `must_consider` 산정의 non-drop 방향 +- `legal_centrality` enum 산정 기준 + +writer report에 산정 근거를 기록할 수 있으나 final 값을 대량 하향 조정하지 않는다. + +## 6. FL0 작업 명세 + +### 6.1 task header + +```yaml +task_name: Task_FL0_fact_source_pack_compiler +mcp: code-executor +tool_name: run_code +parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 +``` + +### 6.2 정확한 입력 + +FL0는 정확히 다음 6개 파일을 읽는다. + +```text +BO.json +legal_effect_structures.json +actio_case_signals.json +case_liability_signals.json +legal_effect_signals.json +evidence_indexed.json +``` + +다음은 읽지 않는다. + +```text +client_goal.json +evidence_all.json +evidence_event_candidates.json +``` + +### 6.3 입력 검증 + +1. `BO.json`은 비어 있지 않은 array이며 BO ID가 고유해야 한다. +2. `legal_effect_structures.json.schema_version == "stage1_legal_effect_structures.v1"`이어야 한다. +3. LES status는 `READY|READY_WITH_REVIEW`만 허용한다. +4. `legal_effect_structures[]`, `structure_index`, `quality_gate`가 존재해야 한다. +5. 5개 named structure index가 모두 object여야 한다. +6. 모든 structure ID, source BO ID, evidence index의 참조를 검증한다. +7. `structure_index.by_bo_id`와 structures의 역참조가 일치해야 한다. +8. Part 3 BO/evidence coverage gate가 false이면 실패한다. +9. signals는 각 canonical root와 status를 검증한다. +10. `evidence_indexed.json`은 Part 1 conservation 완료 schema를 허용하고 evidence ID를 고유하게 만든다. + +5개 structure index는 메모리에서 모두 검증한다. source pack에는 FL1/FL3가 소비하는 `by_bo_id` projection과 ref universe만 저장한다. + +### 6.4 compact BO map + +`bo_by_id`에는 기존 Fact 생성과 deterministic domain gate에 필요한 필드만 넣는다. + +```json +{ + "bh1": { + "bo_id": "bh1", + "bo_type": "string|null", + "action": "string|null", + "action_type": "string|null", + "juristic_act": "string|null", + "reason": "string|null", + "prior_act": "string|null", + "legal_keywords": [], + "amount": null, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [], + "domain_payload_compact": {} + } +} +``` + +`domain_payload_compact`는 다음 named key만 보존한다. + +```text +money_claim_details +possession_state +lien_state +notice_lifecycle +asset_alias +fractional_effect_events +``` + +### 6.5 compact structure map + +`structure_by_id` item은 다음 필드를 유지한다. + +```json +{ + "structure_id": "LES-001", + "candidate_structure_type": "money_claim", + "candidate_structure_types": [], + "structure_label": "string", + "source_bo_ids": [], + "issue_cluster_ids": [], + "asset_cluster_ids": [], + "evidence_indexes": [], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [], + "linked_actio_signal_ids": [], + "legal_effect_roles": [], + "review_required": false, + "routing_audit": { + "blocked_exception_ids": [], + "deterministic_review_ids": [] + } +} +``` + +`structure_refs_by_bo_id`는 다음 shape다. + +```json +{ + "bh1": { + "structure_ids": [], + "issue_cluster_ids": [], + "asset_cluster_ids": [], + "evidence_index_ids": [], + "candidate_structure_types": [], + "claim_group_ids": [], + "liability_group_ids": [], + "downstream_fact_roles": [] + } +} +``` + +### 6.6 compact evidence authority + +실제 Part 1 schema의 named field를 사용한다. + +```json +{ + "E-001": { + "evidence_index": "E-001", + "title": "string", + "doc_type": "string|null", + "key_facts": [], + "key_dates": [], + "key_amounts": [], + "source_pointer": {}, + "authentication": null + } +} +``` + +규칙: + +1. `key_facts`, `key_dates`, `key_amounts`를 읽는다. +2. raw document body나 full evidence record를 넣지 않는다. +3. 각 문자열은 500자 이하로 자른다. +4. 배열은 원래 순서를 유지하며 duplicate만 제거한다. +5. 없는 authentication을 추정하지 않는다. +6. evidence ref가 authority에 없으면 source-integrity warning을 만든다. + +### 6.7 signal hints + +`signal_hints_by_bo_id`는 전역 map 한 벌만 둔다. BO별 source entry에 같은 signal payload를 재복제하지 않는다. + +각 compact signal은 다음만 갖는다. + +```json +{ + "signal_type": "actio|case_liability|legal_effect", + "signal_id": "string|null", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [] +} +``` + +구조화 필드를 다시 문자열화한 `summary`는 넣지 않는다. + +### 6.8 upstream review-by-BO + +Part 3 `quality_gate.review_queue[]`와 structure review를 BO별로 정규화한다. + +```json +{ + "bh1": [ + { + "review_type": "deterministic_review|blocked_exception|review_sensitive_split|structure_review", + "review_id": null, + "exception_id": null, + "review_code": null, + "reason_code": null, + "downstream_owner": "STAGE_2|HUMAN_REVIEW|null", + "structure_ids": [], + "blocking": false + } + ] +} +``` + +`review_type == blocked_exception`이면 `blocking: true`다. 같은 source item을 중복 추가하지 않는다. + +### 6.9 source pack schema + +`stage1_tmp/fact_ledger/fact_source_pack.json`: + +```json +{ + "fact_source_pack": { + "schema_version": "stage1_fact_source_pack.v3", + "status": "READY|READY_WITH_REVIEW", + "run_fingerprint": "sha256:...", + "input_manifest": { + "files": {}, + "source_schema_versions": {} + }, + "bo_by_id": {}, + "structure_by_id": {}, + "structure_refs_by_bo_id": {}, + "signal_hints_by_bo_id": {}, + "evidence_authority_by_index": {}, + "upstream_review_by_bo_id": {}, + "reference_universe": { + "bo_ids": [], + "structure_ids": [], + "evidence_indexes": [] + }, + "source_integrity": { + "warnings": [], + "warning_count": 0, + "reference_validation_pass": true + }, + "source_counts": { + "bo_count": 0, + "structure_count": 0, + "evidence_count": 0, + "review_count": 0, + "blocking_review_count": 0 + } + } +} +``` + +다음을 넣지 않는다. + +- `client_goal_compact` +- `bo_fact_inputs` +- `event_candidates_by_evidence_index` +- `bo_fact_inputs[].event_candidates` +- full 5-way structure index +- duplicate per-BO evidence authority +- duplicate per-BO signal hints + +### 6.10 FL0 status와 stdout + +```text +schema/ref/coverage failure -> FAILED manifest + raise +review 또는 non-blocking integrity warning 존재 -> READY_WITH_REVIEW +그 외 -> READY +``` + +stdout: + +```json +{ + "fact_source_pack_manifest": { + "schema_version": "stage1_fact_source_pack_manifest.v2", + "status": "READY|READY_WITH_REVIEW|FAILED", + "path": "stage1_tmp/fact_ledger/fact_source_pack.json", + "sha256": "sha256:...", + "run_fingerprint": "sha256:...", + "bo_count": 0, + "structure_count": 0, + "evidence_count": 0, + "review_count": 0 + } +} +``` + +artifact 전체를 stdout에 넣지 않는다. + +## 7. FL1 작업 명세 + +### 7.1 task header + +```yaml +task_name: Task_FL1_deterministic_fact_ledger_candidate_builder +mcp: code-executor +tool_name: run_code +parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 +``` + +### 7.2 입력 경계 + +정확히 다음 파일 하나만 읽는다. + +```text +stage1_tmp/fact_ledger/fact_source_pack.json +``` + +FL0 stdout 전체를 Python template에 삽입하지 않는다. source pack schema, hash와 run fingerprint를 검증한다. + +### 7.3 Fact candidate 생성 + +기존 27개 final field 생성 로직과 다음 type-to-module/type-to-role map을 보존한다. + +```text +money_claim +commercial_successor +secured_debt +registry_invalidity +land_use_gain +valuation +fraudulent_transfer +preserved_claim +succession_notice_lien_asset_defense +general_legal_effect +``` + +다음 입력 lookup을 사용한다. + +```text +bo = bo_by_id[source_bo_id] +linked = structure_refs_by_bo_id[source_bo_id] +signals = signal_hints_by_bo_id[source_bo_id] +structures = structure_by_id[linked.structure_ids] +evidence = evidence_authority_by_index[evidence_refs] +upstream_reviews = upstream_review_by_bo_id[source_bo_id] +``` + +같은 값이 BO별 source item에 내장되어 있다고 가정하지 않는다. + +### 7.4 evidence score + +`_score_evidence`는 compact evidence의 실제 필드를 사용한다. + +1. evidence가 없으면 `low/low`. +2. meeting/회의/면담 유형만 있으면 `medium/meeting_only_core`. +3. 문서형 doc_type/title/source pointer와 key facts가 있으면 기존 documentary marker 규칙을 적용한다. +4. evidence 수만으로 authentication을 확정하지 않는다. +5. raw excerpt가 없다는 이유로 title만 직렬화해 판단하지 않는다. + +fixture의 기존 enum이 합리적이면 유지한다. key field 교정으로 값이 달라지면 writer report에 basis code를 기록한다. + +### 7.5 empty derived fact fallback + +문자열 길이 기준을 삭제한다. + +`derived_fact`가 비었을 때만 다음 순서로 결정적 fallback을 만든다. + +```text +BehaviorTime -> Performer -> Subject -> Object -> Action -> Amount +``` + +빈 값을 제외해 공백 한 칸으로 연결한다. fallback 후에도 비면 다음을 적용한다. + +- `EMPTY_DERIVED_FACT_AFTER_FALLBACK` +- deterministic review `FRV-###` +- `must_consider: true` + +짧지만 비어 있지 않은 `물품대금 지급 최고`, `목적물 특정`은 예외가 아니다. + +### 7.6 upstream review 승계 + +BO에 review가 있으면 다음을 Fact에 적용한다. + +| upstream review | row code | 처리 | +|---|---|---| +| deterministic review | `LES_DETERMINISTIC_REVIEW` | `must_consider=true` | +| blocked exception | `LES_BLOCKED_EXCEPTION_REVIEW` | `must_consider=true`, blocking review | +| review-sensitive split | `LES_REVIEW_SENSITIVE_SPLIT` | `must_consider=true` | +| structure review only | `LES_REVIEW_REQUIRED` | `must_consider=true` | + +review를 LLM exception으로 전환하지 않는다. + +### 7.7 기존 deterministic domain gate 유지 + +다음 함수의 법률적 목적과 closed review code를 유지한다. + +- possession state와 legal-cause assertion 분리·충돌 scan +- notice received/arrival/evidence gate +- lien extinction notice 이후 possession assertion scan + +Part 2의 named `possession_state`, `notice_lifecycle`, `lien_state`를 읽는다. + +### 7.8 true exception 4중 조건 + +LLM exception은 다음을 모두 충족해야 한다. + +1. 하나의 final scalar 또는 닫힌 projection을 선택해야 final write가 가능하다. +2. 서로 다른 source ref가 지지하는 후보가 2개 이상이다. +3. 배열 보존, `KEEP_AS_IS`, deterministic 우선순위 또는 Stage 2 defer로 해결할 수 없다. +4. compact context만으로 안전한 비교가 가능하고 item 크기가 예산 이하다. + +하나라도 불충족하면 LLM exception을 만들지 않는다. deterministic row를 보존하거나 review item을 만든다. + +다음은 true exception이 아니다. + +- `legal_effect_roles`가 3개 이상 +- module candidate가 2개 이상 +- claim/liability group ID가 여러 개 +- 비어 있지 않은 짧은 derived fact +- 계산 source가 단순 누락됨 +- Part 3 review가 존재함 +- legal theory 판단이 필요함 + +### 7.9 closed exception type + +```text +conflicting_fact_projection +conflicting_calculation_projection +conflicting_asset_projection +conflicting_structure_role_projection +``` + +`ambiguous_legal_role`, `ambiguous_claim_chain`, `weak_derived_fact`, `legal_calculation_required`, `unresolved_asset_cluster_projection`, `unresolved_structure_role_projection`의 기존 자동 trigger는 삭제한다. + +### 7.10 deterministic review schema + +```json +{ + "review_id": "FRV-001", + "review_code": "EMPTY_DERIVED_FACT_AFTER_FALLBACK|CALCULATION_BASIS_MISSING_STAGE2|LES_REVIEW_REQUIRED|LES_BLOCKED_EXCEPTION_REVIEW|PACK_CONTEXT_TOO_LARGE", + "severity": "WARNING|HARD_WARNING|BLOCK_FINAL_DRAFTING", + "fact_id": "F-001", + "source_bo_id": "bh1", + "structure_ids": [], + "evidence_refs": [], + "reason_code": "closed code", + "downstream_owner": "STAGE_2|HUMAN_REVIEW" +} +``` + +긴 prose를 넣지 않는다. + +### 7.11 exception item schema + +```json +{ + "exception_id": "FX-001", + "exception_type": "conflicting_fact_projection|conflicting_calculation_projection|conflicting_asset_projection|conflicting_structure_role_projection", + "fact_id": "F-001", + "source_bo_id": "bh1", + "target_fields": [], + "candidate_values": [ + { + "field": "amount", + "value": null, + "basis_codes": [], + "source_refs": [] + } + ], + "candidate_fact_subset": {}, + "structure_refs": [], + "evidence_facts": [], + "allowed_output_fields": [], + "allowed_decisions": ["KEEP_AS_IS", "PATCH", "BLOCK_REVIEW"], + "conflict_codes": [] +} +``` + +다음을 넣지 않는다. + +- full BO +- full signals +- full structures +- raw evidence +- unrelated Fact fields +- chain-of-thought 요청 + +### 7.12 micro-pack budget + +FL1 code 상단에 다음 상수를 선언한다. + +```text +MAX_EXCEPTION_ITEMS_PER_PACK = 4 +TARGET_EXCEPTION_ITEMS_PER_PACK = 3 +MAX_EXCEPTION_ITEM_BYTES = 12000 +MAX_EXCEPTION_PACK_BYTES = 25000 +MAX_EXCEPTION_PACKS = 8 +MAX_CONCURRENCY = 2 +``` + +규칙: + +1. 같은 exception type끼리 우선 pack한다. +2. item이 12KB를 넘으면 LLM에 보내지 않고 `PACK_CONTEXT_TOO_LARGE` review로 전환한다. +3. pack이 25KB를 넘으면 더 작은 pack으로 분리한다. +4. 8 pack을 초과하면 뒤를 자르지 않고 FL1을 `BLOCKED/FAILED` 처리한다. +5. serialized byte는 canonical UTF-8 bytes로 측정한다. + +### 7.13 exception input part + +각 pack은 다음 경로에 쓴다. + +```text +stage1_tmp/fact_ledger/fact_exception_input_parts/PACK-001.json +``` + +schema: + +```json +{ + "schema_version": "stage1_fact_exception_input_part.v2", + "status": "READY", + "pack_id": "FL-PACK-001", + "run_fingerprint": "sha256:...", + "exception_type": "conflicting_fact_projection", + "input_exception_ids": [], + "items": [], + "output_path": "stage1_tmp/fact_ledger/fact_exception_adjudication_parts/PACK-001.json" +} +``` + +### 7.14 fan-out descriptor + +`dynamic_fanout[]` item은 다음 shape만 갖는다. + +```json +{ + "pack_id": "FL-PACK-001", + "exception_type": "conflicting_fact_projection", + "exception_ids": ["FX-001"], + "items": [], + "output_path": "stage1_tmp/fact_ledger/fact_exception_adjudication_parts/PACK-001.json", + "run_fingerprint": "sha256:..." +} +``` + +`items`는 해당 micro-pack의 compact exception만 포함한다. serialized descriptor도 25KB 이하여야 한다. full candidate나 source pack을 넣지 않는다. + +### 7.15 exception manifest + +`stage1_tmp/fact_ledger/fact_exception_manifest.json`: + +```json +{ + "fact_exception_manifest": { + "schema_version": "stage1_fact_exception_manifest.v2", + "status": "READY|READY_NO_EXCEPTIONS|READY_WITH_REVIEW", + "run_fingerprint": "sha256:...", + "expected_exception_ids": [], + "exception_count": 0, + "deterministic_review_items": [], + "pack_count": 0, + "packs": [ + { + "pack_id": "FL-PACK-001", + "exception_type": "conflicting_fact_projection", + "exception_ids": [], + "input_path": "stage1_tmp/fact_ledger/fact_exception_input_parts/PACK-001.json", + "output_path": "stage1_tmp/fact_ledger/fact_exception_adjudication_parts/PACK-001.json", + "serialized_bytes": 0 + } + ] + } +} +``` + +`packs[].exception_ids` union은 expected ID set과 정확히 같아야 한다. + +### 7.16 candidate bundle schema + +`stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json`: + +```json +{ + "fact_ledger_candidate_bundle": { + "schema_version": "stage1_fact_ledger_candidate_bundle.v2", + "status": "READY|READY_WITH_REVIEW|READY_WITH_EXCEPTIONS", + "run_fingerprint": "sha256:...", + "source_pack_ref": { + "path": "stage1_tmp/fact_ledger/fact_source_pack.json", + "schema_version": "stage1_fact_source_pack.v3", + "sha256": "sha256:..." + }, + "candidate_items": [], + "exception_manifest_ref": { + "path": "stage1_tmp/fact_ledger/fact_exception_manifest.json", + "schema_version": "stage1_fact_exception_manifest.v2", + "sha256": "sha256:...", + "expected_exception_ids": [] + }, + "compile_gate": { + "source_bo_count": 0, + "candidate_count": 0, + "source_bo_ids": [], + "candidate_bo_ids": [], + "missing_bo_ids": [], + "unknown_bo_ids": [], + "duplicate_bo_ids": [], + "bo_conservation_pass": true, + "candidate_schema_closed": true, + "reference_validation_pass": true + } + } +} +``` + +candidate bundle에 exception item 전체를 내장하지 않는다. + +### 7.17 FL1 stdout + +```json +{ + "fact_ledger_fl1_manifest": { + "schema_version": "stage1_fact_ledger_fl1_manifest.v2", + "status": "READY|READY_NO_EXCEPTIONS|READY_WITH_REVIEW|FAILED", + "candidate_path": "stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json", + "exception_manifest_path": "stage1_tmp/fact_ledger/fact_exception_manifest.json", + "run_fingerprint": "sha256:...", + "candidate_count": 0, + "exception_count": 0, + "review_count": 0, + "pack_count": 0 + }, + "dynamic_fanout": [] +} +``` + +true exception이 없으면 `dynamic_fanout`은 반드시 빈 배열이다. stdout은 1KB 이하를 목표로 한다. + +## 8. FL2 wildcard 작업 명세 + +### 8.1 task header + +```yaml +task_name: Task_FL2_fact_exception_adjudicator_* +llm_provider: google +llm_model: gemini-3.1-flash-lite +llm_reasoning: medium +llm_verbosity: low +max_iterations: 2 +max_concurrency: 2 +use_tools: +- localdocs +cache_control: + mode: auto + ttl: 1h +preflight: false +``` + +### 8.2 입력 경계 + +prompt에는 canonical common prefix 뒤에 다음 item field만 넣는다. + +```text +{{item.pack_id}} +{{item.exception_type}} +{{item.exception_ids}} +{{item.items}} +{{item.output_path}} +{{item.run_fingerprint}} +``` + +`{{prev.Task_FL1_deterministic_fact_ledger_candidate_builder}}` 전체를 넣지 않는다. FL2는 source pack, candidate file, BO, evidence, signals, structures를 읽거나 요청할 수 없다. + +### 8.3 LLM 권한 + +FL2는 다음만 할 수 있다. + +1. assigned item을 정확히 한 번씩 처리한다. +2. item의 `allowed_decisions` 중 하나를 선택한다. +3. `PATCH` 값은 item의 `candidate_values` 부분집합만 사용한다. +4. source ref는 item이 제공한 ref의 부분집합만 사용한다. +5. 불충분하면 `BLOCK_REVIEW`를 선택한다. +6. assigned output path 한 곳에 adjudication part를 쓴다. +7. write 후 compact manifest만 반환한다. + +다음을 할 수 없다. + +- 새 사실·날짜·금액·당사자·자산·ID 생성 +- Part 3 구조/type 재판정 +- legal theory 확정 +- 다른 pack/file read 또는 write +- final Fact Ledger write +- allowed output field 밖 patch + +### 8.4 decision schema + +```json +{ + "exception_id": "FX-001", + "exception_type": "conflicting_fact_projection|conflicting_calculation_projection|conflicting_asset_projection|conflicting_structure_role_projection", + "fact_id": "F-001", + "source_bo_id": "bh1", + "decision": "KEEP_AS_IS|PATCH|BLOCK_REVIEW", + "patch": {}, + "review_required": false, + "reason_code": "KEEP_EXISTING_SOURCE_PRIORITY|SELECT_SOURCE_BACKED_CANDIDATE|INSUFFICIENT_COMPACT_CONTEXT|UNSUPPORTED_PATCH", + "basis_codes_used": [], + "source_refs_used": [], + "conflict_codes_acknowledged": [] +} +``` + +규칙: + +- `KEEP_AS_IS`: patch는 빈 object, review false +- `PATCH`: patch key와 값은 input 허용 범위, review false +- `BLOCK_REVIEW`: patch는 빈 object, review true +- `SPLIT_FACT`는 허용하지 않음 + +### 8.5 adjudication part schema + +각 instance는 `{{item.output_path}}`에 다음을 쓴다. + +```json +{ + "schema_version": "stage1_fact_exception_adjudication_part.v2", + "status": "READY|READY_WITH_BLOCKS|FAILED", + "pack_id": "FL-PACK-001", + "run_fingerprint": "sha256:...", + "exception_type": "conflicting_fact_projection", + "input_exception_ids": [], + "decisions": [], + "coverage": { + "expected": 0, + "actual": 0, + "missing_exception_ids": [], + "unknown_exception_ids": [], + "duplicate_exception_ids": [], + "coverage_pass": true + } +} +``` + +하나라도 `BLOCK_REVIEW`면 `READY_WITH_BLOCKS`, 모두 해결되면 `READY`다. + +### 8.6 FL2 stdout + +write 성공 후 다음 raw JSON만 반환한다. + +```json +{ + "pack_id": "FL-PACK-001", + "status": "READY|READY_WITH_BLOCKS|FAILED", + "output_path": "stage1_tmp/fact_ledger/fact_exception_adjudication_parts/PACK-001.json", + "decision_count": 0 +} +``` + +markdown, code fence, 설명문을 반환하지 않는다. FL3는 stdout을 authoritative decision으로 쓰지 않고 output part를 읽는다. + +### 8.7 0-instance 경로 + +FL1 `dynamic_fanout`이 빈 배열이면 FL2 instance가 생성되지 않아야 한다. no-exception status를 만들기 위해 LLM을 호출하지 않는다. FL3가 exception manifest의 empty expected IDs와 `pack_count: 0`을 검증한다. + +## 9. FL3 작업 명세 + +### 9.1 task header + +```yaml +task_name: Task_FL3_final_fact_ledger_gate_and_writer +mcp: code-executor +tool_name: run_code +parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 +``` + +### 9.2 입력 + +정확히 다음 artifact를 읽는다. + +```text +stage1_tmp/fact_ledger/fact_source_pack.json +stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json +stage1_tmp/fact_ledger/fact_exception_manifest.json +stage1_tmp/fact_ledger/fact_exception_input_parts/PACK-*.json # pack 있을 때 +stage1_tmp/fact_ledger/fact_exception_adjudication_parts/PACK-*.json # pack 있을 때 +``` + +기존 `ADJUDICATION_RAW = {{prev...}}`와 단일 FL2 stdout parser를 삭제한다. + +### 9.3 artifact hash와 fingerprint + +1. source pack ref hash가 실제 source pack canonical hash와 같아야 한다. +2. candidate의 exception manifest ref hash가 실제 manifest hash와 같아야 한다. +3. 모든 artifact의 run fingerprint가 같아야 한다. +4. manifest pack path와 실제 input/output part path가 같아야 한다. +5. 누락 part를 review로 낮추지 않고 hard fail한다. + +### 9.4 exact exception coverage + +다음을 모두 검증한다. + +```text +expected IDs == input-part IDs union +expected IDs == decision IDs union +decision IDs and blocked IDs are represented exactly once +missing IDs == [] +unknown IDs == [] +duplicate IDs == [] +``` + +각 decision의 다음 값이 원 exception과 같아야 한다. + +```text +exception_id +exception_type +fact_id +source_bo_id +``` + +expected ID가 비었으면 pack/part/decision도 모두 비어야 한다. + +### 9.5 patch hard whitelist + +patch 적용 전 다음을 검증한다. + +```text +patch.keys subset of exception.allowed_output_fields +patch.keys subset of FINAL_FIELD_SET +patch value is one of source-backed candidate values +decision source refs subset of exception source refs +``` + +하나라도 실패하면 patch를 적용하지 않는다. warning 후 적용하지 말고 final write를 차단하거나 해당 decision을 `BLOCK_REVIEW`로 처리한다. schema/authority 위반은 원칙적으로 hard fail이다. + +### 9.6 candidate·BO conservation + +다음 set equality를 강제한다. + +```text +source_pack.reference_universe.bo_ids +== candidate.compile_gate.source_bo_ids +== final rows source_bo_id set +``` + +현재 1 BO -> 1 candidate 계약을 유지한다. duplicate source BO row, missing BO, unknown BO가 있으면 실패한다. + +`fact_id`는 final ordering에서 `F-001`부터 재확정하되 candidate ordering과 BO mapping을 바꾸지 않는다. + +### 9.7 reference gate + +각 final row에 대해 다음을 검증한다. + +- source BO ID가 BO universe에 존재 +- `linked_structures.structure_ids[]`가 structure universe의 부분집합 +- `evidence_refs[]`가 evidence universe의 부분집합 +- claim/liability/actio refs가 source pack compact structure/signal에 존재 + +unknown ref를 조용히 삭제하거나 fabricated replacement로 바꾸지 않는다. + +### 9.8 upstream review와 blocked 처리 + +FL1 deterministic review와 FL2 blocked decision을 writer report에 합친다. + +```text +blocked review 0, deterministic review 0 -> READY +review 존재, sealed ledger 작성 가능 -> READY_WITH_REVIEW +schema/coverage/hash/conservation 위반 -> FAILED, final 미작성 +``` + +Part 3 blocked exception이 있는 Fact에는 다음 drafting gate를 추가한다. + +```text +BLOCK_FINAL_DRAFTING_UPSTREAM_LES_REVIEW +``` + +notice receipt missing gate는 별도로 유지한다. + +### 9.9 deterministic domain gate 유지 + +최종 row code에서 다음을 집계한다. + +```text +POSSESSION_CAUSE_CONFLICT +LEGAL_CAUSE_STATE_UNKNOWN_REVIEW +NOTICE_RECEIPT_UNVERIFIED +NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW +LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW +LES_BLOCKED_EXCEPTION_REVIEW +``` + +possession conflict는 `HARD_WARNING`, notice receipt missing은 drafting block, upstream blocked LES는 drafting block으로 기록한다. + +### 9.10 writer report schema + +`stage1_tmp/fact_ledger/fact_ledger_writer_report.json`: + +```json +{ + "schema_version": "stage1_fact_ledger_writer_report.v2", + "status": "READY|READY_WITH_REVIEW|FAILED", + "run_fingerprint": "sha256:...", + "conservation_gate": { + "source_bo_count": 0, + "candidate_count": 0, + "final_count": 0, + "missing_bo_ids": [], + "unknown_bo_ids": [], + "duplicate_bo_ids": [], + "pass": true + }, + "exception_gate": { + "expected_count": 0, + "decision_count": 0, + "blocked_count": 0, + "missing_exception_ids": [], + "unknown_exception_ids": [], + "duplicate_exception_ids": [], + "coverage_pass": true + }, + "reference_gate": { + "unknown_bo_refs": [], + "unknown_structure_refs": [], + "unknown_evidence_refs": [], + "pass": true + }, + "upstream_les_review_gate": { + "review_count": 0, + "blocking_count": 0, + "affected_fact_ids": [], + "affected_bo_ids": [], + "status": "PASS|REVIEW|BLOCK_FINAL_DRAFTING" + }, + "blocked_review_items": [], + "row_warnings": [], + "possession_conflict_gate": { + "status": "PASS|HARD_WARNING", + "conflicts": [] + }, + "drafting_gates": [], + "gate_review_code_summary": {}, + "evidence_score_change_summary": [] +} +``` + +성공 report에 `patch_policy_warnings`를 남기고 위반 patch를 적용하는 기존 구조는 제거한다. 위반은 적용 0건과 failure/block으로 처리한다. + +### 9.11 final writer + +1. candidate와 decision을 메모리에서 결합한다. +2. 모든 hard gate를 실행한다. +3. 실패 시 `Fact_Ledger_base.json`을 쓰지 않는다. +4. 성공/검토 가능 상태에서 final array를 한 번 쓴다. +5. writer report를 한 번 쓴다. +6. 두 파일을 다시 읽어 schema/count/ref/status를 검증한다. +7. compact stdout manifest를 출력한다. + +stdout: + +```json +{ + "fact_ledger_final_writer": { + "schema_version": "stage1_fact_ledger_final_writer.v2", + "status": "READY|READY_WITH_REVIEW|FAILED", + "written_file": "Fact_Ledger_base.json", + "writer_report_file": "stage1_tmp/fact_ledger/fact_ledger_writer_report.json", + "run_fingerprint": "sha256:...", + "fact_count": 0, + "review_count": 0, + "blocked_count": 0, + "drafting_gate_count": 0 + } +} +``` + +## 10. 최종 task_procedure + +다음 mapping을 정확히 사용한다. + +```yaml +task_procedure: + IN: + nexts: + - Task_FL0_fact_source_pack_compiler + wait_until: [] + + Task_FL0_fact_source_pack_compiler: + nexts: + - Task_FL1_deterministic_fact_ledger_candidate_builder + wait_until: + - IN + + Task_FL1_deterministic_fact_ledger_candidate_builder: + nexts: + - Task_FL2_fact_exception_adjudicator_* + - Task_FL3_final_fact_ledger_gate_and_writer + wait_until: + - Task_FL0_fact_source_pack_compiler + + Task_FL2_fact_exception_adjudicator_*: + nexts: + - Task_FL3_final_fact_ledger_gate_and_writer + wait_until: + - Task_FL1_deterministic_fact_ledger_candidate_builder + + Task_FL3_final_fact_ledger_gate_and_writer: + nexts: + - OUT + wait_until: + - Task_FL1_deterministic_fact_ledger_candidate_builder + - all Task_FL2_fact_exception_adjudicator_* + + OUT: + nexts: [] + wait_until: + - Task_FL3_final_fact_ledger_gate_and_writer +``` + +최종 YAML에 `Task_FL2_single_fact_exception_adjudicator` 문자열은 0건이어야 한다. + +## 11. failure와 status 전파 + +### 11.1 FL0 + +- input missing/schema mismatch/BO duplicate/ref coverage failure: `FAILED` manifest 후 raise +- upstream review만 존재: `READY_WITH_REVIEW` +- READY/READY_WITH_REVIEW artifact만 FL1이 소비 + +### 11.2 FL1 + +- source pack hash/schema/fingerprint 불일치: `FAILED` 후 raise +- true exception 없음, review 없음: `READY_NO_EXCEPTIONS`, fan-out empty +- deterministic review만 있음: `READY_WITH_REVIEW`, fan-out empty +- true exception 있음: `READY`, fan-out 1..N +- budget/conservation 실패: `FAILED` 후 raise +- exception을 drop하고 성공하지 않음 + +### 11.3 FL2 + +- assigned exception을 정확히 한 번 결정 +- 부족하면 `BLOCK_REVIEW` +- output write/coverage 실패: `FAILED` +- 다른 pack/file 수정 금지 + +### 11.4 FL3 + +- missing part/hash mismatch/unknown decision/extra patch/schema failure: final 미작성 + raise +- FL2 `FAILED` part가 있으면 final 미작성 +- blocked review는 sealed ledger에 code로 보존하고 `READY_WITH_REVIEW` +- expected decision 누락은 review로 강등하지 않고 hard fail +- post-write reread 실패도 hard fail + +## 12. 구현 순서 + +### Phase 0. 원본 보존과 사전 검사 + +1. 원본 YAML을 target v3 경로로 복제한다. +2. 원본과 target의 초기 SHA-256을 기록한다. +3. 원본 YAML parse와 embedded Python compile을 수행한다. +4. 7월 10일 기준값을 기록한다. + +### Phase 1. header, prefix, DAG + +1. version을 v2.0으로 변경한다. +2. FL2 single task를 wildcard 이름으로 교체한다. +3. canonical prefix를 복제한다. +4. model/cache/concurrency를 설정한다. +5. task procedure를 제10절로 교체한다. + +### Phase 2. Python 공통 helper + +1. 세 Python task의 MCP boilerplate를 SKILL에 맞춘다. +2. outer/inner raw-decode parser를 넣는다. +3. canonical hash/fingerprint helper를 넣는다. +4. `sys.exit(0)`을 제거한다. +5. timestamp를 semantic artifact에서 제거한다. + +### Phase 3. FL0 + +1. read diet를 6개로 변경한다. +2. compact BO/structure/evidence/signal map을 구현한다. +3. upstream review-by-BO를 구현한다. +4. 중복·사장 필드를 제거한다. +5. source pack v3과 compact manifest를 작성한다. + +### Phase 4. FL1 + +1. global map lookup으로 candidate builder를 고친다. +2. derived-fact length trigger를 삭제한다. +3. true-exception 4중 조건과 deterministic review를 구현한다. +4. 기존 6개 broad exception trigger를 삭제한다. +5. micro-pack, manifest, candidate v2를 작성한다. +6. `dynamic_fanout` stdout을 구현한다. + +### Phase 5. FL2 + +1. prompt를 assigned pack 전용으로 축소한다. +2. closed decision과 output part schema를 구현한다. +3. localdocs write를 output path 1개로 제한한다. +4. no-exception 0-instance 경로를 확인한다. + +### Phase 6. FL3 + +1. 단일 stdout injection을 삭제한다. +2. source/candidate/manifest/parts를 file 기반으로 읽는다. +3. exact coverage와 patch whitelist를 구현한다. +4. upstream review·domain gate·drafting gate를 합친다. +5. final/report single write와 reread를 구현한다. + +### Phase 7. 검증 + +1. 정적 검사 +2. 7월 10일 골든 회귀 +3. fault injection +4. 2-run 결정론 +5. downstream smoke test +6. runtime/cost 측정 + +## 13. 정적 검증 명령과 판정 기준 + +### 13.1 YAML parse + +```bash +ruby -ryaml -e 'YAML.load_file(ARGV[0]); puts "YAML_OK"' \ + 'YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_4_Codex_v3.yml' +``` + +### 13.2 task명 + +```bash +ruby -ryaml -e ' +d=YAML.load_file(ARGV[0]); s=d["Agent"]["Stages"].first +names=s["tasks"].map{|t|t["task_name"]} +expected=[ +"Task_FL0_fact_source_pack_compiler", +"Task_FL1_deterministic_fact_ledger_candidate_builder", +"Task_FL2_fact_exception_adjudicator_*", +"Task_FL3_final_fact_ledger_gate_and_writer"] +abort("task names") unless names==expected +puts "TASKS_OK" +' 'YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_4_Codex_v3.yml' +``` + +모든 `nexts`/`wait_until` reference가 task definition, wildcard, `IN`, `OUT`, `all wildcard`에 대응해야 한다. + +### 13.3 Python compile + +Ruby로 세 `parameters.code`를 `/tmp`에 추출하여 `python3 -m py_compile`을 실행한다. 정확히 3개가 성공해야 한다. + +### 13.4 Python/SKILL 검사 + +```text +Python task count: 3 +{{__user_hash__}} count: 3 이상 +{{__workspace_hash__}} count: 3 이상 +raw_decode fallback: 각 Python task 1개 이상 +sys.exit(0): 0건 +FAILED 후 raise/non-zero: 3개 task 모두 +semantic created_at_utc: 0건 +placeholder/ellipsis: 0건 +``` + +### 13.5 common prefix + +FL2 wildcard prefix를 추출해 다음을 확인한다. + +```text +count: 1 +bytes: 3385 +sha256: 6e01f72f1803767b6bb16b5aef6cf34760e6dc1b293c316c75e990d09ad5fe63 +``` + +### 13.6 read diet + +FL0 input constant/read path에 다음이 각각 1회 이상 존재해야 한다. + +```text +BO.json +legal_effect_structures.json +actio_case_signals.json +case_liability_signals.json +legal_effect_signals.json +evidence_indexed.json +``` + +다음은 FL0 input constant/read path에서 0건이어야 한다. + +```text +client_goal.json +evidence_all.json +evidence_event_candidates.json +``` + +prompt의 금지 설명에 파일명이 등장하는 것은 read 위반으로 세지 않는다. + +### 13.7 writer audit + +```text +fact_source_pack.json writer: FL0 1개 +Fact_Ledger_base_candidate.json writer: FL1 1개 +fact_exception_manifest.json writer: FL1 1개 +input part writer: FL1 template 1개 +adjudication part writer: FL2 wildcard template 1개 +Fact_Ledger_base.json writer: FL3 1개 +fact_ledger_writer_report.json writer: FL3 1개 +``` + +### 13.8 forbidden old behavior + +최종 YAML에서 다음을 확인한다. + +```text +Task_FL2_single_fact_exception_adjudicator: 0건 +gemini-3.1-pro-preview in FL2: 0건 +ambiguous_legal_role automatic trigger: 0건 +weak derived length threshold: 0건 +SPLIT_FACT decision: 0건 +ADJUDICATION_RAW previous single output injection: 0건 +warning 후 out-of-scope patch 적용: 0건 +``` + +exception type 이름이 FL2 금지 설명 또는 migration comment에 등장하는 것은 허용하지 않는다. 최종 실행 code/prompt에서 old enum을 완전히 제거한다. + +### 13.9 secret와 무관 변경 + +- 기존 tools header 유지 +- 새 credential literal 0건 +- 원본과 target diff가 version, task_procedure, 네 task body/header에만 존재 +- 다른 파일 변경 0건 + +## 14. 7월 10일 골든 회귀 + +### 14.1 기준값 + +```text +BO: 50 +legal-effect structures: 50 +evidence authority: 30 +candidate Facts: 50 +기존 exception: 42 +기존 exception-linked Facts: 40 +기존 final Facts: 50 +기존 notice weak review code: 2 Facts +기존 Part 4 runtime: 3m 06s +기존 Part 4 cost: $0.58 +``` + +최종 `Fact_Ledger_base.json` baseline 크기는 128,855B다. 175,126B는 BO 파일 크기이므로 혼동하지 않는다. + +### 14.2 기대 결과 + +```text +candidate Facts: 50 +ambiguous role false exception: 0 +weak derived false exception: 0 +true exception: 0 expected +dynamic_fanout: [] +FL2 runtime instance: 0 +final Facts: 50 +BO/evidence/structure refs conserved +notice weak review code: 2 보존 +Part 3 review code/report: 강화 +``` + +### 14.3 의미 동등성 + +다음 필드는 기존 최종본과 비교한다. + +- source BO mapping +- date, parties, object, amount, action +- evidence refs +- legal roles와 linked structures +- claim/actio/downstream route +- derived fact +- calculation object + +다음 additive 변화는 허용한다. + +- `skeleton_validation_codes`의 Part 3 review code +- `derivation_basis`의 closed review basis +- evidence-key 교정에 근거한 credibility/proof-strength 변화 +- writer report v2의 새 gate + +source-backed 근거 없이 substantive Fact 값이 바뀌면 실패다. + +### 14.4 결정론 + +같은 fixture를 두 번 실행하여 다음을 비교한다. + +- source pack canonical hash +- candidate canonical hash +- exception manifest canonical hash +- final Fact Ledger bytes +- writer report semantic bytes +- stable IDs와 배열 순서 + +operational timing을 별도 기록할 경우 semantic hash에서 제외한다. + +## 15. fault injection + +최소 다음 fixture를 만든다. + +1. multi-role non-conflict: array 보존, FL2 0회 +2. multiple claim/liability refs: array 보존, FL2 0회 +3. short nonempty derived fact: 예외 0 +4. empty derived fact: deterministic fallback +5. fallback 불가: deterministic review +6. true scalar conflict: one micro-pack, FL2 1회 +7. pack item 12KB 초과: `PACK_CONTEXT_TOO_LARGE` review +8. pack 25KB 초과: deterministic shard split +9. pack 8개 초과: failure, no drop +10. extra decision ID: FL3 failure +11. missing decision ID: FL3 failure +12. duplicate decision ID: FL3 failure +13. decision fact/BO mismatch: FL3 failure +14. out-of-scope patch: patch 미적용 + failure +15. candidate value 밖 patch: failure +16. FL2 FAILED part: final 미작성 +17. missing BO candidate: conservation failure +18. unknown structure ref: failure +19. unknown evidence ref: failure +20. upstream blocked LES review: `READY_WITH_REVIEW` + drafting gate +21. notice receipt missing: drafting gate +22. possession/legal-cause conflict: hard warning +23. malformed read_docs extra data: raw_decode 성공 +24. malformed LLM part: fail-closed + +## 16. downstream smoke test + +1. `Fact_Ledger_base.json`이 top-level array인지 확인한다. +2. 모든 row가 정확히 27개 필드를 갖는지 확인한다. +3. Stage 2가 참조하는 기존 field 이름과 enum이 남아 있는지 확인한다. +4. BO, legal-effect structure, evidence ID join이 성공하는지 확인한다. +5. `client_goal.json`, BO, legal-effect structures, 3 signals의 Stage 1 handoff가 변경되지 않았는지 확인한다. +6. Stage 2가 source pack/candidate/writer report를 직접 읽는지 consumer map을 기록한다. +7. internal 파일 소비가 확인되기 전에는 파일명이나 경로를 제거하지 않는다. + +## 17. 성능 검증 + +### 17.1 측정 항목 + +- task별 wall time +- localdocs read/write bytes와 호출 수 +- FL2 instance 수 +- LLM input/output token과 비용 +- source pack 크기 +- candidate bundle 크기 +- FL1 stdout 크기 +- final 크기 + +### 17.2 합격 기준 + +| 지표 | baseline | 합격 기준 | +|---|---:|---:| +| false exception | 42 | 0 | +| 정상 FL2 instance | 1 | 0 | +| 정상 LLM 비용 | $0.58 포함 | $0 | +| source pack | 602,059B | 330KB 이하 | +| candidate bundle | 418,076B | 200KB 이하 | +| FL1 stdout | 약 156,444B | 1KB 이하 권고 | +| prefix 종류 | current Codex와 불일치 | Stage 1 전체 1종 | +| runtime | 3m 06s | no-exception p95 90초 이하 | +| out-of-scope patch | 적용 가능 | 적용 0 | +| final Fact count | 50 | 50 | + +### 17.3 P2 진입 조건 + +P0+P1 완료 후에도 no-exception p95가 90초를 넘거나 deterministic I/O가 runtime의 40% 이상이면 별도 작업으로 FL0+FL1 통합을 설계한다. 이번 v3 구현에 통합을 포함하지 않는다. + +## 18. 완료 정의 + +다음이 모두 참이어야 완료다. + +- [ ] 원본이 보존되고 target v3만 수정됨 +- [ ] YAML parse 성공 +- [ ] task definition 정확히 4개 +- [ ] Python task 정확히 3개 compile 성공 +- [ ] FL2 wildcard 이름과 DAG 정확 +- [ ] no-exception에서 FL2 instance 0개 +- [ ] canonical prefix bytes/hash 일치 +- [ ] FL2 flash-lite/medium/max 2 +- [ ] FL0 read diet 6개 +- [ ] source pack 중복·사장 필드 제거 +- [ ] evidence named key 교정 +- [ ] false exception trigger 삭제 +- [ ] true-exception 4중 조건 구현 +- [ ] exception manifest/parts 분리 +- [ ] stdout full pack 제거 +- [ ] FL3 exact coverage 구현 +- [ ] patch hard whitelist 구현 +- [ ] blocked/FAILED status fail-closed +- [ ] Part 3 review deterministic 승계 +- [ ] possession/notice/lien gate 유지 +- [ ] final 27-field contract 유지 +- [ ] `sys.exit(0)` 0건 +- [ ] raw-decode fallback 3개 task 모두 +- [ ] single writer audit 통과 +- [ ] 7월 10일 fixture 50 Fact 보존 +- [ ] fault injection 통과 +- [ ] two-run 결정론 통과 +- [ ] downstream smoke test 통과 +- [ ] 성능 기준 통과 또는 미달 원인 계측 완료 + +하나라도 거짓이면 완료로 보고하지 않는다. + +## 19. 구현자 최종 보고 형식 + +구현 완료 후 다음 순서로 보고한다. + +1. 생성한 YAML 절대경로 +2. SHA-256 +3. Agent version과 task 이름 4개 +4. task procedure 요약 +5. Python compile 결과 3개 +6. common prefix bytes/hash +7. FL0 read 파일 6개와 제거된 read 3개 +8. 7월 10일 candidate/final/exception/LLM instance 결과 +9. source pack/candidate/stdout 크기 +10. writer·coverage·patch·review gate 결과 +11. fault injection 결과 +12. two-run 결정론 결과 +13. downstream smoke test 결과 +14. runtime/cost와 baseline 대비 변화 +15. 미해결 위험 또는 성능 미달 + +“구현했다”, “최적화했다” 같은 서술만으로 끝내지 않는다. 수치, hash, gate, fixture 결과를 함께 제시한다. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_4_Inefficiency_Report_Claude_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_4_Inefficiency_Report_Claude_v1.md new file mode 100644 index 00000000..76c166e1 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_4_Inefficiency_Report_Claude_v1.md @@ -0,0 +1,110 @@ +# Stage 1 Part 4 비효율성 분석 보고서 (Claude v1) + +- 분석 대상: `YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_4_v2.yml` (2,027행, task 4개) +- 실행 실측: 런타임 3분 6초 / 비용 $0.58 (7/10 실행, `Results_July_10_3_15pm`) +- 참조 명세: Part 1 `Stage_1_Part_1_Claude_v3.yml`, Part 2 `Stage_1_Part_2_Claude_v3.yml`, Part 3 `Stage_1_Part_3_Claude_v2.yml`(개정 완료본) +- 작성 관점: 20년 이상 경력의 대한민국 최고 수준 변호사 + 세계적 수준의 AI 아키텍트 + +## 0. 분석 방법 — 실행물 기반 실측 + +Part 3 분석과 동일한 방법론을 적용했다. Part 4의 결정론 task(FL1·FL3) 전체 코드를 7/10 실행물 입력으로 로컬 재실행하여 실제 중간 산출물과의 일치를 먼저 확인했다: FL1이 생성한 `Fact_Ledger_base_candidate.json`의 candidate 50행과 exception pack 42건이 실제 실행물(418,076B)과 **완전 일치**로 재현되었다. 따라서 아래의 모든 수치는 실제 7/10 실행에서 일어난 일 그 자체이며, 특히 §2.2의 "LLM 무효과" 판정은 재현 환경에서의 대조 실험으로 증명된 것이다. + +## 1. Part 4 실행 구조와 데이터 흐름 실측 + +Part 4는 Part 3 v1과 동형의 완전 직렬 4-task 체인이다: `IN → FL0(source pack 컴파일, Python) → FL1(결정론 후보/예외 생성, Python) → FL2(예외 판정, LLM: **gemini-3.1-pro-preview/high/medium, max_iterations 2, 무조건 실행**) → FL3(최종 writer, Python) → OUT`. Stage 1 4개 Part 중 Part 1·2·3이 모두 "결정론 reducer → 조건부 경량 adjudicator → 결정론 writer" 문법으로 개정된 지금, Part 4만 유일하게 "상시 실행되는 고사양 LLM" 패턴이 남아 있으며, **Part 4 비용 $0.58의 전액이 FL2(pro-preview) 한 태스크에서 발생**한다. + +| 구간 | 채널 | 실측 크기 | +|---|---|---| +| FL0 입력 | localdocs read ×9 (client_goal, BO, **legal_effect_structures 255KB**, signal 3종, evidence_indexed, **evidence_all 87KB**, event_candidates) | 합계 약 640KB | +| FL0 → 파일 | `fact_source_pack.json` | **602,059B** (Stage 1 전체에서 가장 큰 중간 파일) | +| FL1 → 파일 | `Fact_Ledger_base_candidate.json` (후보 50행 + **exception pack 147.5KB 내장**) | 418,076B | +| FL1 → stdout | 요약 + **exception pack 전체 재출력** | **156,445B** | +| FL1 stdout → FL2 | `{{prev.…}}` 프롬프트 주입 | 156,445B (프롬프트 총계 ≈ 170KB: prefix 3.8KB + 정적 블록 9.7KB + 주입) | +| FL3 입력 | pack 602KB + candidate 418KB 파일 재읽기 + FL2 stdout 주입 | 1,020KB+ | +| FL3 → 파일 | `Fact_Ledger_base.json` 175,126B + writer report 452B | 175,578B | + +최종 산출물 175KB를 만들기 위해 중간 파일 1,020KB가 쓰이고 다시 읽히며, 156KB가 stdout·프롬프트로 이동한다. 그리고 §2.2가 보여주듯 이 흐름의 정점인 LLM 판정은 최종 산출물을 한 바이트도 바꾸지 못했다. + +## 2. 비효율 판정 항목 + +### 2.1 예외 과잉 생성 — Part 3에서 이미 반증된 원리의 재현 + +**P4-1. 정상적 다중 라우팅을 예외로 오인 (42건/50행 = 84%).** 7/10 실행에서 FL1은 예외 42건을 생성했다: `ambiguous_legal_role` 40건 + `weak_derived_fact` 2건. `ambiguous_legal_role`의 트리거는 "`legal_effect_roles` 3개 이상 AND `downstream_module_candidates` 2개 이상"인데, 후보 50행의 분포를 실측하면 (roles>2, modules>1)이 40행, (False, True)가 10행 — 즉 **구조 연계가 정상적으로 풍부한 행일수록 기계적으로 예외가 된다**. 하나의 사실이 대여금 청구·사해행위·담보관계 모듈에 동시에 라우팅되는 것은 Part 3 개정에서 확정한 원리대로 대체적·예비적 청구를 보전하는 **정상 상태**이지 판정 대상이 아니다. Part 3(LES1)의 type 예외 과잉 생성과 정확히 동형의 질병이다. + +**P4-2. 예외 항목의 페이로드 비대.** 예외 1건의 크기가 중앙값 3,368B, 최대 6,410B다(42건 합계 147.5KB). 각 항목이 `related_bo_compact` 전체, `related_structure_snippets` 최대 8개, `related_signal_hints`, `related_evidence_excerpts` 최대 8개를 통째로 내장하기 때문인데, §2.4에서 보듯 이 "증거 발췌"는 실제로는 전부 비어 있다. + +### 2.2 LLM 추론 비효율 — 판정이 산출물을 바꾸지 못했다 (핵심 실측) + +**P4-3. FL2의 실질 효과 0 — 대조 실험으로 증명.** 실제 실행물의 candidate bundle과 exception pack을 입력으로, **42건 전부를 `KEEP_AS_IS`로 판정한 가상 adjudication을 FL3에 투입**해 최종 `Fact_Ledger_base.json`을 재생성한 결과, 실제 실행의 최종본과 **50행 완전 동일**했다. writer report 실측도 이를 뒷받침한다: blocked 0, patch 정책 경고 0, 미적용 patch 0, row 경고 0. 즉 pro-preview/high가 170KB 프롬프트로 42건을 판정한 결과는 전량 KEEP_AS_IS(또는 기존 값과 동일한 patch)였고, **Part 4 비용 $0.58 전액과 런타임의 LLM 구간이 최종 산출물 기여 0에 소비되었다**. Part 3 LES2(93% BLOCK, 효과 0)에 이어, Stage 1에서 "결정론이 만든 가짜 예외를 고사양 LLM에게 보내는" 패턴이 두 번째로 반증된 것이다. + +**P4-4. 무조건 실행 + 최고 사양 모델.** FL2는 예외 유무와 무관하게 항상 실행되며, Stage 1 조건부 판정 태스크 중 유일하게 **pro-preview + reasoning high + verbosity medium + max_iterations 2**다(Part 1 GB·Part 2 R1·Part 3 L1은 모두 flash-lite/low/low/1 + 조기 종료). 판정 작업의 실체는 enum 선택(PATCH/KEEP_AS_IS/SPLIT_FACT/BLOCK_REVIEW)이므로 이 사양은 어떤 기준으로도 과잉이고, 실측상 그 판정의 부가가치는 0이었다. + +**P4-5. 예외 데이터의 3중 물질화.** 동일한 exception pack 147.5KB가 (1) candidate bundle 파일 내장, (2) FL1 stdout 전체 재출력(156KB), (3) FL2 프롬프트 주입으로 세 번 이동한다. Part 1/2/3 개정이 확립한 "pack 단독 파일 + preflight" 채널이면 한 번이면 된다. + +### 2.3 토큰·전송 비경제 + +**P4-6. fact_source_pack 602KB의 내부 중복과 사장 필드.** pack 구성의 실측과 소비 추적: + +| pack 필드 | compact 크기 | 소비 실측 | 판정 | +|---|---:|---|---| +| `bo_fact_inputs` | 126.3KB | FL1 사용 | 유지하되 내부 중복 제거 | +| `legal_effect_structures_compact` | 125.8KB | `structure_by_id`만 FL1·FL3 사용. **내장 5-way `structure_index`는 다운스트림 소비 0** (FL0 자신이 메모리에서 by_bo_id를 쓴 뒤 통짜 재수록) | index 재수록 삭제 | +| `bo_by_id` | 45.0KB | FL1 스캔 3종 + FL3 사용 — 단 `bo_fact_inputs[].bo_compact`와 **완전 중복** | 한쪽만 유지 | +| `signal_hints_by_bo_id` | 35.5KB | **소비 0** (`bo_fact_inputs[].signal_hints`와 중복) | 삭제 | +| `evidence_authority_by_index` | 7.6KB | FL3가 key 집합만 사용 (`bo_fact_inputs[].evidence_authority`와 중복) | key 목록으로 축소 | +| `event_candidates_by_evidence_index` | 2.8KB | **소비 0** — `bo_fact_inputs[].event_candidates`도 FL1 참조 0회 | **양쪽 모두 삭제** | +| `client_goal_compact` | 1.5KB | FL1·FL3 소비 0 | 삭제 | + +즉 602KB 중 실제로 하류가 소비하는 정보는 후보행 구성에 필요한 ~170KB 안팎이며, 나머지는 중복(bo/signal/evidence의 global+per-BO 이중 수록)과 사장 필드(이벤트 2계열·client_goal·index 재수록)다. + +**P4-7. 입력 read의 낭비.** `evidence_all.json`(87KB)은 authority 빌더에서 setdefault 보충 역할만 하는데, 포함/제외 재실행 비교 결과 **30개 evidence 전부에서 산출 완전 동일**(기여 0 — Part 3 P3-3과 동일 실측). `evidence_event_candidates.json`은 위 표대로 산출이 전혀 소비되지 않고, `client_goal.json`의 compact도 소비 0이다. 9개 read 중 3개가 불필요하다. + +**P4-8. FL3의 이중 대형 재읽기.** FL3는 pack 602KB와 candidate 418KB를 모두 재읽지만, pack에서 실제로 쓰는 것은 ID 집합 3종(ref 검증용)과 bo_by_id의 notice 필드뿐이다. slim pack이면 이 재읽기 자체가 수십 KB로 준다. + +### 2.4 품질 결함 (효율과 별개로 교정 필요) + +**P4-9. 증거 발췌가 전부 비어 있다.** FL0의 excerpt 수집 키(`excerpt/summary/content/text/body`)가 `evidence_indexed.json`의 실제 필드(`key_facts`, `key_dates`, `key_amounts`)와 일치하지 않아, **evidence authority 30건 전부 excerpt=None**이다. 그 결과 (a) `_score_evidence`의 credibility/proof_strength 판정이 제목 문자열에만 의존하고, (b) FL2에 제공된 `related_evidence_excerpts`가 사실상 빈 껍데기였다 — FL2가 42건을 판정할 실질 근거 자체가 프롬프트에 없었던 셈이며, 이는 전량 KEEP_AS_IS 결과의 한 원인으로 추정된다. + +**P4-10. 변별 신호의 포화.** 최종 50행 전부 `must_consider=true`, 전부 `legal_centrality="critical"`이다(구조 연계가 있으면 무조건 critical/must_consider로 산정). Part 3 개정 전의 review 50/50 포화와 동형으로, Stage 2에 전달되는 우선순위 신호의 변별력이 0이다. + +**P4-11. SKILL 정합 결함 3종.** (a) 실패 시 `sys.exit(0)` — FL0/FL1/FL3 모두 실패를 정상 종료로 위장. (b) read_docs **envelope 계층**의 strict `json.loads`(3 task 모두 — inner content에는 raw_decode가 있으나 envelope에는 없음). (c) `datetime.now()` 타임스탬프 3곳(FL0/FL1/FL3 산출물) — 동일 입력 2-run 바이트 결정론이 구조적으로 불가능해 골든 회귀 검증력을 약화시킨다. 또한 FL3의 FL2-stdout 파서는 코드펜스 tail에서 실패해 max_iterations 2 재호출을 유발하는 구조다(Part 3 P3-15와 동일). + +### 2.5 유지해야 할 강점 (실측 확인) + +FL2의 `COMMON_CACHE_PREFIX_STAGE_1`은 canonical(58f2b72d, Part 2 R1·Part 3 L1과 바이트 동일)이며, FL0의 stdout은 이미 1줄 요약이다(Part 3 LES0의 257KB stdout 같은 결함 없음). FL3의 닫힌 스키마 검증(FINAL_FIELDS 27종 강제·임의 필드 거부), patch의 allowed_fields 정책, coverage 검증, possession/notice/lien 결정론 게이트 3종(도메인 법리가 코드화된 자산), FL2 프롬프트의 "LES2 모호성 재판정 금지" 경계는 견실하므로 개정에서 그대로 보존한다. + +## 3. 효율화 방안 — 최소 작업 / 최대 효과 + +Part 1·2·3 개정에서 세 번 검증된 동일 문법의 이식이다. 최종 산출물 `Fact_Ledger_base.json`(FINAL_FIELDS 27종 스키마·행 순서·`F-###` 규칙)과 writer report는 Stage 2 소비 계약이므로 불변으로 고정한다. + +**R-1. task 통합: 4 → 3 (FL0+FL1 → F0').** FL0과 FL1을 단일 결정론 reducer로 병합하고 fact_source_pack을 물질화하지 않는다(602KB 파일·중복 필드 전체 소멸). F0'의 산출은 (a) slim candidate bundle 파일(후보 50행 + FL3 검증용 ID 집합 — exception pack 내장 제거), (b) exception pack 단독 파일(조건부 LLM용), (c) stdout 1줄 요약뿐이다. 입력 read는 9 → 6으로 줄인다(`evidence_all`·`evidence_event_candidates`·`client_goal` 제거 — 기여 0/소비 0 실측). + +**R-2. 예외 생성 교정 — LLM 실질 0회화.** `ambiguous_legal_role`(다중 role/module)은 예외 생성 자체를 폐지하고 정상 다중 라우팅으로 보존한다(Part 3 X-2 확립 원리; KEEP_AS_IS 전량 실증이 무손실 보증). 잔여 유형(weak_derived_fact, legal_calculation_required, 투영 미완 계열)은 Part 2 X-3의 4중 조건(단일 값 선택 필수 + source-backed 근거 2+ + 보존·defer 불가 + compact 판정 충분)을 통과할 때만 pack에 수록하고, 불충족분은 `skeleton_validation_codes`의 결정론 review로 남긴다. 7/10 실측 기준 pack 수록 0건이 예상 결과다. 예외 항목의 compact 계약(항목당 ≤2KB, 빈 발췌 금지)도 함께 강제한다. + +**R-3. FL2 → F1' 조건부 어드주디케이터.** Part 1 GB·Part 2 R1·Part 3 L1과 동일 골격: `gemini-3.1-flash-lite/low/low/max_iterations 1`, `preflight_files: [exception pack]`, `has_exceptions=false`면 no-exception 판정 파일을 쓰고 즉시 종료. 프리픽스는 현행 canonical 블록 바이트 유지. **비용 $0.58 → $0, LLM 구간 런타임 소거.** BLOCK_REVIEW 안전판과 "LES 모호성 재판정 금지" 경계는 현행 문구를 이식한다. + +**R-4. F2'(최종 writer) 정비.** candidate 파일 + exception pack 파일 + 판정 파일의 3-파일 read로 일원화(602KB pack 재읽기 소멸 — ref 검증용 ID 집합은 slim bundle에 수록), §6.7 관용 파서 적용, patch 적용·게이트 3종·닫힌 스키마 검증은 현행 이식. + +**R-5. 품질 교정 동반.** (a) excerpt 수집 키를 `key_facts`/`key_dates`/`key_amounts`로 교정해 증거 근거를 실질화(P4-9 — credibility 판정과 잔여 예외 판정 근거의 회복), (b) `must_consider`/`legal_centrality` 산정에 변별 기준 도입은 Stage 2 소비 계약 영향이 있으므로 필드·의미는 유지하되 변별 신호를 `skeleton_validation_codes`로 보강하는 additive 방식으로 한정, (c) SKILL 정합 일괄(실패 raise, envelope raw_decode, 타임스탬프 제거 또는 고정 — 2-run 결정론 확보). + +**검증 계획(개정 시).** 7/10 골든 회귀: F0'→(F1')→F2' 재실행 → `Fact_Ledger_base.json` 50행이 현행 최종본과 의미 동등(KEEP_AS_IS 실증 덕분에 사실상 바이트 동등 기대 — 타임스탬프 제거 반영), writer report 게이트 결과 동일, 주입 테스트(진짜 계산 근거 결핍 주입 → pack 수록·F1' 발동 / 판정 누락 → coverage hard fail / 코드펜스 → salvage), 2-run 바이트 결정론, Stage 2 소비 필드 무변경. + +## 4. 기대 효과 정량 요약 + +| 지표 | 현행 (실측) | 개정 후 (추정) | +|---|---|---| +| task 수 | 4 (직렬) | 3 (F1'는 실질 0회 조기 종료) | +| LLM | pro-preview/high 무조건 1회, 프롬프트 ≈170KB, 판정 42건 → **효과 0 실증** | flash-lite/low 조건부 (fixture 0회) | +| 비용 | $0.58 (전액 FL2) | ≈ $0 | +| 중간 파일 | 1,020KB (pack 602 + candidate 418) | ≈ 150KB (slim bundle + pack) | +| stdout | 157KB (FL1 예외팩 재출력 포함) | ≈ 1KB | +| 입력 read | 9파일 (기여 0인 evidence_all 87KB 포함) | 6파일 | +| 런타임 | 3분 6초 | 1분 내외 예상 (LLM 구간 소거 + IO 1/4) | +| 최종 산출 | 175KB, must_consider 50/50 포화 | 스키마·행 불변 (KEEP_AS_IS 실증), 변별 신호 additive 보강 | + +## 5. 잔여 리스크와 열린 질문 + +첫째, "LLM 실질 0회"의 근거는 7/10 사건 1건의 실측이므로, F1'를 제거하지 않고 조건부로 남기는 설계가 안전하다 — 진짜 계산 근거 결핍(legal_calculation_required)이 실재하는 사건에서는 F1'가 발동해야 하며, 그 경우를 위해 R-5(a)의 excerpt 실질화가 선행되어야 판정 근거가 프롬프트에 실린다. 둘째, must_consider 포화는 Stage 2가 이 신호를 어떻게 소비하는지 확인 후 손대야 하므로 additive 보강으로 한정했다. 셋째, Part 4 개정이 완료되면 Stage 1 전체가 "결정론 reducer → 조건부 flash-lite 판정 → 결정론 writer" 단일 문법으로 통일되며, 이때 flash-lite 4형제(GB·R1·L1·F1')의 프리픽스 바이트 통일(Part 3 개선보고서 D-2: Part 1 GB의 3,829B 정렬)을 함께 마무리하는 것이 캐시 관점의 마지막 퍼즐이다. + +--- +*본 보고서의 수치는 전량 7/10 실행물과 로컬 재현(FL1 candidate/예외 완전 일치 재현, KEEP_AS_IS 대조 실험, evidence_all 포함/제외 실험)에서 실측되었다. 다음 단계는 Codex 보고서와의 비교를 거친 개정 전략 수립이다.* diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_4_Inefficiency_Report_Codex_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_4_Inefficiency_Report_Codex_v1.md new file mode 100644 index 00000000..a72964e7 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Part_4_Inefficiency_Report_Codex_v1.md @@ -0,0 +1,587 @@ +# Stage 1 Part 4 비효율성 및 최적화 보고서 + +## 1. 평가 목적과 범위 + +이 보고서는 다음 네 작업명세서의 현재 연결 계약을 전제로 `Stage_1_Part_4_v2.yml`의 비효율성과 품질 위험을 평가한다. + +1. `Stage_1_Part_1_Codex_v3.yml` +2. `Stage_1_Part_2_Codex_v3.yml` +3. `Stage_1_Part_3_Codex_v2.yml` +4. `Stage_1_Part_4_v2.yml` + +실측 근거는 `Results_July_10_3_15pm`에 보존된 실행 결과다. 다만 이 결과는 개정 전 Part 1 v2, Part 2 v2, Part 3 v1, Part 4 v2를 실행한 결과이므로, 개정된 Part 1~3의 성능을 직접 측정한 결과는 아니다. 따라서 아래에서는 다음을 구분한다. + +- **실측 판정**: 7월 10일 결과 파일에서 직접 확인한 사실 +- **정적 판정**: 현재 Codex v3/v3/v2와 Part 4 v2의 YAML 계약을 비교해 확인한 사실 +- **예상 효과**: Part 4 개정 후 재실행으로 검증해야 하는 목표 + +법률적 평가는 사실·증거·법률효과 후보를 보존하면서 Stage 1이 최종 법률판단을 선취하지 않아야 한다는 원칙에 따랐다. 아키텍처 평가는 결정적 연산을 Python에 두고, LLM은 구조화된 자료만으로도 복수의 합리적 투영이 남는 진정한 예외에만 사용하는 원칙에 따랐다. + +## 2. 최종 판정 + +**Part 4의 가장 큰 병목은 FL2의 모델 자체보다 FL1이 정상적인 다중값을 대량의 LLM 예외로 오분류하고, FL2를 예외 0건에도 반드시 실행하도록 직렬 연결한 구조다.** + +7월 10일 실행에서 다음이 확인되었다. + +- 50개 Fact 후보 중 40개가 예외 대상이 되었다. +- 총 예외는 42건이었다. +- 그중 40건은 `ambiguous_legal_role`, 2건은 `weak_derived_fact`였다. +- FL2 입력의 `exception_pack`은 minified 기준 146,998바이트였다. +- FL2는 `gemini-3.1-pro-preview`, `reasoning: high`, `max_iterations: 2`로 실행되었다. +- FL2 실행 후 최종 Fact Ledger는 후보본과 **변경 행 0개, 변경 필드 0개**였다. +- Part 4 전체 실행시간은 3분 6초, 비용은 $0.58이었다. + +즉, 이 실행에서 FL2의 한계효용은 0이었고, 최종 결과를 만들기 위한 작업은 사실상 전부 결정적이었다. 더구나 Part 3의 `review_required`와 `quality_gate.review_queue`는 Part 4가 읽고도 Fact Ledger 검토 코드로 충분히 승계하지 않는다. 비용은 정상적인 다중 역할을 재판단하는 데 쓰이고, 정작 upstream 검토 상태는 약하게 전달되는 품질·효율 역전이 발생한다. + +가장 적은 개정으로 가장 큰 효과를 얻는 권고안은 다음과 같다. + +1. FL1의 단순 cardinality 기반 예외를 제거한다. +2. FL2를 단일 필수 task에서 **조건부 wildcard task**로 바꾼다. +3. 정상 경로의 FL2 실행 횟수를 0으로 만든다. +4. 진정한 투영 충돌만 작은 shard로 보내고 `gemini-3.1-flash-lite/medium`을 사용한다. +5. FL0의 source pack은 값을 한 번만 저장하고 BO별 입력은 ID 참조만 갖게 한다. +6. FL3의 보존·참조·patch·upstream review 게이트는 강화한다. + +## 3. 현재 Part 4 실행 구조 + +### 3.1 현재 DAG + +```text +IN + | + v +FL0 fact_source_pack_compiler [Python] + | - 9개 JSON 읽기 + | - 602 KB source pack 작성 + v +FL1 deterministic_fact_ledger_candidate [Python] + | - 50개 후보 작성 + | - 42개 예외 작성 + | - 418 KB candidate bundle 작성 + v +FL2 single_fact_exception_adjudicator [LLM, 필수 1회] + | - exception_pack 전체를 한 번에 입력 + | - Gemini Pro/high + v +FL3 final_fact_ledger_gate_and_writer [Python] + | - source pack + candidate + LLM 결과 재결합 + | - final ledger와 writer report 작성 + v +OUT +``` + +모든 작업이 직렬이다. `exception_count == 0`이어도 FL2를 건너뛸 DAG 경로가 없다. + +### 3.2 작업별 현재 역할 + +| 작업 | 실행유형 | 핵심 역할 | 현재 판정 | +|---|---|---|---| +| FL0 | Python | 9개 파일 검증·축약·join, source pack 작성 | 필요하나 과대 포장 | +| FL1 | Python | BO별 Fact 후보 및 예외 생성, possession/notice/lien gate | 핵심 작업, 예외 규칙 개정 필요 | +| FL2 | LLM | 전체 exception pack 일괄 판정 | 현재 구조의 주 병목 | +| FL3 | Python | patch 적용, 참조 검증, 최종 파일 작성 | 반드시 유지, fail-closed 강화 필요 | + +## 4. 실측 입력·중간 산출물 분석 + +### 4.1 주요 파일 크기와 개수 + +| 파일 | 크기 | 주요 개수 | +|---|---:|---:| +| `BO.json` | 175,126 B | BO 50 | +| `legal_effect_structures.json` | 254,991 B | structure 50, review queue 78 | +| `evidence_indexed.json` | 91,841 B | evidence 30 | +| `evidence_event_candidates.json` | 49,327 B | item 29 | +| 3 signals 합계 | 44,648 B | 5 + 50 + 50 records | +| `client_goal.json` | 6,029 B | 1 object | +| `fact_source_pack.json` | 602,059 B | BO input 50 | +| `Fact_Ledger_base_candidate.json` | 418,076 B | 후보 50, 예외 42 | +| `Fact_Ledger_base.json` | 128,855 B | Fact 50 | +| `fact_ledger_writer_report.json` | 452 B | 경고·차단 0, notice weak review 2 | + +보존된 입력 8개만 합쳐도 약 622 KB다. FL0는 이를 602 KB짜리 source pack으로 다시 작성하고, FL1은 418 KB짜리 candidate bundle을 추가 작성한다. 최종 파일은 129 KB이므로, 최종 결과에 이르기 전에 그 몇 배의 중간 JSON을 직렬로 읽고 쓴다. + +### 4.2 재현성 결함 + +FL0의 `INPUT_FILES`는 `evidence_all.json`을 필수 입력으로 선언한다. 그러나 `Results_July_10_3_15pm`에는 해당 파일이 없다. 실행 당시에는 존재했으나 결과 묶음에서 누락되었을 수 있지만, 보존 결과만으로 Part 4를 재현할 수 없다는 점은 분명하다. + +실제 `fact_source_pack.json`의 30개 evidence authority는 모두 `evidence_indexed.json` 출처였다. 따라서 현재 계약에서는 다음 중 하나를 택해야 한다. + +- `evidence_indexed.json`을 authoritative source로 정하고 `evidence_all.json` 의존성을 제거한다. +- 정말 fallback이 필요하면 optional input으로 바꾸고, 부재를 manifest에 기록한다. + +필수 입력으로 유지하면서 실행 결과 묶음에는 보존하지 않는 현재 방식은 피해야 한다. + +## 5. FL0 비효율성 + +### 5.1 동일 정보의 중복 저장 + +`fact_source_pack.json`의 minified 구성요소를 분석하면 다음 중복이 확인된다. + +| 데이터 | 전역 map | `bo_fact_inputs[]` 내 반복 | 판정 | +|---|---:|---:|---| +| BO compact | 48,376 B | 47,984 B | 거의 완전 중복 | +| signal hints | 34,708 B | 34,316 B | 거의 완전 중복 | +| evidence authority | 7,358 B | 16,227 B | BO별 반복 | +| event candidates | 2,466 B | 5,503 B | BO별 반복, FL1 직접 사용 안 함 | + +또한 `legal_effect_structures_compact.structure_index`는 99,113바이트이며, 세부 구성은 다음과 같다. + +| 인덱스 | 크기 | FL1/FL3 소비 여부 | +|---|---:|---| +| `by_bo_id` | 33,856 B | 사용 | +| `by_issue_cluster_id` | 27,070 B | source pack 이후 미사용 | +| `by_asset_cluster_id` | 16,746 B | source pack 이후 미사용 | +| `by_evidence_index` | 14,816 B | source pack 이후 미사용 | +| `by_candidate_structure_type` | 6,514 B | source pack 이후 미사용 | + +FL0에서 다섯 인덱스를 모두 검증하는 것은 필요하다. 그러나 검증 후 FL1/FL3가 쓰지 않는 네 인덱스를 source pack에 다시 복제할 필요는 없다. + +### 5.2 쓰지만 소비하지 않는 정보 + +다음 필드는 source pack에 작성되지만 후속 작업이 실질적으로 소비하지 않는다. + +- `client_goal_compact` +- `event_candidates_by_evidence_index` +- `bo_fact_inputs[].event_candidates` +- `structure_index` 중 `by_bo_id` 외 네 인덱스 +- `signal_hints_by_bo_id`와 `bo_fact_inputs[].signal_hints` 중 한쪽 사본 +- `bo_by_id`와 `bo_fact_inputs[].bo_compact` 중 한쪽 사본 + +`client_goal.json`은 Stage 1 handoff 파일로 별도 보존하면 된다. Fact Ledger 투영을 위해 FL0 source pack 안에 다시 넣을 필요는 없다. + +### 5.3 I/O 호출 수 + +현재 deterministic task의 localdocs tool 호출은 다음과 같다. + +- FL0: 9 read + 1 write + 1 post-write reread = 11회 +- FL1: 1 read + 1 write + 1 post-write reread = 3회 +- FL3: 2 read + 2 write + 1 post-write reread = 5회 +- 합계: 19회, MCP session 3개 + +post-write reread는 무결성 검증 가치가 있으므로 우선 삭제 대상이 아니다. 먼저 데이터 중복과 FL2 호출을 제거하고, 여전히 Python 구간이 병목일 때 FL0+FL1 병합을 2차 개정으로 검토하는 것이 변경 위험이 가장 낮다. + +## 6. FL1 예외 분리의 구조적 문제 + +### 6.1 예외가 예외가 아닌 상태 + +실행 결과에서 50개 Fact 중 40개, 즉 80%가 하나 이상의 예외를 가졌다. 예외 처리율이 이 수준이면 exception path가 아니라 사실상의 main path다. + +| 예외유형 | 건수 | FL1 trigger | 실질 판정 | +|---|---:|---|---| +| `ambiguous_legal_role` | 40 | role > 2 및 module > 1 | false positive | +| `weak_derived_fact` | 2 | 문자열 길이 < 12 | 한국어에 부적절한 heuristic | +| 기타 4종 | 0 | 누락·투영 실패 조건 | 이번 실행에서는 미발화 | + +`ambiguous_legal_role` 40건의 대부분은 역할 3개, module 2개였다. 최종 스키마의 `legal_effect_roles[]`, `downstream_module_candidates[]`, `must_not_drop_in_claim_types[]`는 원래 복수값을 보존하도록 설계되어 있다. 따라서 복수값의 존재 자체는 모순도, 투영 실패도 아니다. + +법률적으로도 Stage 1에서 병합적·예비적·선택적 청구 가능성과 복수 법률효과 후보를 보존하는 것이 안전하다. 단지 후보가 여러 개라는 이유로 하나를 선택하게 하면 청구구성이나 공격방어방법을 조기에 축소할 위험이 있다. + +### 6.2 `weak_derived_fact`의 언어 편향 + +두 예외의 실제 `derived_fact`는 다음과 같다. + +- `물품대금 지급 최고` 10자 +- `목적물 특정` 6자 + +두 표현 모두 짧지만 법률상 의미를 가진 사실 표제다. 문자열 길이는 사실의 법률적 완결성이나 증거충실도를 대변하지 않는다. 이 조건은 다음처럼 바꾸는 것이 맞다. + +1. `derived_fact`가 비어 있으면 BO의 action/object/date를 이용해 결정적으로 재작성한다. +2. 재작성 후에도 비어 있거나 핵심 source ref가 없으면 review code를 부여한다. +3. 길이만으로 LLM 예외를 만들지 않는다. + +### 6.3 예외별 read diet가 지나치게 넓음 + +각 exception item은 유형과 무관하게 다음을 반복 포함한다. + +- BO compact 전체 +- 연관 structure 최대 8개 +- issue/asset/evidence/type snippets +- 모든 signal hints +- evidence excerpts 최대 8개 + +그러나 `ambiguous_legal_role`은 역할·module·structure type의 충돌 여부만 보면 되고, `weak_derived_fact`는 BO core와 제한된 source excerpt만 필요하다. 모든 유형에 같은 context envelope을 사용한 결과, 예외 42건의 pack이 minified 146,998바이트로 후보 50개의 본문 95,258바이트보다 1.54배 커졌다. + +## 7. FL2 비효율성과 품질 위험 + +### 7.1 필수 단일 호출 + +현재 FL2는 예외 유무와 관계없이 직렬로 한 번 실행된다. prompt는 다음 규모다. + +- 전체 static prompt: 13,528 B, 215 lines +- common cache prefix: 3,829 B +- task overlay: 9,699 B +- dynamic exception pack: 146,998 B(minified 기준) + +예외가 없는 사건에서도 13.5 KB static prompt를 Pro/high 모델에 보내고 `READY_NO_EXCEPTIONS`를 생성해야 FL3가 실행된다. 이는 Python으로 100% 처리할 수 있는 분기다. + +### 7.2 모델 과대 사양 + +현재 FL2 권한은 법률구조를 새로 판단하거나 raw evidence를 읽는 것이 아니라, 이미 결정된 Part 3 구조를 Fact Ledger 필드에 투영하는 것이다. 이 범위에 `gemini-3.1-pro-preview/high`는 과대 사양이다. + +복잡한 법리가 필요한 항목은 FL2가 해결할 항목이 아니라 `BLOCK_REVIEW` 또는 downstream legal-theory 작업으로 보내야 한다. 진정한 구조 투영 충돌만 남긴 뒤 `gemini-3.1-flash-lite/medium`으로 처리하는 편이 권한 경계와 비용 양쪽에서 우월하다. + +### 7.3 실측 한계효용 0 + +FL2 전후를 비교한 결과는 다음과 같다. + +- 변경된 Fact 행: 0 +- 변경된 필드: 0 +- blocked review: 0 +- patch policy warning: 0 +- unapplied patch: 0 + +42건 전부에 LLM 추론을 사용하고 후보와 완전히 동일한 최종 파일을 얻었다. 이 실행에 한해서는 FL2를 건너뛰어도 결과가 동일하다. + +### 7.4 cache prefix 불일치 + +개정된 Part 1~3의 모든 LLM task는 동일한 prefix를 사용한다. + +- bytes: 3,385 +- SHA-256: `6e01f72f1803767b6bb16b5aef6cf34760e6dc1b293c316c75e990d09ad5fe63` + +Part 4 v2의 FL2는 별도 prefix를 사용한다. + +- bytes: 3,829 +- SHA-256: `58f2b72d6c2650739c45d85cc57899150434db86a97bf19cd5a59d92d11dfbb5` + +따라서 Stage 1 전체 common cache prefix가 깨진다. Part 4 개정 시 Part 1~3의 3,385바이트 prefix를 바이트 단위로 동일하게 사용해야 한다. + +## 8. 놓치고 있는 upstream 검토 상태 + +7월 10일 `legal_effect_structures.json`은 다음 상태였다. + +- root status: `READY_WITH_REVIEW` +- structure 50개 전부 `review_required: true` +- quality gate review queue 78개 +- `structure_review_required` 50개 +- `blocked_exception` 28개 + +FL0는 각 structure의 `review_required`를 compact 구조에 보존한다. 그러나 FL1은 이를 exception trigger, `skeleton_validation_codes`, `must_consider` 또는 writer report로 승계하지 않는다. FL3도 Part 3의 `quality_gate.review_queue`를 검증하거나 보고하지 않는다. + +현재 Part 3 Codex v2의 review queue는 `source_bo_ids`, `review_code`, `downstream_owner`, blocked exception 정보 등을 구조적으로 제공하도록 개정되어 있다. Part 4는 이를 다시 LLM에 판단시키지 말고 다음과 같이 결정적으로 승계해야 한다. + +- 관련 Fact에 `LES_REVIEW_REQUIRED` 또는 더 구체적인 review code 부여 +- `must_consider: true` +- writer report에 affected `fact_id`와 `source_bo_id` 기록 +- blocked exception은 최종 writer status를 `READY`로 광고하지 않도록 gate 설정 +- legal-theory 항목은 Stage 2 owner로 전달하고 FL2가 재심하지 않음 + +이는 LLM 호출을 늘리지 않으면서 품질을 높이는 개정이다. + +## 9. FL3의 안전성 결함 + +FL3는 반드시 유지해야 하지만 다음 계약은 강화해야 한다. + +### 9.1 coverage 검사가 exact하지 않음 + +현재 검사는 `expected ⊆ decided ∪ blocked`만 확인한다. 따라서 다음을 차단하지 못한다. + +- 예상하지 않은 extra exception ID +- 동일 exception의 중복 decision +- decision과 blocked에 동시에 들어간 ID +- exception 0건인데 임의 decision이 들어온 경우 +- `input_exception_count`, decision count와 실제 배열의 불일치 + +### 9.2 허용되지 않은 patch도 실제 적용됨 + +현재 `_apply_patch`는 patch field가 `allowed_output_fields` 밖이면 warning을 남기지만, 그 뒤에도 `row[key] = value`를 실행한다. 즉, 경계 위반 patch가 경고와 함께 최종 파일에 반영된다. + +수정 원칙은 명확하다. + +- `patch.keys ⊆ allowed_output_fields ∩ FINAL_FIELD_SET`가 아니면 해당 decision을 거부한다. +- out-of-scope patch는 적용하지 않고 `BLOCK_REVIEW` 또는 gate failure로 보낸다. +- warning 후 적용은 금지한다. + +### 9.3 blocked 상태에서도 READY를 출력할 수 있음 + +FL3의 stdout status는 blocked review가 있어도 일률적으로 `READY`다. 이는 downstream이 미해결 검토를 놓칠 수 있다. + +최소 변경안은 최종 ledger를 sealed artifact로 계속 작성하되 다음을 강제하는 것이다. + +- blocked 0건: `READY` +- blocked 또는 upstream blocking review 존재: `READY_WITH_REVIEW` +- final drafting이 허용되지 않는 경우 `BLOCK_FINAL_DRAFTING` gate 기록 +- schema·coverage·보존 위반: final overwrite 금지, `FAILED` + +## 10. 작업별 삭제·축소 안전성 판정 + +| 대상 | 판정 | 근거 | +|---|---|---| +| FL0 전체 삭제 | 금지 | provenance join과 upstream contract 검증 필요 | +| FL0 source pack 중복값 | 삭제 권고 | 전역 map과 BO별 사본이 중복 | +| `client_goal_compact` | source pack에서 삭제 권고 | FL1/FL3 미사용, 원본은 별도 handoff | +| `evidence_all.json` 필수 의존 | 삭제 또는 optional 권고 | 결과 묶음에 부재, 실제 authority는 indexed 30개 | +| 미사용 4개 structure index의 재직렬화 | 삭제 권고 | FL0에서 검증만 하고 source pack에는 미보존 가능 | +| FL1 전체 삭제 | 금지 | Fact row 작성과 deterministic safety gate의 핵심 | +| cardinality 기반 role/claim 예외 | 삭제 권고 | 배열형 최종 스키마와 충돌, false positive | +| 문자열 길이 기반 weak fact 예외 | 삭제 권고 | 한국어 의미 완결성과 무관 | +| possession/notice/lien scan | 유지 | Stage 1 sealed 품질 게이트 | +| FL2 단일 필수 task | 삭제 권고 | 조건부 wildcard로 대체 | +| FL2 기능 전체 | 조건부 유지 | 진정한 evidence-grounded projection conflict만 처리 | +| FL3 전체 삭제 | 금지 | sole writer·참조·보존·차단 gate 필요 | +| source pack/candidate 파일 자체 | 즉시 삭제 보류 | downstream·감사 소비 여부 확인 후 결정 | +| `Fact_Ledger_base.json` schema | 유지 | Stage 2 핵심 handoff 계약 | + +중간 파일의 물리적 삭제는 Stage 2와 운영 감사 도구의 소비 여부를 별도로 검색한 뒤 결정해야 한다. 이번 1차 개정에서는 파일명은 유지하되 내부 payload를 축소하는 편이 안전하다. + +## 11. 권고 개정안: 최소 변경, 최대 효과 + +### 11.1 개정 우선순위 + +#### P0: 즉시 적용 + +1. FL2를 `Task_FL2_exception_adjudicator_*` 조건부 wildcard로 변경한다. +2. FL1에서 true exception이 0건이면 FL3가 직접 실행되게 한다. +3. `ambiguous_legal_role`의 cardinality-only trigger를 제거한다. +4. `weak_derived_fact`의 문자열 길이 trigger를 제거한다. +5. FL2 모델을 `gemini-3.1-flash-lite`, reasoning `medium`으로 낮춘다. +6. Part 1~3과 동일한 common cache prefix를 사용한다. +7. FL3 patch whitelist와 exact coverage gate를 수정한다. +8. Part 3 review queue를 deterministic하게 Fact/보고서에 승계한다. + +#### P1: 같은 개정에서 적용 권고 + +1. FL0 source pack의 전역 map/BO별 값 중복을 제거한다. +2. BO별 pack은 ID와 ref만 갖게 한다. +3. `evidence_all.json`을 제거하거나 optional fallback으로 바꾼다. +4. 예외 유형별 최소 context schema를 사용한다. +5. FL2 shard 크기와 항목 수에 상한을 둔다. + +#### P2: 재측정 후 적용 + +1. FL0와 FL1을 하나의 deterministic compiler로 병합한다. +2. source pack을 writer가 읽지 않도록 candidate bundle에 최소 validation manifest를 넣는다. +3. 내부 중간 파일의 물리적 생성을 optional debug mode로 바꾼다. + +P2는 추가 성능을 얻을 수 있지만 수정 범위와 회귀 위험이 커진다. 현재 병목의 대부분은 P0의 FL2 정상 경로 제거로 해결할 수 있으므로 1차 개정에는 포함하지 않아도 된다. + +### 11.2 개정 DAG + +```text + true exception 1..N + +-----------------------+ + | v +IN -> FL0_slim -> FL1_candidate_and_exception_planner -> FL2_exception_* + | | + | true exception = 0 | + +------------------+-----------------+ + v + FL3_exact_gate_writer + | + v + OUT +``` + +권장 `task_procedure` 의미는 다음과 같다. + +```text +FL0 -> FL1 +FL1 -> FL2_* and FL3 +FL2_* -> FL3 +FL3 waits for FL1 and all FL2_* +``` + +exception shard가 없으면 wildcard 인스턴스는 0개이고 FL3가 바로 실행된다. 이 패턴은 현재 개정된 Part 1~3의 conditional adjudicator 패턴과 일치한다. + +## 12. FL0 개정 명세 + +### 12.1 입력 축소 + +필수 입력을 다음으로 제한한다. + +- `BO.json` +- `legal_effect_structures.json` +- `actio_case_signals.json` +- `case_liability_signals.json` +- `legal_effect_signals.json` +- `evidence_indexed.json` +- `evidence_event_candidates.json`: provenance/event 보조가 필요한 경우만 + +`client_goal.json`은 Fact Ledger source pack에서 제외한다. `evidence_all.json`은 제거하거나 optional fallback으로 둔다. + +### 12.2 정규화된 단일 저장 + +권장 source pack shape는 다음과 같다. + +```json +{ + "fact_source_pack": { + "schema_version": "stage1_fact_source_pack.v3", + "status": "READY|READY_WITH_REVIEW|FAILED", + "bo_by_id": {}, + "structure_by_id": {}, + "structure_refs_by_bo_id": {}, + "signal_hints_by_bo_id": {}, + "evidence_authority_by_index": {}, + "event_hints_by_evidence_index": {}, + "upstream_review_by_bo_id": {}, + "source_integrity": {}, + "source_counts": {} + } +} +``` + +`bo_fact_inputs[]`는 제거한다. FL1은 `bo_by_id`의 sorted key를 순회하면서 다른 map을 ID로 조회한다. 같은 BO, signal, evidence, event를 각 Fact input 안에 재복제하지 않는다. + +### 12.3 유지해야 할 검증 + +- BO ID set과 `structure_index.by_bo_id` key의 관계 +- 모든 structure source BO의 존재 +- 모든 linked structure ID의 존재 +- 모든 evidence ref의 존재 +- Part 3 root schema/status +- 다섯 structure index의 schema와 내부 참조 +- Part 3 quality gate coverage + +다섯 인덱스는 메모리에서 검증하되 FL1이 쓰는 `by_bo_id`와 최종 ref set만 source pack에 보존한다. + +## 13. FL1 개정 명세 + +### 13.1 예외 라우팅 3분법 + +FL1은 모든 문제를 LLM exception으로 만들지 말고 다음으로 분리한다. + +| 경로 | 성질 | 처리 | +|---|---|---| +| deterministic pass | 정상 복수값, 완전한 구조 투영 | 후보에 그대로 보존 | +| deterministic review | 누락 사실, legal theory 필요, upstream review | code/gate 부여 후 downstream 전달 | +| true LLM exception | 동일 필드에 상충하는 2개 이상 근거가 있고 Stage 1 투영 선택이 필요 | 작은 shard로 FL2 전달 | + +### 13.2 유형별 처리 + +| 기존 유형 | 개정 처리 | +|---|---| +| `ambiguous_legal_role` | role/module 개수만으로 발화 금지. 명시적 상호배타 conflict flag가 있을 때만 true exception | +| `ambiguous_claim_chain` | 여러 claim/liability ID를 배열에 보존. scalar 선택이 필요한 필드가 있을 때만 예외 | +| `weak_derived_fact` | 길이 기준 삭제. 빈 값은 deterministic template 보충, 실패하면 review | +| `legal_calculation_required` | 누락값을 LLM이 창조하지 않음. source에 복수의 명시 후보가 있을 때만 투영 예외, 그 외 downstream calculation review | +| `unresolved_asset_cluster_projection` | unique asset cluster는 deterministic. Part 3 자체가 blocked이면 review 승계 | +| `unresolved_structure_role_projection` | Part 3 type/role map으로 deterministic. 재법리 판단 필요 시 Stage 2 review | + +### 13.3 exception shard 상한 + +- shard당 최대 4개 exception +- minified dynamic payload 권고 상한 25 KB +- 유형별 allowed fields만 포함 +- 동일 BO/structure/evidence context는 shard-local dictionary에 한 번만 저장 +- 상한 초과는 더 작은 shard로 분할 +- 분할 불가 또는 context 부족은 LLM에 보내지 않고 `BLOCK_REVIEW` + +## 14. FL2 개정 명세 + +### 14.1 실행 조건 + +다음 조건을 모두 만족할 때만 wildcard 인스턴스를 만든다. + +1. true exception이 1개 이상이다. +2. exception이 Stage 1 projection authority 안에 있다. +3. 해결에 필요한 모든 후보와 provenance가 compact shard에 있다. +4. legal theory 확정이나 raw evidence 재독해가 필요하지 않다. + +### 14.2 모델과 prompt + +- provider: `google` +- model: `gemini-3.1-flash-lite` +- reasoning: `medium` +- max_iterations: 2 +- tools: none +- common prefix: Part 1~3과 바이트 동일 +- task overlay: exception schema, authority, output contract만 유지 + +Part 4의 투영 범위에서는 Pro/high escalation을 두지 않는 것이 원칙이다. Flash-lite가 안전하게 해결하지 못하는 항목은 법률난도가 높아서가 아니라 authority/context가 부족한 것이므로 `BLOCK_REVIEW`가 맞다. + +### 14.3 출력 최소화 + +- input exception ID당 정확히 1개 decision +- `KEEP_AS_IS`, `PATCH`, `BLOCK_REVIEW`만 유지 +- 실제 autosplit을 하지 않으므로 `SPLIT_FACT` 삭제 +- rationale은 짧은 reason code만 허용 +- patch는 allowed field만 포함 + +## 15. FL3 개정 명세 + +### 15.1 필수 hard gate + +1. `expected_exception_ids == decision_ids ∪ blocked_ids` +2. `decision_ids ∩ blocked_ids == ∅` +3. ID 중복과 extra ID 0건 +4. decision의 fact/BO/type가 원 exception과 정확히 일치 +5. patch key는 allowed fields와 final schema의 교집합에 포함 +6. candidate BO ID set과 upstream BO ID set의 보존 +7. fact ID uniqueness와 결정적 ordering +8. evidence/structure ref가 authoritative set의 부분집합 +9. Part 3 blocked/review 승계 +10. possession/notice/lien sealed gate 유지 + +### 15.2 상태 규칙 + +| 조건 | writer status | 최종 파일 | +|---|---|---| +| 모든 gate 통과, block 없음 | `READY` | 작성 | +| 검토 항목은 있으나 ledger 봉인 가능 | `READY_WITH_REVIEW` | 작성 + drafting gate | +| schema/coverage/conservation/patch 위반 | `FAILED` | overwrite 금지 | + +### 15.3 보존해야 할 현재 기능 + +- `POSSESSION_CAUSE_CONFLICT` +- `LEGAL_CAUSE_STATE_UNKNOWN_REVIEW` +- `NOTICE_RECEIPT_UNVERIFIED` +- `NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW` +- `LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW` +- `BLOCK_FINAL_DRAFTING_NOTICE_RECEIPT_MISSING` + +이 기능들은 결정적이고 법률상 중요한 Stage 1 sealed gate이므로 경량화 대상이 아니다. + +## 16. 기대 효과와 검증 기준 + +### 16.1 7월 10일 fixture에 대한 기대 결과 + +개정 규칙을 같은 결과물에 적용하면 다음이 기대된다. + +- 40건 `ambiguous_legal_role`: cardinality false positive로 제거 +- 2건 `weak_derived_fact`: 빈 값이 아니므로 제거 +- true LLM exception: 0건 +- FL2 실행: 0회 +- 최종 Fact Ledger: 기존 결과와 의미상 동일 +- Part 3 review 상태: 기존보다 명확하게 writer report와 Fact code에 승계 + +### 16.2 정량 합격 기준 + +| 지표 | 현재 | 1차 목표 | +|---|---:|---:| +| 정상 사건 FL2 호출 | 1 | 0 | +| 7월 10일 false exception | 42 | 0 | +| FL2 모델 | Pro/high | Flash-lite/medium | +| source pack | 602,059 B | 330 KB 이하 권고 | +| candidate bundle | 418,076 B | 200 KB 이하 권고 | +| common prefix 종류 | Stage 1 내 2종 | 1종 | +| out-of-scope patch 적용 | warning 후 적용 가능 | 0, hard reject | +| Part 4 LLM 비용 | $0.58 포함 | no-exception 경로 $0 | +| Part 4 runtime | 3분 6초 | no-exception p95 90초 이하 권고 | + +크기·시간 목표는 실제 Liti-agent 재실행으로 확정해야 한다. + +### 16.3 회귀 검증 fixture + +최소 다음 fixture를 자동화해야 한다. + +1. no-exception fixture: FL2 0회, final 작성 +2. multi-role non-conflict fixture: 배열 보존, LLM 미호출 +3. genuine projection conflict fixture: FL2 shard 1개, exact patch +4. empty derived fact fixture: deterministic fallback 또는 review +5. upstream LES review fixture: Fact code와 writer report 승계 +6. blocked LES exception fixture: `READY_WITH_REVIEW`와 drafting gate +7. malicious extra decision fixture: FL3 실패 +8. out-of-scope patch fixture: patch 미적용 및 실패/차단 +9. BO conservation fixture: expected BO와 final source BO set equality +10. unknown evidence/structure ref fixture: fail-closed +11. notice received-date missing fixture: drafting block 유지 +12. possession/legal-cause conflict fixture: hard warning 유지 + +## 17. 최종 권고 + +Part 4는 전면 재설계할 필요가 없다. FL0·FL1·FL3의 deterministic 골격과 최종 `Fact_Ledger_base.json` 계약은 유지할 가치가 있다. 핵심은 다음 한 문장으로 요약된다. + +> **정상적인 복수 역할과 짧은 한국어 사실표제를 예외에서 제거하고, 진정한 상충 투영만 조건부 Flash-lite shard로 보내며, Part 3 review와 FL3 hard gate를 결정적으로 강화한다.** + +우선 P0만 적용해도 7월 10일 사례에서는 FL2를 완전히 건너뛰면서 현재 최종 파일을 그대로 재현할 수 있다. 그 뒤 source pack 단일 저장과 유형별 compact context를 적용하면 I/O와 토큰량을 추가로 줄일 수 있다. FL0+FL1 병합이나 중간 파일 제거는 재측정 후에 결정해야 하며, Stage 2·감사 도구 소비 확인 전에는 공개 handoff 파일명과 최종 Fact Ledger schema를 바꾸지 않는 것이 안전하다. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Prompt_for_Improvement_Strategy_for_Part_1.txt b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Prompt_for_Improvement_Strategy_for_Part_1.txt new file mode 100644 index 00000000..1ea9ad3f --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Prompt_for_Improvement_Strategy_for_Part_1.txt @@ -0,0 +1,177 @@ + +┌──────────────────────────┐ +│Stage 1 -- Part 1 │ +│Improvement Strategy │ +│Claud vs Codex │ +│--> Perform & Compare │ +└──────────────────────────┘ + + +Stage 1 Part 1 4개 작업 명세서 yaml 파일을 분석한 두 개 문서 , 를 비교 분석하여 Stage 1 개정 작업에 가장 적합한 전략 도출 + + + +, 는 모두 에 제시된 프롬프트를 사용하여 Stage 1 4개 yaml 파일들의 에이전트 작업 효율성을 분석한 문서들이다. + + + +에서 읽고 숙지해야 할 section 및 sub-section들: + 2. 현재 Part 1 실행 구조 + 3. 실측 fan-in과 중복량 + 4. 실제 결과에서 확인된 품질 결함 + 5. 두 gate의 작업별 필요성 판정 + - 5.1 B1 gate + - 5.2 B2 gate + 6. Downstream 연계성과 삭제 안전성 + 7. 권고 개정안: 최소 변경, 최대 효과 + - 7.1 설계 원칙 + - 7.2 기존 task를 최대한 재사용하는 개정 + 8. 개정 DAG + 9. 모델 경량화 판단 + 10. 즉시 적용할 핫픽스 + 11. 삭제·유지 최종 판정 + 13. 최종 권고 + + +에서 읽고 숙지해야 할 section 및 sub-section들: + 2. 두 게이트의 실행 방식 (질문 6에 대한 답) + 3. 질문 7에 대한 답: "모아서 한 번에 넘기는" 것이 런타임을 증폭시키는가? + 4. 10개 검증 항목의 성격 분류 — 무엇이 정말 LLM을 필요로 하는가 + 5. 실행 결과가 실증한 품질 결함 — LLM-writer 구조의 직접 비용 + 6. 삭제·축소 안전성 판정 (질문 5에 대한 답 — 하류 연계 근거) + - 6.1 절대 보존해야 할 인터페이스 (하류 소비 실증) + - 6.2 삭제해도 안전한 작업 (근거 포함) + - 6.3 삭제하면 안 되는 것 + 7. 개정 전략 + - 7.1 권장안 A — 게이트 분해: "결정적 통합기 + 조건부 국소 판정" (Part 3/4 검증 패턴 이식) + - 7.3 이행 순서 및 검증 계획 + - 7.2 대안 B — 구조 유지 최소 개정 (권장하지 않음, 비교용): skip reading this subsection and do not consider it for any valuable information + + + +1. 에 제시된 내용을 충실하게 준수하여, 두 개 마크다운 문서에서 필요 section & sub-section들을 읽고 숙지한다. +2. 1번 작업에서 구축한 내용에 기반을 두고, 두 개 markdown 문서를 비교 분석하여 Stage 1 작업의 효율성을 증대시키는 데 가장 필요한 작업들을 선별한다. 선별 기준은 최소의 노력(LLM 추론 작업은 꼭 필요한 곳에만 필요한 수준으로 사용, 100% deterministic task는 python code화, 토큰 경제성을 최대화)으로 최대의 효과(최고 품질 유지, 토큰 경제성 증대, 에이전트 속도 최대화)를 이룩하는 것이다. +3. 2번 작업 결과물을 로 생성하고 저장한다. + + +================================================================================================================= + +┌──────────────────────────┐ +│Stage 1 -- Part 1 │ +│Improvement Strategy Doc │ +└──────────────────────────┘ + + +Stage 1 Part 1 yaml 작업 명세서 를 개정하는 데 사용할 개정 작업 명세서 작성 + + + + 내용으로부터 기존 Stage 1 Part 1 yaml 작업 명세서인 를 개정할 전략서를 작성한다. + +전략서는 "20년 이상 경력을 지닌 대한민국 최고 수준의 변호사 및 세계적인 수준의 인공지능 아키텍트 기술자의 관점"에서 LLM이 읽고 정확하게 이해한 후, 100% 작업을 수행할 수 있는 작업 명세서로 작성한다. + +작성한 작업 명세서는 로 생성하고 저장한다. + + + +================================================================================================================= + +┌──────────────────────────┐ +│Stage 1 -- Part 1 │ +│Update YAML to v3 │ +└──────────────────────────┘ + + +기존 Stage 1 Part 1 작업명세서 를 개정하여 v3 yaml 파일을 작성한다. + + + +1. 를 읽고, 개정 작업 과정을 숙지한다. +2. 를 복제하여 를 생성한다. +3. 에 1번 작업에서 숙지한 개정 작업과정을 정확하게 적용하여 v3.yml을 작성한다. +4. 3번 작업에서 작성한 v3.yml이 1번 작업의 개정 작업 과정을 100% 동일하게 수행했는지, 기존 v2.yml 작업 명세의 작업 목표를 최고 수준에서 달성하는지 두 가지를 "20년 이상 경력을 지닌 대한민국 최고 수준의 변호사 및 세계적인 수준의 인공지능 아키텍트 기술자의 관점"에서 엄격하게 평가한다. +5. 4번 과정의 평가를 통과할 때까지 v3.yml을 재작성한다. +6. 평가를 통과한 v3.yml을 overwrite하여 저장한다. + + +================================================================================================================= + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Prompt_for_Inefficiency_Report_Part_2.txt b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Prompt_for_Inefficiency_Report_Part_2.txt new file mode 100644 index 00000000..f78676c5 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Prompt_for_Inefficiency_Report_Part_2.txt @@ -0,0 +1,227 @@ +Prompt for Improving Stage 1 Part 2 + +--------------------------------------------------------------------------------------------------------- + +Stage 1 4개 yaml 작업 명세서 4개 +1) +2) +3) +4) +를 Liti-agent를 통해 순차적으로 실행하였다. + +순차 실행 후 얻은 결과물은 폴더에 저장되어 있다. + +에이전트가 stage 1 4개 yaml 파일들을 실행했을 때의 런타임(run-time)과 cost는 아래와 같다: +{{ +* Stage 1 Part 1 +런타임: 8min 45sec +비용: $2.09 + +* Stage 1 Part 2 +런타임: 7min 40sec +비용: $4.28 + +* Stage 1 Part 3 +런타임: 3min 40sec +비용: $0 + +* Stage 1 Part 4 +런타임: 3min 6sec +비용: $0.58 +}} + +Stage 1 yaml 개정 전 작업 명세 +- - - - +을 실행했을 때보다 런타임은 더 길어졌고, 비용도 더 상승했다. + +그래서 Stage 1 yaml 작업 명세서 중 Part 2를 지금보다도 훨씬 더 최적화(필요 작업만 남기고 군더더기 작업들을 없애는 경량화 / 최고 품질의 추론이 필요 없을 때는 약간 낮은 성능의 LLM을 사용하여 추론 시간 단축 등)하려고 한다. + +Stage 1 Part 1은 이미 최적화 작업을 마치고 yaml 작업 명세를 업데이트하였다. 업데이트한 Part 1 작업 명세서는 이며, 이 yaml 파일은 의 개정 작업 명세서를 반영하여 작성된 것이다. + + + +Stage 1 Part 2 작업의 비효율성을 검토하고, 작업 효율성을 최적화하는 개정 방안(혹은 전략)을 작성한다. + + + +0. 에 제시된 v.6 폴더의 Stage 1 작업 명세서 4개를 모두 읽고, 세부 작업 내용을 숙지한다. +1. 0번 작업 시 Part 1 작업 명세서는 Stage_1_Part_1_Claude_v3.yml를 읽는다. 즉, +1) +2) +3) +4) +를 읽고 Stage 1 전체 작업 명세를 숙지한다. +2. 에 제시된 폴더에 있는 Stage 1 실행 결과물들을 파악한다. +3. Stage 1 Part 2 yaml 파일()에 제시된 작업 명세를 정확하고 심도 깊게 이해하고, +4. Part 2 작업들의 비효율성(토큰 비경제성, LLM 추론 작업 비효율성, DAG workflow 비효율성 등)을 파악한다. +5. 4번 작업에서 파악한 Part 2 작업 명세의 비효율성을 효율화 할 수 있는 방안을 작성한다. +6. 1~5까지의 작업을 마친 후 결과 보고서를 로 생성하고 저장한다. + + +========================================================================================================= + +┌──────────────────────────┐ +│Stage 1 -- Part 2 │ +│Improvement Strategy │ +│Claud vs Codex │ +│--> Ensemble │ +└──────────────────────────┘ + + +Stage 1 Part 2 작업 명세서 yaml 파일을 분석한 두 개 문서 , 를 비교 분석하여 Stage 1 Part 2 개정 작업에 가장 적합한 전략 도출 + + + +와 를 다음 항목들에 집중하여 비교 분석한다: + - Part 2 비효율 판정 항목들 + - Part 2 실행구조와 병목 발생 지점 + - 삭제/축소 안정성 판정(downstream 작업 연계 근거) + - 권장 개정 전략 + + + +1. 작업을 통해 구축한 비교 분석 내용에 근거하여, +2. Stage 2 작업의 효율성을 증대시키는 데 가장 필요한 작업들을 선별한다. 선별 기준은 최소의 노력(LLM 추론 작업은 꼭 필요한 곳에만 필요한 수준으로 사용, 100% deterministic task는 python code화, 토큰 경제성을 최대화)으로 최대의 효과(최고 품질 유지, 토큰 경제성 증대, 에이전트 속도 최대화)를 이룩하는 것이다. +3. 2번 작업 결과물을 로 생성하고 저장한다. + + + + +================================================================================================================= + +┌──────────────────────────┐ +│Stage 1 -- Part 2 │ +│Improvement Strategy Doc │ +└──────────────────────────┘ + +기존 Stage 1 Part 2 yaml 작업 명세서 를 개정하는 데 사용할 "개정 작업 명세서" 작성 + + + + 내용으로부터 기존 Stage 1 Part 2 yaml 작업 명세서인 를 개정할 전략서를 작성한다. + +전략서는 "20년 이상 경력을 지닌 대한민국 최고 수준의 변호사 및 세계적인 수준의 인공지능 아키텍트 기술자의 관점"에서 LLM이 읽고 정확하게 이해한 후, 100% 작업을 수행할 수 있는 작업 명세서로 작성한다. + +작성한 작업 명세서는 로 생성하고 저장한다. + + + +================================================================================================================= + +┌──────────────────────────┐ +│Stage 1 -- Part 2 │ +│Update YAML to v3 │ +└──────────────────────────┘ + + +기존 Stage 1 Part 2 작업명세서 를 개정하여 v3 yaml 파일을 작성한다. + + + +1. 를 읽고, 개정 작업 과정을 숙지한다. +2. 를 복제하여 를 생성한다. +3. 에 1번 작업에서 숙지한 개정 작업과정을 정확하게 적용하여 v3.yml을 작성한다. 작성 시, LLM 추론 작업 task는 모두 동일한 common cache prefix를 사용해야 하고, python code 작업은 가 제시하는 python code 작성 제약을 100% 준수하도록 한다. +4. 3번 작업에서 작성한 v3.yml이 1번 작업의 개정 작업 과정을 100% 동일하게 수행했는지, 기존 v2.yml 작업 명세의 작업 목표를 최고 수준에서 달성하는지, LLM 추론 작업에서 동일한 common cache prefix를 사용하는지, python code 작업 시 가 제시하는 python code 작성 제약을 100% 준수하는지 네 가지를 "20년 이상 경력을 지닌 대한민국 최고 수준의 변호사 및 세계적인 수준의 인공지능 아키텍트 기술자의 관점"에서 엄격하게 평가한다. +5. 4번 과정의 평가를 통과할 때까지 v3.yml을 재작성한다. +6. 평가를 통과한 v3.yml을 overwrite하여 저장한다. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Prompt_for_Inefficiency_Report_Part_3.txt b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Prompt_for_Inefficiency_Report_Part_3.txt new file mode 100644 index 00000000..de150e9a --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Prompt_for_Inefficiency_Report_Part_3.txt @@ -0,0 +1,182 @@ + +--------------------------------------------------------------------------------------------------------- + +Stage 1 4개 yaml 작업 명세서 4개 +1) +2) +3) +4) +를 Liti-agent를 통해 순차적으로 실행하였다. + +순차 실행 후 얻은 결과물은 폴더에 저장되어 있다. + +에이전트가 stage 1 4개 yaml 파일들을 실행했을 때의 런타임(run-time)과 cost는 아래와 같다: +{{ +* Stage 1 Part 1 +런타임: 8min 45sec +비용: $2.09 + +* Stage 1 Part 2 +런타임: 7min 40sec +비용: $4.28 + +* Stage 1 Part 3 +런타임: 3min 40sec +비용: $0 + +* Stage 1 Part 4 +런타임: 3min 6sec +비용: $0.58 +}} + +Stage 1 yaml 개정 전 작업 명세 +- - - - +을 실행했을 때보다 런타임은 더 길어졌고, 비용도 더 상승했다. + +그래서 Stage 1 yaml 작업 명세서 중 Part 2를 지금보다도 훨씬 더 최적화(필요 작업만 남기고 군더더기 작업들을 없애는 경량화 / 최고 품질의 추론이 필요 없을 때는 약간 낮은 성능의 LLM을 사용하여 추론 시간 단축 등)하려고 한다. + +Stage 1 Part 1과 2는 이미 최적화 작업을 마치고 yaml 작업 명세를 업데이트하였다. 업데이트한 Part 1 작업 명세서는 , 이며, 이 yaml 파일은 와 의 개정 작업 명세서를 각각 반영하여 작성된 것이다. + + + +Stage 1 Part 3 작업의 비효율성을 검토하고, 작업 효율성을 최적화하는 개정 방안(혹은 전략)을 작성한다. + + + +0. 에 제시된 v.6 폴더의 Stage 1 작업 명세서 4개를 모두 읽고, 세부 작업 내용을 숙지한다: +1) +2) +3) +4) +를 읽고 Stage 1 전체 작업 명세를 숙지한다. +2. 에 제시된 폴더에 있는 Stage 1 실행 결과물들을 파악한다. +3. Stage 1 Part 3 yaml 파일()에 제시된 작업 명세를 정확하고 심도 깊게 이해하고, +4. Part 3 작업들의 비효율성(토큰 비경제성, LLM 추론 작업 비효율성, DAG workflow 비효율성 등)을 파악하여 체계적으로 서술한다. +5. 4번 작업에서 파악한 Part 3 작업 명세의 비효율성을 효율화 할 수 있는 방안을 작성한다. 효율화 방식은 최소한의 작업으로 최대한의 효과를 거둘 수 있는 방식이어야 한다. +6. 1~5까지의 작업을 마친 후 결과 보고서를 로 생성하고 저장한다. + + +========================================================================================================= + +┌──────────────────────────┐ +│Stage 1 -- Part 3 │ +│Improvement Strategy │ +│Claud vs Codex │ +│--> Ensemble │ +└──────────────────────────┘ + + +Stage 1 Part 3 작업 명세서 yaml 파일의 비효율성 및 개선 방안을 분석한 두 개 문서 , 를 비교 분석하여 Stage 1 Part 3 개정 작업에 가장 적합한 전략 도출 + + + +와 를 다음 항목들에 집중하여 비교 분석한다: + - Part 3 비효율 판정 항목들 + - Part 3 실행구조와 병목 발생 지점 + - 삭제/축소 안정성 판정(downstream 작업 연계 근거) + - 권장 개정 전략 + + + +1. 작업을 통해 구축한 비교 분석 내용에 근거하여, +2. Stage 1 Part 3 작업의 효율성을 증대시키는 데 가장 필요한 작업들을 선별한다. 선별 기준은 최소의 노력(LLM 추론 작업은 꼭 필요한 곳에만 필요한 수준으로 사용, 100% deterministic task는 python code화, 토큰 경제성을 최대화)으로 최대의 효과(최고 품질 유지, 토큰 경제성 증대, 에이전트 속도 최대화)를 이룩하는 것이다. +3. 2번 작업 결과물을 로 생성하고 저장한다. + + + + +================================================================================================================= + +┌──────────────────────────┐ +│Stage 1 -- Part 3 │ +│Improvement Strategy Doc │ +└──────────────────────────┘ + +기존 Stage 1 Part 3 yaml 작업 명세서 를 개정하는 데 사용할 "개정 작업 명세서" 작성 + + + + 내용으로부터 기존 Stage 1 Part 3 yaml 작업 명세서인 를 개정할 전략서를 작성한다. + +전략서는 "20년 이상 경력을 지닌 대한민국 최고 수준의 변호사 및 세계적인 수준의 인공지능 아키텍트 기술자의 관점"에서 LLM이 읽고 정확하게 이해한 후, 100% 작업을 수행할 수 있는 작업 명세서로 작성한다. + +작성한 작업 명세서는 로 생성하고 저장한다. + + + +================================================================================================================= + +┌──────────────────────────┐ +│Stage 1 -- Part 3 │ +│Update YAML to v2 │ +└──────────────────────────┘ + + +기존 Stage 1 Part 3 작업명세서 를 개정하여 v2 yaml 파일을 작성한다. + + + +1. 를 읽고, 개정 작업 과정을 숙지한다. +2. 를 복제하여 를 생성한다. +3. 에 1번 작업에서 숙지한 개정 작업과정을 정확하게 적용하여 v2.yml을 개정 작성한다. 작성 시, LLM 추론 작업 task는 모두 동일한 common cache prefix를 사용해야 하고, python code 작업은 가 제시하는 python code 작성 제약을 100% 준수하도록 한다. +4. 3번 작업에서 작성한 v2.yml이 1번의 개정 작업 과정을 100% 동일하게 수행했는지, 기존 v1.yml 작업 명세의 작업 목표를 최고 수준에서 달성하는지, LLM 추론 작업에서 동일한 common cache prefix를 사용하는지, python code 작업 시 가 제시하는 python code 작성 제약을 100% 준수하는지 네 가지를 "20년 이상 경력을 지닌 대한민국 최고 수준의 변호사 및 세계적인 수준의 인공지능 아키텍트 기술자의 관점"에서 엄격하게 평가한다. +5. 4번 과정의 평가를 통과할 때까지 v2.yml을 재작성한다. +6. 평가를 통과한 v2.yml을 overwrite하여 저장한다. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Prompt_for_Inefficiency_Report_Part_4.txt b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Prompt_for_Inefficiency_Report_Part_4.txt new file mode 100644 index 00000000..19404676 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Prompt_for_Inefficiency_Report_Part_4.txt @@ -0,0 +1,191 @@ + + +Stage 1 4개 yaml 작업 명세서 4개 +1) +2) +3) +4) +를 Liti-agent를 통해 순차적으로 실행하였다. + +순차 실행 후 얻은 결과물은 폴더에 저장되어 있다. + +에이전트가 stage 1 4개 yaml 파일들을 실행했을 때의 런타임(run-time)과 cost는 아래와 같다: +{{ +* Stage 1 Part 1 +런타임: 8min 45sec +비용: $2.09 + +* Stage 1 Part 2 +런타임: 7min 40sec +비용: $4.28 + +* Stage 1 Part 3 +런타임: 3min 40sec +비용: $0 + +* Stage 1 Part 4 +런타임: 3min 6sec +비용: $0.58 +}} + +Stage 1 yaml 개정 전 작업 명세 +- - - - +을 실행했을 때보다 런타임은 더 길어졌고, 비용도 더 상승했다. + +Stage 1 yaml 작업 명세서 중 Part 4를 지금보다도 훨씬 더 최적화(필요 작업만 남기고 군더더기 작업들을 없애는 경량화 / 최고 품질의 추론이 필요 없을 때는 약간 낮은 성능의 LLM을 사용하여 추론 시간 단축 등)하려고 한다. + +Stage 1 Part 1, 2, 3은 이미 최적화 작업을 마치고 yaml 작업 명세를 업데이트하였다. 업데이트한 Part 1 작업 명세서는 , , 이며, 이 yaml 파일은 와 , 의 개정 작업 명세서를 각각 반영하여 작성된 것이다. + + + +Stage 1 Part 4 작업의 비효율성이 초래되는 원인을 식별하고, 작업 효율성을 최적화하는 개정 방안(혹은 전략)을 작성한다. + + + +0. 에 제시된 v.6 폴더의 Stage 1 작업 명세서 4개를 모두 읽고, 세부 작업 내용을 숙지한다: +1) +2) +3) +4) +를 읽고 Stage 1 전체 작업 명세를 숙지한다. +2. 에 제시된 폴더에 있는 Stage 1 실행 결과물들을 파악한다. +3. Stage 1 Part 4 yaml 파일()에 제시된 작업 명세를 정확하고 심도 깊게 이해하고, +4. Part 4 작업들의 비효율성(토큰 비경제성, LLM 추론 작업 비효율성, DAG workflow 비효율성 등)을 식별하여 체계적으로 서술한다. +5. 4번 작업에서 파악한 Part 4 작업 명세의 비효율성을 효율화 할 수 있는 방안을 작성한다. 효율화 방식은 최소한의 작업으로 최대한의 효과를 거둘 수 있는 방식이어야 한다. +6. 1~5까지의 작업을 마친 후 결과 보고서를 로 생성하고 저장한다. + + +========================================================================================================= + +┌──────────────────────────┐ +│Stage 1 -- Part 4 │ +│Improvement Strategy │ +│Claud vs Codex │ +│--> Ensemble │ +└──────────────────────────┘ + + +Stage 1 Part 4 작업 명세서 yaml 파일의 비효율성 및 개선 방안을 분석한 두 개 문서 , 를 비교 분석하여 Stage 1 Part 4 개정 작업에 가장 적합한 전략 도출 + + + +와 를 다음 항목들에 집중하여 비교 분석한다: + - Part 4 비효율 판정 항목들 + - Part 4 실행구조와 병목 발생 지점 + - 삭제/축소 안정성 판정(downstream 작업 연계 근거) + - 권장 개정 전략 + + + +1. 작업을 통해 구축한 비교 분석 내용에 근거하여, +2. Stage 1 Part 4 작업의 효율성을 증대시키는 데 가장 필요한 작업들을 선별한다. 선별 기준은 최소의 노력(LLM 추론 작업은 꼭 필요한 곳에만 필요한 수준으로 사용, 100% deterministic task는 python code화, 토큰 경제성을 최대화)으로 최대의 효과(최고 품질 유지, 토큰 경제성 증대, 에이전트 속도 최대화)를 이룩하는 것이다. +3. 2번 작업 결과물을 로 생성하고 저장한다. + + + +================================================================================================================= + +┌──────────────────────────┐ +│Stage 1 -- Part 4 │ +│Improvement Strategy Doc │ +└──────────────────────────┘ + +기존 Stage 1 Part 4 yaml 작업 명세서 를 개정하는 데 사용할 "개정 작업 명세서" 작성 + + + + 내용으로부터 기존 Stage 1 Part 4 yaml 작업 명세서인 를 개정할 전략서를 작성한다. + +전략서는 "20년 이상 경력을 지닌 대한민국 최고 수준의 변호사 및 세계적인 수준의 인공지능 아키텍트 기술자의 관점"에서 LLM이 읽고 정확하게 이해한 후, 100% 작업을 수행할 수 있는 작업 명세서로 작성한다. + +작성한 작업 명세서는 로 생성하고 저장한다. + + + +================================================================================================================= + +┌──────────────────────────┐ +│Stage 1 -- Part 4 │ +│Update YAML to v3 │ +└──────────────────────────┘ + + +기존 Stage 1 Part 4 작업명세서 를 개정하여 v3 yaml 파일을 작성한다. + + + +1. 를 읽고, 개정 작업 과정을 숙지한다. +2. 를 복제하여 를 생성한다. +3. 에 1번 작업에서 숙지한 개정 작업과정을 정확하게 적용하여 v3.yml을 개정 작성한다. 작성 시, LLM 추론 작업 task는 모두 동일한 common cache prefix를 사용해야 하고, python code 작업은 가 제시하는 python code 작성 제약을 100% 준수하도록 한다. +4. 3번 작업에서 작성한 v3.yml이 1번의 개정 작업 과정을 100% 동일하게 수행했는지, 기존 v2.yml 작업 명세의 작업 목표를 최고 수준에서 달성하는지, LLM 추론 작업에서 동일한 common cache prefix를 사용하는지, python code 작업 시 가 제시하는 python code 작성 제약을 100% 준수하는지 네 가지를 "20년 이상 경력을 지닌 대한민국 최고 수준의 변호사 및 세계적인 수준의 인공지능 아키텍트 기술자의 관점"에서 엄격하게 평가한다. +5. 4번 과정의 평가를 통과할 때까지 v3.yml을 재작성한다. +6. 평가를 통과한 v3.yml을 overwrite하여 저장한다. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Prompt_for_Inefficiency_v1.txt b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Prompt_for_Inefficiency_v1.txt new file mode 100644 index 00000000..91e0ed0e --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Prompt_for_Inefficiency_v1.txt @@ -0,0 +1,50 @@ + +Stage 1 4개 yaml 작업 명세서 4개 +1) +2) +3) +4) +를 Liti-agent를 통해 순차적으로 실행하였다. + +순차 실행 후 얻은 결과물은 폴더에 저장되어 있다. + +에이전트가 stage 1 4개 yaml 파일들을 실행했을 때의 런타임(run-time)과 cost는 아래와 같다: +{{ +* Stage 1 Part 1 +런타임: 8min 45sec +비용: $2.09 + +* Stage 1 Part 2 +런타임: 7min 40sec +비용: $4.28 + +* Stage 1 Part 3 +런타임: 3min 40sec +비용: $0 + +* Stage 1 Part 4 +런타임: 3min 6sec +비용: $0.58 +}} + +Stage 1 yaml 개정 전 작업 명세 +- - - - +을 실행했을 때보다 런타임은 더 길어졌고, 비용도 더 상승했다. + +그래서 Stage 1 yaml 작업 명세서를 지금보다도 훨씬 더 최적화(필요 작업만 남기고 군더더기 작업들을 없애는 경량화 / 최고 품질의 추론이 필요 없을 때는 약간 낮은 성능의 LLM을 사용하여 추론 시간 단축 등)하려고 한다. + + + +Stage 1 Part 1에서 "B2_quality_gate_event_indexed"와 "B2_quality_gate_event_candidates" 작업들을 최적화하는 개정 방안(혹은 전략)을 작성한다. + + + +1. 에 제시된 v.6 폴더의 Stage 1 작업 명세서 4개를 모두 읽고, 세부 작업 내용을 숙지한다. +2. 에 제시된 폴더에 있는 Stage 1 실행 결과물들을 파악한다. +3. Stage 1 Part 1 yaml 파일을 정확하고 심도 깊게 이해하고, +4. "B2_quality_gate_event_indexed"와 "B2_quality_gate_event_candidates" 작업들 내용을 파악하고, +5. 두 개 sub-task들의 downstream 작업과의 연계성을 감안하여, 반드시 필요하지 않은 작업들을 파악하고 그것들을 삭제해도 문제가 없는지 판단한다. +6. 특히, "B2_quality_gate_event_indexed" 작업 후 "B2_quality_gate_event_candidates" 작업이 어떻게 실행되는지 파악하여, +7. 두 개 작업 중 선행 작업의 개별 작업들의 결과물들을 모두 모아서 후행 작업들에 한 번에 넘겨줘서 런타임을 증폭시키는 비효율성이 초래되는지, 아니면 어떤 비효율성이 존재하는지 파악한다. +8. 1~7까지의 작업을 마친 후 결과 보고서를 로 생성하고 저장한다. (### = Codex | Clause) + \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/BO.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/BO.json new file mode 100644 index 00000000..e7ee728b --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/BO.json @@ -0,0 +1,5781 @@ +[ + { + "BO_ID": "bh1", + "id": "bh1", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "도급" + }, + "Action": "평택시 빌라 신축 공사도급계약 체결 및 중도금 지급", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "도급", + "공사대금" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "평택시 빌라 신축 공사도급계약 체결 및 중도금 지급", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [ + "공사도급계약서" + ], + "Evidence": [ + { + "evidence_index": "E-007", + "source_title": "공사도급계약서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-007" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-007-01", + "EVT-007-02" + ], + "source_meeting_clause_ids": [], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "도급계약", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [ + "박광윤(수급인)", + "김일동(도급인)" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": "공사대금", + "principal_amount_hint": "1,000,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": "박광윤", + "debtor_candidate": "김일동" + }, + "payment_history": [ + "2021. 8. 30. 중도금 4억원 지급" + ], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + }, + { + "BO_ID": "bh2", + "id": "bh2", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "매매" + }, + "Action": "인테리어 자재 매매에 따른 물품대금 채권 발생", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "물품대금", + "매매" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "인테리어 자재 매매에 따른 물품대금 채권 발생", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [ + "이행각서", + "최고 통지서" + ], + "Evidence": [ + { + "evidence_index": "E-021", + "source_title": "이행각서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + }, + { + "evidence_index": "E-027", + "source_title": "최고 통지서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-021", + "E-027" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-027-01", + "EVT-027-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:1-나", + "s03_client_requests:1" + ], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "물품대금 채권 발생", + "summary_hint": "김선웅에 대한 인테리어 자재대금", + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [ + "강용원(채권자)", + "김선웅(채무자)" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": "물품대금", + "principal_amount_hint": "200,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": "강용원", + "debtor_candidate": "김선웅" + }, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + }, + { + "BO_ID": "bh3", + "id": "bh3", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "소비대차" + }, + "Action": "1억 원 대여금 채권 발생 (2022.4.18.)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "대여금", + "소비대차" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "1억 원 대여금 채권 발생 (2022.4.18.)", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [ + "차용증" + ], + "Evidence": [ + { + "evidence_index": "E-024", + "source_title": "차용증", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-024" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-024-01" + ], + "source_meeting_clause_ids": [], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "대여금 채권 발생", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [ + "강용원(대주)", + "오국한(차주)" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": "대여금", + "principal_amount_hint": "100,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": "월 1%", + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [ + "E-024" + ], + "unpaid_balance_hint": null, + "creditor_candidate": "강용원", + "debtor_candidate": "오국한" + }, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + }, + { + "BO_ID": "bh4", + "id": "bh4", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "소비대차" + }, + "Action": "1억 원 대여금 채권 발생 (2022.5.18.)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "대여금", + "소비대차" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "1억 원 대여금 채권 발생 (2022.5.18.)", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [ + "차용증" + ], + "Evidence": [ + { + "evidence_index": "E-024", + "source_title": "차용증", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-024" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-024-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:3-나", + "s03_client_requests:3" + ], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "대여금 채권 발생", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [ + "강용원(대주)", + "오국한(차주)" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": "대여금", + "principal_amount_hint": "100,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": "월 1%", + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [ + "E-024" + ], + "unpaid_balance_hint": null, + "creditor_candidate": "강용원", + "debtor_candidate": "오국한" + }, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + }, + { + "BO_ID": "bh5", + "id": "bh5", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "소비대차" + }, + "Action": "2억 원 대여금 채권 발생 (2023.1.6.)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "대여금", + "소비대차" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "2억 원 대여금 채권 발생 (2023.1.6.)", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [ + "차용증" + ], + "Evidence": [ + { + "evidence_index": "E-025", + "source_title": "차용증", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-025" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-025-01" + ], + "source_meeting_clause_ids": [], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "대여금 채권 발생", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [ + "오민한(대주)", + "강용원(차주)" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": "대여금", + "principal_amount_hint": "200,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": "오민한", + "debtor_candidate": "강용원" + }, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + }, + { + "BO_ID": "bh6", + "id": "bh6", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "소비대차" + }, + "Action": "3억 원 대여금 채권 발생 (2023.7.6.)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "대여금", + "소비대차" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "3억 원 대여금 채권 발생 (2023.7.6.)", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [ + "차용증" + ], + "Evidence": [ + { + "evidence_index": "E-026", + "source_title": "차용증", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-026" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-026-01" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-나" + ], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "대여금 채권 발생", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [ + "오민한(대주)", + "강용원(차주)" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": "대여금", + "principal_amount_hint": "300,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": "연 15%", + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [ + "E-026" + ], + "unpaid_balance_hint": null, + "creditor_candidate": "오민한", + "debtor_candidate": "강용원" + }, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + }, + { + "BO_ID": "bh7", + "id": "bh7", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "영업양도" + }, + "Action": "해드림 영업양도 및 상호속용 약정", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "영업양도", + "상호속용" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "해드림 영업양도 및 상호속용 약정", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [ + "영업양도 계약서" + ], + "Evidence": [ + { + "evidence_index": "E-020", + "source_title": "영업양도 계약서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-020" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-020-01", + "EVT-020-02", + "EVT-020-03" + ], + "source_meeting_clause_ids": [], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "영업양도", + "summary_hint": "영업양도에 따른 양수인의 상호속용 책임", + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [ + "김선웅(양도인)", + "오민한(양수인)" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": null, + "debtor_candidate": null + }, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [ + "해드림(SUN-DREAM) 영업양도 및 상호속용" + ], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + }, + { + "BO_ID": "bh8", + "id": "bh8", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "채무승인" + }, + "Action": "물품대금 채무 승인 및 변제기 유예", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "채무승인", + "변제기 유예" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "물품대금 채무 승인 및 변제기 유예", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "이행각서" + ], + "Evidence": [ + { + "evidence_index": "E-021", + "source_title": "이행각서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-021" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-021-01", + "EVT-021-02" + ], + "source_meeting_clause_ids": [], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "채무승인", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": "강용원", + "debtor_candidate": "김선웅" + }, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [ + "2024. 8. 5. 자 이행각서를 통한 채무 승인" + ], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + }, + { + "BO_ID": "bh9", + "id": "bh9", + "BOType": "event", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": "변제" + }, + "Action": "차용금 변제 명목 4억 원 지급 (채무 미특정)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "변제", + "법정변제충당" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "차용금 변제 명목 4억 원 지급 (채무 미특정)", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [], + "Evidence": [], + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [ + "s01_consultation:2-다" + ], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "일부 변제", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [ + "강용원(변제자)", + "오민한(수령자)" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": null, + "debtor_candidate": null + }, + "payment_history": [ + "2024. 7. 5. 오민한에게 위 차용금 변제로 4억 원 지급, 채무 특정 안됨" + ], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [ + "cross_domain_merge_needed" + ] + } + } + }, + { + "BO_ID": "bh10", + "id": "bh10", + "BOType": "event", + "ActionType": "소송행위(litigation acts)", + "JuristicAct": { + "label": "상계" + }, + "Action": "오국한에 대한 대여금 채권 등을 자동채권으로 한 상계 항변", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "상계", + "항변" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "오국한에 대한 대여금 채권 등을 자동채권으로 한 상계 항변", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [ + "각서 및 금전소비대차계약서", + "내용증명에 대한 답신" + ], + "Evidence": [ + { + "evidence_index": "E-003", + "source_title": "각서 및 금전소비대차계약서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + }, + { + "evidence_index": "E-010", + "source_title": "내용증명에 대한 답신", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-003", + "E-010" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-010-03", + "EVT-003-02" + ], + "source_meeting_clause_ids": [], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "상계 항변", + "summary_hint": null, + "conflict_patterns": [ + "상계 주장" + ], + "asset_bindings": [], + "parties_hint": [ + "오민한", + "강용원" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": "대여금", + "principal_amount_hint": "50,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": "오민한", + "debtor_candidate": null + }, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + }, + { + "BO_ID": "bh11", + "id": "bh11", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "최고" + }, + "Action": "물품대금 지급 최고", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "이행최고", + "통지", + "최고" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "물품대금 지급 최고", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "최고 통지서" + ], + "Evidence": [ + { + "evidence_index": "E-027", + "source_title": "최고 통지서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-027" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-027-01", + "EVT-027-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:1-나" + ], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "최고통지", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": null, + "debtor_candidate": null + }, + "payment_history": [], + "demand_or_notice_hints": [ + "2024. 5. 4. 자 지급 최고서 발송", + "2024. 5. 6. 도달" + ], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + }, + { + "BO_ID": "bh12", + "id": "bh12", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "상호속용에 따른 물품대금 채무 책임 부인 통지", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "통지", + "책임부인" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "상호속용에 따른 물품대금 채무 책임 부인 통지", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "통지서" + ], + "Evidence": [ + { + "evidence_index": "E-028", + "source_title": "통지서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-028" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-028-01" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:1" + ], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "책임 부인 통지", + "summary_hint": null, + "conflict_patterns": [ + "양수인 책임 부인", + "소멸시효 항변" + ], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": null, + "debtor_candidate": null + }, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": "채무 시효 소멸 및 책임 배제 약정 존재 여부 불확실", + "conflict_flags": [ + "source_conflict" + ] + } + } + }, + { + "BO_ID": "bh13", + "id": "bh13", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "공사대금 잔금 3억 원 미지급 상태 및 유치권 충당 항변", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "미지급", + "충당" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "공사대금 잔금 3억 원 미지급 상태 및 유치권 충당 항변", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [ + "답변서" + ], + "Evidence": [ + { + "evidence_index": "E-011", + "source_title": "답변서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-011" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-011-03" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-가" + ], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "잔금 미지급", + "summary_hint": null, + "conflict_patterns": [ + "차임 충당 항변" + ], + "asset_bindings": [], + "parties_hint": [ + "박광윤(채권자)", + "김일동(채무자)" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": "공사대금", + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": "300,000,000원", + "creditor_candidate": "박광윤", + "debtor_candidate": "김일동" + }, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + }, + { + "BO_ID": "bh14", + "id": "bh14", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "(근)저당권 설정 계약" + }, + "Action": "강용원은 성수동 대지에 대한 상속 및 증여로 지분을 취득하고, 오민한에게 차용금 담보를 위해 성수동 대지 지분에 대한 근저당권 설정 약정을 체결함", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "상속", + "증여", + "근저당권설정", + "secured_debt_creation", + "(근)저당권 설정 계약" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "강용원은 성수동 대지에 대한 상속 및 증여로 지분을 취득하고, 오민한에게 차용금 담보를 위해 성수동 대지 지분에 대한 근저당권 설정 약정을 체결함", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [ + "등기사항전부증명서(말소사항 포함)-토지", + "차용증" + ], + "Evidence": [ + { + "evidence_index": "E-013", + "source_title": "등기사항전부증명서(말소사항 포함)-토지", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + }, + { + "evidence_index": "E-025", + "source_title": "차용증", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + }, + { + "evidence_index": "E-026", + "source_title": "차용증", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-013-01", + "EVT-013-02", + "EVT-025-02", + "EVT-026-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-가", + "s01_consultation:2-나" + ], + "source_domain": "B2_Secured_Registry" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "title_hint": "성수동 대지 소유권 변동 및 근저당권 설정", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [ + "성수동 대지" + ], + "parties_hint": [ + "강용원", + "정유심", + "오민한" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [ + "secured_debt_creation" + ], + "event_state_type": "event", + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "registry_rows": [ + { + "registry_ref": "REG-013-01", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제1453호" + }, + { + "registry_ref": "REG-013-02", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제7026호" + }, + { + "registry_ref": "REG-013-03", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제11202호" + }, + { + "registry_ref": "REG-013-04", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제7202호" + }, + { + "registry_ref": "REG-013-05", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제12402호" + }, + { + "registry_ref": "REG-013-06", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제25797호" + }, + { + "registry_ref": "REG-013-07", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제25799호" + } + ], + "fractional_effect_events": [ + { + "fraction_ref": null, + "fraction_display": "3/5", + "fraction_numerator": null, + "fraction_denominator": null, + "effect_kind_hint": "transfer", + "source_event_candidate_ids": [ + "EVT-013-02" + ], + "registry_refs": [ + "REG-013-02" + ], + "evidence_indexes": [ + "E-013" + ] + } + ], + "secured_debt_chain": [ + "EVT-025-02", + "EVT-026-02" + ], + "payment_allocation": [], + "mortgage_extinguishment_hints": [], + "auction_steps": [], + "dividend_distribution_hints": [], + "derivative_invalidity_hints": [], + "registry_object_display": "성수동 대지 등기" + } + } + }, + { + "BO_ID": "bh15", + "id": "bh15", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "근저당권 말소 및 상계" + }, + "Action": "신림동 상가에 강용원(등기상 강용일) 명의의 근저당권이 설정되었다가 무단 말소되었으며, 오민한은 상계를 주장함", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "근저당권설정", + "말소", + "상계", + "secured_debt_chain", + "mortgage_extinguishment", + "근저당권 말소 및 상계" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "신림동 상가에 강용원(등기상 강용일) 명의의 근저당권이 설정되었다가 무단 말소되었으며, 오민한은 상계를 주장함", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [ + "내용증명에 대한 답신", + "등기사항전부증명서 (말소사항 포함) -건물" + ], + "Evidence": [ + { + "evidence_index": "E-010", + "source_title": "내용증명에 대한 답신", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + }, + { + "evidence_index": "E-014", + "source_title": "등기사항전부증명서 (말소사항 포함) -건물", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-010", + "E-014" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-014-01", + "EVT-014-02", + "EVT-014-03", + "EVT-014-04", + "EVT-014-05", + "EVT-014-06", + "EVT-010-03" + ], + "source_meeting_clause_ids": [ + "s01_consultation:3-가", + "s01_consultation:3-라" + ], + "source_domain": "B2_Secured_Registry" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "title_hint": "신림동 상가 근저당권 등기 말소 및 상계 다툼", + "summary_hint": null, + "conflict_patterns": [ + "근저당권 무단 말소", + "상계 주장" + ], + "asset_bindings": [ + "신림동 상가" + ], + "parties_hint": [ + "강용원", + "오국한", + "오민한" + ], + "temporal_hint": null, + "hazard_flags": [ + "conflict_pattern" + ], + "legal_effect_tags": [ + "secured_debt_chain", + "mortgage_extinguishment" + ], + "event_state_type": "event", + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "registry_rows": [ + { + "registry_ref": "REG-014-01", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제24759호" + }, + { + "registry_ref": "REG-014-02", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제56457호" + }, + { + "registry_ref": "REG-014-03", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제11593호" + }, + { + "registry_ref": "REG-014-04", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제50099호" + }, + { + "registry_ref": "REG-014-05", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제26597호" + }, + { + "registry_ref": "REG-014-06", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제74586호" + } + ], + "fractional_effect_events": [], + "secured_debt_chain": [ + "EVT-014-04", + "EVT-014-05" + ], + "payment_allocation": [], + "mortgage_extinguishment_hints": [ + "EVT-014-06", + "EVT-010-03" + ], + "auction_steps": [], + "dividend_distribution_hints": [], + "derivative_invalidity_hints": [], + "registry_object_display": "신림동 상가 등기" + } + } + }, + { + "BO_ID": "bh16", + "id": "bh16", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "소유권이전등기" + }, + "Action": "평택시 서정빌라에 대한 보존등기 및 매매로 인한 소유권이전등기가 마쳐짐", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "소유권보존", + "소유권이전", + "가압류", + "소유권이전등기" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "평택시 서정빌라에 대한 보존등기 및 매매로 인한 소유권이전등기가 마쳐짐", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [ + "등기사항전부증명서(말소사항 포함)-집합건물" + ], + "Evidence": [ + { + "evidence_index": "E-015", + "source_title": "등기사항전부증명서(말소사항 포함)-집합건물", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-015" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-015-01", + "EVT-015-02", + "EVT-015-03", + "EVT-015-04" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-나" + ], + "source_domain": "B2_Secured_Registry" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "title_hint": "평택시 빌라 소유권 이전 및 등기 연혁", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [ + "평택시 빌라" + ], + "parties_hint": [ + "김일동", + "정남이", + "강호연" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": "event", + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "registry_rows": [ + { + "registry_ref": "REG-015-01", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제7233호" + }, + { + "registry_ref": "REG-015-02", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제7871호" + }, + { + "registry_ref": "REG-015-03", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제11593호" + }, + { + "registry_ref": "REG-015-04", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제12321호" + } + ], + "fractional_effect_events": [], + "secured_debt_chain": [], + "payment_allocation": [], + "mortgage_extinguishment_hints": [], + "auction_steps": [], + "dividend_distribution_hints": [], + "derivative_invalidity_hints": [], + "registry_object_display": "서정빌라 집합건물 등기" + } + } + }, + { + "BO_ID": "bh17", + "id": "bh17", + "BOType": "event", + "ActionType": "소송행위(litigation acts)", + "JuristicAct": { + "label": "담보권실행을 위한 경매 및 근저당권 설정" + }, + "Action": "오민한이 경매를 신청하여 이문호가 매각대금을 완납하고 취득한 뒤, 대한은행이 근저당권을 설정함에 따라 원인무효 여부 및 공신력이 다투어짐", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "임의경매", + "공신력", + "등기무효", + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "담보권실행을 위한 경매 및 근저당권 설정" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "오민한이 경매를 신청하여 이문호가 매각대금을 완납하고 취득한 뒤, 대한은행이 근저당권을 설정함에 따라 원인무효 여부 및 공신력이 다투어짐", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [ + "등기말소 신청에 대한 답신", + "등기사항전부증명서(말소사항 포함)-토지" + ], + "Evidence": [ + { + "evidence_index": "E-012", + "source_title": "등기말소 신청에 대한 답신", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + }, + { + "evidence_index": "E-013", + "source_title": "등기사항전부증명서(말소사항 포함)-토지", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-012", + "E-013" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-013-03", + "EVT-013-04", + "EVT-013-05", + "EVT-013-06", + "EVT-012-01", + "EVT-012-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-라", + "s01_consultation:2-마", + "s02_party_assertions:2" + ], + "source_domain": "B2_Secured_Registry" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "title_hint": "성수동 대지 경매 및 대한은행 근저당권 설정 공신력 항변", + "summary_hint": null, + "conflict_patterns": [ + "무효 주장", + "공신력 신뢰 주장" + ], + "asset_bindings": [ + "성수동 대지" + ], + "parties_hint": [ + "오민한", + "이문호", + "주식회사 대한은행" + ], + "temporal_hint": "2024-10-05", + "hazard_flags": [ + "conflict_pattern" + ], + "legal_effect_tags": [ + "auction_invalidity", + "derivative_bank_mortgage_invalidity" + ], + "event_state_type": "event", + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "registry_rows": [], + "fractional_effect_events": [], + "secured_debt_chain": [], + "payment_allocation": [], + "mortgage_extinguishment_hints": [ + "EVT-013-05" + ], + "auction_steps": [ + "EVT-013-03", + "EVT-013-04" + ], + "dividend_distribution_hints": [], + "derivative_invalidity_hints": [ + "EVT-013-06", + "EVT-012-01", + "EVT-012-02" + ], + "registry_object_display": "성수동 대지 경매 진행내역" + } + } + }, + { + "BO_ID": "bh18", + "id": "bh18", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "변제 및 변제충당" + }, + "Action": "강용원이 오민한에게 4억 원을 지급하였으나 다수의 채무 중 충당할 채무를 특정하지 않음", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "변제", + "변제충당", + "payment_allocation_uncertain", + "변제 및 변제충당" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "강용원이 오민한에게 4억 원을 지급하였으나 다수의 채무 중 충당할 채무를 특정하지 않음", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [], + "Evidence": [], + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [ + "s01_consultation:2-다" + ], + "source_domain": "B2_Secured_Registry" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "title_hint": "변제에 따른 채무 소멸 및 지정충당 부존재", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [ + "강용원", + "오민한" + ], + "temporal_hint": "2024-07-05", + "hazard_flags": [ + "conflict_pattern" + ], + "legal_effect_tags": [ + "payment_allocation_uncertain" + ], + "event_state_type": "event", + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "registry_rows": [], + "fractional_effect_events": [], + "secured_debt_chain": [], + "payment_allocation": [ + { + "total_paid": "400,000,000", + "unspecified_allocation": true + } + ], + "mortgage_extinguishment_hints": [], + "auction_steps": [], + "dividend_distribution_hints": [], + "derivative_invalidity_hints": [], + "registry_object_display": null + } + } + }, + { + "BO_ID": "bh19", + "id": "bh19", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "변제충당 주장" + }, + "Action": "박광윤이 평택시 빌라를 점유하며 얻은 임대 및 거주 수익을 피담보채권에 충당하였다고 항변함", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "유치권", + "점유", + "변제충당", + "payment_allocation", + "변제충당 주장" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "박광윤이 평택시 빌라를 점유하며 얻은 임대 및 거주 수익을 피담보채권에 충당하였다고 항변함", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [ + "공사도급계약서", + "답변서" + ], + "Evidence": [ + { + "evidence_index": "E-007", + "source_title": "공사도급계약서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + }, + { + "evidence_index": "E-011", + "source_title": "답변서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-007", + "E-011" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-011-03", + "EVT-007-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-가", + "s01_consultation:4-나", + "s02_party_assertions:4-나" + ], + "source_domain": "B2_Secured_Registry" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "title_hint": "평택시 빌라 임대수익의 피담보채무 변제충당 항변", + "summary_hint": null, + "conflict_patterns": [ + "수익 충당 항변" + ], + "asset_bindings": [ + "평택시 빌라" + ], + "parties_hint": [ + "박광윤" + ], + "temporal_hint": null, + "hazard_flags": [ + "conflict_pattern" + ], + "legal_effect_tags": [ + "payment_allocation" + ], + "event_state_type": "state", + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "registry_rows": [], + "fractional_effect_events": [], + "secured_debt_chain": [], + "payment_allocation": [ + { + "allocation_source": "거주 및 임대 수익", + "target_debt": "공사대금 피담보채권" + } + ], + "mortgage_extinguishment_hints": [], + "auction_steps": [], + "dividend_distribution_hints": [], + "derivative_invalidity_hints": [], + "registry_object_display": null + } + } + }, + { + "BO_ID": "bh20", + "id": "bh20", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "제3자 명의 등기 요청" + }, + "Action": "이수인이 윤건우에게 매매계약에서 합의되지 않은 제3자 명의로의 등기이전을 일방적으로 요구함", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "소유권이전등기", + "요구", + "거절", + "registry_defect_hint", + "제3자 명의 등기 요청" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "이수인이 윤건우에게 매매계약에서 합의되지 않은 제3자 명의로의 등기이전을 일방적으로 요구함", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [ + "통지서_이수인_윤건우" + ], + "Evidence": [ + { + "evidence_index": "E-029", + "source_title": "통지서_이수인_윤건우", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-029" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-029-01" + ], + "source_meeting_clause_ids": [ + "s01_consultation:5-다" + ], + "source_domain": "B2_Secured_Registry" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "title_hint": "흑석동 상가 제3자 명의 등기 요청 및 거절", + "summary_hint": null, + "conflict_patterns": [ + "등기 이전 요구 거절" + ], + "asset_bindings": [ + "흑석동 상가" + ], + "parties_hint": [ + "이수인", + "윤건우" + ], + "temporal_hint": "2024-03-30", + "hazard_flags": [ + "conflict_pattern" + ], + "legal_effect_tags": [ + "registry_defect_hint" + ], + "event_state_type": "event", + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "registry_rows": [], + "fractional_effect_events": [], + "secured_debt_chain": [], + "payment_allocation": [], + "mortgage_extinguishment_hints": [], + "auction_steps": [], + "dividend_distribution_hints": [], + "derivative_invalidity_hints": [], + "registry_object_display": null + } + } + }, + { + "BO_ID": "bh21", + "id": "bh21", + "BOType": "event", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "성수동 대지 차임 시세 감정평가", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "가치평가", + "임대료", + "차임", + "valuation" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "성수동 대지 차임 시세 감정평가", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [ + "감정평가 의견서" + ], + "Evidence": [ + { + "evidence_index": "E-005", + "source_title": "감정평가 의견서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-005" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-005-01" + ], + "source_meeting_clause_ids": [], + "source_domain": "B3_Land_Valuation" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "title_hint": "성수동 대지 차임 시세 감정평가", + "summary_hint": "성수동 256 대지에 대한 월 차임 250만 원~500만 원 시세 평가", + "conflict_patterns": [], + "asset_bindings": [ + "SCH-016-01" + ], + "parties_hint": [ + "강용원", + "감정평가법인 광나루" + ], + "temporal_hint": "2024-12-29", + "hazard_flags": [], + "legal_effect_tags": [ + "valuation" + ], + "event_state_type": "event", + "action_summary": "성수동 대지 차임 시세 평가 회신", + "time_interval_hint": "2024", + "asset_binding_hint": "성수동 256 대", + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "valuation_rows": [], + "valuation_matrix": [], + "valuation_object": "성수동 256 대지", + "use_gain_facts": [], + "possession_state": { + "asset_ref": null, + "possession_events": [], + "legal_cause_assertions": [] + }, + "delivery_or_possession_start_hints": [], + "unregistered_building_hints": [], + "liability_overlap_hints": [] + } + } + }, + { + "BO_ID": "bh22", + "id": "bh22", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "성수동 대지 및 건물 인도, 점유 및 사용", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "인도", + "점유", + "사용", + "possession" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "성수동 대지 및 건물 인도, 점유 및 사용", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "계약서" + ], + "Evidence": [ + { + "evidence_index": "E-006", + "source_title": "계약서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-006" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-006-03", + "EVT-006-06", + "EVT-006-07" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-바", + "s01_consultation:2-사" + ], + "source_domain": "B3_Land_Valuation" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "title_hint": "성수동 대지/건물 점유", + "summary_hint": "이문호가 성수동 대지와 건물을 박성희에게 인도하고, 박성희가 관리 및 사용권 귀속", + "conflict_patterns": [], + "asset_bindings": [ + "SCH-016-01", + "SCH-016-02" + ], + "parties_hint": [ + "이문호", + "박성희" + ], + "temporal_hint": "2024-11-20~현재", + "hazard_flags": [ + "current_state_expressions", + "mixed_contract_candidates" + ], + "legal_effect_tags": [ + "possession" + ], + "event_state_type": "state", + "action_summary": "박성희의 성수동 대지 및 건물 점유", + "time_interval_hint": "2024-11-20 이후", + "asset_binding_hint": "성수동 256 대지 및 지상 건물", + "mixed_contract_split_hint": "성수동 대지 임대차와 성수동 건물 매매가 혼합된 거래로 인한 인도/점유", + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "valuation_rows": [], + "valuation_matrix": [], + "valuation_object": null, + "use_gain_facts": [], + "possession_state": { + "asset_ref": "SCH-016-01", + "possession_events": [ + { + "possessor_candidates": [ + "박성희" + ], + "start_date": "2024-11-20", + "end_date": null, + "source_event_candidate_ids": [ + "EVT-006-07" + ], + "evidence_indexes": [ + "E-006" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "임대차", + "asserted_by": "박성희", + "assertion_date": "2024-11-20", + "source_event_candidate_ids": [ + "EVT-006-03" + ], + "evidence_indexes": [ + "E-006" + ] + } + ] + }, + "delivery_or_possession_start_hints": [ + "2024-10-10 이문호 인도", + "2024-11-20 박성희 인도" + ], + "unregistered_building_hints": [ + "성수동 건물 보존등기 미경료 상태에서 거래" + ], + "liability_overlap_hints": [] + } + } + }, + { + "BO_ID": "bh23", + "id": "bh23", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "신림로 상가 및 포천시 임야 가치평가(감정평가서)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "가치평가", + "감정평가", + "valuation" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "신림로 상가 및 포천시 임야 가치평가(감정평가서)", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "감정평가서" + ], + "Evidence": [ + { + "evidence_index": "E-004", + "source_title": "감정평가서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-004" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-004-01", + "EVT-004-02" + ], + "source_meeting_clause_ids": [], + "source_domain": "B3_Land_Valuation" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "title_hint": "신림로 상가 및 포천시 임야 감정평가", + "summary_hint": "신림로 상가와 포천시 임야에 대한 감정평가 및 시가 산정", + "conflict_patterns": [], + "asset_bindings": [ + "SCH-016-03" + ], + "parties_hint": [], + "temporal_hint": "2022년~현재", + "hazard_flags": [ + "current_state_expressions" + ], + "legal_effect_tags": [ + "valuation" + ], + "event_state_type": "state", + "action_summary": "신림로 상가 및 포천시 임야 시가 평가", + "time_interval_hint": "2024-09-13", + "asset_binding_hint": "신림로 상가, 포천시 임야", + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "valuation_rows": [], + "valuation_matrix": [], + "valuation_object": "신림로 상가, 포천시 임야", + "use_gain_facts": [], + "possession_state": { + "asset_ref": null, + "possession_events": [], + "legal_cause_assertions": [] + }, + "delivery_or_possession_start_hints": [], + "unregistered_building_hints": [], + "liability_overlap_hints": [] + } + } + }, + { + "BO_ID": "bh24", + "id": "bh24", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "평택시 서정빌라 101호 점유 및 유치권 행사", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "점유", + "거주", + "유치권", + "수익", + "possession", + "defense_assertion_benefit_applied_to_secured_claim" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "평택시 서정빌라 101호 점유 및 유치권 행사", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "답변서" + ], + "Evidence": [ + { + "evidence_index": "E-011", + "source_title": "답변서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-011" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-011-02", + "EVT-011-03" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-가", + "s03_client_requests:4" + ], + "source_domain": "B3_Land_Valuation" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "title_hint": "서정빌라 점유 및 유치권주장", + "summary_hint": "박광윤이 평택시 서정빌라를 점유·거주·임대하여 수익을 취하며, 피담보채권 충당 항변", + "conflict_patterns": [ + "유치권", + "수익", + "항변" + ], + "asset_bindings": [ + "SCH-016-04" + ], + "parties_hint": [ + "박광윤" + ], + "temporal_hint": "2022-02-01 ~ 현재", + "hazard_flags": [ + "current_state_expressions", + "conflict_pattern_expressions" + ], + "legal_effect_tags": [ + "possession", + "defense_assertion_benefit_applied_to_secured_claim" + ], + "event_state_type": "state", + "action_summary": "박광윤의 평택시 서정빌라 거주 및 유치권 행사", + "time_interval_hint": "2024-10-31 기준 현재진행", + "asset_binding_hint": "평택시 서정3길 123 서정빌라 101호", + "mixed_contract_split_hint": null, + "conflict_pattern_hint": "유치권 소멸 청구(양수인의 청구)와 유치권자의 피담보채권 충당 항변 충돌", + "candidate_details": {}, + "facts": {}, + "valuation_rows": [], + "valuation_matrix": [], + "valuation_object": null, + "use_gain_facts": [ + "2022. 2. 1.부터 7. 31.까지 임대 사용수익", + "그 후 현재까지 가족과 함께 거주" + ], + "possession_state": { + "asset_ref": "SCH-016-04", + "possession_events": [ + { + "possessor_candidates": [ + "박광윤" + ], + "start_date": "2022-02-01", + "end_date": "2024-10-31", + "source_event_candidate_ids": [ + "EVT-011-02" + ], + "evidence_indexes": [ + "E-011" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "유치권행사중", + "asserted_by": "박광윤", + "assertion_date": "2024-10-31", + "source_event_candidate_ids": [ + "EVT-011-03" + ], + "evidence_indexes": [ + "E-011" + ] + } + ] + }, + "delivery_or_possession_start_hints": [], + "unregistered_building_hints": [], + "liability_overlap_hints": [] + } + } + }, + { + "BO_ID": "bh25", + "id": "bh25", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "평택시 서정빌라 차임 시세 평가", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "가치평가", + "차임", + "valuation" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "평택시 서정빌라 차임 시세 평가", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [ + "임료시세확인서" + ], + "Evidence": [ + { + "evidence_index": "E-023", + "source_title": "임료시세확인서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-023" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-023-01" + ], + "source_meeting_clause_ids": [], + "source_domain": "B3_Land_Valuation" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "title_hint": "서정빌라 임료시세확인", + "summary_hint": "2022. 10. 기준 평택시 서정빌라 월세 200만 원 시세 평가", + "conflict_patterns": [], + "asset_bindings": [ + "SCH-016-04" + ], + "parties_hint": [ + "김민식" + ], + "temporal_hint": "2022-10", + "hazard_flags": [ + "current_state_expressions" + ], + "legal_effect_tags": [ + "valuation" + ], + "event_state_type": "state", + "action_summary": "서정빌라 101호 월 차임 시세 확인", + "time_interval_hint": "2022-10-01", + "asset_binding_hint": "서정빌라 101호", + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "valuation_rows": [], + "valuation_matrix": [], + "valuation_object": "평택시 서정빌라", + "use_gain_facts": [], + "possession_state": { + "asset_ref": null, + "possession_events": [], + "legal_cause_assertions": [] + }, + "delivery_or_possession_start_hints": [], + "unregistered_building_hints": [], + "liability_overlap_hints": [] + } + } + }, + { + "BO_ID": "bh26", + "id": "bh26", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "흑석동 상가 점유 및 운영 (맛나식당)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "인도", + "점유", + "운영", + "사업자등록", + "possession" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "흑석동 상가 점유 및 운영 (맛나식당)", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "부동산 매매계약서", + "사업자등록증", + "임대차계약서" + ], + "Evidence": [ + { + "evidence_index": "E-017", + "source_title": "부동산 매매계약서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + }, + { + "evidence_index": "E-018", + "source_title": "사업자등록증", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + }, + { + "evidence_index": "E-022", + "source_title": "임대차계약서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-017-04", + "EVT-018-01", + "EVT-022-03" + ], + "source_meeting_clause_ids": [ + "s01_consultation:5-가", + "s01_consultation:5-나", + "s01_consultation:5-라" + ], + "source_domain": "B3_Land_Valuation" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "title_hint": "흑석동 상가 점유/운영", + "summary_hint": "양정숙이 흑석동 상가를 이수인으로부터 인도받아 맛나식당을 운영 중임", + "conflict_patterns": [ + "해제", + "인도" + ], + "asset_bindings": [ + "SCH-016-05" + ], + "parties_hint": [ + "양정숙" + ], + "temporal_hint": "2024-02-01 ~ 현재", + "hazard_flags": [ + "current_state_expressions", + "mixed_contract_candidates" + ], + "legal_effect_tags": [ + "possession" + ], + "event_state_type": "state", + "action_summary": "양정숙의 흑석동 상가 맛나식당 운영", + "time_interval_hint": "2024-02-01 이후", + "asset_binding_hint": "흑석동 201호", + "mixed_contract_split_hint": "매수인(이수인)의 임대차와 매도인(윤건우)의 매매 해제 주장으로 인한 점유 충돌", + "conflict_pattern_hint": "매매 해제로 인한 점유권한 상실 여부", + "candidate_details": {}, + "facts": {}, + "valuation_rows": [], + "valuation_matrix": [], + "valuation_object": null, + "use_gain_facts": [], + "possession_state": { + "asset_ref": "SCH-016-05", + "possession_events": [ + { + "possessor_candidates": [ + "양정숙" + ], + "start_date": "2024-02-01", + "end_date": null, + "source_event_candidate_ids": [ + "EVT-022-03", + "EVT-018-01" + ], + "evidence_indexes": [ + "E-018", + "E-022" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "임대차", + "asserted_by": "양정숙", + "assertion_date": "2024-02-01", + "source_event_candidate_ids": [ + "EVT-022-03" + ], + "evidence_indexes": [ + "E-022" + ] + } + ] + }, + "delivery_or_possession_start_hints": [ + "2024-01-01 윤건우가 이수인에게 인도", + "2024-02-01 이수인이 양정숙에게 인도" + ], + "unregistered_building_hints": [], + "liability_overlap_hints": [] + } + } + }, + { + "BO_ID": "bh27", + "id": "bh27", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "매매" + }, + "Action": "채무자 오국한이 수익자 오민한에게 책임재산인 신림동 상가를 1억5천만원에 매도하여 소유권이전등기 경료", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "사해행위", + "매매계약", + "소유권이전", + "책임재산감소", + "fraudulent_act", + "매매" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "채무자 오국한이 수익자 오민한에게 책임재산인 신림동 상가를 1억5천만원에 매도하여 소유권이전등기 경료", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [ + "등기사항전부증명서 (말소사항 포함) -건물" + ], + "Evidence": [ + { + "evidence_index": "E-014", + "source_title": "등기사항전부증명서 (말소사항 포함) -건물", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-014" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-014-03" + ], + "source_meeting_clause_ids": [], + "source_domain": "B4_Actio" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "fraudulent_transfer_BO", + "title_hint": "사해행위 성립 (신림동 상가 매매)", + "summary_hint": "채무자 오국한이 수익자 오민한에게 책임재산 처분", + "conflict_patterns": [], + "asset_bindings": [ + "신림동 상가" + ], + "parties_hint": [ + "오국한(채무자)", + "오민한(수익자)" + ], + "temporal_hint": "2023-03-17", + "hazard_flags": [], + "legal_effect_tags": [ + "fraudulent_act" + ], + "event_state_type": "event", + "action_summary": "신림동 상가 1억5천만원 매매 및 소유권이전", + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "preserved_claim_bundle": {}, + "fraudulent_transfer_facts": [ + { + "transfer_date": "2023-03-17", + "transfer_type": "매매", + "target_asset": "신림동 상가" + } + ], + "debtor_act_hints": [ + "채무자가 유일한 재산을 처분하여 무자력 야기" + ], + "balance_sheet": { + "asset_decrease_hints": [], + "insolvency_hints": [], + "common_collateral_impairment_hints": [] + }, + "encumbrance_timeline": [], + "valuation_matrix": [], + "remedy_mode_hints": [], + "value_compensation_cap_hints": [], + "beneficiary_transferee_hints": [ + "오민한" + ], + "bad_faith_good_faith_hints": [ + "수익자 악의 추정" + ], + "defense_rebuttal": [] + } + } + }, + { + "BO_ID": "bh28", + "id": "bh28", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "소비대차" + }, + "Action": "강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "피보전채권", + "대여금채권", + "preserved_claim", + "소비대차" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [ + "차용증" + ], + "Evidence": [ + { + "evidence_index": "E-024", + "source_title": "차용증", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-024" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-024-01", + "EVT-024-02" + ], + "source_meeting_clause_ids": [], + "source_domain": "B4_Actio" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "preserved_claim_bundle_BO", + "title_hint": "피보전채권 성립", + "summary_hint": "강용원의 오국한에 대한 대여금 2억원 피보전채권 번들", + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [ + "강용원(채권자)", + "오국한(채무자)" + ], + "temporal_hint": "사해행위(2023-03-17) 이전인 2022년 성립", + "hazard_flags": [], + "legal_effect_tags": [ + "preserved_claim" + ], + "event_state_type": "event", + "action_summary": "강용원이 오국한에게 2억원 대여", + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "preserved_claim_bundle": { + "claims": [ + { + "date": "2022-04-18", + "amount": 100000000 + }, + { + "date": "2022-05-18", + "amount": 100000000 + } + ] + }, + "fraudulent_transfer_facts": [], + "debtor_act_hints": [], + "balance_sheet": { + "asset_decrease_hints": [], + "insolvency_hints": [], + "common_collateral_impairment_hints": [] + }, + "encumbrance_timeline": [], + "valuation_matrix": [], + "remedy_mode_hints": [], + "value_compensation_cap_hints": [], + "beneficiary_transferee_hints": [], + "bad_faith_good_faith_hints": [], + "defense_rebuttal": [] + } + } + }, + { + "BO_ID": "bh29", + "id": "bh29", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "변제 및 말소" + }, + "Action": "수익자 오민한이 사해행위 이후 피담보채무 1억3천만원을 대위변제하고 2번 근저당권을 말소시킴으로써 원물반환이 불가능해져 가액배상 요건 성립", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "원물반환불능", + "가액배상", + "근저당권말소", + "value_compensation", + "remedy_cap", + "변제 및 말소" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "수익자 오민한이 사해행위 이후 피담보채무 1억3천만원을 대위변제하고 2번 근저당권을 말소시킴으로써 원물반환이 불가능해져 가액배상 요건 성립", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [ + "등기사항전부증명서 (말소사항 포함) -건물" + ], + "Evidence": [ + { + "evidence_index": "E-014", + "source_title": "등기사항전부증명서 (말소사항 포함) -건물", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-014" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-014-04", + "EVT-014-05", + "EVT-014-06" + ], + "source_meeting_clause_ids": [ + "s01_consultation:3-라" + ], + "source_domain": "B4_Actio" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "remedy_mode_decision_BO", + "title_hint": "가액배상 방식 결정 및 한도", + "summary_hint": "수익자의 대위변제에 의한 저당권 소멸과 가액배상 산정 기준", + "conflict_patterns": [ + "근저당 말소로 인한 반환방법 변경" + ], + "asset_bindings": [ + "신림동 상가" + ], + "parties_hint": [ + "오민한(수익자)" + ], + "temporal_hint": "2024-10-17 근저당 말소 기준", + "hazard_flags": [ + "amount_or_date_uncertain" + ], + "legal_effect_tags": [ + "value_compensation", + "remedy_cap" + ], + "event_state_type": "event", + "action_summary": "강용일 명의 2번 근저당권 말소(수익자 변제)", + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "preserved_claim_bundle": {}, + "fraudulent_transfer_facts": [], + "debtor_act_hints": [], + "balance_sheet": { + "asset_decrease_hints": [], + "insolvency_hints": [], + "common_collateral_impairment_hints": [] + }, + "encumbrance_timeline": [ + { + "date": "2021-08-01", + "event": "나임오 1번 근저당 설정(5000만원)" + }, + { + "date": "2022-04-18", + "event": "강용일 2번 근저당 설정(1.5억원)" + }, + { + "date": "2024-10-17", + "event": "강용일 2번 근저당 말소(오민한 대위변제)" + } + ], + "valuation_matrix": [], + "remedy_mode_hints": [ + "원물반환 불가", + "가액배상" + ], + "value_compensation_cap_hints": [ + "부동산 가액에서 선순위 저당권 피담보채권액을 공제한 잔액 한도" + ], + "beneficiary_transferee_hints": [], + "bad_faith_good_faith_hints": [], + "defense_rebuttal": [] + } + } + }, + { + "BO_ID": "bh30", + "id": "bh30", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "무자력", + "책임재산", + "가치평가", + "insolvency_check" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "감정평가서" + ], + "Evidence": [ + { + "evidence_index": "E-004", + "source_title": "감정평가서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-004" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-004-01", + "EVT-004-02" + ], + "source_meeting_clause_ids": [], + "source_domain": "B4_Actio" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "actio_balance_sheet_BO", + "title_hint": "채무자 무자력 증명", + "summary_hint": "감정평가를 통한 처분당시 오국한의 자산상태 입증", + "conflict_patterns": [], + "asset_bindings": [ + "신림동 상가", + "포천시 임야" + ], + "parties_hint": [ + "오국한" + ], + "temporal_hint": "2023-03-17 기준", + "hazard_flags": [ + "amount_or_date_uncertain" + ], + "legal_effect_tags": [ + "insolvency_check" + ], + "event_state_type": "state", + "action_summary": "처분행위 당시 자산 및 부채 비교", + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "preserved_claim_bundle": {}, + "fraudulent_transfer_facts": [], + "debtor_act_hints": [], + "balance_sheet": { + "asset_decrease_hints": [ + "신림동 상가 처분으로 인한 적극재산 감소" + ], + "insolvency_hints": [ + "채무초과 상태" + ], + "common_collateral_impairment_hints": [ + "공동담보의 부족 초래" + ] + }, + "encumbrance_timeline": [], + "valuation_matrix": [], + "remedy_mode_hints": [], + "value_compensation_cap_hints": [], + "beneficiary_transferee_hints": [], + "bad_faith_good_faith_hints": [], + "defense_rebuttal": [] + } + } + }, + { + "BO_ID": "bh31", + "id": "bh31", + "BOType": "state", + "ActionType": "소송행위(litigation acts)", + "JuristicAct": { + "label": "항변" + }, + "Action": "수익자 오민한의 항변 (명의신탁 주장, 매수자금 및 대여금 참작 주장, 채권 상계 주장)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "수익자항변", + "명의신탁", + "상계항변", + "선의항변", + "defense_assertion", + "항변" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "수익자 오민한의 항변 (명의신탁 주장, 매수자금 및 대여금 참작 주장, 채권 상계 주장)", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "각서 및 금전소비대차계약서", + "내용증명에 대한 답신" + ], + "Evidence": [ + { + "evidence_index": "E-003", + "source_title": "각서 및 금전소비대차계약서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + }, + { + "evidence_index": "E-010", + "source_title": "내용증명에 대한 답신", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-003", + "E-010" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-003-01", + "EVT-003-02", + "EVT-010-03" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:3-가", + "s02_party_assertions:3-나" + ], + "source_domain": "B4_Actio" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "defense_rebuttal_matrix_BO", + "title_hint": "수익자 오민한의 항변 분석", + "summary_hint": "계약명의신탁, 자금제공 참작, 상계 등 수익자 항변 검토", + "conflict_patterns": [ + "사해행위 성립 부정 주장", + "배상금 감액 주장" + ], + "asset_bindings": [ + "신림동 상가" + ], + "parties_hint": [ + "오민한" + ], + "temporal_hint": null, + "hazard_flags": [ + "legal_theory_required" + ], + "legal_effect_tags": [ + "defense_assertion" + ], + "event_state_type": "state", + "action_summary": "수익자가 사해의사 부정 및 상계항변 제출", + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "preserved_claim_bundle": {}, + "fraudulent_transfer_facts": [], + "debtor_act_hints": [], + "balance_sheet": { + "asset_decrease_hints": [], + "insolvency_hints": [], + "common_collateral_impairment_hints": [] + }, + "encumbrance_timeline": [], + "valuation_matrix": [], + "remedy_mode_hints": [], + "value_compensation_cap_hints": [], + "beneficiary_transferee_hints": [], + "bad_faith_good_faith_hints": [], + "defense_rebuttal": [ + { + "defense_type": "계약명의신탁", + "content": "오국한 명의 취득이 수익자 자금에 의한 계약명의신탁이므로 사해행위가 아니라는 주장" + }, + { + "defense_type": "배상금감액", + "content": "수익자가 제공한 매수자금 4000만원 및 대여금 1000만원 참작 주장" + }, + { + "defense_type": "상계항변", + "content": "오민한의 5000만원 채권으로 강용원 채권을 상계한다는 주장" + } + ] + } + } + }, + { + "BO_ID": "bh32", + "id": "bh32", + "BOType": "event", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": "상속개시" + }, + "Action": "강석우 사망으로 인한 정유심, 강용원의 성수동 대지 지분 상속", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "상속개시" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "강석우 사망으로 인한 정유심, 강용원의 성수동 대지 지분 상속", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "등기사항전부증명서(말소사항 포함)-토지" + ], + "Evidence": [ + { + "evidence_index": "E-013", + "source_title": "등기사항전부증명서(말소사항 포함)-토지", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-013" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-013-01" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-가" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "general_succession_status": { + "status_raw_hint": "강석우 사망으로 지분 상속" + } + } + } + }, + { + "BO_ID": "bh33", + "id": "bh33", + "BOType": "event", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": "상속포기" + }, + "Action": "망 강호연의 사망에 따른 상속개시 및 자녀 강형수, 강지수의 상속포기", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "상속포기" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "망 강호연의 사망에 따른 상속개시 및 자녀 강형수, 강지수의 상속포기", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "가정법원심판_강호연", + "가족관계증명서" + ], + "Evidence": [ + { + "evidence_index": "E-001", + "source_title": "가정법원심판_강호연", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + }, + { + "evidence_index": "E-002", + "source_title": "가족관계증명서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-001", + "E-002" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-001-01", + "EVT-001-02", + "EVT-002-01" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-가" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "general_succession_status": { + "status_raw_hint": "강호연 사망, 상속인 상속포기 심판수리 (2024-09-20)" + } + } + } + }, + { + "BO_ID": "bh34", + "id": "bh34", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "최고" + }, + "Action": "강용원이 김선웅에게 물품대금 지급을 최고하는 통지서 발송 및 도달", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "최고" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "강용원이 김선웅에게 물품대금 지급을 최고하는 통지서 발송 및 도달", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "최고 통지서" + ], + "Evidence": [ + { + "evidence_index": "E-027", + "source_title": "최고 통지서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-027" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-027-01", + "EVT-027-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:1-나" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "notice_lifecycle": { + "notice_id": "E-027-NT-01", + "notice_type": "최고", + "drafted_date": null, + "dispatch_date": "2024-05-04", + "arrival_date": "2024-05-06", + "received_date": "2024-05-06", + "legal_effect_date_candidate": "2024-05-06", + "requires_receipt_for_effect": null, + "sender_candidates": [ + "강용원" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "김선웅" + ], + "receipt_evidence_refs": [ + "우편물 배달 증명서" + ], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + } + }, + { + "BO_ID": "bh35", + "id": "bh35", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "최고" + }, + "Action": "강용원이 오민한에게 사해행위취소 및 신림동 상가 소유권 반환을 요구하는 내용증명 발송", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "최고" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "강용원이 오민한에게 사해행위취소 및 신림동 상가 소유권 반환을 요구하는 내용증명 발송", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "내용증명", + "내용증명에 대한 답신" + ], + "Evidence": [ + { + "evidence_index": "E-008", + "source_title": "내용증명", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + }, + { + "evidence_index": "E-010", + "source_title": "내용증명에 대한 답신", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-008", + "E-010" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-008-01", + "EVT-010-01" + ], + "source_meeting_clause_ids": [], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "notice_lifecycle": { + "notice_id": "E-008-NT-01", + "notice_type": "기타", + "drafted_date": "2024-09-15", + "dispatch_date": "2024-09-15", + "arrival_date": "2024-09-20", + "received_date": "2024-09-20", + "legal_effect_date_candidate": "2024-09-20", + "requires_receipt_for_effect": null, + "sender_candidates": [ + "강용원" + ], + "legal_sender_after_resolution": { + "status": "unresolved", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "오민한" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [ + "오민한이 이행최고서를 2024. 9. 20. 수령함" + ] + } + } + } + }, + { + "BO_ID": "bh36", + "id": "bh36", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "강용원과 양정숙이 박광윤에게 평택시 빌라 유치권소멸을 청구하는 통지서 발송 및 도달", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "통지" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "강용원과 양정숙이 박광윤에게 평택시 빌라 유치권소멸을 청구하는 통지서 발송 및 도달", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "답변서", + "소멸청구 통지서" + ], + "Evidence": [ + { + "evidence_index": "E-011", + "source_title": "답변서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + }, + { + "evidence_index": "E-019", + "source_title": "소멸청구 통지서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-011", + "E-019" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-011-01" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-다" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "notice_lifecycle": { + "notice_id": "E-019-NT-01", + "notice_type": "유치권소멸청구통지", + "drafted_date": "2024-08-29", + "dispatch_date": "2024-08-29", + "arrival_date": "2024-09-01", + "received_date": "2024-09-01", + "legal_effect_date_candidate": "2024-09-01", + "requires_receipt_for_effect": true, + "sender_candidates": [ + "강용원", + "양정숙" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "박광윤" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + } + }, + { + "BO_ID": "bh37", + "id": "bh37", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "오민한이 강용원에게 김선웅의 채무에 대한 책임을 부인하는 통지서 발송", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "통지" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "오민한이 강용원에게 김선웅의 채무에 대한 책임을 부인하는 통지서 발송", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "통지서" + ], + "Evidence": [ + { + "evidence_index": "E-028", + "source_title": "통지서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-028" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-028-01" + ], + "source_meeting_clause_ids": [], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "notice_lifecycle": { + "notice_id": "E-028-NT-01", + "notice_type": "기타", + "drafted_date": null, + "dispatch_date": "2024-12-27", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [ + "오민한" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "강용원" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + } + }, + { + "BO_ID": "bh38", + "id": "bh38", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "오민한이 강용원에게 내용증명에 대한 답신 발송", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "통지" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "오민한이 강용원에게 내용증명에 대한 답신 발송", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "내용증명에 대한 답신" + ], + "Evidence": [ + { + "evidence_index": "E-010", + "source_title": "내용증명에 대한 답신", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-010" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-010-02" + ], + "source_meeting_clause_ids": [], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "notice_lifecycle": { + "notice_id": "E-010-NT-01", + "notice_type": "기타", + "drafted_date": null, + "dispatch_date": "2024-09-30", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [ + "오민한" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "강용원" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + } + }, + { + "BO_ID": "bh39", + "id": "bh39", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "윤건우가 양정숙에게 흑석동 상가 임대차계약 해지 및 명도를 요구하는 내용증명 통지", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "통지" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "윤건우가 양정숙에게 흑석동 상가 임대차계약 해지 및 명도를 요구하는 내용증명 통지", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "내용증명" + ], + "Evidence": [ + { + "evidence_index": "E-009", + "source_title": "내용증명", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-009" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-009-01", + "EVT-009-02" + ], + "source_meeting_clause_ids": [], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "notice_lifecycle": { + "notice_id": "E-009-NT-01", + "notice_type": "기타", + "drafted_date": "2024-11-29", + "dispatch_date": "2024-11-29", + "arrival_date": "2024-11-30", + "received_date": "2024-11-30", + "legal_effect_date_candidate": "2024-11-30", + "requires_receipt_for_effect": true, + "sender_candidates": [ + "윤건우" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "양정숙" + ], + "receipt_evidence_refs": [ + "우편물배달증명서" + ], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + } + }, + { + "BO_ID": "bh40", + "id": "bh40", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "윤건우가 이수인에게 흑석동 상가 매매계약 해제 통지서 발송 및 도달", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "통지" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "윤건우가 이수인에게 흑석동 상가 매매계약 해제 통지서 발송 및 도달", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "해제 통지서" + ], + "Evidence": [ + { + "evidence_index": "E-030", + "source_title": "해제 통지서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-030" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-030-01", + "EVT-030-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:5-라" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "notice_lifecycle": { + "notice_id": "E-030-NT-01", + "notice_type": "해제통지", + "drafted_date": null, + "dispatch_date": "2024-05-15", + "arrival_date": "2024-05-16", + "received_date": "2024-05-16", + "legal_effect_date_candidate": "2024-05-16", + "requires_receipt_for_effect": true, + "sender_candidates": [ + "윤건우" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "이수인" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + } + }, + { + "BO_ID": "bh41", + "id": "bh41", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "이수인이 윤건우에게 흑석동 상가 매매대금 감액 및 제3자 명의 등기 이전 요청 통지", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "통지" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "이수인이 윤건우에게 흑석동 상가 매매대금 감액 및 제3자 명의 등기 이전 요청 통지", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "통지서_이수인_윤건우" + ], + "Evidence": [ + { + "evidence_index": "E-029", + "source_title": "통지서_이수인_윤건우", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-029" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-029-01" + ], + "source_meeting_clause_ids": [ + "s01_consultation:5-다" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "notice_lifecycle": { + "notice_id": "E-029-NT-01", + "notice_type": "기타", + "drafted_date": null, + "dispatch_date": "2024-03-30", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [ + "이수인" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "윤건우" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + } + }, + { + "BO_ID": "bh42", + "id": "bh42", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "주식회사 대한은행이 강용원의 성수동 대지 근저당권 말소 신청을 거부하는 통지 발송", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "통지" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "주식회사 대한은행이 강용원의 성수동 대지 근저당권 말소 신청을 거부하는 통지 발송", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "등기말소 신청에 대한 답신" + ], + "Evidence": [ + { + "evidence_index": "E-012", + "source_title": "등기말소 신청에 대한 답신", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-012" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-012-01" + ], + "source_meeting_clause_ids": [], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "notice_lifecycle": { + "notice_id": "E-012-NT-01", + "notice_type": "기타", + "drafted_date": "2024-12-22", + "dispatch_date": "2024-12-22", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [ + "주식회사 대한은행" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "강용원" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + } + }, + { + "BO_ID": "bh43", + "id": "bh43", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "목적물 특정", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "목적물 특정", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "등기사항전부증명서(말소사항 포함)-토지", + "등기사항전부증명서 (말소사항 포함) -건물", + "등기사항전부증명서(말소사항 포함)-집합건물", + "별지 목록" + ], + "Evidence": [ + { + "evidence_index": "E-013", + "source_title": "등기사항전부증명서(말소사항 포함)-토지", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + }, + { + "evidence_index": "E-014", + "source_title": "등기사항전부증명서 (말소사항 포함) -건물", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + }, + { + "evidence_index": "E-015", + "source_title": "등기사항전부증명서(말소사항 포함)-집합건물", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + }, + { + "evidence_index": "E-016", + "source_title": "별지 목록", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "asset_alias": [ + { + "asset_ref": "성수동 대지", + "alias_strings": [ + "성수동 대지", + "서울 성동구 성수동 256" + ], + "canonical_label_candidate": "1. 서울 성동구 성수동 256 대 330㎡", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-01" + ], + "registry_refs": [ + { + "registry_ref": "REG-013-01", + "receipt_no_refs": [ + "제1453호" + ] + }, + { + "registry_ref": "REG-013-02", + "receipt_no_refs": [ + "제7026호" + ] + }, + { + "registry_ref": "REG-013-03", + "receipt_no_refs": [ + "제11202호" + ] + }, + { + "registry_ref": "REG-013-04", + "receipt_no_refs": [ + "제7202호" + ] + }, + { + "registry_ref": "REG-013-05", + "receipt_no_refs": [ + "제12402호" + ] + }, + { + "registry_ref": "REG-013-06", + "receipt_no_refs": [ + "제25797호" + ] + }, + { + "registry_ref": "REG-013-07", + "receipt_no_refs": [ + "제25799호" + ] + } + ], + "source_evidence_indexes": [ + "E-013", + "E-016" + ] + }, + { + "asset_ref": "성수동 건물", + "alias_strings": [ + "성수동 건물", + "서울 성동구 성수동 256 상가" + ], + "canonical_label_candidate": "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-02" + ], + "registry_refs": [], + "source_evidence_indexes": [ + "E-016" + ] + }, + { + "asset_ref": "신림동 상가", + "alias_strings": [ + "신림동 상가", + "서울 관악구 신림로 115" + ], + "canonical_label_candidate": "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-03" + ], + "registry_refs": [ + { + "registry_ref": "REG-014-01", + "receipt_no_refs": [ + "제24759호" + ] + }, + { + "registry_ref": "REG-014-02", + "receipt_no_refs": [ + "제56457호" + ] + }, + { + "registry_ref": "REG-014-03", + "receipt_no_refs": [ + "제11593호" + ] + }, + { + "registry_ref": "REG-014-04", + "receipt_no_refs": [ + "제50099호" + ] + }, + { + "registry_ref": "REG-014-05", + "receipt_no_refs": [ + "제26597호" + ] + }, + { + "registry_ref": "REG-014-06", + "receipt_no_refs": [ + "제74586호" + ] + } + ], + "source_evidence_indexes": [ + "E-014", + "E-016" + ] + }, + { + "asset_ref": "평택시 빌라", + "alias_strings": [ + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123" + ], + "canonical_label_candidate": "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-04" + ], + "registry_refs": [ + { + "registry_ref": "REG-015-01", + "receipt_no_refs": [ + "제7233호" + ] + }, + { + "registry_ref": "REG-015-02", + "receipt_no_refs": [ + "제7871호" + ] + }, + { + "registry_ref": "REG-015-03", + "receipt_no_refs": [ + "제11593호" + ] + }, + { + "registry_ref": "REG-015-04", + "receipt_no_refs": [ + "제12321호" + ] + } + ], + "source_evidence_indexes": [ + "E-015", + "E-016" + ] + }, + { + "asset_ref": "흑석동 상가", + "alias_strings": [ + "흑석동 상가", + "서울 동작구 흑석로 80" + ], + "canonical_label_candidate": "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-05" + ], + "registry_refs": [], + "source_evidence_indexes": [ + "E-016" + ] + } + ] + } + } + }, + { + "BO_ID": "bh44", + "id": "bh44", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "박광윤의 평택시 서정빌라 101호 거주 및 유치권 행사 상태", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "박광윤의 평택시 서정빌라 101호 거주 및 유치권 행사 상태", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "답변서" + ], + "Evidence": [ + { + "evidence_index": "E-011", + "source_title": "답변서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-011" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-011-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-가" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "lien_state": { + "lien_id": "B5:LIEN:001", + "asset_ref": "평택시 빌라", + "lien_holder_candidates": [ + "박광윤" + ], + "secured_claim_hint": { + "debtor_candidate": "김일동", + "basis_text": "공사잔금 3억 원 미지급", + "claimed_remaining_amount_hint": "300,000,000원", + "evidence_indexes": [ + "E-011" + ] + }, + "possession_event_refs": [], + "assertion_events": [ + "EVT-011-02" + ], + "extinction_notice_refs": [ + "E-019-NT-01" + ], + "status_raw_hint": "유치권 행사 중" + }, + "possession_state": { + "asset_ref": "평택시 빌라", + "possession_events": [ + { + "possessor_candidates": [ + "박광윤" + ], + "start_date": null, + "end_date": null, + "source_event_candidate_ids": [ + "EVT-011-02" + ], + "evidence_indexes": [ + "E-011" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "유치권행사중", + "asserted_by": "박광윤", + "assertion_date": "2024-10-31", + "source_event_candidate_ids": [ + "EVT-011-02" + ], + "evidence_indexes": [ + "E-011" + ] + } + ] + } + } + } + }, + { + "BO_ID": "bh45", + "id": "bh45", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "박성희의 성수동 대지 및 건물 점유", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "박성희의 성수동 대지 및 건물 점유", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "계약서" + ], + "Evidence": [ + { + "evidence_index": "E-006", + "source_title": "계약서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-006" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-006-07" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-사" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "possession_state": { + "asset_ref": "성수동 대지 및 성수동 건물", + "possession_events": [ + { + "possessor_candidates": [ + "박성희" + ], + "start_date": "2024-11-20", + "end_date": null, + "source_event_candidate_ids": [ + "EVT-006-07" + ], + "evidence_indexes": [ + "E-006" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "임대차", + "asserted_by": "박성희", + "assertion_date": "2024-11-20", + "source_event_candidate_ids": [ + "EVT-006-07" + ], + "evidence_indexes": [ + "E-006" + ] + } + ] + } + } + } + }, + { + "BO_ID": "bh46", + "id": "bh46", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "양정숙의 흑석동 상가 점유 및 식당 운영 상태", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "양정숙의 흑석동 상가 점유 및 식당 운영 상태", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "사업자등록증", + "임대차계약서" + ], + "Evidence": [ + { + "evidence_index": "E-018", + "source_title": "사업자등록증", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + }, + { + "evidence_index": "E-022", + "source_title": "임대차계약서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-018", + "E-022" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-018-01", + "EVT-022-03" + ], + "source_meeting_clause_ids": [ + "s01_consultation:5-나" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "possession_state": { + "asset_ref": "흑석동 상가", + "possession_events": [ + { + "possessor_candidates": [ + "양정숙" + ], + "start_date": "2024-02-01", + "end_date": null, + "source_event_candidate_ids": [ + "EVT-018-01", + "EVT-022-03" + ], + "evidence_indexes": [ + "E-018", + "E-022" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "임대차", + "asserted_by": "양정숙", + "assertion_date": "2024-02-01", + "source_event_candidate_ids": [ + "EVT-022-03" + ], + "evidence_indexes": [ + "E-022" + ] + } + ] + } + } + } + }, + { + "BO_ID": "bh47", + "id": "bh47", + "BOType": "state", + "ActionType": "소송행위(litigation acts)", + "JuristicAct": { + "label": "상계" + }, + "Action": "오민한의 강용원에 대한 자신의 5,000만 원 채권 상계 주장 항변", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "상계" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "오민한의 강용원에 대한 자신의 5,000만 원 채권 상계 주장 항변", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "EvidenceTitles": [ + "내용증명에 대한 답신" + ], + "Evidence": [ + { + "evidence_index": "E-010", + "source_title": "내용증명에 대한 답신", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-010" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-010-03" + ], + "source_meeting_clause_ids": [], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "setoff_or_payment_defense_hints": [ + { + "hint": "오민한이 자신의 5,000만 원 채권으로 상계하겠다고 주장" + } + ] + } + } + }, + { + "BO_ID": "bh48", + "id": "bh48", + "BOType": "state", + "ActionType": "소송행위(litigation acts)", + "JuristicAct": { + "label": "항변" + }, + "Action": "박광윤의 과거 임대료 및 거주 수익을 채무에 충당하였다는 주장 항변", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "항변" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "박광윤의 과거 임대료 및 거주 수익을 채무에 충당하였다는 주장 항변", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "답변서" + ], + "Evidence": [ + { + "evidence_index": "E-011", + "source_title": "답변서", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-011" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-011-03" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:4-나" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "defense_rebuttal": [ + { + "defense_asserted": "과거 임대료 및 거주 수익을 채무에 충당하여 소멸 주장" + } + ] + } + } + }, + { + "BO_ID": "bh49", + "id": "bh49", + "BOType": "state", + "ActionType": "소송행위(litigation acts)", + "JuristicAct": { + "label": "항변" + }, + "Action": "윤건우의 임대차 책임 부인 및 양정숙 차임 연체 주장 항변", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "항변" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "윤건우의 임대차 책임 부인 및 양정숙 차임 연체 주장 항변", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "내용증명" + ], + "Evidence": [ + { + "evidence_index": "E-009", + "source_title": "내용증명", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-009" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-009-03" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:5" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "defense_rebuttal": [ + { + "defense_asserted": "임대차 책임 부인 및 차임 연체 주장" + } + ] + } + } + }, + { + "BO_ID": "bh50", + "id": "bh50", + "BOType": "state", + "ActionType": "소송행위(litigation acts)", + "JuristicAct": { + "label": "항변" + }, + "Action": "주식회사 대한은행의 경매공신력 주장(민사집행법 제267조)에 따른 근저당 말소 불가 항변", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "항변" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "주식회사 대한은행의 경매공신력 주장(민사집행법 제267조)에 따른 근저당 말소 불가 항변", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "EvidenceTitles": [ + "등기말소 신청에 대한 답신" + ], + "Evidence": [ + { + "evidence_index": "E-012", + "source_title": "등기말소 신청에 대한 답신", + "priority_class": null, + "relevant_content": null, + "authentication_status": null, + "corroboration": null, + "selection_basis": "source_evidence_indexes membership" + } + ], + "source_evidence_indexes": [ + "E-012" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-012-02" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:2" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "defense_rebuttal": [ + { + "defense_asserted": "경매 절차의 적법성과 민사집행법 제267조에 따른 보호 주장" + } + ] + } + } + } +] diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/Fact_Ledger_base.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/Fact_Ledger_base.json new file mode 100644 index 00000000..132cec84 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/Fact_Ledger_base.json @@ -0,0 +1,4757 @@ +[ + { + "fact_id": "F-001", + "source_bo_id": "bh1", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "평택시 빌라 신축 공사도급계약 체결 및 중도금 지급", + "evidence_refs": [ + "E-007" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:011" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-001" + ], + "issue_cluster_ids": [ + "IC-001" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-007" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:011" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-001" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh1" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "평택시 빌라 신축 공사도급계약 체결 및 중도금 지급", + "derivation_basis": "source_bo_id=bh1; evidence_refs=E-007; structure_ids=LES-001", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-002", + "source_bo_id": "bh10", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "오국한에 대한 대여금 채권 등을 자동채권으로 한 상계 항변", + "evidence_refs": [ + "E-003", + "E-010" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:013" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-002" + ], + "issue_cluster_ids": [ + "IC-002" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-003", + "E-010" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:013" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-002" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh10" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "오국한에 대한 대여금 채권 등을 자동채권으로 한 상계 항변", + "derivation_basis": "source_bo_id=bh10; evidence_refs=E-003,E-010; structure_ids=LES-002", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-003", + "source_bo_id": "bh11", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "물품대금 지급 최고", + "evidence_refs": [ + "E-027" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:003" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-003" + ], + "issue_cluster_ids": [ + "IC-003" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-027" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:003" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-003" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh11" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "물품대금 지급 최고", + "derivation_basis": "source_bo_id=bh11; evidence_refs=E-027; structure_ids=LES-003", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-004", + "source_bo_id": "bh12", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "상호속용에 따른 물품대금 채무 책임 부인 통지", + "evidence_refs": [ + "E-028" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "claim_chain_ref", + "linked_structures", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:005" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-004" + ], + "issue_cluster_ids": [ + "IC-004" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-028" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:005" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-004" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh12" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "상호속용에 따른 물품대금 채무 책임 부인 통지", + "derivation_basis": "source_bo_id=bh12; evidence_refs=E-028; structure_ids=LES-004", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-005", + "source_bo_id": "bh13", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "공사대금 잔금 3억 원 미지급 상태 및 유치권 충당 항변", + "evidence_refs": [ + "E-011" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:012" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-005" + ], + "issue_cluster_ids": [ + "IC-005" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-011" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:012" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-005" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh13" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "공사대금 잔금 3억 원 미지급 상태 및 유치권 충당 항변", + "derivation_basis": "source_bo_id=bh13; evidence_refs=E-011; structure_ids=LES-005", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-006", + "source_bo_id": "bh14", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[\"AC-001\"]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "강용원은 성수동 대지에 대한 상속 및 증여로 지분을 취득하고, 오민한에게 차용금 담보를 위해 성수동 대지 지분에 대한 근저당권 설정 약정을 체결함", + "evidence_refs": [ + "E-013", + "E-025", + "E-026" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B2:001" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-006" + ], + "issue_cluster_ids": [ + "IC-006" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_index_ids": [ + "E-013", + "E-025", + "E-026" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:001" + ] + }, + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-006" + ], + "asset_cluster_ids": [ + "AC-001" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "강용원은 성수동 대지에 대한 상속 및 증여로 지분을 취득하고, 오민한에게 차용금 담보를 위해 성수동 대지 지분에 대한 근저당권 설정 약정을 체결함", + "derivation_basis": "source_bo_id=bh14; evidence_refs=E-013,E-025,E-026; structure_ids=LES-006", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-001\"]", + "candidate_structure_types": [ + "secured_debt" + ] + } + }, + { + "fact_id": "F-007", + "source_bo_id": "bh15", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[\"AC-002\"]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "신림동 상가에 강용원(등기상 강용일) 명의의 근저당권이 설정되었다가 무단 말소되었으며, 오민한은 상계를 주장함", + "evidence_refs": [ + "E-010", + "E-014" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B2:004" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-007" + ], + "issue_cluster_ids": [ + "IC-007" + ], + "asset_cluster_ids": [ + "AC-002" + ], + "evidence_index_ids": [ + "E-010", + "E-014" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:004" + ] + }, + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-007" + ], + "asset_cluster_ids": [ + "AC-002" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "신림동 상가에 강용원(등기상 강용일) 명의의 근저당권이 설정되었다가 무단 말소되었으며, 오민한은 상계를 주장함", + "derivation_basis": "source_bo_id=bh15; evidence_refs=E-010,E-014; structure_ids=LES-007", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-002\"]", + "candidate_structure_types": [ + "secured_debt" + ] + } + }, + { + "fact_id": "F-008", + "source_bo_id": "bh16", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[\"AC-003\"]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "평택시 서정빌라에 대한 보존등기 및 매매로 인한 소유권이전등기가 마쳐짐", + "evidence_refs": [ + "E-015" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B2:006" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-008" + ], + "issue_cluster_ids": [ + "IC-008" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_index_ids": [ + "E-015" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:006" + ] + }, + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-008" + ], + "asset_cluster_ids": [ + "AC-003" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "평택시 서정빌라에 대한 보존등기 및 매매로 인한 소유권이전등기가 마쳐짐", + "derivation_basis": "source_bo_id=bh16; evidence_refs=E-015; structure_ids=LES-008", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-003\"]", + "candidate_structure_types": [ + "secured_debt" + ] + } + }, + { + "fact_id": "F-009", + "source_bo_id": "bh17", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[\"AC-001\"]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "오민한이 경매를 신청하여 이문호가 매각대금을 완납하고 취득한 뒤, 대한은행이 근저당권을 설정함에 따라 원인무효 여부 및 공신력이 다투어짐", + "evidence_refs": [ + "E-012", + "E-013" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B2:003" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-009" + ], + "issue_cluster_ids": [ + "IC-009" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_index_ids": [ + "E-012", + "E-013" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:003" + ] + }, + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-009" + ], + "asset_cluster_ids": [ + "AC-001" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "오민한이 경매를 신청하여 이문호가 매각대금을 완납하고 취득한 뒤, 대한은행이 근저당권을 설정함에 따라 원인무효 여부 및 공신력이 다투어짐", + "derivation_basis": "source_bo_id=bh17; evidence_refs=E-012,E-013; structure_ids=LES-009", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-001\"]", + "candidate_structure_types": [ + "secured_debt" + ] + } + }, + { + "fact_id": "F-010", + "source_bo_id": "bh18", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "강용원이 오민한에게 4억 원을 지급하였으나 다수의 채무 중 충당할 채무를 특정하지 않음", + "evidence_refs": [], + "credibility": "low", + "legal_centrality": "critical", + "proof_strength": "low", + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B2:002" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-010" + ], + "issue_cluster_ids": [ + "IC-010" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:002" + ] + }, + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-010" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "skeleton_validation_codes": [ + "NO_EVIDENCE_REFS" + ], + "derived_fact": "강용원이 오민한에게 4억 원을 지급하였으나 다수의 채무 중 충당할 채무를 특정하지 않음", + "derivation_basis": "source_bo_id=bh18; structure_ids=LES-010", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "secured_debt" + ] + } + }, + { + "fact_id": "F-011", + "source_bo_id": "bh19", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[\"AC-003\"]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "박광윤이 평택시 빌라를 점유하며 얻은 임대 및 거주 수익을 피담보채권에 충당하였다고 항변함", + "evidence_refs": [ + "E-007", + "E-011" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "legal_calculation_object", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B2:005" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-011" + ], + "issue_cluster_ids": [ + "IC-011" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_index_ids": [ + "E-007", + "E-011" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:005" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-011" + ], + "asset_cluster_ids": [ + "AC-003" + ] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh19" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "박광윤이 평택시 빌라를 점유하며 얻은 임대 및 거주 수익을 피담보채권에 충당하였다고 항변함", + "derivation_basis": "source_bo_id=bh19; evidence_refs=E-007,E-011; structure_ids=LES-011", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-003\"]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-012", + "source_bo_id": "bh2", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "인테리어 자재 매매에 따른 물품대금 채권 발생", + "evidence_refs": [ + "E-021", + "E-027" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "high", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:001" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-012" + ], + "issue_cluster_ids": [ + "IC-012" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-021", + "E-027" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:001" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-012" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh2" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "인테리어 자재 매매에 따른 물품대금 채권 발생", + "derivation_basis": "source_bo_id=bh2; evidence_refs=E-021,E-027; structure_ids=LES-012", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-013", + "source_bo_id": "bh20", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[\"AC-004\"]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "이수인이 윤건우에게 매매계약에서 합의되지 않은 제3자 명의로의 등기이전을 일방적으로 요구함", + "evidence_refs": [ + "E-029" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B2:007" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-013" + ], + "issue_cluster_ids": [ + "IC-013" + ], + "asset_cluster_ids": [ + "AC-004" + ], + "evidence_index_ids": [ + "E-029" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:007" + ] + }, + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-013" + ], + "asset_cluster_ids": [ + "AC-004" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "이수인이 윤건우에게 매매계약에서 합의되지 않은 제3자 명의로의 등기이전을 일방적으로 요구함", + "derivation_basis": "source_bo_id=bh20; evidence_refs=E-029; structure_ids=LES-013", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-004\"]", + "candidate_structure_types": [ + "secured_debt" + ] + } + }, + { + "fact_id": "F-014", + "source_bo_id": "bh21", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[\"AC-005\", \"AC-006\"]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "성수동 대지 차임 시세 감정평가", + "evidence_refs": [ + "E-005" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B3:002" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-014" + ], + "issue_cluster_ids": [ + "IC-014" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006" + ], + "evidence_index_ids": [ + "E-005" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:002" + ] + }, + "must_not_drop_in_claim_types": [ + "land_use_gain", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-014" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "land_use_gain", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "성수동 대지 차임 시세 감정평가", + "derivation_basis": "source_bo_id=bh21; evidence_refs=E-005; structure_ids=LES-014", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-005\", \"AC-006\"]", + "candidate_structure_types": [ + "land_use_gain" + ] + } + }, + { + "fact_id": "F-015", + "source_bo_id": "bh22", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[\"AC-005\", \"AC-007\", \"AC-008\"]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "성수동 대지 및 건물 인도, 점유 및 사용", + "evidence_refs": [ + "E-006" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_structures", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B3:004" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-015" + ], + "issue_cluster_ids": [ + "IC-015" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-007", + "AC-008" + ], + "evidence_index_ids": [ + "E-006" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:004" + ] + }, + "must_not_drop_in_claim_types": [ + "land_use_gain", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-015" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-007", + "AC-008" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "land_use_gain", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "성수동 대지 및 건물 인도, 점유 및 사용", + "derivation_basis": "source_bo_id=bh22; evidence_refs=E-006; structure_ids=LES-015", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-005\", \"AC-007\", \"AC-008\"]", + "candidate_structure_types": [ + "land_use_gain" + ] + } + }, + { + "fact_id": "F-016", + "source_bo_id": "bh23", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[\"AC-009\", \"AC-010\"]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "신림로 상가 및 포천시 임야 가치평가(감정평가서)", + "evidence_refs": [ + "E-004" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B3:001" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-016" + ], + "issue_cluster_ids": [ + "IC-016" + ], + "asset_cluster_ids": [ + "AC-009", + "AC-010" + ], + "evidence_index_ids": [ + "E-004" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:001" + ] + }, + "must_not_drop_in_claim_types": [ + "land_use_gain", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-016" + ], + "asset_cluster_ids": [ + "AC-009", + "AC-010" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "land_use_gain", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "신림로 상가 및 포천시 임야 가치평가(감정평가서)", + "derivation_basis": "source_bo_id=bh23; evidence_refs=E-004; structure_ids=LES-016", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-009\", \"AC-010\"]", + "candidate_structure_types": [ + "land_use_gain" + ] + } + }, + { + "fact_id": "F-017", + "source_bo_id": "bh24", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[\"AC-011\", \"AC-012\"]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "평택시 서정빌라 101호 점유 및 유치권 행사", + "evidence_refs": [ + "E-011" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "legal_calculation_object", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B3:005" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-017" + ], + "issue_cluster_ids": [ + "IC-017" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-012" + ], + "evidence_index_ids": [ + "E-011" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:005" + ] + }, + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-017" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-012" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "평택시 서정빌라 101호 점유 및 유치권 행사", + "derivation_basis": "source_bo_id=bh24; evidence_refs=E-011; structure_ids=LES-017", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-011\", \"AC-012\"]", + "candidate_structure_types": [ + "secured_debt" + ] + } + }, + { + "fact_id": "F-018", + "source_bo_id": "bh25", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[\"AC-011\", \"AC-013\"]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "평택시 서정빌라 차임 시세 평가", + "evidence_refs": [ + "E-023" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B3:003" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-018" + ], + "issue_cluster_ids": [ + "IC-018" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-013" + ], + "evidence_index_ids": [ + "E-023" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:003" + ] + }, + "must_not_drop_in_claim_types": [ + "land_use_gain", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-018" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-013" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "land_use_gain", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "평택시 서정빌라 차임 시세 평가", + "derivation_basis": "source_bo_id=bh25; evidence_refs=E-023; structure_ids=LES-018", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-011\", \"AC-013\"]", + "candidate_structure_types": [ + "land_use_gain" + ] + } + }, + { + "fact_id": "F-019", + "source_bo_id": "bh26", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[\"AC-014\", \"AC-015\"]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "흑석동 상가 점유 및 운영 (맛나식당)", + "evidence_refs": [ + "E-017", + "E-018", + "E-022" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_structures", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B3:006" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-019" + ], + "issue_cluster_ids": [ + "IC-019" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "evidence_index_ids": [ + "E-017", + "E-018", + "E-022" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:006" + ] + }, + "must_not_drop_in_claim_types": [ + "land_use_gain", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-019" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "land_use_gain", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "흑석동 상가 점유 및 운영 (맛나식당)", + "derivation_basis": "source_bo_id=bh26; evidence_refs=E-017,E-018,E-022; structure_ids=LES-019", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-014\", \"AC-015\"]", + "candidate_structure_types": [ + "land_use_gain" + ] + } + }, + { + "fact_id": "F-020", + "source_bo_id": "bh27", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[\"AC-016\", \"AC-002\"]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "채무자 오국한이 수익자 오민한에게 책임재산인 신림동 상가를 1억5천만원에 매도하여 소유권이전등기 경료", + "evidence_refs": [ + "E-014" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "actio_roles", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B4:002" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-020" + ], + "issue_cluster_ids": [ + "IC-020" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_index_ids": [ + "E-014" + ], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:002" + ] + }, + "must_not_drop_in_claim_types": [ + "actio", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-020" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-002", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "beneficiary", + "value_candidate": "{}", + "date_candidate": null, + "related_bo_ids": [ + "bh27" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh27\"], \"related_evidence_indexes\": [\"E-014\"], \"role\": \"beneficiary\", \"value_candidate\": \"{}\"}], \"actio_scope\": [\"fraudulent_act\", \"beneficiary_or_transferee\", \"target_asset\"], \"preserved_claim_candidates\": [{\"bo_id\": \"bh27\", \"claim_date_candidate\": null, \"claim_type_candidate\": null, \"creditor_candidate\": null, \"debtor_candidate\": null, \"related_evidence_indexes\": [\"E-014\"]}], \"related_bo_ids\": [\"bh27\"], \"related_evidence_indexe" + } + ], + "linked_actio_structures": { + "structure_ids": [ + "LES-020" + ], + "actio_signal_count": 1 + }, + "downstream_module_candidates": [ + "actio", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "채무자 오국한이 수익자 오민한에게 책임재산인 신림동 상가를 1억5천만원에 매도하여 소유권이전등기 경료", + "derivation_basis": "source_bo_id=bh27; evidence_refs=E-014; structure_ids=LES-020", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-016\", \"AC-002\"]", + "candidate_structure_types": [ + "fraudulent_transfer" + ] + } + }, + { + "fact_id": "F-021", + "source_bo_id": "bh28", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생", + "evidence_refs": [ + "E-024" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "actio_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B4:001" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-021" + ], + "issue_cluster_ids": [ + "IC-021" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-024" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:001" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "actio", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-021" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh28" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-001", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-024" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "debtor_act", + "value_candidate": "강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생", + "date_candidate": null, + "related_bo_ids": [ + "bh28" + ], + "related_evidence_indexes": [ + "E-024" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh28\"], \"related_evidence_indexes\": [\"E-024\"], \"role\": \"debtor_act\", \"value_candidate\": \"강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생\"}], \"actio_scope\": [\"preserved_claim\"], \"preserved_claim_candidates\": [{\"bo_id\": null, \"claim_date_candidate\": null, \"claim_type_candidate\": null, \"creditor_candidate\": null, \"debtor_candidate\": null, \"related_evidence_indexes\": [\"E-024\"]}], \"related_bo_ids\": [\"bh28\"], \"related_evidence_indexes\": [\"E" + } + ], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 1 + }, + "downstream_module_candidates": [ + "money_claim", + "actio", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생", + "derivation_basis": "source_bo_id=bh28; evidence_refs=E-024; structure_ids=LES-021", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-022", + "source_bo_id": "bh29", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[\"AC-016\", \"AC-017\", \"AC-018\", \"AC-019\", \"AC-002\"]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "수익자 오민한이 사해행위 이후 피담보채무 1억3천만원을 대위변제하고 2번 근저당권을 말소시킴으로써 원물반환이 불가능해져 가액배상 요건 성립", + "evidence_refs": [ + "E-014" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_actio_structures", + "linked_structures", + "actio_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B4:004" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-022" + ], + "issue_cluster_ids": [ + "IC-022" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-002" + ], + "evidence_index_ids": [ + "E-014" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:004" + ] + }, + "must_not_drop_in_claim_types": [ + "secured_property", + "actio", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-022" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-002" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-004", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "remedy_or_cap", + "value_candidate": "원물반환 불가", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "remedy_or_cap", + "value_candidate": "가액배상", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2021-08-01', 'event': '나임오 1번 근저당 설정(5000만원)'}", + "date_candidate": "2021-08-01", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2022-04-18', 'event': '강용일 2번 근저당 설정(1.5억원)'}", + "date_candidate": "2022-04-18", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2024-10-17', 'event': '강용일 2번 근저당 말소(오민한 대위변제)'}", + "date_candidate": "2024-10-17", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh29\"], \"related_evidence_indexes\": [\"E-014\"], \"role\": \"remedy_or_cap\", \"value_candidate\": \"원물반환 불가\"}, {\"date_candidate\": null, \"related_bo_ids\": [\"bh29\"], \"related_evidence_indexes\": [\"E-014\"], \"role\": \"remedy_or_cap\", \"value_candidate\": \"가액배상\"}, {\"date_candidate\": \"2021-08-01\", \"related_bo_ids\": [\"bh29\"], \"related_evidence_indexes\": [\"E-014\"], \"role\": \"encumbrance\", \"value_candidate\": \"{'date': '2021-08-01', 'event': '나임오 1번 근저" + } + ], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 1 + }, + "downstream_module_candidates": [ + "secured_property", + "actio", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "수익자 오민한이 사해행위 이후 피담보채무 1억3천만원을 대위변제하고 2번 근저당권을 말소시킴으로써 원물반환이 불가능해져 가액배상 요건 성립", + "derivation_basis": "source_bo_id=bh29; evidence_refs=E-014; structure_ids=LES-022", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-016\", \"AC-017\", \"AC-018\", \"AC-019\", \"AC-002\"]", + "candidate_structure_types": [ + "secured_debt" + ] + } + }, + { + "fact_id": "F-023", + "source_bo_id": "bh3", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "1억 원 대여금 채권 발생 (2022.4.18.)", + "evidence_refs": [ + "E-024" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:006" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-023" + ], + "issue_cluster_ids": [ + "IC-023" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-024" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:006" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-023" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh3" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "1억 원 대여금 채권 발생 (2022.4.18.)", + "derivation_basis": "source_bo_id=bh3; evidence_refs=E-024; structure_ids=LES-023", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-024", + "source_bo_id": "bh30", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[\"AC-020\", \"AC-021\"]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "evidence_refs": [ + "E-004" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "legal_effect_roles", + "actio_roles", + "legal_calculation_object", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B4:003" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-024" + ], + "issue_cluster_ids": [ + "IC-024" + ], + "asset_cluster_ids": [ + "AC-020", + "AC-021" + ], + "evidence_index_ids": [ + "E-004" + ], + "candidate_structure_types": [ + "valuation" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:003" + ] + }, + "must_not_drop_in_claim_types": [ + "valuation", + "actio", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-024" + ], + "asset_cluster_ids": [ + "AC-020", + "AC-021" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-003", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-004" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "debtor_act", + "value_candidate": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "date_candidate": null, + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh30\"], \"related_evidence_indexes\": [\"E-004\"], \"role\": \"debtor_act\", \"value_candidate\": \"사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)\"}], \"actio_scope\": [\"target_asset\"], \"preserved_claim_candidates\": [{\"bo_id\": \"bh30\", \"claim_date_candidate\": null, \"claim_type_candidate\": null, \"creditor_candidate\": null, \"debtor_candidate\": null, \"related_evidence_indexes\": [\"E-004\"]}], \"related_bo_ids\": [\"bh30\"], \"related_evidence_i" + } + ], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 1 + }, + "downstream_module_candidates": [ + "valuation", + "actio", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "derivation_basis": "source_bo_id=bh30; evidence_refs=E-004; structure_ids=LES-024", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-020\", \"AC-021\"]", + "candidate_structure_types": [ + "valuation" + ] + } + }, + { + "fact_id": "F-025", + "source_bo_id": "bh31", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[\"AC-016\", \"AC-002\"]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "수익자 오민한의 항변 (명의신탁 주장, 매수자금 및 대여금 참작 주장, 채권 상계 주장)", + "evidence_refs": [ + "E-003", + "E-010" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "legal_effect_roles", + "actio_roles", + "linked_structures", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B4:005" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-025" + ], + "issue_cluster_ids": [ + "IC-025" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_index_ids": [ + "E-003", + "E-010" + ], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:005" + ] + }, + "must_not_drop_in_claim_types": [ + "actio", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-025" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-005", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "defense", + "value_candidate": "{'defense_type': '계약명의신탁', 'content': '오국한 명의 취득이 수익자 자금에 의한 계약명의신탁이므로 사해행위가 아니라는 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "role": "defense", + "value_candidate": "{'defense_type': '배상금감액', 'content': '수익자가 제공한 매수자금 4000만원 및 대여금 1000만원 참작 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "role": "defense", + "value_candidate": "{'defense_type': '상계항변', 'content': '오민한의 5000만원 채권으로 강용원 채권을 상계한다는 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh31\"], \"related_evidence_indexes\": [\"E-003\", \"E-010\"], \"role\": \"defense\", \"value_candidate\": \"{'defense_type': '계약명의신탁', 'content': '오국한 명의 취득이 수익자 자금에 의한 계약명의신탁이므로 사해행위가 아니라는 주장'}\"}, {\"date_candidate\": null, \"related_bo_ids\": [\"bh31\"], \"related_evidence_indexes\": [\"E-003\", \"E-010\"], \"role\": \"defense\", \"value_candidate\": \"{'defense_type': '배상금감액', 'content': '수익자가 제공한 매수자금 4000만원 및 대여금 1000만원 참작 주장'}\"}, {\"date_candidate\": null, " + } + ], + "linked_actio_structures": { + "structure_ids": [ + "LES-025" + ], + "actio_signal_count": 1 + }, + "downstream_module_candidates": [ + "actio", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "수익자 오민한의 항변 (명의신탁 주장, 매수자금 및 대여금 참작 주장, 채권 상계 주장)", + "derivation_basis": "source_bo_id=bh31; evidence_refs=E-003,E-010; structure_ids=LES-025", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-016\", \"AC-002\"]", + "candidate_structure_types": [ + "fraudulent_transfer" + ] + } + }, + { + "fact_id": "F-026", + "source_bo_id": "bh32", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "강석우 사망으로 인한 정유심, 강용원의 성수동 대지 지분 상속", + "evidence_refs": [ + "E-013" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:002" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-026" + ], + "issue_cluster_ids": [ + "IC-026" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-013" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:002" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-026" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "강석우 사망으로 인한 정유심, 강용원의 성수동 대지 지분 상속", + "derivation_basis": "source_bo_id=bh32; evidence_refs=E-013; structure_ids=LES-026", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-027", + "source_bo_id": "bh33", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "망 강호연의 사망에 따른 상속개시 및 자녀 강형수, 강지수의 상속포기", + "evidence_refs": [ + "E-001", + "E-002" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "high", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:003" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-027" + ], + "issue_cluster_ids": [ + "IC-027" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-001", + "E-002" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:003" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-027" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "망 강호연의 사망에 따른 상속개시 및 자녀 강형수, 강지수의 상속포기", + "derivation_basis": "source_bo_id=bh33; evidence_refs=E-001,E-002; structure_ids=LES-027", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-028", + "source_bo_id": "bh34", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "강용원이 김선웅에게 물품대금 지급을 최고하는 통지서 발송 및 도달", + "evidence_refs": [ + "E-027" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:009" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-028" + ], + "issue_cluster_ids": [ + "IC-028" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-027" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:009" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-028" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "강용원이 김선웅에게 물품대금 지급을 최고하는 통지서 발송 및 도달", + "derivation_basis": "source_bo_id=bh34; evidence_refs=E-027; structure_ids=LES-028", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-029", + "source_bo_id": "bh35", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "강용원이 오민한에게 사해행위취소 및 신림동 상가 소유권 반환을 요구하는 내용증명 발송", + "evidence_refs": [ + "E-008", + "E-010" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "high", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "actio_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:004" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-029" + ], + "issue_cluster_ids": [ + "IC-029" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-008", + "E-010" + ], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:004" + ] + }, + "must_not_drop_in_claim_types": [ + "actio", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-029" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [ + "LES-029" + ], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "actio", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "강용원이 오민한에게 사해행위취소 및 신림동 상가 소유권 반환을 요구하는 내용증명 발송", + "derivation_basis": "source_bo_id=bh35; evidence_refs=E-008,E-010; structure_ids=LES-029", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "fraudulent_transfer" + ] + } + }, + { + "fact_id": "F-030", + "source_bo_id": "bh36", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "강용원과 양정숙이 박광윤에게 평택시 빌라 유치권소멸을 청구하는 통지서 발송 및 도달", + "evidence_refs": [ + "E-011", + "E-019" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "high", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:007" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-030" + ], + "issue_cluster_ids": [ + "IC-030" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-011", + "E-019" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:007" + ] + }, + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-030" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "skeleton_validation_codes": [ + "NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW" + ], + "derived_fact": "강용원과 양정숙이 박광윤에게 평택시 빌라 유치권소멸을 청구하는 통지서 발송 및 도달", + "derivation_basis": "source_bo_id=bh36; evidence_refs=E-011,E-019; structure_ids=LES-030", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "secured_debt" + ] + } + }, + { + "fact_id": "F-031", + "source_bo_id": "bh37", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "오민한이 강용원에게 김선웅의 채무에 대한 책임을 부인하는 통지서 발송", + "evidence_refs": [ + "E-028" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:010" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-031" + ], + "issue_cluster_ids": [ + "IC-031" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-028" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:010" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-031" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "오민한이 강용원에게 김선웅의 채무에 대한 책임을 부인하는 통지서 발송", + "derivation_basis": "source_bo_id=bh37; evidence_refs=E-028; structure_ids=LES-031", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-032", + "source_bo_id": "bh38", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "오민한이 강용원에게 내용증명에 대한 답신 발송", + "evidence_refs": [ + "E-010" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:006" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-032" + ], + "issue_cluster_ids": [ + "IC-032" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-010" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:006" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-032" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "오민한이 강용원에게 내용증명에 대한 답신 발송", + "derivation_basis": "source_bo_id=bh38; evidence_refs=E-010; structure_ids=LES-032", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-033", + "source_bo_id": "bh39", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "윤건우가 양정숙에게 흑석동 상가 임대차계약 해지 및 명도를 요구하는 내용증명 통지", + "evidence_refs": [ + "E-009" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:005" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-033" + ], + "issue_cluster_ids": [ + "IC-033" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-009" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:005" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-033" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "윤건우가 양정숙에게 흑석동 상가 임대차계약 해지 및 명도를 요구하는 내용증명 통지", + "derivation_basis": "source_bo_id=bh39; evidence_refs=E-009; structure_ids=LES-033", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-034", + "source_bo_id": "bh4", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "1억 원 대여금 채권 발생 (2022.5.18.)", + "evidence_refs": [ + "E-024" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:007" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-034" + ], + "issue_cluster_ids": [ + "IC-034" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-024" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:007" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-034" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh4" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "1억 원 대여금 채권 발생 (2022.5.18.)", + "derivation_basis": "source_bo_id=bh4; evidence_refs=E-024; structure_ids=LES-034", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-035", + "source_bo_id": "bh40", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "윤건우가 이수인에게 흑석동 상가 매매계약 해제 통지서 발송 및 도달", + "evidence_refs": [ + "E-030" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:012" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-035" + ], + "issue_cluster_ids": [ + "IC-035" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-030" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:012" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-035" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [ + "NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW" + ], + "derived_fact": "윤건우가 이수인에게 흑석동 상가 매매계약 해제 통지서 발송 및 도달", + "derivation_basis": "source_bo_id=bh40; evidence_refs=E-030; structure_ids=LES-035", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-036", + "source_bo_id": "bh41", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "이수인이 윤건우에게 흑석동 상가 매매대금 감액 및 제3자 명의 등기 이전 요청 통지", + "evidence_refs": [ + "E-029" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:011" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-036" + ], + "issue_cluster_ids": [ + "IC-036" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-029" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:011" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-036" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh41" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "이수인이 윤건우에게 흑석동 상가 매매대금 감액 및 제3자 명의 등기 이전 요청 통지", + "derivation_basis": "source_bo_id=bh41; evidence_refs=E-029; structure_ids=LES-036", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-037", + "source_bo_id": "bh42", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "주식회사 대한은행이 강용원의 성수동 대지 근저당권 말소 신청을 거부하는 통지 발송", + "evidence_refs": [ + "E-012" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:008" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-037" + ], + "issue_cluster_ids": [ + "IC-037" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-012" + ], + "candidate_structure_types": [ + "registry_invalidity" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:008" + ] + }, + "must_not_drop_in_claim_types": [ + "registry_invalidity", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-037" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "registry_invalidity", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "주식회사 대한은행이 강용원의 성수동 대지 근저당권 말소 신청을 거부하는 통지 발송", + "derivation_basis": "source_bo_id=bh42; evidence_refs=E-012; structure_ids=LES-037", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "registry_invalidity" + ] + } + }, + { + "fact_id": "F-038", + "source_bo_id": "bh43", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[\"AC-022\", \"AC-023\", \"AC-024\", \"AC-025\", \"AC-026\"]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "목적물 특정", + "evidence_refs": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:001" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-038" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "asset_cluster_ids": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "evidence_index_ids": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:001" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-038" + ], + "asset_cluster_ids": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "목적물 특정", + "derivation_basis": "source_bo_id=bh43; evidence_refs=E-013,E-014,E-015,E-016; structure_ids=LES-038", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-022\", \"AC-023\", \"AC-024\", \"AC-025\", \"AC-026\"]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-039", + "source_bo_id": "bh44", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "박광윤의 평택시 서정빌라 101호 거주 및 유치권 행사 상태", + "evidence_refs": [ + "E-011" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:013" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-039" + ], + "issue_cluster_ids": [ + "IC-039" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-011" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:013" + ] + }, + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-039" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "박광윤의 평택시 서정빌라 101호 거주 및 유치권 행사 상태", + "derivation_basis": "source_bo_id=bh44; evidence_refs=E-011; structure_ids=LES-039", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "secured_debt" + ] + } + }, + { + "fact_id": "F-040", + "source_bo_id": "bh45", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "박성희의 성수동 대지 및 건물 점유", + "evidence_refs": [ + "E-006" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:014" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-040" + ], + "issue_cluster_ids": [ + "IC-040" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-006" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:014" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-040" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "박성희의 성수동 대지 및 건물 점유", + "derivation_basis": "source_bo_id=bh45; evidence_refs=E-006; structure_ids=LES-040", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-041", + "source_bo_id": "bh46", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "양정숙의 흑석동 상가 점유 및 식당 운영 상태", + "evidence_refs": [ + "E-018", + "E-022" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:015" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-041" + ], + "issue_cluster_ids": [ + "IC-041" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-018", + "E-022" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:015" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-041" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "양정숙의 흑석동 상가 점유 및 식당 운영 상태", + "derivation_basis": "source_bo_id=bh46; evidence_refs=E-018,E-022; structure_ids=LES-041", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-042", + "source_bo_id": "bh47", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "오민한의 강용원에 대한 자신의 5,000만 원 채권 상계 주장 항변", + "evidence_refs": [ + "E-010" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:017" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-042" + ], + "issue_cluster_ids": [ + "IC-042" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-010" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:017" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-042" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh47" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "오민한의 강용원에 대한 자신의 5,000만 원 채권 상계 주장 항변", + "derivation_basis": "source_bo_id=bh47; evidence_refs=E-010; structure_ids=LES-042", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-043", + "source_bo_id": "bh48", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "박광윤의 과거 임대료 및 거주 수익을 채무에 충당하였다는 주장 항변", + "evidence_refs": [ + "E-011" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:018" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-043" + ], + "issue_cluster_ids": [ + "IC-043" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-011" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:018" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-043" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "박광윤의 과거 임대료 및 거주 수익을 채무에 충당하였다는 주장 항변", + "derivation_basis": "source_bo_id=bh48; evidence_refs=E-011; structure_ids=LES-043", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-044", + "source_bo_id": "bh49", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "윤건우의 임대차 책임 부인 및 양정숙 차임 연체 주장 항변", + "evidence_refs": [ + "E-009" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:016" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-044" + ], + "issue_cluster_ids": [ + "IC-044" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-009" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:016" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-044" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "윤건우의 임대차 책임 부인 및 양정숙 차임 연체 주장 항변", + "derivation_basis": "source_bo_id=bh49; evidence_refs=E-009; structure_ids=LES-044", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-045", + "source_bo_id": "bh5", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "2억 원 대여금 채권 발생 (2023.1.6.)", + "evidence_refs": [ + "E-025" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:008" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-045" + ], + "issue_cluster_ids": [ + "IC-045" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-025" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:008" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-045" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh5" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "2억 원 대여금 채권 발생 (2023.1.6.)", + "derivation_basis": "source_bo_id=bh5; evidence_refs=E-025; structure_ids=LES-045", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-046", + "source_bo_id": "bh50", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "주식회사 대한은행의 경매공신력 주장(민사집행법 제267조)에 따른 근저당 말소 불가 항변", + "evidence_refs": [ + "E-012" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:019" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-046" + ], + "issue_cluster_ids": [ + "IC-046" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-012" + ], + "candidate_structure_types": [ + "registry_invalidity" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:019" + ] + }, + "must_not_drop_in_claim_types": [ + "registry_invalidity", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-046" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "registry_invalidity", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "주식회사 대한은행의 경매공신력 주장(민사집행법 제267조)에 따른 근저당 말소 불가 항변", + "derivation_basis": "source_bo_id=bh50; evidence_refs=E-012; structure_ids=LES-046", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "registry_invalidity" + ] + } + }, + { + "fact_id": "F-047", + "source_bo_id": "bh6", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "3억 원 대여금 채권 발생 (2023.7.6.)", + "evidence_refs": [ + "E-026" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:009" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-047" + ], + "issue_cluster_ids": [ + "IC-047" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-026" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:009" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-047" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh6" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "3억 원 대여금 채권 발생 (2023.7.6.)", + "derivation_basis": "source_bo_id=bh6; evidence_refs=E-026; structure_ids=LES-047", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-048", + "source_bo_id": "bh7", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "해드림 영업양도 및 상호속용 약정", + "evidence_refs": [ + "E-020" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:004" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-048" + ], + "issue_cluster_ids": [ + "IC-048" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-020" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:004" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-048" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh7" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "해드림 영업양도 및 상호속용 약정", + "derivation_basis": "source_bo_id=bh7; evidence_refs=E-020; structure_ids=LES-048", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-049", + "source_bo_id": "bh8", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "물품대금 채무 승인 및 변제기 유예", + "evidence_refs": [ + "E-021" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:002" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-049" + ], + "issue_cluster_ids": [ + "IC-049" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-021" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:002" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-049" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh8" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "물품대금 채무 승인 및 변제기 유예", + "derivation_basis": "source_bo_id=bh8; evidence_refs=E-021; structure_ids=LES-049", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-050", + "source_bo_id": "bh9", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "차용금 변제 명목 4억 원 지급 (채무 미특정)", + "evidence_refs": [], + "credibility": "low", + "legal_centrality": "critical", + "proof_strength": "low", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:010" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-050" + ], + "issue_cluster_ids": [ + "IC-050" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:010" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-050" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh9" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [ + "NO_EVIDENCE_REFS" + ], + "derived_fact": "차용금 변제 명목 4억 원 지급 (채무 미특정)", + "derivation_basis": "source_bo_id=bh9; structure_ids=LES-050", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + } +] \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/actio_case_signals.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/actio_case_signals.json new file mode 100644 index 00000000..e6f9b6eb --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/actio_case_signals.json @@ -0,0 +1,367 @@ +{ + "schema_version": "actio_case_signals.v1", + "status": "READY_WITH_REVIEW", + "actio_case_signals": [ + { + "signal_id": "actio-signal-001", + "actio_scope": [ + "preserved_claim" + ], + "suspicion_level": "LOW", + "suspicion_reasons": "피보전채권 후보가 B4 seed/domain payload에서 확인됨", + "related_bo_ids": [ + "bh28" + ], + "related_evidence_indexes": [ + "E-024" + ], + "target_property_candidates": [], + "preserved_claim_candidates": [ + { + "bo_id": null, + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-024" + ] + } + ], + "actio_role_hints": [ + { + "role": "debtor_act", + "value_candidate": "강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생", + "date_candidate": null, + "related_bo_ids": [ + "bh28" + ], + "related_evidence_indexes": [ + "E-024" + ] + } + ] + }, + { + "signal_id": "actio-signal-002", + "actio_scope": [ + "fraudulent_act", + "beneficiary_or_transferee", + "target_asset" + ], + "suspicion_level": "LOW", + "suspicion_reasons": "채무자 처분행위 또는 사해행위 후보 사실이 확인됨, 수익자 또는 전득자 후보가 확인됨, 취소·원상회복 대상 재산 후보가 확인됨", + "related_bo_ids": [ + "bh27" + ], + "related_evidence_indexes": [ + "E-014" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh27" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh27", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "actio_role_hints": [ + { + "role": "beneficiary", + "value_candidate": "{}", + "date_candidate": null, + "related_bo_ids": [ + "bh27" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ] + }, + { + "signal_id": "actio-signal-003", + "actio_scope": [ + "target_asset" + ], + "suspicion_level": "LOW", + "suspicion_reasons": "취소·원상회복 대상 재산 후보가 확인됨", + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + }, + { + "asset_label": "포천시 임야", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh30", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-004" + ] + } + ], + "actio_role_hints": [ + { + "role": "debtor_act", + "value_candidate": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "date_candidate": null, + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + } + ] + }, + { + "signal_id": "actio-signal-004", + "actio_scope": [ + "target_asset", + "encumbrance", + "remedy_or_cap" + ], + "suspicion_level": "LOW", + "suspicion_reasons": "취소·원상회복 대상 재산 후보가 확인됨, 담보권·가압류 등 부담 관련 단서가 확인됨, 원상회복 방법 또는 가액배상 한도 단서가 확인됨", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2021-08-01", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2022-04-18", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2024-10-17", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh29", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "actio_role_hints": [ + { + "role": "remedy_or_cap", + "value_candidate": "원물반환 불가", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "remedy_or_cap", + "value_candidate": "가액배상", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2021-08-01', 'event': '나임오 1번 근저당 설정(5000만원)'}", + "date_candidate": "2021-08-01", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2022-04-18', 'event': '강용일 2번 근저당 설정(1.5억원)'}", + "date_candidate": "2022-04-18", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2024-10-17', 'event': '강용일 2번 근저당 말소(오민한 대위변제)'}", + "date_candidate": "2024-10-17", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ] + }, + { + "signal_id": "actio-signal-005", + "actio_scope": [ + "target_asset", + "defense" + ], + "suspicion_level": "LOW", + "suspicion_reasons": "취소·원상회복 대상 재산 후보가 확인됨, 항변 또는 선의·악의 관련 반박 단서가 확인됨", + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh31", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ], + "actio_role_hints": [ + { + "role": "defense", + "value_candidate": "{'defense_type': '계약명의신탁', 'content': '오국한 명의 취득이 수익자 자금에 의한 계약명의신탁이므로 사해행위가 아니라는 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "role": "defense", + "value_candidate": "{'defense_type': '배상금감액', 'content': '수익자가 제공한 매수자금 4000만원 및 대여금 1000만원 참작 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "role": "defense", + "value_candidate": "{'defense_type': '상계항변', 'content': '오민한의 5000만원 채권으로 강용원 채권을 상계한다는 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ] + } + ] +} diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/case_liability_signals.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/case_liability_signals.json new file mode 100644 index 00000000..ffe803e0 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/case_liability_signals.json @@ -0,0 +1,669 @@ +{ + "schema_version": "case_liability_signals.v1", + "status": "READY_WITH_REVIEW", + "case_liability_signals": [ + { + "liability_candidate_type": "commercial_successor", + "related_bo_ids": [ + "bh1" + ], + "related_evidence_indexes": [ + "E-007" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:011", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "commercial_successor", + "related_bo_ids": [ + "bh2" + ], + "related_evidence_indexes": [ + "E-021", + "E-027" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:001", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "commercial_successor", + "related_bo_ids": [ + "bh3" + ], + "related_evidence_indexes": [ + "E-024" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:006", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "commercial_successor", + "related_bo_ids": [ + "bh4" + ], + "related_evidence_indexes": [ + "E-024" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:007", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "commercial_successor", + "related_bo_ids": [ + "bh5" + ], + "related_evidence_indexes": [ + "E-025" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:008", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "commercial_successor", + "related_bo_ids": [ + "bh6" + ], + "related_evidence_indexes": [ + "E-026" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:009", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "commercial_successor", + "related_bo_ids": [ + "bh7" + ], + "related_evidence_indexes": [ + "E-020" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:004", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "commercial_successor", + "related_bo_ids": [ + "bh8" + ], + "related_evidence_indexes": [ + "E-021" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:002", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "commercial_successor", + "related_bo_ids": [ + "bh9" + ], + "related_evidence_indexes": [], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:010", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "commercial_successor", + "related_bo_ids": [ + "bh10" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:013", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "commercial_successor", + "related_bo_ids": [ + "bh11" + ], + "related_evidence_indexes": [ + "E-027" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:003", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "commercial_successor", + "related_bo_ids": [ + "bh12" + ], + "related_evidence_indexes": [ + "E-028" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:005", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "commercial_successor", + "related_bo_ids": [ + "bh13" + ], + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:012", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "secured_debt", + "related_bo_ids": [ + "bh14" + ], + "related_evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B2:001", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "registry_invalidity", + "related_bo_ids": [ + "bh15" + ], + "related_evidence_indexes": [ + "E-010", + "E-014" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B2:004", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "secured_debt", + "related_bo_ids": [ + "bh16" + ], + "related_evidence_indexes": [ + "E-015" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B2:006", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "registry_invalidity", + "related_bo_ids": [ + "bh17" + ], + "related_evidence_indexes": [ + "E-012", + "E-013" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B2:003", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "secured_debt", + "related_bo_ids": [ + "bh18" + ], + "related_evidence_indexes": [], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B2:002", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "money_claim", + "related_bo_ids": [ + "bh19" + ], + "related_evidence_indexes": [ + "E-007", + "E-011" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B2:005", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "secured_debt", + "related_bo_ids": [ + "bh20" + ], + "related_evidence_indexes": [ + "E-029" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B2:007", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "land_use_gain", + "related_bo_ids": [ + "bh21" + ], + "related_evidence_indexes": [ + "E-005" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B3:002", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "land_use_gain", + "related_bo_ids": [ + "bh22" + ], + "related_evidence_indexes": [ + "E-006" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B3:004", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "land_use_gain", + "related_bo_ids": [ + "bh23" + ], + "related_evidence_indexes": [ + "E-004" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B3:001", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "secured_debt", + "related_bo_ids": [ + "bh24" + ], + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B3:005", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "land_use_gain", + "related_bo_ids": [ + "bh25" + ], + "related_evidence_indexes": [ + "E-023" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B3:003", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "land_use_gain", + "related_bo_ids": [ + "bh26" + ], + "related_evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B3:006", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "actio_related", + "related_bo_ids": [ + "bh27" + ], + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B4:002", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "actio_related", + "related_bo_ids": [ + "bh28" + ], + "related_evidence_indexes": [ + "E-024" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B4:001", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "actio_related", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B4:004", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "actio_related", + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B4:003", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "actio_related", + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B4:005", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "succession_related", + "related_bo_ids": [ + "bh32" + ], + "related_evidence_indexes": [ + "E-013" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:002", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "succession_related", + "related_bo_ids": [ + "bh33" + ], + "related_evidence_indexes": [ + "E-001", + "E-002" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:003", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "succession_related", + "related_bo_ids": [ + "bh34" + ], + "related_evidence_indexes": [ + "E-027" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:009", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "actio_related", + "related_bo_ids": [ + "bh35" + ], + "related_evidence_indexes": [ + "E-008", + "E-010" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:004", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "secured_debt", + "related_bo_ids": [ + "bh36" + ], + "related_evidence_indexes": [ + "E-011", + "E-019" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:007", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "succession_related", + "related_bo_ids": [ + "bh37" + ], + "related_evidence_indexes": [ + "E-028" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:010", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "succession_related", + "related_bo_ids": [ + "bh38" + ], + "related_evidence_indexes": [ + "E-010" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:006", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "succession_related", + "related_bo_ids": [ + "bh39" + ], + "related_evidence_indexes": [ + "E-009" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:005", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "succession_related", + "related_bo_ids": [ + "bh40" + ], + "related_evidence_indexes": [ + "E-030" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:012", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "money_claim", + "related_bo_ids": [ + "bh41" + ], + "related_evidence_indexes": [ + "E-029" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:011", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "registry_invalidity", + "related_bo_ids": [ + "bh42" + ], + "related_evidence_indexes": [ + "E-012" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:008", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "succession_related", + "related_bo_ids": [ + "bh43" + ], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:001", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "secured_debt", + "related_bo_ids": [ + "bh44" + ], + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:013", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "succession_related", + "related_bo_ids": [ + "bh45" + ], + "related_evidence_indexes": [ + "E-006" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:014", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "succession_related", + "related_bo_ids": [ + "bh46" + ], + "related_evidence_indexes": [ + "E-018", + "E-022" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:015", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "money_claim", + "related_bo_ids": [ + "bh47" + ], + "related_evidence_indexes": [ + "E-010" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:017", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "succession_related", + "related_bo_ids": [ + "bh48" + ], + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:018", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "succession_related", + "related_bo_ids": [ + "bh49" + ], + "related_evidence_indexes": [ + "E-009" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:016", + "linked_structure_ids": [] + }, + { + "liability_candidate_type": "registry_invalidity", + "related_bo_ids": [ + "bh50" + ], + "related_evidence_indexes": [ + "E-012" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:019", + "linked_structure_ids": [] + } + ] +} diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/client_goal.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/client_goal.json new file mode 100644 index 00000000..80458e6a --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/client_goal.json @@ -0,0 +1,152 @@ +{ + "primary_goal": "강용원 및 양정숙의 5개 분쟁군별 채권 실현, 부동산 소유·점유 권리 확인, 점유 해소 및 임대차 관계의 적법성 확인을 위한 소 제기", + "constraints": [ + "오국한에 대한 금전대여금 청구 소송 제기 불가" + ], + "summary_key_incidents": [ + "강용원의 인테리어 자재대금 청구와 오민한의 인수 책임 여부", + "성수동 대지 경매 이후 이문호·박성희의 소유·점유 권리 분쟁", + "신림동 상가에 대한 오국한의 사해행위 및 오민한 명의 등기 분쟁", + "망 강호연 소유 평택시 빌라 유치권 소멸 및 인도 청구", + "양정숙의 흑석동 상가 임차권 적법성 및 윤건우의 매매계약 해제 분쟁" + ], + "key_facts": [ + "강용원-김선웅 간 2억 원 자재 매매 및 오민한의 사업 인수", + "성수동 대지 경매 및 이문호-대한은행 근저당권 설정", + "오국한의 신림동 상가 사해행위성 사촌 오민한 명의 이전", + "망 강호연 소유 평택시 빌라 박광윤 유치권 행사", + "양정숙의 흑석동 상가 인도 및 사업자등록과 윤건우의 매매해제 통지" + ], + "claim_type_candidates": ["금전", "물건인도", "확인", "형성"], + "parties": { + "plaintiffs": [ + { "name": "강용원", "type": "자연인" }, + { "name": "양정숙", "type": "자연인" } + ], + "defendants": [ + { "name": "오민한", "type": "자연인", "asset_status": "사업승계인, 사해행위 수익자 후보" }, + { "name": "이문호", "type": "자연인", "asset_status": "성수동 대지 소유자" }, + { "name": "주식회사 대한은행", "type": "법인", "asset_status": "성수동 대지 근저당권자" }, + { "name": "박성희", "type": "자연인", "asset_status": "성수동 대지/건물 점유자" }, + { "name": "박광윤", "type": "자연인", "asset_status": "평택시 빌라 유치권자" }, + { "name": "윤건우", "type": "자연인", "asset_status": "흑석동 상가 매도인" } + ], + "third_parties": [ + { "name": "오국한", "relationship": "채무자" }, + { "name": "김선웅", "relationship": "원채무자" } + ] + }, + "issue_clusters": [ + { + "cluster_id": "IC-01", + "cluster_label": "인테리어 자재대금", + "candidate_plaintiffs": ["강용원"], + "candidate_defendants_or_counterparties": ["오민한"], + "desired_relief_keywords": ["금전"], + "domain_profiles": { + "monetary_claim_profile": { + "underlying_claim_type": "물품대금", + "principal_debtor_candidates": ["김선웅"], + "successor_or_secondary_liability_candidates": ["오민한"], + "requires_money_claim_timeline": true + } + } + }, + { + "cluster_id": "IC-02", + "cluster_label": "성수동 대지/건물", + "candidate_plaintiffs": ["강용원"], + "candidate_defendants_or_counterparties": ["이문호", "주식회사 대한은행", "박성희"], + "desired_relief_keywords": ["확인", "명도", "철거"], + "domain_profiles": { + "secured_property_dispute_profile": { + "auction_or_registry_invalidity_expected": true + } + } + }, + { + "cluster_id": "IC-03", + "cluster_label": "신림동 상가 사해행위", + "candidate_plaintiffs": ["강용원"], + "candidate_defendants_or_counterparties": ["오민한"], + "desired_relief_keywords": ["금전", "형성"], + "domain_profiles": { + "actio_property_transfer_profile": { + "debtor": "오국한", + "beneficiary": "오민한", + "fraudulent_act_asset": "신림동 상가", + "fraudulent_act_type": "소유권이전등기" + } + } + }, + { + "cluster_id": "IC-04", + "cluster_label": "평택시 빌라 유치권", + "candidate_plaintiffs": ["강용원", "양정숙"], + "preferred_plaintiffs_for_complaint": [], + "succession_resolution_needed": true, + "candidate_defendants_or_counterparties": ["박광윤"], + "desired_relief_keywords": ["확인", "명도", "금전"], + "domain_profiles": { + "possession_lien_succession_profile": { + "succession_resolution_needed": true, + "possession_basis_asserted": "유치권" + } + } + }, + { + "cluster_id": "IC-05", + "cluster_label": "흑석동 상가 임대차", + "candidate_plaintiffs": ["양정숙"], + "candidate_defendants_or_counterparties": ["윤건우"], + "desired_relief_keywords": ["확인"], + "domain_profiles": { + "possession_lien_succession_profile": { + "possession_basis_asserted": "임대차" + } + } + } + ], + "plaintiff_capacity_matrix": [ + { + "cluster_id": "IC-04", + "plaintiff_name": "강용원", + "capacity_basis": "상속인", + "source_basis_text": "망 강호연의 아버지", + "certainty": "provisional" + }, + { + "cluster_id": "IC-04", + "plaintiff_name": "양정숙", + "capacity_basis": "상속인", + "source_basis_text": "망 강호연의 배우자", + "certainty": "provisional" + } + ], + "current_control_map_4axis": [ + { + "object": "성수동 대지/건물", + "cluster_id": "IC-02", + "current_right_holder_candidate": "이문호", + "current_possessor": "박성희", + "certainty": "final" + }, + { + "object": "평택시 빌라", + "cluster_id": "IC-04", + "current_possessor": "박광윤", + "certainty": "final" + }, + { + "object": "흑석동 상가", + "cluster_id": "IC-05", + "current_possessor": "양정숙", + "current_user": "양정숙", + "certainty": "final" + } + ], + "standing_watchpoints": [ + "평택시 빌라 관련 망 강호연의 상속인 확정 및 승계 resolution 필요", + "성수동 대지 경매 공신력 주장과 이문호 소유권 취득 적법성 재검토" + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-001.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-001.json new file mode 100644 index 00000000..70a5eef2 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-001.json @@ -0,0 +1,144 @@ +{ + "evidence_index_proposed": "E-001", + "title": "가정법원심판_강호연", + "doc_type": "판결/결정", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 1 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "succession_doc", + "status_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-001-01", + "event_or_state": "event", + "event_kind": "상속포기", + "event_subkind": "succession_renunciation_filing", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "강형수와 강지수가 망 강호연의 재산상속을 포기하는 신고를 함.", + "source_evidence_index_proposed": "E-001", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [ + "강형수", + "강지수" + ], + "counterparty_candidates": [], + "beneficiary_candidates": [], + "third_party_candidates": [ + "강호연" + ] + }, + "object_spec": "재산상속 포기 신고", + "fraction_ref": null, + "amount": null, + "event_date": "2024-09-11", + "event_end_date": null, + "time_text": "2024. 9. 11.", + "time_precision": "exact", + "location": null, + "legal_keywords": [ + "상속포기", + "신고" + ], + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "주문", + "excerpt": "청구인들이 피상속인 망 강호연의 재산상속을 포기하는 2024. 9. 11. 자 신고", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": { + "deceased_candidate": "강호연", + "spouse_candidates": [], + "lineal_ascendant_candidates": [], + "lineal_descendant_candidates": [], + "renunciant_candidates": [ + "강형수", + "강지수" + ] + }, + "legal_effect_candidate": null, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "event_상속포기_강형수_강지수_망강호연_2024-09-11", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-001-02", + "event_or_state": "event", + "event_kind": "판결/결정", + "event_subkind": "succession_renunciation_acceptance", + "action_type_candidate": "소송행위(litigation acts)", + "action_summary": "서울가정법원이 강형수와 강지수의 상속포기 신고를 수리하는 심판을 내림.", + "source_evidence_index_proposed": "E-001", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [ + "서울가정법원" + ], + "counterparty_candidates": [ + "강형수", + "강지수" + ], + "beneficiary_candidates": [], + "third_party_candidates": [ + "강호연" + ] + }, + "object_spec": "상속포기 신고 수리", + "fraction_ref": null, + "amount": null, + "event_date": "2024-09-20", + "event_end_date": null, + "time_text": "2024. 9. 20.", + "time_precision": "exact", + "location": null, + "legal_keywords": [ + "심판", + "상속포기", + "수리" + ], + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "주문", + "excerpt": "청구인들이 피상속인 망 강호연의 재산상속을 포기하는 2024. 9. 11. 자 신고는 이를 수리한다.", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": { + "deceased_candidate": "강호연", + "spouse_candidates": [], + "lineal_ascendant_candidates": [], + "lineal_descendant_candidates": [], + "renunciant_candidates": [ + "강형수", + "강지수" + ] + }, + "legal_effect_candidate": null, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "event_판결/결정_서울가정법원_망강호연_상속포기수리_2024-09-20", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-002.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-002.json new file mode 100644 index 00000000..240da7f8 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-002.json @@ -0,0 +1,85 @@ +{ + "evidence_index_proposed": "E-002", + "title": "가족관계증명서 [폐쇄]", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 2 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "status_doc", + "succession_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-002-01", + "event_or_state": "event", + "event_kind": "상속개시", + "event_subkind": null, + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "망 강호연의 사망으로 인한 상속개시", + "source_evidence_index_proposed": "E-002", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [ + "강호연" + ], + "counterparty_candidates": [], + "beneficiary_candidates": [], + "third_party_candidates": [ + "강용원", + "양정숙", + "강형수", + "강지수" + ] + }, + "object_spec": null, + "fraction_ref": null, + "amount": null, + "event_date": "2024-08-10", + "event_end_date": null, + "time_text": "2024-08-10", + "time_precision": "exact", + "location": null, + "legal_keywords": [], + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "본인 강호연 사망", + "excerpt": "강호연(姜鎬然) 사망", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": { + "deceased_candidate": "강호연", + "spouse_candidates": [ + "양정숙" + ], + "lineal_ascendant_candidates": [ + "강용원" + ], + "lineal_descendant_candidates": [ + "강형수", + "강지수" + ], + "renunciant_candidates": [] + }, + "legal_effect_candidate": null, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "event|상속개시|강호연|2024-08-10", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-003.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-003.json new file mode 100644 index 00000000..d086444a --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-003.json @@ -0,0 +1,135 @@ +{ + "evidence_index_proposed": "E-003", + "title": "각서 및 금전소비대차계약서", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 3 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "claim_doc", + "defense_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-003-01", + "event_or_state": "event", + "event_kind": "기타", + "event_subkind": null, + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "오국한이 오민한의 자금으로 신림동 상가를 매수하여 명의신탁했음을 확인하는 각서 작성", + "source_evidence_index_proposed": "E-003", + "source_fact_candidate": null, + "participants": { + "actor_candidates": ["오국한"], + "counterparty_candidates": [], + "beneficiary_candidates": ["오민한"], + "third_party_candidates": ["이경주"] + }, + "object_spec": "신림동 상가 명의신탁", + "fraction_ref": null, + "amount": "40,000,000원", + "event_date": "2015-09-17", + "event_end_date": null, + "time_text": "2015년 9월 17일", + "time_precision": "exact", + "location": null, + "legal_keywords": ["명의신탁", "각서"], + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "각서 1항", + "excerpt": "신림동 상가는 명의만 오국한으로 되어 있을 뿐, 오민한 소유임을 확인", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "명의신탁 확인", + "related_claim_type": "사해행위취소 대상", + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": { + "candidate_role_tags": ["사해행위목적물후보"] + }, + "identity_signature": "event-기타-오국한-오민한-신림동상가명의신탁-2015-09-17", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-003-02", + "event_or_state": "event", + "event_kind": "채권발생", + "event_subkind": "underlying_claim_arising", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "오국한이 오민한으로부터 1,000만 원을 차용하는 금전소비대차계약 체결", + "source_evidence_index_proposed": "E-003", + "source_fact_candidate": null, + "participants": { + "actor_candidates": ["오국한"], + "counterparty_candidates": ["오민한"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "대여금 채무", + "fraction_ref": null, + "amount": "10,000,000원", + "event_date": "2022-07-18", + "event_end_date": null, + "time_text": "2022. 7. 18.", + "time_precision": "exact", + "location": null, + "legal_keywords": ["금전소비대차", "대여", "차용"], + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "금전소비대차계약서", + "excerpt": "오국한은 오민한으로부터 1,000만 원을 차용", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "채권발생", + "related_claim_type": "대여금 채권", + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": { + "candidate_role_tags": ["원고채권후보"] + }, + "identity_signature": "event-채권발생-오국한-오민한-대여금채무-2022-07-18", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-004.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-004.json new file mode 100644 index 00000000..72d25594 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-004.json @@ -0,0 +1,74 @@ +{ + "evidence_index_proposed": "E-004", + "title": "감정평가서", + "doc_type": "기타", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 4 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "valuation_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-004-01", + "event_or_state": "state", + "event_kind": "가치평가", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "신림로 상가의 2022년~현재 시가 평가", + "source_evidence_index_proposed": "E-004", + "object_spec": "신림로 상가", + "event_date": null, + "time_precision": "range", + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "신림로 상가 감정평가 표", + "excerpt": "신림로 상가 시가 평가", + "directness": "직접" + } + ], + "current_state_candidate": { + "state_summary": "신림로 상가 시가 평가 내역", + "state_as_of": "2024-09-13", + "is_ongoing": true + }, + "identity_signature": "state|가치평가|신림로 상가|2024-09-13", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-004-02", + "event_or_state": "state", + "event_kind": "가치평가", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "포천시 임야의 2022년~현재 시가 평가", + "source_evidence_index_proposed": "E-004", + "object_spec": "포천시 임야", + "event_date": null, + "time_precision": "range", + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "포천시 임야 감정평가 표", + "excerpt": "포천시 임야 시가 평가", + "directness": "직접" + } + ], + "current_state_candidate": { + "state_summary": "포천시 임야 시가 평가 내역", + "state_as_of": "2024-09-13", + "is_ongoing": true + }, + "identity_signature": "state|가치평가|포천시 임야|2024-09-13", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-005.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-005.json new file mode 100644 index 00000000..840829d7 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-005.json @@ -0,0 +1,78 @@ +{ + "evidence_index_proposed": "E-005", + "title": "감정평가 의견서", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 5 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "valuation_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-005-01", + "event_or_state": "event", + "event_kind": "가치평가", + "event_subkind": null, + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "대상 토지(성수동 256 대)에 대한 2024년도 차임 시세 감정 결과 회신", + "source_evidence_index_proposed": "E-005", + "source_fact_candidate": null, + "participants": { + "actor_candidates": ["감정평가법인 광나루"], + "counterparty_candidates": ["강용원"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "성수동 대지 차임 시세", + "fraction_ref": null, + "amount": null, + "event_date": "2024-12-29", + "event_end_date": null, + "time_text": "2024년 12월 29일", + "time_precision": "exact", + "location": "서울 성동구 성수동 256", + "legal_keywords": ["감정평가", "차임 시세"], + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "차임 시세 확인", + "excerpt": "지상에 건물이 있는 경우, 없는 경우에 따른 보증금별 월 차임", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "가치평가", + "related_claim_type": null, + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": { + "candidate_role_tags": ["현재사용수익후보"] + }, + "identity_signature": "event|가치평가|감정평가법인광나루|강용원|성수동대지차임시세|2024-12-29", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-006.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-006.json new file mode 100644 index 00000000..98b96024 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-006.json @@ -0,0 +1,220 @@ +{ + "evidence_index_proposed": "E-006", + "title": "계약서", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 6 + }, + "property_cluster_id": null, + "doc_semantic_flags": ["mixed_contract_doc"], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-006-01", + "event_or_state": "event", + "event_kind": "임대차", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "성수동 대지 임대차 계약 체결", + "source_evidence_index_proposed": "E-006", + "participants": { + "actor_candidates": ["이문호"], + "counterparty_candidates": ["박성희"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "성수동 대지", + "event_date": "2024-11-20", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "1", + "excerpt": "임대차기간 계약일부터 2년으로 정하여 임대하기로 하고", + "directness": "직접" + } + ], + "identity_signature": "event임대차이문호박성희성수동대지2024-11-20", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-006-02", + "event_or_state": "event", + "event_kind": "기타", + "event_subkind": "대가수수", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "임대차보증금 지급", + "source_evidence_index_proposed": "E-006", + "participants": { + "actor_candidates": ["박성희"], + "counterparty_candidates": ["이문호"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "임대차보증금 3억 원", + "amount": "300,000,000원", + "event_date": "2024-11-20", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "1", + "excerpt": "계약 당일 임대차보증금을 지급받음", + "directness": "직접" + } + ], + "identity_signature": "event기타박성희이문호임대차보증금3억2024-11-20", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-006-03", + "event_or_state": "event", + "event_kind": "인도", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "성수동 대지 인도", + "source_evidence_index_proposed": "E-006", + "participants": { + "actor_candidates": ["이문호"], + "counterparty_candidates": ["박성희"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "성수동 대지", + "event_date": "2024-11-20", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "1", + "excerpt": "을에게 현상대로 인도한다", + "directness": "직접" + } + ], + "identity_signature": "event인도이문호박성희성수동대지2024-11-20", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-006-04", + "event_or_state": "event", + "event_kind": "처분", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "성수동 건물 매매 계약 체결", + "source_evidence_index_proposed": "E-006", + "participants": { + "actor_candidates": ["이문호"], + "counterparty_candidates": ["박성희"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "성수동 건물", + "event_date": "2024-11-20", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "2", + "excerpt": "상가를 대금 2억 원에 매도하기로 하고", + "directness": "직접" + } + ], + "identity_signature": "event처분이문호박성희성수동건물2024-11-20", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-006-05", + "event_or_state": "event", + "event_kind": "기타", + "event_subkind": "대가수수", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "매매대금 지급", + "source_evidence_index_proposed": "E-006", + "participants": { + "actor_candidates": ["박성희"], + "counterparty_candidates": ["이문호"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "매매대금 2억 원", + "amount": "200,000,000원", + "event_date": "2024-11-20", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "2", + "excerpt": "계약 당일 매매대금을 전액 지급받음", + "directness": "직접" + } + ], + "identity_signature": "event기타박성희이문호매매대금2억2024-11-20", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-006-06", + "event_or_state": "event", + "event_kind": "인도", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "성수동 건물 인도", + "source_evidence_index_proposed": "E-006", + "participants": { + "actor_candidates": ["이문호"], + "counterparty_candidates": ["박성희"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "성수동 건물", + "event_date": "2024-11-20", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "2", + "excerpt": "동시에 을에게 현상대로 인도한다", + "directness": "직접" + } + ], + "identity_signature": "event인도이문호박성희성수동건물2024-11-20", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-006-07", + "event_or_state": "state", + "event_kind": "점유", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "성수동 대지 및 건물 관리·사용권 귀속", + "source_evidence_index_proposed": "E-006", + "participants": { + "actor_candidates": [], + "counterparty_candidates": [], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "current_state_candidate": { + "state_summary": "박성희의 대지 및 건물 관리·사용 상태", + "state_as_of": "2024-11-20", + "state_start_date": "2024-11-20", + "is_ongoing": true, + "current_possessor_candidates": ["박성희"], + "current_user_candidates": ["박성희"], + "current_benefit_holder_candidates": ["박성희"], + "current_right_holder_candidates": [] + }, + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "4", + "excerpt": "을은 자신의 책임하에 상가와 대지를 관리·사용하고", + "directness": "직접" + } + ], + "identity_signature": "state점유박성희성수동대지및건물2024-11-20", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-007.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-007.json new file mode 100644 index 00000000..c355e230 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-007.json @@ -0,0 +1,146 @@ +{ + "evidence_index_proposed": "E-007", + "title": "공사도급계약서", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 7 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "claim_doc", + "schedule_doc", + "relief_object_reference_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-007-01", + "event_or_state": "event", + "event_kind": "기타", + "event_subkind": "contract_formation", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "경기 평택시 서정3길 123 건물 신축 공사도급계약 체결", + "source_evidence_index_proposed": "E-007", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [ + "김일동" + ], + "counterparty_candidates": [ + "박광윤" + ], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "평택시 서정3길 123 건물 신축공사", + "fraction_ref": null, + "amount": "1,000,000,000원", + "event_date": "2021-05-01", + "event_end_date": null, + "time_text": "2021. 5. 1.", + "time_precision": "exact", + "location": "경기 평택시 서정3길 123", + "legal_keywords": [ + "공사도급계약", + "신축공사" + ], + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "공사기간", + "excerpt": "2021. 5. 1. ~ 2021. 11. 10.", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "contract_formation", + "related_claim_type": "공사대금채권", + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "event+기타+김일동+박광윤+평택시건물신축공사+2021-05-01", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-007-02", + "event_or_state": "event", + "event_kind": "변제", + "event_subkind": "payment_consideration", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "공사대금 중 중도금 4억 원 지급 및 수령", + "source_evidence_index_proposed": "E-007", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [ + "김일동" + ], + "counterparty_candidates": [ + "박광윤" + ], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "평택시 서정3길 123 건물 신축공사 중도금", + "fraction_ref": null, + "amount": "400,000,000원", + "event_date": "2021-08-30", + "event_end_date": null, + "time_text": "2021. 8. 30.", + "time_precision": "exact", + "location": "경기 평택시 서정3길 123", + "legal_keywords": [ + "중도금", + "영수증" + ], + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "공사대금", + "excerpt": "중도금 4억 원은 2021. 8. 30. 기성고율 70% 달성 확인 시 지급한다.", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "payment_consideration", + "related_claim_type": "공사대금채권", + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "event+변제+김일동+박광윤+평택시건물신축공사중도금4억+2021-08-30", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-008.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-008.json new file mode 100644 index 00000000..222ea3b6 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-008.json @@ -0,0 +1,49 @@ +{ + "evidence_index_proposed": "E-008", + "title": "내용증명", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 8 + }, + "zero_event_reason": null, + "event_candidates": [ + { + "candidate_id_proposed": "EVT-008-01", + "event_or_state": "event", + "event_kind": "통지", + "event_subkind": "demand_notice_dispatch", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "강용원이 오민한에게 사해행위취소 및 소유권 반환을 요구하는 내용증명 발송", + "source_evidence_index_proposed": "E-008", + "participants": { + "actor_candidates": ["강용원"], + "counterparty_candidates": ["오민한"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "신림동 상가 소유권 반환 및 배상", + "event_date": "2024-09-15", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "우체국 증명", + "excerpt": "2024. 9. 15. 내용증명우편물로 발송", + "directness": "직접" + } + ], + "legal_effect_candidate": { + "effect_type": "사해행위취소 및 반환 청구", + "review_flags": ["사해행위취소 검토"] + }, + "identity_signature": "event통지강용원오민한신림동상가반환요구2024-09-15", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-009.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-009.json new file mode 100644 index 00000000..610aab44 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-009.json @@ -0,0 +1,227 @@ +{ + "evidence_index_proposed": "E-009", + "title": "내용증명", + "doc_type": "기타", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 9 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "notice_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-009-01", + "event_or_state": "event", + "event_kind": "통지", + "event_subkind": "해제/해지의사표시", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "윤건우가 양정숙에게 임대차계약 해지 및 상가 인도 통지", + "source_evidence_index_proposed": "E-009", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [ + "윤건우" + ], + "counterparty_candidates": [ + "양정숙" + ], + "beneficiary_candidates": [], + "third_party_candidates": [ + "이수인" + ] + }, + "object_spec": "흑석동 상가 임대차계약", + "fraction_ref": null, + "amount": null, + "event_date": "2024-11-29", + "event_end_date": null, + "time_text": "2024-11-29", + "time_precision": "exact", + "location": null, + "legal_keywords": [ + "임대차계약 해지", + "명도" + ], + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "본문", + "excerpt": "따라서 이 서면을 통해 임대차계약을 해지하오니", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "임대차계약 해지 통지", + "related_claim_type": "임대차", + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [ + "5-라" + ], + "actio_relevance_candidates": null, + "identity_signature": "event통지윤건우양정숙흑석동상가2024-11-29", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-009-02", + "event_or_state": "event", + "event_kind": "도달", + "event_subkind": null, + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "내용증명 도달", + "source_evidence_index_proposed": "E-009", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [ + "윤건우" + ], + "counterparty_candidates": [ + "양정숙" + ], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "내용증명", + "fraction_ref": null, + "amount": null, + "event_date": "2024-11-30", + "event_end_date": null, + "time_text": "2024-11-30", + "time_precision": "exact", + "location": null, + "legal_keywords": [ + "도달" + ], + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "우편물배달증명서", + "excerpt": "배달연월일 2024년11월30일", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "통지 도달", + "related_claim_type": "임대차", + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [ + "5-라" + ], + "actio_relevance_candidates": null, + "identity_signature": "event도달윤건우양정숙내용증명2024-11-30", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-009-03", + "event_or_state": "state", + "event_kind": "항변", + "event_subkind": "책임부인", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "소유자의 임대차 책임 부인 및 차임 연체 주장", + "source_evidence_index_proposed": "E-009", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [ + "윤건우" + ], + "counterparty_candidates": [ + "양정숙" + ], + "beneficiary_candidates": [], + "third_party_candidates": [ + "이수인" + ] + }, + "object_spec": "임대차 책임", + "fraction_ref": null, + "amount": null, + "event_date": null, + "event_end_date": null, + "time_text": null, + "time_precision": "unknown", + "location": null, + "legal_keywords": [ + "책임부인", + "차임연체" + ], + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "본문", + "excerpt": "매매계약이 해제된 마당에 제가 이수인이 체결한 임대차계약에 대해서 책임을 질 하등의 이유가 없습니다.", + "directness": "직접" + } + ], + "current_state_candidate": { + "state_summary": "소유자의 임대차 책임 부인 및 연체 주장", + "state_as_of": "2024-11-29", + "state_start_date": null, + "state_end_date_candidate": null, + "superseded_by_candidate_id": null, + "is_ongoing": true, + "current_possessor_candidates": [], + "current_user_candidates": [], + "current_benefit_holder_candidates": [], + "current_right_holder_candidates": [], + "possession_legal_cause_asserted": "미상", + "possession_legal_cause_asserted_by": [] + }, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "임대차 책임 부인 주장", + "related_claim_type": "임대차", + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [ + "5-라" + ], + "actio_relevance_candidates": null, + "identity_signature": "state항변윤건우양정숙임대차책임", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-010.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-010.json new file mode 100644 index 00000000..ee3b3eec --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-010.json @@ -0,0 +1,154 @@ +{ + "evidence_index_proposed": "E-010", + "title": "내용증명에 대한 답신", + "doc_type": "통신기록", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 10 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "response_doc", + "notice_doc", + "admission_doc", + "defense_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-010-01", + "event_or_state": "event", + "event_kind": "통지", + "event_subkind": "demand_notice_arrival", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "오민한이 강용원의 이행최고서를 2024. 9. 20. 수령함", + "source_evidence_index_proposed": "E-010", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [ + "강용원" + ], + "counterparty_candidates": [ + "오민한" + ], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "이행최고서", + "fraction_ref": null, + "amount": null, + "event_date": "2024-09-20", + "event_end_date": null, + "time_text": "2024. 9. 20.", + "time_precision": "exact", + "location": null, + "legal_keywords": [ + "이행최고서" + ], + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "제1항", + "excerpt": "귀하가 보낸 이행최고서는 2024. 9. 20. 잘 받아 보았습니다.", + "directness": "직접" + } + ], + "legal_effect_candidate": { + "effect_type": "notice_arrival", + "related_claim_type": "인테리어 자재대금", + "review_flags": [] + }, + "identity_signature": "event|통지|강용원|오민한|이행최고서|2024-09-20", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-010-02", + "event_or_state": "event", + "event_kind": "통지", + "event_subkind": "demand_notice_dispatch", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "오민한이 강용원에게 답신 내용증명을 2024. 9. 30. 발송함", + "source_evidence_index_proposed": "E-010", + "participants": { + "actor_candidates": [ + "오민한" + ], + "counterparty_candidates": [ + "강용원" + ], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "내용증명", + "event_date": "2024-09-30", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "발송증명", + "excerpt": "2024. 9. 30. 내용증명우편물로 발송하였음을 증명함", + "directness": "직접" + } + ], + "legal_effect_candidate": { + "effect_type": "notice_dispatch", + "related_claim_type": "인테리어 자재대금", + "review_flags": [] + }, + "identity_signature": "event|통지|오민한|강용원|내용증명|2024-09-30", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-010-03", + "event_or_state": "event", + "event_kind": "항변", + "event_subkind": "defense_assertion_setoff", + "action_type_candidate": "소송행위(litigation acts)", + "action_summary": "오민한이 자신의 5,000만 원 채권으로 강용원의 채권을 상계하겠다고 주장함", + "source_evidence_index_proposed": "E-010", + "participants": { + "actor_candidates": [ + "오민한" + ], + "counterparty_candidates": [ + "강용원" + ], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "5,000만 원 채권", + "amount": "50,000,000원", + "legal_keywords": [ + "상계" + ], + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "제6항", + "excerpt": "본인의 채권 합계금 5,000만 원으로 귀하의 채권을 소멸시키겠습니다.", + "directness": "직접" + } + ], + "legal_effect_candidate": { + "effect_type": "defense_assertion", + "related_claim_type": "인테리어 자재대금", + "review_flags": [ + "requires_rebuttal_in_stage2" + ] + }, + "defense_candidate": { + "defense_type": "setoff", + "assertion_summary": "본인 채권 5,000만 원으로 상계하겠음", + "requires_rebuttal_in_stage2": true + }, + "identity_signature": "event|항변|오민한|강용원|50000000원|null", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-011.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-011.json new file mode 100644 index 00000000..47124bcc --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-011.json @@ -0,0 +1,124 @@ +{ + "evidence_index_proposed": "E-011", + "title": "답변서", + "doc_type": "기타", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 11 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "response_doc", + "status_doc", + "defense_doc", + "notice_receipt_admission_doc", + "lien_assertion_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-011-01", + "event_or_state": "event", + "event_kind": "도달", + "event_subkind": "demand_notice_arrival", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "2024. 8. 29. 자 유치권소멸청구 통지서 수령", + "source_evidence_index_proposed": "E-011", + "participants": { + "actor_candidates": ["박광윤"], + "counterparty_candidates": ["강용원", "양정숙"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "유치권소멸청구 통지서", + "event_date": "2024-09-01", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "1번 항목", + "excerpt": "귀하들이 보낸 2024. 8. 29. 자 통지서는 2024. 9. 1. 잘 받아 보았습니다.", + "directness": "직접" + } + ], + "legal_effect_candidate": { + "effect_type": "notice_receipt", + "review_flags": ["receipt_admitted"] + }, + "identity_signature": "event|도달|박광윤|강용원양정숙|유치권소멸청구통지서|2024-09-01", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-011-02", + "event_or_state": "state", + "event_kind": "점유", + "event_subkind": "ongoing_physical_possession", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "서정빌라 101호에 대한 유치권 행사 및 거주", + "source_evidence_index_proposed": "E-011", + "participants": { + "actor_candidates": ["박광윤"], + "counterparty_candidates": ["강용원", "양정숙"], + "beneficiary_candidates": [], + "third_party_candidates": ["김일동"] + }, + "object_spec": "서정빌라 101호", + "current_state_candidate": { + "state_summary": "현재까지 유치권 행사 중이며 가족과 함께 거주", + "state_as_of": "2024-10-31", + "is_ongoing": true, + "current_possessor_candidates": ["박광윤"], + "current_user_candidates": ["박광윤"], + "possession_legal_cause_asserted": "유치권행사중", + "possession_legal_cause_asserted_by": ["박광윤"] + }, + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "2번 항목", + "excerpt": "현재까지 서정빌라 101호에 관한 유치권을 행사 중입니다.", + "directness": "직접" + } + ], + "legal_effect_candidate": { + "effect_type": "lien_assertion" + }, + "identity_signature": "state|점유|박광윤|강용원양정숙|서정빌라101호|2024-10-31", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-011-03", + "event_or_state": "state", + "event_kind": "항변", + "event_subkind": "defense_assertion_benefit_applied_to_secured_claim", + "action_type_candidate": "소송행위(litigation acts)", + "action_summary": "과거 임대료 및 거주 수익을 채무에 충당 주장", + "source_evidence_index_proposed": "E-011", + "participants": { + "actor_candidates": ["박광윤"], + "counterparty_candidates": ["강용원", "양정숙"] + }, + "object_spec": "공사대금 채권", + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "4번 항목", + "excerpt": "위 차임이나 저와 제 가족이 서정빌라 101호에 거주하는 기간의 차임을 공제하더라도 여전히 3억 원 가까운 채무가 남아 있고", + "directness": "직접" + } + ], + "legal_effect_candidate": { + "effect_type": "defense_assertion", + "review_flags": ["requires_rebuttal_in_stage2"] + }, + "identity_signature": "state|항변|박광윤|강용원양정숙|공사대금채권|2024-10-31", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-012.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-012.json new file mode 100644 index 00000000..dfc4519f --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-012.json @@ -0,0 +1,156 @@ +{ + "evidence_index_proposed": "E-012", + "title": "등기말소 신청에 대한 답신", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 12 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "notice_doc", + "response_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-012-01", + "event_or_state": "event", + "event_kind": "통지", + "event_subkind": "통지", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "주식회사 대한은행이 강용원의 성수동 대지 근저당권 말소 신청을 거부하는 통지를 발송함", + "source_evidence_index_proposed": "E-012", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [ + "주식회사 대한은행" + ], + "counterparty_candidates": [ + "강용원" + ], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "성수동 대지 근저당권 말소 신청 거부 통지", + "fraction_ref": null, + "amount": null, + "event_date": "2024-12-22", + "event_end_date": null, + "time_text": "2024-12-22", + "time_precision": "exact", + "location": null, + "legal_keywords": [ + "등기말소 신청", + "거부", + "답신" + ], + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "본사는 소관 지점을 통해 귀하가 2024. 11. 20. 자로 제출한 [등기말소 신청서]를 검토한 결과, 아래와 같이 답신하고자 합니다.", + "excerpt": "등기말소 신청에 대한 답신", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "통지", + "related_claim_type": null, + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "event+통지+주식회사 대한은행+강용원+성수동 대지 근저당권 말소 신청 거부 통지+2024-12-22", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-012-02", + "event_or_state": "event", + "event_kind": "항변", + "event_subkind": "defense_assertion_subsequent_owner_no_extinction_right", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "주식회사 대한은행이 경매 절차의 적법성과 경매의 공신력(민사집행법 제267조)을 주장하며 근저당권 말소 불가 통지", + "source_evidence_index_proposed": "E-012", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [ + "주식회사 대한은행" + ], + "counterparty_candidates": [ + "강용원" + ], + "beneficiary_candidates": [], + "third_party_candidates": [ + "경매법원" + ] + }, + "object_spec": "경매의 공신력 주장", + "fraction_ref": null, + "amount": null, + "event_date": "2024-12-22", + "event_end_date": null, + "time_text": "2024-12-22", + "time_precision": "exact", + "location": null, + "legal_keywords": [ + "민사집행법 제267조", + "경매의 공신력", + "경매절차" + ], + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "민사집행법 제267조는 경매의 공신력을 인정하는 규정을 두고 있는 것입니다.", + "excerpt": "경매의 공신력 주장", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "항변", + "related_claim_type": "근저당권 말소", + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [ + "Stage 2 claim description" + ], + "review_flags": [ + "Stage 2 claim description" + ] + }, + "defense_candidate": { + "defense_type": "소유권 방어", + "requires_rebuttal_in_stage2": true + }, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "event+항변+주식회사 대한은행+강용원+경매 공신력 주장+2024-12-22", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-013.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-013.json new file mode 100644 index 00000000..cee2706b --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-013.json @@ -0,0 +1,181 @@ +{ + "evidence_index_proposed": "E-013", + "title": "등기사항전부증명서(말소사항 포함)-토지", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 13 + }, + "property_cluster_id": "성수동_토지", + "doc_semantic_flags": [ + "registry_doc", + "multi_event_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": ["성수동_토지"], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-013-01", + "event_or_state": "event", + "event_kind": "소유권이전", + "event_subkind": null, + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "강석우 사망으로 인한 정유심, 강용원 지분 상속", + "source_evidence_index_proposed": "E-013", + "participants": { + "actor_candidates": ["정유심", "강용원"], + "third_party_candidates": ["강석우"] + }, + "object_spec": "성수동 대지 5/3 및 5/2 지분", + "fraction_ref": "3/5, 2/5", + "event_date": "2022-11-08", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "갑구 순위번호 3", + "excerpt": "상속", + "directness": "직접" + } + ], + "identity_signature": "event|소유권이전|정유심,강용원|강석우|성수동대지지분|2022-11-08", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-013-02", + "event_or_state": "event", + "event_kind": "증여", + "event_subkind": null, + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "정유심의 성수동 대지 3/5 지분 강용원에게 증여", + "source_evidence_index_proposed": "E-013", + "participants": { + "actor_candidates": ["정유심"], + "beneficiary_candidates": ["강용원"] + }, + "object_spec": "성수동 대지 3/5 지분", + "fraction_ref": "3/5", + "event_date": "2023-04-06", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "갑구 순위번호 4", + "excerpt": "증여", + "directness": "직접" + } + ], + "identity_signature": "event|증여|정유심|강용원|성수동대지3/5지분|2023-04-06", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-013-03", + "event_or_state": "event", + "event_kind": "경매개시", + "event_subkind": "auction_application_after_extinguishment", + "action_type_candidate": "소송행위(litigation acts)", + "action_summary": "오민환의 담보권실행을 위한 경매개시결정", + "source_evidence_index_proposed": "E-013", + "participants": { + "actor_candidates": ["오민환"], + "third_party_candidates": ["법원"] + }, + "object_spec": "성수동 대지", + "event_date": "2024-07-21", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "갑구 순위번호 5", + "excerpt": "담보권실행을위한경매개시결정", + "directness": "직접" + } + ], + "identity_signature": "event|경매개시|오민환|법원|성수동대지|2024-07-21", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-013-04", + "event_or_state": "event", + "event_kind": "경매매각", + "event_subkind": "auction_sale_and_registry_transfer", + "action_type_candidate": "소송행위(litigation acts)", + "action_summary": "경매로 인한 이문호의 소유권 취득", + "source_evidence_index_proposed": "E-013", + "participants": { + "actor_candidates": ["이문호"], + "third_party_candidates": ["법원"] + }, + "object_spec": "성수동 대지", + "event_date": "2024-10-05", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "갑구 순위번호 6", + "excerpt": "담보권실행을위한경매로인한매각", + "directness": "직접" + } + ], + "identity_signature": "event|경매매각|이문호|법원|성수동대지|2024-10-05", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-013-05", + "event_or_state": "event", + "event_kind": "담보말소", + "event_subkind": "mortgage_extinguishment_by_fraction", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "경매로 인한 오민환의 기존 근저당권 말소", + "source_evidence_index_proposed": "E-013", + "participants": { + "counterparty_candidates": ["오민환"] + }, + "object_spec": "기존 근저당권", + "event_date": "2024-10-05", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "을구 순위번호 3", + "excerpt": "근저당권설정등기말소", + "directness": "직접" + } + ], + "identity_signature": "event|담보말소|미상|오민환|기존근저당권|2024-10-05", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-013-06", + "event_or_state": "event", + "event_kind": "담보설정", + "event_subkind": "secured_debt_creation", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "주식회사 대한은행의 근저당권 설정", + "source_evidence_index_proposed": "E-013", + "participants": { + "actor_candidates": ["주식회사 대한은행"], + "counterparty_candidates": ["이문호"] + }, + "object_spec": "성수동 대지", + "amount": "520,000,000원", + "event_date": "2024-10-05", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "을구 순위번호 4", + "excerpt": "근저당권설정", + "directness": "직접" + } + ], + "identity_signature": "event|담보설정|주식회사대한은행|이문호|성수동대지/520000000원|2024-10-05", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-014.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-014.json new file mode 100644 index 00000000..bc95cf47 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-014.json @@ -0,0 +1,198 @@ +{ + "evidence_index_proposed": "E-014", + "title": "등기사항전부증명서 (말소사항 포함) -건물", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 14 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "registry_doc", + "multi_event_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-014-01", + "event_or_state": "state", + "event_kind": "소유권이전", + "event_subkind": null, + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "이경주 소유권보존", + "source_evidence_index_proposed": "E-014", + "participants": { + "actor_candidates": ["이경주"], + "counterparty_candidates": [], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "서울특별시 관악구 신림로 115 건물", + "event_date": "2006-05-08", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "갑구 순위번호 1", + "excerpt": "소유권보존 이경주", + "directness": "직접" + } + ], + "identity_signature": "state|소유권이전|이경주|null|서울특별시 관악구 신림로 115 건물|2006-05-08", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-014-02", + "event_or_state": "event", + "event_kind": "소유권이전", + "event_subkind": null, + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "오국환 소유권이전(매매)", + "source_evidence_index_proposed": "E-014", + "participants": { + "actor_candidates": ["이경주"], + "counterparty_candidates": ["오국환"], + "beneficiary_candidates": ["오국환"], + "third_party_candidates": [] + }, + "object_spec": "서울특별시 관악구 신림로 115 건물", + "amount": "40,000,000원", + "event_date": "2015-09-17", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "갑구 순위번호 2", + "excerpt": "매매 오국환", + "directness": "직접" + } + ], + "identity_signature": "event|소유권이전|이경주|오국환|서울특별시 관악구 신림로 115 건물|2015-09-17", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-014-03", + "event_or_state": "event", + "event_kind": "소유권이전", + "event_subkind": null, + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "오민한 소유권이전(매매)", + "source_evidence_index_proposed": "E-014", + "participants": { + "actor_candidates": ["오국환"], + "counterparty_candidates": ["오민한"], + "beneficiary_candidates": ["오민한"], + "third_party_candidates": [] + }, + "object_spec": "서울특별시 관악구 신림로 115 건물", + "amount": "150,000,000원", + "event_date": "2023-03-17", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "갑구 순위번호 3", + "excerpt": "매매 오민한", + "directness": "직접" + } + ], + "actio_relevance_candidates": { + "candidate_role_tags": ["사해행위목적물후보"] + }, + "identity_signature": "event|소유권이전|오국환|오민한|서울특별시 관악구 신림로 115 건물|2023-03-17", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-014-04", + "event_or_state": "event", + "event_kind": "담보설정", + "event_subkind": "secured_debt_creation", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "나임오 근저당권 설정", + "source_evidence_index_proposed": "E-014", + "participants": { + "actor_candidates": ["오국환"], + "counterparty_candidates": ["나임오"], + "beneficiary_candidates": ["나임오"], + "third_party_candidates": [] + }, + "object_spec": "서울특별시 관악구 신림로 115 건물", + "amount": "50,000,000원", + "event_date": "2021-08-01", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "을구 순위번호 1", + "excerpt": "근저당권설정 나임오", + "directness": "직접" + } + ], + "identity_signature": "event|담보설정|오국환|나임오|서울특별시 관악구 신림로 115 건물|2021-08-01", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-014-05", + "event_or_state": "event", + "event_kind": "담보설정", + "event_subkind": "secured_debt_creation", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "강용일 근저당권 설정", + "source_evidence_index_proposed": "E-014", + "participants": { + "actor_candidates": ["오국환"], + "counterparty_candidates": ["강용일"], + "beneficiary_candidates": ["강용일"], + "third_party_candidates": [] + }, + "object_spec": "서울특별시 관악구 신림로 115 건물", + "amount": "150,000,000원", + "event_date": "2022-04-18", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "을구 순위번호 2", + "excerpt": "근저당권설정 강용일", + "directness": "직접" + } + ], + "identity_signature": "event|담보설정|오국환|강용일|서울특별시 관악구 신림로 115 건물|2022-04-18", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-014-06", + "event_or_state": "event", + "event_kind": "담보말소", + "event_subkind": "mortgage_extinguishment_by_fraction", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "2번 근저당권 말소", + "source_evidence_index_proposed": "E-014", + "participants": { + "actor_candidates": [], + "counterparty_candidates": [], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "2번 근저당권", + "event_date": "2024-10-17", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "을구 순위번호 3", + "excerpt": "2번근저당권 설정등기말소", + "directness": "직접" + } + ], + "identity_signature": "event|담보말소|null|null|2번 근저당권|2024-10-17", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-015.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-015.json new file mode 100644 index 00000000..5ae9e18a --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-015.json @@ -0,0 +1,168 @@ +{ + "evidence_index_proposed": "E-015", + "title": "등기사항전부증명서(말소사항 포함)-집합건물", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 15 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "registry_doc", + "multi_event_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-015-01", + "event_or_state": "event", + "event_kind": "소유권이전", + "event_subkind": "소유권보존", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "소유권보존등기(김일동)", + "source_evidence_index_proposed": "E-015", + "source_fact_candidate": null, + "participants": { + "actor_candidates": ["김일동"], + "counterparty_candidates": [], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "평택시 서정3길 123 서정빌라 101호", + "fraction_ref": null, + "amount": null, + "event_date": "2021-12-10", + "event_end_date": null, + "time_text": "2021년12월10일", + "time_precision": "exact", + "location": "경기도 평택시 서정3길 123", + "legal_keywords": ["소유권보존"], + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "갑구1", + "excerpt": "소유권보존", + "directness": "직접" + } + ], + "identity_signature": "event|소유권이전|김일동|없음|서정빌라 101호|2021-12-10", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-015-02", + "event_or_state": "event", + "event_kind": "소유권이전", + "event_subkind": "매매", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "매매로 인한 소유권이전(정남이)", + "source_evidence_index_proposed": "E-015", + "source_fact_candidate": null, + "participants": { + "actor_candidates": ["정남이"], + "counterparty_candidates": ["김일동"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "평택시 서정3길 123 서정빌라 101호", + "fraction_ref": null, + "amount": null, + "event_date": "2021-12-30", + "event_end_date": null, + "time_text": "2021년12월30일", + "time_precision": "exact", + "location": "경기도 평택시 서정3길 123", + "legal_keywords": ["소유권이전", "매매"], + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "갑구2", + "excerpt": "소유권이전", + "directness": "직접" + } + ], + "identity_signature": "event|소유권이전|정남이|김일동|서정빌라 101호|2021-12-30", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-015-03", + "event_or_state": "event", + "event_kind": "가압류/압류", + "event_subkind": "가압류", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "가압류 설정(김동국)", + "source_evidence_index_proposed": "E-015", + "source_fact_candidate": null, + "participants": { + "actor_candidates": ["김동국"], + "counterparty_candidates": ["정남이"], + "beneficiary_candidates": [], + "third_party_candidates": ["수원지방법원 평택지원"] + }, + "object_spec": "평택시 서정3길 123 서정빌라 101호", + "fraction_ref": null, + "amount": "14,000,000원", + "event_date": "2022-09-01", + "event_end_date": null, + "time_text": "2022년9월1일", + "time_precision": "exact", + "location": "경기도 평택시 서정3길 123", + "legal_keywords": ["가압류"], + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "갑구3", + "excerpt": "가압류", + "directness": "직접" + } + ], + "legal_effect_candidate": { + "effect_type": "가압류", + "related_claim_type": "금전채권", + "review_flags": ["가압류_말소"] + }, + "identity_signature": "event|가압류/압류|김동국|정남이|14,000,000원|2022-09-01", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-015-04", + "event_or_state": "event", + "event_kind": "소유권이전", + "event_subkind": null, + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "소유권이전(강호연)", + "source_evidence_index_proposed": "E-015", + "source_fact_candidate": null, + "participants": { + "actor_candidates": ["강호연"], + "counterparty_candidates": ["정남이"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "평택시 서정3길 123 서정빌라 101호", + "fraction_ref": null, + "amount": null, + "event_date": "2022-10-01", + "event_end_date": null, + "time_text": "2022년10월1일", + "time_precision": "exact", + "location": "경기도 평택시 서정3길 123", + "legal_keywords": ["소유권이전"], + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "갑구4", + "excerpt": "소유권이전", + "directness": "직접" + } + ], + "identity_signature": "event|소유권이전|강호연|정남이|서정빌라 101호|2022-10-01", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-016.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-016.json new file mode 100644 index 00000000..9288bece --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-016.json @@ -0,0 +1,22 @@ +{ + "evidence_index_proposed": "E-016", + "title": "별지 목록", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 16 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "schedule_doc", + "relief_object_reference_doc" + ], + "zero_event_reason": "REFERENCE_ONLY_DOC", + "affected_asset_cluster_ids": [], + "event_candidates": [], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-017.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-017.json new file mode 100644 index 00000000..31da71c1 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-017.json @@ -0,0 +1,138 @@ +{ + "evidence_index_proposed": "E-017", + "title": "부동산 매매계약서", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 17 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "claim_doc", + "mixed_contract_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-017-01", + "event_or_state": "event", + "event_kind": "처분", + "event_subkind": null, + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "흑석동 상가에 대한 매매계약 체결", + "source_evidence_index_proposed": "E-017", + "participants": { + "actor_candidates": ["윤건우"], + "counterparty_candidates": ["이수인"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "흑석동 상가(201호)", + "amount": "1,000,000,000원", + "event_date": "2023-10-01", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "계약내용", + "excerpt": "매매대금: 10억 원(1,000,000,000원)", + "directness": "직접" + } + ], + "identity_signature": "event|처분|윤건우|이수인|흑석동상가|2023-10-01", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-017-02", + "event_or_state": "event", + "event_kind": "기타", + "event_subkind": "대가수수", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "계약금 2억 원 지급", + "source_evidence_index_proposed": "E-017", + "participants": { + "actor_candidates": ["이수인"], + "counterparty_candidates": ["윤건우"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "계약금", + "amount": "200,000,000원", + "event_date": "2023-10-01", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "계약내용", + "excerpt": "계 약 금: 2억 원(200,000,000원) 계약 당일 지급", + "directness": "직접" + } + ], + "identity_signature": "event|기타|이수인|윤건우|계약금|2023-10-01", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-017-03", + "event_or_state": "event", + "event_kind": "기타", + "event_subkind": "대가수수", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "중도금 5억 원 지급", + "source_evidence_index_proposed": "E-017", + "participants": { + "actor_candidates": ["이수인"], + "counterparty_candidates": ["윤건우"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "중도금", + "amount": "500,000,000원", + "event_date": "2024-01-01", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "계약내용", + "excerpt": "중 도 금: 5억 원(500,000,000원) 2024. 1. 1. 지급", + "directness": "직접" + } + ], + "identity_signature": "event|기타|이수인|윤건우|중도금|2024-01-01", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-017-04", + "event_or_state": "event", + "event_kind": "인도", + "event_subkind": null, + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "흑석동 상가 인도", + "source_evidence_index_proposed": "E-017", + "participants": { + "actor_candidates": ["윤건우"], + "counterparty_candidates": ["이수인"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "흑석동 상가(201호)", + "event_date": "2024-01-01", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "특약사항", + "excerpt": "매도인은 매수인으로부터 중도금을 지급받은 날 매수인에게 위 부동산을 인도한다.", + "directness": "직접" + } + ], + "identity_signature": "event|인도|윤건우|이수인|흑석동상가|2024-01-01", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-018.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-018.json new file mode 100644 index 00000000..3f1d657c --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-018.json @@ -0,0 +1,85 @@ +{ + "evidence_index_proposed": "E-018", + "title": "사업자등록증", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 18 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "status_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-018-01", + "event_or_state": "state", + "event_kind": "사업자등록", + "event_subkind": null, + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "양정숙의 맛나식당 사업자등록 및 영업 개시", + "source_evidence_index_proposed": "E-018", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [ + "양정숙" + ], + "counterparty_candidates": [], + "beneficiary_candidates": [], + "third_party_candidates": [ + "동작 세무서장" + ] + }, + "object_spec": "맛나식당", + "fraction_ref": null, + "amount": null, + "event_date": "2024-02-01", + "event_end_date": null, + "time_text": "2024년 2월 1일", + "time_precision": "exact", + "location": "서울 동작구 흑석로 80, 201호", + "legal_keywords": [], + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "개업 연월일", + "excerpt": "2024년 2월 1일", + "directness": "직접" + } + ], + "current_state_candidate": { + "state_summary": "맛나식당 운영 중", + "state_as_of": "현재", + "state_start_date": "2024-02-01", + "state_end_date_candidate": null, + "superseded_by_candidate_id": null, + "is_ongoing": true, + "current_possessor_candidates": [], + "current_user_candidates": [ + "양정숙" + ], + "current_benefit_holder_candidates": [ + "양정숙" + ], + "current_right_holder_candidates": [], + "possession_legal_cause_asserted": null, + "possession_legal_cause_asserted_by": [] + }, + "succession_candidate": null, + "legal_effect_candidate": null, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "state|사업자등록|양정숙|맛나식당|2024-02-01", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-019.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-019.json new file mode 100644 index 00000000..d547f9d1 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-019.json @@ -0,0 +1,121 @@ +{ + "evidence_index_proposed": "E-019", + "title": "소멸청구 통지서", + "doc_type": "기타", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 19 + }, + "property_cluster_id": "해당 주거용 부동산", + "doc_semantic_flags": [ + "notice_doc", + "lien_assertion_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-019-01", + "event_or_state": "event", + "event_kind": "통지", + "event_subkind": "lien_extinction_notice_dispatch", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "유치권 소멸청구 통지서 발송", + "source_evidence_index_proposed": "E-019", + "participants": { + "actor_candidates": ["강용원", "양정숙"], + "counterparty_candidates": ["박광윤"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "평택시 서정동 서정빌라 101호 유치권 소멸청구", + "event_date": "2024-08-29", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "7", + "excerpt": "발신인들은 이 통지서를 통해 귀하가 행사하는 유치권의 소멸을 청구하오니", + "directness": "직접" + } + ], + "legal_effect_candidate": { + "effect_type": "유치권소멸청구", + "review_flags": ["NOTICE_DISPATCH"] + }, + "identity_signature": "event|통지|강용원,양정숙|박광윤|평택시서정빌라101호유치권소멸청구|2024-08-29", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-019-02", + "event_or_state": "state", + "event_kind": "점유", + "event_subkind": "ongoing_physical_possession", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "박광윤의 평택시 빌라 101호 현재 점유 상태", + "source_evidence_index_proposed": "E-019", + "participants": { + "actor_candidates": ["박광윤"], + "counterparty_candidates": [], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "평택시 서정동 서정빌라 101호", + "current_state_candidate": { + "state_summary": "평택시 빌라 101호 현재 거주 및 점유 중", + "state_as_of": "2024-08-29", + "is_ongoing": true, + "current_possessor_candidates": ["박광윤"], + "possession_legal_cause_asserted": "유치권행사중" + }, + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "3", + "excerpt": "서정빌라 101호를 점거하고, 직접 거주까지 해 오셨습니다.", + "directness": "직접" + } + ], + "identity_signature": "state|점유|박광윤||평택시서정빌라101호|2024-08-29", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-019-03", + "event_or_state": "event", + "event_kind": "기타", + "event_subkind": "lien_violation_candidate", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "유치권자 박광윤의 평택시 빌라 임의 임대 행위", + "source_evidence_index_proposed": "E-019", + "participants": { + "actor_candidates": ["박광윤"], + "counterparty_candidates": [], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "평택시 서정동 서정빌라 101호 임대", + "event_date": "2022-02-01", + "event_end_date": "2022-07-31", + "time_precision": "range", + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "4", + "excerpt": "귀하는 2022년 2월부터 7월까지 6개월간 서정빌라 101호를 인근 공장의 직원 숙소로 임대를 놓아 임대수익도 벌어들였습니다.", + "directness": "직접" + } + ], + "legal_effect_candidate": { + "effect_type": "유치권의무위반", + "review_flags": ["LIEN_VIOLATION"] + }, + "identity_signature": "event|기타|박광윤||평택시서정빌라101호임대|2022-02-01", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-020.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-020.json new file mode 100644 index 00000000..4a931524 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-020.json @@ -0,0 +1,190 @@ +{ + "evidence_index_proposed": "E-020", + "title": "영업양도 계약서", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 20 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "business_transfer_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-020-01", + "event_or_state": "event", + "event_kind": "영업양도", + "event_subkind": null, + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "김선웅과 오민한 사이의 해드림(SUN-DREAM) 영업양도 계약 성립", + "source_evidence_index_proposed": "E-020", + "source_fact_candidate": null, + "participants": { + "actor_candidates": ["김선웅"], + "counterparty_candidates": ["오민한"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "해드림(SUN-DREAM) 영업 일체", + "fraction_ref": null, + "amount": null, + "event_date": "2024-07-05", + "event_end_date": null, + "time_text": "2024년 7월 5일", + "time_precision": "exact", + "location": "하남시 감북동 238", + "legal_keywords": ["영업양도", "양도양수"], + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "제1조", + "excerpt": "갑은 ... 영업에 필요한 자산 일체를 을에게 양도하고", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "business_transfer_formation", + "related_claim_type": null, + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": ["1-다"], + "actio_relevance_candidates": null, + "identity_signature": "event|영업양도|김선웅|오민한|해드림|2024-07-05", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-020-02", + "event_or_state": "event", + "event_kind": "기타", + "event_subkind": "대가수수", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "영업양도 대가 2억 원 지급 약정", + "source_evidence_index_proposed": "E-020", + "source_fact_candidate": null, + "participants": { + "actor_candidates": ["오민한"], + "counterparty_candidates": ["김선웅"], + "beneficiary_candidates": ["김선웅"], + "third_party_candidates": [] + }, + "object_spec": "영업양도 대금", + "fraction_ref": null, + "amount": "200,000,000원", + "event_date": "2024-07-31", + "event_end_date": null, + "time_text": "2024. 7. 말일까지", + "time_precision": "exact", + "location": null, + "legal_keywords": ["양도대금", "지급"], + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "제2조", + "excerpt": "을은 위 양도의 대가로 2024. 7. 말일까지 갑에게 대금 2억 원을 지급한다.", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "payment_obligation", + "related_claim_type": null, + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "event|기타|오민한|김선웅|2억|2024-07-31", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-020-03", + "event_or_state": "event", + "event_kind": "영업양도", + "event_subkind": "commercial_successor_liability_candidate", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "영업양도에 따른 고객관계 및 고용관계 승계 약정", + "source_evidence_index_proposed": "E-020", + "source_fact_candidate": null, + "participants": { + "actor_candidates": ["오민한"], + "counterparty_candidates": ["김선웅"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "고객관계, 고용관계", + "fraction_ref": null, + "amount": null, + "event_date": "2024-07-05", + "event_end_date": null, + "time_text": "본 계약과 동시에", + "time_precision": "exact", + "location": null, + "legal_keywords": ["승계", "고객관계", "고용관계"], + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "제3조", + "excerpt": "을은 ... 고객관계, 고용관계를 승계하고", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "business_succession", + "related_claim_type": null, + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": { + "transferor_name": "김선웅", + "transferee_name": "오민한", + "business_name": "해드림(SUN-DREAM)", + "trade_name_continuation_candidate": true, + "debt_exclusion_clause_candidate": true + }, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "event|영업양도|오민한|김선웅|고객고용관계|2024-07-05", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-021.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-021.json new file mode 100644 index 00000000..46920ac8 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-021.json @@ -0,0 +1,102 @@ +{ + "evidence_index_proposed": "E-021", + "title": "이행각서", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 21 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "claim_doc", + "status_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-021-01", + "event_or_state": "event", + "event_kind": "채권관리", + "event_subkind": "debt_acknowledgement", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "김선웅이 강용원에게 2021.6.5.자 물품대금 채무를 인정하고 2024.8.말까지 변제하겠다는 내용의 이행각서 작성", + "source_evidence_index_proposed": "E-021", + "participants": { + "actor_candidates": [ + "김선웅" + ], + "counterparty_candidates": [ + "강용원" + ], + "beneficiary_candidates": [ + "강용원" + ], + "third_party_candidates": [] + }, + "object_spec": "2021.6.5.자 물품대금 채무", + "event_date": "2024-08-05", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "이행각서 내용", + "excerpt": "본인은 2021. 6. 5. 자 인테리어 자재 물품대금 채무를 2024. 8. 말까지 차질 없이 갚겠습니다.", + "directness": "직접" + } + ], + "legal_effect_candidate": { + "effect_type": "debt_acknowledgement", + "acknowledgement_of_debt": true, + "limitation_interruption_candidate": true, + "related_claim_type": "물품대금" + }, + "identity_signature": "event|채권관리|김선웅|강용원|2021.6.5.자 물품대금 채무|2024-08-05", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-021-02", + "event_or_state": "event", + "event_kind": "채권관리", + "event_subkind": "new_due_date", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "물품대금 채무의 변제기를 2024.8.31.로 설정", + "source_evidence_index_proposed": "E-021", + "participants": { + "actor_candidates": [ + "김선웅" + ], + "counterparty_candidates": [ + "강용원" + ], + "beneficiary_candidates": [ + "강용원" + ], + "third_party_candidates": [] + }, + "object_spec": "물품대금 채무 변제기", + "event_date": "2024-08-31", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "이행각서 변제기", + "excerpt": "2024. 8. 말까지 차질 없이 갚겠습니다.", + "directness": "직접" + } + ], + "legal_effect_candidate": { + "effect_type": "new_due_date", + "new_due_date_candidate": "2024-08-31", + "related_claim_type": "물품대금" + }, + "identity_signature": "event|채권관리|김선웅|강용원|물품대금 채무 변제기|2024-08-31", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-022.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-022.json new file mode 100644 index 00000000..cbc9f676 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-022.json @@ -0,0 +1,201 @@ +{ + "evidence_index_proposed": "E-022", + "title": "임대차계약서", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 22 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "mixed_contract_doc", + "claim_doc", + "status_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-022-01", + "event_or_state": "event", + "event_kind": "임대차", + "event_subkind": null, + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "이수인과 양정숙 간 흑석동 상가 임대차계약 체결", + "source_evidence_index_proposed": "E-022", + "source_fact_candidate": null, + "participants": { + "actor_candidates": ["이수인"], + "counterparty_candidates": ["양정숙"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "흑석동 상가 201호", + "fraction_ref": null, + "amount": null, + "event_date": "2024-02-01", + "event_end_date": null, + "time_text": "2024. 2. 1.", + "time_precision": "exact", + "location": "서울 동작구 흑석로 80", + "legal_keywords": ["임대차계약", "임대인", "임차인"], + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "계약서 전문", + "excerpt": "이수인(임대인)과 양정숙(임차인)은 아래와 같이 임대차계약을 체결함", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": null, + "related_claim_type": null, + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "event|임대차|이수인|양정숙|흑석동 상가 201호|2024-02-01", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-022-02", + "event_or_state": "event", + "event_kind": "기타", + "event_subkind": "대가수수", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "양정숙의 임대차보증금 3억 원 지급", + "source_evidence_index_proposed": "E-022", + "source_fact_candidate": null, + "participants": { + "actor_candidates": ["양정숙"], + "counterparty_candidates": ["이수인"], + "beneficiary_candidates": ["이수인"], + "third_party_candidates": [] + }, + "object_spec": "임대차보증금", + "fraction_ref": null, + "amount": "300,000,000원", + "event_date": "2024-02-01", + "event_end_date": null, + "time_text": "2024. 2. 1.", + "time_precision": "exact", + "location": null, + "legal_keywords": ["보증금", "지급완료"], + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "제3조", + "excerpt": "임대차보증금은 임차인이 계약 당일 목적물을 인도받음과 동시에 지급완료하였음", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": null, + "related_claim_type": null, + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "event|기타|양정숙|이수인|임대차보증금 300,000,000원|2024-02-01", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-022-03", + "event_or_state": "state", + "event_kind": "점유", + "event_subkind": null, + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "양정숙의 흑석동 상가 점유 및 운영 상태", + "source_evidence_index_proposed": "E-022", + "source_fact_candidate": null, + "participants": { + "actor_candidates": ["양정숙"], + "counterparty_candidates": [], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "흑석동 상가 201호", + "fraction_ref": null, + "amount": null, + "event_date": "2024-02-01", + "event_end_date": null, + "time_text": "2024. 2. 1.", + "time_precision": "exact", + "location": "서울 동작구 흑석로 80", + "legal_keywords": ["점유", "운영"], + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "제3조", + "excerpt": "목적물을 인도받음", + "directness": "직접" + } + ], + "current_state_candidate": { + "state_summary": "양정숙의 흑석동 상가 점유 및 운영 상태", + "state_as_of": "현재", + "state_start_date": "2024-02-01", + "state_end_date_candidate": null, + "superseded_by_candidate_id": null, + "is_ongoing": true, + "current_possessor_candidates": ["양정숙"], + "current_user_candidates": ["양정숙"], + "current_benefit_holder_candidates": ["양정숙"], + "current_right_holder_candidates": [], + "possession_legal_cause_asserted": "임대차", + "possession_legal_cause_asserted_by": ["양정숙"] + }, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": null, + "related_claim_type": null, + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": { + "candidate_role_tags": ["현재점유후보", "현재사용수익후보"] + }, + "identity_signature": "state|점유|양정숙|이수인|흑석동 상가 201호|현재", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-023.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-023.json new file mode 100644 index 00000000..3f8c97e4 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-023.json @@ -0,0 +1,83 @@ +{ + "evidence_index_proposed": "E-023", + "title": "임료시세확인서", + "doc_type": "기타", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 23 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "valuation_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-023-01", + "event_or_state": "state", + "event_kind": "가치평가", + "event_subkind": null, + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "경기도 평택시 서정빌라의 2022. 10. 기준 월 차임 시세를 보증금 없는 월세 200만 원으로 평가함.", + "source_evidence_index_proposed": "E-023", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [ + "김민식" + ], + "counterparty_candidates": [], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "경기도 평택시 서정3길 123(서정동, 서정빌라)", + "fraction_ref": null, + "amount": "월세 200만 원", + "event_date": "2022-10-01", + "event_end_date": null, + "time_text": "2022. 10.경", + "time_precision": "approximate", + "location": null, + "legal_keywords": [], + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "월 차임 시세", + "excerpt": "보증금 없이 월세 200만 원", + "directness": "직접" + } + ], + "current_state_candidate": { + "state_summary": "2022. 10. 기준 임료 시세 평가", + "state_as_of": "2022-10-01", + "state_start_date": "2022-10-01", + "state_end_date_candidate": null, + "superseded_by_candidate_id": null, + "is_ongoing": false, + "current_possessor_candidates": [], + "current_user_candidates": [], + "current_benefit_holder_candidates": [], + "current_right_holder_candidates": [], + "possession_legal_cause_asserted": null, + "possession_legal_cause_asserted_by": [] + }, + "succession_candidate": null, + "legal_effect_candidate": null, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": { + "candidate_role_tags": [ + "현재사용수익후보" + ] + }, + "identity_signature": "state|가치평가|김민식||월세 200만 원|2022-10", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-024.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-024.json new file mode 100644 index 00000000..2c66e3e2 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-024.json @@ -0,0 +1,142 @@ +{ + "evidence_index_proposed": "E-024", + "title": "차용증", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 24 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "claim_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-024-01", + "event_or_state": "event", + "event_kind": "채권발생", + "event_subkind": "underlying_claim_arising", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "강용원이 오국한에게 1억 원을 대여하는 계약 체결", + "source_evidence_index_proposed": "E-024", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [ + "강용원" + ], + "counterparty_candidates": [ + "오국한" + ], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "1억 원 대여금", + "fraction_ref": null, + "amount": "100,000,000원", + "event_date": "2022-04-18", + "event_end_date": null, + "time_text": "2022. 4. 18.", + "time_precision": "exact", + "location": null, + "legal_keywords": [ + "차용" + ], + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "차용증", + "excerpt": "오국한은 아래와 같이 금전을 차용합니다. 원 금 : 100,000,000원 변제기 : 2022. 12. 17. 이 자 : 월 1% 2022. 4. 18.", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "underlying_claim_arising", + "related_claim_type": "대여금", + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "event:채권발생:강용원:오국한:100000000원:2022-04-18", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-024-02", + "event_or_state": "event", + "event_kind": "채권발생", + "event_subkind": "underlying_claim_arising", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "강용원이 오국한에게 1억 원을 추가로 대여하는 계약 체결", + "source_evidence_index_proposed": "E-024", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [ + "강용원" + ], + "counterparty_candidates": [ + "오국한" + ], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "1억 원 대여금", + "fraction_ref": null, + "amount": "100,000,000원", + "event_date": "2022-05-18", + "event_end_date": null, + "time_text": "2022. 5. 18.", + "time_precision": "exact", + "location": null, + "legal_keywords": [ + "차용" + ], + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "차용증", + "excerpt": "오국한은 아래와 같이 금전을 차용합니다. 원 금 : 100,000,000원 변제기 : 2023. 3. 17. 이 자 : 월 1% 2022. 5. 18.", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "underlying_claim_arising", + "related_claim_type": "대여금", + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "event:채권발생:강용원:오국한:100000000원:2022-05-18", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-025.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-025.json new file mode 100644 index 00000000..c6f71767 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-025.json @@ -0,0 +1,90 @@ +{ + "evidence_index_proposed": "E-025", + "title": "차용증", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 25 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "claim_doc", + "secured_debt_components" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-025-01", + "event_or_state": "event", + "event_kind": "채권발생", + "event_subkind": "underlying_claim_arising", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "강용원과 오민한 사이의 2억 원 차용 계약 체결 및 차용증 작성", + "source_evidence_index_proposed": "E-025", + "participants": { + "actor_candidates": ["강용원"], + "counterparty_candidates": ["오민한"], + "beneficiary_candidates": ["오민한"], + "third_party_candidates": [] + }, + "object_spec": "영업자금 2억 원 차용", + "amount": "200,000,000원", + "event_date": "2023-01-06", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "원금 2억 원, 변제기 2024. 1. 5.", + "excerpt": "2023년 1월 6일", + "directness": "직접" + } + ], + "legal_effect_candidate": { + "effect_type": "채권발생", + "related_claim_type": "대여금", + "review_flags": [] + }, + "identity_signature": "event|채권발생|강용원|오민한|200,000,000원|2023-01-06", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-025-02", + "event_or_state": "event", + "event_kind": "담보설정", + "event_subkind": "secured_debt_creation", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "차용금 채무 담보를 위한 성수동 대지 지분 근저당권 설정 약정", + "source_evidence_index_proposed": "E-025", + "participants": { + "actor_candidates": ["강용원"], + "counterparty_candidates": ["오민한"], + "beneficiary_candidates": ["오민한"], + "third_party_candidates": [] + }, + "object_spec": "서울 성동구 성수동 256 대 330㎡의 5분의 2 지분", + "event_date": "2023-01-06", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "특약", + "excerpt": "서울 성동구 성수동 256 대 330㎡의 5분의 2 지분에 1순위 근저당권을 설정하기로 함", + "directness": "직접" + } + ], + "legal_effect_candidate": { + "effect_type": "담보설정", + "related_claim_type": "근저당권", + "review_flags": [] + }, + "identity_signature": "event|담보설정|강용원|오민한|성수동 대지 지분|2023-01-06", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-026.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-026.json new file mode 100644 index 00000000..eeaa61bc --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-026.json @@ -0,0 +1,57 @@ +{ + "evidence_index_proposed": "E-026", + "title": "차 용 증", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 26 + }, + "property_cluster_id": "해당 주거용 부동산", + "doc_semantic_flags": ["claim_doc"], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-026-01", + "event_or_state": "event", + "event_kind": "채권발생", + "event_subkind": "secured_debt_creation", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "강용원이 오민한으로부터 영업자금 목적으로 3억 원을 차용함", + "source_evidence_index_proposed": "E-026", + "participants": { + "actor_candidates": ["강용원"], + "counterparty_candidates": ["오민한"] + }, + "object_spec": "영업자금 3억 원", + "amount": "300,000,000원", + "event_date": "2023-07-06", + "time_precision": "exact", + "identity_signature": "event채권발생강용원오민한300,000,000원2023-07-06", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-026-02", + "event_or_state": "event", + "event_kind": "담보설정", + "event_subkind": "secured_debt_creation", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "차용금 반환 담보를 위해 성수동 256번지 대지 5/3 지분에 1순위 근저당권 설정 약정", + "source_evidence_index_proposed": "E-026", + "participants": { + "actor_candidates": ["강용원"], + "counterparty_candidates": ["오민한"] + }, + "object_spec": "서울 성동구 성수동 256 대 330㎡의 5분의 3 지분", + "event_date": "2023-07-06", + "time_precision": "exact", + "identity_signature": "event담보설정강용원오민한성수동256대지5/3지분2023-07-06", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-027.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-027.json new file mode 100644 index 00000000..70e9f1e4 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-027.json @@ -0,0 +1,84 @@ +{ + "evidence_index_proposed": "E-027", + "title": "최고 통지서", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 27 + }, + "property_cluster_id": null, + "doc_semantic_flags": ["claim_doc", "notice_doc"], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-027-01", + "event_or_state": "event", + "event_kind": "통지", + "event_subkind": "demand_notice_dispatch", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "강용원이 김선웅에게 물품대금 지급을 최고하는 통지서를 발송함", + "source_evidence_index_proposed": "E-027", + "participants": { + "actor_candidates": ["강용원"], + "counterparty_candidates": ["김선웅"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "인테리어 자재대금 지급 최고", + "event_date": "2024-05-04", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "heading", + "locator_hint": "최고 통지서", + "excerpt": "2024년 5월 4일", + "directness": "직접" + } + ], + "legal_effect_candidate": { + "effect_type": "최고", + "related_claim_type": "물품대금채권" + }, + "identity_signature": "event|통지|강용원|김선웅|자재대금최고|2024-05-04", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-027-02", + "event_or_state": "event", + "event_kind": "도달", + "event_subkind": "demand_notice_arrival", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "강용원이 발송한 최고 통지서가 김선웅에게 도달함", + "source_evidence_index_proposed": "E-027", + "participants": { + "actor_candidates": ["강용원"], + "counterparty_candidates": ["김선웅"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "인테리어 자재대금 지급 최고", + "event_date": "2024-05-06", + "time_precision": "exact", + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "우편물 배달 증명서", + "excerpt": "배달연월일 2024년5월6일", + "directness": "직접" + } + ], + "legal_effect_candidate": { + "effect_type": "도달", + "related_claim_type": "물품대금채권" + }, + "identity_signature": "event|도달|강용원|김선웅|자재대금최고|2024-05-06", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-028.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-028.json new file mode 100644 index 00000000..5e9e24e3 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-028.json @@ -0,0 +1,69 @@ +{ + "evidence_index_proposed": "E-028", + "title": "통지서", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 28 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "notice_doc", + "response_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-028-01", + "event_or_state": "event", + "event_kind": "통지", + "event_subkind": "demand_notice_arrival", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "오민한이 강용원에게 김선웅의 채무에 대한 책임을 부인하는 내용의 통지서를 발송함.", + "source_evidence_index_proposed": "E-028", + "participants": { + "actor_candidates": [ + "오민한" + ], + "counterparty_candidates": [ + "강용원" + ], + "beneficiary_candidates": [], + "third_party_candidates": [ + "김선웅" + ] + }, + "object_spec": "김선웅의 물품대금 채무 관련 책임 부인 통지", + "event_date": "2024-12-27", + "time_precision": "exact", + "legal_keywords": [ + "채무불이행", + "소멸시효", + "영업양도" + ], + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "본문 전체", + "excerpt": "김선웅씨의 채무를 변제하라는 요구를 받은 오민한입니다.", + "directness": "직접" + } + ], + "legal_effect_candidate": { + "effect_type": "defense_assertion", + "related_claim_type": "인테리어 자재대금 채권", + "review_flags": [ + "Stage 2 검토 필요" + ] + }, + "identity_signature": "event|통지|오민한|강용원|김선웅 물품대금 채무 책임 부인|2024-12-27", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-029.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-029.json new file mode 100644 index 00000000..56263ab8 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-029.json @@ -0,0 +1,85 @@ +{ + "evidence_index_proposed": "E-029", + "title": "통지서_이수인_윤건우", + "doc_type": "통신기록", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 29 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "notice_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-029-01", + "event_or_state": "event", + "event_kind": "통지", + "event_subkind": null, + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "흑석동 상가 매매계약에 관하여 이수인이 윤건우에게 매매대금 감액 및 제3자 명의 소유권이전등기를 요청하는 통지", + "source_evidence_index_proposed": "E-029", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [ + "이수인" + ], + "counterparty_candidates": [ + "윤건우" + ], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "흑석동 상가 매매계약 조건 변경 요청", + "fraction_ref": null, + "amount": null, + "event_date": "2024-03-30", + "event_end_date": null, + "time_text": "2024. 3. 30.", + "time_precision": "exact", + "location": null, + "legal_keywords": [ + "매매대금 감액", + "제3자 명의 소유권이전등기" + ], + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "본문 2항, 3항", + "excerpt": "매매대금을 10% 정도라도 감액하여 주시고, 다른 사람 이름으로 소유권이전등기를 해 주실 것을 당부드립니다.", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": null, + "related_claim_type": null, + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [ + "5-다" + ], + "actio_relevance_candidates": null, + "identity_signature": "event|통지|이수인|윤건우|흑석동상가매매계약조건변경요청|2024-03-30", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-030.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-030.json new file mode 100644 index 00000000..e75b3e41 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidate_parts/E-030.json @@ -0,0 +1,130 @@ +{ + "evidence_index_proposed": "E-030", + "title": "해제 통지서", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 30 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "notice_doc" + ], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-030-01", + "event_or_state": "event", + "event_kind": "통지", + "event_subkind": "해제/해지 통지", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "윤건우가 이수인에게 흑석동 상가 매매계약 해제 통지를 발송함", + "source_evidence_index_proposed": "E-030", + "source_fact_candidate": null, + "participants": { + "actor_candidates": ["윤건우"], + "counterparty_candidates": ["이수인"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "흑석동 상가 매매계약 해제", + "fraction_ref": null, + "amount": null, + "event_date": "2024-05-15", + "event_end_date": null, + "time_text": "2024-05-15", + "time_precision": "exact", + "location": null, + "legal_keywords": ["매매계약해제", "내용증명"], + "support_locators": [ + { + "locator_type": "paragraph", + "locator_hint": "해제 통지", + "excerpt": "2023. 10. 1. 자 매매계약을 지금 이 통지로써 해제하겠습니다.", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "contract_cancellation", + "related_claim_type": "매매대금반환", + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": ["dispatch_date"], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "event|통지|윤건우|이수인|흑석동상가매매계약해제|2024-05-15", + "confidence": "high" + }, + { + "candidate_id_proposed": "EVT-030-02", + "event_or_state": "event", + "event_kind": "도달", + "event_subkind": null, + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "이수인이 흑석동 상가 매매계약 해제 통지서를 수령함", + "source_evidence_index_proposed": "E-030", + "source_fact_candidate": null, + "participants": { + "actor_candidates": ["이수인"], + "counterparty_candidates": ["윤건우"], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": "흑석동 상가 매매계약 해제 통지서", + "fraction_ref": null, + "amount": null, + "event_date": "2024-05-16", + "event_end_date": null, + "time_text": "2024-05-16", + "time_precision": "exact", + "location": null, + "legal_keywords": ["도달"], + "support_locators": [ + { + "locator_type": "table_row", + "locator_hint": "배달연월일", + "excerpt": "배달연월일 2024년 5월 16일", + "directness": "직접" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "cancellation_arrival", + "related_claim_type": "매매대금반환", + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": ["arrival_date"], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "event|도달|이수인|윤건우|흑석동상가매매계약해제통지서|2024-05-16", + "confidence": "high" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidates.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidates.json new file mode 100644 index 00000000..b2295488 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_event_candidates.json @@ -0,0 +1,1231 @@ +{ + "schema_version": "evidence_event_candidates.v1", + "items": [ + { + "evidence_index": "E-001", + "title": "가정법원심판_강호연", + "doc_type": "판결/결정", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 1 + }, + "event_candidates": [ + { + "candidate_id": "EVT-001-01", + "event_or_state": "event", + "event_kind": "상속포기", + "event_subkind": "succession_renunciation_filing", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "강형수와 강지수가 망 강호연의 재산상속을 포기하는 신고를 함.", + "source_evidence_index": "E-001", + "participants": { + "actor_candidates": ["강형수", "강지수"], + "third_party_candidates": ["강호연"] + }, + "object_spec": "재산상속 포기 신고", + "event_date": "2024-09-11", + "identity_signature": "event_상속포기_강형수_강지수_망강호연_2024-09-11", + "confidence": "high" + }, + { + "candidate_id": "EVT-001-02", + "event_or_state": "event", + "event_kind": "판결/결정", + "event_subkind": "succession_renunciation_acceptance", + "action_type_candidate": "소송행위(litigation acts)", + "action_summary": "서울가정법원이 강형수와 강지수의 상속포기 신고를 수리하는 심판을 내림.", + "source_evidence_index": "E-001", + "participants": { + "actor_candidates": ["서울가정법원"], + "counterparty_candidates": ["강형수", "강지수"], + "third_party_candidates": ["강호연"] + }, + "object_spec": "상속포기 신고 수리", + "event_date": "2024-09-20", + "identity_signature": "event_판결/결정_서울가정법원_망강호연_상속포기수리_2024-09-20", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-002", + "title": "가족관계증명서 [폐쇄]", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 2 + }, + "event_candidates": [ + { + "candidate_id": "EVT-002-01", + "event_or_state": "event", + "event_kind": "상속개시", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "망 강호연의 사망으로 인한 상속개시", + "source_evidence_index": "E-002", + "participants": { + "actor_candidates": ["강호연"], + "third_party_candidates": ["강용원", "양정숙", "강형수", "강지수"] + }, + "event_date": "2024-08-10", + "identity_signature": "event|상속개시|강호연|2024-08-10", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-003", + "title": "각서 및 금전소비대차계약서", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 3 + }, + "event_candidates": [ + { + "candidate_id": "EVT-003-01", + "event_or_state": "event", + "event_kind": "기타", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "오국한이 오민한의 자금으로 신림동 상가를 매수하여 명의신탁했음을 확인하는 각서 작성", + "source_evidence_index": "E-003", + "participants": { + "actor_candidates": ["오국한"], + "beneficiary_candidates": ["오민한"], + "third_party_candidates": ["이경주"] + }, + "event_date": "2015-09-17", + "identity_signature": "event-기타-오국한-오민한-신림동상가명의신탁-2015-09-17", + "confidence": "high" + }, + { + "candidate_id": "EVT-003-02", + "event_or_state": "event", + "event_kind": "채권발생", + "event_subkind": "underlying_claim_arising", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "오국한이 오민한으로부터 1,000만 원을 차용하는 금전소비대차계약 체결", + "source_evidence_index": "E-003", + "participants": { + "actor_candidates": ["오국한"], + "counterparty_candidates": ["오민한"] + }, + "amount": "10,000,000원", + "event_date": "2022-07-18", + "identity_signature": "event-채권발생-오국한-오민한-대여금채무-2022-07-18", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-004", + "title": "감정평가서", + "doc_type": "기타", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 4 + }, + "event_candidates": [ + { + "candidate_id": "EVT-004-01", + "event_or_state": "state", + "event_kind": "가치평가", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "신림로 상가의 2022년~현재 시가 평가", + "source_evidence_index": "E-004", + "identity_signature": "state|가치평가|신림로 상가|2024-09-13", + "confidence": "high" + }, + { + "candidate_id": "EVT-004-02", + "event_or_state": "state", + "event_kind": "가치평가", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "포천시 임야의 2022년~현재 시가 평가", + "source_evidence_index": "E-004", + "identity_signature": "state|가치평가|포천시 임야|2024-09-13", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-005", + "title": "감정평가 의견서", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 5 + }, + "event_candidates": [ + { + "candidate_id": "EVT-005-01", + "event_or_state": "event", + "event_kind": "가치평가", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "대상 토지(성수동 256 대)에 대한 2024년도 차임 시세 감정 결과 회신", + "source_evidence_index": "E-005", + "participants": { + "actor_candidates": ["감정평가법인 광나루"], + "counterparty_candidates": ["강용원"] + }, + "event_date": "2024-12-29", + "identity_signature": "event|가치평가|감정평가법인광나루|강용원|성수동대지차임시세|2024-12-29", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-006", + "title": "계약서", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 6 + }, + "event_candidates": [ + { + "candidate_id": "EVT-006-01", + "event_or_state": "event", + "event_kind": "임대차", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "성수동 대지 임대차 계약 체결", + "source_evidence_index": "E-006", + "participants": { + "actor_candidates": ["이문호"], + "counterparty_candidates": ["박성희"] + }, + "event_date": "2024-11-20", + "identity_signature": "event임대차이문호박성희성수동대지2024-11-20", + "confidence": "high" + }, + { + "candidate_id": "EVT-006-02", + "event_or_state": "event", + "event_kind": "기타", + "event_subkind": "대가수수", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "임대차보증금 지급", + "source_evidence_index": "E-006", + "amount": "300,000,000원", + "event_date": "2024-11-20", + "identity_signature": "event기타박성희이문호임대차보증금3억2024-11-20", + "confidence": "high" + }, + { + "candidate_id": "EVT-006-03", + "event_or_state": "event", + "event_kind": "인도", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "성수동 대지 인도", + "source_evidence_index": "E-006", + "participants": { + "actor_candidates": ["이문호"], + "counterparty_candidates": ["박성희"] + }, + "event_date": "2024-11-20", + "identity_signature": "event인도이문호박성희성수동대지2024-11-20", + "confidence": "high" + }, + { + "candidate_id": "EVT-006-04", + "event_or_state": "event", + "event_kind": "처분", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "성수동 건물 매매 계약 체결", + "source_evidence_index": "E-006", + "participants": { + "actor_candidates": ["이문호"], + "counterparty_candidates": ["박성희"] + }, + "event_date": "2024-11-20", + "identity_signature": "event처분이문호박성희성수동건물2024-11-20", + "confidence": "high" + }, + { + "candidate_id": "EVT-006-05", + "event_or_state": "event", + "event_kind": "기타", + "event_subkind": "대가수수", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "매매대금 지급", + "source_evidence_index": "E-006", + "amount": "200,000,000원", + "event_date": "2024-11-20", + "identity_signature": "event기타박성희이문호매매대금2억2024-11-20", + "confidence": "high" + }, + { + "candidate_id": "EVT-006-06", + "event_or_state": "event", + "event_kind": "인도", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "성수동 건물 인도", + "source_evidence_index": "E-006", + "participants": { + "actor_candidates": ["이문호"], + "counterparty_candidates": ["박성희"] + }, + "event_date": "2024-11-20", + "identity_signature": "event인도이문호박성희성수동건물2024-11-20", + "confidence": "high" + }, + { + "candidate_id": "EVT-006-07", + "event_or_state": "state", + "event_kind": "점유", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "성수동 대지 및 건물 관리·사용권 귀속", + "source_evidence_index": "E-006", + "event_date": "2024-11-20", + "identity_signature": "state점유박성희성수동대지및건물2024-11-20", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-007", + "title": "공사도급계약서", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 7 + }, + "event_candidates": [ + { + "candidate_id": "EVT-007-01", + "event_or_state": "event", + "event_kind": "기타", + "event_subkind": "contract_formation", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "경기 평택시 서정3길 123 건물 신축 공사도급계약 체결", + "source_evidence_index": "E-007", + "participants": { + "actor_candidates": ["김일동"], + "counterparty_candidates": ["박광윤"] + }, + "amount": "1,000,000,000원", + "event_date": "2021-05-01", + "identity_signature": "event+기타+김일동+박광윤+평택시건물신축공사+2021-05-01", + "confidence": "high" + }, + { + "candidate_id": "EVT-007-02", + "event_or_state": "event", + "event_kind": "변제", + "event_subkind": "payment_consideration", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "공사대금 중 중도금 4억 원 지급 및 수령", + "source_evidence_index": "E-007", + "participants": { + "actor_candidates": ["김일동"], + "counterparty_candidates": ["박광윤"] + }, + "amount": "400,000,000원", + "event_date": "2021-08-30", + "identity_signature": "event+변제+김일동+박광윤+평택시건물신축공사중도금4억+2021-08-30", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-008", + "title": "내용증명", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 8 + }, + "event_candidates": [ + { + "candidate_id": "EVT-008-01", + "event_or_state": "event", + "event_kind": "통지", + "event_subkind": "demand_notice_dispatch", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "강용원이 오민한에게 사해행위취소 및 소유권 반환을 요구하는 내용증명 발송", + "source_evidence_index": "E-008", + "participants": { + "actor_candidates": ["강용원"], + "counterparty_candidates": ["오민한"] + }, + "event_date": "2024-09-15", + "identity_signature": "event통지강용원오민한신림동상가반환요구2024-09-15", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-009", + "title": "내용증명", + "doc_type": "기타", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 9 + }, + "event_candidates": [ + { + "candidate_id": "EVT-009-01", + "event_or_state": "event", + "event_kind": "통지", + "event_subkind": "해제/해지의사표시", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "윤건우가 양정숙에게 임대차계약 해지 및 상가 인도 통지", + "source_evidence_index": "E-009", + "participants": { + "actor_candidates": ["윤건우"], + "counterparty_candidates": ["양정숙"] + }, + "event_date": "2024-11-29", + "identity_signature": "event통지윤건우양정숙흑석동상가2024-11-29", + "confidence": "high" + }, + { + "candidate_id": "EVT-009-02", + "event_or_state": "event", + "event_kind": "도달", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "내용증명 도달", + "source_evidence_index": "E-009", + "event_date": "2024-11-30", + "identity_signature": "event도달윤건우양정숙내용증명2024-11-30", + "confidence": "high" + }, + { + "candidate_id": "EVT-009-03", + "event_or_state": "state", + "event_kind": "항변", + "event_subkind": "책임부인", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "소유자의 임대차 책임 부인 및 차임 연체 주장", + "source_evidence_index": "E-009", + "identity_signature": "state항변윤건우양정숙임대차책임", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-010", + "title": "내용증명에 대한 답신", + "doc_type": "통신기록", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 10 + }, + "event_candidates": [ + { + "candidate_id": "EVT-010-01", + "event_or_state": "event", + "event_kind": "통지", + "event_subkind": "demand_notice_arrival", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "오민한이 강용원의 이행최고서를 2024. 9. 20. 수령함", + "source_evidence_index": "E-010", + "participants": { + "actor_candidates": ["강용원"], + "counterparty_candidates": ["오민한"] + }, + "event_date": "2024-09-20", + "identity_signature": "event|통지|강용원|오민한|이행최고서|2024-09-20", + "confidence": "high" + }, + { + "candidate_id": "EVT-010-02", + "event_or_state": "event", + "event_kind": "통지", + "event_subkind": "demand_notice_dispatch", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "오민한이 강용원에게 답신 내용증명을 2024. 9. 30. 발송함", + "source_evidence_index": "E-010", + "participants": { + "actor_candidates": ["오민한"], + "counterparty_candidates": ["강용원"] + }, + "event_date": "2024-09-30", + "identity_signature": "event|통지|오민한|강용원|내용증명|2024-09-30", + "confidence": "high" + }, + { + "candidate_id": "EVT-010-03", + "event_or_state": "event", + "event_kind": "항변", + "event_subkind": "defense_assertion_setoff", + "action_type_candidate": "소송행위(litigation acts)", + "action_summary": "오민한이 자신의 5,000만 원 채권으로 강용원의 채권을 상계하겠다고 주장함", + "source_evidence_index": "E-010", + "amount": "50,000,000원", + "identity_signature": "event|항변|오민한|강용원|50000000원|null", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-011", + "title": "답변서", + "doc_type": "기타", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 11 + }, + "event_candidates": [ + { + "candidate_id": "EVT-011-01", + "event_or_state": "event", + "event_kind": "도달", + "event_subkind": "demand_notice_arrival", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "2024. 8. 29. 자 유치권소멸청구 통지서 수령", + "source_evidence_index": "E-011", + "event_date": "2024-09-01", + "identity_signature": "event|도달|박광윤|강용원양정숙|유치권소멸청구통지서|2024-09-01", + "confidence": "high" + }, + { + "candidate_id": "EVT-011-02", + "event_or_state": "state", + "event_kind": "점유", + "event_subkind": "ongoing_physical_possession", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "서정빌라 101호에 대한 유치권 행사 및 거주", + "source_evidence_index": "E-011", + "identity_signature": "state|점유|박광윤|강용원양정숙|서정빌라101호|2024-10-31", + "confidence": "high" + }, + { + "candidate_id": "EVT-011-03", + "event_or_state": "state", + "event_kind": "항변", + "event_subkind": "defense_assertion_benefit_applied_to_secured_claim", + "action_type_candidate": "소송행위(litigation acts)", + "action_summary": "과거 임대료 및 거주 수익을 채무에 충당 주장", + "source_evidence_index": "E-011", + "identity_signature": "state|항변|박광윤|강용원양정숙|공사대금채권|2024-10-31", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-012", + "title": "등기말소 신청에 대한 답신", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 12 + }, + "event_candidates": [ + { + "candidate_id": "EVT-012-01", + "event_or_state": "event", + "event_kind": "통지", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "주식회사 대한은행이 강용원의 성수동 대지 근저당권 말소 신청을 거부하는 통지를 발송함", + "source_evidence_index": "E-012", + "event_date": "2024-12-22", + "identity_signature": "event+통지+주식회사 대한은행+강용원+성수동 대지 근저당권 말소 신청 거부 통지+2024-12-22", + "confidence": "high" + }, + { + "candidate_id": "EVT-012-02", + "event_or_state": "event", + "event_kind": "항변", + "event_subkind": "defense_assertion_subsequent_owner_no_extinction_right", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "주식회사 대한은행이 경매 절차의 적법성과 경매의 공신력(민사집행법 제267조)을 주장하며 근저당권 말소 불가 통지", + "source_evidence_index": "E-012", + "event_date": "2024-12-22", + "identity_signature": "event+항변+주식회사 대한은행+강용원+경매 공신력 주장+2024-12-22", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-013", + "title": "등기사항전부증명서(말소사항 포함)-토지", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 13 + }, + "event_candidates": [ + { + "candidate_id": "EVT-013-01", + "event_or_state": "event", + "event_kind": "소유권이전", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "강석우 사망으로 인한 정유심, 강용원 지분 상속", + "source_evidence_index": "E-013", + "event_date": "2022-11-08", + "identity_signature": "event|소유권이전|정유심,강용원|강석우|성수동대지지분|2022-11-08", + "confidence": "high" + }, + { + "candidate_id": "EVT-013-02", + "event_or_state": "event", + "event_kind": "증여", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "정유심의 성수동 대지 3/5 지분 강용원에게 증여", + "source_evidence_index": "E-013", + "event_date": "2023-04-06", + "identity_signature": "event|증여|정유심|강용원|성수동대지3/5지분|2023-04-06", + "confidence": "high" + }, + { + "candidate_id": "EVT-013-03", + "event_or_state": "event", + "event_kind": "경매개시", + "event_subkind": "auction_application_after_extinguishment", + "action_type_candidate": "소송행위(litigation acts)", + "action_summary": "오민환의 담보권실행을 위한 경매개시결정", + "source_evidence_index": "E-013", + "event_date": "2024-07-21", + "identity_signature": "event|경매개시|오민환|법원|성수동대지|2024-07-21", + "confidence": "high" + }, + { + "candidate_id": "EVT-013-04", + "event_or_state": "event", + "event_kind": "경매매각", + "event_subkind": "auction_sale_and_registry_transfer", + "action_type_candidate": "소송행위(litigation acts)", + "action_summary": "경매로 인한 이문호의 소유권 취득", + "source_evidence_index": "E-013", + "event_date": "2024-10-05", + "identity_signature": "event|경매매각|이문호|법원|성수동대지|2024-10-05", + "confidence": "high" + }, + { + "candidate_id": "EVT-013-05", + "event_or_state": "event", + "event_kind": "담보말소", + "event_subkind": "mortgage_extinguishment_by_fraction", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "경매로 인한 오민환의 기존 근저당권 말소", + "source_evidence_index": "E-013", + "event_date": "2024-10-05", + "identity_signature": "event|담보말소|미상|오민환|기존근저당권|2024-10-05", + "confidence": "high" + }, + { + "candidate_id": "EVT-013-06", + "event_or_state": "event", + "event_kind": "담보설정", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "주식회사 대한은행의 근저당권 설정", + "source_evidence_index": "E-013", + "amount": "520,000,000원", + "event_date": "2024-10-05", + "identity_signature": "event|담보설정|주식회사대한은행|이문호|성수동대지/520000000원|2024-10-05", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-014", + "title": "등기사항전부증명서 (말소사항 포함) -건물", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 14 + }, + "event_candidates": [ + { + "candidate_id": "EVT-014-01", + "event_or_state": "state", + "event_kind": "소유권이전", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "이경주 소유권보존", + "source_evidence_index": "E-014", + "event_date": "2006-05-08", + "identity_signature": "state|소유권이전|이경주|null|서울특별시 관악구 신림로 115 건물|2006-05-08", + "confidence": "high" + }, + { + "candidate_id": "EVT-014-02", + "event_or_state": "event", + "event_kind": "소유권이전", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "오국환 소유권이전(매매)", + "source_evidence_index": "E-014", + "amount": "40,000,000원", + "event_date": "2015-09-17", + "identity_signature": "event|소유권이전|이경주|오국환|서울특별시 관악구 신림로 115 건물|2015-09-17", + "confidence": "high" + }, + { + "candidate_id": "EVT-014-03", + "event_or_state": "event", + "event_kind": "소유권이전", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "오민한 소유권이전(매매)", + "source_evidence_index": "E-014", + "amount": "150,000,000원", + "event_date": "2023-03-17", + "identity_signature": "event|소유권이전|오국환|오민한|서울특별시 관악구 신림로 115 건물|2023-03-17", + "confidence": "high" + }, + { + "candidate_id": "EVT-014-04", + "event_or_state": "event", + "event_kind": "담보설정", + "event_subkind": "secured_debt_creation", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "나임오 근저당권 설정", + "source_evidence_index": "E-014", + "amount": "50,000,000원", + "event_date": "2021-08-01", + "identity_signature": "event|담보설정|오국환|나임오|서울특별시 관악구 신림로 115 건물|2021-08-01", + "confidence": "high" + }, + { + "candidate_id": "EVT-014-05", + "event_or_state": "event", + "event_kind": "담보설정", + "event_subkind": "secured_debt_creation", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "강용일 근저당권 설정", + "source_evidence_index": "E-014", + "amount": "150,000,000원", + "event_date": "2022-04-18", + "identity_signature": "event|담보설정|오국환|강용일|서울특별시 관악구 신림로 115 건물|2022-04-18", + "confidence": "high" + }, + { + "candidate_id": "EVT-014-06", + "event_or_state": "event", + "event_kind": "담보말소", + "event_subkind": "mortgage_extinguishment_by_fraction", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "2번 근저당권 말소", + "source_evidence_index": "E-014", + "event_date": "2024-10-17", + "identity_signature": "event|담보말소|null|null|2번 근저당권|2024-10-17", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-015", + "title": "등기사항전부증명서(말소사항 포함)-집합건물", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 15 + }, + "event_candidates": [ + { + "candidate_id": "EVT-015-01", + "event_or_state": "event", + "event_kind": "소유권이전", + "event_subkind": "소유권보존", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "소유권보존등기(김일동)", + "source_evidence_index": "E-015", + "event_date": "2021-12-10", + "identity_signature": "event|소유권이전|김일동|없음|서정빌라 101호|2021-12-10", + "confidence": "high" + }, + { + "candidate_id": "EVT-015-02", + "event_or_state": "event", + "event_kind": "소유권이전", + "event_subkind": "매매", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "매매로 인한 소유권이전(정남이)", + "source_evidence_index": "E-015", + "event_date": "2021-12-30", + "identity_signature": "event|소유권이전|정남이|김일동|서정빌라 101호|2021-12-30", + "confidence": "high" + }, + { + "candidate_id": "EVT-015-03", + "event_or_state": "event", + "event_kind": "가압류/압류", + "event_subkind": "가압류", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "가압류 설정(김동국)", + "source_evidence_index": "E-015", + "amount": "14,000,000원", + "event_date": "2022-09-01", + "identity_signature": "event|가압류/압류|김동국|정남이|14,000,000원|2022-09-01", + "confidence": "high" + }, + { + "candidate_id": "EVT-015-04", + "event_or_state": "event", + "event_kind": "소유권이전", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "소유권이전(강호연)", + "source_evidence_index": "E-015", + "event_date": "2022-10-01", + "identity_signature": "event|소유권이전|강호연|정남이|서정빌라 101호|2022-10-01", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-016", + "title": "별지 목록", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 16 + }, + "zero_event_reason": "REFERENCE_ONLY_DOC" + }, + { + "evidence_index": "E-017", + "title": "부동산 매매계약서", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 17 + }, + "event_candidates": [ + { + "candidate_id": "EVT-017-01", + "event_or_state": "event", + "event_kind": "처분", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "흑석동 상가에 대한 매매계약 체결", + "source_evidence_index": "E-017", + "amount": "1,000,000,000원", + "event_date": "2023-10-01", + "identity_signature": "event|처분|윤건우|이수인|흑석동상가|2023-10-01", + "confidence": "high" + }, + { + "candidate_id": "EVT-017-02", + "event_or_state": "event", + "event_kind": "기타", + "event_subkind": "대가수수", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "계약금 2억 원 지급", + "source_evidence_index": "E-017", + "amount": "200,000,000원", + "event_date": "2023-10-01", + "identity_signature": "event|기타|이수인|윤건우|계약금|2023-10-01", + "confidence": "high" + }, + { + "candidate_id": "EVT-017-03", + "event_or_state": "event", + "event_kind": "기타", + "event_subkind": "대가수수", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "중도금 5억 원 지급", + "source_evidence_index": "E-017", + "amount": "500,000,000원", + "event_date": "2024-01-01", + "identity_signature": "event|기타|이수인|윤건우|중도금|2024-01-01", + "confidence": "high" + }, + { + "candidate_id": "EVT-017-04", + "event_or_state": "event", + "event_kind": "인도", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "흑석동 상가 인도", + "source_evidence_index": "E-017", + "event_date": "2024-01-01", + "identity_signature": "event|인도|윤건우|이수인|흑석동상가|2024-01-01", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-018", + "title": "사업자등록증", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 18 + }, + "event_candidates": [ + { + "candidate_id": "EVT-018-01", + "event_or_state": "state", + "event_kind": "사업자등록", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "양정숙의 맛나식당 사업자등록 및 영업 개시", + "source_evidence_index": "E-018", + "event_date": "2024-02-01", + "identity_signature": "state|사업자등록|양정숙|맛나식당|2024-02-01", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-020", + "title": "영업양도 계약서", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 20 + }, + "event_candidates": [ + { + "candidate_id": "EVT-020-01", + "event_or_state": "event", + "event_kind": "영업양도", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "김선웅과 오민한 사이의 해드림(SUN-DREAM) 영업양도 계약 성립", + "source_evidence_index": "E-020", + "event_date": "2024-07-05", + "identity_signature": "event|영업양도|김선웅|오민한|해드림|2024-07-05", + "confidence": "high" + }, + { + "candidate_id": "EVT-020-02", + "event_or_state": "event", + "event_kind": "기타", + "event_subkind": "대가수수", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "영업양도 대가 2억 원 지급 약정", + "source_evidence_index": "E-020", + "amount": "200,000,000원", + "event_date": "2024-07-31", + "identity_signature": "event|기타|오민한|김선웅|2억|2024-07-31", + "confidence": "high" + }, + { + "candidate_id": "EVT-020-03", + "event_or_state": "event", + "event_kind": "영업양도", + "event_subkind": "commercial_successor_liability_candidate", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "영업양도에 따른 고객관계 및 고용관계 승계 약정", + "source_evidence_index": "E-020", + "event_date": "2024-07-05", + "identity_signature": "event|영업양도|오민한|김선웅|고객고용관계|2024-07-05", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-021", + "title": "이행각서", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 21 + }, + "event_candidates": [ + { + "candidate_id": "EVT-021-01", + "event_or_state": "event", + "event_kind": "채권관리", + "event_subkind": "debt_acknowledgement", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "김선웅이 강용원에게 2021.6.5.자 물품대금 채무를 인정하고 2024.8.말까지 변제하겠다는 내용의 이행각서 작성", + "source_evidence_index": "E-021", + "event_date": "2024-08-05", + "identity_signature": "event|채권관리|김선웅|강용원|2021.6.5.자 물품대금 채무|2024-08-05", + "confidence": "high" + }, + { + "candidate_id": "EVT-021-02", + "event_or_state": "event", + "event_kind": "채권관리", + "event_subkind": "new_due_date", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "물품대금 채무의 변제기를 2024.8.31.로 설정", + "source_evidence_index": "E-021", + "event_date": "2024-08-31", + "identity_signature": "event|채권관리|김선웅|강용원|물품대금 채무 변제기|2024-08-31", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-022", + "title": "임대차계약서", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 22 + }, + "event_candidates": [ + { + "candidate_id": "EVT-022-01", + "event_or_state": "event", + "event_kind": "임대차", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "이수인과 양정숙 간 흑석동 상가 임대차계약 체결", + "source_evidence_index": "E-022", + "event_date": "2024-02-01", + "identity_signature": "event|임대차|이수인|양정숙|흑석동 상가 201호|2024-02-01", + "confidence": "high" + }, + { + "candidate_id": "EVT-022-02", + "event_or_state": "event", + "event_kind": "기타", + "event_subkind": "대가수수", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "양정숙의 임대차보증금 3억 원 지급", + "source_evidence_index": "E-022", + "amount": "300,000,000원", + "event_date": "2024-02-01", + "identity_signature": "event|기타|양정숙|이수인|임대차보증금 300,000,000원|2024-02-01", + "confidence": "high" + }, + { + "candidate_id": "EVT-022-03", + "event_or_state": "state", + "event_kind": "점유", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "양정숙의 흑석동 상가 점유 및 운영 상태", + "source_evidence_index": "E-022", + "event_date": "2024-02-01", + "identity_signature": "state|점유|양정숙|이수인|흑석동 상가 201호|현재", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-023", + "title": "임료시세확인서", + "doc_type": "기타", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 23 + }, + "event_candidates": [ + { + "candidate_id": "EVT-023-01", + "event_or_state": "state", + "event_kind": "가치평가", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "경기도 평택시 서정빌라의 2022. 10. 기준 월 차임 시세를 보증금 없는 월세 200만 원으로 평가함.", + "source_evidence_index": "E-023", + "amount": "2,000,000원", + "event_date": "2022-10-01", + "identity_signature": "state|가치평가|김민식||월세 200만 원|2022-10", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-024", + "title": "차용증", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 24 + }, + "event_candidates": [ + { + "candidate_id": "EVT-024-01", + "event_or_state": "event", + "event_kind": "채권발생", + "event_subkind": "underlying_claim_arising", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "강용원이 오국한에게 1억 원을 대여하는 계약 체결", + "source_evidence_index": "E-024", + "amount": "100,000,000원", + "event_date": "2022-04-18", + "identity_signature": "event:채권발생:강용원:오국한:100000000원:2022-04-18", + "confidence": "high" + }, + { + "candidate_id": "EVT-024-02", + "event_or_state": "event", + "event_kind": "채권발생", + "event_subkind": "underlying_claim_arising", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "강용원이 오국한에게 1억 원을 추가로 대여하는 계약 체결", + "source_evidence_index": "E-024", + "amount": "100,000,000원", + "event_date": "2022-05-18", + "identity_signature": "event:채권발생:강용원:오국한:100000000원:2022-05-18", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-025", + "title": "차용증", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 25 + }, + "event_candidates": [ + { + "candidate_id": "EVT-025-01", + "event_or_state": "event", + "event_kind": "채권발생", + "event_subkind": "underlying_claim_arising", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "강용원과 오민한 사이의 2억 원 차용 계약 체결 및 차용증 작성", + "source_evidence_index": "E-025", + "amount": "200,000,000원", + "event_date": "2023-01-06", + "identity_signature": "event|채권발생|강용원|오민한|200,000,000원|2023-01-06", + "confidence": "high" + }, + { + "candidate_id": "EVT-025-02", + "event_or_state": "event", + "event_kind": "담보설정", + "event_subkind": "secured_debt_creation", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "차용금 채무 담보를 위한 성수동 대지 지분 근저당권 설정 약정", + "source_evidence_index": "E-025", + "event_date": "2023-01-06", + "identity_signature": "event|담보설정|강용원|오민한|성수동 대지 지분|2023-01-06", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-026", + "title": "차 용 증", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 26 + }, + "event_candidates": [ + { + "candidate_id": "EVT-026-01", + "event_or_state": "event", + "event_kind": "채권발생", + "event_subkind": "secured_debt_creation", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "강용원이 오민한으로부터 영업자금 목적으로 3억 원을 차용함", + "source_evidence_index": "E-026", + "amount": "300,000,000원", + "event_date": "2023-07-06", + "identity_signature": "event채권발생강용원오민한300,000,000원2023-07-06", + "confidence": "high" + }, + { + "candidate_id": "EVT-026-02", + "event_or_state": "event", + "event_kind": "담보설정", + "event_subkind": "secured_debt_creation", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "차용금 반환 담보를 위해 성수동 256번지 대지 5/3 지분에 1순위 근저당권 설정 약정", + "source_evidence_index": "E-026", + "event_date": "2023-07-06", + "identity_signature": "event담보설정강용원오민한성수동256대지5/3지분2023-07-06", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-027", + "title": "최고 통지서", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 27 + }, + "event_candidates": [ + { + "candidate_id": "EVT-027-01", + "event_or_state": "event", + "event_kind": "통지", + "event_subkind": "demand_notice_dispatch", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "강용원이 김선웅에게 물품대금 지급을 최고하는 통지서를 발송함", + "source_evidence_index": "E-027", + "event_date": "2024-05-04", + "identity_signature": "event|통지|강용원|김선웅|자재대금최고|2024-05-04", + "confidence": "high" + }, + { + "candidate_id": "EVT-027-02", + "event_or_state": "event", + "event_kind": "도달", + "event_subkind": "demand_notice_arrival", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "강용원이 발송한 최고 통지서가 김선웅에게 도달함", + "source_evidence_index": "E-027", + "event_date": "2024-05-06", + "identity_signature": "event|도달|강용원|김선웅|자재대금최고|2024-05-06", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-028", + "title": "통지서", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 28 + }, + "event_candidates": [ + { + "candidate_id": "EVT-028-01", + "event_or_state": "event", + "event_kind": "통지", + "event_subkind": "demand_notice_arrival", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "오민한이 강용원에게 김선웅의 채무에 대한 책임을 부인하는 내용의 통지서를 발송함.", + "source_evidence_index": "E-028", + "event_date": "2024-12-27", + "identity_signature": "event|통지|오민한|강용원|김선웅 물품대금 채무 책임 부인|2024-12-27", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-029", + "title": "통지서_이수인_윤건우", + "doc_type": "통신기록", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 29 + }, + "event_candidates": [ + { + "candidate_id": "EVT-029-01", + "event_or_state": "event", + "event_kind": "통지", + "action_type_candidate": "준법률행위(quasi-legal acts)", + "action_summary": "흑석동 상가 매매계약에 관하여 이수인이 윤건우에게 매매대금 감액 및 제3자 명의 소유권이전등기를 요청하는 통지", + "source_evidence_index": "E-029", + "event_date": "2024-03-30", + "identity_signature": "event|통지|이수인|윤건우|흑석동상가매매계약조건변경요청|2024-03-30", + "confidence": "high" + } + ] + }, + { + "evidence_index": "E-030", + "title": "해제 통지서", + "doc_type": "처분문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 30 + }, + "event_candidates": [ + { + "candidate_id": "EVT-030-01", + "event_or_state": "event", + "event_kind": "통지", + "event_subkind": "해제/해지 통지", + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "윤건우가 이수인에게 흑석동 상가 매매계약 해제 통지를 발송함", + "source_evidence_index": "E-030", + "event_date": "2024-05-15", + "identity_signature": "event|통지|윤건우|이수인|흑석동상가매매계약해제|2024-05-15", + "confidence": "high" + }, + { + "candidate_id": "EVT-030-02", + "event_or_state": "event", + "event_kind": "도달", + "action_type_candidate": "사실행위(factual acts)", + "action_summary": "이수인이 흑석동 상가 매매계약 해제 통지서를 수령함", + "source_evidence_index": "E-030", + "event_date": "2024-05-16", + "identity_signature": "event|도달|이수인|윤건우|흑석동상가매매계약해제통지서|2024-05-16", + "confidence": "high" + } + ] + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed.json new file mode 100644 index 00000000..fcb88a7a --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed.json @@ -0,0 +1,3109 @@ +{ + "schema_contract_version": "evidence_indexed.v3", + "items": [ + { + "evidence_index": "E-001", + "doc_uid": "DOC-001-가정법원심판_강호연", + "title": "가정법원심판_강호연", + "title_normalized": "가정법원심판_강호연", + "doc_type": "판결/결정", + "key_facts": [ + "2024느단52199 상속포기", + "강호연 사망(2024. 8. 10.)", + "상속포기 신고 수리(2024. 9. 11.)" + ], + "key_dates": [ + "2024. 8. 10.", + "2024. 9. 11.", + "2024. 9. 20." + ], + "key_amounts": [], + "key_parties": [ + "강형수", + "강지수", + "강호연" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 1 + }, + "succession_document_components": { + "decedent": "강호연", + "death_date": "2024-08-10", + "succession_refusers": [ + "강형수", + "강지수" + ], + "filing_date": "2024-09-11", + "decision_date": "2024-09-20" + } + }, + { + "evidence_index": "E-002", + "doc_uid": "DOC-002-가족관계증명서_강호연", + "title": "가족관계증명서", + "title_normalized": "가족관계증명서", + "doc_type": "공문서", + "key_facts": [ + "강호연 본인 가족관계", + "부 강용원, 배우자 양정숙, 자녀 강형수, 강지수" + ], + "key_dates": [ + "1973-05-19", + "1947-12-11", + "1949-08-10", + "1974-11-21", + "2000-03-18", + "2002-11-30" + ], + "key_amounts": [], + "key_parties": [ + "강호연", + "강용원", + "김순자", + "양정숙", + "강형수", + "강지수" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 2 + } + }, + { + "evidence_index": "E-003", + "doc_uid": "DOC-003-각서_금전대차소비계약서_오국한_오민한", + "title": "각서 및 금전소비대차계약서", + "title_normalized": "각서_및_금전소비대차계약서", + "doc_type": "처분문서", + "key_facts": [ + "오국한이 오민한의 자금으로 신림동 상가를 매수하여 명의신탁", + "오국한이 오민한으로부터 1,000만 원을 차용(2022. 7. 18.)", + "오국한의 2023. 7. 17. 변제기 약정" + ], + "key_dates": [ + "2015-09-17", + "2022-07-18", + "2023-07-17" + ], + "key_amounts": [ + "40,000,000원", + "10,000,000원" + ], + "key_parties": [ + "오국한", + "오민한", + "이경주" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 3 + }, + "claim_document_components": [ + { + "component_kind": "debt_acknowledgement", + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "각서 1항", + "excerpt": "신림동 상가는 명의만 오국한으로 되어 있을 뿐, 오민한 소유임을 확인" + } + }, + { + "component_kind": "underlying_claim", + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "금전소비대차계약서", + "excerpt": "오국한은 오민한으로부터 1,000만 원을 차용" + } + } + ], + "loan_components": [ + { + "loan_component_id": "E-003-LC-01", + "principal_amount": "10,000,000원", + "loan_date": "2022-07-18", + "due_date": "2023-07-17", + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "rate_text": null, + "rate_source_locator": null, + "interest_accrual_start_hint": null, + "collateral_object_hint": null, + "role_in_claim_hint": "오민한의 오국한에 대한 대여금 채권" + } + ], + "defense_assertions": [ + { + "assertion_kind": "명의신탁 주장", + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "각서 1항", + "excerpt": "신림동 상가는 명의만 오국한으로 되어 있을 뿐, 오민한 소유" + } + } + ] + }, + { + "evidence_index": "E-004", + "doc_uid": "DOC-004-감정평가서", + "title": "감정평가서", + "title_normalized": "감정평가서", + "doc_type": "기타", + "key_facts": [ + "신림로 상가 및 포천시 임야 감정평가", + "서울감정평가사사무소 작성", + "2024. 9. 13. 작성" + ], + "key_dates": [ + "2024. 9. 13." + ], + "key_amounts": [], + "key_parties": [ + "강용원", + "장수현" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 4 + }, + "valuation_object": { + "valuation_kind": "market_value", + "target_object": "신림로 상가 및 포천시 임야", + "basis_date_or_period": "2022. 1. 1. ~ 현재", + "value_rows": [ + { + "label": "신림로 상가 (2022.1.1~2022.12.31)", + "amount": "2억 3,000만 원", + "unit": "총액" + }, + { + "label": "신림로 상가 (2023.1.1~2023.12.31)", + "amount": "2억 4,000만 원", + "unit": "총액" + }, + { + "label": "신림로 상가 (2024.1.1~현재)", + "amount": "2억 5,000만 원", + "unit": "총액" + }, + { + "label": "포천시 임야 (2022.1.1~2022.12.31)", + "amount": "3,000만 원", + "unit": "총액" + }, + { + "label": "포천시 임야 (2023.1.1~2023.12.31)", + "amount": "3,000만 원", + "unit": "총액" + }, + { + "label": "포천시 임야 (2024.1.1~현재)", + "amount": "3,500만 원", + "unit": "총액" + } + ] + }, + "valuation_rows": [ + { + "label": "신림로 상가 2022", + "amount": "2억 3,000만 원", + "unit": "총액", + "legal_use_case": "가액배상 기준" + }, + { + "label": "신림로 상가 2023", + "amount": "2억 4,000만 원", + "unit": "총액", + "legal_use_case": "가액배상 기준" + }, + { + "label": "신림로 상가 2024", + "amount": "2억 5,000만 원", + "unit": "총액", + "legal_use_case": "가액배상 기준" + }, + { + "label": "포천시 임야 2022", + "amount": "3,000만 원", + "unit": "총액", + "legal_use_case": "가액배상 기준" + }, + { + "label": "포천시 임야 2023", + "amount": "3,000만 원", + "unit": "총액", + "legal_use_case": "가액배상 기준" + }, + { + "label": "포천시 임야 2024", + "amount": "3,500만 원", + "unit": "총액", + "legal_use_case": "가액배상 기준" + } + ] + }, + { + "evidence_index": "E-005", + "doc_uid": "DOC-005-감정평가의견서_성수동", + "title": "감정평가 의견서", + "title_normalized": "감정평가 의견서", + "doc_type": "거래기록", + "key_facts": [ + "대상 토지: 서울 성동구 성수동 256 대 330㎡", + "2024년도 차임 시세 감정 결과 회신" + ], + "key_dates": [ + "2024-12-29" + ], + "key_amounts": [ + "월 250만 원", + "월 400만 원", + "월 350만 원", + "월 500만 원" + ], + "key_parties": [ + "강용원", + "감정평가법인 광나루", + "이선욱" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 5 + }, + "valuation_object": { + "valuation_kind": "rent_value", + "target_object": "서울 성동구 성수동 256 대 330㎡", + "basis_date_or_period": "2024년", + "value_rows": [ + { + "label": "지상에 건물이 있는 경우, 보증금 3억 원", + "amount": "월 250만 원", + "unit": "월액" + }, + { + "label": "지상에 건물이 있는 경우, 보증금 없음", + "amount": "월 400만 원", + "unit": "월액" + }, + { + "label": "지상에 건물이 없는 경우, 보증금 3억 원", + "amount": "월 350만 원", + "unit": "월액" + }, + { + "label": "지상에 건물이 없는 경우, 보증금 없음", + "amount": "월 500만 원", + "unit": "월액" + } + ] + } + }, + { + "evidence_index": "E-006", + "doc_uid": "DOC-006-계약서_이문호_박성희", + "title": "계약서", + "title_normalized": "계약서", + "doc_type": "처분문서", + "key_facts": [ + "성수동 대지 임대차 계약", + "성수동 건물 매매 계약", + "2024. 11. 20. 체결" + ], + "key_dates": [ + "2024-11-20" + ], + "key_amounts": [ + "임대보증금 300,000,000원", + "차임 월 3,000,000원", + "매매대금 200,000,000원" + ], + "key_parties": [ + "이문호", + "박성희" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 6 + }, + "contract_components": [ + { + "component_id": "E-006-CC-01", + "component_kind": "lease", + "object_spec": "서울 성동구 성수동 256 대 330㎡", + "amount": "300,000,000원", + "date": "2024-11-20", + "payer_candidates": [ + "박성희" + ], + "recipient_candidates": [ + "이문호" + ], + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "1", + "excerpt": "임대목적물인 대지를 보증금 3억 원, 차임 월 300만 원" + } + }, + { + "component_id": "E-006-CC-02", + "component_kind": "sale", + "object_spec": "위 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "amount": "200,000,000원", + "date": "2024-11-20", + "payer_candidates": [ + "박성희" + ], + "recipient_candidates": [ + "이문호" + ], + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "2", + "excerpt": "상가를 대금 2억 원에 매도하기로 하고" + } + } + ] + }, + { + "evidence_index": "E-007", + "doc_uid": "DOC-007-공사도급계약서", + "title": "공사도급계약서", + "title_normalized": "공사도급계약서", + "doc_type": "처분문서", + "key_facts": [ + "평택시 서정3길 123 건물 신축 공사도급계약", + "총 공사대금 10억 원", + "乙 박광윤의 중도금 4억 원 영수증 포함" + ], + "key_dates": [ + "2021-05-01", + "2021-08-30", + "2021-11-10" + ], + "key_amounts": [ + "1,000,000,000원", + "300,000,000원", + "400,000,000원" + ], + "key_parties": [ + "김일동(도급인)", + "박광윤(수급인)" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 7 + }, + "contract_components": [ + { + "component_id": "E-007-CC-01", + "component_kind": "other", + "object_spec": "평택시 서정3길 123 건물 신축공사", + "amount": "1,000,000,000원", + "date": "2021-05-01", + "payer_candidates": [ + "김일동" + ], + "recipient_candidates": [ + "박광윤" + ], + "source_locator": { + "locator_type": "table_row", + "locator_hint": "공사대금", + "excerpt": "총 공사금: 10억 원" + } + } + ], + "claim_document_components": [ + { + "component_kind": "underlying_claim", + "principal_amount": "400,000,000원", + "role_in_claim_hint": "중도금 수령" + } + ] + }, + { + "evidence_index": "E-008", + "doc_uid": "DOC-008-내용증명", + "title": "내용증명", + "title_normalized": "내용증명", + "doc_type": "처분문서", + "key_facts": [ + "강용원이 오민한에게 채권 관련 최고", + "오국환의 신림동 상가 오민한 명의 이전 인지", + "사해행위취소 검토 및 반환 요청" + ], + "key_dates": [ + "2024-09-15" + ], + "key_amounts": [ + "200,000,000원", + "30,000,000원", + "40,000,000원", + "70,000,000원" + ], + "key_parties": [ + "강용원", + "오민한", + "오국환", + "오칠성", + "나일오" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 8 + }, + "notice_components": [ + { + "notice_id_proposed": "E-008-NT-01", + "notice_type": "기타", + "drafted_date": "2024-09-15", + "dispatch_date": "2024-09-15", + "sender_candidates": [ + "강용원" + ], + "receiver_candidates": [ + "오민한" + ], + "legal_effect_candidate_hint": "사해행위취소 및 소유권 반환 요구" + } + ] + }, + { + "evidence_index": "E-009", + "doc_uid": "DOC-009-내용증명", + "title": "내용증명", + "title_normalized": "내용증명", + "doc_type": "기타", + "key_facts": [ + "윤건우의 흑석동 상가 매매계약 해제 통보", + "임대차계약 해지 및 명도 요구" + ], + "key_dates": [ + "2024-11-29", + "2024-11-30" + ], + "key_amounts": [], + "key_parties": [ + "윤건우", + "양정숙" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 9 + }, + "notice_components": [ + { + "notice_id_proposed": "E-009-NT-01", + "notice_type": "기타", + "drafted_date": "2024-11-29", + "dispatch_date": "2024-11-29", + "arrival_date": "2024-11-30", + "received_date": "2024-11-30", + "legal_effect_date_candidate": "2024-11-30", + "requires_receipt_for_effect": true, + "sender_candidates": [ + "윤건우" + ], + "receiver_candidates": [ + "양정숙" + ], + "target_object_hint": "서울 동작구 흑석로 80 상가건물 201호", + "receipt_evidence_refs": [ + "우편물배달증명서" + ], + "legal_effect_candidate_hint": "임대차계약 해지 및 상가 인도 요구" + } + ] + }, + { + "evidence_index": "E-010", + "doc_uid": "DOC-010-내용증명에_대한_답신", + "title": "내용증명에 대한 답신", + "title_normalized": "내용증명에 대한 답신", + "doc_type": "통신기록", + "key_facts": [ + "오민한의 신림동 상가 사해행위 부인 주장", + "오민한의 5,000만 원 채권 상계 주장", + "2024. 9. 30. 내용증명 우편물 발송" + ], + "key_dates": [ + "2024. 9. 30.", + "2022. 7. 18." + ], + "key_amounts": [ + "4,000만 원", + "1,000만 원", + "5,000만 원" + ], + "key_parties": [ + "오민한", + "강용원" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 10 + }, + "notice_components": [ + { + "notice_id_proposed": "E-010-NT-01", + "notice_type": "기타", + "dispatch_date": "2024-09-30", + "sender_candidates": [ + "오민한" + ], + "receiver_candidates": [ + "강용원" + ] + } + ] + }, + { + "evidence_index": "E-011", + "doc_uid": "DOC-011-답변서_박광윤_to_강용원_양정숙", + "title": "답변서", + "title_normalized": "답변서", + "doc_type": "기타", + "key_facts": [ + "박광윤의 서정빌라 101호 유치권 행사 주장", + "건축주 김일동으로부터 공사잔금 3억 원 미지급 주장", + "임대료 및 본인/가족 거주 차임을 채무에 충당 주장" + ], + "key_dates": [ + "2024-08-29", + "2024-09-01", + "2024-10-31" + ], + "key_amounts": [ + "300,000,000원", + "2,000,000원" + ], + "key_parties": [ + "박광윤", + "강용원", + "양정숙", + "김일동" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 11 + } + }, + { + "evidence_index": "E-012", + "doc_uid": "DOC-012-등기말소_신청에_대한_답신", + "title": "등기말소 신청에 대한 답신", + "title_normalized": "등기말소 신청에 대한 답신", + "doc_type": "처분문서", + "key_facts": [ + "대한은행이 강용원의 근저당권 말소 신청 거부", + "경매절차 하자 없음을 주장", + "민사집행법 제267조에 따른 경매의 공신력 주장" + ], + "key_dates": [ + "2024-12-22" + ], + "key_amounts": [], + "key_parties": [ + "강용원", + "주식회사 대한은행" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 12 + }, + "notice_components": [ + { + "notice_id_proposed": "E-012-NT-01", + "notice_type": "기타", + "drafted_date": "2024-12-22", + "dispatch_date": "2024-12-22", + "sender_candidates": [ + "주식회사 대한은행" + ], + "receiver_candidates": [ + "강용원" + ], + "target_object_hint": "성수동 대지 근저당권", + "legal_effect_candidate_hint": "등기말소 신청 거부 통지" + } + ] + }, + { + "evidence_index": "E-013", + "doc_uid": "DOC-013-등기사항전부증명서_성수동", + "title": "등기사항전부증명서(말소사항 포함)-토지", + "title_normalized": "등기사항전부증명서(말소사항 포함)-토지", + "doc_type": "공문서", + "key_facts": [ + "성수동 대지(256)에 관한 등기사항전부증명서", + "2024.10.5. 경매로 인한 매각으로 이문호 소유권 취득", + "2024.10.5. 주식회사 대한은행 근저당권 설정" + ], + "key_dates": [ + "2022.12.8. (강용원/정유심 공유 지분 등기)", + "2024.10.5. (경매에 의한 소유권이전)", + "2024.10.5. (주식회사 대한은행 근저당권 설정)" + ], + "key_amounts": [ + "520,000,000원 (대한은행 채권최고액)" + ], + "key_parties": [ + "강용원", + "정유심", + "이문호", + "오민환", + "주식회사 대한은행" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 13 + }, + "registry_row_components": [ + { + "registry_ref": "REG-013-01", + "section": "갑구", + "row_seq": 1, + "receipt_no": "제1453호", + "event_date": "1997-03-15", + "purpose": "소유권이전", + "right_holder": "박재경" + }, + { + "registry_ref": "REG-013-02", + "section": "갑구", + "row_seq": 2, + "receipt_no": "제7026호", + "event_date": "2017-06-30", + "purpose": "소유권이전", + "right_holder": "강석우" + }, + { + "registry_ref": "REG-013-03", + "section": "갑구", + "row_seq": 3, + "receipt_no": "제11202호", + "event_date": "2022-11-08", + "purpose": "소유권이전", + "right_holder": "정유심, 강용원" + }, + { + "registry_ref": "REG-013-04", + "section": "갑구", + "row_seq": 4, + "receipt_no": "제7202호", + "event_date": "2023-04-06", + "purpose": "증여", + "right_holder": "강용원" + }, + { + "registry_ref": "REG-013-05", + "section": "갑구", + "row_seq": 5, + "receipt_no": "제12402호", + "event_date": "2024-07-21", + "purpose": "담보권실행을위한경매개시결정(일괄)", + "right_holder": "오민환" + }, + { + "registry_ref": "REG-013-06", + "section": "갑구", + "row_seq": 6, + "receipt_no": "제25797호", + "event_date": "2024-10-05", + "purpose": "담보권실행을 위한 경매로 인한 매각", + "right_holder": "이문호" + }, + { + "registry_ref": "REG-013-07", + "section": "을구", + "row_seq": 4, + "receipt_no": "제25799호", + "event_date": "2024-10-05", + "purpose": "근저당권설정", + "right_holder": "주식회사 대한은행", + "secured_debt_amount": "520,000,000원" + } + ] + }, + { + "evidence_index": "E-014", + "doc_uid": "DOC-014-등기사항전부증명서_말소사항_포함_건물", + "title": "등기사항전부증명서 (말소사항 포함) -건물", + "title_normalized": "등기사항전부증명서 (말소사항 포함) -건물", + "doc_type": "공문서", + "key_facts": [ + "2015.9.17 오국환 소유권이전", + "2023.3.17 오민한 소유권이전(매매)", + "2024.10.17 2번 근저당권 말소" + ], + "key_dates": [ + "2006-05-08", + "2015-09-17", + "2023-03-17", + "2024-10-17" + ], + "key_amounts": [ + "40,000,000원", + "150,000,000원", + "50,000,000원" + ], + "key_parties": [ + "이경주", + "오국환", + "오민한", + "나임오", + "강용일" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 14 + }, + "registry_row_components": [ + { + "registry_ref": "REG-014-01", + "section": "갑구", + "row_seq": 1, + "registry_purpose": "소유권보존", + "receipt_no": "제24759호", + "registry_date": "2006-05-08", + "rights_holder": "이경주" + }, + { + "registry_ref": "REG-014-02", + "section": "갑구", + "row_seq": 2, + "registry_purpose": "소유권이전", + "receipt_no": "제56457호", + "registry_date": "2015-09-17", + "rights_holder": "오국환" + }, + { + "registry_ref": "REG-014-03", + "section": "갑구", + "row_seq": 3, + "registry_purpose": "소유권이전", + "receipt_no": "제11593호", + "registry_date": "2023-03-17", + "rights_holder": "오민한" + }, + { + "registry_ref": "REG-014-04", + "section": "을구", + "row_seq": 4, + "registry_purpose": "근저당권설정", + "receipt_no": "제50099호", + "registry_date": "2021-08-01", + "rights_holder": "나임오" + }, + { + "registry_ref": "REG-014-05", + "section": "을구", + "row_seq": 5, + "registry_purpose": "근저당권설정", + "receipt_no": "제26597호", + "registry_date": "2022-04-18", + "rights_holder": "강용일" + }, + { + "registry_ref": "REG-014-06", + "section": "을구", + "row_seq": 6, + "registry_purpose": "근저당권설정등기말소", + "receipt_no": "제74586호", + "registry_date": "2024-10-17", + "rights_holder": null + } + ] + }, + { + "evidence_index": "E-015", + "doc_uid": "DOC-015-등기사항전부증명서_말소사항_포함_집합건물", + "title": "등기사항전부증명서(말소사항 포함)-집합건물", + "title_normalized": "등기사항전부증명서_말소사항_포함_집합건물", + "doc_type": "공문서", + "key_facts": [ + "경기도 평택시 서정3길 123 서정빌라 101호 등기부", + "2022. 10. 1. 강호연 소유권이전", + "2022. 9. 1. 김동국 가압류(말소)" + ], + "key_dates": [ + "2021-12-10", + "2021-12-30", + "2022-10-01" + ], + "key_amounts": [ + "14,000,000원" + ], + "key_parties": [ + "김일동", + "정남이", + "강호연", + "김동국" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 15 + }, + "registry_row_components": [ + { + "registry_ref": "REG-015-01", + "row_seq": 1, + "kind": "갑구", + "rank": "1", + "purpose": "소유권보존", + "receipt_date": "2021년12월10일", + "receipt_no": "제7233호", + "party_name": "김일동" + }, + { + "registry_ref": "REG-015-02", + "row_seq": 2, + "kind": "갑구", + "rank": "2", + "purpose": "소유권이전", + "receipt_date": "2021년12월30일", + "receipt_no": "제7871호", + "cause": "매매", + "party_name": "정남이" + }, + { + "registry_ref": "REG-015-03", + "row_seq": 3, + "kind": "갑구", + "rank": "3", + "purpose": "가압류", + "receipt_date": "2022년9월1일", + "receipt_no": "제11593호", + "cause": "수원지방법원 평택지원의 가압류결정(2022카단43434)", + "party_name": "김동국", + "amount": "14,000,000원" + }, + { + "registry_ref": "REG-015-04", + "row_seq": 4, + "kind": "갑구", + "rank": "4", + "purpose": "소유권이전", + "receipt_date": "2022년10월1일", + "receipt_no": "제12321호", + "party_name": "강호연" + } + ] + }, + { + "evidence_index": "E-016", + "doc_uid": "DOC-016-별지_목록", + "title": "별지 목록", + "title_normalized": "별지 목록", + "doc_type": "공문서", + "key_facts": [ + "성수동 대지 및 건물 목록", + "신림동 상가 목록", + "평택시 빌라 및 대지권 목록", + "흑석동 상가 및 대지권 목록" + ], + "key_dates": [], + "key_amounts": [], + "key_parties": [], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 16 + }, + "schedule_items": [ + { + "schedule_ref": "SCH-016-01", + "canonical_label_candidate": "1. 서울 성동구 성수동 256 대 330㎡", + "usable_in_relief": "yes" + }, + { + "schedule_ref": "SCH-016-02", + "canonical_label_candidate": "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "usable_in_relief": "yes" + }, + { + "schedule_ref": "SCH-016-03", + "canonical_label_candidate": "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "usable_in_relief": "yes" + }, + { + "schedule_ref": "SCH-016-04", + "canonical_label_candidate": "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "usable_in_relief": "yes" + }, + { + "schedule_ref": "SCH-016-05", + "canonical_label_candidate": "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물", + "usable_in_relief": "yes" + } + ] + }, + { + "evidence_index": "E-017", + "doc_uid": "DOC-017-부동산_매매계약서", + "title": "부동산 매매계약서", + "title_normalized": "부동산 매매계약서", + "doc_type": "처분문서", + "key_facts": [ + "흑석동 상가 매매계약(2023.10.1.)", + "매매대금 10억 원", + "윤건우(매도인), 이수인(매수인)" + ], + "key_dates": [ + "2023-10-01", + "2024-01-01", + "2024-05-01" + ], + "key_amounts": [ + "1,000,000,000원", + "200,000,000원", + "500,000,000원", + "300,000,000원" + ], + "key_parties": [ + "윤건우", + "이수인", + "양재혁" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 17 + }, + "contract_components": [ + { + "component_id": "E-017-CC-01", + "component_kind": "sale", + "object_spec": "흑석동 상가(201호)", + "amount": "1,000,000,000원", + "date": "2023-10-01", + "payer_candidates": [ + "이수인" + ], + "recipient_candidates": [ + "윤건우" + ], + "source_locator": { + "locator_type": "table_row", + "locator_hint": "계약내용", + "excerpt": "매매대금 10억 원" + } + }, + { + "component_id": "E-017-CC-02", + "component_kind": "delivery", + "object_spec": "흑석동 상가(201호)", + "possession_recipient_candidates": [ + "이수인" + ], + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "특약사항", + "excerpt": "중도금을 지급받은 날 매수인에게 위 부동산을 인도한다." + } + } + ] + }, + { + "evidence_index": "E-018", + "doc_uid": "DOC-018-사_업_자_등_록_증", + "title": "사업자등록증", + "title_normalized": "사업자등록증", + "doc_type": "공문서", + "key_facts": [ + "양정숙 명의 맛나식당 사업자등록", + "개업일 2024년 2월 1일", + "사업장소재지 서울 동작구 흑석로 80, 201호" + ], + "key_dates": [ + "2024-02-01", + "2024-02-02" + ], + "key_amounts": [], + "key_parties": [ + "양정숙", + "동작 세무서장" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 18 + } + }, + { + "evidence_index": "E-019", + "doc_uid": "DOC-019-소멸청구_통지서", + "title": "소멸청구 통지서", + "title_normalized": "소멸청구 통지서", + "doc_type": "기타", + "key_facts": [ + "유치권 소멸청구 통지", + "평택시 빌라 인도 요구", + "사용수익 반환 요구" + ], + "key_dates": [ + "2024-08-29" + ], + "key_amounts": [], + "key_parties": [ + "강용원", + "양정숙", + "박광윤" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 19 + }, + "notice_components": [ + { + "notice_id_proposed": "E-019-NT-01", + "notice_type": "유치권소멸청구통지", + "drafted_date": "2024-08-29", + "dispatch_date": "2024-08-29", + "sender_candidates": [ + "강용원", + "양정숙" + ], + "receiver_candidates": [ + "박광윤" + ], + "target_object_hint": "평택시 서정3길 123, 101호(서정동, 서정빌라)", + "requires_receipt_for_effect": true + } + ] + }, + { + "evidence_index": "E-020", + "doc_uid": "DOC-020-영업양도계약서_김선웅_오민한", + "title": "영업양도 계약서", + "title_normalized": "영업양도 계약서", + "doc_type": "처분문서", + "key_facts": [ + "김선웅이 오민한에게 해드림 영업을 양도함", + "계약일 2024. 7. 5.", + "양도대금 2억 원" + ], + "key_dates": [ + "2024. 7. 5." + ], + "key_amounts": [ + "200,000,000원" + ], + "key_parties": [ + "김선웅", + "오민한" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 20 + }, + "business_transfer_components": [ + { + "business_name": "해드림(SUN-DREAM)", + "transferor_candidates": [ + "김선웅" + ], + "transferee_candidates": [ + "오민한" + ], + "business_assets_transferred": [ + "임차권", + "설비", + "비품", + "자재", + "공구" + ], + "trade_name_continuation_clause": "제4조: 을은 상호를 계속 사용할 수 있음", + "customer_or_employee_succession_clause": "제3조: 고객관계, 고용관계 승계", + "pre_transfer_debt_exclusion_clause": "제5조: 을은 계약일 전 발생 채무 책임지지 아니함" + } + ] + }, + { + "evidence_index": "E-021", + "doc_uid": "DOC-021-이행각서", + "title": "이행각서", + "title_normalized": "이행각서", + "doc_type": "처분문서", + "key_facts": [ + "김선웅이 강용원에게 인테리어 자재 물품대금 채무 변제 약속", + "2021. 6. 5. 자 물품대금 채무", + "2024. 8. 말까지 변제 약속" + ], + "key_dates": [ + "2021-06-05", + "2024-08-05", + "2024-08-31" + ], + "key_amounts": [], + "key_parties": [ + "김선웅", + "강용원" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 21 + }, + "claim_document_components": [ + { + "component_kind": "debt_acknowledgement", + "drafted_date": "2024-08-05", + "new_due_date": "2024-08-31" + } + ] + }, + { + "evidence_index": "E-022", + "doc_uid": "DOC-022-임대차계약서_이수인_양정숙", + "title": "임대차계약서", + "title_normalized": "임대차계약서", + "doc_type": "처분문서", + "key_facts": [ + "이수인(임대인)과 양정숙(임차인) 간 흑석동 상가 임대차계약 체결", + "2024. 2. 1. 계약 체결 및 보증금 지급 완료", + "보증금 3억 원, 월 차임 500만 원" + ], + "key_dates": [ + "2024-02-01" + ], + "key_amounts": [ + "300,000,000원", + "5,000,000원" + ], + "key_parties": [ + "이수인", + "양정숙" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 22 + }, + "contract_components": [ + { + "component_id": "E-022-CC-01", + "component_kind": "lease", + "object_spec": "흑석동 상가 201호", + "amount": "300,000,000원", + "date": "2024-02-01", + "payer_candidates": [ + "양정숙" + ], + "recipient_candidates": [ + "이수인" + ], + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "제1조, 제3조", + "excerpt": "흑석동 상가 201호, 보증금 3억" + } + } + ] + }, + { + "evidence_index": "E-023", + "doc_uid": "DOC-023-임료시세확인서", + "title": "임료시세확인서", + "title_normalized": "임료시세확인서", + "doc_type": "기타", + "key_facts": [ + "평택시 서정빌라 월 차임 시세 확인", + "보증금 없는 월세 200만 원", + "기준일 2022. 10.경" + ], + "key_dates": [ + "2022. 10.경", + "2024. 12. 23." + ], + "key_amounts": [ + "월세 200만 원" + ], + "key_parties": [ + "김민식" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 23 + }, + "valuation_object": { + "valuation_kind": "rent_value", + "target_object": "경기도 평택시 서정3길 123(서정동, 서정빌라)", + "basis_date_or_period": "2022. 10.경", + "value_rows": [ + { + "label": "보증금 없이", + "amount": "월세 200만 원", + "unit": "월액" + } + ] + } + }, + { + "evidence_index": "E-024", + "doc_uid": "DOC-024-차용증", + "title": "차용증", + "title_normalized": "차용증", + "doc_type": "처분문서", + "key_facts": [ + "2022. 4. 18.자 1억 원 차용증", + "2022. 5. 18.자 1억 원 차용증" + ], + "key_dates": [ + "2022-04-18", + "2022-05-18", + "2022-12-17", + "2023-03-17" + ], + "key_amounts": [ + "100,000,000원" + ], + "key_parties": [ + "강용원", + "오국한" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 24 + }, + "claim_document_components": [ + { + "component_kind": "underlying_claim", + "principal_amount": "100,000,000원", + "loan_date": "2022-04-18", + "due_date": "2022-12-17" + }, + { + "component_kind": "underlying_claim", + "principal_amount": "100,000,000원", + "loan_date": "2022-05-18", + "due_date": "2023-03-17" + } + ], + "loan_components": [ + { + "loan_component_id": "E-024-LC-01", + "principal_amount": "100,000,000원", + "loan_date": "2022-04-18", + "due_date": "2022-12-17", + "agreed_interest_rate": "월 1%", + "agreed_delay_damage_rate": null, + "rate_text": "월 1%" + }, + { + "loan_component_id": "E-024-LC-02", + "principal_amount": "100,000,000원", + "loan_date": "2022-05-18", + "due_date": "2023-03-17", + "agreed_interest_rate": "월 1%", + "agreed_delay_damage_rate": null, + "rate_text": "월 1%" + } + ] + }, + { + "evidence_index": "E-025", + "doc_uid": "DOC-025-차용증", + "title": "차용증", + "title_normalized": "차용증", + "doc_type": "처분문서", + "key_facts": [ + "강용원이 오민한에게 2억 원을 차용함", + "2024. 1. 5. 변제기", + "성수동 대지 5분의 2 지분에 1순위 근저당권 설정 약정" + ], + "key_dates": [ + "2023-01-06" + ], + "key_amounts": [ + "200,000,000원" + ], + "key_parties": [ + "오민한", + "강용원" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 25 + }, + "claim_document_components": [ + { + "component_kind": "underlying_claim", + "principal_amount": "200,000,000원", + "due_date": "2024-01-05", + "agreed_interest_rate": "연 10%", + "rate_text": "연 10%" + } + ], + "secured_debt_components": [ + { + "collateral_object_hint": "서울 성동구 성수동 256 대 330㎡의 5분의 2 지분", + "due_date": "2024-01-05" + } + ] + }, + { + "evidence_index": "E-026", + "doc_uid": "DOC-026-차용증", + "title": "차용증", + "title_normalized": "차 용 증", + "doc_type": "처분문서", + "key_facts": [ + "강용원의 오민한에 대한 300,000,000원 차용", + "서울 성동구 성수동 256 대 330㎡ 5/3 지분 근저당권 설정 약정" + ], + "key_dates": [ + "2023. 7. 6.", + "2024. 7. 5." + ], + "key_amounts": [ + "300,000,000원" + ], + "key_parties": [ + "강용원", + "오민한" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 26 + }, + "loan_components": [ + { + "loan_component_id": "E-026-LC-01", + "principal_amount": "300,000,000원", + "loan_date": "2023-07-06", + "due_date": "2024-07-05", + "agreed_interest_rate": "연 15%", + "collateral_object_hint": "서울 성동구 성수동 256 대 330㎡의 5분의 3 지분", + "role_in_claim_hint": "차용" + } + ], + "secured_debt_components": [ + { + "due_date": "2024-07-05", + "agreed_interest_rate": "연 15%" + } + ] + }, + { + "evidence_index": "E-027", + "doc_uid": "DOC-027-최고_통지서", + "title": "최고 통지서", + "title_normalized": "최고 통지서", + "doc_type": "처분문서", + "key_facts": [ + "2021. 6. 5. 강용원이 김선웅에게 2억 원 상당 인테리어 자재 납품", + "2024. 5. 4. 강용원이 김선웅에게 대금 지급 최고 통지서 발송", + "2024. 5. 6. 김선웅에게 최고 통지서 도달" + ], + "key_dates": [ + "2021-06-05", + "2024-05-04", + "2024-05-06" + ], + "key_amounts": [ + "200,000,000원" + ], + "key_parties": [ + "강용원", + "김선웅" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 27 + }, + "claim_document_components": [ + { + "component_kind": "demand_notice", + "dispatch_date": "2024-05-04", + "arrival_date": "2024-05-06" + } + ], + "notice_components": [ + { + "notice_id_proposed": "E-027-NT-01", + "notice_type": "최고", + "dispatch_date": "2024-05-04", + "arrival_date": "2024-05-06", + "sender_candidates": [ + "강용원" + ], + "receiver_candidates": [ + "김선웅" + ], + "receipt_evidence_refs": [ + "우편물 배달 증명서" + ] + } + ] + }, + { + "evidence_index": "E-028", + "doc_uid": "DOC-028-통지서", + "title": "통지서", + "title_normalized": "통지서", + "doc_type": "처분문서", + "key_facts": [ + "오민한의 김선웅 채무 책임 부인", + "오민한의 채무 시효 소멸 주장", + "오민한의 채무 인수 당시 책임 배제 약정 주장" + ], + "key_dates": [ + "2024-12-27" + ], + "key_amounts": [], + "key_parties": [ + "오민한", + "강용원", + "김선웅" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 28 + }, + "notice_components": [ + { + "notice_id_proposed": "E-028-NT-01", + "notice_type": "기타", + "dispatch_date": "2024-12-27", + "sender_candidates": [ + "오민한" + ], + "receiver_candidates": [ + "강용원" + ] + } + ], + "defense_assertions": [ + "김선웅의 채무 인수 당시 책임 배제 약정 존재", + "물품대금 채무 시효 소멸", + "본인에게 채무 책임 없음" + ] + }, + { + "evidence_index": "E-029", + "doc_uid": "DOC-029-통지서_이수인_윤건우", + "title": "통지서_이수인_윤건우", + "title_normalized": "통지서 이수인 윤건우", + "doc_type": "통신기록", + "key_facts": [ + "이수인이 윤건우에게 흑석동 상가 매매계약 관련 매매대금 감액 및 제3자 명의 등기 요청" + ], + "key_dates": [ + "2024-03-30" + ], + "key_amounts": [], + "key_parties": [ + "이수인", + "윤건우" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 29 + }, + "notice_components": [ + { + "notice_id_proposed": "E-029-NT-01", + "notice_type": "기타", + "dispatch_date": "2024-03-30", + "sender_candidates": [ + "이수인" + ], + "receiver_candidates": [ + "윤건우" + ], + "target_object_hint": "흑석동 상가" + } + ] + }, + { + "evidence_index": "E-030", + "doc_uid": "DOC-030-해제_통지서", + "title": "해제 통지서", + "title_normalized": "해제 통지서", + "doc_type": "처분문서", + "key_facts": [ + "2023.10.1. 흑석동 상가 매매계약 체결", + "2024.5.15. 윤건우가 이수인에게 매매계약 해제 통지", + "2024.5.16. 해제 통지서 수령" + ], + "key_dates": [ + "2023-10-01", + "2024-05-15", + "2024-05-16" + ], + "key_parties": [ + "윤건우", + "이수인" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 30 + }, + "notice_components": [ + { + "notice_id_proposed": "E-030-NT-01", + "notice_type": "해제통지", + "dispatch_date": "2024-05-15", + "arrival_date": "2024-05-16", + "sender_candidates": [ + "윤건우" + ], + "receiver_candidates": [ + "이수인" + ], + "target_object_hint": "흑석동 상가", + "legal_effect_date_candidate": "2024-05-16" + } + ] + } + ], + "evidence_indexed": [ + { + "evidence_index": "E-001", + "doc_uid": "DOC-001-가정법원심판_강호연", + "title": "가정법원심판_강호연", + "title_normalized": "가정법원심판_강호연", + "doc_type": "판결/결정", + "key_facts": [ + "2024느단52199 상속포기", + "강호연 사망(2024. 8. 10.)", + "상속포기 신고 수리(2024. 9. 11.)" + ], + "key_dates": [ + "2024. 8. 10.", + "2024. 9. 11.", + "2024. 9. 20." + ], + "key_amounts": [], + "key_parties": [ + "강형수", + "강지수", + "강호연" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 1 + }, + "succession_document_components": { + "decedent": "강호연", + "death_date": "2024-08-10", + "succession_refusers": [ + "강형수", + "강지수" + ], + "filing_date": "2024-09-11", + "decision_date": "2024-09-20" + } + }, + { + "evidence_index": "E-002", + "doc_uid": "DOC-002-가족관계증명서_강호연", + "title": "가족관계증명서", + "title_normalized": "가족관계증명서", + "doc_type": "공문서", + "key_facts": [ + "강호연 본인 가족관계", + "부 강용원, 배우자 양정숙, 자녀 강형수, 강지수" + ], + "key_dates": [ + "1973-05-19", + "1947-12-11", + "1949-08-10", + "1974-11-21", + "2000-03-18", + "2002-11-30" + ], + "key_amounts": [], + "key_parties": [ + "강호연", + "강용원", + "김순자", + "양정숙", + "강형수", + "강지수" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 2 + } + }, + { + "evidence_index": "E-003", + "doc_uid": "DOC-003-각서_금전대차소비계약서_오국한_오민한", + "title": "각서 및 금전소비대차계약서", + "title_normalized": "각서_및_금전소비대차계약서", + "doc_type": "처분문서", + "key_facts": [ + "오국한이 오민한의 자금으로 신림동 상가를 매수하여 명의신탁", + "오국한이 오민한으로부터 1,000만 원을 차용(2022. 7. 18.)", + "오국한의 2023. 7. 17. 변제기 약정" + ], + "key_dates": [ + "2015-09-17", + "2022-07-18", + "2023-07-17" + ], + "key_amounts": [ + "40,000,000원", + "10,000,000원" + ], + "key_parties": [ + "오국한", + "오민한", + "이경주" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 3 + }, + "claim_document_components": [ + { + "component_kind": "debt_acknowledgement", + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "각서 1항", + "excerpt": "신림동 상가는 명의만 오국한으로 되어 있을 뿐, 오민한 소유임을 확인" + } + }, + { + "component_kind": "underlying_claim", + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "금전소비대차계약서", + "excerpt": "오국한은 오민한으로부터 1,000만 원을 차용" + } + } + ], + "loan_components": [ + { + "loan_component_id": "E-003-LC-01", + "principal_amount": "10,000,000원", + "loan_date": "2022-07-18", + "due_date": "2023-07-17", + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "rate_text": null, + "rate_source_locator": null, + "interest_accrual_start_hint": null, + "collateral_object_hint": null, + "role_in_claim_hint": "오민한의 오국한에 대한 대여금 채권" + } + ], + "defense_assertions": [ + { + "assertion_kind": "명의신탁 주장", + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "각서 1항", + "excerpt": "신림동 상가는 명의만 오국한으로 되어 있을 뿐, 오민한 소유" + } + } + ] + }, + { + "evidence_index": "E-004", + "doc_uid": "DOC-004-감정평가서", + "title": "감정평가서", + "title_normalized": "감정평가서", + "doc_type": "기타", + "key_facts": [ + "신림로 상가 및 포천시 임야 감정평가", + "서울감정평가사사무소 작성", + "2024. 9. 13. 작성" + ], + "key_dates": [ + "2024. 9. 13." + ], + "key_amounts": [], + "key_parties": [ + "강용원", + "장수현" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 4 + }, + "valuation_object": { + "valuation_kind": "market_value", + "target_object": "신림로 상가 및 포천시 임야", + "basis_date_or_period": "2022. 1. 1. ~ 현재", + "value_rows": [ + { + "label": "신림로 상가 (2022.1.1~2022.12.31)", + "amount": "2억 3,000만 원", + "unit": "총액" + }, + { + "label": "신림로 상가 (2023.1.1~2023.12.31)", + "amount": "2억 4,000만 원", + "unit": "총액" + }, + { + "label": "신림로 상가 (2024.1.1~현재)", + "amount": "2억 5,000만 원", + "unit": "총액" + }, + { + "label": "포천시 임야 (2022.1.1~2022.12.31)", + "amount": "3,000만 원", + "unit": "총액" + }, + { + "label": "포천시 임야 (2023.1.1~2023.12.31)", + "amount": "3,000만 원", + "unit": "총액" + }, + { + "label": "포천시 임야 (2024.1.1~현재)", + "amount": "3,500만 원", + "unit": "총액" + } + ] + }, + "valuation_rows": [ + { + "label": "신림로 상가 2022", + "amount": "2억 3,000만 원", + "unit": "총액", + "legal_use_case": "가액배상 기준" + }, + { + "label": "신림로 상가 2023", + "amount": "2억 4,000만 원", + "unit": "총액", + "legal_use_case": "가액배상 기준" + }, + { + "label": "신림로 상가 2024", + "amount": "2억 5,000만 원", + "unit": "총액", + "legal_use_case": "가액배상 기준" + }, + { + "label": "포천시 임야 2022", + "amount": "3,000만 원", + "unit": "총액", + "legal_use_case": "가액배상 기준" + }, + { + "label": "포천시 임야 2023", + "amount": "3,000만 원", + "unit": "총액", + "legal_use_case": "가액배상 기준" + }, + { + "label": "포천시 임야 2024", + "amount": "3,500만 원", + "unit": "총액", + "legal_use_case": "가액배상 기준" + } + ] + }, + { + "evidence_index": "E-005", + "doc_uid": "DOC-005-감정평가의견서_성수동", + "title": "감정평가 의견서", + "title_normalized": "감정평가 의견서", + "doc_type": "거래기록", + "key_facts": [ + "대상 토지: 서울 성동구 성수동 256 대 330㎡", + "2024년도 차임 시세 감정 결과 회신" + ], + "key_dates": [ + "2024-12-29" + ], + "key_amounts": [ + "월 250만 원", + "월 400만 원", + "월 350만 원", + "월 500만 원" + ], + "key_parties": [ + "강용원", + "감정평가법인 광나루", + "이선욱" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 5 + }, + "valuation_object": { + "valuation_kind": "rent_value", + "target_object": "서울 성동구 성수동 256 대 330㎡", + "basis_date_or_period": "2024년", + "value_rows": [ + { + "label": "지상에 건물이 있는 경우, 보증금 3억 원", + "amount": "월 250만 원", + "unit": "월액" + }, + { + "label": "지상에 건물이 있는 경우, 보증금 없음", + "amount": "월 400만 원", + "unit": "월액" + }, + { + "label": "지상에 건물이 없는 경우, 보증금 3억 원", + "amount": "월 350만 원", + "unit": "월액" + }, + { + "label": "지상에 건물이 없는 경우, 보증금 없음", + "amount": "월 500만 원", + "unit": "월액" + } + ] + } + }, + { + "evidence_index": "E-006", + "doc_uid": "DOC-006-계약서_이문호_박성희", + "title": "계약서", + "title_normalized": "계약서", + "doc_type": "처분문서", + "key_facts": [ + "성수동 대지 임대차 계약", + "성수동 건물 매매 계약", + "2024. 11. 20. 체결" + ], + "key_dates": [ + "2024-11-20" + ], + "key_amounts": [ + "임대보증금 300,000,000원", + "차임 월 3,000,000원", + "매매대금 200,000,000원" + ], + "key_parties": [ + "이문호", + "박성희" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 6 + }, + "contract_components": [ + { + "component_id": "E-006-CC-01", + "component_kind": "lease", + "object_spec": "서울 성동구 성수동 256 대 330㎡", + "amount": "300,000,000원", + "date": "2024-11-20", + "payer_candidates": [ + "박성희" + ], + "recipient_candidates": [ + "이문호" + ], + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "1", + "excerpt": "임대목적물인 대지를 보증금 3억 원, 차임 월 300만 원" + } + }, + { + "component_id": "E-006-CC-02", + "component_kind": "sale", + "object_spec": "위 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "amount": "200,000,000원", + "date": "2024-11-20", + "payer_candidates": [ + "박성희" + ], + "recipient_candidates": [ + "이문호" + ], + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "2", + "excerpt": "상가를 대금 2억 원에 매도하기로 하고" + } + } + ] + }, + { + "evidence_index": "E-007", + "doc_uid": "DOC-007-공사도급계약서", + "title": "공사도급계약서", + "title_normalized": "공사도급계약서", + "doc_type": "처분문서", + "key_facts": [ + "평택시 서정3길 123 건물 신축 공사도급계약", + "총 공사대금 10억 원", + "乙 박광윤의 중도금 4억 원 영수증 포함" + ], + "key_dates": [ + "2021-05-01", + "2021-08-30", + "2021-11-10" + ], + "key_amounts": [ + "1,000,000,000원", + "300,000,000원", + "400,000,000원" + ], + "key_parties": [ + "김일동(도급인)", + "박광윤(수급인)" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 7 + }, + "contract_components": [ + { + "component_id": "E-007-CC-01", + "component_kind": "other", + "object_spec": "평택시 서정3길 123 건물 신축공사", + "amount": "1,000,000,000원", + "date": "2021-05-01", + "payer_candidates": [ + "김일동" + ], + "recipient_candidates": [ + "박광윤" + ], + "source_locator": { + "locator_type": "table_row", + "locator_hint": "공사대금", + "excerpt": "총 공사금: 10억 원" + } + } + ], + "claim_document_components": [ + { + "component_kind": "underlying_claim", + "principal_amount": "400,000,000원", + "role_in_claim_hint": "중도금 수령" + } + ] + }, + { + "evidence_index": "E-008", + "doc_uid": "DOC-008-내용증명", + "title": "내용증명", + "title_normalized": "내용증명", + "doc_type": "처분문서", + "key_facts": [ + "강용원이 오민한에게 채권 관련 최고", + "오국환의 신림동 상가 오민한 명의 이전 인지", + "사해행위취소 검토 및 반환 요청" + ], + "key_dates": [ + "2024-09-15" + ], + "key_amounts": [ + "200,000,000원", + "30,000,000원", + "40,000,000원", + "70,000,000원" + ], + "key_parties": [ + "강용원", + "오민한", + "오국환", + "오칠성", + "나일오" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 8 + }, + "notice_components": [ + { + "notice_id_proposed": "E-008-NT-01", + "notice_type": "기타", + "drafted_date": "2024-09-15", + "dispatch_date": "2024-09-15", + "sender_candidates": [ + "강용원" + ], + "receiver_candidates": [ + "오민한" + ], + "legal_effect_candidate_hint": "사해행위취소 및 소유권 반환 요구" + } + ] + }, + { + "evidence_index": "E-009", + "doc_uid": "DOC-009-내용증명", + "title": "내용증명", + "title_normalized": "내용증명", + "doc_type": "기타", + "key_facts": [ + "윤건우의 흑석동 상가 매매계약 해제 통보", + "임대차계약 해지 및 명도 요구" + ], + "key_dates": [ + "2024-11-29", + "2024-11-30" + ], + "key_amounts": [], + "key_parties": [ + "윤건우", + "양정숙" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 9 + }, + "notice_components": [ + { + "notice_id_proposed": "E-009-NT-01", + "notice_type": "기타", + "drafted_date": "2024-11-29", + "dispatch_date": "2024-11-29", + "arrival_date": "2024-11-30", + "received_date": "2024-11-30", + "legal_effect_date_candidate": "2024-11-30", + "requires_receipt_for_effect": true, + "sender_candidates": [ + "윤건우" + ], + "receiver_candidates": [ + "양정숙" + ], + "target_object_hint": "서울 동작구 흑석로 80 상가건물 201호", + "receipt_evidence_refs": [ + "우편물배달증명서" + ], + "legal_effect_candidate_hint": "임대차계약 해지 및 상가 인도 요구" + } + ] + }, + { + "evidence_index": "E-010", + "doc_uid": "DOC-010-내용증명에_대한_답신", + "title": "내용증명에 대한 답신", + "title_normalized": "내용증명에 대한 답신", + "doc_type": "통신기록", + "key_facts": [ + "오민한의 신림동 상가 사해행위 부인 주장", + "오민한의 5,000만 원 채권 상계 주장", + "2024. 9. 30. 내용증명 우편물 발송" + ], + "key_dates": [ + "2024. 9. 30.", + "2022. 7. 18." + ], + "key_amounts": [ + "4,000만 원", + "1,000만 원", + "5,000만 원" + ], + "key_parties": [ + "오민한", + "강용원" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 10 + }, + "notice_components": [ + { + "notice_id_proposed": "E-010-NT-01", + "notice_type": "기타", + "dispatch_date": "2024-09-30", + "sender_candidates": [ + "오민한" + ], + "receiver_candidates": [ + "강용원" + ] + } + ] + }, + { + "evidence_index": "E-011", + "doc_uid": "DOC-011-답변서_박광윤_to_강용원_양정숙", + "title": "답변서", + "title_normalized": "답변서", + "doc_type": "기타", + "key_facts": [ + "박광윤의 서정빌라 101호 유치권 행사 주장", + "건축주 김일동으로부터 공사잔금 3억 원 미지급 주장", + "임대료 및 본인/가족 거주 차임을 채무에 충당 주장" + ], + "key_dates": [ + "2024-08-29", + "2024-09-01", + "2024-10-31" + ], + "key_amounts": [ + "300,000,000원", + "2,000,000원" + ], + "key_parties": [ + "박광윤", + "강용원", + "양정숙", + "김일동" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 11 + } + }, + { + "evidence_index": "E-012", + "doc_uid": "DOC-012-등기말소_신청에_대한_답신", + "title": "등기말소 신청에 대한 답신", + "title_normalized": "등기말소 신청에 대한 답신", + "doc_type": "처분문서", + "key_facts": [ + "대한은행이 강용원의 근저당권 말소 신청 거부", + "경매절차 하자 없음을 주장", + "민사집행법 제267조에 따른 경매의 공신력 주장" + ], + "key_dates": [ + "2024-12-22" + ], + "key_amounts": [], + "key_parties": [ + "강용원", + "주식회사 대한은행" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 12 + }, + "notice_components": [ + { + "notice_id_proposed": "E-012-NT-01", + "notice_type": "기타", + "drafted_date": "2024-12-22", + "dispatch_date": "2024-12-22", + "sender_candidates": [ + "주식회사 대한은행" + ], + "receiver_candidates": [ + "강용원" + ], + "target_object_hint": "성수동 대지 근저당권", + "legal_effect_candidate_hint": "등기말소 신청 거부 통지" + } + ] + }, + { + "evidence_index": "E-013", + "doc_uid": "DOC-013-등기사항전부증명서_성수동", + "title": "등기사항전부증명서(말소사항 포함)-토지", + "title_normalized": "등기사항전부증명서(말소사항 포함)-토지", + "doc_type": "공문서", + "key_facts": [ + "성수동 대지(256)에 관한 등기사항전부증명서", + "2024.10.5. 경매로 인한 매각으로 이문호 소유권 취득", + "2024.10.5. 주식회사 대한은행 근저당권 설정" + ], + "key_dates": [ + "2022.12.8. (강용원/정유심 공유 지분 등기)", + "2024.10.5. (경매에 의한 소유권이전)", + "2024.10.5. (주식회사 대한은행 근저당권 설정)" + ], + "key_amounts": [ + "520,000,000원 (대한은행 채권최고액)" + ], + "key_parties": [ + "강용원", + "정유심", + "이문호", + "오민환", + "주식회사 대한은행" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 13 + }, + "registry_row_components": [ + { + "registry_ref": "REG-013-01", + "section": "갑구", + "row_seq": 1, + "receipt_no": "제1453호", + "event_date": "1997-03-15", + "purpose": "소유권이전", + "right_holder": "박재경" + }, + { + "registry_ref": "REG-013-02", + "section": "갑구", + "row_seq": 2, + "receipt_no": "제7026호", + "event_date": "2017-06-30", + "purpose": "소유권이전", + "right_holder": "강석우" + }, + { + "registry_ref": "REG-013-03", + "section": "갑구", + "row_seq": 3, + "receipt_no": "제11202호", + "event_date": "2022-11-08", + "purpose": "소유권이전", + "right_holder": "정유심, 강용원" + }, + { + "registry_ref": "REG-013-04", + "section": "갑구", + "row_seq": 4, + "receipt_no": "제7202호", + "event_date": "2023-04-06", + "purpose": "증여", + "right_holder": "강용원" + }, + { + "registry_ref": "REG-013-05", + "section": "갑구", + "row_seq": 5, + "receipt_no": "제12402호", + "event_date": "2024-07-21", + "purpose": "담보권실행을위한경매개시결정(일괄)", + "right_holder": "오민환" + }, + { + "registry_ref": "REG-013-06", + "section": "갑구", + "row_seq": 6, + "receipt_no": "제25797호", + "event_date": "2024-10-05", + "purpose": "담보권실행을 위한 경매로 인한 매각", + "right_holder": "이문호" + }, + { + "registry_ref": "REG-013-07", + "section": "을구", + "row_seq": 4, + "receipt_no": "제25799호", + "event_date": "2024-10-05", + "purpose": "근저당권설정", + "right_holder": "주식회사 대한은행", + "secured_debt_amount": "520,000,000원" + } + ] + }, + { + "evidence_index": "E-014", + "doc_uid": "DOC-014-등기사항전부증명서_말소사항_포함_건물", + "title": "등기사항전부증명서 (말소사항 포함) -건물", + "title_normalized": "등기사항전부증명서 (말소사항 포함) -건물", + "doc_type": "공문서", + "key_facts": [ + "2015.9.17 오국환 소유권이전", + "2023.3.17 오민한 소유권이전(매매)", + "2024.10.17 2번 근저당권 말소" + ], + "key_dates": [ + "2006-05-08", + "2015-09-17", + "2023-03-17", + "2024-10-17" + ], + "key_amounts": [ + "40,000,000원", + "150,000,000원", + "50,000,000원" + ], + "key_parties": [ + "이경주", + "오국환", + "오민한", + "나임오", + "강용일" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 14 + }, + "registry_row_components": [ + { + "registry_ref": "REG-014-01", + "section": "갑구", + "row_seq": 1, + "registry_purpose": "소유권보존", + "receipt_no": "제24759호", + "registry_date": "2006-05-08", + "rights_holder": "이경주" + }, + { + "registry_ref": "REG-014-02", + "section": "갑구", + "row_seq": 2, + "registry_purpose": "소유권이전", + "receipt_no": "제56457호", + "registry_date": "2015-09-17", + "rights_holder": "오국환" + }, + { + "registry_ref": "REG-014-03", + "section": "갑구", + "row_seq": 3, + "registry_purpose": "소유권이전", + "receipt_no": "제11593호", + "registry_date": "2023-03-17", + "rights_holder": "오민한" + }, + { + "registry_ref": "REG-014-04", + "section": "을구", + "row_seq": 4, + "registry_purpose": "근저당권설정", + "receipt_no": "제50099호", + "registry_date": "2021-08-01", + "rights_holder": "나임오" + }, + { + "registry_ref": "REG-014-05", + "section": "을구", + "row_seq": 5, + "registry_purpose": "근저당권설정", + "receipt_no": "제26597호", + "registry_date": "2022-04-18", + "rights_holder": "강용일" + }, + { + "registry_ref": "REG-014-06", + "section": "을구", + "row_seq": 6, + "registry_purpose": "근저당권설정등기말소", + "receipt_no": "제74586호", + "registry_date": "2024-10-17", + "rights_holder": null + } + ] + }, + { + "evidence_index": "E-015", + "doc_uid": "DOC-015-등기사항전부증명서_말소사항_포함_집합건물", + "title": "등기사항전부증명서(말소사항 포함)-집합건물", + "title_normalized": "등기사항전부증명서_말소사항_포함_집합건물", + "doc_type": "공문서", + "key_facts": [ + "경기도 평택시 서정3길 123 서정빌라 101호 등기부", + "2022. 10. 1. 강호연 소유권이전", + "2022. 9. 1. 김동국 가압류(말소)" + ], + "key_dates": [ + "2021-12-10", + "2021-12-30", + "2022-10-01" + ], + "key_amounts": [ + "14,000,000원" + ], + "key_parties": [ + "김일동", + "정남이", + "강호연", + "김동국" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 15 + }, + "registry_row_components": [ + { + "registry_ref": "REG-015-01", + "row_seq": 1, + "kind": "갑구", + "rank": "1", + "purpose": "소유권보존", + "receipt_date": "2021년12월10일", + "receipt_no": "제7233호", + "party_name": "김일동" + }, + { + "registry_ref": "REG-015-02", + "row_seq": 2, + "kind": "갑구", + "rank": "2", + "purpose": "소유권이전", + "receipt_date": "2021년12월30일", + "receipt_no": "제7871호", + "cause": "매매", + "party_name": "정남이" + }, + { + "registry_ref": "REG-015-03", + "row_seq": 3, + "kind": "갑구", + "rank": "3", + "purpose": "가압류", + "receipt_date": "2022년9월1일", + "receipt_no": "제11593호", + "cause": "수원지방법원 평택지원의 가압류결정(2022카단43434)", + "party_name": "김동국", + "amount": "14,000,000원" + }, + { + "registry_ref": "REG-015-04", + "row_seq": 4, + "kind": "갑구", + "rank": "4", + "purpose": "소유권이전", + "receipt_date": "2022년10월1일", + "receipt_no": "제12321호", + "party_name": "강호연" + } + ] + }, + { + "evidence_index": "E-016", + "doc_uid": "DOC-016-별지_목록", + "title": "별지 목록", + "title_normalized": "별지 목록", + "doc_type": "공문서", + "key_facts": [ + "성수동 대지 및 건물 목록", + "신림동 상가 목록", + "평택시 빌라 및 대지권 목록", + "흑석동 상가 및 대지권 목록" + ], + "key_dates": [], + "key_amounts": [], + "key_parties": [], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 16 + }, + "schedule_items": [ + { + "schedule_ref": "SCH-016-01", + "canonical_label_candidate": "1. 서울 성동구 성수동 256 대 330㎡", + "usable_in_relief": "yes" + }, + { + "schedule_ref": "SCH-016-02", + "canonical_label_candidate": "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "usable_in_relief": "yes" + }, + { + "schedule_ref": "SCH-016-03", + "canonical_label_candidate": "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "usable_in_relief": "yes" + }, + { + "schedule_ref": "SCH-016-04", + "canonical_label_candidate": "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "usable_in_relief": "yes" + }, + { + "schedule_ref": "SCH-016-05", + "canonical_label_candidate": "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물", + "usable_in_relief": "yes" + } + ] + }, + { + "evidence_index": "E-017", + "doc_uid": "DOC-017-부동산_매매계약서", + "title": "부동산 매매계약서", + "title_normalized": "부동산 매매계약서", + "doc_type": "처분문서", + "key_facts": [ + "흑석동 상가 매매계약(2023.10.1.)", + "매매대금 10억 원", + "윤건우(매도인), 이수인(매수인)" + ], + "key_dates": [ + "2023-10-01", + "2024-01-01", + "2024-05-01" + ], + "key_amounts": [ + "1,000,000,000원", + "200,000,000원", + "500,000,000원", + "300,000,000원" + ], + "key_parties": [ + "윤건우", + "이수인", + "양재혁" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 17 + }, + "contract_components": [ + { + "component_id": "E-017-CC-01", + "component_kind": "sale", + "object_spec": "흑석동 상가(201호)", + "amount": "1,000,000,000원", + "date": "2023-10-01", + "payer_candidates": [ + "이수인" + ], + "recipient_candidates": [ + "윤건우" + ], + "source_locator": { + "locator_type": "table_row", + "locator_hint": "계약내용", + "excerpt": "매매대금 10억 원" + } + }, + { + "component_id": "E-017-CC-02", + "component_kind": "delivery", + "object_spec": "흑석동 상가(201호)", + "possession_recipient_candidates": [ + "이수인" + ], + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "특약사항", + "excerpt": "중도금을 지급받은 날 매수인에게 위 부동산을 인도한다." + } + } + ] + }, + { + "evidence_index": "E-018", + "doc_uid": "DOC-018-사_업_자_등_록_증", + "title": "사업자등록증", + "title_normalized": "사업자등록증", + "doc_type": "공문서", + "key_facts": [ + "양정숙 명의 맛나식당 사업자등록", + "개업일 2024년 2월 1일", + "사업장소재지 서울 동작구 흑석로 80, 201호" + ], + "key_dates": [ + "2024-02-01", + "2024-02-02" + ], + "key_amounts": [], + "key_parties": [ + "양정숙", + "동작 세무서장" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 18 + } + }, + { + "evidence_index": "E-019", + "doc_uid": "DOC-019-소멸청구_통지서", + "title": "소멸청구 통지서", + "title_normalized": "소멸청구 통지서", + "doc_type": "기타", + "key_facts": [ + "유치권 소멸청구 통지", + "평택시 빌라 인도 요구", + "사용수익 반환 요구" + ], + "key_dates": [ + "2024-08-29" + ], + "key_amounts": [], + "key_parties": [ + "강용원", + "양정숙", + "박광윤" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 19 + }, + "notice_components": [ + { + "notice_id_proposed": "E-019-NT-01", + "notice_type": "유치권소멸청구통지", + "drafted_date": "2024-08-29", + "dispatch_date": "2024-08-29", + "sender_candidates": [ + "강용원", + "양정숙" + ], + "receiver_candidates": [ + "박광윤" + ], + "target_object_hint": "평택시 서정3길 123, 101호(서정동, 서정빌라)", + "requires_receipt_for_effect": true + } + ] + }, + { + "evidence_index": "E-020", + "doc_uid": "DOC-020-영업양도계약서_김선웅_오민한", + "title": "영업양도 계약서", + "title_normalized": "영업양도 계약서", + "doc_type": "처분문서", + "key_facts": [ + "김선웅이 오민한에게 해드림 영업을 양도함", + "계약일 2024. 7. 5.", + "양도대금 2억 원" + ], + "key_dates": [ + "2024. 7. 5." + ], + "key_amounts": [ + "200,000,000원" + ], + "key_parties": [ + "김선웅", + "오민한" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 20 + }, + "business_transfer_components": [ + { + "business_name": "해드림(SUN-DREAM)", + "transferor_candidates": [ + "김선웅" + ], + "transferee_candidates": [ + "오민한" + ], + "business_assets_transferred": [ + "임차권", + "설비", + "비품", + "자재", + "공구" + ], + "trade_name_continuation_clause": "제4조: 을은 상호를 계속 사용할 수 있음", + "customer_or_employee_succession_clause": "제3조: 고객관계, 고용관계 승계", + "pre_transfer_debt_exclusion_clause": "제5조: 을은 계약일 전 발생 채무 책임지지 아니함" + } + ] + }, + { + "evidence_index": "E-021", + "doc_uid": "DOC-021-이행각서", + "title": "이행각서", + "title_normalized": "이행각서", + "doc_type": "처분문서", + "key_facts": [ + "김선웅이 강용원에게 인테리어 자재 물품대금 채무 변제 약속", + "2021. 6. 5. 자 물품대금 채무", + "2024. 8. 말까지 변제 약속" + ], + "key_dates": [ + "2021-06-05", + "2024-08-05", + "2024-08-31" + ], + "key_amounts": [], + "key_parties": [ + "김선웅", + "강용원" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 21 + }, + "claim_document_components": [ + { + "component_kind": "debt_acknowledgement", + "drafted_date": "2024-08-05", + "new_due_date": "2024-08-31" + } + ] + }, + { + "evidence_index": "E-022", + "doc_uid": "DOC-022-임대차계약서_이수인_양정숙", + "title": "임대차계약서", + "title_normalized": "임대차계약서", + "doc_type": "처분문서", + "key_facts": [ + "이수인(임대인)과 양정숙(임차인) 간 흑석동 상가 임대차계약 체결", + "2024. 2. 1. 계약 체결 및 보증금 지급 완료", + "보증금 3억 원, 월 차임 500만 원" + ], + "key_dates": [ + "2024-02-01" + ], + "key_amounts": [ + "300,000,000원", + "5,000,000원" + ], + "key_parties": [ + "이수인", + "양정숙" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 22 + }, + "contract_components": [ + { + "component_id": "E-022-CC-01", + "component_kind": "lease", + "object_spec": "흑석동 상가 201호", + "amount": "300,000,000원", + "date": "2024-02-01", + "payer_candidates": [ + "양정숙" + ], + "recipient_candidates": [ + "이수인" + ], + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "제1조, 제3조", + "excerpt": "흑석동 상가 201호, 보증금 3억" + } + } + ] + }, + { + "evidence_index": "E-023", + "doc_uid": "DOC-023-임료시세확인서", + "title": "임료시세확인서", + "title_normalized": "임료시세확인서", + "doc_type": "기타", + "key_facts": [ + "평택시 서정빌라 월 차임 시세 확인", + "보증금 없는 월세 200만 원", + "기준일 2022. 10.경" + ], + "key_dates": [ + "2022. 10.경", + "2024. 12. 23." + ], + "key_amounts": [ + "월세 200만 원" + ], + "key_parties": [ + "김민식" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 23 + }, + "valuation_object": { + "valuation_kind": "rent_value", + "target_object": "경기도 평택시 서정3길 123(서정동, 서정빌라)", + "basis_date_or_period": "2022. 10.경", + "value_rows": [ + { + "label": "보증금 없이", + "amount": "월세 200만 원", + "unit": "월액" + } + ] + } + }, + { + "evidence_index": "E-024", + "doc_uid": "DOC-024-차용증", + "title": "차용증", + "title_normalized": "차용증", + "doc_type": "처분문서", + "key_facts": [ + "2022. 4. 18.자 1억 원 차용증", + "2022. 5. 18.자 1억 원 차용증" + ], + "key_dates": [ + "2022-04-18", + "2022-05-18", + "2022-12-17", + "2023-03-17" + ], + "key_amounts": [ + "100,000,000원" + ], + "key_parties": [ + "강용원", + "오국한" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 24 + }, + "claim_document_components": [ + { + "component_kind": "underlying_claim", + "principal_amount": "100,000,000원", + "loan_date": "2022-04-18", + "due_date": "2022-12-17" + }, + { + "component_kind": "underlying_claim", + "principal_amount": "100,000,000원", + "loan_date": "2022-05-18", + "due_date": "2023-03-17" + } + ], + "loan_components": [ + { + "loan_component_id": "E-024-LC-01", + "principal_amount": "100,000,000원", + "loan_date": "2022-04-18", + "due_date": "2022-12-17", + "agreed_interest_rate": "월 1%", + "agreed_delay_damage_rate": null, + "rate_text": "월 1%" + }, + { + "loan_component_id": "E-024-LC-02", + "principal_amount": "100,000,000원", + "loan_date": "2022-05-18", + "due_date": "2023-03-17", + "agreed_interest_rate": "월 1%", + "agreed_delay_damage_rate": null, + "rate_text": "월 1%" + } + ] + }, + { + "evidence_index": "E-025", + "doc_uid": "DOC-025-차용증", + "title": "차용증", + "title_normalized": "차용증", + "doc_type": "처분문서", + "key_facts": [ + "강용원이 오민한에게 2억 원을 차용함", + "2024. 1. 5. 변제기", + "성수동 대지 5분의 2 지분에 1순위 근저당권 설정 약정" + ], + "key_dates": [ + "2023-01-06" + ], + "key_amounts": [ + "200,000,000원" + ], + "key_parties": [ + "오민한", + "강용원" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 25 + }, + "claim_document_components": [ + { + "component_kind": "underlying_claim", + "principal_amount": "200,000,000원", + "due_date": "2024-01-05", + "agreed_interest_rate": "연 10%", + "rate_text": "연 10%" + } + ], + "secured_debt_components": [ + { + "collateral_object_hint": "서울 성동구 성수동 256 대 330㎡의 5분의 2 지분", + "due_date": "2024-01-05" + } + ] + }, + { + "evidence_index": "E-026", + "doc_uid": "DOC-026-차용증", + "title": "차용증", + "title_normalized": "차 용 증", + "doc_type": "처분문서", + "key_facts": [ + "강용원의 오민한에 대한 300,000,000원 차용", + "서울 성동구 성수동 256 대 330㎡ 5/3 지분 근저당권 설정 약정" + ], + "key_dates": [ + "2023. 7. 6.", + "2024. 7. 5." + ], + "key_amounts": [ + "300,000,000원" + ], + "key_parties": [ + "강용원", + "오민한" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 26 + }, + "loan_components": [ + { + "loan_component_id": "E-026-LC-01", + "principal_amount": "300,000,000원", + "loan_date": "2023-07-06", + "due_date": "2024-07-05", + "agreed_interest_rate": "연 15%", + "collateral_object_hint": "서울 성동구 성수동 256 대 330㎡의 5분의 3 지분", + "role_in_claim_hint": "차용" + } + ], + "secured_debt_components": [ + { + "due_date": "2024-07-05", + "agreed_interest_rate": "연 15%" + } + ] + }, + { + "evidence_index": "E-027", + "doc_uid": "DOC-027-최고_통지서", + "title": "최고 통지서", + "title_normalized": "최고 통지서", + "doc_type": "처분문서", + "key_facts": [ + "2021. 6. 5. 강용원이 김선웅에게 2억 원 상당 인테리어 자재 납품", + "2024. 5. 4. 강용원이 김선웅에게 대금 지급 최고 통지서 발송", + "2024. 5. 6. 김선웅에게 최고 통지서 도달" + ], + "key_dates": [ + "2021-06-05", + "2024-05-04", + "2024-05-06" + ], + "key_amounts": [ + "200,000,000원" + ], + "key_parties": [ + "강용원", + "김선웅" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 27 + }, + "claim_document_components": [ + { + "component_kind": "demand_notice", + "dispatch_date": "2024-05-04", + "arrival_date": "2024-05-06" + } + ], + "notice_components": [ + { + "notice_id_proposed": "E-027-NT-01", + "notice_type": "최고", + "dispatch_date": "2024-05-04", + "arrival_date": "2024-05-06", + "sender_candidates": [ + "강용원" + ], + "receiver_candidates": [ + "김선웅" + ], + "receipt_evidence_refs": [ + "우편물 배달 증명서" + ] + } + ] + }, + { + "evidence_index": "E-028", + "doc_uid": "DOC-028-통지서", + "title": "통지서", + "title_normalized": "통지서", + "doc_type": "처분문서", + "key_facts": [ + "오민한의 김선웅 채무 책임 부인", + "오민한의 채무 시효 소멸 주장", + "오민한의 채무 인수 당시 책임 배제 약정 주장" + ], + "key_dates": [ + "2024-12-27" + ], + "key_amounts": [], + "key_parties": [ + "오민한", + "강용원", + "김선웅" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 28 + }, + "notice_components": [ + { + "notice_id_proposed": "E-028-NT-01", + "notice_type": "기타", + "dispatch_date": "2024-12-27", + "sender_candidates": [ + "오민한" + ], + "receiver_candidates": [ + "강용원" + ] + } + ], + "defense_assertions": [ + "김선웅의 채무 인수 당시 책임 배제 약정 존재", + "물품대금 채무 시효 소멸", + "본인에게 채무 책임 없음" + ] + }, + { + "evidence_index": "E-029", + "doc_uid": "DOC-029-통지서_이수인_윤건우", + "title": "통지서_이수인_윤건우", + "title_normalized": "통지서 이수인 윤건우", + "doc_type": "통신기록", + "key_facts": [ + "이수인이 윤건우에게 흑석동 상가 매매계약 관련 매매대금 감액 및 제3자 명의 등기 요청" + ], + "key_dates": [ + "2024-03-30" + ], + "key_amounts": [], + "key_parties": [ + "이수인", + "윤건우" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 29 + }, + "notice_components": [ + { + "notice_id_proposed": "E-029-NT-01", + "notice_type": "기타", + "dispatch_date": "2024-03-30", + "sender_candidates": [ + "이수인" + ], + "receiver_candidates": [ + "윤건우" + ], + "target_object_hint": "흑석동 상가" + } + ] + }, + { + "evidence_index": "E-030", + "doc_uid": "DOC-030-해제_통지서", + "title": "해제 통지서", + "title_normalized": "해제 통지서", + "doc_type": "처분문서", + "key_facts": [ + "2023.10.1. 흑석동 상가 매매계약 체결", + "2024.5.15. 윤건우가 이수인에게 매매계약 해제 통지", + "2024.5.16. 해제 통지서 수령" + ], + "key_dates": [ + "2023-10-01", + "2024-05-15", + "2024-05-16" + ], + "key_parties": [ + "윤건우", + "이수인" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 30 + }, + "notice_components": [ + { + "notice_id_proposed": "E-030-NT-01", + "notice_type": "해제통지", + "dispatch_date": "2024-05-15", + "arrival_date": "2024-05-16", + "sender_candidates": [ + "윤건우" + ], + "receiver_candidates": [ + "이수인" + ], + "target_object_hint": "흑석동 상가", + "legal_effect_date_candidate": "2024-05-16" + } + ] + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-001.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-001.json new file mode 100644 index 00000000..ee691605 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-001.json @@ -0,0 +1,68 @@ +{ + "evidence_index_proposed": "E-001", + "doc_uid": "DOC-001-가정법원심판_강호연", + "title": "가정법원심판_강호연", + "title_normalized": "가정법원심판_강호연", + "doc_type": "판결/결정", + "key_facts": [ + "2024느단52199 상속포기", + "강호연 사망(2024. 8. 10.)", + "상속포기 신고 수리(2024. 9. 11.)" + ], + "key_dates": [ + "2024. 8. 10.", + "2024. 9. 11.", + "2024. 9. 20." + ], + "key_amounts": [], + "key_parties": [ + "강형수", + "강지수", + "강호연" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 1 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "succession_doc", + "status_doc" + ], + "multi_event_registry": null, + "contract_components": [], + "valuation_object": null, + "legal_calculation_object": null, + "legal_calculation_objects": [], + "claim_document_components": [], + "business_transfer_components": [], + "registry_row_components": [], + "secured_debt_components": [], + "valuation_rows": [], + "mixed_contract_payment_semantics": {}, + "loan_components": [], + "valuation_matrix": {}, + "registry_row_role_map": {}, + "defense_assertions": [], + "admission_components": [], + "defense_components": [], + "notice_components": [], + "party_assertion_components": [], + "schedule_items": [], + "registry_object_display": {}, + "succession_document_components": { + "decedent": "강호연", + "death_date": "2024-08-10", + "succession_refusers": [ + "강형수", + "강지수" + ], + "filing_date": "2024-09-11", + "decision_date": "2024-09-20" + }, + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-002.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-002.json new file mode 100644 index 00000000..7c5058bb --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-002.json @@ -0,0 +1,49 @@ +{ + "evidence_index_proposed": "E-002", + "doc_uid": "DOC-002-가족관계증명서_폐쇄", + "title": "가족관계증명서 [폐쇄]", + "title_normalized": "가족관계증명서 [폐쇄]", + "doc_type": "공문서", + "key_facts": [ + "강호연 2024. 8. 10. 사망", + "강용원(부), 양정숙(배우자) 유족 확인" + ], + "key_dates": [ + "1973-05-19", + "2024-08-10", + "2024-12-27" + ], + "key_amounts": [], + "key_parties": [ + "강호연", + "강용원", + "양정숙" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 2 + }, + "doc_semantic_flags": [ + "status_doc", + "succession_doc" + ], + "succession_document_components": { + "decedent": "강호연", + "date_of_death": "2024-08-10", + "spouse_candidates": [ + "양정숙" + ], + "lineal_descendant_candidates": [ + "강형수", + "강지수" + ], + "lineal_ascendant_candidates": [ + "강용원" + ] + }, + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-003.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-003.json new file mode 100644 index 00000000..76411604 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-003.json @@ -0,0 +1,82 @@ +{ + "evidence_index_proposed": "E-003", + "doc_uid": "DOC-003-각서_금전대차소비계약서_오국한_오민한", + "title": "각서 및 금전소비대차계약서", + "title_normalized": "각서_및_금전소비대차계약서", + "doc_type": "처분문서", + "key_facts": [ + "오국한이 오민한의 자금으로 신림동 상가를 매수하여 명의신탁", + "오국한이 오민한으로부터 1,000만 원을 차용(2022. 7. 18.)", + "오국한의 2023. 7. 17. 변제기 약정" + ], + "key_dates": [ + "2015-09-17", + "2022-07-18", + "2023-07-17" + ], + "key_amounts": [ + "40,000,000원", + "10,000,000원" + ], + "key_parties": [ + "오국한", + "오민한", + "이경주" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 3 + }, + "doc_semantic_flags": [ + "claim_doc", + "defense_doc" + ], + "claim_document_components": [ + { + "component_kind": "debt_acknowledgement", + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "각서 1항", + "excerpt": "신림동 상가는 명의만 오국한으로 되어 있을 뿐, 오민한 소유임을 확인" + } + }, + { + "component_kind": "underlying_claim", + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "금전소비대차계약서", + "excerpt": "오국한은 오민한으로부터 1,000만 원을 차용" + } + } + ], + "loan_components": [ + { + "loan_component_id": "E-003-LC-01", + "principal_amount": "10,000,000원", + "loan_date": "2022-07-18", + "due_date": "2023-07-17", + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "rate_text": null, + "rate_source_locator": null, + "interest_accrual_start_hint": null, + "collateral_object_hint": null, + "role_in_claim_hint": "오민한의 오국한에 대한 대여금 채권" + } + ], + "defense_assertions": [ + { + "assertion_kind": "명의신탁 주장", + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "각서 1항", + "excerpt": "신림동 상가는 명의만 오국한으로 되어 있을 뿐, 오민한 소유" + } + } + ], + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-004.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-004.json new file mode 100644 index 00000000..e9a969f3 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-004.json @@ -0,0 +1,143 @@ +{ + "evidence_index_proposed": "E-004", + "doc_uid": "DOC-004-감정평가서", + "title": "감정평가서", + "title_normalized": "감정평가서", + "doc_type": "기타", + "key_facts": [ + "신림로 상가 및 포천시 임야 감정평가", + "서울감정평가사사무소 작성", + "2024. 9. 13. 작성" + ], + "key_dates": [ + "2024. 9. 13." + ], + "key_amounts": [], + "key_parties": [ + "강용원", + "장수현" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 4 + }, + "doc_semantic_flags": [ + "valuation_doc" + ], + "valuation_object": { + "valuation_kind": "market_value", + "target_object": "신림로 상가 및 포천시 임야", + "basis_date_or_period": "2022. 1. 1. ~ 현재", + "value_rows": [ + { + "label": "신림로 상가 (2022.1.1~2022.12.31)", + "amount": "2억 3,000만 원", + "unit": "총액" + }, + { + "label": "신림로 상가 (2023.1.1~2023.12.31)", + "amount": "2억 4,000만 원", + "unit": "총액" + }, + { + "label": "신림로 상가 (2024.1.1~현재)", + "amount": "2억 5,000만 원", + "unit": "총액" + }, + { + "label": "포천시 임야 (2022.1.1~2022.12.31)", + "amount": "3,000만 원", + "unit": "총액" + }, + { + "label": "포천시 임야 (2023.1.1~2023.12.31)", + "amount": "3,000만 원", + "unit": "총액" + }, + { + "label": "포천시 임야 (2024.1.1~현재)", + "amount": "3,500만 원", + "unit": "총액" + } + ] + }, + "valuation_rows": [ + { + "label": "신림로 상가 2022", + "amount": "2억 3,000만 원", + "unit": "총액", + "legal_use_case": "가액배상 기준" + }, + { + "label": "신림로 상가 2023", + "amount": "2억 4,000만 원", + "unit": "총액", + "legal_use_case": "가액배상 기준" + }, + { + "label": "신림로 상가 2024", + "amount": "2억 5,000만 원", + "unit": "총액", + "legal_use_case": "가액배상 기준" + }, + { + "label": "포천시 임야 2022", + "amount": "3,000만 원", + "unit": "총액", + "legal_use_case": "가액배상 기준" + }, + { + "label": "포천시 임야 2023", + "amount": "3,000만 원", + "unit": "총액", + "legal_use_case": "가액배상 기준" + }, + { + "label": "포천시 임야 2024", + "amount": "3,500만 원", + "unit": "총액", + "legal_use_case": "가액배상 기준" + } + ], + "valuation_matrix": { + "신림로_상가": [ + { + "period": "2022. 1. 1. ~ 2022. 12. 31.", + "amount": "2억 3,000만 원", + "legal_uses": "가액배상 기준" + }, + { + "period": "2023. 1. 1. ~ 2023. 12. 31.", + "amount": "2억 4,000만 원", + "legal_uses": "가액배상 기준" + }, + { + "period": "2024. 1. 1. ~ 현재", + "amount": "2억 5,000만 원", + "legal_uses": "가액배상 기준" + } + ], + "포천시_임야": [ + { + "period": "2022. 1. 1. ~ 2022. 12. 31.", + "amount": "3,000만 원", + "legal_uses": "가액배상 기준" + }, + { + "period": "2023. 1. 1. ~ 2023. 12. 31.", + "amount": "3,000만 원", + "legal_uses": "가액배상 기준" + }, + { + "period": "2024. 1. 1. ~ 현재", + "amount": "3,500만 원", + "legal_uses": "가액배상 기준" + } + ] + }, + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-005.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-005.json new file mode 100644 index 00000000..d13543f0 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-005.json @@ -0,0 +1,64 @@ +{ + "evidence_index_proposed": "E-005", + "doc_uid": "DOC-005-감정평가의견서_성수동", + "title": "감정평가 의견서", + "title_normalized": "감정평가 의견서", + "doc_type": "거래기록", + "key_facts": [ + "대상 토지: 서울 성동구 성수동 256 대 330㎡", + "2024년도 차임 시세 감정 결과 회신" + ], + "key_dates": [ + "2024-12-29" + ], + "key_amounts": [ + "월 250만 원", + "월 400만 원", + "월 350만 원", + "월 500만 원" + ], + "key_parties": [ + "강용원", + "감정평가법인 광나루", + "이선욱" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 5 + }, + "doc_semantic_flags": [ + "valuation_doc" + ], + "valuation_object": { + "valuation_kind": "rent_value", + "target_object": "서울 성동구 성수동 256 대 330㎡", + "basis_date_or_period": "2024년", + "value_rows": [ + { + "label": "지상에 건물이 있는 경우, 보증금 3억 원", + "amount": "월 250만 원", + "unit": "월액" + }, + { + "label": "지상에 건물이 있는 경우, 보증금 없음", + "amount": "월 400만 원", + "unit": "월액" + }, + { + "label": "지상에 건물이 없는 경우, 보증금 3억 원", + "amount": "월 350만 원", + "unit": "월액" + }, + { + "label": "지상에 건물이 없는 경우, 보증금 없음", + "amount": "월 500만 원", + "unit": "월액" + } + ] + }, + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-006.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-006.json new file mode 100644 index 00000000..0791a5d8 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-006.json @@ -0,0 +1,76 @@ +{ + "evidence_index_proposed": "E-006", + "doc_uid": "DOC-006-계약서_이문호_박성희", + "title": "계약서", + "title_normalized": "계약서", + "doc_type": "처분문서", + "key_facts": [ + "성수동 대지 임대차 계약", + "성수동 건물 매매 계약", + "2024. 11. 20. 체결" + ], + "key_dates": [ + "2024-11-20" + ], + "key_amounts": [ + "임대보증금 300,000,000원", + "차임 월 3,000,000원", + "매매대금 200,000,000원" + ], + "key_parties": [ + "이문호", + "박성희" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 6 + }, + "doc_semantic_flags": [ + "mixed_contract_doc", + "schedule_doc", + "relief_object_reference_doc" + ], + "contract_components": [ + { + "component_id": "E-006-CC-01", + "component_kind": "lease", + "object_spec": "서울 성동구 성수동 256 대 330㎡", + "amount": "300,000,000원", + "date": "2024-11-20", + "payer_candidates": [ + "박성희" + ], + "recipient_candidates": [ + "이문호" + ], + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "1", + "excerpt": "임대목적물인 대지를 보증금 3억 원, 차임 월 300만 원" + } + }, + { + "component_id": "E-006-CC-02", + "component_kind": "sale", + "object_spec": "위 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "amount": "200,000,000원", + "date": "2024-11-20", + "payer_candidates": [ + "박성희" + ], + "recipient_candidates": [ + "이문호" + ], + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "2", + "excerpt": "상가를 대금 2억 원에 매도하기로 하고" + } + } + ], + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-007.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-007.json new file mode 100644 index 00000000..0af367d4 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-007.json @@ -0,0 +1,67 @@ +{ + "evidence_index_proposed": "E-007", + "doc_uid": "DOC-007-공사도급계약서", + "title": "공사도급계약서", + "title_normalized": "공사도급계약서", + "doc_type": "처분문서", + "key_facts": [ + "평택시 서정3길 123 건물 신축 공사도급계약", + "총 공사대금 10억 원", + "乙 박광윤의 중도금 4억 원 영수증 포함" + ], + "key_dates": [ + "2021-05-01", + "2021-08-30", + "2021-11-10" + ], + "key_amounts": [ + "1,000,000,000원", + "300,000,000원", + "400,000,000원" + ], + "key_parties": [ + "김일동(도급인)", + "박광윤(수급인)" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 7 + }, + "doc_semantic_flags": [ + "claim_doc", + "schedule_doc", + "relief_object_reference_doc" + ], + "contract_components": [ + { + "component_id": "E-007-CC-01", + "component_kind": "other", + "object_spec": "평택시 서정3길 123 건물 신축공사", + "amount": "1,000,000,000원", + "date": "2021-05-01", + "payer_candidates": [ + "김일동" + ], + "recipient_candidates": [ + "박광윤" + ], + "source_locator": { + "locator_type": "table_row", + "locator_hint": "공사대금", + "excerpt": "총 공사금: 10억 원" + } + } + ], + "claim_document_components": [ + { + "component_kind": "underlying_claim", + "principal_amount": "400,000,000원", + "role_in_claim_hint": "중도금 수령" + } + ], + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-008.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-008.json new file mode 100644 index 00000000..463f48bf --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-008.json @@ -0,0 +1,57 @@ +{ + "evidence_index_proposed": "E-008", + "doc_uid": "DOC-008-내용증명", + "title": "내용증명", + "title_normalized": "내용증명", + "doc_type": "처분문서", + "key_facts": [ + "강용원이 오민한에게 채권 관련 최고", + "오국환의 신림동 상가 오민한 명의 이전 인지", + "사해행위취소 검토 및 반환 요청" + ], + "key_dates": [ + "2024-09-15" + ], + "key_amounts": [ + "200,000,000원", + "30,000,000원", + "40,000,000원", + "70,000,000원" + ], + "key_parties": [ + "강용원", + "오민한", + "오국환", + "오칠성", + "나일오" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 8 + }, + "doc_semantic_flags": [ + "notice_doc", + "claim_doc", + "defense_doc" + ], + "notice_components": [ + { + "notice_id_proposed": "E-008-NT-01", + "notice_type": "기타", + "drafted_date": "2024-09-15", + "dispatch_date": "2024-09-15", + "sender_candidates": [ + "강용원" + ], + "receiver_candidates": [ + "오민한" + ], + "legal_effect_candidate_hint": "사해행위취소 및 소유권 반환 요구" + } + ], + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-009.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-009.json new file mode 100644 index 00000000..bdccd0ec --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-009.json @@ -0,0 +1,79 @@ +{ + "evidence_index_proposed": "E-009", + "doc_uid": "DOC-009-내용증명", + "title": "내용증명", + "title_normalized": "내용증명", + "doc_type": "기타", + "key_facts": [ + "윤건우의 흑석동 상가 매매계약 해제 통보", + "임대차계약 해지 및 명도 요구" + ], + "key_dates": [ + "2024-11-29", + "2024-11-30" + ], + "key_amounts": [], + "key_parties": [ + "윤건우", + "양정숙" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 9 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "notice_doc", + "status_doc" + ], + "multi_event_registry": null, + "contract_components": [], + "valuation_object": null, + "legal_calculation_object": null, + "legal_calculation_objects": [], + "claim_document_components": [], + "business_transfer_components": [], + "registry_row_components": [], + "secured_debt_components": [], + "valuation_rows": [], + "mixed_contract_payment_semantics": {}, + "loan_components": [], + "valuation_matrix": {}, + "registry_row_role_map": {}, + "defense_assertions": [], + "admission_components": [], + "defense_components": [], + "notice_components": [ + { + "notice_id_proposed": "E-009-NT-01", + "notice_type": "기타", + "drafted_date": "2024-11-29", + "dispatch_date": "2024-11-29", + "arrival_date": "2024-11-30", + "received_date": "2024-11-30", + "legal_effect_date_candidate": "2024-11-30", + "requires_receipt_for_effect": true, + "sender_candidates": [ + "윤건우" + ], + "receiver_candidates": [ + "양정숙" + ], + "target_object_hint": "서울 동작구 흑석로 80 상가건물 201호", + "receipt_evidence_refs": [ + "우편물배달증명서" + ], + "recipient_admission_text": null, + "legal_effect_candidate_hint": "임대차계약 해지 및 상가 인도 요구" + } + ], + "party_assertion_components": [], + "schedule_items": [], + "registry_object_display": {}, + "succession_document_components": null, + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-010.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-010.json new file mode 100644 index 00000000..7d7cf0b9 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-010.json @@ -0,0 +1,85 @@ +{ + "evidence_index_proposed": "E-010", + "doc_uid": "DOC-010-내용증명에_대한_답신", + "title": "내용증명에 대한 답신", + "title_normalized": "내용증명에 대한 답신", + "doc_type": "통신기록", + "key_facts": [ + "오민한의 신림동 상가 사해행위 부인 주장", + "오민한의 5,000만 원 채권 상계 주장", + "2024. 9. 30. 내용증명 우편물 발송" + ], + "key_dates": [ + "2024. 9. 30.", + "2022. 7. 18." + ], + "key_amounts": [ + "4,000만 원", + "1,000만 원", + "5,000만 원" + ], + "key_parties": [ + "오민한", + "강용원" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 10 + }, + "doc_semantic_flags": [ + "response_doc", + "notice_doc", + "admission_doc", + "defense_doc" + ], + "admission_components": [ + { + "component_kind": "other", + "excerpt": "귀하가 보낸 이행최고서는 2024. 9. 20. 잘 받아 보았습니다." + } + ], + "defense_components": [ + { + "component_kind": "other", + "assertion": "신림동 상가는 애초부터 본인 소유", + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "제2항" + } + }, + { + "component_kind": "other", + "assertion": "사해행위에 해당하지 않음", + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "제3항" + } + }, + { + "component_kind": "other", + "assertion": "본인 채권 5,000만 원으로 상계하겠음", + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "제6항" + } + } + ], + "notice_components": [ + { + "notice_id_proposed": "E-010-NT-01", + "notice_type": "기타", + "dispatch_date": "2024-09-30", + "sender_candidates": [ + "오민한" + ], + "receiver_candidates": [ + "강용원" + ] + } + ], + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-011.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-011.json new file mode 100644 index 00000000..c0a51379 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-011.json @@ -0,0 +1,60 @@ +{ + "evidence_index_proposed": "E-011", + "doc_uid": "DOC-011-답변서_박광윤_to_강용원_양정숙", + "title": "답변서", + "title_normalized": "답변서", + "doc_type": "기타", + "key_facts": [ + "박광윤의 서정빌라 101호 유치권 행사 주장", + "건축주 김일동으로부터 공사잔금 3억 원 미지급 주장", + "임대료 및 본인/가족 거주 차임을 채무에 충당 주장" + ], + "key_dates": [ + "2024-08-29", + "2024-09-01", + "2024-10-31" + ], + "key_amounts": [ + "300,000,000원", + "2,000,000원" + ], + "key_parties": [ + "박광윤", + "강용원", + "양정숙", + "김일동" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 11 + }, + "doc_semantic_flags": [ + "response_doc", + "status_doc", + "defense_doc", + "notice_receipt_admission_doc", + "lien_assertion_doc" + ], + "admission_components": [ + { + "notice_type": "유치권소멸청구통지", + "received_date": "2024-09-01", + "recipient_admission_text": "귀하들이 보낸 2024. 8. 29. 자 통지서는 2024. 9. 1. 잘 받아 보았습니다." + } + ], + "defense_components": [ + { + "assertion": "공사잔금 미지급으로 인한 유치권 행사", + "basis": "서정빌라 101호 거주 및 유치권" + }, + { + "assertion": "기존 수익을 채무에 충당 중", + "basis": "월 200만 원 임대료 및 본인/가족 거주 차임 공제" + } + ], + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-012.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-012.json new file mode 100644 index 00000000..a3e50a1a --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-012.json @@ -0,0 +1,55 @@ +{ + "evidence_index_proposed": "E-012", + "doc_uid": "DOC-012-등기말소_신청에_대한_답신", + "title": "등기말소 신청에 대한 답신", + "title_normalized": "등기말소 신청에 대한 답신", + "doc_type": "처분문서", + "key_facts": [ + "대한은행이 강용원의 근저당권 말소 신청 거부", + "경매절차 하자 없음을 주장", + "민사집행법 제267조에 따른 경매의 공신력 주장" + ], + "key_dates": [ + "2024-12-22" + ], + "key_amounts": [], + "key_parties": [ + "강용원", + "주식회사 대한은행" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 12 + }, + "doc_semantic_flags": [ + "notice_doc", + "response_doc" + ], + "notice_components": [ + { + "notice_id_proposed": "E-012-NT-01", + "notice_type": "기타", + "drafted_date": "2024-12-22", + "dispatch_date": "2024-12-22", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [ + "주식회사 대한은행" + ], + "receiver_candidates": [ + "강용원" + ], + "target_object_hint": "성수동 대지 근저당권", + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "legal_effect_candidate_hint": "등기말소 신청 거부 통지" + } + ], + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-013.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-013.json new file mode 100644 index 00000000..634d841a --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-013.json @@ -0,0 +1,107 @@ +{ + "evidence_index_proposed": "E-013", + "doc_uid": "DOC-013-등기사항전부증명서_성수동", + "title": "등기사항전부증명서(말소사항 포함)-토지", + "title_normalized": "등기사항전부증명서(말소사항 포함)-토지", + "doc_type": "공문서", + "key_facts": [ + "성수동 대지(256)에 관한 등기사항전부증명서", + "2024.10.5. 경매로 인한 매각으로 이문호 소유권 취득", + "2024.10.5. 주식회사 대한은행 근저당권 설정" + ], + "key_dates": [ + "2022.12.8. (강용원/정유심 공유 지분 등기)", + "2024.10.5. (경매에 의한 소유권이전)", + "2024.10.5. (주식회사 대한은행 근저당권 설정)" + ], + "key_amounts": [ + "520,000,000원 (대한은행 채권최고액)" + ], + "key_parties": [ + "강용원", + "정유심", + "이문호", + "오민환", + "주식회사 대한은행" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 13 + }, + "doc_semantic_flags": [ + "registry_doc", + "multi_event_doc" + ], + "multi_event_registry": true, + "registry_row_components": [ + { + "registry_ref": "REG-013-01", + "section": "갑구", + "row_seq": 1, + "receipt_no": "제1453호", + "event_date": "1997-03-15", + "purpose": "소유권이전", + "right_holder": "박재경" + }, + { + "registry_ref": "REG-013-02", + "section": "갑구", + "row_seq": 2, + "receipt_no": "제7026호", + "event_date": "2017-06-30", + "purpose": "소유권이전", + "right_holder": "강석우" + }, + { + "registry_ref": "REG-013-03", + "section": "갑구", + "row_seq": 3, + "receipt_no": "제11202호", + "event_date": "2022-11-08", + "purpose": "소유권이전", + "right_holder": "정유심, 강용원" + }, + { + "registry_ref": "REG-013-04", + "section": "갑구", + "row_seq": 4, + "receipt_no": "제7202호", + "event_date": "2023-04-06", + "purpose": "증여", + "right_holder": "강용원" + }, + { + "registry_ref": "REG-013-05", + "section": "갑구", + "row_seq": 5, + "receipt_no": "제12402호", + "event_date": "2024-07-21", + "purpose": "담보권실행을위한경매개시결정(일괄)", + "right_holder": "오민환" + }, + { + "registry_ref": "REG-013-06", + "section": "갑구", + "row_seq": 6, + "receipt_no": "제25797호", + "event_date": "2024-10-05", + "purpose": "담보권실행을 위한 경매로 인한 매각", + "right_holder": "이문호" + }, + { + "registry_ref": "REG-013-07", + "section": "을구", + "row_seq": 4, + "receipt_no": "제25799호", + "event_date": "2024-10-05", + "purpose": "근저당권설정", + "right_holder": "주식회사 대한은행", + "secured_debt_amount": "520,000,000원" + } + ], + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-014.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-014.json new file mode 100644 index 00000000..4acabdaf --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-014.json @@ -0,0 +1,100 @@ +{ + "evidence_index_proposed": "E-014", + "doc_uid": "DOC-014-등기사항전부증명서_말소사항_포함_건물", + "title": "등기사항전부증명서 (말소사항 포함) -건물", + "title_normalized": "등기사항전부증명서 (말소사항 포함) -건물", + "doc_type": "공문서", + "key_facts": [ + "2015.9.17 오국환 소유권이전", + "2023.3.17 오민한 소유권이전(매매)", + "2024.10.17 2번 근저당권 말소" + ], + "key_dates": [ + "2006-05-08", + "2015-09-17", + "2023-03-17", + "2024-10-17" + ], + "key_amounts": [ + "40,000,000원", + "150,000,000원", + "50,000,000원" + ], + "key_parties": [ + "이경주", + "오국환", + "오민한", + "나임오", + "강용일" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 14 + }, + "doc_semantic_flags": [ + "registry_doc", + "multi_event_doc" + ], + "multi_event_registry": true, + "registry_row_components": [ + { + "registry_ref": "REG-014-01", + "section": "갑구", + "row_seq": 1, + "registry_purpose": "소유권보존", + "receipt_no": "제24759호", + "registry_date": "2006-05-08", + "rights_holder": "이경주" + }, + { + "registry_ref": "REG-014-02", + "section": "갑구", + "row_seq": 2, + "registry_purpose": "소유권이전", + "receipt_no": "제56457호", + "registry_date": "2015-09-17", + "rights_holder": "오국환" + }, + { + "registry_ref": "REG-014-03", + "section": "갑구", + "row_seq": 3, + "registry_purpose": "소유권이전", + "receipt_no": "제11593호", + "registry_date": "2023-03-17", + "rights_holder": "오민한" + }, + { + "registry_ref": "REG-014-04", + "section": "을구", + "row_seq": 4, + "registry_purpose": "근저당권설정", + "receipt_no": "제50099호", + "registry_date": "2021-08-01", + "rights_holder": "나임오" + }, + { + "registry_ref": "REG-014-05", + "section": "을구", + "row_seq": 5, + "registry_purpose": "근저당권설정", + "receipt_no": "제26597호", + "registry_date": "2022-04-18", + "rights_holder": "강용일" + }, + { + "registry_ref": "REG-014-06", + "section": "을구", + "row_seq": 6, + "registry_purpose": "근저당권설정등기말소", + "receipt_no": "제74586호", + "registry_date": "2024-10-17", + "rights_holder": null + } + ], + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-015.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-015.json new file mode 100644 index 00000000..fde59ec1 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-015.json @@ -0,0 +1,88 @@ +{ + "evidence_index_proposed": "E-015", + "doc_uid": "DOC-015-등기사항전부증명서_말소사항_포함_집합건물", + "title": "등기사항전부증명서(말소사항 포함)-집합건물", + "title_normalized": "등기사항전부증명서_말소사항_포함_집합건물", + "doc_type": "공문서", + "key_facts": [ + "경기도 평택시 서정3길 123 서정빌라 101호 등기부", + "2022. 10. 1. 강호연 소유권이전", + "2022. 9. 1. 김동국 가압류(말소)" + ], + "key_dates": [ + "2021-12-10", + "2021-12-30", + "2022-10-01" + ], + "key_amounts": [ + "14,000,000원" + ], + "key_parties": [ + "김일동", + "정남이", + "강호연", + "김동국" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 15 + }, + "doc_semantic_flags": [ + "registry_doc", + "multi_event_doc" + ], + "multi_event_registry": true, + "registry_row_components": [ + { + "registry_ref": "REG-015-01", + "row_seq": 1, + "kind": "갑구", + "rank": "1", + "purpose": "소유권보존", + "receipt_date": "2021년12월10일", + "receipt_no": "제7233호", + "party_name": "김일동" + }, + { + "registry_ref": "REG-015-02", + "row_seq": 2, + "kind": "갑구", + "rank": "2", + "purpose": "소유권이전", + "receipt_date": "2021년12월30일", + "receipt_no": "제7871호", + "cause": "매매", + "party_name": "정남이" + }, + { + "registry_ref": "REG-015-03", + "row_seq": 3, + "kind": "갑구", + "rank": "3", + "purpose": "가압류", + "receipt_date": "2022년9월1일", + "receipt_no": "제11593호", + "cause": "수원지방법원 평택지원의 가압류결정(2022카단43434)", + "party_name": "김동국", + "amount": "14,000,000원" + }, + { + "registry_ref": "REG-015-04", + "row_seq": 4, + "kind": "갑구", + "rank": "4", + "purpose": "소유권이전", + "receipt_date": "2022년10월1일", + "receipt_no": "제12321호", + "party_name": "강호연" + } + ], + "registry_row_role_map": { + "REG-015-03": "release_role_potential" + }, + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-016.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-016.json new file mode 100644 index 00000000..a87d5fff --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-016.json @@ -0,0 +1,56 @@ +{ + "evidence_index_proposed": "E-016", + "doc_uid": "DOC-016-별지_목록", + "title": "별지 목록", + "title_normalized": "별지 목록", + "doc_type": "공문서", + "key_facts": [ + "성수동 대지 및 건물 목록", + "신림동 상가 목록", + "평택시 빌라 및 대지권 목록", + "흑석동 상가 및 대지권 목록" + ], + "key_dates": [], + "key_amounts": [], + "key_parties": [], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 16 + }, + "doc_semantic_flags": [ + "schedule_doc", + "relief_object_reference_doc" + ], + "schedule_items": [ + { + "schedule_ref": "SCH-016-01", + "canonical_label_candidate": "1. 서울 성동구 성수동 256 대 330㎡", + "usable_in_relief": "yes" + }, + { + "schedule_ref": "SCH-016-02", + "canonical_label_candidate": "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "usable_in_relief": "yes" + }, + { + "schedule_ref": "SCH-016-03", + "canonical_label_candidate": "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "usable_in_relief": "yes" + }, + { + "schedule_ref": "SCH-016-04", + "canonical_label_candidate": "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "usable_in_relief": "yes" + }, + { + "schedule_ref": "SCH-016-05", + "canonical_label_candidate": "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물", + "usable_in_relief": "yes" + } + ], + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-017.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-017.json new file mode 100644 index 00000000..c4eda8bb --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-017.json @@ -0,0 +1,95 @@ +{ + "evidence_index_proposed": "E-017", + "doc_uid": "DOC-017-부동산_매매계약서", + "title": "부동산 매매계약서", + "title_normalized": "부동산 매매계약서", + "doc_type": "처분문서", + "key_facts": [ + "흑석동 상가 매매계약(2023.10.1.)", + "매매대금 10억 원", + "윤건우(매도인), 이수인(매수인)" + ], + "key_dates": [ + "2023-10-01", + "2024-01-01", + "2024-05-01" + ], + "key_amounts": [ + "1,000,000,000원", + "200,000,000원", + "500,000,000원", + "300,000,000원" + ], + "key_parties": [ + "윤건우", + "이수인", + "양재혁" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 17 + }, + "doc_semantic_flags": [ + "claim_doc", + "mixed_contract_doc" + ], + "contract_components": [ + { + "component_id": "E-017-CC-01", + "component_kind": "sale", + "object_spec": "흑석동 상가(201호)", + "amount": "1,000,000,000원", + "date": "2023-10-01", + "payer_candidates": [ + "이수인" + ], + "recipient_candidates": [ + "윤건우" + ], + "source_locator": { + "locator_type": "table_row", + "locator_hint": "계약내용", + "excerpt": "매매대금 10억 원" + } + }, + { + "component_id": "E-017-CC-02", + "component_kind": "delivery", + "object_spec": "흑석동 상가(201호)", + "possession_recipient_candidates": [ + "이수인" + ], + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "특약사항", + "excerpt": "중도금을 지급받은 날 매수인에게 위 부동산을 인도한다." + } + } + ], + "legal_calculation_object": { + "slot_id": "E-017-LCO-01", + "slot_kind": "sale", + "source_locator": { + "locator_type": "table_row", + "locator_hint": "계약내용", + "excerpt": "부동산 매매계약서" + }, + "transaction_type": "매매", + "transaction_date": "2023-10-01", + "sale_price": "1,000,000,000원", + "consideration_breakdown": [ + "계약금 2억", + "중도금 5억", + "잔금 3억" + ], + "parties": [ + "윤건우", + "이수인" + ] + }, + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-018.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-018.json new file mode 100644 index 00000000..7306d413 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-018.json @@ -0,0 +1,33 @@ +{ + "evidence_index_proposed": "E-018", + "doc_uid": "DOC-018-사_업_자_등_록_증", + "title": "사업자등록증", + "title_normalized": "사업자등록증", + "doc_type": "공문서", + "key_facts": [ + "양정숙 명의 맛나식당 사업자등록", + "개업일 2024년 2월 1일", + "사업장소재지 서울 동작구 흑석로 80, 201호" + ], + "key_dates": [ + "2024-02-01", + "2024-02-02" + ], + "key_amounts": [], + "key_parties": [ + "양정숙", + "동작 세무서장" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 18 + }, + "doc_semantic_flags": [ + "status_doc" + ], + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-019.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-019.json new file mode 100644 index 00000000..da3db50d --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-019.json @@ -0,0 +1,79 @@ +{ + "evidence_index_proposed": "E-019", + "doc_uid": "DOC-019-소멸청구_통지서", + "title": "소멸청구 통지서", + "title_normalized": "소멸청구 통지서", + "doc_type": "기타", + "key_facts": [ + "유치권 소멸청구 통지", + "평택시 빌라 인도 요구", + "사용수익 반환 요구" + ], + "key_dates": [ + "2024-08-29" + ], + "key_amounts": [], + "key_parties": [ + "강용원", + "양정숙", + "박광윤" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 19 + }, + "property_cluster_id": "해당 주거용 부동산", + "doc_semantic_flags": [ + "notice_doc", + "lien_assertion_doc" + ], + "multi_event_registry": null, + "contract_components": [], + "valuation_object": null, + "legal_calculation_object": null, + "legal_calculation_objects": [], + "claim_document_components": [], + "business_transfer_components": [], + "registry_row_components": [], + "secured_debt_components": [], + "valuation_rows": [], + "mixed_contract_payment_semantics": {}, + "loan_components": [], + "valuation_matrix": {}, + "registry_row_role_map": {}, + "defense_assertions": [], + "admission_components": [], + "defense_components": [], + "notice_components": [ + { + "notice_id_proposed": "E-019-NT-01", + "notice_type": "유치권소멸청구통지", + "drafted_date": "2024-08-29", + "dispatch_date": "2024-08-29", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": true, + "sender_candidates": [ + "강용원", + "양정숙" + ], + "receiver_candidates": [ + "박광윤" + ], + "target_object_hint": "평택시 서정3길 123, 101호(서정동, 서정빌라)", + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "legal_effect_candidate_hint": null + } + ], + "party_assertion_components": [], + "schedule_items": [], + "registry_object_display": {}, + "succession_document_components": null, + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-020.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-020.json new file mode 100644 index 00000000..9fc142c7 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-020.json @@ -0,0 +1,45 @@ +{ + "evidence_index_proposed": "E-020", + "doc_uid": "DOC-020-영업양도계약서_김선웅_오민한", + "title": "영업양도 계약서", + "title_normalized": "영업양도 계약서", + "doc_type": "처분문서", + "key_facts": [ + "김선웅이 오민한에게 해드림 영업을 양도함", + "계약일 2024. 7. 5.", + "양도대금 2억 원" + ], + "key_dates": [ + "2024. 7. 5." + ], + "key_amounts": [ + "200,000,000원" + ], + "key_parties": [ + "김선웅", + "오민한" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 20 + }, + "doc_semantic_flags": [ + "business_transfer_doc" + ], + "business_transfer_components": [ + { + "business_name": "해드림(SUN-DREAM)", + "transferor_candidates": ["김선웅"], + "transferee_candidates": ["오민한"], + "business_assets_transferred": ["임차권", "설비", "비품", "자재", "공구"], + "trade_name_continuation_clause": "제4조: 을은 상호를 계속 사용할 수 있음", + "customer_or_employee_succession_clause": "제3조: 고객관계, 고용관계 승계", + "pre_transfer_debt_exclusion_clause": "제5조: 을은 계약일 전 발생 채무 책임지지 아니함" + } + ], + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-021.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-021.json new file mode 100644 index 00000000..6980964c --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-021.json @@ -0,0 +1,64 @@ +{ + "evidence_index_proposed": "E-021", + "doc_uid": "DOC-021-이행각서", + "title": "이행각서", + "title_normalized": "이행각서", + "doc_type": "처분문서", + "key_facts": [ + "김선웅이 강용원에게 인테리어 자재 물품대금 채무 변제 약속", + "2021. 6. 5. 자 물품대금 채무", + "2024. 8. 말까지 변제 약속" + ], + "key_dates": [ + "2021-06-05", + "2024-08-05", + "2024-08-31" + ], + "key_amounts": [], + "key_parties": [ + "김선웅", + "강용원" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 21 + }, + "property_cluster_id": null, + "doc_semantic_flags": [ + "claim_doc", + "status_doc" + ], + "multi_event_registry": null, + "contract_components": [], + "valuation_object": null, + "legal_calculation_object": null, + "legal_calculation_objects": [], + "claim_document_components": [ + { + "component_kind": "debt_acknowledgement", + "drafted_date": "2024-08-05", + "new_due_date": "2024-08-31" + } + ], + "business_transfer_components": [], + "registry_row_components": [], + "secured_debt_components": [], + "valuation_rows": [], + "mixed_contract_payment_semantics": {}, + "loan_components": [], + "valuation_matrix": {}, + "registry_row_role_map": {}, + "defense_assertions": [], + "admission_components": [], + "defense_components": [], + "notice_components": [], + "party_assertion_components": [], + "schedule_items": [], + "registry_object_display": {}, + "succession_document_components": null, + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-022.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-022.json new file mode 100644 index 00000000..638df902 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-022.json @@ -0,0 +1,57 @@ +{ + "evidence_index_proposed": "E-022", + "doc_uid": "DOC-022-임대차계약서_이수인_양정숙", + "title": "임대차계약서", + "title_normalized": "임대차계약서", + "doc_type": "처분문서", + "key_facts": [ + "이수인(임대인)과 양정숙(임차인) 간 흑석동 상가 임대차계약 체결", + "2024. 2. 1. 계약 체결 및 보증금 지급 완료", + "보증금 3억 원, 월 차임 500만 원" + ], + "key_dates": [ + "2024-02-01" + ], + "key_amounts": [ + "300,000,000원", + "5,000,000원" + ], + "key_parties": [ + "이수인", + "양정숙" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 22 + }, + "doc_semantic_flags": [ + "mixed_contract_doc", + "claim_doc", + "status_doc" + ], + "contract_components": [ + { + "component_id": "E-022-CC-01", + "component_kind": "lease", + "object_spec": "흑석동 상가 201호", + "amount": "300,000,000원", + "date": "2024-02-01", + "payer_candidates": [ + "양정숙" + ], + "recipient_candidates": [ + "이수인" + ], + "source_locator": { + "locator_type": "paragraph", + "locator_hint": "제1조, 제3조", + "excerpt": "흑석동 상가 201호, 보증금 3억" + } + } + ], + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-023.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-023.json new file mode 100644 index 00000000..c1ac5fad --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-023.json @@ -0,0 +1,47 @@ +{ + "evidence_index_proposed": "E-023", + "doc_uid": "DOC-023-임료시세확인서", + "title": "임료시세확인서", + "title_normalized": "임료시세확인서", + "doc_type": "기타", + "key_facts": [ + "평택시 서정빌라 월 차임 시세 확인", + "보증금 없는 월세 200만 원", + "기준일 2022. 10.경" + ], + "key_dates": [ + "2022. 10.경", + "2024. 12. 23." + ], + "key_amounts": [ + "월세 200만 원" + ], + "key_parties": [ + "김민식" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 23 + }, + "doc_semantic_flags": [ + "valuation_doc", + "supporting_reference_doc" + ], + "valuation_object": { + "valuation_kind": "rent_value", + "target_object": "경기도 평택시 서정3길 123(서정동, 서정빌라)", + "basis_date_or_period": "2022. 10.경", + "value_rows": [ + { + "label": "보증금 없이", + "amount": "월세 200만 원", + "unit": "월액" + } + ] + }, + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-024.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-024.json new file mode 100644 index 00000000..d310344f --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-024.json @@ -0,0 +1,84 @@ +{ + "evidence_index_proposed": "E-024", + "doc_uid": "DOC-024-차용증", + "title": "차용증", + "title_normalized": "차용증", + "doc_type": "처분문서", + "key_facts": ["2022. 4. 18.자 1억 원 차용증", "2022. 5. 18.자 1억 원 차용증"], + "key_dates": ["2022-04-18", "2022-05-18", "2022-12-17", "2023-03-17"], + "key_amounts": ["100,000,000원"], + "key_parties": ["강용원", "오국한"], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 24 + }, + "property_cluster_id": null, + "doc_semantic_flags": ["claim_doc"], + "multi_event_registry": null, + "contract_components": [], + "valuation_object": null, + "legal_calculation_object": null, + "legal_calculation_objects": [], + "claim_document_components": [ + { + "component_kind": "underlying_claim", + "principal_amount": "100,000,000원", + "loan_date": "2022-04-18", + "due_date": "2022-12-17" + }, + { + "component_kind": "underlying_claim", + "principal_amount": "100,000,000원", + "loan_date": "2022-05-18", + "due_date": "2023-03-17" + } + ], + "business_transfer_components": [], + "registry_row_components": [], + "secured_debt_components": [], + "valuation_rows": [], + "mixed_contract_payment_semantics": {}, + "loan_components": [ + { + "loan_component_id": "E-024-LC-01", + "principal_amount": "100,000,000원", + "loan_date": "2022-04-18", + "due_date": "2022-12-17", + "agreed_interest_rate": "월 1%", + "agreed_delay_damage_rate": null, + "rate_text": "월 1%", + "rate_source_locator": null, + "interest_accrual_start_hint": null, + "collateral_object_hint": null, + "role_in_claim_hint": null + }, + { + "loan_component_id": "E-024-LC-02", + "principal_amount": "100,000,000원", + "loan_date": "2022-05-18", + "due_date": "2023-03-17", + "agreed_interest_rate": "월 1%", + "agreed_delay_damage_rate": null, + "rate_text": "월 1%", + "rate_source_locator": null, + "interest_accrual_start_hint": null, + "collateral_object_hint": null, + "role_in_claim_hint": null + } + ], + "valuation_matrix": {}, + "registry_row_role_map": {}, + "defense_assertions": [], + "admission_components": [], + "defense_components": [], + "notice_components": [], + "party_assertion_components": [], + "schedule_items": [], + "registry_object_display": {}, + "succession_document_components": null, + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-025.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-025.json new file mode 100644 index 00000000..04c6bf32 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-025.json @@ -0,0 +1,50 @@ +{ + "evidence_index_proposed": "E-025", + "doc_uid": "DOC-025-차용증", + "title": "차용증", + "title_normalized": "차용증", + "doc_type": "처분문서", + "key_facts": [ + "강용원이 오민한에게 2억 원을 차용함", + "2024. 1. 5. 변제기", + "성수동 대지 5분의 2 지분에 1순위 근저당권 설정 약정" + ], + "key_dates": [ + "2023-01-06" + ], + "key_amounts": [ + "200,000,000원" + ], + "key_parties": [ + "오민한", + "강용원" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 25 + }, + "doc_semantic_flags": [ + "claim_doc", + "secured_debt_components" + ], + "claim_document_components": [ + { + "component_kind": "underlying_claim", + "principal_amount": "200,000,000원", + "due_date": "2024-01-05", + "agreed_interest_rate": "연 10%", + "rate_text": "연 10%" + } + ], + "secured_debt_components": [ + { + "collateral_object_hint": "서울 성동구 성수동 256 대 330㎡의 5분의 2 지분", + "due_date": "2024-01-05" + } + ], + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-026.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-026.json new file mode 100644 index 00000000..c817439f --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-026.json @@ -0,0 +1,52 @@ +{ + "evidence_index_proposed": "E-026", + "doc_uid": "DOC-026-차용증", + "title": "차 용 증", + "title_normalized": "차 용 증", + "doc_type": "처분문서", + "key_facts": [ + "강용원의 오민한에 대한 300,000,000원 차용", + "서울 성동구 성수동 256 대 330㎡ 5/3 지분 근저당권 설정 약정" + ], + "key_dates": [ + "2023. 7. 6.", + "2024. 7. 5." + ], + "key_amounts": [ + "300,000,000원" + ], + "key_parties": [ + "강용원", + "오민한" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 26 + }, + "property_cluster_id": "해당 주거용 부동산", + "doc_semantic_flags": [ + "claim_doc" + ], + "loan_components": [ + { + "loan_component_id": "E-026-LC-01", + "principal_amount": "300,000,000원", + "loan_date": "2023-07-06", + "due_date": "2024-07-05", + "agreed_interest_rate": "연 15%", + "collateral_object_hint": "서울 성동구 성수동 256 대 330㎡의 5분의 3 지분", + "role_in_claim_hint": "차용" + } + ], + "secured_debt_components": [ + { + "due_date": "2024-07-05", + "agreed_interest_rate": "연 15%" + } + ], + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-027.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-027.json new file mode 100644 index 00000000..93601893 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-027.json @@ -0,0 +1,62 @@ +{ + "evidence_index_proposed": "E-027", + "doc_uid": "DOC-027-최고_통지서", + "title": "최고 통지서", + "title_normalized": "최고 통지서", + "doc_type": "처분문서", + "key_facts": [ + "2021. 6. 5. 강용원이 김선웅에게 2억 원 상당 인테리어 자재 납품", + "2024. 5. 4. 강용원이 김선웅에게 대금 지급 최고 통지서 발송", + "2024. 5. 6. 김선웅에게 최고 통지서 도달" + ], + "key_dates": [ + "2021-06-05", + "2024-05-04", + "2024-05-06" + ], + "key_amounts": [ + "200,000,000원" + ], + "key_parties": [ + "강용원", + "김선웅" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 27 + }, + "doc_semantic_flags": [ + "claim_doc", + "notice_doc" + ], + "claim_document_components": [ + { + "component_kind": "demand_notice", + "dispatch_date": "2024-05-04", + "arrival_date": "2024-05-06" + } + ], + "notice_components": [ + { + "notice_id_proposed": "E-027-NT-01", + "notice_type": "최고", + "dispatch_date": "2024-05-04", + "arrival_date": "2024-05-06", + "received_date": "2024-05-06", + "sender_candidates": [ + "강용원" + ], + "receiver_candidates": [ + "김선웅" + ], + "receipt_evidence_refs": [ + "우편물 배달 증명서" + ] + } + ], + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-028.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-028.json new file mode 100644 index 00000000..a60efb1f --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-028.json @@ -0,0 +1,52 @@ +{ + "evidence_index_proposed": "E-028", + "doc_uid": "DOC-028-통지서", + "title": "통지서", + "title_normalized": "통지서", + "doc_type": "처분문서", + "key_facts": [ + "오민한의 김선웅 채무 책임 부인", + "오민한의 채무 시효 소멸 주장", + "오민한의 채무 인수 당시 책임 배제 약정 주장" + ], + "key_dates": [ + "2024-12-27" + ], + "key_amounts": [], + "key_parties": [ + "오민한", + "강용원", + "김선웅" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 28 + }, + "doc_semantic_flags": [ + "notice_doc", + "response_doc" + ], + "notice_components": [ + { + "notice_id_proposed": "E-028-NT-01", + "notice_type": "기타", + "dispatch_date": "2024-12-27", + "sender_candidates": [ + "오민한" + ], + "receiver_candidates": [ + "강용원" + ] + } + ], + "defense_assertions": [ + "김선웅의 채무 인수 당시 책임 배제 약정 존재", + "물품대금 채무 시효 소멸", + "본인에게 채무 책임 없음" + ], + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-029.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-029.json new file mode 100644 index 00000000..8c16d092 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-029.json @@ -0,0 +1,44 @@ +{ + "evidence_index_proposed": "E-029", + "doc_uid": "DOC-029-통지서_이수인_윤건우", + "title": "통지서_이수인_윤건우", + "title_normalized": "통지서 이수인 윤건우", + "doc_type": "통신기록", + "key_facts": [ + "이수인이 윤건우에게 흑석동 상가 매매계약 관련 매매대금 감액 및 제3자 명의 등기 요청" + ], + "key_dates": [ + "2024-03-30" + ], + "key_amounts": [], + "key_parties": [ + "이수인", + "윤건우" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 29 + }, + "doc_semantic_flags": [ + "notice_doc" + ], + "notice_components": [ + { + "notice_id_proposed": "E-029-NT-01", + "notice_type": "기타", + "dispatch_date": "2024-03-30", + "sender_candidates": [ + "이수인" + ], + "receiver_candidates": [ + "윤건우" + ], + "target_object_hint": "흑석동 상가" + } + ], + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-030.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-030.json new file mode 100644 index 00000000..dcff21d3 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_indexed_parts/E-030.json @@ -0,0 +1,50 @@ +{ + "evidence_index_proposed": "E-030", + "doc_uid": "DOC-030-해제_통지서", + "title": "해제 통지서", + "title_normalized": "해제 통지서", + "doc_type": "처분문서", + "key_facts": [ + "2023.10.1. 흑석동 상가 매매계약 체결", + "2024.5.15. 윤건우가 이수인에게 매매계약 해제 통지", + "2024.5.16. 해제 통지서 수령" + ], + "key_dates": [ + "2023-10-01", + "2024-05-15", + "2024-05-16" + ], + "key_parties": [ + "윤건우", + "이수인" + ], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": 30 + }, + "doc_semantic_flags": [ + "notice_doc", + "response_doc" + ], + "notice_components": [ + { + "notice_id_proposed": "E-030-NT-01", + "notice_type": "해제통지", + "dispatch_date": "2024-05-15", + "arrival_date": "2024-05-16", + "sender_candidates": [ + "윤건우" + ], + "receiver_candidates": [ + "이수인" + ], + "target_object_hint": "흑석동 상가", + "legal_effect_date_candidate": "2024-05-16" + } + ], + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shard_plan.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shard_plan.json new file mode 100644 index 00000000..8b74cb83 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shard_plan.json @@ -0,0 +1,2279 @@ +{ + "schema_version": "evidence_shard_plan.v1", + "planner_task": "Task_Evidence_shard_planner", + "created_at_utc": "2026-07-10T06:49:06.549470+00:00", + "source_file": "evidence_all.json", + "source_selector": "$.items || $", + "shard_count": 30, + "expected_ordinals": [ + "001", + "002", + "003", + "004", + "005", + "006", + "007", + "008", + "009", + "010", + "011", + "012", + "013", + "014", + "015", + "016", + "017", + "018", + "019", + "020", + "021", + "022", + "023", + "024", + "025", + "026", + "027", + "028", + "029", + "030" + ], + "expected_evidence_ids": [ + "E-001", + "E-002", + "E-003", + "E-004", + "E-005", + "E-006", + "E-007", + "E-008", + "E-009", + "E-010", + "E-011", + "E-012", + "E-013", + "E-014", + "E-015", + "E-016", + "E-017", + "E-018", + "E-019", + "E-020", + "E-021", + "E-022", + "E-023", + "E-024", + "E-025", + "E-026", + "E-027", + "E-028", + "E-029", + "E-030" + ], + "ordinal_policy": { + "base": 1, + "assignment": "array_order", + "format": "%03d", + "assigned_ordinal_var": "__assigned_ordinal__", + "evidence_index_candidate": "E-{ordinal:03d}", + "no_legal_sorting": true, + "no_content_based_reordering": true + }, + "shards": [ + { + "assigned_ordinal": 1, + "ordinal_label": "001", + "evidence_index_candidate": "E-001", + "b1_task_id": "Task_B1_map_doc_001", + "b2_task_id": "Task_B2_map_events_001", + "b1_output_path": "evidence_indexed_parts/E-001.json", + "b2_output_path": "evidence_event_candidate_parts/E-001.json", + "shard_doc_path": "evidence_shards/E-001.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 1, + "array_index": 0, + "doc_uid_candidate": "DOC-001", + "title_candidate": "evidence_all[1]", + "content_sha256": "4d0e6a0591acb2914019fec3b7903db5451a98f146fbe8dbc7cd8ef66e864c7a", + "shard_doc_path": "evidence_shards/E-001.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 1, + "__ordinal_label__": "001", + "__evidence_index_candidate__": "E-001", + "__b1_part_path__": "evidence_indexed_parts/E-001.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-001.json", + "__shard_doc_path__": "evidence_shards/E-001.json" + } + }, + { + "assigned_ordinal": 2, + "ordinal_label": "002", + "evidence_index_candidate": "E-002", + "b1_task_id": "Task_B1_map_doc_002", + "b2_task_id": "Task_B2_map_events_002", + "b1_output_path": "evidence_indexed_parts/E-002.json", + "b2_output_path": "evidence_event_candidate_parts/E-002.json", + "shard_doc_path": "evidence_shards/E-002.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 2, + "array_index": 1, + "doc_uid_candidate": "DOC-002", + "title_candidate": "evidence_all[2]", + "content_sha256": "b86edc5acd22e6167f3be49d3bfead67b5632e9c3a2c028e00854e45c68060a4", + "shard_doc_path": "evidence_shards/E-002.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 2, + "__ordinal_label__": "002", + "__evidence_index_candidate__": "E-002", + "__b1_part_path__": "evidence_indexed_parts/E-002.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-002.json", + "__shard_doc_path__": "evidence_shards/E-002.json" + } + }, + { + "assigned_ordinal": 3, + "ordinal_label": "003", + "evidence_index_candidate": "E-003", + "b1_task_id": "Task_B1_map_doc_003", + "b2_task_id": "Task_B2_map_events_003", + "b1_output_path": "evidence_indexed_parts/E-003.json", + "b2_output_path": "evidence_event_candidate_parts/E-003.json", + "shard_doc_path": "evidence_shards/E-003.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 3, + "array_index": 2, + "doc_uid_candidate": "DOC-003", + "title_candidate": "evidence_all[3]", + "content_sha256": "92e8ed6927ebe493083153083d6ed3fe85f3b41719750df957e63fa1192bfee7", + "shard_doc_path": "evidence_shards/E-003.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 3, + "__ordinal_label__": "003", + "__evidence_index_candidate__": "E-003", + "__b1_part_path__": "evidence_indexed_parts/E-003.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-003.json", + "__shard_doc_path__": "evidence_shards/E-003.json" + } + }, + { + "assigned_ordinal": 4, + "ordinal_label": "004", + "evidence_index_candidate": "E-004", + "b1_task_id": "Task_B1_map_doc_004", + "b2_task_id": "Task_B2_map_events_004", + "b1_output_path": "evidence_indexed_parts/E-004.json", + "b2_output_path": "evidence_event_candidate_parts/E-004.json", + "shard_doc_path": "evidence_shards/E-004.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 4, + "array_index": 3, + "doc_uid_candidate": "DOC-004", + "title_candidate": "evidence_all[4]", + "content_sha256": "720bf15b68ed74efbd370a635823e93ec49ff03bde2e0ca4de21a75c1636bff9", + "shard_doc_path": "evidence_shards/E-004.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 4, + "__ordinal_label__": "004", + "__evidence_index_candidate__": "E-004", + "__b1_part_path__": "evidence_indexed_parts/E-004.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-004.json", + "__shard_doc_path__": "evidence_shards/E-004.json" + } + }, + { + "assigned_ordinal": 5, + "ordinal_label": "005", + "evidence_index_candidate": "E-005", + "b1_task_id": "Task_B1_map_doc_005", + "b2_task_id": "Task_B2_map_events_005", + "b1_output_path": "evidence_indexed_parts/E-005.json", + "b2_output_path": "evidence_event_candidate_parts/E-005.json", + "shard_doc_path": "evidence_shards/E-005.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 5, + "array_index": 4, + "doc_uid_candidate": "DOC-005", + "title_candidate": "evidence_all[5]", + "content_sha256": "c249aaa55d584db081e42e3597f95dd0c41897ae9e28d4bc3366ab1c6ae300df", + "shard_doc_path": "evidence_shards/E-005.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 5, + "__ordinal_label__": "005", + "__evidence_index_candidate__": "E-005", + "__b1_part_path__": "evidence_indexed_parts/E-005.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-005.json", + "__shard_doc_path__": "evidence_shards/E-005.json" + } + }, + { + "assigned_ordinal": 6, + "ordinal_label": "006", + "evidence_index_candidate": "E-006", + "b1_task_id": "Task_B1_map_doc_006", + "b2_task_id": "Task_B2_map_events_006", + "b1_output_path": "evidence_indexed_parts/E-006.json", + "b2_output_path": "evidence_event_candidate_parts/E-006.json", + "shard_doc_path": "evidence_shards/E-006.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 6, + "array_index": 5, + "doc_uid_candidate": "DOC-006", + "title_candidate": "evidence_all[6]", + "content_sha256": "ddcda43c5dcc63a769aaa9195c46a4e8cffb950d96639eef709063d495c6142b", + "shard_doc_path": "evidence_shards/E-006.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 6, + "__ordinal_label__": "006", + "__evidence_index_candidate__": "E-006", + "__b1_part_path__": "evidence_indexed_parts/E-006.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-006.json", + "__shard_doc_path__": "evidence_shards/E-006.json" + } + }, + { + "assigned_ordinal": 7, + "ordinal_label": "007", + "evidence_index_candidate": "E-007", + "b1_task_id": "Task_B1_map_doc_007", + "b2_task_id": "Task_B2_map_events_007", + "b1_output_path": "evidence_indexed_parts/E-007.json", + "b2_output_path": "evidence_event_candidate_parts/E-007.json", + "shard_doc_path": "evidence_shards/E-007.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 7, + "array_index": 6, + "doc_uid_candidate": "DOC-007", + "title_candidate": "evidence_all[7]", + "content_sha256": "d9720e409a380cd96d57f75b9ec2e452c8c25da285847dec3c348e13f84798c3", + "shard_doc_path": "evidence_shards/E-007.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 7, + "__ordinal_label__": "007", + "__evidence_index_candidate__": "E-007", + "__b1_part_path__": "evidence_indexed_parts/E-007.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-007.json", + "__shard_doc_path__": "evidence_shards/E-007.json" + } + }, + { + "assigned_ordinal": 8, + "ordinal_label": "008", + "evidence_index_candidate": "E-008", + "b1_task_id": "Task_B1_map_doc_008", + "b2_task_id": "Task_B2_map_events_008", + "b1_output_path": "evidence_indexed_parts/E-008.json", + "b2_output_path": "evidence_event_candidate_parts/E-008.json", + "shard_doc_path": "evidence_shards/E-008.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 8, + "array_index": 7, + "doc_uid_candidate": "DOC-008", + "title_candidate": "evidence_all[8]", + "content_sha256": "4d83e1fb28584e0568e7034dd2c796b0ca4c05c991701d972aa2cbe32b23290c", + "shard_doc_path": "evidence_shards/E-008.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 8, + "__ordinal_label__": "008", + "__evidence_index_candidate__": "E-008", + "__b1_part_path__": "evidence_indexed_parts/E-008.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-008.json", + "__shard_doc_path__": "evidence_shards/E-008.json" + } + }, + { + "assigned_ordinal": 9, + "ordinal_label": "009", + "evidence_index_candidate": "E-009", + "b1_task_id": "Task_B1_map_doc_009", + "b2_task_id": "Task_B2_map_events_009", + "b1_output_path": "evidence_indexed_parts/E-009.json", + "b2_output_path": "evidence_event_candidate_parts/E-009.json", + "shard_doc_path": "evidence_shards/E-009.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 9, + "array_index": 8, + "doc_uid_candidate": "DOC-009", + "title_candidate": "evidence_all[9]", + "content_sha256": "580c40fc58e2033779715b0a455de4b7b5be9c8ccde29cfbf37c49e7ff097c5a", + "shard_doc_path": "evidence_shards/E-009.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 9, + "__ordinal_label__": "009", + "__evidence_index_candidate__": "E-009", + "__b1_part_path__": "evidence_indexed_parts/E-009.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-009.json", + "__shard_doc_path__": "evidence_shards/E-009.json" + } + }, + { + "assigned_ordinal": 10, + "ordinal_label": "010", + "evidence_index_candidate": "E-010", + "b1_task_id": "Task_B1_map_doc_010", + "b2_task_id": "Task_B2_map_events_010", + "b1_output_path": "evidence_indexed_parts/E-010.json", + "b2_output_path": "evidence_event_candidate_parts/E-010.json", + "shard_doc_path": "evidence_shards/E-010.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 10, + "array_index": 9, + "doc_uid_candidate": "DOC-010", + "title_candidate": "evidence_all[10]", + "content_sha256": "94267643f63a855d3ec82d24d4da54b0660a8c5249df3997b195b4a5fbf9ade0", + "shard_doc_path": "evidence_shards/E-010.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 10, + "__ordinal_label__": "010", + "__evidence_index_candidate__": "E-010", + "__b1_part_path__": "evidence_indexed_parts/E-010.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-010.json", + "__shard_doc_path__": "evidence_shards/E-010.json" + } + }, + { + "assigned_ordinal": 11, + "ordinal_label": "011", + "evidence_index_candidate": "E-011", + "b1_task_id": "Task_B1_map_doc_011", + "b2_task_id": "Task_B2_map_events_011", + "b1_output_path": "evidence_indexed_parts/E-011.json", + "b2_output_path": "evidence_event_candidate_parts/E-011.json", + "shard_doc_path": "evidence_shards/E-011.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 11, + "array_index": 10, + "doc_uid_candidate": "DOC-011", + "title_candidate": "evidence_all[11]", + "content_sha256": "ca5da2134ff2aead725e5c44f429ecc202da584c4b4879e8cd73a390e53818f6", + "shard_doc_path": "evidence_shards/E-011.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 11, + "__ordinal_label__": "011", + "__evidence_index_candidate__": "E-011", + "__b1_part_path__": "evidence_indexed_parts/E-011.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-011.json", + "__shard_doc_path__": "evidence_shards/E-011.json" + } + }, + { + "assigned_ordinal": 12, + "ordinal_label": "012", + "evidence_index_candidate": "E-012", + "b1_task_id": "Task_B1_map_doc_012", + "b2_task_id": "Task_B2_map_events_012", + "b1_output_path": "evidence_indexed_parts/E-012.json", + "b2_output_path": "evidence_event_candidate_parts/E-012.json", + "shard_doc_path": "evidence_shards/E-012.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 12, + "array_index": 11, + "doc_uid_candidate": "DOC-012", + "title_candidate": "evidence_all[12]", + "content_sha256": "e1d22418d8c3b053c2bf35d199a1e3a65db7f3ad3a2cfb675f52105d294d71b3", + "shard_doc_path": "evidence_shards/E-012.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 12, + "__ordinal_label__": "012", + "__evidence_index_candidate__": "E-012", + "__b1_part_path__": "evidence_indexed_parts/E-012.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-012.json", + "__shard_doc_path__": "evidence_shards/E-012.json" + } + }, + { + "assigned_ordinal": 13, + "ordinal_label": "013", + "evidence_index_candidate": "E-013", + "b1_task_id": "Task_B1_map_doc_013", + "b2_task_id": "Task_B2_map_events_013", + "b1_output_path": "evidence_indexed_parts/E-013.json", + "b2_output_path": "evidence_event_candidate_parts/E-013.json", + "shard_doc_path": "evidence_shards/E-013.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 13, + "array_index": 12, + "doc_uid_candidate": "DOC-013", + "title_candidate": "evidence_all[13]", + "content_sha256": "f8c1e817957626716993941b82cdd6e6f793ccba2fd74a1e5ef6b7087120a076", + "shard_doc_path": "evidence_shards/E-013.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 13, + "__ordinal_label__": "013", + "__evidence_index_candidate__": "E-013", + "__b1_part_path__": "evidence_indexed_parts/E-013.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-013.json", + "__shard_doc_path__": "evidence_shards/E-013.json" + } + }, + { + "assigned_ordinal": 14, + "ordinal_label": "014", + "evidence_index_candidate": "E-014", + "b1_task_id": "Task_B1_map_doc_014", + "b2_task_id": "Task_B2_map_events_014", + "b1_output_path": "evidence_indexed_parts/E-014.json", + "b2_output_path": "evidence_event_candidate_parts/E-014.json", + "shard_doc_path": "evidence_shards/E-014.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 14, + "array_index": 13, + "doc_uid_candidate": "DOC-014", + "title_candidate": "evidence_all[14]", + "content_sha256": "d934284c54106a78e75e82dd15aeecf462ac385160a72ab5dad63531739ee33e", + "shard_doc_path": "evidence_shards/E-014.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 14, + "__ordinal_label__": "014", + "__evidence_index_candidate__": "E-014", + "__b1_part_path__": "evidence_indexed_parts/E-014.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-014.json", + "__shard_doc_path__": "evidence_shards/E-014.json" + } + }, + { + "assigned_ordinal": 15, + "ordinal_label": "015", + "evidence_index_candidate": "E-015", + "b1_task_id": "Task_B1_map_doc_015", + "b2_task_id": "Task_B2_map_events_015", + "b1_output_path": "evidence_indexed_parts/E-015.json", + "b2_output_path": "evidence_event_candidate_parts/E-015.json", + "shard_doc_path": "evidence_shards/E-015.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 15, + "array_index": 14, + "doc_uid_candidate": "DOC-015", + "title_candidate": "evidence_all[15]", + "content_sha256": "531066b36bb947d5505e9226d5d2d9e3d7c2fa18d88ef42157dbf1b843d06af9", + "shard_doc_path": "evidence_shards/E-015.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 15, + "__ordinal_label__": "015", + "__evidence_index_candidate__": "E-015", + "__b1_part_path__": "evidence_indexed_parts/E-015.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-015.json", + "__shard_doc_path__": "evidence_shards/E-015.json" + } + }, + { + "assigned_ordinal": 16, + "ordinal_label": "016", + "evidence_index_candidate": "E-016", + "b1_task_id": "Task_B1_map_doc_016", + "b2_task_id": "Task_B2_map_events_016", + "b1_output_path": "evidence_indexed_parts/E-016.json", + "b2_output_path": "evidence_event_candidate_parts/E-016.json", + "shard_doc_path": "evidence_shards/E-016.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 16, + "array_index": 15, + "doc_uid_candidate": "DOC-016", + "title_candidate": "evidence_all[16]", + "content_sha256": "b184bfcc5f473bd3d5abf8b6d6e37eb35edc09deaa56b99b08ee288e965bd924", + "shard_doc_path": "evidence_shards/E-016.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 16, + "__ordinal_label__": "016", + "__evidence_index_candidate__": "E-016", + "__b1_part_path__": "evidence_indexed_parts/E-016.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-016.json", + "__shard_doc_path__": "evidence_shards/E-016.json" + } + }, + { + "assigned_ordinal": 17, + "ordinal_label": "017", + "evidence_index_candidate": "E-017", + "b1_task_id": "Task_B1_map_doc_017", + "b2_task_id": "Task_B2_map_events_017", + "b1_output_path": "evidence_indexed_parts/E-017.json", + "b2_output_path": "evidence_event_candidate_parts/E-017.json", + "shard_doc_path": "evidence_shards/E-017.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 17, + "array_index": 16, + "doc_uid_candidate": "DOC-017", + "title_candidate": "evidence_all[17]", + "content_sha256": "cdbff127d01067e9df8596972134466d750b4e912f75f20f2cd18d46abb2d07d", + "shard_doc_path": "evidence_shards/E-017.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 17, + "__ordinal_label__": "017", + "__evidence_index_candidate__": "E-017", + "__b1_part_path__": "evidence_indexed_parts/E-017.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-017.json", + "__shard_doc_path__": "evidence_shards/E-017.json" + } + }, + { + "assigned_ordinal": 18, + "ordinal_label": "018", + "evidence_index_candidate": "E-018", + "b1_task_id": "Task_B1_map_doc_018", + "b2_task_id": "Task_B2_map_events_018", + "b1_output_path": "evidence_indexed_parts/E-018.json", + "b2_output_path": "evidence_event_candidate_parts/E-018.json", + "shard_doc_path": "evidence_shards/E-018.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 18, + "array_index": 17, + "doc_uid_candidate": "DOC-018", + "title_candidate": "evidence_all[18]", + "content_sha256": "6bf413db8de62698beb61b037b7b35f3c938bfd60672edf9a57c2032fc5e3ceb", + "shard_doc_path": "evidence_shards/E-018.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 18, + "__ordinal_label__": "018", + "__evidence_index_candidate__": "E-018", + "__b1_part_path__": "evidence_indexed_parts/E-018.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-018.json", + "__shard_doc_path__": "evidence_shards/E-018.json" + } + }, + { + "assigned_ordinal": 19, + "ordinal_label": "019", + "evidence_index_candidate": "E-019", + "b1_task_id": "Task_B1_map_doc_019", + "b2_task_id": "Task_B2_map_events_019", + "b1_output_path": "evidence_indexed_parts/E-019.json", + "b2_output_path": "evidence_event_candidate_parts/E-019.json", + "shard_doc_path": "evidence_shards/E-019.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 19, + "array_index": 18, + "doc_uid_candidate": "DOC-019", + "title_candidate": "evidence_all[19]", + "content_sha256": "46d579b0a1617ef1af226b3c89ee95004a498682c80fbaed96a7e30cf937b42c", + "shard_doc_path": "evidence_shards/E-019.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 19, + "__ordinal_label__": "019", + "__evidence_index_candidate__": "E-019", + "__b1_part_path__": "evidence_indexed_parts/E-019.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-019.json", + "__shard_doc_path__": "evidence_shards/E-019.json" + } + }, + { + "assigned_ordinal": 20, + "ordinal_label": "020", + "evidence_index_candidate": "E-020", + "b1_task_id": "Task_B1_map_doc_020", + "b2_task_id": "Task_B2_map_events_020", + "b1_output_path": "evidence_indexed_parts/E-020.json", + "b2_output_path": "evidence_event_candidate_parts/E-020.json", + "shard_doc_path": "evidence_shards/E-020.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 20, + "array_index": 19, + "doc_uid_candidate": "DOC-020", + "title_candidate": "evidence_all[20]", + "content_sha256": "a567a3cf7bb97692084614c65b3075d5960d9863512223b76fa042b37030d3ab", + "shard_doc_path": "evidence_shards/E-020.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 20, + "__ordinal_label__": "020", + "__evidence_index_candidate__": "E-020", + "__b1_part_path__": "evidence_indexed_parts/E-020.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-020.json", + "__shard_doc_path__": "evidence_shards/E-020.json" + } + }, + { + "assigned_ordinal": 21, + "ordinal_label": "021", + "evidence_index_candidate": "E-021", + "b1_task_id": "Task_B1_map_doc_021", + "b2_task_id": "Task_B2_map_events_021", + "b1_output_path": "evidence_indexed_parts/E-021.json", + "b2_output_path": "evidence_event_candidate_parts/E-021.json", + "shard_doc_path": "evidence_shards/E-021.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 21, + "array_index": 20, + "doc_uid_candidate": "DOC-021", + "title_candidate": "evidence_all[21]", + "content_sha256": "7a0839128deb0799256cfb7804a75be51790e8acfc93adc60ef6759f5fd7a837", + "shard_doc_path": "evidence_shards/E-021.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 21, + "__ordinal_label__": "021", + "__evidence_index_candidate__": "E-021", + "__b1_part_path__": "evidence_indexed_parts/E-021.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-021.json", + "__shard_doc_path__": "evidence_shards/E-021.json" + } + }, + { + "assigned_ordinal": 22, + "ordinal_label": "022", + "evidence_index_candidate": "E-022", + "b1_task_id": "Task_B1_map_doc_022", + "b2_task_id": "Task_B2_map_events_022", + "b1_output_path": "evidence_indexed_parts/E-022.json", + "b2_output_path": "evidence_event_candidate_parts/E-022.json", + "shard_doc_path": "evidence_shards/E-022.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 22, + "array_index": 21, + "doc_uid_candidate": "DOC-022", + "title_candidate": "evidence_all[22]", + "content_sha256": "e907302872e5723053da0b250e0a0c79817a79d04b4cee3b3e1ffd525665e160", + "shard_doc_path": "evidence_shards/E-022.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 22, + "__ordinal_label__": "022", + "__evidence_index_candidate__": "E-022", + "__b1_part_path__": "evidence_indexed_parts/E-022.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-022.json", + "__shard_doc_path__": "evidence_shards/E-022.json" + } + }, + { + "assigned_ordinal": 23, + "ordinal_label": "023", + "evidence_index_candidate": "E-023", + "b1_task_id": "Task_B1_map_doc_023", + "b2_task_id": "Task_B2_map_events_023", + "b1_output_path": "evidence_indexed_parts/E-023.json", + "b2_output_path": "evidence_event_candidate_parts/E-023.json", + "shard_doc_path": "evidence_shards/E-023.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 23, + "array_index": 22, + "doc_uid_candidate": "DOC-023", + "title_candidate": "evidence_all[23]", + "content_sha256": "d28d680e6ed050fd0b7aa4e315e3996f6cc917c77039132d16d221123dc506af", + "shard_doc_path": "evidence_shards/E-023.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 23, + "__ordinal_label__": "023", + "__evidence_index_candidate__": "E-023", + "__b1_part_path__": "evidence_indexed_parts/E-023.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-023.json", + "__shard_doc_path__": "evidence_shards/E-023.json" + } + }, + { + "assigned_ordinal": 24, + "ordinal_label": "024", + "evidence_index_candidate": "E-024", + "b1_task_id": "Task_B1_map_doc_024", + "b2_task_id": "Task_B2_map_events_024", + "b1_output_path": "evidence_indexed_parts/E-024.json", + "b2_output_path": "evidence_event_candidate_parts/E-024.json", + "shard_doc_path": "evidence_shards/E-024.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 24, + "array_index": 23, + "doc_uid_candidate": "DOC-024", + "title_candidate": "evidence_all[24]", + "content_sha256": "dfe2abd7b20962f77abf248cfcf340d7887e05dd6555de2a41d3aedd89f74e6e", + "shard_doc_path": "evidence_shards/E-024.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 24, + "__ordinal_label__": "024", + "__evidence_index_candidate__": "E-024", + "__b1_part_path__": "evidence_indexed_parts/E-024.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-024.json", + "__shard_doc_path__": "evidence_shards/E-024.json" + } + }, + { + "assigned_ordinal": 25, + "ordinal_label": "025", + "evidence_index_candidate": "E-025", + "b1_task_id": "Task_B1_map_doc_025", + "b2_task_id": "Task_B2_map_events_025", + "b1_output_path": "evidence_indexed_parts/E-025.json", + "b2_output_path": "evidence_event_candidate_parts/E-025.json", + "shard_doc_path": "evidence_shards/E-025.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 25, + "array_index": 24, + "doc_uid_candidate": "DOC-025", + "title_candidate": "evidence_all[25]", + "content_sha256": "fe06309a2adca6633d9588e53daa2f0d7bcae7f9090f270e408be9c7757faab0", + "shard_doc_path": "evidence_shards/E-025.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 25, + "__ordinal_label__": "025", + "__evidence_index_candidate__": "E-025", + "__b1_part_path__": "evidence_indexed_parts/E-025.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-025.json", + "__shard_doc_path__": "evidence_shards/E-025.json" + } + }, + { + "assigned_ordinal": 26, + "ordinal_label": "026", + "evidence_index_candidate": "E-026", + "b1_task_id": "Task_B1_map_doc_026", + "b2_task_id": "Task_B2_map_events_026", + "b1_output_path": "evidence_indexed_parts/E-026.json", + "b2_output_path": "evidence_event_candidate_parts/E-026.json", + "shard_doc_path": "evidence_shards/E-026.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 26, + "array_index": 25, + "doc_uid_candidate": "DOC-026", + "title_candidate": "evidence_all[26]", + "content_sha256": "095ced33786b6d5861630487bd387d2995bf27b787191ecd289c688ecc88587f", + "shard_doc_path": "evidence_shards/E-026.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 26, + "__ordinal_label__": "026", + "__evidence_index_candidate__": "E-026", + "__b1_part_path__": "evidence_indexed_parts/E-026.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-026.json", + "__shard_doc_path__": "evidence_shards/E-026.json" + } + }, + { + "assigned_ordinal": 27, + "ordinal_label": "027", + "evidence_index_candidate": "E-027", + "b1_task_id": "Task_B1_map_doc_027", + "b2_task_id": "Task_B2_map_events_027", + "b1_output_path": "evidence_indexed_parts/E-027.json", + "b2_output_path": "evidence_event_candidate_parts/E-027.json", + "shard_doc_path": "evidence_shards/E-027.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 27, + "array_index": 26, + "doc_uid_candidate": "DOC-027", + "title_candidate": "evidence_all[27]", + "content_sha256": "719cb5cf9c765d09070f22af546b9a9bc19a39b38ea0c8f674bf2af43f01770a", + "shard_doc_path": "evidence_shards/E-027.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 27, + "__ordinal_label__": "027", + "__evidence_index_candidate__": "E-027", + "__b1_part_path__": "evidence_indexed_parts/E-027.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-027.json", + "__shard_doc_path__": "evidence_shards/E-027.json" + } + }, + { + "assigned_ordinal": 28, + "ordinal_label": "028", + "evidence_index_candidate": "E-028", + "b1_task_id": "Task_B1_map_doc_028", + "b2_task_id": "Task_B2_map_events_028", + "b1_output_path": "evidence_indexed_parts/E-028.json", + "b2_output_path": "evidence_event_candidate_parts/E-028.json", + "shard_doc_path": "evidence_shards/E-028.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 28, + "array_index": 27, + "doc_uid_candidate": "DOC-028", + "title_candidate": "evidence_all[28]", + "content_sha256": "94a0445b12ff5fd201be4ea4e8678296f7d33f13ec5538bdd4f0067e6518a049", + "shard_doc_path": "evidence_shards/E-028.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 28, + "__ordinal_label__": "028", + "__evidence_index_candidate__": "E-028", + "__b1_part_path__": "evidence_indexed_parts/E-028.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-028.json", + "__shard_doc_path__": "evidence_shards/E-028.json" + } + }, + { + "assigned_ordinal": 29, + "ordinal_label": "029", + "evidence_index_candidate": "E-029", + "b1_task_id": "Task_B1_map_doc_029", + "b2_task_id": "Task_B2_map_events_029", + "b1_output_path": "evidence_indexed_parts/E-029.json", + "b2_output_path": "evidence_event_candidate_parts/E-029.json", + "shard_doc_path": "evidence_shards/E-029.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 29, + "array_index": 28, + "doc_uid_candidate": "DOC-029", + "title_candidate": "evidence_all[29]", + "content_sha256": "0161dfa6a44ea6a283d866626db3b7537d240e0d7f5b3b04a00803e893f8bfdc", + "shard_doc_path": "evidence_shards/E-029.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 29, + "__ordinal_label__": "029", + "__evidence_index_candidate__": "E-029", + "__b1_part_path__": "evidence_indexed_parts/E-029.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-029.json", + "__shard_doc_path__": "evidence_shards/E-029.json" + } + }, + { + "assigned_ordinal": 30, + "ordinal_label": "030", + "evidence_index_candidate": "E-030", + "b1_task_id": "Task_B1_map_doc_030", + "b2_task_id": "Task_B2_map_events_030", + "b1_output_path": "evidence_indexed_parts/E-030.json", + "b2_output_path": "evidence_event_candidate_parts/E-030.json", + "shard_doc_path": "evidence_shards/E-030.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 30, + "array_index": 29, + "doc_uid_candidate": "DOC-030", + "title_candidate": "evidence_all[30]", + "content_sha256": "3f655d616f6200333891b2522e68e7d03c2d30975dd12e9dc2e1a73ac1919edc", + "shard_doc_path": "evidence_shards/E-030.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 30, + "__ordinal_label__": "030", + "__evidence_index_candidate__": "E-030", + "__b1_part_path__": "evidence_indexed_parts/E-030.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-030.json", + "__shard_doc_path__": "evidence_shards/E-030.json" + } + } + ], + "dynamic_fanout": [ + { + "assigned_ordinal": 1, + "ordinal_label": "001", + "evidence_index_candidate": "E-001", + "b1_task_id": "Task_B1_map_doc_001", + "b2_task_id": "Task_B2_map_events_001", + "b1_output_path": "evidence_indexed_parts/E-001.json", + "b2_output_path": "evidence_event_candidate_parts/E-001.json", + "shard_doc_path": "evidence_shards/E-001.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 1, + "array_index": 0, + "doc_uid_candidate": "DOC-001", + "title_candidate": "evidence_all[1]", + "content_sha256": "4d0e6a0591acb2914019fec3b7903db5451a98f146fbe8dbc7cd8ef66e864c7a", + "shard_doc_path": "evidence_shards/E-001.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 1, + "__ordinal_label__": "001", + "__evidence_index_candidate__": "E-001", + "__b1_part_path__": "evidence_indexed_parts/E-001.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-001.json", + "__shard_doc_path__": "evidence_shards/E-001.json" + } + }, + { + "assigned_ordinal": 2, + "ordinal_label": "002", + "evidence_index_candidate": "E-002", + "b1_task_id": "Task_B1_map_doc_002", + "b2_task_id": "Task_B2_map_events_002", + "b1_output_path": "evidence_indexed_parts/E-002.json", + "b2_output_path": "evidence_event_candidate_parts/E-002.json", + "shard_doc_path": "evidence_shards/E-002.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 2, + "array_index": 1, + "doc_uid_candidate": "DOC-002", + "title_candidate": "evidence_all[2]", + "content_sha256": "b86edc5acd22e6167f3be49d3bfead67b5632e9c3a2c028e00854e45c68060a4", + "shard_doc_path": "evidence_shards/E-002.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 2, + "__ordinal_label__": "002", + "__evidence_index_candidate__": "E-002", + "__b1_part_path__": "evidence_indexed_parts/E-002.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-002.json", + "__shard_doc_path__": "evidence_shards/E-002.json" + } + }, + { + "assigned_ordinal": 3, + "ordinal_label": "003", + "evidence_index_candidate": "E-003", + "b1_task_id": "Task_B1_map_doc_003", + "b2_task_id": "Task_B2_map_events_003", + "b1_output_path": "evidence_indexed_parts/E-003.json", + "b2_output_path": "evidence_event_candidate_parts/E-003.json", + "shard_doc_path": "evidence_shards/E-003.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 3, + "array_index": 2, + "doc_uid_candidate": "DOC-003", + "title_candidate": "evidence_all[3]", + "content_sha256": "92e8ed6927ebe493083153083d6ed3fe85f3b41719750df957e63fa1192bfee7", + "shard_doc_path": "evidence_shards/E-003.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 3, + "__ordinal_label__": "003", + "__evidence_index_candidate__": "E-003", + "__b1_part_path__": "evidence_indexed_parts/E-003.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-003.json", + "__shard_doc_path__": "evidence_shards/E-003.json" + } + }, + { + "assigned_ordinal": 4, + "ordinal_label": "004", + "evidence_index_candidate": "E-004", + "b1_task_id": "Task_B1_map_doc_004", + "b2_task_id": "Task_B2_map_events_004", + "b1_output_path": "evidence_indexed_parts/E-004.json", + "b2_output_path": "evidence_event_candidate_parts/E-004.json", + "shard_doc_path": "evidence_shards/E-004.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 4, + "array_index": 3, + "doc_uid_candidate": "DOC-004", + "title_candidate": "evidence_all[4]", + "content_sha256": "720bf15b68ed74efbd370a635823e93ec49ff03bde2e0ca4de21a75c1636bff9", + "shard_doc_path": "evidence_shards/E-004.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 4, + "__ordinal_label__": "004", + "__evidence_index_candidate__": "E-004", + "__b1_part_path__": "evidence_indexed_parts/E-004.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-004.json", + "__shard_doc_path__": "evidence_shards/E-004.json" + } + }, + { + "assigned_ordinal": 5, + "ordinal_label": "005", + "evidence_index_candidate": "E-005", + "b1_task_id": "Task_B1_map_doc_005", + "b2_task_id": "Task_B2_map_events_005", + "b1_output_path": "evidence_indexed_parts/E-005.json", + "b2_output_path": "evidence_event_candidate_parts/E-005.json", + "shard_doc_path": "evidence_shards/E-005.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 5, + "array_index": 4, + "doc_uid_candidate": "DOC-005", + "title_candidate": "evidence_all[5]", + "content_sha256": "c249aaa55d584db081e42e3597f95dd0c41897ae9e28d4bc3366ab1c6ae300df", + "shard_doc_path": "evidence_shards/E-005.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 5, + "__ordinal_label__": "005", + "__evidence_index_candidate__": "E-005", + "__b1_part_path__": "evidence_indexed_parts/E-005.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-005.json", + "__shard_doc_path__": "evidence_shards/E-005.json" + } + }, + { + "assigned_ordinal": 6, + "ordinal_label": "006", + "evidence_index_candidate": "E-006", + "b1_task_id": "Task_B1_map_doc_006", + "b2_task_id": "Task_B2_map_events_006", + "b1_output_path": "evidence_indexed_parts/E-006.json", + "b2_output_path": "evidence_event_candidate_parts/E-006.json", + "shard_doc_path": "evidence_shards/E-006.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 6, + "array_index": 5, + "doc_uid_candidate": "DOC-006", + "title_candidate": "evidence_all[6]", + "content_sha256": "ddcda43c5dcc63a769aaa9195c46a4e8cffb950d96639eef709063d495c6142b", + "shard_doc_path": "evidence_shards/E-006.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 6, + "__ordinal_label__": "006", + "__evidence_index_candidate__": "E-006", + "__b1_part_path__": "evidence_indexed_parts/E-006.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-006.json", + "__shard_doc_path__": "evidence_shards/E-006.json" + } + }, + { + "assigned_ordinal": 7, + "ordinal_label": "007", + "evidence_index_candidate": "E-007", + "b1_task_id": "Task_B1_map_doc_007", + "b2_task_id": "Task_B2_map_events_007", + "b1_output_path": "evidence_indexed_parts/E-007.json", + "b2_output_path": "evidence_event_candidate_parts/E-007.json", + "shard_doc_path": "evidence_shards/E-007.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 7, + "array_index": 6, + "doc_uid_candidate": "DOC-007", + "title_candidate": "evidence_all[7]", + "content_sha256": "d9720e409a380cd96d57f75b9ec2e452c8c25da285847dec3c348e13f84798c3", + "shard_doc_path": "evidence_shards/E-007.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 7, + "__ordinal_label__": "007", + "__evidence_index_candidate__": "E-007", + "__b1_part_path__": "evidence_indexed_parts/E-007.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-007.json", + "__shard_doc_path__": "evidence_shards/E-007.json" + } + }, + { + "assigned_ordinal": 8, + "ordinal_label": "008", + "evidence_index_candidate": "E-008", + "b1_task_id": "Task_B1_map_doc_008", + "b2_task_id": "Task_B2_map_events_008", + "b1_output_path": "evidence_indexed_parts/E-008.json", + "b2_output_path": "evidence_event_candidate_parts/E-008.json", + "shard_doc_path": "evidence_shards/E-008.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 8, + "array_index": 7, + "doc_uid_candidate": "DOC-008", + "title_candidate": "evidence_all[8]", + "content_sha256": "4d83e1fb28584e0568e7034dd2c796b0ca4c05c991701d972aa2cbe32b23290c", + "shard_doc_path": "evidence_shards/E-008.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 8, + "__ordinal_label__": "008", + "__evidence_index_candidate__": "E-008", + "__b1_part_path__": "evidence_indexed_parts/E-008.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-008.json", + "__shard_doc_path__": "evidence_shards/E-008.json" + } + }, + { + "assigned_ordinal": 9, + "ordinal_label": "009", + "evidence_index_candidate": "E-009", + "b1_task_id": "Task_B1_map_doc_009", + "b2_task_id": "Task_B2_map_events_009", + "b1_output_path": "evidence_indexed_parts/E-009.json", + "b2_output_path": "evidence_event_candidate_parts/E-009.json", + "shard_doc_path": "evidence_shards/E-009.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 9, + "array_index": 8, + "doc_uid_candidate": "DOC-009", + "title_candidate": "evidence_all[9]", + "content_sha256": "580c40fc58e2033779715b0a455de4b7b5be9c8ccde29cfbf37c49e7ff097c5a", + "shard_doc_path": "evidence_shards/E-009.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 9, + "__ordinal_label__": "009", + "__evidence_index_candidate__": "E-009", + "__b1_part_path__": "evidence_indexed_parts/E-009.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-009.json", + "__shard_doc_path__": "evidence_shards/E-009.json" + } + }, + { + "assigned_ordinal": 10, + "ordinal_label": "010", + "evidence_index_candidate": "E-010", + "b1_task_id": "Task_B1_map_doc_010", + "b2_task_id": "Task_B2_map_events_010", + "b1_output_path": "evidence_indexed_parts/E-010.json", + "b2_output_path": "evidence_event_candidate_parts/E-010.json", + "shard_doc_path": "evidence_shards/E-010.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 10, + "array_index": 9, + "doc_uid_candidate": "DOC-010", + "title_candidate": "evidence_all[10]", + "content_sha256": "94267643f63a855d3ec82d24d4da54b0660a8c5249df3997b195b4a5fbf9ade0", + "shard_doc_path": "evidence_shards/E-010.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 10, + "__ordinal_label__": "010", + "__evidence_index_candidate__": "E-010", + "__b1_part_path__": "evidence_indexed_parts/E-010.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-010.json", + "__shard_doc_path__": "evidence_shards/E-010.json" + } + }, + { + "assigned_ordinal": 11, + "ordinal_label": "011", + "evidence_index_candidate": "E-011", + "b1_task_id": "Task_B1_map_doc_011", + "b2_task_id": "Task_B2_map_events_011", + "b1_output_path": "evidence_indexed_parts/E-011.json", + "b2_output_path": "evidence_event_candidate_parts/E-011.json", + "shard_doc_path": "evidence_shards/E-011.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 11, + "array_index": 10, + "doc_uid_candidate": "DOC-011", + "title_candidate": "evidence_all[11]", + "content_sha256": "ca5da2134ff2aead725e5c44f429ecc202da584c4b4879e8cd73a390e53818f6", + "shard_doc_path": "evidence_shards/E-011.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 11, + "__ordinal_label__": "011", + "__evidence_index_candidate__": "E-011", + "__b1_part_path__": "evidence_indexed_parts/E-011.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-011.json", + "__shard_doc_path__": "evidence_shards/E-011.json" + } + }, + { + "assigned_ordinal": 12, + "ordinal_label": "012", + "evidence_index_candidate": "E-012", + "b1_task_id": "Task_B1_map_doc_012", + "b2_task_id": "Task_B2_map_events_012", + "b1_output_path": "evidence_indexed_parts/E-012.json", + "b2_output_path": "evidence_event_candidate_parts/E-012.json", + "shard_doc_path": "evidence_shards/E-012.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 12, + "array_index": 11, + "doc_uid_candidate": "DOC-012", + "title_candidate": "evidence_all[12]", + "content_sha256": "e1d22418d8c3b053c2bf35d199a1e3a65db7f3ad3a2cfb675f52105d294d71b3", + "shard_doc_path": "evidence_shards/E-012.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 12, + "__ordinal_label__": "012", + "__evidence_index_candidate__": "E-012", + "__b1_part_path__": "evidence_indexed_parts/E-012.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-012.json", + "__shard_doc_path__": "evidence_shards/E-012.json" + } + }, + { + "assigned_ordinal": 13, + "ordinal_label": "013", + "evidence_index_candidate": "E-013", + "b1_task_id": "Task_B1_map_doc_013", + "b2_task_id": "Task_B2_map_events_013", + "b1_output_path": "evidence_indexed_parts/E-013.json", + "b2_output_path": "evidence_event_candidate_parts/E-013.json", + "shard_doc_path": "evidence_shards/E-013.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 13, + "array_index": 12, + "doc_uid_candidate": "DOC-013", + "title_candidate": "evidence_all[13]", + "content_sha256": "f8c1e817957626716993941b82cdd6e6f793ccba2fd74a1e5ef6b7087120a076", + "shard_doc_path": "evidence_shards/E-013.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 13, + "__ordinal_label__": "013", + "__evidence_index_candidate__": "E-013", + "__b1_part_path__": "evidence_indexed_parts/E-013.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-013.json", + "__shard_doc_path__": "evidence_shards/E-013.json" + } + }, + { + "assigned_ordinal": 14, + "ordinal_label": "014", + "evidence_index_candidate": "E-014", + "b1_task_id": "Task_B1_map_doc_014", + "b2_task_id": "Task_B2_map_events_014", + "b1_output_path": "evidence_indexed_parts/E-014.json", + "b2_output_path": "evidence_event_candidate_parts/E-014.json", + "shard_doc_path": "evidence_shards/E-014.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 14, + "array_index": 13, + "doc_uid_candidate": "DOC-014", + "title_candidate": "evidence_all[14]", + "content_sha256": "d934284c54106a78e75e82dd15aeecf462ac385160a72ab5dad63531739ee33e", + "shard_doc_path": "evidence_shards/E-014.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 14, + "__ordinal_label__": "014", + "__evidence_index_candidate__": "E-014", + "__b1_part_path__": "evidence_indexed_parts/E-014.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-014.json", + "__shard_doc_path__": "evidence_shards/E-014.json" + } + }, + { + "assigned_ordinal": 15, + "ordinal_label": "015", + "evidence_index_candidate": "E-015", + "b1_task_id": "Task_B1_map_doc_015", + "b2_task_id": "Task_B2_map_events_015", + "b1_output_path": "evidence_indexed_parts/E-015.json", + "b2_output_path": "evidence_event_candidate_parts/E-015.json", + "shard_doc_path": "evidence_shards/E-015.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 15, + "array_index": 14, + "doc_uid_candidate": "DOC-015", + "title_candidate": "evidence_all[15]", + "content_sha256": "531066b36bb947d5505e9226d5d2d9e3d7c2fa18d88ef42157dbf1b843d06af9", + "shard_doc_path": "evidence_shards/E-015.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 15, + "__ordinal_label__": "015", + "__evidence_index_candidate__": "E-015", + "__b1_part_path__": "evidence_indexed_parts/E-015.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-015.json", + "__shard_doc_path__": "evidence_shards/E-015.json" + } + }, + { + "assigned_ordinal": 16, + "ordinal_label": "016", + "evidence_index_candidate": "E-016", + "b1_task_id": "Task_B1_map_doc_016", + "b2_task_id": "Task_B2_map_events_016", + "b1_output_path": "evidence_indexed_parts/E-016.json", + "b2_output_path": "evidence_event_candidate_parts/E-016.json", + "shard_doc_path": "evidence_shards/E-016.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 16, + "array_index": 15, + "doc_uid_candidate": "DOC-016", + "title_candidate": "evidence_all[16]", + "content_sha256": "b184bfcc5f473bd3d5abf8b6d6e37eb35edc09deaa56b99b08ee288e965bd924", + "shard_doc_path": "evidence_shards/E-016.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 16, + "__ordinal_label__": "016", + "__evidence_index_candidate__": "E-016", + "__b1_part_path__": "evidence_indexed_parts/E-016.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-016.json", + "__shard_doc_path__": "evidence_shards/E-016.json" + } + }, + { + "assigned_ordinal": 17, + "ordinal_label": "017", + "evidence_index_candidate": "E-017", + "b1_task_id": "Task_B1_map_doc_017", + "b2_task_id": "Task_B2_map_events_017", + "b1_output_path": "evidence_indexed_parts/E-017.json", + "b2_output_path": "evidence_event_candidate_parts/E-017.json", + "shard_doc_path": "evidence_shards/E-017.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 17, + "array_index": 16, + "doc_uid_candidate": "DOC-017", + "title_candidate": "evidence_all[17]", + "content_sha256": "cdbff127d01067e9df8596972134466d750b4e912f75f20f2cd18d46abb2d07d", + "shard_doc_path": "evidence_shards/E-017.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 17, + "__ordinal_label__": "017", + "__evidence_index_candidate__": "E-017", + "__b1_part_path__": "evidence_indexed_parts/E-017.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-017.json", + "__shard_doc_path__": "evidence_shards/E-017.json" + } + }, + { + "assigned_ordinal": 18, + "ordinal_label": "018", + "evidence_index_candidate": "E-018", + "b1_task_id": "Task_B1_map_doc_018", + "b2_task_id": "Task_B2_map_events_018", + "b1_output_path": "evidence_indexed_parts/E-018.json", + "b2_output_path": "evidence_event_candidate_parts/E-018.json", + "shard_doc_path": "evidence_shards/E-018.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 18, + "array_index": 17, + "doc_uid_candidate": "DOC-018", + "title_candidate": "evidence_all[18]", + "content_sha256": "6bf413db8de62698beb61b037b7b35f3c938bfd60672edf9a57c2032fc5e3ceb", + "shard_doc_path": "evidence_shards/E-018.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 18, + "__ordinal_label__": "018", + "__evidence_index_candidate__": "E-018", + "__b1_part_path__": "evidence_indexed_parts/E-018.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-018.json", + "__shard_doc_path__": "evidence_shards/E-018.json" + } + }, + { + "assigned_ordinal": 19, + "ordinal_label": "019", + "evidence_index_candidate": "E-019", + "b1_task_id": "Task_B1_map_doc_019", + "b2_task_id": "Task_B2_map_events_019", + "b1_output_path": "evidence_indexed_parts/E-019.json", + "b2_output_path": "evidence_event_candidate_parts/E-019.json", + "shard_doc_path": "evidence_shards/E-019.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 19, + "array_index": 18, + "doc_uid_candidate": "DOC-019", + "title_candidate": "evidence_all[19]", + "content_sha256": "46d579b0a1617ef1af226b3c89ee95004a498682c80fbaed96a7e30cf937b42c", + "shard_doc_path": "evidence_shards/E-019.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 19, + "__ordinal_label__": "019", + "__evidence_index_candidate__": "E-019", + "__b1_part_path__": "evidence_indexed_parts/E-019.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-019.json", + "__shard_doc_path__": "evidence_shards/E-019.json" + } + }, + { + "assigned_ordinal": 20, + "ordinal_label": "020", + "evidence_index_candidate": "E-020", + "b1_task_id": "Task_B1_map_doc_020", + "b2_task_id": "Task_B2_map_events_020", + "b1_output_path": "evidence_indexed_parts/E-020.json", + "b2_output_path": "evidence_event_candidate_parts/E-020.json", + "shard_doc_path": "evidence_shards/E-020.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 20, + "array_index": 19, + "doc_uid_candidate": "DOC-020", + "title_candidate": "evidence_all[20]", + "content_sha256": "a567a3cf7bb97692084614c65b3075d5960d9863512223b76fa042b37030d3ab", + "shard_doc_path": "evidence_shards/E-020.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 20, + "__ordinal_label__": "020", + "__evidence_index_candidate__": "E-020", + "__b1_part_path__": "evidence_indexed_parts/E-020.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-020.json", + "__shard_doc_path__": "evidence_shards/E-020.json" + } + }, + { + "assigned_ordinal": 21, + "ordinal_label": "021", + "evidence_index_candidate": "E-021", + "b1_task_id": "Task_B1_map_doc_021", + "b2_task_id": "Task_B2_map_events_021", + "b1_output_path": "evidence_indexed_parts/E-021.json", + "b2_output_path": "evidence_event_candidate_parts/E-021.json", + "shard_doc_path": "evidence_shards/E-021.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 21, + "array_index": 20, + "doc_uid_candidate": "DOC-021", + "title_candidate": "evidence_all[21]", + "content_sha256": "7a0839128deb0799256cfb7804a75be51790e8acfc93adc60ef6759f5fd7a837", + "shard_doc_path": "evidence_shards/E-021.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 21, + "__ordinal_label__": "021", + "__evidence_index_candidate__": "E-021", + "__b1_part_path__": "evidence_indexed_parts/E-021.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-021.json", + "__shard_doc_path__": "evidence_shards/E-021.json" + } + }, + { + "assigned_ordinal": 22, + "ordinal_label": "022", + "evidence_index_candidate": "E-022", + "b1_task_id": "Task_B1_map_doc_022", + "b2_task_id": "Task_B2_map_events_022", + "b1_output_path": "evidence_indexed_parts/E-022.json", + "b2_output_path": "evidence_event_candidate_parts/E-022.json", + "shard_doc_path": "evidence_shards/E-022.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 22, + "array_index": 21, + "doc_uid_candidate": "DOC-022", + "title_candidate": "evidence_all[22]", + "content_sha256": "e907302872e5723053da0b250e0a0c79817a79d04b4cee3b3e1ffd525665e160", + "shard_doc_path": "evidence_shards/E-022.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 22, + "__ordinal_label__": "022", + "__evidence_index_candidate__": "E-022", + "__b1_part_path__": "evidence_indexed_parts/E-022.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-022.json", + "__shard_doc_path__": "evidence_shards/E-022.json" + } + }, + { + "assigned_ordinal": 23, + "ordinal_label": "023", + "evidence_index_candidate": "E-023", + "b1_task_id": "Task_B1_map_doc_023", + "b2_task_id": "Task_B2_map_events_023", + "b1_output_path": "evidence_indexed_parts/E-023.json", + "b2_output_path": "evidence_event_candidate_parts/E-023.json", + "shard_doc_path": "evidence_shards/E-023.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 23, + "array_index": 22, + "doc_uid_candidate": "DOC-023", + "title_candidate": "evidence_all[23]", + "content_sha256": "d28d680e6ed050fd0b7aa4e315e3996f6cc917c77039132d16d221123dc506af", + "shard_doc_path": "evidence_shards/E-023.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 23, + "__ordinal_label__": "023", + "__evidence_index_candidate__": "E-023", + "__b1_part_path__": "evidence_indexed_parts/E-023.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-023.json", + "__shard_doc_path__": "evidence_shards/E-023.json" + } + }, + { + "assigned_ordinal": 24, + "ordinal_label": "024", + "evidence_index_candidate": "E-024", + "b1_task_id": "Task_B1_map_doc_024", + "b2_task_id": "Task_B2_map_events_024", + "b1_output_path": "evidence_indexed_parts/E-024.json", + "b2_output_path": "evidence_event_candidate_parts/E-024.json", + "shard_doc_path": "evidence_shards/E-024.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 24, + "array_index": 23, + "doc_uid_candidate": "DOC-024", + "title_candidate": "evidence_all[24]", + "content_sha256": "dfe2abd7b20962f77abf248cfcf340d7887e05dd6555de2a41d3aedd89f74e6e", + "shard_doc_path": "evidence_shards/E-024.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 24, + "__ordinal_label__": "024", + "__evidence_index_candidate__": "E-024", + "__b1_part_path__": "evidence_indexed_parts/E-024.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-024.json", + "__shard_doc_path__": "evidence_shards/E-024.json" + } + }, + { + "assigned_ordinal": 25, + "ordinal_label": "025", + "evidence_index_candidate": "E-025", + "b1_task_id": "Task_B1_map_doc_025", + "b2_task_id": "Task_B2_map_events_025", + "b1_output_path": "evidence_indexed_parts/E-025.json", + "b2_output_path": "evidence_event_candidate_parts/E-025.json", + "shard_doc_path": "evidence_shards/E-025.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 25, + "array_index": 24, + "doc_uid_candidate": "DOC-025", + "title_candidate": "evidence_all[25]", + "content_sha256": "fe06309a2adca6633d9588e53daa2f0d7bcae7f9090f270e408be9c7757faab0", + "shard_doc_path": "evidence_shards/E-025.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 25, + "__ordinal_label__": "025", + "__evidence_index_candidate__": "E-025", + "__b1_part_path__": "evidence_indexed_parts/E-025.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-025.json", + "__shard_doc_path__": "evidence_shards/E-025.json" + } + }, + { + "assigned_ordinal": 26, + "ordinal_label": "026", + "evidence_index_candidate": "E-026", + "b1_task_id": "Task_B1_map_doc_026", + "b2_task_id": "Task_B2_map_events_026", + "b1_output_path": "evidence_indexed_parts/E-026.json", + "b2_output_path": "evidence_event_candidate_parts/E-026.json", + "shard_doc_path": "evidence_shards/E-026.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 26, + "array_index": 25, + "doc_uid_candidate": "DOC-026", + "title_candidate": "evidence_all[26]", + "content_sha256": "095ced33786b6d5861630487bd387d2995bf27b787191ecd289c688ecc88587f", + "shard_doc_path": "evidence_shards/E-026.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 26, + "__ordinal_label__": "026", + "__evidence_index_candidate__": "E-026", + "__b1_part_path__": "evidence_indexed_parts/E-026.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-026.json", + "__shard_doc_path__": "evidence_shards/E-026.json" + } + }, + { + "assigned_ordinal": 27, + "ordinal_label": "027", + "evidence_index_candidate": "E-027", + "b1_task_id": "Task_B1_map_doc_027", + "b2_task_id": "Task_B2_map_events_027", + "b1_output_path": "evidence_indexed_parts/E-027.json", + "b2_output_path": "evidence_event_candidate_parts/E-027.json", + "shard_doc_path": "evidence_shards/E-027.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 27, + "array_index": 26, + "doc_uid_candidate": "DOC-027", + "title_candidate": "evidence_all[27]", + "content_sha256": "719cb5cf9c765d09070f22af546b9a9bc19a39b38ea0c8f674bf2af43f01770a", + "shard_doc_path": "evidence_shards/E-027.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 27, + "__ordinal_label__": "027", + "__evidence_index_candidate__": "E-027", + "__b1_part_path__": "evidence_indexed_parts/E-027.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-027.json", + "__shard_doc_path__": "evidence_shards/E-027.json" + } + }, + { + "assigned_ordinal": 28, + "ordinal_label": "028", + "evidence_index_candidate": "E-028", + "b1_task_id": "Task_B1_map_doc_028", + "b2_task_id": "Task_B2_map_events_028", + "b1_output_path": "evidence_indexed_parts/E-028.json", + "b2_output_path": "evidence_event_candidate_parts/E-028.json", + "shard_doc_path": "evidence_shards/E-028.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 28, + "array_index": 27, + "doc_uid_candidate": "DOC-028", + "title_candidate": "evidence_all[28]", + "content_sha256": "94a0445b12ff5fd201be4ea4e8678296f7d33f13ec5538bdd4f0067e6518a049", + "shard_doc_path": "evidence_shards/E-028.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 28, + "__ordinal_label__": "028", + "__evidence_index_candidate__": "E-028", + "__b1_part_path__": "evidence_indexed_parts/E-028.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-028.json", + "__shard_doc_path__": "evidence_shards/E-028.json" + } + }, + { + "assigned_ordinal": 29, + "ordinal_label": "029", + "evidence_index_candidate": "E-029", + "b1_task_id": "Task_B1_map_doc_029", + "b2_task_id": "Task_B2_map_events_029", + "b1_output_path": "evidence_indexed_parts/E-029.json", + "b2_output_path": "evidence_event_candidate_parts/E-029.json", + "shard_doc_path": "evidence_shards/E-029.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 29, + "array_index": 28, + "doc_uid_candidate": "DOC-029", + "title_candidate": "evidence_all[29]", + "content_sha256": "0161dfa6a44ea6a283d866626db3b7537d240e0d7f5b3b04a00803e893f8bfdc", + "shard_doc_path": "evidence_shards/E-029.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 29, + "__ordinal_label__": "029", + "__evidence_index_candidate__": "E-029", + "__b1_part_path__": "evidence_indexed_parts/E-029.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-029.json", + "__shard_doc_path__": "evidence_shards/E-029.json" + } + }, + { + "assigned_ordinal": 30, + "ordinal_label": "030", + "evidence_index_candidate": "E-030", + "b1_task_id": "Task_B1_map_doc_030", + "b2_task_id": "Task_B2_map_events_030", + "b1_output_path": "evidence_indexed_parts/E-030.json", + "b2_output_path": "evidence_event_candidate_parts/E-030.json", + "shard_doc_path": "evidence_shards/E-030.json", + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": 30, + "array_index": 29, + "doc_uid_candidate": "DOC-030", + "title_candidate": "evidence_all[30]", + "content_sha256": "3f655d616f6200333891b2522e68e7d03c2d30975dd12e9dc2e1a73ac1919edc", + "shard_doc_path": "evidence_shards/E-030.json" + }, + "runtime_bindings": { + "__assigned_ordinal__": 30, + "__ordinal_label__": "030", + "__evidence_index_candidate__": "E-030", + "__b1_part_path__": "evidence_indexed_parts/E-030.json", + "__b2_part_path__": "evidence_event_candidate_parts/E-030.json", + "__shard_doc_path__": "evidence_shards/E-030.json" + } + } + ], + "fanout_meta": [ + { + "id": "B1_evidence_doc_fanout", + "count": 30, + "task_template": "Task_B1_map_doc", + "instance_id_template": "Task_B1_map_doc_{ordinal:03d}", + "output_path_template": "evidence_indexed_parts/E-{ordinal:03d}.json" + }, + { + "id": "B2_event_candidate_fanout", + "count": 30, + "task_template": "Task_B2_map_events", + "instance_id_template": "Task_B2_map_events_{ordinal:03d}", + "output_path_template": "evidence_event_candidate_parts/E-{ordinal:03d}.json", + "wait_until_template": "Task_B1_map_doc_{ordinal:03d}" + } + ], + "streaming_edges": [ + { + "from": "Task_B1_map_doc_001", + "to": "Task_B2_map_events_001", + "condition": { + "file_exists": "evidence_indexed_parts/E-001.json" + }, + "bind": { + "__assigned_ordinal__": 1 + } + }, + { + "from": "Task_B1_map_doc_002", + "to": "Task_B2_map_events_002", + "condition": { + "file_exists": "evidence_indexed_parts/E-002.json" + }, + "bind": { + "__assigned_ordinal__": 2 + } + }, + { + "from": "Task_B1_map_doc_003", + "to": "Task_B2_map_events_003", + "condition": { + "file_exists": "evidence_indexed_parts/E-003.json" + }, + "bind": { + "__assigned_ordinal__": 3 + } + }, + { + "from": "Task_B1_map_doc_004", + "to": "Task_B2_map_events_004", + "condition": { + "file_exists": "evidence_indexed_parts/E-004.json" + }, + "bind": { + "__assigned_ordinal__": 4 + } + }, + { + "from": "Task_B1_map_doc_005", + "to": "Task_B2_map_events_005", + "condition": { + "file_exists": "evidence_indexed_parts/E-005.json" + }, + "bind": { + "__assigned_ordinal__": 5 + } + }, + { + "from": "Task_B1_map_doc_006", + "to": "Task_B2_map_events_006", + "condition": { + "file_exists": "evidence_indexed_parts/E-006.json" + }, + "bind": { + "__assigned_ordinal__": 6 + } + }, + { + "from": "Task_B1_map_doc_007", + "to": "Task_B2_map_events_007", + "condition": { + "file_exists": "evidence_indexed_parts/E-007.json" + }, + "bind": { + "__assigned_ordinal__": 7 + } + }, + { + "from": "Task_B1_map_doc_008", + "to": "Task_B2_map_events_008", + "condition": { + "file_exists": "evidence_indexed_parts/E-008.json" + }, + "bind": { + "__assigned_ordinal__": 8 + } + }, + { + "from": "Task_B1_map_doc_009", + "to": "Task_B2_map_events_009", + "condition": { + "file_exists": "evidence_indexed_parts/E-009.json" + }, + "bind": { + "__assigned_ordinal__": 9 + } + }, + { + "from": "Task_B1_map_doc_010", + "to": "Task_B2_map_events_010", + "condition": { + "file_exists": "evidence_indexed_parts/E-010.json" + }, + "bind": { + "__assigned_ordinal__": 10 + } + }, + { + "from": "Task_B1_map_doc_011", + "to": "Task_B2_map_events_011", + "condition": { + "file_exists": "evidence_indexed_parts/E-011.json" + }, + "bind": { + "__assigned_ordinal__": 11 + } + }, + { + "from": "Task_B1_map_doc_012", + "to": "Task_B2_map_events_012", + "condition": { + "file_exists": "evidence_indexed_parts/E-012.json" + }, + "bind": { + "__assigned_ordinal__": 12 + } + }, + { + "from": "Task_B1_map_doc_013", + "to": "Task_B2_map_events_013", + "condition": { + "file_exists": "evidence_indexed_parts/E-013.json" + }, + "bind": { + "__assigned_ordinal__": 13 + } + }, + { + "from": "Task_B1_map_doc_014", + "to": "Task_B2_map_events_014", + "condition": { + "file_exists": "evidence_indexed_parts/E-014.json" + }, + "bind": { + "__assigned_ordinal__": 14 + } + }, + { + "from": "Task_B1_map_doc_015", + "to": "Task_B2_map_events_015", + "condition": { + "file_exists": "evidence_indexed_parts/E-015.json" + }, + "bind": { + "__assigned_ordinal__": 15 + } + }, + { + "from": "Task_B1_map_doc_016", + "to": "Task_B2_map_events_016", + "condition": { + "file_exists": "evidence_indexed_parts/E-016.json" + }, + "bind": { + "__assigned_ordinal__": 16 + } + }, + { + "from": "Task_B1_map_doc_017", + "to": "Task_B2_map_events_017", + "condition": { + "file_exists": "evidence_indexed_parts/E-017.json" + }, + "bind": { + "__assigned_ordinal__": 17 + } + }, + { + "from": "Task_B1_map_doc_018", + "to": "Task_B2_map_events_018", + "condition": { + "file_exists": "evidence_indexed_parts/E-018.json" + }, + "bind": { + "__assigned_ordinal__": 18 + } + }, + { + "from": "Task_B1_map_doc_019", + "to": "Task_B2_map_events_019", + "condition": { + "file_exists": "evidence_indexed_parts/E-019.json" + }, + "bind": { + "__assigned_ordinal__": 19 + } + }, + { + "from": "Task_B1_map_doc_020", + "to": "Task_B2_map_events_020", + "condition": { + "file_exists": "evidence_indexed_parts/E-020.json" + }, + "bind": { + "__assigned_ordinal__": 20 + } + }, + { + "from": "Task_B1_map_doc_021", + "to": "Task_B2_map_events_021", + "condition": { + "file_exists": "evidence_indexed_parts/E-021.json" + }, + "bind": { + "__assigned_ordinal__": 21 + } + }, + { + "from": "Task_B1_map_doc_022", + "to": "Task_B2_map_events_022", + "condition": { + "file_exists": "evidence_indexed_parts/E-022.json" + }, + "bind": { + "__assigned_ordinal__": 22 + } + }, + { + "from": "Task_B1_map_doc_023", + "to": "Task_B2_map_events_023", + "condition": { + "file_exists": "evidence_indexed_parts/E-023.json" + }, + "bind": { + "__assigned_ordinal__": 23 + } + }, + { + "from": "Task_B1_map_doc_024", + "to": "Task_B2_map_events_024", + "condition": { + "file_exists": "evidence_indexed_parts/E-024.json" + }, + "bind": { + "__assigned_ordinal__": 24 + } + }, + { + "from": "Task_B1_map_doc_025", + "to": "Task_B2_map_events_025", + "condition": { + "file_exists": "evidence_indexed_parts/E-025.json" + }, + "bind": { + "__assigned_ordinal__": 25 + } + }, + { + "from": "Task_B1_map_doc_026", + "to": "Task_B2_map_events_026", + "condition": { + "file_exists": "evidence_indexed_parts/E-026.json" + }, + "bind": { + "__assigned_ordinal__": 26 + } + }, + { + "from": "Task_B1_map_doc_027", + "to": "Task_B2_map_events_027", + "condition": { + "file_exists": "evidence_indexed_parts/E-027.json" + }, + "bind": { + "__assigned_ordinal__": 27 + } + }, + { + "from": "Task_B1_map_doc_028", + "to": "Task_B2_map_events_028", + "condition": { + "file_exists": "evidence_indexed_parts/E-028.json" + }, + "bind": { + "__assigned_ordinal__": 28 + } + }, + { + "from": "Task_B1_map_doc_029", + "to": "Task_B2_map_events_029", + "condition": { + "file_exists": "evidence_indexed_parts/E-029.json" + }, + "bind": { + "__assigned_ordinal__": 29 + } + }, + { + "from": "Task_B1_map_doc_030", + "to": "Task_B2_map_events_030", + "condition": { + "file_exists": "evidence_indexed_parts/E-030.json" + }, + "bind": { + "__assigned_ordinal__": 30 + } + } + ], + "barriers": { + "B1_all_parts_barrier": { + "wait_for_tasks": [ + "Task_B1_map_doc_001", + "Task_B1_map_doc_002", + "Task_B1_map_doc_003", + "Task_B1_map_doc_004", + "Task_B1_map_doc_005", + "Task_B1_map_doc_006", + "Task_B1_map_doc_007", + "Task_B1_map_doc_008", + "Task_B1_map_doc_009", + "Task_B1_map_doc_010", + "Task_B1_map_doc_011", + "Task_B1_map_doc_012", + "Task_B1_map_doc_013", + "Task_B1_map_doc_014", + "Task_B1_map_doc_015", + "Task_B1_map_doc_016", + "Task_B1_map_doc_017", + "Task_B1_map_doc_018", + "Task_B1_map_doc_019", + "Task_B1_map_doc_020", + "Task_B1_map_doc_021", + "Task_B1_map_doc_022", + "Task_B1_map_doc_023", + "Task_B1_map_doc_024", + "Task_B1_map_doc_025", + "Task_B1_map_doc_026", + "Task_B1_map_doc_027", + "Task_B1_map_doc_028", + "Task_B1_map_doc_029", + "Task_B1_map_doc_030" + ], + "require_files": [ + "evidence_indexed_parts/E-001.json", + "evidence_indexed_parts/E-002.json", + "evidence_indexed_parts/E-003.json", + "evidence_indexed_parts/E-004.json", + "evidence_indexed_parts/E-005.json", + "evidence_indexed_parts/E-006.json", + "evidence_indexed_parts/E-007.json", + "evidence_indexed_parts/E-008.json", + "evidence_indexed_parts/E-009.json", + "evidence_indexed_parts/E-010.json", + "evidence_indexed_parts/E-011.json", + "evidence_indexed_parts/E-012.json", + "evidence_indexed_parts/E-013.json", + "evidence_indexed_parts/E-014.json", + "evidence_indexed_parts/E-015.json", + "evidence_indexed_parts/E-016.json", + "evidence_indexed_parts/E-017.json", + "evidence_indexed_parts/E-018.json", + "evidence_indexed_parts/E-019.json", + "evidence_indexed_parts/E-020.json", + "evidence_indexed_parts/E-021.json", + "evidence_indexed_parts/E-022.json", + "evidence_indexed_parts/E-023.json", + "evidence_indexed_parts/E-024.json", + "evidence_indexed_parts/E-025.json", + "evidence_indexed_parts/E-026.json", + "evidence_indexed_parts/E-027.json", + "evidence_indexed_parts/E-028.json", + "evidence_indexed_parts/E-029.json", + "evidence_indexed_parts/E-030.json" + ], + "next": "Task_B1_quality_gate_evidence_indexed" + }, + "B2_all_parts_barrier": { + "wait_for_tasks": [ + "Task_B2_map_events_001", + "Task_B2_map_events_002", + "Task_B2_map_events_003", + "Task_B2_map_events_004", + "Task_B2_map_events_005", + "Task_B2_map_events_006", + "Task_B2_map_events_007", + "Task_B2_map_events_008", + "Task_B2_map_events_009", + "Task_B2_map_events_010", + "Task_B2_map_events_011", + "Task_B2_map_events_012", + "Task_B2_map_events_013", + "Task_B2_map_events_014", + "Task_B2_map_events_015", + "Task_B2_map_events_016", + "Task_B2_map_events_017", + "Task_B2_map_events_018", + "Task_B2_map_events_019", + "Task_B2_map_events_020", + "Task_B2_map_events_021", + "Task_B2_map_events_022", + "Task_B2_map_events_023", + "Task_B2_map_events_024", + "Task_B2_map_events_025", + "Task_B2_map_events_026", + "Task_B2_map_events_027", + "Task_B2_map_events_028", + "Task_B2_map_events_029", + "Task_B2_map_events_030" + ], + "require_files": [ + "evidence_event_candidate_parts/E-001.json", + "evidence_event_candidate_parts/E-002.json", + "evidence_event_candidate_parts/E-003.json", + "evidence_event_candidate_parts/E-004.json", + "evidence_event_candidate_parts/E-005.json", + "evidence_event_candidate_parts/E-006.json", + "evidence_event_candidate_parts/E-007.json", + "evidence_event_candidate_parts/E-008.json", + "evidence_event_candidate_parts/E-009.json", + "evidence_event_candidate_parts/E-010.json", + "evidence_event_candidate_parts/E-011.json", + "evidence_event_candidate_parts/E-012.json", + "evidence_event_candidate_parts/E-013.json", + "evidence_event_candidate_parts/E-014.json", + "evidence_event_candidate_parts/E-015.json", + "evidence_event_candidate_parts/E-016.json", + "evidence_event_candidate_parts/E-017.json", + "evidence_event_candidate_parts/E-018.json", + "evidence_event_candidate_parts/E-019.json", + "evidence_event_candidate_parts/E-020.json", + "evidence_event_candidate_parts/E-021.json", + "evidence_event_candidate_parts/E-022.json", + "evidence_event_candidate_parts/E-023.json", + "evidence_event_candidate_parts/E-024.json", + "evidence_event_candidate_parts/E-025.json", + "evidence_event_candidate_parts/E-026.json", + "evidence_event_candidate_parts/E-027.json", + "evidence_event_candidate_parts/E-028.json", + "evidence_event_candidate_parts/E-029.json", + "evidence_event_candidate_parts/E-030.json" + ], + "plus": [ + "Task_B1_quality_gate_evidence_indexed" + ], + "next": "Task_B2_quality_gate_event_candidates" + } + }, + "stale_part_policy": { + "mode": "block", + "allowed_ordinals": [ + "001", + "002", + "003", + "004", + "005", + "006", + "007", + "008", + "009", + "010", + "011", + "012", + "013", + "014", + "015", + "016", + "017", + "018", + "019", + "020", + "021", + "022", + "023", + "024", + "025", + "026", + "027", + "028", + "029", + "030" + ], + "checked_directories": [ + "evidence_indexed_parts", + "evidence_event_candidate_parts" + ], + "detected_stale_parts": { + "evidence_indexed_parts": [], + "evidence_event_candidate_parts": [] + }, + "downstream_gate_instruction": "B1/B2 quality gates must BLOCK if detected_stale_parts is non-empty or if files outside expected_ordinals are present." + }, + "non_legal_assertions": { + "does_not_classify_document_type": true, + "does_not_extract_facts": true, + "does_not_evaluate_evidence": true, + "does_not_calculate_amounts": true, + "does_not_decide_legal_effect": true, + "does_not_finalize_evidence_index": true, + "does_not_finalize_candidate_id": true + }, + "planner_status": "READY", + "mapper_execution_allowed": true, + "blocked_reason": null +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-001.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-001.json new file mode 100644 index 00000000..bdf3b98d --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-001.json @@ -0,0 +1,58 @@ +{ + "source_file": "가정법원심판_강호연.json", + "content": [ + { + "type": "text", + "content": "서 울 가 정 법 원" + }, + { + "type": "text", + "content": "심 판" + }, + { + "type": "table", + "grid": [ + [ + "사 건", + "2024느단52199 상속포기" + ], + [ + "청 구 인", + "1. 강형수\n2. 강지수\n청구인들 주소 서울 관악구 봉천3길 12, 301호\n (성현동, 성현연립)\n청구인들 등록기준지 경기 평택군 송탄읍 서정리 111\n청구인들 소송대리인 변호사 김수현, 최진혁" + ], + [ + "피 상 속 인", + "망 강호연\n2024. 8. 10. 사망\n최후주소 서울 관악구 봉천3길 12, 301호(성현동, 성현연립)\n등록기준지 경기 평택군 송탄읍 서정리 111" + ] + ] + }, + { + "type": "text", + "content": "주 문" + }, + { + "type": "text", + "content": "청구인들이 피상속인 망 강호연의 재산상속을 포기하는 2024. 9. 11. 자 신고는 이를 수리한다." + }, + { + "type": "text", + "content": "이 유" + }, + { + "type": "text", + "content": "이 사건 청구는 이유 있으므로 주문과 같이 심판한다." + }, + { + "type": "text", + "content": "2024. 9. 20.\n\n사법보좌관 이승희" + }, + { + "type": "text", + "content": "정본입니다.\n2024. 10. 8.\n법원주사 최지현" + }, + { + "type": "text", + "content": "서울가\n정법원\n주사인" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-002.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-002.json new file mode 100644 index 00000000..4b723818 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-002.json @@ -0,0 +1,114 @@ +{ + "source_file": "가족관계증명서_강호연.json", + "content": [ + { + "type": "text", + "content": "가족관계증명서 [폐쇄]" + }, + { + "type": "table", + "grid": [ + [ + "등록기준지", + "경기도 평택군 송탄읍 서정리 111" + ] + ] + }, + { + "type": "table", + "grid": [ + [ + "구 분", + "성 명", + "출생연월일", + "주민등록번호", + "성별", + "본" + ], + [ + "본 인", + "강호연(姜鎬然) 사망", + "1973년 5월 19일", + "730519-1******", + "남", + "晉州" + ] + ] + }, + { + "type": "text", + "content": "가 족 사 항" + }, + { + "type": "table", + "grid": [ + [ + "구 분", + "성 명", + "출생연월일", + "주민등록번호", + "성별", + "본" + ], + [ + "부", + "강용원(姜容元)", + "1947년 12월 11일", + "471211-1******", + "남", + "晉州" + ], + [ + "모", + "김순자(金順子) 사망", + "1949년 8월 10일", + "490810-2******", + "여", + "全州" + ] + ] + }, + { + "type": "table", + "grid": [ + [ + "배우자", + "양정숙(梁晶淑)", + "1974년 11월 21일", + "741121-2******", + "여", + "濟州" + ] + ] + }, + { + "type": "table", + "grid": [ + [ + "자녀", + "강형수(姜亨壽)", + "2000년 3월 18일", + "000318-3******", + "남", + "晉州" + ], + [ + "자녀", + "강지수(姜智壽)", + "2002년 11월 30일", + "021130-4******", + "여", + "晉州" + ] + ] + }, + { + "type": "text", + "content": "위 가족관계증명서는 가족관계등록부의 기록사항과 틀림없음을 증명합니다." + }, + { + "type": "text", + "content": "서기 2024년 12월 27일\n서울특별시 관악구청장 서울특\n별시관\n악구청\n장의인" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-003.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-003.json new file mode 100644 index 00000000..f2652343 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-003.json @@ -0,0 +1,37 @@ +{ + "source_file": "각서_금전대차소비계약서_오국한_오민환.json", + "content": [ + { + "type": "text", + "content": "각 서" + }, + { + "type": "text", + "content": "1. 각서인 오국한은 오민한의 요청으로 매수자금 4,000만 원을 받아 서울 관악구 신림동 779 지상 단층 근린상가를 이경주로부터 매수하여 각서인 명의로 등기를 마쳤습니다. 따라서 위 신림동 상가는 명의만 오국한으로 되어 있을 뿐, 오민한(540607-1******) 소유임을 확인합니다.\n2. 향후 본인은 어떠한 경우에도 이경주에게 실제 매수인이 오민한임을 밝히지 않을 것이고, 추후 신림동 상가에 대한 소유권 등 어떠한 권리도 주장하지 않을 것임을 약속합니다.\n3. 만일 본인이 위 약속을 어길 시 어떠한 민형사상 처벌도 감수할 것임을 분명하게 약속합니다." + }, + { + "type": "text", + "content": "2015년 9월 17일" + }, + { + "type": "text", + "content": "각서인 오 국 한 (470415-1******) (인)吳漢\n菊" + }, + { + "type": "text", + "content": "금전소비대차계약서" + }, + { + "type": "text", + "content": "오국한은 오민한으로부터 아래와 같이 1,000만 원을 차용하고, 변제기일까지 이를 틀림없이 지급하겠습니다.\n원 금 : 10,000,000원\n변제기 : 2023. 7. 17." + }, + { + "type": "text", + "content": "2022. 7. 18." + }, + { + "type": "text", + "content": "대여인 : 오민한 (540607-1******) (인)吳民\n印\n차용인 : 오국한 (470415-1******) (인)吳漢\n菊" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-004.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-004.json new file mode 100644 index 00000000..809cc450 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-004.json @@ -0,0 +1,83 @@ +{ + "source_file": "감정평가서_신림로_포천시.json", + "content": [ + { + "type": "text", + "content": "감정평가서" + }, + { + "type": "text", + "content": "수신 : 강용원 귀하" + }, + { + "type": "text", + "content": "1. 서울 관악구 신림로 115 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡" + }, + { + "type": "table", + "grid": [ + [ + "해당기간", + "건물 시가" + ], + [ + "2022. 1. 1. ~ 2022. 12. 31.", + "2억 3,000만 원" + ], + [ + "2023. 1. 1. ~ 2023. 12. 31.", + "2억 4,000만 원" + ], + [ + "2024. 1. 1. ~ 현재", + "2억 5,000만 원" + ] + ] + }, + { + "type": "text", + "content": "2. 경기 포천시 일동면 218 임야 978㎡" + }, + { + "type": "table", + "grid": [ + [ + "해당기간", + "임야 시가" + ], + [ + "2022. 1. 1. ~ 2022. 12. 31.", + "3,000만 원" + ], + [ + "2023. 1. 1. ~ 2023. 12. 31.", + "3,000만 원" + ], + [ + "2024. 1. 1. ~ 현재", + "3,500만 원" + ] + ] + }, + { + "type": "text", + "content": "※ 앞으로도 특별한 사정이 없는 한 2, 3년 동안 같은 시세를 유지할 것으로 예상됩니다." + }, + { + "type": "text", + "content": "의뢰하신 부동산들의 시가를 위와 같이 감정평가하여 회신합니다." + }, + { + "type": "text", + "content": "2024. 9. 13." + }, + { + "type": "text", + "content": "서울감정평가사사무소" + }, + { + "type": "text", + "content": "감정평가사 장수현장수\n현인" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-005.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-005.json new file mode 100644 index 00000000..aba87b35 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-005.json @@ -0,0 +1,68 @@ +{ + "source_file": "감정평가의견서_성수동.json", + "content": [ + { + "type": "text", + "content": "감정평가 의견서" + }, + { + "type": "text", + "content": "강용원 귀하" + }, + { + "type": "text", + "content": "대상 토지: 서울 성동구 성수동 256 대 330㎡ (전체)" + }, + { + "type": "text", + "content": "귀하의 의뢰에 따라, 대상 토지에 관하여 2024년도 차임 ~~시세를~~ 다음과 같이 확인하여 회신합니다." + }, + { + "type": "table", + "grid": [ + [ + { + "t": "지상에 건물이 있는 경우", + "r": 2 + }, + "보증금 3억 원인 경우", + "월 250만 원" + ], + [ + null, + "보증금 없는 경우", + "월 400만 원" + ], + [ + { + "t": "지상에 건물이 없는 경우", + "r": 2 + }, + "보증금 3억 원인 경우", + "월 350만 원" + ], + [ + null, + "보증금 없는 경우", + "월 500만 원" + ] + ] + }, + { + "type": "text", + "content": "참고: 현재의 부동산 시장 상황에 비추어, 특별한 사정이 없는 한 이후로도 2, 3년간은 같은 시세가 유지될 것으로 전망됩니다." + }, + { + "type": "text", + "content": "2024년 12월 29일" + }, + { + "type": "text", + "content": "감정평가법인 광나루 (법인등록번호 383202-1******)\n서울 성동구 성수3길 209호 (성수동, 광진빌딩)\n감정평가사 이선욱" + }, + { + "type": "text", + "content": "감정평\n가법인\n광나루" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-006.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-006.json new file mode 100644 index 00000000..25ae9ea8 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-006.json @@ -0,0 +1,65 @@ +{ + "source_file": "계약서_이문호_박성희.json", + "content": [ + { + "type": "text", + "content": "계 약 서" + }, + { + "type": "text", + "content": "임대목적물의 표시: 서울 성동구 성수동 256 대 330㎡" + }, + { + "type": "text", + "content": "매매목적물의 표시: 위 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡" + }, + { + "type": "text", + "content": "갑 (임대인/매도인) : 이문호" + }, + { + "type": "text", + "content": "을 (임차인/매수인) : 박성희" + }, + { + "type": "text", + "content": "1. 갑은 을에게 임대목적물인 대지를 보증금 3억 원, 차임 월 300만 원(매월 말일 지급), 임대차기간 계약일부터 2년으로 정하여 임대하기로 하고, 계약 당일 임대차보증금을 지급받음과 동시에 을에게 현상대로 인도한다." + }, + { + "type": "text", + "content": "2. 갑은 을에게 매매목적물인 상가를 대금 2억 원에 매도하기로 하고, 계약 당일 매매대금을 전액 지급받음과 동시에 을에게 현상대로 인도한다." + }, + { + "type": "text", + "content": "3. 갑은 본 계약일로부터 1개월 내에 위 상가에 관한 건축허가명의를 을로 변경하는 데 필요한 서류를 을에게 교부하고, 을은 자신의 책임하에 위 상가의 건축허가조건을 충족시켜서 을의 명의로 소유권보존등기를 하며, 갑은 이에 필요한 절차에 협력한다." + }, + { + "type": "text", + "content": "4. 갑이 위 상가의 매매대금을 전액 지급받은 후, 을은 자신의 책임하에 상가와 대지를 관리·사용하고, 그에 대한 제세공과금을 부담하며, 갑은 이에 대하여 일체의 이의를 제기할 수 없다." + }, + { + "type": "text", + "content": "5. 위 상가건물이 존속하는 동안 갑은 위 대지에 관하여 을의 임대차계약 갱신요구를 거절하지 못한다. 다만 위 대지에 관하여 5기 이상의 차임을 연체할 경우에는 예외로 한다." + }, + { + "type": "text", + "content": "2024. 11. 20." + }, + { + "type": "text", + "content": "갑 (임대인/매도인) : 이문호 鎬李\n印文" + }, + { + "type": "text", + "content": "서울 마포구 동교3길 338" + }, + { + "type": "text", + "content": "을 (임차인/매수인) : 박성희 박\n성\n희" + }, + { + "type": "text", + "content": "서울 강동구 고덕2길 101동 203호 (고덕동, 강동빌라트)" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-007.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-007.json new file mode 100644 index 00000000..c8c68b06 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-007.json @@ -0,0 +1,44 @@ +{ + "source_file": "공사도급계약서.json", + "content": [ + { + "type": "table", + "grid": [ + [ + { + "t": "공사도급 계약서", + "c": 2 + }, + null + ], + [ + "공사목적물", + "● 위치: 경기 평택시 서정3길 123\n● 신축건물\n 철근콘크리트조 슬래브지붕 3층 건물(다세대주택)\n 1층 171㎡, 2층 171㎡, 3층 145.2㎡." + ], + [ + "공사기간", + "● 2021. 5. 1. ~ 2021. 11. 10." + ], + [ + "공사대금", + "● 총 공사금: 10억 원 (1,000,000,000원)\n\n● 지급방법: ① 甲은 계약금으로 공사대금 중 3억 원을\n 본 계약과 동시에 지급한다.\n ~~위 계약금 3억 원을 정히 영수함 박광윤~~ 潤朴 印光\n ② 중도금 4억 원은 2021. 8. 30. 기성고율 70% 달성\n 확인 시 지급한다.\n ③ 잔금 3억 원은 완공일에 지급한다." + ], + [ + { + "t": "2021. 5. 1.\n甲(도급인): 김일동 童金 印日\n경기도 평택시 합정동 885-1\n\n乙(수급인): 박광윤(상호: 광윤건업) 潤朴 印光\n경기도 평택시 포승읍 신영리 333", + "c": 2 + }, + null + ] + ] + }, + { + "type": "text", + "content": "영 수 증" + }, + { + "type": "text", + "content": "금 4억 원정 (400,000,000원)\n\n위 돈을 경기 평택시 서정3길 123 지상 건물신축공사 중도금으로 수령합니다.\n2021. 8. 30.\n박광윤(광윤건업) 潤朴 印光" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-008.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-008.json new file mode 100644 index 00000000..835b1473 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-008.json @@ -0,0 +1,68 @@ +{ + "source_file": "내용증명_강용원_오민환.json", + "content": [ + { + "type": "text", + "content": "내용증명" + }, + { + "type": "table", + "grid": [ + [ + "발신인", + "강용원" + ], + [ + "", + "서울 서초구 방배대로 111, 101동 502호(방배동, 서래아파트)" + ], + [ + "수신인", + "오민한" + ], + [ + "", + "서울 강서구 공항대로 37" + ] + ] + }, + { + "type": "text", + "content": "1. 안녕하십니까. 본인은 귀하의 사촌 형인 오국환에게 2억 원을 대여한 채권자입니다. 당시 오국환은 본인에게 오국환 소유의 서울 관악구 신림로 115 지상 상가(2022. 4. 18. 자 채무 담보) 및 아버지 오칠성 소유의 포천시 일동면 218 소재 임야(2022. 5. 18. 자 채무 담보)에 대해 각 근저당권을 설정해주었습니다." + }, + { + "type": "text", + "content": "2. 그러나 오국환이 원리금을 변제하지 않아 상황을 알아보니 설정 당시 시가 2억 원을 호가한다고 들었던 포천시 소재 임야는 시가 3,000만 원에 불과합니다. 또한 최근 부동산등기부등본을 확인해 본 결과 오국환이 2023. 3. 17. 그 소유의 신림동 상가를 귀하에게 매도한 사실을 알게 되었습니다." + }, + { + "type": "text", + "content": "3. 주변 소문으로는 신림동 상가의 매도 전부터 오국환은 경제적으로 매우 어려운 상황이었다고 합니다. 위 신림동 상가의 1순위 근저당권자인 나일오 이야기도 들어 보니, 나일오 역시 2023. 3. 17. 당시에는 채무가 4,000만 원에 불과했으나, 그 뒤에 추가로 대여하여 현재 오국환에게 빌려준 대여금 합계가 채권최고액인 5,000만 원보다 많은 7,000만 원에 이른다고 합니다." + }, + { + "type": "text", + "content": "4. 주변에 알아보니 이러한 경우 제가 매매계약을 취소할 수 있다고 합니다. 빠른 시간 내에 위 건물의 소유권을 오국환에게 돌려놓거나 본인에게 그에 상응하는 배상을 하시기 바랍니다." + }, + { + "type": "text", + "content": "첨부 : 차용증 각 1부" + }, + { + "type": "text", + "content": "2024. 9. 15." + }, + { + "type": "text", + "content": "강용원 (인) 元姜\n印容" + }, + { + "type": "table", + "grid": [ + [ + "서울서초우체국\n2024. 9. 15.\n24 - 17146", + "본 우편물은 2024-9-15\n제17146호에 의하여\n내용증명우편물로 발송하였음을 증명함\n서울서초우체국장", + "서울서\n초우체\n국장인" + ] + ] + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-009.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-009.json new file mode 100644 index 00000000..c654672e --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-009.json @@ -0,0 +1,106 @@ +{ + "source_file": "내용증명_윤건우_양정숙.json", + "content": [ + { + "type": "text", + "content": "내용증명" + }, + { + "type": "table", + "grid": [ + [ + "발신인", + "윤건우\n서울 영등포구 당산대로 75, 101동 202호 (당산동, 선유도아파트)" + ], + [ + "수신인", + "양정숙\n서울 관악구 봉천3길 12, 301호 (성현동, 성현연립)" + ] + ] + }, + { + "type": "text", + "content": "저는 귀하가 임차한 서울 동작구 흑석로 80 지상 상가건물 201호의 소유자입니다." + }, + { + "type": "text", + "content": "귀하와 임대차계약을 체결한 이수인은 제 소유의 위 흑석동 상가를 매수했던 사람입니다. 그런데 이수인의 귀책사유로 위 흑석동 상가에 관한 매매계약은 이미 해제되었습니다." + }, + { + "type": "text", + "content": "매매계약이 해제된 마당에 제가 이수인이 체결한 임대차계약에 대해서 책임을 질 하등의 이유가 없습니다. 그러므로 즉시 식당 영업을 중단하고 흑석동 상가를 본인에게 인도하시기 바랍니다." + }, + { + "type": "text", + "content": "확인해 보니 귀하는 식당영업을 하면서 이수인에게 지급해야 할 차임을 2024년 3월 및 2024년 4월 총 2번 연체한 적이 있는데 현재까지도 미지급되었습니다. 매매계약 해제 직후 제가 귀하에게 명도요구를 하자 귀하는 이수인을 통해 제 계좌번호를 알아냈는지 일방적으로 제 계좌에 차임을 입금했는데, 확인해 보니 그마저도 2024년 8월분과 2024년 9월분 차임은 입금되어 있지 않았습니다." + }, + { + "type": "text", + "content": "제가 임대인이 아닌 이상 차임을 지급받을 이유도 없지만, 만일 임대인의 입장이 된다 하더라도 상가건물 임대차보호법에 따르면 3회 이상 차임 연체 시에는 임대차를 해지할 수 있는 것으로 알고 있습니다." + }, + { + "type": "text", + "content": "따라서 이 서면을 통해 임대차계약을 해지하오니, 받는 즉시 건물을 비워 주시기 바라며 이에 응하지 않을 시 무단점거의 법적 책임을 묻게 될 것임을 통보합니다." + }, + { + "type": "text", + "content": "2024. 11. 29.\n\n윤건우 인" + }, + { + "type": "text", + "content": "서울영등포우체국\n2024. 11. 29.\n24 - 15679" + }, + { + "type": "text", + "content": "본 우편물은 2024-11-29\n제15679호에 의하여\n내용증명우편물로 발송하였음을 증명함\n서울영등포우체국장" + }, + { + "type": "text", + "content": "서울영\n등포우\n체국장" + }, + { + "type": "table", + "grid": [ + [ + { + "t": "우 편 물 배 달 증 명 서", + "c": 4 + }, + null, + null, + null + ], + [ + { + "t": "수취인의 주거 및 성명\n 서울 관악구 봉천3길 12, 301호\n (성현동, 성현연립) 양정숙", + "c": 4 + }, + null, + null, + null + ], + [ + "접수국명", + "서울영등포", + "접수연월일", + "2024년11월29일" + ], + [ + "접수번호", + "15679", + "배달연월일", + "2024년11월30일" + ], + [ + "적 요", + { + "t": "수취인과의 관계\n본인 수령\n양정숙", + "c": 2 + }, + null, + "서울관\n악우체\n국장인\n서울관악우체국장" + ] + ] + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-010.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-010.json new file mode 100644 index 00000000..d65d4189 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-010.json @@ -0,0 +1,57 @@ +{ + "source_file": "내용증명에대한답신_오민환_강용원.json", + "content": [ + { + "type": "text", + "content": "내용증명에 대한 답신" + }, + { + "type": "text", + "content": "발신인: 오민한\n수신인: 강용원" + }, + { + "type": "text", + "content": "1. 귀하가 보낸 이행최고서는 2024. 9. 20. 잘 받아 보았습니다." + }, + { + "type": "text", + "content": "2. 사실 엄밀히 말해 본인은 오국한의 최대 피해자 중 한 명입니다. 신림동 상가는 애초부터 본인 소유로, 처음 신림동 상가를 매수할 당시 신림동 상가의 전 소유자 이경주와 본인 간의 다툼이 있어 사촌 형인 오국한에게 2015. 9. 17. 매수대금 4,000만 원을 지급하고 매수를 부탁하였던 것입니다. 그런데 본인이 신경을 쓰지 못하는 사이 오국한이 마치 신림동 상가가 자기 소유인 것처럼 나일오와 귀하에게 근저당권을 설정해 준 것입니다." + }, + { + "type": "text", + "content": "3. 작년에 오국한과 원만히 문제를 해결하기 위해 소정의 금액을 지급하고 신림동 상가를 매수하는 형식을 취하였으나, 해당 부동산은 어디까지나 당초부터 본인 소유였기에 제가 양수받은 것이 사해행위에 해당할 수는 없습니다." + }, + { + "type": "text", + "content": "4. 주변에 물어본 결과 ‘신탁자인 본인이 명의를 이전받은 것이 오국한의 일반채권자에게는 문제될 여지가 있어도, 신림동 상가 및 포천시 임야에 이미 충분한 담보를 확보하고 있던 귀하에 대하여는 사해행위가 되지 않는다’고 합니다." + }, + { + "type": "text", + "content": "5. 정말 만에 하나 귀하의 주장과 같이 매매계약이 사해행위로 인정된다면 저로서는 이미 그 재산 없다고 생각하고 살아온 세월이 오래된 까닭에, 일부 원래대로 돌려놓을 마음은 있습니다만, 어떠한 경우에도 제가 귀하에게 추가적인 금전지급책임은 지지 않는 것으로 알고 있습니다." + }, + { + "type": "text", + "content": "6. 본인 역시 오국한에게 상당한 액수의 채권을 가진 채권자이므로, 만일 제가 금전지급책임을 져야 한다면 본인의 채권 합계금 5,000만 원(신림동 상가 매수대금 4,000만 원, 2022. 7. 18. 자 대여금 1,000만 원)으로 귀하의 채권을 소멸시키겠습니다." + }, + { + "type": "text", + "content": "[첨부 : 각서, 금전소비대차계약서 각 1부]" + }, + { + "type": "text", + "content": "서울강서우체국\n2024. 9. 30.\n24 - 7920" + }, + { + "type": "text", + "content": "2024. 9. 30.\n오민한 (인)\n吳民\n漢印" + }, + { + "type": "text", + "content": "본 우편물은 2024-9-30\n제7920호에 의하여\n내용증명우편물로 발송하였음을 증명함\n서울강서우체국장" + }, + { + "type": "text", + "content": "서울강\n서우체\n국장인" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-011.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-011.json new file mode 100644 index 00000000..5ddf8a24 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-011.json @@ -0,0 +1,70 @@ +{ + "source_file": "답변서_박광윤_to_강용원_양정숙.json", + "content": [ + { + "type": "text", + "content": "답 변 서" + }, + { + "type": "table", + "grid": [ + [ + "수신인:", + "강용원, 서울 서초구 방배대로 111, 101동 502호(방배동, 서래아파트)\n양정숙, 서울 관악구 봉천3길 12, 301호(성현동, 성현연립)" + ] + ] + }, + { + "type": "text", + "content": "1. 귀하들이 보낸 2024. 8. 29. 자 통지서는 2024. 9. 1. 잘 받아 보았습니다." + }, + { + "type": "text", + "content": "2. 그간 전화로 누차 설명드린 바와 같이 본인은 평택시 소재 서정빌라를 기일에 맞춰 완공하였지만, 건축주 김일동으로부터 공사잔금 3억 원을 받지 못하여, 그 무렵부터 현재까지 서정빌라 101호에 관한 유치권을 행사 중입니다." + }, + { + "type": "text", + "content": "3. 공사업자가 공사대금을 받지 못하여 유치권을 행사할 때, 자기가 지은 건물의 용도 그대로 사용하는 것은 전혀 문제되지 않는 것으로 알고 있습니다." + }, + { + "type": "text", + "content": "4. 서정빌라 101호는 주택이기에, 본인이 스스로 판단하여 2022. 2. 1.부터 7. 31.까지 인근 공장 직원숙소로 월 200만 원에 임대하여 위 잔금 변제에 충당하였습니다. 잔금 3억 원에 지연이자까지 생각하면, 위 차임이나 저와 제 가족이 서정빌라 101호에 거주하는 기간의 차임을 공제하더라도 여전히 3억 원 가까운 채무가 남아 있고, 이 돈이 다 변제될 때까지 저는 계속 위 101호에 거주할 수 있습니다." + }, + { + "type": "text", + "content": "5. 따라서 최근까지도 계속 지급하겠다는 다짐만 할 뿐 지금껏 잔금을 한 푼도 갚지 못한 건축주가 시비 걸 일도 아니거니와, 전 소유자인 정남이와의 분쟁 과정에서도 명확히 밝힌 바와 같이 제3자인 건물주가 문제 삼을 일도 아닙니다. 그럼에도 이제 와서 그 당시의 소유자도 아닌 강호연 씨 가족이 왈가왈부할 수 없습니다." + }, + { + "type": "text", + "content": "6. 그러므로 저는 서정빌라 101호에 그동안 거주하면서 얻은 수익을 위 공사대금 원리금 채권에 충당하였다는 점을 분명히 말씀드리고, 향후에도 마찬가지로 충당할 것입니다." + }, + { + "type": "text", + "content": "2024년 10월 31일" + }, + { + "type": "text", + "content": "발신인 박광윤, 평택시 서정3길 123, 101호(서정동, 서정빌라) 朴\n光 潤\n印" + }, + { + "type": "text", + "content": "평택우체국" + }, + { + "type": "text", + "content": "2024. 10. 31." + }, + { + "type": "text", + "content": "24 - 4501" + }, + { + "type": "text", + "content": "본 우편물은 2024-10-31\n제4501호에 의하여\n내용증명우편물로 발송하였음을 증명함\n평택우체국장" + }, + { + "type": "text", + "content": "평택우\n체국장" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-012.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-012.json new file mode 100644 index 00000000..25572770 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-012.json @@ -0,0 +1,70 @@ +{ + "source_file": "등기말소신청에대한답신.json", + "content": [ + { + "type": "text", + "content": "등기말소 신청에 대한 답신" + }, + { + "type": "table", + "grid": [ + [ + "수신인", + "강용원\n서울 서초구 방배대로 111, 101동 502호 (방배동, 서래아파트)" + ] + ] + }, + { + "type": "text", + "content": "1. 본사는 소관 지점을 통해 귀하가 2024. 11. 20. 자로 제출한 [등기말소 신청서]를 검토한 결과, 아래와 같이 답신하고자 합니다." + }, + { + "type": "text", + "content": "2. 귀하는 위 신청서를 통해 서울 성동구 성수동 256 대지에 관하여 본사가 채권자로 된 근저당권설정등기가 원인무효라고 주장하였는데, 이는 귀하가 아래와 같이 부동산경매에 관한 법리를 오해한 데 따른 것임을 알려 드립니다." + }, + { + "type": "text", + "content": "○ 위 대지에 관하여서는 법원이 경매개시결정을 하고, 그에 따라 일련의 경매절차가 진행된 결과 최고가 매수신고인이 매각대금을 완납함으로써 소유권이전등기까지 이루어졌는바, 그 과정에 어떠한 절차상의 하자도 없었습니다." + }, + { + "type": "text", + "content": "○ 본사는 성수동 대지의 등기부와 최고가 매수신고인으로부터 제출받은 증빙자료를 통해 법원이 정상적으로 매각허가결정을 하였음을 확인하고서 본사의 경매대금 대출기준에 따라 대출하고 근저당권설정등기를 마쳤습니다." + }, + { + "type": "text", + "content": "○ 경매개시결정 근거가 된 근저당권설정등기나 경매법원의 촉탁으로 이루어지는 소유권이전등기를 신뢰하고서 대출 절차를 진행한 경우 해당 금융기관이 보호받지 못한다면 금융제도의 안정성에 큰 위해가 되고, 이는 결과적으로 금융소비자들에 대한 책임으로 전가될 것입니다." + }, + { + "type": "text", + "content": "○ 위와 같은 취지에서 민사집행법 제267조는 경매의 공신력을 인정하는 규정을 두고 있는 것입니다." + }, + { + "type": "text", + "content": "3. 본 답신이 귀하의 오해를 해소하는 데 도움이 되기를 기원합니다." + }, + { + "type": "text", + "content": "2024년 12월 22일" + }, + { + "type": "text", + "content": "발신인 주식회사 대한은행\n서울 종로구 율곡로 131\n대표이사 최창근" + }, + { + "type": "text", + "content": "주식회사\n대한은행\n대표이사" + }, + { + "type": "text", + "content": "서울종로우체국\n2024. 12. 22.\n24 - 34732" + }, + { + "type": "text", + "content": "본 우편물은 2024-12-22\n제34732호에 의하여\n내용증명우편물로 발송하였음을 증명함\n서울종로우체국장" + }, + { + "type": "text", + "content": "서울종\n로우체\n국장인" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-013.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-013.json new file mode 100644 index 00000000..13c4670d --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-013.json @@ -0,0 +1,171 @@ +{ + "source_file": "등기사항전부증명서_성수동.json", + "content": [ + { + "type": "text", + "content": "등기사항전부증명서(말소사항 포함)-토지" + }, + { + "type": "text", + "content": "[토지] 서울특별시 성동구 성수동 256 고유번호 3103-1325-341248" + }, + { + "type": "text", + "content": "【표 제 부】 (토지의 표시)" + }, + { + "type": "table", + "grid": [ + [ + "표시번호", + "접 수", + "소 재 지 번", + "지 목", + "면 적", + "등기원인 및 기타사항" + ], + [ + "1\n(전2)", + "1994년10월15일", + "서울특별시 성동구 성수동 256\n[도로명주소]\n서울특별시 성동구 성수로 30", + "대", + "330㎡", + "부동산등기법 제177조의6 제1항의\n규정에 의하여 2001년10월11일 전\n산이기" + ] + ] + }, + { + "type": "text", + "content": "【갑 구】 (소유권에 관한 사항)" + }, + { + "type": "table", + "grid": [ + [ + "순위번호", + "등 기 목 적", + "접 수", + "등 기 원 인", + "권리자 및 기타사항" + ], + [ + "1\n(전5)", + "소유권이전", + "1997년4월16일\n제1453호", + "1997년3월15일\n매매", + "소유자 박재경 330812-1******\n 서울특별시 성북구 정릉동 32 미래빌\n 502호\n부동산등기법 제177조의6 제1항의 규정\n에 의하여 2001년10월11일 전산이기" + ], + [ + "2", + "소유권이전", + "2017년7월2일\n제7026호", + "2017년6월30일\n매매", + "소유자 강석우 250712-1******\n 서울특별시 강동구 고덕동 32" + ], + [ + "3", + "소유권이전", + "2022년12월8일\n제11202호", + "2022년11월8일\n상속", + "공유자 지분 5분의 3\n 정유심 270420-2******\n 서울특별시 강동구 고덕동 32\n공유자 지분 5분의 2\n 강용원 471211-1******\n 서울특별시 서초구 방배대로 111, 101\n 동 502호 (방배동, 서래아파트)" + ], + [ + "4", + "3번정유심지분\n전부이전", + "2023년4월8일\n제7202호", + "2023년4월6일\n증여", + "공유자 지분 5분의 3\n 강용원 471211-1******\n 서울특별시 서초구 방배대로 111, 101\n 동 502호 (방배동, 서래아파트)" + ], + [ + "~~5~~", + "~~담보권실행을위\n한경매개시결정\n(일괄)~~", + "~~2024년7월22일\n제12402호~~", + "~~2024년7월21일\n서울동부지방법\n원의경매개시결\n정(2024타경\n12786,12787)~~", + "~~채권자 오민환 540607-1******\n 서울특별시 강서구 공항대로 37~~" + ], + [ + "6", + "소유권이전", + "2024년10월5일\n제25797호", + "2024년10월5일\n담보권실행을\n위한 경매로 인\n한 매각", + "소유자 이문호 641003-1******\n 서울특별시 마포구 동교3길 338" + ], + [ + "7", + "5번담보권실행\n을위한경매개시\n결정등기말소", + "2024년10월5일\n제25800호", + "2024년10월5일\n담보권실행을\n위한 경매로 인\n한 매각", + "" + ] + ] + }, + { + "type": "text", + "content": "【을 구】 (소유권 이외의 권리에 관한 사항)" + }, + { + "type": "table", + "grid": [ + [ + "순위번호", + "등 기 목 적", + "접 수", + "등 기 원 인", + "권리자 및 기타사항" + ], + [ + "~~1~~", + "~~갑구3번강용원\n지분5분의2근저\n당권설정~~", + "~~2023년1월6일\n제1088호~~", + "~~2023년1월6일\n설정계약~~", + "~~채권최고액 금 260,000,000원\n채무자 강용원 471211-1******\n 서울특별시 서초구 방배대로 111, 101\n 동 502호 (방배동, 서래아파트)\n근저당권자 오민환 540607-1******\n 서울특별시 강서구 공항대로 37~~" + ], + [ + "~~2~~", + "~~갑구4번강용원\n지분5분의3근저\n당권설정~~", + "~~2023년7월6일\n제8293호~~", + "~~2023년7월6일\n설정계약~~", + "~~채권최고액 금 390,000,000원\n채무자 강용원 471211-1******\n 서울특별시 서초구 방배대로 111, 101\n 동 502호 (방배동, 서래아파트)\n근저당권자 오민환 540607-1******\n 서울특별시 강서구 공항대로 37~~" + ], + [ + "3", + "1,2번근저당권설\n정등기말소", + "2024년10월5일\n제25798호", + "2024년10월5일\n담보권실행을\n위한 경매로 인\n한 매각", + "" + ], + [ + "4", + "근저당권설정", + "2024년10월5일\n제25799호", + "2024년10월5일\n설정계약", + "채권최고액 금 520,000,000원\n채무자 이문호 641003-1******\n 서울특별시 마포구 동교3길 338\n근저당권자 주식회사 대한은행\n (105912-0******)\n 서울특별시 종로구 율곡로 131\n 대표이사 최창근" + ] + ] + }, + { + "type": "text", + "content": "---- 이 하 여 백 ----" + }, + { + "type": "text", + "content": "수수료 1,000원 영수함\n관할등기소 서울동부지방법원 등기국 / 발행등기소 법원행정처 등기정보중앙관리소" + }, + { + "type": "text", + "content": "이 증명서는 등기기록의 내용과 틀림없음을 증명합니다.\n\n서기 2025년 1월 7일\n\n법원행정처 등기정보중앙관리소 전산운영책임관\n등기정보\n중앙관리\n소전산운\n영책임관" + }, + { + "type": "text", + "content": "*실선으로 그어진 부분은 말소사항을 표시함. *등기기록에 기록된 사항이 없는 갑구 또는 을구는 생략함." + }, + { + "type": "text", + "content": "문서 하단의 바코드를 스캐너로 확인하거나 인터넷등기소(http://iros.go.kr)의 발급확인 메뉴에서 발급확인번호를 입력하여 위·\n변조 여부를 확인할 수 있습니다. 발급확인번호를 통한 확인은 발행일부터 3개월까지 5회에 한하여 가능합니다.\n발행번호 038263152217942191ULDO704942QNG26858142722 1/1 발급확인번호 AKSD-XOHR-3764 발행일 2025/01/07" + }, + { + "type": "text", + "content": "대 법 원" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-014.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-014.json new file mode 100644 index 00000000..98b56da3 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-014.json @@ -0,0 +1,154 @@ +{ + "source_file": "등기사항전부증명서_신림로.json", + "content": [ + { + "type": "text", + "content": "등기사항전부증명서 (말소사항 포함) -건물" + }, + { + "type": "text", + "content": "[건물] 서울특별시 관악구 신림로 115\n고유번호 1436-2018-250456" + }, + { + "type": "text", + "content": "【 표 제 부 】 (건물의 표시)" + }, + { + "type": "table", + "grid": [ + [ + "표시번호", + "접 수", + "소재지번 및 건물번호", + "건 물 내 역", + "등기원인 및 기타사항" + ], + [ + "1", + "2006년 5월 8일", + "서울특별시 관악구 신림동 779\n[도로명주소]\n서울특별시 관악구 신림로 115", + "철근콘크리트조 콘크리트\n지붕 단층 근린상가 100㎡", + "" + ] + ] + }, + { + "type": "text", + "content": "【 갑 구 】 (소유권에 관한 사항)" + }, + { + "type": "table", + "grid": [ + [ + "순위번호", + "등기목적", + "접 수", + "등 기 원 인", + "권리자 및 기타사항" + ], + [ + "1", + "소유권보존", + "2006년5월8일\n제24759호", + "", + "소유자 이경주 590722-1******\n경기도 화성시 동탄면 1278" + ], + [ + "2", + "소유권이전", + "2015년9월17일\n제56457호", + "2015년9월17일\n매매", + "소유자 오국환 470415-1******\n서울특별시 도봉구 도봉로 168, 302호\n거래가액 금 40,000,000원" + ], + [ + "3", + "소유권이전", + "2023년3월17일\n제11593호", + "2023년3월17일\n매매", + "소유자 오민한 540607-1******\n서울특별시 강서구 공항대로 37\n거래가액 금 150,000,000원" + ] + ] + }, + { + "type": "text", + "content": "【 을 구 】 (소유권 이외의 권리에 관한 사항)" + }, + { + "type": "table", + "grid": [ + [ + "순위번호", + "등기목적", + "접 수", + "등 기 원 인", + "권리자 및 기타사항" + ], + [ + "1", + "근저당권설정", + "2021년8월1일\n제50099호", + "2021년8월1일\n설정계약", + "채권최고액 50,000,000원\n채무자 오국환\n서울특별시 도봉구 도봉로 168, 302호\n근저당권자 나임오 470812-1******\n서울특별시 강서구 화곡로 154" + ], + [ + "2", + "~~근저당권설정~~", + "~~2022년4월18일~~\n~~제26597호~~", + "~~2022년4월18일~~\n~~설정계약~~", + "~~채권최고액 150,000,000원~~\n~~채무자 오국환~~\n~~서울특별시 도봉구 도봉로 168, 302호~~\n~~근저당권자 강용일 471211-1******~~\n~~서울특별시 서초구 방배대로 111, 101동~~\n~~ 502호~~" + ], + [ + "3", + "2번근저당권\n설정등기말소", + "2024년10월17일\n제74586호", + "2024년10월17일\n해지", + "" + ] + ] + }, + { + "type": "text", + "content": "- 이 하 여 백 -" + }, + { + "type": "text", + "content": "등기정보\n중앙관리\n소전산운\n영책임관" + }, + { + "type": "text", + "content": "수수료 금 1,000원 영수함" + }, + { + "type": "text", + "content": "관할등기소 서울중앙지방법원 등기국 / 발행등기소 법원행정처 등기정보중앙관리소" + }, + { + "type": "text", + "content": "이 증명서는 등기기록의 내용과 틀림없음을 증명합니다." + }, + { + "type": "text", + "content": "서기 2025년 1월 7일" + }, + { + "type": "text", + "content": "법원행정처 등기정보중앙관리소 전산운영책임관" + }, + { + "type": "text", + "content": "*실선으로 그어진 부분은 말소사항을 표시함.\n*등기기록에 기록된 사항이 없는 갑구 또는 을구는 생략함." + }, + { + "type": "text", + "content": "문서 하단의 바코드를 스캐너로 확인하거나 인터넷등기소(http://iros.go.kr)의 발급확인 메뉴에서 발급확인번호를 입력하여 위·변조 여부를 확인할 수 있습니다. 발급확인번호를 통한 확인은 발행일부터 3개월까지 5회에 한하여 가능합니다." + }, + { + "type": "text", + "content": "발행번호 121076247715349871ULDO704942CPAF27463145429\n1/1\n발급확인번호 WGIT-XLBJ-7632\n발행일 2025/01/07" + }, + { + "type": "text", + "content": "대 법 원" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-015.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-015.json new file mode 100644 index 00000000..307eabbd --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-015.json @@ -0,0 +1,162 @@ +{ + "source_file": "등기사항전부증명서_평택시.json", + "content": [ + { + "type": "text", + "content": "등기사항전부증명서(말소사항 포함)-집합건물" + }, + { + "type": "text", + "content": "[집합건물] 경기도 평택시 서정3길 123" + }, + { + "type": "text", + "content": "고유번호 1145-5548-165651" + }, + { + "type": "text", + "content": "【표 제 부】 (1동의 건물의 표시)" + }, + { + "type": "table", + "grid": [ + [ + "표시번호", + "접 수", + "소재지번 및 건물번호", + "건물내역", + "등기원인 및 기타사항" + ], + [ + "1", + "2021년12월10일", + "경기도 평택시 서정동 550\n[도로명주소]\n경기도 평택시 서정3길 123(서정동, 서정빌라)", + "철근콘크리트조 슬래브지붕 3층 건물\n1층 171㎡\n2층 171㎡\n3층 145.2㎡", + "도면편철장 제4책 제43면" + ] + ] + }, + { + "type": "table", + "grid": [ + [ + { + "t": "(대지권의 목적인 토지의 표시)", + "c": 5 + }, + null, + null, + null, + null + ], + [ + "표시번호", + "소재지번", + "지목", + "면적", + "등기원인 및 기타사항" + ], + [ + "1", + "경기도 평택시 서정3길 123", + "대", + "312㎡", + "2021년12월10일" + ] + ] + }, + { + "type": "text", + "content": "【표 제 부】 (전유부분의 건물의 표시)" + }, + { + "type": "table", + "grid": [ + [ + "표시번호", + "접 수", + "건물번호", + "건물내역", + "등기원인 및 기타사항" + ], + [ + "1", + "2021년12월10일", + "제1층 제101호", + "철근콘크리트조 54.85㎡", + "도면편철장 제4책 제43면" + ] + ] + }, + { + "type": "table", + "grid": [ + [ + { + "t": "(대지권의 표시)", + "c": 4 + }, + null, + null, + null + ], + [ + "표시번호", + "대지권의 종류", + "대지권비율", + "등기원인 및 기타사항" + ], + [ + "1", + "소유권대지권", + "312분의 47", + "2021년12월10일 대지권\n2021년12월10일 등기" + ] + ] + }, + { + "type": "text", + "content": "【갑 구】 (소유권에 관한 사항)" + }, + { + "type": "table", + "grid": [ + [ + "순위번호", + "등 기 목 적", + "접 수", + "등 기 원 인", + "권리자 및 기타사항" + ], + [ + "1", + "소유권보존", + "2021년12월10일\n제7233호", + "", + "소유자 김일동 681017-1******\n 경기도 안산시 상록수길 112\n거래가액 " + ], + [ + "2", + "소유권이전", + "2021년12월30일\n제7871호", + "2021년12월30일\n매매", + "소유자 정남이 570411-1******\n 경기도 평택시 평택3길 2\n거래가액 " + ], + [ + "~~3~~", + "~~가압류~~", + "~~2022년9월1일~~\n~~제11593호~~", + "~~2022년9월1일~~\n~~수원지방법원~~\n~~평택지원의~~\n~~가압류결정~~\n~~(2022카단43434)~~", + "~~청구금액 금14,000,000원~~\n~~채권자 김동국 690127-1******~~\n~~ 경기도 수원시 원천로 3~~" + ], + [ + "4", + "소유권이전", + "2022년10월1일\n제12321호", + "", + "" + ] + ] + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-016.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-016.json new file mode 100644 index 00000000..5f7519ec --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-016.json @@ -0,0 +1,113 @@ +{ + "source_file": "별지목록.json", + "content": [ + { + "type": "text", + "content": "별지" + }, + { + "type": "text", + "content": "목 록" + }, + { + "type": "text", + "content": "1. 서울 성동구 성수동 256 대 330㎡" + }, + { + "type": "text", + "content": "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡" + }, + { + "type": "text", + "content": "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡" + }, + { + "type": "text", + "content": "4. (1동의 건물의 표시)" + }, + { + "type": "text", + "content": "평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물" + }, + { + "type": "text", + "content": "1층 171㎡" + }, + { + "type": "text", + "content": "2층 171㎡" + }, + { + "type": "text", + "content": "3층 145.2㎡" + }, + { + "type": "text", + "content": "(대지권의 목적인 토지의 표시)" + }, + { + "type": "text", + "content": "평택시 서정3길 123 대 312㎡" + }, + { + "type": "text", + "content": "(전유부분의 건물의 표시)" + }, + { + "type": "text", + "content": "제1층 제101호 철근콘크리트조 54.85㎡" + }, + { + "type": "text", + "content": "(대지권의 표시)" + }, + { + "type": "text", + "content": "소유권대지권 312분의 47" + }, + { + "type": "text", + "content": "5. (1동의 건물의 표시)" + }, + { + "type": "text", + "content": "서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물" + }, + { + "type": "text", + "content": "1층 200㎡" + }, + { + "type": "text", + "content": "2층 192㎡" + }, + { + "type": "text", + "content": "3층 188㎡" + }, + { + "type": "text", + "content": "(대지권의 목적인 토지의 표시)" + }, + { + "type": "text", + "content": "서울 동작구 흑석로 80 대 380㎡" + }, + { + "type": "text", + "content": "(전유부분의 건물의 표시)" + }, + { + "type": "text", + "content": "제2층 제201호 철근콘크리트조 150.32㎡" + }, + { + "type": "text", + "content": "(대지권의 표시)" + }, + { + "type": "text", + "content": "소유권대지권 380분의 54. 끝." + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-017.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-017.json new file mode 100644 index 00000000..2f3e9b5f --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-017.json @@ -0,0 +1,33 @@ +{ + "source_file": "부동산매매계약서_윤건우_이수인.json", + "content": [ + { + "type": "text", + "content": "부동산 매매계약서" + }, + { + "type": "text", + "content": "매도인과 매수인 쌍방은 아래와 같이 부동산 매매계약을 체결한다." + }, + { + "type": "table", + "grid": [ + [ + "1. 부동산의 표시\n 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물 201호(150.32㎡)" + ], + [ + "2. 계약내용\n제1조 매수인은 매도인에게 매매대금을 아래와 같이 지불하기로 한다.\n 매매대금: 10억 원(1,000,000,000원)\n 계 약 금: 2억 원(200,000,000원) 계약 당일 지급\n 중 도 금: 5억 원(500,000,000원) 2024. 1. 1. 지급\n 잔 금: 3억 원(300,000,000원) 2024. 5. 1. 지급\n제2조 매도인은 매수인으로부터 잔금을 지급받음과 동시에 매수인에게 소유권이전등기에\n 필요한 모든 서류를 교부한다.\n제3조 매도인은 위 부동산에 설정된 제3자 명의의 저당권, 지상권, 임차권 등 소유권의 행\n 사를 제한하는 사유가 있거나, 조세·공과금 기타 부담금의 미납금 등이 있을 때에는\n 잔금 수수일까지 그 권리의 하자 및 부담 등을 제거한 상태로 소유권이전등기를 하\n 여야 한다.\n제4조 매수인이 매도인에게 중도금을 지불할 때까지는 매도인은 계약금의 배액을 상환하\n 고, 매수인은 계약금을 포기하고 이 계약을 해제할 수 있다.\n\n특 약 사 항\n매도인은 매수인으로부터 중도금을 지급받은 날 매수인에게 위 부동산을 인도한다.\n\n\n2023년 10월 1일" + ], + [ + "매도인: 윤 건 우 (770722-1******)\n 서울 영등포구 당산대로 75, 101동 202호 (당산동, 선유도아파트)" + ], + [ + "매수인: 이 수 인 (820901-2******)\n 서울 동작구 만양로 18, 701동 101호 (노량진동, 달빛아파트)" + ], + [ + "중개인: 행운부동산 (서울 영등포구 당산대로 74, 해피상가 101호)\n 공인중개사 양 재 혁 (등록번호 3624-03-1577)" + ] + ] + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-018.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-018.json new file mode 100644 index 00000000..3ecf5ab6 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-018.json @@ -0,0 +1,99 @@ +{ + "source_file": "사업자등록증_양정숙.json", + "content": [ + { + "type": "text", + "content": "NTS" + }, + { + "type": "text", + "content": "국 세 청" + }, + { + "type": "text", + "content": "사 업 자 등 록 증" + }, + { + "type": "text", + "content": "(일반과세자)" + }, + { + "type": "text", + "content": "등록번호 : 261-53-00201" + }, + { + "type": "table", + "grid": [ + [ + "상호", + ":", + { + "t": "맛나식당", + "c": 2 + }, + null + ], + [ + "성명", + ":", + "양정숙", + "생 년 월 일 : 1974년 11월 21일" + ], + [ + "개업 연월일", + ":", + { + "t": "2024년 2월 1일", + "c": 2 + }, + null + ], + [ + "사업장소재지", + ":", + { + "t": "서울 동작구 흑석로 80, 201호", + "c": 2 + }, + null + ] + ] + }, + { + "type": "table", + "grid": [ + [ + "사업의 종류", + ":", + "업태 음식점업 종목 한식" + ] + ] + }, + { + "type": "table", + "grid": [ + [ + "교 부 사 유", + ":", + "신규" + ] + ] + }, + { + "type": "text", + "content": "사업자단위과세 적용사업자 여부: 여( ) 부( V )" + }, + { + "type": "text", + "content": "2024년 2월 2일" + }, + { + "type": "text", + "content": "동작 세무서장 동작세무\n서장의인\n민원실용" + }, + { + "type": "text", + "content": "NTS 국세청" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-019.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-019.json new file mode 100644 index 00000000..898c3dd1 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-019.json @@ -0,0 +1,70 @@ +{ + "source_file": "소멸청구통지서_강용원_양정숙_박광윤.json", + "content": [ + { + "type": "text", + "content": "소멸청구 통지서" + }, + { + "type": "text", + "content": "수신인: 박광윤\n평택시 서정3길 123, 101호(서정동, 서정빌라)" + }, + { + "type": "text", + "content": "1. 발신인들은 귀하가 가족들과 함께 거주하는 평택시 서정동 서정빌라 101호의 소유주 였던 강호연씨의 아버지(강용원)와 아내(양정숙)입니다." + }, + { + "type": "text", + "content": "2. 잘 아시다시피, 귀하가 거주하는 서정빌라 101호는 강호연의 소유였고, 발신인들은 강호연씨의 상속인으로서 위 빌라에 관한 소유권을 행사하고자 합니다." + }, + { + "type": "text", + "content": "3. 그동안 귀하는 발신인 측에서는 알지도 못하는 서정빌라 건축주로부터 공사대금을 받지 못하였다며 서정빌라 101호를 점거하고, 직접 거주까지 해 오셨습니다. 아무리 강호연이 저렴하게 매입했다 하더라도, 타인의 집을 이렇게 승낙 없이 임의로 사용해 오신 점에 대해서는 심히 유감을 표하지 않을 수 없습니다." + }, + { + "type": "text", + "content": "4. 귀하도 확인해 주셨다시피, 귀하는 2022년 2월부터 7월까지 6개월간 서정빌라 101호를 인근 공장의 직원 숙소로 임대를 놓아 임대수익도 벌어들였습니다." + }, + { + "type": "text", + "content": "5. 발신인들이 확인해 보니, 아무리 공사대금을 받기 위해 유치권을 행사하는 업자분이라 하더라도, 위와 같이 소유자의 승낙 없이 건물을 사용하거나 대여하는 행위는 법적으로 금지되어 있고, 이를 위반하면 소유자가 유치권 소멸을 청구할 수 있다고 합니다." + }, + { + "type": "text", + "content": "6. 강호연의 사망 후 발신인 양정숙 등 가족들은 경제적으로 매우 어려운 지경에 이르러, 서정빌라에서 임대수익이라도 벌어들여야 할 상황입니다." + }, + { + "type": "text", + "content": "7. 이에 발신인들은 이 통지서를 통해 귀하가 행사하는 유치권의 소멸을 청구하오니, 조속히 서정빌라 101호를 발신인들에게 인도해 주시고, 나아가 강호연이 소유권을 취득한 2022년 10월부터의 사용수익도 지급해 주시기를 바랍니다." + }, + { + "type": "text", + "content": "2024년 8월 29일" + }, + { + "type": "table", + "grid": [ + [ + "발신인 강용원, 서울 서초구 방배대로 111, 101동 502호(방배동, 서래아파트)", + "姜元\n溶印" + ], + [ + "양정숙, 서울 관악구 봉천3길 12, 301호(성현동, 성현연립)", + "梁\n晶\n淑" + ] + ] + }, + { + "type": "text", + "content": "서울관악우체국\n2024. 8. 29.\n24 - 1991" + }, + { + "type": "text", + "content": "본 우편물은 2024-8-29\n제1991호에 의하여\n내용증명우편물로 발송하였음을 증명함\n서울관악우체국장" + }, + { + "type": "text", + "content": "서울관\n악우체\n국장인" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-020.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-020.json new file mode 100644 index 00000000..1f50b8d3 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-020.json @@ -0,0 +1,65 @@ +{ + "source_file": "영업양도계약서_김선웅_오민한.json", + "content": [ + { + "type": "text", + "content": "영업양도 계약서" + }, + { + "type": "text", + "content": "갑: 김선웅 (600123-1******)\n을: 오민한 (540607-1******)" + }, + { + "type": "text", + "content": "갑과 을은 인테리어 시공업체 [해드림(SUN-DREAM)]의 양도양수를 위하여 아래\n와 같이 약정한다." + }, + { + "type": "text", + "content": "제1조: 갑은 본 계약과 동시에 하남시 감북동 238 대 250㎡ 및 그 지상 건물에 관한 임차권과 계약일 현재 위 지상 및 건물에 보관 중인 설비, 비품, 자재, 공구 등 영업에 필요한 자산 일체를 을에게 양도하고, 갑은 임대인의 동의절차에 협조한다." + }, + { + "type": "text", + "content": "제2조: 을은 위 양도의 대가로 2024. 7. 말일까지 갑에게 대금 2억 원을 지급한다." + }, + { + "type": "text", + "content": "제3조: 을은 본 계약과 동시에 갑이 위 지상에서 운영하고 있는 인테리어 시공업체 [해드림(SUN-DREAM)]에 관하여 고객관계, 고용관계를 승계하고, 갑은 그 승계 및 동일 영업을 위한 인허가 등 절차에 협조한다." + }, + { + "type": "text", + "content": "제4조: 을은 [해드림(SUN-DREAM)] 상호를 계속 사용할 수 있고, 갑은 이에 대하여 이의를 제기하거나 대가의 지급을 요구하지 아니한다." + }, + { + "type": "text", + "content": "제5조: 을은 본 계약일 전에 갑의 영업으로 인해 발생한 채무에 대하여 책임을 지지 아니한다." + }, + { + "type": "text", + "content": "2024년 7월 5일" + }, + { + "type": "text", + "content": "갑(양도인) : 김선웅 (600123-1******)" + }, + { + "type": "text", + "content": "웅김\n(인)" + }, + { + "type": "text", + "content": "서울 강남구 역삼2길 339 에이동 302호 (삼성동, 선릉빌라)" + }, + { + "type": "text", + "content": "을(양수인) : 오민한 (540607-1******)" + }, + { + "type": "text", + "content": "漢吳\n印民" + }, + { + "type": "text", + "content": "서울 강서구 공항대로 37" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-021.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-021.json new file mode 100644 index 00000000..6d8d6509 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-021.json @@ -0,0 +1,33 @@ +{ + "source_file": "이행각서_김선웅_강용원.json", + "content": [ + { + "type": "text", + "content": "이 행 각 서" + }, + { + "type": "text", + "content": "강용원 귀하" + }, + { + "type": "text", + "content": "본인은 2021. 6. 5. 자 인테리어 자재 물품대금 채무를 2024. 8. 말까지 차질 없이\n갚겠습니다." + }, + { + "type": "text", + "content": "만일 약속한 기일까지 갚지 못하면 채무를 담보하기에 충분한 보증인이라도 세워 드릴 것\n을 약속드립니다." + }, + { + "type": "text", + "content": "2024. 8. 5." + }, + { + "type": "text", + "content": "각서인 김선웅" + }, + { + "type": "text", + "content": "김선\n웅인" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-022.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-022.json new file mode 100644 index 00000000..af618beb --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-022.json @@ -0,0 +1,45 @@ +{ + "source_file": "임대차계약서_이수인_양정숙.json", + "content": [ + { + "type": "text", + "content": "임대차계약서" + }, + { + "type": "text", + "content": "이수인(임대인)과 양정숙(임차인)은 아래와 같이 임대차계약을 체결함" + }, + { + "type": "text", + "content": "제1조 (임대물건의 표시)\n서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물 201호(150.32㎡)" + }, + { + "type": "text", + "content": "제2조 (임대차기간)\n2024년 2월 1일부터 2029년 1월 31일까지 5년" + }, + { + "type": "text", + "content": "제3조 (임대차보증금 및 차임)\n임대차보증금은 3억 원(₩300,000,000), 월 차임은 500만 원으로 하고, 차임은 매월 말일 지급한다." + }, + { + "type": "text", + "content": "※ 임대차보증금은 임차인이 계약 당일 목적물을 인도받음과 동시에 지급완료하였음을 쌍방 확인\n 함. 이수인 이수인, 양정숙 梁晶淑" + }, + { + "type": "text", + "content": "제4조 (증·개축부분의 소유권 귀속)\n임차인이 임차 건물을 증·개축하였을 때에는 임대인의 승낙 유무에 불구하고 그 부분은 임대인의 소유로 귀속된다." + }, + { + "type": "text", + "content": "2024. 2. 1." + }, + { + "type": "text", + "content": "임대인 : 이수인 (820901-2******) 이수인인\n서울 동작구 만양로 18, 701동 101호(노량진동, 달빛아파트)" + }, + { + "type": "text", + "content": "임차인 : 양정숙 (741121-2******) 梁晶淑\n서울 관악구 봉천3길 12, 301호(성현동, 성현연립)" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-023.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-023.json new file mode 100644 index 00000000..cb9cd299 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-023.json @@ -0,0 +1,38 @@ +{ + "source_file": "임료시세확인서.json", + "content": [ + { + "type": "text", + "content": "임 료 시 세 확 인 서" + }, + { + "type": "table", + "grid": [ + [ + "목적물", + "경기도 평택시 서정3길 123(서정동, 서정빌라)" + ], + [ + "월 차임\n시세", + "보증금 없이\n월세 200만 원" + ], + [ + "기준일", + "2022. 10.경" + ] + ] + }, + { + "type": "text", + "content": "참고\n:\n기준일 이후 현재까지도 시세의 변동은 없는 것으로 판단되고, 특별한 사정이 없\n는 한 이후로도 2~3년간은 같은 시세가 유지될 것으로 전망됨." + }, + { + "type": "text", + "content": "2024. 12. 23." + }, + { + "type": "text", + "content": "공인중개사 김민식\n金旼\n湜印\n(등록번호 3735-97-0827)\n경기도 평택시 중앙동 13 중앙상가 103호" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-024.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-024.json new file mode 100644 index 00000000..2c925298 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-024.json @@ -0,0 +1,53 @@ +{ + "source_file": "차용증_강용원_오국환.json", + "content": [ + { + "type": "text", + "content": "차용증" + }, + { + "type": "text", + "content": "강용원 귀하" + }, + { + "type": "text", + "content": "오국한은 아래와 같이 금전을 차용합니다." + }, + { + "type": "text", + "content": "원 금 : 100,000,000원\n변제기 : 2022. 12. 17.\n이 자 : 월 1%" + }, + { + "type": "text", + "content": "2022. 4. 18." + }, + { + "type": "text", + "content": "대 주 : 강용원 (471211-1******) (元姜印容)\n차 주 : 오국한 (470415-1******) (漢吳印國)" + }, + { + "type": "text", + "content": "차용증" + }, + { + "type": "text", + "content": "강용원 귀하" + }, + { + "type": "text", + "content": "오국한은 아래와 같이 금전을 차용합니다." + }, + { + "type": "text", + "content": "원 금 : 100,000,000원\n변제기 : 2023. 3. 17.\n이 자 : 월 1%" + }, + { + "type": "text", + "content": "2022. 5. 18." + }, + { + "type": "text", + "content": "대 주 : 강용원 (471211-1******) (元姜印容)\n차 주 : 오국한 (470415-1******) (漢吳印國)" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-025.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-025.json new file mode 100644 index 00000000..da6e12ac --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-025.json @@ -0,0 +1,54 @@ +{ + "source_file": "차용증_오민한_강용원_01_2023.json", + "content": [ + { + "type": "text", + "content": "차 용 증" + }, + { + "type": "text", + "content": "오민한 귀하" + }, + { + "type": "text", + "content": "본인은 귀하로부터 아래와 같이 금전을 차용합니다." + }, + { + "type": "table", + "grid": [ + [ + "원 금", + "200,000,000원" + ], + [ + "이 자", + "연 10%" + ], + [ + "변 제 기", + "2024. 1. 5." + ], + [ + "차용금의 용도", + "영업자금" + ], + [ + "특 약", + "본 차용금반환채무의 담보를 위하여 차용인은 서울 성동구 성수동 256 대 330㎡의 5분의 2 지분에 1순위 근저당권을 설정하기로 함" + ] + ] + }, + { + "type": "text", + "content": "2023년 1월 6일" + }, + { + "type": "text", + "content": "차용인: 강용원(471211-1******) 元姜\n印容" + }, + { + "type": "text", + "content": "서울 서초구 방배대로 111, 101동 502호 (방배동, 서래아파트)" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-026.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-026.json new file mode 100644 index 00000000..2e7c29bb --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-026.json @@ -0,0 +1,50 @@ +{ + "source_file": "차용증_오민한_강용원_07_2023.json", + "content": [ + { + "type": "text", + "content": "차 용 증" + }, + { + "type": "text", + "content": "오민한 귀하" + }, + { + "type": "text", + "content": "본인은 귀하로부터 아래와 같이 금전을 차용합니다." + }, + { + "type": "table", + "grid": [ + [ + "원 금", + "300,000,000원" + ], + [ + "이 자", + "연 15%" + ], + [ + "변제기", + "2024. 7. 5." + ], + [ + "차용금의 용도", + "영업자금" + ], + [ + "특 약", + "본 차용금반환채무의 담보를 위하여 차용인은 서울 성동구 성수동 256 대 330㎡의 5분의 3 지분에 1순위 근저당권을 설정하기로 함" + ] + ] + }, + { + "type": "text", + "content": "2023년 7월 6일" + }, + { + "type": "text", + "content": "차용인: 강용원(471211-1******) 元姜 印容\n서울 서초구 방배대로 111, 101동 502호 (방배동, 서래아파트)" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-027.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-027.json new file mode 100644 index 00000000..e0925bc8 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-027.json @@ -0,0 +1,98 @@ +{ + "source_file": "최고통지서_강용원_김선웅.json", + "content": [ + { + "type": "text", + "content": "최고 통지서" + }, + { + "type": "table", + "grid": [ + [ + "수신인", + "김선웅\n서울 강남구 역삼2길 339 에이동 302호 (삼성동, 선릉빌라)" + ] + ] + }, + { + "type": "text", + "content": "1. 귀하의 건승을 기원합니다." + }, + { + "type": "text", + "content": "2. 귀하도 잘 기억하고 계시겠지만, 본인은 2021. 6. 5. 귀하가 운영하는 인테리어 시공업체 [해드림(SUN-DREAM)]에 인테리어 자재 2억 원어치를 납품하였지만, 귀하는 아직도 그 대금을 지급하지 않고 있습니다." + }, + { + "type": "text", + "content": "3. 본인은 2021. 11. 5. 전화와 문자메시지로 귀하에게 변제를 요구한 것 외에, 아직까지 한번도 독촉을 한 적이 없는데, 이는 오랫동안 귀하와 거래하면서 쌓아온 인간관계를 신뢰하였기 때문입니다." + }, + { + "type": "text", + "content": "4. 그런데 귀하가 최근 사업운영이 어려워져서 사업체를 매각하려고 한다는 소문을 듣고서 부득이 최고 통지서를 보내게 되었으니, 혜량하시기 바랍니다." + }, + { + "type": "text", + "content": "5. 귀하도 아시다시피, 최근에는 경기가 어려워져서 본인도 자금사정에 어려움을 겪고 있으니, 부디 늦지 않게 일부라도 변제해 주시기를 당부드립니다." + }, + { + "type": "text", + "content": "2024년 5월 4일" + }, + { + "type": "text", + "content": "발신인 강용원 元姜\n印容\n서울 서초구 방배대로 111, 101동 502호 (방배동, 서래아파트)" + }, + { + "type": "table", + "grid": [ + [ + { + "t": "우 편 물 배 달 증 명 서", + "c": 4 + }, + null, + null, + null + ], + [ + { + "t": "수취인의 주거 및 성명\n 서울 강남구 역삼2길 339 에이동 302호 (삼성\n 동, 선릉빌라) 김선웅", + "c": 4 + }, + null, + null, + null + ], + [ + "접수국명", + "서울서초", + "접수연월일", + "2024년5월4일" + ], + [ + "접수번호", + "12867", + "배달연월일", + "2024년5월6일" + ], + [ + "적 요", + { + "t": "수취인과의 관계\n본인 수령\n김선웅", + "c": 2 + }, + null, + "서울강\n남우체\n국장인\n서울강남우체국장" + ] + ] + }, + { + "type": "text", + "content": "서울서초우체국\n2024. 5. 4.\n24 - 12867" + }, + { + "type": "text", + "content": "본 우편물은 2024-5-4\n제12867호에 의하여\n내용증명우편물로 발송하였음을 증명함\n서울서초우체국장 서울강\n남우체\n국장인" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-028.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-028.json new file mode 100644 index 00000000..3a118ff9 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-028.json @@ -0,0 +1,62 @@ +{ + "source_file": "통지서_오민한_강용원.json", + "content": [ + { + "type": "text", + "content": "통 지 서" + }, + { + "type": "table", + "grid": [ + [ + "수신인", + "강용원\n서울 서초구 방배대로 111, 101동 502호 (방배동, 서래아파트)" + ] + ] + }, + { + "type": "text", + "content": "1. 본인은 수일 전에 귀하로부터 전화로 김선웅씨의 채무를 변제하라는 요구를 받은 오민한입니다." + }, + { + "type": "text", + "content": "2. 본인은 그날 귀하로부터 생각지도 않은 말을 듣고서 당황한 나머지 제대로 답변을 못하였는데, 그 후 김선웅씨를 만나서 전후 상황을 확인하고 법률 자문까지 받은 터라 이렇게 서신으로 본인의 분명한 입장을 전달해 드리고자 합니다." + }, + { + "type": "text", + "content": "3. 귀하는 본인과 김선웅씨와의 계약관계를 잘 알지 못하고, 또 본인이 현재까지 [해드림(SUN-DREAM)] 상호나 간판을 그대로 사용하고 있다 보니까 오해를 한 나머지 저에게 그런 전화를 했던 것 같습니다." + }, + { + "type": "text", + "content": "4. 본인은 김선웅씨와 계약할 당시에, 본인이 [해드림(SUN-DREAM)]의 영업을 인수하기 전에 발생한 채무에 관하여서는 제가 책임지지 않는다는 점을 분명히 약정하고서 계약서에도 그 점을 기재했습니다. [해드림(SUN-DREAM)]은 법인이 아니라 김선웅씨의 개인사업체에 불과하기 때문에 제가 김선웅씨의 자재대금 채무를 대신 갚을 법적인 근거도 없는 것입니다." + }, + { + "type": "text", + "content": "5. 더구나 김선웅씨가 귀하로부터 자재 납품을 받은 것은 이미 오래 전이라 그 채무는 이미 시효로 소멸되었을 것이고, 혹시라도 귀하와 김선웅씨 사이에서 시효 중단이나 시효이익 포기로 볼 사정이 있었더라도 그것이 제3자인 본인에게까지 효력이 있을 수는 없는 것입니다." + }, + { + "type": "text", + "content": "2024년 12월 27일" + }, + { + "type": "text", + "content": "발신인 오민한 漢吳 印民" + }, + { + "type": "text", + "content": "서울 강서구 공항대로 37" + }, + { + "type": "text", + "content": "서울강서우체국\n2024. 12. 27.\n24 - 14227" + }, + { + "type": "text", + "content": "본 우편물은 2024-12-27\n제14227호에 의하여\n내용증명우편물로 발송하였음을 증명함\n서울강서우체국장" + }, + { + "type": "text", + "content": "서울강\n서우체\n국장인" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-029.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-029.json new file mode 100644 index 00000000..c23cace1 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-029.json @@ -0,0 +1,58 @@ +{ + "source_file": "통지서_이수인_윤건우.json", + "content": [ + { + "type": "text", + "content": "통 지 서" + }, + { + "type": "table", + "grid": [ + [ + "발신인", + "이수인\n서울 동작구 만양로 18, 701동 101호 (노량진동, 달빛아파트)" + ], + [ + "수신인", + "윤건우\n서울 영등포구 당산대로 75, 101동 202호 (당산동, 선유도아파트)" + ] + ] + }, + { + "type": "text", + "content": "1. 댁내 두루 평안하시길 기원합니다." + }, + { + "type": "text", + "content": "2. 서울 동작구 흑석로 80 지상 상가건물 201호 관련하여, 제가 세금부담 문제를 충분히 고려하지 않고서 매매계약을 한 점에 대해서는 지난번에 전화로 말씀드린 바와 같이 거듭 양해를 당부드리고, 이 상태로는 제가 소유권이전등기를 넘겨받을 수 없다는 점을 다시 한번 말씀드립니다." + }, + { + "type": "text", + "content": "3. 거듭 청하옵건대, 매매대금을 10% 정도라도 감액하여 주시고, 다른 사람 이름으로 소유권이전등기를 해 주실 것을 당부드립니다." + }, + { + "type": "text", + "content": "4. 귀하의 현명한 답변을 기다리겠습니다." + }, + { + "type": "text", + "content": "2024. 3. 30." + }, + { + "type": "text", + "content": "이수인 인" + }, + { + "type": "text", + "content": "서울동작우체국\n2024. 3. 30.\n24 - 3455" + }, + { + "type": "text", + "content": "본 우편물은 2024-3-30\n제3455호에 의하여\n내용증명우편물로 발송하였음을 증명함\n서울동작우체국장" + }, + { + "type": "text", + "content": "서울동\n작우체\n국장인" + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-030.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-030.json new file mode 100644 index 00000000..e9cce6de --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/evidence_shards/E-030.json @@ -0,0 +1,105 @@ +{ + "source_file": "해제통지서_윤건우_이수인.json", + "content": [ + { + "type": "text", + "content": "해제 통지서" + }, + { + "type": "text", + "content": "발신인 윤건우" + }, + { + "type": "text", + "content": "서울 영등포구 당산대로 75, 101동 202호 (당산동, 선유도아파트)" + }, + { + "type": "text", + "content": "수신인 이수인" + }, + { + "type": "text", + "content": "서울 동작구 만양로 18, 701동 101호 (노량진동, 달빛아파트)" + }, + { + "type": "text", + "content": "귀하의 통지서를 2024. 4. 1. 받아 보았습니다. 저는 지난번 마지막 통화에서 귀하의 요청을 받아들일 수 없다고 분명히 말씀드렸는데, 다시 같은 요구를 하면서 통지서를 보내시니 매우 당혹스럽습니다." + }, + { + "type": "text", + "content": "다른 사람 앞으로 등기를 이전하는 것은 계약 당시 없던 요구였을 뿐 아니라, 현행법상 금지된 것으로 알고 있습니다. 또한 이제 와서 매매대금을 감액해 달라는 요구는 도저히 받아들일 수 없다는 점을 다시 한번 분명히 말씀드립니다." + }, + { + "type": "text", + "content": "귀하도 알다시피 부동산매도인이 소유권이전등기를 넘겨주기 위해서는 매수인의 주민등록상의 인적사항이 명기된 [부동산매도용 인감증명서]를 발급받아야 하는데, 지난번 통화 이후로 귀하는 매수인 인적사항을 알려주기는커녕, 전화·문자메시지도 받지 않는 등 일체의 연락을 피하고 있으니, 본인으로서는 귀하가 더 이상 매매계약을 이행할 의사가 없다고 판단할 수밖에 없습니다." + }, + { + "type": "text", + "content": "따라서 부득이 본인은 귀하와 체결한 흑석동 상가에 관한 2023. 10. 1. 자 매매계약을 지금 이 통지로써 해제하겠습니다. 귀하에게 매도했던 것은 없던 일로 할 테니, 그렇게 아시기 바랍니다." + }, + { + "type": "text", + "content": "2024. 5. 15.\n윤건우 (인) 尹建\n宇印" + }, + { + "type": "text", + "content": "서울영등포우체국\n2024. 5. 15.\n24 - 9813" + }, + { + "type": "text", + "content": "본 우편물은 2024-5-15\n제9813호에 의하여\n내용증명우편물로 발송하였음을 증명함\n서울영등포우체국장" + }, + { + "type": "text", + "content": "서울영\n등포우\n체국장" + }, + { + "type": "text", + "content": "우 편 물 배 달 증 명 서" + }, + { + "type": "table", + "grid": [ + [ + { + "t": "수취인의 주거 및 성명", + "c": 4 + }, + null, + null, + null + ], + [ + { + "t": "서울 동작구 만양로 18, 701동 101호\n(노량진동, 달빛아파트) 이수인", + "c": 4 + }, + null, + null, + null + ], + [ + "접수국명", + "서울영등포", + "접수연월일", + "2024년 5월 15일" + ], + [ + "접수번호", + "9813", + "배달연월일", + "2024년 5월 16일" + ], + [ + "적 요", + { + "t": "수취인과의 관계\n본인 수령\n이수인", + "c": 2 + }, + null, + "서울동\n작우체\n국장인\n서울동작우체국장" + ] + ] + } + ] +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/legal_effect_signals.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/legal_effect_signals.json new file mode 100644 index 00000000..83697dbf --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/legal_effect_signals.json @@ -0,0 +1,797 @@ +{ + "schema_version": "legal_effect_signals.v1", + "status": "READY_WITH_REVIEW", + "bo_legal_effect_routes": [ + { + "bo_id": "bh1", + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "도급", + "공사대금", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-007" + ] + }, + { + "bo_id": "bh2", + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "물품대금", + "매매", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-021", + "E-027" + ] + }, + { + "bo_id": "bh3", + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "대여금", + "소비대차", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-024" + ] + }, + { + "bo_id": "bh4", + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "대여금", + "소비대차", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-024" + ] + }, + { + "bo_id": "bh5", + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "대여금", + "소비대차", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-025" + ] + }, + { + "bo_id": "bh6", + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "대여금", + "소비대차", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-026" + ] + }, + { + "bo_id": "bh7", + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "영업양도", + "상호속용", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-020" + ] + }, + { + "bo_id": "bh8", + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "채무승인", + "변제기 유예", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-021" + ] + }, + { + "bo_id": "bh9", + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "변제", + "법정변제충당", + "event", + "사실행위(factual acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [] + }, + { + "bo_id": "bh10", + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "상계", + "항변", + "event", + "소송행위(litigation acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "bo_id": "bh11", + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "이행최고", + "통지", + "최고", + "event", + "준법률행위(quasi-legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-027" + ] + }, + { + "bo_id": "bh12", + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "통지", + "책임부인", + "event", + "준법률행위(quasi-legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-028" + ] + }, + { + "bo_id": "bh13", + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "미지급", + "충당", + "state", + "사실행위(factual acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ] + }, + { + "bo_id": "bh14", + "candidate_structure_types": [ + "secured_debt_creation", + "secured_registry_or_auction", + "상속", + "증여", + "근저당권설정", + "(근)저당권 설정 계약", + "event", + "법률행위(legal acts)", + "B2_Secured_Registry" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ] + }, + { + "bo_id": "bh15", + "candidate_structure_types": [ + "secured_debt_chain", + "mortgage_extinguishment", + "secured_registry_or_auction", + "근저당권설정", + "말소", + "상계", + "근저당권 말소 및 상계", + "event", + "법률행위(legal acts)", + "B2_Secured_Registry" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-010", + "E-014" + ] + }, + { + "bo_id": "bh16", + "candidate_structure_types": [ + "secured_registry_or_auction", + "소유권보존", + "소유권이전", + "가압류", + "소유권이전등기", + "event", + "법률행위(legal acts)", + "B2_Secured_Registry" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-015" + ] + }, + { + "bo_id": "bh17", + "candidate_structure_types": [ + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "secured_registry_or_auction", + "임의경매", + "공신력", + "등기무효", + "담보권실행을 위한 경매 및 근저당권 설정", + "event", + "소송행위(litigation acts)", + "B2_Secured_Registry" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-012", + "E-013" + ] + }, + { + "bo_id": "bh18", + "candidate_structure_types": [ + "payment_allocation_uncertain", + "secured_registry_or_auction", + "변제", + "변제충당", + "변제 및 변제충당", + "event", + "준법률행위(quasi-legal acts)", + "B2_Secured_Registry" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [] + }, + { + "bo_id": "bh19", + "candidate_structure_types": [ + "payment_allocation", + "secured_registry_or_auction", + "유치권", + "점유", + "변제충당", + "변제충당 주장", + "event", + "준법률행위(quasi-legal acts)", + "B2_Secured_Registry" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-007", + "E-011" + ] + }, + { + "bo_id": "bh20", + "candidate_structure_types": [ + "registry_defect_hint", + "secured_registry_or_auction", + "소유권이전등기", + "요구", + "거절", + "제3자 명의 등기 요청", + "event", + "준법률행위(quasi-legal acts)", + "B2_Secured_Registry" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-029" + ] + }, + { + "bo_id": "bh21", + "candidate_structure_types": [ + "valuation", + "land_use_gain_or_valuation", + "가치평가", + "임대료", + "차임", + "event", + "사실행위(factual acts)", + "B3_Land_Valuation" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-005" + ] + }, + { + "bo_id": "bh22", + "candidate_structure_types": [ + "possession", + "land_use_gain_or_valuation", + "인도", + "점유", + "사용", + "state", + "사실행위(factual acts)", + "B3_Land_Valuation" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-006" + ] + }, + { + "bo_id": "bh23", + "candidate_structure_types": [ + "valuation", + "land_use_gain_or_valuation", + "가치평가", + "감정평가", + "state", + "사실행위(factual acts)", + "B3_Land_Valuation" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-004" + ] + }, + { + "bo_id": "bh24", + "candidate_structure_types": [ + "possession", + "defense_assertion_benefit_applied_to_secured_claim", + "land_use_gain_or_valuation", + "점유", + "거주", + "유치권", + "수익", + "state", + "사실행위(factual acts)", + "B3_Land_Valuation" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ] + }, + { + "bo_id": "bh25", + "candidate_structure_types": [ + "valuation", + "land_use_gain_or_valuation", + "가치평가", + "차임", + "state", + "사실행위(factual acts)", + "B3_Land_Valuation" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-023" + ] + }, + { + "bo_id": "bh26", + "candidate_structure_types": [ + "possession", + "land_use_gain_or_valuation", + "인도", + "점유", + "운영", + "사업자등록", + "state", + "사실행위(factual acts)", + "B3_Land_Valuation" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ] + }, + { + "bo_id": "bh27", + "candidate_structure_types": [ + "fraudulent_act", + "fraudulent_transfer_BO", + "사해행위", + "매매계약", + "소유권이전", + "책임재산감소", + "매매", + "event", + "법률행위(legal acts)", + "B4_Actio" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "bo_id": "bh28", + "candidate_structure_types": [ + "preserved_claim", + "preserved_claim_bundle_BO", + "피보전채권", + "대여금채권", + "소비대차", + "event", + "법률행위(legal acts)", + "B4_Actio" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-024" + ] + }, + { + "bo_id": "bh29", + "candidate_structure_types": [ + "value_compensation", + "remedy_cap", + "remedy_mode_decision_BO", + "원물반환불능", + "가액배상", + "근저당권말소", + "변제 및 말소", + "event", + "준법률행위(quasi-legal acts)", + "B4_Actio" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "bo_id": "bh30", + "candidate_structure_types": [ + "insolvency_check", + "actio_balance_sheet_BO", + "무자력", + "책임재산", + "가치평가", + "state", + "사실행위(factual acts)", + "B4_Actio" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-004" + ] + }, + { + "bo_id": "bh31", + "candidate_structure_types": [ + "defense_assertion", + "defense_rebuttal_matrix_BO", + "수익자항변", + "명의신탁", + "상계항변", + "선의항변", + "항변", + "state", + "소송행위(litigation acts)", + "B4_Actio" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "bo_id": "bh32", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상속개시", + "event", + "사실행위(factual acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-013" + ] + }, + { + "bo_id": "bh33", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상속포기", + "event", + "사실행위(factual acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-001", + "E-002" + ] + }, + { + "bo_id": "bh34", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-027" + ] + }, + { + "bo_id": "bh35", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-008", + "E-010" + ] + }, + { + "bo_id": "bh36", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "통지", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011", + "E-019" + ] + }, + { + "bo_id": "bh37", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "통지", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-028" + ] + }, + { + "bo_id": "bh38", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "통지", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-010" + ] + }, + { + "bo_id": "bh39", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "통지", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-009" + ] + }, + { + "bo_id": "bh40", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "통지", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-030" + ] + }, + { + "bo_id": "bh41", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "통지", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-029" + ] + }, + { + "bo_id": "bh42", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "통지", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-012" + ] + }, + { + "bo_id": "bh43", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ] + }, + { + "bo_id": "bh44", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ] + }, + { + "bo_id": "bh45", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-006" + ] + }, + { + "bo_id": "bh46", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-018", + "E-022" + ] + }, + { + "bo_id": "bh47", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상계", + "state", + "소송행위(litigation acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-010" + ] + }, + { + "bo_id": "bh48", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "항변", + "state", + "소송행위(litigation acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ] + }, + { + "bo_id": "bh49", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "항변", + "state", + "소송행위(litigation acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-009" + ] + }, + { + "bo_id": "bh50", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "항변", + "state", + "소송행위(litigation acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-012" + ] + } + ] +} diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/legal_effect_structures.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/legal_effect_structures.json new file mode 100644 index 00000000..de752f77 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/legal_effect_structures.json @@ -0,0 +1,9824 @@ +{ + "schema_version": "stage1_legal_effect_structures.v1", + "status": "READY_WITH_REVIEW", + "legal_effect_structures": [ + { + "structure_id": "LES-001", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh1", + "source_bo_ids": [ + "bh1" + ], + "issue_cluster_ids": [ + "IC-001" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-007" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:011" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-007" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-001", + "candidate_structure_types": [ + "money_claim", + "도급", + "공사대금", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-002", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh10", + "source_bo_ids": [ + "bh10" + ], + "issue_cluster_ids": [ + "IC-002" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:013" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-002", + "candidate_structure_types": [ + "money_claim", + "상계", + "succession_notice_lien_asset_defense", + "event", + "소송행위(litigation_acts)", + "commercial_successor" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-003", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh11", + "source_bo_ids": [ + "bh11" + ], + "issue_cluster_ids": [ + "IC-003" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-027" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:003" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-027" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-003", + "candidate_structure_types": [ + "money_claim", + "이행최고", + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal_acts)", + "commercial_successor" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-004", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh12", + "source_bo_ids": [ + "bh12" + ], + "issue_cluster_ids": [ + "IC-004" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-028" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:005" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "linked_structures", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-028" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-004", + "candidate_structure_types": [ + "money_claim", + "succession_notice_lien_asset_defense", + "책임부인", + "event", + "준법률행위(quasi-legal_acts)", + "commercial_successor" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-005", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh13", + "source_bo_ids": [ + "bh13" + ], + "issue_cluster_ids": [ + "IC-005" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-011" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:012" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-011" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-005", + "candidate_structure_types": [ + "money_claim", + "충당", + "state", + "사실행위(factual_acts)", + "commercial_successor" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-006", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh14", + "source_bo_ids": [ + "bh14" + ], + "issue_cluster_ids": [ + "IC-006" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:001" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-006", + "candidate_structure_types": [ + "secured_debt", + "상속", + "증여", + "(근)저당권_설정_계약", + "event", + "법률행위(legal_acts)" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-007", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh15", + "source_bo_ids": [ + "bh15" + ], + "issue_cluster_ids": [ + "IC-007" + ], + "asset_cluster_ids": [ + "AC-002" + ], + "evidence_indexes": [ + "E-010", + "E-014" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:004" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-010", + "E-014" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-007", + "candidate_structure_types": [ + "secured_debt", + "mortgage_extinguishment", + "registry_invalidity", + "상계", + "event", + "법률행위(legal_acts)" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-008", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh16", + "source_bo_ids": [ + "bh16" + ], + "issue_cluster_ids": [ + "IC-008" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_indexes": [ + "E-015" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:006" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-015" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-008", + "candidate_structure_types": [ + "secured_debt", + "소유권보존", + "소유권이전", + "가압류", + "registry_invalidity", + "event", + "법률행위(legal_acts)" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-009", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh17", + "source_bo_ids": [ + "bh17" + ], + "issue_cluster_ids": [ + "IC-009" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_indexes": [ + "E-012", + "E-013" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:003" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-012", + "E-013" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-009", + "candidate_structure_types": [ + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "secured_debt", + "임의경매", + "공신력", + "registry_invalidity", + "event", + "소송행위(litigation_acts)" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-010", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh18", + "source_bo_ids": [ + "bh18" + ], + "issue_cluster_ids": [ + "IC-010" + ], + "asset_cluster_ids": [], + "evidence_indexes": [], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:002" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-010", + "candidate_structure_types": [ + "payment_allocation_uncertain", + "secured_debt", + "변제", + "변제충당", + "변제_및_변제충당", + "event", + "준법률행위(quasi-legal_acts)" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-011", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh19", + "source_bo_ids": [ + "bh19" + ], + "issue_cluster_ids": [ + "IC-011" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_indexes": [ + "E-007", + "E-011" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:005" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "legal_calculation_object", + "claim_chain_ref" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-007", + "E-011" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-011", + "candidate_structure_types": [ + "payment_allocation", + "secured_debt", + "succession_notice_lien_asset_defense", + "land_use_gain", + "변제충당", + "변제충당_주장", + "event", + "준법률행위(quasi-legal_acts)", + "money_claim" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-012", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh2", + "source_bo_ids": [ + "bh2" + ], + "issue_cluster_ids": [ + "IC-012" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-021", + "E-027" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:001" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-021", + "E-027" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-012", + "candidate_structure_types": [ + "money_claim", + "물품대금", + "매매", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-013", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh20", + "source_bo_ids": [ + "bh20" + ], + "issue_cluster_ids": [ + "IC-013" + ], + "asset_cluster_ids": [ + "AC-004" + ], + "evidence_indexes": [ + "E-029" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:007" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-029" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-013", + "candidate_structure_types": [ + "registry_invalidity", + "secured_debt", + "요구", + "거절", + "event", + "준법률행위(quasi-legal_acts)" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-014", + "candidate_structure_type": "land_use_gain", + "structure_label": "land use gain structure for bh21", + "source_bo_ids": [ + "bh21" + ], + "issue_cluster_ids": [ + "IC-014" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006" + ], + "evidence_indexes": [ + "E-005" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:002" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-005" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-014", + "candidate_structure_types": [ + "valuation", + "land_use_gain", + "임대료", + "차임", + "event", + "사실행위(factual_acts)" + ], + "multi_asset_bundle_refs": [ + "bundle:bh21" + ] + } + }, + { + "structure_id": "LES-015", + "candidate_structure_type": "land_use_gain", + "structure_label": "land use gain structure for bh22", + "source_bo_ids": [ + "bh22" + ], + "issue_cluster_ids": [ + "IC-015" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-007", + "AC-008" + ], + "evidence_indexes": [ + "E-006" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:004" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_structures" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-006" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-015", + "candidate_structure_types": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "사용", + "state", + "사실행위(factual_acts)" + ], + "multi_asset_bundle_refs": [ + "bundle:bh22" + ] + } + }, + { + "structure_id": "LES-016", + "candidate_structure_type": "land_use_gain", + "structure_label": "land use gain structure for bh23", + "source_bo_ids": [ + "bh23" + ], + "issue_cluster_ids": [ + "IC-016" + ], + "asset_cluster_ids": [ + "AC-009", + "AC-010" + ], + "evidence_indexes": [ + "E-004" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:001" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-004" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-016", + "candidate_structure_types": [ + "valuation", + "land_use_gain", + "state", + "사실행위(factual_acts)" + ], + "multi_asset_bundle_refs": [ + "bundle:bh23" + ] + } + }, + { + "structure_id": "LES-017", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh24", + "source_bo_ids": [ + "bh24" + ], + "issue_cluster_ids": [ + "IC-017" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-012" + ], + "evidence_indexes": [ + "E-011" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:005" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "legal_calculation_object" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-011" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-017", + "candidate_structure_types": [ + "possession", + "secured_debt", + "land_use_gain", + "거주", + "succession_notice_lien_asset_defense", + "수익", + "state", + "사실행위(factual_acts)" + ], + "multi_asset_bundle_refs": [ + "bundle:bh24" + ] + } + }, + { + "structure_id": "LES-018", + "candidate_structure_type": "land_use_gain", + "structure_label": "land use gain structure for bh25", + "source_bo_ids": [ + "bh25" + ], + "issue_cluster_ids": [ + "IC-018" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-013" + ], + "evidence_indexes": [ + "E-023" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:003" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-023" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-018", + "candidate_structure_types": [ + "valuation", + "land_use_gain", + "차임", + "state", + "사실행위(factual_acts)" + ], + "multi_asset_bundle_refs": [ + "bundle:bh25" + ] + } + }, + { + "structure_id": "LES-019", + "candidate_structure_type": "land_use_gain", + "structure_label": "land use gain structure for bh26", + "source_bo_ids": [ + "bh26" + ], + "issue_cluster_ids": [ + "IC-019" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:006" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_structures" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-019", + "candidate_structure_types": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "운영", + "사업자등록", + "state", + "사실행위(factual_acts)" + ], + "multi_asset_bundle_refs": [ + "bundle:bh26" + ] + } + }, + { + "structure_id": "LES-020", + "candidate_structure_type": "fraudulent_transfer", + "structure_label": "fraudulent transfer structure for bh27", + "source_bo_ids": [ + "bh27" + ], + "issue_cluster_ids": [ + "IC-020" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_indexes": [ + "E-014" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B4:002" + ], + "linked_actio_signal_ids": [ + "actio-signal-002" + ], + "legal_effect_roles": [ + "actio_roles", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "actio_case_signals.json", + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-014" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-020", + "candidate_structure_types": [ + "fraudulent_transfer", + "매매계약", + "소유권이전", + "책임재산감소", + "매매", + "event", + "법률행위(legal_acts)" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-021", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh28", + "source_bo_ids": [ + "bh28" + ], + "issue_cluster_ids": [ + "IC-021" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-024" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B4:001" + ], + "linked_actio_signal_ids": [ + "actio-signal-001" + ], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "actio_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "actio_case_signals.json", + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-024" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-021", + "candidate_structure_types": [ + "preserved_claim", + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "fraudulent_transfer" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-022", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh29", + "source_bo_ids": [ + "bh29" + ], + "issue_cluster_ids": [ + "IC-022" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-002" + ], + "evidence_indexes": [ + "E-014" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B4:004" + ], + "linked_actio_signal_ids": [ + "actio-signal-004" + ], + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_actio_structures", + "linked_structures", + "actio_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "actio_case_signals.json", + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-014" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-022", + "candidate_structure_types": [ + "value_compensation", + "valuation", + "actio_remedy_or_cap", + "원물반환불능", + "secured_debt", + "registry_invalidity", + "event", + "준법률행위(quasi-legal_acts)", + "fraudulent_transfer" + ], + "multi_asset_bundle_refs": [ + "bundle:bh29" + ] + } + }, + { + "structure_id": "LES-023", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh3", + "source_bo_ids": [ + "bh3" + ], + "issue_cluster_ids": [ + "IC-023" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-024" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:006" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-024" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-023", + "candidate_structure_types": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-024", + "candidate_structure_type": "valuation", + "structure_label": "valuation structure for bh30", + "source_bo_ids": [ + "bh30" + ], + "issue_cluster_ids": [ + "IC-024" + ], + "asset_cluster_ids": [ + "AC-020", + "AC-021" + ], + "evidence_indexes": [ + "E-004" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B4:003" + ], + "linked_actio_signal_ids": [ + "actio-signal-003" + ], + "legal_effect_roles": [ + "legal_effect_roles", + "actio_roles", + "legal_calculation_object" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "actio_case_signals.json", + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-004" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-024", + "candidate_structure_types": [ + "insolvency_check", + "fraudulent_transfer", + "무자력", + "책임재산", + "valuation", + "state", + "사실행위(factual_acts)" + ], + "multi_asset_bundle_refs": [ + "bundle:bh30" + ] + } + }, + { + "structure_id": "LES-025", + "candidate_structure_type": "fraudulent_transfer", + "structure_label": "fraudulent transfer structure for bh31", + "source_bo_ids": [ + "bh31" + ], + "issue_cluster_ids": [ + "IC-025" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B4:005" + ], + "linked_actio_signal_ids": [ + "actio-signal-005" + ], + "legal_effect_roles": [ + "legal_effect_roles", + "actio_roles", + "linked_structures" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "actio_case_signals.json", + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-025", + "candidate_structure_types": [ + "defense_assertion", + "defense_rebuttal_matrix_bo", + "fraudulent_transfer", + "명의신탁", + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-026", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh32", + "source_bo_ids": [ + "bh32" + ], + "issue_cluster_ids": [ + "IC-026" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-013" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:002" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-013" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-026", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상속개시", + "event", + "사실행위(factual_acts)" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-027", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh33", + "source_bo_ids": [ + "bh33" + ], + "issue_cluster_ids": [ + "IC-027" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-001", + "E-002" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:003" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-001", + "E-002" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-027", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상속포기", + "event", + "사실행위(factual_acts)" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-028", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh34", + "source_bo_ids": [ + "bh34" + ], + "issue_cluster_ids": [ + "IC-028" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-027" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:009" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-027" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-028", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal_acts)" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-029", + "candidate_structure_type": "fraudulent_transfer", + "structure_label": "fraudulent transfer structure for bh35", + "source_bo_ids": [ + "bh35" + ], + "issue_cluster_ids": [ + "IC-029" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-008", + "E-010" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:004" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "actio_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-008", + "E-010" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-029", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal_acts)", + "fraudulent_transfer" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-030", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh36", + "source_bo_ids": [ + "bh36" + ], + "issue_cluster_ids": [ + "IC-030" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-011", + "E-019" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:007" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-011", + "E-019" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-030", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)", + "secured_debt" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-031", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh37", + "source_bo_ids": [ + "bh37" + ], + "issue_cluster_ids": [ + "IC-031" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-028" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:010" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-028" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-031", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-032", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh38", + "source_bo_ids": [ + "bh38" + ], + "issue_cluster_ids": [ + "IC-032" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-010" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:006" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-010" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-032", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-033", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh39", + "source_bo_ids": [ + "bh39" + ], + "issue_cluster_ids": [ + "IC-033" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-009" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:005" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-009" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-033", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-034", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh4", + "source_bo_ids": [ + "bh4" + ], + "issue_cluster_ids": [ + "IC-034" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-024" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:007" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-024" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-034", + "candidate_structure_types": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-035", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh40", + "source_bo_ids": [ + "bh40" + ], + "issue_cluster_ids": [ + "IC-035" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-030" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:012" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-030" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-035", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-036", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh41", + "source_bo_ids": [ + "bh41" + ], + "issue_cluster_ids": [ + "IC-036" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-029" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:011" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-029" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-036", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)", + "money_claim" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-037", + "candidate_structure_type": "registry_invalidity", + "structure_label": "registry invalidity structure for bh42", + "source_bo_ids": [ + "bh42" + ], + "issue_cluster_ids": [ + "IC-037" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-012" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:008" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-012" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-037", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)", + "registry_invalidity" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-038", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh43", + "source_bo_ids": [ + "bh43" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "asset_cluster_ids": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:001" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-038", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "multi_asset_bundle_refs": [ + "bundle:bh43" + ] + } + }, + { + "structure_id": "LES-039", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh44", + "source_bo_ids": [ + "bh44" + ], + "issue_cluster_ids": [ + "IC-039" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-011" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:013" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-011" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-039", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)", + "secured_debt" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-040", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh45", + "source_bo_ids": [ + "bh45" + ], + "issue_cluster_ids": [ + "IC-040" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-006" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:014" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-006" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-040", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-041", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh46", + "source_bo_ids": [ + "bh46" + ], + "issue_cluster_ids": [ + "IC-041" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-018", + "E-022" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:015" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-018", + "E-022" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-041", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-042", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh47", + "source_bo_ids": [ + "bh47" + ], + "issue_cluster_ids": [ + "IC-042" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-010" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:017" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-010" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-042", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상계", + "state", + "소송행위(litigation_acts)", + "money_claim" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-043", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh48", + "source_bo_ids": [ + "bh48" + ], + "issue_cluster_ids": [ + "IC-043" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-011" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:018" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-011" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-043", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-044", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh49", + "source_bo_ids": [ + "bh49" + ], + "issue_cluster_ids": [ + "IC-044" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-009" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:016" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-009" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-044", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-045", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh5", + "source_bo_ids": [ + "bh5" + ], + "issue_cluster_ids": [ + "IC-045" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-025" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:008" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-025" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-045", + "candidate_structure_types": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-046", + "candidate_structure_type": "registry_invalidity", + "structure_label": "registry invalidity structure for bh50", + "source_bo_ids": [ + "bh50" + ], + "issue_cluster_ids": [ + "IC-046" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-012" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:019" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-012" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-046", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)", + "registry_invalidity" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-047", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh6", + "source_bo_ids": [ + "bh6" + ], + "issue_cluster_ids": [ + "IC-047" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-026" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:009" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-026" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-047", + "candidate_structure_types": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-048", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh7", + "source_bo_ids": [ + "bh7" + ], + "issue_cluster_ids": [ + "IC-048" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-020" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:004" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-020" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-048", + "candidate_structure_types": [ + "money_claim", + "영업양도", + "commercial_successor", + "event", + "법률행위(legal_acts)" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-049", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh8", + "source_bo_ids": [ + "bh8" + ], + "issue_cluster_ids": [ + "IC-049" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-021" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:002" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [ + "E-021" + ] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-049", + "candidate_structure_types": [ + "money_claim", + "채무승인", + "변제기_유예", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "multi_asset_bundle_refs": [] + } + }, + { + "structure_id": "LES-050", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh9", + "source_bo_ids": [ + "bh9" + ], + "issue_cluster_ids": [ + "IC-050" + ], + "asset_cluster_ids": [], + "evidence_indexes": [], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:010" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis": { + "primary_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "source_signal_files": [ + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "source_evidence_indexes": [] + }, + "review_required": true, + "routing_audit": { + "source_seed_id": "LESS-050", + "candidate_structure_types": [ + "money_claim", + "변제", + "법정변제충당", + "event", + "사실행위(factual_acts)", + "commercial_successor" + ], + "multi_asset_bundle_refs": [] + } + } + ], + "structure_index": { + "by_bo_id": { + "bh1": { + "bo_id": "bh1", + "structure_ids": [ + "LES-001" + ], + "structure_labels": [ + "money claim structure for bh1" + ], + "issue_cluster_ids": [ + "IC-001" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-007" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:011" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh10": { + "bo_id": "bh10", + "structure_ids": [ + "LES-002" + ], + "structure_labels": [ + "money claim structure for bh10" + ], + "issue_cluster_ids": [ + "IC-002" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:013" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh11": { + "bo_id": "bh11", + "structure_ids": [ + "LES-003" + ], + "structure_labels": [ + "money claim structure for bh11" + ], + "issue_cluster_ids": [ + "IC-003" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-027" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:003" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh12": { + "bo_id": "bh12", + "structure_ids": [ + "LES-004" + ], + "structure_labels": [ + "money claim structure for bh12" + ], + "issue_cluster_ids": [ + "IC-004" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-028" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:005" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh13": { + "bo_id": "bh13", + "structure_ids": [ + "LES-005" + ], + "structure_labels": [ + "money claim structure for bh13" + ], + "issue_cluster_ids": [ + "IC-005" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-011" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:012" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh14": { + "bo_id": "bh14", + "structure_ids": [ + "LES-006" + ], + "structure_labels": [ + "secured debt structure for bh14" + ], + "issue_cluster_ids": [ + "IC-006" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:001" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh15": { + "bo_id": "bh15", + "structure_ids": [ + "LES-007" + ], + "structure_labels": [ + "secured debt structure for bh15" + ], + "issue_cluster_ids": [ + "IC-007" + ], + "asset_cluster_ids": [ + "AC-002" + ], + "evidence_indexes": [ + "E-010", + "E-014" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:004" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh16": { + "bo_id": "bh16", + "structure_ids": [ + "LES-008" + ], + "structure_labels": [ + "secured debt structure for bh16" + ], + "issue_cluster_ids": [ + "IC-008" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_indexes": [ + "E-015" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:006" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh17": { + "bo_id": "bh17", + "structure_ids": [ + "LES-009" + ], + "structure_labels": [ + "secured debt structure for bh17" + ], + "issue_cluster_ids": [ + "IC-009" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_indexes": [ + "E-012", + "E-013" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:003" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh18": { + "bo_id": "bh18", + "structure_ids": [ + "LES-010" + ], + "structure_labels": [ + "secured debt structure for bh18" + ], + "issue_cluster_ids": [ + "IC-010" + ], + "asset_cluster_ids": [], + "evidence_indexes": [], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:002" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh19": { + "bo_id": "bh19", + "structure_ids": [ + "LES-011" + ], + "structure_labels": [ + "money claim structure for bh19" + ], + "issue_cluster_ids": [ + "IC-011" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_indexes": [ + "E-007", + "E-011" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:005" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "legal_calculation_object", + "claim_chain_ref" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh2": { + "bo_id": "bh2", + "structure_ids": [ + "LES-012" + ], + "structure_labels": [ + "money claim structure for bh2" + ], + "issue_cluster_ids": [ + "IC-012" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-021", + "E-027" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:001" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh20": { + "bo_id": "bh20", + "structure_ids": [ + "LES-013" + ], + "structure_labels": [ + "secured debt structure for bh20" + ], + "issue_cluster_ids": [ + "IC-013" + ], + "asset_cluster_ids": [ + "AC-004" + ], + "evidence_indexes": [ + "E-029" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:007" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh21": { + "bo_id": "bh21", + "structure_ids": [ + "LES-014" + ], + "structure_labels": [ + "land use gain structure for bh21" + ], + "issue_cluster_ids": [ + "IC-014" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006" + ], + "evidence_indexes": [ + "E-005" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:002" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [ + "bundle:bh21" + ], + "review_required": true + }, + "bh22": { + "bo_id": "bh22", + "structure_ids": [ + "LES-015" + ], + "structure_labels": [ + "land use gain structure for bh22" + ], + "issue_cluster_ids": [ + "IC-015" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-007", + "AC-008" + ], + "evidence_indexes": [ + "E-006" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:004" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [ + "bundle:bh22" + ], + "review_required": true + }, + "bh23": { + "bo_id": "bh23", + "structure_ids": [ + "LES-016" + ], + "structure_labels": [ + "land use gain structure for bh23" + ], + "issue_cluster_ids": [ + "IC-016" + ], + "asset_cluster_ids": [ + "AC-009", + "AC-010" + ], + "evidence_indexes": [ + "E-004" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:001" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [ + "bundle:bh23" + ], + "review_required": true + }, + "bh24": { + "bo_id": "bh24", + "structure_ids": [ + "LES-017" + ], + "structure_labels": [ + "secured debt structure for bh24" + ], + "issue_cluster_ids": [ + "IC-017" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-012" + ], + "evidence_indexes": [ + "E-011" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:005" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "legal_calculation_object" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [ + "bundle:bh24" + ], + "review_required": true + }, + "bh25": { + "bo_id": "bh25", + "structure_ids": [ + "LES-018" + ], + "structure_labels": [ + "land use gain structure for bh25" + ], + "issue_cluster_ids": [ + "IC-018" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-013" + ], + "evidence_indexes": [ + "E-023" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:003" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [ + "bundle:bh25" + ], + "review_required": true + }, + "bh26": { + "bo_id": "bh26", + "structure_ids": [ + "LES-019" + ], + "structure_labels": [ + "land use gain structure for bh26" + ], + "issue_cluster_ids": [ + "IC-019" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:006" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [ + "bundle:bh26" + ], + "review_required": true + }, + "bh27": { + "bo_id": "bh27", + "structure_ids": [ + "LES-020" + ], + "structure_labels": [ + "fraudulent transfer structure for bh27" + ], + "issue_cluster_ids": [ + "IC-020" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_indexes": [ + "E-014" + ], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:002" + ], + "related_actio_signal_ids": [ + "actio-signal-002" + ], + "legal_effect_roles": [ + "actio_roles", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh28": { + "bo_id": "bh28", + "structure_ids": [ + "LES-021" + ], + "structure_labels": [ + "money claim structure for bh28" + ], + "issue_cluster_ids": [ + "IC-021" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-024" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:001" + ], + "related_actio_signal_ids": [ + "actio-signal-001" + ], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "actio_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh29": { + "bo_id": "bh29", + "structure_ids": [ + "LES-022" + ], + "structure_labels": [ + "secured debt structure for bh29" + ], + "issue_cluster_ids": [ + "IC-022" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-002" + ], + "evidence_indexes": [ + "E-014" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:004" + ], + "related_actio_signal_ids": [ + "actio-signal-004" + ], + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_actio_structures", + "linked_structures", + "actio_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [ + "bundle:bh29" + ], + "review_required": true + }, + "bh3": { + "bo_id": "bh3", + "structure_ids": [ + "LES-023" + ], + "structure_labels": [ + "money claim structure for bh3" + ], + "issue_cluster_ids": [ + "IC-023" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-024" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:006" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh30": { + "bo_id": "bh30", + "structure_ids": [ + "LES-024" + ], + "structure_labels": [ + "valuation structure for bh30" + ], + "issue_cluster_ids": [ + "IC-024" + ], + "asset_cluster_ids": [ + "AC-020", + "AC-021" + ], + "evidence_indexes": [ + "E-004" + ], + "candidate_structure_types": [ + "valuation" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:003" + ], + "related_actio_signal_ids": [ + "actio-signal-003" + ], + "legal_effect_roles": [ + "legal_effect_roles", + "actio_roles", + "legal_calculation_object" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [ + "bundle:bh30" + ], + "review_required": true + }, + "bh31": { + "bo_id": "bh31", + "structure_ids": [ + "LES-025" + ], + "structure_labels": [ + "fraudulent transfer structure for bh31" + ], + "issue_cluster_ids": [ + "IC-025" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:005" + ], + "related_actio_signal_ids": [ + "actio-signal-005" + ], + "legal_effect_roles": [ + "legal_effect_roles", + "actio_roles", + "linked_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh32": { + "bo_id": "bh32", + "structure_ids": [ + "LES-026" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh32" + ], + "issue_cluster_ids": [ + "IC-026" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-013" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:002" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh33": { + "bo_id": "bh33", + "structure_ids": [ + "LES-027" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh33" + ], + "issue_cluster_ids": [ + "IC-027" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-001", + "E-002" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:003" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh34": { + "bo_id": "bh34", + "structure_ids": [ + "LES-028" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh34" + ], + "issue_cluster_ids": [ + "IC-028" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-027" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:009" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh35": { + "bo_id": "bh35", + "structure_ids": [ + "LES-029" + ], + "structure_labels": [ + "fraudulent transfer structure for bh35" + ], + "issue_cluster_ids": [ + "IC-029" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-008", + "E-010" + ], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:004" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "actio_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh36": { + "bo_id": "bh36", + "structure_ids": [ + "LES-030" + ], + "structure_labels": [ + "secured debt structure for bh36" + ], + "issue_cluster_ids": [ + "IC-030" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-011", + "E-019" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:007" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh37": { + "bo_id": "bh37", + "structure_ids": [ + "LES-031" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh37" + ], + "issue_cluster_ids": [ + "IC-031" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-028" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:010" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh38": { + "bo_id": "bh38", + "structure_ids": [ + "LES-032" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh38" + ], + "issue_cluster_ids": [ + "IC-032" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-010" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:006" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh39": { + "bo_id": "bh39", + "structure_ids": [ + "LES-033" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh39" + ], + "issue_cluster_ids": [ + "IC-033" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-009" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:005" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh4": { + "bo_id": "bh4", + "structure_ids": [ + "LES-034" + ], + "structure_labels": [ + "money claim structure for bh4" + ], + "issue_cluster_ids": [ + "IC-034" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-024" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:007" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh40": { + "bo_id": "bh40", + "structure_ids": [ + "LES-035" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh40" + ], + "issue_cluster_ids": [ + "IC-035" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-030" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:012" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh41": { + "bo_id": "bh41", + "structure_ids": [ + "LES-036" + ], + "structure_labels": [ + "money claim structure for bh41" + ], + "issue_cluster_ids": [ + "IC-036" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-029" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:011" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh42": { + "bo_id": "bh42", + "structure_ids": [ + "LES-037" + ], + "structure_labels": [ + "registry invalidity structure for bh42" + ], + "issue_cluster_ids": [ + "IC-037" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-012" + ], + "candidate_structure_types": [ + "registry_invalidity" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:008" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh43": { + "bo_id": "bh43", + "structure_ids": [ + "LES-038" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh43" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "asset_cluster_ids": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:001" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [ + "bundle:bh43" + ], + "review_required": true + }, + "bh44": { + "bo_id": "bh44", + "structure_ids": [ + "LES-039" + ], + "structure_labels": [ + "secured debt structure for bh44" + ], + "issue_cluster_ids": [ + "IC-039" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-011" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:013" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh45": { + "bo_id": "bh45", + "structure_ids": [ + "LES-040" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh45" + ], + "issue_cluster_ids": [ + "IC-040" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-006" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:014" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh46": { + "bo_id": "bh46", + "structure_ids": [ + "LES-041" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh46" + ], + "issue_cluster_ids": [ + "IC-041" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-018", + "E-022" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:015" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh47": { + "bo_id": "bh47", + "structure_ids": [ + "LES-042" + ], + "structure_labels": [ + "money claim structure for bh47" + ], + "issue_cluster_ids": [ + "IC-042" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-010" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:017" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh48": { + "bo_id": "bh48", + "structure_ids": [ + "LES-043" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh48" + ], + "issue_cluster_ids": [ + "IC-043" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-011" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:018" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh49": { + "bo_id": "bh49", + "structure_ids": [ + "LES-044" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh49" + ], + "issue_cluster_ids": [ + "IC-044" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-009" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:016" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh5": { + "bo_id": "bh5", + "structure_ids": [ + "LES-045" + ], + "structure_labels": [ + "money claim structure for bh5" + ], + "issue_cluster_ids": [ + "IC-045" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-025" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:008" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh50": { + "bo_id": "bh50", + "structure_ids": [ + "LES-046" + ], + "structure_labels": [ + "registry invalidity structure for bh50" + ], + "issue_cluster_ids": [ + "IC-046" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-012" + ], + "candidate_structure_types": [ + "registry_invalidity" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:019" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh6": { + "bo_id": "bh6", + "structure_ids": [ + "LES-047" + ], + "structure_labels": [ + "money claim structure for bh6" + ], + "issue_cluster_ids": [ + "IC-047" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-026" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:009" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh7": { + "bo_id": "bh7", + "structure_ids": [ + "LES-048" + ], + "structure_labels": [ + "money claim structure for bh7" + ], + "issue_cluster_ids": [ + "IC-048" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-020" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:004" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh8": { + "bo_id": "bh8", + "structure_ids": [ + "LES-049" + ], + "structure_labels": [ + "money claim structure for bh8" + ], + "issue_cluster_ids": [ + "IC-049" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-021" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:002" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh9": { + "bo_id": "bh9", + "structure_ids": [ + "LES-050" + ], + "structure_labels": [ + "money claim structure for bh9" + ], + "issue_cluster_ids": [ + "IC-050" + ], + "asset_cluster_ids": [], + "evidence_indexes": [], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:010" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + } + }, + "by_issue_cluster_id": { + "IC-001": { + "issue_cluster_id": "IC-001", + "issue_type": "B1:011", + "issue_label": "hard:B1:011", + "source_bo_ids": [ + "bh1" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:011" + ], + "evidence_indexes": [ + "E-007" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-001" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-002": { + "issue_cluster_id": "IC-002", + "issue_type": "B1:013", + "issue_label": "hard:B1:013", + "source_bo_ids": [ + "bh10" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:013" + ], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-002" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-003": { + "issue_cluster_id": "IC-003", + "issue_type": "B1:003", + "issue_label": "hard:B1:003", + "source_bo_ids": [ + "bh11" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:003" + ], + "evidence_indexes": [ + "E-027" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-003" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-004": { + "issue_cluster_id": "IC-004", + "issue_type": "B1:005", + "issue_label": "hard:B1:005", + "source_bo_ids": [ + "bh12" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:005" + ], + "evidence_indexes": [ + "E-028" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-004" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-005": { + "issue_cluster_id": "IC-005", + "issue_type": "B1:012", + "issue_label": "hard:B1:012", + "source_bo_ids": [ + "bh13" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:012" + ], + "evidence_indexes": [ + "E-011" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-005" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-006": { + "issue_cluster_id": "IC-006", + "issue_type": "B2:001", + "issue_label": "hard:B2:001", + "source_bo_ids": [ + "bh14" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:001" + ], + "evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "structure_ids": [ + "LES-006" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-007": { + "issue_cluster_id": "IC-007", + "issue_type": "B2:004", + "issue_label": "hard:B2:004", + "source_bo_ids": [ + "bh15" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:004" + ], + "evidence_indexes": [ + "E-010", + "E-014" + ], + "asset_cluster_ids": [ + "AC-002" + ], + "structure_ids": [ + "LES-007" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-008": { + "issue_cluster_id": "IC-008", + "issue_type": "B2:006", + "issue_label": "hard:B2:006", + "source_bo_ids": [ + "bh16" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:006" + ], + "evidence_indexes": [ + "E-015" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "structure_ids": [ + "LES-008" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-009": { + "issue_cluster_id": "IC-009", + "issue_type": "B2:003", + "issue_label": "hard:B2:003", + "source_bo_ids": [ + "bh17" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:003" + ], + "evidence_indexes": [ + "E-012", + "E-013" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "structure_ids": [ + "LES-009" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-010": { + "issue_cluster_id": "IC-010", + "issue_type": "B2:002", + "issue_label": "hard:B2:002", + "source_bo_ids": [ + "bh18" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:002" + ], + "evidence_indexes": [], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-010" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-011": { + "issue_cluster_id": "IC-011", + "issue_type": "B2:005", + "issue_label": "hard:B2:005", + "source_bo_ids": [ + "bh19" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:005" + ], + "evidence_indexes": [ + "E-007", + "E-011" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "structure_ids": [ + "LES-011" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-012": { + "issue_cluster_id": "IC-012", + "issue_type": "B1:001", + "issue_label": "hard:B1:001", + "source_bo_ids": [ + "bh2" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:001" + ], + "evidence_indexes": [ + "E-021", + "E-027" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-012" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-013": { + "issue_cluster_id": "IC-013", + "issue_type": "B2:007", + "issue_label": "hard:B2:007", + "source_bo_ids": [ + "bh20" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:007" + ], + "evidence_indexes": [ + "E-029" + ], + "asset_cluster_ids": [ + "AC-004" + ], + "structure_ids": [ + "LES-013" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-014": { + "issue_cluster_id": "IC-014", + "issue_type": "B3:002", + "issue_label": "hard:B3:002", + "source_bo_ids": [ + "bh21" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:002" + ], + "evidence_indexes": [ + "E-005" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006" + ], + "structure_ids": [ + "LES-014" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-015": { + "issue_cluster_id": "IC-015", + "issue_type": "B3:004", + "issue_label": "hard:B3:004", + "source_bo_ids": [ + "bh22" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:004" + ], + "evidence_indexes": [ + "E-006" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-007", + "AC-008" + ], + "structure_ids": [ + "LES-015" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-016": { + "issue_cluster_id": "IC-016", + "issue_type": "B3:001", + "issue_label": "hard:B3:001", + "source_bo_ids": [ + "bh23" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:001" + ], + "evidence_indexes": [ + "E-004" + ], + "asset_cluster_ids": [ + "AC-009", + "AC-010" + ], + "structure_ids": [ + "LES-016" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-017": { + "issue_cluster_id": "IC-017", + "issue_type": "B3:005", + "issue_label": "hard:B3:005", + "source_bo_ids": [ + "bh24" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:005" + ], + "evidence_indexes": [ + "E-011" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-012" + ], + "structure_ids": [ + "LES-017" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-018": { + "issue_cluster_id": "IC-018", + "issue_type": "B3:003", + "issue_label": "hard:B3:003", + "source_bo_ids": [ + "bh25" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:003" + ], + "evidence_indexes": [ + "E-023" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-013" + ], + "structure_ids": [ + "LES-018" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-019": { + "issue_cluster_id": "IC-019", + "issue_type": "B3:006", + "issue_label": "hard:B3:006", + "source_bo_ids": [ + "bh26" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:006" + ], + "evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "structure_ids": [ + "LES-019" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-020": { + "issue_cluster_id": "IC-020", + "issue_type": "B4:002", + "issue_label": "hard:B4:002", + "source_bo_ids": [ + "bh27" + ], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:002" + ], + "evidence_indexes": [ + "E-014" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "structure_ids": [ + "LES-020" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-021": { + "issue_cluster_id": "IC-021", + "issue_type": "B4:001", + "issue_label": "hard:B4:001", + "source_bo_ids": [ + "bh28" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:001" + ], + "evidence_indexes": [ + "E-024" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-021" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-022": { + "issue_cluster_id": "IC-022", + "issue_type": "B4:004", + "issue_label": "hard:B4:004", + "source_bo_ids": [ + "bh29" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:004" + ], + "evidence_indexes": [ + "E-014" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-002" + ], + "structure_ids": [ + "LES-022" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-023": { + "issue_cluster_id": "IC-023", + "issue_type": "B1:006", + "issue_label": "hard:B1:006", + "source_bo_ids": [ + "bh3" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:006" + ], + "evidence_indexes": [ + "E-024" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-023" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-024": { + "issue_cluster_id": "IC-024", + "issue_type": "B4:003", + "issue_label": "hard:B4:003", + "source_bo_ids": [ + "bh30" + ], + "candidate_structure_types": [ + "valuation" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:003" + ], + "evidence_indexes": [ + "E-004" + ], + "asset_cluster_ids": [ + "AC-020", + "AC-021" + ], + "structure_ids": [ + "LES-024" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-025": { + "issue_cluster_id": "IC-025", + "issue_type": "B4:005", + "issue_label": "hard:B4:005", + "source_bo_ids": [ + "bh31" + ], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:005" + ], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "structure_ids": [ + "LES-025" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-026": { + "issue_cluster_id": "IC-026", + "issue_type": "B5:002", + "issue_label": "hard:B5:002", + "source_bo_ids": [ + "bh32" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:002" + ], + "evidence_indexes": [ + "E-013" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-026" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-027": { + "issue_cluster_id": "IC-027", + "issue_type": "B5:003", + "issue_label": "hard:B5:003", + "source_bo_ids": [ + "bh33" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:003" + ], + "evidence_indexes": [ + "E-001", + "E-002" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-027" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-028": { + "issue_cluster_id": "IC-028", + "issue_type": "B5:009", + "issue_label": "hard:B5:009", + "source_bo_ids": [ + "bh34" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:009" + ], + "evidence_indexes": [ + "E-027" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-028" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-029": { + "issue_cluster_id": "IC-029", + "issue_type": "B5:004", + "issue_label": "hard:B5:004", + "source_bo_ids": [ + "bh35" + ], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:004" + ], + "evidence_indexes": [ + "E-008", + "E-010" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-029" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-030": { + "issue_cluster_id": "IC-030", + "issue_type": "B5:007", + "issue_label": "hard:B5:007", + "source_bo_ids": [ + "bh36" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:007" + ], + "evidence_indexes": [ + "E-011", + "E-019" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-030" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-031": { + "issue_cluster_id": "IC-031", + "issue_type": "B5:010", + "issue_label": "hard:B5:010", + "source_bo_ids": [ + "bh37" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:010" + ], + "evidence_indexes": [ + "E-028" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-031" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-032": { + "issue_cluster_id": "IC-032", + "issue_type": "B5:006", + "issue_label": "hard:B5:006", + "source_bo_ids": [ + "bh38" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:006" + ], + "evidence_indexes": [ + "E-010" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-032" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-033": { + "issue_cluster_id": "IC-033", + "issue_type": "B5:005", + "issue_label": "hard:B5:005", + "source_bo_ids": [ + "bh39" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:005" + ], + "evidence_indexes": [ + "E-009" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-033" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-034": { + "issue_cluster_id": "IC-034", + "issue_type": "B1:007", + "issue_label": "hard:B1:007", + "source_bo_ids": [ + "bh4" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:007" + ], + "evidence_indexes": [ + "E-024" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-034" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-035": { + "issue_cluster_id": "IC-035", + "issue_type": "B5:012", + "issue_label": "hard:B5:012", + "source_bo_ids": [ + "bh40" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:012" + ], + "evidence_indexes": [ + "E-030" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-035" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-036": { + "issue_cluster_id": "IC-036", + "issue_type": "B5:011", + "issue_label": "hard:B5:011", + "source_bo_ids": [ + "bh41" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:011" + ], + "evidence_indexes": [ + "E-029" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-036" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-037": { + "issue_cluster_id": "IC-037", + "issue_type": "B5:008", + "issue_label": "hard:B5:008", + "source_bo_ids": [ + "bh42" + ], + "candidate_structure_types": [ + "registry_invalidity" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:008" + ], + "evidence_indexes": [ + "E-012" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-037" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-038": { + "issue_cluster_id": "IC-038", + "issue_type": "B5:001", + "issue_label": "hard:B5:001", + "source_bo_ids": [ + "bh43" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:001" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "asset_cluster_ids": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "structure_ids": [ + "LES-038" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-039": { + "issue_cluster_id": "IC-039", + "issue_type": "B5:013", + "issue_label": "hard:B5:013", + "source_bo_ids": [ + "bh44" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:013" + ], + "evidence_indexes": [ + "E-011" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-039" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-040": { + "issue_cluster_id": "IC-040", + "issue_type": "B5:014", + "issue_label": "hard:B5:014", + "source_bo_ids": [ + "bh45" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:014" + ], + "evidence_indexes": [ + "E-006" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-040" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-041": { + "issue_cluster_id": "IC-041", + "issue_type": "B5:015", + "issue_label": "hard:B5:015", + "source_bo_ids": [ + "bh46" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:015" + ], + "evidence_indexes": [ + "E-018", + "E-022" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-041" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-042": { + "issue_cluster_id": "IC-042", + "issue_type": "B5:017", + "issue_label": "hard:B5:017", + "source_bo_ids": [ + "bh47" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:017" + ], + "evidence_indexes": [ + "E-010" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-042" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-043": { + "issue_cluster_id": "IC-043", + "issue_type": "B5:018", + "issue_label": "hard:B5:018", + "source_bo_ids": [ + "bh48" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:018" + ], + "evidence_indexes": [ + "E-011" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-043" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-044": { + "issue_cluster_id": "IC-044", + "issue_type": "B5:016", + "issue_label": "hard:B5:016", + "source_bo_ids": [ + "bh49" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:016" + ], + "evidence_indexes": [ + "E-009" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-044" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-045": { + "issue_cluster_id": "IC-045", + "issue_type": "B1:008", + "issue_label": "hard:B1:008", + "source_bo_ids": [ + "bh5" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:008" + ], + "evidence_indexes": [ + "E-025" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-045" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-046": { + "issue_cluster_id": "IC-046", + "issue_type": "B5:019", + "issue_label": "hard:B5:019", + "source_bo_ids": [ + "bh50" + ], + "candidate_structure_types": [ + "registry_invalidity" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:019" + ], + "evidence_indexes": [ + "E-012" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-046" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-047": { + "issue_cluster_id": "IC-047", + "issue_type": "B1:009", + "issue_label": "hard:B1:009", + "source_bo_ids": [ + "bh6" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:009" + ], + "evidence_indexes": [ + "E-026" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-047" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-048": { + "issue_cluster_id": "IC-048", + "issue_type": "B1:004", + "issue_label": "hard:B1:004", + "source_bo_ids": [ + "bh7" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:004" + ], + "evidence_indexes": [ + "E-020" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-048" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-049": { + "issue_cluster_id": "IC-049", + "issue_type": "B1:002", + "issue_label": "hard:B1:002", + "source_bo_ids": [ + "bh8" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:002" + ], + "evidence_indexes": [ + "E-021" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-049" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-050": { + "issue_cluster_id": "IC-050", + "issue_type": "B1:010", + "issue_label": "hard:B1:010", + "source_bo_ids": [ + "bh9" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:010" + ], + "evidence_indexes": [], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-050" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + } + }, + "by_asset_cluster_id": { + "AC-001": { + "asset_cluster_id": "AC-001", + "canonical_asset_label": "성수동 대지", + "asset_kind": "real_property", + "member_asset_mentions": [ + "성수동 대지" + ], + "source_bo_ids": [ + "bh14", + "bh17" + ], + "evidence_indexes": [ + "E-013", + "E-025", + "E-026", + "E-012" + ], + "issue_cluster_ids": [ + "IC-006", + "IC-009" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "structure_ids": [ + "LES-006", + "LES-009" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": true + }, + "AC-002": { + "asset_cluster_id": "AC-002", + "canonical_asset_label": "신림동 상가", + "asset_kind": "real_property", + "member_asset_mentions": [ + "신림동 상가", + "2021-08-01", + "2022-04-18", + "2024-10-17", + "포천시 임야" + ], + "source_bo_ids": [ + "bh15", + "bh27", + "bh29", + "bh31" + ], + "evidence_indexes": [ + "E-010", + "E-014", + "E-003" + ], + "issue_cluster_ids": [ + "IC-007", + "IC-020", + "IC-022", + "IC-025" + ], + "candidate_structure_types": [ + "secured_debt", + "fraudulent_transfer" + ], + "structure_ids": [ + "LES-007", + "LES-020", + "LES-022", + "LES-025" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "actio_case_signals.target_property_candidates" + ], + "review_required": true + }, + "AC-003": { + "asset_cluster_id": "AC-003", + "canonical_asset_label": "평택시 빌라", + "asset_kind": "unknown", + "member_asset_mentions": [ + "평택시 빌라" + ], + "source_bo_ids": [ + "bh16", + "bh19" + ], + "evidence_indexes": [ + "E-015", + "E-007", + "E-011" + ], + "issue_cluster_ids": [ + "IC-008", + "IC-011" + ], + "candidate_structure_types": [ + "secured_debt", + "money_claim" + ], + "structure_ids": [ + "LES-008", + "LES-011" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": true + }, + "AC-004": { + "asset_cluster_id": "AC-004", + "canonical_asset_label": "흑석동 상가", + "asset_kind": "real_property", + "member_asset_mentions": [ + "흑석동 상가" + ], + "source_bo_ids": [ + "bh20" + ], + "evidence_indexes": [ + "E-029" + ], + "issue_cluster_ids": [ + "IC-013" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "structure_ids": [ + "LES-013" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": true + }, + "AC-005": { + "asset_cluster_id": "AC-005", + "canonical_asset_label": "SCH-016-01", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-01", + "성수동 256 대", + "SCH-016-02", + "성수동 256 대지 및 지상 건물" + ], + "source_bo_ids": [ + "bh21", + "bh22" + ], + "evidence_indexes": [ + "E-005", + "E-006" + ], + "issue_cluster_ids": [ + "IC-014", + "IC-015" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "structure_ids": [ + "LES-014", + "LES-015" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-006": { + "asset_cluster_id": "AC-006", + "canonical_asset_label": "SCH-016-01", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-01", + "성수동 256 대" + ], + "source_bo_ids": [ + "bh21" + ], + "evidence_indexes": [ + "E-005" + ], + "issue_cluster_ids": [ + "IC-014" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "structure_ids": [ + "LES-014" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-007": { + "asset_cluster_id": "AC-007", + "canonical_asset_label": "SCH-016-01", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-01", + "SCH-016-02", + "성수동 256 대지 및 지상 건물" + ], + "source_bo_ids": [ + "bh22" + ], + "evidence_indexes": [ + "E-006" + ], + "issue_cluster_ids": [ + "IC-015" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "structure_ids": [ + "LES-015" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-008": { + "asset_cluster_id": "AC-008", + "canonical_asset_label": "SCH-016-01", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-01", + "SCH-016-02", + "성수동 256 대지 및 지상 건물" + ], + "source_bo_ids": [ + "bh22" + ], + "evidence_indexes": [ + "E-006" + ], + "issue_cluster_ids": [ + "IC-015" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "structure_ids": [ + "LES-015" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-009": { + "asset_cluster_id": "AC-009", + "canonical_asset_label": "SCH-016-03", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-03", + "신림로 상가, 포천시 임야" + ], + "source_bo_ids": [ + "bh23" + ], + "evidence_indexes": [ + "E-004" + ], + "issue_cluster_ids": [ + "IC-016" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "structure_ids": [ + "LES-016" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-010": { + "asset_cluster_id": "AC-010", + "canonical_asset_label": "SCH-016-03", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-03", + "신림로 상가, 포천시 임야" + ], + "source_bo_ids": [ + "bh23" + ], + "evidence_indexes": [ + "E-004" + ], + "issue_cluster_ids": [ + "IC-016" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "structure_ids": [ + "LES-016" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-011": { + "asset_cluster_id": "AC-011", + "canonical_asset_label": "SCH-016-04", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-04", + "평택시 서정3길 123 서정빌라 101호", + "서정빌라 101호" + ], + "source_bo_ids": [ + "bh24", + "bh25" + ], + "evidence_indexes": [ + "E-011", + "E-023" + ], + "issue_cluster_ids": [ + "IC-017", + "IC-018" + ], + "candidate_structure_types": [ + "secured_debt", + "land_use_gain" + ], + "structure_ids": [ + "LES-017", + "LES-018" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-012": { + "asset_cluster_id": "AC-012", + "canonical_asset_label": "SCH-016-04", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-04", + "평택시 서정3길 123 서정빌라 101호" + ], + "source_bo_ids": [ + "bh24" + ], + "evidence_indexes": [ + "E-011" + ], + "issue_cluster_ids": [ + "IC-017" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "structure_ids": [ + "LES-017" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-013": { + "asset_cluster_id": "AC-013", + "canonical_asset_label": "SCH-016-04", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-04", + "서정빌라 101호" + ], + "source_bo_ids": [ + "bh25" + ], + "evidence_indexes": [ + "E-023" + ], + "issue_cluster_ids": [ + "IC-018" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "structure_ids": [ + "LES-018" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-014": { + "asset_cluster_id": "AC-014", + "canonical_asset_label": "SCH-016-05", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-05", + "흑석동 201호" + ], + "source_bo_ids": [ + "bh26" + ], + "evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "issue_cluster_ids": [ + "IC-019" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "structure_ids": [ + "LES-019" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-015": { + "asset_cluster_id": "AC-015", + "canonical_asset_label": "SCH-016-05", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-05", + "흑석동 201호" + ], + "source_bo_ids": [ + "bh26" + ], + "evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "issue_cluster_ids": [ + "IC-019" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "structure_ids": [ + "LES-019" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-016": { + "asset_cluster_id": "AC-016", + "canonical_asset_label": "신림동 상가", + "asset_kind": "unknown", + "member_asset_mentions": [ + "신림동 상가", + "2021-08-01", + "2022-04-18", + "2024-10-17", + "포천시 임야" + ], + "source_bo_ids": [ + "bh27", + "bh29", + "bh31" + ], + "evidence_indexes": [ + "E-014", + "E-003", + "E-010" + ], + "issue_cluster_ids": [ + "IC-020", + "IC-022", + "IC-025" + ], + "candidate_structure_types": [ + "fraudulent_transfer", + "secured_debt" + ], + "structure_ids": [ + "LES-020", + "LES-022", + "LES-025" + ], + "basis_fields": [ + "actio_case_signals.target_property_candidates", + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": true + }, + "AC-017": { + "asset_cluster_id": "AC-017", + "canonical_asset_label": "신림동 상가", + "asset_kind": "unknown", + "member_asset_mentions": [ + "신림동 상가", + "2021-08-01", + "2022-04-18", + "2024-10-17" + ], + "source_bo_ids": [ + "bh29" + ], + "evidence_indexes": [ + "E-014" + ], + "issue_cluster_ids": [ + "IC-022" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "structure_ids": [ + "LES-022" + ], + "basis_fields": [ + "actio_case_signals.target_property_candidates", + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": true + }, + "AC-018": { + "asset_cluster_id": "AC-018", + "canonical_asset_label": "신림동 상가", + "asset_kind": "unknown", + "member_asset_mentions": [ + "신림동 상가", + "2021-08-01", + "2022-04-18", + "2024-10-17" + ], + "source_bo_ids": [ + "bh29" + ], + "evidence_indexes": [ + "E-014" + ], + "issue_cluster_ids": [ + "IC-022" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "structure_ids": [ + "LES-022" + ], + "basis_fields": [ + "actio_case_signals.target_property_candidates", + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": true + }, + "AC-019": { + "asset_cluster_id": "AC-019", + "canonical_asset_label": "신림동 상가", + "asset_kind": "unknown", + "member_asset_mentions": [ + "신림동 상가", + "2021-08-01", + "2022-04-18", + "2024-10-17" + ], + "source_bo_ids": [ + "bh29" + ], + "evidence_indexes": [ + "E-014" + ], + "issue_cluster_ids": [ + "IC-022" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "structure_ids": [ + "LES-022" + ], + "basis_fields": [ + "actio_case_signals.target_property_candidates", + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": true + }, + "AC-020": { + "asset_cluster_id": "AC-020", + "canonical_asset_label": "신림동 상가", + "asset_kind": "unknown", + "member_asset_mentions": [ + "신림동 상가", + "포천시 임야" + ], + "source_bo_ids": [ + "bh30" + ], + "evidence_indexes": [ + "E-004" + ], + "issue_cluster_ids": [ + "IC-024" + ], + "candidate_structure_types": [ + "valuation" + ], + "structure_ids": [ + "LES-024" + ], + "basis_fields": [ + "actio_case_signals.target_property_candidates", + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": true + }, + "AC-021": { + "asset_cluster_id": "AC-021", + "canonical_asset_label": "신림동 상가", + "asset_kind": "unknown", + "member_asset_mentions": [ + "신림동 상가", + "포천시 임야" + ], + "source_bo_ids": [ + "bh30" + ], + "evidence_indexes": [ + "E-004" + ], + "issue_cluster_ids": [ + "IC-024" + ], + "candidate_structure_types": [ + "valuation" + ], + "structure_ids": [ + "LES-024" + ], + "basis_fields": [ + "actio_case_signals.target_property_candidates", + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": true + }, + "AC-022": { + "asset_cluster_id": "AC-022", + "canonical_asset_label": "성수동 대지", + "asset_kind": "real_property", + "member_asset_mentions": [ + "성수동 대지", + "서울 성동구 성수동 256", + "1. 서울 성동구 성수동 256 대 330㎡", + "성수동 건물", + "서울 성동구 성수동 256 상가", + "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "신림동 상가", + "서울 관악구 신림로 115", + "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123", + "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "흑석동 상가", + "서울 동작구 흑석로 80", + "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물" + ], + "source_bo_ids": [ + "bh43" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "structure_ids": [ + "LES-038" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_alias" + ], + "review_required": true + }, + "AC-023": { + "asset_cluster_id": "AC-023", + "canonical_asset_label": "성수동 대지", + "asset_kind": "real_property", + "member_asset_mentions": [ + "성수동 대지", + "서울 성동구 성수동 256", + "1. 서울 성동구 성수동 256 대 330㎡", + "성수동 건물", + "서울 성동구 성수동 256 상가", + "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "신림동 상가", + "서울 관악구 신림로 115", + "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123", + "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "흑석동 상가", + "서울 동작구 흑석로 80", + "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물" + ], + "source_bo_ids": [ + "bh43" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "structure_ids": [ + "LES-038" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_alias" + ], + "review_required": true + }, + "AC-024": { + "asset_cluster_id": "AC-024", + "canonical_asset_label": "성수동 대지", + "asset_kind": "real_property", + "member_asset_mentions": [ + "성수동 대지", + "서울 성동구 성수동 256", + "1. 서울 성동구 성수동 256 대 330㎡", + "성수동 건물", + "서울 성동구 성수동 256 상가", + "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "신림동 상가", + "서울 관악구 신림로 115", + "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123", + "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "흑석동 상가", + "서울 동작구 흑석로 80", + "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물" + ], + "source_bo_ids": [ + "bh43" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "structure_ids": [ + "LES-038" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_alias" + ], + "review_required": true + }, + "AC-025": { + "asset_cluster_id": "AC-025", + "canonical_asset_label": "성수동 대지", + "asset_kind": "real_property", + "member_asset_mentions": [ + "성수동 대지", + "서울 성동구 성수동 256", + "1. 서울 성동구 성수동 256 대 330㎡", + "성수동 건물", + "서울 성동구 성수동 256 상가", + "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "신림동 상가", + "서울 관악구 신림로 115", + "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123", + "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "흑석동 상가", + "서울 동작구 흑석로 80", + "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물" + ], + "source_bo_ids": [ + "bh43" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "structure_ids": [ + "LES-038" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_alias" + ], + "review_required": true + }, + "AC-026": { + "asset_cluster_id": "AC-026", + "canonical_asset_label": "성수동 대지", + "asset_kind": "real_property", + "member_asset_mentions": [ + "성수동 대지", + "서울 성동구 성수동 256", + "1. 서울 성동구 성수동 256 대 330㎡", + "성수동 건물", + "서울 성동구 성수동 256 상가", + "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "신림동 상가", + "서울 관악구 신림로 115", + "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123", + "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "흑석동 상가", + "서울 동작구 흑석로 80", + "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물" + ], + "source_bo_ids": [ + "bh43" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "structure_ids": [ + "LES-038" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_alias" + ], + "review_required": true + } + }, + "by_evidence_index": { + "E-007": { + "evidence_index": "E-007", + "source_title": "공사도급계약서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh1", + "bh19" + ], + "structure_ids": [ + "LES-001", + "LES-011" + ], + "issue_cluster_ids": [ + "IC-001", + "IC-011" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "candidate_structure_types": [ + "money_claim" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B1:011", + "B2:005" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-003": { + "evidence_index": "E-003", + "source_title": "각서 및 금전소비대차계약서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh10", + "bh31" + ], + "structure_ids": [ + "LES-002", + "LES-025" + ], + "issue_cluster_ids": [ + "IC-002", + "IC-025" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "candidate_structure_types": [ + "money_claim", + "fraudulent_transfer" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B1:013", + "actio-signal-005", + "B4:005" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-010": { + "evidence_index": "E-010", + "source_title": "내용증명에 대한 답신", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh10", + "bh15", + "bh31", + "bh35", + "bh38", + "bh47" + ], + "structure_ids": [ + "LES-002", + "LES-007", + "LES-025", + "LES-029", + "LES-032", + "LES-042" + ], + "issue_cluster_ids": [ + "IC-002", + "IC-007", + "IC-025", + "IC-029", + "IC-032", + "IC-042" + ], + "asset_cluster_ids": [ + "AC-002", + "AC-016" + ], + "candidate_structure_types": [ + "money_claim", + "secured_debt", + "fraudulent_transfer", + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B1:013", + "B2:004", + "actio-signal-005", + "B4:005", + "B5:004", + "B5:006", + "B5:017" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-027": { + "evidence_index": "E-027", + "source_title": "최고 통지서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh11", + "bh2", + "bh34" + ], + "structure_ids": [ + "LES-003", + "LES-012", + "LES-028" + ], + "issue_cluster_ids": [ + "IC-003", + "IC-012", + "IC-028" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "money_claim", + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B1:003", + "B1:001", + "B5:009" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-028": { + "evidence_index": "E-028", + "source_title": "통지서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh12", + "bh37" + ], + "structure_ids": [ + "LES-004", + "LES-031" + ], + "issue_cluster_ids": [ + "IC-004", + "IC-031" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "money_claim", + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B1:005", + "B5:010" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-011": { + "evidence_index": "E-011", + "source_title": "답변서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh13", + "bh19", + "bh24", + "bh36", + "bh44", + "bh48" + ], + "structure_ids": [ + "LES-005", + "LES-011", + "LES-017", + "LES-030", + "LES-039", + "LES-043" + ], + "issue_cluster_ids": [ + "IC-005", + "IC-011", + "IC-017", + "IC-030", + "IC-039", + "IC-043" + ], + "asset_cluster_ids": [ + "AC-003", + "AC-011", + "AC-012" + ], + "candidate_structure_types": [ + "money_claim", + "secured_debt", + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B1:012", + "B2:005", + "B3:005", + "B5:007", + "B5:013", + "B5:018" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-013": { + "evidence_index": "E-013", + "source_title": "등기사항전부증명서(말소사항 포함)-토지", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh14", + "bh17", + "bh32", + "bh43" + ], + "structure_ids": [ + "LES-006", + "LES-009", + "LES-026", + "LES-038" + ], + "issue_cluster_ids": [ + "IC-006", + "IC-009", + "IC-026", + "IC-038" + ], + "asset_cluster_ids": [ + "AC-001", + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "candidate_structure_types": [ + "secured_debt", + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B2:001", + "B2:003", + "B5:002", + "B5:001" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-025": { + "evidence_index": "E-025", + "source_title": "차용증", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh14", + "bh5" + ], + "structure_ids": [ + "LES-006", + "LES-045" + ], + "issue_cluster_ids": [ + "IC-006", + "IC-045" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "candidate_structure_types": [ + "secured_debt", + "money_claim" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B2:001", + "B1:008" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-026": { + "evidence_index": "E-026", + "source_title": "차용증", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh14", + "bh6" + ], + "structure_ids": [ + "LES-006", + "LES-047" + ], + "issue_cluster_ids": [ + "IC-006", + "IC-047" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "candidate_structure_types": [ + "secured_debt", + "money_claim" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B2:001", + "B1:009" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-014": { + "evidence_index": "E-014", + "source_title": "등기사항전부증명서 (말소사항 포함) -건물", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh15", + "bh27", + "bh29", + "bh43" + ], + "structure_ids": [ + "LES-007", + "LES-020", + "LES-022", + "LES-038" + ], + "issue_cluster_ids": [ + "IC-007", + "IC-020", + "IC-022", + "IC-038" + ], + "asset_cluster_ids": [ + "AC-002", + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "candidate_structure_types": [ + "secured_debt", + "fraudulent_transfer", + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B2:004", + "actio-signal-002", + "B4:002", + "actio-signal-004", + "B4:004", + "B5:001" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-015": { + "evidence_index": "E-015", + "source_title": "등기사항전부증명서(말소사항 포함)-집합건물", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh16", + "bh43" + ], + "structure_ids": [ + "LES-008", + "LES-038" + ], + "issue_cluster_ids": [ + "IC-008", + "IC-038" + ], + "asset_cluster_ids": [ + "AC-003", + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "candidate_structure_types": [ + "secured_debt", + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B2:006", + "B5:001" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-012": { + "evidence_index": "E-012", + "source_title": "등기말소 신청에 대한 답신", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh17", + "bh42", + "bh50" + ], + "structure_ids": [ + "LES-009", + "LES-037", + "LES-046" + ], + "issue_cluster_ids": [ + "IC-009", + "IC-037", + "IC-046" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "candidate_structure_types": [ + "secured_debt", + "registry_invalidity" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B2:003", + "B5:008", + "B5:019" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-021": { + "evidence_index": "E-021", + "source_title": "이행각서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh2", + "bh8" + ], + "structure_ids": [ + "LES-012", + "LES-049" + ], + "issue_cluster_ids": [ + "IC-012", + "IC-049" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "money_claim" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B1:001", + "B1:002" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-029": { + "evidence_index": "E-029", + "source_title": "통지서_이수인_윤건우", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh20", + "bh41" + ], + "structure_ids": [ + "LES-013", + "LES-036" + ], + "issue_cluster_ids": [ + "IC-013", + "IC-036" + ], + "asset_cluster_ids": [ + "AC-004" + ], + "candidate_structure_types": [ + "secured_debt", + "money_claim" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B2:007", + "B5:011" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-005": { + "evidence_index": "E-005", + "source_title": "감정평가 의견서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh21" + ], + "structure_ids": [ + "LES-014" + ], + "issue_cluster_ids": [ + "IC-014" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B3:002" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-006": { + "evidence_index": "E-006", + "source_title": "계약서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh22", + "bh45" + ], + "structure_ids": [ + "LES-015", + "LES-040" + ], + "issue_cluster_ids": [ + "IC-015", + "IC-040" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-007", + "AC-008" + ], + "candidate_structure_types": [ + "land_use_gain", + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B3:004", + "B5:014" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-004": { + "evidence_index": "E-004", + "source_title": "감정평가서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh23", + "bh30" + ], + "structure_ids": [ + "LES-016", + "LES-024" + ], + "issue_cluster_ids": [ + "IC-016", + "IC-024" + ], + "asset_cluster_ids": [ + "AC-009", + "AC-010", + "AC-020", + "AC-021" + ], + "candidate_structure_types": [ + "land_use_gain", + "valuation" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B3:001", + "actio-signal-003", + "B4:003" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-023": { + "evidence_index": "E-023", + "source_title": "임료시세확인서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh25" + ], + "structure_ids": [ + "LES-018" + ], + "issue_cluster_ids": [ + "IC-018" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-013" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B3:003" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-017": { + "evidence_index": "E-017", + "source_title": "부동산 매매계약서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh26" + ], + "structure_ids": [ + "LES-019" + ], + "issue_cluster_ids": [ + "IC-019" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B3:006" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-018": { + "evidence_index": "E-018", + "source_title": "사업자등록증", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh26", + "bh46" + ], + "structure_ids": [ + "LES-019", + "LES-041" + ], + "issue_cluster_ids": [ + "IC-019", + "IC-041" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "candidate_structure_types": [ + "land_use_gain", + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B3:006", + "B5:015" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-022": { + "evidence_index": "E-022", + "source_title": "임대차계약서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh26", + "bh46" + ], + "structure_ids": [ + "LES-019", + "LES-041" + ], + "issue_cluster_ids": [ + "IC-019", + "IC-041" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "candidate_structure_types": [ + "land_use_gain", + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B3:006", + "B5:015" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-024": { + "evidence_index": "E-024", + "source_title": "차용증", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh28", + "bh3", + "bh4" + ], + "structure_ids": [ + "LES-021", + "LES-023", + "LES-034" + ], + "issue_cluster_ids": [ + "IC-021", + "IC-023", + "IC-034" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "money_claim" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "actio-signal-001", + "B4:001", + "B1:006", + "B1:007" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-001": { + "evidence_index": "E-001", + "source_title": "가정법원심판_강호연", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh33" + ], + "structure_ids": [ + "LES-027" + ], + "issue_cluster_ids": [ + "IC-027" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B5:003" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-002": { + "evidence_index": "E-002", + "source_title": "가족관계증명서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh33" + ], + "structure_ids": [ + "LES-027" + ], + "issue_cluster_ids": [ + "IC-027" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B5:003" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-008": { + "evidence_index": "E-008", + "source_title": "내용증명", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh35" + ], + "structure_ids": [ + "LES-029" + ], + "issue_cluster_ids": [ + "IC-029" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B5:004" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-019": { + "evidence_index": "E-019", + "source_title": "소멸청구 통지서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh36" + ], + "structure_ids": [ + "LES-030" + ], + "issue_cluster_ids": [ + "IC-030" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "secured_debt" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B5:007" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-009": { + "evidence_index": "E-009", + "source_title": "내용증명", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh39", + "bh49" + ], + "structure_ids": [ + "LES-033", + "LES-044" + ], + "issue_cluster_ids": [ + "IC-033", + "IC-044" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B5:005", + "B5:016" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-030": { + "evidence_index": "E-030", + "source_title": "해제 통지서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh40" + ], + "structure_ids": [ + "LES-035" + ], + "issue_cluster_ids": [ + "IC-035" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B5:012" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-016": { + "evidence_index": "E-016", + "source_title": "별지 목록", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh43" + ], + "structure_ids": [ + "LES-038" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "asset_cluster_ids": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B5:001" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-020": { + "evidence_index": "E-020", + "source_title": "영업양도 계약서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh7" + ], + "structure_ids": [ + "LES-048" + ], + "issue_cluster_ids": [ + "IC-048" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "money_claim" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B1:004" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + } + }, + "by_candidate_structure_type": { + "money_claim": { + "candidate_structure_type": "money_claim", + "structure_ids": [ + "LES-001", + "LES-002", + "LES-003", + "LES-004", + "LES-005", + "LES-011", + "LES-012", + "LES-021", + "LES-023", + "LES-034", + "LES-036", + "LES-042", + "LES-045", + "LES-047", + "LES-048", + "LES-049", + "LES-050" + ], + "source_bo_ids": [ + "bh1", + "bh10", + "bh11", + "bh12", + "bh13", + "bh19", + "bh2", + "bh28", + "bh3", + "bh4", + "bh41", + "bh47", + "bh5", + "bh6", + "bh7", + "bh8", + "bh9" + ], + "issue_cluster_ids": [ + "IC-001", + "IC-002", + "IC-003", + "IC-004", + "IC-005", + "IC-011", + "IC-012", + "IC-021", + "IC-023", + "IC-034", + "IC-036", + "IC-042", + "IC-045", + "IC-047", + "IC-048", + "IC-049", + "IC-050" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_indexes": [ + "E-007", + "E-003", + "E-010", + "E-027", + "E-028", + "E-011", + "E-021", + "E-024", + "E-029", + "E-025", + "E-026", + "E-020" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:011", + "B1:013", + "B1:003", + "B1:005", + "B1:012", + "B2:005", + "B1:001", + "B4:001", + "B1:006", + "B1:007", + "B5:011", + "B5:017", + "B1:008", + "B1:009", + "B1:004", + "B1:002", + "B1:010" + ], + "related_actio_signal_ids": [ + "actio-signal-001" + ], + "downstream_fact_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures", + "legal_calculation_object", + "actio_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "secured_debt": { + "candidate_structure_type": "secured_debt", + "structure_ids": [ + "LES-006", + "LES-007", + "LES-008", + "LES-009", + "LES-010", + "LES-013", + "LES-017", + "LES-022", + "LES-030", + "LES-039" + ], + "source_bo_ids": [ + "bh14", + "bh15", + "bh16", + "bh17", + "bh18", + "bh20", + "bh24", + "bh29", + "bh36", + "bh44" + ], + "issue_cluster_ids": [ + "IC-006", + "IC-007", + "IC-008", + "IC-009", + "IC-010", + "IC-013", + "IC-017", + "IC-022", + "IC-030", + "IC-039" + ], + "asset_cluster_ids": [ + "AC-001", + "AC-002", + "AC-003", + "AC-004", + "AC-011", + "AC-012", + "AC-016", + "AC-017", + "AC-018", + "AC-019" + ], + "evidence_indexes": [ + "E-013", + "E-025", + "E-026", + "E-010", + "E-014", + "E-015", + "E-012", + "E-029", + "E-011", + "E-019" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:001", + "B2:004", + "B2:006", + "B2:003", + "B2:002", + "B2:007", + "B3:005", + "B4:004", + "B5:007", + "B5:013" + ], + "related_actio_signal_ids": [ + "actio-signal-004" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles", + "legal_calculation_object", + "linked_actio_structures", + "actio_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "land_use_gain": { + "candidate_structure_type": "land_use_gain", + "structure_ids": [ + "LES-014", + "LES-015", + "LES-016", + "LES-018", + "LES-019" + ], + "source_bo_ids": [ + "bh21", + "bh22", + "bh23", + "bh25", + "bh26" + ], + "issue_cluster_ids": [ + "IC-014", + "IC-015", + "IC-016", + "IC-018", + "IC-019" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006", + "AC-007", + "AC-008", + "AC-009", + "AC-010", + "AC-011", + "AC-013", + "AC-014", + "AC-015" + ], + "evidence_indexes": [ + "E-005", + "E-006", + "E-004", + "E-023", + "E-017", + "E-018", + "E-022" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:002", + "B3:004", + "B3:001", + "B3:003", + "B3:006" + ], + "related_actio_signal_ids": [], + "downstream_fact_roles": [ + "legal_calculation_object", + "legal_effect_roles", + "linked_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "fraudulent_transfer": { + "candidate_structure_type": "fraudulent_transfer", + "structure_ids": [ + "LES-020", + "LES-025", + "LES-029" + ], + "source_bo_ids": [ + "bh27", + "bh31", + "bh35" + ], + "issue_cluster_ids": [ + "IC-020", + "IC-025", + "IC-029" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_indexes": [ + "E-014", + "E-003", + "E-010", + "E-008" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:002", + "B4:005", + "B5:004" + ], + "related_actio_signal_ids": [ + "actio-signal-002", + "actio-signal-005" + ], + "downstream_fact_roles": [ + "actio_roles", + "legal_effect_roles", + "linked_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "valuation": { + "candidate_structure_type": "valuation", + "structure_ids": [ + "LES-024" + ], + "source_bo_ids": [ + "bh30" + ], + "issue_cluster_ids": [ + "IC-024" + ], + "asset_cluster_ids": [ + "AC-020", + "AC-021" + ], + "evidence_indexes": [ + "E-004" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:003" + ], + "related_actio_signal_ids": [ + "actio-signal-003" + ], + "downstream_fact_roles": [ + "legal_effect_roles", + "actio_roles", + "legal_calculation_object" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "succession_notice_lien_asset_defense": { + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_ids": [ + "LES-026", + "LES-027", + "LES-028", + "LES-031", + "LES-032", + "LES-033", + "LES-035", + "LES-038", + "LES-040", + "LES-041", + "LES-043", + "LES-044" + ], + "source_bo_ids": [ + "bh32", + "bh33", + "bh34", + "bh37", + "bh38", + "bh39", + "bh40", + "bh43", + "bh45", + "bh46", + "bh48", + "bh49" + ], + "issue_cluster_ids": [ + "IC-026", + "IC-027", + "IC-028", + "IC-031", + "IC-032", + "IC-033", + "IC-035", + "IC-038", + "IC-040", + "IC-041", + "IC-043", + "IC-044" + ], + "asset_cluster_ids": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "evidence_indexes": [ + "E-013", + "E-001", + "E-002", + "E-027", + "E-028", + "E-010", + "E-009", + "E-030", + "E-014", + "E-015", + "E-016", + "E-006", + "E-018", + "E-022", + "E-011" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:002", + "B5:003", + "B5:009", + "B5:010", + "B5:006", + "B5:005", + "B5:012", + "B5:001", + "B5:014", + "B5:015", + "B5:018", + "B5:016" + ], + "related_actio_signal_ids": [], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "registry_invalidity": { + "candidate_structure_type": "registry_invalidity", + "structure_ids": [ + "LES-037", + "LES-046" + ], + "source_bo_ids": [ + "bh42", + "bh50" + ], + "issue_cluster_ids": [ + "IC-037", + "IC-046" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-012" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:008", + "B5:019" + ], + "related_actio_signal_ids": [], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + } + } + }, + "quality_gate": { + "bo_coverage": { + "source_bo_count": 50, + "covered_bo_count": 50, + "missing_bo_ids": [], + "coverage_pass": true + }, + "evidence_coverage": { + "source_evidence_count": 30, + "covered_evidence_count": 30, + "missing_evidence_indexes": [], + "coverage_pass": true + }, + "review_queue": [ + { + "review_type": "structure_review_required", + "structure_id": "LES-001", + "source_bo_ids": [ + "bh1" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-002", + "source_bo_ids": [ + "bh10" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-003", + "source_bo_ids": [ + "bh11" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-004", + "source_bo_ids": [ + "bh12" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-005", + "source_bo_ids": [ + "bh13" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-006", + "source_bo_ids": [ + "bh14" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-007", + "source_bo_ids": [ + "bh15" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-008", + "source_bo_ids": [ + "bh16" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-009", + "source_bo_ids": [ + "bh17" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-010", + "source_bo_ids": [ + "bh18" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-011", + "source_bo_ids": [ + "bh19" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-012", + "source_bo_ids": [ + "bh2" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-013", + "source_bo_ids": [ + "bh20" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-014", + "source_bo_ids": [ + "bh21" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-015", + "source_bo_ids": [ + "bh22" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-016", + "source_bo_ids": [ + "bh23" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-017", + "source_bo_ids": [ + "bh24" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-018", + "source_bo_ids": [ + "bh25" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-019", + "source_bo_ids": [ + "bh26" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-020", + "source_bo_ids": [ + "bh27" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-021", + "source_bo_ids": [ + "bh28" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-022", + "source_bo_ids": [ + "bh29" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-023", + "source_bo_ids": [ + "bh3" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-024", + "source_bo_ids": [ + "bh30" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-025", + "source_bo_ids": [ + "bh31" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-026", + "source_bo_ids": [ + "bh32" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-027", + "source_bo_ids": [ + "bh33" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-028", + "source_bo_ids": [ + "bh34" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-029", + "source_bo_ids": [ + "bh35" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-030", + "source_bo_ids": [ + "bh36" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-031", + "source_bo_ids": [ + "bh37" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-032", + "source_bo_ids": [ + "bh38" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-033", + "source_bo_ids": [ + "bh39" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-034", + "source_bo_ids": [ + "bh4" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-035", + "source_bo_ids": [ + "bh40" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-036", + "source_bo_ids": [ + "bh41" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-037", + "source_bo_ids": [ + "bh42" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-038", + "source_bo_ids": [ + "bh43" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-039", + "source_bo_ids": [ + "bh44" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-040", + "source_bo_ids": [ + "bh45" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-041", + "source_bo_ids": [ + "bh46" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-042", + "source_bo_ids": [ + "bh47" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-043", + "source_bo_ids": [ + "bh48" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-044", + "source_bo_ids": [ + "bh49" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-045", + "source_bo_ids": [ + "bh5" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-046", + "source_bo_ids": [ + "bh50" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-047", + "source_bo_ids": [ + "bh6" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-048", + "source_bo_ids": [ + "bh7" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-049", + "source_bo_ids": [ + "bh8" + ] + }, + { + "review_type": "structure_review_required", + "structure_id": "LES-050", + "source_bo_ids": [ + "bh9" + ] + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-001", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh21" + ], + "block_reason": "conflicting_allowed_decisions" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-003", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh24" + ], + "block_reason": "conflicting_allowed_decisions" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-004", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh25" + ], + "block_reason": "conflicting_allowed_decisions" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-005", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh26" + ], + "block_reason": "conflicting_allowed_decisions" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-029", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh3", + "bh4", + "bh5", + "bh6" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-037", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh37", + "bh38", + "bh39", + "bh40" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-040", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh43", + "bh45", + "bh46" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-043", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh48", + "bh49" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-007", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh1" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-008", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh10" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-009", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh11" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-010", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh12" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-011", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh13" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-012", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh14" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-013", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh15" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-014", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh16" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-015", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh17" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-016", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh18" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-018", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh2" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-019", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh20" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-020", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh21" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-021", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh22" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-022", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh23" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-023", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh24" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-024", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh25" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-025", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh26" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-026", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh27" + ], + "block_reason": "insufficient_compact_record" + }, + { + "review_type": "blocked_exception", + "exception_id": "LEX-027", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh28" + ], + "block_reason": "insufficient_compact_record" + } + ] + } +} diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/quality_gates/B1_evidence_indexed_gate.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/quality_gates/B1_evidence_indexed_gate.json new file mode 100644 index 00000000..f3fba7c7 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/quality_gates/B1_evidence_indexed_gate.json @@ -0,0 +1,124 @@ +{ + "gate_id": "B1_evidence_indexed", + "executed_at": "2026-07-10T12:00:00Z", + "input_parts_count": 30, + "expected_ordinals": [ + "001", + "002", + "003", + "004", + "005", + "006", + "007", + "008", + "009", + "010", + "011", + "012", + "013", + "014", + "015", + "016", + "017", + "018", + "019", + "020", + "021", + "022", + "023", + "024", + "025", + "026", + "027", + "028", + "029", + "030" + ], + "planner_source": "evidence_shard_plan.json", + "planner_shard_count": 30, + "received_ordinals": [ + "001", + "002", + "003", + "004", + "005", + "006", + "007", + "008", + "009", + "010", + "011", + "012", + "013", + "014", + "015", + "016", + "017", + "018", + "019", + "020", + "021", + "022", + "023", + "024", + "025", + "026", + "027", + "028", + "029", + "030" + ], + "checks": [ + { + "check_id": "shard_completeness", + "severity": "PASS", + "detail": "모든 증거 파트가 정상적으로 수집되었습니다.", + "missing_ordinals": [] + }, + { + "check_id": "evidence_index_determinism", + "severity": "PASS", + "detail": "모든 evidence_index가 E-### 형식으로 정확히 지정되었습니다.", + "violations": [] + }, + { + "check_id": "duplicate_document_alias", + "severity": "PASS", + "detail": "중복된 문서 alias가 발견되지 않았습니다.", + "duplicate_groups": [], + "canonical_choices": [] + }, + { + "check_id": "document_component_coverage", + "severity": "PASS", + "detail": "모든 핵심 문서 구성요소가 확인되었습니다.", + "missing_components": [] + }, + { + "check_id": "authority_drift_detection", + "severity": "PASS", + "detail": "문서 간 사실 주입(authority drift)이 발견되지 않았습니다.", + "drift_findings": [] + } + ], + "overall_severity": "PASS", + "block_reasons": [], + "warning_reasons": [], + "hard_gate_findings": [], + "review_findings": [], + "rerun_targets": [], + "stage2_risk_notes": [], + "review_gate_summary": { + "review_gate_count": 0, + "highest_review_severity": "PASS" + }, + "handoff_export_summary": { + "is_soft_handoff_writer": false, + "handoff_writer_task": "Task_B2_SHA256_soft_gate_handoff_writer", + "exportable_review_count": 0 + }, + "downstream_progression": "ALLOWED", + "stage2_auto_progression_allowed": true, + "dropped_aliases": [], + "schema_contract_version": "B1_quality_gate.v3" +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/quality_gates/B2_event_candidates_gate.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/quality_gates/B2_event_candidates_gate.json new file mode 100644 index 00000000..288ac548 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/quality_gates/B2_event_candidates_gate.json @@ -0,0 +1,136 @@ +{ + "gate_id": "B2_event_candidates", + "executed_at": "2026-07-10T12:00:00Z", + "input_parts_count": 30, + "expected_ordinals": [ + "001", + "002", + "003", + "004", + "005", + "006", + "007", + "008", + "009", + "010", + "011", + "012", + "013", + "014", + "015", + "016", + "017", + "018", + "019", + "020", + "021", + "022", + "023", + "024", + "025", + "026", + "027", + "028", + "029", + "030" + ], + "planner_source": "evidence_shard_plan.json", + "planner_shard_count": 30, + "received_ordinals": [ + "001", + "002", + "003", + "004", + "005", + "006", + "007", + "008", + "009", + "010", + "011", + "012", + "013", + "014", + "015", + "016", + "017", + "018", + "019", + "020", + "021", + "022", + "023", + "024", + "025", + "026", + "027", + "028", + "029", + "030" + ], + "schema_invariant_findings": [], + "checks": [ + { + "check_id": "event_candidate_completeness", + "severity": "PASS", + "detail": "모든 증거 파트가 정상적으로 수집되었습니다.", + "missing_ordinals": [] + }, + { + "check_id": "evidence_reference_integrity", + "severity": "PASS", + "detail": "모든 evidence_index가 B1 결과와 정합합니다.", + "violations": [] + }, + { + "check_id": "candidate_id_collision", + "severity": "PASS", + "detail": "Candidate ID 충돌이 없습니다.", + "format_violations": [], + "dedup_findings": [] + }, + { + "check_id": "chronological_ordering", + "severity": "PASS", + "detail": "타임라인이 대체로 정합합니다.", + "ordering_violations": [] + }, + { + "check_id": "legal_effect_event_coverage", + "severity": "PASS", + "detail": "필수 법률효과 이벤트가 모두 포함되어 있습니다.", + "missing_subkinds_by_ordinal": [] + } + ], + "overall_severity": "PASS", + "block_reasons": [], + "warning_reasons": [], + "hard_gate_findings": [], + "review_findings": [ + { + "source_check": "review_findings_in_mapper", + "severity": "SOFT_WARNING", + "issue_type": "legal_relevance_uncertainty", + "concise_basis": "상계 주장 등 일부 항변 사항은 Stage 2에서 검토 필요", + "source_refs": ["E-010", "E-012", "E-011", "E-028"] + } + ], + "rerun_targets": [], + "stage2_risk_notes": [], + "review_gate_summary": { + "review_gate_count": 4, + "highest_review_severity": "SOFT_WARNING", + "b1_review_count": 0, + "b2_review_count": 4 + }, + "handoff_export_summary": { + "is_soft_handoff_writer": false, + "handoff_writer_task": "Task_B2_SHA256_soft_gate_handoff_writer", + "handoff_path": "quality_gates/stage1_part1_soft_gate_handoff.json", + "exported_review_count": 4, + "handoff_status_candidate": "READY_WITH_REVIEW" + }, + "downstream_progression": "ALLOWED_WITH_WARNING", + "stage2_auto_progression_allowed": true, + "schema_contract_version": "B2_quality_gate.v3" +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/quality_gates/stage1_part1_soft_gate_handoff.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/quality_gates/stage1_part1_soft_gate_handoff.json new file mode 100644 index 00000000..0670ac80 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/quality_gates/stage1_part1_soft_gate_handoff.json @@ -0,0 +1,82 @@ +{ + "schema_version": "stage1_part1_soft_gate_handoff.v1", + "source_stage": "Stage_1.Part_1", + "source_gate_files": [ + "quality_gates/B1_evidence_indexed_gate.json", + "quality_gates/B2_event_candidates_gate.json" + ], + "handoff_status": "READY_WITH_REVIEW", + "stage2_auto_progression_allowed": true, + "hard_gate_summary": { + "hard_gate_count": 0, + "hard_gate_ids": [] + }, + "review_gate_summary": { + "review_gate_count": 1, + "highest_review_severity": "SOFT_WARNING", + "b1_review_count": 0, + "b2_review_count": 1 + }, + "digest_guard": { + "evidence_indexed_sha256": "2f7e3a3fdd1682e853aad08242a4ddf009c0d6cb066b83845744f75770f8272a", + "evidence_event_candidates_sha256": "666c3167a4058df9b4e98fd4bb0504e3823befb7d30b8b32cc319554dbc347e0", + "b1_gate_sha256": "1f9184c03cd24307ee91b701b0f0c3c246e91962b7ec2b8894b33f2dd397c74d", + "b2_gate_sha256": "c8d2375dd17d03381fe331dc49c703fd6f85966fb356097639a4629c7f710c4a" + }, + "review_items": [ + { + "review_id": "P1RG-0001", + "source_gate": "B2", + "source_check": "review_findings_in_mapper", + "severity": "SOFT_WARNING", + "issue_type": "legal_relevance_uncertainty", + "lawyer_workflow_role": "cause_of_action_element_build", + "domain_hints": [], + "target_stage_hints": [], + "target_task_hints": [], + "source_refs": { + "evidence_indexes": [], + "event_candidate_ids": [], + "meeting_clause_refs": [], + "doc_titles": [] + }, + "review_instruction": "후속 검토 단계에서 해당 review finding의 증거와 법률상 의미를 재검토한다.", + "negative_instruction": "사실 확정 또는 자동 차단으로 취급하지 말 것", + "candidate_use_policy": "preserve_as_needs_review", + "concise_basis": "상계 주장 등 일부 항변 사항은 Stage 2에서 검토 필요", + "created_from": { + "gate_file": "quality_gates/B2_event_candidates_gate.json", + "gate_finding_id": null + } + } + ], + "routing_index": { + "by_evidence_index": {}, + "by_event_candidate_id": {}, + "by_meeting_clause_ref": {}, + "by_issue_type": { + "legal_relevance_uncertainty": [ + "P1RG-0001" + ] + }, + "by_domain_hint": {}, + "by_target_stage": {}, + "by_lawyer_workflow_role": { + "cause_of_action_element_build": [ + "P1RG-0001" + ] + } + }, + "lawyer_workflow_queues": { + "claim_right_identification": [], + "cause_of_action_element_build": [ + "P1RG-0001" + ], + "evidence_weight_and_gap_check": [], + "chronology_and_state_split": [], + "asset_registry_succession_check": [], + "defense_and_obstacle_check": [], + "relief_scope_and_quantification": [], + "actio_value_compensation_check": [] + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json new file mode 100644 index 00000000..2b28adcd --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json @@ -0,0 +1,12709 @@ +{ + "fact_ledger_candidate_bundle": { + "schema_version": "stage1_fact_ledger_candidate_bundle.v1", + "status": "READY_WITH_EXCEPTIONS", + "created_at_utc": "2026-07-10T07:53:41.315216+00:00", + "source_pack_file": "stage1_tmp/fact_ledger/fact_source_pack.json", + "candidate_items": [ + { + "fact_id": "F-001", + "source_bo_id": "bh1", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "평택시 빌라 신축 공사도급계약 체결 및 중도금 지급", + "evidence_refs": [ + "E-007" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:011" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-001" + ], + "issue_cluster_ids": [ + "IC-001" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-007" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:011" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-001" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh1" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "평택시 빌라 신축 공사도급계약 체결 및 중도금 지급", + "derivation_basis": "source_bo_id=bh1; evidence_refs=E-007; structure_ids=LES-001", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-002", + "source_bo_id": "bh10", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "오국한에 대한 대여금 채권 등을 자동채권으로 한 상계 항변", + "evidence_refs": [ + "E-003", + "E-010" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:013" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-002" + ], + "issue_cluster_ids": [ + "IC-002" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-003", + "E-010" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:013" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-002" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh10" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "오국한에 대한 대여금 채권 등을 자동채권으로 한 상계 항변", + "derivation_basis": "source_bo_id=bh10; evidence_refs=E-003,E-010; structure_ids=LES-002", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-003", + "source_bo_id": "bh11", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "물품대금 지급 최고", + "evidence_refs": [ + "E-027" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:003" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-003" + ], + "issue_cluster_ids": [ + "IC-003" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-027" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:003" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-003" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh11" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "물품대금 지급 최고", + "derivation_basis": "source_bo_id=bh11; evidence_refs=E-027; structure_ids=LES-003", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-004", + "source_bo_id": "bh12", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "상호속용에 따른 물품대금 채무 책임 부인 통지", + "evidence_refs": [ + "E-028" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "claim_chain_ref", + "linked_structures", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:005" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-004" + ], + "issue_cluster_ids": [ + "IC-004" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-028" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:005" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-004" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh12" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "상호속용에 따른 물품대금 채무 책임 부인 통지", + "derivation_basis": "source_bo_id=bh12; evidence_refs=E-028; structure_ids=LES-004", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-005", + "source_bo_id": "bh13", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "공사대금 잔금 3억 원 미지급 상태 및 유치권 충당 항변", + "evidence_refs": [ + "E-011" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:012" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-005" + ], + "issue_cluster_ids": [ + "IC-005" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-011" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:012" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-005" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh13" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "공사대금 잔금 3억 원 미지급 상태 및 유치권 충당 항변", + "derivation_basis": "source_bo_id=bh13; evidence_refs=E-011; structure_ids=LES-005", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-006", + "source_bo_id": "bh14", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[\"AC-001\"]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "강용원은 성수동 대지에 대한 상속 및 증여로 지분을 취득하고, 오민한에게 차용금 담보를 위해 성수동 대지 지분에 대한 근저당권 설정 약정을 체결함", + "evidence_refs": [ + "E-013", + "E-025", + "E-026" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B2:001" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-006" + ], + "issue_cluster_ids": [ + "IC-006" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_index_ids": [ + "E-013", + "E-025", + "E-026" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:001" + ] + }, + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-006" + ], + "asset_cluster_ids": [ + "AC-001" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "강용원은 성수동 대지에 대한 상속 및 증여로 지분을 취득하고, 오민한에게 차용금 담보를 위해 성수동 대지 지분에 대한 근저당권 설정 약정을 체결함", + "derivation_basis": "source_bo_id=bh14; evidence_refs=E-013,E-025,E-026; structure_ids=LES-006", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-001\"]", + "candidate_structure_types": [ + "secured_debt" + ] + } + }, + { + "fact_id": "F-007", + "source_bo_id": "bh15", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[\"AC-002\"]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "신림동 상가에 강용원(등기상 강용일) 명의의 근저당권이 설정되었다가 무단 말소되었으며, 오민한은 상계를 주장함", + "evidence_refs": [ + "E-010", + "E-014" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B2:004" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-007" + ], + "issue_cluster_ids": [ + "IC-007" + ], + "asset_cluster_ids": [ + "AC-002" + ], + "evidence_index_ids": [ + "E-010", + "E-014" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:004" + ] + }, + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-007" + ], + "asset_cluster_ids": [ + "AC-002" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "신림동 상가에 강용원(등기상 강용일) 명의의 근저당권이 설정되었다가 무단 말소되었으며, 오민한은 상계를 주장함", + "derivation_basis": "source_bo_id=bh15; evidence_refs=E-010,E-014; structure_ids=LES-007", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-002\"]", + "candidate_structure_types": [ + "secured_debt" + ] + } + }, + { + "fact_id": "F-008", + "source_bo_id": "bh16", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[\"AC-003\"]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "평택시 서정빌라에 대한 보존등기 및 매매로 인한 소유권이전등기가 마쳐짐", + "evidence_refs": [ + "E-015" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B2:006" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-008" + ], + "issue_cluster_ids": [ + "IC-008" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_index_ids": [ + "E-015" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:006" + ] + }, + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-008" + ], + "asset_cluster_ids": [ + "AC-003" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "평택시 서정빌라에 대한 보존등기 및 매매로 인한 소유권이전등기가 마쳐짐", + "derivation_basis": "source_bo_id=bh16; evidence_refs=E-015; structure_ids=LES-008", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-003\"]", + "candidate_structure_types": [ + "secured_debt" + ] + } + }, + { + "fact_id": "F-009", + "source_bo_id": "bh17", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[\"AC-001\"]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "오민한이 경매를 신청하여 이문호가 매각대금을 완납하고 취득한 뒤, 대한은행이 근저당권을 설정함에 따라 원인무효 여부 및 공신력이 다투어짐", + "evidence_refs": [ + "E-012", + "E-013" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B2:003" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-009" + ], + "issue_cluster_ids": [ + "IC-009" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_index_ids": [ + "E-012", + "E-013" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:003" + ] + }, + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-009" + ], + "asset_cluster_ids": [ + "AC-001" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "오민한이 경매를 신청하여 이문호가 매각대금을 완납하고 취득한 뒤, 대한은행이 근저당권을 설정함에 따라 원인무효 여부 및 공신력이 다투어짐", + "derivation_basis": "source_bo_id=bh17; evidence_refs=E-012,E-013; structure_ids=LES-009", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-001\"]", + "candidate_structure_types": [ + "secured_debt" + ] + } + }, + { + "fact_id": "F-010", + "source_bo_id": "bh18", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "강용원이 오민한에게 4억 원을 지급하였으나 다수의 채무 중 충당할 채무를 특정하지 않음", + "evidence_refs": [], + "credibility": "low", + "legal_centrality": "critical", + "proof_strength": "low", + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B2:002" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-010" + ], + "issue_cluster_ids": [ + "IC-010" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:002" + ] + }, + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-010" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "skeleton_validation_codes": [ + "NO_EVIDENCE_REFS" + ], + "derived_fact": "강용원이 오민한에게 4억 원을 지급하였으나 다수의 채무 중 충당할 채무를 특정하지 않음", + "derivation_basis": "source_bo_id=bh18; structure_ids=LES-010", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "secured_debt" + ] + } + }, + { + "fact_id": "F-011", + "source_bo_id": "bh19", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[\"AC-003\"]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "박광윤이 평택시 빌라를 점유하며 얻은 임대 및 거주 수익을 피담보채권에 충당하였다고 항변함", + "evidence_refs": [ + "E-007", + "E-011" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "legal_calculation_object", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B2:005" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-011" + ], + "issue_cluster_ids": [ + "IC-011" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_index_ids": [ + "E-007", + "E-011" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:005" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-011" + ], + "asset_cluster_ids": [ + "AC-003" + ] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh19" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "박광윤이 평택시 빌라를 점유하며 얻은 임대 및 거주 수익을 피담보채권에 충당하였다고 항변함", + "derivation_basis": "source_bo_id=bh19; evidence_refs=E-007,E-011; structure_ids=LES-011", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-003\"]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-012", + "source_bo_id": "bh2", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "인테리어 자재 매매에 따른 물품대금 채권 발생", + "evidence_refs": [ + "E-021", + "E-027" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "high", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:001" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-012" + ], + "issue_cluster_ids": [ + "IC-012" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-021", + "E-027" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:001" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-012" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh2" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "인테리어 자재 매매에 따른 물품대금 채권 발생", + "derivation_basis": "source_bo_id=bh2; evidence_refs=E-021,E-027; structure_ids=LES-012", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-013", + "source_bo_id": "bh20", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[\"AC-004\"]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "이수인이 윤건우에게 매매계약에서 합의되지 않은 제3자 명의로의 등기이전을 일방적으로 요구함", + "evidence_refs": [ + "E-029" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B2:007" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-013" + ], + "issue_cluster_ids": [ + "IC-013" + ], + "asset_cluster_ids": [ + "AC-004" + ], + "evidence_index_ids": [ + "E-029" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:007" + ] + }, + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-013" + ], + "asset_cluster_ids": [ + "AC-004" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "이수인이 윤건우에게 매매계약에서 합의되지 않은 제3자 명의로의 등기이전을 일방적으로 요구함", + "derivation_basis": "source_bo_id=bh20; evidence_refs=E-029; structure_ids=LES-013", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-004\"]", + "candidate_structure_types": [ + "secured_debt" + ] + } + }, + { + "fact_id": "F-014", + "source_bo_id": "bh21", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[\"AC-005\", \"AC-006\"]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "성수동 대지 차임 시세 감정평가", + "evidence_refs": [ + "E-005" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B3:002" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-014" + ], + "issue_cluster_ids": [ + "IC-014" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006" + ], + "evidence_index_ids": [ + "E-005" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:002" + ] + }, + "must_not_drop_in_claim_types": [ + "land_use_gain", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-014" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "land_use_gain", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "성수동 대지 차임 시세 감정평가", + "derivation_basis": "source_bo_id=bh21; evidence_refs=E-005; structure_ids=LES-014", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-005\", \"AC-006\"]", + "candidate_structure_types": [ + "land_use_gain" + ] + } + }, + { + "fact_id": "F-015", + "source_bo_id": "bh22", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[\"AC-005\", \"AC-007\", \"AC-008\"]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "성수동 대지 및 건물 인도, 점유 및 사용", + "evidence_refs": [ + "E-006" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_structures", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B3:004" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-015" + ], + "issue_cluster_ids": [ + "IC-015" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-007", + "AC-008" + ], + "evidence_index_ids": [ + "E-006" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:004" + ] + }, + "must_not_drop_in_claim_types": [ + "land_use_gain", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-015" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-007", + "AC-008" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "land_use_gain", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "성수동 대지 및 건물 인도, 점유 및 사용", + "derivation_basis": "source_bo_id=bh22; evidence_refs=E-006; structure_ids=LES-015", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-005\", \"AC-007\", \"AC-008\"]", + "candidate_structure_types": [ + "land_use_gain" + ] + } + }, + { + "fact_id": "F-016", + "source_bo_id": "bh23", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[\"AC-009\", \"AC-010\"]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "신림로 상가 및 포천시 임야 가치평가(감정평가서)", + "evidence_refs": [ + "E-004" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B3:001" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-016" + ], + "issue_cluster_ids": [ + "IC-016" + ], + "asset_cluster_ids": [ + "AC-009", + "AC-010" + ], + "evidence_index_ids": [ + "E-004" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:001" + ] + }, + "must_not_drop_in_claim_types": [ + "land_use_gain", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-016" + ], + "asset_cluster_ids": [ + "AC-009", + "AC-010" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "land_use_gain", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "신림로 상가 및 포천시 임야 가치평가(감정평가서)", + "derivation_basis": "source_bo_id=bh23; evidence_refs=E-004; structure_ids=LES-016", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-009\", \"AC-010\"]", + "candidate_structure_types": [ + "land_use_gain" + ] + } + }, + { + "fact_id": "F-017", + "source_bo_id": "bh24", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[\"AC-011\", \"AC-012\"]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "평택시 서정빌라 101호 점유 및 유치권 행사", + "evidence_refs": [ + "E-011" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "legal_calculation_object", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B3:005" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-017" + ], + "issue_cluster_ids": [ + "IC-017" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-012" + ], + "evidence_index_ids": [ + "E-011" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:005" + ] + }, + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-017" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-012" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "평택시 서정빌라 101호 점유 및 유치권 행사", + "derivation_basis": "source_bo_id=bh24; evidence_refs=E-011; structure_ids=LES-017", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-011\", \"AC-012\"]", + "candidate_structure_types": [ + "secured_debt" + ] + } + }, + { + "fact_id": "F-018", + "source_bo_id": "bh25", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[\"AC-011\", \"AC-013\"]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "평택시 서정빌라 차임 시세 평가", + "evidence_refs": [ + "E-023" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B3:003" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-018" + ], + "issue_cluster_ids": [ + "IC-018" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-013" + ], + "evidence_index_ids": [ + "E-023" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:003" + ] + }, + "must_not_drop_in_claim_types": [ + "land_use_gain", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-018" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-013" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "land_use_gain", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "평택시 서정빌라 차임 시세 평가", + "derivation_basis": "source_bo_id=bh25; evidence_refs=E-023; structure_ids=LES-018", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-011\", \"AC-013\"]", + "candidate_structure_types": [ + "land_use_gain" + ] + } + }, + { + "fact_id": "F-019", + "source_bo_id": "bh26", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[\"AC-014\", \"AC-015\"]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "흑석동 상가 점유 및 운영 (맛나식당)", + "evidence_refs": [ + "E-017", + "E-018", + "E-022" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_structures", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B3:006" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-019" + ], + "issue_cluster_ids": [ + "IC-019" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "evidence_index_ids": [ + "E-017", + "E-018", + "E-022" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:006" + ] + }, + "must_not_drop_in_claim_types": [ + "land_use_gain", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-019" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "land_use_gain", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "흑석동 상가 점유 및 운영 (맛나식당)", + "derivation_basis": "source_bo_id=bh26; evidence_refs=E-017,E-018,E-022; structure_ids=LES-019", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-014\", \"AC-015\"]", + "candidate_structure_types": [ + "land_use_gain" + ] + } + }, + { + "fact_id": "F-020", + "source_bo_id": "bh27", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[\"AC-016\", \"AC-002\"]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "채무자 오국한이 수익자 오민한에게 책임재산인 신림동 상가를 1억5천만원에 매도하여 소유권이전등기 경료", + "evidence_refs": [ + "E-014" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "actio_roles", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B4:002" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-020" + ], + "issue_cluster_ids": [ + "IC-020" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_index_ids": [ + "E-014" + ], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:002" + ] + }, + "must_not_drop_in_claim_types": [ + "actio", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-020" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-002", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "beneficiary", + "value_candidate": "{}", + "date_candidate": null, + "related_bo_ids": [ + "bh27" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh27\"], \"related_evidence_indexes\": [\"E-014\"], \"role\": \"beneficiary\", \"value_candidate\": \"{}\"}], \"actio_scope\": [\"fraudulent_act\", \"beneficiary_or_transferee\", \"target_asset\"], \"preserved_claim_candidates\": [{\"bo_id\": \"bh27\", \"claim_date_candidate\": null, \"claim_type_candidate\": null, \"creditor_candidate\": null, \"debtor_candidate\": null, \"related_evidence_indexes\": [\"E-014\"]}], \"related_bo_ids\": [\"bh27\"], \"related_evidence_indexe" + } + ], + "linked_actio_structures": { + "structure_ids": [ + "LES-020" + ], + "actio_signal_count": 1 + }, + "downstream_module_candidates": [ + "actio", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "채무자 오국한이 수익자 오민한에게 책임재산인 신림동 상가를 1억5천만원에 매도하여 소유권이전등기 경료", + "derivation_basis": "source_bo_id=bh27; evidence_refs=E-014; structure_ids=LES-020", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-016\", \"AC-002\"]", + "candidate_structure_types": [ + "fraudulent_transfer" + ] + } + }, + { + "fact_id": "F-021", + "source_bo_id": "bh28", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생", + "evidence_refs": [ + "E-024" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "actio_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B4:001" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-021" + ], + "issue_cluster_ids": [ + "IC-021" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-024" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:001" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "actio", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-021" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh28" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-001", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-024" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "debtor_act", + "value_candidate": "강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생", + "date_candidate": null, + "related_bo_ids": [ + "bh28" + ], + "related_evidence_indexes": [ + "E-024" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh28\"], \"related_evidence_indexes\": [\"E-024\"], \"role\": \"debtor_act\", \"value_candidate\": \"강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생\"}], \"actio_scope\": [\"preserved_claim\"], \"preserved_claim_candidates\": [{\"bo_id\": null, \"claim_date_candidate\": null, \"claim_type_candidate\": null, \"creditor_candidate\": null, \"debtor_candidate\": null, \"related_evidence_indexes\": [\"E-024\"]}], \"related_bo_ids\": [\"bh28\"], \"related_evidence_indexes\": [\"E" + } + ], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 1 + }, + "downstream_module_candidates": [ + "money_claim", + "actio", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생", + "derivation_basis": "source_bo_id=bh28; evidence_refs=E-024; structure_ids=LES-021", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-022", + "source_bo_id": "bh29", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[\"AC-016\", \"AC-017\", \"AC-018\", \"AC-019\", \"AC-002\"]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "수익자 오민한이 사해행위 이후 피담보채무 1억3천만원을 대위변제하고 2번 근저당권을 말소시킴으로써 원물반환이 불가능해져 가액배상 요건 성립", + "evidence_refs": [ + "E-014" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_actio_structures", + "linked_structures", + "actio_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B4:004" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-022" + ], + "issue_cluster_ids": [ + "IC-022" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-002" + ], + "evidence_index_ids": [ + "E-014" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:004" + ] + }, + "must_not_drop_in_claim_types": [ + "secured_property", + "actio", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-022" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-002" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-004", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "remedy_or_cap", + "value_candidate": "원물반환 불가", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "remedy_or_cap", + "value_candidate": "가액배상", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2021-08-01', 'event': '나임오 1번 근저당 설정(5000만원)'}", + "date_candidate": "2021-08-01", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2022-04-18', 'event': '강용일 2번 근저당 설정(1.5억원)'}", + "date_candidate": "2022-04-18", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2024-10-17', 'event': '강용일 2번 근저당 말소(오민한 대위변제)'}", + "date_candidate": "2024-10-17", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh29\"], \"related_evidence_indexes\": [\"E-014\"], \"role\": \"remedy_or_cap\", \"value_candidate\": \"원물반환 불가\"}, {\"date_candidate\": null, \"related_bo_ids\": [\"bh29\"], \"related_evidence_indexes\": [\"E-014\"], \"role\": \"remedy_or_cap\", \"value_candidate\": \"가액배상\"}, {\"date_candidate\": \"2021-08-01\", \"related_bo_ids\": [\"bh29\"], \"related_evidence_indexes\": [\"E-014\"], \"role\": \"encumbrance\", \"value_candidate\": \"{'date': '2021-08-01', 'event': '나임오 1번 근저" + } + ], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 1 + }, + "downstream_module_candidates": [ + "secured_property", + "actio", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "수익자 오민한이 사해행위 이후 피담보채무 1억3천만원을 대위변제하고 2번 근저당권을 말소시킴으로써 원물반환이 불가능해져 가액배상 요건 성립", + "derivation_basis": "source_bo_id=bh29; evidence_refs=E-014; structure_ids=LES-022", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-016\", \"AC-017\", \"AC-018\", \"AC-019\", \"AC-002\"]", + "candidate_structure_types": [ + "secured_debt" + ] + } + }, + { + "fact_id": "F-023", + "source_bo_id": "bh3", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "1억 원 대여금 채권 발생 (2022.4.18.)", + "evidence_refs": [ + "E-024" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:006" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-023" + ], + "issue_cluster_ids": [ + "IC-023" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-024" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:006" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-023" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh3" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "1억 원 대여금 채권 발생 (2022.4.18.)", + "derivation_basis": "source_bo_id=bh3; evidence_refs=E-024; structure_ids=LES-023", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-024", + "source_bo_id": "bh30", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[\"AC-020\", \"AC-021\"]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "evidence_refs": [ + "E-004" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "legal_effect_roles", + "actio_roles", + "legal_calculation_object", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B4:003" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-024" + ], + "issue_cluster_ids": [ + "IC-024" + ], + "asset_cluster_ids": [ + "AC-020", + "AC-021" + ], + "evidence_index_ids": [ + "E-004" + ], + "candidate_structure_types": [ + "valuation" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:003" + ] + }, + "must_not_drop_in_claim_types": [ + "valuation", + "actio", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-024" + ], + "asset_cluster_ids": [ + "AC-020", + "AC-021" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-003", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-004" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "debtor_act", + "value_candidate": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "date_candidate": null, + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh30\"], \"related_evidence_indexes\": [\"E-004\"], \"role\": \"debtor_act\", \"value_candidate\": \"사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)\"}], \"actio_scope\": [\"target_asset\"], \"preserved_claim_candidates\": [{\"bo_id\": \"bh30\", \"claim_date_candidate\": null, \"claim_type_candidate\": null, \"creditor_candidate\": null, \"debtor_candidate\": null, \"related_evidence_indexes\": [\"E-004\"]}], \"related_bo_ids\": [\"bh30\"], \"related_evidence_i" + } + ], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 1 + }, + "downstream_module_candidates": [ + "valuation", + "actio", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "derivation_basis": "source_bo_id=bh30; evidence_refs=E-004; structure_ids=LES-024", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-020\", \"AC-021\"]", + "candidate_structure_types": [ + "valuation" + ] + } + }, + { + "fact_id": "F-025", + "source_bo_id": "bh31", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[\"AC-016\", \"AC-002\"]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "수익자 오민한의 항변 (명의신탁 주장, 매수자금 및 대여금 참작 주장, 채권 상계 주장)", + "evidence_refs": [ + "E-003", + "E-010" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "legal_effect_roles", + "actio_roles", + "linked_structures", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B4:005" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-025" + ], + "issue_cluster_ids": [ + "IC-025" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_index_ids": [ + "E-003", + "E-010" + ], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:005" + ] + }, + "must_not_drop_in_claim_types": [ + "actio", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-025" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-005", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "defense", + "value_candidate": "{'defense_type': '계약명의신탁', 'content': '오국한 명의 취득이 수익자 자금에 의한 계약명의신탁이므로 사해행위가 아니라는 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "role": "defense", + "value_candidate": "{'defense_type': '배상금감액', 'content': '수익자가 제공한 매수자금 4000만원 및 대여금 1000만원 참작 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "role": "defense", + "value_candidate": "{'defense_type': '상계항변', 'content': '오민한의 5000만원 채권으로 강용원 채권을 상계한다는 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh31\"], \"related_evidence_indexes\": [\"E-003\", \"E-010\"], \"role\": \"defense\", \"value_candidate\": \"{'defense_type': '계약명의신탁', 'content': '오국한 명의 취득이 수익자 자금에 의한 계약명의신탁이므로 사해행위가 아니라는 주장'}\"}, {\"date_candidate\": null, \"related_bo_ids\": [\"bh31\"], \"related_evidence_indexes\": [\"E-003\", \"E-010\"], \"role\": \"defense\", \"value_candidate\": \"{'defense_type': '배상금감액', 'content': '수익자가 제공한 매수자금 4000만원 및 대여금 1000만원 참작 주장'}\"}, {\"date_candidate\": null, " + } + ], + "linked_actio_structures": { + "structure_ids": [ + "LES-025" + ], + "actio_signal_count": 1 + }, + "downstream_module_candidates": [ + "actio", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "수익자 오민한의 항변 (명의신탁 주장, 매수자금 및 대여금 참작 주장, 채권 상계 주장)", + "derivation_basis": "source_bo_id=bh31; evidence_refs=E-003,E-010; structure_ids=LES-025", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-016\", \"AC-002\"]", + "candidate_structure_types": [ + "fraudulent_transfer" + ] + } + }, + { + "fact_id": "F-026", + "source_bo_id": "bh32", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "강석우 사망으로 인한 정유심, 강용원의 성수동 대지 지분 상속", + "evidence_refs": [ + "E-013" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:002" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-026" + ], + "issue_cluster_ids": [ + "IC-026" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-013" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:002" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-026" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "강석우 사망으로 인한 정유심, 강용원의 성수동 대지 지분 상속", + "derivation_basis": "source_bo_id=bh32; evidence_refs=E-013; structure_ids=LES-026", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-027", + "source_bo_id": "bh33", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "망 강호연의 사망에 따른 상속개시 및 자녀 강형수, 강지수의 상속포기", + "evidence_refs": [ + "E-001", + "E-002" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "high", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:003" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-027" + ], + "issue_cluster_ids": [ + "IC-027" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-001", + "E-002" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:003" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-027" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "망 강호연의 사망에 따른 상속개시 및 자녀 강형수, 강지수의 상속포기", + "derivation_basis": "source_bo_id=bh33; evidence_refs=E-001,E-002; structure_ids=LES-027", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-028", + "source_bo_id": "bh34", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "강용원이 김선웅에게 물품대금 지급을 최고하는 통지서 발송 및 도달", + "evidence_refs": [ + "E-027" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:009" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-028" + ], + "issue_cluster_ids": [ + "IC-028" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-027" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:009" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-028" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "강용원이 김선웅에게 물품대금 지급을 최고하는 통지서 발송 및 도달", + "derivation_basis": "source_bo_id=bh34; evidence_refs=E-027; structure_ids=LES-028", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-029", + "source_bo_id": "bh35", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "강용원이 오민한에게 사해행위취소 및 신림동 상가 소유권 반환을 요구하는 내용증명 발송", + "evidence_refs": [ + "E-008", + "E-010" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "high", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "actio_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:004" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-029" + ], + "issue_cluster_ids": [ + "IC-029" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-008", + "E-010" + ], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:004" + ] + }, + "must_not_drop_in_claim_types": [ + "actio", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-029" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [ + "LES-029" + ], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "actio", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "강용원이 오민한에게 사해행위취소 및 신림동 상가 소유권 반환을 요구하는 내용증명 발송", + "derivation_basis": "source_bo_id=bh35; evidence_refs=E-008,E-010; structure_ids=LES-029", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "fraudulent_transfer" + ] + } + }, + { + "fact_id": "F-030", + "source_bo_id": "bh36", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "강용원과 양정숙이 박광윤에게 평택시 빌라 유치권소멸을 청구하는 통지서 발송 및 도달", + "evidence_refs": [ + "E-011", + "E-019" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "high", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:007" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-030" + ], + "issue_cluster_ids": [ + "IC-030" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-011", + "E-019" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:007" + ] + }, + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-030" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "skeleton_validation_codes": [ + "NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW" + ], + "derived_fact": "강용원과 양정숙이 박광윤에게 평택시 빌라 유치권소멸을 청구하는 통지서 발송 및 도달", + "derivation_basis": "source_bo_id=bh36; evidence_refs=E-011,E-019; structure_ids=LES-030", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "secured_debt" + ] + } + }, + { + "fact_id": "F-031", + "source_bo_id": "bh37", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "오민한이 강용원에게 김선웅의 채무에 대한 책임을 부인하는 통지서 발송", + "evidence_refs": [ + "E-028" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:010" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-031" + ], + "issue_cluster_ids": [ + "IC-031" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-028" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:010" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-031" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "오민한이 강용원에게 김선웅의 채무에 대한 책임을 부인하는 통지서 발송", + "derivation_basis": "source_bo_id=bh37; evidence_refs=E-028; structure_ids=LES-031", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-032", + "source_bo_id": "bh38", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "오민한이 강용원에게 내용증명에 대한 답신 발송", + "evidence_refs": [ + "E-010" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:006" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-032" + ], + "issue_cluster_ids": [ + "IC-032" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-010" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:006" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-032" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "오민한이 강용원에게 내용증명에 대한 답신 발송", + "derivation_basis": "source_bo_id=bh38; evidence_refs=E-010; structure_ids=LES-032", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-033", + "source_bo_id": "bh39", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "윤건우가 양정숙에게 흑석동 상가 임대차계약 해지 및 명도를 요구하는 내용증명 통지", + "evidence_refs": [ + "E-009" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:005" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-033" + ], + "issue_cluster_ids": [ + "IC-033" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-009" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:005" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-033" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "윤건우가 양정숙에게 흑석동 상가 임대차계약 해지 및 명도를 요구하는 내용증명 통지", + "derivation_basis": "source_bo_id=bh39; evidence_refs=E-009; structure_ids=LES-033", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-034", + "source_bo_id": "bh4", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "1억 원 대여금 채권 발생 (2022.5.18.)", + "evidence_refs": [ + "E-024" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:007" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-034" + ], + "issue_cluster_ids": [ + "IC-034" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-024" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:007" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-034" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh4" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "1억 원 대여금 채권 발생 (2022.5.18.)", + "derivation_basis": "source_bo_id=bh4; evidence_refs=E-024; structure_ids=LES-034", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-035", + "source_bo_id": "bh40", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "윤건우가 이수인에게 흑석동 상가 매매계약 해제 통지서 발송 및 도달", + "evidence_refs": [ + "E-030" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:012" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-035" + ], + "issue_cluster_ids": [ + "IC-035" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-030" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:012" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-035" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [ + "NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW" + ], + "derived_fact": "윤건우가 이수인에게 흑석동 상가 매매계약 해제 통지서 발송 및 도달", + "derivation_basis": "source_bo_id=bh40; evidence_refs=E-030; structure_ids=LES-035", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-036", + "source_bo_id": "bh41", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "이수인이 윤건우에게 흑석동 상가 매매대금 감액 및 제3자 명의 등기 이전 요청 통지", + "evidence_refs": [ + "E-029" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:011" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-036" + ], + "issue_cluster_ids": [ + "IC-036" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-029" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:011" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-036" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh41" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "이수인이 윤건우에게 흑석동 상가 매매대금 감액 및 제3자 명의 등기 이전 요청 통지", + "derivation_basis": "source_bo_id=bh41; evidence_refs=E-029; structure_ids=LES-036", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-037", + "source_bo_id": "bh42", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "주식회사 대한은행이 강용원의 성수동 대지 근저당권 말소 신청을 거부하는 통지 발송", + "evidence_refs": [ + "E-012" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:008" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-037" + ], + "issue_cluster_ids": [ + "IC-037" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-012" + ], + "candidate_structure_types": [ + "registry_invalidity" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:008" + ] + }, + "must_not_drop_in_claim_types": [ + "registry_invalidity", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-037" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "registry_invalidity", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "주식회사 대한은행이 강용원의 성수동 대지 근저당권 말소 신청을 거부하는 통지 발송", + "derivation_basis": "source_bo_id=bh42; evidence_refs=E-012; structure_ids=LES-037", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "registry_invalidity" + ] + } + }, + { + "fact_id": "F-038", + "source_bo_id": "bh43", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[\"AC-022\", \"AC-023\", \"AC-024\", \"AC-025\", \"AC-026\"]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "목적물 특정", + "evidence_refs": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:001" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-038" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "asset_cluster_ids": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "evidence_index_ids": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:001" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-038" + ], + "asset_cluster_ids": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "목적물 특정", + "derivation_basis": "source_bo_id=bh43; evidence_refs=E-013,E-014,E-015,E-016; structure_ids=LES-038", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[\"AC-022\", \"AC-023\", \"AC-024\", \"AC-025\", \"AC-026\"]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-039", + "source_bo_id": "bh44", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "박광윤의 평택시 서정빌라 101호 거주 및 유치권 행사 상태", + "evidence_refs": [ + "E-011" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:013" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-039" + ], + "issue_cluster_ids": [ + "IC-039" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-011" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:013" + ] + }, + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-039" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "박광윤의 평택시 서정빌라 101호 거주 및 유치권 행사 상태", + "derivation_basis": "source_bo_id=bh44; evidence_refs=E-011; structure_ids=LES-039", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "secured_debt" + ] + } + }, + { + "fact_id": "F-040", + "source_bo_id": "bh45", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "박성희의 성수동 대지 및 건물 점유", + "evidence_refs": [ + "E-006" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:014" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-040" + ], + "issue_cluster_ids": [ + "IC-040" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-006" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:014" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-040" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "박성희의 성수동 대지 및 건물 점유", + "derivation_basis": "source_bo_id=bh45; evidence_refs=E-006; structure_ids=LES-040", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-041", + "source_bo_id": "bh46", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "양정숙의 흑석동 상가 점유 및 식당 운영 상태", + "evidence_refs": [ + "E-018", + "E-022" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:015" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-041" + ], + "issue_cluster_ids": [ + "IC-041" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-018", + "E-022" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:015" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-041" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "양정숙의 흑석동 상가 점유 및 식당 운영 상태", + "derivation_basis": "source_bo_id=bh46; evidence_refs=E-018,E-022; structure_ids=LES-041", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-042", + "source_bo_id": "bh47", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "오민한의 강용원에 대한 자신의 5,000만 원 채권 상계 주장 항변", + "evidence_refs": [ + "E-010" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:017" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-042" + ], + "issue_cluster_ids": [ + "IC-042" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-010" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:017" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-042" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh47" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "오민한의 강용원에 대한 자신의 5,000만 원 채권 상계 주장 항변", + "derivation_basis": "source_bo_id=bh47; evidence_refs=E-010; structure_ids=LES-042", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-043", + "source_bo_id": "bh48", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "박광윤의 과거 임대료 및 거주 수익을 채무에 충당하였다는 주장 항변", + "evidence_refs": [ + "E-011" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:018" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-043" + ], + "issue_cluster_ids": [ + "IC-043" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-011" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:018" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-043" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "박광윤의 과거 임대료 및 거주 수익을 채무에 충당하였다는 주장 항변", + "derivation_basis": "source_bo_id=bh48; evidence_refs=E-011; structure_ids=LES-043", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-044", + "source_bo_id": "bh49", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "윤건우의 임대차 책임 부인 및 양정숙 차임 연체 주장 항변", + "evidence_refs": [ + "E-009" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:016" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-044" + ], + "issue_cluster_ids": [ + "IC-044" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-009" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:016" + ] + }, + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-044" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "윤건우의 임대차 책임 부인 및 양정숙 차임 연체 주장 항변", + "derivation_basis": "source_bo_id=bh49; evidence_refs=E-009; structure_ids=LES-044", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ] + } + }, + { + "fact_id": "F-045", + "source_bo_id": "bh5", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "2억 원 대여금 채권 발생 (2023.1.6.)", + "evidence_refs": [ + "E-025" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:008" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-045" + ], + "issue_cluster_ids": [ + "IC-045" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-025" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:008" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-045" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh5" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "2억 원 대여금 채권 발생 (2023.1.6.)", + "derivation_basis": "source_bo_id=bh5; evidence_refs=E-025; structure_ids=LES-045", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-046", + "source_bo_id": "bh50", + "type": "state", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "주식회사 대한은행의 경매공신력 주장(민사집행법 제267조)에 따른 근저당 말소 불가 항변", + "evidence_refs": [ + "E-012" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:019" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-046" + ], + "issue_cluster_ids": [ + "IC-046" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-012" + ], + "candidate_structure_types": [ + "registry_invalidity" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:019" + ] + }, + "must_not_drop_in_claim_types": [ + "registry_invalidity", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "state", + "issue_cluster_ids": [ + "IC-046" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": false, + "source_bo_id": null + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "registry_invalidity", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "주식회사 대한은행의 경매공신력 주장(민사집행법 제267조)에 따른 근저당 말소 불가 항변", + "derivation_basis": "source_bo_id=bh50; evidence_refs=E-012; structure_ids=LES-046", + "legal_calculation_object": { + "calculation_required": false, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "registry_invalidity" + ] + } + }, + { + "fact_id": "F-047", + "source_bo_id": "bh6", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "3억 원 대여금 채권 발생 (2023.7.6.)", + "evidence_refs": [ + "E-026" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:009" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-047" + ], + "issue_cluster_ids": [ + "IC-047" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-026" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:009" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-047" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh6" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "3억 원 대여금 채권 발생 (2023.7.6.)", + "derivation_basis": "source_bo_id=bh6; evidence_refs=E-026; structure_ids=LES-047", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-048", + "source_bo_id": "bh7", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "해드림 영업양도 및 상호속용 약정", + "evidence_refs": [ + "E-020" + ], + "credibility": "high", + "legal_centrality": "critical", + "proof_strength": "documentary", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:004" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-048" + ], + "issue_cluster_ids": [ + "IC-048" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-020" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:004" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-048" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh7" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "해드림 영업양도 및 상호속용 약정", + "derivation_basis": "source_bo_id=bh7; evidence_refs=E-020; structure_ids=LES-048", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-049", + "source_bo_id": "bh8", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "action": "물품대금 채무 승인 및 변제기 유예", + "evidence_refs": [ + "E-021" + ], + "credibility": "medium", + "legal_centrality": "critical", + "proof_strength": "medium", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:002" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-049" + ], + "issue_cluster_ids": [ + "IC-049" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-021" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:002" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-049" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh8" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [], + "derived_fact": "물품대금 채무 승인 및 변제기 유예", + "derivation_basis": "source_bo_id=bh8; evidence_refs=E-021; structure_ids=LES-049", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": null, \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + }, + { + "fact_id": "F-050", + "source_bo_id": "bh9", + "type": "event", + "date": null, + "parties": [], + "object_spec": "[]", + "amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "action": "차용금 변제 명목 4억 원 지급 (채무 미특정)", + "evidence_refs": [], + "credibility": "low", + "legal_centrality": "critical", + "proof_strength": "low", + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "claim_chain_ref": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:010" + ], + "signal_count": 1 + }, + "linked_structures": { + "structure_ids": [ + "LES-050" + ], + "issue_cluster_ids": [ + "IC-050" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:010" + ] + }, + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true, + "state_context": { + "bo_type": "event", + "issue_cluster_ids": [ + "IC-050" + ], + "asset_cluster_ids": [] + }, + "money_claim_effect": { + "enabled": true, + "source_bo_id": "bh9" + }, + "commercial_successor_effect": { + "enabled": false, + "source_bo_id": null + }, + "actio_roles": [], + "linked_actio_structures": { + "structure_ids": [], + "actio_signal_count": 0 + }, + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "skeleton_validation_codes": [ + "NO_EVIDENCE_REFS" + ], + "derived_fact": "차용금 변제 명목 4억 원 지급 (채무 미특정)", + "derivation_basis": "source_bo_id=bh9; structure_ids=LES-050", + "legal_calculation_object": { + "calculation_required": true, + "amount_source": "BO.amount", + "amount_text": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}", + "object_basis": "[]", + "candidate_structure_types": [ + "money_claim" + ] + } + } + ], + "exception_pack": { + "schema_version": "stage1_fact_ledger_exception_pack.v1", + "status": "READY", + "has_exceptions": true, + "exception_count": 42, + "exceptions": [ + { + "exception_id": "FX-001", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-002", + "source_bo_id": "bh10", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh10", + "bo_type": "event", + "action": "오국한에 대한 대여금 채권 등을 자동채권으로 한 상계 항변", + "action_type": "소송행위(litigation acts)", + "juristic_act": { + "label": "상계" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "상계", + "항변" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-003", + "E-010" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": "대여금", + "principal_amount_hint": "50,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": "오민한", + "debtor_candidate": null + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-002", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh10", + "source_bo_ids": [ + "bh10" + ], + "issue_cluster_ids": [ + "IC-002" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:013" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-002" + ], + "asset": [], + "evidence": [ + "E-003", + "E-010" + ], + "candidate_structure_type": [ + "money_claim" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:013\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh10\"], \"related_evidence_indexes\": [\"E-003\", \"E-010\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-003", + "title": "각서 및 금전소비대차계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + { + "evidence_index": "E-010", + "title": "내용증명에 대한 답신", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-002", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-003", + "source_bo_id": "bh11", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh11", + "bo_type": "event", + "action": "물품대금 지급 최고", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "최고" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "이행최고", + "통지", + "최고" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-027" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": null, + "debtor_candidate": null + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-003", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh11", + "source_bo_ids": [ + "bh11" + ], + "issue_cluster_ids": [ + "IC-003" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-027" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:003" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-003" + ], + "asset": [], + "evidence": [ + "E-027" + ], + "candidate_structure_type": [ + "money_claim" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-027" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:003\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh11\"], \"related_evidence_indexes\": [\"E-027\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-027", + "title": "최고 통지서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-003", + "exception_type": "weak_derived_fact", + "fact_id": "F-003", + "source_bo_id": "bh11", + "severity": "review", + "trigger_reason": "derived_fact_too_weak", + "candidate_fact_row_subset": { + "derived_fact": "물품대금 지급 최고", + "derivation_basis": "source_bo_id=bh11; evidence_refs=E-027; structure_ids=LES-003", + "skeleton_validation_codes": [] + }, + "related_bo_compact": { + "bo_id": "bh11", + "bo_type": "event", + "action": "물품대금 지급 최고", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "최고" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "이행최고", + "통지", + "최고" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-027" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": null, + "debtor_candidate": null + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-003", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh11", + "source_bo_ids": [ + "bh11" + ], + "issue_cluster_ids": [ + "IC-003" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-027" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:003" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-003" + ], + "asset": [], + "evidence": [ + "E-027" + ], + "candidate_structure_type": [ + "money_claim" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-027" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:003\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh11\"], \"related_evidence_indexes\": [\"E-027\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-027", + "title": "최고 통지서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "derived_fact", + "derivation_basis", + "skeleton_validation_codes" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "WEAK_DERIVED_FACT" + ] + }, + { + "exception_id": "FX-004", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-004", + "source_bo_id": "bh12", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "claim_chain_ref", + "linked_structures", + "legal_effect_roles" + ], + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh12", + "bo_type": "event", + "action": "상호속용에 따른 물품대금 채무 책임 부인 통지", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지", + "책임부인" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-028" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": null, + "debtor_candidate": null + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-004", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh12", + "source_bo_ids": [ + "bh12" + ], + "issue_cluster_ids": [ + "IC-004" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-028" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:005" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-004" + ], + "asset": [], + "evidence": [ + "E-028" + ], + "candidate_structure_type": [ + "money_claim" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-028" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:005\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh12\"], \"related_evidence_indexes\": [\"E-028\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-028", + "title": "통지서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-005", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-006", + "source_bo_id": "bh14", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh14", + "bo_type": "event", + "action": "강용원은 성수동 대지에 대한 상속 및 증여로 지분을 취득하고, 오민한에게 차용금 담보를 위해 성수동 대지 지분에 대한 근저당권 설정 약정을 체결함", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "(근)저당권 설정 계약" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "상속", + "증여", + "근저당권설정", + "secured_debt_creation", + "(근)저당권 설정 계약" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "domain_payload_compact": { + "fractional_effect_events": [ + { + "fraction_ref": null, + "fraction_display": "3/5", + "fraction_numerator": null, + "fraction_denominator": null, + "effect_kind_hint": "transfer", + "source_event_candidate_ids": [ + "EVT-013-02" + ], + "registry_refs": [ + "REG-013-02" + ], + "evidence_indexes": [ + "E-013" + ] + } + ] + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-006", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh14", + "source_bo_ids": [ + "bh14" + ], + "issue_cluster_ids": [ + "IC-006" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:001" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-006" + ], + "asset": [ + "AC-001" + ], + "evidence": [ + "E-013", + "E-025", + "E-026" + ], + "candidate_structure_type": [ + "secured_debt" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"secured_debt\", \"liability_group_id\": \"B2:001\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh14\"], \"related_evidence_indexes\": [\"E-013\", \"E-025\", \"E-026\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-013", + "title": "등기사항전부증명서(말소사항 포함)-토지", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + { + "evidence_index": "E-025", + "title": "차용증", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + { + "evidence_index": "E-026", + "title": "차용증", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-006", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-007", + "source_bo_id": "bh15", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh15", + "bo_type": "event", + "action": "신림동 상가에 강용원(등기상 강용일) 명의의 근저당권이 설정되었다가 무단 말소되었으며, 오민한은 상계를 주장함", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "근저당권 말소 및 상계" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "근저당권설정", + "말소", + "상계", + "secured_debt_chain", + "mortgage_extinguishment", + "근저당권 말소 및 상계" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-010", + "E-014" + ], + "domain_payload_compact": {} + }, + "related_structure_snippets": [ + { + "structure_id": "LES-007", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh15", + "source_bo_ids": [ + "bh15" + ], + "issue_cluster_ids": [ + "IC-007" + ], + "asset_cluster_ids": [ + "AC-002" + ], + "evidence_indexes": [ + "E-010", + "E-014" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:004" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-007" + ], + "asset": [ + "AC-002" + ], + "evidence": [ + "E-010", + "E-014" + ], + "candidate_structure_type": [ + "secured_debt" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-010", + "E-014" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"registry_invalidity\", \"liability_group_id\": \"B2:004\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh15\"], \"related_evidence_indexes\": [\"E-010\", \"E-014\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-010", + "title": "내용증명에 대한 답신", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + { + "evidence_index": "E-014", + "title": "등기사항전부증명서 (말소사항 포함) -건물", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-007", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-008", + "source_bo_id": "bh16", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh16", + "bo_type": "event", + "action": "평택시 서정빌라에 대한 보존등기 및 매매로 인한 소유권이전등기가 마쳐짐", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "소유권이전등기" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "소유권보존", + "소유권이전", + "가압류", + "소유권이전등기" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-015" + ], + "domain_payload_compact": {} + }, + "related_structure_snippets": [ + { + "structure_id": "LES-008", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh16", + "source_bo_ids": [ + "bh16" + ], + "issue_cluster_ids": [ + "IC-008" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_indexes": [ + "E-015" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:006" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-008" + ], + "asset": [ + "AC-003" + ], + "evidence": [ + "E-015" + ], + "candidate_structure_type": [ + "secured_debt" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-015" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"secured_debt\", \"liability_group_id\": \"B2:006\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh16\"], \"related_evidence_indexes\": [\"E-015\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-015", + "title": "등기사항전부증명서(말소사항 포함)-집합건물", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-008", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-009", + "source_bo_id": "bh17", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh17", + "bo_type": "event", + "action": "오민한이 경매를 신청하여 이문호가 매각대금을 완납하고 취득한 뒤, 대한은행이 근저당권을 설정함에 따라 원인무효 여부 및 공신력이 다투어짐", + "action_type": "소송행위(litigation acts)", + "juristic_act": { + "label": "담보권실행을 위한 경매 및 근저당권 설정" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "임의경매", + "공신력", + "등기무효", + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "담보권실행을 위한 경매 및 근저당권 설정" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-012", + "E-013" + ], + "domain_payload_compact": {} + }, + "related_structure_snippets": [ + { + "structure_id": "LES-009", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh17", + "source_bo_ids": [ + "bh17" + ], + "issue_cluster_ids": [ + "IC-009" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_indexes": [ + "E-012", + "E-013" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:003" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-009" + ], + "asset": [ + "AC-001" + ], + "evidence": [ + "E-012", + "E-013" + ], + "candidate_structure_type": [ + "secured_debt" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-012", + "E-013" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"registry_invalidity\", \"liability_group_id\": \"B2:003\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh17\"], \"related_evidence_indexes\": [\"E-012\", \"E-013\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-012", + "title": "등기말소 신청에 대한 답신", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + { + "evidence_index": "E-013", + "title": "등기사항전부증명서(말소사항 포함)-토지", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-009", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-010", + "source_bo_id": "bh18", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh18", + "bo_type": "event", + "action": "강용원이 오민한에게 4억 원을 지급하였으나 다수의 채무 중 충당할 채무를 특정하지 않음", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "변제 및 변제충당" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "변제", + "변제충당", + "payment_allocation_uncertain", + "변제 및 변제충당" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [], + "domain_payload_compact": {} + }, + "related_structure_snippets": [ + { + "structure_id": "LES-010", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh18", + "source_bo_ids": [ + "bh18" + ], + "issue_cluster_ids": [ + "IC-010" + ], + "asset_cluster_ids": [], + "evidence_indexes": [], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:002" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-010" + ], + "asset": [], + "evidence": [], + "candidate_structure_type": [ + "secured_debt" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"secured_debt\", \"liability_group_id\": \"B2:002\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh18\"], \"related_evidence_indexes\": []}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-010", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-011", + "source_bo_id": "bh19", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "legal_calculation_object", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh19", + "bo_type": "event", + "action": "박광윤이 평택시 빌라를 점유하며 얻은 임대 및 거주 수익을 피담보채권에 충당하였다고 항변함", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "변제충당 주장" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "유치권", + "점유", + "변제충당", + "payment_allocation", + "변제충당 주장" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-007", + "E-011" + ], + "domain_payload_compact": {} + }, + "related_structure_snippets": [ + { + "structure_id": "LES-011", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh19", + "source_bo_ids": [ + "bh19" + ], + "issue_cluster_ids": [ + "IC-011" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_indexes": [ + "E-007", + "E-011" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:005" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "legal_calculation_object", + "claim_chain_ref" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-011" + ], + "asset": [ + "AC-003" + ], + "evidence": [ + "E-007", + "E-011" + ], + "candidate_structure_type": [ + "money_claim" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-007", + "E-011" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"money_claim\", \"liability_group_id\": \"B2:005\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh19\"], \"related_evidence_indexes\": [\"E-007\", \"E-011\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-007", + "title": "공사도급계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + { + "evidence_index": "E-011", + "title": "답변서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-011", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-013", + "source_bo_id": "bh20", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh20", + "bo_type": "event", + "action": "이수인이 윤건우에게 매매계약에서 합의되지 않은 제3자 명의로의 등기이전을 일방적으로 요구함", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "제3자 명의 등기 요청" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "소유권이전등기", + "요구", + "거절", + "registry_defect_hint", + "제3자 명의 등기 요청" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-029" + ], + "domain_payload_compact": {} + }, + "related_structure_snippets": [ + { + "structure_id": "LES-013", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh20", + "source_bo_ids": [ + "bh20" + ], + "issue_cluster_ids": [ + "IC-013" + ], + "asset_cluster_ids": [ + "AC-004" + ], + "evidence_indexes": [ + "E-029" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:007" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-013" + ], + "asset": [ + "AC-004" + ], + "evidence": [ + "E-029" + ], + "candidate_structure_type": [ + "secured_debt" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-029" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"secured_debt\", \"liability_group_id\": \"B2:007\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh20\"], \"related_evidence_indexes\": [\"E-029\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-029", + "title": "통지서_이수인_윤건우", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-012", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-014", + "source_bo_id": "bh21", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "land_use_gain", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "land_use_gain", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh21", + "bo_type": "event", + "action": "성수동 대지 차임 시세 감정평가", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "가치평가", + "임대료", + "차임", + "valuation" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-005" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": null, + "possession_events": [], + "legal_cause_assertions": [] + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-014", + "candidate_structure_type": "land_use_gain", + "structure_label": "land use gain structure for bh21", + "source_bo_ids": [ + "bh21" + ], + "issue_cluster_ids": [ + "IC-014" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006" + ], + "evidence_indexes": [ + "E-005" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:002" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-014" + ], + "asset": [ + "AC-005", + "AC-006" + ], + "evidence": [ + "E-005" + ], + "candidate_structure_type": [ + "land_use_gain" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-005" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"land_use_gain\", \"liability_group_id\": \"B3:002\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh21\"], \"related_evidence_indexes\": [\"E-005\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-005", + "title": "감정평가 의견서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-013", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-015", + "source_bo_id": "bh22", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_structures", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "land_use_gain", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "land_use_gain", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh22", + "bo_type": "state", + "action": "성수동 대지 및 건물 인도, 점유 및 사용", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "인도", + "점유", + "사용", + "possession" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-006" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": "SCH-016-01", + "possession_events": [ + { + "possessor_candidates": [ + "박성희" + ], + "start_date": "2024-11-20", + "end_date": null, + "source_event_candidate_ids": [ + "EVT-006-07" + ], + "evidence_indexes": [ + "E-006" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "임대차", + "asserted_by": "박성희", + "assertion_date": "2024-11-20", + "source_event_candidate_ids": [ + "EVT-006-03" + ], + "evidence_indexes": [ + "E-006" + ] + } + ] + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-015", + "candidate_structure_type": "land_use_gain", + "structure_label": "land use gain structure for bh22", + "source_bo_ids": [ + "bh22" + ], + "issue_cluster_ids": [ + "IC-015" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-007", + "AC-008" + ], + "evidence_indexes": [ + "E-006" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:004" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_structures" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-015" + ], + "asset": [ + "AC-005", + "AC-007", + "AC-008" + ], + "evidence": [ + "E-006" + ], + "candidate_structure_type": [ + "land_use_gain" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-006" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"land_use_gain\", \"liability_group_id\": \"B3:004\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh22\"], \"related_evidence_indexes\": [\"E-006\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-006", + "title": "계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-014", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-016", + "source_bo_id": "bh23", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "land_use_gain", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "land_use_gain", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh23", + "bo_type": "state", + "action": "신림로 상가 및 포천시 임야 가치평가(감정평가서)", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "가치평가", + "감정평가", + "valuation" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-004" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": null, + "possession_events": [], + "legal_cause_assertions": [] + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-016", + "candidate_structure_type": "land_use_gain", + "structure_label": "land use gain structure for bh23", + "source_bo_ids": [ + "bh23" + ], + "issue_cluster_ids": [ + "IC-016" + ], + "asset_cluster_ids": [ + "AC-009", + "AC-010" + ], + "evidence_indexes": [ + "E-004" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:001" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-016" + ], + "asset": [ + "AC-009", + "AC-010" + ], + "evidence": [ + "E-004" + ], + "candidate_structure_type": [ + "land_use_gain" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-004" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"land_use_gain\", \"liability_group_id\": \"B3:001\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh23\"], \"related_evidence_indexes\": [\"E-004\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-004", + "title": "감정평가서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-015", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-017", + "source_bo_id": "bh24", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "legal_calculation_object", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh24", + "bo_type": "state", + "action": "평택시 서정빌라 101호 점유 및 유치권 행사", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "점유", + "거주", + "유치권", + "수익", + "possession", + "defense_assertion_benefit_applied_to_secured_claim" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-011" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": "SCH-016-04", + "possession_events": [ + { + "possessor_candidates": [ + "박광윤" + ], + "start_date": "2022-02-01", + "end_date": "2024-10-31", + "source_event_candidate_ids": [ + "EVT-011-02" + ], + "evidence_indexes": [ + "E-011" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "유치권행사중", + "asserted_by": "박광윤", + "assertion_date": "2024-10-31", + "source_event_candidate_ids": [ + "EVT-011-03" + ], + "evidence_indexes": [ + "E-011" + ] + } + ] + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-017", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh24", + "source_bo_ids": [ + "bh24" + ], + "issue_cluster_ids": [ + "IC-017" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-012" + ], + "evidence_indexes": [ + "E-011" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:005" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "legal_calculation_object" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-017" + ], + "asset": [ + "AC-011", + "AC-012" + ], + "evidence": [ + "E-011" + ], + "candidate_structure_type": [ + "secured_debt" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"secured_debt\", \"liability_group_id\": \"B3:005\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh24\"], \"related_evidence_indexes\": [\"E-011\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-011", + "title": "답변서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-016", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-018", + "source_bo_id": "bh25", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "land_use_gain", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "land_use_gain", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh25", + "bo_type": "state", + "action": "평택시 서정빌라 차임 시세 평가", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "가치평가", + "차임", + "valuation" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-023" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": null, + "possession_events": [], + "legal_cause_assertions": [] + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-018", + "candidate_structure_type": "land_use_gain", + "structure_label": "land use gain structure for bh25", + "source_bo_ids": [ + "bh25" + ], + "issue_cluster_ids": [ + "IC-018" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-013" + ], + "evidence_indexes": [ + "E-023" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:003" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-018" + ], + "asset": [ + "AC-011", + "AC-013" + ], + "evidence": [ + "E-023" + ], + "candidate_structure_type": [ + "land_use_gain" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-023" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"land_use_gain\", \"liability_group_id\": \"B3:003\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh25\"], \"related_evidence_indexes\": [\"E-023\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-023", + "title": "임료시세확인서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-017", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-019", + "source_bo_id": "bh26", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_structures", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "land_use_gain", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "land_use_gain", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh26", + "bo_type": "state", + "action": "흑석동 상가 점유 및 운영 (맛나식당)", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "인도", + "점유", + "운영", + "사업자등록", + "possession" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": "SCH-016-05", + "possession_events": [ + { + "possessor_candidates": [ + "양정숙" + ], + "start_date": "2024-02-01", + "end_date": null, + "source_event_candidate_ids": [ + "EVT-022-03", + "EVT-018-01" + ], + "evidence_indexes": [ + "E-018", + "E-022" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "임대차", + "asserted_by": "양정숙", + "assertion_date": "2024-02-01", + "source_event_candidate_ids": [ + "EVT-022-03" + ], + "evidence_indexes": [ + "E-022" + ] + } + ] + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-019", + "candidate_structure_type": "land_use_gain", + "structure_label": "land use gain structure for bh26", + "source_bo_ids": [ + "bh26" + ], + "issue_cluster_ids": [ + "IC-019" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:006" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_structures" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-019" + ], + "asset": [ + "AC-014", + "AC-015" + ], + "evidence": [ + "E-017", + "E-018", + "E-022" + ], + "candidate_structure_type": [ + "land_use_gain" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"land_use_gain\", \"liability_group_id\": \"B3:006\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh26\"], \"related_evidence_indexes\": [\"E-017\", \"E-018\", \"E-022\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-017", + "title": "부동산 매매계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + { + "evidence_index": "E-018", + "title": "사업자등록증", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + { + "evidence_index": "E-022", + "title": "임대차계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-018", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-020", + "source_bo_id": "bh27", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "actio_roles", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "actio", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "actio", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh27", + "bo_type": "event", + "action": "채무자 오국한이 수익자 오민한에게 책임재산인 신림동 상가를 1억5천만원에 매도하여 소유권이전등기 경료", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "매매" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "사해행위", + "매매계약", + "소유권이전", + "책임재산감소", + "fraudulent_act", + "매매" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-014" + ], + "domain_payload_compact": {} + }, + "related_structure_snippets": [ + { + "structure_id": "LES-020", + "candidate_structure_type": "fraudulent_transfer", + "structure_label": "fraudulent transfer structure for bh27", + "source_bo_ids": [ + "bh27" + ], + "issue_cluster_ids": [ + "IC-020" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_indexes": [ + "E-014" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B4:002" + ], + "linked_actio_signal_ids": [ + "actio-signal-002" + ], + "legal_effect_roles": [ + "actio_roles", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-020" + ], + "asset": [ + "AC-016", + "AC-002" + ], + "evidence": [ + "E-014" + ], + "candidate_structure_type": [ + "fraudulent_transfer" + ] + }, + "related_signal_hints": { + "actio": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-002", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "beneficiary", + "value_candidate": "{}", + "date_candidate": null, + "related_bo_ids": [ + "bh27" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh27\"], \"related_evidence_indexes\": [\"E-014\"], \"role\": \"beneficiary\", \"value_candidate\": \"{}\"}], \"actio_scope\": [\"fraudulent_act\", \"beneficiary_or_transferee\", \"target_asset\"], \"preserved_claim_candidates\": [{\"bo_id\": \"bh27\", \"claim_date_candidate\": null, \"claim_type_candidate\": null, \"creditor_candidate\": null, \"debtor_candidate\": null, \"related_evidence_indexes\": [\"E-014\"]}], \"related_bo_ids\": [\"bh27\"], \"related_evidence_indexe" + } + ], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"actio_related\", \"liability_group_id\": \"B4:002\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh27\"], \"related_evidence_indexes\": [\"E-014\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-014", + "title": "등기사항전부증명서 (말소사항 포함) -건물", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-019", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-021", + "source_bo_id": "bh28", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "actio_roles" + ], + "downstream_module_candidates": [ + "money_claim", + "actio", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "money_claim", + "actio", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh28", + "bo_type": "event", + "action": "강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "소비대차" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "피보전채권", + "대여금채권", + "preserved_claim", + "소비대차" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-024" + ], + "domain_payload_compact": {} + }, + "related_structure_snippets": [ + { + "structure_id": "LES-021", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh28", + "source_bo_ids": [ + "bh28" + ], + "issue_cluster_ids": [ + "IC-021" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-024" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B4:001" + ], + "linked_actio_signal_ids": [ + "actio-signal-001" + ], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "actio_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-021" + ], + "asset": [], + "evidence": [ + "E-024" + ], + "candidate_structure_type": [ + "money_claim" + ] + }, + "related_signal_hints": { + "actio": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-001", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-024" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "debtor_act", + "value_candidate": "강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생", + "date_candidate": null, + "related_bo_ids": [ + "bh28" + ], + "related_evidence_indexes": [ + "E-024" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh28\"], \"related_evidence_indexes\": [\"E-024\"], \"role\": \"debtor_act\", \"value_candidate\": \"강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생\"}], \"actio_scope\": [\"preserved_claim\"], \"preserved_claim_candidates\": [{\"bo_id\": null, \"claim_date_candidate\": null, \"claim_type_candidate\": null, \"creditor_candidate\": null, \"debtor_candidate\": null, \"related_evidence_indexes\": [\"E-024\"]}], \"related_bo_ids\": [\"bh28\"], \"related_evidence_indexes\": [\"E" + } + ], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-024" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"actio_related\", \"liability_group_id\": \"B4:001\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh28\"], \"related_evidence_indexes\": [\"E-024\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-024", + "title": "차용증", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-020", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-022", + "source_bo_id": "bh29", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_actio_structures", + "linked_structures", + "actio_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "secured_property", + "actio", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "secured_property", + "actio", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh29", + "bo_type": "event", + "action": "수익자 오민한이 사해행위 이후 피담보채무 1억3천만원을 대위변제하고 2번 근저당권을 말소시킴으로써 원물반환이 불가능해져 가액배상 요건 성립", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "변제 및 말소" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "원물반환불능", + "가액배상", + "근저당권말소", + "value_compensation", + "remedy_cap", + "변제 및 말소" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-014" + ], + "domain_payload_compact": {} + }, + "related_structure_snippets": [ + { + "structure_id": "LES-022", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh29", + "source_bo_ids": [ + "bh29" + ], + "issue_cluster_ids": [ + "IC-022" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-002" + ], + "evidence_indexes": [ + "E-014" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B4:004" + ], + "linked_actio_signal_ids": [ + "actio-signal-004" + ], + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_actio_structures", + "linked_structures", + "actio_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-022" + ], + "asset": [ + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-002" + ], + "evidence": [ + "E-014" + ], + "candidate_structure_type": [ + "secured_debt" + ] + }, + "related_signal_hints": { + "actio": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-004", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "remedy_or_cap", + "value_candidate": "원물반환 불가", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "remedy_or_cap", + "value_candidate": "가액배상", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2021-08-01', 'event': '나임오 1번 근저당 설정(5000만원)'}", + "date_candidate": "2021-08-01", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2022-04-18', 'event': '강용일 2번 근저당 설정(1.5억원)'}", + "date_candidate": "2022-04-18", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2024-10-17', 'event': '강용일 2번 근저당 말소(오민한 대위변제)'}", + "date_candidate": "2024-10-17", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh29\"], \"related_evidence_indexes\": [\"E-014\"], \"role\": \"remedy_or_cap\", \"value_candidate\": \"원물반환 불가\"}, {\"date_candidate\": null, \"related_bo_ids\": [\"bh29\"], \"related_evidence_indexes\": [\"E-014\"], \"role\": \"remedy_or_cap\", \"value_candidate\": \"가액배상\"}, {\"date_candidate\": \"2021-08-01\", \"related_bo_ids\": [\"bh29\"], \"related_evidence_indexes\": [\"E-014\"], \"role\": \"encumbrance\", \"value_candidate\": \"{'date': '2021-08-01', 'event': '나임오 1번 근저" + } + ], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"actio_related\", \"liability_group_id\": \"B4:004\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh29\"], \"related_evidence_indexes\": [\"E-014\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-014", + "title": "등기사항전부증명서 (말소사항 포함) -건물", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-021", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-024", + "source_bo_id": "bh30", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "legal_effect_roles", + "actio_roles", + "legal_calculation_object", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "valuation", + "actio", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "valuation", + "actio", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh30", + "bo_type": "state", + "action": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "무자력", + "책임재산", + "가치평가", + "insolvency_check" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-004" + ], + "domain_payload_compact": {} + }, + "related_structure_snippets": [ + { + "structure_id": "LES-024", + "candidate_structure_type": "valuation", + "structure_label": "valuation structure for bh30", + "source_bo_ids": [ + "bh30" + ], + "issue_cluster_ids": [ + "IC-024" + ], + "asset_cluster_ids": [ + "AC-020", + "AC-021" + ], + "evidence_indexes": [ + "E-004" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B4:003" + ], + "linked_actio_signal_ids": [ + "actio-signal-003" + ], + "legal_effect_roles": [ + "legal_effect_roles", + "actio_roles", + "legal_calculation_object" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-024" + ], + "asset": [ + "AC-020", + "AC-021" + ], + "evidence": [ + "E-004" + ], + "candidate_structure_type": [ + "valuation" + ] + }, + "related_signal_hints": { + "actio": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-003", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-004" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "debtor_act", + "value_candidate": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "date_candidate": null, + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh30\"], \"related_evidence_indexes\": [\"E-004\"], \"role\": \"debtor_act\", \"value_candidate\": \"사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)\"}], \"actio_scope\": [\"target_asset\"], \"preserved_claim_candidates\": [{\"bo_id\": \"bh30\", \"claim_date_candidate\": null, \"claim_type_candidate\": null, \"creditor_candidate\": null, \"debtor_candidate\": null, \"related_evidence_indexes\": [\"E-004\"]}], \"related_bo_ids\": [\"bh30\"], \"related_evidence_i" + } + ], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-004" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"actio_related\", \"liability_group_id\": \"B4:003\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh30\"], \"related_evidence_indexes\": [\"E-004\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-004", + "title": "감정평가서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-022", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-025", + "source_bo_id": "bh31", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "legal_effect_roles", + "actio_roles", + "linked_structures", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "actio", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "actio", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh31", + "bo_type": "state", + "action": "수익자 오민한의 항변 (명의신탁 주장, 매수자금 및 대여금 참작 주장, 채권 상계 주장)", + "action_type": "소송행위(litigation acts)", + "juristic_act": { + "label": "항변" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "수익자항변", + "명의신탁", + "상계항변", + "선의항변", + "defense_assertion", + "항변" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-003", + "E-010" + ], + "domain_payload_compact": {} + }, + "related_structure_snippets": [ + { + "structure_id": "LES-025", + "candidate_structure_type": "fraudulent_transfer", + "structure_label": "fraudulent transfer structure for bh31", + "source_bo_ids": [ + "bh31" + ], + "issue_cluster_ids": [ + "IC-025" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B4:005" + ], + "linked_actio_signal_ids": [ + "actio-signal-005" + ], + "legal_effect_roles": [ + "legal_effect_roles", + "actio_roles", + "linked_structures" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-025" + ], + "asset": [ + "AC-016", + "AC-002" + ], + "evidence": [ + "E-003", + "E-010" + ], + "candidate_structure_type": [ + "fraudulent_transfer" + ] + }, + "related_signal_hints": { + "actio": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-005", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "defense", + "value_candidate": "{'defense_type': '계약명의신탁', 'content': '오국한 명의 취득이 수익자 자금에 의한 계약명의신탁이므로 사해행위가 아니라는 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "role": "defense", + "value_candidate": "{'defense_type': '배상금감액', 'content': '수익자가 제공한 매수자금 4000만원 및 대여금 1000만원 참작 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "role": "defense", + "value_candidate": "{'defense_type': '상계항변', 'content': '오민한의 5000만원 채권으로 강용원 채권을 상계한다는 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh31\"], \"related_evidence_indexes\": [\"E-003\", \"E-010\"], \"role\": \"defense\", \"value_candidate\": \"{'defense_type': '계약명의신탁', 'content': '오국한 명의 취득이 수익자 자금에 의한 계약명의신탁이므로 사해행위가 아니라는 주장'}\"}, {\"date_candidate\": null, \"related_bo_ids\": [\"bh31\"], \"related_evidence_indexes\": [\"E-003\", \"E-010\"], \"role\": \"defense\", \"value_candidate\": \"{'defense_type': '배상금감액', 'content': '수익자가 제공한 매수자금 4000만원 및 대여금 1000만원 참작 주장'}\"}, {\"date_candidate\": null, " + } + ], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"actio_related\", \"liability_group_id\": \"B4:005\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh31\"], \"related_evidence_indexes\": [\"E-003\", \"E-010\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-003", + "title": "각서 및 금전소비대차계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + { + "evidence_index": "E-010", + "title": "내용증명에 대한 답신", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-023", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-026", + "source_bo_id": "bh32", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh32", + "bo_type": "event", + "action": "강석우 사망으로 인한 정유심, 강용원의 성수동 대지 지분 상속", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": "상속개시" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "상속개시" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-013" + ], + "domain_payload_compact": {} + }, + "related_structure_snippets": [ + { + "structure_id": "LES-026", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh32", + "source_bo_ids": [ + "bh32" + ], + "issue_cluster_ids": [ + "IC-026" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-013" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:002" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-026" + ], + "asset": [], + "evidence": [ + "E-013" + ], + "candidate_structure_type": [ + "succession_notice_lien_asset_defense" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-013" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:002\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh32\"], \"related_evidence_indexes\": [\"E-013\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-013", + "title": "등기사항전부증명서(말소사항 포함)-토지", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-024", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-027", + "source_bo_id": "bh33", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh33", + "bo_type": "event", + "action": "망 강호연의 사망에 따른 상속개시 및 자녀 강형수, 강지수의 상속포기", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": "상속포기" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "상속포기" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-001", + "E-002" + ], + "domain_payload_compact": {} + }, + "related_structure_snippets": [ + { + "structure_id": "LES-027", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh33", + "source_bo_ids": [ + "bh33" + ], + "issue_cluster_ids": [ + "IC-027" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-001", + "E-002" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:003" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-027" + ], + "asset": [], + "evidence": [ + "E-001", + "E-002" + ], + "candidate_structure_type": [ + "succession_notice_lien_asset_defense" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-001", + "E-002" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:003\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh33\"], \"related_evidence_indexes\": [\"E-001\", \"E-002\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-001", + "title": "가정법원심판_강호연", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + { + "evidence_index": "E-002", + "title": "가족관계증명서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-025", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-028", + "source_bo_id": "bh34", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh34", + "bo_type": "event", + "action": "강용원이 김선웅에게 물품대금 지급을 최고하는 통지서 발송 및 도달", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "최고" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "최고" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-027" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-027-NT-01", + "notice_type": "최고", + "drafted_date": null, + "dispatch_date": "2024-05-04", + "arrival_date": "2024-05-06", + "received_date": "2024-05-06", + "legal_effect_date_candidate": "2024-05-06", + "requires_receipt_for_effect": null, + "sender_candidates": [ + "강용원" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "김선웅" + ], + "receipt_evidence_refs": [ + "우편물 배달 증명서" + ], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-028", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh34", + "source_bo_ids": [ + "bh34" + ], + "issue_cluster_ids": [ + "IC-028" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-027" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:009" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-028" + ], + "asset": [], + "evidence": [ + "E-027" + ], + "candidate_structure_type": [ + "succession_notice_lien_asset_defense" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-027" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:009\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh34\"], \"related_evidence_indexes\": [\"E-027\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-027", + "title": "최고 통지서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-026", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-029", + "source_bo_id": "bh35", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "actio_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "actio", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "actio", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh35", + "bo_type": "event", + "action": "강용원이 오민한에게 사해행위취소 및 신림동 상가 소유권 반환을 요구하는 내용증명 발송", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "최고" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "최고" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-008", + "E-010" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-008-NT-01", + "notice_type": "기타", + "drafted_date": "2024-09-15", + "dispatch_date": "2024-09-15", + "arrival_date": "2024-09-20", + "received_date": "2024-09-20", + "legal_effect_date_candidate": "2024-09-20", + "requires_receipt_for_effect": null, + "sender_candidates": [ + "강용원" + ], + "legal_sender_after_resolution": { + "status": "unresolved", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "오민한" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [ + "오민한이 이행최고서를 2024. 9. 20. 수령함" + ] + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-029", + "candidate_structure_type": "fraudulent_transfer", + "structure_label": "fraudulent transfer structure for bh35", + "source_bo_ids": [ + "bh35" + ], + "issue_cluster_ids": [ + "IC-029" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-008", + "E-010" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:004" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "actio_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-029" + ], + "asset": [], + "evidence": [ + "E-008", + "E-010" + ], + "candidate_structure_type": [ + "fraudulent_transfer" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-008", + "E-010" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"actio_related\", \"liability_group_id\": \"B5:004\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh35\"], \"related_evidence_indexes\": [\"E-008\", \"E-010\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-008", + "title": "내용증명", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + { + "evidence_index": "E-010", + "title": "내용증명에 대한 답신", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-027", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-030", + "source_bo_id": "bh36", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh36", + "bo_type": "event", + "action": "강용원과 양정숙이 박광윤에게 평택시 빌라 유치권소멸을 청구하는 통지서 발송 및 도달", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-011", + "E-019" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-019-NT-01", + "notice_type": "유치권소멸청구통지", + "drafted_date": "2024-08-29", + "dispatch_date": "2024-08-29", + "arrival_date": "2024-09-01", + "received_date": "2024-09-01", + "legal_effect_date_candidate": "2024-09-01", + "requires_receipt_for_effect": true, + "sender_candidates": [ + "강용원", + "양정숙" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "박광윤" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-030", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh36", + "source_bo_ids": [ + "bh36" + ], + "issue_cluster_ids": [ + "IC-030" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-011", + "E-019" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:007" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-030" + ], + "asset": [], + "evidence": [ + "E-011", + "E-019" + ], + "candidate_structure_type": [ + "secured_debt" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011", + "E-019" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"secured_debt\", \"liability_group_id\": \"B5:007\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh36\"], \"related_evidence_indexes\": [\"E-011\", \"E-019\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-011", + "title": "답변서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + { + "evidence_index": "E-019", + "title": "소멸청구 통지서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json" + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-028", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-031", + "source_bo_id": "bh37", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh37", + "bo_type": "event", + "action": "오민한이 강용원에게 김선웅의 채무에 대한 책임을 부인하는 통지서 발송", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-028" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-028-NT-01", + "notice_type": "기타", + "drafted_date": null, + "dispatch_date": "2024-12-27", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [ + "오민한" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "강용원" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-031", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh37", + "source_bo_ids": [ + "bh37" + ], + "issue_cluster_ids": [ + "IC-031" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-028" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:010" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-031" + ], + "asset": [], + "evidence": [ + "E-028" + ], + "candidate_structure_type": [ + "succession_notice_lien_asset_defense" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-028" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:010\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh37\"], \"related_evidence_indexes\": [\"E-028\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-028", + "title": "통지서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-029", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-032", + "source_bo_id": "bh38", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh38", + "bo_type": "event", + "action": "오민한이 강용원에게 내용증명에 대한 답신 발송", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-010" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-010-NT-01", + "notice_type": "기타", + "drafted_date": null, + "dispatch_date": "2024-09-30", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [ + "오민한" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "강용원" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-032", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh38", + "source_bo_ids": [ + "bh38" + ], + "issue_cluster_ids": [ + "IC-032" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-010" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:006" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-032" + ], + "asset": [], + "evidence": [ + "E-010" + ], + "candidate_structure_type": [ + "succession_notice_lien_asset_defense" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-010" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:006\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh38\"], \"related_evidence_indexes\": [\"E-010\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-010", + "title": "내용증명에 대한 답신", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-030", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-033", + "source_bo_id": "bh39", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh39", + "bo_type": "event", + "action": "윤건우가 양정숙에게 흑석동 상가 임대차계약 해지 및 명도를 요구하는 내용증명 통지", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-009" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-009-NT-01", + "notice_type": "기타", + "drafted_date": "2024-11-29", + "dispatch_date": "2024-11-29", + "arrival_date": "2024-11-30", + "received_date": "2024-11-30", + "legal_effect_date_candidate": "2024-11-30", + "requires_receipt_for_effect": true, + "sender_candidates": [ + "윤건우" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "양정숙" + ], + "receipt_evidence_refs": [ + "우편물배달증명서" + ], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-033", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh39", + "source_bo_ids": [ + "bh39" + ], + "issue_cluster_ids": [ + "IC-033" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-009" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:005" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-033" + ], + "asset": [], + "evidence": [ + "E-009" + ], + "candidate_structure_type": [ + "succession_notice_lien_asset_defense" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-009" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:005\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh39\"], \"related_evidence_indexes\": [\"E-009\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-009", + "title": "내용증명", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-031", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-035", + "source_bo_id": "bh40", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh40", + "bo_type": "event", + "action": "윤건우가 이수인에게 흑석동 상가 매매계약 해제 통지서 발송 및 도달", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-030" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-030-NT-01", + "notice_type": "해제통지", + "drafted_date": null, + "dispatch_date": "2024-05-15", + "arrival_date": "2024-05-16", + "received_date": "2024-05-16", + "legal_effect_date_candidate": "2024-05-16", + "requires_receipt_for_effect": true, + "sender_candidates": [ + "윤건우" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "이수인" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-035", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh40", + "source_bo_ids": [ + "bh40" + ], + "issue_cluster_ids": [ + "IC-035" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-030" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:012" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-035" + ], + "asset": [], + "evidence": [ + "E-030" + ], + "candidate_structure_type": [ + "succession_notice_lien_asset_defense" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-030" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:012\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh40\"], \"related_evidence_indexes\": [\"E-030\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-030", + "title": "해제 통지서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-032", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-036", + "source_bo_id": "bh41", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh41", + "bo_type": "event", + "action": "이수인이 윤건우에게 흑석동 상가 매매대금 감액 및 제3자 명의 등기 이전 요청 통지", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-029" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-029-NT-01", + "notice_type": "기타", + "drafted_date": null, + "dispatch_date": "2024-03-30", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [ + "이수인" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "윤건우" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-036", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh41", + "source_bo_ids": [ + "bh41" + ], + "issue_cluster_ids": [ + "IC-036" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-029" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:011" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-036" + ], + "asset": [], + "evidence": [ + "E-029" + ], + "candidate_structure_type": [ + "money_claim" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-029" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"money_claim\", \"liability_group_id\": \"B5:011\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh41\"], \"related_evidence_indexes\": [\"E-029\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-029", + "title": "통지서_이수인_윤건우", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-033", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-037", + "source_bo_id": "bh42", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "registry_invalidity", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "registry_invalidity", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh42", + "bo_type": "event", + "action": "주식회사 대한은행이 강용원의 성수동 대지 근저당권 말소 신청을 거부하는 통지 발송", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-012" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-012-NT-01", + "notice_type": "기타", + "drafted_date": "2024-12-22", + "dispatch_date": "2024-12-22", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [ + "주식회사 대한은행" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "강용원" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-037", + "candidate_structure_type": "registry_invalidity", + "structure_label": "registry invalidity structure for bh42", + "source_bo_ids": [ + "bh42" + ], + "issue_cluster_ids": [ + "IC-037" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-012" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:008" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-037" + ], + "asset": [], + "evidence": [ + "E-012" + ], + "candidate_structure_type": [ + "registry_invalidity" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-012" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"registry_invalidity\", \"liability_group_id\": \"B5:008\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh42\"], \"related_evidence_indexes\": [\"E-012\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-012", + "title": "등기말소 신청에 대한 답신", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-034", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-038", + "source_bo_id": "bh43", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh43", + "bo_type": "state", + "action": "목적물 특정", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "domain_payload_compact": { + "asset_alias": [ + { + "asset_ref": "성수동 대지", + "alias_strings": [ + "성수동 대지", + "서울 성동구 성수동 256" + ], + "canonical_label_candidate": "1. 서울 성동구 성수동 256 대 330㎡", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-01" + ], + "registry_refs": [ + { + "registry_ref": "REG-013-01", + "receipt_no_refs": [ + "제1453호" + ] + }, + { + "registry_ref": "REG-013-02", + "receipt_no_refs": [ + "제7026호" + ] + }, + { + "registry_ref": "REG-013-03", + "receipt_no_refs": [ + "제11202호" + ] + }, + { + "registry_ref": "REG-013-04", + "receipt_no_refs": [ + "제7202호" + ] + }, + { + "registry_ref": "REG-013-05", + "receipt_no_refs": [ + "제12402호" + ] + }, + { + "registry_ref": "REG-013-06", + "receipt_no_refs": [ + "제25797호" + ] + }, + { + "registry_ref": "REG-013-07", + "receipt_no_refs": [ + "제25799호" + ] + } + ], + "source_evidence_indexes": [ + "E-013", + "E-016" + ] + }, + { + "asset_ref": "성수동 건물", + "alias_strings": [ + "성수동 건물", + "서울 성동구 성수동 256 상가" + ], + "canonical_label_candidate": "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-02" + ], + "registry_refs": [], + "source_evidence_indexes": [ + "E-016" + ] + }, + { + "asset_ref": "신림동 상가", + "alias_strings": [ + "신림동 상가", + "서울 관악구 신림로 115" + ], + "canonical_label_candidate": "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-03" + ], + "registry_refs": [ + { + "registry_ref": "REG-014-01", + "receipt_no_refs": [ + "제24759호" + ] + }, + { + "registry_ref": "REG-014-02", + "receipt_no_refs": [ + "제56457호" + ] + }, + { + "registry_ref": "REG-014-03", + "receipt_no_refs": [ + "제11593호" + ] + }, + { + "registry_ref": "REG-014-04", + "receipt_no_refs": [ + "제50099호" + ] + }, + { + "registry_ref": "REG-014-05", + "receipt_no_refs": [ + "제26597호" + ] + }, + { + "registry_ref": "REG-014-06", + "receipt_no_refs": [ + "제74586호" + ] + } + ], + "source_evidence_indexes": [ + "E-014", + "E-016" + ] + }, + { + "asset_ref": "평택시 빌라", + "alias_strings": [ + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123" + ], + "canonical_label_candidate": "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-04" + ], + "registry_refs": [ + { + "registry_ref": "REG-015-01", + "receipt_no_refs": [ + "제7233호" + ] + }, + { + "registry_ref": "REG-015-02", + "receipt_no_refs": [ + "제7871호" + ] + }, + { + "registry_ref": "REG-015-03", + "receipt_no_refs": [ + "제11593호" + ] + }, + { + "registry_ref": "REG-015-04", + "receipt_no_refs": [ + "제12321호" + ] + } + ], + "source_evidence_indexes": [ + "E-015", + "E-016" + ] + }, + { + "asset_ref": "흑석동 상가", + "alias_strings": [ + "흑석동 상가", + "서울 동작구 흑석로 80" + ], + "canonical_label_candidate": "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-05" + ], + "registry_refs": [], + "source_evidence_indexes": [ + "E-016" + ] + } + ] + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-038", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh43", + "source_bo_ids": [ + "bh43" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "asset_cluster_ids": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:001" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-038" + ], + "asset": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "evidence": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "candidate_structure_type": [ + "succession_notice_lien_asset_defense" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:001\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh43\"], \"related_evidence_indexes\": [\"E-013\", \"E-014\", \"E-015\", \"E-016\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-013", + "title": "등기사항전부증명서(말소사항 포함)-토지", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + { + "evidence_index": "E-014", + "title": "등기사항전부증명서 (말소사항 포함) -건물", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + { + "evidence_index": "E-015", + "title": "등기사항전부증명서(말소사항 포함)-집합건물", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + { + "evidence_index": "E-016", + "title": "별지 목록", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-035", + "exception_type": "weak_derived_fact", + "fact_id": "F-038", + "source_bo_id": "bh43", + "severity": "review", + "trigger_reason": "derived_fact_too_weak", + "candidate_fact_row_subset": { + "derived_fact": "목적물 특정", + "derivation_basis": "source_bo_id=bh43; evidence_refs=E-013,E-014,E-015,E-016; structure_ids=LES-038", + "skeleton_validation_codes": [] + }, + "related_bo_compact": { + "bo_id": "bh43", + "bo_type": "state", + "action": "목적물 특정", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "domain_payload_compact": { + "asset_alias": [ + { + "asset_ref": "성수동 대지", + "alias_strings": [ + "성수동 대지", + "서울 성동구 성수동 256" + ], + "canonical_label_candidate": "1. 서울 성동구 성수동 256 대 330㎡", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-01" + ], + "registry_refs": [ + { + "registry_ref": "REG-013-01", + "receipt_no_refs": [ + "제1453호" + ] + }, + { + "registry_ref": "REG-013-02", + "receipt_no_refs": [ + "제7026호" + ] + }, + { + "registry_ref": "REG-013-03", + "receipt_no_refs": [ + "제11202호" + ] + }, + { + "registry_ref": "REG-013-04", + "receipt_no_refs": [ + "제7202호" + ] + }, + { + "registry_ref": "REG-013-05", + "receipt_no_refs": [ + "제12402호" + ] + }, + { + "registry_ref": "REG-013-06", + "receipt_no_refs": [ + "제25797호" + ] + }, + { + "registry_ref": "REG-013-07", + "receipt_no_refs": [ + "제25799호" + ] + } + ], + "source_evidence_indexes": [ + "E-013", + "E-016" + ] + }, + { + "asset_ref": "성수동 건물", + "alias_strings": [ + "성수동 건물", + "서울 성동구 성수동 256 상가" + ], + "canonical_label_candidate": "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-02" + ], + "registry_refs": [], + "source_evidence_indexes": [ + "E-016" + ] + }, + { + "asset_ref": "신림동 상가", + "alias_strings": [ + "신림동 상가", + "서울 관악구 신림로 115" + ], + "canonical_label_candidate": "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-03" + ], + "registry_refs": [ + { + "registry_ref": "REG-014-01", + "receipt_no_refs": [ + "제24759호" + ] + }, + { + "registry_ref": "REG-014-02", + "receipt_no_refs": [ + "제56457호" + ] + }, + { + "registry_ref": "REG-014-03", + "receipt_no_refs": [ + "제11593호" + ] + }, + { + "registry_ref": "REG-014-04", + "receipt_no_refs": [ + "제50099호" + ] + }, + { + "registry_ref": "REG-014-05", + "receipt_no_refs": [ + "제26597호" + ] + }, + { + "registry_ref": "REG-014-06", + "receipt_no_refs": [ + "제74586호" + ] + } + ], + "source_evidence_indexes": [ + "E-014", + "E-016" + ] + }, + { + "asset_ref": "평택시 빌라", + "alias_strings": [ + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123" + ], + "canonical_label_candidate": "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-04" + ], + "registry_refs": [ + { + "registry_ref": "REG-015-01", + "receipt_no_refs": [ + "제7233호" + ] + }, + { + "registry_ref": "REG-015-02", + "receipt_no_refs": [ + "제7871호" + ] + }, + { + "registry_ref": "REG-015-03", + "receipt_no_refs": [ + "제11593호" + ] + }, + { + "registry_ref": "REG-015-04", + "receipt_no_refs": [ + "제12321호" + ] + } + ], + "source_evidence_indexes": [ + "E-015", + "E-016" + ] + }, + { + "asset_ref": "흑석동 상가", + "alias_strings": [ + "흑석동 상가", + "서울 동작구 흑석로 80" + ], + "canonical_label_candidate": "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-05" + ], + "registry_refs": [], + "source_evidence_indexes": [ + "E-016" + ] + } + ] + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-038", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh43", + "source_bo_ids": [ + "bh43" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "asset_cluster_ids": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:001" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-038" + ], + "asset": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "evidence": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "candidate_structure_type": [ + "succession_notice_lien_asset_defense" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:001\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh43\"], \"related_evidence_indexes\": [\"E-013\", \"E-014\", \"E-015\", \"E-016\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-013", + "title": "등기사항전부증명서(말소사항 포함)-토지", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + { + "evidence_index": "E-014", + "title": "등기사항전부증명서 (말소사항 포함) -건물", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + { + "evidence_index": "E-015", + "title": "등기사항전부증명서(말소사항 포함)-집합건물", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + { + "evidence_index": "E-016", + "title": "별지 목록", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "derived_fact", + "derivation_basis", + "skeleton_validation_codes" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "WEAK_DERIVED_FACT" + ] + }, + { + "exception_id": "FX-036", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-039", + "source_bo_id": "bh44", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "secured_property", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "secured_property", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh44", + "bo_type": "state", + "action": "박광윤의 평택시 서정빌라 101호 거주 및 유치권 행사 상태", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-011" + ], + "domain_payload_compact": { + "lien_state": { + "lien_id": "B5:LIEN:001", + "asset_ref": "평택시 빌라", + "lien_holder_candidates": [ + "박광윤" + ], + "secured_claim_hint": { + "debtor_candidate": "김일동", + "basis_text": "공사잔금 3억 원 미지급", + "claimed_remaining_amount_hint": "300,000,000원", + "evidence_indexes": [ + "E-011" + ] + }, + "possession_event_refs": [], + "assertion_events": [ + "EVT-011-02" + ], + "extinction_notice_refs": [ + "E-019-NT-01" + ], + "status_raw_hint": "유치권 행사 중" + }, + "possession_state": { + "asset_ref": "평택시 빌라", + "possession_events": [ + { + "possessor_candidates": [ + "박광윤" + ], + "start_date": null, + "end_date": null, + "source_event_candidate_ids": [ + "EVT-011-02" + ], + "evidence_indexes": [ + "E-011" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "유치권행사중", + "asserted_by": "박광윤", + "assertion_date": "2024-10-31", + "source_event_candidate_ids": [ + "EVT-011-02" + ], + "evidence_indexes": [ + "E-011" + ] + } + ] + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-039", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh44", + "source_bo_ids": [ + "bh44" + ], + "issue_cluster_ids": [ + "IC-039" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-011" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:013" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-039" + ], + "asset": [], + "evidence": [ + "E-011" + ], + "candidate_structure_type": [ + "secured_debt" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"secured_debt\", \"liability_group_id\": \"B5:013\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh44\"], \"related_evidence_indexes\": [\"E-011\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-011", + "title": "답변서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-037", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-040", + "source_bo_id": "bh45", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh45", + "bo_type": "state", + "action": "박성희의 성수동 대지 및 건물 점유", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-006" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": "성수동 대지 및 성수동 건물", + "possession_events": [ + { + "possessor_candidates": [ + "박성희" + ], + "start_date": "2024-11-20", + "end_date": null, + "source_event_candidate_ids": [ + "EVT-006-07" + ], + "evidence_indexes": [ + "E-006" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "임대차", + "asserted_by": "박성희", + "assertion_date": "2024-11-20", + "source_event_candidate_ids": [ + "EVT-006-07" + ], + "evidence_indexes": [ + "E-006" + ] + } + ] + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-040", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh45", + "source_bo_ids": [ + "bh45" + ], + "issue_cluster_ids": [ + "IC-040" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-006" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:014" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-040" + ], + "asset": [], + "evidence": [ + "E-006" + ], + "candidate_structure_type": [ + "succession_notice_lien_asset_defense" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-006" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:014\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh45\"], \"related_evidence_indexes\": [\"E-006\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-006", + "title": "계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-038", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-041", + "source_bo_id": "bh46", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh46", + "bo_type": "state", + "action": "양정숙의 흑석동 상가 점유 및 식당 운영 상태", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-018", + "E-022" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": "흑석동 상가", + "possession_events": [ + { + "possessor_candidates": [ + "양정숙" + ], + "start_date": "2024-02-01", + "end_date": null, + "source_event_candidate_ids": [ + "EVT-018-01", + "EVT-022-03" + ], + "evidence_indexes": [ + "E-018", + "E-022" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "임대차", + "asserted_by": "양정숙", + "assertion_date": "2024-02-01", + "source_event_candidate_ids": [ + "EVT-022-03" + ], + "evidence_indexes": [ + "E-022" + ] + } + ] + } + } + }, + "related_structure_snippets": [ + { + "structure_id": "LES-041", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh46", + "source_bo_ids": [ + "bh46" + ], + "issue_cluster_ids": [ + "IC-041" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-018", + "E-022" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:015" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-041" + ], + "asset": [], + "evidence": [ + "E-018", + "E-022" + ], + "candidate_structure_type": [ + "succession_notice_lien_asset_defense" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-018", + "E-022" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:015\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh46\"], \"related_evidence_indexes\": [\"E-018\", \"E-022\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-018", + "title": "사업자등록증", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + { + "evidence_index": "E-022", + "title": "임대차계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-039", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-042", + "source_bo_id": "bh47", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "money_claim", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "money_claim", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh47", + "bo_type": "state", + "action": "오민한의 강용원에 대한 자신의 5,000만 원 채권 상계 주장 항변", + "action_type": "소송행위(litigation acts)", + "juristic_act": { + "label": "상계" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "상계" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-010" + ], + "domain_payload_compact": {} + }, + "related_structure_snippets": [ + { + "structure_id": "LES-042", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh47", + "source_bo_ids": [ + "bh47" + ], + "issue_cluster_ids": [ + "IC-042" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-010" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:017" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-042" + ], + "asset": [], + "evidence": [ + "E-010" + ], + "candidate_structure_type": [ + "money_claim" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-010" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"money_claim\", \"liability_group_id\": \"B5:017\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh47\"], \"related_evidence_indexes\": [\"E-010\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-010", + "title": "내용증명에 대한 답신", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-040", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-043", + "source_bo_id": "bh48", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh48", + "bo_type": "state", + "action": "박광윤의 과거 임대료 및 거주 수익을 채무에 충당하였다는 주장 항변", + "action_type": "소송행위(litigation acts)", + "juristic_act": { + "label": "항변" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "항변" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-011" + ], + "domain_payload_compact": {} + }, + "related_structure_snippets": [ + { + "structure_id": "LES-043", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh48", + "source_bo_ids": [ + "bh48" + ], + "issue_cluster_ids": [ + "IC-043" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-011" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:018" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-043" + ], + "asset": [], + "evidence": [ + "E-011" + ], + "candidate_structure_type": [ + "succession_notice_lien_asset_defense" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:018\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh48\"], \"related_evidence_indexes\": [\"E-011\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-011", + "title": "답변서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-041", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-044", + "source_bo_id": "bh49", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "succession_notice_lien_asset_defense", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh49", + "bo_type": "state", + "action": "윤건우의 임대차 책임 부인 및 양정숙 차임 연체 주장 항변", + "action_type": "소송행위(litigation acts)", + "juristic_act": { + "label": "항변" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "항변" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-009" + ], + "domain_payload_compact": {} + }, + "related_structure_snippets": [ + { + "structure_id": "LES-044", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh49", + "source_bo_ids": [ + "bh49" + ], + "issue_cluster_ids": [ + "IC-044" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-009" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:016" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-044" + ], + "asset": [], + "evidence": [ + "E-009" + ], + "candidate_structure_type": [ + "succession_notice_lien_asset_defense" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-009" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:016\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh49\"], \"related_evidence_indexes\": [\"E-009\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-009", + "title": "내용증명", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + }, + { + "exception_id": "FX-042", + "exception_type": "ambiguous_legal_role", + "fact_id": "F-046", + "source_bo_id": "bh50", + "severity": "review", + "trigger_reason": "multiple_legal_roles_and_modules", + "candidate_fact_row_subset": { + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "downstream_module_candidates": [ + "registry_invalidity", + "claim_liability" + ], + "must_not_drop_in_claim_types": [ + "registry_invalidity", + "claim_liability" + ], + "must_consider": true + }, + "related_bo_compact": { + "bo_id": "bh50", + "bo_type": "state", + "action": "주식회사 대한은행의 경매공신력 주장(민사집행법 제267조)에 따른 근저당 말소 불가 항변", + "action_type": "소송행위(litigation acts)", + "juristic_act": { + "label": "항변" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "항변" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-012" + ], + "domain_payload_compact": {} + }, + "related_structure_snippets": [ + { + "structure_id": "LES-046", + "candidate_structure_type": "registry_invalidity", + "structure_label": "registry invalidity structure for bh50", + "source_bo_ids": [ + "bh50" + ], + "issue_cluster_ids": [ + "IC-046" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-012" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:019" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + } + ], + "related_issue_asset_evidence_index_snippets": { + "issue": [ + "IC-046" + ], + "asset": [], + "evidence": [ + "E-012" + ], + "candidate_structure_type": [ + "registry_invalidity" + ] + }, + "related_signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-012" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"registry_invalidity\", \"liability_group_id\": \"B5:019\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh50\"], \"related_evidence_indexes\": [\"E-012\"]}" + } + ], + "legal_effect": [] + }, + "related_evidence_excerpts": [ + { + "evidence_index": "E-012", + "title": "등기말소 신청에 대한 답신", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + ], + "allowed_output_fields": [ + "legal_effect_roles", + "downstream_module_candidates", + "must_not_drop_in_claim_types", + "must_consider" + ], + "allowed_decisions": [ + "PATCH", + "KEEP_AS_IS", + "BLOCK_REVIEW" + ], + "conflict_flags": [ + "MULTIPLE_ROLE_PROJECTION" + ] + } + ], + "closed_exception_type_enum": [ + "ambiguous_legal_role", + "ambiguous_claim_chain", + "weak_derived_fact", + "legal_calculation_required", + "unresolved_asset_cluster_projection", + "unresolved_structure_role_projection" + ] + }, + "compile_gate": { + "candidate_count": 50, + "exception_count": 42, + "candidate_schema_closed": true, + "exception_schema_closed": true + } + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/fact_ledger/fact_ledger_writer_report.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/fact_ledger/fact_ledger_writer_report.json new file mode 100644 index 00000000..a37960ab --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/fact_ledger/fact_ledger_writer_report.json @@ -0,0 +1,19 @@ +{ + "schema_version": "stage1_fact_ledger_writer_report.v1", + "created_at_utc": "2026-07-10T07:56:45.970926+00:00", + "blocked_review_items": [], + "patch_policy_warnings": [], + "unapplied_patch_fields": [], + "row_warnings": [], + "possession_conflict_gate": { + "status": "PASS", + "conflicts": [] + }, + "drafting_gates": [], + "gate_review_code_summary": { + "NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW": [ + "F-030", + "F-035" + ] + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/fact_ledger/fact_source_pack.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/fact_ledger/fact_source_pack.json new file mode 100644 index 00000000..f4879d7c --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/fact_ledger/fact_source_pack.json @@ -0,0 +1,19482 @@ +{ + "fact_source_pack": { + "schema_version": "stage1_fact_source_pack.v2", + "status": "READY", + "created_at_utc": "2026-07-10T07:53:37.818692+00:00", + "input_files": { + "client_goal": "client_goal.json", + "bo": "BO.json", + "legal_effect_structures": "legal_effect_structures.json", + "actio_case_signals": "actio_case_signals.json", + "case_liability_signals": "case_liability_signals.json", + "legal_effect_signals": "legal_effect_signals.json", + "evidence_indexed": "evidence_indexed.json", + "evidence_all": "evidence_all.json", + "evidence_event_candidates": "evidence_event_candidates.json" + }, + "client_goal_compact": { + "goal_type": null, + "requested_claims": [], + "parties": { + "plaintiffs": [ + { + "name": "강용원", + "type": "자연인" + }, + { + "name": "양정숙", + "type": "자연인" + } + ], + "defendants": [ + { + "name": "오민한", + "type": "자연인", + "asset_status": "사업승계인, 사해행위 수익자 후보" + }, + { + "name": "이문호", + "type": "자연인", + "asset_status": "성수동 대지 소유자" + }, + { + "name": "주식회사 대한은행", + "type": "법인", + "asset_status": "성수동 대지 근저당권자" + }, + { + "name": "박성희", + "type": "자연인", + "asset_status": "성수동 대지/건물 점유자" + }, + { + "name": "박광윤", + "type": "자연인", + "asset_status": "평택시 빌라 유치권자" + }, + { + "name": "윤건우", + "type": "자연인", + "asset_status": "흑석동 상가 매도인" + } + ], + "third_parties": [ + { + "name": "오국한", + "relationship": "채무자" + }, + { + "name": "김선웅", + "relationship": "원채무자" + } + ] + }, + "summary": "{\"claim_type_candidates\": [\"금전\", \"물건인도\", \"확인\", \"형성\"], \"constraints\": [\"오국한에 대한 금전대여금 청구 소송 제기 불가\"], \"current_control_map_4axis\": [{\"certainty\": \"final\", \"cluster_id\": \"IC-02\", \"current_possessor\": \"박성희\", \"current_right_holder_candidate\": \"이문호\", \"object\": \"성수동 대지/건물\"}, {\"certainty\": \"final\", \"cluster_id\": \"IC-04\", \"current_possessor\": \"박광윤\", \"object\": \"평택시 빌라\"}, {\"certainty\": \"final\", \"cluster_id\": \"IC-05\", \"current_possessor\": \"양정숙\", \"current_user\": \"양정숙\", \"object\": \"흑석동 상가\"}], \"issue_clusters\": [{\"candidate_defendants_or_counterparties\": [\"오민한\"], \"candidate_plaintiffs\": [\"강용원\"], \"cluster_id\": \"IC-01\", \"cluster_label\": \"인테리어 자재대금\", \"desired_relief_keywords\": [\"금전\"], \"domain_profiles\": {\"mone" + }, + "bo_by_id": { + "bh1": { + "bo_id": "bh1", + "bo_type": "event", + "action": "평택시 빌라 신축 공사도급계약 체결 및 중도금 지급", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "도급" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "도급", + "공사대금" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-007" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": "공사대금", + "principal_amount_hint": "1,000,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": "박광윤", + "debtor_candidate": "김일동" + } + } + }, + "bh2": { + "bo_id": "bh2", + "bo_type": "event", + "action": "인테리어 자재 매매에 따른 물품대금 채권 발생", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "매매" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "물품대금", + "매매" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-021", + "E-027" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": "물품대금", + "principal_amount_hint": "200,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": "강용원", + "debtor_candidate": "김선웅" + } + } + }, + "bh3": { + "bo_id": "bh3", + "bo_type": "event", + "action": "1억 원 대여금 채권 발생 (2022.4.18.)", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "소비대차" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "대여금", + "소비대차" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-024" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": "대여금", + "principal_amount_hint": "100,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": "월 1%", + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [ + "E-024" + ], + "unpaid_balance_hint": null, + "creditor_candidate": "강용원", + "debtor_candidate": "오국한" + } + } + }, + "bh4": { + "bo_id": "bh4", + "bo_type": "event", + "action": "1억 원 대여금 채권 발생 (2022.5.18.)", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "소비대차" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "대여금", + "소비대차" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-024" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": "대여금", + "principal_amount_hint": "100,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": "월 1%", + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [ + "E-024" + ], + "unpaid_balance_hint": null, + "creditor_candidate": "강용원", + "debtor_candidate": "오국한" + } + } + }, + "bh5": { + "bo_id": "bh5", + "bo_type": "event", + "action": "2억 원 대여금 채권 발생 (2023.1.6.)", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "소비대차" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "대여금", + "소비대차" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-025" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": "대여금", + "principal_amount_hint": "200,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": "오민한", + "debtor_candidate": "강용원" + } + } + }, + "bh6": { + "bo_id": "bh6", + "bo_type": "event", + "action": "3억 원 대여금 채권 발생 (2023.7.6.)", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "소비대차" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "대여금", + "소비대차" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-026" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": "대여금", + "principal_amount_hint": "300,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": "연 15%", + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [ + "E-026" + ], + "unpaid_balance_hint": null, + "creditor_candidate": "오민한", + "debtor_candidate": "강용원" + } + } + }, + "bh7": { + "bo_id": "bh7", + "bo_type": "event", + "action": "해드림 영업양도 및 상호속용 약정", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "영업양도" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "영업양도", + "상호속용" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-020" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": null, + "debtor_candidate": null + } + } + }, + "bh8": { + "bo_id": "bh8", + "bo_type": "event", + "action": "물품대금 채무 승인 및 변제기 유예", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "채무승인" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "채무승인", + "변제기 유예" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-021" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": "강용원", + "debtor_candidate": "김선웅" + } + } + }, + "bh9": { + "bo_id": "bh9", + "bo_type": "event", + "action": "차용금 변제 명목 4억 원 지급 (채무 미특정)", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": "변제" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "변제", + "법정변제충당" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": null, + "debtor_candidate": null + } + } + }, + "bh10": { + "bo_id": "bh10", + "bo_type": "event", + "action": "오국한에 대한 대여금 채권 등을 자동채권으로 한 상계 항변", + "action_type": "소송행위(litigation acts)", + "juristic_act": { + "label": "상계" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "상계", + "항변" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-003", + "E-010" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": "대여금", + "principal_amount_hint": "50,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": "오민한", + "debtor_candidate": null + } + } + }, + "bh11": { + "bo_id": "bh11", + "bo_type": "event", + "action": "물품대금 지급 최고", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "최고" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "이행최고", + "통지", + "최고" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-027" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": null, + "debtor_candidate": null + } + } + }, + "bh12": { + "bo_id": "bh12", + "bo_type": "event", + "action": "상호속용에 따른 물품대금 채무 책임 부인 통지", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지", + "책임부인" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-028" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": null, + "debtor_candidate": null + } + } + }, + "bh13": { + "bo_id": "bh13", + "bo_type": "state", + "action": "공사대금 잔금 3억 원 미지급 상태 및 유치권 충당 항변", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "미지급", + "충당" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-011" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": "공사대금", + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": "300,000,000원", + "creditor_candidate": "박광윤", + "debtor_candidate": "김일동" + } + } + }, + "bh14": { + "bo_id": "bh14", + "bo_type": "event", + "action": "강용원은 성수동 대지에 대한 상속 및 증여로 지분을 취득하고, 오민한에게 차용금 담보를 위해 성수동 대지 지분에 대한 근저당권 설정 약정을 체결함", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "(근)저당권 설정 계약" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "상속", + "증여", + "근저당권설정", + "secured_debt_creation", + "(근)저당권 설정 계약" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "domain_payload_compact": { + "fractional_effect_events": [ + { + "fraction_ref": null, + "fraction_display": "3/5", + "fraction_numerator": null, + "fraction_denominator": null, + "effect_kind_hint": "transfer", + "source_event_candidate_ids": [ + "EVT-013-02" + ], + "registry_refs": [ + "REG-013-02" + ], + "evidence_indexes": [ + "E-013" + ] + } + ] + } + }, + "bh15": { + "bo_id": "bh15", + "bo_type": "event", + "action": "신림동 상가에 강용원(등기상 강용일) 명의의 근저당권이 설정되었다가 무단 말소되었으며, 오민한은 상계를 주장함", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "근저당권 말소 및 상계" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "근저당권설정", + "말소", + "상계", + "secured_debt_chain", + "mortgage_extinguishment", + "근저당권 말소 및 상계" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-010", + "E-014" + ], + "domain_payload_compact": {} + }, + "bh16": { + "bo_id": "bh16", + "bo_type": "event", + "action": "평택시 서정빌라에 대한 보존등기 및 매매로 인한 소유권이전등기가 마쳐짐", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "소유권이전등기" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "소유권보존", + "소유권이전", + "가압류", + "소유권이전등기" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-015" + ], + "domain_payload_compact": {} + }, + "bh17": { + "bo_id": "bh17", + "bo_type": "event", + "action": "오민한이 경매를 신청하여 이문호가 매각대금을 완납하고 취득한 뒤, 대한은행이 근저당권을 설정함에 따라 원인무효 여부 및 공신력이 다투어짐", + "action_type": "소송행위(litigation acts)", + "juristic_act": { + "label": "담보권실행을 위한 경매 및 근저당권 설정" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "임의경매", + "공신력", + "등기무효", + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "담보권실행을 위한 경매 및 근저당권 설정" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-012", + "E-013" + ], + "domain_payload_compact": {} + }, + "bh18": { + "bo_id": "bh18", + "bo_type": "event", + "action": "강용원이 오민한에게 4억 원을 지급하였으나 다수의 채무 중 충당할 채무를 특정하지 않음", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "변제 및 변제충당" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "변제", + "변제충당", + "payment_allocation_uncertain", + "변제 및 변제충당" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [], + "domain_payload_compact": {} + }, + "bh19": { + "bo_id": "bh19", + "bo_type": "event", + "action": "박광윤이 평택시 빌라를 점유하며 얻은 임대 및 거주 수익을 피담보채권에 충당하였다고 항변함", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "변제충당 주장" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "유치권", + "점유", + "변제충당", + "payment_allocation", + "변제충당 주장" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-007", + "E-011" + ], + "domain_payload_compact": {} + }, + "bh20": { + "bo_id": "bh20", + "bo_type": "event", + "action": "이수인이 윤건우에게 매매계약에서 합의되지 않은 제3자 명의로의 등기이전을 일방적으로 요구함", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "제3자 명의 등기 요청" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "소유권이전등기", + "요구", + "거절", + "registry_defect_hint", + "제3자 명의 등기 요청" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-029" + ], + "domain_payload_compact": {} + }, + "bh21": { + "bo_id": "bh21", + "bo_type": "event", + "action": "성수동 대지 차임 시세 감정평가", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "가치평가", + "임대료", + "차임", + "valuation" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-005" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": null, + "possession_events": [], + "legal_cause_assertions": [] + } + } + }, + "bh22": { + "bo_id": "bh22", + "bo_type": "state", + "action": "성수동 대지 및 건물 인도, 점유 및 사용", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "인도", + "점유", + "사용", + "possession" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-006" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": "SCH-016-01", + "possession_events": [ + { + "possessor_candidates": [ + "박성희" + ], + "start_date": "2024-11-20", + "end_date": null, + "source_event_candidate_ids": [ + "EVT-006-07" + ], + "evidence_indexes": [ + "E-006" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "임대차", + "asserted_by": "박성희", + "assertion_date": "2024-11-20", + "source_event_candidate_ids": [ + "EVT-006-03" + ], + "evidence_indexes": [ + "E-006" + ] + } + ] + } + } + }, + "bh23": { + "bo_id": "bh23", + "bo_type": "state", + "action": "신림로 상가 및 포천시 임야 가치평가(감정평가서)", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "가치평가", + "감정평가", + "valuation" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-004" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": null, + "possession_events": [], + "legal_cause_assertions": [] + } + } + }, + "bh24": { + "bo_id": "bh24", + "bo_type": "state", + "action": "평택시 서정빌라 101호 점유 및 유치권 행사", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "점유", + "거주", + "유치권", + "수익", + "possession", + "defense_assertion_benefit_applied_to_secured_claim" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-011" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": "SCH-016-04", + "possession_events": [ + { + "possessor_candidates": [ + "박광윤" + ], + "start_date": "2022-02-01", + "end_date": "2024-10-31", + "source_event_candidate_ids": [ + "EVT-011-02" + ], + "evidence_indexes": [ + "E-011" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "유치권행사중", + "asserted_by": "박광윤", + "assertion_date": "2024-10-31", + "source_event_candidate_ids": [ + "EVT-011-03" + ], + "evidence_indexes": [ + "E-011" + ] + } + ] + } + } + }, + "bh25": { + "bo_id": "bh25", + "bo_type": "state", + "action": "평택시 서정빌라 차임 시세 평가", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "가치평가", + "차임", + "valuation" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-023" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": null, + "possession_events": [], + "legal_cause_assertions": [] + } + } + }, + "bh26": { + "bo_id": "bh26", + "bo_type": "state", + "action": "흑석동 상가 점유 및 운영 (맛나식당)", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "인도", + "점유", + "운영", + "사업자등록", + "possession" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": "SCH-016-05", + "possession_events": [ + { + "possessor_candidates": [ + "양정숙" + ], + "start_date": "2024-02-01", + "end_date": null, + "source_event_candidate_ids": [ + "EVT-022-03", + "EVT-018-01" + ], + "evidence_indexes": [ + "E-018", + "E-022" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "임대차", + "asserted_by": "양정숙", + "assertion_date": "2024-02-01", + "source_event_candidate_ids": [ + "EVT-022-03" + ], + "evidence_indexes": [ + "E-022" + ] + } + ] + } + } + }, + "bh27": { + "bo_id": "bh27", + "bo_type": "event", + "action": "채무자 오국한이 수익자 오민한에게 책임재산인 신림동 상가를 1억5천만원에 매도하여 소유권이전등기 경료", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "매매" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "사해행위", + "매매계약", + "소유권이전", + "책임재산감소", + "fraudulent_act", + "매매" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-014" + ], + "domain_payload_compact": {} + }, + "bh28": { + "bo_id": "bh28", + "bo_type": "event", + "action": "강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "소비대차" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "피보전채권", + "대여금채권", + "preserved_claim", + "소비대차" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-024" + ], + "domain_payload_compact": {} + }, + "bh29": { + "bo_id": "bh29", + "bo_type": "event", + "action": "수익자 오민한이 사해행위 이후 피담보채무 1억3천만원을 대위변제하고 2번 근저당권을 말소시킴으로써 원물반환이 불가능해져 가액배상 요건 성립", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "변제 및 말소" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "원물반환불능", + "가액배상", + "근저당권말소", + "value_compensation", + "remedy_cap", + "변제 및 말소" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-014" + ], + "domain_payload_compact": {} + }, + "bh30": { + "bo_id": "bh30", + "bo_type": "state", + "action": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "무자력", + "책임재산", + "가치평가", + "insolvency_check" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-004" + ], + "domain_payload_compact": {} + }, + "bh31": { + "bo_id": "bh31", + "bo_type": "state", + "action": "수익자 오민한의 항변 (명의신탁 주장, 매수자금 및 대여금 참작 주장, 채권 상계 주장)", + "action_type": "소송행위(litigation acts)", + "juristic_act": { + "label": "항변" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "수익자항변", + "명의신탁", + "상계항변", + "선의항변", + "defense_assertion", + "항변" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-003", + "E-010" + ], + "domain_payload_compact": {} + }, + "bh32": { + "bo_id": "bh32", + "bo_type": "event", + "action": "강석우 사망으로 인한 정유심, 강용원의 성수동 대지 지분 상속", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": "상속개시" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "상속개시" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-013" + ], + "domain_payload_compact": {} + }, + "bh33": { + "bo_id": "bh33", + "bo_type": "event", + "action": "망 강호연의 사망에 따른 상속개시 및 자녀 강형수, 강지수의 상속포기", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": "상속포기" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "상속포기" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-001", + "E-002" + ], + "domain_payload_compact": {} + }, + "bh34": { + "bo_id": "bh34", + "bo_type": "event", + "action": "강용원이 김선웅에게 물품대금 지급을 최고하는 통지서 발송 및 도달", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "최고" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "최고" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-027" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-027-NT-01", + "notice_type": "최고", + "drafted_date": null, + "dispatch_date": "2024-05-04", + "arrival_date": "2024-05-06", + "received_date": "2024-05-06", + "legal_effect_date_candidate": "2024-05-06", + "requires_receipt_for_effect": null, + "sender_candidates": [ + "강용원" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "김선웅" + ], + "receipt_evidence_refs": [ + "우편물 배달 증명서" + ], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "bh35": { + "bo_id": "bh35", + "bo_type": "event", + "action": "강용원이 오민한에게 사해행위취소 및 신림동 상가 소유권 반환을 요구하는 내용증명 발송", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "최고" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "최고" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-008", + "E-010" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-008-NT-01", + "notice_type": "기타", + "drafted_date": "2024-09-15", + "dispatch_date": "2024-09-15", + "arrival_date": "2024-09-20", + "received_date": "2024-09-20", + "legal_effect_date_candidate": "2024-09-20", + "requires_receipt_for_effect": null, + "sender_candidates": [ + "강용원" + ], + "legal_sender_after_resolution": { + "status": "unresolved", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "오민한" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [ + "오민한이 이행최고서를 2024. 9. 20. 수령함" + ] + } + } + }, + "bh36": { + "bo_id": "bh36", + "bo_type": "event", + "action": "강용원과 양정숙이 박광윤에게 평택시 빌라 유치권소멸을 청구하는 통지서 발송 및 도달", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-011", + "E-019" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-019-NT-01", + "notice_type": "유치권소멸청구통지", + "drafted_date": "2024-08-29", + "dispatch_date": "2024-08-29", + "arrival_date": "2024-09-01", + "received_date": "2024-09-01", + "legal_effect_date_candidate": "2024-09-01", + "requires_receipt_for_effect": true, + "sender_candidates": [ + "강용원", + "양정숙" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "박광윤" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "bh37": { + "bo_id": "bh37", + "bo_type": "event", + "action": "오민한이 강용원에게 김선웅의 채무에 대한 책임을 부인하는 통지서 발송", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-028" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-028-NT-01", + "notice_type": "기타", + "drafted_date": null, + "dispatch_date": "2024-12-27", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [ + "오민한" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "강용원" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "bh38": { + "bo_id": "bh38", + "bo_type": "event", + "action": "오민한이 강용원에게 내용증명에 대한 답신 발송", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-010" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-010-NT-01", + "notice_type": "기타", + "drafted_date": null, + "dispatch_date": "2024-09-30", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [ + "오민한" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "강용원" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "bh39": { + "bo_id": "bh39", + "bo_type": "event", + "action": "윤건우가 양정숙에게 흑석동 상가 임대차계약 해지 및 명도를 요구하는 내용증명 통지", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-009" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-009-NT-01", + "notice_type": "기타", + "drafted_date": "2024-11-29", + "dispatch_date": "2024-11-29", + "arrival_date": "2024-11-30", + "received_date": "2024-11-30", + "legal_effect_date_candidate": "2024-11-30", + "requires_receipt_for_effect": true, + "sender_candidates": [ + "윤건우" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "양정숙" + ], + "receipt_evidence_refs": [ + "우편물배달증명서" + ], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "bh40": { + "bo_id": "bh40", + "bo_type": "event", + "action": "윤건우가 이수인에게 흑석동 상가 매매계약 해제 통지서 발송 및 도달", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-030" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-030-NT-01", + "notice_type": "해제통지", + "drafted_date": null, + "dispatch_date": "2024-05-15", + "arrival_date": "2024-05-16", + "received_date": "2024-05-16", + "legal_effect_date_candidate": "2024-05-16", + "requires_receipt_for_effect": true, + "sender_candidates": [ + "윤건우" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "이수인" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "bh41": { + "bo_id": "bh41", + "bo_type": "event", + "action": "이수인이 윤건우에게 흑석동 상가 매매대금 감액 및 제3자 명의 등기 이전 요청 통지", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-029" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-029-NT-01", + "notice_type": "기타", + "drafted_date": null, + "dispatch_date": "2024-03-30", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [ + "이수인" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "윤건우" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "bh42": { + "bo_id": "bh42", + "bo_type": "event", + "action": "주식회사 대한은행이 강용원의 성수동 대지 근저당권 말소 신청을 거부하는 통지 발송", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-012" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-012-NT-01", + "notice_type": "기타", + "drafted_date": "2024-12-22", + "dispatch_date": "2024-12-22", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [ + "주식회사 대한은행" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "강용원" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "bh43": { + "bo_id": "bh43", + "bo_type": "state", + "action": "목적물 특정", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "domain_payload_compact": { + "asset_alias": [ + { + "asset_ref": "성수동 대지", + "alias_strings": [ + "성수동 대지", + "서울 성동구 성수동 256" + ], + "canonical_label_candidate": "1. 서울 성동구 성수동 256 대 330㎡", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-01" + ], + "registry_refs": [ + { + "registry_ref": "REG-013-01", + "receipt_no_refs": [ + "제1453호" + ] + }, + { + "registry_ref": "REG-013-02", + "receipt_no_refs": [ + "제7026호" + ] + }, + { + "registry_ref": "REG-013-03", + "receipt_no_refs": [ + "제11202호" + ] + }, + { + "registry_ref": "REG-013-04", + "receipt_no_refs": [ + "제7202호" + ] + }, + { + "registry_ref": "REG-013-05", + "receipt_no_refs": [ + "제12402호" + ] + }, + { + "registry_ref": "REG-013-06", + "receipt_no_refs": [ + "제25797호" + ] + }, + { + "registry_ref": "REG-013-07", + "receipt_no_refs": [ + "제25799호" + ] + } + ], + "source_evidence_indexes": [ + "E-013", + "E-016" + ] + }, + { + "asset_ref": "성수동 건물", + "alias_strings": [ + "성수동 건물", + "서울 성동구 성수동 256 상가" + ], + "canonical_label_candidate": "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-02" + ], + "registry_refs": [], + "source_evidence_indexes": [ + "E-016" + ] + }, + { + "asset_ref": "신림동 상가", + "alias_strings": [ + "신림동 상가", + "서울 관악구 신림로 115" + ], + "canonical_label_candidate": "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-03" + ], + "registry_refs": [ + { + "registry_ref": "REG-014-01", + "receipt_no_refs": [ + "제24759호" + ] + }, + { + "registry_ref": "REG-014-02", + "receipt_no_refs": [ + "제56457호" + ] + }, + { + "registry_ref": "REG-014-03", + "receipt_no_refs": [ + "제11593호" + ] + }, + { + "registry_ref": "REG-014-04", + "receipt_no_refs": [ + "제50099호" + ] + }, + { + "registry_ref": "REG-014-05", + "receipt_no_refs": [ + "제26597호" + ] + }, + { + "registry_ref": "REG-014-06", + "receipt_no_refs": [ + "제74586호" + ] + } + ], + "source_evidence_indexes": [ + "E-014", + "E-016" + ] + }, + { + "asset_ref": "평택시 빌라", + "alias_strings": [ + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123" + ], + "canonical_label_candidate": "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-04" + ], + "registry_refs": [ + { + "registry_ref": "REG-015-01", + "receipt_no_refs": [ + "제7233호" + ] + }, + { + "registry_ref": "REG-015-02", + "receipt_no_refs": [ + "제7871호" + ] + }, + { + "registry_ref": "REG-015-03", + "receipt_no_refs": [ + "제11593호" + ] + }, + { + "registry_ref": "REG-015-04", + "receipt_no_refs": [ + "제12321호" + ] + } + ], + "source_evidence_indexes": [ + "E-015", + "E-016" + ] + }, + { + "asset_ref": "흑석동 상가", + "alias_strings": [ + "흑석동 상가", + "서울 동작구 흑석로 80" + ], + "canonical_label_candidate": "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-05" + ], + "registry_refs": [], + "source_evidence_indexes": [ + "E-016" + ] + } + ] + } + }, + "bh44": { + "bo_id": "bh44", + "bo_type": "state", + "action": "박광윤의 평택시 서정빌라 101호 거주 및 유치권 행사 상태", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-011" + ], + "domain_payload_compact": { + "lien_state": { + "lien_id": "B5:LIEN:001", + "asset_ref": "평택시 빌라", + "lien_holder_candidates": [ + "박광윤" + ], + "secured_claim_hint": { + "debtor_candidate": "김일동", + "basis_text": "공사잔금 3억 원 미지급", + "claimed_remaining_amount_hint": "300,000,000원", + "evidence_indexes": [ + "E-011" + ] + }, + "possession_event_refs": [], + "assertion_events": [ + "EVT-011-02" + ], + "extinction_notice_refs": [ + "E-019-NT-01" + ], + "status_raw_hint": "유치권 행사 중" + }, + "possession_state": { + "asset_ref": "평택시 빌라", + "possession_events": [ + { + "possessor_candidates": [ + "박광윤" + ], + "start_date": null, + "end_date": null, + "source_event_candidate_ids": [ + "EVT-011-02" + ], + "evidence_indexes": [ + "E-011" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "유치권행사중", + "asserted_by": "박광윤", + "assertion_date": "2024-10-31", + "source_event_candidate_ids": [ + "EVT-011-02" + ], + "evidence_indexes": [ + "E-011" + ] + } + ] + } + } + }, + "bh45": { + "bo_id": "bh45", + "bo_type": "state", + "action": "박성희의 성수동 대지 및 건물 점유", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-006" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": "성수동 대지 및 성수동 건물", + "possession_events": [ + { + "possessor_candidates": [ + "박성희" + ], + "start_date": "2024-11-20", + "end_date": null, + "source_event_candidate_ids": [ + "EVT-006-07" + ], + "evidence_indexes": [ + "E-006" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "임대차", + "asserted_by": "박성희", + "assertion_date": "2024-11-20", + "source_event_candidate_ids": [ + "EVT-006-07" + ], + "evidence_indexes": [ + "E-006" + ] + } + ] + } + } + }, + "bh46": { + "bo_id": "bh46", + "bo_type": "state", + "action": "양정숙의 흑석동 상가 점유 및 식당 운영 상태", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-018", + "E-022" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": "흑석동 상가", + "possession_events": [ + { + "possessor_candidates": [ + "양정숙" + ], + "start_date": "2024-02-01", + "end_date": null, + "source_event_candidate_ids": [ + "EVT-018-01", + "EVT-022-03" + ], + "evidence_indexes": [ + "E-018", + "E-022" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "임대차", + "asserted_by": "양정숙", + "assertion_date": "2024-02-01", + "source_event_candidate_ids": [ + "EVT-022-03" + ], + "evidence_indexes": [ + "E-022" + ] + } + ] + } + } + }, + "bh47": { + "bo_id": "bh47", + "bo_type": "state", + "action": "오민한의 강용원에 대한 자신의 5,000만 원 채권 상계 주장 항변", + "action_type": "소송행위(litigation acts)", + "juristic_act": { + "label": "상계" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "상계" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-010" + ], + "domain_payload_compact": {} + }, + "bh48": { + "bo_id": "bh48", + "bo_type": "state", + "action": "박광윤의 과거 임대료 및 거주 수익을 채무에 충당하였다는 주장 항변", + "action_type": "소송행위(litigation acts)", + "juristic_act": { + "label": "항변" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "항변" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-011" + ], + "domain_payload_compact": {} + }, + "bh49": { + "bo_id": "bh49", + "bo_type": "state", + "action": "윤건우의 임대차 책임 부인 및 양정숙 차임 연체 주장 항변", + "action_type": "소송행위(litigation acts)", + "juristic_act": { + "label": "항변" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "항변" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-009" + ], + "domain_payload_compact": {} + }, + "bh50": { + "bo_id": "bh50", + "bo_type": "state", + "action": "주식회사 대한은행의 경매공신력 주장(민사집행법 제267조)에 따른 근저당 말소 불가 항변", + "action_type": "소송행위(litigation acts)", + "juristic_act": { + "label": "항변" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "항변" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-012" + ], + "domain_payload_compact": {} + } + }, + "legal_effect_structures_compact": { + "schema_version": "stage1_legal_effect_structures.v1", + "status": "READY_WITH_REVIEW", + "structure_by_id": { + "LES-001": { + "structure_id": "LES-001", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh1", + "source_bo_ids": [ + "bh1" + ], + "issue_cluster_ids": [ + "IC-001" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-007" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:011" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-002": { + "structure_id": "LES-002", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh10", + "source_bo_ids": [ + "bh10" + ], + "issue_cluster_ids": [ + "IC-002" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:013" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "review_required": true + }, + "LES-003": { + "structure_id": "LES-003", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh11", + "source_bo_ids": [ + "bh11" + ], + "issue_cluster_ids": [ + "IC-003" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-027" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:003" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "review_required": true + }, + "LES-004": { + "structure_id": "LES-004", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh12", + "source_bo_ids": [ + "bh12" + ], + "issue_cluster_ids": [ + "IC-004" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-028" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:005" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-005": { + "structure_id": "LES-005", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh13", + "source_bo_ids": [ + "bh13" + ], + "issue_cluster_ids": [ + "IC-005" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-011" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:012" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-006": { + "structure_id": "LES-006", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh14", + "source_bo_ids": [ + "bh14" + ], + "issue_cluster_ids": [ + "IC-006" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:001" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-007": { + "structure_id": "LES-007", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh15", + "source_bo_ids": [ + "bh15" + ], + "issue_cluster_ids": [ + "IC-007" + ], + "asset_cluster_ids": [ + "AC-002" + ], + "evidence_indexes": [ + "E-010", + "E-014" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:004" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-008": { + "structure_id": "LES-008", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh16", + "source_bo_ids": [ + "bh16" + ], + "issue_cluster_ids": [ + "IC-008" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_indexes": [ + "E-015" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:006" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-009": { + "structure_id": "LES-009", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh17", + "source_bo_ids": [ + "bh17" + ], + "issue_cluster_ids": [ + "IC-009" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_indexes": [ + "E-012", + "E-013" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:003" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "review_required": true + }, + "LES-010": { + "structure_id": "LES-010", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh18", + "source_bo_ids": [ + "bh18" + ], + "issue_cluster_ids": [ + "IC-010" + ], + "asset_cluster_ids": [], + "evidence_indexes": [], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:002" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "review_required": true + }, + "LES-011": { + "structure_id": "LES-011", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh19", + "source_bo_ids": [ + "bh19" + ], + "issue_cluster_ids": [ + "IC-011" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_indexes": [ + "E-007", + "E-011" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:005" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "legal_calculation_object", + "claim_chain_ref" + ], + "review_required": true + }, + "LES-012": { + "structure_id": "LES-012", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh2", + "source_bo_ids": [ + "bh2" + ], + "issue_cluster_ids": [ + "IC-012" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-021", + "E-027" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:001" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-013": { + "structure_id": "LES-013", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh20", + "source_bo_ids": [ + "bh20" + ], + "issue_cluster_ids": [ + "IC-013" + ], + "asset_cluster_ids": [ + "AC-004" + ], + "evidence_indexes": [ + "E-029" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:007" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-014": { + "structure_id": "LES-014", + "candidate_structure_type": "land_use_gain", + "structure_label": "land use gain structure for bh21", + "source_bo_ids": [ + "bh21" + ], + "issue_cluster_ids": [ + "IC-014" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006" + ], + "evidence_indexes": [ + "E-005" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:002" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-015": { + "structure_id": "LES-015", + "candidate_structure_type": "land_use_gain", + "structure_label": "land use gain structure for bh22", + "source_bo_ids": [ + "bh22" + ], + "issue_cluster_ids": [ + "IC-015" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-007", + "AC-008" + ], + "evidence_indexes": [ + "E-006" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:004" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_structures" + ], + "review_required": true + }, + "LES-016": { + "structure_id": "LES-016", + "candidate_structure_type": "land_use_gain", + "structure_label": "land use gain structure for bh23", + "source_bo_ids": [ + "bh23" + ], + "issue_cluster_ids": [ + "IC-016" + ], + "asset_cluster_ids": [ + "AC-009", + "AC-010" + ], + "evidence_indexes": [ + "E-004" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:001" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-017": { + "structure_id": "LES-017", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh24", + "source_bo_ids": [ + "bh24" + ], + "issue_cluster_ids": [ + "IC-017" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-012" + ], + "evidence_indexes": [ + "E-011" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:005" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "legal_calculation_object" + ], + "review_required": true + }, + "LES-018": { + "structure_id": "LES-018", + "candidate_structure_type": "land_use_gain", + "structure_label": "land use gain structure for bh25", + "source_bo_ids": [ + "bh25" + ], + "issue_cluster_ids": [ + "IC-018" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-013" + ], + "evidence_indexes": [ + "E-023" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:003" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-019": { + "structure_id": "LES-019", + "candidate_structure_type": "land_use_gain", + "structure_label": "land use gain structure for bh26", + "source_bo_ids": [ + "bh26" + ], + "issue_cluster_ids": [ + "IC-019" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:006" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_structures" + ], + "review_required": true + }, + "LES-020": { + "structure_id": "LES-020", + "candidate_structure_type": "fraudulent_transfer", + "structure_label": "fraudulent transfer structure for bh27", + "source_bo_ids": [ + "bh27" + ], + "issue_cluster_ids": [ + "IC-020" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_indexes": [ + "E-014" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B4:002" + ], + "linked_actio_signal_ids": [ + "actio-signal-002" + ], + "legal_effect_roles": [ + "actio_roles", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-021": { + "structure_id": "LES-021", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh28", + "source_bo_ids": [ + "bh28" + ], + "issue_cluster_ids": [ + "IC-021" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-024" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B4:001" + ], + "linked_actio_signal_ids": [ + "actio-signal-001" + ], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "actio_roles" + ], + "review_required": true + }, + "LES-022": { + "structure_id": "LES-022", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh29", + "source_bo_ids": [ + "bh29" + ], + "issue_cluster_ids": [ + "IC-022" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-002" + ], + "evidence_indexes": [ + "E-014" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B4:004" + ], + "linked_actio_signal_ids": [ + "actio-signal-004" + ], + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_actio_structures", + "linked_structures", + "actio_roles" + ], + "review_required": true + }, + "LES-023": { + "structure_id": "LES-023", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh3", + "source_bo_ids": [ + "bh3" + ], + "issue_cluster_ids": [ + "IC-023" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-024" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:006" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-024": { + "structure_id": "LES-024", + "candidate_structure_type": "valuation", + "structure_label": "valuation structure for bh30", + "source_bo_ids": [ + "bh30" + ], + "issue_cluster_ids": [ + "IC-024" + ], + "asset_cluster_ids": [ + "AC-020", + "AC-021" + ], + "evidence_indexes": [ + "E-004" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B4:003" + ], + "linked_actio_signal_ids": [ + "actio-signal-003" + ], + "legal_effect_roles": [ + "legal_effect_roles", + "actio_roles", + "legal_calculation_object" + ], + "review_required": true + }, + "LES-025": { + "structure_id": "LES-025", + "candidate_structure_type": "fraudulent_transfer", + "structure_label": "fraudulent transfer structure for bh31", + "source_bo_ids": [ + "bh31" + ], + "issue_cluster_ids": [ + "IC-025" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B4:005" + ], + "linked_actio_signal_ids": [ + "actio-signal-005" + ], + "legal_effect_roles": [ + "legal_effect_roles", + "actio_roles", + "linked_structures" + ], + "review_required": true + }, + "LES-026": { + "structure_id": "LES-026", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh32", + "source_bo_ids": [ + "bh32" + ], + "issue_cluster_ids": [ + "IC-026" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-013" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:002" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-027": { + "structure_id": "LES-027", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh33", + "source_bo_ids": [ + "bh33" + ], + "issue_cluster_ids": [ + "IC-027" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-001", + "E-002" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:003" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-028": { + "structure_id": "LES-028", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh34", + "source_bo_ids": [ + "bh34" + ], + "issue_cluster_ids": [ + "IC-028" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-027" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:009" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-029": { + "structure_id": "LES-029", + "candidate_structure_type": "fraudulent_transfer", + "structure_label": "fraudulent transfer structure for bh35", + "source_bo_ids": [ + "bh35" + ], + "issue_cluster_ids": [ + "IC-029" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-008", + "E-010" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:004" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "actio_roles" + ], + "review_required": true + }, + "LES-030": { + "structure_id": "LES-030", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh36", + "source_bo_ids": [ + "bh36" + ], + "issue_cluster_ids": [ + "IC-030" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-011", + "E-019" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:007" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-031": { + "structure_id": "LES-031", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh37", + "source_bo_ids": [ + "bh37" + ], + "issue_cluster_ids": [ + "IC-031" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-028" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:010" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-032": { + "structure_id": "LES-032", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh38", + "source_bo_ids": [ + "bh38" + ], + "issue_cluster_ids": [ + "IC-032" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-010" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:006" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-033": { + "structure_id": "LES-033", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh39", + "source_bo_ids": [ + "bh39" + ], + "issue_cluster_ids": [ + "IC-033" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-009" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:005" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-034": { + "structure_id": "LES-034", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh4", + "source_bo_ids": [ + "bh4" + ], + "issue_cluster_ids": [ + "IC-034" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-024" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:007" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-035": { + "structure_id": "LES-035", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh40", + "source_bo_ids": [ + "bh40" + ], + "issue_cluster_ids": [ + "IC-035" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-030" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:012" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-036": { + "structure_id": "LES-036", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh41", + "source_bo_ids": [ + "bh41" + ], + "issue_cluster_ids": [ + "IC-036" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-029" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:011" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "review_required": true + }, + "LES-037": { + "structure_id": "LES-037", + "candidate_structure_type": "registry_invalidity", + "structure_label": "registry invalidity structure for bh42", + "source_bo_ids": [ + "bh42" + ], + "issue_cluster_ids": [ + "IC-037" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-012" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:008" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-038": { + "structure_id": "LES-038", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh43", + "source_bo_ids": [ + "bh43" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "asset_cluster_ids": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:001" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-039": { + "structure_id": "LES-039", + "candidate_structure_type": "secured_debt", + "structure_label": "secured debt structure for bh44", + "source_bo_ids": [ + "bh44" + ], + "issue_cluster_ids": [ + "IC-039" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-011" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:013" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-040": { + "structure_id": "LES-040", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh45", + "source_bo_ids": [ + "bh45" + ], + "issue_cluster_ids": [ + "IC-040" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-006" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:014" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-041": { + "structure_id": "LES-041", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh46", + "source_bo_ids": [ + "bh46" + ], + "issue_cluster_ids": [ + "IC-041" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-018", + "E-022" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:015" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-042": { + "structure_id": "LES-042", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh47", + "source_bo_ids": [ + "bh47" + ], + "issue_cluster_ids": [ + "IC-042" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-010" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:017" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "review_required": true + }, + "LES-043": { + "structure_id": "LES-043", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh48", + "source_bo_ids": [ + "bh48" + ], + "issue_cluster_ids": [ + "IC-043" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-011" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:018" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-044": { + "structure_id": "LES-044", + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_label": "succession notice lien asset defense structure for bh49", + "source_bo_ids": [ + "bh49" + ], + "issue_cluster_ids": [ + "IC-044" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-009" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:016" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-045": { + "structure_id": "LES-045", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh5", + "source_bo_ids": [ + "bh5" + ], + "issue_cluster_ids": [ + "IC-045" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-025" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:008" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-046": { + "structure_id": "LES-046", + "candidate_structure_type": "registry_invalidity", + "structure_label": "registry invalidity structure for bh50", + "source_bo_ids": [ + "bh50" + ], + "issue_cluster_ids": [ + "IC-046" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-012" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:019" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-047": { + "structure_id": "LES-047", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh6", + "source_bo_ids": [ + "bh6" + ], + "issue_cluster_ids": [ + "IC-047" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-026" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:009" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-048": { + "structure_id": "LES-048", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh7", + "source_bo_ids": [ + "bh7" + ], + "issue_cluster_ids": [ + "IC-048" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-020" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:004" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-049": { + "structure_id": "LES-049", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh8", + "source_bo_ids": [ + "bh8" + ], + "issue_cluster_ids": [ + "IC-049" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-021" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:002" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "review_required": true + }, + "LES-050": { + "structure_id": "LES-050", + "candidate_structure_type": "money_claim", + "structure_label": "money claim structure for bh9", + "source_bo_ids": [ + "bh9" + ], + "issue_cluster_ids": [ + "IC-050" + ], + "asset_cluster_ids": [], + "evidence_indexes": [], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:010" + ], + "linked_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "review_required": true + } + }, + "structure_index": { + "by_bo_id": { + "bh1": { + "bo_id": "bh1", + "structure_ids": [ + "LES-001" + ], + "structure_labels": [ + "money claim structure for bh1" + ], + "issue_cluster_ids": [ + "IC-001" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-007" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:011" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh10": { + "bo_id": "bh10", + "structure_ids": [ + "LES-002" + ], + "structure_labels": [ + "money claim structure for bh10" + ], + "issue_cluster_ids": [ + "IC-002" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:013" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh11": { + "bo_id": "bh11", + "structure_ids": [ + "LES-003" + ], + "structure_labels": [ + "money claim structure for bh11" + ], + "issue_cluster_ids": [ + "IC-003" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-027" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:003" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh12": { + "bo_id": "bh12", + "structure_ids": [ + "LES-004" + ], + "structure_labels": [ + "money claim structure for bh12" + ], + "issue_cluster_ids": [ + "IC-004" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-028" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:005" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh13": { + "bo_id": "bh13", + "structure_ids": [ + "LES-005" + ], + "structure_labels": [ + "money claim structure for bh13" + ], + "issue_cluster_ids": [ + "IC-005" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-011" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:012" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh14": { + "bo_id": "bh14", + "structure_ids": [ + "LES-006" + ], + "structure_labels": [ + "secured debt structure for bh14" + ], + "issue_cluster_ids": [ + "IC-006" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:001" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh15": { + "bo_id": "bh15", + "structure_ids": [ + "LES-007" + ], + "structure_labels": [ + "secured debt structure for bh15" + ], + "issue_cluster_ids": [ + "IC-007" + ], + "asset_cluster_ids": [ + "AC-002" + ], + "evidence_indexes": [ + "E-010", + "E-014" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:004" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh16": { + "bo_id": "bh16", + "structure_ids": [ + "LES-008" + ], + "structure_labels": [ + "secured debt structure for bh16" + ], + "issue_cluster_ids": [ + "IC-008" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_indexes": [ + "E-015" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:006" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh17": { + "bo_id": "bh17", + "structure_ids": [ + "LES-009" + ], + "structure_labels": [ + "secured debt structure for bh17" + ], + "issue_cluster_ids": [ + "IC-009" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_indexes": [ + "E-012", + "E-013" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:003" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh18": { + "bo_id": "bh18", + "structure_ids": [ + "LES-010" + ], + "structure_labels": [ + "secured debt structure for bh18" + ], + "issue_cluster_ids": [ + "IC-010" + ], + "asset_cluster_ids": [], + "evidence_indexes": [], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:002" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh19": { + "bo_id": "bh19", + "structure_ids": [ + "LES-011" + ], + "structure_labels": [ + "money claim structure for bh19" + ], + "issue_cluster_ids": [ + "IC-011" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_indexes": [ + "E-007", + "E-011" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:005" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "legal_calculation_object", + "claim_chain_ref" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh2": { + "bo_id": "bh2", + "structure_ids": [ + "LES-012" + ], + "structure_labels": [ + "money claim structure for bh2" + ], + "issue_cluster_ids": [ + "IC-012" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-021", + "E-027" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:001" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh20": { + "bo_id": "bh20", + "structure_ids": [ + "LES-013" + ], + "structure_labels": [ + "secured debt structure for bh20" + ], + "issue_cluster_ids": [ + "IC-013" + ], + "asset_cluster_ids": [ + "AC-004" + ], + "evidence_indexes": [ + "E-029" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:007" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh21": { + "bo_id": "bh21", + "structure_ids": [ + "LES-014" + ], + "structure_labels": [ + "land use gain structure for bh21" + ], + "issue_cluster_ids": [ + "IC-014" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006" + ], + "evidence_indexes": [ + "E-005" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:002" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [ + "bundle:bh21" + ], + "review_required": true + }, + "bh22": { + "bo_id": "bh22", + "structure_ids": [ + "LES-015" + ], + "structure_labels": [ + "land use gain structure for bh22" + ], + "issue_cluster_ids": [ + "IC-015" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-007", + "AC-008" + ], + "evidence_indexes": [ + "E-006" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:004" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [ + "bundle:bh22" + ], + "review_required": true + }, + "bh23": { + "bo_id": "bh23", + "structure_ids": [ + "LES-016" + ], + "structure_labels": [ + "land use gain structure for bh23" + ], + "issue_cluster_ids": [ + "IC-016" + ], + "asset_cluster_ids": [ + "AC-009", + "AC-010" + ], + "evidence_indexes": [ + "E-004" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:001" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [ + "bundle:bh23" + ], + "review_required": true + }, + "bh24": { + "bo_id": "bh24", + "structure_ids": [ + "LES-017" + ], + "structure_labels": [ + "secured debt structure for bh24" + ], + "issue_cluster_ids": [ + "IC-017" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-012" + ], + "evidence_indexes": [ + "E-011" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:005" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "linked_structures", + "legal_calculation_object" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [ + "bundle:bh24" + ], + "review_required": true + }, + "bh25": { + "bo_id": "bh25", + "structure_ids": [ + "LES-018" + ], + "structure_labels": [ + "land use gain structure for bh25" + ], + "issue_cluster_ids": [ + "IC-018" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-013" + ], + "evidence_indexes": [ + "E-023" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:003" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_calculation_object", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [ + "bundle:bh25" + ], + "review_required": true + }, + "bh26": { + "bo_id": "bh26", + "structure_ids": [ + "LES-019" + ], + "structure_labels": [ + "land use gain structure for bh26" + ], + "issue_cluster_ids": [ + "IC-019" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:006" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [ + "bundle:bh26" + ], + "review_required": true + }, + "bh27": { + "bo_id": "bh27", + "structure_ids": [ + "LES-020" + ], + "structure_labels": [ + "fraudulent transfer structure for bh27" + ], + "issue_cluster_ids": [ + "IC-020" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_indexes": [ + "E-014" + ], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:002" + ], + "related_actio_signal_ids": [ + "actio-signal-002" + ], + "legal_effect_roles": [ + "actio_roles", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh28": { + "bo_id": "bh28", + "structure_ids": [ + "LES-021" + ], + "structure_labels": [ + "money claim structure for bh28" + ], + "issue_cluster_ids": [ + "IC-021" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-024" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:001" + ], + "related_actio_signal_ids": [ + "actio-signal-001" + ], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "actio_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh29": { + "bo_id": "bh29", + "structure_ids": [ + "LES-022" + ], + "structure_labels": [ + "secured debt structure for bh29" + ], + "issue_cluster_ids": [ + "IC-022" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-002" + ], + "evidence_indexes": [ + "E-014" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:004" + ], + "related_actio_signal_ids": [ + "actio-signal-004" + ], + "legal_effect_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_actio_structures", + "linked_structures", + "actio_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [ + "bundle:bh29" + ], + "review_required": true + }, + "bh3": { + "bo_id": "bh3", + "structure_ids": [ + "LES-023" + ], + "structure_labels": [ + "money claim structure for bh3" + ], + "issue_cluster_ids": [ + "IC-023" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-024" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:006" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh30": { + "bo_id": "bh30", + "structure_ids": [ + "LES-024" + ], + "structure_labels": [ + "valuation structure for bh30" + ], + "issue_cluster_ids": [ + "IC-024" + ], + "asset_cluster_ids": [ + "AC-020", + "AC-021" + ], + "evidence_indexes": [ + "E-004" + ], + "candidate_structure_types": [ + "valuation" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:003" + ], + "related_actio_signal_ids": [ + "actio-signal-003" + ], + "legal_effect_roles": [ + "legal_effect_roles", + "actio_roles", + "legal_calculation_object" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [ + "bundle:bh30" + ], + "review_required": true + }, + "bh31": { + "bo_id": "bh31", + "structure_ids": [ + "LES-025" + ], + "structure_labels": [ + "fraudulent transfer structure for bh31" + ], + "issue_cluster_ids": [ + "IC-025" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:005" + ], + "related_actio_signal_ids": [ + "actio-signal-005" + ], + "legal_effect_roles": [ + "legal_effect_roles", + "actio_roles", + "linked_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh32": { + "bo_id": "bh32", + "structure_ids": [ + "LES-026" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh32" + ], + "issue_cluster_ids": [ + "IC-026" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-013" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:002" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh33": { + "bo_id": "bh33", + "structure_ids": [ + "LES-027" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh33" + ], + "issue_cluster_ids": [ + "IC-027" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-001", + "E-002" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:003" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh34": { + "bo_id": "bh34", + "structure_ids": [ + "LES-028" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh34" + ], + "issue_cluster_ids": [ + "IC-028" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-027" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:009" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh35": { + "bo_id": "bh35", + "structure_ids": [ + "LES-029" + ], + "structure_labels": [ + "fraudulent transfer structure for bh35" + ], + "issue_cluster_ids": [ + "IC-029" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-008", + "E-010" + ], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:004" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "actio_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh36": { + "bo_id": "bh36", + "structure_ids": [ + "LES-030" + ], + "structure_labels": [ + "secured debt structure for bh36" + ], + "issue_cluster_ids": [ + "IC-030" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-011", + "E-019" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:007" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh37": { + "bo_id": "bh37", + "structure_ids": [ + "LES-031" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh37" + ], + "issue_cluster_ids": [ + "IC-031" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-028" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:010" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh38": { + "bo_id": "bh38", + "structure_ids": [ + "LES-032" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh38" + ], + "issue_cluster_ids": [ + "IC-032" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-010" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:006" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh39": { + "bo_id": "bh39", + "structure_ids": [ + "LES-033" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh39" + ], + "issue_cluster_ids": [ + "IC-033" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-009" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:005" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh4": { + "bo_id": "bh4", + "structure_ids": [ + "LES-034" + ], + "structure_labels": [ + "money claim structure for bh4" + ], + "issue_cluster_ids": [ + "IC-034" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-024" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:007" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh40": { + "bo_id": "bh40", + "structure_ids": [ + "LES-035" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh40" + ], + "issue_cluster_ids": [ + "IC-035" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-030" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:012" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh41": { + "bo_id": "bh41", + "structure_ids": [ + "LES-036" + ], + "structure_labels": [ + "money claim structure for bh41" + ], + "issue_cluster_ids": [ + "IC-036" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-029" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:011" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh42": { + "bo_id": "bh42", + "structure_ids": [ + "LES-037" + ], + "structure_labels": [ + "registry invalidity structure for bh42" + ], + "issue_cluster_ids": [ + "IC-037" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-012" + ], + "candidate_structure_types": [ + "registry_invalidity" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:008" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh43": { + "bo_id": "bh43", + "structure_ids": [ + "LES-038" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh43" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "asset_cluster_ids": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:001" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [ + "bundle:bh43" + ], + "review_required": true + }, + "bh44": { + "bo_id": "bh44", + "structure_ids": [ + "LES-039" + ], + "structure_labels": [ + "secured debt structure for bh44" + ], + "issue_cluster_ids": [ + "IC-039" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-011" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:013" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh45": { + "bo_id": "bh45", + "structure_ids": [ + "LES-040" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh45" + ], + "issue_cluster_ids": [ + "IC-040" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-006" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:014" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh46": { + "bo_id": "bh46", + "structure_ids": [ + "LES-041" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh46" + ], + "issue_cluster_ids": [ + "IC-041" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-018", + "E-022" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:015" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh47": { + "bo_id": "bh47", + "structure_ids": [ + "LES-042" + ], + "structure_labels": [ + "money claim structure for bh47" + ], + "issue_cluster_ids": [ + "IC-042" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-010" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:017" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh48": { + "bo_id": "bh48", + "structure_ids": [ + "LES-043" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh48" + ], + "issue_cluster_ids": [ + "IC-043" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-011" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:018" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh49": { + "bo_id": "bh49", + "structure_ids": [ + "LES-044" + ], + "structure_labels": [ + "succession notice lien asset defense structure for bh49" + ], + "issue_cluster_ids": [ + "IC-044" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-009" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:016" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh5": { + "bo_id": "bh5", + "structure_ids": [ + "LES-045" + ], + "structure_labels": [ + "money claim structure for bh5" + ], + "issue_cluster_ids": [ + "IC-045" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-025" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:008" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh50": { + "bo_id": "bh50", + "structure_ids": [ + "LES-046" + ], + "structure_labels": [ + "registry invalidity structure for bh50" + ], + "issue_cluster_ids": [ + "IC-046" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-012" + ], + "candidate_structure_types": [ + "registry_invalidity" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:019" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh6": { + "bo_id": "bh6", + "structure_ids": [ + "LES-047" + ], + "structure_labels": [ + "money claim structure for bh6" + ], + "issue_cluster_ids": [ + "IC-047" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-026" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:009" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh7": { + "bo_id": "bh7", + "structure_ids": [ + "LES-048" + ], + "structure_labels": [ + "money claim structure for bh7" + ], + "issue_cluster_ids": [ + "IC-048" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-020" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:004" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh8": { + "bo_id": "bh8", + "structure_ids": [ + "LES-049" + ], + "structure_labels": [ + "money claim structure for bh8" + ], + "issue_cluster_ids": [ + "IC-049" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-021" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:002" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + }, + "bh9": { + "bo_id": "bh9", + "structure_ids": [ + "LES-050" + ], + "structure_labels": [ + "money claim structure for bh9" + ], + "issue_cluster_ids": [ + "IC-050" + ], + "asset_cluster_ids": [], + "evidence_indexes": [], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:010" + ], + "related_actio_signal_ids": [], + "legal_effect_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "multi_asset_bundle_refs": [], + "review_required": true + } + }, + "by_issue_cluster_id": { + "IC-001": { + "issue_cluster_id": "IC-001", + "issue_type": "B1:011", + "issue_label": "hard:B1:011", + "source_bo_ids": [ + "bh1" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:011" + ], + "evidence_indexes": [ + "E-007" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-001" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-002": { + "issue_cluster_id": "IC-002", + "issue_type": "B1:013", + "issue_label": "hard:B1:013", + "source_bo_ids": [ + "bh10" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:013" + ], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-002" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-003": { + "issue_cluster_id": "IC-003", + "issue_type": "B1:003", + "issue_label": "hard:B1:003", + "source_bo_ids": [ + "bh11" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:003" + ], + "evidence_indexes": [ + "E-027" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-003" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-004": { + "issue_cluster_id": "IC-004", + "issue_type": "B1:005", + "issue_label": "hard:B1:005", + "source_bo_ids": [ + "bh12" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:005" + ], + "evidence_indexes": [ + "E-028" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-004" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-005": { + "issue_cluster_id": "IC-005", + "issue_type": "B1:012", + "issue_label": "hard:B1:012", + "source_bo_ids": [ + "bh13" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:012" + ], + "evidence_indexes": [ + "E-011" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-005" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-006": { + "issue_cluster_id": "IC-006", + "issue_type": "B2:001", + "issue_label": "hard:B2:001", + "source_bo_ids": [ + "bh14" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:001" + ], + "evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "structure_ids": [ + "LES-006" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-007": { + "issue_cluster_id": "IC-007", + "issue_type": "B2:004", + "issue_label": "hard:B2:004", + "source_bo_ids": [ + "bh15" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:004" + ], + "evidence_indexes": [ + "E-010", + "E-014" + ], + "asset_cluster_ids": [ + "AC-002" + ], + "structure_ids": [ + "LES-007" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-008": { + "issue_cluster_id": "IC-008", + "issue_type": "B2:006", + "issue_label": "hard:B2:006", + "source_bo_ids": [ + "bh16" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:006" + ], + "evidence_indexes": [ + "E-015" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "structure_ids": [ + "LES-008" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-009": { + "issue_cluster_id": "IC-009", + "issue_type": "B2:003", + "issue_label": "hard:B2:003", + "source_bo_ids": [ + "bh17" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:003" + ], + "evidence_indexes": [ + "E-012", + "E-013" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "structure_ids": [ + "LES-009" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-010": { + "issue_cluster_id": "IC-010", + "issue_type": "B2:002", + "issue_label": "hard:B2:002", + "source_bo_ids": [ + "bh18" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:002" + ], + "evidence_indexes": [], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-010" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-011": { + "issue_cluster_id": "IC-011", + "issue_type": "B2:005", + "issue_label": "hard:B2:005", + "source_bo_ids": [ + "bh19" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:005" + ], + "evidence_indexes": [ + "E-007", + "E-011" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "structure_ids": [ + "LES-011" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-012": { + "issue_cluster_id": "IC-012", + "issue_type": "B1:001", + "issue_label": "hard:B1:001", + "source_bo_ids": [ + "bh2" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:001" + ], + "evidence_indexes": [ + "E-021", + "E-027" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-012" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-013": { + "issue_cluster_id": "IC-013", + "issue_type": "B2:007", + "issue_label": "hard:B2:007", + "source_bo_ids": [ + "bh20" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:007" + ], + "evidence_indexes": [ + "E-029" + ], + "asset_cluster_ids": [ + "AC-004" + ], + "structure_ids": [ + "LES-013" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-014": { + "issue_cluster_id": "IC-014", + "issue_type": "B3:002", + "issue_label": "hard:B3:002", + "source_bo_ids": [ + "bh21" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:002" + ], + "evidence_indexes": [ + "E-005" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006" + ], + "structure_ids": [ + "LES-014" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-015": { + "issue_cluster_id": "IC-015", + "issue_type": "B3:004", + "issue_label": "hard:B3:004", + "source_bo_ids": [ + "bh22" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:004" + ], + "evidence_indexes": [ + "E-006" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-007", + "AC-008" + ], + "structure_ids": [ + "LES-015" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-016": { + "issue_cluster_id": "IC-016", + "issue_type": "B3:001", + "issue_label": "hard:B3:001", + "source_bo_ids": [ + "bh23" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:001" + ], + "evidence_indexes": [ + "E-004" + ], + "asset_cluster_ids": [ + "AC-009", + "AC-010" + ], + "structure_ids": [ + "LES-016" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-017": { + "issue_cluster_id": "IC-017", + "issue_type": "B3:005", + "issue_label": "hard:B3:005", + "source_bo_ids": [ + "bh24" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:005" + ], + "evidence_indexes": [ + "E-011" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-012" + ], + "structure_ids": [ + "LES-017" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-018": { + "issue_cluster_id": "IC-018", + "issue_type": "B3:003", + "issue_label": "hard:B3:003", + "source_bo_ids": [ + "bh25" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:003" + ], + "evidence_indexes": [ + "E-023" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-013" + ], + "structure_ids": [ + "LES-018" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-019": { + "issue_cluster_id": "IC-019", + "issue_type": "B3:006", + "issue_label": "hard:B3:006", + "source_bo_ids": [ + "bh26" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:006" + ], + "evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "structure_ids": [ + "LES-019" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-020": { + "issue_cluster_id": "IC-020", + "issue_type": "B4:002", + "issue_label": "hard:B4:002", + "source_bo_ids": [ + "bh27" + ], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:002" + ], + "evidence_indexes": [ + "E-014" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "structure_ids": [ + "LES-020" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-021": { + "issue_cluster_id": "IC-021", + "issue_type": "B4:001", + "issue_label": "hard:B4:001", + "source_bo_ids": [ + "bh28" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:001" + ], + "evidence_indexes": [ + "E-024" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-021" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-022": { + "issue_cluster_id": "IC-022", + "issue_type": "B4:004", + "issue_label": "hard:B4:004", + "source_bo_ids": [ + "bh29" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:004" + ], + "evidence_indexes": [ + "E-014" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-002" + ], + "structure_ids": [ + "LES-022" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-023": { + "issue_cluster_id": "IC-023", + "issue_type": "B1:006", + "issue_label": "hard:B1:006", + "source_bo_ids": [ + "bh3" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:006" + ], + "evidence_indexes": [ + "E-024" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-023" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-024": { + "issue_cluster_id": "IC-024", + "issue_type": "B4:003", + "issue_label": "hard:B4:003", + "source_bo_ids": [ + "bh30" + ], + "candidate_structure_types": [ + "valuation" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:003" + ], + "evidence_indexes": [ + "E-004" + ], + "asset_cluster_ids": [ + "AC-020", + "AC-021" + ], + "structure_ids": [ + "LES-024" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-025": { + "issue_cluster_id": "IC-025", + "issue_type": "B4:005", + "issue_label": "hard:B4:005", + "source_bo_ids": [ + "bh31" + ], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:005" + ], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "structure_ids": [ + "LES-025" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-026": { + "issue_cluster_id": "IC-026", + "issue_type": "B5:002", + "issue_label": "hard:B5:002", + "source_bo_ids": [ + "bh32" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:002" + ], + "evidence_indexes": [ + "E-013" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-026" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-027": { + "issue_cluster_id": "IC-027", + "issue_type": "B5:003", + "issue_label": "hard:B5:003", + "source_bo_ids": [ + "bh33" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:003" + ], + "evidence_indexes": [ + "E-001", + "E-002" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-027" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-028": { + "issue_cluster_id": "IC-028", + "issue_type": "B5:009", + "issue_label": "hard:B5:009", + "source_bo_ids": [ + "bh34" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:009" + ], + "evidence_indexes": [ + "E-027" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-028" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-029": { + "issue_cluster_id": "IC-029", + "issue_type": "B5:004", + "issue_label": "hard:B5:004", + "source_bo_ids": [ + "bh35" + ], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:004" + ], + "evidence_indexes": [ + "E-008", + "E-010" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-029" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-030": { + "issue_cluster_id": "IC-030", + "issue_type": "B5:007", + "issue_label": "hard:B5:007", + "source_bo_ids": [ + "bh36" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:007" + ], + "evidence_indexes": [ + "E-011", + "E-019" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-030" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-031": { + "issue_cluster_id": "IC-031", + "issue_type": "B5:010", + "issue_label": "hard:B5:010", + "source_bo_ids": [ + "bh37" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:010" + ], + "evidence_indexes": [ + "E-028" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-031" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-032": { + "issue_cluster_id": "IC-032", + "issue_type": "B5:006", + "issue_label": "hard:B5:006", + "source_bo_ids": [ + "bh38" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:006" + ], + "evidence_indexes": [ + "E-010" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-032" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-033": { + "issue_cluster_id": "IC-033", + "issue_type": "B5:005", + "issue_label": "hard:B5:005", + "source_bo_ids": [ + "bh39" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:005" + ], + "evidence_indexes": [ + "E-009" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-033" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-034": { + "issue_cluster_id": "IC-034", + "issue_type": "B1:007", + "issue_label": "hard:B1:007", + "source_bo_ids": [ + "bh4" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:007" + ], + "evidence_indexes": [ + "E-024" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-034" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-035": { + "issue_cluster_id": "IC-035", + "issue_type": "B5:012", + "issue_label": "hard:B5:012", + "source_bo_ids": [ + "bh40" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:012" + ], + "evidence_indexes": [ + "E-030" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-035" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-036": { + "issue_cluster_id": "IC-036", + "issue_type": "B5:011", + "issue_label": "hard:B5:011", + "source_bo_ids": [ + "bh41" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:011" + ], + "evidence_indexes": [ + "E-029" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-036" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-037": { + "issue_cluster_id": "IC-037", + "issue_type": "B5:008", + "issue_label": "hard:B5:008", + "source_bo_ids": [ + "bh42" + ], + "candidate_structure_types": [ + "registry_invalidity" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:008" + ], + "evidence_indexes": [ + "E-012" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-037" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-038": { + "issue_cluster_id": "IC-038", + "issue_type": "B5:001", + "issue_label": "hard:B5:001", + "source_bo_ids": [ + "bh43" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:001" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "asset_cluster_ids": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "structure_ids": [ + "LES-038" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-039": { + "issue_cluster_id": "IC-039", + "issue_type": "B5:013", + "issue_label": "hard:B5:013", + "source_bo_ids": [ + "bh44" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:013" + ], + "evidence_indexes": [ + "E-011" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-039" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-040": { + "issue_cluster_id": "IC-040", + "issue_type": "B5:014", + "issue_label": "hard:B5:014", + "source_bo_ids": [ + "bh45" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:014" + ], + "evidence_indexes": [ + "E-006" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-040" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-041": { + "issue_cluster_id": "IC-041", + "issue_type": "B5:015", + "issue_label": "hard:B5:015", + "source_bo_ids": [ + "bh46" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:015" + ], + "evidence_indexes": [ + "E-018", + "E-022" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-041" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-042": { + "issue_cluster_id": "IC-042", + "issue_type": "B5:017", + "issue_label": "hard:B5:017", + "source_bo_ids": [ + "bh47" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:017" + ], + "evidence_indexes": [ + "E-010" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-042" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-043": { + "issue_cluster_id": "IC-043", + "issue_type": "B5:018", + "issue_label": "hard:B5:018", + "source_bo_ids": [ + "bh48" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:018" + ], + "evidence_indexes": [ + "E-011" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-043" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-044": { + "issue_cluster_id": "IC-044", + "issue_type": "B5:016", + "issue_label": "hard:B5:016", + "source_bo_ids": [ + "bh49" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:016" + ], + "evidence_indexes": [ + "E-009" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-044" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-045": { + "issue_cluster_id": "IC-045", + "issue_type": "B1:008", + "issue_label": "hard:B1:008", + "source_bo_ids": [ + "bh5" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:008" + ], + "evidence_indexes": [ + "E-025" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-045" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-046": { + "issue_cluster_id": "IC-046", + "issue_type": "B5:019", + "issue_label": "hard:B5:019", + "source_bo_ids": [ + "bh50" + ], + "candidate_structure_types": [ + "registry_invalidity" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:019" + ], + "evidence_indexes": [ + "E-012" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-046" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-047": { + "issue_cluster_id": "IC-047", + "issue_type": "B1:009", + "issue_label": "hard:B1:009", + "source_bo_ids": [ + "bh6" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:009" + ], + "evidence_indexes": [ + "E-026" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-047" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-048": { + "issue_cluster_id": "IC-048", + "issue_type": "B1:004", + "issue_label": "hard:B1:004", + "source_bo_ids": [ + "bh7" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:004" + ], + "evidence_indexes": [ + "E-020" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-048" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-049": { + "issue_cluster_id": "IC-049", + "issue_type": "B1:002", + "issue_label": "hard:B1:002", + "source_bo_ids": [ + "bh8" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:002" + ], + "evidence_indexes": [ + "E-021" + ], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-049" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "IC-050": { + "issue_cluster_id": "IC-050", + "issue_type": "B1:010", + "issue_label": "hard:B1:010", + "source_bo_ids": [ + "bh9" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:010" + ], + "evidence_indexes": [], + "asset_cluster_ids": [], + "structure_ids": [ + "LES-050" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + } + }, + "by_asset_cluster_id": { + "AC-001": { + "asset_cluster_id": "AC-001", + "canonical_asset_label": "성수동 대지", + "asset_kind": "real_property", + "member_asset_mentions": [ + "성수동 대지" + ], + "source_bo_ids": [ + "bh14", + "bh17" + ], + "evidence_indexes": [ + "E-013", + "E-025", + "E-026", + "E-012" + ], + "issue_cluster_ids": [ + "IC-006", + "IC-009" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "structure_ids": [ + "LES-006", + "LES-009" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": true + }, + "AC-002": { + "asset_cluster_id": "AC-002", + "canonical_asset_label": "신림동 상가", + "asset_kind": "real_property", + "member_asset_mentions": [ + "신림동 상가", + "2021-08-01", + "2022-04-18", + "2024-10-17", + "포천시 임야" + ], + "source_bo_ids": [ + "bh15", + "bh27", + "bh29", + "bh31" + ], + "evidence_indexes": [ + "E-010", + "E-014", + "E-003" + ], + "issue_cluster_ids": [ + "IC-007", + "IC-020", + "IC-022", + "IC-025" + ], + "candidate_structure_types": [ + "secured_debt", + "fraudulent_transfer" + ], + "structure_ids": [ + "LES-007", + "LES-020", + "LES-022", + "LES-025" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "actio_case_signals.target_property_candidates" + ], + "review_required": true + }, + "AC-003": { + "asset_cluster_id": "AC-003", + "canonical_asset_label": "평택시 빌라", + "asset_kind": "unknown", + "member_asset_mentions": [ + "평택시 빌라" + ], + "source_bo_ids": [ + "bh16", + "bh19" + ], + "evidence_indexes": [ + "E-015", + "E-007", + "E-011" + ], + "issue_cluster_ids": [ + "IC-008", + "IC-011" + ], + "candidate_structure_types": [ + "secured_debt", + "money_claim" + ], + "structure_ids": [ + "LES-008", + "LES-011" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": true + }, + "AC-004": { + "asset_cluster_id": "AC-004", + "canonical_asset_label": "흑석동 상가", + "asset_kind": "real_property", + "member_asset_mentions": [ + "흑석동 상가" + ], + "source_bo_ids": [ + "bh20" + ], + "evidence_indexes": [ + "E-029" + ], + "issue_cluster_ids": [ + "IC-013" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "structure_ids": [ + "LES-013" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": true + }, + "AC-005": { + "asset_cluster_id": "AC-005", + "canonical_asset_label": "SCH-016-01", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-01", + "성수동 256 대", + "SCH-016-02", + "성수동 256 대지 및 지상 건물" + ], + "source_bo_ids": [ + "bh21", + "bh22" + ], + "evidence_indexes": [ + "E-005", + "E-006" + ], + "issue_cluster_ids": [ + "IC-014", + "IC-015" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "structure_ids": [ + "LES-014", + "LES-015" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-006": { + "asset_cluster_id": "AC-006", + "canonical_asset_label": "SCH-016-01", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-01", + "성수동 256 대" + ], + "source_bo_ids": [ + "bh21" + ], + "evidence_indexes": [ + "E-005" + ], + "issue_cluster_ids": [ + "IC-014" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "structure_ids": [ + "LES-014" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-007": { + "asset_cluster_id": "AC-007", + "canonical_asset_label": "SCH-016-01", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-01", + "SCH-016-02", + "성수동 256 대지 및 지상 건물" + ], + "source_bo_ids": [ + "bh22" + ], + "evidence_indexes": [ + "E-006" + ], + "issue_cluster_ids": [ + "IC-015" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "structure_ids": [ + "LES-015" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-008": { + "asset_cluster_id": "AC-008", + "canonical_asset_label": "SCH-016-01", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-01", + "SCH-016-02", + "성수동 256 대지 및 지상 건물" + ], + "source_bo_ids": [ + "bh22" + ], + "evidence_indexes": [ + "E-006" + ], + "issue_cluster_ids": [ + "IC-015" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "structure_ids": [ + "LES-015" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-009": { + "asset_cluster_id": "AC-009", + "canonical_asset_label": "SCH-016-03", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-03", + "신림로 상가, 포천시 임야" + ], + "source_bo_ids": [ + "bh23" + ], + "evidence_indexes": [ + "E-004" + ], + "issue_cluster_ids": [ + "IC-016" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "structure_ids": [ + "LES-016" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-010": { + "asset_cluster_id": "AC-010", + "canonical_asset_label": "SCH-016-03", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-03", + "신림로 상가, 포천시 임야" + ], + "source_bo_ids": [ + "bh23" + ], + "evidence_indexes": [ + "E-004" + ], + "issue_cluster_ids": [ + "IC-016" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "structure_ids": [ + "LES-016" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-011": { + "asset_cluster_id": "AC-011", + "canonical_asset_label": "SCH-016-04", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-04", + "평택시 서정3길 123 서정빌라 101호", + "서정빌라 101호" + ], + "source_bo_ids": [ + "bh24", + "bh25" + ], + "evidence_indexes": [ + "E-011", + "E-023" + ], + "issue_cluster_ids": [ + "IC-017", + "IC-018" + ], + "candidate_structure_types": [ + "secured_debt", + "land_use_gain" + ], + "structure_ids": [ + "LES-017", + "LES-018" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-012": { + "asset_cluster_id": "AC-012", + "canonical_asset_label": "SCH-016-04", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-04", + "평택시 서정3길 123 서정빌라 101호" + ], + "source_bo_ids": [ + "bh24" + ], + "evidence_indexes": [ + "E-011" + ], + "issue_cluster_ids": [ + "IC-017" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "structure_ids": [ + "LES-017" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-013": { + "asset_cluster_id": "AC-013", + "canonical_asset_label": "SCH-016-04", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-04", + "서정빌라 101호" + ], + "source_bo_ids": [ + "bh25" + ], + "evidence_indexes": [ + "E-023" + ], + "issue_cluster_ids": [ + "IC-018" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "structure_ids": [ + "LES-018" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-014": { + "asset_cluster_id": "AC-014", + "canonical_asset_label": "SCH-016-05", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-05", + "흑석동 201호" + ], + "source_bo_ids": [ + "bh26" + ], + "evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "issue_cluster_ids": [ + "IC-019" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "structure_ids": [ + "LES-019" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-015": { + "asset_cluster_id": "AC-015", + "canonical_asset_label": "SCH-016-05", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-05", + "흑석동 201호" + ], + "source_bo_ids": [ + "bh26" + ], + "evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "issue_cluster_ids": [ + "IC-019" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "structure_ids": [ + "LES-019" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": true + }, + "AC-016": { + "asset_cluster_id": "AC-016", + "canonical_asset_label": "신림동 상가", + "asset_kind": "unknown", + "member_asset_mentions": [ + "신림동 상가", + "2021-08-01", + "2022-04-18", + "2024-10-17", + "포천시 임야" + ], + "source_bo_ids": [ + "bh27", + "bh29", + "bh31" + ], + "evidence_indexes": [ + "E-014", + "E-003", + "E-010" + ], + "issue_cluster_ids": [ + "IC-020", + "IC-022", + "IC-025" + ], + "candidate_structure_types": [ + "fraudulent_transfer", + "secured_debt" + ], + "structure_ids": [ + "LES-020", + "LES-022", + "LES-025" + ], + "basis_fields": [ + "actio_case_signals.target_property_candidates", + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": true + }, + "AC-017": { + "asset_cluster_id": "AC-017", + "canonical_asset_label": "신림동 상가", + "asset_kind": "unknown", + "member_asset_mentions": [ + "신림동 상가", + "2021-08-01", + "2022-04-18", + "2024-10-17" + ], + "source_bo_ids": [ + "bh29" + ], + "evidence_indexes": [ + "E-014" + ], + "issue_cluster_ids": [ + "IC-022" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "structure_ids": [ + "LES-022" + ], + "basis_fields": [ + "actio_case_signals.target_property_candidates", + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": true + }, + "AC-018": { + "asset_cluster_id": "AC-018", + "canonical_asset_label": "신림동 상가", + "asset_kind": "unknown", + "member_asset_mentions": [ + "신림동 상가", + "2021-08-01", + "2022-04-18", + "2024-10-17" + ], + "source_bo_ids": [ + "bh29" + ], + "evidence_indexes": [ + "E-014" + ], + "issue_cluster_ids": [ + "IC-022" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "structure_ids": [ + "LES-022" + ], + "basis_fields": [ + "actio_case_signals.target_property_candidates", + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": true + }, + "AC-019": { + "asset_cluster_id": "AC-019", + "canonical_asset_label": "신림동 상가", + "asset_kind": "unknown", + "member_asset_mentions": [ + "신림동 상가", + "2021-08-01", + "2022-04-18", + "2024-10-17" + ], + "source_bo_ids": [ + "bh29" + ], + "evidence_indexes": [ + "E-014" + ], + "issue_cluster_ids": [ + "IC-022" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "structure_ids": [ + "LES-022" + ], + "basis_fields": [ + "actio_case_signals.target_property_candidates", + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": true + }, + "AC-020": { + "asset_cluster_id": "AC-020", + "canonical_asset_label": "신림동 상가", + "asset_kind": "unknown", + "member_asset_mentions": [ + "신림동 상가", + "포천시 임야" + ], + "source_bo_ids": [ + "bh30" + ], + "evidence_indexes": [ + "E-004" + ], + "issue_cluster_ids": [ + "IC-024" + ], + "candidate_structure_types": [ + "valuation" + ], + "structure_ids": [ + "LES-024" + ], + "basis_fields": [ + "actio_case_signals.target_property_candidates", + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": true + }, + "AC-021": { + "asset_cluster_id": "AC-021", + "canonical_asset_label": "신림동 상가", + "asset_kind": "unknown", + "member_asset_mentions": [ + "신림동 상가", + "포천시 임야" + ], + "source_bo_ids": [ + "bh30" + ], + "evidence_indexes": [ + "E-004" + ], + "issue_cluster_ids": [ + "IC-024" + ], + "candidate_structure_types": [ + "valuation" + ], + "structure_ids": [ + "LES-024" + ], + "basis_fields": [ + "actio_case_signals.target_property_candidates", + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": true + }, + "AC-022": { + "asset_cluster_id": "AC-022", + "canonical_asset_label": "성수동 대지", + "asset_kind": "real_property", + "member_asset_mentions": [ + "성수동 대지", + "서울 성동구 성수동 256", + "1. 서울 성동구 성수동 256 대 330㎡", + "성수동 건물", + "서울 성동구 성수동 256 상가", + "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "신림동 상가", + "서울 관악구 신림로 115", + "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123", + "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "흑석동 상가", + "서울 동작구 흑석로 80", + "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물" + ], + "source_bo_ids": [ + "bh43" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "structure_ids": [ + "LES-038" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_alias" + ], + "review_required": true + }, + "AC-023": { + "asset_cluster_id": "AC-023", + "canonical_asset_label": "성수동 대지", + "asset_kind": "real_property", + "member_asset_mentions": [ + "성수동 대지", + "서울 성동구 성수동 256", + "1. 서울 성동구 성수동 256 대 330㎡", + "성수동 건물", + "서울 성동구 성수동 256 상가", + "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "신림동 상가", + "서울 관악구 신림로 115", + "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123", + "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "흑석동 상가", + "서울 동작구 흑석로 80", + "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물" + ], + "source_bo_ids": [ + "bh43" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "structure_ids": [ + "LES-038" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_alias" + ], + "review_required": true + }, + "AC-024": { + "asset_cluster_id": "AC-024", + "canonical_asset_label": "성수동 대지", + "asset_kind": "real_property", + "member_asset_mentions": [ + "성수동 대지", + "서울 성동구 성수동 256", + "1. 서울 성동구 성수동 256 대 330㎡", + "성수동 건물", + "서울 성동구 성수동 256 상가", + "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "신림동 상가", + "서울 관악구 신림로 115", + "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123", + "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "흑석동 상가", + "서울 동작구 흑석로 80", + "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물" + ], + "source_bo_ids": [ + "bh43" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "structure_ids": [ + "LES-038" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_alias" + ], + "review_required": true + }, + "AC-025": { + "asset_cluster_id": "AC-025", + "canonical_asset_label": "성수동 대지", + "asset_kind": "real_property", + "member_asset_mentions": [ + "성수동 대지", + "서울 성동구 성수동 256", + "1. 서울 성동구 성수동 256 대 330㎡", + "성수동 건물", + "서울 성동구 성수동 256 상가", + "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "신림동 상가", + "서울 관악구 신림로 115", + "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123", + "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "흑석동 상가", + "서울 동작구 흑석로 80", + "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물" + ], + "source_bo_ids": [ + "bh43" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "structure_ids": [ + "LES-038" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_alias" + ], + "review_required": true + }, + "AC-026": { + "asset_cluster_id": "AC-026", + "canonical_asset_label": "성수동 대지", + "asset_kind": "real_property", + "member_asset_mentions": [ + "성수동 대지", + "서울 성동구 성수동 256", + "1. 서울 성동구 성수동 256 대 330㎡", + "성수동 건물", + "서울 성동구 성수동 256 상가", + "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "신림동 상가", + "서울 관악구 신림로 115", + "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123", + "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "흑석동 상가", + "서울 동작구 흑석로 80", + "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물" + ], + "source_bo_ids": [ + "bh43" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "structure_ids": [ + "LES-038" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_alias" + ], + "review_required": true + } + }, + "by_evidence_index": { + "E-007": { + "evidence_index": "E-007", + "source_title": "공사도급계약서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh1", + "bh19" + ], + "structure_ids": [ + "LES-001", + "LES-011" + ], + "issue_cluster_ids": [ + "IC-001", + "IC-011" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "candidate_structure_types": [ + "money_claim" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B1:011", + "B2:005" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-003": { + "evidence_index": "E-003", + "source_title": "각서 및 금전소비대차계약서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh10", + "bh31" + ], + "structure_ids": [ + "LES-002", + "LES-025" + ], + "issue_cluster_ids": [ + "IC-002", + "IC-025" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "candidate_structure_types": [ + "money_claim", + "fraudulent_transfer" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B1:013", + "actio-signal-005", + "B4:005" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-010": { + "evidence_index": "E-010", + "source_title": "내용증명에 대한 답신", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh10", + "bh15", + "bh31", + "bh35", + "bh38", + "bh47" + ], + "structure_ids": [ + "LES-002", + "LES-007", + "LES-025", + "LES-029", + "LES-032", + "LES-042" + ], + "issue_cluster_ids": [ + "IC-002", + "IC-007", + "IC-025", + "IC-029", + "IC-032", + "IC-042" + ], + "asset_cluster_ids": [ + "AC-002", + "AC-016" + ], + "candidate_structure_types": [ + "money_claim", + "secured_debt", + "fraudulent_transfer", + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B1:013", + "B2:004", + "actio-signal-005", + "B4:005", + "B5:004", + "B5:006", + "B5:017" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-027": { + "evidence_index": "E-027", + "source_title": "최고 통지서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh11", + "bh2", + "bh34" + ], + "structure_ids": [ + "LES-003", + "LES-012", + "LES-028" + ], + "issue_cluster_ids": [ + "IC-003", + "IC-012", + "IC-028" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "money_claim", + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B1:003", + "B1:001", + "B5:009" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-028": { + "evidence_index": "E-028", + "source_title": "통지서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh12", + "bh37" + ], + "structure_ids": [ + "LES-004", + "LES-031" + ], + "issue_cluster_ids": [ + "IC-004", + "IC-031" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "money_claim", + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B1:005", + "B5:010" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-011": { + "evidence_index": "E-011", + "source_title": "답변서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh13", + "bh19", + "bh24", + "bh36", + "bh44", + "bh48" + ], + "structure_ids": [ + "LES-005", + "LES-011", + "LES-017", + "LES-030", + "LES-039", + "LES-043" + ], + "issue_cluster_ids": [ + "IC-005", + "IC-011", + "IC-017", + "IC-030", + "IC-039", + "IC-043" + ], + "asset_cluster_ids": [ + "AC-003", + "AC-011", + "AC-012" + ], + "candidate_structure_types": [ + "money_claim", + "secured_debt", + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B1:012", + "B2:005", + "B3:005", + "B5:007", + "B5:013", + "B5:018" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-013": { + "evidence_index": "E-013", + "source_title": "등기사항전부증명서(말소사항 포함)-토지", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh14", + "bh17", + "bh32", + "bh43" + ], + "structure_ids": [ + "LES-006", + "LES-009", + "LES-026", + "LES-038" + ], + "issue_cluster_ids": [ + "IC-006", + "IC-009", + "IC-026", + "IC-038" + ], + "asset_cluster_ids": [ + "AC-001", + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "candidate_structure_types": [ + "secured_debt", + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B2:001", + "B2:003", + "B5:002", + "B5:001" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-025": { + "evidence_index": "E-025", + "source_title": "차용증", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh14", + "bh5" + ], + "structure_ids": [ + "LES-006", + "LES-045" + ], + "issue_cluster_ids": [ + "IC-006", + "IC-045" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "candidate_structure_types": [ + "secured_debt", + "money_claim" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B2:001", + "B1:008" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-026": { + "evidence_index": "E-026", + "source_title": "차용증", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh14", + "bh6" + ], + "structure_ids": [ + "LES-006", + "LES-047" + ], + "issue_cluster_ids": [ + "IC-006", + "IC-047" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "candidate_structure_types": [ + "secured_debt", + "money_claim" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B2:001", + "B1:009" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-014": { + "evidence_index": "E-014", + "source_title": "등기사항전부증명서 (말소사항 포함) -건물", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh15", + "bh27", + "bh29", + "bh43" + ], + "structure_ids": [ + "LES-007", + "LES-020", + "LES-022", + "LES-038" + ], + "issue_cluster_ids": [ + "IC-007", + "IC-020", + "IC-022", + "IC-038" + ], + "asset_cluster_ids": [ + "AC-002", + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "candidate_structure_types": [ + "secured_debt", + "fraudulent_transfer", + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B2:004", + "actio-signal-002", + "B4:002", + "actio-signal-004", + "B4:004", + "B5:001" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-015": { + "evidence_index": "E-015", + "source_title": "등기사항전부증명서(말소사항 포함)-집합건물", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh16", + "bh43" + ], + "structure_ids": [ + "LES-008", + "LES-038" + ], + "issue_cluster_ids": [ + "IC-008", + "IC-038" + ], + "asset_cluster_ids": [ + "AC-003", + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "candidate_structure_types": [ + "secured_debt", + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B2:006", + "B5:001" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-012": { + "evidence_index": "E-012", + "source_title": "등기말소 신청에 대한 답신", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh17", + "bh42", + "bh50" + ], + "structure_ids": [ + "LES-009", + "LES-037", + "LES-046" + ], + "issue_cluster_ids": [ + "IC-009", + "IC-037", + "IC-046" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "candidate_structure_types": [ + "secured_debt", + "registry_invalidity" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B2:003", + "B5:008", + "B5:019" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-021": { + "evidence_index": "E-021", + "source_title": "이행각서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh2", + "bh8" + ], + "structure_ids": [ + "LES-012", + "LES-049" + ], + "issue_cluster_ids": [ + "IC-012", + "IC-049" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "money_claim" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B1:001", + "B1:002" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-029": { + "evidence_index": "E-029", + "source_title": "통지서_이수인_윤건우", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh20", + "bh41" + ], + "structure_ids": [ + "LES-013", + "LES-036" + ], + "issue_cluster_ids": [ + "IC-013", + "IC-036" + ], + "asset_cluster_ids": [ + "AC-004" + ], + "candidate_structure_types": [ + "secured_debt", + "money_claim" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B2:007", + "B5:011" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-005": { + "evidence_index": "E-005", + "source_title": "감정평가 의견서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh21" + ], + "structure_ids": [ + "LES-014" + ], + "issue_cluster_ids": [ + "IC-014" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B3:002" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-006": { + "evidence_index": "E-006", + "source_title": "계약서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh22", + "bh45" + ], + "structure_ids": [ + "LES-015", + "LES-040" + ], + "issue_cluster_ids": [ + "IC-015", + "IC-040" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-007", + "AC-008" + ], + "candidate_structure_types": [ + "land_use_gain", + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B3:004", + "B5:014" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-004": { + "evidence_index": "E-004", + "source_title": "감정평가서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh23", + "bh30" + ], + "structure_ids": [ + "LES-016", + "LES-024" + ], + "issue_cluster_ids": [ + "IC-016", + "IC-024" + ], + "asset_cluster_ids": [ + "AC-009", + "AC-010", + "AC-020", + "AC-021" + ], + "candidate_structure_types": [ + "land_use_gain", + "valuation" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B3:001", + "actio-signal-003", + "B4:003" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-023": { + "evidence_index": "E-023", + "source_title": "임료시세확인서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh25" + ], + "structure_ids": [ + "LES-018" + ], + "issue_cluster_ids": [ + "IC-018" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-013" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B3:003" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-017": { + "evidence_index": "E-017", + "source_title": "부동산 매매계약서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh26" + ], + "structure_ids": [ + "LES-019" + ], + "issue_cluster_ids": [ + "IC-019" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B3:006" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-018": { + "evidence_index": "E-018", + "source_title": "사업자등록증", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh26", + "bh46" + ], + "structure_ids": [ + "LES-019", + "LES-041" + ], + "issue_cluster_ids": [ + "IC-019", + "IC-041" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "candidate_structure_types": [ + "land_use_gain", + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B3:006", + "B5:015" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-022": { + "evidence_index": "E-022", + "source_title": "임대차계약서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh26", + "bh46" + ], + "structure_ids": [ + "LES-019", + "LES-041" + ], + "issue_cluster_ids": [ + "IC-019", + "IC-041" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "candidate_structure_types": [ + "land_use_gain", + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B3:006", + "B5:015" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-024": { + "evidence_index": "E-024", + "source_title": "차용증", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh28", + "bh3", + "bh4" + ], + "structure_ids": [ + "LES-021", + "LES-023", + "LES-034" + ], + "issue_cluster_ids": [ + "IC-021", + "IC-023", + "IC-034" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "money_claim" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "actio-signal-001", + "B4:001", + "B1:006", + "B1:007" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-001": { + "evidence_index": "E-001", + "source_title": "가정법원심판_강호연", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh33" + ], + "structure_ids": [ + "LES-027" + ], + "issue_cluster_ids": [ + "IC-027" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B5:003" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-002": { + "evidence_index": "E-002", + "source_title": "가족관계증명서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh33" + ], + "structure_ids": [ + "LES-027" + ], + "issue_cluster_ids": [ + "IC-027" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B5:003" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-008": { + "evidence_index": "E-008", + "source_title": "내용증명", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh35" + ], + "structure_ids": [ + "LES-029" + ], + "issue_cluster_ids": [ + "IC-029" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B5:004" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-019": { + "evidence_index": "E-019", + "source_title": "소멸청구 통지서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh36" + ], + "structure_ids": [ + "LES-030" + ], + "issue_cluster_ids": [ + "IC-030" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "secured_debt" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B5:007" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-009": { + "evidence_index": "E-009", + "source_title": "내용증명", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh39", + "bh49" + ], + "structure_ids": [ + "LES-033", + "LES-044" + ], + "issue_cluster_ids": [ + "IC-033", + "IC-044" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B5:005", + "B5:016" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-030": { + "evidence_index": "E-030", + "source_title": "해제 통지서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh40" + ], + "structure_ids": [ + "LES-035" + ], + "issue_cluster_ids": [ + "IC-035" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B5:012" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-016": { + "evidence_index": "E-016", + "source_title": "별지 목록", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh43" + ], + "structure_ids": [ + "LES-038" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "asset_cluster_ids": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B5:001" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-020": { + "evidence_index": "E-020", + "source_title": "영업양도 계약서", + "priority_class": null, + "authentication_status": null, + "source_bo_ids": [ + "bh7" + ], + "structure_ids": [ + "LES-048" + ], + "issue_cluster_ids": [ + "IC-048" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "money_claim" + ], + "related_event_candidate_ids": [], + "related_signal_refs": [ + "B1:004" + ], + "support_role": "primary", + "gap_flags": [ + "missing_evidence_excerpt" + ] + } + }, + "by_candidate_structure_type": { + "money_claim": { + "candidate_structure_type": "money_claim", + "structure_ids": [ + "LES-001", + "LES-002", + "LES-003", + "LES-004", + "LES-005", + "LES-011", + "LES-012", + "LES-021", + "LES-023", + "LES-034", + "LES-036", + "LES-042", + "LES-045", + "LES-047", + "LES-048", + "LES-049", + "LES-050" + ], + "source_bo_ids": [ + "bh1", + "bh10", + "bh11", + "bh12", + "bh13", + "bh19", + "bh2", + "bh28", + "bh3", + "bh4", + "bh41", + "bh47", + "bh5", + "bh6", + "bh7", + "bh8", + "bh9" + ], + "issue_cluster_ids": [ + "IC-001", + "IC-002", + "IC-003", + "IC-004", + "IC-005", + "IC-011", + "IC-012", + "IC-021", + "IC-023", + "IC-034", + "IC-036", + "IC-042", + "IC-045", + "IC-047", + "IC-048", + "IC-049", + "IC-050" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_indexes": [ + "E-007", + "E-003", + "E-010", + "E-027", + "E-028", + "E-011", + "E-021", + "E-024", + "E-029", + "E-025", + "E-026", + "E-020" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:011", + "B1:013", + "B1:003", + "B1:005", + "B1:012", + "B2:005", + "B1:001", + "B4:001", + "B1:006", + "B1:007", + "B5:011", + "B5:017", + "B1:008", + "B1:009", + "B1:004", + "B1:002", + "B1:010" + ], + "related_actio_signal_ids": [ + "actio-signal-001" + ], + "downstream_fact_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures", + "legal_calculation_object", + "actio_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "secured_debt": { + "candidate_structure_type": "secured_debt", + "structure_ids": [ + "LES-006", + "LES-007", + "LES-008", + "LES-009", + "LES-010", + "LES-013", + "LES-017", + "LES-022", + "LES-030", + "LES-039" + ], + "source_bo_ids": [ + "bh14", + "bh15", + "bh16", + "bh17", + "bh18", + "bh20", + "bh24", + "bh29", + "bh36", + "bh44" + ], + "issue_cluster_ids": [ + "IC-006", + "IC-007", + "IC-008", + "IC-009", + "IC-010", + "IC-013", + "IC-017", + "IC-022", + "IC-030", + "IC-039" + ], + "asset_cluster_ids": [ + "AC-001", + "AC-002", + "AC-003", + "AC-004", + "AC-011", + "AC-012", + "AC-016", + "AC-017", + "AC-018", + "AC-019" + ], + "evidence_indexes": [ + "E-013", + "E-025", + "E-026", + "E-010", + "E-014", + "E-015", + "E-012", + "E-029", + "E-011", + "E-019" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:001", + "B2:004", + "B2:006", + "B2:003", + "B2:002", + "B2:007", + "B3:005", + "B4:004", + "B5:007", + "B5:013" + ], + "related_actio_signal_ids": [ + "actio-signal-004" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles", + "legal_calculation_object", + "linked_actio_structures", + "actio_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "land_use_gain": { + "candidate_structure_type": "land_use_gain", + "structure_ids": [ + "LES-014", + "LES-015", + "LES-016", + "LES-018", + "LES-019" + ], + "source_bo_ids": [ + "bh21", + "bh22", + "bh23", + "bh25", + "bh26" + ], + "issue_cluster_ids": [ + "IC-014", + "IC-015", + "IC-016", + "IC-018", + "IC-019" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006", + "AC-007", + "AC-008", + "AC-009", + "AC-010", + "AC-011", + "AC-013", + "AC-014", + "AC-015" + ], + "evidence_indexes": [ + "E-005", + "E-006", + "E-004", + "E-023", + "E-017", + "E-018", + "E-022" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:002", + "B3:004", + "B3:001", + "B3:003", + "B3:006" + ], + "related_actio_signal_ids": [], + "downstream_fact_roles": [ + "legal_calculation_object", + "legal_effect_roles", + "linked_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "fraudulent_transfer": { + "candidate_structure_type": "fraudulent_transfer", + "structure_ids": [ + "LES-020", + "LES-025", + "LES-029" + ], + "source_bo_ids": [ + "bh27", + "bh31", + "bh35" + ], + "issue_cluster_ids": [ + "IC-020", + "IC-025", + "IC-029" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_indexes": [ + "E-014", + "E-003", + "E-010", + "E-008" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:002", + "B4:005", + "B5:004" + ], + "related_actio_signal_ids": [ + "actio-signal-002", + "actio-signal-005" + ], + "downstream_fact_roles": [ + "actio_roles", + "legal_effect_roles", + "linked_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "valuation": { + "candidate_structure_type": "valuation", + "structure_ids": [ + "LES-024" + ], + "source_bo_ids": [ + "bh30" + ], + "issue_cluster_ids": [ + "IC-024" + ], + "asset_cluster_ids": [ + "AC-020", + "AC-021" + ], + "evidence_indexes": [ + "E-004" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:003" + ], + "related_actio_signal_ids": [ + "actio-signal-003" + ], + "downstream_fact_roles": [ + "legal_effect_roles", + "actio_roles", + "legal_calculation_object" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "succession_notice_lien_asset_defense": { + "candidate_structure_type": "succession_notice_lien_asset_defense", + "structure_ids": [ + "LES-026", + "LES-027", + "LES-028", + "LES-031", + "LES-032", + "LES-033", + "LES-035", + "LES-038", + "LES-040", + "LES-041", + "LES-043", + "LES-044" + ], + "source_bo_ids": [ + "bh32", + "bh33", + "bh34", + "bh37", + "bh38", + "bh39", + "bh40", + "bh43", + "bh45", + "bh46", + "bh48", + "bh49" + ], + "issue_cluster_ids": [ + "IC-026", + "IC-027", + "IC-028", + "IC-031", + "IC-032", + "IC-033", + "IC-035", + "IC-038", + "IC-040", + "IC-041", + "IC-043", + "IC-044" + ], + "asset_cluster_ids": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "evidence_indexes": [ + "E-013", + "E-001", + "E-002", + "E-027", + "E-028", + "E-010", + "E-009", + "E-030", + "E-014", + "E-015", + "E-016", + "E-006", + "E-018", + "E-022", + "E-011" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:002", + "B5:003", + "B5:009", + "B5:010", + "B5:006", + "B5:005", + "B5:012", + "B5:001", + "B5:014", + "B5:015", + "B5:018", + "B5:016" + ], + "related_actio_signal_ids": [], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "registry_invalidity": { + "candidate_structure_type": "registry_invalidity", + "structure_ids": [ + "LES-037", + "LES-046" + ], + "source_bo_ids": [ + "bh42", + "bh50" + ], + "issue_cluster_ids": [ + "IC-037", + "IC-046" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-012" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:008", + "B5:019" + ], + "related_actio_signal_ids": [], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + } + } + } + }, + "signal_hints_by_bo_id": { + "bh35": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-008", + "E-010" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"actio_related\", \"liability_group_id\": \"B5:004\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh35\"], \"related_evidence_indexes\": [\"E-008\", \"E-010\"]}" + } + ], + "legal_effect": [] + }, + "bh28": { + "actio": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-001", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-024" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "debtor_act", + "value_candidate": "강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생", + "date_candidate": null, + "related_bo_ids": [ + "bh28" + ], + "related_evidence_indexes": [ + "E-024" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh28\"], \"related_evidence_indexes\": [\"E-024\"], \"role\": \"debtor_act\", \"value_candidate\": \"강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생\"}], \"actio_scope\": [\"preserved_claim\"], \"preserved_claim_candidates\": [{\"bo_id\": null, \"claim_date_candidate\": null, \"claim_type_candidate\": null, \"creditor_candidate\": null, \"debtor_candidate\": null, \"related_evidence_indexes\": [\"E-024\"]}], \"related_bo_ids\": [\"bh28\"], \"related_evidence_indexes\": [\"E" + } + ], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-024" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"actio_related\", \"liability_group_id\": \"B4:001\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh28\"], \"related_evidence_indexes\": [\"E-024\"]}" + } + ], + "legal_effect": [] + }, + "bh32": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-013" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:002\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh32\"], \"related_evidence_indexes\": [\"E-013\"]}" + } + ], + "legal_effect": [] + }, + "bh24": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"secured_debt\", \"liability_group_id\": \"B3:005\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh24\"], \"related_evidence_indexes\": [\"E-011\"]}" + } + ], + "legal_effect": [] + }, + "bh18": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"secured_debt\", \"liability_group_id\": \"B2:002\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh18\"], \"related_evidence_indexes\": []}" + } + ], + "legal_effect": [] + }, + "bh41": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-029" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"money_claim\", \"liability_group_id\": \"B5:011\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh41\"], \"related_evidence_indexes\": [\"E-029\"]}" + } + ], + "legal_effect": [] + }, + "bh29": { + "actio": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-004", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "remedy_or_cap", + "value_candidate": "원물반환 불가", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "remedy_or_cap", + "value_candidate": "가액배상", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2021-08-01', 'event': '나임오 1번 근저당 설정(5000만원)'}", + "date_candidate": "2021-08-01", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2022-04-18', 'event': '강용일 2번 근저당 설정(1.5억원)'}", + "date_candidate": "2022-04-18", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2024-10-17', 'event': '강용일 2번 근저당 말소(오민한 대위변제)'}", + "date_candidate": "2024-10-17", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh29\"], \"related_evidence_indexes\": [\"E-014\"], \"role\": \"remedy_or_cap\", \"value_candidate\": \"원물반환 불가\"}, {\"date_candidate\": null, \"related_bo_ids\": [\"bh29\"], \"related_evidence_indexes\": [\"E-014\"], \"role\": \"remedy_or_cap\", \"value_candidate\": \"가액배상\"}, {\"date_candidate\": \"2021-08-01\", \"related_bo_ids\": [\"bh29\"], \"related_evidence_indexes\": [\"E-014\"], \"role\": \"encumbrance\", \"value_candidate\": \"{'date': '2021-08-01', 'event': '나임오 1번 근저" + } + ], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"actio_related\", \"liability_group_id\": \"B4:004\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh29\"], \"related_evidence_indexes\": [\"E-014\"]}" + } + ], + "legal_effect": [] + }, + "bh20": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-029" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"secured_debt\", \"liability_group_id\": \"B2:007\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh20\"], \"related_evidence_indexes\": [\"E-029\"]}" + } + ], + "legal_effect": [] + }, + "bh40": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-030" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:012\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh40\"], \"related_evidence_indexes\": [\"E-030\"]}" + } + ], + "legal_effect": [] + }, + "bh2": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-021", + "E-027" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:001\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh2\"], \"related_evidence_indexes\": [\"E-021\", \"E-027\"]}" + } + ], + "legal_effect": [] + }, + "bh4": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-024" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:007\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh4\"], \"related_evidence_indexes\": [\"E-024\"]}" + } + ], + "legal_effect": [] + }, + "bh21": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-005" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"land_use_gain\", \"liability_group_id\": \"B3:002\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh21\"], \"related_evidence_indexes\": [\"E-005\"]}" + } + ], + "legal_effect": [] + }, + "bh5": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-025" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:008\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh5\"], \"related_evidence_indexes\": [\"E-025\"]}" + } + ], + "legal_effect": [] + }, + "bh27": { + "actio": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-002", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "beneficiary", + "value_candidate": "{}", + "date_candidate": null, + "related_bo_ids": [ + "bh27" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh27\"], \"related_evidence_indexes\": [\"E-014\"], \"role\": \"beneficiary\", \"value_candidate\": \"{}\"}], \"actio_scope\": [\"fraudulent_act\", \"beneficiary_or_transferee\", \"target_asset\"], \"preserved_claim_candidates\": [{\"bo_id\": \"bh27\", \"claim_date_candidate\": null, \"claim_type_candidate\": null, \"creditor_candidate\": null, \"debtor_candidate\": null, \"related_evidence_indexes\": [\"E-014\"]}], \"related_bo_ids\": [\"bh27\"], \"related_evidence_indexe" + } + ], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"actio_related\", \"liability_group_id\": \"B4:002\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh27\"], \"related_evidence_indexes\": [\"E-014\"]}" + } + ], + "legal_effect": [] + }, + "bh44": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"secured_debt\", \"liability_group_id\": \"B5:013\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh44\"], \"related_evidence_indexes\": [\"E-011\"]}" + } + ], + "legal_effect": [] + }, + "bh12": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-028" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:005\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh12\"], \"related_evidence_indexes\": [\"E-028\"]}" + } + ], + "legal_effect": [] + }, + "bh10": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:013\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh10\"], \"related_evidence_indexes\": [\"E-003\", \"E-010\"]}" + } + ], + "legal_effect": [] + }, + "bh13": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:012\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh13\"], \"related_evidence_indexes\": [\"E-011\"]}" + } + ], + "legal_effect": [] + }, + "bh14": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"secured_debt\", \"liability_group_id\": \"B2:001\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh14\"], \"related_evidence_indexes\": [\"E-013\", \"E-025\", \"E-026\"]}" + } + ], + "legal_effect": [] + }, + "bh48": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:018\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh48\"], \"related_evidence_indexes\": [\"E-011\"]}" + } + ], + "legal_effect": [] + }, + "bh25": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-023" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"land_use_gain\", \"liability_group_id\": \"B3:003\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh25\"], \"related_evidence_indexes\": [\"E-023\"]}" + } + ], + "legal_effect": [] + }, + "bh23": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-004" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"land_use_gain\", \"liability_group_id\": \"B3:001\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh23\"], \"related_evidence_indexes\": [\"E-004\"]}" + } + ], + "legal_effect": [] + }, + "bh33": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-001", + "E-002" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:003\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh33\"], \"related_evidence_indexes\": [\"E-001\", \"E-002\"]}" + } + ], + "legal_effect": [] + }, + "bh47": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-010" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"money_claim\", \"liability_group_id\": \"B5:017\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh47\"], \"related_evidence_indexes\": [\"E-010\"]}" + } + ], + "legal_effect": [] + }, + "bh7": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-020" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:004\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh7\"], \"related_evidence_indexes\": [\"E-020\"]}" + } + ], + "legal_effect": [] + }, + "bh39": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-009" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:005\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh39\"], \"related_evidence_indexes\": [\"E-009\"]}" + } + ], + "legal_effect": [] + }, + "bh16": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-015" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"secured_debt\", \"liability_group_id\": \"B2:006\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh16\"], \"related_evidence_indexes\": [\"E-015\"]}" + } + ], + "legal_effect": [] + }, + "bh26": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"land_use_gain\", \"liability_group_id\": \"B3:006\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh26\"], \"related_evidence_indexes\": [\"E-017\", \"E-018\", \"E-022\"]}" + } + ], + "legal_effect": [] + }, + "bh31": { + "actio": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-005", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "defense", + "value_candidate": "{'defense_type': '계약명의신탁', 'content': '오국한 명의 취득이 수익자 자금에 의한 계약명의신탁이므로 사해행위가 아니라는 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "role": "defense", + "value_candidate": "{'defense_type': '배상금감액', 'content': '수익자가 제공한 매수자금 4000만원 및 대여금 1000만원 참작 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "role": "defense", + "value_candidate": "{'defense_type': '상계항변', 'content': '오민한의 5000만원 채권으로 강용원 채권을 상계한다는 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh31\"], \"related_evidence_indexes\": [\"E-003\", \"E-010\"], \"role\": \"defense\", \"value_candidate\": \"{'defense_type': '계약명의신탁', 'content': '오국한 명의 취득이 수익자 자금에 의한 계약명의신탁이므로 사해행위가 아니라는 주장'}\"}, {\"date_candidate\": null, \"related_bo_ids\": [\"bh31\"], \"related_evidence_indexes\": [\"E-003\", \"E-010\"], \"role\": \"defense\", \"value_candidate\": \"{'defense_type': '배상금감액', 'content': '수익자가 제공한 매수자금 4000만원 및 대여금 1000만원 참작 주장'}\"}, {\"date_candidate\": null, " + } + ], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"actio_related\", \"liability_group_id\": \"B4:005\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh31\"], \"related_evidence_indexes\": [\"E-003\", \"E-010\"]}" + } + ], + "legal_effect": [] + }, + "bh46": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-018", + "E-022" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:015\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh46\"], \"related_evidence_indexes\": [\"E-018\", \"E-022\"]}" + } + ], + "legal_effect": [] + }, + "bh34": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-027" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:009\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh34\"], \"related_evidence_indexes\": [\"E-027\"]}" + } + ], + "legal_effect": [] + }, + "bh43": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:001\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh43\"], \"related_evidence_indexes\": [\"E-013\", \"E-014\", \"E-015\", \"E-016\"]}" + } + ], + "legal_effect": [] + }, + "bh19": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-007", + "E-011" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"money_claim\", \"liability_group_id\": \"B2:005\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh19\"], \"related_evidence_indexes\": [\"E-007\", \"E-011\"]}" + } + ], + "legal_effect": [] + }, + "bh15": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-010", + "E-014" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"registry_invalidity\", \"liability_group_id\": \"B2:004\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh15\"], \"related_evidence_indexes\": [\"E-010\", \"E-014\"]}" + } + ], + "legal_effect": [] + }, + "bh17": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-012", + "E-013" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"registry_invalidity\", \"liability_group_id\": \"B2:003\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh17\"], \"related_evidence_indexes\": [\"E-012\", \"E-013\"]}" + } + ], + "legal_effect": [] + }, + "bh11": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-027" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:003\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh11\"], \"related_evidence_indexes\": [\"E-027\"]}" + } + ], + "legal_effect": [] + }, + "bh22": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-006" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"land_use_gain\", \"liability_group_id\": \"B3:004\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh22\"], \"related_evidence_indexes\": [\"E-006\"]}" + } + ], + "legal_effect": [] + }, + "bh37": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-028" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:010\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh37\"], \"related_evidence_indexes\": [\"E-028\"]}" + } + ], + "legal_effect": [] + }, + "bh38": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-010" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:006\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh38\"], \"related_evidence_indexes\": [\"E-010\"]}" + } + ], + "legal_effect": [] + }, + "bh3": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-024" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:006\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh3\"], \"related_evidence_indexes\": [\"E-024\"]}" + } + ], + "legal_effect": [] + }, + "bh49": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-009" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:016\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh49\"], \"related_evidence_indexes\": [\"E-009\"]}" + } + ], + "legal_effect": [] + }, + "bh8": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-021" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:002\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh8\"], \"related_evidence_indexes\": [\"E-021\"]}" + } + ], + "legal_effect": [] + }, + "bh50": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-012" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"registry_invalidity\", \"liability_group_id\": \"B5:019\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh50\"], \"related_evidence_indexes\": [\"E-012\"]}" + } + ], + "legal_effect": [] + }, + "bh42": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-012" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"registry_invalidity\", \"liability_group_id\": \"B5:008\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh42\"], \"related_evidence_indexes\": [\"E-012\"]}" + } + ], + "legal_effect": [] + }, + "bh9": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:010\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh9\"], \"related_evidence_indexes\": []}" + } + ], + "legal_effect": [] + }, + "bh1": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-007" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:011\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh1\"], \"related_evidence_indexes\": [\"E-007\"]}" + } + ], + "legal_effect": [] + }, + "bh30": { + "actio": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-003", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-004" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "debtor_act", + "value_candidate": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "date_candidate": null, + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh30\"], \"related_evidence_indexes\": [\"E-004\"], \"role\": \"debtor_act\", \"value_candidate\": \"사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)\"}], \"actio_scope\": [\"target_asset\"], \"preserved_claim_candidates\": [{\"bo_id\": \"bh30\", \"claim_date_candidate\": null, \"claim_type_candidate\": null, \"creditor_candidate\": null, \"debtor_candidate\": null, \"related_evidence_indexes\": [\"E-004\"]}], \"related_bo_ids\": [\"bh30\"], \"related_evidence_i" + } + ], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-004" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"actio_related\", \"liability_group_id\": \"B4:003\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh30\"], \"related_evidence_indexes\": [\"E-004\"]}" + } + ], + "legal_effect": [] + }, + "bh45": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-006" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:014\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh45\"], \"related_evidence_indexes\": [\"E-006\"]}" + } + ], + "legal_effect": [] + }, + "bh36": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011", + "E-019" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"secured_debt\", \"liability_group_id\": \"B5:007\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh36\"], \"related_evidence_indexes\": [\"E-011\", \"E-019\"]}" + } + ], + "legal_effect": [] + }, + "bh6": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-026" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:009\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh6\"], \"related_evidence_indexes\": [\"E-026\"]}" + } + ], + "legal_effect": [] + } + }, + "evidence_authority_by_index": { + "E-001": { + "evidence_index": "E-001", + "title": "가정법원심판_강호연", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-002": { + "evidence_index": "E-002", + "title": "가족관계증명서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-003": { + "evidence_index": "E-003", + "title": "각서 및 금전소비대차계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-004": { + "evidence_index": "E-004", + "title": "감정평가서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-005": { + "evidence_index": "E-005", + "title": "감정평가 의견서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-006": { + "evidence_index": "E-006", + "title": "계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-007": { + "evidence_index": "E-007", + "title": "공사도급계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-008": { + "evidence_index": "E-008", + "title": "내용증명", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-009": { + "evidence_index": "E-009", + "title": "내용증명", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-010": { + "evidence_index": "E-010", + "title": "내용증명에 대한 답신", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-011": { + "evidence_index": "E-011", + "title": "답변서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-012": { + "evidence_index": "E-012", + "title": "등기말소 신청에 대한 답신", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-013": { + "evidence_index": "E-013", + "title": "등기사항전부증명서(말소사항 포함)-토지", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-014": { + "evidence_index": "E-014", + "title": "등기사항전부증명서 (말소사항 포함) -건물", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-015": { + "evidence_index": "E-015", + "title": "등기사항전부증명서(말소사항 포함)-집합건물", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-016": { + "evidence_index": "E-016", + "title": "별지 목록", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-017": { + "evidence_index": "E-017", + "title": "부동산 매매계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-018": { + "evidence_index": "E-018", + "title": "사업자등록증", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-019": { + "evidence_index": "E-019", + "title": "소멸청구 통지서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json" + }, + "E-020": { + "evidence_index": "E-020", + "title": "영업양도 계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-021": { + "evidence_index": "E-021", + "title": "이행각서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-022": { + "evidence_index": "E-022", + "title": "임대차계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-023": { + "evidence_index": "E-023", + "title": "임료시세확인서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-024": { + "evidence_index": "E-024", + "title": "차용증", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-025": { + "evidence_index": "E-025", + "title": "차용증", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-026": { + "evidence_index": "E-026", + "title": "차용증", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-027": { + "evidence_index": "E-027", + "title": "최고 통지서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-028": { + "evidence_index": "E-028", + "title": "통지서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-029": { + "evidence_index": "E-029", + "title": "통지서_이수인_윤건우", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-030": { + "evidence_index": "E-030", + "title": "해제 통지서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates_by_evidence_index": { + "E-001": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-002": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-003": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-004": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-005": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-006": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-007": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-008": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-009": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-010": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-011": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-012": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-013": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-014": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-015": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-016": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-017": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-018": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-020": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-021": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-022": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-023": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-024": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-025": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-026": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-027": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-028": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-029": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-030": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "bo_fact_inputs": [ + { + "source_bo_id": "bh1", + "bo_compact": { + "bo_id": "bh1", + "bo_type": "event", + "action": "평택시 빌라 신축 공사도급계약 체결 및 중도금 지급", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "도급" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "도급", + "공사대금" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-007" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": "공사대금", + "principal_amount_hint": "1,000,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": "박광윤", + "debtor_candidate": "김일동" + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-001" + ], + "issue_cluster_ids": [ + "IC-001" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-007" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:011" + ], + "downstream_fact_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-007" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:011\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh1\"], \"related_evidence_indexes\": [\"E-007\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-007" + ], + "evidence_authority": { + "E-007": { + "evidence_index": "E-007", + "title": "공사도급계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-007": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh10", + "bo_compact": { + "bo_id": "bh10", + "bo_type": "event", + "action": "오국한에 대한 대여금 채권 등을 자동채권으로 한 상계 항변", + "action_type": "소송행위(litigation acts)", + "juristic_act": { + "label": "상계" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "상계", + "항변" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-003", + "E-010" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": "대여금", + "principal_amount_hint": "50,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": "오민한", + "debtor_candidate": null + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-002" + ], + "issue_cluster_ids": [ + "IC-002" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-003", + "E-010" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:013" + ], + "downstream_fact_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:013\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh10\"], \"related_evidence_indexes\": [\"E-003\", \"E-010\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-003", + "E-010" + ], + "evidence_authority": { + "E-003": { + "evidence_index": "E-003", + "title": "각서 및 금전소비대차계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-010": { + "evidence_index": "E-010", + "title": "내용증명에 대한 답신", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-003": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-010": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh11", + "bo_compact": { + "bo_id": "bh11", + "bo_type": "event", + "action": "물품대금 지급 최고", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "최고" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "이행최고", + "통지", + "최고" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-027" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": null, + "debtor_candidate": null + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-003" + ], + "issue_cluster_ids": [ + "IC-003" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-027" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:003" + ], + "downstream_fact_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-027" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:003\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh11\"], \"related_evidence_indexes\": [\"E-027\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-027" + ], + "evidence_authority": { + "E-027": { + "evidence_index": "E-027", + "title": "최고 통지서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-027": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh12", + "bo_compact": { + "bo_id": "bh12", + "bo_type": "event", + "action": "상호속용에 따른 물품대금 채무 책임 부인 통지", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지", + "책임부인" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-028" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": null, + "debtor_candidate": null + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-004" + ], + "issue_cluster_ids": [ + "IC-004" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-028" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:005" + ], + "downstream_fact_roles": [ + "claim_chain_ref", + "linked_structures", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-028" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:005\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh12\"], \"related_evidence_indexes\": [\"E-028\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-028" + ], + "evidence_authority": { + "E-028": { + "evidence_index": "E-028", + "title": "통지서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-028": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh13", + "bo_compact": { + "bo_id": "bh13", + "bo_type": "state", + "action": "공사대금 잔금 3억 원 미지급 상태 및 유치권 충당 항변", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "미지급", + "충당" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-011" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": "공사대금", + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": "300,000,000원", + "creditor_candidate": "박광윤", + "debtor_candidate": "김일동" + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-005" + ], + "issue_cluster_ids": [ + "IC-005" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-011" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:012" + ], + "downstream_fact_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:012\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh13\"], \"related_evidence_indexes\": [\"E-011\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-011" + ], + "evidence_authority": { + "E-011": { + "evidence_index": "E-011", + "title": "답변서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-011": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh14", + "bo_compact": { + "bo_id": "bh14", + "bo_type": "event", + "action": "강용원은 성수동 대지에 대한 상속 및 증여로 지분을 취득하고, 오민한에게 차용금 담보를 위해 성수동 대지 지분에 대한 근저당권 설정 약정을 체결함", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "(근)저당권 설정 계약" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "상속", + "증여", + "근저당권설정", + "secured_debt_creation", + "(근)저당권 설정 계약" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "domain_payload_compact": { + "fractional_effect_events": [ + { + "fraction_ref": null, + "fraction_display": "3/5", + "fraction_numerator": null, + "fraction_denominator": null, + "effect_kind_hint": "transfer", + "source_event_candidate_ids": [ + "EVT-013-02" + ], + "registry_refs": [ + "REG-013-02" + ], + "evidence_indexes": [ + "E-013" + ] + } + ] + } + }, + "linked_structures": { + "structure_ids": [ + "LES-006" + ], + "issue_cluster_ids": [ + "IC-006" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_index_ids": [ + "E-013", + "E-025", + "E-026" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:001" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"secured_debt\", \"liability_group_id\": \"B2:001\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh14\"], \"related_evidence_indexes\": [\"E-013\", \"E-025\", \"E-026\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-013", + "E-025", + "E-026" + ], + "evidence_authority": { + "E-013": { + "evidence_index": "E-013", + "title": "등기사항전부증명서(말소사항 포함)-토지", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-025": { + "evidence_index": "E-025", + "title": "차용증", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-026": { + "evidence_index": "E-026", + "title": "차용증", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-013": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-025": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-026": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh15", + "bo_compact": { + "bo_id": "bh15", + "bo_type": "event", + "action": "신림동 상가에 강용원(등기상 강용일) 명의의 근저당권이 설정되었다가 무단 말소되었으며, 오민한은 상계를 주장함", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "근저당권 말소 및 상계" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "근저당권설정", + "말소", + "상계", + "secured_debt_chain", + "mortgage_extinguishment", + "근저당권 말소 및 상계" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-010", + "E-014" + ], + "domain_payload_compact": {} + }, + "linked_structures": { + "structure_ids": [ + "LES-007" + ], + "issue_cluster_ids": [ + "IC-007" + ], + "asset_cluster_ids": [ + "AC-002" + ], + "evidence_index_ids": [ + "E-010", + "E-014" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:004" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-010", + "E-014" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"registry_invalidity\", \"liability_group_id\": \"B2:004\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh15\"], \"related_evidence_indexes\": [\"E-010\", \"E-014\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-010", + "E-014" + ], + "evidence_authority": { + "E-010": { + "evidence_index": "E-010", + "title": "내용증명에 대한 답신", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-014": { + "evidence_index": "E-014", + "title": "등기사항전부증명서 (말소사항 포함) -건물", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-010": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-014": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh16", + "bo_compact": { + "bo_id": "bh16", + "bo_type": "event", + "action": "평택시 서정빌라에 대한 보존등기 및 매매로 인한 소유권이전등기가 마쳐짐", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "소유권이전등기" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "소유권보존", + "소유권이전", + "가압류", + "소유권이전등기" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-015" + ], + "domain_payload_compact": {} + }, + "linked_structures": { + "structure_ids": [ + "LES-008" + ], + "issue_cluster_ids": [ + "IC-008" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_index_ids": [ + "E-015" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:006" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-015" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"secured_debt\", \"liability_group_id\": \"B2:006\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh16\"], \"related_evidence_indexes\": [\"E-015\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-015" + ], + "evidence_authority": { + "E-015": { + "evidence_index": "E-015", + "title": "등기사항전부증명서(말소사항 포함)-집합건물", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-015": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh17", + "bo_compact": { + "bo_id": "bh17", + "bo_type": "event", + "action": "오민한이 경매를 신청하여 이문호가 매각대금을 완납하고 취득한 뒤, 대한은행이 근저당권을 설정함에 따라 원인무효 여부 및 공신력이 다투어짐", + "action_type": "소송행위(litigation acts)", + "juristic_act": { + "label": "담보권실행을 위한 경매 및 근저당권 설정" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "임의경매", + "공신력", + "등기무효", + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "담보권실행을 위한 경매 및 근저당권 설정" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-012", + "E-013" + ], + "domain_payload_compact": {} + }, + "linked_structures": { + "structure_ids": [ + "LES-009" + ], + "issue_cluster_ids": [ + "IC-009" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_index_ids": [ + "E-012", + "E-013" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:003" + ], + "downstream_fact_roles": [ + "legal_effect_roles", + "linked_structures" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-012", + "E-013" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"registry_invalidity\", \"liability_group_id\": \"B2:003\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh17\"], \"related_evidence_indexes\": [\"E-012\", \"E-013\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-012", + "E-013" + ], + "evidence_authority": { + "E-012": { + "evidence_index": "E-012", + "title": "등기말소 신청에 대한 답신", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-013": { + "evidence_index": "E-013", + "title": "등기사항전부증명서(말소사항 포함)-토지", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-012": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-013": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh18", + "bo_compact": { + "bo_id": "bh18", + "bo_type": "event", + "action": "강용원이 오민한에게 4억 원을 지급하였으나 다수의 채무 중 충당할 채무를 특정하지 않음", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "변제 및 변제충당" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "변제", + "변제충당", + "payment_allocation_uncertain", + "변제 및 변제충당" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [], + "domain_payload_compact": {} + }, + "linked_structures": { + "structure_ids": [ + "LES-010" + ], + "issue_cluster_ids": [ + "IC-010" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:002" + ], + "downstream_fact_roles": [ + "legal_effect_roles", + "linked_structures" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"secured_debt\", \"liability_group_id\": \"B2:002\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh18\"], \"related_evidence_indexes\": []}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [], + "evidence_authority": {}, + "event_candidates": {} + }, + { + "source_bo_id": "bh19", + "bo_compact": { + "bo_id": "bh19", + "bo_type": "event", + "action": "박광윤이 평택시 빌라를 점유하며 얻은 임대 및 거주 수익을 피담보채권에 충당하였다고 항변함", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "변제충당 주장" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "유치권", + "점유", + "변제충당", + "payment_allocation", + "변제충당 주장" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-007", + "E-011" + ], + "domain_payload_compact": {} + }, + "linked_structures": { + "structure_ids": [ + "LES-011" + ], + "issue_cluster_ids": [ + "IC-011" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_index_ids": [ + "E-007", + "E-011" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:005" + ], + "downstream_fact_roles": [ + "legal_effect_roles", + "linked_structures", + "legal_calculation_object", + "claim_chain_ref" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-007", + "E-011" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"money_claim\", \"liability_group_id\": \"B2:005\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh19\"], \"related_evidence_indexes\": [\"E-007\", \"E-011\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-007", + "E-011" + ], + "evidence_authority": { + "E-007": { + "evidence_index": "E-007", + "title": "공사도급계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-011": { + "evidence_index": "E-011", + "title": "답변서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-007": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-011": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh2", + "bo_compact": { + "bo_id": "bh2", + "bo_type": "event", + "action": "인테리어 자재 매매에 따른 물품대금 채권 발생", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "매매" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "물품대금", + "매매" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-021", + "E-027" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": "물품대금", + "principal_amount_hint": "200,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": "강용원", + "debtor_candidate": "김선웅" + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-012" + ], + "issue_cluster_ids": [ + "IC-012" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-021", + "E-027" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:001" + ], + "downstream_fact_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-021", + "E-027" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:001\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh2\"], \"related_evidence_indexes\": [\"E-021\", \"E-027\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-021", + "E-027" + ], + "evidence_authority": { + "E-021": { + "evidence_index": "E-021", + "title": "이행각서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-027": { + "evidence_index": "E-027", + "title": "최고 통지서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-021": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-027": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh20", + "bo_compact": { + "bo_id": "bh20", + "bo_type": "event", + "action": "이수인이 윤건우에게 매매계약에서 합의되지 않은 제3자 명의로의 등기이전을 일방적으로 요구함", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "제3자 명의 등기 요청" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "소유권이전등기", + "요구", + "거절", + "registry_defect_hint", + "제3자 명의 등기 요청" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-029" + ], + "domain_payload_compact": {} + }, + "linked_structures": { + "structure_ids": [ + "LES-013" + ], + "issue_cluster_ids": [ + "IC-013" + ], + "asset_cluster_ids": [ + "AC-004" + ], + "evidence_index_ids": [ + "E-029" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:007" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-029" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"secured_debt\", \"liability_group_id\": \"B2:007\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh20\"], \"related_evidence_indexes\": [\"E-029\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-029" + ], + "evidence_authority": { + "E-029": { + "evidence_index": "E-029", + "title": "통지서_이수인_윤건우", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-029": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh21", + "bo_compact": { + "bo_id": "bh21", + "bo_type": "event", + "action": "성수동 대지 차임 시세 감정평가", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "가치평가", + "임대료", + "차임", + "valuation" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-005" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": null, + "possession_events": [], + "legal_cause_assertions": [] + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-014" + ], + "issue_cluster_ids": [ + "IC-014" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006" + ], + "evidence_index_ids": [ + "E-005" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:002" + ], + "downstream_fact_roles": [ + "legal_calculation_object", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-005" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"land_use_gain\", \"liability_group_id\": \"B3:002\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh21\"], \"related_evidence_indexes\": [\"E-005\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-005" + ], + "evidence_authority": { + "E-005": { + "evidence_index": "E-005", + "title": "감정평가 의견서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-005": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh22", + "bo_compact": { + "bo_id": "bh22", + "bo_type": "state", + "action": "성수동 대지 및 건물 인도, 점유 및 사용", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "인도", + "점유", + "사용", + "possession" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-006" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": "SCH-016-01", + "possession_events": [ + { + "possessor_candidates": [ + "박성희" + ], + "start_date": "2024-11-20", + "end_date": null, + "source_event_candidate_ids": [ + "EVT-006-07" + ], + "evidence_indexes": [ + "E-006" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "임대차", + "asserted_by": "박성희", + "assertion_date": "2024-11-20", + "source_event_candidate_ids": [ + "EVT-006-03" + ], + "evidence_indexes": [ + "E-006" + ] + } + ] + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-015" + ], + "issue_cluster_ids": [ + "IC-015" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-007", + "AC-008" + ], + "evidence_index_ids": [ + "E-006" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:004" + ], + "downstream_fact_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_structures" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-006" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"land_use_gain\", \"liability_group_id\": \"B3:004\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh22\"], \"related_evidence_indexes\": [\"E-006\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-006" + ], + "evidence_authority": { + "E-006": { + "evidence_index": "E-006", + "title": "계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-006": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh23", + "bo_compact": { + "bo_id": "bh23", + "bo_type": "state", + "action": "신림로 상가 및 포천시 임야 가치평가(감정평가서)", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "가치평가", + "감정평가", + "valuation" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-004" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": null, + "possession_events": [], + "legal_cause_assertions": [] + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-016" + ], + "issue_cluster_ids": [ + "IC-016" + ], + "asset_cluster_ids": [ + "AC-009", + "AC-010" + ], + "evidence_index_ids": [ + "E-004" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:001" + ], + "downstream_fact_roles": [ + "legal_calculation_object", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-004" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"land_use_gain\", \"liability_group_id\": \"B3:001\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh23\"], \"related_evidence_indexes\": [\"E-004\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-004" + ], + "evidence_authority": { + "E-004": { + "evidence_index": "E-004", + "title": "감정평가서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-004": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh24", + "bo_compact": { + "bo_id": "bh24", + "bo_type": "state", + "action": "평택시 서정빌라 101호 점유 및 유치권 행사", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "점유", + "거주", + "유치권", + "수익", + "possession", + "defense_assertion_benefit_applied_to_secured_claim" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-011" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": "SCH-016-04", + "possession_events": [ + { + "possessor_candidates": [ + "박광윤" + ], + "start_date": "2022-02-01", + "end_date": "2024-10-31", + "source_event_candidate_ids": [ + "EVT-011-02" + ], + "evidence_indexes": [ + "E-011" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "유치권행사중", + "asserted_by": "박광윤", + "assertion_date": "2024-10-31", + "source_event_candidate_ids": [ + "EVT-011-03" + ], + "evidence_indexes": [ + "E-011" + ] + } + ] + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-017" + ], + "issue_cluster_ids": [ + "IC-017" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-012" + ], + "evidence_index_ids": [ + "E-011" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:005" + ], + "downstream_fact_roles": [ + "legal_effect_roles", + "linked_structures", + "legal_calculation_object" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"secured_debt\", \"liability_group_id\": \"B3:005\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh24\"], \"related_evidence_indexes\": [\"E-011\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-011" + ], + "evidence_authority": { + "E-011": { + "evidence_index": "E-011", + "title": "답변서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-011": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh25", + "bo_compact": { + "bo_id": "bh25", + "bo_type": "state", + "action": "평택시 서정빌라 차임 시세 평가", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "가치평가", + "차임", + "valuation" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-023" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": null, + "possession_events": [], + "legal_cause_assertions": [] + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-018" + ], + "issue_cluster_ids": [ + "IC-018" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-013" + ], + "evidence_index_ids": [ + "E-023" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:003" + ], + "downstream_fact_roles": [ + "legal_calculation_object", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-023" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"land_use_gain\", \"liability_group_id\": \"B3:003\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh25\"], \"related_evidence_indexes\": [\"E-023\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-023" + ], + "evidence_authority": { + "E-023": { + "evidence_index": "E-023", + "title": "임료시세확인서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-023": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh26", + "bo_compact": { + "bo_id": "bh26", + "bo_type": "state", + "action": "흑석동 상가 점유 및 운영 (맛나식당)", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "인도", + "점유", + "운영", + "사업자등록", + "possession" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": "SCH-016-05", + "possession_events": [ + { + "possessor_candidates": [ + "양정숙" + ], + "start_date": "2024-02-01", + "end_date": null, + "source_event_candidate_ids": [ + "EVT-022-03", + "EVT-018-01" + ], + "evidence_indexes": [ + "E-018", + "E-022" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "임대차", + "asserted_by": "양정숙", + "assertion_date": "2024-02-01", + "source_event_candidate_ids": [ + "EVT-022-03" + ], + "evidence_indexes": [ + "E-022" + ] + } + ] + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-019" + ], + "issue_cluster_ids": [ + "IC-019" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "evidence_index_ids": [ + "E-017", + "E-018", + "E-022" + ], + "candidate_structure_types": [ + "land_use_gain" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:006" + ], + "downstream_fact_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_structures" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"land_use_gain\", \"liability_group_id\": \"B3:006\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh26\"], \"related_evidence_indexes\": [\"E-017\", \"E-018\", \"E-022\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-017", + "E-018", + "E-022" + ], + "evidence_authority": { + "E-017": { + "evidence_index": "E-017", + "title": "부동산 매매계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-018": { + "evidence_index": "E-018", + "title": "사업자등록증", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-022": { + "evidence_index": "E-022", + "title": "임대차계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-017": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-018": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-022": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh27", + "bo_compact": { + "bo_id": "bh27", + "bo_type": "event", + "action": "채무자 오국한이 수익자 오민한에게 책임재산인 신림동 상가를 1억5천만원에 매도하여 소유권이전등기 경료", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "매매" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "사해행위", + "매매계약", + "소유권이전", + "책임재산감소", + "fraudulent_act", + "매매" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-014" + ], + "domain_payload_compact": {} + }, + "linked_structures": { + "structure_ids": [ + "LES-020" + ], + "issue_cluster_ids": [ + "IC-020" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_index_ids": [ + "E-014" + ], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:002" + ], + "downstream_fact_roles": [ + "actio_roles", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-002", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "beneficiary", + "value_candidate": "{}", + "date_candidate": null, + "related_bo_ids": [ + "bh27" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh27\"], \"related_evidence_indexes\": [\"E-014\"], \"role\": \"beneficiary\", \"value_candidate\": \"{}\"}], \"actio_scope\": [\"fraudulent_act\", \"beneficiary_or_transferee\", \"target_asset\"], \"preserved_claim_candidates\": [{\"bo_id\": \"bh27\", \"claim_date_candidate\": null, \"claim_type_candidate\": null, \"creditor_candidate\": null, \"debtor_candidate\": null, \"related_evidence_indexes\": [\"E-014\"]}], \"related_bo_ids\": [\"bh27\"], \"related_evidence_indexe" + } + ], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"actio_related\", \"liability_group_id\": \"B4:002\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh27\"], \"related_evidence_indexes\": [\"E-014\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-014" + ], + "evidence_authority": { + "E-014": { + "evidence_index": "E-014", + "title": "등기사항전부증명서 (말소사항 포함) -건물", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-014": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh28", + "bo_compact": { + "bo_id": "bh28", + "bo_type": "event", + "action": "강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "소비대차" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "피보전채권", + "대여금채권", + "preserved_claim", + "소비대차" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-024" + ], + "domain_payload_compact": {} + }, + "linked_structures": { + "structure_ids": [ + "LES-021" + ], + "issue_cluster_ids": [ + "IC-021" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-024" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:001" + ], + "downstream_fact_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "actio_roles" + ] + }, + "signal_hints": { + "actio": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-001", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-024" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "debtor_act", + "value_candidate": "강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생", + "date_candidate": null, + "related_bo_ids": [ + "bh28" + ], + "related_evidence_indexes": [ + "E-024" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh28\"], \"related_evidence_indexes\": [\"E-024\"], \"role\": \"debtor_act\", \"value_candidate\": \"강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생\"}], \"actio_scope\": [\"preserved_claim\"], \"preserved_claim_candidates\": [{\"bo_id\": null, \"claim_date_candidate\": null, \"claim_type_candidate\": null, \"creditor_candidate\": null, \"debtor_candidate\": null, \"related_evidence_indexes\": [\"E-024\"]}], \"related_bo_ids\": [\"bh28\"], \"related_evidence_indexes\": [\"E" + } + ], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-024" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"actio_related\", \"liability_group_id\": \"B4:001\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh28\"], \"related_evidence_indexes\": [\"E-024\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-024" + ], + "evidence_authority": { + "E-024": { + "evidence_index": "E-024", + "title": "차용증", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-024": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh29", + "bo_compact": { + "bo_id": "bh29", + "bo_type": "event", + "action": "수익자 오민한이 사해행위 이후 피담보채무 1억3천만원을 대위변제하고 2번 근저당권을 말소시킴으로써 원물반환이 불가능해져 가액배상 요건 성립", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "변제 및 말소" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "원물반환불능", + "가액배상", + "근저당권말소", + "value_compensation", + "remedy_cap", + "변제 및 말소" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-014" + ], + "domain_payload_compact": {} + }, + "linked_structures": { + "structure_ids": [ + "LES-022" + ], + "issue_cluster_ids": [ + "IC-022" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-002" + ], + "evidence_index_ids": [ + "E-014" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:004" + ], + "downstream_fact_roles": [ + "legal_effect_roles", + "legal_calculation_object", + "linked_actio_structures", + "linked_structures", + "actio_roles" + ] + }, + "signal_hints": { + "actio": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-004", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "remedy_or_cap", + "value_candidate": "원물반환 불가", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "remedy_or_cap", + "value_candidate": "가액배상", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2021-08-01', 'event': '나임오 1번 근저당 설정(5000만원)'}", + "date_candidate": "2021-08-01", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2022-04-18', 'event': '강용일 2번 근저당 설정(1.5억원)'}", + "date_candidate": "2022-04-18", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2024-10-17', 'event': '강용일 2번 근저당 말소(오민한 대위변제)'}", + "date_candidate": "2024-10-17", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh29\"], \"related_evidence_indexes\": [\"E-014\"], \"role\": \"remedy_or_cap\", \"value_candidate\": \"원물반환 불가\"}, {\"date_candidate\": null, \"related_bo_ids\": [\"bh29\"], \"related_evidence_indexes\": [\"E-014\"], \"role\": \"remedy_or_cap\", \"value_candidate\": \"가액배상\"}, {\"date_candidate\": \"2021-08-01\", \"related_bo_ids\": [\"bh29\"], \"related_evidence_indexes\": [\"E-014\"], \"role\": \"encumbrance\", \"value_candidate\": \"{'date': '2021-08-01', 'event': '나임오 1번 근저" + } + ], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"actio_related\", \"liability_group_id\": \"B4:004\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh29\"], \"related_evidence_indexes\": [\"E-014\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-014" + ], + "evidence_authority": { + "E-014": { + "evidence_index": "E-014", + "title": "등기사항전부증명서 (말소사항 포함) -건물", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-014": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh3", + "bo_compact": { + "bo_id": "bh3", + "bo_type": "event", + "action": "1억 원 대여금 채권 발생 (2022.4.18.)", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "소비대차" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "대여금", + "소비대차" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-024" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": "대여금", + "principal_amount_hint": "100,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": "월 1%", + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [ + "E-024" + ], + "unpaid_balance_hint": null, + "creditor_candidate": "강용원", + "debtor_candidate": "오국한" + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-023" + ], + "issue_cluster_ids": [ + "IC-023" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-024" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:006" + ], + "downstream_fact_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-024" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:006\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh3\"], \"related_evidence_indexes\": [\"E-024\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-024" + ], + "evidence_authority": { + "E-024": { + "evidence_index": "E-024", + "title": "차용증", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-024": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh30", + "bo_compact": { + "bo_id": "bh30", + "bo_type": "state", + "action": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "무자력", + "책임재산", + "가치평가", + "insolvency_check" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-004" + ], + "domain_payload_compact": {} + }, + "linked_structures": { + "structure_ids": [ + "LES-024" + ], + "issue_cluster_ids": [ + "IC-024" + ], + "asset_cluster_ids": [ + "AC-020", + "AC-021" + ], + "evidence_index_ids": [ + "E-004" + ], + "candidate_structure_types": [ + "valuation" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:003" + ], + "downstream_fact_roles": [ + "legal_effect_roles", + "actio_roles", + "legal_calculation_object" + ] + }, + "signal_hints": { + "actio": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-003", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-004" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "debtor_act", + "value_candidate": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "date_candidate": null, + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh30\"], \"related_evidence_indexes\": [\"E-004\"], \"role\": \"debtor_act\", \"value_candidate\": \"사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)\"}], \"actio_scope\": [\"target_asset\"], \"preserved_claim_candidates\": [{\"bo_id\": \"bh30\", \"claim_date_candidate\": null, \"claim_type_candidate\": null, \"creditor_candidate\": null, \"debtor_candidate\": null, \"related_evidence_indexes\": [\"E-004\"]}], \"related_bo_ids\": [\"bh30\"], \"related_evidence_i" + } + ], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-004" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"actio_related\", \"liability_group_id\": \"B4:003\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh30\"], \"related_evidence_indexes\": [\"E-004\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-004" + ], + "evidence_authority": { + "E-004": { + "evidence_index": "E-004", + "title": "감정평가서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-004": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh31", + "bo_compact": { + "bo_id": "bh31", + "bo_type": "state", + "action": "수익자 오민한의 항변 (명의신탁 주장, 매수자금 및 대여금 참작 주장, 채권 상계 주장)", + "action_type": "소송행위(litigation acts)", + "juristic_act": { + "label": "항변" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "수익자항변", + "명의신탁", + "상계항변", + "선의항변", + "defense_assertion", + "항변" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-003", + "E-010" + ], + "domain_payload_compact": {} + }, + "linked_structures": { + "structure_ids": [ + "LES-025" + ], + "issue_cluster_ids": [ + "IC-025" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_index_ids": [ + "E-003", + "E-010" + ], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:005" + ], + "downstream_fact_roles": [ + "legal_effect_roles", + "actio_roles", + "linked_structures" + ] + }, + "signal_hints": { + "actio": [ + { + "signal_type": "actio", + "signal_id": "actio-signal-005", + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "party_roles": [], + "actio_role_hints": [ + { + "role": "defense", + "value_candidate": "{'defense_type': '계약명의신탁', 'content': '오국한 명의 취득이 수익자 자금에 의한 계약명의신탁이므로 사해행위가 아니라는 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "role": "defense", + "value_candidate": "{'defense_type': '배상금감액', 'content': '수익자가 제공한 매수자금 4000만원 및 대여금 1000만원 참작 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "role": "defense", + "value_candidate": "{'defense_type': '상계항변', 'content': '오민한의 5000만원 채권으로 강용원 채권을 상계한다는 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"actio_role_hints\": [{\"date_candidate\": null, \"related_bo_ids\": [\"bh31\"], \"related_evidence_indexes\": [\"E-003\", \"E-010\"], \"role\": \"defense\", \"value_candidate\": \"{'defense_type': '계약명의신탁', 'content': '오국한 명의 취득이 수익자 자금에 의한 계약명의신탁이므로 사해행위가 아니라는 주장'}\"}, {\"date_candidate\": null, \"related_bo_ids\": [\"bh31\"], \"related_evidence_indexes\": [\"E-003\", \"E-010\"], \"role\": \"defense\", \"value_candidate\": \"{'defense_type': '배상금감액', 'content': '수익자가 제공한 매수자금 4000만원 및 대여금 1000만원 참작 주장'}\"}, {\"date_candidate\": null, " + } + ], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"actio_related\", \"liability_group_id\": \"B4:005\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh31\"], \"related_evidence_indexes\": [\"E-003\", \"E-010\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-003", + "E-010" + ], + "evidence_authority": { + "E-003": { + "evidence_index": "E-003", + "title": "각서 및 금전소비대차계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-010": { + "evidence_index": "E-010", + "title": "내용증명에 대한 답신", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-003": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-010": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh32", + "bo_compact": { + "bo_id": "bh32", + "bo_type": "event", + "action": "강석우 사망으로 인한 정유심, 강용원의 성수동 대지 지분 상속", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": "상속개시" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "상속개시" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-013" + ], + "domain_payload_compact": {} + }, + "linked_structures": { + "structure_ids": [ + "LES-026" + ], + "issue_cluster_ids": [ + "IC-026" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-013" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:002" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-013" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:002\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh32\"], \"related_evidence_indexes\": [\"E-013\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-013" + ], + "evidence_authority": { + "E-013": { + "evidence_index": "E-013", + "title": "등기사항전부증명서(말소사항 포함)-토지", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-013": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh33", + "bo_compact": { + "bo_id": "bh33", + "bo_type": "event", + "action": "망 강호연의 사망에 따른 상속개시 및 자녀 강형수, 강지수의 상속포기", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": "상속포기" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "상속포기" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-001", + "E-002" + ], + "domain_payload_compact": {} + }, + "linked_structures": { + "structure_ids": [ + "LES-027" + ], + "issue_cluster_ids": [ + "IC-027" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-001", + "E-002" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:003" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-001", + "E-002" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:003\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh33\"], \"related_evidence_indexes\": [\"E-001\", \"E-002\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-001", + "E-002" + ], + "evidence_authority": { + "E-001": { + "evidence_index": "E-001", + "title": "가정법원심판_강호연", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-002": { + "evidence_index": "E-002", + "title": "가족관계증명서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-001": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-002": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh34", + "bo_compact": { + "bo_id": "bh34", + "bo_type": "event", + "action": "강용원이 김선웅에게 물품대금 지급을 최고하는 통지서 발송 및 도달", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "최고" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "최고" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-027" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-027-NT-01", + "notice_type": "최고", + "drafted_date": null, + "dispatch_date": "2024-05-04", + "arrival_date": "2024-05-06", + "received_date": "2024-05-06", + "legal_effect_date_candidate": "2024-05-06", + "requires_receipt_for_effect": null, + "sender_candidates": [ + "강용원" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "김선웅" + ], + "receipt_evidence_refs": [ + "우편물 배달 증명서" + ], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-028" + ], + "issue_cluster_ids": [ + "IC-028" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-027" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:009" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-027" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:009\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh34\"], \"related_evidence_indexes\": [\"E-027\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-027" + ], + "evidence_authority": { + "E-027": { + "evidence_index": "E-027", + "title": "최고 통지서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-027": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh35", + "bo_compact": { + "bo_id": "bh35", + "bo_type": "event", + "action": "강용원이 오민한에게 사해행위취소 및 신림동 상가 소유권 반환을 요구하는 내용증명 발송", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "최고" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "최고" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-008", + "E-010" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-008-NT-01", + "notice_type": "기타", + "drafted_date": "2024-09-15", + "dispatch_date": "2024-09-15", + "arrival_date": "2024-09-20", + "received_date": "2024-09-20", + "legal_effect_date_candidate": "2024-09-20", + "requires_receipt_for_effect": null, + "sender_candidates": [ + "강용원" + ], + "legal_sender_after_resolution": { + "status": "unresolved", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "오민한" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [ + "오민한이 이행최고서를 2024. 9. 20. 수령함" + ] + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-029" + ], + "issue_cluster_ids": [ + "IC-029" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-008", + "E-010" + ], + "candidate_structure_types": [ + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:004" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles", + "actio_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-008", + "E-010" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"actio_related\", \"liability_group_id\": \"B5:004\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh35\"], \"related_evidence_indexes\": [\"E-008\", \"E-010\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-008", + "E-010" + ], + "evidence_authority": { + "E-008": { + "evidence_index": "E-008", + "title": "내용증명", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-010": { + "evidence_index": "E-010", + "title": "내용증명에 대한 답신", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-008": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-010": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh36", + "bo_compact": { + "bo_id": "bh36", + "bo_type": "event", + "action": "강용원과 양정숙이 박광윤에게 평택시 빌라 유치권소멸을 청구하는 통지서 발송 및 도달", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-011", + "E-019" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-019-NT-01", + "notice_type": "유치권소멸청구통지", + "drafted_date": "2024-08-29", + "dispatch_date": "2024-08-29", + "arrival_date": "2024-09-01", + "received_date": "2024-09-01", + "legal_effect_date_candidate": "2024-09-01", + "requires_receipt_for_effect": true, + "sender_candidates": [ + "강용원", + "양정숙" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "박광윤" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-030" + ], + "issue_cluster_ids": [ + "IC-030" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-011", + "E-019" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:007" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011", + "E-019" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"secured_debt\", \"liability_group_id\": \"B5:007\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh36\"], \"related_evidence_indexes\": [\"E-011\", \"E-019\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-011", + "E-019" + ], + "evidence_authority": { + "E-011": { + "evidence_index": "E-011", + "title": "답변서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-019": { + "evidence_index": "E-019", + "title": "소멸청구 통지서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json" + } + }, + "event_candidates": { + "E-011": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-019": [] + } + }, + { + "source_bo_id": "bh37", + "bo_compact": { + "bo_id": "bh37", + "bo_type": "event", + "action": "오민한이 강용원에게 김선웅의 채무에 대한 책임을 부인하는 통지서 발송", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-028" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-028-NT-01", + "notice_type": "기타", + "drafted_date": null, + "dispatch_date": "2024-12-27", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [ + "오민한" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "강용원" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-031" + ], + "issue_cluster_ids": [ + "IC-031" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-028" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:010" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-028" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:010\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh37\"], \"related_evidence_indexes\": [\"E-028\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-028" + ], + "evidence_authority": { + "E-028": { + "evidence_index": "E-028", + "title": "통지서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-028": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh38", + "bo_compact": { + "bo_id": "bh38", + "bo_type": "event", + "action": "오민한이 강용원에게 내용증명에 대한 답신 발송", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-010" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-010-NT-01", + "notice_type": "기타", + "drafted_date": null, + "dispatch_date": "2024-09-30", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [ + "오민한" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "강용원" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-032" + ], + "issue_cluster_ids": [ + "IC-032" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-010" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:006" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-010" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:006\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh38\"], \"related_evidence_indexes\": [\"E-010\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-010" + ], + "evidence_authority": { + "E-010": { + "evidence_index": "E-010", + "title": "내용증명에 대한 답신", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-010": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh39", + "bo_compact": { + "bo_id": "bh39", + "bo_type": "event", + "action": "윤건우가 양정숙에게 흑석동 상가 임대차계약 해지 및 명도를 요구하는 내용증명 통지", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-009" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-009-NT-01", + "notice_type": "기타", + "drafted_date": "2024-11-29", + "dispatch_date": "2024-11-29", + "arrival_date": "2024-11-30", + "received_date": "2024-11-30", + "legal_effect_date_candidate": "2024-11-30", + "requires_receipt_for_effect": true, + "sender_candidates": [ + "윤건우" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "양정숙" + ], + "receipt_evidence_refs": [ + "우편물배달증명서" + ], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-033" + ], + "issue_cluster_ids": [ + "IC-033" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-009" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:005" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-009" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:005\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh39\"], \"related_evidence_indexes\": [\"E-009\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-009" + ], + "evidence_authority": { + "E-009": { + "evidence_index": "E-009", + "title": "내용증명", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-009": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh4", + "bo_compact": { + "bo_id": "bh4", + "bo_type": "event", + "action": "1억 원 대여금 채권 발생 (2022.5.18.)", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "소비대차" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "대여금", + "소비대차" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-024" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": "대여금", + "principal_amount_hint": "100,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": "월 1%", + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [ + "E-024" + ], + "unpaid_balance_hint": null, + "creditor_candidate": "강용원", + "debtor_candidate": "오국한" + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-034" + ], + "issue_cluster_ids": [ + "IC-034" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-024" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:007" + ], + "downstream_fact_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-024" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:007\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh4\"], \"related_evidence_indexes\": [\"E-024\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-024" + ], + "evidence_authority": { + "E-024": { + "evidence_index": "E-024", + "title": "차용증", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-024": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh40", + "bo_compact": { + "bo_id": "bh40", + "bo_type": "event", + "action": "윤건우가 이수인에게 흑석동 상가 매매계약 해제 통지서 발송 및 도달", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-030" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-030-NT-01", + "notice_type": "해제통지", + "drafted_date": null, + "dispatch_date": "2024-05-15", + "arrival_date": "2024-05-16", + "received_date": "2024-05-16", + "legal_effect_date_candidate": "2024-05-16", + "requires_receipt_for_effect": true, + "sender_candidates": [ + "윤건우" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "이수인" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-035" + ], + "issue_cluster_ids": [ + "IC-035" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-030" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:012" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-030" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:012\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh40\"], \"related_evidence_indexes\": [\"E-030\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-030" + ], + "evidence_authority": { + "E-030": { + "evidence_index": "E-030", + "title": "해제 통지서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-030": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh41", + "bo_compact": { + "bo_id": "bh41", + "bo_type": "event", + "action": "이수인이 윤건우에게 흑석동 상가 매매대금 감액 및 제3자 명의 등기 이전 요청 통지", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-029" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-029-NT-01", + "notice_type": "기타", + "drafted_date": null, + "dispatch_date": "2024-03-30", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [ + "이수인" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "윤건우" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-036" + ], + "issue_cluster_ids": [ + "IC-036" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-029" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:011" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-029" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"money_claim\", \"liability_group_id\": \"B5:011\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh41\"], \"related_evidence_indexes\": [\"E-029\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-029" + ], + "evidence_authority": { + "E-029": { + "evidence_index": "E-029", + "title": "통지서_이수인_윤건우", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-029": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh42", + "bo_compact": { + "bo_id": "bh42", + "bo_type": "event", + "action": "주식회사 대한은행이 강용원의 성수동 대지 근저당권 말소 신청을 거부하는 통지 발송", + "action_type": "준법률행위(quasi-legal acts)", + "juristic_act": { + "label": "통지" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "통지" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-012" + ], + "domain_payload_compact": { + "notice_lifecycle": { + "notice_id": "E-012-NT-01", + "notice_type": "기타", + "drafted_date": "2024-12-22", + "dispatch_date": "2024-12-22", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [ + "주식회사 대한은행" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "강용원" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-037" + ], + "issue_cluster_ids": [ + "IC-037" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-012" + ], + "candidate_structure_types": [ + "registry_invalidity" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:008" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-012" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"registry_invalidity\", \"liability_group_id\": \"B5:008\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh42\"], \"related_evidence_indexes\": [\"E-012\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-012" + ], + "evidence_authority": { + "E-012": { + "evidence_index": "E-012", + "title": "등기말소 신청에 대한 답신", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-012": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh43", + "bo_compact": { + "bo_id": "bh43", + "bo_type": "state", + "action": "목적물 특정", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "domain_payload_compact": { + "asset_alias": [ + { + "asset_ref": "성수동 대지", + "alias_strings": [ + "성수동 대지", + "서울 성동구 성수동 256" + ], + "canonical_label_candidate": "1. 서울 성동구 성수동 256 대 330㎡", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-01" + ], + "registry_refs": [ + { + "registry_ref": "REG-013-01", + "receipt_no_refs": [ + "제1453호" + ] + }, + { + "registry_ref": "REG-013-02", + "receipt_no_refs": [ + "제7026호" + ] + }, + { + "registry_ref": "REG-013-03", + "receipt_no_refs": [ + "제11202호" + ] + }, + { + "registry_ref": "REG-013-04", + "receipt_no_refs": [ + "제7202호" + ] + }, + { + "registry_ref": "REG-013-05", + "receipt_no_refs": [ + "제12402호" + ] + }, + { + "registry_ref": "REG-013-06", + "receipt_no_refs": [ + "제25797호" + ] + }, + { + "registry_ref": "REG-013-07", + "receipt_no_refs": [ + "제25799호" + ] + } + ], + "source_evidence_indexes": [ + "E-013", + "E-016" + ] + }, + { + "asset_ref": "성수동 건물", + "alias_strings": [ + "성수동 건물", + "서울 성동구 성수동 256 상가" + ], + "canonical_label_candidate": "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-02" + ], + "registry_refs": [], + "source_evidence_indexes": [ + "E-016" + ] + }, + { + "asset_ref": "신림동 상가", + "alias_strings": [ + "신림동 상가", + "서울 관악구 신림로 115" + ], + "canonical_label_candidate": "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-03" + ], + "registry_refs": [ + { + "registry_ref": "REG-014-01", + "receipt_no_refs": [ + "제24759호" + ] + }, + { + "registry_ref": "REG-014-02", + "receipt_no_refs": [ + "제56457호" + ] + }, + { + "registry_ref": "REG-014-03", + "receipt_no_refs": [ + "제11593호" + ] + }, + { + "registry_ref": "REG-014-04", + "receipt_no_refs": [ + "제50099호" + ] + }, + { + "registry_ref": "REG-014-05", + "receipt_no_refs": [ + "제26597호" + ] + }, + { + "registry_ref": "REG-014-06", + "receipt_no_refs": [ + "제74586호" + ] + } + ], + "source_evidence_indexes": [ + "E-014", + "E-016" + ] + }, + { + "asset_ref": "평택시 빌라", + "alias_strings": [ + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123" + ], + "canonical_label_candidate": "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-04" + ], + "registry_refs": [ + { + "registry_ref": "REG-015-01", + "receipt_no_refs": [ + "제7233호" + ] + }, + { + "registry_ref": "REG-015-02", + "receipt_no_refs": [ + "제7871호" + ] + }, + { + "registry_ref": "REG-015-03", + "receipt_no_refs": [ + "제11593호" + ] + }, + { + "registry_ref": "REG-015-04", + "receipt_no_refs": [ + "제12321호" + ] + } + ], + "source_evidence_indexes": [ + "E-015", + "E-016" + ] + }, + { + "asset_ref": "흑석동 상가", + "alias_strings": [ + "흑석동 상가", + "서울 동작구 흑석로 80" + ], + "canonical_label_candidate": "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-05" + ], + "registry_refs": [], + "source_evidence_indexes": [ + "E-016" + ] + } + ] + } + }, + "linked_structures": { + "structure_ids": [ + "LES-038" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "asset_cluster_ids": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "evidence_index_ids": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:001" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:001\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh43\"], \"related_evidence_indexes\": [\"E-013\", \"E-014\", \"E-015\", \"E-016\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "evidence_authority": { + "E-013": { + "evidence_index": "E-013", + "title": "등기사항전부증명서(말소사항 포함)-토지", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-014": { + "evidence_index": "E-014", + "title": "등기사항전부증명서 (말소사항 포함) -건물", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-015": { + "evidence_index": "E-015", + "title": "등기사항전부증명서(말소사항 포함)-집합건물", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-016": { + "evidence_index": "E-016", + "title": "별지 목록", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-013": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-014": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-015": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-016": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh44", + "bo_compact": { + "bo_id": "bh44", + "bo_type": "state", + "action": "박광윤의 평택시 서정빌라 101호 거주 및 유치권 행사 상태", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-011" + ], + "domain_payload_compact": { + "lien_state": { + "lien_id": "B5:LIEN:001", + "asset_ref": "평택시 빌라", + "lien_holder_candidates": [ + "박광윤" + ], + "secured_claim_hint": { + "debtor_candidate": "김일동", + "basis_text": "공사잔금 3억 원 미지급", + "claimed_remaining_amount_hint": "300,000,000원", + "evidence_indexes": [ + "E-011" + ] + }, + "possession_event_refs": [], + "assertion_events": [ + "EVT-011-02" + ], + "extinction_notice_refs": [ + "E-019-NT-01" + ], + "status_raw_hint": "유치권 행사 중" + }, + "possession_state": { + "asset_ref": "평택시 빌라", + "possession_events": [ + { + "possessor_candidates": [ + "박광윤" + ], + "start_date": null, + "end_date": null, + "source_event_candidate_ids": [ + "EVT-011-02" + ], + "evidence_indexes": [ + "E-011" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "유치권행사중", + "asserted_by": "박광윤", + "assertion_date": "2024-10-31", + "source_event_candidate_ids": [ + "EVT-011-02" + ], + "evidence_indexes": [ + "E-011" + ] + } + ] + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-039" + ], + "issue_cluster_ids": [ + "IC-039" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-011" + ], + "candidate_structure_types": [ + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:013" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"secured_debt\", \"liability_group_id\": \"B5:013\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh44\"], \"related_evidence_indexes\": [\"E-011\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-011" + ], + "evidence_authority": { + "E-011": { + "evidence_index": "E-011", + "title": "답변서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-011": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh45", + "bo_compact": { + "bo_id": "bh45", + "bo_type": "state", + "action": "박성희의 성수동 대지 및 건물 점유", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-006" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": "성수동 대지 및 성수동 건물", + "possession_events": [ + { + "possessor_candidates": [ + "박성희" + ], + "start_date": "2024-11-20", + "end_date": null, + "source_event_candidate_ids": [ + "EVT-006-07" + ], + "evidence_indexes": [ + "E-006" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "임대차", + "asserted_by": "박성희", + "assertion_date": "2024-11-20", + "source_event_candidate_ids": [ + "EVT-006-07" + ], + "evidence_indexes": [ + "E-006" + ] + } + ] + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-040" + ], + "issue_cluster_ids": [ + "IC-040" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-006" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:014" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-006" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:014\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh45\"], \"related_evidence_indexes\": [\"E-006\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-006" + ], + "evidence_authority": { + "E-006": { + "evidence_index": "E-006", + "title": "계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-006": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh46", + "bo_compact": { + "bo_id": "bh46", + "bo_type": "state", + "action": "양정숙의 흑석동 상가 점유 및 식당 운영 상태", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": null + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-018", + "E-022" + ], + "domain_payload_compact": { + "possession_state": { + "asset_ref": "흑석동 상가", + "possession_events": [ + { + "possessor_candidates": [ + "양정숙" + ], + "start_date": "2024-02-01", + "end_date": null, + "source_event_candidate_ids": [ + "EVT-018-01", + "EVT-022-03" + ], + "evidence_indexes": [ + "E-018", + "E-022" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "임대차", + "asserted_by": "양정숙", + "assertion_date": "2024-02-01", + "source_event_candidate_ids": [ + "EVT-022-03" + ], + "evidence_indexes": [ + "E-022" + ] + } + ] + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-041" + ], + "issue_cluster_ids": [ + "IC-041" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-018", + "E-022" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:015" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-018", + "E-022" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:015\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh46\"], \"related_evidence_indexes\": [\"E-018\", \"E-022\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-018", + "E-022" + ], + "evidence_authority": { + "E-018": { + "evidence_index": "E-018", + "title": "사업자등록증", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + }, + "E-022": { + "evidence_index": "E-022", + "title": "임대차계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-018": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ], + "E-022": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh47", + "bo_compact": { + "bo_id": "bh47", + "bo_type": "state", + "action": "오민한의 강용원에 대한 자신의 5,000만 원 채권 상계 주장 항변", + "action_type": "소송행위(litigation acts)", + "juristic_act": { + "label": "상계" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "상계" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-010" + ], + "domain_payload_compact": {} + }, + "linked_structures": { + "structure_ids": [ + "LES-042" + ], + "issue_cluster_ids": [ + "IC-042" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-010" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:017" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles", + "claim_chain_ref" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-010" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"money_claim\", \"liability_group_id\": \"B5:017\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh47\"], \"related_evidence_indexes\": [\"E-010\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-010" + ], + "evidence_authority": { + "E-010": { + "evidence_index": "E-010", + "title": "내용증명에 대한 답신", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-010": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh48", + "bo_compact": { + "bo_id": "bh48", + "bo_type": "state", + "action": "박광윤의 과거 임대료 및 거주 수익을 채무에 충당하였다는 주장 항변", + "action_type": "소송행위(litigation acts)", + "juristic_act": { + "label": "항변" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "항변" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-011" + ], + "domain_payload_compact": {} + }, + "linked_structures": { + "structure_ids": [ + "LES-043" + ], + "issue_cluster_ids": [ + "IC-043" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-011" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:018" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:018\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh48\"], \"related_evidence_indexes\": [\"E-011\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-011" + ], + "evidence_authority": { + "E-011": { + "evidence_index": "E-011", + "title": "답변서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-011": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh49", + "bo_compact": { + "bo_id": "bh49", + "bo_type": "state", + "action": "윤건우의 임대차 책임 부인 및 양정숙 차임 연체 주장 항변", + "action_type": "소송행위(litigation acts)", + "juristic_act": { + "label": "항변" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "항변" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-009" + ], + "domain_payload_compact": {} + }, + "linked_structures": { + "structure_ids": [ + "LES-044" + ], + "issue_cluster_ids": [ + "IC-044" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-009" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:016" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-009" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"succession_related\", \"liability_group_id\": \"B5:016\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh49\"], \"related_evidence_indexes\": [\"E-009\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-009" + ], + "evidence_authority": { + "E-009": { + "evidence_index": "E-009", + "title": "내용증명", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-009": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh5", + "bo_compact": { + "bo_id": "bh5", + "bo_type": "event", + "action": "2억 원 대여금 채권 발생 (2023.1.6.)", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "소비대차" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "대여금", + "소비대차" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-025" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": "대여금", + "principal_amount_hint": "200,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": "오민한", + "debtor_candidate": "강용원" + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-045" + ], + "issue_cluster_ids": [ + "IC-045" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-025" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:008" + ], + "downstream_fact_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-025" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:008\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh5\"], \"related_evidence_indexes\": [\"E-025\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-025" + ], + "evidence_authority": { + "E-025": { + "evidence_index": "E-025", + "title": "차용증", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-025": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh50", + "bo_compact": { + "bo_id": "bh50", + "bo_type": "state", + "action": "주식회사 대한은행의 경매공신력 주장(민사집행법 제267조)에 따른 근저당 말소 불가 항변", + "action_type": "소송행위(litigation acts)", + "juristic_act": { + "label": "항변" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "항변" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-012" + ], + "domain_payload_compact": {} + }, + "linked_structures": { + "structure_ids": [ + "LES-046" + ], + "issue_cluster_ids": [ + "IC-046" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-012" + ], + "candidate_structure_types": [ + "registry_invalidity" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:019" + ], + "downstream_fact_roles": [ + "linked_structures", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-012" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"registry_invalidity\", \"liability_group_id\": \"B5:019\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh50\"], \"related_evidence_indexes\": [\"E-012\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-012" + ], + "evidence_authority": { + "E-012": { + "evidence_index": "E-012", + "title": "등기말소 신청에 대한 답신", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-012": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh6", + "bo_compact": { + "bo_id": "bh6", + "bo_type": "event", + "action": "3억 원 대여금 채권 발생 (2023.7.6.)", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "소비대차" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "대여금", + "소비대차" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-026" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": "대여금", + "principal_amount_hint": "300,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": "연 15%", + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [ + "E-026" + ], + "unpaid_balance_hint": null, + "creditor_candidate": "오민한", + "debtor_candidate": "강용원" + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-047" + ], + "issue_cluster_ids": [ + "IC-047" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-026" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:009" + ], + "downstream_fact_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-026" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:009\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh6\"], \"related_evidence_indexes\": [\"E-026\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-026" + ], + "evidence_authority": { + "E-026": { + "evidence_index": "E-026", + "title": "차용증", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-026": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh7", + "bo_compact": { + "bo_id": "bh7", + "bo_type": "event", + "action": "해드림 영업양도 및 상호속용 약정", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "영업양도" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "영업양도", + "상호속용" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-020" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": null, + "debtor_candidate": null + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-048" + ], + "issue_cluster_ids": [ + "IC-048" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-020" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:004" + ], + "downstream_fact_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-020" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:004\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh7\"], \"related_evidence_indexes\": [\"E-020\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-020" + ], + "evidence_authority": { + "E-020": { + "evidence_index": "E-020", + "title": "영업양도 계약서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-020": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh8", + "bo_compact": { + "bo_id": "bh8", + "bo_type": "event", + "action": "물품대금 채무 승인 및 변제기 유예", + "action_type": "법률행위(legal acts)", + "juristic_act": { + "label": "채무승인" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "채무승인", + "변제기 유예" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [ + "E-021" + ], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": "강용원", + "debtor_candidate": "김선웅" + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-049" + ], + "issue_cluster_ids": [ + "IC-049" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [ + "E-021" + ], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:002" + ], + "downstream_fact_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-021" + ], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:002\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh8\"], \"related_evidence_indexes\": [\"E-021\"]}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [ + "E-021" + ], + "evidence_authority": { + "E-021": { + "evidence_index": "E-021", + "title": "이행각서", + "excerpt": "", + "priority": null, + "authentication": null, + "source": "evidence_indexed.json", + "event_candidates": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + "event_candidates": { + "E-021": [ + { + "event_date": null, + "actor": null, + "action": null, + "amount": null, + "object": null + } + ] + } + }, + { + "source_bo_id": "bh9", + "bo_compact": { + "bo_id": "bh9", + "bo_type": "event", + "action": "차용금 변제 명목 4억 원 지급 (채무 미특정)", + "action_type": "사실행위(factual acts)", + "juristic_act": { + "label": "변제" + }, + "reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "prior_act": null, + "legal_keywords": [ + "변제", + "법정변제충당" + ], + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "date": null, + "core_field_base": { + "BehaviorTime": null, + "Performer": null, + "Subject": null, + "Object": null, + "Amount": null, + "Action": null + }, + "source_evidence_indexes": [], + "domain_payload_compact": { + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": null, + "debtor_candidate": null + } + } + }, + "linked_structures": { + "structure_ids": [ + "LES-050" + ], + "issue_cluster_ids": [ + "IC-050" + ], + "asset_cluster_ids": [], + "evidence_index_ids": [], + "candidate_structure_types": [ + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:010" + ], + "downstream_fact_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ] + }, + "signal_hints": { + "actio": [], + "case_liability": [ + { + "signal_type": "case_liability", + "signal_id": null, + "candidate_structure_types": [], + "linked_structure_ids": [], + "related_evidence_indexes": [], + "party_roles": [], + "actio_role_hints": [], + "claim_group_candidates": [], + "liability_group_candidates": [], + "summary": "{\"claim_group_id\": null, \"liability_candidate_type\": \"commercial_successor\", \"liability_group_id\": \"B1:010\", \"linked_structure_ids\": [], \"party_roles\": [], \"related_bo_ids\": [\"bh9\"], \"related_evidence_indexes\": []}" + } + ], + "legal_effect": [] + }, + "evidence_refs": [], + "evidence_authority": {}, + "event_candidates": {} + } + ], + "source_integrity_warnings": [], + "source_counts": { + "bo_count": 50, + "structure_count": 50, + "evidence_authority_count": 30, + "bo_fact_input_count": 50 + } + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/legal_effect_structure_exception_pack.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/legal_effect_structure_exception_pack.json new file mode 100644 index 00000000..432c5fad --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/legal_effect_structure_exception_pack.json @@ -0,0 +1,1850 @@ +{ + "legal_structure_exception_pack": { + "schema_version": "stage1_legal_structure_exception_pack.v1", + "status": "READY", + "issue_cluster_exceptions": [], + "asset_cluster_exceptions": [ + { + "exception_id": "LEX-001", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh21" + ], + "candidate_values": [ + "SCH-016-01", + "성수동 256 대" + ], + "basis_fields": [ + "asset_candidates" + ], + "score_table": [ + { + "candidate_value": "SCH-016-01", + "normalized_label": "sch-016-01", + "asset_kind": "unknown" + }, + { + "candidate_value": "성수동 256 대", + "normalized_label": "성수동256대", + "asset_kind": "unknown" + } + ], + "conflict_flags": [ + "multi_asset_bundle_or_separate_clusters_ambiguous" + ], + "compact_context_class": [ + [ + "unknown" + ], + [ + "sch-016-01", + "성수동256대" + ] + ], + "allowed_decisions": [ + "SEPARATE_CLUSTER", + "MULTI_ASSET_BUNDLE", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-002", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh23" + ], + "candidate_values": [ + "SCH-016-03", + "신림로 상가, 포천시 임야" + ], + "basis_fields": [ + "asset_candidates" + ], + "score_table": [ + { + "candidate_value": "SCH-016-03", + "normalized_label": "sch-016-03", + "asset_kind": "unknown" + }, + { + "candidate_value": "신림로 상가, 포천시 임야", + "normalized_label": "신림로상가포천시임야", + "asset_kind": "real_property" + } + ], + "conflict_flags": [ + "multi_asset_bundle_or_separate_clusters_ambiguous" + ], + "compact_context_class": [ + [ + "real_property", + "unknown" + ], + [ + "sch-016-03", + "신림로상가포천시임야" + ] + ], + "allowed_decisions": [ + "SEPARATE_CLUSTER", + "MULTI_ASSET_BUNDLE", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-003", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh24" + ], + "candidate_values": [ + "SCH-016-04", + "평택시 서정3길 123 서정빌라 101호" + ], + "basis_fields": [ + "asset_candidates" + ], + "score_table": [ + { + "candidate_value": "SCH-016-04", + "normalized_label": "sch-016-04", + "asset_kind": "unknown" + }, + { + "candidate_value": "평택시 서정3길 123 서정빌라 101호", + "normalized_label": "평택시서정3길123서정빌라101호", + "asset_kind": "unknown" + } + ], + "conflict_flags": [ + "multi_asset_bundle_or_separate_clusters_ambiguous" + ], + "compact_context_class": [ + [ + "unknown" + ], + [ + "sch-016-04", + "평택시서정3길123서정빌라101호" + ] + ], + "allowed_decisions": [ + "SEPARATE_CLUSTER", + "MULTI_ASSET_BUNDLE", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-004", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh25" + ], + "candidate_values": [ + "SCH-016-04", + "서정빌라 101호" + ], + "basis_fields": [ + "asset_candidates" + ], + "score_table": [ + { + "candidate_value": "SCH-016-04", + "normalized_label": "sch-016-04", + "asset_kind": "unknown" + }, + { + "candidate_value": "서정빌라 101호", + "normalized_label": "서정빌라101호", + "asset_kind": "unknown" + } + ], + "conflict_flags": [ + "multi_asset_bundle_or_separate_clusters_ambiguous" + ], + "compact_context_class": [ + [ + "unknown" + ], + [ + "sch-016-04", + "서정빌라101호" + ] + ], + "allowed_decisions": [ + "SEPARATE_CLUSTER", + "MULTI_ASSET_BUNDLE", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-005", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh26" + ], + "candidate_values": [ + "SCH-016-05", + "흑석동 201호" + ], + "basis_fields": [ + "asset_candidates" + ], + "score_table": [ + { + "candidate_value": "SCH-016-05", + "normalized_label": "sch-016-05", + "asset_kind": "unknown" + }, + { + "candidate_value": "흑석동 201호", + "normalized_label": "흑석동201호", + "asset_kind": "unknown" + } + ], + "conflict_flags": [ + "multi_asset_bundle_or_separate_clusters_ambiguous" + ], + "compact_context_class": [ + [ + "unknown" + ], + [ + "sch-016-05", + "흑석동201호" + ] + ], + "allowed_decisions": [ + "SEPARATE_CLUSTER", + "MULTI_ASSET_BUNDLE", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-006", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh30" + ], + "candidate_values": [ + "신림동 상가", + "포천시 임야" + ], + "basis_fields": [ + "asset_candidates" + ], + "score_table": [ + { + "candidate_value": "신림동 상가", + "normalized_label": "신림동상", + "asset_kind": "unknown" + }, + { + "candidate_value": "포천시 임야", + "normalized_label": "포천시임야", + "asset_kind": "unknown" + }, + { + "candidate_value": "신림동 상가", + "normalized_label": "신림동상", + "asset_kind": "real_property" + }, + { + "candidate_value": "포천시 임야", + "normalized_label": "포천시임야", + "asset_kind": "real_property" + } + ], + "conflict_flags": [ + "multi_asset_bundle_or_separate_clusters_ambiguous" + ], + "compact_context_class": [ + [ + "real_property", + "unknown" + ], + [ + "신림동상", + "포천시임야" + ] + ], + "allowed_decisions": [ + "SEPARATE_CLUSTER", + "MULTI_ASSET_BUNDLE", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + } + ], + "candidate_structure_type_exceptions": [ + { + "exception_id": "LEX-007", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh1" + ], + "candidate_values": [ + "money_claim", + "도급", + "공사대금", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "도급", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "공사대금", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "commercial_successor", + "downstream_role": "claim_chain_ref" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "commercial_successor", + "event", + "money_claim", + "공사대금", + "도급", + "법률행위(legal_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-008", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh10" + ], + "candidate_values": [ + "money_claim", + "상계", + "succession_notice_lien_asset_defense", + "event", + "소송행위(litigation_acts)", + "commercial_successor" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "상계", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "소송행위(litigation_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "commercial_successor", + "downstream_role": "claim_chain_ref" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "commercial_successor", + "event", + "money_claim", + "succession_notice_lien_asset_defense", + "상계", + "소송행위(litigation_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-009", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh11" + ], + "candidate_values": [ + "money_claim", + "이행최고", + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal_acts)", + "commercial_successor" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "이행최고", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "최고", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "준법률행위(quasi-legal_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "commercial_successor", + "downstream_role": "claim_chain_ref" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "commercial_successor", + "event", + "money_claim", + "succession_notice_lien_asset_defense", + "이행최고", + "준법률행위(quasi-legal_acts)", + "최고" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-010", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh12" + ], + "candidate_values": [ + "money_claim", + "succession_notice_lien_asset_defense", + "책임부인", + "event", + "준법률행위(quasi-legal_acts)", + "commercial_successor" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "책임부인", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "준법률행위(quasi-legal_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "commercial_successor", + "downstream_role": "claim_chain_ref" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "commercial_successor", + "event", + "money_claim", + "succession_notice_lien_asset_defense", + "준법률행위(quasi-legal_acts)", + "책임부인" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-011", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh13" + ], + "candidate_values": [ + "money_claim", + "충당", + "state", + "사실행위(factual_acts)", + "commercial_successor" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "충당", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "commercial_successor", + "downstream_role": "claim_chain_ref" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "commercial_successor", + "money_claim", + "state", + "사실행위(factual_acts)", + "충당" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-012", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh14" + ], + "candidate_values": [ + "secured_debt", + "상속", + "증여", + "(근)저당권_설정_계약", + "event", + "법률행위(legal_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "secured_debt", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "상속", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "증여", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "(근)저당권_설정_계약", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "(근)저당권_설정_계약", + "event", + "secured_debt", + "법률행위(legal_acts)", + "상속", + "증여" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-013", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh15" + ], + "candidate_values": [ + "secured_debt", + "mortgage_extinguishment", + "registry_invalidity", + "상계", + "event", + "법률행위(legal_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "secured_debt", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "mortgage_extinguishment", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "registry_invalidity", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "상계", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "mortgage_extinguishment", + "registry_invalidity", + "secured_debt", + "법률행위(legal_acts)", + "상계" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-014", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh16" + ], + "candidate_values": [ + "secured_debt", + "소유권보존", + "소유권이전", + "가압류", + "registry_invalidity", + "event", + "법률행위(legal_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "secured_debt", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "소유권보존", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "소유권이전", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "가압류", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "registry_invalidity", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "registry_invalidity", + "secured_debt", + "가압류", + "법률행위(legal_acts)", + "소유권보존", + "소유권이전" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-015", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh17" + ], + "candidate_values": [ + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "secured_debt", + "임의경매", + "공신력", + "registry_invalidity", + "event", + "소송행위(litigation_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "auction_invalidity", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "derivative_bank_mortgage_invalidity", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "secured_debt", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "임의경매", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "공신력", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "registry_invalidity", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "소송행위(litigation_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "event", + "registry_invalidity", + "secured_debt", + "공신력", + "소송행위(litigation_acts)", + "임의경매" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-016", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh18" + ], + "candidate_values": [ + "payment_allocation_uncertain", + "secured_debt", + "변제", + "변제충당", + "변제_및_변제충당", + "event", + "준법률행위(quasi-legal_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "payment_allocation_uncertain", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "secured_debt", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "변제", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "변제충당", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "변제_및_변제충당", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "준법률행위(quasi-legal_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "payment_allocation_uncertain", + "secured_debt", + "변제", + "변제_및_변제충당", + "변제충당", + "준법률행위(quasi-legal_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-018", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh2" + ], + "candidate_values": [ + "money_claim", + "물품대금", + "매매", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "물품대금", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "매매", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "commercial_successor", + "downstream_role": "claim_chain_ref" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "commercial_successor", + "event", + "money_claim", + "매매", + "물품대금", + "법률행위(legal_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-019", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh20" + ], + "candidate_values": [ + "registry_invalidity", + "secured_debt", + "요구", + "거절", + "event", + "준법률행위(quasi-legal_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "registry_invalidity", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "secured_debt", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "요구", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "거절", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "준법률행위(quasi-legal_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "registry_invalidity", + "secured_debt", + "거절", + "요구", + "준법률행위(quasi-legal_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-020", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh21" + ], + "candidate_values": [ + "valuation", + "land_use_gain", + "임대료", + "차임", + "event", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "valuation", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "land_use_gain", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "임대료", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "차임", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "land_use_gain", + "valuation", + "사실행위(factual_acts)", + "임대료", + "차임" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-021", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh22" + ], + "candidate_values": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "사용", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "possession", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "land_use_gain", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "사용", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "land_use_gain", + "possession", + "state", + "succession_notice_lien_asset_defense", + "사실행위(factual_acts)", + "사용" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-022", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh23" + ], + "candidate_values": [ + "valuation", + "land_use_gain", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "valuation", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "land_use_gain", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "land_use_gain", + "state", + "valuation", + "사실행위(factual_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-023", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh24" + ], + "candidate_values": [ + "possession", + "secured_debt", + "land_use_gain", + "거주", + "succession_notice_lien_asset_defense", + "수익", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "possession", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "secured_debt", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "land_use_gain", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "거주", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "수익", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "land_use_gain", + "possession", + "secured_debt", + "state", + "succession_notice_lien_asset_defense", + "거주", + "사실행위(factual_acts)", + "수익" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-024", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh25" + ], + "candidate_values": [ + "valuation", + "land_use_gain", + "차임", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "valuation", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "land_use_gain", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "차임", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "land_use_gain", + "state", + "valuation", + "사실행위(factual_acts)", + "차임" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-025", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh26" + ], + "candidate_values": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "운영", + "사업자등록", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "possession", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "land_use_gain", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "운영", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사업자등록", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "land_use_gain", + "possession", + "state", + "succession_notice_lien_asset_defense", + "사실행위(factual_acts)", + "사업자등록", + "운영" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-026", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh27" + ], + "candidate_values": [ + "fraudulent_transfer", + "매매계약", + "소유권이전", + "책임재산감소", + "매매", + "event", + "법률행위(legal_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "fraudulent_transfer", + "downstream_role": "actio_roles" + }, + { + "candidate_value": "매매계약", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "소유권이전", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "책임재산감소", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "매매", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "fraudulent_transfer", + "매매", + "매매계약", + "법률행위(legal_acts)", + "소유권이전", + "책임재산감소" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-027", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh28" + ], + "candidate_values": [ + "preserved_claim", + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "fraudulent_transfer" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "preserved_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "소비대차", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "fraudulent_transfer", + "downstream_role": "actio_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "fraudulent_transfer", + "money_claim", + "preserved_claim", + "법률행위(legal_acts)", + "소비대차" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-029", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh3", + "bh4", + "bh5", + "bh6" + ], + "candidate_values": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "소비대차", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "commercial_successor", + "downstream_role": "claim_chain_ref" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "commercial_successor", + "event", + "money_claim", + "법률행위(legal_acts)", + "소비대차" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 4 + }, + { + "exception_id": "LEX-037", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh37", + "bh38", + "bh39", + "bh40" + ], + "candidate_values": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "준법률행위(quasi-legal_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "succession_notice_lien_asset_defense", + "준법률행위(quasi-legal_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 4 + }, + { + "exception_id": "LEX-040", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh43", + "bh45", + "bh46" + ], + "candidate_values": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "state", + "succession_notice_lien_asset_defense", + "사실행위(factual_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 3 + }, + { + "exception_id": "LEX-043", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh48", + "bh49" + ], + "candidate_values": [ + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "소송행위(litigation_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "state", + "succession_notice_lien_asset_defense", + "소송행위(litigation_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 2 + } + ] + } +} diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/legal_effect_structure_input_pack.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/legal_effect_structure_input_pack.json new file mode 100644 index 00000000..13492a20 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/legal_effect_structure_input_pack.json @@ -0,0 +1,13288 @@ +{ + "legal_structure_input_pack": { + "schema_version": "stage1_legal_structure_input_pack.v1", + "status": "READY", + "input_files": [ + "client_goal.json", + "BO.json", + "evidence_indexed.json", + "evidence_all.json", + "evidence_event_candidates.json", + "actio_case_signals.json", + "case_liability_signals.json", + "legal_effect_signals.json" + ], + "client_goal_compact": { + "parties": { + "plaintiffs": [ + { + "name": "강용원", + "type": "자연인" + }, + { + "name": "양정숙", + "type": "자연인" + } + ], + "defendants": [ + { + "name": "오민한", + "type": "자연인", + "asset_status": "사업승계인, 사해행위 수익자 후보" + }, + { + "name": "이문호", + "type": "자연인", + "asset_status": "성수동 대지 소유자" + }, + { + "name": "주식회사 대한은행", + "type": "법인", + "asset_status": "성수동 대지 근저당권자" + }, + { + "name": "박성희", + "type": "자연인", + "asset_status": "성수동 대지/건물 점유자" + }, + { + "name": "박광윤", + "type": "자연인", + "asset_status": "평택시 빌라 유치권자" + }, + { + "name": "윤건우", + "type": "자연인", + "asset_status": "흑석동 상가 매도인" + } + ], + "third_parties": [ + { + "name": "오국한", + "relationship": "채무자" + }, + { + "name": "김선웅", + "relationship": "원채무자" + } + ] + } + }, + "bo_count": 50, + "bo_by_id": { + "bh1": { + "BO_ID": "bh1", + "id": "bh1", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "도급" + }, + "Action": "평택시 빌라 신축 공사도급계약 체결 및 중도금 지급", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "도급", + "공사대금" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "평택시 빌라 신축 공사도급계약 체결 및 중도금 지급", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [ + "E-007" + ], + "Evidence": [ + { + "evidence_index": "E-007", + "source_title": "공사도급계약서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-007-01", + "EVT-007-02" + ], + "source_meeting_clause_ids": [], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "박광윤(수급인)", + "김일동(도급인)" + ], + "action_summary": null, + "title_hint": "도급계약", + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh2": { + "BO_ID": "bh2", + "id": "bh2", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "매매" + }, + "Action": "인테리어 자재 매매에 따른 물품대금 채권 발생", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "물품대금", + "매매" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "인테리어 자재 매매에 따른 물품대금 채권 발생", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [ + "E-021", + "E-027" + ], + "Evidence": [ + { + "evidence_index": "E-021", + "source_title": "이행각서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + }, + { + "evidence_index": "E-027", + "source_title": "최고 통지서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-027-01", + "EVT-027-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:1-나", + "s03_client_requests:1" + ], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "강용원(채권자)", + "김선웅(채무자)" + ], + "action_summary": null, + "title_hint": "물품대금 채권 발생", + "summary_hint": "김선웅에 대한 인테리어 자재대금", + "event_state_type": null, + "asset_alias": [] + } + }, + "bh3": { + "BO_ID": "bh3", + "id": "bh3", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "소비대차" + }, + "Action": "1억 원 대여금 채권 발생 (2022.4.18.)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "대여금", + "소비대차" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "1억 원 대여금 채권 발생 (2022.4.18.)", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [ + "E-024" + ], + "Evidence": [ + { + "evidence_index": "E-024", + "source_title": "차용증", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-024-01" + ], + "source_meeting_clause_ids": [], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "강용원(대주)", + "오국한(차주)" + ], + "action_summary": null, + "title_hint": "대여금 채권 발생", + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh4": { + "BO_ID": "bh4", + "id": "bh4", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "소비대차" + }, + "Action": "1억 원 대여금 채권 발생 (2022.5.18.)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "대여금", + "소비대차" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "1억 원 대여금 채권 발생 (2022.5.18.)", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [ + "E-024" + ], + "Evidence": [ + { + "evidence_index": "E-024", + "source_title": "차용증", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-024-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:3-나", + "s03_client_requests:3" + ], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "강용원(대주)", + "오국한(차주)" + ], + "action_summary": null, + "title_hint": "대여금 채권 발생", + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh5": { + "BO_ID": "bh5", + "id": "bh5", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "소비대차" + }, + "Action": "2억 원 대여금 채권 발생 (2023.1.6.)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "대여금", + "소비대차" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "2억 원 대여금 채권 발생 (2023.1.6.)", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [ + "E-025" + ], + "Evidence": [ + { + "evidence_index": "E-025", + "source_title": "차용증", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-025-01" + ], + "source_meeting_clause_ids": [], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "오민한(대주)", + "강용원(차주)" + ], + "action_summary": null, + "title_hint": "대여금 채권 발생", + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh6": { + "BO_ID": "bh6", + "id": "bh6", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "소비대차" + }, + "Action": "3억 원 대여금 채권 발생 (2023.7.6.)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "대여금", + "소비대차" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "3억 원 대여금 채권 발생 (2023.7.6.)", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [ + "E-026" + ], + "Evidence": [ + { + "evidence_index": "E-026", + "source_title": "차용증", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-026-01" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-나" + ], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "오민한(대주)", + "강용원(차주)" + ], + "action_summary": null, + "title_hint": "대여금 채권 발생", + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh7": { + "BO_ID": "bh7", + "id": "bh7", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "영업양도" + }, + "Action": "해드림 영업양도 및 상호속용 약정", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "영업양도", + "상호속용" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "해드림 영업양도 및 상호속용 약정", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [ + "E-020" + ], + "Evidence": [ + { + "evidence_index": "E-020", + "source_title": "영업양도 계약서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-020-01", + "EVT-020-02", + "EVT-020-03" + ], + "source_meeting_clause_ids": [], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "김선웅(양도인)", + "오민한(양수인)" + ], + "action_summary": null, + "title_hint": "영업양도", + "summary_hint": "영업양도에 따른 양수인의 상호속용 책임", + "event_state_type": null, + "asset_alias": [] + } + }, + "bh8": { + "BO_ID": "bh8", + "id": "bh8", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "채무승인" + }, + "Action": "물품대금 채무 승인 및 변제기 유예", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "채무승인", + "변제기 유예" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "물품대금 채무 승인 및 변제기 유예", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-021" + ], + "Evidence": [ + { + "evidence_index": "E-021", + "source_title": "이행각서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-021-01", + "EVT-021-02" + ], + "source_meeting_clause_ids": [], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [], + "action_summary": null, + "title_hint": "채무승인", + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh9": { + "BO_ID": "bh9", + "id": "bh9", + "BOType": "event", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": "변제" + }, + "Action": "차용금 변제 명목 4억 원 지급 (채무 미특정)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "변제", + "법정변제충당" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "차용금 변제 명목 4억 원 지급 (채무 미특정)", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [], + "Evidence": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [ + "s01_consultation:2-다" + ], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "강용원(변제자)", + "오민한(수령자)" + ], + "action_summary": null, + "title_hint": "일부 변제", + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh10": { + "BO_ID": "bh10", + "id": "bh10", + "BOType": "event", + "ActionType": "소송행위(litigation acts)", + "JuristicAct": { + "label": "상계" + }, + "Action": "오국한에 대한 대여금 채권 등을 자동채권으로 한 상계 항변", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "상계", + "항변" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "오국한에 대한 대여금 채권 등을 자동채권으로 한 상계 항변", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [ + "E-003", + "E-010" + ], + "Evidence": [ + { + "evidence_index": "E-003", + "source_title": "각서 및 금전소비대차계약서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + }, + { + "evidence_index": "E-010", + "source_title": "내용증명에 대한 답신", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-010-03", + "EVT-003-02" + ], + "source_meeting_clause_ids": [], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "오민한", + "강용원" + ], + "action_summary": null, + "title_hint": "상계 항변", + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh11": { + "BO_ID": "bh11", + "id": "bh11", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "최고" + }, + "Action": "물품대금 지급 최고", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "이행최고", + "통지", + "최고" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "물품대금 지급 최고", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-027" + ], + "Evidence": [ + { + "evidence_index": "E-027", + "source_title": "최고 통지서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-027-01", + "EVT-027-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:1-나" + ], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [], + "action_summary": null, + "title_hint": "최고통지", + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh12": { + "BO_ID": "bh12", + "id": "bh12", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "상호속용에 따른 물품대금 채무 책임 부인 통지", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "통지", + "책임부인" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "상호속용에 따른 물품대금 채무 책임 부인 통지", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-028" + ], + "Evidence": [ + { + "evidence_index": "E-028", + "source_title": "통지서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-028-01" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:1" + ], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [], + "action_summary": null, + "title_hint": "책임 부인 통지", + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh13": { + "BO_ID": "bh13", + "id": "bh13", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "공사대금 잔금 3억 원 미지급 상태 및 유치권 충당 항변", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "미지급", + "충당" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "공사대금 잔금 3억 원 미지급 상태 및 유치권 충당 항변", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [ + "E-011" + ], + "Evidence": [ + { + "evidence_index": "E-011", + "source_title": "답변서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-011-03" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-가" + ], + "source_domain": "B1_Money_Successor" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "박광윤(채권자)", + "김일동(채무자)" + ], + "action_summary": null, + "title_hint": "잔금 미지급", + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh14": { + "BO_ID": "bh14", + "id": "bh14", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "(근)저당권 설정 계약" + }, + "Action": "강용원은 성수동 대지에 대한 상속 및 증여로 지분을 취득하고, 오민한에게 차용금 담보를 위해 성수동 대지 지분에 대한 근저당권 설정 약정을 체결함", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "상속", + "증여", + "근저당권설정", + "secured_debt_creation", + "(근)저당권 설정 계약" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "강용원은 성수동 대지에 대한 상속 및 증여로 지분을 취득하고, 오민한에게 차용금 담보를 위해 성수동 대지 지분에 대한 근저당권 설정 약정을 체결함", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "Evidence": [ + { + "evidence_index": "E-013", + "source_title": "등기사항전부증명서(말소사항 포함)-토지", + "priority_class": null, + "authentication_status": null, + "excerpt": null + }, + { + "evidence_index": "E-025", + "source_title": "차용증", + "priority_class": null, + "authentication_status": null, + "excerpt": null + }, + { + "evidence_index": "E-026", + "source_title": "차용증", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-013-01", + "EVT-013-02", + "EVT-025-02", + "EVT-026-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-가", + "s01_consultation:2-나" + ], + "source_domain": "B2_Secured_Registry" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "legal_effect_tags": [ + "secured_debt_creation" + ], + "asset_bindings": [ + "성수동 대지" + ], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "강용원", + "정유심", + "오민한" + ], + "action_summary": null, + "title_hint": "성수동 대지 소유권 변동 및 근저당권 설정", + "summary_hint": null, + "event_state_type": "event", + "asset_alias": [] + } + }, + "bh15": { + "BO_ID": "bh15", + "id": "bh15", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "근저당권 말소 및 상계" + }, + "Action": "신림동 상가에 강용원(등기상 강용일) 명의의 근저당권이 설정되었다가 무단 말소되었으며, 오민한은 상계를 주장함", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "근저당권설정", + "말소", + "상계", + "secured_debt_chain", + "mortgage_extinguishment", + "근저당권 말소 및 상계" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "신림동 상가에 강용원(등기상 강용일) 명의의 근저당권이 설정되었다가 무단 말소되었으며, 오민한은 상계를 주장함", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [ + "E-010", + "E-014" + ], + "Evidence": [ + { + "evidence_index": "E-010", + "source_title": "내용증명에 대한 답신", + "priority_class": null, + "authentication_status": null, + "excerpt": null + }, + { + "evidence_index": "E-014", + "source_title": "등기사항전부증명서 (말소사항 포함) -건물", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-014-01", + "EVT-014-02", + "EVT-014-03", + "EVT-014-04", + "EVT-014-05", + "EVT-014-06", + "EVT-010-03" + ], + "source_meeting_clause_ids": [ + "s01_consultation:3-가", + "s01_consultation:3-라" + ], + "source_domain": "B2_Secured_Registry" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "legal_effect_tags": [ + "secured_debt_chain", + "mortgage_extinguishment" + ], + "asset_bindings": [ + "신림동 상가" + ], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "강용원", + "오국한", + "오민한" + ], + "action_summary": null, + "title_hint": "신림동 상가 근저당권 등기 말소 및 상계 다툼", + "summary_hint": null, + "event_state_type": "event", + "asset_alias": [] + } + }, + "bh16": { + "BO_ID": "bh16", + "id": "bh16", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "소유권이전등기" + }, + "Action": "평택시 서정빌라에 대한 보존등기 및 매매로 인한 소유권이전등기가 마쳐짐", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "소유권보존", + "소유권이전", + "가압류", + "소유권이전등기" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "평택시 서정빌라에 대한 보존등기 및 매매로 인한 소유권이전등기가 마쳐짐", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [ + "E-015" + ], + "Evidence": [ + { + "evidence_index": "E-015", + "source_title": "등기사항전부증명서(말소사항 포함)-집합건물", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-015-01", + "EVT-015-02", + "EVT-015-03", + "EVT-015-04" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-나" + ], + "source_domain": "B2_Secured_Registry" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "legal_effect_tags": [], + "asset_bindings": [ + "평택시 빌라" + ], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "김일동", + "정남이", + "강호연" + ], + "action_summary": null, + "title_hint": "평택시 빌라 소유권 이전 및 등기 연혁", + "summary_hint": null, + "event_state_type": "event", + "asset_alias": [] + } + }, + "bh17": { + "BO_ID": "bh17", + "id": "bh17", + "BOType": "event", + "ActionType": "소송행위(litigation acts)", + "JuristicAct": { + "label": "담보권실행을 위한 경매 및 근저당권 설정" + }, + "Action": "오민한이 경매를 신청하여 이문호가 매각대금을 완납하고 취득한 뒤, 대한은행이 근저당권을 설정함에 따라 원인무효 여부 및 공신력이 다투어짐", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "임의경매", + "공신력", + "등기무효", + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "담보권실행을 위한 경매 및 근저당권 설정" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "오민한이 경매를 신청하여 이문호가 매각대금을 완납하고 취득한 뒤, 대한은행이 근저당권을 설정함에 따라 원인무효 여부 및 공신력이 다투어짐", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [ + "E-012", + "E-013" + ], + "Evidence": [ + { + "evidence_index": "E-012", + "source_title": "등기말소 신청에 대한 답신", + "priority_class": null, + "authentication_status": null, + "excerpt": null + }, + { + "evidence_index": "E-013", + "source_title": "등기사항전부증명서(말소사항 포함)-토지", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-013-03", + "EVT-013-04", + "EVT-013-05", + "EVT-013-06", + "EVT-012-01", + "EVT-012-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-라", + "s01_consultation:2-마", + "s02_party_assertions:2" + ], + "source_domain": "B2_Secured_Registry" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "legal_effect_tags": [ + "auction_invalidity", + "derivative_bank_mortgage_invalidity" + ], + "asset_bindings": [ + "성수동 대지" + ], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "오민한", + "이문호", + "주식회사 대한은행" + ], + "action_summary": null, + "title_hint": "성수동 대지 경매 및 대한은행 근저당권 설정 공신력 항변", + "summary_hint": null, + "event_state_type": "event", + "asset_alias": [] + } + }, + "bh18": { + "BO_ID": "bh18", + "id": "bh18", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "변제 및 변제충당" + }, + "Action": "강용원이 오민한에게 4억 원을 지급하였으나 다수의 채무 중 충당할 채무를 특정하지 않음", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "변제", + "변제충당", + "payment_allocation_uncertain", + "변제 및 변제충당" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "강용원이 오민한에게 4억 원을 지급하였으나 다수의 채무 중 충당할 채무를 특정하지 않음", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [], + "Evidence": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [ + "s01_consultation:2-다" + ], + "source_domain": "B2_Secured_Registry" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "legal_effect_tags": [ + "payment_allocation_uncertain" + ], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "강용원", + "오민한" + ], + "action_summary": null, + "title_hint": "변제에 따른 채무 소멸 및 지정충당 부존재", + "summary_hint": null, + "event_state_type": "event", + "asset_alias": [] + } + }, + "bh19": { + "BO_ID": "bh19", + "id": "bh19", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "변제충당 주장" + }, + "Action": "박광윤이 평택시 빌라를 점유하며 얻은 임대 및 거주 수익을 피담보채권에 충당하였다고 항변함", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "유치권", + "점유", + "변제충당", + "payment_allocation", + "변제충당 주장" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "박광윤이 평택시 빌라를 점유하며 얻은 임대 및 거주 수익을 피담보채권에 충당하였다고 항변함", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [ + "E-007", + "E-011" + ], + "Evidence": [ + { + "evidence_index": "E-007", + "source_title": "공사도급계약서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + }, + { + "evidence_index": "E-011", + "source_title": "답변서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-011-03", + "EVT-007-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-가", + "s01_consultation:4-나", + "s02_party_assertions:4-나" + ], + "source_domain": "B2_Secured_Registry" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "legal_effect_tags": [ + "payment_allocation" + ], + "asset_bindings": [ + "평택시 빌라" + ], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "박광윤" + ], + "action_summary": null, + "title_hint": "평택시 빌라 임대수익의 피담보채무 변제충당 항변", + "summary_hint": null, + "event_state_type": "state", + "asset_alias": [] + } + }, + "bh20": { + "BO_ID": "bh20", + "id": "bh20", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "제3자 명의 등기 요청" + }, + "Action": "이수인이 윤건우에게 매매계약에서 합의되지 않은 제3자 명의로의 등기이전을 일방적으로 요구함", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "소유권이전등기", + "요구", + "거절", + "registry_defect_hint", + "제3자 명의 등기 요청" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "이수인이 윤건우에게 매매계약에서 합의되지 않은 제3자 명의로의 등기이전을 일방적으로 요구함", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [ + "E-029" + ], + "Evidence": [ + { + "evidence_index": "E-029", + "source_title": "통지서_이수인_윤건우", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-029-01" + ], + "source_meeting_clause_ids": [ + "s01_consultation:5-다" + ], + "source_domain": "B2_Secured_Registry" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "legal_effect_tags": [ + "registry_defect_hint" + ], + "asset_bindings": [ + "흑석동 상가" + ], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "이수인", + "윤건우" + ], + "action_summary": null, + "title_hint": "흑석동 상가 제3자 명의 등기 요청 및 거절", + "summary_hint": null, + "event_state_type": "event", + "asset_alias": [] + } + }, + "bh21": { + "BO_ID": "bh21", + "id": "bh21", + "BOType": "event", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "성수동 대지 차임 시세 감정평가", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "가치평가", + "임대료", + "차임", + "valuation" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "성수동 대지 차임 시세 감정평가", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [ + "E-005" + ], + "Evidence": [ + { + "evidence_index": "E-005", + "source_title": "감정평가 의견서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-005-01" + ], + "source_meeting_clause_ids": [], + "source_domain": "B3_Land_Valuation" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "legal_effect_tags": [ + "valuation" + ], + "asset_bindings": [ + "SCH-016-01" + ], + "asset_binding_hint": "성수동 256 대", + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "강용원", + "감정평가법인 광나루" + ], + "action_summary": "성수동 대지 차임 시세 평가 회신", + "title_hint": "성수동 대지 차임 시세 감정평가", + "summary_hint": "성수동 256 대지에 대한 월 차임 250만 원~500만 원 시세 평가", + "event_state_type": "event", + "asset_alias": [] + } + }, + "bh22": { + "BO_ID": "bh22", + "id": "bh22", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "성수동 대지 및 건물 인도, 점유 및 사용", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "인도", + "점유", + "사용", + "possession" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "성수동 대지 및 건물 인도, 점유 및 사용", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-006" + ], + "Evidence": [ + { + "evidence_index": "E-006", + "source_title": "계약서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-006-03", + "EVT-006-06", + "EVT-006-07" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-바", + "s01_consultation:2-사" + ], + "source_domain": "B3_Land_Valuation" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "legal_effect_tags": [ + "possession" + ], + "asset_bindings": [ + "SCH-016-01", + "SCH-016-02" + ], + "asset_binding_hint": "성수동 256 대지 및 지상 건물", + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "이문호", + "박성희" + ], + "action_summary": "박성희의 성수동 대지 및 건물 점유", + "title_hint": "성수동 대지/건물 점유", + "summary_hint": "이문호가 성수동 대지와 건물을 박성희에게 인도하고, 박성희가 관리 및 사용권 귀속", + "event_state_type": "state", + "asset_alias": [] + } + }, + "bh23": { + "BO_ID": "bh23", + "id": "bh23", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "신림로 상가 및 포천시 임야 가치평가(감정평가서)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "가치평가", + "감정평가", + "valuation" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "신림로 상가 및 포천시 임야 가치평가(감정평가서)", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-004" + ], + "Evidence": [ + { + "evidence_index": "E-004", + "source_title": "감정평가서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-004-01", + "EVT-004-02" + ], + "source_meeting_clause_ids": [], + "source_domain": "B3_Land_Valuation" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "legal_effect_tags": [ + "valuation" + ], + "asset_bindings": [ + "SCH-016-03" + ], + "asset_binding_hint": "신림로 상가, 포천시 임야", + "candidate_details": {}, + "facts": {}, + "parties_hint": [], + "action_summary": "신림로 상가 및 포천시 임야 시가 평가", + "title_hint": "신림로 상가 및 포천시 임야 감정평가", + "summary_hint": "신림로 상가와 포천시 임야에 대한 감정평가 및 시가 산정", + "event_state_type": "state", + "asset_alias": [] + } + }, + "bh24": { + "BO_ID": "bh24", + "id": "bh24", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "평택시 서정빌라 101호 점유 및 유치권 행사", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "점유", + "거주", + "유치권", + "수익", + "possession", + "defense_assertion_benefit_applied_to_secured_claim" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "평택시 서정빌라 101호 점유 및 유치권 행사", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-011" + ], + "Evidence": [ + { + "evidence_index": "E-011", + "source_title": "답변서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-011-02", + "EVT-011-03" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-가", + "s03_client_requests:4" + ], + "source_domain": "B3_Land_Valuation" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "legal_effect_tags": [ + "possession", + "defense_assertion_benefit_applied_to_secured_claim" + ], + "asset_bindings": [ + "SCH-016-04" + ], + "asset_binding_hint": "평택시 서정3길 123 서정빌라 101호", + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "박광윤" + ], + "action_summary": "박광윤의 평택시 서정빌라 거주 및 유치권 행사", + "title_hint": "서정빌라 점유 및 유치권주장", + "summary_hint": "박광윤이 평택시 서정빌라를 점유·거주·임대하여 수익을 취하며, 피담보채권 충당 항변", + "event_state_type": "state", + "asset_alias": [] + } + }, + "bh25": { + "BO_ID": "bh25", + "id": "bh25", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "평택시 서정빌라 차임 시세 평가", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "가치평가", + "차임", + "valuation" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "평택시 서정빌라 차임 시세 평가", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [ + "E-023" + ], + "Evidence": [ + { + "evidence_index": "E-023", + "source_title": "임료시세확인서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-023-01" + ], + "source_meeting_clause_ids": [], + "source_domain": "B3_Land_Valuation" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "legal_effect_tags": [ + "valuation" + ], + "asset_bindings": [ + "SCH-016-04" + ], + "asset_binding_hint": "서정빌라 101호", + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "김민식" + ], + "action_summary": "서정빌라 101호 월 차임 시세 확인", + "title_hint": "서정빌라 임료시세확인", + "summary_hint": "2022. 10. 기준 평택시 서정빌라 월세 200만 원 시세 평가", + "event_state_type": "state", + "asset_alias": [] + } + }, + "bh26": { + "BO_ID": "bh26", + "id": "bh26", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "흑석동 상가 점유 및 운영 (맛나식당)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "인도", + "점유", + "운영", + "사업자등록", + "possession" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "흑석동 상가 점유 및 운영 (맛나식당)", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "Evidence": [ + { + "evidence_index": "E-017", + "source_title": "부동산 매매계약서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + }, + { + "evidence_index": "E-018", + "source_title": "사업자등록증", + "priority_class": null, + "authentication_status": null, + "excerpt": null + }, + { + "evidence_index": "E-022", + "source_title": "임대차계약서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-017-04", + "EVT-018-01", + "EVT-022-03" + ], + "source_meeting_clause_ids": [ + "s01_consultation:5-가", + "s01_consultation:5-나", + "s01_consultation:5-라" + ], + "source_domain": "B3_Land_Valuation" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "legal_effect_tags": [ + "possession" + ], + "asset_bindings": [ + "SCH-016-05" + ], + "asset_binding_hint": "흑석동 201호", + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "양정숙" + ], + "action_summary": "양정숙의 흑석동 상가 맛나식당 운영", + "title_hint": "흑석동 상가 점유/운영", + "summary_hint": "양정숙이 흑석동 상가를 이수인으로부터 인도받아 맛나식당을 운영 중임", + "event_state_type": "state", + "asset_alias": [] + } + }, + "bh27": { + "BO_ID": "bh27", + "id": "bh27", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "매매" + }, + "Action": "채무자 오국한이 수익자 오민한에게 책임재산인 신림동 상가를 1억5천만원에 매도하여 소유권이전등기 경료", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "사해행위", + "매매계약", + "소유권이전", + "책임재산감소", + "fraudulent_act", + "매매" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "채무자 오국한이 수익자 오민한에게 책임재산인 신림동 상가를 1억5천만원에 매도하여 소유권이전등기 경료", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [ + "E-014" + ], + "Evidence": [ + { + "evidence_index": "E-014", + "source_title": "등기사항전부증명서 (말소사항 포함) -건물", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-014-03" + ], + "source_meeting_clause_ids": [], + "source_domain": "B4_Actio" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "fraudulent_transfer_BO", + "legal_effect_tags": [ + "fraudulent_act" + ], + "asset_bindings": [ + "신림동 상가" + ], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "오국한(채무자)", + "오민한(수익자)" + ], + "action_summary": "신림동 상가 1억5천만원 매매 및 소유권이전", + "title_hint": "사해행위 성립 (신림동 상가 매매)", + "summary_hint": "채무자 오국한이 수익자 오민한에게 책임재산 처분", + "event_state_type": "event", + "asset_alias": [] + } + }, + "bh28": { + "BO_ID": "bh28", + "id": "bh28", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "소비대차" + }, + "Action": "강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "피보전채권", + "대여금채권", + "preserved_claim", + "소비대차" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [ + "E-024" + ], + "Evidence": [ + { + "evidence_index": "E-024", + "source_title": "차용증", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-024-01", + "EVT-024-02" + ], + "source_meeting_clause_ids": [], + "source_domain": "B4_Actio" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "preserved_claim_bundle_BO", + "legal_effect_tags": [ + "preserved_claim" + ], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "강용원(채권자)", + "오국한(채무자)" + ], + "action_summary": "강용원이 오국한에게 2억원 대여", + "title_hint": "피보전채권 성립", + "summary_hint": "강용원의 오국한에 대한 대여금 2억원 피보전채권 번들", + "event_state_type": "event", + "asset_alias": [] + } + }, + "bh29": { + "BO_ID": "bh29", + "id": "bh29", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "변제 및 말소" + }, + "Action": "수익자 오민한이 사해행위 이후 피담보채무 1억3천만원을 대위변제하고 2번 근저당권을 말소시킴으로써 원물반환이 불가능해져 가액배상 요건 성립", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "원물반환불능", + "가액배상", + "근저당권말소", + "value_compensation", + "remedy_cap", + "변제 및 말소" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "수익자 오민한이 사해행위 이후 피담보채무 1억3천만원을 대위변제하고 2번 근저당권을 말소시킴으로써 원물반환이 불가능해져 가액배상 요건 성립", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [ + "E-014" + ], + "Evidence": [ + { + "evidence_index": "E-014", + "source_title": "등기사항전부증명서 (말소사항 포함) -건물", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-014-04", + "EVT-014-05", + "EVT-014-06" + ], + "source_meeting_clause_ids": [ + "s01_consultation:3-라" + ], + "source_domain": "B4_Actio" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "remedy_mode_decision_BO", + "legal_effect_tags": [ + "value_compensation", + "remedy_cap" + ], + "asset_bindings": [ + "신림동 상가" + ], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "오민한(수익자)" + ], + "action_summary": "강용일 명의 2번 근저당권 말소(수익자 변제)", + "title_hint": "가액배상 방식 결정 및 한도", + "summary_hint": "수익자의 대위변제에 의한 저당권 소멸과 가액배상 산정 기준", + "event_state_type": "event", + "asset_alias": [] + } + }, + "bh30": { + "BO_ID": "bh30", + "id": "bh30", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "무자력", + "책임재산", + "가치평가", + "insolvency_check" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-004" + ], + "Evidence": [ + { + "evidence_index": "E-004", + "source_title": "감정평가서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-004-01", + "EVT-004-02" + ], + "source_meeting_clause_ids": [], + "source_domain": "B4_Actio" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "actio_balance_sheet_BO", + "legal_effect_tags": [ + "insolvency_check" + ], + "asset_bindings": [ + "신림동 상가", + "포천시 임야" + ], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "오국한" + ], + "action_summary": "처분행위 당시 자산 및 부채 비교", + "title_hint": "채무자 무자력 증명", + "summary_hint": "감정평가를 통한 처분당시 오국한의 자산상태 입증", + "event_state_type": "state", + "asset_alias": [] + } + }, + "bh31": { + "BO_ID": "bh31", + "id": "bh31", + "BOType": "state", + "ActionType": "소송행위(litigation acts)", + "JuristicAct": { + "label": "항변" + }, + "Action": "수익자 오민한의 항변 (명의신탁 주장, 매수자금 및 대여금 참작 주장, 채권 상계 주장)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "수익자항변", + "명의신탁", + "상계항변", + "선의항변", + "defense_assertion", + "항변" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "수익자 오민한의 항변 (명의신탁 주장, 매수자금 및 대여금 참작 주장, 채권 상계 주장)", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-003", + "E-010" + ], + "Evidence": [ + { + "evidence_index": "E-003", + "source_title": "각서 및 금전소비대차계약서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + }, + { + "evidence_index": "E-010", + "source_title": "내용증명에 대한 답신", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-003-01", + "EVT-003-02", + "EVT-010-03" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:3-가", + "s02_party_assertions:3-나" + ], + "source_domain": "B4_Actio" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "defense_rebuttal_matrix_BO", + "legal_effect_tags": [ + "defense_assertion" + ], + "asset_bindings": [ + "신림동 상가" + ], + "asset_binding_hint": null, + "candidate_details": {}, + "facts": {}, + "parties_hint": [ + "오민한" + ], + "action_summary": "수익자가 사해의사 부정 및 상계항변 제출", + "title_hint": "수익자 오민한의 항변 분석", + "summary_hint": "계약명의신탁, 자금제공 참작, 상계 등 수익자 항변 검토", + "event_state_type": "state", + "asset_alias": [] + } + }, + "bh32": { + "BO_ID": "bh32", + "id": "bh32", + "BOType": "event", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": "상속개시" + }, + "Action": "강석우 사망으로 인한 정유심, 강용원의 성수동 대지 지분 상속", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "상속개시" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "강석우 사망으로 인한 정유심, 강용원의 성수동 대지 지분 상속", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-013" + ], + "Evidence": [ + { + "evidence_index": "E-013", + "source_title": "등기사항전부증명서(말소사항 포함)-토지", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-013-01" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-가" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": null, + "facts": null, + "parties_hint": [], + "action_summary": null, + "title_hint": null, + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh33": { + "BO_ID": "bh33", + "id": "bh33", + "BOType": "event", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": "상속포기" + }, + "Action": "망 강호연의 사망에 따른 상속개시 및 자녀 강형수, 강지수의 상속포기", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "상속포기" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "망 강호연의 사망에 따른 상속개시 및 자녀 강형수, 강지수의 상속포기", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-001", + "E-002" + ], + "Evidence": [ + { + "evidence_index": "E-001", + "source_title": "가정법원심판_강호연", + "priority_class": null, + "authentication_status": null, + "excerpt": null + }, + { + "evidence_index": "E-002", + "source_title": "가족관계증명서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-001-01", + "EVT-001-02", + "EVT-002-01" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-가" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": null, + "facts": null, + "parties_hint": [], + "action_summary": null, + "title_hint": null, + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh34": { + "BO_ID": "bh34", + "id": "bh34", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "최고" + }, + "Action": "강용원이 김선웅에게 물품대금 지급을 최고하는 통지서 발송 및 도달", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "최고" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "강용원이 김선웅에게 물품대금 지급을 최고하는 통지서 발송 및 도달", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-027" + ], + "Evidence": [ + { + "evidence_index": "E-027", + "source_title": "최고 통지서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-027-01", + "EVT-027-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:1-나" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": null, + "facts": null, + "parties_hint": [], + "action_summary": null, + "title_hint": null, + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh35": { + "BO_ID": "bh35", + "id": "bh35", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "최고" + }, + "Action": "강용원이 오민한에게 사해행위취소 및 신림동 상가 소유권 반환을 요구하는 내용증명 발송", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "최고" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "강용원이 오민한에게 사해행위취소 및 신림동 상가 소유권 반환을 요구하는 내용증명 발송", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-008", + "E-010" + ], + "Evidence": [ + { + "evidence_index": "E-008", + "source_title": "내용증명", + "priority_class": null, + "authentication_status": null, + "excerpt": null + }, + { + "evidence_index": "E-010", + "source_title": "내용증명에 대한 답신", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-008-01", + "EVT-010-01" + ], + "source_meeting_clause_ids": [], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": null, + "facts": null, + "parties_hint": [], + "action_summary": null, + "title_hint": null, + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh36": { + "BO_ID": "bh36", + "id": "bh36", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "강용원과 양정숙이 박광윤에게 평택시 빌라 유치권소멸을 청구하는 통지서 발송 및 도달", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "통지" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "강용원과 양정숙이 박광윤에게 평택시 빌라 유치권소멸을 청구하는 통지서 발송 및 도달", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-011", + "E-019" + ], + "Evidence": [ + { + "evidence_index": "E-011", + "source_title": "답변서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + }, + { + "evidence_index": "E-019", + "source_title": "소멸청구 통지서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-011-01" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-다" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": null, + "facts": null, + "parties_hint": [], + "action_summary": null, + "title_hint": null, + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh37": { + "BO_ID": "bh37", + "id": "bh37", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "오민한이 강용원에게 김선웅의 채무에 대한 책임을 부인하는 통지서 발송", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "통지" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "오민한이 강용원에게 김선웅의 채무에 대한 책임을 부인하는 통지서 발송", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-028" + ], + "Evidence": [ + { + "evidence_index": "E-028", + "source_title": "통지서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-028-01" + ], + "source_meeting_clause_ids": [], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": null, + "facts": null, + "parties_hint": [], + "action_summary": null, + "title_hint": null, + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh38": { + "BO_ID": "bh38", + "id": "bh38", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "오민한이 강용원에게 내용증명에 대한 답신 발송", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "통지" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "오민한이 강용원에게 내용증명에 대한 답신 발송", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-010" + ], + "Evidence": [ + { + "evidence_index": "E-010", + "source_title": "내용증명에 대한 답신", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-010-02" + ], + "source_meeting_clause_ids": [], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": null, + "facts": null, + "parties_hint": [], + "action_summary": null, + "title_hint": null, + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh39": { + "BO_ID": "bh39", + "id": "bh39", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "윤건우가 양정숙에게 흑석동 상가 임대차계약 해지 및 명도를 요구하는 내용증명 통지", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "통지" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "윤건우가 양정숙에게 흑석동 상가 임대차계약 해지 및 명도를 요구하는 내용증명 통지", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-009" + ], + "Evidence": [ + { + "evidence_index": "E-009", + "source_title": "내용증명", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-009-01", + "EVT-009-02" + ], + "source_meeting_clause_ids": [], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": null, + "facts": null, + "parties_hint": [], + "action_summary": null, + "title_hint": null, + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh40": { + "BO_ID": "bh40", + "id": "bh40", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "윤건우가 이수인에게 흑석동 상가 매매계약 해제 통지서 발송 및 도달", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "통지" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "윤건우가 이수인에게 흑석동 상가 매매계약 해제 통지서 발송 및 도달", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-030" + ], + "Evidence": [ + { + "evidence_index": "E-030", + "source_title": "해제 통지서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-030-01", + "EVT-030-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:5-라" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": null, + "facts": null, + "parties_hint": [], + "action_summary": null, + "title_hint": null, + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh41": { + "BO_ID": "bh41", + "id": "bh41", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "이수인이 윤건우에게 흑석동 상가 매매대금 감액 및 제3자 명의 등기 이전 요청 통지", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "통지" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "이수인이 윤건우에게 흑석동 상가 매매대금 감액 및 제3자 명의 등기 이전 요청 통지", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-029" + ], + "Evidence": [ + { + "evidence_index": "E-029", + "source_title": "통지서_이수인_윤건우", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-029-01" + ], + "source_meeting_clause_ids": [ + "s01_consultation:5-다" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": null, + "facts": null, + "parties_hint": [], + "action_summary": null, + "title_hint": null, + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh42": { + "BO_ID": "bh42", + "id": "bh42", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "주식회사 대한은행이 강용원의 성수동 대지 근저당권 말소 신청을 거부하는 통지 발송", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "통지" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "주식회사 대한은행이 강용원의 성수동 대지 근저당권 말소 신청을 거부하는 통지 발송", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "event", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-012" + ], + "Evidence": [ + { + "evidence_index": "E-012", + "source_title": "등기말소 신청에 대한 답신", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-012-01" + ], + "source_meeting_clause_ids": [], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": null, + "facts": null, + "parties_hint": [], + "action_summary": null, + "title_hint": null, + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh43": { + "BO_ID": "bh43", + "id": "bh43", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "목적물 특정", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "목적물 특정", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "Evidence": [ + { + "evidence_index": "E-013", + "source_title": "등기사항전부증명서(말소사항 포함)-토지", + "priority_class": null, + "authentication_status": null, + "excerpt": null + }, + { + "evidence_index": "E-014", + "source_title": "등기사항전부증명서 (말소사항 포함) -건물", + "priority_class": null, + "authentication_status": null, + "excerpt": null + }, + { + "evidence_index": "E-015", + "source_title": "등기사항전부증명서(말소사항 포함)-집합건물", + "priority_class": null, + "authentication_status": null, + "excerpt": null + }, + { + "evidence_index": "E-016", + "source_title": "별지 목록", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": null, + "facts": null, + "parties_hint": [], + "action_summary": null, + "title_hint": null, + "summary_hint": null, + "event_state_type": null, + "asset_alias": [ + { + "asset_ref": "성수동 대지", + "alias_strings": [ + "성수동 대지", + "서울 성동구 성수동 256" + ], + "canonical_label_candidate": "1. 서울 성동구 성수동 256 대 330㎡", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-01" + ], + "registry_refs": [ + { + "registry_ref": "REG-013-01", + "receipt_no_refs": [ + "제1453호" + ] + }, + { + "registry_ref": "REG-013-02", + "receipt_no_refs": [ + "제7026호" + ] + }, + { + "registry_ref": "REG-013-03", + "receipt_no_refs": [ + "제11202호" + ] + }, + { + "registry_ref": "REG-013-04", + "receipt_no_refs": [ + "제7202호" + ] + }, + { + "registry_ref": "REG-013-05", + "receipt_no_refs": [ + "제12402호" + ] + }, + { + "registry_ref": "REG-013-06", + "receipt_no_refs": [ + "제25797호" + ] + }, + { + "registry_ref": "REG-013-07", + "receipt_no_refs": [ + "제25799호" + ] + } + ], + "source_evidence_indexes": [ + "E-013", + "E-016" + ] + }, + { + "asset_ref": "성수동 건물", + "alias_strings": [ + "성수동 건물", + "서울 성동구 성수동 256 상가" + ], + "canonical_label_candidate": "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-02" + ], + "registry_refs": [], + "source_evidence_indexes": [ + "E-016" + ] + }, + { + "asset_ref": "신림동 상가", + "alias_strings": [ + "신림동 상가", + "서울 관악구 신림로 115" + ], + "canonical_label_candidate": "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-03" + ], + "registry_refs": [ + { + "registry_ref": "REG-014-01", + "receipt_no_refs": [ + "제24759호" + ] + }, + { + "registry_ref": "REG-014-02", + "receipt_no_refs": [ + "제56457호" + ] + }, + { + "registry_ref": "REG-014-03", + "receipt_no_refs": [ + "제11593호" + ] + }, + { + "registry_ref": "REG-014-04", + "receipt_no_refs": [ + "제50099호" + ] + }, + { + "registry_ref": "REG-014-05", + "receipt_no_refs": [ + "제26597호" + ] + }, + { + "registry_ref": "REG-014-06", + "receipt_no_refs": [ + "제74586호" + ] + } + ], + "source_evidence_indexes": [ + "E-014", + "E-016" + ] + }, + { + "asset_ref": "평택시 빌라", + "alias_strings": [ + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123" + ], + "canonical_label_candidate": "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-04" + ], + "registry_refs": [ + { + "registry_ref": "REG-015-01", + "receipt_no_refs": [ + "제7233호" + ] + }, + { + "registry_ref": "REG-015-02", + "receipt_no_refs": [ + "제7871호" + ] + }, + { + "registry_ref": "REG-015-03", + "receipt_no_refs": [ + "제11593호" + ] + }, + { + "registry_ref": "REG-015-04", + "receipt_no_refs": [ + "제12321호" + ] + } + ], + "source_evidence_indexes": [ + "E-015", + "E-016" + ] + }, + { + "asset_ref": "흑석동 상가", + "alias_strings": [ + "흑석동 상가", + "서울 동작구 흑석로 80" + ], + "canonical_label_candidate": "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-05" + ], + "registry_refs": [], + "source_evidence_indexes": [ + "E-016" + ] + } + ] + } + }, + "bh44": { + "BO_ID": "bh44", + "id": "bh44", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "박광윤의 평택시 서정빌라 101호 거주 및 유치권 행사 상태", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "박광윤의 평택시 서정빌라 101호 거주 및 유치권 행사 상태", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-011" + ], + "Evidence": [ + { + "evidence_index": "E-011", + "source_title": "답변서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-011-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-가" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": null, + "facts": null, + "parties_hint": [], + "action_summary": null, + "title_hint": null, + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh45": { + "BO_ID": "bh45", + "id": "bh45", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "박성희의 성수동 대지 및 건물 점유", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "박성희의 성수동 대지 및 건물 점유", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-006" + ], + "Evidence": [ + { + "evidence_index": "E-006", + "source_title": "계약서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-006-07" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-사" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": null, + "facts": null, + "parties_hint": [], + "action_summary": null, + "title_hint": null, + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh46": { + "BO_ID": "bh46", + "id": "bh46", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "양정숙의 흑석동 상가 점유 및 식당 운영 상태", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "양정숙의 흑석동 상가 점유 및 식당 운영 상태", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-018", + "E-022" + ], + "Evidence": [ + { + "evidence_index": "E-018", + "source_title": "사업자등록증", + "priority_class": null, + "authentication_status": null, + "excerpt": null + }, + { + "evidence_index": "E-022", + "source_title": "임대차계약서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-018-01", + "EVT-022-03" + ], + "source_meeting_clause_ids": [ + "s01_consultation:5-나" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": null, + "facts": null, + "parties_hint": [], + "action_summary": null, + "title_hint": null, + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh47": { + "BO_ID": "bh47", + "id": "bh47", + "BOType": "state", + "ActionType": "소송행위(litigation acts)", + "JuristicAct": { + "label": "상계" + }, + "Action": "오민한의 강용원에 대한 자신의 5,000만 원 채권 상계 주장 항변", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "상계" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "오민한의 강용원에 대한 자신의 5,000만 원 채권 상계 주장 항변", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": "KRW" + }, + "source_evidence_indexes": [ + "E-010" + ], + "Evidence": [ + { + "evidence_index": "E-010", + "source_title": "내용증명에 대한 답신", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-010-03" + ], + "source_meeting_clause_ids": [], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": null, + "facts": null, + "parties_hint": [], + "action_summary": null, + "title_hint": null, + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh48": { + "BO_ID": "bh48", + "id": "bh48", + "BOType": "state", + "ActionType": "소송행위(litigation acts)", + "JuristicAct": { + "label": "항변" + }, + "Action": "박광윤의 과거 임대료 및 거주 수익을 채무에 충당하였다는 주장 항변", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "항변" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "박광윤의 과거 임대료 및 거주 수익을 채무에 충당하였다는 주장 항변", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-011" + ], + "Evidence": [ + { + "evidence_index": "E-011", + "source_title": "답변서", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-011-03" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:4-나" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": null, + "facts": null, + "parties_hint": [], + "action_summary": null, + "title_hint": null, + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh49": { + "BO_ID": "bh49", + "id": "bh49", + "BOType": "state", + "ActionType": "소송행위(litigation acts)", + "JuristicAct": { + "label": "항변" + }, + "Action": "윤건우의 임대차 책임 부인 및 양정숙 차임 연체 주장 항변", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "항변" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "윤건우의 임대차 책임 부인 및 양정숙 차임 연체 주장 항변", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-009" + ], + "Evidence": [ + { + "evidence_index": "E-009", + "source_title": "내용증명", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-009-03" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:5" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": null, + "facts": null, + "parties_hint": [], + "action_summary": null, + "title_hint": null, + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + }, + "bh50": { + "BO_ID": "bh50", + "id": "bh50", + "BOType": "state", + "ActionType": "소송행위(litigation acts)", + "JuristicAct": { + "label": "항변" + }, + "Action": "주식회사 대한은행의 경매공신력 주장(민사집행법 제267조)에 따른 근저당 말소 불가 항변", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "항변" + ], + "core_field_base": { + "Performer": null, + "PerformerType": null, + "Action_proposal": "주식회사 대한은행의 경매공신력 주장(민사집행법 제267조)에 따른 근저당 말소 불가 항변", + "Subject": null, + "Object": null, + "BehaviorTime": null, + "TimeText": null, + "TimePrecision": null, + "StatementType": "state", + "Perspective": "plaintiff" + }, + "amount": { + "value_text": null, + "numeric_value": null, + "currency": null + }, + "source_evidence_indexes": [ + "E-012" + ], + "Evidence": [ + { + "evidence_index": "E-012", + "source_title": "등기말소 신청에 대한 답신", + "priority_class": null, + "authentication_status": null, + "excerpt": null + } + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-012-02" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:2" + ], + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense" + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": [], + "canonical_theory_graph_seed_ref_proposed": null, + "legal_effect_structure_seed_ref_proposed": null + }, + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [], + "asset_bindings": [], + "asset_binding_hint": null, + "candidate_details": null, + "facts": null, + "parties_hint": [], + "action_summary": null, + "title_hint": null, + "summary_hint": null, + "event_state_type": null, + "asset_alias": [] + } + } + }, + "evidence_authority_by_index": { + "E-001": { + "evidence_index": "E-001", + "source_title": "가정법원심판_강호연", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-002": { + "evidence_index": "E-002", + "source_title": "가족관계증명서", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-003": { + "evidence_index": "E-003", + "source_title": "각서 및 금전소비대차계약서", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-004": { + "evidence_index": "E-004", + "source_title": "감정평가서", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-005": { + "evidence_index": "E-005", + "source_title": "감정평가 의견서", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-006": { + "evidence_index": "E-006", + "source_title": "계약서", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-007": { + "evidence_index": "E-007", + "source_title": "공사도급계약서", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-008": { + "evidence_index": "E-008", + "source_title": "내용증명", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-009": { + "evidence_index": "E-009", + "source_title": "내용증명", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-010": { + "evidence_index": "E-010", + "source_title": "내용증명에 대한 답신", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-011": { + "evidence_index": "E-011", + "source_title": "답변서", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-012": { + "evidence_index": "E-012", + "source_title": "등기말소 신청에 대한 답신", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-013": { + "evidence_index": "E-013", + "source_title": "등기사항전부증명서(말소사항 포함)-토지", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-014": { + "evidence_index": "E-014", + "source_title": "등기사항전부증명서 (말소사항 포함) -건물", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-015": { + "evidence_index": "E-015", + "source_title": "등기사항전부증명서(말소사항 포함)-집합건물", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-016": { + "evidence_index": "E-016", + "source_title": "별지 목록", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-017": { + "evidence_index": "E-017", + "source_title": "부동산 매매계약서", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-018": { + "evidence_index": "E-018", + "source_title": "사업자등록증", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-019": { + "evidence_index": "E-019", + "source_title": "소멸청구 통지서", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-020": { + "evidence_index": "E-020", + "source_title": "영업양도 계약서", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-021": { + "evidence_index": "E-021", + "source_title": "이행각서", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-022": { + "evidence_index": "E-022", + "source_title": "임대차계약서", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-023": { + "evidence_index": "E-023", + "source_title": "임료시세확인서", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-024": { + "evidence_index": "E-024", + "source_title": "차용증", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-025": { + "evidence_index": "E-025", + "source_title": "차용증", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-026": { + "evidence_index": "E-026", + "source_title": "차용증", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-027": { + "evidence_index": "E-027", + "source_title": "최고 통지서", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-028": { + "evidence_index": "E-028", + "source_title": "통지서", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-029": { + "evidence_index": "E-029", + "source_title": "통지서_이수인_윤건우", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + }, + "E-030": { + "evidence_index": "E-030", + "source_title": "해제 통지서", + "priority_class": null, + "authentication_status": null, + "excerpt": null, + "source_file": "evidence_indexed.json" + } + }, + "events_by_evidence_index": { + "E-001": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-002": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-003": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-004": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-005": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-006": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-007": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-008": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-009": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-010": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-011": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-012": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-013": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-014": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-015": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-016": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-017": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-018": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-020": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-021": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-022": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-023": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-024": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-025": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-026": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-027": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-028": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-029": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "E-030": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ] + }, + "signals_by_bo_id": { + "bh1": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-001", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-007" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:011", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "도급", + "공사대금", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-007" + ] + } + ] + }, + "bh2": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-002", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-021", + "E-027" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:001", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "물품대금", + "매매", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-021", + "E-027" + ] + } + ] + }, + "bh3": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-003", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-024" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:006", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "대여금", + "소비대차", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-024" + ] + } + ] + }, + "bh4": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-004", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-024" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:007", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "대여금", + "소비대차", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-024" + ] + } + ] + }, + "bh5": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-005", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-025" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:008", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "대여금", + "소비대차", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-025" + ] + } + ] + }, + "bh6": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-006", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-026" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:009", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "대여금", + "소비대차", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-026" + ] + } + ] + }, + "bh7": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-007", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-020" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:004", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "영업양도", + "상호속용", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-020" + ] + } + ] + }, + "bh8": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-008", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-021" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:002", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "채무승인", + "변제기 유예", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-021" + ] + } + ] + }, + "bh9": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-009", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:010", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "변제", + "법정변제충당", + "event", + "사실행위(factual acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [] + } + ] + }, + "bh10": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-010", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:013", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "상계", + "항변", + "event", + "소송행위(litigation acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ] + }, + "bh11": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-011", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-027" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:003", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "이행최고", + "통지", + "최고", + "event", + "준법률행위(quasi-legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-027" + ] + } + ] + }, + "bh12": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-012", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-028" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:005", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "통지", + "책임부인", + "event", + "준법률행위(quasi-legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-028" + ] + } + ] + }, + "bh13": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-013", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:012", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "미지급", + "충당", + "state", + "사실행위(factual acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ] + } + ] + }, + "bh14": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-014", + "liability_candidate_type": "secured_debt", + "related_evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B2:001", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "secured_debt_creation", + "secured_registry_or_auction", + "상속", + "증여", + "근저당권설정", + "(근)저당권 설정 계약", + "event", + "법률행위(legal acts)", + "B2_Secured_Registry" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ] + } + ] + }, + "bh15": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-015", + "liability_candidate_type": "registry_invalidity", + "related_evidence_indexes": [ + "E-010", + "E-014" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B2:004", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "secured_debt_chain", + "mortgage_extinguishment", + "secured_registry_or_auction", + "근저당권설정", + "말소", + "상계", + "근저당권 말소 및 상계", + "event", + "법률행위(legal acts)", + "B2_Secured_Registry" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-010", + "E-014" + ] + } + ] + }, + "bh16": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-016", + "liability_candidate_type": "secured_debt", + "related_evidence_indexes": [ + "E-015" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B2:006", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "secured_registry_or_auction", + "소유권보존", + "소유권이전", + "가압류", + "소유권이전등기", + "event", + "법률행위(legal acts)", + "B2_Secured_Registry" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-015" + ] + } + ] + }, + "bh17": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-017", + "liability_candidate_type": "registry_invalidity", + "related_evidence_indexes": [ + "E-012", + "E-013" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B2:003", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "secured_registry_or_auction", + "임의경매", + "공신력", + "등기무효", + "담보권실행을 위한 경매 및 근저당권 설정", + "event", + "소송행위(litigation acts)", + "B2_Secured_Registry" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-012", + "E-013" + ] + } + ] + }, + "bh18": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-018", + "liability_candidate_type": "secured_debt", + "related_evidence_indexes": [], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B2:002", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "payment_allocation_uncertain", + "secured_registry_or_auction", + "변제", + "변제충당", + "변제 및 변제충당", + "event", + "준법률행위(quasi-legal acts)", + "B2_Secured_Registry" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [] + } + ] + }, + "bh19": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-019", + "liability_candidate_type": "money_claim", + "related_evidence_indexes": [ + "E-007", + "E-011" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B2:005", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "payment_allocation", + "secured_registry_or_auction", + "유치권", + "점유", + "변제충당", + "변제충당 주장", + "event", + "준법률행위(quasi-legal acts)", + "B2_Secured_Registry" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-007", + "E-011" + ] + } + ] + }, + "bh20": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-020", + "liability_candidate_type": "secured_debt", + "related_evidence_indexes": [ + "E-029" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B2:007", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "registry_defect_hint", + "secured_registry_or_auction", + "소유권이전등기", + "요구", + "거절", + "제3자 명의 등기 요청", + "event", + "준법률행위(quasi-legal acts)", + "B2_Secured_Registry" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-029" + ] + } + ] + }, + "bh21": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-021", + "liability_candidate_type": "land_use_gain", + "related_evidence_indexes": [ + "E-005" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B3:002", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "valuation", + "land_use_gain_or_valuation", + "가치평가", + "임대료", + "차임", + "event", + "사실행위(factual acts)", + "B3_Land_Valuation" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-005" + ] + } + ] + }, + "bh22": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-022", + "liability_candidate_type": "land_use_gain", + "related_evidence_indexes": [ + "E-006" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B3:004", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "possession", + "land_use_gain_or_valuation", + "인도", + "점유", + "사용", + "state", + "사실행위(factual acts)", + "B3_Land_Valuation" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-006" + ] + } + ] + }, + "bh23": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-023", + "liability_candidate_type": "land_use_gain", + "related_evidence_indexes": [ + "E-004" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B3:001", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "valuation", + "land_use_gain_or_valuation", + "가치평가", + "감정평가", + "state", + "사실행위(factual acts)", + "B3_Land_Valuation" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-004" + ] + } + ] + }, + "bh24": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-024", + "liability_candidate_type": "secured_debt", + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B3:005", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "possession", + "defense_assertion_benefit_applied_to_secured_claim", + "land_use_gain_or_valuation", + "점유", + "거주", + "유치권", + "수익", + "state", + "사실행위(factual acts)", + "B3_Land_Valuation" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ] + } + ] + }, + "bh25": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-025", + "liability_candidate_type": "land_use_gain", + "related_evidence_indexes": [ + "E-023" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B3:003", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "valuation", + "land_use_gain_or_valuation", + "가치평가", + "차임", + "state", + "사실행위(factual acts)", + "B3_Land_Valuation" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-023" + ] + } + ] + }, + "bh26": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-026", + "liability_candidate_type": "land_use_gain", + "related_evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B3:006", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "possession", + "land_use_gain_or_valuation", + "인도", + "점유", + "운영", + "사업자등록", + "state", + "사실행위(factual acts)", + "B3_Land_Valuation" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ] + } + ] + }, + "bh27": { + "actio": [ + { + "signal_id": "actio-signal-002", + "actio_scope": [ + "fraudulent_act", + "beneficiary_or_transferee", + "target_asset" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-014" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh27" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh27", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "actio_role_hints": [ + { + "role": "beneficiary", + "value_candidate": "{}", + "date_candidate": null, + "related_bo_ids": [ + "bh27" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ] + }, + { + "signal_id": "actio-signal-002", + "actio_scope": [ + "fraudulent_act", + "beneficiary_or_transferee", + "target_asset" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-014" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh27" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh27", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "actio_role_hints": [ + { + "role": "beneficiary", + "value_candidate": "{}", + "date_candidate": null, + "related_bo_ids": [ + "bh27" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ] + } + ], + "case_liability": [ + { + "signal_ref": "case-liability-signal-027", + "liability_candidate_type": "actio_related", + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B4:002", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "fraudulent_act", + "fraudulent_transfer_BO", + "사해행위", + "매매계약", + "소유권이전", + "책임재산감소", + "매매", + "event", + "법률행위(legal acts)", + "B4_Actio" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ] + } + ] + }, + "bh28": { + "actio": [ + { + "signal_id": "actio-signal-001", + "actio_scope": [ + "preserved_claim" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-024" + ], + "target_property_candidates": [], + "preserved_claim_candidates": [ + { + "bo_id": null, + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-024" + ] + } + ], + "actio_role_hints": [ + { + "role": "debtor_act", + "value_candidate": "강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생", + "date_candidate": null, + "related_bo_ids": [ + "bh28" + ], + "related_evidence_indexes": [ + "E-024" + ] + } + ] + } + ], + "case_liability": [ + { + "signal_ref": "case-liability-signal-028", + "liability_candidate_type": "actio_related", + "related_evidence_indexes": [ + "E-024" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B4:001", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "preserved_claim", + "preserved_claim_bundle_BO", + "피보전채권", + "대여금채권", + "소비대차", + "event", + "법률행위(legal acts)", + "B4_Actio" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-024" + ] + } + ] + }, + "bh29": { + "actio": [ + { + "signal_id": "actio-signal-004", + "actio_scope": [ + "target_asset", + "encumbrance", + "remedy_or_cap" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-014" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2021-08-01", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2022-04-18", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2024-10-17", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh29", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "actio_role_hints": [ + { + "role": "remedy_or_cap", + "value_candidate": "원물반환 불가", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "remedy_or_cap", + "value_candidate": "가액배상", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2021-08-01', 'event': '나임오 1번 근저당 설정(5000만원)'}", + "date_candidate": "2021-08-01", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2022-04-18', 'event': '강용일 2번 근저당 설정(1.5억원)'}", + "date_candidate": "2022-04-18", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2024-10-17', 'event': '강용일 2번 근저당 말소(오민한 대위변제)'}", + "date_candidate": "2024-10-17", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ] + }, + { + "signal_id": "actio-signal-004", + "actio_scope": [ + "target_asset", + "encumbrance", + "remedy_or_cap" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-014" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2021-08-01", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2022-04-18", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2024-10-17", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh29", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "actio_role_hints": [ + { + "role": "remedy_or_cap", + "value_candidate": "원물반환 불가", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "remedy_or_cap", + "value_candidate": "가액배상", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2021-08-01', 'event': '나임오 1번 근저당 설정(5000만원)'}", + "date_candidate": "2021-08-01", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2022-04-18', 'event': '강용일 2번 근저당 설정(1.5억원)'}", + "date_candidate": "2022-04-18", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2024-10-17', 'event': '강용일 2번 근저당 말소(오민한 대위변제)'}", + "date_candidate": "2024-10-17", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ] + }, + { + "signal_id": "actio-signal-004", + "actio_scope": [ + "target_asset", + "encumbrance", + "remedy_or_cap" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-014" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2021-08-01", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2022-04-18", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2024-10-17", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh29", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "actio_role_hints": [ + { + "role": "remedy_or_cap", + "value_candidate": "원물반환 불가", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "remedy_or_cap", + "value_candidate": "가액배상", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2021-08-01', 'event': '나임오 1번 근저당 설정(5000만원)'}", + "date_candidate": "2021-08-01", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2022-04-18', 'event': '강용일 2번 근저당 설정(1.5억원)'}", + "date_candidate": "2022-04-18", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2024-10-17', 'event': '강용일 2번 근저당 말소(오민한 대위변제)'}", + "date_candidate": "2024-10-17", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ] + }, + { + "signal_id": "actio-signal-004", + "actio_scope": [ + "target_asset", + "encumbrance", + "remedy_or_cap" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-014" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2021-08-01", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2022-04-18", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2024-10-17", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh29", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "actio_role_hints": [ + { + "role": "remedy_or_cap", + "value_candidate": "원물반환 불가", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "remedy_or_cap", + "value_candidate": "가액배상", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2021-08-01', 'event': '나임오 1번 근저당 설정(5000만원)'}", + "date_candidate": "2021-08-01", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2022-04-18', 'event': '강용일 2번 근저당 설정(1.5억원)'}", + "date_candidate": "2022-04-18", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2024-10-17', 'event': '강용일 2번 근저당 말소(오민한 대위변제)'}", + "date_candidate": "2024-10-17", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ] + }, + { + "signal_id": "actio-signal-004", + "actio_scope": [ + "target_asset", + "encumbrance", + "remedy_or_cap" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-014" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2021-08-01", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2022-04-18", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2024-10-17", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh29", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "actio_role_hints": [ + { + "role": "remedy_or_cap", + "value_candidate": "원물반환 불가", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "remedy_or_cap", + "value_candidate": "가액배상", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2021-08-01', 'event': '나임오 1번 근저당 설정(5000만원)'}", + "date_candidate": "2021-08-01", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2022-04-18', 'event': '강용일 2번 근저당 설정(1.5억원)'}", + "date_candidate": "2022-04-18", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2024-10-17', 'event': '강용일 2번 근저당 말소(오민한 대위변제)'}", + "date_candidate": "2024-10-17", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ] + } + ], + "case_liability": [ + { + "signal_ref": "case-liability-signal-029", + "liability_candidate_type": "actio_related", + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B4:004", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "value_compensation", + "remedy_cap", + "remedy_mode_decision_BO", + "원물반환불능", + "가액배상", + "근저당권말소", + "변제 및 말소", + "event", + "준법률행위(quasi-legal acts)", + "B4_Actio" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ] + } + ] + }, + "bh30": { + "actio": [ + { + "signal_id": "actio-signal-003", + "actio_scope": [ + "target_asset" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-004" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + }, + { + "asset_label": "포천시 임야", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh30", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-004" + ] + } + ], + "actio_role_hints": [ + { + "role": "debtor_act", + "value_candidate": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "date_candidate": null, + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + } + ] + }, + { + "signal_id": "actio-signal-003", + "actio_scope": [ + "target_asset" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-004" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + }, + { + "asset_label": "포천시 임야", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh30", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-004" + ] + } + ], + "actio_role_hints": [ + { + "role": "debtor_act", + "value_candidate": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "date_candidate": null, + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + } + ] + }, + { + "signal_id": "actio-signal-003", + "actio_scope": [ + "target_asset" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-004" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + }, + { + "asset_label": "포천시 임야", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh30", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-004" + ] + } + ], + "actio_role_hints": [ + { + "role": "debtor_act", + "value_candidate": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "date_candidate": null, + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + } + ] + } + ], + "case_liability": [ + { + "signal_ref": "case-liability-signal-030", + "liability_candidate_type": "actio_related", + "related_evidence_indexes": [ + "E-004" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B4:003", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "insolvency_check", + "actio_balance_sheet_BO", + "무자력", + "책임재산", + "가치평가", + "state", + "사실행위(factual acts)", + "B4_Actio" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-004" + ] + } + ] + }, + "bh31": { + "actio": [ + { + "signal_id": "actio-signal-005", + "actio_scope": [ + "target_asset", + "defense" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh31", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ], + "actio_role_hints": [ + { + "role": "defense", + "value_candidate": "{'defense_type': '계약명의신탁', 'content': '오국한 명의 취득이 수익자 자금에 의한 계약명의신탁이므로 사해행위가 아니라는 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "role": "defense", + "value_candidate": "{'defense_type': '배상금감액', 'content': '수익자가 제공한 매수자금 4000만원 및 대여금 1000만원 참작 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "role": "defense", + "value_candidate": "{'defense_type': '상계항변', 'content': '오민한의 5000만원 채권으로 강용원 채권을 상계한다는 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ] + }, + { + "signal_id": "actio-signal-005", + "actio_scope": [ + "target_asset", + "defense" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh31", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ], + "actio_role_hints": [ + { + "role": "defense", + "value_candidate": "{'defense_type': '계약명의신탁', 'content': '오국한 명의 취득이 수익자 자금에 의한 계약명의신탁이므로 사해행위가 아니라는 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "role": "defense", + "value_candidate": "{'defense_type': '배상금감액', 'content': '수익자가 제공한 매수자금 4000만원 및 대여금 1000만원 참작 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "role": "defense", + "value_candidate": "{'defense_type': '상계항변', 'content': '오민한의 5000만원 채권으로 강용원 채권을 상계한다는 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ] + } + ], + "case_liability": [ + { + "signal_ref": "case-liability-signal-031", + "liability_candidate_type": "actio_related", + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B4:005", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "defense_assertion", + "defense_rebuttal_matrix_BO", + "수익자항변", + "명의신탁", + "상계항변", + "선의항변", + "항변", + "state", + "소송행위(litigation acts)", + "B4_Actio" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ] + }, + "bh32": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-032", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-013" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:002", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상속개시", + "event", + "사실행위(factual acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-013" + ] + } + ] + }, + "bh33": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-033", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-001", + "E-002" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:003", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상속포기", + "event", + "사실행위(factual acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-001", + "E-002" + ] + } + ] + }, + "bh34": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-034", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-027" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:009", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-027" + ] + } + ] + }, + "bh35": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-035", + "liability_candidate_type": "actio_related", + "related_evidence_indexes": [ + "E-008", + "E-010" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:004", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-008", + "E-010" + ] + } + ] + }, + "bh36": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-036", + "liability_candidate_type": "secured_debt", + "related_evidence_indexes": [ + "E-011", + "E-019" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:007", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "통지", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011", + "E-019" + ] + } + ] + }, + "bh37": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-037", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-028" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:010", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "통지", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-028" + ] + } + ] + }, + "bh38": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-038", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-010" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:006", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "통지", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-010" + ] + } + ] + }, + "bh39": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-039", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-009" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:005", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "통지", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-009" + ] + } + ] + }, + "bh40": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-040", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-030" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:012", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "통지", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-030" + ] + } + ] + }, + "bh41": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-041", + "liability_candidate_type": "money_claim", + "related_evidence_indexes": [ + "E-029" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:011", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "통지", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-029" + ] + } + ] + }, + "bh42": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-042", + "liability_candidate_type": "registry_invalidity", + "related_evidence_indexes": [ + "E-012" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:008", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "통지", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-012" + ] + } + ] + }, + "bh43": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-043", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:001", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ] + } + ] + }, + "bh44": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-044", + "liability_candidate_type": "secured_debt", + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:013", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ] + } + ] + }, + "bh45": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-045", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-006" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:014", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-006" + ] + } + ] + }, + "bh46": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-046", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-018", + "E-022" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:015", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-018", + "E-022" + ] + } + ] + }, + "bh47": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-047", + "liability_candidate_type": "money_claim", + "related_evidence_indexes": [ + "E-010" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:017", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상계", + "state", + "소송행위(litigation acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-010" + ] + } + ] + }, + "bh48": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-048", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:018", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "항변", + "state", + "소송행위(litigation acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ] + } + ] + }, + "bh49": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-049", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-009" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:016", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "항변", + "state", + "소송행위(litigation acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-009" + ] + } + ] + }, + "bh50": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-050", + "liability_candidate_type": "registry_invalidity", + "related_evidence_indexes": [ + "E-012" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:019", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "항변", + "state", + "소송행위(litigation acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-012" + ] + } + ] + } + }, + "candidate_inputs_by_bo_id": { + "bh1": { + "bo_id": "bh1", + "source_domain": "B1_Money_Successor", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:011" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "money_claim", + "도급", + "공사대금", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-007" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "도급", + "공사대금" + ], + "Action": "평택시 빌라 신축 공사도급계약 체결 및 중도금 지급", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "도급", + "ActionType": "법률행위(legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-001", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-007" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:011", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "도급", + "공사대금", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-007" + ] + } + ] + } + }, + "bh2": { + "bo_id": "bh2", + "source_domain": "B1_Money_Successor", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:001" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "money_claim", + "물품대금", + "매매", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-021", + "E-027" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + }, + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "물품대금", + "매매" + ], + "Action": "인테리어 자재 매매에 따른 물품대금 채권 발생", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "매매", + "ActionType": "법률행위(legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-002", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-021", + "E-027" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:001", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "물품대금", + "매매", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-021", + "E-027" + ] + } + ] + } + }, + "bh3": { + "bo_id": "bh3", + "source_domain": "B1_Money_Successor", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:006" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-024" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "대여금", + "소비대차" + ], + "Action": "1억 원 대여금 채권 발생 (2022.4.18.)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "소비대차", + "ActionType": "법률행위(legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-003", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-024" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:006", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "대여금", + "소비대차", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-024" + ] + } + ] + } + }, + "bh4": { + "bo_id": "bh4", + "source_domain": "B1_Money_Successor", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:007" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-024" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "대여금", + "소비대차" + ], + "Action": "1억 원 대여금 채권 발생 (2022.5.18.)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "소비대차", + "ActionType": "법률행위(legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-004", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-024" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:007", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "대여금", + "소비대차", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-024" + ] + } + ] + } + }, + "bh5": { + "bo_id": "bh5", + "source_domain": "B1_Money_Successor", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:008" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-025" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "대여금", + "소비대차" + ], + "Action": "2억 원 대여금 채권 발생 (2023.1.6.)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "소비대차", + "ActionType": "법률행위(legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-005", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-025" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:008", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "대여금", + "소비대차", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-025" + ] + } + ] + } + }, + "bh6": { + "bo_id": "bh6", + "source_domain": "B1_Money_Successor", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:009" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-026" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "대여금", + "소비대차" + ], + "Action": "3억 원 대여금 채권 발생 (2023.7.6.)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "소비대차", + "ActionType": "법률행위(legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-006", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-026" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:009", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "대여금", + "소비대차", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-026" + ] + } + ] + } + }, + "bh7": { + "bo_id": "bh7", + "source_domain": "B1_Money_Successor", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:004" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "money_claim", + "영업양도", + "commercial_successor", + "event", + "법률행위(legal_acts)" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-020" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "영업양도", + "상호속용" + ], + "Action": "해드림 영업양도 및 상호속용 약정", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "영업양도", + "ActionType": "법률행위(legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-007", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-020" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:004", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "영업양도", + "상호속용", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-020" + ] + } + ] + } + }, + "bh8": { + "bo_id": "bh8", + "source_domain": "B1_Money_Successor", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:002" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "money_claim", + "채무승인", + "변제기_유예", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-021" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "채무승인", + "변제기 유예" + ], + "Action": "물품대금 채무 승인 및 변제기 유예", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "채무승인", + "ActionType": "법률행위(legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-008", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-021" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:002", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "채무승인", + "변제기 유예", + "event", + "법률행위(legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-021" + ] + } + ] + } + }, + "bh9": { + "bo_id": "bh9", + "source_domain": "B1_Money_Successor", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:010" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "money_claim", + "변제", + "법정변제충당", + "event", + "사실행위(factual_acts)", + "commercial_successor" + ], + "asset_candidates": [], + "evidence_indexes": [], + "related_event_candidates": [], + "keyword_fields": { + "Legal_Keywords": [ + "변제", + "법정변제충당" + ], + "Action": "차용금 변제 명목 4억 원 지급 (채무 미특정)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "변제", + "ActionType": "사실행위(factual acts)", + "BOType": "event", + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-009", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:010", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "변제", + "법정변제충당", + "event", + "사실행위(factual acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [] + } + ] + } + }, + "bh10": { + "bo_id": "bh10", + "source_domain": "B1_Money_Successor", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:013" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "money_claim", + "상계", + "succession_notice_lien_asset_defense", + "event", + "소송행위(litigation_acts)", + "commercial_successor" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + }, + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "상계", + "항변" + ], + "Action": "오국한에 대한 대여금 채권 등을 자동채권으로 한 상계 항변", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "상계", + "ActionType": "소송행위(litigation acts)", + "BOType": "event", + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-010", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:013", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "상계", + "항변", + "event", + "소송행위(litigation acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ] + } + }, + "bh11": { + "bo_id": "bh11", + "source_domain": "B1_Money_Successor", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:003" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "money_claim", + "이행최고", + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal_acts)", + "commercial_successor" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-027" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "이행최고", + "통지", + "최고" + ], + "Action": "물품대금 지급 최고", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "최고", + "ActionType": "준법률행위(quasi-legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-011", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-027" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:003", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "이행최고", + "통지", + "최고", + "event", + "준법률행위(quasi-legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-027" + ] + } + ] + } + }, + "bh12": { + "bo_id": "bh12", + "source_domain": "B1_Money_Successor", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:005" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "money_claim", + "succession_notice_lien_asset_defense", + "책임부인", + "event", + "준법률행위(quasi-legal_acts)", + "commercial_successor" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-028" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "통지", + "책임부인" + ], + "Action": "상호속용에 따른 물품대금 채무 책임 부인 통지", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "통지", + "ActionType": "준법률행위(quasi-legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-012", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-028" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:005", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "통지", + "책임부인", + "event", + "준법률행위(quasi-legal acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-028" + ] + } + ] + } + }, + "bh13": { + "bo_id": "bh13", + "source_domain": "B1_Money_Successor", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B1:012" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "money_claim", + "충당", + "state", + "사실행위(factual_acts)", + "commercial_successor" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-011" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "미지급", + "충당" + ], + "Action": "공사대금 잔금 3억 원 미지급 상태 및 유치권 충당 항변", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": null, + "ActionType": "사실행위(factual acts)", + "BOType": "state", + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-013", + "liability_candidate_type": "commercial_successor", + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B1:012", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "money_claim_or_successor_liability", + "미지급", + "충당", + "state", + "사실행위(factual acts)", + "B1_Money_Successor" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ] + } + ] + } + }, + "bh14": { + "bo_id": "bh14", + "source_domain": "B2_Secured_Registry", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B2:001" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "secured_debt", + "상속", + "증여", + "(근)저당권_설정_계약", + "event", + "법률행위(legal_acts)" + ], + "asset_candidates": [ + { + "asset_label": "성수동 대지", + "asset_kind": "real_property", + "source": "BO.extensions.domain_payload.asset_bindings", + "related_bo_ids": [ + "bh14" + ], + "related_evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ] + } + ], + "evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + }, + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + }, + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "상속", + "증여", + "근저당권설정", + "secured_debt_creation", + "(근)저당권 설정 계약" + ], + "Action": "강용원은 성수동 대지에 대한 상속 및 증여로 지분을 취득하고, 오민한에게 차용금 담보를 위해 성수동 대지 지분에 대한 근저당권 설정 약정을 체결함", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "(근)저당권 설정 계약", + "ActionType": "법률행위(legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "secured_registry_or_auction", + "legal_effect_tags": [ + "secured_debt_creation" + ] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-014", + "liability_candidate_type": "secured_debt", + "related_evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B2:001", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "secured_debt_creation", + "secured_registry_or_auction", + "상속", + "증여", + "근저당권설정", + "(근)저당권 설정 계약", + "event", + "법률행위(legal acts)", + "B2_Secured_Registry" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ] + } + ] + } + }, + "bh15": { + "bo_id": "bh15", + "source_domain": "B2_Secured_Registry", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B2:004" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "secured_debt", + "mortgage_extinguishment", + "registry_invalidity", + "상계", + "event", + "법률행위(legal_acts)" + ], + "asset_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "real_property", + "source": "BO.extensions.domain_payload.asset_bindings", + "related_bo_ids": [ + "bh15" + ], + "related_evidence_indexes": [ + "E-010", + "E-014" + ] + } + ], + "evidence_indexes": [ + "E-010", + "E-014" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + }, + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "근저당권설정", + "말소", + "상계", + "secured_debt_chain", + "mortgage_extinguishment", + "근저당권 말소 및 상계" + ], + "Action": "신림동 상가에 강용원(등기상 강용일) 명의의 근저당권이 설정되었다가 무단 말소되었으며, 오민한은 상계를 주장함", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "근저당권 말소 및 상계", + "ActionType": "법률행위(legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "secured_registry_or_auction", + "legal_effect_tags": [ + "secured_debt_chain", + "mortgage_extinguishment" + ] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-015", + "liability_candidate_type": "registry_invalidity", + "related_evidence_indexes": [ + "E-010", + "E-014" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B2:004", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "secured_debt_chain", + "mortgage_extinguishment", + "secured_registry_or_auction", + "근저당권설정", + "말소", + "상계", + "근저당권 말소 및 상계", + "event", + "법률행위(legal acts)", + "B2_Secured_Registry" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-010", + "E-014" + ] + } + ] + } + }, + "bh16": { + "bo_id": "bh16", + "source_domain": "B2_Secured_Registry", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B2:006" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "secured_debt", + "소유권보존", + "소유권이전", + "가압류", + "registry_invalidity", + "event", + "법률행위(legal_acts)" + ], + "asset_candidates": [ + { + "asset_label": "평택시 빌라", + "asset_kind": "unknown", + "source": "BO.extensions.domain_payload.asset_bindings", + "related_bo_ids": [ + "bh16" + ], + "related_evidence_indexes": [ + "E-015" + ] + } + ], + "evidence_indexes": [ + "E-015" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "소유권보존", + "소유권이전", + "가압류", + "소유권이전등기" + ], + "Action": "평택시 서정빌라에 대한 보존등기 및 매매로 인한 소유권이전등기가 마쳐짐", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "소유권이전등기", + "ActionType": "법률행위(legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "secured_registry_or_auction", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-016", + "liability_candidate_type": "secured_debt", + "related_evidence_indexes": [ + "E-015" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B2:006", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "secured_registry_or_auction", + "소유권보존", + "소유권이전", + "가압류", + "소유권이전등기", + "event", + "법률행위(legal acts)", + "B2_Secured_Registry" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-015" + ] + } + ] + } + }, + "bh17": { + "bo_id": "bh17", + "source_domain": "B2_Secured_Registry", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B2:003" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "secured_debt", + "임의경매", + "공신력", + "registry_invalidity", + "event", + "소송행위(litigation_acts)" + ], + "asset_candidates": [ + { + "asset_label": "성수동 대지", + "asset_kind": "real_property", + "source": "BO.extensions.domain_payload.asset_bindings", + "related_bo_ids": [ + "bh17" + ], + "related_evidence_indexes": [ + "E-012", + "E-013" + ] + } + ], + "evidence_indexes": [ + "E-012", + "E-013" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + }, + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "임의경매", + "공신력", + "등기무효", + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "담보권실행을 위한 경매 및 근저당권 설정" + ], + "Action": "오민한이 경매를 신청하여 이문호가 매각대금을 완납하고 취득한 뒤, 대한은행이 근저당권을 설정함에 따라 원인무효 여부 및 공신력이 다투어짐", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "담보권실행을 위한 경매 및 근저당권 설정", + "ActionType": "소송행위(litigation acts)", + "BOType": "event", + "domain_legal_effect_tag": "secured_registry_or_auction", + "legal_effect_tags": [ + "auction_invalidity", + "derivative_bank_mortgage_invalidity" + ] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-017", + "liability_candidate_type": "registry_invalidity", + "related_evidence_indexes": [ + "E-012", + "E-013" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B2:003", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "secured_registry_or_auction", + "임의경매", + "공신력", + "등기무효", + "담보권실행을 위한 경매 및 근저당권 설정", + "event", + "소송행위(litigation acts)", + "B2_Secured_Registry" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-012", + "E-013" + ] + } + ] + } + }, + "bh18": { + "bo_id": "bh18", + "source_domain": "B2_Secured_Registry", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B2:002" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "payment_allocation_uncertain", + "secured_debt", + "변제", + "변제충당", + "변제_및_변제충당", + "event", + "준법률행위(quasi-legal_acts)" + ], + "asset_candidates": [], + "evidence_indexes": [], + "related_event_candidates": [], + "keyword_fields": { + "Legal_Keywords": [ + "변제", + "변제충당", + "payment_allocation_uncertain", + "변제 및 변제충당" + ], + "Action": "강용원이 오민한에게 4억 원을 지급하였으나 다수의 채무 중 충당할 채무를 특정하지 않음", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "변제 및 변제충당", + "ActionType": "준법률행위(quasi-legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "secured_registry_or_auction", + "legal_effect_tags": [ + "payment_allocation_uncertain" + ] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-018", + "liability_candidate_type": "secured_debt", + "related_evidence_indexes": [], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B2:002", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "payment_allocation_uncertain", + "secured_registry_or_auction", + "변제", + "변제충당", + "변제 및 변제충당", + "event", + "준법률행위(quasi-legal acts)", + "B2_Secured_Registry" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [] + } + ] + } + }, + "bh19": { + "bo_id": "bh19", + "source_domain": "B2_Secured_Registry", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B2:005" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "payment_allocation", + "secured_debt", + "succession_notice_lien_asset_defense", + "land_use_gain", + "변제충당", + "변제충당_주장", + "event", + "준법률행위(quasi-legal_acts)", + "money_claim" + ], + "asset_candidates": [ + { + "asset_label": "평택시 빌라", + "asset_kind": "unknown", + "source": "BO.extensions.domain_payload.asset_bindings", + "related_bo_ids": [ + "bh19" + ], + "related_evidence_indexes": [ + "E-007", + "E-011" + ] + } + ], + "evidence_indexes": [ + "E-007", + "E-011" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + }, + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "유치권", + "점유", + "변제충당", + "payment_allocation", + "변제충당 주장" + ], + "Action": "박광윤이 평택시 빌라를 점유하며 얻은 임대 및 거주 수익을 피담보채권에 충당하였다고 항변함", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "변제충당 주장", + "ActionType": "준법률행위(quasi-legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "secured_registry_or_auction", + "legal_effect_tags": [ + "payment_allocation" + ] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-019", + "liability_candidate_type": "money_claim", + "related_evidence_indexes": [ + "E-007", + "E-011" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B2:005", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "payment_allocation", + "secured_registry_or_auction", + "유치권", + "점유", + "변제충당", + "변제충당 주장", + "event", + "준법률행위(quasi-legal acts)", + "B2_Secured_Registry" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-007", + "E-011" + ] + } + ] + } + }, + "bh20": { + "bo_id": "bh20", + "source_domain": "B2_Secured_Registry", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B2:007" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "registry_invalidity", + "secured_debt", + "요구", + "거절", + "event", + "준법률행위(quasi-legal_acts)" + ], + "asset_candidates": [ + { + "asset_label": "흑석동 상가", + "asset_kind": "real_property", + "source": "BO.extensions.domain_payload.asset_bindings", + "related_bo_ids": [ + "bh20" + ], + "related_evidence_indexes": [ + "E-029" + ] + } + ], + "evidence_indexes": [ + "E-029" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "소유권이전등기", + "요구", + "거절", + "registry_defect_hint", + "제3자 명의 등기 요청" + ], + "Action": "이수인이 윤건우에게 매매계약에서 합의되지 않은 제3자 명의로의 등기이전을 일방적으로 요구함", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "제3자 명의 등기 요청", + "ActionType": "준법률행위(quasi-legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "secured_registry_or_auction", + "legal_effect_tags": [ + "registry_defect_hint" + ] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-020", + "liability_candidate_type": "secured_debt", + "related_evidence_indexes": [ + "E-029" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B2:007", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "registry_defect_hint", + "secured_registry_or_auction", + "소유권이전등기", + "요구", + "거절", + "제3자 명의 등기 요청", + "event", + "준법률행위(quasi-legal acts)", + "B2_Secured_Registry" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-029" + ] + } + ] + } + }, + "bh21": { + "bo_id": "bh21", + "source_domain": "B3_Land_Valuation", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B3:002" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "valuation", + "land_use_gain", + "임대료", + "차임", + "event", + "사실행위(factual_acts)" + ], + "asset_candidates": [ + { + "asset_label": "SCH-016-01", + "asset_kind": "unknown", + "source": "BO.extensions.domain_payload.asset_bindings", + "related_bo_ids": [ + "bh21" + ], + "related_evidence_indexes": [ + "E-005" + ] + }, + { + "asset_label": "성수동 256 대", + "asset_kind": "unknown", + "source": "BO.extensions.domain_payload.asset_binding_hint", + "related_bo_ids": [ + "bh21" + ], + "related_evidence_indexes": [ + "E-005" + ] + } + ], + "evidence_indexes": [ + "E-005" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "가치평가", + "임대료", + "차임", + "valuation" + ], + "Action": "성수동 대지 차임 시세 감정평가", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": null, + "ActionType": "사실행위(factual acts)", + "BOType": "event", + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "legal_effect_tags": [ + "valuation" + ] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-021", + "liability_candidate_type": "land_use_gain", + "related_evidence_indexes": [ + "E-005" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B3:002", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "valuation", + "land_use_gain_or_valuation", + "가치평가", + "임대료", + "차임", + "event", + "사실행위(factual acts)", + "B3_Land_Valuation" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-005" + ] + } + ] + } + }, + "bh22": { + "bo_id": "bh22", + "source_domain": "B3_Land_Valuation", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B3:004" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "사용", + "state", + "사실행위(factual_acts)" + ], + "asset_candidates": [ + { + "asset_label": "SCH-016-01", + "asset_kind": "unknown", + "source": "BO.extensions.domain_payload.asset_bindings", + "related_bo_ids": [ + "bh22" + ], + "related_evidence_indexes": [ + "E-006" + ] + }, + { + "asset_label": "SCH-016-02", + "asset_kind": "unknown", + "source": "BO.extensions.domain_payload.asset_bindings", + "related_bo_ids": [ + "bh22" + ], + "related_evidence_indexes": [ + "E-006" + ] + }, + { + "asset_label": "성수동 256 대지 및 지상 건물", + "asset_kind": "real_property", + "source": "BO.extensions.domain_payload.asset_binding_hint", + "related_bo_ids": [ + "bh22" + ], + "related_evidence_indexes": [ + "E-006" + ] + } + ], + "evidence_indexes": [ + "E-006" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "인도", + "점유", + "사용", + "possession" + ], + "Action": "성수동 대지 및 건물 인도, 점유 및 사용", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": null, + "ActionType": "사실행위(factual acts)", + "BOType": "state", + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "legal_effect_tags": [ + "possession" + ] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-022", + "liability_candidate_type": "land_use_gain", + "related_evidence_indexes": [ + "E-006" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B3:004", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "possession", + "land_use_gain_or_valuation", + "인도", + "점유", + "사용", + "state", + "사실행위(factual acts)", + "B3_Land_Valuation" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-006" + ] + } + ] + } + }, + "bh23": { + "bo_id": "bh23", + "source_domain": "B3_Land_Valuation", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B3:001" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "valuation", + "land_use_gain", + "state", + "사실행위(factual_acts)" + ], + "asset_candidates": [ + { + "asset_label": "SCH-016-03", + "asset_kind": "unknown", + "source": "BO.extensions.domain_payload.asset_bindings", + "related_bo_ids": [ + "bh23" + ], + "related_evidence_indexes": [ + "E-004" + ] + }, + { + "asset_label": "신림로 상가, 포천시 임야", + "asset_kind": "real_property", + "source": "BO.extensions.domain_payload.asset_binding_hint", + "related_bo_ids": [ + "bh23" + ], + "related_evidence_indexes": [ + "E-004" + ] + } + ], + "evidence_indexes": [ + "E-004" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "가치평가", + "감정평가", + "valuation" + ], + "Action": "신림로 상가 및 포천시 임야 가치평가(감정평가서)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": null, + "ActionType": "사실행위(factual acts)", + "BOType": "state", + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "legal_effect_tags": [ + "valuation" + ] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-023", + "liability_candidate_type": "land_use_gain", + "related_evidence_indexes": [ + "E-004" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B3:001", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "valuation", + "land_use_gain_or_valuation", + "가치평가", + "감정평가", + "state", + "사실행위(factual acts)", + "B3_Land_Valuation" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-004" + ] + } + ] + } + }, + "bh24": { + "bo_id": "bh24", + "source_domain": "B3_Land_Valuation", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B3:005" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "possession", + "secured_debt", + "land_use_gain", + "거주", + "succession_notice_lien_asset_defense", + "수익", + "state", + "사실행위(factual_acts)" + ], + "asset_candidates": [ + { + "asset_label": "SCH-016-04", + "asset_kind": "unknown", + "source": "BO.extensions.domain_payload.asset_bindings", + "related_bo_ids": [ + "bh24" + ], + "related_evidence_indexes": [ + "E-011" + ] + }, + { + "asset_label": "평택시 서정3길 123 서정빌라 101호", + "asset_kind": "unknown", + "source": "BO.extensions.domain_payload.asset_binding_hint", + "related_bo_ids": [ + "bh24" + ], + "related_evidence_indexes": [ + "E-011" + ] + } + ], + "evidence_indexes": [ + "E-011" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "점유", + "거주", + "유치권", + "수익", + "possession", + "defense_assertion_benefit_applied_to_secured_claim" + ], + "Action": "평택시 서정빌라 101호 점유 및 유치권 행사", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": null, + "ActionType": "사실행위(factual acts)", + "BOType": "state", + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "legal_effect_tags": [ + "possession", + "defense_assertion_benefit_applied_to_secured_claim" + ] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-024", + "liability_candidate_type": "secured_debt", + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B3:005", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "possession", + "defense_assertion_benefit_applied_to_secured_claim", + "land_use_gain_or_valuation", + "점유", + "거주", + "유치권", + "수익", + "state", + "사실행위(factual acts)", + "B3_Land_Valuation" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ] + } + ] + } + }, + "bh25": { + "bo_id": "bh25", + "source_domain": "B3_Land_Valuation", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B3:003" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "valuation", + "land_use_gain", + "차임", + "state", + "사실행위(factual_acts)" + ], + "asset_candidates": [ + { + "asset_label": "SCH-016-04", + "asset_kind": "unknown", + "source": "BO.extensions.domain_payload.asset_bindings", + "related_bo_ids": [ + "bh25" + ], + "related_evidence_indexes": [ + "E-023" + ] + }, + { + "asset_label": "서정빌라 101호", + "asset_kind": "unknown", + "source": "BO.extensions.domain_payload.asset_binding_hint", + "related_bo_ids": [ + "bh25" + ], + "related_evidence_indexes": [ + "E-023" + ] + } + ], + "evidence_indexes": [ + "E-023" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "가치평가", + "차임", + "valuation" + ], + "Action": "평택시 서정빌라 차임 시세 평가", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": null, + "ActionType": "사실행위(factual acts)", + "BOType": "state", + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "legal_effect_tags": [ + "valuation" + ] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-025", + "liability_candidate_type": "land_use_gain", + "related_evidence_indexes": [ + "E-023" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B3:003", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "valuation", + "land_use_gain_or_valuation", + "가치평가", + "차임", + "state", + "사실행위(factual acts)", + "B3_Land_Valuation" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-023" + ] + } + ] + } + }, + "bh26": { + "bo_id": "bh26", + "source_domain": "B3_Land_Valuation", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B3:006" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "운영", + "사업자등록", + "state", + "사실행위(factual_acts)" + ], + "asset_candidates": [ + { + "asset_label": "SCH-016-05", + "asset_kind": "unknown", + "source": "BO.extensions.domain_payload.asset_bindings", + "related_bo_ids": [ + "bh26" + ], + "related_evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ] + }, + { + "asset_label": "흑석동 201호", + "asset_kind": "unknown", + "source": "BO.extensions.domain_payload.asset_binding_hint", + "related_bo_ids": [ + "bh26" + ], + "related_evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ] + } + ], + "evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + }, + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + }, + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "인도", + "점유", + "운영", + "사업자등록", + "possession" + ], + "Action": "흑석동 상가 점유 및 운영 (맛나식당)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": null, + "ActionType": "사실행위(factual acts)", + "BOType": "state", + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "legal_effect_tags": [ + "possession" + ] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-026", + "liability_candidate_type": "land_use_gain", + "related_evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B3:006", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "possession", + "land_use_gain_or_valuation", + "인도", + "점유", + "운영", + "사업자등록", + "state", + "사실행위(factual acts)", + "B3_Land_Valuation" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ] + } + ] + } + }, + "bh27": { + "bo_id": "bh27", + "source_domain": "B4_Actio", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B4:002" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "fraudulent_transfer", + "매매계약", + "소유권이전", + "책임재산감소", + "매매", + "event", + "법률행위(legal_acts)" + ], + "asset_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "source": "actio_case_signals.target_property_candidates", + "related_bo_ids": [ + "bh27" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "신림동 상가", + "asset_kind": "real_property", + "source": "BO.extensions.domain_payload.asset_bindings", + "related_bo_ids": [ + "bh27" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "evidence_indexes": [ + "E-014" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "사해행위", + "매매계약", + "소유권이전", + "책임재산감소", + "fraudulent_act", + "매매" + ], + "Action": "채무자 오국한이 수익자 오민한에게 책임재산인 신림동 상가를 1억5천만원에 매도하여 소유권이전등기 경료", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "매매", + "ActionType": "법률행위(legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "fraudulent_transfer_BO", + "legal_effect_tags": [ + "fraudulent_act" + ] + }, + "signals": { + "actio": [ + { + "signal_id": "actio-signal-002", + "actio_scope": [ + "fraudulent_act", + "beneficiary_or_transferee", + "target_asset" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-014" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh27" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh27", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "actio_role_hints": [ + { + "role": "beneficiary", + "value_candidate": "{}", + "date_candidate": null, + "related_bo_ids": [ + "bh27" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ] + }, + { + "signal_id": "actio-signal-002", + "actio_scope": [ + "fraudulent_act", + "beneficiary_or_transferee", + "target_asset" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-014" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh27" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh27", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "actio_role_hints": [ + { + "role": "beneficiary", + "value_candidate": "{}", + "date_candidate": null, + "related_bo_ids": [ + "bh27" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ] + } + ], + "case_liability": [ + { + "signal_ref": "case-liability-signal-027", + "liability_candidate_type": "actio_related", + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B4:002", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "fraudulent_act", + "fraudulent_transfer_BO", + "사해행위", + "매매계약", + "소유권이전", + "책임재산감소", + "매매", + "event", + "법률행위(legal acts)", + "B4_Actio" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ] + } + ] + } + }, + "bh28": { + "bo_id": "bh28", + "source_domain": "B4_Actio", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B4:001" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "preserved_claim", + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "fraudulent_transfer" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-024" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "피보전채권", + "대여금채권", + "preserved_claim", + "소비대차" + ], + "Action": "강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "소비대차", + "ActionType": "법률행위(legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "preserved_claim_bundle_BO", + "legal_effect_tags": [ + "preserved_claim" + ] + }, + "signals": { + "actio": [ + { + "signal_id": "actio-signal-001", + "actio_scope": [ + "preserved_claim" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-024" + ], + "target_property_candidates": [], + "preserved_claim_candidates": [ + { + "bo_id": null, + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-024" + ] + } + ], + "actio_role_hints": [ + { + "role": "debtor_act", + "value_candidate": "강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생", + "date_candidate": null, + "related_bo_ids": [ + "bh28" + ], + "related_evidence_indexes": [ + "E-024" + ] + } + ] + } + ], + "case_liability": [ + { + "signal_ref": "case-liability-signal-028", + "liability_candidate_type": "actio_related", + "related_evidence_indexes": [ + "E-024" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B4:001", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "preserved_claim", + "preserved_claim_bundle_BO", + "피보전채권", + "대여금채권", + "소비대차", + "event", + "법률행위(legal acts)", + "B4_Actio" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-024" + ] + } + ] + } + }, + "bh29": { + "bo_id": "bh29", + "source_domain": "B4_Actio", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B4:004" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "value_compensation", + "valuation", + "actio_remedy_or_cap", + "원물반환불능", + "secured_debt", + "registry_invalidity", + "event", + "준법률행위(quasi-legal_acts)", + "fraudulent_transfer" + ], + "asset_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "source": "actio_case_signals.target_property_candidates", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2021-08-01", + "asset_kind": "unknown", + "source": "actio_case_signals.target_property_candidates", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2022-04-18", + "asset_kind": "unknown", + "source": "actio_case_signals.target_property_candidates", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2024-10-17", + "asset_kind": "unknown", + "source": "actio_case_signals.target_property_candidates", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "신림동 상가", + "asset_kind": "real_property", + "source": "BO.extensions.domain_payload.asset_bindings", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "evidence_indexes": [ + "E-014" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "원물반환불능", + "가액배상", + "근저당권말소", + "value_compensation", + "remedy_cap", + "변제 및 말소" + ], + "Action": "수익자 오민한이 사해행위 이후 피담보채무 1억3천만원을 대위변제하고 2번 근저당권을 말소시킴으로써 원물반환이 불가능해져 가액배상 요건 성립", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "변제 및 말소", + "ActionType": "준법률행위(quasi-legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "remedy_mode_decision_BO", + "legal_effect_tags": [ + "value_compensation", + "remedy_cap" + ] + }, + "signals": { + "actio": [ + { + "signal_id": "actio-signal-004", + "actio_scope": [ + "target_asset", + "encumbrance", + "remedy_or_cap" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-014" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2021-08-01", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2022-04-18", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2024-10-17", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh29", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "actio_role_hints": [ + { + "role": "remedy_or_cap", + "value_candidate": "원물반환 불가", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "remedy_or_cap", + "value_candidate": "가액배상", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2021-08-01', 'event': '나임오 1번 근저당 설정(5000만원)'}", + "date_candidate": "2021-08-01", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2022-04-18', 'event': '강용일 2번 근저당 설정(1.5억원)'}", + "date_candidate": "2022-04-18", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2024-10-17', 'event': '강용일 2번 근저당 말소(오민한 대위변제)'}", + "date_candidate": "2024-10-17", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ] + }, + { + "signal_id": "actio-signal-004", + "actio_scope": [ + "target_asset", + "encumbrance", + "remedy_or_cap" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-014" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2021-08-01", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2022-04-18", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2024-10-17", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh29", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "actio_role_hints": [ + { + "role": "remedy_or_cap", + "value_candidate": "원물반환 불가", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "remedy_or_cap", + "value_candidate": "가액배상", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2021-08-01', 'event': '나임오 1번 근저당 설정(5000만원)'}", + "date_candidate": "2021-08-01", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2022-04-18', 'event': '강용일 2번 근저당 설정(1.5억원)'}", + "date_candidate": "2022-04-18", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2024-10-17', 'event': '강용일 2번 근저당 말소(오민한 대위변제)'}", + "date_candidate": "2024-10-17", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ] + }, + { + "signal_id": "actio-signal-004", + "actio_scope": [ + "target_asset", + "encumbrance", + "remedy_or_cap" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-014" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2021-08-01", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2022-04-18", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2024-10-17", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh29", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "actio_role_hints": [ + { + "role": "remedy_or_cap", + "value_candidate": "원물반환 불가", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "remedy_or_cap", + "value_candidate": "가액배상", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2021-08-01', 'event': '나임오 1번 근저당 설정(5000만원)'}", + "date_candidate": "2021-08-01", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2022-04-18', 'event': '강용일 2번 근저당 설정(1.5억원)'}", + "date_candidate": "2022-04-18", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2024-10-17', 'event': '강용일 2번 근저당 말소(오민한 대위변제)'}", + "date_candidate": "2024-10-17", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ] + }, + { + "signal_id": "actio-signal-004", + "actio_scope": [ + "target_asset", + "encumbrance", + "remedy_or_cap" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-014" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2021-08-01", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2022-04-18", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2024-10-17", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh29", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "actio_role_hints": [ + { + "role": "remedy_or_cap", + "value_candidate": "원물반환 불가", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "remedy_or_cap", + "value_candidate": "가액배상", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2021-08-01', 'event': '나임오 1번 근저당 설정(5000만원)'}", + "date_candidate": "2021-08-01", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2022-04-18', 'event': '강용일 2번 근저당 설정(1.5억원)'}", + "date_candidate": "2022-04-18", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2024-10-17', 'event': '강용일 2번 근저당 말소(오민한 대위변제)'}", + "date_candidate": "2024-10-17", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ] + }, + { + "signal_id": "actio-signal-004", + "actio_scope": [ + "target_asset", + "encumbrance", + "remedy_or_cap" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-014" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2021-08-01", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2022-04-18", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "asset_label": "2024-10-17", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh29", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-014" + ] + } + ], + "actio_role_hints": [ + { + "role": "remedy_or_cap", + "value_candidate": "원물반환 불가", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "remedy_or_cap", + "value_candidate": "가액배상", + "date_candidate": null, + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2021-08-01', 'event': '나임오 1번 근저당 설정(5000만원)'}", + "date_candidate": "2021-08-01", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2022-04-18', 'event': '강용일 2번 근저당 설정(1.5억원)'}", + "date_candidate": "2022-04-18", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + }, + { + "role": "encumbrance", + "value_candidate": "{'date': '2024-10-17', 'event': '강용일 2번 근저당 말소(오민한 대위변제)'}", + "date_candidate": "2024-10-17", + "related_bo_ids": [ + "bh29" + ], + "related_evidence_indexes": [ + "E-014" + ] + } + ] + } + ], + "case_liability": [ + { + "signal_ref": "case-liability-signal-029", + "liability_candidate_type": "actio_related", + "related_evidence_indexes": [ + "E-014" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B4:004", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "value_compensation", + "remedy_cap", + "remedy_mode_decision_BO", + "원물반환불능", + "가액배상", + "근저당권말소", + "변제 및 말소", + "event", + "준법률행위(quasi-legal acts)", + "B4_Actio" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-014" + ] + } + ] + } + }, + "bh30": { + "bo_id": "bh30", + "source_domain": "B4_Actio", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B4:003" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "insolvency_check", + "fraudulent_transfer", + "무자력", + "책임재산", + "valuation", + "state", + "사실행위(factual_acts)" + ], + "asset_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "source": "actio_case_signals.target_property_candidates", + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + }, + { + "asset_label": "포천시 임야", + "asset_kind": "unknown", + "source": "actio_case_signals.target_property_candidates", + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + }, + { + "asset_label": "신림동 상가", + "asset_kind": "real_property", + "source": "BO.extensions.domain_payload.asset_bindings", + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + }, + { + "asset_label": "포천시 임야", + "asset_kind": "real_property", + "source": "BO.extensions.domain_payload.asset_bindings", + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + } + ], + "evidence_indexes": [ + "E-004" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "무자력", + "책임재산", + "가치평가", + "insolvency_check" + ], + "Action": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": null, + "ActionType": "사실행위(factual acts)", + "BOType": "state", + "domain_legal_effect_tag": "actio_balance_sheet_BO", + "legal_effect_tags": [ + "insolvency_check" + ] + }, + "signals": { + "actio": [ + { + "signal_id": "actio-signal-003", + "actio_scope": [ + "target_asset" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-004" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + }, + { + "asset_label": "포천시 임야", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh30", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-004" + ] + } + ], + "actio_role_hints": [ + { + "role": "debtor_act", + "value_candidate": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "date_candidate": null, + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + } + ] + }, + { + "signal_id": "actio-signal-003", + "actio_scope": [ + "target_asset" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-004" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + }, + { + "asset_label": "포천시 임야", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh30", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-004" + ] + } + ], + "actio_role_hints": [ + { + "role": "debtor_act", + "value_candidate": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "date_candidate": null, + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + } + ] + }, + { + "signal_id": "actio-signal-003", + "actio_scope": [ + "target_asset" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-004" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + }, + { + "asset_label": "포천시 임야", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh30", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-004" + ] + } + ], + "actio_role_hints": [ + { + "role": "debtor_act", + "value_candidate": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "date_candidate": null, + "related_bo_ids": [ + "bh30" + ], + "related_evidence_indexes": [ + "E-004" + ] + } + ] + } + ], + "case_liability": [ + { + "signal_ref": "case-liability-signal-030", + "liability_candidate_type": "actio_related", + "related_evidence_indexes": [ + "E-004" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B4:003", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "insolvency_check", + "actio_balance_sheet_BO", + "무자력", + "책임재산", + "가치평가", + "state", + "사실행위(factual acts)", + "B4_Actio" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-004" + ] + } + ] + } + }, + "bh31": { + "bo_id": "bh31", + "source_domain": "B4_Actio", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B4:005" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "defense_assertion", + "defense_rebuttal_matrix_bo", + "fraudulent_transfer", + "명의신탁", + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)" + ], + "asset_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "source": "actio_case_signals.target_property_candidates", + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "asset_label": "신림동 상가", + "asset_kind": "real_property", + "source": "BO.extensions.domain_payload.asset_bindings", + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + }, + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "수익자항변", + "명의신탁", + "상계항변", + "선의항변", + "defense_assertion", + "항변" + ], + "Action": "수익자 오민한의 항변 (명의신탁 주장, 매수자금 및 대여금 참작 주장, 채권 상계 주장)", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "항변", + "ActionType": "소송행위(litigation acts)", + "BOType": "state", + "domain_legal_effect_tag": "defense_rebuttal_matrix_BO", + "legal_effect_tags": [ + "defense_assertion" + ] + }, + "signals": { + "actio": [ + { + "signal_id": "actio-signal-005", + "actio_scope": [ + "target_asset", + "defense" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh31", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ], + "actio_role_hints": [ + { + "role": "defense", + "value_candidate": "{'defense_type': '계약명의신탁', 'content': '오국한 명의 취득이 수익자 자금에 의한 계약명의신탁이므로 사해행위가 아니라는 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "role": "defense", + "value_candidate": "{'defense_type': '배상금감액', 'content': '수익자가 제공한 매수자금 4000만원 및 대여금 1000만원 참작 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "role": "defense", + "value_candidate": "{'defense_type': '상계항변', 'content': '오민한의 5000만원 채권으로 강용원 채권을 상계한다는 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ] + }, + { + "signal_id": "actio-signal-005", + "actio_scope": [ + "target_asset", + "defense" + ], + "suspicion_level": "LOW", + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "target_property_candidates": [ + { + "asset_label": "신림동 상가", + "asset_kind": "unknown", + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ], + "preserved_claim_candidates": [ + { + "bo_id": "bh31", + "claim_type_candidate": null, + "creditor_candidate": null, + "debtor_candidate": null, + "claim_date_candidate": null, + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ], + "actio_role_hints": [ + { + "role": "defense", + "value_candidate": "{'defense_type': '계약명의신탁', 'content': '오국한 명의 취득이 수익자 자금에 의한 계약명의신탁이므로 사해행위가 아니라는 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "role": "defense", + "value_candidate": "{'defense_type': '배상금감액', 'content': '수익자가 제공한 매수자금 4000만원 및 대여금 1000만원 참작 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + { + "role": "defense", + "value_candidate": "{'defense_type': '상계항변', 'content': '오민한의 5000만원 채권으로 강용원 채권을 상계한다는 주장'}", + "date_candidate": null, + "related_bo_ids": [ + "bh31" + ], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ] + } + ], + "case_liability": [ + { + "signal_ref": "case-liability-signal-031", + "liability_candidate_type": "actio_related", + "related_evidence_indexes": [ + "E-003", + "E-010" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B4:005", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "defense_assertion", + "defense_rebuttal_matrix_BO", + "수익자항변", + "명의신탁", + "상계항변", + "선의항변", + "항변", + "state", + "소송행위(litigation acts)", + "B4_Actio" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-003", + "E-010" + ] + } + ] + } + }, + "bh32": { + "bo_id": "bh32", + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:002" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상속개시", + "event", + "사실행위(factual_acts)" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-013" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "상속개시" + ], + "Action": "강석우 사망으로 인한 정유심, 강용원의 성수동 대지 지분 상속", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "상속개시", + "ActionType": "사실행위(factual acts)", + "BOType": "event", + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-032", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-013" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:002", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상속개시", + "event", + "사실행위(factual acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-013" + ] + } + ] + } + }, + "bh33": { + "bo_id": "bh33", + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:003" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상속포기", + "event", + "사실행위(factual_acts)" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-001", + "E-002" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + }, + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "상속포기" + ], + "Action": "망 강호연의 사망에 따른 상속개시 및 자녀 강형수, 강지수의 상속포기", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "상속포기", + "ActionType": "사실행위(factual acts)", + "BOType": "event", + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-033", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-001", + "E-002" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:003", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상속포기", + "event", + "사실행위(factual acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-001", + "E-002" + ] + } + ] + } + }, + "bh34": { + "bo_id": "bh34", + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:009" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal_acts)" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-027" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "최고" + ], + "Action": "강용원이 김선웅에게 물품대금 지급을 최고하는 통지서 발송 및 도달", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "최고", + "ActionType": "준법률행위(quasi-legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-034", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-027" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:009", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-027" + ] + } + ] + } + }, + "bh35": { + "bo_id": "bh35", + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:004" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal_acts)", + "fraudulent_transfer" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-008", + "E-010" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + }, + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "최고" + ], + "Action": "강용원이 오민한에게 사해행위취소 및 신림동 상가 소유권 반환을 요구하는 내용증명 발송", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "최고", + "ActionType": "준법률행위(quasi-legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-035", + "liability_candidate_type": "actio_related", + "related_evidence_indexes": [ + "E-008", + "E-010" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:004", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-008", + "E-010" + ] + } + ] + } + }, + "bh36": { + "bo_id": "bh36", + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:007" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)", + "secured_debt" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-011", + "E-019" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "통지" + ], + "Action": "강용원과 양정숙이 박광윤에게 평택시 빌라 유치권소멸을 청구하는 통지서 발송 및 도달", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "통지", + "ActionType": "준법률행위(quasi-legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-036", + "liability_candidate_type": "secured_debt", + "related_evidence_indexes": [ + "E-011", + "E-019" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:007", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "통지", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011", + "E-019" + ] + } + ] + } + }, + "bh37": { + "bo_id": "bh37", + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:010" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-028" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "통지" + ], + "Action": "오민한이 강용원에게 김선웅의 채무에 대한 책임을 부인하는 통지서 발송", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "통지", + "ActionType": "준법률행위(quasi-legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-037", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-028" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:010", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "통지", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-028" + ] + } + ] + } + }, + "bh38": { + "bo_id": "bh38", + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:006" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-010" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "통지" + ], + "Action": "오민한이 강용원에게 내용증명에 대한 답신 발송", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "통지", + "ActionType": "준법률행위(quasi-legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-038", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-010" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:006", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "통지", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-010" + ] + } + ] + } + }, + "bh39": { + "bo_id": "bh39", + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:005" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-009" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "통지" + ], + "Action": "윤건우가 양정숙에게 흑석동 상가 임대차계약 해지 및 명도를 요구하는 내용증명 통지", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "통지", + "ActionType": "준법률행위(quasi-legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-039", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-009" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:005", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "통지", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-009" + ] + } + ] + } + }, + "bh40": { + "bo_id": "bh40", + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:012" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-030" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "통지" + ], + "Action": "윤건우가 이수인에게 흑석동 상가 매매계약 해제 통지서 발송 및 도달", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "통지", + "ActionType": "준법률행위(quasi-legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-040", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-030" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:012", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "통지", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-030" + ] + } + ] + } + }, + "bh41": { + "bo_id": "bh41", + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:011" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)", + "money_claim" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-029" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "통지" + ], + "Action": "이수인이 윤건우에게 흑석동 상가 매매대금 감액 및 제3자 명의 등기 이전 요청 통지", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "통지", + "ActionType": "준법률행위(quasi-legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-041", + "liability_candidate_type": "money_claim", + "related_evidence_indexes": [ + "E-029" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:011", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "통지", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-029" + ] + } + ] + } + }, + "bh42": { + "bo_id": "bh42", + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:008" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)", + "registry_invalidity" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-012" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "통지" + ], + "Action": "주식회사 대한은행이 강용원의 성수동 대지 근저당권 말소 신청을 거부하는 통지 발송", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "통지", + "ActionType": "준법률행위(quasi-legal acts)", + "BOType": "event", + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-042", + "liability_candidate_type": "registry_invalidity", + "related_evidence_indexes": [ + "E-012" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:008", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "통지", + "event", + "준법률행위(quasi-legal acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-012" + ] + } + ] + } + }, + "bh43": { + "bo_id": "bh43", + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:001" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "asset_candidates": [ + { + "asset_label": "성수동 대지", + "asset_kind": "real_property", + "source": "BO.extensions.domain_payload.asset_alias", + "asset_ref_keys": [ + "SCH-016-01", + "REG-013-01", + "REG-013-02", + "REG-013-03", + "REG-013-04", + "REG-013-05", + "REG-013-06", + "REG-013-07" + ], + "related_bo_ids": [ + "bh43" + ], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ] + }, + { + "asset_label": "서울 성동구 성수동 256", + "asset_kind": "unknown", + "source": "BO.extensions.domain_payload.asset_alias", + "asset_ref_keys": [ + "SCH-016-01", + "REG-013-01", + "REG-013-02", + "REG-013-03", + "REG-013-04", + "REG-013-05", + "REG-013-06", + "REG-013-07" + ], + "related_bo_ids": [ + "bh43" + ], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ] + }, + { + "asset_label": "1. 서울 성동구 성수동 256 대 330㎡", + "asset_kind": "unknown", + "source": "BO.extensions.domain_payload.asset_alias", + "asset_ref_keys": [ + "SCH-016-01", + "REG-013-01", + "REG-013-02", + "REG-013-03", + "REG-013-04", + "REG-013-05", + "REG-013-06", + "REG-013-07" + ], + "related_bo_ids": [ + "bh43" + ], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ] + }, + { + "asset_label": "성수동 건물", + "asset_kind": "real_property", + "source": "BO.extensions.domain_payload.asset_alias", + "asset_ref_keys": [ + "SCH-016-02" + ], + "related_bo_ids": [ + "bh43" + ], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ] + }, + { + "asset_label": "서울 성동구 성수동 256 상가", + "asset_kind": "real_property", + "source": "BO.extensions.domain_payload.asset_alias", + "asset_ref_keys": [ + "SCH-016-02" + ], + "related_bo_ids": [ + "bh43" + ], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ] + }, + { + "asset_label": "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "asset_kind": "real_property", + "source": "BO.extensions.domain_payload.asset_alias", + "asset_ref_keys": [ + "SCH-016-02" + ], + "related_bo_ids": [ + "bh43" + ], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ] + }, + { + "asset_label": "신림동 상가", + "asset_kind": "real_property", + "source": "BO.extensions.domain_payload.asset_alias", + "asset_ref_keys": [ + "SCH-016-03", + "REG-014-01", + "REG-014-02", + "REG-014-03", + "REG-014-04", + "REG-014-05", + "REG-014-06" + ], + "related_bo_ids": [ + "bh43" + ], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ] + }, + { + "asset_label": "서울 관악구 신림로 115", + "asset_kind": "unknown", + "source": "BO.extensions.domain_payload.asset_alias", + "asset_ref_keys": [ + "SCH-016-03", + "REG-014-01", + "REG-014-02", + "REG-014-03", + "REG-014-04", + "REG-014-05", + "REG-014-06" + ], + "related_bo_ids": [ + "bh43" + ], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ] + }, + { + "asset_label": "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "asset_kind": "real_property", + "source": "BO.extensions.domain_payload.asset_alias", + "asset_ref_keys": [ + "SCH-016-03", + "REG-014-01", + "REG-014-02", + "REG-014-03", + "REG-014-04", + "REG-014-05", + "REG-014-06" + ], + "related_bo_ids": [ + "bh43" + ], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ] + }, + { + "asset_label": "평택시 빌라", + "asset_kind": "unknown", + "source": "BO.extensions.domain_payload.asset_alias", + "asset_ref_keys": [ + "SCH-016-04", + "REG-015-01", + "REG-015-02", + "REG-015-03", + "REG-015-04" + ], + "related_bo_ids": [ + "bh43" + ], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ] + }, + { + "asset_label": "서정빌라 101호", + "asset_kind": "unknown", + "source": "BO.extensions.domain_payload.asset_alias", + "asset_ref_keys": [ + "SCH-016-04", + "REG-015-01", + "REG-015-02", + "REG-015-03", + "REG-015-04" + ], + "related_bo_ids": [ + "bh43" + ], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ] + }, + { + "asset_label": "평택시 서정3길 123", + "asset_kind": "unknown", + "source": "BO.extensions.domain_payload.asset_alias", + "asset_ref_keys": [ + "SCH-016-04", + "REG-015-01", + "REG-015-02", + "REG-015-03", + "REG-015-04" + ], + "related_bo_ids": [ + "bh43" + ], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ] + }, + { + "asset_label": "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "asset_kind": "real_property", + "source": "BO.extensions.domain_payload.asset_alias", + "asset_ref_keys": [ + "SCH-016-04", + "REG-015-01", + "REG-015-02", + "REG-015-03", + "REG-015-04" + ], + "related_bo_ids": [ + "bh43" + ], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ] + }, + { + "asset_label": "흑석동 상가", + "asset_kind": "real_property", + "source": "BO.extensions.domain_payload.asset_alias", + "asset_ref_keys": [ + "SCH-016-05" + ], + "related_bo_ids": [ + "bh43" + ], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ] + }, + { + "asset_label": "서울 동작구 흑석로 80", + "asset_kind": "unknown", + "source": "BO.extensions.domain_payload.asset_alias", + "asset_ref_keys": [ + "SCH-016-05" + ], + "related_bo_ids": [ + "bh43" + ], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ] + }, + { + "asset_label": "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물", + "asset_kind": "real_property", + "source": "BO.extensions.domain_payload.asset_alias", + "asset_ref_keys": [ + "SCH-016-05" + ], + "related_bo_ids": [ + "bh43" + ], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ] + } + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + }, + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + }, + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + }, + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [], + "Action": "목적물 특정", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": null, + "ActionType": "사실행위(factual acts)", + "BOType": "state", + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-043", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:001", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ] + } + ] + } + }, + "bh44": { + "bo_id": "bh44", + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:013" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)", + "secured_debt" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-011" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [], + "Action": "박광윤의 평택시 서정빌라 101호 거주 및 유치권 행사 상태", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": null, + "ActionType": "사실행위(factual acts)", + "BOType": "state", + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-044", + "liability_candidate_type": "secured_debt", + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:013", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ] + } + ] + } + }, + "bh45": { + "bo_id": "bh45", + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:014" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-006" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [], + "Action": "박성희의 성수동 대지 및 건물 점유", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": null, + "ActionType": "사실행위(factual acts)", + "BOType": "state", + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-045", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-006" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:014", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-006" + ] + } + ] + } + }, + "bh46": { + "bo_id": "bh46", + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:015" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-018", + "E-022" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + }, + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [], + "Action": "양정숙의 흑석동 상가 점유 및 식당 운영 상태", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": null, + "ActionType": "사실행위(factual acts)", + "BOType": "state", + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-046", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-018", + "E-022" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:015", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-018", + "E-022" + ] + } + ] + } + }, + "bh47": { + "bo_id": "bh47", + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:017" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상계", + "state", + "소송행위(litigation_acts)", + "money_claim" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-010" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "상계" + ], + "Action": "오민한의 강용원에 대한 자신의 5,000만 원 채권 상계 주장 항변", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "상계", + "ActionType": "소송행위(litigation acts)", + "BOType": "state", + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-047", + "liability_candidate_type": "money_claim", + "related_evidence_indexes": [ + "E-010" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:017", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상계", + "state", + "소송행위(litigation acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-010" + ] + } + ] + } + }, + "bh48": { + "bo_id": "bh48", + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:018" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-011" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "항변" + ], + "Action": "박광윤의 과거 임대료 및 거주 수익을 채무에 충당하였다는 주장 항변", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "항변", + "ActionType": "소송행위(litigation acts)", + "BOType": "state", + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-048", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-011" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:018", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "항변", + "state", + "소송행위(litigation acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-011" + ] + } + ] + } + }, + "bh49": { + "bo_id": "bh49", + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:016" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-009" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "항변" + ], + "Action": "윤건우의 임대차 책임 부인 및 양정숙 차임 연체 주장 항변", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "항변", + "ActionType": "소송행위(litigation acts)", + "BOType": "state", + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-049", + "liability_candidate_type": "succession_related", + "related_evidence_indexes": [ + "E-009" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:016", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "항변", + "state", + "소송행위(litigation acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-009" + ] + } + ] + } + }, + "bh50": { + "bo_id": "bh50", + "source_domain": "B5_Succession_Notice_Lien_Asset_Defense", + "hard_issue_keys": { + "claim_group_ids": [], + "liability_group_ids": [ + "B5:019" + ], + "linked_structure_ids": [], + "canonical_theory_graph_seed_ref": [] + }, + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)", + "registry_invalidity" + ], + "asset_candidates": [], + "evidence_indexes": [ + "E-012" + ], + "related_event_candidates": [ + { + "event_candidate_id": null, + "event_date": null, + "action_summary": null, + "object": null + } + ], + "keyword_fields": { + "Legal_Keywords": [ + "항변" + ], + "Action": "주식회사 대한은행의 경매공신력 주장(민사집행법 제267조)에 따른 근저당 말소 불가 항변", + "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "JuristicAct_label": "항변", + "ActionType": "소송행위(litigation acts)", + "BOType": "state", + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "legal_effect_tags": [] + }, + "signals": { + "actio": [], + "case_liability": [ + { + "signal_ref": "case-liability-signal-050", + "liability_candidate_type": "registry_invalidity", + "related_evidence_indexes": [ + "E-012" + ], + "party_roles": [], + "claim_group_id": null, + "liability_group_id": "B5:019", + "linked_structure_ids": [] + } + ], + "legal_effect": [ + { + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "항변", + "state", + "소송행위(litigation acts)", + "B5_Succession_Notice_Lien_Asset_Defense" + ], + "linked_structure_ids": [], + "related_evidence_indexes": [ + "E-012" + ] + } + ] + } + } + }, + "input_warnings": [] + } +} diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/legal_effect_structure_seed_bundle.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/legal_effect_structure_seed_bundle.json new file mode 100644 index 00000000..fbd82585 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/legal_effect_structure_seed_bundle.json @@ -0,0 +1,15301 @@ +{ + "legal_structure_seed_bundle": { + "schema_version": "stage1_legal_structure_seed_bundle.v1", + "status": "READY_WITH_REVIEW", + "source_input_pack_schema": "stage1_legal_structure_input_pack.v1", + "structure_seed_items": [ + { + "seed_id": "LESS-001", + "source_bo_ids": [ + "bh1" + ], + "representative_candidate_structure_type": "money_claim", + "candidate_structure_types": [ + "money_claim", + "도급", + "공사대금", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "issue_cluster_ids": [ + "IC-001" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-007" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:011" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "commercial_successor", + "score": 1, + "basis": [ + "successor" + ] + }, + { + "candidate_value": "money_claim", + "score": 1, + "basis": [ + "money" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "money_claim", + "도급", + "공사대금", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "downstream_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "representative_candidate_structure_type": "money_claim", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-002", + "source_bo_ids": [ + "bh10" + ], + "representative_candidate_structure_type": "money_claim", + "candidate_structure_types": [ + "money_claim", + "상계", + "succession_notice_lien_asset_defense", + "event", + "소송행위(litigation_acts)", + "commercial_successor" + ], + "issue_cluster_ids": [ + "IC-002" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:013" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 3, + "basis": [ + "notice", + "lien", + "항변" + ] + }, + { + "candidate_value": "money_claim", + "score": 3, + "basis": [ + "money", + "대여금", + "채권" + ] + }, + { + "candidate_value": "commercial_successor", + "score": 1, + "basis": [ + "successor" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "money_claim", + "상계", + "succession_notice_lien_asset_defense", + "event", + "소송행위(litigation_acts)", + "commercial_successor" + ], + "downstream_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "money_claim", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-003", + "source_bo_ids": [ + "bh11" + ], + "representative_candidate_structure_type": "money_claim", + "candidate_structure_types": [ + "money_claim", + "이행최고", + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal_acts)", + "commercial_successor" + ], + "issue_cluster_ids": [ + "IC-003" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-027" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:003" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 3, + "basis": [ + "notice", + "lien", + "통지" + ] + }, + { + "candidate_value": "commercial_successor", + "score": 1, + "basis": [ + "successor" + ] + }, + { + "candidate_value": "money_claim", + "score": 1, + "basis": [ + "money" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "money_claim", + "이행최고", + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal_acts)", + "commercial_successor" + ], + "downstream_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "money_claim", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-004", + "source_bo_ids": [ + "bh12" + ], + "representative_candidate_structure_type": "money_claim", + "candidate_structure_types": [ + "money_claim", + "succession_notice_lien_asset_defense", + "책임부인", + "event", + "준법률행위(quasi-legal_acts)", + "commercial_successor" + ], + "issue_cluster_ids": [ + "IC-004" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-028" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:005" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 3, + "basis": [ + "notice", + "lien", + "통지" + ] + }, + { + "candidate_value": "commercial_successor", + "score": 2, + "basis": [ + "successor", + "상호속용" + ] + }, + { + "candidate_value": "money_claim", + "score": 1, + "basis": [ + "money" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "money_claim", + "succession_notice_lien_asset_defense", + "책임부인", + "event", + "준법률행위(quasi-legal_acts)", + "commercial_successor" + ], + "downstream_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "money_claim", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-005", + "source_bo_ids": [ + "bh13" + ], + "representative_candidate_structure_type": "money_claim", + "candidate_structure_types": [ + "money_claim", + "충당", + "state", + "사실행위(factual_acts)", + "commercial_successor" + ], + "issue_cluster_ids": [ + "IC-005" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-011" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:012" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 2, + "basis": [ + "유치권", + "항변" + ] + }, + { + "candidate_value": "money_claim", + "score": 2, + "basis": [ + "money", + "미지급" + ] + }, + { + "candidate_value": "commercial_successor", + "score": 1, + "basis": [ + "successor" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "money_claim", + "충당", + "state", + "사실행위(factual_acts)", + "commercial_successor" + ], + "downstream_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "representative_candidate_structure_type": "money_claim", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-006", + "source_bo_ids": [ + "bh14" + ], + "representative_candidate_structure_type": "secured_debt", + "candidate_structure_types": [ + "secured_debt", + "상속", + "증여", + "(근)저당권_설정_계약", + "event", + "법률행위(legal_acts)" + ], + "issue_cluster_ids": [ + "IC-006" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:001" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "secured_debt", + "score": 4, + "basis": [ + "secured", + "담보", + "근저당", + "저당" + ] + }, + { + "candidate_value": "registry_invalidity", + "score": 1, + "basis": [ + "registry" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "secured_debt", + "상속", + "증여", + "(근)저당권_설정_계약", + "event", + "법률행위(legal_acts)" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "secured_debt", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-007", + "source_bo_ids": [ + "bh15" + ], + "representative_candidate_structure_type": "secured_debt", + "candidate_structure_types": [ + "secured_debt", + "mortgage_extinguishment", + "registry_invalidity", + "상계", + "event", + "법률행위(legal_acts)" + ], + "issue_cluster_ids": [ + "IC-007" + ], + "asset_cluster_ids": [ + "AC-002" + ], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-010", + "E-014" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:004" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "registry_invalidity", + "score": 3, + "basis": [ + "registry", + "등기", + "말소" + ] + }, + { + "candidate_value": "secured_debt", + "score": 3, + "basis": [ + "secured", + "근저당", + "저당" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "secured_debt", + "mortgage_extinguishment", + "registry_invalidity", + "상계", + "event", + "법률행위(legal_acts)" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "secured_debt", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-008", + "source_bo_ids": [ + "bh16" + ], + "representative_candidate_structure_type": "secured_debt", + "candidate_structure_types": [ + "secured_debt", + "소유권보존", + "소유권이전", + "가압류", + "registry_invalidity", + "event", + "법률행위(legal_acts)" + ], + "issue_cluster_ids": [ + "IC-008" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-015" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:006" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "registry_invalidity", + "score": 3, + "basis": [ + "registry", + "등기", + "소유권이전" + ] + }, + { + "candidate_value": "secured_debt", + "score": 1, + "basis": [ + "secured" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "secured_debt", + "소유권보존", + "소유권이전", + "가압류", + "registry_invalidity", + "event", + "법률행위(legal_acts)" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "secured_debt", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-009", + "source_bo_ids": [ + "bh17" + ], + "representative_candidate_structure_type": "secured_debt", + "candidate_structure_types": [ + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "secured_debt", + "임의경매", + "공신력", + "registry_invalidity", + "event", + "소송행위(litigation_acts)" + ], + "issue_cluster_ids": [ + "IC-009" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-012", + "E-013" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:003" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "secured_debt", + "score": 4, + "basis": [ + "secured", + "담보", + "근저당", + "저당" + ] + }, + { + "candidate_value": "registry_invalidity", + "score": 3, + "basis": [ + "registry", + "등기", + "무효" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "secured_debt", + "임의경매", + "공신력", + "registry_invalidity", + "event", + "소송행위(litigation_acts)" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "secured_debt", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-010", + "source_bo_ids": [ + "bh18" + ], + "representative_candidate_structure_type": "secured_debt", + "candidate_structure_types": [ + "payment_allocation_uncertain", + "secured_debt", + "변제", + "변제충당", + "변제_및_변제충당", + "event", + "준법률행위(quasi-legal_acts)" + ], + "issue_cluster_ids": [ + "IC-010" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:002" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "money_claim", + "score": 1, + "basis": [ + "변제" + ] + }, + { + "candidate_value": "registry_invalidity", + "score": 1, + "basis": [ + "registry" + ] + }, + { + "candidate_value": "secured_debt", + "score": 1, + "basis": [ + "secured" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "payment_allocation_uncertain", + "secured_debt", + "변제", + "변제충당", + "변제_및_변제충당", + "event", + "준법률행위(quasi-legal_acts)" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "secured_debt", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-011", + "source_bo_ids": [ + "bh19" + ], + "representative_candidate_structure_type": "money_claim", + "candidate_structure_types": [ + "payment_allocation", + "secured_debt", + "succession_notice_lien_asset_defense", + "land_use_gain", + "변제충당", + "변제충당_주장", + "event", + "준법률행위(quasi-legal_acts)", + "money_claim" + ], + "issue_cluster_ids": [ + "IC-011" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-007", + "E-011" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:005" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 4, + "basis": [ + "notice", + "lien", + "유치권", + "항변" + ] + }, + { + "candidate_value": "money_claim", + "score": 3, + "basis": [ + "money", + "채권", + "변제" + ] + }, + { + "candidate_value": "secured_debt", + "score": 3, + "basis": [ + "secured", + "담보", + "피담보" + ] + }, + { + "candidate_value": "land_use_gain", + "score": 2, + "basis": [ + "land", + "점유" + ] + }, + { + "candidate_value": "registry_invalidity", + "score": 1, + "basis": [ + "registry" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "payment_allocation", + "secured_debt", + "succession_notice_lien_asset_defense", + "land_use_gain", + "변제충당", + "변제충당_주장", + "event", + "준법률행위(quasi-legal_acts)", + "money_claim" + ], + "downstream_roles": [ + "claim_chain_ref", + "legal_calculation_object", + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "money_claim", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-012", + "source_bo_ids": [ + "bh2" + ], + "representative_candidate_structure_type": "money_claim", + "candidate_structure_types": [ + "money_claim", + "물품대금", + "매매", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "issue_cluster_ids": [ + "IC-012" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-021", + "E-027" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:001" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "money_claim", + "score": 2, + "basis": [ + "money", + "채권" + ] + }, + { + "candidate_value": "commercial_successor", + "score": 1, + "basis": [ + "successor" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "money_claim", + "물품대금", + "매매", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "downstream_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "representative_candidate_structure_type": "money_claim", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-013", + "source_bo_ids": [ + "bh20" + ], + "representative_candidate_structure_type": "secured_debt", + "candidate_structure_types": [ + "registry_invalidity", + "secured_debt", + "요구", + "거절", + "event", + "준법률행위(quasi-legal_acts)" + ], + "issue_cluster_ids": [ + "IC-013" + ], + "asset_cluster_ids": [ + "AC-004" + ], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-029" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B2:007" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "registry_invalidity", + "score": 3, + "basis": [ + "registry", + "등기", + "소유권이전" + ] + }, + { + "candidate_value": "secured_debt", + "score": 1, + "basis": [ + "secured" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "registry_invalidity", + "secured_debt", + "요구", + "거절", + "event", + "준법률행위(quasi-legal_acts)" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "secured_debt", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-014", + "source_bo_ids": [ + "bh21" + ], + "representative_candidate_structure_type": "land_use_gain", + "candidate_structure_types": [ + "valuation", + "land_use_gain", + "임대료", + "차임", + "event", + "사실행위(factual_acts)" + ], + "issue_cluster_ids": [ + "IC-014" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006" + ], + "multi_asset_bundle_refs": [ + "bundle:bh21" + ], + "evidence_indexes": [ + "E-005" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:002" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "valuation", + "score": 3, + "basis": [ + "valuation", + "감정", + "평가" + ] + }, + { + "candidate_value": "land_use_gain", + "score": 1, + "basis": [ + "land" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "valuation", + "land_use_gain", + "임대료", + "차임", + "event", + "사실행위(factual_acts)" + ], + "downstream_roles": [ + "legal_calculation_object", + "legal_effect_roles" + ], + "representative_candidate_structure_type": "land_use_gain", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-015", + "source_bo_ids": [ + "bh22" + ], + "representative_candidate_structure_type": "land_use_gain", + "candidate_structure_types": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "사용", + "state", + "사실행위(factual_acts)" + ], + "issue_cluster_ids": [ + "IC-015" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-007", + "AC-008" + ], + "multi_asset_bundle_refs": [ + "bundle:bh22" + ], + "evidence_indexes": [ + "E-006" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:004" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "land_use_gain", + "score": 3, + "basis": [ + "land", + "점유", + "인도" + ] + }, + { + "candidate_value": "asset_defense", + "score": 2, + "basis": [ + "notice", + "lien" + ] + }, + { + "candidate_value": "valuation", + "score": 1, + "basis": [ + "valuation" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "사용", + "state", + "사실행위(factual_acts)" + ], + "downstream_roles": [ + "legal_calculation_object", + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "land_use_gain", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-016", + "source_bo_ids": [ + "bh23" + ], + "representative_candidate_structure_type": "land_use_gain", + "candidate_structure_types": [ + "valuation", + "land_use_gain", + "state", + "사실행위(factual_acts)" + ], + "issue_cluster_ids": [ + "IC-016" + ], + "asset_cluster_ids": [ + "AC-009", + "AC-010" + ], + "multi_asset_bundle_refs": [ + "bundle:bh23" + ], + "evidence_indexes": [ + "E-004" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:001" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "valuation", + "score": 3, + "basis": [ + "valuation", + "감정", + "평가" + ] + }, + { + "candidate_value": "land_use_gain", + "score": 1, + "basis": [ + "land" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "valuation", + "land_use_gain", + "state", + "사실행위(factual_acts)" + ], + "downstream_roles": [ + "legal_calculation_object", + "legal_effect_roles" + ], + "representative_candidate_structure_type": "land_use_gain", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-017", + "source_bo_ids": [ + "bh24" + ], + "representative_candidate_structure_type": "secured_debt", + "candidate_structure_types": [ + "possession", + "secured_debt", + "land_use_gain", + "거주", + "succession_notice_lien_asset_defense", + "수익", + "state", + "사실행위(factual_acts)" + ], + "issue_cluster_ids": [ + "IC-017" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-012" + ], + "multi_asset_bundle_refs": [ + "bundle:bh24" + ], + "evidence_indexes": [ + "E-011" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:005" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 3, + "basis": [ + "notice", + "lien", + "유치권" + ] + }, + { + "candidate_value": "land_use_gain", + "score": 2, + "basis": [ + "land", + "점유" + ] + }, + { + "candidate_value": "secured_debt", + "score": 1, + "basis": [ + "secured" + ] + }, + { + "candidate_value": "valuation", + "score": 1, + "basis": [ + "valuation" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "possession", + "secured_debt", + "land_use_gain", + "거주", + "succession_notice_lien_asset_defense", + "수익", + "state", + "사실행위(factual_acts)" + ], + "downstream_roles": [ + "legal_calculation_object", + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "secured_debt", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-018", + "source_bo_ids": [ + "bh25" + ], + "representative_candidate_structure_type": "land_use_gain", + "candidate_structure_types": [ + "valuation", + "land_use_gain", + "차임", + "state", + "사실행위(factual_acts)" + ], + "issue_cluster_ids": [ + "IC-018" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-013" + ], + "multi_asset_bundle_refs": [ + "bundle:bh25" + ], + "evidence_indexes": [ + "E-023" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:003" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "valuation", + "score": 2, + "basis": [ + "valuation", + "평가" + ] + }, + { + "candidate_value": "land_use_gain", + "score": 1, + "basis": [ + "land" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "valuation", + "land_use_gain", + "차임", + "state", + "사실행위(factual_acts)" + ], + "downstream_roles": [ + "legal_calculation_object", + "legal_effect_roles" + ], + "representative_candidate_structure_type": "land_use_gain", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-019", + "source_bo_ids": [ + "bh26" + ], + "representative_candidate_structure_type": "land_use_gain", + "candidate_structure_types": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "운영", + "사업자등록", + "state", + "사실행위(factual_acts)" + ], + "issue_cluster_ids": [ + "IC-019" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "multi_asset_bundle_refs": [ + "bundle:bh26" + ], + "evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B3:006" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "land_use_gain", + "score": 3, + "basis": [ + "land", + "점유", + "인도" + ] + }, + { + "candidate_value": "asset_defense", + "score": 2, + "basis": [ + "notice", + "lien" + ] + }, + { + "candidate_value": "valuation", + "score": 1, + "basis": [ + "valuation" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "운영", + "사업자등록", + "state", + "사실행위(factual_acts)" + ], + "downstream_roles": [ + "legal_calculation_object", + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "land_use_gain", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-020", + "source_bo_ids": [ + "bh27" + ], + "representative_candidate_structure_type": "fraudulent_transfer", + "candidate_structure_types": [ + "fraudulent_transfer", + "매매계약", + "소유권이전", + "책임재산감소", + "매매", + "event", + "법률행위(legal_acts)" + ], + "issue_cluster_ids": [ + "IC-020" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-014" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B4:002" + ], + "linked_actio_signal_ids": [ + "actio-signal-002" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "actio", + "score": 4, + "basis": [ + "fraudulent", + "actio", + "사해", + "수익자" + ] + }, + { + "candidate_value": "registry_invalidity", + "score": 2, + "basis": [ + "등기", + "소유권이전" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "fraudulent_transfer", + "매매계약", + "소유권이전", + "책임재산감소", + "매매", + "event", + "법률행위(legal_acts)" + ], + "downstream_roles": [ + "actio_roles", + "legal_effect_roles" + ], + "representative_candidate_structure_type": "fraudulent_transfer", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-021", + "source_bo_ids": [ + "bh28" + ], + "representative_candidate_structure_type": "money_claim", + "candidate_structure_types": [ + "preserved_claim", + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "fraudulent_transfer" + ], + "issue_cluster_ids": [ + "IC-021" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-024" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B4:001" + ], + "linked_actio_signal_ids": [ + "actio-signal-001" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "actio", + "score": 4, + "basis": [ + "fraudulent", + "actio", + "사해", + "피보전" + ] + }, + { + "candidate_value": "money_claim", + "score": 3, + "basis": [ + "money", + "대여금", + "채권" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "preserved_claim", + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "fraudulent_transfer" + ], + "downstream_roles": [ + "actio_roles", + "claim_chain_ref", + "legal_effect_roles" + ], + "representative_candidate_structure_type": "money_claim", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-022", + "source_bo_ids": [ + "bh29" + ], + "representative_candidate_structure_type": "secured_debt", + "candidate_structure_types": [ + "value_compensation", + "valuation", + "actio_remedy_or_cap", + "원물반환불능", + "secured_debt", + "registry_invalidity", + "event", + "준법률행위(quasi-legal_acts)", + "fraudulent_transfer" + ], + "issue_cluster_ids": [ + "IC-022" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-002" + ], + "multi_asset_bundle_refs": [ + "bundle:bh29" + ], + "evidence_indexes": [ + "E-014" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B4:004" + ], + "linked_actio_signal_ids": [ + "actio-signal-004" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "actio", + "score": 5, + "basis": [ + "fraudulent", + "actio", + "사해", + "수익자", + "가액배상" + ] + }, + { + "candidate_value": "secured_debt", + "score": 5, + "basis": [ + "secured", + "담보", + "근저당", + "피담보", + "저당" + ] + }, + { + "candidate_value": "registry_invalidity", + "score": 2, + "basis": [ + "registry", + "말소" + ] + }, + { + "candidate_value": "valuation", + "score": 2, + "basis": [ + "valuation", + "가액" + ] + }, + { + "candidate_value": "money_claim", + "score": 1, + "basis": [ + "변제" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "value_compensation", + "valuation", + "actio_remedy_or_cap", + "원물반환불능", + "secured_debt", + "registry_invalidity", + "event", + "준법률행위(quasi-legal_acts)", + "fraudulent_transfer" + ], + "downstream_roles": [ + "actio_roles", + "legal_calculation_object", + "legal_effect_roles", + "linked_actio_structures", + "linked_structures" + ], + "representative_candidate_structure_type": "secured_debt", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-023", + "source_bo_ids": [ + "bh3" + ], + "representative_candidate_structure_type": "money_claim", + "candidate_structure_types": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "issue_cluster_ids": [ + "IC-023" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-024" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:006" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "money_claim", + "score": 3, + "basis": [ + "money", + "대여금", + "채권" + ] + }, + { + "candidate_value": "commercial_successor", + "score": 1, + "basis": [ + "successor" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "downstream_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "representative_candidate_structure_type": "money_claim", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-024", + "source_bo_ids": [ + "bh30" + ], + "representative_candidate_structure_type": "valuation", + "candidate_structure_types": [ + "insolvency_check", + "fraudulent_transfer", + "무자력", + "책임재산", + "valuation", + "state", + "사실행위(factual_acts)" + ], + "issue_cluster_ids": [ + "IC-024" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-020", + "AC-002", + "AC-021" + ], + "multi_asset_bundle_refs": [ + "bundle:bh30" + ], + "evidence_indexes": [ + "E-004" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B4:003" + ], + "linked_actio_signal_ids": [ + "actio-signal-003" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "actio", + "score": 3, + "basis": [ + "fraudulent", + "actio", + "사해" + ] + }, + { + "candidate_value": "valuation", + "score": 2, + "basis": [ + "valuation", + "평가" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "insolvency_check", + "fraudulent_transfer", + "무자력", + "책임재산", + "valuation", + "state", + "사실행위(factual_acts)" + ], + "downstream_roles": [ + "actio_roles", + "legal_calculation_object", + "legal_effect_roles" + ], + "representative_candidate_structure_type": "valuation", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-025", + "source_bo_ids": [ + "bh31" + ], + "representative_candidate_structure_type": "fraudulent_transfer", + "candidate_structure_types": [ + "defense_assertion", + "defense_rebuttal_matrix_bo", + "fraudulent_transfer", + "명의신탁", + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)" + ], + "issue_cluster_ids": [ + "IC-025" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B4:005" + ], + "linked_actio_signal_ids": [ + "actio-signal-005" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "actio", + "score": 3, + "basis": [ + "fraudulent", + "actio", + "수익자" + ] + }, + { + "candidate_value": "asset_defense", + "score": 3, + "basis": [ + "notice", + "lien", + "항변" + ] + }, + { + "candidate_value": "money_claim", + "score": 2, + "basis": [ + "대여금", + "채권" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "defense_assertion", + "defense_rebuttal_matrix_bo", + "fraudulent_transfer", + "명의신탁", + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)" + ], + "downstream_roles": [ + "actio_roles", + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "fraudulent_transfer", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-026", + "source_bo_ids": [ + "bh32" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상속개시", + "event", + "사실행위(factual_acts)" + ], + "issue_cluster_ids": [ + "IC-026" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-013" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:002" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 2, + "basis": [ + "notice", + "lien" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "succession_notice_lien_asset_defense", + "상속개시", + "event", + "사실행위(factual_acts)" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-027", + "source_bo_ids": [ + "bh33" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상속포기", + "event", + "사실행위(factual_acts)" + ], + "issue_cluster_ids": [ + "IC-027" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-001", + "E-002" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:003" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 2, + "basis": [ + "notice", + "lien" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "succession_notice_lien_asset_defense", + "상속포기", + "event", + "사실행위(factual_acts)" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-028", + "source_bo_ids": [ + "bh34" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal_acts)" + ], + "issue_cluster_ids": [ + "IC-028" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-027" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:009" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 3, + "basis": [ + "notice", + "lien", + "통지" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal_acts)" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-029", + "source_bo_ids": [ + "bh35" + ], + "representative_candidate_structure_type": "fraudulent_transfer", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal_acts)", + "fraudulent_transfer" + ], + "issue_cluster_ids": [ + "IC-029" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-008", + "E-010" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:004" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "actio", + "score": 2, + "basis": [ + "fraudulent", + "사해" + ] + }, + { + "candidate_value": "asset_defense", + "score": 2, + "basis": [ + "notice", + "lien" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal_acts)", + "fraudulent_transfer" + ], + "downstream_roles": [ + "actio_roles", + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "fraudulent_transfer", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-030", + "source_bo_ids": [ + "bh36" + ], + "representative_candidate_structure_type": "secured_debt", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)", + "secured_debt" + ], + "issue_cluster_ids": [ + "IC-030" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-011", + "E-019" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:007" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 4, + "basis": [ + "notice", + "lien", + "통지", + "유치권" + ] + }, + { + "candidate_value": "secured_debt", + "score": 1, + "basis": [ + "secured" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)", + "secured_debt" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "secured_debt", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-031", + "source_bo_ids": [ + "bh37" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "issue_cluster_ids": [ + "IC-031" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-028" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:010" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 3, + "basis": [ + "notice", + "lien", + "통지" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-032", + "source_bo_ids": [ + "bh38" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "issue_cluster_ids": [ + "IC-032" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-010" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:006" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 3, + "basis": [ + "notice", + "lien", + "통지" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-033", + "source_bo_ids": [ + "bh39" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "issue_cluster_ids": [ + "IC-033" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-009" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:005" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 3, + "basis": [ + "notice", + "lien", + "통지" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-034", + "source_bo_ids": [ + "bh4" + ], + "representative_candidate_structure_type": "money_claim", + "candidate_structure_types": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "issue_cluster_ids": [ + "IC-034" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-024" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:007" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "money_claim", + "score": 3, + "basis": [ + "money", + "대여금", + "채권" + ] + }, + { + "candidate_value": "commercial_successor", + "score": 1, + "basis": [ + "successor" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "downstream_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "representative_candidate_structure_type": "money_claim", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-035", + "source_bo_ids": [ + "bh40" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "issue_cluster_ids": [ + "IC-035" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-030" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:012" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 3, + "basis": [ + "notice", + "lien", + "통지" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-036", + "source_bo_ids": [ + "bh41" + ], + "representative_candidate_structure_type": "money_claim", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)", + "money_claim" + ], + "issue_cluster_ids": [ + "IC-036" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-029" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:011" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 3, + "basis": [ + "notice", + "lien", + "통지" + ] + }, + { + "candidate_value": "money_claim", + "score": 2, + "basis": [ + "money", + "매매대금" + ] + }, + { + "candidate_value": "registry_invalidity", + "score": 1, + "basis": [ + "등기" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)", + "money_claim" + ], + "downstream_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "money_claim", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-037", + "source_bo_ids": [ + "bh42" + ], + "representative_candidate_structure_type": "registry_invalidity", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)", + "registry_invalidity" + ], + "issue_cluster_ids": [ + "IC-037" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-012" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:008" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 3, + "basis": [ + "notice", + "lien", + "통지" + ] + }, + { + "candidate_value": "registry_invalidity", + "score": 2, + "basis": [ + "registry", + "말소" + ] + }, + { + "candidate_value": "secured_debt", + "score": 2, + "basis": [ + "근저당", + "저당" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)", + "registry_invalidity" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "registry_invalidity", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-038", + "source_bo_ids": [ + "bh43" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "asset_cluster_ids": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "multi_asset_bundle_refs": [ + "bundle:bh43" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:001" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 2, + "basis": [ + "notice", + "lien" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-039", + "source_bo_ids": [ + "bh44" + ], + "representative_candidate_structure_type": "secured_debt", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)", + "secured_debt" + ], + "issue_cluster_ids": [ + "IC-039" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-011" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:013" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 3, + "basis": [ + "notice", + "lien", + "유치권" + ] + }, + { + "candidate_value": "secured_debt", + "score": 1, + "basis": [ + "secured" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)", + "secured_debt" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "secured_debt", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-040", + "source_bo_ids": [ + "bh45" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "issue_cluster_ids": [ + "IC-040" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-006" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:014" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 2, + "basis": [ + "notice", + "lien" + ] + }, + { + "candidate_value": "land_use_gain", + "score": 1, + "basis": [ + "점유" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-041", + "source_bo_ids": [ + "bh46" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "issue_cluster_ids": [ + "IC-041" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-018", + "E-022" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:015" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 2, + "basis": [ + "notice", + "lien" + ] + }, + { + "candidate_value": "land_use_gain", + "score": 1, + "basis": [ + "점유" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-042", + "source_bo_ids": [ + "bh47" + ], + "representative_candidate_structure_type": "money_claim", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상계", + "state", + "소송행위(litigation_acts)", + "money_claim" + ], + "issue_cluster_ids": [ + "IC-042" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-010" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:017" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 3, + "basis": [ + "notice", + "lien", + "항변" + ] + }, + { + "candidate_value": "money_claim", + "score": 2, + "basis": [ + "money", + "채권" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "succession_notice_lien_asset_defense", + "상계", + "state", + "소송행위(litigation_acts)", + "money_claim" + ], + "downstream_roles": [ + "claim_chain_ref", + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "money_claim", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-043", + "source_bo_ids": [ + "bh48" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)" + ], + "issue_cluster_ids": [ + "IC-043" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-011" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:018" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 3, + "basis": [ + "notice", + "lien", + "항변" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-044", + "source_bo_ids": [ + "bh49" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)" + ], + "issue_cluster_ids": [ + "IC-044" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-009" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:016" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 3, + "basis": [ + "notice", + "lien", + "항변" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "succession_notice_lien_asset_defense", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-045", + "source_bo_ids": [ + "bh5" + ], + "representative_candidate_structure_type": "money_claim", + "candidate_structure_types": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "issue_cluster_ids": [ + "IC-045" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-025" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:008" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "money_claim", + "score": 3, + "basis": [ + "money", + "대여금", + "채권" + ] + }, + { + "candidate_value": "commercial_successor", + "score": 1, + "basis": [ + "successor" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "downstream_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "representative_candidate_structure_type": "money_claim", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-046", + "source_bo_ids": [ + "bh50" + ], + "representative_candidate_structure_type": "registry_invalidity", + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)", + "registry_invalidity" + ], + "issue_cluster_ids": [ + "IC-046" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-012" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B5:019" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "asset_defense", + "score": 3, + "basis": [ + "notice", + "lien", + "항변" + ] + }, + { + "candidate_value": "registry_invalidity", + "score": 2, + "basis": [ + "registry", + "말소" + ] + }, + { + "candidate_value": "secured_debt", + "score": 2, + "basis": [ + "근저당", + "저당" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)", + "registry_invalidity" + ], + "downstream_roles": [ + "legal_effect_roles", + "linked_structures" + ], + "representative_candidate_structure_type": "registry_invalidity", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-047", + "source_bo_ids": [ + "bh6" + ], + "representative_candidate_structure_type": "money_claim", + "candidate_structure_types": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "issue_cluster_ids": [ + "IC-047" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-026" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:009" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "money_claim", + "score": 3, + "basis": [ + "money", + "대여금", + "채권" + ] + }, + { + "candidate_value": "commercial_successor", + "score": 1, + "basis": [ + "successor" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "downstream_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "representative_candidate_structure_type": "money_claim", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-048", + "source_bo_ids": [ + "bh7" + ], + "representative_candidate_structure_type": "money_claim", + "candidate_structure_types": [ + "money_claim", + "영업양도", + "commercial_successor", + "event", + "법률행위(legal_acts)" + ], + "issue_cluster_ids": [ + "IC-048" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-020" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:004" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "commercial_successor", + "score": 2, + "basis": [ + "successor", + "상호속용" + ] + }, + { + "candidate_value": "money_claim", + "score": 1, + "basis": [ + "money" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "money_claim", + "영업양도", + "commercial_successor", + "event", + "법률행위(legal_acts)" + ], + "downstream_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "representative_candidate_structure_type": "money_claim", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-049", + "source_bo_ids": [ + "bh8" + ], + "representative_candidate_structure_type": "money_claim", + "candidate_structure_types": [ + "money_claim", + "채무승인", + "변제기_유예", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "issue_cluster_ids": [ + "IC-049" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [ + "E-021" + ], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:002" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "money_claim", + "score": 2, + "basis": [ + "money", + "변제" + ] + }, + { + "candidate_value": "commercial_successor", + "score": 1, + "basis": [ + "successor" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "money_claim", + "채무승인", + "변제기_유예", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "downstream_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "representative_candidate_structure_type": "money_claim", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + }, + { + "seed_id": "LESS-050", + "source_bo_ids": [ + "bh9" + ], + "representative_candidate_structure_type": "money_claim", + "candidate_structure_types": [ + "money_claim", + "변제", + "법정변제충당", + "event", + "사실행위(factual_acts)", + "commercial_successor" + ], + "issue_cluster_ids": [ + "IC-050" + ], + "asset_cluster_ids": [], + "multi_asset_bundle_refs": [], + "evidence_indexes": [], + "linked_claim_group_ids": [], + "linked_liability_group_ids": [ + "B1:010" + ], + "linked_actio_signal_ids": [], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "confidence": "LOW", + "review_required": true, + "diagnostics": { + "issue": { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [ + "liability_group_ids" + ], + "demoted_hard_keys": {}, + "score_table": [ + { + "candidate_value": "money_claim", + "score": 2, + "basis": [ + "money", + "변제" + ] + }, + { + "candidate_value": "commercial_successor", + "score": 1, + "basis": [ + "successor" + ] + } + ], + "conflict_flags": [] + }, + "candidate_structure_type": { + "candidate_types": [ + "money_claim", + "변제", + "법정변제충당", + "event", + "사실행위(factual_acts)", + "commercial_successor" + ], + "downstream_roles": [ + "claim_chain_ref", + "legal_effect_roles" + ], + "representative_candidate_structure_type": "money_claim", + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "decision": "llm_required" + } + } + } + ], + "issue_cluster_seed_map": { + "IC-001": { + "issue_cluster_id": "IC-001", + "issue_key": "hard:B1:011", + "issue_type": "B1:011", + "source_bo_ids": [ + "bh1" + ], + "candidate_structure_types": [ + "money_claim", + "도급", + "공사대금", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:011" + ], + "evidence_indexes": [ + "E-007" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-002": { + "issue_cluster_id": "IC-002", + "issue_key": "hard:B1:013", + "issue_type": "B1:013", + "source_bo_ids": [ + "bh10" + ], + "candidate_structure_types": [ + "money_claim", + "상계", + "succession_notice_lien_asset_defense", + "event", + "소송행위(litigation_acts)", + "commercial_successor" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:013" + ], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-003": { + "issue_cluster_id": "IC-003", + "issue_key": "hard:B1:003", + "issue_type": "B1:003", + "source_bo_ids": [ + "bh11" + ], + "candidate_structure_types": [ + "money_claim", + "이행최고", + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal_acts)", + "commercial_successor" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:003" + ], + "evidence_indexes": [ + "E-027" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-004": { + "issue_cluster_id": "IC-004", + "issue_key": "hard:B1:005", + "issue_type": "B1:005", + "source_bo_ids": [ + "bh12" + ], + "candidate_structure_types": [ + "money_claim", + "succession_notice_lien_asset_defense", + "책임부인", + "event", + "준법률행위(quasi-legal_acts)", + "commercial_successor" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:005" + ], + "evidence_indexes": [ + "E-028" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-005": { + "issue_cluster_id": "IC-005", + "issue_key": "hard:B1:012", + "issue_type": "B1:012", + "source_bo_ids": [ + "bh13" + ], + "candidate_structure_types": [ + "money_claim", + "충당", + "state", + "사실행위(factual_acts)", + "commercial_successor" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:012" + ], + "evidence_indexes": [ + "E-011" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-006": { + "issue_cluster_id": "IC-006", + "issue_key": "hard:B2:001", + "issue_type": "B2:001", + "source_bo_ids": [ + "bh14" + ], + "candidate_structure_types": [ + "secured_debt", + "상속", + "증여", + "(근)저당권_설정_계약", + "event", + "법률행위(legal_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:001" + ], + "evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-007": { + "issue_cluster_id": "IC-007", + "issue_key": "hard:B2:004", + "issue_type": "B2:004", + "source_bo_ids": [ + "bh15" + ], + "candidate_structure_types": [ + "secured_debt", + "mortgage_extinguishment", + "registry_invalidity", + "상계", + "event", + "법률행위(legal_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:004" + ], + "evidence_indexes": [ + "E-010", + "E-014" + ], + "asset_cluster_ids": [ + "AC-002" + ], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-008": { + "issue_cluster_id": "IC-008", + "issue_key": "hard:B2:006", + "issue_type": "B2:006", + "source_bo_ids": [ + "bh16" + ], + "candidate_structure_types": [ + "secured_debt", + "소유권보존", + "소유권이전", + "가압류", + "registry_invalidity", + "event", + "법률행위(legal_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:006" + ], + "evidence_indexes": [ + "E-015" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-009": { + "issue_cluster_id": "IC-009", + "issue_key": "hard:B2:003", + "issue_type": "B2:003", + "source_bo_ids": [ + "bh17" + ], + "candidate_structure_types": [ + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "secured_debt", + "임의경매", + "공신력", + "registry_invalidity", + "event", + "소송행위(litigation_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:003" + ], + "evidence_indexes": [ + "E-012", + "E-013" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-010": { + "issue_cluster_id": "IC-010", + "issue_key": "hard:B2:002", + "issue_type": "B2:002", + "source_bo_ids": [ + "bh18" + ], + "candidate_structure_types": [ + "payment_allocation_uncertain", + "secured_debt", + "변제", + "변제충당", + "변제_및_변제충당", + "event", + "준법률행위(quasi-legal_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:002" + ], + "evidence_indexes": [], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-011": { + "issue_cluster_id": "IC-011", + "issue_key": "hard:B2:005", + "issue_type": "B2:005", + "source_bo_ids": [ + "bh19" + ], + "candidate_structure_types": [ + "payment_allocation", + "secured_debt", + "succession_notice_lien_asset_defense", + "land_use_gain", + "변제충당", + "변제충당_주장", + "event", + "준법률행위(quasi-legal_acts)", + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:005" + ], + "evidence_indexes": [ + "E-007", + "E-011" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-012": { + "issue_cluster_id": "IC-012", + "issue_key": "hard:B1:001", + "issue_type": "B1:001", + "source_bo_ids": [ + "bh2" + ], + "candidate_structure_types": [ + "money_claim", + "물품대금", + "매매", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:001" + ], + "evidence_indexes": [ + "E-021", + "E-027" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-013": { + "issue_cluster_id": "IC-013", + "issue_key": "hard:B2:007", + "issue_type": "B2:007", + "source_bo_ids": [ + "bh20" + ], + "candidate_structure_types": [ + "registry_invalidity", + "secured_debt", + "요구", + "거절", + "event", + "준법률행위(quasi-legal_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B2:007" + ], + "evidence_indexes": [ + "E-029" + ], + "asset_cluster_ids": [ + "AC-004" + ], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-014": { + "issue_cluster_id": "IC-014", + "issue_key": "hard:B3:002", + "issue_type": "B3:002", + "source_bo_ids": [ + "bh21" + ], + "candidate_structure_types": [ + "valuation", + "land_use_gain", + "임대료", + "차임", + "event", + "사실행위(factual_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:002" + ], + "evidence_indexes": [ + "E-005" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006" + ], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-015": { + "issue_cluster_id": "IC-015", + "issue_key": "hard:B3:004", + "issue_type": "B3:004", + "source_bo_ids": [ + "bh22" + ], + "candidate_structure_types": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "사용", + "state", + "사실행위(factual_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:004" + ], + "evidence_indexes": [ + "E-006" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-007", + "AC-008" + ], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-016": { + "issue_cluster_id": "IC-016", + "issue_key": "hard:B3:001", + "issue_type": "B3:001", + "source_bo_ids": [ + "bh23" + ], + "candidate_structure_types": [ + "valuation", + "land_use_gain", + "state", + "사실행위(factual_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:001" + ], + "evidence_indexes": [ + "E-004" + ], + "asset_cluster_ids": [ + "AC-009", + "AC-010" + ], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-017": { + "issue_cluster_id": "IC-017", + "issue_key": "hard:B3:005", + "issue_type": "B3:005", + "source_bo_ids": [ + "bh24" + ], + "candidate_structure_types": [ + "possession", + "secured_debt", + "land_use_gain", + "거주", + "succession_notice_lien_asset_defense", + "수익", + "state", + "사실행위(factual_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:005" + ], + "evidence_indexes": [ + "E-011" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-012" + ], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-018": { + "issue_cluster_id": "IC-018", + "issue_key": "hard:B3:003", + "issue_type": "B3:003", + "source_bo_ids": [ + "bh25" + ], + "candidate_structure_types": [ + "valuation", + "land_use_gain", + "차임", + "state", + "사실행위(factual_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:003" + ], + "evidence_indexes": [ + "E-023" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-013" + ], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-019": { + "issue_cluster_id": "IC-019", + "issue_key": "hard:B3:006", + "issue_type": "B3:006", + "source_bo_ids": [ + "bh26" + ], + "candidate_structure_types": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "운영", + "사업자등록", + "state", + "사실행위(factual_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B3:006" + ], + "evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-020": { + "issue_cluster_id": "IC-020", + "issue_key": "hard:B4:002", + "issue_type": "B4:002", + "source_bo_ids": [ + "bh27" + ], + "candidate_structure_types": [ + "fraudulent_transfer", + "매매계약", + "소유권이전", + "책임재산감소", + "매매", + "event", + "법률행위(legal_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:002" + ], + "evidence_indexes": [ + "E-014" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-021": { + "issue_cluster_id": "IC-021", + "issue_key": "hard:B4:001", + "issue_type": "B4:001", + "source_bo_ids": [ + "bh28" + ], + "candidate_structure_types": [ + "preserved_claim", + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:001" + ], + "evidence_indexes": [ + "E-024" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-022": { + "issue_cluster_id": "IC-022", + "issue_key": "hard:B4:004", + "issue_type": "B4:004", + "source_bo_ids": [ + "bh29" + ], + "candidate_structure_types": [ + "value_compensation", + "valuation", + "actio_remedy_or_cap", + "원물반환불능", + "secured_debt", + "registry_invalidity", + "event", + "준법률행위(quasi-legal_acts)", + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:004" + ], + "evidence_indexes": [ + "E-014" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-002" + ], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-023": { + "issue_cluster_id": "IC-023", + "issue_key": "hard:B1:006", + "issue_type": "B1:006", + "source_bo_ids": [ + "bh3" + ], + "candidate_structure_types": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:006" + ], + "evidence_indexes": [ + "E-024" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-024": { + "issue_cluster_id": "IC-024", + "issue_key": "hard:B4:003", + "issue_type": "B4:003", + "source_bo_ids": [ + "bh30" + ], + "candidate_structure_types": [ + "insolvency_check", + "fraudulent_transfer", + "무자력", + "책임재산", + "valuation", + "state", + "사실행위(factual_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:003" + ], + "evidence_indexes": [ + "E-004" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-020", + "AC-002", + "AC-021" + ], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-025": { + "issue_cluster_id": "IC-025", + "issue_key": "hard:B4:005", + "issue_type": "B4:005", + "source_bo_ids": [ + "bh31" + ], + "candidate_structure_types": [ + "defense_assertion", + "defense_rebuttal_matrix_bo", + "fraudulent_transfer", + "명의신탁", + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B4:005" + ], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-026": { + "issue_cluster_id": "IC-026", + "issue_key": "hard:B5:002", + "issue_type": "B5:002", + "source_bo_ids": [ + "bh32" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상속개시", + "event", + "사실행위(factual_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:002" + ], + "evidence_indexes": [ + "E-013" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-027": { + "issue_cluster_id": "IC-027", + "issue_key": "hard:B5:003", + "issue_type": "B5:003", + "source_bo_ids": [ + "bh33" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상속포기", + "event", + "사실행위(factual_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:003" + ], + "evidence_indexes": [ + "E-001", + "E-002" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-028": { + "issue_cluster_id": "IC-028", + "issue_key": "hard:B5:009", + "issue_type": "B5:009", + "source_bo_ids": [ + "bh34" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:009" + ], + "evidence_indexes": [ + "E-027" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-029": { + "issue_cluster_id": "IC-029", + "issue_key": "hard:B5:004", + "issue_type": "B5:004", + "source_bo_ids": [ + "bh35" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal_acts)", + "fraudulent_transfer" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:004" + ], + "evidence_indexes": [ + "E-008", + "E-010" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-030": { + "issue_cluster_id": "IC-030", + "issue_key": "hard:B5:007", + "issue_type": "B5:007", + "source_bo_ids": [ + "bh36" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)", + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:007" + ], + "evidence_indexes": [ + "E-011", + "E-019" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-031": { + "issue_cluster_id": "IC-031", + "issue_key": "hard:B5:010", + "issue_type": "B5:010", + "source_bo_ids": [ + "bh37" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:010" + ], + "evidence_indexes": [ + "E-028" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-032": { + "issue_cluster_id": "IC-032", + "issue_key": "hard:B5:006", + "issue_type": "B5:006", + "source_bo_ids": [ + "bh38" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:006" + ], + "evidence_indexes": [ + "E-010" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-033": { + "issue_cluster_id": "IC-033", + "issue_key": "hard:B5:005", + "issue_type": "B5:005", + "source_bo_ids": [ + "bh39" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:005" + ], + "evidence_indexes": [ + "E-009" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-034": { + "issue_cluster_id": "IC-034", + "issue_key": "hard:B1:007", + "issue_type": "B1:007", + "source_bo_ids": [ + "bh4" + ], + "candidate_structure_types": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:007" + ], + "evidence_indexes": [ + "E-024" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-035": { + "issue_cluster_id": "IC-035", + "issue_key": "hard:B5:012", + "issue_type": "B5:012", + "source_bo_ids": [ + "bh40" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:012" + ], + "evidence_indexes": [ + "E-030" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-036": { + "issue_cluster_id": "IC-036", + "issue_key": "hard:B5:011", + "issue_type": "B5:011", + "source_bo_ids": [ + "bh41" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)", + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:011" + ], + "evidence_indexes": [ + "E-029" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-037": { + "issue_cluster_id": "IC-037", + "issue_key": "hard:B5:008", + "issue_type": "B5:008", + "source_bo_ids": [ + "bh42" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)", + "registry_invalidity" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:008" + ], + "evidence_indexes": [ + "E-012" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-038": { + "issue_cluster_id": "IC-038", + "issue_key": "hard:B5:001", + "issue_type": "B5:001", + "source_bo_ids": [ + "bh43" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:001" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "asset_cluster_ids": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-039": { + "issue_cluster_id": "IC-039", + "issue_key": "hard:B5:013", + "issue_type": "B5:013", + "source_bo_ids": [ + "bh44" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)", + "secured_debt" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:013" + ], + "evidence_indexes": [ + "E-011" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-040": { + "issue_cluster_id": "IC-040", + "issue_key": "hard:B5:014", + "issue_type": "B5:014", + "source_bo_ids": [ + "bh45" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:014" + ], + "evidence_indexes": [ + "E-006" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-041": { + "issue_cluster_id": "IC-041", + "issue_key": "hard:B5:015", + "issue_type": "B5:015", + "source_bo_ids": [ + "bh46" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:015" + ], + "evidence_indexes": [ + "E-018", + "E-022" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-042": { + "issue_cluster_id": "IC-042", + "issue_key": "hard:B5:017", + "issue_type": "B5:017", + "source_bo_ids": [ + "bh47" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상계", + "state", + "소송행위(litigation_acts)", + "money_claim" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:017" + ], + "evidence_indexes": [ + "E-010" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-043": { + "issue_cluster_id": "IC-043", + "issue_key": "hard:B5:018", + "issue_type": "B5:018", + "source_bo_ids": [ + "bh48" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:018" + ], + "evidence_indexes": [ + "E-011" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-044": { + "issue_cluster_id": "IC-044", + "issue_key": "hard:B5:016", + "issue_type": "B5:016", + "source_bo_ids": [ + "bh49" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:016" + ], + "evidence_indexes": [ + "E-009" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-045": { + "issue_cluster_id": "IC-045", + "issue_key": "hard:B1:008", + "issue_type": "B1:008", + "source_bo_ids": [ + "bh5" + ], + "candidate_structure_types": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:008" + ], + "evidence_indexes": [ + "E-025" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-046": { + "issue_cluster_id": "IC-046", + "issue_key": "hard:B5:019", + "issue_type": "B5:019", + "source_bo_ids": [ + "bh50" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)", + "registry_invalidity" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B5:019" + ], + "evidence_indexes": [ + "E-012" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-047": { + "issue_cluster_id": "IC-047", + "issue_key": "hard:B1:009", + "issue_type": "B1:009", + "source_bo_ids": [ + "bh6" + ], + "candidate_structure_types": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:009" + ], + "evidence_indexes": [ + "E-026" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-048": { + "issue_cluster_id": "IC-048", + "issue_key": "hard:B1:004", + "issue_type": "B1:004", + "source_bo_ids": [ + "bh7" + ], + "candidate_structure_types": [ + "money_claim", + "영업양도", + "commercial_successor", + "event", + "법률행위(legal_acts)" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:004" + ], + "evidence_indexes": [ + "E-020" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-049": { + "issue_cluster_id": "IC-049", + "issue_key": "hard:B1:002", + "issue_type": "B1:002", + "source_bo_ids": [ + "bh8" + ], + "candidate_structure_types": [ + "money_claim", + "채무승인", + "변제기_유예", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:002" + ], + "evidence_indexes": [ + "E-021" + ], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + }, + "IC-050": { + "issue_cluster_id": "IC-050", + "issue_key": "hard:B1:010", + "issue_type": "B1:010", + "source_bo_ids": [ + "bh9" + ], + "candidate_structure_types": [ + "money_claim", + "변제", + "법정변제충당", + "event", + "사실행위(factual_acts)", + "commercial_successor" + ], + "claim_group_ids": [], + "liability_group_ids": [ + "B1:010" + ], + "evidence_indexes": [], + "asset_cluster_ids": [], + "basis_fields": [ + "liability_group_ids" + ], + "review_required": true + } + }, + "asset_cluster_seed_map": { + "AC-001": { + "asset_cluster_id": "AC-001", + "canonical_asset_label": "성수동 대지", + "asset_kind": "real_property", + "member_asset_mentions": [ + "성수동 대지" + ], + "source_bo_ids": [ + "bh14", + "bh17" + ], + "evidence_indexes": [ + "E-013", + "E-025", + "E-026", + "E-012" + ], + "issue_cluster_ids": [ + "IC-006", + "IC-009" + ], + "candidate_structure_types": [ + "secured_debt", + "상속", + "증여", + "(근)저당권_설정_계약", + "event", + "법률행위(legal_acts)", + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "임의경매", + "공신력", + "registry_invalidity", + "소송행위(litigation_acts)" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": false + }, + "AC-002": { + "asset_cluster_id": "AC-002", + "canonical_asset_label": "신림동 상가", + "asset_kind": "real_property", + "member_asset_mentions": [ + "신림동 상가", + "2021-08-01", + "2022-04-18", + "2024-10-17", + "포천시 임야" + ], + "source_bo_ids": [ + "bh15", + "bh27", + "bh29", + "bh30", + "bh31" + ], + "evidence_indexes": [ + "E-010", + "E-014", + "E-004", + "E-003" + ], + "issue_cluster_ids": [ + "IC-007", + "IC-020", + "IC-022", + "IC-024", + "IC-025" + ], + "candidate_structure_types": [ + "secured_debt", + "mortgage_extinguishment", + "registry_invalidity", + "상계", + "event", + "법률행위(legal_acts)", + "fraudulent_transfer", + "매매계약", + "소유권이전", + "책임재산감소", + "매매", + "value_compensation", + "valuation", + "actio_remedy_or_cap", + "원물반환불능", + "준법률행위(quasi-legal_acts)", + "insolvency_check", + "무자력", + "책임재산", + "state", + "사실행위(factual_acts)", + "defense_assertion", + "defense_rebuttal_matrix_bo", + "명의신탁", + "succession_notice_lien_asset_defense", + "소송행위(litigation_acts)" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "actio_case_signals.target_property_candidates" + ], + "review_required": false + }, + "AC-003": { + "asset_cluster_id": "AC-003", + "canonical_asset_label": "평택시 빌라", + "asset_kind": "unknown", + "member_asset_mentions": [ + "평택시 빌라" + ], + "source_bo_ids": [ + "bh16", + "bh19" + ], + "evidence_indexes": [ + "E-015", + "E-007", + "E-011" + ], + "issue_cluster_ids": [ + "IC-008", + "IC-011" + ], + "candidate_structure_types": [ + "secured_debt", + "소유권보존", + "소유권이전", + "가압류", + "registry_invalidity", + "event", + "법률행위(legal_acts)", + "payment_allocation", + "succession_notice_lien_asset_defense", + "land_use_gain", + "변제충당", + "변제충당_주장", + "준법률행위(quasi-legal_acts)", + "money_claim" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": false + }, + "AC-004": { + "asset_cluster_id": "AC-004", + "canonical_asset_label": "흑석동 상가", + "asset_kind": "real_property", + "member_asset_mentions": [ + "흑석동 상가" + ], + "source_bo_ids": [ + "bh20" + ], + "evidence_indexes": [ + "E-029" + ], + "issue_cluster_ids": [ + "IC-013" + ], + "candidate_structure_types": [ + "registry_invalidity", + "secured_debt", + "요구", + "거절", + "event", + "준법률행위(quasi-legal_acts)" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": false + }, + "AC-005": { + "asset_cluster_id": "AC-005", + "canonical_asset_label": "SCH-016-01", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-01", + "성수동 256 대", + "SCH-016-02", + "성수동 256 대지 및 지상 건물" + ], + "source_bo_ids": [ + "bh21", + "bh22" + ], + "evidence_indexes": [ + "E-005", + "E-006" + ], + "issue_cluster_ids": [ + "IC-014", + "IC-015" + ], + "candidate_structure_types": [ + "valuation", + "land_use_gain", + "임대료", + "차임", + "event", + "사실행위(factual_acts)", + "possession", + "succession_notice_lien_asset_defense", + "사용", + "state" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": false + }, + "AC-006": { + "asset_cluster_id": "AC-006", + "canonical_asset_label": "SCH-016-01", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-01", + "성수동 256 대" + ], + "source_bo_ids": [ + "bh21" + ], + "evidence_indexes": [ + "E-005" + ], + "issue_cluster_ids": [ + "IC-014" + ], + "candidate_structure_types": [ + "valuation", + "land_use_gain", + "임대료", + "차임", + "event", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": false + }, + "AC-007": { + "asset_cluster_id": "AC-007", + "canonical_asset_label": "SCH-016-01", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-01", + "SCH-016-02", + "성수동 256 대지 및 지상 건물" + ], + "source_bo_ids": [ + "bh22" + ], + "evidence_indexes": [ + "E-006" + ], + "issue_cluster_ids": [ + "IC-015" + ], + "candidate_structure_types": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "사용", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": false + }, + "AC-008": { + "asset_cluster_id": "AC-008", + "canonical_asset_label": "SCH-016-01", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-01", + "SCH-016-02", + "성수동 256 대지 및 지상 건물" + ], + "source_bo_ids": [ + "bh22" + ], + "evidence_indexes": [ + "E-006" + ], + "issue_cluster_ids": [ + "IC-015" + ], + "candidate_structure_types": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "사용", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": false + }, + "AC-009": { + "asset_cluster_id": "AC-009", + "canonical_asset_label": "SCH-016-03", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-03", + "신림로 상가, 포천시 임야" + ], + "source_bo_ids": [ + "bh23" + ], + "evidence_indexes": [ + "E-004" + ], + "issue_cluster_ids": [ + "IC-016" + ], + "candidate_structure_types": [ + "valuation", + "land_use_gain", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": false + }, + "AC-010": { + "asset_cluster_id": "AC-010", + "canonical_asset_label": "SCH-016-03", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-03", + "신림로 상가, 포천시 임야" + ], + "source_bo_ids": [ + "bh23" + ], + "evidence_indexes": [ + "E-004" + ], + "issue_cluster_ids": [ + "IC-016" + ], + "candidate_structure_types": [ + "valuation", + "land_use_gain", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": false + }, + "AC-011": { + "asset_cluster_id": "AC-011", + "canonical_asset_label": "SCH-016-04", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-04", + "평택시 서정3길 123 서정빌라 101호", + "서정빌라 101호" + ], + "source_bo_ids": [ + "bh24", + "bh25" + ], + "evidence_indexes": [ + "E-011", + "E-023" + ], + "issue_cluster_ids": [ + "IC-017", + "IC-018" + ], + "candidate_structure_types": [ + "possession", + "secured_debt", + "land_use_gain", + "거주", + "succession_notice_lien_asset_defense", + "수익", + "state", + "사실행위(factual_acts)", + "valuation", + "차임" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": false + }, + "AC-012": { + "asset_cluster_id": "AC-012", + "canonical_asset_label": "SCH-016-04", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-04", + "평택시 서정3길 123 서정빌라 101호" + ], + "source_bo_ids": [ + "bh24" + ], + "evidence_indexes": [ + "E-011" + ], + "issue_cluster_ids": [ + "IC-017" + ], + "candidate_structure_types": [ + "possession", + "secured_debt", + "land_use_gain", + "거주", + "succession_notice_lien_asset_defense", + "수익", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": false + }, + "AC-013": { + "asset_cluster_id": "AC-013", + "canonical_asset_label": "SCH-016-04", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-04", + "서정빌라 101호" + ], + "source_bo_ids": [ + "bh25" + ], + "evidence_indexes": [ + "E-023" + ], + "issue_cluster_ids": [ + "IC-018" + ], + "candidate_structure_types": [ + "valuation", + "land_use_gain", + "차임", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": false + }, + "AC-014": { + "asset_cluster_id": "AC-014", + "canonical_asset_label": "SCH-016-05", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-05", + "흑석동 201호" + ], + "source_bo_ids": [ + "bh26" + ], + "evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "issue_cluster_ids": [ + "IC-019" + ], + "candidate_structure_types": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "운영", + "사업자등록", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": false + }, + "AC-015": { + "asset_cluster_id": "AC-015", + "canonical_asset_label": "SCH-016-05", + "asset_kind": "unknown", + "member_asset_mentions": [ + "SCH-016-05", + "흑석동 201호" + ], + "source_bo_ids": [ + "bh26" + ], + "evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "issue_cluster_ids": [ + "IC-019" + ], + "candidate_structure_types": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "운영", + "사업자등록", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_bindings", + "BO.extensions.domain_payload.asset_binding_hint" + ], + "review_required": false + }, + "AC-016": { + "asset_cluster_id": "AC-016", + "canonical_asset_label": "신림동 상가", + "asset_kind": "unknown", + "member_asset_mentions": [ + "신림동 상가", + "2021-08-01", + "2022-04-18", + "2024-10-17", + "포천시 임야" + ], + "source_bo_ids": [ + "bh27", + "bh29", + "bh30", + "bh31" + ], + "evidence_indexes": [ + "E-014", + "E-004", + "E-003", + "E-010" + ], + "issue_cluster_ids": [ + "IC-020", + "IC-022", + "IC-024", + "IC-025" + ], + "candidate_structure_types": [ + "fraudulent_transfer", + "매매계약", + "소유권이전", + "책임재산감소", + "매매", + "event", + "법률행위(legal_acts)", + "value_compensation", + "valuation", + "actio_remedy_or_cap", + "원물반환불능", + "secured_debt", + "registry_invalidity", + "준법률행위(quasi-legal_acts)", + "insolvency_check", + "무자력", + "책임재산", + "state", + "사실행위(factual_acts)", + "defense_assertion", + "defense_rebuttal_matrix_bo", + "명의신탁", + "succession_notice_lien_asset_defense", + "소송행위(litigation_acts)" + ], + "basis_fields": [ + "actio_case_signals.target_property_candidates", + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": false + }, + "AC-017": { + "asset_cluster_id": "AC-017", + "canonical_asset_label": "신림동 상가", + "asset_kind": "unknown", + "member_asset_mentions": [ + "신림동 상가", + "2021-08-01", + "2022-04-18", + "2024-10-17" + ], + "source_bo_ids": [ + "bh29" + ], + "evidence_indexes": [ + "E-014" + ], + "issue_cluster_ids": [ + "IC-022" + ], + "candidate_structure_types": [ + "value_compensation", + "valuation", + "actio_remedy_or_cap", + "원물반환불능", + "secured_debt", + "registry_invalidity", + "event", + "준법률행위(quasi-legal_acts)", + "fraudulent_transfer" + ], + "basis_fields": [ + "actio_case_signals.target_property_candidates", + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": false + }, + "AC-018": { + "asset_cluster_id": "AC-018", + "canonical_asset_label": "신림동 상가", + "asset_kind": "unknown", + "member_asset_mentions": [ + "신림동 상가", + "2021-08-01", + "2022-04-18", + "2024-10-17" + ], + "source_bo_ids": [ + "bh29" + ], + "evidence_indexes": [ + "E-014" + ], + "issue_cluster_ids": [ + "IC-022" + ], + "candidate_structure_types": [ + "value_compensation", + "valuation", + "actio_remedy_or_cap", + "원물반환불능", + "secured_debt", + "registry_invalidity", + "event", + "준법률행위(quasi-legal_acts)", + "fraudulent_transfer" + ], + "basis_fields": [ + "actio_case_signals.target_property_candidates", + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": false + }, + "AC-019": { + "asset_cluster_id": "AC-019", + "canonical_asset_label": "신림동 상가", + "asset_kind": "unknown", + "member_asset_mentions": [ + "신림동 상가", + "2021-08-01", + "2022-04-18", + "2024-10-17" + ], + "source_bo_ids": [ + "bh29" + ], + "evidence_indexes": [ + "E-014" + ], + "issue_cluster_ids": [ + "IC-022" + ], + "candidate_structure_types": [ + "value_compensation", + "valuation", + "actio_remedy_or_cap", + "원물반환불능", + "secured_debt", + "registry_invalidity", + "event", + "준법률행위(quasi-legal_acts)", + "fraudulent_transfer" + ], + "basis_fields": [ + "actio_case_signals.target_property_candidates", + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": false + }, + "AC-020": { + "asset_cluster_id": "AC-020", + "canonical_asset_label": "신림동 상가", + "asset_kind": "unknown", + "member_asset_mentions": [ + "신림동 상가", + "포천시 임야" + ], + "source_bo_ids": [ + "bh30" + ], + "evidence_indexes": [ + "E-004" + ], + "issue_cluster_ids": [ + "IC-024" + ], + "candidate_structure_types": [ + "insolvency_check", + "fraudulent_transfer", + "무자력", + "책임재산", + "valuation", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "actio_case_signals.target_property_candidates", + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": false + }, + "AC-021": { + "asset_cluster_id": "AC-021", + "canonical_asset_label": "신림동 상가", + "asset_kind": "unknown", + "member_asset_mentions": [ + "신림동 상가", + "포천시 임야" + ], + "source_bo_ids": [ + "bh30" + ], + "evidence_indexes": [ + "E-004" + ], + "issue_cluster_ids": [ + "IC-024" + ], + "candidate_structure_types": [ + "insolvency_check", + "fraudulent_transfer", + "무자력", + "책임재산", + "valuation", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "actio_case_signals.target_property_candidates", + "BO.extensions.domain_payload.asset_bindings" + ], + "review_required": false + }, + "AC-022": { + "asset_cluster_id": "AC-022", + "canonical_asset_label": "성수동 대지", + "asset_kind": "real_property", + "member_asset_mentions": [ + "성수동 대지", + "서울 성동구 성수동 256", + "1. 서울 성동구 성수동 256 대 330㎡", + "성수동 건물", + "서울 성동구 성수동 256 상가", + "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "신림동 상가", + "서울 관악구 신림로 115", + "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123", + "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "흑석동 상가", + "서울 동작구 흑석로 80", + "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물" + ], + "source_bo_ids": [ + "bh43" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_alias" + ], + "review_required": false + }, + "AC-023": { + "asset_cluster_id": "AC-023", + "canonical_asset_label": "성수동 대지", + "asset_kind": "real_property", + "member_asset_mentions": [ + "성수동 대지", + "서울 성동구 성수동 256", + "1. 서울 성동구 성수동 256 대 330㎡", + "성수동 건물", + "서울 성동구 성수동 256 상가", + "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "신림동 상가", + "서울 관악구 신림로 115", + "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123", + "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "흑석동 상가", + "서울 동작구 흑석로 80", + "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물" + ], + "source_bo_ids": [ + "bh43" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_alias" + ], + "review_required": false + }, + "AC-024": { + "asset_cluster_id": "AC-024", + "canonical_asset_label": "성수동 대지", + "asset_kind": "real_property", + "member_asset_mentions": [ + "성수동 대지", + "서울 성동구 성수동 256", + "1. 서울 성동구 성수동 256 대 330㎡", + "성수동 건물", + "서울 성동구 성수동 256 상가", + "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "신림동 상가", + "서울 관악구 신림로 115", + "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123", + "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "흑석동 상가", + "서울 동작구 흑석로 80", + "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물" + ], + "source_bo_ids": [ + "bh43" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_alias" + ], + "review_required": false + }, + "AC-025": { + "asset_cluster_id": "AC-025", + "canonical_asset_label": "성수동 대지", + "asset_kind": "real_property", + "member_asset_mentions": [ + "성수동 대지", + "서울 성동구 성수동 256", + "1. 서울 성동구 성수동 256 대 330㎡", + "성수동 건물", + "서울 성동구 성수동 256 상가", + "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "신림동 상가", + "서울 관악구 신림로 115", + "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123", + "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "흑석동 상가", + "서울 동작구 흑석로 80", + "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물" + ], + "source_bo_ids": [ + "bh43" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_alias" + ], + "review_required": false + }, + "AC-026": { + "asset_cluster_id": "AC-026", + "canonical_asset_label": "성수동 대지", + "asset_kind": "real_property", + "member_asset_mentions": [ + "성수동 대지", + "서울 성동구 성수동 256", + "1. 서울 성동구 성수동 256 대 330㎡", + "성수동 건물", + "서울 성동구 성수동 256 상가", + "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "신림동 상가", + "서울 관악구 신림로 115", + "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123", + "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "흑석동 상가", + "서울 동작구 흑석로 80", + "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물" + ], + "source_bo_ids": [ + "bh43" + ], + "evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "BO.extensions.domain_payload.asset_alias" + ], + "review_required": false + } + }, + "multi_asset_bundles": [ + { + "bundle_ref": "bundle:bh21", + "bundle_type": "multi_asset_bundle", + "source_bo_ids": [ + "bh21" + ], + "member_asset_labels": [ + "SCH-016-01", + "성수동 256 대" + ], + "review_required": true + }, + { + "bundle_ref": "bundle:bh22", + "bundle_type": "multi_asset_bundle", + "source_bo_ids": [ + "bh22" + ], + "member_asset_labels": [ + "SCH-016-01", + "SCH-016-02", + "성수동 256 대지 및 지상 건물" + ], + "review_required": false + }, + { + "bundle_ref": "bundle:bh23", + "bundle_type": "multi_asset_bundle", + "source_bo_ids": [ + "bh23" + ], + "member_asset_labels": [ + "SCH-016-03", + "신림로 상가, 포천시 임야" + ], + "review_required": true + }, + { + "bundle_ref": "bundle:bh24", + "bundle_type": "multi_asset_bundle", + "source_bo_ids": [ + "bh24" + ], + "member_asset_labels": [ + "SCH-016-04", + "평택시 서정3길 123 서정빌라 101호" + ], + "review_required": true + }, + { + "bundle_ref": "bundle:bh25", + "bundle_type": "multi_asset_bundle", + "source_bo_ids": [ + "bh25" + ], + "member_asset_labels": [ + "SCH-016-04", + "서정빌라 101호" + ], + "review_required": true + }, + { + "bundle_ref": "bundle:bh26", + "bundle_type": "multi_asset_bundle", + "source_bo_ids": [ + "bh26" + ], + "member_asset_labels": [ + "SCH-016-05", + "흑석동 201호" + ], + "review_required": true + }, + { + "bundle_ref": "bundle:bh29", + "bundle_type": "multi_asset_bundle", + "source_bo_ids": [ + "bh29" + ], + "member_asset_labels": [ + "신림동 상가", + "2021-08-01", + "2022-04-18", + "2024-10-17" + ], + "review_required": false + }, + { + "bundle_ref": "bundle:bh30", + "bundle_type": "multi_asset_bundle", + "source_bo_ids": [ + "bh30" + ], + "member_asset_labels": [ + "신림동 상가", + "포천시 임야" + ], + "review_required": true + }, + { + "bundle_ref": "bundle:bh43", + "bundle_type": "multi_asset_bundle", + "source_bo_ids": [ + "bh43" + ], + "member_asset_labels": [ + "성수동 대지", + "서울 성동구 성수동 256", + "1. 서울 성동구 성수동 256 대 330㎡", + "성수동 건물", + "서울 성동구 성수동 256 상가", + "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "신림동 상가", + "서울 관악구 신림로 115", + "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123", + "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "흑석동 상가", + "서울 동작구 흑석로 80", + "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물" + ], + "review_required": false + } + ], + "candidate_structure_type_index_seed": { + "money_claim": { + "candidate_structure_type": "money_claim", + "seed_ids": [ + "LESS-001", + "LESS-002", + "LESS-003", + "LESS-004", + "LESS-005", + "LESS-011", + "LESS-012", + "LESS-021", + "LESS-023", + "LESS-034", + "LESS-036", + "LESS-042", + "LESS-045", + "LESS-047", + "LESS-048", + "LESS-049", + "LESS-050" + ], + "source_bo_ids": [ + "bh1", + "bh10", + "bh11", + "bh12", + "bh13", + "bh19", + "bh2", + "bh28", + "bh3", + "bh4", + "bh41", + "bh47", + "bh5", + "bh6", + "bh7", + "bh8", + "bh9" + ], + "issue_cluster_ids": [ + "IC-001", + "IC-002", + "IC-003", + "IC-004", + "IC-005", + "IC-011", + "IC-012", + "IC-021", + "IC-023", + "IC-034", + "IC-036", + "IC-042", + "IC-045", + "IC-047", + "IC-048", + "IC-049", + "IC-050" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_indexes": [ + "E-007", + "E-003", + "E-010", + "E-027", + "E-028", + "E-011", + "E-021", + "E-024", + "E-029", + "E-025", + "E-026", + "E-020" + ], + "downstream_fact_roles": [ + "claim_chain_ref" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "도급": { + "candidate_structure_type": "도급", + "seed_ids": [ + "LESS-001" + ], + "source_bo_ids": [ + "bh1" + ], + "issue_cluster_ids": [ + "IC-001" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-007" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "공사대금": { + "candidate_structure_type": "공사대금", + "seed_ids": [ + "LESS-001" + ], + "source_bo_ids": [ + "bh1" + ], + "issue_cluster_ids": [ + "IC-001" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-007" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "event": { + "candidate_structure_type": "event", + "seed_ids": [ + "LESS-001", + "LESS-002", + "LESS-003", + "LESS-004", + "LESS-006", + "LESS-007", + "LESS-008", + "LESS-009", + "LESS-010", + "LESS-011", + "LESS-012", + "LESS-013", + "LESS-014", + "LESS-020", + "LESS-021", + "LESS-022", + "LESS-023", + "LESS-026", + "LESS-027", + "LESS-028", + "LESS-029", + "LESS-030", + "LESS-031", + "LESS-032", + "LESS-033", + "LESS-034", + "LESS-035", + "LESS-036", + "LESS-037", + "LESS-045", + "LESS-047", + "LESS-048", + "LESS-049", + "LESS-050" + ], + "source_bo_ids": [ + "bh1", + "bh10", + "bh11", + "bh12", + "bh14", + "bh15", + "bh16", + "bh17", + "bh18", + "bh19", + "bh2", + "bh20", + "bh21", + "bh27", + "bh28", + "bh29", + "bh3", + "bh32", + "bh33", + "bh34", + "bh35", + "bh36", + "bh37", + "bh38", + "bh39", + "bh4", + "bh40", + "bh41", + "bh42", + "bh5", + "bh6", + "bh7", + "bh8", + "bh9" + ], + "issue_cluster_ids": [ + "IC-001", + "IC-002", + "IC-003", + "IC-004", + "IC-006", + "IC-007", + "IC-008", + "IC-009", + "IC-010", + "IC-011", + "IC-012", + "IC-013", + "IC-014", + "IC-020", + "IC-021", + "IC-022", + "IC-023", + "IC-026", + "IC-027", + "IC-028", + "IC-029", + "IC-030", + "IC-031", + "IC-032", + "IC-033", + "IC-034", + "IC-035", + "IC-036", + "IC-037", + "IC-045", + "IC-047", + "IC-048", + "IC-049", + "IC-050" + ], + "asset_cluster_ids": [ + "AC-001", + "AC-002", + "AC-003", + "AC-004", + "AC-005", + "AC-006", + "AC-016", + "AC-017", + "AC-018", + "AC-019" + ], + "evidence_indexes": [ + "E-007", + "E-003", + "E-010", + "E-027", + "E-028", + "E-013", + "E-025", + "E-026", + "E-014", + "E-015", + "E-012", + "E-011", + "E-021", + "E-029", + "E-005", + "E-024", + "E-001", + "E-002", + "E-008", + "E-019", + "E-009", + "E-030", + "E-020" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "법률행위(legal_acts)": { + "candidate_structure_type": "법률행위(legal_acts)", + "seed_ids": [ + "LESS-001", + "LESS-006", + "LESS-007", + "LESS-008", + "LESS-012", + "LESS-020", + "LESS-021", + "LESS-023", + "LESS-034", + "LESS-045", + "LESS-047", + "LESS-048", + "LESS-049" + ], + "source_bo_ids": [ + "bh1", + "bh14", + "bh15", + "bh16", + "bh2", + "bh27", + "bh28", + "bh3", + "bh4", + "bh5", + "bh6", + "bh7", + "bh8" + ], + "issue_cluster_ids": [ + "IC-001", + "IC-006", + "IC-007", + "IC-008", + "IC-012", + "IC-020", + "IC-021", + "IC-023", + "IC-034", + "IC-045", + "IC-047", + "IC-048", + "IC-049" + ], + "asset_cluster_ids": [ + "AC-001", + "AC-002", + "AC-003", + "AC-016" + ], + "evidence_indexes": [ + "E-007", + "E-013", + "E-025", + "E-026", + "E-010", + "E-014", + "E-015", + "E-021", + "E-027", + "E-024", + "E-020" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "commercial_successor": { + "candidate_structure_type": "commercial_successor", + "seed_ids": [ + "LESS-001", + "LESS-002", + "LESS-003", + "LESS-004", + "LESS-005", + "LESS-012", + "LESS-023", + "LESS-034", + "LESS-045", + "LESS-047", + "LESS-048", + "LESS-049", + "LESS-050" + ], + "source_bo_ids": [ + "bh1", + "bh10", + "bh11", + "bh12", + "bh13", + "bh2", + "bh3", + "bh4", + "bh5", + "bh6", + "bh7", + "bh8", + "bh9" + ], + "issue_cluster_ids": [ + "IC-001", + "IC-002", + "IC-003", + "IC-004", + "IC-005", + "IC-012", + "IC-023", + "IC-034", + "IC-045", + "IC-047", + "IC-048", + "IC-049", + "IC-050" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-007", + "E-003", + "E-010", + "E-027", + "E-028", + "E-011", + "E-021", + "E-024", + "E-025", + "E-026", + "E-020" + ], + "downstream_fact_roles": [ + "claim_chain_ref" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "상계": { + "candidate_structure_type": "상계", + "seed_ids": [ + "LESS-002", + "LESS-007", + "LESS-042" + ], + "source_bo_ids": [ + "bh10", + "bh15", + "bh47" + ], + "issue_cluster_ids": [ + "IC-002", + "IC-007", + "IC-042" + ], + "asset_cluster_ids": [ + "AC-002" + ], + "evidence_indexes": [ + "E-003", + "E-010", + "E-014" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "succession_notice_lien_asset_defense": { + "candidate_structure_type": "succession_notice_lien_asset_defense", + "seed_ids": [ + "LESS-002", + "LESS-003", + "LESS-004", + "LESS-011", + "LESS-015", + "LESS-017", + "LESS-019", + "LESS-025", + "LESS-026", + "LESS-027", + "LESS-028", + "LESS-029", + "LESS-030", + "LESS-031", + "LESS-032", + "LESS-033", + "LESS-035", + "LESS-036", + "LESS-037", + "LESS-038", + "LESS-039", + "LESS-040", + "LESS-041", + "LESS-042", + "LESS-043", + "LESS-044", + "LESS-046" + ], + "source_bo_ids": [ + "bh10", + "bh11", + "bh12", + "bh19", + "bh22", + "bh24", + "bh26", + "bh31", + "bh32", + "bh33", + "bh34", + "bh35", + "bh36", + "bh37", + "bh38", + "bh39", + "bh40", + "bh41", + "bh42", + "bh43", + "bh44", + "bh45", + "bh46", + "bh47", + "bh48", + "bh49", + "bh50" + ], + "issue_cluster_ids": [ + "IC-002", + "IC-003", + "IC-004", + "IC-011", + "IC-015", + "IC-017", + "IC-019", + "IC-025", + "IC-026", + "IC-027", + "IC-028", + "IC-029", + "IC-030", + "IC-031", + "IC-032", + "IC-033", + "IC-035", + "IC-036", + "IC-037", + "IC-038", + "IC-039", + "IC-040", + "IC-041", + "IC-042", + "IC-043", + "IC-044", + "IC-046" + ], + "asset_cluster_ids": [ + "AC-003", + "AC-005", + "AC-007", + "AC-008", + "AC-011", + "AC-012", + "AC-014", + "AC-015", + "AC-016", + "AC-002", + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "evidence_indexes": [ + "E-003", + "E-010", + "E-027", + "E-028", + "E-007", + "E-011", + "E-006", + "E-017", + "E-018", + "E-022", + "E-013", + "E-001", + "E-002", + "E-008", + "E-019", + "E-009", + "E-030", + "E-029", + "E-012", + "E-014", + "E-015", + "E-016" + ], + "downstream_fact_roles": [ + "linked_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "소송행위(litigation_acts)": { + "candidate_structure_type": "소송행위(litigation_acts)", + "seed_ids": [ + "LESS-002", + "LESS-009", + "LESS-025", + "LESS-042", + "LESS-043", + "LESS-044", + "LESS-046" + ], + "source_bo_ids": [ + "bh10", + "bh17", + "bh31", + "bh47", + "bh48", + "bh49", + "bh50" + ], + "issue_cluster_ids": [ + "IC-002", + "IC-009", + "IC-025", + "IC-042", + "IC-043", + "IC-044", + "IC-046" + ], + "asset_cluster_ids": [ + "AC-001", + "AC-016", + "AC-002" + ], + "evidence_indexes": [ + "E-003", + "E-010", + "E-012", + "E-013", + "E-011", + "E-009" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "이행최고": { + "candidate_structure_type": "이행최고", + "seed_ids": [ + "LESS-003" + ], + "source_bo_ids": [ + "bh11" + ], + "issue_cluster_ids": [ + "IC-003" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-027" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "최고": { + "candidate_structure_type": "최고", + "seed_ids": [ + "LESS-003", + "LESS-028", + "LESS-029" + ], + "source_bo_ids": [ + "bh11", + "bh34", + "bh35" + ], + "issue_cluster_ids": [ + "IC-003", + "IC-028", + "IC-029" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-027", + "E-008", + "E-010" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "준법률행위(quasi-legal_acts)": { + "candidate_structure_type": "준법률행위(quasi-legal_acts)", + "seed_ids": [ + "LESS-003", + "LESS-004", + "LESS-010", + "LESS-011", + "LESS-013", + "LESS-022", + "LESS-028", + "LESS-029", + "LESS-030", + "LESS-031", + "LESS-032", + "LESS-033", + "LESS-035", + "LESS-036", + "LESS-037" + ], + "source_bo_ids": [ + "bh11", + "bh12", + "bh18", + "bh19", + "bh20", + "bh29", + "bh34", + "bh35", + "bh36", + "bh37", + "bh38", + "bh39", + "bh40", + "bh41", + "bh42" + ], + "issue_cluster_ids": [ + "IC-003", + "IC-004", + "IC-010", + "IC-011", + "IC-013", + "IC-022", + "IC-028", + "IC-029", + "IC-030", + "IC-031", + "IC-032", + "IC-033", + "IC-035", + "IC-036", + "IC-037" + ], + "asset_cluster_ids": [ + "AC-003", + "AC-004", + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-002" + ], + "evidence_indexes": [ + "E-027", + "E-028", + "E-007", + "E-011", + "E-029", + "E-014", + "E-008", + "E-010", + "E-019", + "E-009", + "E-030", + "E-012" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "책임부인": { + "candidate_structure_type": "책임부인", + "seed_ids": [ + "LESS-004" + ], + "source_bo_ids": [ + "bh12" + ], + "issue_cluster_ids": [ + "IC-004" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-028" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "충당": { + "candidate_structure_type": "충당", + "seed_ids": [ + "LESS-005" + ], + "source_bo_ids": [ + "bh13" + ], + "issue_cluster_ids": [ + "IC-005" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-011" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "state": { + "candidate_structure_type": "state", + "seed_ids": [ + "LESS-005", + "LESS-015", + "LESS-016", + "LESS-017", + "LESS-018", + "LESS-019", + "LESS-024", + "LESS-025", + "LESS-038", + "LESS-039", + "LESS-040", + "LESS-041", + "LESS-042", + "LESS-043", + "LESS-044", + "LESS-046" + ], + "source_bo_ids": [ + "bh13", + "bh22", + "bh23", + "bh24", + "bh25", + "bh26", + "bh30", + "bh31", + "bh43", + "bh44", + "bh45", + "bh46", + "bh47", + "bh48", + "bh49", + "bh50" + ], + "issue_cluster_ids": [ + "IC-005", + "IC-015", + "IC-016", + "IC-017", + "IC-018", + "IC-019", + "IC-024", + "IC-025", + "IC-038", + "IC-039", + "IC-040", + "IC-041", + "IC-042", + "IC-043", + "IC-044", + "IC-046" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-007", + "AC-008", + "AC-009", + "AC-010", + "AC-011", + "AC-012", + "AC-013", + "AC-014", + "AC-015", + "AC-016", + "AC-020", + "AC-002", + "AC-021", + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "evidence_indexes": [ + "E-011", + "E-006", + "E-004", + "E-023", + "E-017", + "E-018", + "E-022", + "E-003", + "E-010", + "E-013", + "E-014", + "E-015", + "E-016", + "E-009", + "E-012" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "사실행위(factual_acts)": { + "candidate_structure_type": "사실행위(factual_acts)", + "seed_ids": [ + "LESS-005", + "LESS-014", + "LESS-015", + "LESS-016", + "LESS-017", + "LESS-018", + "LESS-019", + "LESS-024", + "LESS-026", + "LESS-027", + "LESS-038", + "LESS-039", + "LESS-040", + "LESS-041", + "LESS-050" + ], + "source_bo_ids": [ + "bh13", + "bh21", + "bh22", + "bh23", + "bh24", + "bh25", + "bh26", + "bh30", + "bh32", + "bh33", + "bh43", + "bh44", + "bh45", + "bh46", + "bh9" + ], + "issue_cluster_ids": [ + "IC-005", + "IC-014", + "IC-015", + "IC-016", + "IC-017", + "IC-018", + "IC-019", + "IC-024", + "IC-026", + "IC-027", + "IC-038", + "IC-039", + "IC-040", + "IC-041", + "IC-050" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006", + "AC-007", + "AC-008", + "AC-009", + "AC-010", + "AC-011", + "AC-012", + "AC-013", + "AC-014", + "AC-015", + "AC-016", + "AC-020", + "AC-002", + "AC-021", + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "evidence_indexes": [ + "E-011", + "E-005", + "E-006", + "E-004", + "E-023", + "E-017", + "E-018", + "E-022", + "E-013", + "E-001", + "E-002", + "E-014", + "E-015", + "E-016" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "secured_debt": { + "candidate_structure_type": "secured_debt", + "seed_ids": [ + "LESS-006", + "LESS-007", + "LESS-008", + "LESS-009", + "LESS-010", + "LESS-011", + "LESS-013", + "LESS-017", + "LESS-022", + "LESS-030", + "LESS-039" + ], + "source_bo_ids": [ + "bh14", + "bh15", + "bh16", + "bh17", + "bh18", + "bh19", + "bh20", + "bh24", + "bh29", + "bh36", + "bh44" + ], + "issue_cluster_ids": [ + "IC-006", + "IC-007", + "IC-008", + "IC-009", + "IC-010", + "IC-011", + "IC-013", + "IC-017", + "IC-022", + "IC-030", + "IC-039" + ], + "asset_cluster_ids": [ + "AC-001", + "AC-002", + "AC-003", + "AC-004", + "AC-011", + "AC-012", + "AC-016", + "AC-017", + "AC-018", + "AC-019" + ], + "evidence_indexes": [ + "E-013", + "E-025", + "E-026", + "E-010", + "E-014", + "E-015", + "E-012", + "E-007", + "E-011", + "E-029", + "E-019" + ], + "downstream_fact_roles": [ + "linked_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "상속": { + "candidate_structure_type": "상속", + "seed_ids": [ + "LESS-006" + ], + "source_bo_ids": [ + "bh14" + ], + "issue_cluster_ids": [ + "IC-006" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "증여": { + "candidate_structure_type": "증여", + "seed_ids": [ + "LESS-006" + ], + "source_bo_ids": [ + "bh14" + ], + "issue_cluster_ids": [ + "IC-006" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "(근)저당권_설정_계약": { + "candidate_structure_type": "(근)저당권_설정_계약", + "seed_ids": [ + "LESS-006" + ], + "source_bo_ids": [ + "bh14" + ], + "issue_cluster_ids": [ + "IC-006" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "mortgage_extinguishment": { + "candidate_structure_type": "mortgage_extinguishment", + "seed_ids": [ + "LESS-007" + ], + "source_bo_ids": [ + "bh15" + ], + "issue_cluster_ids": [ + "IC-007" + ], + "asset_cluster_ids": [ + "AC-002" + ], + "evidence_indexes": [ + "E-010", + "E-014" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "registry_invalidity": { + "candidate_structure_type": "registry_invalidity", + "seed_ids": [ + "LESS-007", + "LESS-008", + "LESS-009", + "LESS-013", + "LESS-022", + "LESS-037", + "LESS-046" + ], + "source_bo_ids": [ + "bh15", + "bh16", + "bh17", + "bh20", + "bh29", + "bh42", + "bh50" + ], + "issue_cluster_ids": [ + "IC-007", + "IC-008", + "IC-009", + "IC-013", + "IC-022", + "IC-037", + "IC-046" + ], + "asset_cluster_ids": [ + "AC-002", + "AC-003", + "AC-001", + "AC-004", + "AC-016", + "AC-017", + "AC-018", + "AC-019" + ], + "evidence_indexes": [ + "E-010", + "E-014", + "E-015", + "E-012", + "E-013", + "E-029" + ], + "downstream_fact_roles": [ + "linked_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "소유권보존": { + "candidate_structure_type": "소유권보존", + "seed_ids": [ + "LESS-008" + ], + "source_bo_ids": [ + "bh16" + ], + "issue_cluster_ids": [ + "IC-008" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_indexes": [ + "E-015" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "소유권이전": { + "candidate_structure_type": "소유권이전", + "seed_ids": [ + "LESS-008", + "LESS-020" + ], + "source_bo_ids": [ + "bh16", + "bh27" + ], + "issue_cluster_ids": [ + "IC-008", + "IC-020" + ], + "asset_cluster_ids": [ + "AC-003", + "AC-016", + "AC-002" + ], + "evidence_indexes": [ + "E-015", + "E-014" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "가압류": { + "candidate_structure_type": "가압류", + "seed_ids": [ + "LESS-008" + ], + "source_bo_ids": [ + "bh16" + ], + "issue_cluster_ids": [ + "IC-008" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_indexes": [ + "E-015" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "auction_invalidity": { + "candidate_structure_type": "auction_invalidity", + "seed_ids": [ + "LESS-009" + ], + "source_bo_ids": [ + "bh17" + ], + "issue_cluster_ids": [ + "IC-009" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_indexes": [ + "E-012", + "E-013" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "derivative_bank_mortgage_invalidity": { + "candidate_structure_type": "derivative_bank_mortgage_invalidity", + "seed_ids": [ + "LESS-009" + ], + "source_bo_ids": [ + "bh17" + ], + "issue_cluster_ids": [ + "IC-009" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_indexes": [ + "E-012", + "E-013" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "임의경매": { + "candidate_structure_type": "임의경매", + "seed_ids": [ + "LESS-009" + ], + "source_bo_ids": [ + "bh17" + ], + "issue_cluster_ids": [ + "IC-009" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_indexes": [ + "E-012", + "E-013" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "공신력": { + "candidate_structure_type": "공신력", + "seed_ids": [ + "LESS-009" + ], + "source_bo_ids": [ + "bh17" + ], + "issue_cluster_ids": [ + "IC-009" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "evidence_indexes": [ + "E-012", + "E-013" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "payment_allocation_uncertain": { + "candidate_structure_type": "payment_allocation_uncertain", + "seed_ids": [ + "LESS-010" + ], + "source_bo_ids": [ + "bh18" + ], + "issue_cluster_ids": [ + "IC-010" + ], + "asset_cluster_ids": [], + "evidence_indexes": [], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "변제": { + "candidate_structure_type": "변제", + "seed_ids": [ + "LESS-010", + "LESS-050" + ], + "source_bo_ids": [ + "bh18", + "bh9" + ], + "issue_cluster_ids": [ + "IC-010", + "IC-050" + ], + "asset_cluster_ids": [], + "evidence_indexes": [], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "변제충당": { + "candidate_structure_type": "변제충당", + "seed_ids": [ + "LESS-010", + "LESS-011" + ], + "source_bo_ids": [ + "bh18", + "bh19" + ], + "issue_cluster_ids": [ + "IC-010", + "IC-011" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_indexes": [ + "E-007", + "E-011" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "변제_및_변제충당": { + "candidate_structure_type": "변제_및_변제충당", + "seed_ids": [ + "LESS-010" + ], + "source_bo_ids": [ + "bh18" + ], + "issue_cluster_ids": [ + "IC-010" + ], + "asset_cluster_ids": [], + "evidence_indexes": [], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "payment_allocation": { + "candidate_structure_type": "payment_allocation", + "seed_ids": [ + "LESS-011" + ], + "source_bo_ids": [ + "bh19" + ], + "issue_cluster_ids": [ + "IC-011" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_indexes": [ + "E-007", + "E-011" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "land_use_gain": { + "candidate_structure_type": "land_use_gain", + "seed_ids": [ + "LESS-011", + "LESS-014", + "LESS-015", + "LESS-016", + "LESS-017", + "LESS-018", + "LESS-019" + ], + "source_bo_ids": [ + "bh19", + "bh21", + "bh22", + "bh23", + "bh24", + "bh25", + "bh26" + ], + "issue_cluster_ids": [ + "IC-011", + "IC-014", + "IC-015", + "IC-016", + "IC-017", + "IC-018", + "IC-019" + ], + "asset_cluster_ids": [ + "AC-003", + "AC-005", + "AC-006", + "AC-007", + "AC-008", + "AC-009", + "AC-010", + "AC-011", + "AC-012", + "AC-013", + "AC-014", + "AC-015" + ], + "evidence_indexes": [ + "E-007", + "E-011", + "E-005", + "E-006", + "E-004", + "E-023", + "E-017", + "E-018", + "E-022" + ], + "downstream_fact_roles": [ + "legal_calculation_object" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "변제충당_주장": { + "candidate_structure_type": "변제충당_주장", + "seed_ids": [ + "LESS-011" + ], + "source_bo_ids": [ + "bh19" + ], + "issue_cluster_ids": [ + "IC-011" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "evidence_indexes": [ + "E-007", + "E-011" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "물품대금": { + "candidate_structure_type": "물품대금", + "seed_ids": [ + "LESS-012" + ], + "source_bo_ids": [ + "bh2" + ], + "issue_cluster_ids": [ + "IC-012" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-021", + "E-027" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "매매": { + "candidate_structure_type": "매매", + "seed_ids": [ + "LESS-012", + "LESS-020" + ], + "source_bo_ids": [ + "bh2", + "bh27" + ], + "issue_cluster_ids": [ + "IC-012", + "IC-020" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_indexes": [ + "E-021", + "E-027", + "E-014" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "요구": { + "candidate_structure_type": "요구", + "seed_ids": [ + "LESS-013" + ], + "source_bo_ids": [ + "bh20" + ], + "issue_cluster_ids": [ + "IC-013" + ], + "asset_cluster_ids": [ + "AC-004" + ], + "evidence_indexes": [ + "E-029" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "거절": { + "candidate_structure_type": "거절", + "seed_ids": [ + "LESS-013" + ], + "source_bo_ids": [ + "bh20" + ], + "issue_cluster_ids": [ + "IC-013" + ], + "asset_cluster_ids": [ + "AC-004" + ], + "evidence_indexes": [ + "E-029" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "valuation": { + "candidate_structure_type": "valuation", + "seed_ids": [ + "LESS-014", + "LESS-016", + "LESS-018", + "LESS-022", + "LESS-024" + ], + "source_bo_ids": [ + "bh21", + "bh23", + "bh25", + "bh29", + "bh30" + ], + "issue_cluster_ids": [ + "IC-014", + "IC-016", + "IC-018", + "IC-022", + "IC-024" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006", + "AC-009", + "AC-010", + "AC-011", + "AC-013", + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-002", + "AC-020", + "AC-021" + ], + "evidence_indexes": [ + "E-005", + "E-004", + "E-023", + "E-014" + ], + "downstream_fact_roles": [ + "legal_calculation_object" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "임대료": { + "candidate_structure_type": "임대료", + "seed_ids": [ + "LESS-014" + ], + "source_bo_ids": [ + "bh21" + ], + "issue_cluster_ids": [ + "IC-014" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006" + ], + "evidence_indexes": [ + "E-005" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "차임": { + "candidate_structure_type": "차임", + "seed_ids": [ + "LESS-014", + "LESS-018" + ], + "source_bo_ids": [ + "bh21", + "bh25" + ], + "issue_cluster_ids": [ + "IC-014", + "IC-018" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006", + "AC-011", + "AC-013" + ], + "evidence_indexes": [ + "E-005", + "E-023" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "possession": { + "candidate_structure_type": "possession", + "seed_ids": [ + "LESS-015", + "LESS-017", + "LESS-019" + ], + "source_bo_ids": [ + "bh22", + "bh24", + "bh26" + ], + "issue_cluster_ids": [ + "IC-015", + "IC-017", + "IC-019" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-007", + "AC-008", + "AC-011", + "AC-012", + "AC-014", + "AC-015" + ], + "evidence_indexes": [ + "E-006", + "E-011", + "E-017", + "E-018", + "E-022" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "사용": { + "candidate_structure_type": "사용", + "seed_ids": [ + "LESS-015" + ], + "source_bo_ids": [ + "bh22" + ], + "issue_cluster_ids": [ + "IC-015" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-007", + "AC-008" + ], + "evidence_indexes": [ + "E-006" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "거주": { + "candidate_structure_type": "거주", + "seed_ids": [ + "LESS-017" + ], + "source_bo_ids": [ + "bh24" + ], + "issue_cluster_ids": [ + "IC-017" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-012" + ], + "evidence_indexes": [ + "E-011" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "수익": { + "candidate_structure_type": "수익", + "seed_ids": [ + "LESS-017" + ], + "source_bo_ids": [ + "bh24" + ], + "issue_cluster_ids": [ + "IC-017" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-012" + ], + "evidence_indexes": [ + "E-011" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "운영": { + "candidate_structure_type": "운영", + "seed_ids": [ + "LESS-019" + ], + "source_bo_ids": [ + "bh26" + ], + "issue_cluster_ids": [ + "IC-019" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "사업자등록": { + "candidate_structure_type": "사업자등록", + "seed_ids": [ + "LESS-019" + ], + "source_bo_ids": [ + "bh26" + ], + "issue_cluster_ids": [ + "IC-019" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "fraudulent_transfer": { + "candidate_structure_type": "fraudulent_transfer", + "seed_ids": [ + "LESS-020", + "LESS-021", + "LESS-022", + "LESS-024", + "LESS-025", + "LESS-029" + ], + "source_bo_ids": [ + "bh27", + "bh28", + "bh29", + "bh30", + "bh31", + "bh35" + ], + "issue_cluster_ids": [ + "IC-020", + "IC-021", + "IC-022", + "IC-024", + "IC-025", + "IC-029" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002", + "AC-017", + "AC-018", + "AC-019", + "AC-020", + "AC-021" + ], + "evidence_indexes": [ + "E-014", + "E-024", + "E-004", + "E-003", + "E-010", + "E-008" + ], + "downstream_fact_roles": [ + "actio_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "매매계약": { + "candidate_structure_type": "매매계약", + "seed_ids": [ + "LESS-020" + ], + "source_bo_ids": [ + "bh27" + ], + "issue_cluster_ids": [ + "IC-020" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_indexes": [ + "E-014" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "책임재산감소": { + "candidate_structure_type": "책임재산감소", + "seed_ids": [ + "LESS-020" + ], + "source_bo_ids": [ + "bh27" + ], + "issue_cluster_ids": [ + "IC-020" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_indexes": [ + "E-014" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "preserved_claim": { + "candidate_structure_type": "preserved_claim", + "seed_ids": [ + "LESS-021" + ], + "source_bo_ids": [ + "bh28" + ], + "issue_cluster_ids": [ + "IC-021" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-024" + ], + "downstream_fact_roles": [ + "claim_chain_ref" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "소비대차": { + "candidate_structure_type": "소비대차", + "seed_ids": [ + "LESS-021", + "LESS-023", + "LESS-034", + "LESS-045", + "LESS-047" + ], + "source_bo_ids": [ + "bh28", + "bh3", + "bh4", + "bh5", + "bh6" + ], + "issue_cluster_ids": [ + "IC-021", + "IC-023", + "IC-034", + "IC-045", + "IC-047" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-024", + "E-025", + "E-026" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "value_compensation": { + "candidate_structure_type": "value_compensation", + "seed_ids": [ + "LESS-022" + ], + "source_bo_ids": [ + "bh29" + ], + "issue_cluster_ids": [ + "IC-022" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-002" + ], + "evidence_indexes": [ + "E-014" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "actio_remedy_or_cap": { + "candidate_structure_type": "actio_remedy_or_cap", + "seed_ids": [ + "LESS-022" + ], + "source_bo_ids": [ + "bh29" + ], + "issue_cluster_ids": [ + "IC-022" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-002" + ], + "evidence_indexes": [ + "E-014" + ], + "downstream_fact_roles": [ + "linked_actio_structures" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "원물반환불능": { + "candidate_structure_type": "원물반환불능", + "seed_ids": [ + "LESS-022" + ], + "source_bo_ids": [ + "bh29" + ], + "issue_cluster_ids": [ + "IC-022" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-002" + ], + "evidence_indexes": [ + "E-014" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "insolvency_check": { + "candidate_structure_type": "insolvency_check", + "seed_ids": [ + "LESS-024" + ], + "source_bo_ids": [ + "bh30" + ], + "issue_cluster_ids": [ + "IC-024" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-020", + "AC-002", + "AC-021" + ], + "evidence_indexes": [ + "E-004" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "무자력": { + "candidate_structure_type": "무자력", + "seed_ids": [ + "LESS-024" + ], + "source_bo_ids": [ + "bh30" + ], + "issue_cluster_ids": [ + "IC-024" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-020", + "AC-002", + "AC-021" + ], + "evidence_indexes": [ + "E-004" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "책임재산": { + "candidate_structure_type": "책임재산", + "seed_ids": [ + "LESS-024" + ], + "source_bo_ids": [ + "bh30" + ], + "issue_cluster_ids": [ + "IC-024" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-020", + "AC-002", + "AC-021" + ], + "evidence_indexes": [ + "E-004" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "defense_assertion": { + "candidate_structure_type": "defense_assertion", + "seed_ids": [ + "LESS-025" + ], + "source_bo_ids": [ + "bh31" + ], + "issue_cluster_ids": [ + "IC-025" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "defense_rebuttal_matrix_bo": { + "candidate_structure_type": "defense_rebuttal_matrix_bo", + "seed_ids": [ + "LESS-025" + ], + "source_bo_ids": [ + "bh31" + ], + "issue_cluster_ids": [ + "IC-025" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "명의신탁": { + "candidate_structure_type": "명의신탁", + "seed_ids": [ + "LESS-025" + ], + "source_bo_ids": [ + "bh31" + ], + "issue_cluster_ids": [ + "IC-025" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "evidence_indexes": [ + "E-003", + "E-010" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "상속개시": { + "candidate_structure_type": "상속개시", + "seed_ids": [ + "LESS-026" + ], + "source_bo_ids": [ + "bh32" + ], + "issue_cluster_ids": [ + "IC-026" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-013" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "상속포기": { + "candidate_structure_type": "상속포기", + "seed_ids": [ + "LESS-027" + ], + "source_bo_ids": [ + "bh33" + ], + "issue_cluster_ids": [ + "IC-027" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-001", + "E-002" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "영업양도": { + "candidate_structure_type": "영업양도", + "seed_ids": [ + "LESS-048" + ], + "source_bo_ids": [ + "bh7" + ], + "issue_cluster_ids": [ + "IC-048" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-020" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "채무승인": { + "candidate_structure_type": "채무승인", + "seed_ids": [ + "LESS-049" + ], + "source_bo_ids": [ + "bh8" + ], + "issue_cluster_ids": [ + "IC-049" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-021" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "변제기_유예": { + "candidate_structure_type": "변제기_유예", + "seed_ids": [ + "LESS-049" + ], + "source_bo_ids": [ + "bh8" + ], + "issue_cluster_ids": [ + "IC-049" + ], + "asset_cluster_ids": [], + "evidence_indexes": [ + "E-021" + ], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + }, + "법정변제충당": { + "candidate_structure_type": "법정변제충당", + "seed_ids": [ + "LESS-050" + ], + "source_bo_ids": [ + "bh9" + ], + "issue_cluster_ids": [ + "IC-050" + ], + "asset_cluster_ids": [], + "evidence_indexes": [], + "downstream_fact_roles": [ + "legal_effect_roles" + ], + "basis_fields": [ + "liability_group_ids", + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload", + "BO.source_evidence_indexes", + "candidate_inputs_by_bo_id" + ], + "review_required": true + } + }, + "evidence_index_seed": { + "E-007": { + "evidence_index": "E-007", + "source_title": "공사도급계약서", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-001", + "LESS-011" + ], + "source_bo_ids": [ + "bh1", + "bh19" + ], + "issue_cluster_ids": [ + "IC-001", + "IC-011" + ], + "asset_cluster_ids": [ + "AC-003" + ], + "candidate_structure_types": [ + "money_claim", + "도급", + "공사대금", + "event", + "법률행위(legal_acts)", + "commercial_successor", + "payment_allocation", + "secured_debt", + "succession_notice_lien_asset_defense", + "land_use_gain", + "변제충당", + "변제충당_주장", + "준법률행위(quasi-legal_acts)" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-003": { + "evidence_index": "E-003", + "source_title": "각서 및 금전소비대차계약서", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-002", + "LESS-025" + ], + "source_bo_ids": [ + "bh10", + "bh31" + ], + "issue_cluster_ids": [ + "IC-002", + "IC-025" + ], + "asset_cluster_ids": [ + "AC-016", + "AC-002" + ], + "candidate_structure_types": [ + "money_claim", + "상계", + "succession_notice_lien_asset_defense", + "event", + "소송행위(litigation_acts)", + "commercial_successor", + "defense_assertion", + "defense_rebuttal_matrix_bo", + "fraudulent_transfer", + "명의신탁", + "state" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-010": { + "evidence_index": "E-010", + "source_title": "내용증명에 대한 답신", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-002", + "LESS-007", + "LESS-025", + "LESS-029", + "LESS-032", + "LESS-042" + ], + "source_bo_ids": [ + "bh10", + "bh15", + "bh31", + "bh35", + "bh38", + "bh47" + ], + "issue_cluster_ids": [ + "IC-002", + "IC-007", + "IC-025", + "IC-029", + "IC-032", + "IC-042" + ], + "asset_cluster_ids": [ + "AC-002", + "AC-016" + ], + "candidate_structure_types": [ + "money_claim", + "상계", + "succession_notice_lien_asset_defense", + "event", + "소송행위(litigation_acts)", + "commercial_successor", + "secured_debt", + "mortgage_extinguishment", + "registry_invalidity", + "법률행위(legal_acts)", + "defense_assertion", + "defense_rebuttal_matrix_bo", + "fraudulent_transfer", + "명의신탁", + "state", + "최고", + "준법률행위(quasi-legal_acts)" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-027": { + "evidence_index": "E-027", + "source_title": "최고 통지서", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-003", + "LESS-012", + "LESS-028" + ], + "source_bo_ids": [ + "bh11", + "bh2", + "bh34" + ], + "issue_cluster_ids": [ + "IC-003", + "IC-012", + "IC-028" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "money_claim", + "이행최고", + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal_acts)", + "commercial_successor", + "물품대금", + "매매", + "법률행위(legal_acts)" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-028": { + "evidence_index": "E-028", + "source_title": "통지서", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-004", + "LESS-031" + ], + "source_bo_ids": [ + "bh12", + "bh37" + ], + "issue_cluster_ids": [ + "IC-004", + "IC-031" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "money_claim", + "succession_notice_lien_asset_defense", + "책임부인", + "event", + "준법률행위(quasi-legal_acts)", + "commercial_successor" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-011": { + "evidence_index": "E-011", + "source_title": "답변서", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-005", + "LESS-011", + "LESS-017", + "LESS-030", + "LESS-039", + "LESS-043" + ], + "source_bo_ids": [ + "bh13", + "bh19", + "bh24", + "bh36", + "bh44", + "bh48" + ], + "issue_cluster_ids": [ + "IC-005", + "IC-011", + "IC-017", + "IC-030", + "IC-039", + "IC-043" + ], + "asset_cluster_ids": [ + "AC-003", + "AC-011", + "AC-012" + ], + "candidate_structure_types": [ + "money_claim", + "충당", + "state", + "사실행위(factual_acts)", + "commercial_successor", + "payment_allocation", + "secured_debt", + "succession_notice_lien_asset_defense", + "land_use_gain", + "변제충당", + "변제충당_주장", + "event", + "준법률행위(quasi-legal_acts)", + "possession", + "거주", + "수익", + "소송행위(litigation_acts)" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-013": { + "evidence_index": "E-013", + "source_title": "등기사항전부증명서(말소사항 포함)-토지", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-006", + "LESS-009", + "LESS-026", + "LESS-038" + ], + "source_bo_ids": [ + "bh14", + "bh17", + "bh32", + "bh43" + ], + "issue_cluster_ids": [ + "IC-006", + "IC-009", + "IC-026", + "IC-038" + ], + "asset_cluster_ids": [ + "AC-001", + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "candidate_structure_types": [ + "secured_debt", + "상속", + "증여", + "(근)저당권_설정_계약", + "event", + "법률행위(legal_acts)", + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "임의경매", + "공신력", + "registry_invalidity", + "소송행위(litigation_acts)", + "succession_notice_lien_asset_defense", + "상속개시", + "사실행위(factual_acts)", + "state" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-025": { + "evidence_index": "E-025", + "source_title": "차용증", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-006", + "LESS-045" + ], + "source_bo_ids": [ + "bh14", + "bh5" + ], + "issue_cluster_ids": [ + "IC-006", + "IC-045" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "candidate_structure_types": [ + "secured_debt", + "상속", + "증여", + "(근)저당권_설정_계약", + "event", + "법률행위(legal_acts)", + "money_claim", + "소비대차", + "commercial_successor" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-026": { + "evidence_index": "E-026", + "source_title": "차용증", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-006", + "LESS-047" + ], + "source_bo_ids": [ + "bh14", + "bh6" + ], + "issue_cluster_ids": [ + "IC-006", + "IC-047" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "candidate_structure_types": [ + "secured_debt", + "상속", + "증여", + "(근)저당권_설정_계약", + "event", + "법률행위(legal_acts)", + "money_claim", + "소비대차", + "commercial_successor" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-014": { + "evidence_index": "E-014", + "source_title": "등기사항전부증명서 (말소사항 포함) -건물", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-007", + "LESS-020", + "LESS-022", + "LESS-038" + ], + "source_bo_ids": [ + "bh15", + "bh27", + "bh29", + "bh43" + ], + "issue_cluster_ids": [ + "IC-007", + "IC-020", + "IC-022", + "IC-038" + ], + "asset_cluster_ids": [ + "AC-002", + "AC-016", + "AC-017", + "AC-018", + "AC-019", + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "candidate_structure_types": [ + "secured_debt", + "mortgage_extinguishment", + "registry_invalidity", + "상계", + "event", + "법률행위(legal_acts)", + "fraudulent_transfer", + "매매계약", + "소유권이전", + "책임재산감소", + "매매", + "value_compensation", + "valuation", + "actio_remedy_or_cap", + "원물반환불능", + "준법률행위(quasi-legal_acts)", + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-015": { + "evidence_index": "E-015", + "source_title": "등기사항전부증명서(말소사항 포함)-집합건물", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-008", + "LESS-038" + ], + "source_bo_ids": [ + "bh16", + "bh43" + ], + "issue_cluster_ids": [ + "IC-008", + "IC-038" + ], + "asset_cluster_ids": [ + "AC-003", + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "candidate_structure_types": [ + "secured_debt", + "소유권보존", + "소유권이전", + "가압류", + "registry_invalidity", + "event", + "법률행위(legal_acts)", + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-012": { + "evidence_index": "E-012", + "source_title": "등기말소 신청에 대한 답신", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-009", + "LESS-037", + "LESS-046" + ], + "source_bo_ids": [ + "bh17", + "bh42", + "bh50" + ], + "issue_cluster_ids": [ + "IC-009", + "IC-037", + "IC-046" + ], + "asset_cluster_ids": [ + "AC-001" + ], + "candidate_structure_types": [ + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "secured_debt", + "임의경매", + "공신력", + "registry_invalidity", + "event", + "소송행위(litigation_acts)", + "succession_notice_lien_asset_defense", + "준법률행위(quasi-legal_acts)", + "state" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-021": { + "evidence_index": "E-021", + "source_title": "이행각서", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-012", + "LESS-049" + ], + "source_bo_ids": [ + "bh2", + "bh8" + ], + "issue_cluster_ids": [ + "IC-012", + "IC-049" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "money_claim", + "물품대금", + "매매", + "event", + "법률행위(legal_acts)", + "commercial_successor", + "채무승인", + "변제기_유예" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-029": { + "evidence_index": "E-029", + "source_title": "통지서_이수인_윤건우", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-013", + "LESS-036" + ], + "source_bo_ids": [ + "bh20", + "bh41" + ], + "issue_cluster_ids": [ + "IC-013", + "IC-036" + ], + "asset_cluster_ids": [ + "AC-004" + ], + "candidate_structure_types": [ + "registry_invalidity", + "secured_debt", + "요구", + "거절", + "event", + "준법률행위(quasi-legal_acts)", + "succession_notice_lien_asset_defense", + "money_claim" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-005": { + "evidence_index": "E-005", + "source_title": "감정평가 의견서", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-014" + ], + "source_bo_ids": [ + "bh21" + ], + "issue_cluster_ids": [ + "IC-014" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-006" + ], + "candidate_structure_types": [ + "valuation", + "land_use_gain", + "임대료", + "차임", + "event", + "사실행위(factual_acts)" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-006": { + "evidence_index": "E-006", + "source_title": "계약서", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-015", + "LESS-040" + ], + "source_bo_ids": [ + "bh22", + "bh45" + ], + "issue_cluster_ids": [ + "IC-015", + "IC-040" + ], + "asset_cluster_ids": [ + "AC-005", + "AC-007", + "AC-008" + ], + "candidate_structure_types": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "사용", + "state", + "사실행위(factual_acts)" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-004": { + "evidence_index": "E-004", + "source_title": "감정평가서", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-016", + "LESS-024" + ], + "source_bo_ids": [ + "bh23", + "bh30" + ], + "issue_cluster_ids": [ + "IC-016", + "IC-024" + ], + "asset_cluster_ids": [ + "AC-009", + "AC-010", + "AC-016", + "AC-020", + "AC-002", + "AC-021" + ], + "candidate_structure_types": [ + "valuation", + "land_use_gain", + "state", + "사실행위(factual_acts)", + "insolvency_check", + "fraudulent_transfer", + "무자력", + "책임재산" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-023": { + "evidence_index": "E-023", + "source_title": "임료시세확인서", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-018" + ], + "source_bo_ids": [ + "bh25" + ], + "issue_cluster_ids": [ + "IC-018" + ], + "asset_cluster_ids": [ + "AC-011", + "AC-013" + ], + "candidate_structure_types": [ + "valuation", + "land_use_gain", + "차임", + "state", + "사실행위(factual_acts)" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-017": { + "evidence_index": "E-017", + "source_title": "부동산 매매계약서", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-019" + ], + "source_bo_ids": [ + "bh26" + ], + "issue_cluster_ids": [ + "IC-019" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "candidate_structure_types": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "운영", + "사업자등록", + "state", + "사실행위(factual_acts)" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-018": { + "evidence_index": "E-018", + "source_title": "사업자등록증", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-019", + "LESS-041" + ], + "source_bo_ids": [ + "bh26", + "bh46" + ], + "issue_cluster_ids": [ + "IC-019", + "IC-041" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "candidate_structure_types": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "운영", + "사업자등록", + "state", + "사실행위(factual_acts)" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-022": { + "evidence_index": "E-022", + "source_title": "임대차계약서", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-019", + "LESS-041" + ], + "source_bo_ids": [ + "bh26", + "bh46" + ], + "issue_cluster_ids": [ + "IC-019", + "IC-041" + ], + "asset_cluster_ids": [ + "AC-014", + "AC-015" + ], + "candidate_structure_types": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "운영", + "사업자등록", + "state", + "사실행위(factual_acts)" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-024": { + "evidence_index": "E-024", + "source_title": "차용증", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-021", + "LESS-023", + "LESS-034" + ], + "source_bo_ids": [ + "bh28", + "bh3", + "bh4" + ], + "issue_cluster_ids": [ + "IC-021", + "IC-023", + "IC-034" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "preserved_claim", + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "fraudulent_transfer", + "commercial_successor" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-001": { + "evidence_index": "E-001", + "source_title": "가정법원심판_강호연", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-027" + ], + "source_bo_ids": [ + "bh33" + ], + "issue_cluster_ids": [ + "IC-027" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상속포기", + "event", + "사실행위(factual_acts)" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-002": { + "evidence_index": "E-002", + "source_title": "가족관계증명서", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-027" + ], + "source_bo_ids": [ + "bh33" + ], + "issue_cluster_ids": [ + "IC-027" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "상속포기", + "event", + "사실행위(factual_acts)" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-008": { + "evidence_index": "E-008", + "source_title": "내용증명", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-029" + ], + "source_bo_ids": [ + "bh35" + ], + "issue_cluster_ids": [ + "IC-029" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal_acts)", + "fraudulent_transfer" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-019": { + "evidence_index": "E-019", + "source_title": "소멸청구 통지서", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-030" + ], + "source_bo_ids": [ + "bh36" + ], + "issue_cluster_ids": [ + "IC-030" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)", + "secured_debt" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-009": { + "evidence_index": "E-009", + "source_title": "내용증명", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-033", + "LESS-044" + ], + "source_bo_ids": [ + "bh39", + "bh49" + ], + "issue_cluster_ids": [ + "IC-033", + "IC-044" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)", + "state", + "소송행위(litigation_acts)" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-030": { + "evidence_index": "E-030", + "source_title": "해제 통지서", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-035" + ], + "source_bo_ids": [ + "bh40" + ], + "issue_cluster_ids": [ + "IC-035" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-016": { + "evidence_index": "E-016", + "source_title": "별지 목록", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-038" + ], + "source_bo_ids": [ + "bh43" + ], + "issue_cluster_ids": [ + "IC-038" + ], + "asset_cluster_ids": [ + "AC-022", + "AC-023", + "AC-024", + "AC-025", + "AC-026" + ], + "candidate_structure_types": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + }, + "E-020": { + "evidence_index": "E-020", + "source_title": "영업양도 계약서", + "priority_class": null, + "authentication_status": null, + "seed_ids": [ + "LESS-048" + ], + "source_bo_ids": [ + "bh7" + ], + "issue_cluster_ids": [ + "IC-048" + ], + "asset_cluster_ids": [], + "candidate_structure_types": [ + "money_claim", + "영업양도", + "commercial_successor", + "event", + "법률행위(legal_acts)" + ], + "gap_flags": [ + "missing_evidence_excerpt" + ] + } + }, + "ambiguity_diagnostics": { + "issue_cluster_exceptions": [], + "asset_cluster_exceptions": [ + { + "exception_id": "LEX-001", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh21" + ], + "candidate_values": [ + "SCH-016-01", + "성수동 256 대" + ], + "basis_fields": [ + "asset_candidates" + ], + "score_table": [ + { + "candidate_value": "SCH-016-01", + "normalized_label": "sch-016-01", + "asset_kind": "unknown" + }, + { + "candidate_value": "성수동 256 대", + "normalized_label": "성수동256대", + "asset_kind": "unknown" + } + ], + "conflict_flags": [ + "multi_asset_bundle_or_separate_clusters_ambiguous" + ], + "compact_context_class": [ + [ + "unknown" + ], + [ + "sch-016-01", + "성수동256대" + ] + ], + "allowed_decisions": [ + "SEPARATE_CLUSTER", + "MULTI_ASSET_BUNDLE", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-002", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh23" + ], + "candidate_values": [ + "SCH-016-03", + "신림로 상가, 포천시 임야" + ], + "basis_fields": [ + "asset_candidates" + ], + "score_table": [ + { + "candidate_value": "SCH-016-03", + "normalized_label": "sch-016-03", + "asset_kind": "unknown" + }, + { + "candidate_value": "신림로 상가, 포천시 임야", + "normalized_label": "신림로상가포천시임야", + "asset_kind": "real_property" + } + ], + "conflict_flags": [ + "multi_asset_bundle_or_separate_clusters_ambiguous" + ], + "compact_context_class": [ + [ + "real_property", + "unknown" + ], + [ + "sch-016-03", + "신림로상가포천시임야" + ] + ], + "allowed_decisions": [ + "SEPARATE_CLUSTER", + "MULTI_ASSET_BUNDLE", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-003", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh24" + ], + "candidate_values": [ + "SCH-016-04", + "평택시 서정3길 123 서정빌라 101호" + ], + "basis_fields": [ + "asset_candidates" + ], + "score_table": [ + { + "candidate_value": "SCH-016-04", + "normalized_label": "sch-016-04", + "asset_kind": "unknown" + }, + { + "candidate_value": "평택시 서정3길 123 서정빌라 101호", + "normalized_label": "평택시서정3길123서정빌라101호", + "asset_kind": "unknown" + } + ], + "conflict_flags": [ + "multi_asset_bundle_or_separate_clusters_ambiguous" + ], + "compact_context_class": [ + [ + "unknown" + ], + [ + "sch-016-04", + "평택시서정3길123서정빌라101호" + ] + ], + "allowed_decisions": [ + "SEPARATE_CLUSTER", + "MULTI_ASSET_BUNDLE", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-004", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh25" + ], + "candidate_values": [ + "SCH-016-04", + "서정빌라 101호" + ], + "basis_fields": [ + "asset_candidates" + ], + "score_table": [ + { + "candidate_value": "SCH-016-04", + "normalized_label": "sch-016-04", + "asset_kind": "unknown" + }, + { + "candidate_value": "서정빌라 101호", + "normalized_label": "서정빌라101호", + "asset_kind": "unknown" + } + ], + "conflict_flags": [ + "multi_asset_bundle_or_separate_clusters_ambiguous" + ], + "compact_context_class": [ + [ + "unknown" + ], + [ + "sch-016-04", + "서정빌라101호" + ] + ], + "allowed_decisions": [ + "SEPARATE_CLUSTER", + "MULTI_ASSET_BUNDLE", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-005", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh26" + ], + "candidate_values": [ + "SCH-016-05", + "흑석동 201호" + ], + "basis_fields": [ + "asset_candidates" + ], + "score_table": [ + { + "candidate_value": "SCH-016-05", + "normalized_label": "sch-016-05", + "asset_kind": "unknown" + }, + { + "candidate_value": "흑석동 201호", + "normalized_label": "흑석동201호", + "asset_kind": "unknown" + } + ], + "conflict_flags": [ + "multi_asset_bundle_or_separate_clusters_ambiguous" + ], + "compact_context_class": [ + [ + "unknown" + ], + [ + "sch-016-05", + "흑석동201호" + ] + ], + "allowed_decisions": [ + "SEPARATE_CLUSTER", + "MULTI_ASSET_BUNDLE", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-006", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh30" + ], + "candidate_values": [ + "신림동 상가", + "포천시 임야" + ], + "basis_fields": [ + "asset_candidates" + ], + "score_table": [ + { + "candidate_value": "신림동 상가", + "normalized_label": "신림동상", + "asset_kind": "unknown" + }, + { + "candidate_value": "포천시 임야", + "normalized_label": "포천시임야", + "asset_kind": "unknown" + }, + { + "candidate_value": "신림동 상가", + "normalized_label": "신림동상", + "asset_kind": "real_property" + }, + { + "candidate_value": "포천시 임야", + "normalized_label": "포천시임야", + "asset_kind": "real_property" + } + ], + "conflict_flags": [ + "multi_asset_bundle_or_separate_clusters_ambiguous" + ], + "compact_context_class": [ + [ + "real_property", + "unknown" + ], + [ + "신림동상", + "포천시임야" + ] + ], + "allowed_decisions": [ + "SEPARATE_CLUSTER", + "MULTI_ASSET_BUNDLE", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + } + ], + "candidate_structure_type_exceptions": [ + { + "exception_id": "LEX-007", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh1" + ], + "candidate_values": [ + "money_claim", + "도급", + "공사대금", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "도급", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "공사대금", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "commercial_successor", + "downstream_role": "claim_chain_ref" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "commercial_successor", + "event", + "money_claim", + "공사대금", + "도급", + "법률행위(legal_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-008", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh10" + ], + "candidate_values": [ + "money_claim", + "상계", + "succession_notice_lien_asset_defense", + "event", + "소송행위(litigation_acts)", + "commercial_successor" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "상계", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "소송행위(litigation_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "commercial_successor", + "downstream_role": "claim_chain_ref" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "commercial_successor", + "event", + "money_claim", + "succession_notice_lien_asset_defense", + "상계", + "소송행위(litigation_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-009", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh11" + ], + "candidate_values": [ + "money_claim", + "이행최고", + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal_acts)", + "commercial_successor" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "이행최고", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "최고", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "준법률행위(quasi-legal_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "commercial_successor", + "downstream_role": "claim_chain_ref" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "commercial_successor", + "event", + "money_claim", + "succession_notice_lien_asset_defense", + "이행최고", + "준법률행위(quasi-legal_acts)", + "최고" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-010", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh12" + ], + "candidate_values": [ + "money_claim", + "succession_notice_lien_asset_defense", + "책임부인", + "event", + "준법률행위(quasi-legal_acts)", + "commercial_successor" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "책임부인", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "준법률행위(quasi-legal_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "commercial_successor", + "downstream_role": "claim_chain_ref" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "commercial_successor", + "event", + "money_claim", + "succession_notice_lien_asset_defense", + "준법률행위(quasi-legal_acts)", + "책임부인" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-011", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh13" + ], + "candidate_values": [ + "money_claim", + "충당", + "state", + "사실행위(factual_acts)", + "commercial_successor" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "충당", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "commercial_successor", + "downstream_role": "claim_chain_ref" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "commercial_successor", + "money_claim", + "state", + "사실행위(factual_acts)", + "충당" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-012", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh14" + ], + "candidate_values": [ + "secured_debt", + "상속", + "증여", + "(근)저당권_설정_계약", + "event", + "법률행위(legal_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "secured_debt", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "상속", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "증여", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "(근)저당권_설정_계약", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "(근)저당권_설정_계약", + "event", + "secured_debt", + "법률행위(legal_acts)", + "상속", + "증여" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-013", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh15" + ], + "candidate_values": [ + "secured_debt", + "mortgage_extinguishment", + "registry_invalidity", + "상계", + "event", + "법률행위(legal_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "secured_debt", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "mortgage_extinguishment", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "registry_invalidity", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "상계", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "mortgage_extinguishment", + "registry_invalidity", + "secured_debt", + "법률행위(legal_acts)", + "상계" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-014", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh16" + ], + "candidate_values": [ + "secured_debt", + "소유권보존", + "소유권이전", + "가압류", + "registry_invalidity", + "event", + "법률행위(legal_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "secured_debt", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "소유권보존", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "소유권이전", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "가압류", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "registry_invalidity", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "registry_invalidity", + "secured_debt", + "가압류", + "법률행위(legal_acts)", + "소유권보존", + "소유권이전" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-015", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh17" + ], + "candidate_values": [ + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "secured_debt", + "임의경매", + "공신력", + "registry_invalidity", + "event", + "소송행위(litigation_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "auction_invalidity", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "derivative_bank_mortgage_invalidity", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "secured_debt", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "임의경매", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "공신력", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "registry_invalidity", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "소송행위(litigation_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "event", + "registry_invalidity", + "secured_debt", + "공신력", + "소송행위(litigation_acts)", + "임의경매" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-016", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh18" + ], + "candidate_values": [ + "payment_allocation_uncertain", + "secured_debt", + "변제", + "변제충당", + "변제_및_변제충당", + "event", + "준법률행위(quasi-legal_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "payment_allocation_uncertain", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "secured_debt", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "변제", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "변제충당", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "변제_및_변제충당", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "준법률행위(quasi-legal_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "payment_allocation_uncertain", + "secured_debt", + "변제", + "변제_및_변제충당", + "변제충당", + "준법률행위(quasi-legal_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-018", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh2" + ], + "candidate_values": [ + "money_claim", + "물품대금", + "매매", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "물품대금", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "매매", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "commercial_successor", + "downstream_role": "claim_chain_ref" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "commercial_successor", + "event", + "money_claim", + "매매", + "물품대금", + "법률행위(legal_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-019", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh20" + ], + "candidate_values": [ + "registry_invalidity", + "secured_debt", + "요구", + "거절", + "event", + "준법률행위(quasi-legal_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "registry_invalidity", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "secured_debt", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "요구", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "거절", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "준법률행위(quasi-legal_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "registry_invalidity", + "secured_debt", + "거절", + "요구", + "준법률행위(quasi-legal_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-020", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh21" + ], + "candidate_values": [ + "valuation", + "land_use_gain", + "임대료", + "차임", + "event", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "valuation", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "land_use_gain", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "임대료", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "차임", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "land_use_gain", + "valuation", + "사실행위(factual_acts)", + "임대료", + "차임" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-021", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh22" + ], + "candidate_values": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "사용", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "possession", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "land_use_gain", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "사용", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "land_use_gain", + "possession", + "state", + "succession_notice_lien_asset_defense", + "사실행위(factual_acts)", + "사용" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-022", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh23" + ], + "candidate_values": [ + "valuation", + "land_use_gain", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "valuation", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "land_use_gain", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "land_use_gain", + "state", + "valuation", + "사실행위(factual_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-023", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh24" + ], + "candidate_values": [ + "possession", + "secured_debt", + "land_use_gain", + "거주", + "succession_notice_lien_asset_defense", + "수익", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "possession", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "secured_debt", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "land_use_gain", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "거주", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "수익", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "land_use_gain", + "possession", + "secured_debt", + "state", + "succession_notice_lien_asset_defense", + "거주", + "사실행위(factual_acts)", + "수익" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-024", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh25" + ], + "candidate_values": [ + "valuation", + "land_use_gain", + "차임", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "valuation", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "land_use_gain", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "차임", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "land_use_gain", + "state", + "valuation", + "사실행위(factual_acts)", + "차임" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-025", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh26" + ], + "candidate_values": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "운영", + "사업자등록", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "possession", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "land_use_gain", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "운영", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사업자등록", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "land_use_gain", + "possession", + "state", + "succession_notice_lien_asset_defense", + "사실행위(factual_acts)", + "사업자등록", + "운영" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-026", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh27" + ], + "candidate_values": [ + "fraudulent_transfer", + "매매계약", + "소유권이전", + "책임재산감소", + "매매", + "event", + "법률행위(legal_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "fraudulent_transfer", + "downstream_role": "actio_roles" + }, + { + "candidate_value": "매매계약", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "소유권이전", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "책임재산감소", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "매매", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "fraudulent_transfer", + "매매", + "매매계약", + "법률행위(legal_acts)", + "소유권이전", + "책임재산감소" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-027", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh28" + ], + "candidate_values": [ + "preserved_claim", + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "fraudulent_transfer" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "preserved_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "소비대차", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "fraudulent_transfer", + "downstream_role": "actio_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "fraudulent_transfer", + "money_claim", + "preserved_claim", + "법률행위(legal_acts)", + "소비대차" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-029", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh3", + "bh4", + "bh5", + "bh6" + ], + "candidate_values": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "소비대차", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "commercial_successor", + "downstream_role": "claim_chain_ref" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "commercial_successor", + "event", + "money_claim", + "법률행위(legal_acts)", + "소비대차" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 4 + }, + { + "exception_id": "LEX-037", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh37", + "bh38", + "bh39", + "bh40" + ], + "candidate_values": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "준법률행위(quasi-legal_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "succession_notice_lien_asset_defense", + "준법률행위(quasi-legal_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 4 + }, + { + "exception_id": "LEX-040", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh43", + "bh45", + "bh46" + ], + "candidate_values": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "state", + "succession_notice_lien_asset_defense", + "사실행위(factual_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 3 + }, + { + "exception_id": "LEX-043", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh48", + "bh49" + ], + "candidate_values": [ + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "소송행위(litigation_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "state", + "succession_notice_lien_asset_defense", + "소송행위(litigation_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 2 + } + ], + "auto_decided_items": [], + "auto_blocked_items": [ + { + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh19" + ], + "reason": "budget_overflow" + }, + { + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh29" + ], + "reason": "budget_overflow" + }, + { + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh30" + ], + "reason": "budget_overflow" + }, + { + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh31" + ], + "reason": "budget_overflow" + }, + { + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh32" + ], + "reason": "budget_overflow" + }, + { + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh33" + ], + "reason": "budget_overflow" + }, + { + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh34" + ], + "reason": "budget_overflow" + }, + { + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh35" + ], + "reason": "budget_overflow" + }, + { + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh36" + ], + "reason": "budget_overflow" + }, + { + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh41" + ], + "reason": "budget_overflow" + }, + { + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh42" + ], + "reason": "budget_overflow" + }, + { + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh44" + ], + "reason": "budget_overflow" + }, + { + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh47" + ], + "reason": "budget_overflow" + }, + { + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh50" + ], + "reason": "budget_overflow" + }, + { + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh7" + ], + "reason": "budget_overflow" + }, + { + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh8" + ], + "reason": "budget_overflow" + }, + { + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh9" + ], + "reason": "budget_overflow" + } + ], + "merge_groups": [ + { + "representative_exception_id": "LEX-029", + "source_bo_ids": [ + "bh3", + "bh4", + "bh5", + "bh6" + ], + "merged_from_count": 4 + }, + { + "representative_exception_id": "LEX-037", + "source_bo_ids": [ + "bh37", + "bh38", + "bh39", + "bh40" + ], + "merged_from_count": 4 + }, + { + "representative_exception_id": "LEX-040", + "source_bo_ids": [ + "bh43", + "bh45", + "bh46" + ], + "merged_from_count": 3 + }, + { + "representative_exception_id": "LEX-043", + "source_bo_ids": [ + "bh48", + "bh49" + ], + "merged_from_count": 2 + } + ] + }, + "legal_structure_exception_pack": { + "schema_version": "stage1_legal_structure_exception_pack.v1", + "status": "READY", + "issue_cluster_exceptions": [], + "asset_cluster_exceptions": [ + { + "exception_id": "LEX-001", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh21" + ], + "candidate_values": [ + "SCH-016-01", + "성수동 256 대" + ], + "basis_fields": [ + "asset_candidates" + ], + "score_table": [ + { + "candidate_value": "SCH-016-01", + "normalized_label": "sch-016-01", + "asset_kind": "unknown" + }, + { + "candidate_value": "성수동 256 대", + "normalized_label": "성수동256대", + "asset_kind": "unknown" + } + ], + "conflict_flags": [ + "multi_asset_bundle_or_separate_clusters_ambiguous" + ], + "compact_context_class": [ + [ + "unknown" + ], + [ + "sch-016-01", + "성수동256대" + ] + ], + "allowed_decisions": [ + "SEPARATE_CLUSTER", + "MULTI_ASSET_BUNDLE", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-002", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh23" + ], + "candidate_values": [ + "SCH-016-03", + "신림로 상가, 포천시 임야" + ], + "basis_fields": [ + "asset_candidates" + ], + "score_table": [ + { + "candidate_value": "SCH-016-03", + "normalized_label": "sch-016-03", + "asset_kind": "unknown" + }, + { + "candidate_value": "신림로 상가, 포천시 임야", + "normalized_label": "신림로상가포천시임야", + "asset_kind": "real_property" + } + ], + "conflict_flags": [ + "multi_asset_bundle_or_separate_clusters_ambiguous" + ], + "compact_context_class": [ + [ + "real_property", + "unknown" + ], + [ + "sch-016-03", + "신림로상가포천시임야" + ] + ], + "allowed_decisions": [ + "SEPARATE_CLUSTER", + "MULTI_ASSET_BUNDLE", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-003", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh24" + ], + "candidate_values": [ + "SCH-016-04", + "평택시 서정3길 123 서정빌라 101호" + ], + "basis_fields": [ + "asset_candidates" + ], + "score_table": [ + { + "candidate_value": "SCH-016-04", + "normalized_label": "sch-016-04", + "asset_kind": "unknown" + }, + { + "candidate_value": "평택시 서정3길 123 서정빌라 101호", + "normalized_label": "평택시서정3길123서정빌라101호", + "asset_kind": "unknown" + } + ], + "conflict_flags": [ + "multi_asset_bundle_or_separate_clusters_ambiguous" + ], + "compact_context_class": [ + [ + "unknown" + ], + [ + "sch-016-04", + "평택시서정3길123서정빌라101호" + ] + ], + "allowed_decisions": [ + "SEPARATE_CLUSTER", + "MULTI_ASSET_BUNDLE", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-004", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh25" + ], + "candidate_values": [ + "SCH-016-04", + "서정빌라 101호" + ], + "basis_fields": [ + "asset_candidates" + ], + "score_table": [ + { + "candidate_value": "SCH-016-04", + "normalized_label": "sch-016-04", + "asset_kind": "unknown" + }, + { + "candidate_value": "서정빌라 101호", + "normalized_label": "서정빌라101호", + "asset_kind": "unknown" + } + ], + "conflict_flags": [ + "multi_asset_bundle_or_separate_clusters_ambiguous" + ], + "compact_context_class": [ + [ + "unknown" + ], + [ + "sch-016-04", + "서정빌라101호" + ] + ], + "allowed_decisions": [ + "SEPARATE_CLUSTER", + "MULTI_ASSET_BUNDLE", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-005", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh26" + ], + "candidate_values": [ + "SCH-016-05", + "흑석동 201호" + ], + "basis_fields": [ + "asset_candidates" + ], + "score_table": [ + { + "candidate_value": "SCH-016-05", + "normalized_label": "sch-016-05", + "asset_kind": "unknown" + }, + { + "candidate_value": "흑석동 201호", + "normalized_label": "흑석동201호", + "asset_kind": "unknown" + } + ], + "conflict_flags": [ + "multi_asset_bundle_or_separate_clusters_ambiguous" + ], + "compact_context_class": [ + [ + "unknown" + ], + [ + "sch-016-05", + "흑석동201호" + ] + ], + "allowed_decisions": [ + "SEPARATE_CLUSTER", + "MULTI_ASSET_BUNDLE", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-006", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [ + "bh30" + ], + "candidate_values": [ + "신림동 상가", + "포천시 임야" + ], + "basis_fields": [ + "asset_candidates" + ], + "score_table": [ + { + "candidate_value": "신림동 상가", + "normalized_label": "신림동상", + "asset_kind": "unknown" + }, + { + "candidate_value": "포천시 임야", + "normalized_label": "포천시임야", + "asset_kind": "unknown" + }, + { + "candidate_value": "신림동 상가", + "normalized_label": "신림동상", + "asset_kind": "real_property" + }, + { + "candidate_value": "포천시 임야", + "normalized_label": "포천시임야", + "asset_kind": "real_property" + } + ], + "conflict_flags": [ + "multi_asset_bundle_or_separate_clusters_ambiguous" + ], + "compact_context_class": [ + [ + "real_property", + "unknown" + ], + [ + "신림동상", + "포천시임야" + ] + ], + "allowed_decisions": [ + "SEPARATE_CLUSTER", + "MULTI_ASSET_BUNDLE", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + } + ], + "candidate_structure_type_exceptions": [ + { + "exception_id": "LEX-007", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh1" + ], + "candidate_values": [ + "money_claim", + "도급", + "공사대금", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "도급", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "공사대금", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "commercial_successor", + "downstream_role": "claim_chain_ref" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "commercial_successor", + "event", + "money_claim", + "공사대금", + "도급", + "법률행위(legal_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-008", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh10" + ], + "candidate_values": [ + "money_claim", + "상계", + "succession_notice_lien_asset_defense", + "event", + "소송행위(litigation_acts)", + "commercial_successor" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "상계", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "소송행위(litigation_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "commercial_successor", + "downstream_role": "claim_chain_ref" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "commercial_successor", + "event", + "money_claim", + "succession_notice_lien_asset_defense", + "상계", + "소송행위(litigation_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-009", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh11" + ], + "candidate_values": [ + "money_claim", + "이행최고", + "succession_notice_lien_asset_defense", + "최고", + "event", + "준법률행위(quasi-legal_acts)", + "commercial_successor" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "이행최고", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "최고", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "준법률행위(quasi-legal_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "commercial_successor", + "downstream_role": "claim_chain_ref" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "commercial_successor", + "event", + "money_claim", + "succession_notice_lien_asset_defense", + "이행최고", + "준법률행위(quasi-legal_acts)", + "최고" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-010", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh12" + ], + "candidate_values": [ + "money_claim", + "succession_notice_lien_asset_defense", + "책임부인", + "event", + "준법률행위(quasi-legal_acts)", + "commercial_successor" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "책임부인", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "준법률행위(quasi-legal_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "commercial_successor", + "downstream_role": "claim_chain_ref" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "commercial_successor", + "event", + "money_claim", + "succession_notice_lien_asset_defense", + "준법률행위(quasi-legal_acts)", + "책임부인" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-011", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh13" + ], + "candidate_values": [ + "money_claim", + "충당", + "state", + "사실행위(factual_acts)", + "commercial_successor" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "충당", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "commercial_successor", + "downstream_role": "claim_chain_ref" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "commercial_successor", + "money_claim", + "state", + "사실행위(factual_acts)", + "충당" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-012", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh14" + ], + "candidate_values": [ + "secured_debt", + "상속", + "증여", + "(근)저당권_설정_계약", + "event", + "법률행위(legal_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "secured_debt", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "상속", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "증여", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "(근)저당권_설정_계약", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "(근)저당권_설정_계약", + "event", + "secured_debt", + "법률행위(legal_acts)", + "상속", + "증여" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-013", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh15" + ], + "candidate_values": [ + "secured_debt", + "mortgage_extinguishment", + "registry_invalidity", + "상계", + "event", + "법률행위(legal_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "secured_debt", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "mortgage_extinguishment", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "registry_invalidity", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "상계", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "mortgage_extinguishment", + "registry_invalidity", + "secured_debt", + "법률행위(legal_acts)", + "상계" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-014", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh16" + ], + "candidate_values": [ + "secured_debt", + "소유권보존", + "소유권이전", + "가압류", + "registry_invalidity", + "event", + "법률행위(legal_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "secured_debt", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "소유권보존", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "소유권이전", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "가압류", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "registry_invalidity", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "registry_invalidity", + "secured_debt", + "가압류", + "법률행위(legal_acts)", + "소유권보존", + "소유권이전" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-015", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh17" + ], + "candidate_values": [ + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "secured_debt", + "임의경매", + "공신력", + "registry_invalidity", + "event", + "소송행위(litigation_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "auction_invalidity", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "derivative_bank_mortgage_invalidity", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "secured_debt", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "임의경매", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "공신력", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "registry_invalidity", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "소송행위(litigation_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "auction_invalidity", + "derivative_bank_mortgage_invalidity", + "event", + "registry_invalidity", + "secured_debt", + "공신력", + "소송행위(litigation_acts)", + "임의경매" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-016", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh18" + ], + "candidate_values": [ + "payment_allocation_uncertain", + "secured_debt", + "변제", + "변제충당", + "변제_및_변제충당", + "event", + "준법률행위(quasi-legal_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "payment_allocation_uncertain", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "secured_debt", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "변제", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "변제충당", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "변제_및_변제충당", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "준법률행위(quasi-legal_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "payment_allocation_uncertain", + "secured_debt", + "변제", + "변제_및_변제충당", + "변제충당", + "준법률행위(quasi-legal_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-018", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh2" + ], + "candidate_values": [ + "money_claim", + "물품대금", + "매매", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "물품대금", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "매매", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "commercial_successor", + "downstream_role": "claim_chain_ref" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "commercial_successor", + "event", + "money_claim", + "매매", + "물품대금", + "법률행위(legal_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-019", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh20" + ], + "candidate_values": [ + "registry_invalidity", + "secured_debt", + "요구", + "거절", + "event", + "준법률행위(quasi-legal_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "registry_invalidity", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "secured_debt", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "요구", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "거절", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "준법률행위(quasi-legal_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "registry_invalidity", + "secured_debt", + "거절", + "요구", + "준법률행위(quasi-legal_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-020", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh21" + ], + "candidate_values": [ + "valuation", + "land_use_gain", + "임대료", + "차임", + "event", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "valuation", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "land_use_gain", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "임대료", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "차임", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "land_use_gain", + "valuation", + "사실행위(factual_acts)", + "임대료", + "차임" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-021", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh22" + ], + "candidate_values": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "사용", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "possession", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "land_use_gain", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "사용", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "land_use_gain", + "possession", + "state", + "succession_notice_lien_asset_defense", + "사실행위(factual_acts)", + "사용" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-022", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh23" + ], + "candidate_values": [ + "valuation", + "land_use_gain", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "valuation", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "land_use_gain", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "land_use_gain", + "state", + "valuation", + "사실행위(factual_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-023", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh24" + ], + "candidate_values": [ + "possession", + "secured_debt", + "land_use_gain", + "거주", + "succession_notice_lien_asset_defense", + "수익", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "possession", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "secured_debt", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "land_use_gain", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "거주", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "수익", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "land_use_gain", + "possession", + "secured_debt", + "state", + "succession_notice_lien_asset_defense", + "거주", + "사실행위(factual_acts)", + "수익" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-024", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh25" + ], + "candidate_values": [ + "valuation", + "land_use_gain", + "차임", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "valuation", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "land_use_gain", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "차임", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "land_use_gain", + "state", + "valuation", + "사실행위(factual_acts)", + "차임" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-025", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh26" + ], + "candidate_values": [ + "possession", + "land_use_gain", + "succession_notice_lien_asset_defense", + "운영", + "사업자등록", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "possession", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "land_use_gain", + "downstream_role": "legal_calculation_object" + }, + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "운영", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사업자등록", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "land_use_gain", + "possession", + "state", + "succession_notice_lien_asset_defense", + "사실행위(factual_acts)", + "사업자등록", + "운영" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-026", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh27" + ], + "candidate_values": [ + "fraudulent_transfer", + "매매계약", + "소유권이전", + "책임재산감소", + "매매", + "event", + "법률행위(legal_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "fraudulent_transfer", + "downstream_role": "actio_roles" + }, + { + "candidate_value": "매매계약", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "소유권이전", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "책임재산감소", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "매매", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "fraudulent_transfer", + "매매", + "매매계약", + "법률행위(legal_acts)", + "소유권이전", + "책임재산감소" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-027", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh28" + ], + "candidate_values": [ + "preserved_claim", + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "fraudulent_transfer" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "preserved_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "소비대차", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "fraudulent_transfer", + "downstream_role": "actio_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "fraudulent_transfer", + "money_claim", + "preserved_claim", + "법률행위(legal_acts)", + "소비대차" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 1 + }, + { + "exception_id": "LEX-029", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh3", + "bh4", + "bh5", + "bh6" + ], + "candidate_values": [ + "money_claim", + "소비대차", + "event", + "법률행위(legal_acts)", + "commercial_successor" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "money_claim", + "downstream_role": "claim_chain_ref" + }, + { + "candidate_value": "소비대차", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "법률행위(legal_acts)", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "commercial_successor", + "downstream_role": "claim_chain_ref" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "commercial_successor", + "event", + "money_claim", + "법률행위(legal_acts)", + "소비대차" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 4 + }, + { + "exception_id": "LEX-037", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh37", + "bh38", + "bh39", + "bh40" + ], + "candidate_values": [ + "succession_notice_lien_asset_defense", + "event", + "준법률행위(quasi-legal_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "event", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "준법률행위(quasi-legal_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "event", + "succession_notice_lien_asset_defense", + "준법률행위(quasi-legal_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 4 + }, + { + "exception_id": "LEX-040", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh43", + "bh45", + "bh46" + ], + "candidate_values": [ + "succession_notice_lien_asset_defense", + "state", + "사실행위(factual_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "사실행위(factual_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "state", + "succession_notice_lien_asset_defense", + "사실행위(factual_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 3 + }, + { + "exception_id": "LEX-043", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [ + "bh48", + "bh49" + ], + "candidate_values": [ + "succession_notice_lien_asset_defense", + "state", + "소송행위(litigation_acts)" + ], + "basis_fields": [ + "legal_effect_signals", + "case_liability_signals", + "actio_case_signals", + "BO.downstream_seed_refs", + "BO.domain_payload" + ], + "score_table": [ + { + "candidate_value": "succession_notice_lien_asset_defense", + "downstream_role": "linked_structures" + }, + { + "candidate_value": "state", + "downstream_role": "legal_effect_roles" + }, + { + "candidate_value": "소송행위(litigation_acts)", + "downstream_role": "legal_effect_roles" + } + ], + "conflict_flags": [ + "candidate_types_map_to_different_downstream_fact_roles" + ], + "compact_context_class": [ + "state", + "succession_notice_lien_asset_defense", + "소송행위(litigation_acts)" + ], + "allowed_decisions": [ + "SELECT_REPRESENTATIVE_TYPE", + "PRESERVE_MULTI_ROUTING", + "SPLIT_STRUCTURE_SEED", + "BLOCK_REVIEW" + ], + "merged_from_count": 2 + } + ] + } + } +} diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B1_Money_Successor.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B1_Money_Successor.json new file mode 100644 index 00000000..86d781ee --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B1_Money_Successor.json @@ -0,0 +1,1549 @@ +{ + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B1_Money_Successor", + "status": "READY_WITH_REVIEW", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B1_Money_Successor", + "domain_label": "금전채권 및 상호속용 책임", + "slice_guard": { + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": "2026-07-10T07:27:06.635927+00:00", + "input_digests_sha256": { + "client_meeting.md": "cb95320487cf2bc7a02cecd6324743e45677de84f7303c7e6087b47481577fa8", + "evidence_event_candidates.json": "666c3167a4058df9b4e98fd4bb0504e3823befb7d30b8b32cc319554dbc347e0", + "evidence_indexed.json": "2f7e3a3fdd1682e853aad08242a4ddf009c0d6cb066b83845744f75770f8272a", + "Default_Agent/Juristic_Act.md": "c3cd23f52d6fed5168f95074e801a381b3fdea607ba50a60247666bbb6a82e49" + }, + "source_universe": { + "meeting_clause_ids": [ + "s01_consultation:1-나", + "s01_consultation:2-가", + "s01_consultation:2-나", + "s01_consultation:2-다", + "s01_consultation:2-라", + "s01_consultation:2-사", + "s01_consultation:3-나", + "s01_consultation:4-가", + "s01_consultation:4-다", + "s01_consultation:5-다", + "s02_party_assertions:1", + "s02_party_assertions:4-가", + "s03_client_requests:1", + "s03_client_requests:3" + ], + "event_candidate_ids": [ + "EVT-003-02", + "EVT-007-01", + "EVT-007-02", + "EVT-010-03", + "EVT-011-03", + "EVT-020-01", + "EVT-020-02", + "EVT-020-03", + "EVT-021-01", + "EVT-021-02", + "EVT-024-01", + "EVT-024-02", + "EVT-025-01", + "EVT-026-01", + "EVT-027-01", + "EVT-027-02", + "EVT-028-01" + ], + "evidence_indexes": [ + "E-003", + "E-007", + "E-010", + "E-011", + "E-020", + "E-021", + "E-024", + "E-025", + "E-026", + "E-027", + "E-028" + ] + }, + "stage_b_worker_contract": { + "context_is_read_only": true, + "bo_id_finalization_allowed": false, + "candidate_generation_allowed": true + } + } + }, + "transport_candidate_refs": [ + { + "candidate_ref": "B1:001", + "payload_index": 0, + "source_event_candidate_ids": [ + "EVT-027-01", + "EVT-027-02" + ], + "source_evidence_indexes": [ + "E-021", + "E-027" + ], + "source_meeting_clause_ids": [ + "s01_consultation:1-나", + "s03_client_requests:1" + ] + }, + { + "candidate_ref": "B1:002", + "payload_index": 1, + "source_event_candidate_ids": [ + "EVT-021-01", + "EVT-021-02" + ], + "source_evidence_indexes": [ + "E-021" + ], + "source_meeting_clause_ids": [] + }, + { + "candidate_ref": "B1:003", + "payload_index": 2, + "source_event_candidate_ids": [ + "EVT-027-01", + "EVT-027-02" + ], + "source_evidence_indexes": [ + "E-027" + ], + "source_meeting_clause_ids": [ + "s01_consultation:1-나" + ] + }, + { + "candidate_ref": "B1:004", + "payload_index": 3, + "source_event_candidate_ids": [ + "EVT-020-01", + "EVT-020-02", + "EVT-020-03" + ], + "source_evidence_indexes": [ + "E-020" + ], + "source_meeting_clause_ids": [] + }, + { + "candidate_ref": "B1:005", + "payload_index": 4, + "source_event_candidate_ids": [ + "EVT-028-01" + ], + "source_evidence_indexes": [ + "E-028" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:1" + ] + }, + { + "candidate_ref": "B1:006", + "payload_index": 5, + "source_event_candidate_ids": [ + "EVT-024-01" + ], + "source_evidence_indexes": [ + "E-024" + ], + "source_meeting_clause_ids": [] + }, + { + "candidate_ref": "B1:007", + "payload_index": 6, + "source_event_candidate_ids": [ + "EVT-024-02" + ], + "source_evidence_indexes": [ + "E-024" + ], + "source_meeting_clause_ids": [ + "s01_consultation:3-나", + "s03_client_requests:3" + ] + }, + { + "candidate_ref": "B1:008", + "payload_index": 7, + "source_event_candidate_ids": [ + "EVT-025-01" + ], + "source_evidence_indexes": [ + "E-025" + ], + "source_meeting_clause_ids": [] + }, + { + "candidate_ref": "B1:009", + "payload_index": 8, + "source_event_candidate_ids": [ + "EVT-026-01" + ], + "source_evidence_indexes": [ + "E-026" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-나" + ] + }, + { + "candidate_ref": "B1:010", + "payload_index": 9, + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [ + "s01_consultation:2-다" + ] + }, + { + "candidate_ref": "B1:011", + "payload_index": 10, + "source_event_candidate_ids": [ + "EVT-007-01", + "EVT-007-02" + ], + "source_evidence_indexes": [ + "E-007" + ], + "source_meeting_clause_ids": [] + }, + { + "candidate_ref": "B1:012", + "payload_index": 11, + "source_event_candidate_ids": [ + "EVT-011-03" + ], + "source_evidence_indexes": [ + "E-011" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-가", + "s02_party_assertions:4-나" + ] + }, + { + "candidate_ref": "B1:013", + "payload_index": 12, + "source_event_candidate_ids": [ + "EVT-010-03", + "EVT-003-02" + ], + "source_evidence_indexes": [ + "E-010", + "E-003" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:3-나" + ] + } + ], + "bo_seed_candidates": [ + { + "candidate_ref": "B1:001", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "매매" + }, + "Action": "인테리어 자재 매매에 따른 물품대금 채권 발생", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "물품대금", + "매매" + ], + "core_field_base": { + "date": "2021-06-05", + "amount": "200000000", + "parties": [ + "강용원", + "김선웅" + ], + "asset_refs": [], + "legal_effect_summary": "강용원의 김선웅에 대한 2억원 물품대금 채권" + }, + "amount": { + "value": "200000000", + "currency": "KRW", + "basis": null + }, + "source_evidence_indexes": [ + "E-021", + "E-027" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-027-01", + "EVT-027-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:1-나", + "s03_client_requests:1" + ], + "source_evidence_indexes": [ + "E-021", + "E-027" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "물품대금 채권 발생", + "summary_hint": "김선웅에 대한 인테리어 자재대금", + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [ + "강용원(채권자)", + "김선웅(채무자)" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": "물품대금", + "principal_amount_hint": "200,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": "강용원", + "debtor_candidate": "김선웅" + }, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + }, + { + "candidate_ref": "B1:002", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "채무승인" + }, + "Action": "물품대금 채무 승인 및 변제기 유예", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "채무승인", + "변제기 유예" + ], + "core_field_base": { + "date": "2024-08-05", + "amount": null, + "parties": [ + "강용원", + "김선웅" + ], + "asset_refs": [], + "legal_effect_summary": "물품대금 채무 인정 및 2024. 8. 31. 까지 변제 약정" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-021" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-021-01", + "EVT-021-02" + ], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [ + "E-021" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "채무승인", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": "강용원", + "debtor_candidate": "김선웅" + }, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [ + "2024. 8. 5. 자 이행각서를 통한 채무 승인" + ], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + }, + { + "candidate_ref": "B1:003", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "최고" + }, + "Action": "물품대금 지급 최고", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "이행최고", + "통지" + ], + "core_field_base": { + "date": "2024-05-04", + "amount": null, + "parties": [ + "강용원", + "김선웅" + ], + "asset_refs": [], + "legal_effect_summary": "물품대금 지급 이행최고 도달" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-027" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-027-01", + "EVT-027-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:1-나" + ], + "source_evidence_indexes": [ + "E-027" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "최고통지", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": null, + "debtor_candidate": null + }, + "payment_history": [], + "demand_or_notice_hints": [ + "2024. 5. 4. 자 지급 최고서 발송", + "2024. 5. 6. 도달" + ], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + }, + { + "candidate_ref": "B1:004", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "영업양도" + }, + "Action": "해드림 영업양도 및 상호속용 약정", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "영업양도", + "상호속용" + ], + "core_field_base": { + "date": "2024-07-05", + "amount": "200000000", + "parties": [ + "김선웅", + "오민한" + ], + "asset_refs": [], + "legal_effect_summary": "영업양도에 따른 상호속용인의 책임 발생" + }, + "amount": { + "value": "200000000", + "currency": "KRW", + "basis": null + }, + "source_evidence_indexes": [ + "E-020" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-020-01", + "EVT-020-02", + "EVT-020-03" + ], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [ + "E-020" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "영업양도", + "summary_hint": "영업양도에 따른 양수인의 상호속용 책임", + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [ + "김선웅(양도인)", + "오민한(양수인)" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": null, + "debtor_candidate": null + }, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [ + "해드림(SUN-DREAM) 영업양도 및 상호속용" + ], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + }, + { + "candidate_ref": "B1:005", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "상호속용에 따른 물품대금 채무 책임 부인 통지", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "통지", + "책임부인" + ], + "core_field_base": { + "date": "2024-12-27", + "amount": null, + "parties": [ + "오민한", + "강용원" + ], + "asset_refs": [], + "legal_effect_summary": "양수인의 책임 배제 및 소멸시효 주장 통지" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-028" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-028-01" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:1" + ], + "source_evidence_indexes": [ + "E-028" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "책임 부인 통지", + "summary_hint": null, + "conflict_patterns": [ + "양수인 책임 부인", + "소멸시효 항변" + ], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": null, + "debtor_candidate": null + }, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": "채무 시효 소멸 및 책임 배제 약정 존재 여부 불확실", + "conflict_flags": [ + "source_conflict" + ] + } + } + }, + { + "candidate_ref": "B1:006", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "소비대차" + }, + "Action": "1억 원 대여금 채권 발생 (2022.4.18.)", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "대여금", + "소비대차" + ], + "core_field_base": { + "date": "2022-04-18", + "amount": "100000000", + "parties": [ + "강용원", + "오국한" + ], + "asset_refs": [], + "legal_effect_summary": "강용원의 오국한에 대한 대여금 채권" + }, + "amount": { + "value": "100000000", + "currency": "KRW", + "basis": null + }, + "source_evidence_indexes": [ + "E-024" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-024-01" + ], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [ + "E-024" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "대여금 채권 발생", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [ + "강용원(대주)", + "오국한(차주)" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": "대여금", + "principal_amount_hint": "100,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": "월 1%", + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [ + "E-024" + ], + "unpaid_balance_hint": null, + "creditor_candidate": "강용원", + "debtor_candidate": "오국한" + }, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + }, + { + "candidate_ref": "B1:007", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "소비대차" + }, + "Action": "1억 원 대여금 채권 발생 (2022.5.18.)", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "대여금", + "소비대차" + ], + "core_field_base": { + "date": "2022-05-18", + "amount": "100000000", + "parties": [ + "강용원", + "오국한" + ], + "asset_refs": [], + "legal_effect_summary": "강용원의 오국한에 대한 추가 대여금 채권" + }, + "amount": { + "value": "100000000", + "currency": "KRW", + "basis": null + }, + "source_evidence_indexes": [ + "E-024" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-024-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:3-나", + "s03_client_requests:3" + ], + "source_evidence_indexes": [ + "E-024" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "대여금 채권 발생", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [ + "강용원(대주)", + "오국한(차주)" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": "대여금", + "principal_amount_hint": "100,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": "월 1%", + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [ + "E-024" + ], + "unpaid_balance_hint": null, + "creditor_candidate": "강용원", + "debtor_candidate": "오국한" + }, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + }, + { + "candidate_ref": "B1:008", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "소비대차" + }, + "Action": "2억 원 대여금 채권 발생 (2023.1.6.)", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "대여금", + "소비대차" + ], + "core_field_base": { + "date": "2023-01-06", + "amount": "200000000", + "parties": [ + "오민한", + "강용원" + ], + "asset_refs": [], + "legal_effect_summary": "오민한의 강용원에 대한 대여금 채권" + }, + "amount": { + "value": "200000000", + "currency": "KRW", + "basis": null + }, + "source_evidence_indexes": [ + "E-025" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-025-01" + ], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [ + "E-025" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "대여금 채권 발생", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [ + "오민한(대주)", + "강용원(차주)" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": "대여금", + "principal_amount_hint": "200,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": "오민한", + "debtor_candidate": "강용원" + }, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + }, + { + "candidate_ref": "B1:009", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "소비대차" + }, + "Action": "3억 원 대여금 채권 발생 (2023.7.6.)", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "대여금", + "소비대차" + ], + "core_field_base": { + "date": "2023-07-06", + "amount": "300000000", + "parties": [ + "오민한", + "강용원" + ], + "asset_refs": [], + "legal_effect_summary": "오민한의 강용원에 대한 대여금 채권" + }, + "amount": { + "value": "300000000", + "currency": "KRW", + "basis": null + }, + "source_evidence_indexes": [ + "E-026" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-026-01" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-나" + ], + "source_evidence_indexes": [ + "E-026" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "대여금 채권 발생", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [ + "오민한(대주)", + "강용원(차주)" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": "대여금", + "principal_amount_hint": "300,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": "연 15%", + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [ + "E-026" + ], + "unpaid_balance_hint": null, + "creditor_candidate": "오민한", + "debtor_candidate": "강용원" + }, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + }, + { + "candidate_ref": "B1:010", + "BOType": "event", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": "변제" + }, + "Action": "차용금 변제 명목 4억 원 지급 (채무 미특정)", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "변제", + "법정변제충당" + ], + "core_field_base": { + "date": "2024-07-05", + "amount": "400000000", + "parties": [ + "강용원", + "오민한" + ], + "asset_refs": [], + "legal_effect_summary": "4억원 일부 변제 및 변제충당 문제" + }, + "amount": { + "value": "400000000", + "currency": "KRW", + "basis": null + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [ + "s01_consultation:2-다" + ], + "source_evidence_indexes": [] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "일부 변제", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [ + "강용원(변제자)", + "오민한(수령자)" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": null, + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": null, + "debtor_candidate": null + }, + "payment_history": [ + "2024. 7. 5. 오민한에게 위 차용금 변제로 4억 원 지급, 채무 특정 안됨" + ], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [ + "cross_domain_merge_needed" + ] + } + } + }, + { + "candidate_ref": "B1:011", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "도급" + }, + "Action": "평택시 빌라 신축 공사도급계약 체결 및 중도금 지급", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "도급", + "공사대금" + ], + "core_field_base": { + "date": "2021-05-01", + "amount": "1000000000", + "parties": [ + "박광윤", + "김일동" + ], + "asset_refs": [], + "legal_effect_summary": "공사대금 10억원 도급계약 및 4억원 중도금 지급" + }, + "amount": { + "value": "1000000000", + "currency": "KRW", + "basis": null + }, + "source_evidence_indexes": [ + "E-007" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-007-01", + "EVT-007-02" + ], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [ + "E-007" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "도급계약", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [ + "박광윤(수급인)", + "김일동(도급인)" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": "공사대금", + "principal_amount_hint": "1,000,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": "박광윤", + "debtor_candidate": "김일동" + }, + "payment_history": [ + "2021. 8. 30. 중도금 4억원 지급" + ], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + }, + { + "candidate_ref": "B1:012", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "공사대금 잔금 3억 원 미지급 상태 및 유치권 충당 항변", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "미지급", + "충당" + ], + "core_field_base": { + "date": null, + "amount": "300000000", + "parties": [ + "박광윤", + "김일동" + ], + "asset_refs": [], + "legal_effect_summary": "공사대금 잔금 3억원 채권 존재 항변" + }, + "amount": { + "value": "300000000", + "currency": "KRW", + "basis": null + }, + "source_evidence_indexes": [ + "E-011" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-011-03" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-가", + "s02_party_assertions:4-나" + ], + "source_evidence_indexes": [ + "E-011" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "잔금 미지급", + "summary_hint": null, + "conflict_patterns": [ + "차임 충당 항변" + ], + "asset_bindings": [], + "parties_hint": [ + "박광윤(채권자)", + "김일동(채무자)" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": "공사대금", + "principal_amount_hint": null, + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": "300,000,000원", + "creditor_candidate": "박광윤", + "debtor_candidate": "김일동" + }, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + }, + { + "candidate_ref": "B1:013", + "BOType": "event", + "ActionType": "소송행위(litigation acts)", + "JuristicAct": { + "label": "상계" + }, + "Action": "오국한에 대한 대여금 채권 등을 자동채권으로 한 상계 항변", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "상계", + "항변" + ], + "core_field_base": { + "date": "2024-09-30", + "amount": "50000000", + "parties": [ + "오민한", + "강용원" + ], + "asset_refs": [], + "legal_effect_summary": "오민한의 5천만원 채권에 기한 상계 항변" + }, + "amount": { + "value": "50000000", + "currency": "KRW", + "basis": null + }, + "source_evidence_indexes": [ + "E-003", + "E-010" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-010-03", + "EVT-003-02" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:3-나" + ], + "source_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": "상계 항변", + "summary_hint": null, + "conflict_patterns": [ + "상계 주장" + ], + "asset_bindings": [], + "parties_hint": [ + "오민한", + "강용원" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": { + "claim_type_candidate": "대여금", + "principal_amount_hint": "50,000,000원", + "interest_hint": null, + "delay_damage_hint": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "interest_accrual_start_hint": null, + "interest_rate_source_evidence_refs": [], + "unpaid_balance_hint": null, + "creditor_candidate": "오민한", + "debtor_candidate": null + }, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": null, + "conflict_flags": [] + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B1:review:001", + "severity": "HARD_WARNING", + "issue_type": "legal_theory_required", + "description": "강용원이 오민한에게 4억 원을 지급하였으나 변제할 채무를 특정하지 않아 법정변제충당 계산 필요 (2억 원 차용 및 3억 원 차용)", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [ + "s01_consultation:2-다" + ], + "recommended_downstream_owner": "C0" + }, + { + "review_id": "B1:review:002", + "severity": "SOFT_WARNING", + "issue_type": "source_conflict", + "description": "오민한은 영업양수인으로서의 책임을 부인하며 소멸시효 완성 및 책임배제 약정을 주장함", + "source_event_candidate_ids": [ + "EVT-028-01" + ], + "source_evidence_indexes": [ + "E-028" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:1" + ], + "recommended_downstream_owner": "C5" + }, + { + "review_id": "B1:review:003", + "severity": "SOFT_WARNING", + "issue_type": "legal_theory_required", + "description": "오국한에 대한 대여금 청구의 소 제기는 희망하지 않음 (의뢰인 희망사항)", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [ + "s03_client_requests:3" + ], + "recommended_downstream_owner": "C0" + } + ], + "handoff_guard": { + "schema_version": "task_c_bo_stage_b_seed_handoff_guard.v1", + "handoff_target": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B1_Money_Successor.json", + "publisher_task": "Task_C_BO_Stage_B_worker_output_publisher", + "source_worker_task": "Task_C_BO_Stage_B_B1_Money_Successor", + "source_task": "B1_Money_Successor", + "source_contract": "direct_prev_task_json_output.stage_b_domain_bo_seed_output", + "source_prev_path": "prev.Task_C_BO_Stage_B_B1_Money_Successor.json_output", + "domain_id": "B1_Money_Successor", + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": "2026-07-10T07:27:06.635927+00:00", + "input_digests_sha256": { + "client_meeting.md": "cb95320487cf2bc7a02cecd6324743e45677de84f7303c7e6087b47481577fa8", + "evidence_event_candidates.json": "666c3167a4058df9b4e98fd4bb0504e3823befb7d30b8b32cc319554dbc347e0", + "evidence_indexed.json": "2f7e3a3fdd1682e853aad08242a4ddf009c0d6cb066b83845744f75770f8272a", + "Default_Agent/Juristic_Act.md": "c3cd23f52d6fed5168f95074e801a381b3fdea607ba50a60247666bbb6a82e49" + } + } + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B2_Secured_Registry.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B2_Secured_Registry.json new file mode 100644 index 00000000..92884bd8 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B2_Secured_Registry.json @@ -0,0 +1,1214 @@ +{ + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B2_Secured_Registry", + "status": "READY_WITH_REVIEW", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B2_Secured_Registry", + "domain_label": "담보채무, 변제충당, 등기무효, 경매", + "slice_guard": { + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": "2026-07-10T07:27:06.635927+00:00", + "input_digests_sha256": { + "client_meeting.md": "cb95320487cf2bc7a02cecd6324743e45677de84f7303c7e6087b47481577fa8", + "evidence_event_candidates.json": "666c3167a4058df9b4e98fd4bb0504e3823befb7d30b8b32cc319554dbc347e0", + "evidence_indexed.json": "2f7e3a3fdd1682e853aad08242a4ddf009c0d6cb066b83845744f75770f8272a", + "Default_Agent/Juristic_Act.md": "c3cd23f52d6fed5168f95074e801a381b3fdea607ba50a60247666bbb6a82e49" + }, + "source_universe": { + "meeting_clause_ids": [ + "s01_consultation:2-가", + "s01_consultation:2-나", + "s01_consultation:2-다", + "s01_consultation:2-라", + "s01_consultation:2-마", + "s01_consultation:2-사", + "s01_consultation:3-가", + "s01_consultation:3-나", + "s01_consultation:3-다", + "s01_consultation:3-라", + "s01_consultation:4-가", + "s01_consultation:4-나", + "s01_consultation:5-다", + "s02_party_assertions:2", + "s02_party_assertions:4-나" + ], + "event_candidate_ids": [ + "EVT-001-01", + "EVT-001-02", + "EVT-002-01", + "EVT-003-01", + "EVT-003-02", + "EVT-004-01", + "EVT-004-02", + "EVT-005-01", + "EVT-006-01", + "EVT-006-02", + "EVT-006-03", + "EVT-006-04", + "EVT-006-05", + "EVT-006-06", + "EVT-006-07", + "EVT-007-01", + "EVT-007-02", + "EVT-008-01", + "EVT-009-01", + "EVT-009-02", + "EVT-009-03", + "EVT-010-01", + "EVT-010-02", + "EVT-010-03", + "EVT-011-01", + "EVT-011-02", + "EVT-011-03", + "EVT-012-01", + "EVT-012-02", + "EVT-013-01", + "EVT-013-02", + "EVT-013-03", + "EVT-013-04", + "EVT-013-05", + "EVT-013-06", + "EVT-014-01", + "EVT-014-02", + "EVT-014-03", + "EVT-014-04", + "EVT-014-05", + "EVT-014-06", + "EVT-015-01", + "EVT-015-02", + "EVT-015-03", + "EVT-015-04", + "EVT-017-01", + "EVT-017-02", + "EVT-017-03", + "EVT-017-04", + "EVT-018-01", + "EVT-020-01", + "EVT-020-02", + "EVT-020-03", + "EVT-021-01", + "EVT-021-02", + "EVT-022-01", + "EVT-022-02", + "EVT-022-03", + "EVT-023-01", + "EVT-024-01", + "EVT-024-02", + "EVT-025-01", + "EVT-025-02", + "EVT-026-01", + "EVT-026-02", + "EVT-027-01", + "EVT-027-02", + "EVT-028-01", + "EVT-029-01", + "EVT-030-01", + "EVT-030-02" + ], + "evidence_indexes": [ + "E-001", + "E-002", + "E-003", + "E-004", + "E-005", + "E-006", + "E-007", + "E-008", + "E-009", + "E-010", + "E-011", + "E-012", + "E-013", + "E-014", + "E-015", + "E-016", + "E-017", + "E-018", + "E-019", + "E-020", + "E-021", + "E-022", + "E-023", + "E-024", + "E-025", + "E-026", + "E-027", + "E-028", + "E-029", + "E-030" + ] + }, + "stage_b_worker_contract": { + "context_is_read_only": true, + "bo_id_finalization_allowed": false, + "candidate_generation_allowed": true + } + } + }, + "transport_candidate_refs": [ + { + "candidate_ref": "B2:001", + "payload_index": 0, + "source_event_candidate_ids": [ + "EVT-013-01", + "EVT-013-02", + "EVT-025-02", + "EVT-026-02" + ], + "source_evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-가", + "s01_consultation:2-나" + ] + }, + { + "candidate_ref": "B2:002", + "payload_index": 1, + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [ + "s01_consultation:2-다" + ] + }, + { + "candidate_ref": "B2:003", + "payload_index": 2, + "source_event_candidate_ids": [ + "EVT-013-03", + "EVT-013-04", + "EVT-013-05", + "EVT-013-06", + "EVT-012-01", + "EVT-012-02" + ], + "source_evidence_indexes": [ + "E-012", + "E-013" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-라", + "s01_consultation:2-마", + "s02_party_assertions:2" + ] + }, + { + "candidate_ref": "B2:004", + "payload_index": 3, + "source_event_candidate_ids": [ + "EVT-014-01", + "EVT-014-02", + "EVT-014-03", + "EVT-014-04", + "EVT-014-05", + "EVT-014-06", + "EVT-010-03" + ], + "source_evidence_indexes": [ + "E-010", + "E-014" + ], + "source_meeting_clause_ids": [ + "s01_consultation:3-가", + "s01_consultation:3-라" + ] + }, + { + "candidate_ref": "B2:005", + "payload_index": 4, + "source_event_candidate_ids": [ + "EVT-011-03", + "EVT-007-02" + ], + "source_evidence_indexes": [ + "E-007", + "E-011" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-가", + "s01_consultation:4-나", + "s02_party_assertions:4-나" + ] + }, + { + "candidate_ref": "B2:006", + "payload_index": 5, + "source_event_candidate_ids": [ + "EVT-015-01", + "EVT-015-02", + "EVT-015-03", + "EVT-015-04" + ], + "source_evidence_indexes": [ + "E-015" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-나" + ] + }, + { + "candidate_ref": "B2:007", + "payload_index": 6, + "source_event_candidate_ids": [ + "EVT-029-01" + ], + "source_evidence_indexes": [ + "E-029" + ], + "source_meeting_clause_ids": [ + "s01_consultation:5-다" + ] + } + ], + "bo_seed_candidates": [ + { + "candidate_ref": "B2:001", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "(근)저당권 설정 계약" + }, + "Action": "강용원은 성수동 대지에 대한 상속 및 증여로 지분을 취득하고, 오민한에게 차용금 담보를 위해 성수동 대지 지분에 대한 근저당권 설정 약정을 체결함", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "상속", + "증여", + "근저당권설정" + ], + "core_field_base": { + "date": "2023-01-06", + "amount": null, + "parties": [ + "강용원", + "정유심", + "오민한" + ], + "asset_refs": [ + "성수동 대지" + ], + "legal_effect_summary": "소유권 지분 이전 및 근저당권 설정" + }, + "amount": { + "value": null, + "currency": "KRW", + "basis": null + }, + "source_evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-013-01", + "EVT-013-02", + "EVT-025-02", + "EVT-026-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-가", + "s01_consultation:2-나" + ], + "source_evidence_indexes": [ + "E-013", + "E-025", + "E-026" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "title_hint": "성수동 대지 소유권 변동 및 근저당권 설정", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [ + "성수동 대지" + ], + "parties_hint": [ + "강용원", + "정유심", + "오민한" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [ + "secured_debt_creation" + ], + "event_state_type": "event", + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "registry_rows": [ + { + "registry_ref": "REG-013-01", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제1453호" + }, + { + "registry_ref": "REG-013-02", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제7026호" + }, + { + "registry_ref": "REG-013-03", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제11202호" + }, + { + "registry_ref": "REG-013-04", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제7202호" + }, + { + "registry_ref": "REG-013-05", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제12402호" + }, + { + "registry_ref": "REG-013-06", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제25797호" + }, + { + "registry_ref": "REG-013-07", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제25799호" + } + ], + "fractional_effect_events": [ + { + "fraction_ref": null, + "fraction_display": "3/5", + "fraction_numerator": null, + "fraction_denominator": null, + "effect_kind_hint": "transfer", + "source_event_candidate_ids": [ + "EVT-013-02" + ], + "registry_refs": [ + "REG-013-02" + ], + "evidence_indexes": [ + "E-013" + ] + } + ], + "secured_debt_chain": [ + "EVT-025-02", + "EVT-026-02" + ], + "payment_allocation": [], + "mortgage_extinguishment_hints": [], + "auction_steps": [], + "dividend_distribution_hints": [], + "derivative_invalidity_hints": [], + "registry_object_display": "성수동 대지 등기" + } + } + }, + { + "candidate_ref": "B2:002", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "변제 및 변제충당" + }, + "Action": "강용원이 오민한에게 4억 원을 지급하였으나 다수의 채무 중 충당할 채무를 특정하지 않음", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "변제", + "변제충당" + ], + "core_field_base": { + "date": "2024-07-05", + "amount": "400,000,000", + "parties": [ + "강용원", + "오민한" + ], + "asset_refs": [], + "legal_effect_summary": "4억 원 변제 및 변제충당의 불특정" + }, + "amount": { + "value": "400000000", + "currency": "KRW", + "basis": "변제금액" + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [ + "s01_consultation:2-다" + ], + "source_evidence_indexes": [] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "title_hint": "변제에 따른 채무 소멸 및 지정충당 부존재", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [ + "강용원", + "오민한" + ], + "temporal_hint": "2024-07-05", + "hazard_flags": [ + "conflict_pattern" + ], + "legal_effect_tags": [ + "payment_allocation_uncertain" + ], + "event_state_type": "event", + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "registry_rows": [], + "fractional_effect_events": [], + "secured_debt_chain": [], + "payment_allocation": [ + { + "total_paid": "400,000,000", + "unspecified_allocation": true + } + ], + "mortgage_extinguishment_hints": [], + "auction_steps": [], + "dividend_distribution_hints": [], + "derivative_invalidity_hints": [], + "registry_object_display": null + } + } + }, + { + "candidate_ref": "B2:003", + "BOType": "event", + "ActionType": "소송행위(litigation acts)", + "JuristicAct": { + "label": "담보권실행을 위한 경매 및 근저당권 설정" + }, + "Action": "오민한이 경매를 신청하여 이문호가 매각대금을 완납하고 취득한 뒤, 대한은행이 근저당권을 설정함에 따라 원인무효 여부 및 공신력이 다투어짐", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "임의경매", + "공신력", + "등기무효" + ], + "core_field_base": { + "date": "2024-10-05", + "amount": null, + "parties": [ + "오민한", + "이문호", + "주식회사 대한은행", + "강용원" + ], + "asset_refs": [ + "성수동 대지" + ], + "legal_effect_summary": "경매 매각에 따른 소유권 취득 및 대한은행 근저당 무효 분쟁" + }, + "amount": { + "value": null, + "currency": "KRW", + "basis": null + }, + "source_evidence_indexes": [ + "E-012", + "E-013" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-013-03", + "EVT-013-04", + "EVT-013-05", + "EVT-013-06", + "EVT-012-01", + "EVT-012-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-라", + "s01_consultation:2-마", + "s02_party_assertions:2" + ], + "source_evidence_indexes": [ + "E-012", + "E-013" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "title_hint": "성수동 대지 경매 및 대한은행 근저당권 설정 공신력 항변", + "summary_hint": null, + "conflict_patterns": [ + "무효 주장", + "공신력 신뢰 주장" + ], + "asset_bindings": [ + "성수동 대지" + ], + "parties_hint": [ + "오민한", + "이문호", + "주식회사 대한은행" + ], + "temporal_hint": "2024-10-05", + "hazard_flags": [ + "conflict_pattern" + ], + "legal_effect_tags": [ + "auction_invalidity", + "derivative_bank_mortgage_invalidity" + ], + "event_state_type": "event", + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "registry_rows": [], + "fractional_effect_events": [], + "secured_debt_chain": [], + "payment_allocation": [], + "mortgage_extinguishment_hints": [ + "EVT-013-05" + ], + "auction_steps": [ + "EVT-013-03", + "EVT-013-04" + ], + "dividend_distribution_hints": [], + "derivative_invalidity_hints": [ + "EVT-013-06", + "EVT-012-01", + "EVT-012-02" + ], + "registry_object_display": "성수동 대지 경매 진행내역" + } + } + }, + { + "candidate_ref": "B2:004", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "근저당권 말소 및 상계" + }, + "Action": "신림동 상가에 강용원(등기상 강용일) 명의의 근저당권이 설정되었다가 무단 말소되었으며, 오민한은 상계를 주장함", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "근저당권설정", + "말소", + "상계" + ], + "core_field_base": { + "date": "2024-10-17", + "amount": null, + "parties": [ + "강용원", + "오국한", + "오민한", + "이경주", + "나임오" + ], + "asset_refs": [ + "신림동 상가" + ], + "legal_effect_summary": "신림동 상가 담보 설정, 무단 말소 및 상계 항변" + }, + "amount": { + "value": null, + "currency": "KRW", + "basis": null + }, + "source_evidence_indexes": [ + "E-010", + "E-014" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-014-01", + "EVT-014-02", + "EVT-014-03", + "EVT-014-04", + "EVT-014-05", + "EVT-014-06", + "EVT-010-03" + ], + "source_meeting_clause_ids": [ + "s01_consultation:3-가", + "s01_consultation:3-라" + ], + "source_evidence_indexes": [ + "E-010", + "E-014" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "title_hint": "신림동 상가 근저당권 등기 말소 및 상계 다툼", + "summary_hint": null, + "conflict_patterns": [ + "근저당권 무단 말소", + "상계 주장" + ], + "asset_bindings": [ + "신림동 상가" + ], + "parties_hint": [ + "강용원", + "오국한", + "오민한" + ], + "temporal_hint": null, + "hazard_flags": [ + "conflict_pattern" + ], + "legal_effect_tags": [ + "secured_debt_chain", + "mortgage_extinguishment" + ], + "event_state_type": "event", + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "registry_rows": [ + { + "registry_ref": "REG-014-01", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제24759호" + }, + { + "registry_ref": "REG-014-02", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제56457호" + }, + { + "registry_ref": "REG-014-03", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제11593호" + }, + { + "registry_ref": "REG-014-04", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제50099호" + }, + { + "registry_ref": "REG-014-05", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제26597호" + }, + { + "registry_ref": "REG-014-06", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제74586호" + } + ], + "fractional_effect_events": [], + "secured_debt_chain": [ + "EVT-014-04", + "EVT-014-05" + ], + "payment_allocation": [], + "mortgage_extinguishment_hints": [ + "EVT-014-06", + "EVT-010-03" + ], + "auction_steps": [], + "dividend_distribution_hints": [], + "derivative_invalidity_hints": [], + "registry_object_display": "신림동 상가 등기" + } + } + }, + { + "candidate_ref": "B2:005", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "변제충당 주장" + }, + "Action": "박광윤이 평택시 빌라를 점유하며 얻은 임대 및 거주 수익을 피담보채권에 충당하였다고 항변함", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "유치권", + "점유", + "변제충당" + ], + "core_field_base": { + "date": null, + "amount": null, + "parties": [ + "박광윤", + "김일동", + "정남이", + "강호연" + ], + "asset_refs": [ + "평택시 빌라" + ], + "legal_effect_summary": "유치권 행사 중 발생한 과실에 의한 피담보채무 변제충당 주장" + }, + "amount": { + "value": null, + "currency": "KRW", + "basis": null + }, + "source_evidence_indexes": [ + "E-007", + "E-011" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-011-03", + "EVT-007-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-가", + "s01_consultation:4-나", + "s02_party_assertions:4-나" + ], + "source_evidence_indexes": [ + "E-007", + "E-011" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "title_hint": "평택시 빌라 임대수익의 피담보채무 변제충당 항변", + "summary_hint": null, + "conflict_patterns": [ + "수익 충당 항변" + ], + "asset_bindings": [ + "평택시 빌라" + ], + "parties_hint": [ + "박광윤" + ], + "temporal_hint": null, + "hazard_flags": [ + "conflict_pattern" + ], + "legal_effect_tags": [ + "payment_allocation" + ], + "event_state_type": "state", + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "registry_rows": [], + "fractional_effect_events": [], + "secured_debt_chain": [], + "payment_allocation": [ + { + "allocation_source": "거주 및 임대 수익", + "target_debt": "공사대금 피담보채권" + } + ], + "mortgage_extinguishment_hints": [], + "auction_steps": [], + "dividend_distribution_hints": [], + "derivative_invalidity_hints": [], + "registry_object_display": null + } + } + }, + { + "candidate_ref": "B2:006", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "소유권이전등기" + }, + "Action": "평택시 서정빌라에 대한 보존등기 및 매매로 인한 소유권이전등기가 마쳐짐", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "소유권보존", + "소유권이전", + "가압류" + ], + "core_field_base": { + "date": "2022-10-01", + "amount": null, + "parties": [ + "김일동", + "정남이", + "강호연", + "김동국" + ], + "asset_refs": [ + "평택시 빌라" + ], + "legal_effect_summary": "평택시 빌라 소유권 변동 내역" + }, + "amount": { + "value": null, + "currency": "KRW", + "basis": null + }, + "source_evidence_indexes": [ + "E-015" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-015-01", + "EVT-015-02", + "EVT-015-03", + "EVT-015-04" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-나" + ], + "source_evidence_indexes": [ + "E-015" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "title_hint": "평택시 빌라 소유권 이전 및 등기 연혁", + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [ + "평택시 빌라" + ], + "parties_hint": [ + "김일동", + "정남이", + "강호연" + ], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": "event", + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "registry_rows": [ + { + "registry_ref": "REG-015-01", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제7233호" + }, + { + "registry_ref": "REG-015-02", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제7871호" + }, + { + "registry_ref": "REG-015-03", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제11593호" + }, + { + "registry_ref": "REG-015-04", + "fraction_ref": null, + "fraction_display": null, + "fraction_numerator": null, + "fraction_denominator": null, + "receipt_no": "제12321호" + } + ], + "fractional_effect_events": [], + "secured_debt_chain": [], + "payment_allocation": [], + "mortgage_extinguishment_hints": [], + "auction_steps": [], + "dividend_distribution_hints": [], + "derivative_invalidity_hints": [], + "registry_object_display": "서정빌라 집합건물 등기" + } + } + }, + { + "candidate_ref": "B2:007", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "제3자 명의 등기 요청" + }, + "Action": "이수인이 윤건우에게 매매계약에서 합의되지 않은 제3자 명의로의 등기이전을 일방적으로 요구함", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "소유권이전등기", + "요구", + "거절" + ], + "core_field_base": { + "date": "2024-03-30", + "amount": null, + "parties": [ + "이수인", + "윤건우" + ], + "asset_refs": [ + "흑석동 상가" + ], + "legal_effect_summary": "제3자 명의 소유권이전등기 무단 요청에 따른 분쟁" + }, + "amount": { + "value": null, + "currency": "KRW", + "basis": null + }, + "source_evidence_indexes": [ + "E-029" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-029-01" + ], + "source_meeting_clause_ids": [ + "s01_consultation:5-다" + ], + "source_evidence_indexes": [ + "E-029" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "title_hint": "흑석동 상가 제3자 명의 등기 요청 및 거절", + "summary_hint": null, + "conflict_patterns": [ + "등기 이전 요구 거절" + ], + "asset_bindings": [ + "흑석동 상가" + ], + "parties_hint": [ + "이수인", + "윤건우" + ], + "temporal_hint": "2024-03-30", + "hazard_flags": [ + "conflict_pattern" + ], + "legal_effect_tags": [ + "registry_defect_hint" + ], + "event_state_type": "event", + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "registry_rows": [], + "fractional_effect_events": [], + "secured_debt_chain": [], + "payment_allocation": [], + "mortgage_extinguishment_hints": [], + "auction_steps": [], + "dividend_distribution_hints": [], + "derivative_invalidity_hints": [], + "registry_object_display": null + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B2:review:001", + "severity": "SOFT_WARNING", + "issue_type": "amount_or_date_uncertain", + "description": "강용원이 오민한에게 4억 원을 변제하였으나 합의된 충당 내역이 없으므로 법정충당 순서 적용을 위한 사실관계(금리, 이행기 등) 확인 필요", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [ + "s01_consultation:2-다" + ], + "recommended_downstream_owner": "C2" + }, + { + "review_id": "B2:review:002", + "severity": "HARD_WARNING", + "issue_type": "source_conflict", + "description": "신림동 상가 1.5억 원 근저당권자가 상담내역(s01_consultation:3-가) 상으로는 '강용원'으로 되어있으나, 등기부(E-014) 상으로는 '강용일'로 기재되어 있어 명의신탁 여부 또는 단순 오기 여부 확인 필요", + "source_event_candidate_ids": [ + "EVT-014-05" + ], + "source_evidence_indexes": [ + "E-014" + ], + "source_meeting_clause_ids": [ + "s01_consultation:3-가" + ], + "recommended_downstream_owner": "C0" + }, + { + "review_id": "B2:review:003", + "severity": "SOFT_WARNING", + "issue_type": "amount_or_date_uncertain", + "description": "박광윤이 평택시 빌라 유치권 행사 중 발생한 거주 및 임대 수익을 공사대금 피담보채권에 충당하였다고 주장하나, 정확한 수익 금액 및 충당 계산 내역 불명확함", + "source_event_candidate_ids": [ + "EVT-011-03" + ], + "source_evidence_indexes": [ + "E-011" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:4-나" + ], + "recommended_downstream_owner": "C2" + } + ], + "handoff_guard": { + "schema_version": "task_c_bo_stage_b_seed_handoff_guard.v1", + "handoff_target": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B2_Secured_Registry.json", + "publisher_task": "Task_C_BO_Stage_B_worker_output_publisher", + "source_worker_task": "Task_C_BO_Stage_B_B2_Secured_Registry", + "source_task": "B2_Secured_Registry", + "source_contract": "direct_prev_task_json_output.stage_b_domain_bo_seed_output", + "source_prev_path": "prev.Task_C_BO_Stage_B_B2_Secured_Registry.json_output", + "domain_id": "B2_Secured_Registry", + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": "2026-07-10T07:27:06.635927+00:00", + "input_digests_sha256": { + "client_meeting.md": "cb95320487cf2bc7a02cecd6324743e45677de84f7303c7e6087b47481577fa8", + "evidence_event_candidates.json": "666c3167a4058df9b4e98fd4bb0504e3823befb7d30b8b32cc319554dbc347e0", + "evidence_indexed.json": "2f7e3a3fdd1682e853aad08242a4ddf009c0d6cb066b83845744f75770f8272a", + "Default_Agent/Juristic_Act.md": "c3cd23f52d6fed5168f95074e801a381b3fdea607ba50a60247666bbb6a82e49" + } + } + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B3_Land_Valuation.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B3_Land_Valuation.json new file mode 100644 index 00000000..7acd9083 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B3_Land_Valuation.json @@ -0,0 +1,891 @@ +{ + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B3_Land_Valuation", + "status": "READY_WITH_REVIEW", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B3_Land_Valuation", + "domain_label": "토지 사용이익, 점유, 사용수익, 가치평가", + "slice_guard": { + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": "2026-07-10T07:27:06.635927+00:00", + "input_digests_sha256": { + "client_meeting.md": "cb95320487cf2bc7a02cecd6324743e45677de84f7303c7e6087b47481577fa8", + "evidence_event_candidates.json": "666c3167a4058df9b4e98fd4bb0504e3823befb7d30b8b32cc319554dbc347e0", + "evidence_indexed.json": "2f7e3a3fdd1682e853aad08242a4ddf009c0d6cb066b83845744f75770f8272a", + "Default_Agent/Juristic_Act.md": "c3cd23f52d6fed5168f95074e801a381b3fdea607ba50a60247666bbb6a82e49" + }, + "source_universe": { + "meeting_clause_ids": [ + "s01_consultation:2", + "s01_consultation:2-바", + "s01_consultation:2-사", + "s01_consultation:4-가", + "s01_consultation:5-가", + "s01_consultation:5-나", + "s01_consultation:5-라", + "s02_party_assertions:2", + "s02_party_assertions:5", + "s03_client_requests:2", + "s03_client_requests:4" + ], + "event_candidate_ids": [ + "EVT-004-01", + "EVT-004-02", + "EVT-005-01", + "EVT-006-03", + "EVT-006-06", + "EVT-006-07", + "EVT-011-02", + "EVT-011-03", + "EVT-017-04", + "EVT-018-01", + "EVT-022-03", + "EVT-023-01" + ], + "evidence_indexes": [ + "E-004", + "E-005", + "E-006", + "E-011", + "E-017", + "E-018", + "E-022", + "E-023" + ] + }, + "stage_b_worker_contract": { + "context_is_read_only": true, + "bo_id_finalization_allowed": false, + "candidate_generation_allowed": true + } + } + }, + "transport_candidate_refs": [ + { + "candidate_ref": "B3:001", + "payload_index": 0, + "source_event_candidate_ids": [ + "EVT-004-01", + "EVT-004-02" + ], + "source_evidence_indexes": [ + "E-004" + ], + "source_meeting_clause_ids": [] + }, + { + "candidate_ref": "B3:002", + "payload_index": 1, + "source_event_candidate_ids": [ + "EVT-005-01" + ], + "source_evidence_indexes": [ + "E-005" + ], + "source_meeting_clause_ids": [] + }, + { + "candidate_ref": "B3:003", + "payload_index": 2, + "source_event_candidate_ids": [ + "EVT-023-01" + ], + "source_evidence_indexes": [ + "E-023" + ], + "source_meeting_clause_ids": [] + }, + { + "candidate_ref": "B3:004", + "payload_index": 3, + "source_event_candidate_ids": [ + "EVT-006-03", + "EVT-006-06", + "EVT-006-07" + ], + "source_evidence_indexes": [ + "E-006" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-바", + "s01_consultation:2-사" + ] + }, + { + "candidate_ref": "B3:005", + "payload_index": 4, + "source_event_candidate_ids": [ + "EVT-011-02", + "EVT-011-03" + ], + "source_evidence_indexes": [ + "E-011" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-가", + "s03_client_requests:4" + ] + }, + { + "candidate_ref": "B3:006", + "payload_index": 5, + "source_event_candidate_ids": [ + "EVT-017-04", + "EVT-018-01", + "EVT-022-03" + ], + "source_evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "source_meeting_clause_ids": [ + "s01_consultation:5-가", + "s01_consultation:5-나", + "s01_consultation:5-라" + ] + } + ], + "bo_seed_candidates": [ + { + "candidate_ref": "B3:001", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "신림로 상가 및 포천시 임야 가치평가(감정평가서)", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "가치평가", + "감정평가" + ], + "core_field_base": { + "date": "2024-09-13", + "amount": null, + "parties": [], + "asset_refs": [ + "SCH-016-03" + ], + "legal_effect_summary": "신림로 상가 및 포천시 임야의 가치(시가) 평가 상태" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-004" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-004-01", + "EVT-004-02" + ], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [ + "E-004" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "title_hint": "신림로 상가 및 포천시 임야 감정평가", + "summary_hint": "신림로 상가와 포천시 임야에 대한 감정평가 및 시가 산정", + "conflict_patterns": [], + "asset_bindings": [ + "SCH-016-03" + ], + "parties_hint": [], + "temporal_hint": "2022년~현재", + "hazard_flags": [ + "current_state_expressions" + ], + "legal_effect_tags": [ + "valuation" + ], + "event_state_type": "state", + "action_summary": "신림로 상가 및 포천시 임야 시가 평가", + "time_interval_hint": "2024-09-13", + "asset_binding_hint": "신림로 상가, 포천시 임야", + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "valuation_rows": [], + "valuation_matrix": [], + "valuation_object": "신림로 상가, 포천시 임야", + "use_gain_facts": [], + "possession_state": { + "asset_ref": null, + "possession_events": [], + "legal_cause_assertions": [] + }, + "delivery_or_possession_start_hints": [], + "unregistered_building_hints": [], + "liability_overlap_hints": [] + } + } + }, + { + "candidate_ref": "B3:002", + "BOType": "event", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "성수동 대지 차임 시세 감정평가", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "가치평가", + "임대료", + "차임" + ], + "core_field_base": { + "date": "2024-12-29", + "amount": "5000000", + "parties": [ + "강용원", + "감정평가법인 광나루" + ], + "asset_refs": [ + "SCH-016-01" + ], + "legal_effect_summary": "성수동 대지에 대한 2024년도 차임 시세 평가" + }, + "amount": { + "value": "5000000", + "currency": "KRW", + "basis": "월 차임" + }, + "source_evidence_indexes": [ + "E-005" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-005-01" + ], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [ + "E-005" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "title_hint": "성수동 대지 차임 시세 감정평가", + "summary_hint": "성수동 256 대지에 대한 월 차임 250만 원~500만 원 시세 평가", + "conflict_patterns": [], + "asset_bindings": [ + "SCH-016-01" + ], + "parties_hint": [ + "강용원", + "감정평가법인 광나루" + ], + "temporal_hint": "2024-12-29", + "hazard_flags": [], + "legal_effect_tags": [ + "valuation" + ], + "event_state_type": "event", + "action_summary": "성수동 대지 차임 시세 평가 회신", + "time_interval_hint": "2024", + "asset_binding_hint": "성수동 256 대", + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "valuation_rows": [], + "valuation_matrix": [], + "valuation_object": "성수동 256 대지", + "use_gain_facts": [], + "possession_state": { + "asset_ref": null, + "possession_events": [], + "legal_cause_assertions": [] + }, + "delivery_or_possession_start_hints": [], + "unregistered_building_hints": [], + "liability_overlap_hints": [] + } + } + }, + { + "candidate_ref": "B3:003", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "평택시 서정빌라 차임 시세 평가", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "가치평가", + "차임" + ], + "core_field_base": { + "date": "2022-10-01", + "amount": "2000000", + "parties": [], + "asset_refs": [ + "SCH-016-04" + ], + "legal_effect_summary": "서정빌라 101호에 대한 월 차임(200만 원) 시세 확인" + }, + "amount": { + "value": "2000000", + "currency": "KRW", + "basis": "보증금 없는 월세" + }, + "source_evidence_indexes": [ + "E-023" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-023-01" + ], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [ + "E-023" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "title_hint": "서정빌라 임료시세확인", + "summary_hint": "2022. 10. 기준 평택시 서정빌라 월세 200만 원 시세 평가", + "conflict_patterns": [], + "asset_bindings": [ + "SCH-016-04" + ], + "parties_hint": [ + "김민식" + ], + "temporal_hint": "2022-10", + "hazard_flags": [ + "current_state_expressions" + ], + "legal_effect_tags": [ + "valuation" + ], + "event_state_type": "state", + "action_summary": "서정빌라 101호 월 차임 시세 확인", + "time_interval_hint": "2022-10-01", + "asset_binding_hint": "서정빌라 101호", + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "valuation_rows": [], + "valuation_matrix": [], + "valuation_object": "평택시 서정빌라", + "use_gain_facts": [], + "possession_state": { + "asset_ref": null, + "possession_events": [], + "legal_cause_assertions": [] + }, + "delivery_or_possession_start_hints": [], + "unregistered_building_hints": [], + "liability_overlap_hints": [] + } + } + }, + { + "candidate_ref": "B3:004", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "성수동 대지 및 건물 인도, 점유 및 사용", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "인도", + "점유", + "사용" + ], + "core_field_base": { + "date": "2024-11-20", + "amount": null, + "parties": [ + "이문호", + "박성희" + ], + "asset_refs": [ + "SCH-016-01", + "SCH-016-02" + ], + "legal_effect_summary": "성수동 대지 및 건물의 인도 및 박성희의 점유·사용 개시" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-006" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-006-03", + "EVT-006-06", + "EVT-006-07" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-바", + "s01_consultation:2-사" + ], + "source_evidence_indexes": [ + "E-006" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "title_hint": "성수동 대지/건물 점유", + "summary_hint": "이문호가 성수동 대지와 건물을 박성희에게 인도하고, 박성희가 관리 및 사용권 귀속", + "conflict_patterns": [], + "asset_bindings": [ + "SCH-016-01", + "SCH-016-02" + ], + "parties_hint": [ + "이문호", + "박성희" + ], + "temporal_hint": "2024-11-20~현재", + "hazard_flags": [ + "current_state_expressions", + "mixed_contract_candidates" + ], + "legal_effect_tags": [ + "possession" + ], + "event_state_type": "state", + "action_summary": "박성희의 성수동 대지 및 건물 점유", + "time_interval_hint": "2024-11-20 이후", + "asset_binding_hint": "성수동 256 대지 및 지상 건물", + "mixed_contract_split_hint": "성수동 대지 임대차와 성수동 건물 매매가 혼합된 거래로 인한 인도/점유", + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "valuation_rows": [], + "valuation_matrix": [], + "valuation_object": null, + "use_gain_facts": [], + "possession_state": { + "asset_ref": "SCH-016-01", + "possession_events": [ + { + "possessor_candidates": [ + "박성희" + ], + "start_date": "2024-11-20", + "end_date": null, + "source_event_candidate_ids": [ + "EVT-006-07" + ], + "evidence_indexes": [ + "E-006" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "임대차", + "asserted_by": "박성희", + "assertion_date": "2024-11-20", + "source_event_candidate_ids": [ + "EVT-006-03" + ], + "evidence_indexes": [ + "E-006" + ] + } + ] + }, + "delivery_or_possession_start_hints": [ + "2024-10-10 이문호 인도", + "2024-11-20 박성희 인도" + ], + "unregistered_building_hints": [ + "성수동 건물 보존등기 미경료 상태에서 거래" + ], + "liability_overlap_hints": [] + } + } + }, + { + "candidate_ref": "B3:005", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "평택시 서정빌라 101호 점유 및 유치권 행사", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "점유", + "거주", + "유치권", + "수익" + ], + "core_field_base": { + "date": null, + "amount": null, + "parties": [ + "박광윤", + "강용원", + "양정숙" + ], + "asset_refs": [ + "SCH-016-04" + ], + "legal_effect_summary": "박광윤의 서정빌라 101호 점유 및 과실(임대수익 등) 발생, 유치권 항변" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-011" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-011-02", + "EVT-011-03" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-가", + "s03_client_requests:4" + ], + "source_evidence_indexes": [ + "E-011" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "title_hint": "서정빌라 점유 및 유치권주장", + "summary_hint": "박광윤이 평택시 서정빌라를 점유·거주·임대하여 수익을 취하며, 피담보채권 충당 항변", + "conflict_patterns": [ + "유치권", + "수익", + "항변" + ], + "asset_bindings": [ + "SCH-016-04" + ], + "parties_hint": [ + "박광윤" + ], + "temporal_hint": "2022-02-01 ~ 현재", + "hazard_flags": [ + "current_state_expressions", + "conflict_pattern_expressions" + ], + "legal_effect_tags": [ + "possession", + "defense_assertion_benefit_applied_to_secured_claim" + ], + "event_state_type": "state", + "action_summary": "박광윤의 평택시 서정빌라 거주 및 유치권 행사", + "time_interval_hint": "2024-10-31 기준 현재진행", + "asset_binding_hint": "평택시 서정3길 123 서정빌라 101호", + "mixed_contract_split_hint": null, + "conflict_pattern_hint": "유치권 소멸 청구(양수인의 청구)와 유치권자의 피담보채권 충당 항변 충돌", + "candidate_details": {}, + "facts": {}, + "valuation_rows": [], + "valuation_matrix": [], + "valuation_object": null, + "use_gain_facts": [ + "2022. 2. 1.부터 7. 31.까지 임대 사용수익", + "그 후 현재까지 가족과 함께 거주" + ], + "possession_state": { + "asset_ref": "SCH-016-04", + "possession_events": [ + { + "possessor_candidates": [ + "박광윤" + ], + "start_date": "2022-02-01", + "end_date": "2024-10-31", + "source_event_candidate_ids": [ + "EVT-011-02" + ], + "evidence_indexes": [ + "E-011" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "유치권행사중", + "asserted_by": "박광윤", + "assertion_date": "2024-10-31", + "source_event_candidate_ids": [ + "EVT-011-03" + ], + "evidence_indexes": [ + "E-011" + ] + } + ] + }, + "delivery_or_possession_start_hints": [], + "unregistered_building_hints": [], + "liability_overlap_hints": [] + } + } + }, + { + "candidate_ref": "B3:006", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "흑석동 상가 점유 및 운영 (맛나식당)", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "인도", + "점유", + "운영", + "사업자등록" + ], + "core_field_base": { + "date": "2024-02-01", + "amount": null, + "parties": [ + "양정숙", + "이수인", + "윤건우" + ], + "asset_refs": [ + "SCH-016-05" + ], + "legal_effect_summary": "양정숙의 흑석동 상가 인도받음 및 맛나식당 영업 개시/운영 상태" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-017-04", + "EVT-018-01", + "EVT-022-03" + ], + "source_meeting_clause_ids": [ + "s01_consultation:5-가", + "s01_consultation:5-나", + "s01_consultation:5-라" + ], + "source_evidence_indexes": [ + "E-017", + "E-018", + "E-022" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "title_hint": "흑석동 상가 점유/운영", + "summary_hint": "양정숙이 흑석동 상가를 이수인으로부터 인도받아 맛나식당을 운영 중임", + "conflict_patterns": [ + "해제", + "인도" + ], + "asset_bindings": [ + "SCH-016-05" + ], + "parties_hint": [ + "양정숙" + ], + "temporal_hint": "2024-02-01 ~ 현재", + "hazard_flags": [ + "current_state_expressions", + "mixed_contract_candidates" + ], + "legal_effect_tags": [ + "possession" + ], + "event_state_type": "state", + "action_summary": "양정숙의 흑석동 상가 맛나식당 운영", + "time_interval_hint": "2024-02-01 이후", + "asset_binding_hint": "흑석동 201호", + "mixed_contract_split_hint": "매수인(이수인)의 임대차와 매도인(윤건우)의 매매 해제 주장으로 인한 점유 충돌", + "conflict_pattern_hint": "매매 해제로 인한 점유권한 상실 여부", + "candidate_details": {}, + "facts": {}, + "valuation_rows": [], + "valuation_matrix": [], + "valuation_object": null, + "use_gain_facts": [], + "possession_state": { + "asset_ref": "SCH-016-05", + "possession_events": [ + { + "possessor_candidates": [ + "양정숙" + ], + "start_date": "2024-02-01", + "end_date": null, + "source_event_candidate_ids": [ + "EVT-022-03", + "EVT-018-01" + ], + "evidence_indexes": [ + "E-018", + "E-022" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "임대차", + "asserted_by": "양정숙", + "assertion_date": "2024-02-01", + "source_event_candidate_ids": [ + "EVT-022-03" + ], + "evidence_indexes": [ + "E-022" + ] + } + ] + }, + "delivery_or_possession_start_hints": [ + "2024-01-01 윤건우가 이수인에게 인도", + "2024-02-01 이수인이 양정숙에게 인도" + ], + "unregistered_building_hints": [], + "liability_overlap_hints": [] + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B3:review:001", + "severity": "SOFT_WARNING", + "issue_type": "legal_theory_required", + "description": "박광윤의 평택시 서정빌라 유치권 주장과 양수인의 소멸 청구, 및 과실수취(임대/거주 수익)에 대한 피담보채권 충당의 법적 우선순위와 적법성 확정 필요", + "source_event_candidate_ids": [ + "EVT-011-02", + "EVT-011-03" + ], + "source_evidence_indexes": [ + "E-011" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:4-가", + "s02_party_assertions:4-나" + ], + "recommended_downstream_owner": "C5" + }, + { + "review_id": "B3:review:002", + "severity": "SOFT_WARNING", + "issue_type": "legal_theory_required", + "description": "윤건우의 흑석동 상가 매매계약 해제 통보가 양정숙의 임대차 점유 권원에 미치는 효력(계약해제와 제3자보호 등) 판단 필요", + "source_event_candidate_ids": [ + "EVT-009-01", + "EVT-009-03", + "EVT-017-04", + "EVT-022-03" + ], + "source_evidence_indexes": [ + "E-009", + "E-017", + "E-022" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:5" + ], + "recommended_downstream_owner": "C5" + } + ], + "handoff_guard": { + "schema_version": "task_c_bo_stage_b_seed_handoff_guard.v1", + "handoff_target": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B3_Land_Valuation.json", + "publisher_task": "Task_C_BO_Stage_B_worker_output_publisher", + "source_worker_task": "Task_C_BO_Stage_B_B3_Land_Valuation", + "source_task": "B3_Land_Valuation", + "source_contract": "direct_prev_task_json_output.stage_b_domain_bo_seed_output", + "source_prev_path": "prev.Task_C_BO_Stage_B_B3_Land_Valuation.json_output", + "domain_id": "B3_Land_Valuation", + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": "2026-07-10T07:27:06.635927+00:00", + "input_digests_sha256": { + "client_meeting.md": "cb95320487cf2bc7a02cecd6324743e45677de84f7303c7e6087b47481577fa8", + "evidence_event_candidates.json": "666c3167a4058df9b4e98fd4bb0504e3823befb7d30b8b32cc319554dbc347e0", + "evidence_indexed.json": "2f7e3a3fdd1682e853aad08242a4ddf009c0d6cb066b83845744f75770f8272a", + "Default_Agent/Juristic_Act.md": "c3cd23f52d6fed5168f95074e801a381b3fdea607ba50a60247666bbb6a82e49" + } + } + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B4_Actio.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B4_Actio.json new file mode 100644 index 00000000..bcaff9a0 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B4_Actio.json @@ -0,0 +1,754 @@ +{ + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B4_Actio", + "status": "READY_WITH_REVIEW", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B4_Actio", + "domain_label": "사해행위취소, 피보전채권, 책임재산, 가액배상", + "slice_guard": { + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": "2026-07-10T07:27:06.635927+00:00", + "input_digests_sha256": { + "client_meeting.md": "cb95320487cf2bc7a02cecd6324743e45677de84f7303c7e6087b47481577fa8", + "evidence_event_candidates.json": "666c3167a4058df9b4e98fd4bb0504e3823befb7d30b8b32cc319554dbc347e0", + "evidence_indexed.json": "2f7e3a3fdd1682e853aad08242a4ddf009c0d6cb066b83845744f75770f8272a", + "Default_Agent/Juristic_Act.md": "c3cd23f52d6fed5168f95074e801a381b3fdea607ba50a60247666bbb6a82e49" + }, + "source_universe": { + "meeting_clause_ids": [ + "s01_consultation:3-라", + "s02_party_assertions:3-가", + "s02_party_assertions:3-나" + ], + "event_candidate_ids": [ + "EVT-003-01", + "EVT-003-02", + "EVT-004-01", + "EVT-004-02", + "EVT-010-03", + "EVT-014-03", + "EVT-014-04", + "EVT-014-05", + "EVT-014-06", + "EVT-024-01", + "EVT-024-02" + ], + "evidence_indexes": [ + "E-003", + "E-004", + "E-010", + "E-014", + "E-024" + ] + }, + "stage_b_worker_contract": { + "context_is_read_only": true, + "bo_id_finalization_allowed": false, + "candidate_generation_allowed": true + } + } + }, + "transport_candidate_refs": [ + { + "candidate_ref": "B4:001", + "payload_index": 0, + "source_event_candidate_ids": [ + "EVT-024-01", + "EVT-024-02" + ], + "source_evidence_indexes": [ + "E-024" + ], + "source_meeting_clause_ids": [] + }, + { + "candidate_ref": "B4:002", + "payload_index": 1, + "source_event_candidate_ids": [ + "EVT-014-03" + ], + "source_evidence_indexes": [ + "E-014" + ], + "source_meeting_clause_ids": [] + }, + { + "candidate_ref": "B4:003", + "payload_index": 2, + "source_event_candidate_ids": [ + "EVT-004-01", + "EVT-004-02" + ], + "source_evidence_indexes": [ + "E-004" + ], + "source_meeting_clause_ids": [] + }, + { + "candidate_ref": "B4:004", + "payload_index": 3, + "source_event_candidate_ids": [ + "EVT-014-04", + "EVT-014-05", + "EVT-014-06" + ], + "source_evidence_indexes": [ + "E-014" + ], + "source_meeting_clause_ids": [ + "s01_consultation:3-라" + ] + }, + { + "candidate_ref": "B4:005", + "payload_index": 4, + "source_event_candidate_ids": [ + "EVT-003-01", + "EVT-003-02", + "EVT-010-03" + ], + "source_evidence_indexes": [ + "E-003", + "E-010" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:3-가", + "s02_party_assertions:3-나" + ] + } + ], + "bo_seed_candidates": [ + { + "candidate_ref": "B4:001", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "소비대차" + }, + "Action": "강용원이 오국한에게 합계 2억원을 대여하여 사해행위취소소송의 피보전채권 발생", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "피보전채권", + "대여금채권" + ], + "core_field_base": { + "date": "2022-04-18", + "amount": "200000000", + "parties": [ + "강용원", + "오국한" + ], + "asset_refs": [], + "legal_effect_summary": "사해행위 이전에 성립한 2억원의 대여금 채권 존재" + }, + "amount": { + "value": 200000000, + "currency": "KRW", + "basis": "2022-04-18 1억원, 2022-05-18 1억원 합계" + }, + "source_evidence_indexes": [ + "E-024" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-024-01", + "EVT-024-02" + ], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [ + "E-024" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "preserved_claim_bundle_BO", + "title_hint": "피보전채권 성립", + "summary_hint": "강용원의 오국한에 대한 대여금 2억원 피보전채권 번들", + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [ + "강용원(채권자)", + "오국한(채무자)" + ], + "temporal_hint": "사해행위(2023-03-17) 이전인 2022년 성립", + "hazard_flags": [], + "legal_effect_tags": [ + "preserved_claim" + ], + "event_state_type": "event", + "action_summary": "강용원이 오국한에게 2억원 대여", + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "preserved_claim_bundle": { + "claims": [ + { + "date": "2022-04-18", + "amount": 100000000 + }, + { + "date": "2022-05-18", + "amount": 100000000 + } + ] + }, + "fraudulent_transfer_facts": [], + "debtor_act_hints": [], + "balance_sheet": { + "asset_decrease_hints": [], + "insolvency_hints": [], + "common_collateral_impairment_hints": [] + }, + "encumbrance_timeline": [], + "valuation_matrix": [], + "remedy_mode_hints": [], + "value_compensation_cap_hints": [], + "beneficiary_transferee_hints": [], + "bad_faith_good_faith_hints": [], + "defense_rebuttal": [] + } + } + }, + { + "candidate_ref": "B4:002", + "BOType": "event", + "ActionType": "법률행위(legal acts)", + "JuristicAct": { + "label": "매매" + }, + "Action": "채무자 오국한이 수익자 오민한에게 책임재산인 신림동 상가를 1억5천만원에 매도하여 소유권이전등기 경료", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "사해행위", + "매매계약", + "소유권이전", + "책임재산감소" + ], + "core_field_base": { + "date": "2023-03-17", + "amount": "150000000", + "parties": [ + "오국한", + "오민한" + ], + "asset_refs": [ + "서울특별시 관악구 신림로 115 건물" + ], + "legal_effect_summary": "채무초과 상태에서의 유일한 부동산 처분행위(사해행위)" + }, + "amount": { + "value": 150000000, + "currency": "KRW", + "basis": "매매대금" + }, + "source_evidence_indexes": [ + "E-014" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-014-03" + ], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [ + "E-014" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "fraudulent_transfer_BO", + "title_hint": "사해행위 성립 (신림동 상가 매매)", + "summary_hint": "채무자 오국한이 수익자 오민한에게 책임재산 처분", + "conflict_patterns": [], + "asset_bindings": [ + "신림동 상가" + ], + "parties_hint": [ + "오국한(채무자)", + "오민한(수익자)" + ], + "temporal_hint": "2023-03-17", + "hazard_flags": [], + "legal_effect_tags": [ + "fraudulent_act" + ], + "event_state_type": "event", + "action_summary": "신림동 상가 1억5천만원 매매 및 소유권이전", + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "preserved_claim_bundle": {}, + "fraudulent_transfer_facts": [ + { + "transfer_date": "2023-03-17", + "transfer_type": "매매", + "target_asset": "신림동 상가" + } + ], + "debtor_act_hints": [ + "채무자가 유일한 재산을 처분하여 무자력 야기" + ], + "balance_sheet": { + "asset_decrease_hints": [], + "insolvency_hints": [], + "common_collateral_impairment_hints": [] + }, + "encumbrance_timeline": [], + "valuation_matrix": [], + "remedy_mode_hints": [], + "value_compensation_cap_hints": [], + "beneficiary_transferee_hints": [ + "오민한" + ], + "bad_faith_good_faith_hints": [ + "수익자 악의 추정" + ], + "defense_rebuttal": [] + } + } + }, + { + "candidate_ref": "B4:003", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "사해행위 당시 채무자 오국한의 무자력 상태 및 책임재산 보유 현황 (신림동 상가, 포천시 임야 등)", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "무자력", + "책임재산", + "가치평가" + ], + "core_field_base": { + "date": "2023-03-17", + "amount": null, + "parties": [ + "오국한" + ], + "asset_refs": [ + "서울특별시 관악구 신림로 115 건물", + "포천시 임야" + ], + "legal_effect_summary": "사해행위 성립요건인 채무자 무자력 요건 사실" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-004" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-004-01", + "EVT-004-02" + ], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [ + "E-004" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "actio_balance_sheet_BO", + "title_hint": "채무자 무자력 증명", + "summary_hint": "감정평가를 통한 처분당시 오국한의 자산상태 입증", + "conflict_patterns": [], + "asset_bindings": [ + "신림동 상가", + "포천시 임야" + ], + "parties_hint": [ + "오국한" + ], + "temporal_hint": "2023-03-17 기준", + "hazard_flags": [ + "amount_or_date_uncertain" + ], + "legal_effect_tags": [ + "insolvency_check" + ], + "event_state_type": "state", + "action_summary": "처분행위 당시 자산 및 부채 비교", + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "preserved_claim_bundle": {}, + "fraudulent_transfer_facts": [], + "debtor_act_hints": [], + "balance_sheet": { + "asset_decrease_hints": [ + "신림동 상가 처분으로 인한 적극재산 감소" + ], + "insolvency_hints": [ + "채무초과 상태" + ], + "common_collateral_impairment_hints": [ + "공동담보의 부족 초래" + ] + }, + "encumbrance_timeline": [], + "valuation_matrix": [], + "remedy_mode_hints": [], + "value_compensation_cap_hints": [], + "beneficiary_transferee_hints": [], + "bad_faith_good_faith_hints": [], + "defense_rebuttal": [] + } + } + }, + { + "candidate_ref": "B4:004", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "변제 및 말소" + }, + "Action": "수익자 오민한이 사해행위 이후 피담보채무 1억3천만원을 대위변제하고 2번 근저당권을 말소시킴으로써 원물반환이 불가능해져 가액배상 요건 성립", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "원물반환불능", + "가액배상", + "근저당권말소" + ], + "core_field_base": { + "date": "2024-10-17", + "amount": "130000000", + "parties": [ + "오민한", + "강용일" + ], + "asset_refs": [ + "서울특별시 관악구 신림로 115 건물" + ], + "legal_effect_summary": "사해행위 후 저당권 말소로 인한 가액배상 원칙 전환" + }, + "amount": { + "value": 130000000, + "currency": "KRW", + "basis": "변제된 원리금" + }, + "source_evidence_indexes": [ + "E-014" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-014-04", + "EVT-014-05", + "EVT-014-06" + ], + "source_meeting_clause_ids": [ + "s01_consultation:3-라" + ], + "source_evidence_indexes": [ + "E-014" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "remedy_mode_decision_BO", + "title_hint": "가액배상 방식 결정 및 한도", + "summary_hint": "수익자의 대위변제에 의한 저당권 소멸과 가액배상 산정 기준", + "conflict_patterns": [ + "근저당 말소로 인한 반환방법 변경" + ], + "asset_bindings": [ + "신림동 상가" + ], + "parties_hint": [ + "오민한(수익자)" + ], + "temporal_hint": "2024-10-17 근저당 말소 기준", + "hazard_flags": [ + "amount_or_date_uncertain" + ], + "legal_effect_tags": [ + "value_compensation", + "remedy_cap" + ], + "event_state_type": "event", + "action_summary": "강용일 명의 2번 근저당권 말소(수익자 변제)", + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "preserved_claim_bundle": {}, + "fraudulent_transfer_facts": [], + "debtor_act_hints": [], + "balance_sheet": { + "asset_decrease_hints": [], + "insolvency_hints": [], + "common_collateral_impairment_hints": [] + }, + "encumbrance_timeline": [ + { + "date": "2021-08-01", + "event": "나임오 1번 근저당 설정(5000만원)" + }, + { + "date": "2022-04-18", + "event": "강용일 2번 근저당 설정(1.5억원)" + }, + { + "date": "2024-10-17", + "event": "강용일 2번 근저당 말소(오민한 대위변제)" + } + ], + "valuation_matrix": [], + "remedy_mode_hints": [ + "원물반환 불가", + "가액배상" + ], + "value_compensation_cap_hints": [ + "부동산 가액에서 선순위 저당권 피담보채권액을 공제한 잔액 한도" + ], + "beneficiary_transferee_hints": [], + "bad_faith_good_faith_hints": [], + "defense_rebuttal": [] + } + } + }, + { + "candidate_ref": "B4:005", + "BOType": "state", + "ActionType": "소송행위(litigation acts)", + "JuristicAct": { + "label": "항변" + }, + "Action": "수익자 오민한의 항변 (명의신탁 주장, 매수자금 및 대여금 참작 주장, 채권 상계 주장)", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [ + "수익자항변", + "명의신탁", + "상계항변", + "선의항변" + ], + "core_field_base": { + "date": null, + "amount": null, + "parties": [ + "오민한" + ], + "asset_refs": [ + "서울특별시 관악구 신림로 115 건물" + ], + "legal_effect_summary": "수익자 오민한의 책임 조각 및 감경 사유 주장" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-003", + "E-010" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-003-01", + "EVT-003-02", + "EVT-010-03" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:3-가", + "s02_party_assertions:3-나" + ], + "source_evidence_indexes": [ + "E-003", + "E-010" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "defense_rebuttal_matrix_BO", + "title_hint": "수익자 오민한의 항변 분석", + "summary_hint": "계약명의신탁, 자금제공 참작, 상계 등 수익자 항변 검토", + "conflict_patterns": [ + "사해행위 성립 부정 주장", + "배상금 감액 주장" + ], + "asset_bindings": [ + "신림동 상가" + ], + "parties_hint": [ + "오민한" + ], + "temporal_hint": null, + "hazard_flags": [ + "legal_theory_required" + ], + "legal_effect_tags": [ + "defense_assertion" + ], + "event_state_type": "state", + "action_summary": "수익자가 사해의사 부정 및 상계항변 제출", + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "preserved_claim_bundle": {}, + "fraudulent_transfer_facts": [], + "debtor_act_hints": [], + "balance_sheet": { + "asset_decrease_hints": [], + "insolvency_hints": [], + "common_collateral_impairment_hints": [] + }, + "encumbrance_timeline": [], + "valuation_matrix": [], + "remedy_mode_hints": [], + "value_compensation_cap_hints": [], + "beneficiary_transferee_hints": [], + "bad_faith_good_faith_hints": [], + "defense_rebuttal": [ + { + "defense_type": "계약명의신탁", + "content": "오국한 명의 취득이 수익자 자금에 의한 계약명의신탁이므로 사해행위가 아니라는 주장" + }, + { + "defense_type": "배상금감액", + "content": "수익자가 제공한 매수자금 4000만원 및 대여금 1000만원 참작 주장" + }, + { + "defense_type": "상계항변", + "content": "오민한의 5000만원 채권으로 강용원 채권을 상계한다는 주장" + } + ] + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B4:review:001", + "severity": "HARD_WARNING", + "issue_type": "amount_or_date_uncertain", + "description": "가액배상 한도액의 정확한 산정을 위해서는 처분행위 당시 또는 변론종결시 기준의 목적물 가액과, 존속하는 1번 근저당권(나임오, 5000만원) 및 말소된 2번 근저당권(강용일, 실제 피담보채무 1.3억 변제액 등)의 정확한 피담보채권액 공제 계산이 필요함.", + "source_event_candidate_ids": [ + "EVT-014-04", + "EVT-014-05", + "EVT-014-06", + "EVT-004-01" + ], + "source_evidence_indexes": [ + "E-014", + "E-004" + ], + "source_meeting_clause_ids": [ + "s01_consultation:3-라" + ], + "recommended_downstream_owner": "C0" + }, + { + "review_id": "B4:review:002", + "severity": "SOFT_WARNING", + "issue_type": "legal_theory_required", + "description": "수익자 오민한의 계약명의신탁 항변 및 상계 항변의 인용 가능성에 대한 법리적 타당성 검토 필요.", + "source_event_candidate_ids": [ + "EVT-003-01", + "EVT-010-03" + ], + "source_evidence_indexes": [ + "E-003", + "E-010" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:3-가", + "s02_party_assertions:3-나" + ], + "recommended_downstream_owner": "D" + }, + { + "review_id": "B4:review:003", + "severity": "SOFT_WARNING", + "issue_type": "amount_or_date_uncertain", + "description": "사해행위 당시 채무자 오국한의 무자력 판단에 있어 포천시 임야의 교환가치 산정 및 피담보채무 공제 후 잔존가치 유무 확인 필요.", + "source_event_candidate_ids": [ + "EVT-004-02" + ], + "source_evidence_indexes": [ + "E-004" + ], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "C0" + } + ], + "handoff_guard": { + "schema_version": "task_c_bo_stage_b_seed_handoff_guard.v1", + "handoff_target": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B4_Actio.json", + "publisher_task": "Task_C_BO_Stage_B_worker_output_publisher", + "source_worker_task": "Task_C_BO_Stage_B_B4_Actio", + "source_task": "B4_Actio", + "source_contract": "direct_prev_task_json_output.stage_b_domain_bo_seed_output", + "source_prev_path": "prev.Task_C_BO_Stage_B_B4_Actio.json_output", + "domain_id": "B4_Actio", + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": "2026-07-10T07:27:06.635927+00:00", + "input_digests_sha256": { + "client_meeting.md": "cb95320487cf2bc7a02cecd6324743e45677de84f7303c7e6087b47481577fa8", + "evidence_event_candidates.json": "666c3167a4058df9b4e98fd4bb0504e3823befb7d30b8b32cc319554dbc347e0", + "evidence_indexed.json": "2f7e3a3fdd1682e853aad08242a4ddf009c0d6cb066b83845744f75770f8272a", + "Default_Agent/Juristic_Act.md": "c3cd23f52d6fed5168f95074e801a381b3fdea607ba50a60247666bbb6a82e49" + } + } + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B5_Succession_Notice_Lien_Asset_Defense.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B5_Succession_Notice_Lien_Asset_Defense.json new file mode 100644 index 00000000..592c9f92 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Results_July_10_3_15pm/stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B5_Succession_Notice_Lien_Asset_Defense.json @@ -0,0 +1,2083 @@ +{ + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B5_Succession_Notice_Lien_Asset_Defense", + "status": "READY_WITH_REVIEW", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B5_Succession_Notice_Lien_Asset_Defense", + "domain_label": "상속, 통지, 유치권, 목적물 특정, 항변", + "slice_guard": { + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": "2026-07-10T07:27:06.635927+00:00", + "input_digests_sha256": { + "client_meeting.md": "cb95320487cf2bc7a02cecd6324743e45677de84f7303c7e6087b47481577fa8", + "evidence_event_candidates.json": "666c3167a4058df9b4e98fd4bb0504e3823befb7d30b8b32cc319554dbc347e0", + "evidence_indexed.json": "2f7e3a3fdd1682e853aad08242a4ddf009c0d6cb066b83845744f75770f8272a", + "Default_Agent/Juristic_Act.md": "c3cd23f52d6fed5168f95074e801a381b3fdea607ba50a60247666bbb6a82e49" + }, + "source_universe": { + "meeting_clause_ids": [ + "s01_consultation:1", + "s01_consultation:1-가", + "s01_consultation:1-나", + "s01_consultation:1-다", + "s01_consultation:1-라", + "s01_consultation:2", + "s01_consultation:2-가", + "s01_consultation:2-나", + "s01_consultation:2-다", + "s01_consultation:2-라", + "s01_consultation:2-마", + "s01_consultation:2-바", + "s01_consultation:2-사", + "s01_consultation:3", + "s01_consultation:3-가", + "s01_consultation:3-나", + "s01_consultation:3-다", + "s01_consultation:3-라", + "s01_consultation:4", + "s01_consultation:4-가", + "s01_consultation:4-나", + "s01_consultation:4-다", + "s01_consultation:5", + "s01_consultation:5-가", + "s01_consultation:5-나", + "s01_consultation:5-다", + "s01_consultation:5-라", + "s02_party_assertions:1", + "s02_party_assertions:2", + "s02_party_assertions:3", + "s02_party_assertions:3-가", + "s02_party_assertions:3-나", + "s02_party_assertions:4", + "s02_party_assertions:4-가", + "s02_party_assertions:4-나", + "s02_party_assertions:5", + "s03_client_requests:1", + "s03_client_requests:2", + "s03_client_requests:3", + "s03_client_requests:4", + "s03_client_requests:5" + ], + "event_candidate_ids": [ + "EVT-001-01", + "EVT-001-02", + "EVT-002-01", + "EVT-003-01", + "EVT-003-02", + "EVT-004-01", + "EVT-004-02", + "EVT-005-01", + "EVT-006-01", + "EVT-006-02", + "EVT-006-03", + "EVT-006-04", + "EVT-006-05", + "EVT-006-06", + "EVT-006-07", + "EVT-007-01", + "EVT-007-02", + "EVT-008-01", + "EVT-009-01", + "EVT-009-02", + "EVT-009-03", + "EVT-010-01", + "EVT-010-02", + "EVT-010-03", + "EVT-011-01", + "EVT-011-02", + "EVT-011-03", + "EVT-012-01", + "EVT-012-02", + "EVT-013-01", + "EVT-013-02", + "EVT-013-03", + "EVT-013-04", + "EVT-013-05", + "EVT-013-06", + "EVT-014-01", + "EVT-014-02", + "EVT-014-03", + "EVT-014-04", + "EVT-014-05", + "EVT-014-06", + "EVT-015-01", + "EVT-015-02", + "EVT-015-03", + "EVT-015-04", + "EVT-017-01", + "EVT-017-02", + "EVT-017-03", + "EVT-017-04", + "EVT-018-01", + "EVT-020-01", + "EVT-020-02", + "EVT-020-03", + "EVT-021-01", + "EVT-021-02", + "EVT-022-01", + "EVT-022-02", + "EVT-022-03", + "EVT-023-01", + "EVT-024-01", + "EVT-024-02", + "EVT-025-01", + "EVT-025-02", + "EVT-026-01", + "EVT-026-02", + "EVT-027-01", + "EVT-027-02", + "EVT-028-01", + "EVT-029-01", + "EVT-030-01", + "EVT-030-02" + ], + "evidence_indexes": [ + "E-001", + "E-002", + "E-003", + "E-004", + "E-005", + "E-006", + "E-007", + "E-008", + "E-009", + "E-010", + "E-011", + "E-012", + "E-013", + "E-014", + "E-015", + "E-016", + "E-017", + "E-018", + "E-019", + "E-020", + "E-021", + "E-022", + "E-023", + "E-024", + "E-025", + "E-026", + "E-027", + "E-028", + "E-029", + "E-030" + ] + }, + "stage_b_worker_contract": { + "context_is_read_only": true, + "bo_id_finalization_allowed": false, + "candidate_generation_allowed": true + } + } + }, + "transport_candidate_refs": [ + { + "candidate_ref": "B5:001", + "payload_index": 0, + "source_event_candidate_ids": [], + "source_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "source_meeting_clause_ids": [] + }, + { + "candidate_ref": "B5:002", + "payload_index": 1, + "source_event_candidate_ids": [ + "EVT-013-01" + ], + "source_evidence_indexes": [ + "E-013" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-가" + ] + }, + { + "candidate_ref": "B5:003", + "payload_index": 2, + "source_event_candidate_ids": [ + "EVT-001-01", + "EVT-001-02", + "EVT-002-01" + ], + "source_evidence_indexes": [ + "E-001", + "E-002" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-가" + ] + }, + { + "candidate_ref": "B5:004", + "payload_index": 3, + "source_event_candidate_ids": [ + "EVT-008-01", + "EVT-010-01" + ], + "source_evidence_indexes": [ + "E-008", + "E-010" + ], + "source_meeting_clause_ids": [] + }, + { + "candidate_ref": "B5:005", + "payload_index": 4, + "source_event_candidate_ids": [ + "EVT-009-01", + "EVT-009-02" + ], + "source_evidence_indexes": [ + "E-009" + ], + "source_meeting_clause_ids": [] + }, + { + "candidate_ref": "B5:006", + "payload_index": 5, + "source_event_candidate_ids": [ + "EVT-010-02" + ], + "source_evidence_indexes": [ + "E-010" + ], + "source_meeting_clause_ids": [] + }, + { + "candidate_ref": "B5:007", + "payload_index": 6, + "source_event_candidate_ids": [ + "EVT-011-01" + ], + "source_evidence_indexes": [ + "E-011", + "E-019" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-다" + ] + }, + { + "candidate_ref": "B5:008", + "payload_index": 7, + "source_event_candidate_ids": [ + "EVT-012-01" + ], + "source_evidence_indexes": [ + "E-012" + ], + "source_meeting_clause_ids": [] + }, + { + "candidate_ref": "B5:009", + "payload_index": 8, + "source_event_candidate_ids": [ + "EVT-027-01", + "EVT-027-02" + ], + "source_evidence_indexes": [ + "E-027" + ], + "source_meeting_clause_ids": [ + "s01_consultation:1-나" + ] + }, + { + "candidate_ref": "B5:010", + "payload_index": 9, + "source_event_candidate_ids": [ + "EVT-028-01" + ], + "source_evidence_indexes": [ + "E-028" + ], + "source_meeting_clause_ids": [] + }, + { + "candidate_ref": "B5:011", + "payload_index": 10, + "source_event_candidate_ids": [ + "EVT-029-01" + ], + "source_evidence_indexes": [ + "E-029" + ], + "source_meeting_clause_ids": [ + "s01_consultation:5-다" + ] + }, + { + "candidate_ref": "B5:012", + "payload_index": 11, + "source_event_candidate_ids": [ + "EVT-030-01", + "EVT-030-02" + ], + "source_evidence_indexes": [ + "E-030" + ], + "source_meeting_clause_ids": [ + "s01_consultation:5-라" + ] + }, + { + "candidate_ref": "B5:013", + "payload_index": 12, + "source_event_candidate_ids": [ + "EVT-011-02" + ], + "source_evidence_indexes": [ + "E-011" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-가" + ] + }, + { + "candidate_ref": "B5:014", + "payload_index": 13, + "source_event_candidate_ids": [ + "EVT-006-07" + ], + "source_evidence_indexes": [ + "E-006" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-사" + ] + }, + { + "candidate_ref": "B5:015", + "payload_index": 14, + "source_event_candidate_ids": [ + "EVT-018-01", + "EVT-022-03" + ], + "source_evidence_indexes": [ + "E-018", + "E-022" + ], + "source_meeting_clause_ids": [ + "s01_consultation:5-나" + ] + }, + { + "candidate_ref": "B5:016", + "payload_index": 15, + "source_event_candidate_ids": [ + "EVT-009-03" + ], + "source_evidence_indexes": [ + "E-009" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:5" + ] + }, + { + "candidate_ref": "B5:017", + "payload_index": 16, + "source_event_candidate_ids": [ + "EVT-010-03" + ], + "source_evidence_indexes": [ + "E-010" + ], + "source_meeting_clause_ids": [] + }, + { + "candidate_ref": "B5:018", + "payload_index": 17, + "source_event_candidate_ids": [ + "EVT-011-03" + ], + "source_evidence_indexes": [ + "E-011" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:4-나" + ] + }, + { + "candidate_ref": "B5:019", + "payload_index": 18, + "source_event_candidate_ids": [ + "EVT-012-02" + ], + "source_evidence_indexes": [ + "E-012" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:2" + ] + } + ], + "bo_seed_candidates": [ + { + "candidate_ref": "B5:001", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "목적물 특정", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [], + "asset_refs": [], + "legal_effect_summary": null + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [ + "E-013", + "E-014", + "E-015", + "E-016" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "asset_alias": [ + { + "asset_ref": "성수동 대지", + "alias_strings": [ + "성수동 대지", + "서울 성동구 성수동 256" + ], + "canonical_label_candidate": "1. 서울 성동구 성수동 256 대 330㎡", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-01" + ], + "registry_refs": [ + { + "registry_ref": "REG-013-01", + "receipt_no_refs": [ + "제1453호" + ] + }, + { + "registry_ref": "REG-013-02", + "receipt_no_refs": [ + "제7026호" + ] + }, + { + "registry_ref": "REG-013-03", + "receipt_no_refs": [ + "제11202호" + ] + }, + { + "registry_ref": "REG-013-04", + "receipt_no_refs": [ + "제7202호" + ] + }, + { + "registry_ref": "REG-013-05", + "receipt_no_refs": [ + "제12402호" + ] + }, + { + "registry_ref": "REG-013-06", + "receipt_no_refs": [ + "제25797호" + ] + }, + { + "registry_ref": "REG-013-07", + "receipt_no_refs": [ + "제25799호" + ] + } + ], + "source_evidence_indexes": [ + "E-013", + "E-016" + ] + }, + { + "asset_ref": "성수동 건물", + "alias_strings": [ + "성수동 건물", + "서울 성동구 성수동 256 상가" + ], + "canonical_label_candidate": "2. 서울 성동구 성수동 256([도로명주소] 서울 성동구 성수로 30) 지상 경량철골조 샌드위치패널지붕 단층 근린상가 200㎡", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-02" + ], + "registry_refs": [], + "source_evidence_indexes": [ + "E-016" + ] + }, + { + "asset_ref": "신림동 상가", + "alias_strings": [ + "신림동 상가", + "서울 관악구 신림로 115" + ], + "canonical_label_candidate": "3. 서울 관악구 신림동 779([도로명주소] 서울 관악구 신림로 115) 지상 철근콘크리트조 콘크리트지붕 단층 근린상가 100㎡", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-03" + ], + "registry_refs": [ + { + "registry_ref": "REG-014-01", + "receipt_no_refs": [ + "제24759호" + ] + }, + { + "registry_ref": "REG-014-02", + "receipt_no_refs": [ + "제56457호" + ] + }, + { + "registry_ref": "REG-014-03", + "receipt_no_refs": [ + "제11593호" + ] + }, + { + "registry_ref": "REG-014-04", + "receipt_no_refs": [ + "제50099호" + ] + }, + { + "registry_ref": "REG-014-05", + "receipt_no_refs": [ + "제26597호" + ] + }, + { + "registry_ref": "REG-014-06", + "receipt_no_refs": [ + "제74586호" + ] + } + ], + "source_evidence_indexes": [ + "E-014", + "E-016" + ] + }, + { + "asset_ref": "평택시 빌라", + "alias_strings": [ + "평택시 빌라", + "서정빌라 101호", + "평택시 서정3길 123" + ], + "canonical_label_candidate": "4. (1동의 건물의 표시) 평택시 서정3길 123 지상 철근콘크리트조 슬래브지붕 3층 건물", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-04" + ], + "registry_refs": [ + { + "registry_ref": "REG-015-01", + "receipt_no_refs": [ + "제7233호" + ] + }, + { + "registry_ref": "REG-015-02", + "receipt_no_refs": [ + "제7871호" + ] + }, + { + "registry_ref": "REG-015-03", + "receipt_no_refs": [ + "제11593호" + ] + }, + { + "registry_ref": "REG-015-04", + "receipt_no_refs": [ + "제12321호" + ] + } + ], + "source_evidence_indexes": [ + "E-015", + "E-016" + ] + }, + { + "asset_ref": "흑석동 상가", + "alias_strings": [ + "흑석동 상가", + "서울 동작구 흑석로 80" + ], + "canonical_label_candidate": "5. (1동의 건물의 표시) 서울 동작구 흑석로 80 지상 철근콘크리트조 슬래브지붕 3층 건물", + "usable_in_relief": "yes", + "schedule_refs": [ + "SCH-016-05" + ], + "registry_refs": [], + "source_evidence_indexes": [ + "E-016" + ] + } + ] + } + } + }, + { + "candidate_ref": "B5:002", + "BOType": "event", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": "상속개시" + }, + "Action": "강석우 사망으로 인한 정유심, 강용원의 성수동 대지 지분 상속", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": "2022-11-08", + "amount": null, + "parties": [ + "강석우", + "정유심", + "강용원" + ], + "asset_refs": [ + "성수동 대지" + ], + "legal_effect_summary": "상속으로 인한 소유권이전" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-013" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-013-01" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-가" + ], + "source_evidence_indexes": [ + "E-013" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "general_succession_status": { + "status_raw_hint": "강석우 사망으로 지분 상속" + } + } + } + }, + { + "candidate_ref": "B5:003", + "BOType": "event", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": "상속포기" + }, + "Action": "망 강호연의 사망에 따른 상속개시 및 자녀 강형수, 강지수의 상속포기", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": "2024-08-10", + "amount": null, + "parties": [ + "강호연", + "강형수", + "강지수" + ], + "asset_refs": [], + "legal_effect_summary": "상속개시 및 포기 완료" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-001", + "E-002" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-001-01", + "EVT-001-02", + "EVT-002-01" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-가" + ], + "source_evidence_indexes": [ + "E-001", + "E-002" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "general_succession_status": { + "status_raw_hint": "강호연 사망, 상속인 상속포기 심판수리 (2024-09-20)" + } + } + } + }, + { + "candidate_ref": "B5:004", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "최고" + }, + "Action": "강용원이 오민한에게 사해행위취소 및 신림동 상가 소유권 반환을 요구하는 내용증명 발송", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": "2024-09-15", + "amount": null, + "parties": [ + "강용원", + "오민한" + ], + "asset_refs": [ + "신림동 상가" + ], + "legal_effect_summary": "반환 요구 통지" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-008", + "E-010" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-008-01", + "EVT-010-01" + ], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [ + "E-008", + "E-010" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "notice_lifecycle": { + "notice_id": "E-008-NT-01", + "notice_type": "기타", + "drafted_date": "2024-09-15", + "dispatch_date": "2024-09-15", + "arrival_date": "2024-09-20", + "received_date": "2024-09-20", + "legal_effect_date_candidate": "2024-09-20", + "requires_receipt_for_effect": null, + "sender_candidates": [ + "강용원" + ], + "legal_sender_after_resolution": { + "status": "unresolved", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "오민한" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [ + "오민한이 이행최고서를 2024. 9. 20. 수령함" + ] + } + } + } + }, + { + "candidate_ref": "B5:005", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "윤건우가 양정숙에게 흑석동 상가 임대차계약 해지 및 명도를 요구하는 내용증명 통지", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": "2024-11-29", + "amount": null, + "parties": [ + "윤건우", + "양정숙" + ], + "asset_refs": [ + "흑석동 상가" + ], + "legal_effect_summary": "임대차 해지 통지" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-009" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-009-01", + "EVT-009-02" + ], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [ + "E-009" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "notice_lifecycle": { + "notice_id": "E-009-NT-01", + "notice_type": "기타", + "drafted_date": "2024-11-29", + "dispatch_date": "2024-11-29", + "arrival_date": "2024-11-30", + "received_date": "2024-11-30", + "legal_effect_date_candidate": "2024-11-30", + "requires_receipt_for_effect": true, + "sender_candidates": [ + "윤건우" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "양정숙" + ], + "receipt_evidence_refs": [ + "우편물배달증명서" + ], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + } + }, + { + "candidate_ref": "B5:006", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "오민한이 강용원에게 내용증명에 대한 답신 발송", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": "2024-09-30", + "amount": null, + "parties": [ + "오민한", + "강용원" + ], + "asset_refs": [], + "legal_effect_summary": "답신 통지" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-010" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-010-02" + ], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [ + "E-010" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "notice_lifecycle": { + "notice_id": "E-010-NT-01", + "notice_type": "기타", + "drafted_date": null, + "dispatch_date": "2024-09-30", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [ + "오민한" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "강용원" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + } + }, + { + "candidate_ref": "B5:007", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "강용원과 양정숙이 박광윤에게 평택시 빌라 유치권소멸을 청구하는 통지서 발송 및 도달", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": "2024-08-29", + "amount": null, + "parties": [ + "강용원", + "양정숙", + "박광윤" + ], + "asset_refs": [ + "평택시 빌라" + ], + "legal_effect_summary": "유치권소멸청구" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-011", + "E-019" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-011-01" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-다" + ], + "source_evidence_indexes": [ + "E-011", + "E-019" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "notice_lifecycle": { + "notice_id": "E-019-NT-01", + "notice_type": "유치권소멸청구통지", + "drafted_date": "2024-08-29", + "dispatch_date": "2024-08-29", + "arrival_date": "2024-09-01", + "received_date": "2024-09-01", + "legal_effect_date_candidate": "2024-09-01", + "requires_receipt_for_effect": true, + "sender_candidates": [ + "강용원", + "양정숙" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "박광윤" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + } + }, + { + "candidate_ref": "B5:008", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "주식회사 대한은행이 강용원의 성수동 대지 근저당권 말소 신청을 거부하는 통지 발송", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": "2024-12-22", + "amount": null, + "parties": [ + "주식회사 대한은행", + "강용원" + ], + "asset_refs": [ + "성수동 대지" + ], + "legal_effect_summary": "말소 거부 통지" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-012" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-012-01" + ], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [ + "E-012" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "notice_lifecycle": { + "notice_id": "E-012-NT-01", + "notice_type": "기타", + "drafted_date": "2024-12-22", + "dispatch_date": "2024-12-22", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [ + "주식회사 대한은행" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "강용원" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + } + }, + { + "candidate_ref": "B5:009", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "최고" + }, + "Action": "강용원이 김선웅에게 물품대금 지급을 최고하는 통지서 발송 및 도달", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": "2024-05-04", + "amount": null, + "parties": [ + "강용원", + "김선웅" + ], + "asset_refs": [], + "legal_effect_summary": "대금지급 최고" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-027" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-027-01", + "EVT-027-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:1-나" + ], + "source_evidence_indexes": [ + "E-027" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "notice_lifecycle": { + "notice_id": "E-027-NT-01", + "notice_type": "최고", + "drafted_date": null, + "dispatch_date": "2024-05-04", + "arrival_date": "2024-05-06", + "received_date": "2024-05-06", + "legal_effect_date_candidate": "2024-05-06", + "requires_receipt_for_effect": null, + "sender_candidates": [ + "강용원" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "김선웅" + ], + "receipt_evidence_refs": [ + "우편물 배달 증명서" + ], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + } + }, + { + "candidate_ref": "B5:010", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "오민한이 강용원에게 김선웅의 채무에 대한 책임을 부인하는 통지서 발송", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": "2024-12-27", + "amount": null, + "parties": [ + "오민한", + "강용원" + ], + "asset_refs": [], + "legal_effect_summary": "책임 부인 통지" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-028" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-028-01" + ], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [ + "E-028" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "notice_lifecycle": { + "notice_id": "E-028-NT-01", + "notice_type": "기타", + "drafted_date": null, + "dispatch_date": "2024-12-27", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [ + "오민한" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "강용원" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + } + }, + { + "candidate_ref": "B5:011", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "이수인이 윤건우에게 흑석동 상가 매매대금 감액 및 제3자 명의 등기 이전 요청 통지", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": "2024-03-30", + "amount": null, + "parties": [ + "이수인", + "윤건우" + ], + "asset_refs": [ + "흑석동 상가" + ], + "legal_effect_summary": "조건변경 요청 통지" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-029" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-029-01" + ], + "source_meeting_clause_ids": [ + "s01_consultation:5-다" + ], + "source_evidence_indexes": [ + "E-029" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "notice_lifecycle": { + "notice_id": "E-029-NT-01", + "notice_type": "기타", + "drafted_date": null, + "dispatch_date": "2024-03-30", + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [ + "이수인" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "윤건우" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + } + }, + { + "candidate_ref": "B5:012", + "BOType": "event", + "ActionType": "준법률행위(quasi-legal acts)", + "JuristicAct": { + "label": "통지" + }, + "Action": "윤건우가 이수인에게 흑석동 상가 매매계약 해제 통지서 발송 및 도달", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": "2024-05-15", + "amount": null, + "parties": [ + "윤건우", + "이수인" + ], + "asset_refs": [ + "흑석동 상가" + ], + "legal_effect_summary": "계약 해제 통지" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-030" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-030-01", + "EVT-030-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:5-라" + ], + "source_evidence_indexes": [ + "E-030" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "notice_lifecycle": { + "notice_id": "E-030-NT-01", + "notice_type": "해제통지", + "drafted_date": null, + "dispatch_date": "2024-05-15", + "arrival_date": "2024-05-16", + "received_date": "2024-05-16", + "legal_effect_date_candidate": "2024-05-16", + "requires_receipt_for_effect": true, + "sender_candidates": [ + "윤건우" + ], + "legal_sender_after_resolution": { + "status": "not_required", + "party": null, + "succession_resolution_ref": null + }, + "receiver_candidates": [ + "이수인" + ], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + } + } + } + }, + { + "candidate_ref": "B5:013", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "박광윤의 평택시 서정빌라 101호 거주 및 유치권 행사 상태", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [ + "박광윤" + ], + "asset_refs": [ + "평택시 빌라" + ], + "legal_effect_summary": "점유 및 유치권" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-011" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-011-02" + ], + "source_meeting_clause_ids": [ + "s01_consultation:4-가" + ], + "source_evidence_indexes": [ + "E-011" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "lien_state": { + "lien_id": "B5:LIEN:001", + "asset_ref": "평택시 빌라", + "lien_holder_candidates": [ + "박광윤" + ], + "secured_claim_hint": { + "debtor_candidate": "김일동", + "basis_text": "공사잔금 3억 원 미지급", + "claimed_remaining_amount_hint": "300,000,000원", + "evidence_indexes": [ + "E-011" + ] + }, + "possession_event_refs": [], + "assertion_events": [ + "EVT-011-02" + ], + "extinction_notice_refs": [ + "E-019-NT-01" + ], + "status_raw_hint": "유치권 행사 중" + }, + "possession_state": { + "asset_ref": "평택시 빌라", + "possession_events": [ + { + "possessor_candidates": [ + "박광윤" + ], + "start_date": null, + "end_date": null, + "source_event_candidate_ids": [ + "EVT-011-02" + ], + "evidence_indexes": [ + "E-011" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "유치권행사중", + "asserted_by": "박광윤", + "assertion_date": "2024-10-31", + "source_event_candidate_ids": [ + "EVT-011-02" + ], + "evidence_indexes": [ + "E-011" + ] + } + ] + } + } + } + }, + { + "candidate_ref": "B5:014", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "박성희의 성수동 대지 및 건물 점유", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": "2024-11-20", + "amount": null, + "parties": [ + "박성희" + ], + "asset_refs": [ + "성수동 대지", + "성수동 건물" + ], + "legal_effect_summary": "점유" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-006" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-006-07" + ], + "source_meeting_clause_ids": [ + "s01_consultation:2-사" + ], + "source_evidence_indexes": [ + "E-006" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "possession_state": { + "asset_ref": "성수동 대지 및 성수동 건물", + "possession_events": [ + { + "possessor_candidates": [ + "박성희" + ], + "start_date": "2024-11-20", + "end_date": null, + "source_event_candidate_ids": [ + "EVT-006-07" + ], + "evidence_indexes": [ + "E-006" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "임대차", + "asserted_by": "박성희", + "assertion_date": "2024-11-20", + "source_event_candidate_ids": [ + "EVT-006-07" + ], + "evidence_indexes": [ + "E-006" + ] + } + ] + } + } + } + }, + { + "candidate_ref": "B5:015", + "BOType": "state", + "ActionType": "사실행위(factual acts)", + "JuristicAct": { + "label": null + }, + "Action": "양정숙의 흑석동 상가 점유 및 식당 운영 상태", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": "2024-02-01", + "amount": null, + "parties": [ + "양정숙" + ], + "asset_refs": [ + "흑석동 상가" + ], + "legal_effect_summary": "점유 및 운영" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-018", + "E-022" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-018-01", + "EVT-022-03" + ], + "source_meeting_clause_ids": [ + "s01_consultation:5-나" + ], + "source_evidence_indexes": [ + "E-018", + "E-022" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "possession_state": { + "asset_ref": "흑석동 상가", + "possession_events": [ + { + "possessor_candidates": [ + "양정숙" + ], + "start_date": "2024-02-01", + "end_date": null, + "source_event_candidate_ids": [ + "EVT-018-01", + "EVT-022-03" + ], + "evidence_indexes": [ + "E-018", + "E-022" + ] + } + ], + "legal_cause_assertions": [ + { + "cause_asserted": "임대차", + "asserted_by": "양정숙", + "assertion_date": "2024-02-01", + "source_event_candidate_ids": [ + "EVT-022-03" + ], + "evidence_indexes": [ + "E-022" + ] + } + ] + } + } + } + }, + { + "candidate_ref": "B5:016", + "BOType": "state", + "ActionType": "소송행위(litigation acts)", + "JuristicAct": { + "label": "항변" + }, + "Action": "윤건우의 임대차 책임 부인 및 양정숙 차임 연체 주장 항변", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [ + "윤건우", + "양정숙" + ], + "asset_refs": [ + "흑석동 상가" + ], + "legal_effect_summary": "항변 주장" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-009" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-009-03" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:5" + ], + "source_evidence_indexes": [ + "E-009" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "defense_rebuttal": [ + { + "defense_asserted": "임대차 책임 부인 및 차임 연체 주장" + } + ] + } + } + }, + { + "candidate_ref": "B5:017", + "BOType": "state", + "ActionType": "소송행위(litigation acts)", + "JuristicAct": { + "label": "상계" + }, + "Action": "오민한의 강용원에 대한 자신의 5,000만 원 채권 상계 주장 항변", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [ + "오민한", + "강용원" + ], + "asset_refs": [], + "legal_effect_summary": "상계 주장" + }, + "amount": { + "value": "50000000", + "currency": "KRW", + "basis": null + }, + "source_evidence_indexes": [ + "E-010" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-010-03" + ], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [ + "E-010" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "setoff_or_payment_defense_hints": [ + { + "hint": "오민한이 자신의 5,000만 원 채권으로 상계하겠다고 주장" + } + ] + } + } + }, + { + "candidate_ref": "B5:018", + "BOType": "state", + "ActionType": "소송행위(litigation acts)", + "JuristicAct": { + "label": "항변" + }, + "Action": "박광윤의 과거 임대료 및 거주 수익을 채무에 충당하였다는 주장 항변", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [ + "박광윤", + "강용원", + "양정숙" + ], + "asset_refs": [ + "평택시 빌라" + ], + "legal_effect_summary": "채무충당 주장" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-011" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-011-03" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:4-나" + ], + "source_evidence_indexes": [ + "E-011" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "defense_rebuttal": [ + { + "defense_asserted": "과거 임대료 및 거주 수익을 채무에 충당하여 소멸 주장" + } + ] + } + } + }, + { + "candidate_ref": "B5:019", + "BOType": "state", + "ActionType": "소송행위(litigation acts)", + "JuristicAct": { + "label": "항변" + }, + "Action": "주식회사 대한은행의 경매공신력 주장(민사집행법 제267조)에 따른 근저당 말소 불가 항변", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": "2024-12-22", + "amount": null, + "parties": [ + "주식회사 대한은행" + ], + "asset_refs": [ + "성수동 대지" + ], + "legal_effect_summary": "경매공신력 주장" + }, + "amount": { + "value": null, + "currency": null, + "basis": null + }, + "source_evidence_indexes": [ + "E-012" + ], + "provenance": { + "source_event_candidate_ids": [ + "EVT-012-02" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:2" + ], + "source_evidence_indexes": [ + "E-012" + ] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "defense_rebuttal": [ + { + "defense_asserted": "경매 절차의 적법성과 민사집행법 제267조에 따른 보호 주장" + } + ] + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B5:review:001", + "severity": "SOFT_WARNING", + "issue_type": "legal_theory_required", + "description": "오민한의 김선웅 해드림 영업양도(E-020)로 인한 고객 및 고용관계 승계가 물품대금 채무 인수를 포함하는지에 대한 법적 평가 및 확정 필요", + "source_event_candidate_ids": [ + "EVT-020-03", + "EVT-028-01" + ], + "source_evidence_indexes": [ + "E-020", + "E-028" + ], + "source_meeting_clause_ids": [ + "s02_party_assertions:1" + ], + "recommended_downstream_owner": "C5" + }, + { + "review_id": "B5:review:002", + "severity": "SOFT_WARNING", + "issue_type": "missing_source", + "description": "오민한이 발송한 내용증명(E-010-NT-01)의 수취인 도달 여부 및 일자를 확인할 수 있는 객관적 증빙자료 누락", + "source_event_candidate_ids": [ + "EVT-010-02" + ], + "source_evidence_indexes": [ + "E-010" + ], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "publisher" + } + ], + "handoff_guard": { + "schema_version": "task_c_bo_stage_b_seed_handoff_guard.v1", + "handoff_target": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B5_Succession_Notice_Lien_Asset_Defense.json", + "publisher_task": "Task_C_BO_Stage_B_worker_output_publisher", + "source_worker_task": "Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense", + "source_task": "B5_Succession_Notice_Lien_Asset_Defense", + "source_contract": "direct_prev_task_json_output.stage_b_domain_bo_seed_output", + "source_prev_path": "prev.Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense.json_output", + "domain_id": "B5_Succession_Notice_Lien_Asset_Defense", + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": "2026-07-10T07:27:06.635927+00:00", + "input_digests_sha256": { + "client_meeting.md": "cb95320487cf2bc7a02cecd6324743e45677de84f7303c7e6087b47481577fa8", + "evidence_event_candidates.json": "666c3167a4058df9b4e98fd4bb0504e3823befb7d30b8b32cc319554dbc347e0", + "evidence_indexed.json": "2f7e3a3fdd1682e853aad08242a4ddf009c0d6cb066b83845744f75770f8272a", + "Default_Agent/Juristic_Act.md": "c3cd23f52d6fed5168f95074e801a381b3fdea607ba50a60247666bbb6a82e49" + } + } + } +} \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_1_Claude_v3.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_1_Claude_v3.yml new file mode 100644 index 00000000..50f2e87c --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_1_Claude_v3.yml @@ -0,0 +1,4504 @@ +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_1 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Part 1 기초작업 + version: v.2 + + Stages: + - name: stage1_사건개요파악_기초작업 + description: 사건개요 파악, 기초정보 추출 + llm_provider: openai + llm_model: gpt-4o-2024-08-06 + tools: + mcpServers: + localdocs: + type: streamable-http + url: "http://mcp-localdocs:8012/mcp" + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + + tasks: + - task_name: Task_A_client_goal + llm_provider: google + llm_model: 'gemini-3.1-flash-lite' + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + preflight: false + preflight_files: + - client_meeting.md + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + - Aggregate, compile, publish, commit, or write only the inputs and final/derived file authorized by the current task. + - Preserve stable order, source IDs, support IDs, BO_ID, fact_id, structure_id, claim_group_id, liability_group_id, and required enum semantics exactly. + - Create or finalize IDs only from the current task's specified pattern and deterministic ordering rule. + - Do not merge, deduplicate, supersede, normalize, repair, or drop candidates unless the current reducer/writer schema authorizes that action. + - If validation or source support is insufficient, use the current failure, rollback, warning, or blocked path instead of fabricating a passing final artifact. + - Treat downstream authority and handoff fields as machine contracts; do not add prose, alternative files, or extra writer attestations outside the schema. + + + + + Task_A_client_goal + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. + + + `client_meeting.md`만 읽고 `client_goal.json`을 생성한다. + 이 파일은 고객의 소송 목적, 명시적 제약, 핵심 사건군, 당사자 universe를 Stage 1의 가장 앞단에서 안정적으로 구조화한 요약본이어야 한다. + v.3에서도 기존 필수 schema는 유지하되, 복수 원고와 복수 분쟁군이 뒤섞여 later stage가 공동원고를 과잉 일반화하지 않도록 사건군 단위의 optional 구조, 사건군별 원고적격 구조, 현재 지배상태 구조를 함께 남긴다. + 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않는다. + 특히 상속·승계형 사건군에서는 meeting만으로 확정되지 않은 친족관계 또는 공동행동 정보를 direct standing anchor처럼 과잉 일반화하지 않는다. + 상속이 개입된 사건군에서는 `legal entitlement layer`와 `complaint drafting preference layer`를 분리하여 기록한다. + 사망한 종전 권리주체의 과거 소유사실, 매수사실, 등기명의사실을 living plaintiff의 현재 `소유자/final`로 치환하지 않는다. + 필요하면 사건군별 optional additive field `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`, `preferred_plaintiff_selection_basis`, `succession_resolution_needed`를 사용하여, later stage가 실체법상 가능한 권리자 후보층과 실제 소장에 세울 우선 원고층을 혼동하지 않도록 한다. + 또한 `current_control_map`은 보조 메모가 아니라 later stage의 부당이득·인도·철거 청구 식별에 직접 쓰이는 authority map이므로, 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속을 가능한 한 4축으로 분해 저장한다. + v.3 기준으로 Task_A_client_goal은 모든 issue cluster에 사건군별 법률효과 구조의 seed를 남겨야 한다. 단순 사건 요약이 아니라 금전채권 timeline, 상호속용 책임, 담보부 부동산·지분별 권리상태, 사해행위취소 가액배상, 유치권·상속·통지 lifecycle을 downstream이 구조적으로 읽을 수 있게 `domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints`, `legal_entitlement_candidates`, `current_control_map_4axis`를 additive하게 생성한다. + `client_requested_relief`는 의뢰인이 원하는 청구 기간·금액·상대방을 보존하는 층이고, `legal_sustainability_watchpoints`는 법률상 기산일·원고적격·청구범위 재검토 필요성을 보존하는 층이다. 두 층을 절대 하나로 합치지 않는다. + 상속·승계형 사건군에서 current right holder가 meeting만으로 확정되지 않으면 `current_control_map`의 해당 row는 `certainty="provisional"`로 두고 `standing_watchpoints`에 succession-resolution trigger를 반드시 남긴다. + 또한 현재 무자력/집행난과 실제 제소 유지 의사를 혼동하지 않으며, 특정 상대방을 현재 무자력이라는 이유만으로 소 제기 대상에서 소거하지 않는다. + 필요하면 optional additive field `defendant_target_matrix`를 사용하여 피고별 소제기 유지/제외 의사, 현재 회수위험, 장래 회수신호를 분리 보존한다. + 본 task는 v.3 architecture에서 final writer로서 `client_goal.json`을 직접 작성한다(Task_A는 island/mapper가 없으므로 propose 단계 없이 직접 finalize 가능). + + + + + - In this task, legal/domain fields are meeting-grounded routing seeds and watchpoints for downstream stages. + - They are not final legal conclusions, norm search results, merits judgments, or complaint-drafting advice. + - The EXTRACTION overlay's prohibition on norm search and final legal-effect classification does not prohibit schema-authorized `domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints`, or entitlement/watchpoint seed fields. + + + + + - `client_meeting.md` + + + - `client_goal.json` + + + - 다른 파일 확인/읽기 + - 중간 파일 생성 + - `client_goal.json` 재읽기 + - meeting에 없는 당사자, 사건군, 제약, 희망사항, 청구유형을 창작하기 + - 사건군별 당사자 범위를 무시하고 전 사건 공통 사실처럼 과잉 압축하기 + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하기 + + + + + 1. `list_docs`로 `client_meeting.md` 존재만 확인한다. + 2. `read_docs`로 `client_meeting.md`만 읽는다. + 3. 메모리에서 `client_goal.json` 객체를 생성한다. + 4. `write_file(overwrite=true)`로 `client_goal.json`을 저장한다. + 5. `list_docs`로 `client_goal.json` 존재만 확인한다. 절대 읽지 않는다. + 6. 정확히 `"고객의사 초안 생성 완료"`만 출력하고 종료한다. + + + + - 필수 입력 누락, 빈 파일, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. 파일은 쓰지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + + - `client_meeting.md`만 사실 원천이다. + - 상대방 주장, 의뢰인 희망사항, 상담서두의 인적 관계, 본문 1~5항의 사건군을 모두 읽되, meeting에 직접 나타난 범위만 구조화한다. + - meeting의 문장들 사이를 조합해 새로운 사실을 만들지 않는다. + + + + + - `primary_goal`은 사건 전체를 한 문장으로 요약하되, 개별 분쟁군을 완전히 뭉개지 않을 정도의 수준으로만 추상화한다. + - `희망사항`이 복수이면 공통 분모를 중심으로 요약하되, 특정 핵심 구제수단이 반복되면 반영할 수 있다. + - 희망사항이 피고별 비대칭 전략을 직접 드러내면, 그 비대칭을 보존한다. + - 현재 회수곤란 사실이 있더라도 장래 재산취득, 상속, 보험금 회수, 급여압류 가능성 등으로 특정 피고를 제소 대상으로 유지하려는 의사가 직접 서술되면, 이를 `primary_goal` 또는 `summary_key_incidents`에 반영한다. + - 직접 금전청구 대상과 사해행위취소 대상이 다르면 한쪽으로 뭉개지 않는다. + - 불명확하면 `null`. + + + + - `constraints`에는 의뢰인이 명시적으로 원하지 않는 소, 특정 대상 제외, 가능하면/다만/아울러와 같은 제한적 요청, 상속·경매 공신력 등 사건 진행상의 명시적 제약을 넣는다. + - `constraints`는 소송전략상 하지 말 것, 제한할 것, 절차상·법률상 한계만 담는다. + - 단순한 회수곤란 사실, 현재 무자력, 현재 재산 부족, 집행난은 `constraints`에 넣지 않는다. + - 현재 무자력과 장래 재산취득 가능성이 함께 서술되면, 이를 부정적 제약으로 압축하지 않는다. + - 특정 상대방을 제외하라는 명시적 no-sue 지시가 없는 한, current collectability만으로 대상 제외 제약을 만들지 않는다. + - 해석상 추정 제약은 넣지 않는다. + - 배열 순서는 meeting 서술 순서를 최대한 따른다. + + + + - `summary_key_incidents`는 최대 4문장, 각 100자 이하. + - 각 문장은 가능한 한 서로 다른 사건군을 대표하도록 구성한다. + - 사건군 라벨은 입력자료에 직접 나타난 분쟁 대상, 거래유형, 자산군, 청구 방향을 짧게 추상화하여 만든다. 특정 사건의 지명·상호·인명·자산명을 예시값처럼 재사용하지 않는다. + - 하나의 문장에 서로 다른 사건군을 과도하게 병합하지 않는다. + - 직접 서술된 날짜/당사자/핵심 행위만 사용한다. + - 원고가 특정 피고를 직접 금전청구 대상으로 유지하려는 의사가 직접 서술되면, 사건군 대표 문장 중 하나에 그 방향성을 반영할 수 있다. + - 현재 회수곤란 사실과 장래 회수가능성 신호가 같은 사람에 관하여 함께 직접 서술되면, 둘 중 하나만 선택적으로 삭제하지 않는다. + + + + - `key_facts`는 최대 5개, 각 60자 이하. + - 가능한 한 사건군별 핵심 fact를 1개씩 대표시킨다. + - later stage의 원고적격 판단에 중요할 수 있는 소유관계, 상속관계, 현재 권리귀속, 현재 점유/사용/수익귀속 상태, 특정 처분/경매/통지 사실을 우선한다. + - 직접 서술이 없는 금액·날짜·상태를 보충하지 않는다. + + + + - `claim_type_candidates`는 의뢰인의 희망사항과 사건군 구조를 바탕으로 선택한다. + - 허용 enum만 사용한다: `"금전"|"물건인도"|"행위"|"확인"|"형성"|"보전(가처분/가압류)"`. + - later stage 분류기를 돕기 위한 상위 유형 수준만 남기고, 세부 청구명을 쓰지 않는다. + + + + + + - 이름은 원문 우선, 명백히 동일인/동일법인인 경우만 canonicalize 한다. + - 별칭/변형은 `aliases`에 기록한다. + - 당사자 배열은 사건 전체 participant universe다. 모든 분쟁군의 실질 원고적격/피고적격을 의미하지 않는다. + + + + - `parties.plaintiffs`에는 실제 의뢰인 또는 meeting 서두와 희망사항에서 원고 측 client로 직접 읽히는 사람만 넣는다. + - `parties.plaintiffs`는 client universe일 뿐, 사건군별 실질 원고적격을 의미하지 않는다. + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않는다. + - 사건군별 원고 범위 차이는 `issue_clusters`, `plaintiff_capacity_matrix`, `standing_watchpoints`에서 보정한다. + + + + - `parties.defendants`에는 meeting 본문, 상대방 주장, 희망사항에서 분쟁 상대방 또는 소송상 직접 대상으로 명시된 자만 넣는다. + - `asset_status`는 meeting에 직접 나온 재산 상태, 현재 소유/점유/담보권 설정 상태 등 later stage에 직접 도움이 되는 짧은 상태만 넣는다. + - `asset_status`는 현재 재산 상태에 관한 사실기술일 뿐, 소 제기 대상 제외 신호가 아니다. + - meeting이 "현재 무자력이나 장래 상속/재산취득 가능성 있음"을 직접 서술하면, `asset_status`는 그 둘을 함께 보존하는 방향으로 적는다. + - 현재 무자력만을 이유로 defendants에서 제외하지 않는다. 제외는 명시적 no-sue 지시가 있을 때만 허용한다. + - meeting에 없는 재산상태 추정 금지. + + + + - `parties.third_parties`에는 사건군에 중요하지만 즉시 원고/피고로 확정되지 않은 자를 넣는다. + - `relationship`은 later stage가 읽을 수 있도록 짧고 직접적으로 적는다. + + + + + - optional additive field `defendant_target_matrix`를 허용한다. + - 이 필드는 피고별 소제기 유지/제외 의사와 현재 회수위험을 분리 기록하는 전략 매트릭스다. + - `target_status`는 제소 유지 여부를 나타내고, `execution_risk`는 current collectability를 나타낸다. + - 현재 무자력은 `execution_risk`에 반영하고, 제소 유지 여부는 `target_status`에 별도로 반영한다. + - 장래 상속, 장래 재산취득, 보험금 회수 가능성, 급여압류 가능성 등 미래 회수신호가 직접 서술되면 `future_recovery_signal`에 반영한다. + - 명시적 no-sue 지시가 없는 한, 단순한 current uncollectability만으로 `exclude_now`를 부여하지 않는다. + - 각 원소는 아래 구조를 따른다. + { + "name": "string", + "target_status": "target_now|exclude_now|secondary_target|review", + "basis_text": "meeting 직접 문장에 근거한 짧은 사유", + "execution_risk": "low|medium|high|unknown", + "future_recovery_signal": "none|possible|explicit" + } + - `basis_text`는 반드시 meeting 직접 문장에 근거한 짧은 근거여야 한다. + - 이 필드는 optional이다. 명시적 피고별 타기팅 차이가 없으면 생략할 수 있다. + + + + - optional additive field `issue_clusters`를 허용한다. + - 각 cluster는 meeting의 독립 분쟁군을 1개씩 반영한다. + - 이 필드는 later stage가 전 사건 공통 parties 배열만 보고 과잉 일반화하는 것을 줄이기 위한 보조 구조다. + - `candidate_plaintiffs`는 사건군별 잠정 plaintiff anchor일 뿐, 최종 원고적격 확정값이 아니다. + - `legal_entitlement_candidates`는 해당 사건군에서 meeting 직접 서술상 실체법상 권리자일 가능성이 있는 의뢰인 후보층이다. + - `preferred_plaintiffs_for_complaint`는 later stage가 실제 소장 원고로 우선 검토할 수 있는 narrower drafting layer다. + - 상속·승계형 사건군에서는 `candidate_plaintiffs`, `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`를 서로 자동 동일시하지 않는다. + - `candidate_plaintiffs`에는 meeting에서 해당 사건군의 직접 권리주체, 직접 채권자, 직접 임차인, 직접 점유권자 등으로 읽히는 의뢰인만 넣는다. + - 친족관계, 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 `candidate_plaintiffs`를 확장하지 않는다. + - 사망한 자가 해당 사건군의 종전 권리주체로 보이는데 meeting만으로 승계가 해소되지 않으면, 친족관계나 공동 통지 사실만으로 `candidate_plaintiffs`를 채우지 않는다. + - 위와 같은 unresolved succession cluster에서는 meeting이 직접 보여주는 친족관계와 자산 연결 범위 안에서만 `legal_entitlement_candidates`를 별도 기록할 수 있다. + - `preferred_plaintiffs_for_complaint`는 meeting이 특정 의뢰인을 직접 권리주체 또는 직접 소송유지 주체로 좁혀 보여주거나, 비상속형 사건군에서 direct right-holder가 명백할 때만 채운다. + - meeting만으로 drafting preference가 명백하지 않은 unresolved succession cluster에서는 `preferred_plaintiffs_for_complaint`를 빈 배열로 둘 수 있다. + - `preferred_plaintiff_selection_basis` 허용값은 `"direct_right_holder|resolved_succession|resolved_succession_needed|drafting_preference|review|불명"`이다. + - `succession_resolution_needed`는 사망한 종전 권리주체와 사건군의 연결은 보이지만 meeting만으로 현재 권리귀속 또는 complaint plaintiff narrowing이 해소되지 않을 때 `true`로 둔다. + - meeting이 직접 금전청구 대상 피고와 사해행위 상대방/수익자를 구별해 보여주면, 이를 별도 cluster로 분리할 수 있다. + - 직접 금전회수 대상이 특정 상대방으로 서술되면, property transfer cluster와 별도의 monetary recovery cluster를 허용한다. + - cluster 수는 1개 이상 6개 이하로 제한한다. + - 각 cluster는 아래 구조를 따른다. + { + "cluster_id": "IC-01", + "cluster_label": "짧은 사건군명", + "candidate_plaintiffs": ["이 사건군에서 직접 권리주체로 읽히는 의뢰인"], + "legal_entitlement_candidates": ["실체법상 권리자 후보로 읽히는 의뢰인"], + "preferred_plaintiffs_for_complaint": ["실제 소장 원고로 우선 검토할 의뢰인"], + "preferred_plaintiff_selection_basis": "direct_right_holder|resolved_succession|resolved_succession_needed|drafting_preference|review|불명", + "succession_resolution_needed": true, + "candidate_defendants_or_counterparties": ["직접 상대방"], + "desired_relief_keywords": ["금전", "명도", "퇴거", "확인", "보전" 등 짧은 키워드] + } + - 비상속형 사건군에서 direct right-holder가 명백하면 `candidate_plaintiffs`, `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`가 동일할 수 있다. + - `issue_clusters`는 optional이다. 명확하지 않으면 생략 가능하다. + - 추가 사실 창작 없이 meeting 본문과 희망사항에서 직접 읽히는 범위만 기록한다. + + + + - optional additive field `domain_profiles`를 각 `issue_clusters[*]` 내부에 허용한다. + - `domain_profiles`는 아래 네 하위 profile을 담는 object다. 해당하지 않는 profile은 `null`로 둔다. + - `monetary_claim_profile` + - `secured_property_dispute_profile` + - `actio_property_transfer_profile` + - `possession_lien_succession_profile` + - `client_requested_relief`와 `legal_sustainability_watchpoints`를 각 cluster 내부에 둘 수 있다. + - 의뢰인이 원하는 청구취지·기간·금액은 `client_requested_relief`에 보존하고, 법률상 기산일·원고적격·범위가 불명확한 점은 `legal_sustainability_watchpoints`에 분리한다. + - 당사자가 원하는 기간이나 금액을 법률상 청구취지에 바로 사용할 수 있다고 단정하지 않는다. + + + - 금전청구 사건군에서 원채무자 외 보증인, 채무인수자, 영업양수인, 상호속용자, 공동채무자 후보가 함께 등장하면 이를 별도 후보 피고 목록에만 두지 말고 `monetary_claim_profile`, `liability_group_hint`, `commercial_successor_liability_hint`로 묶어 보존한다. + - 동일 금액, 동일 원채권, 동일 원고를 기준으로 복수 피고가 책임질 가능성이 있으면 `liability_group_hint.same_underlying_debt=true`로 둔다. + - 지급기일, 이행청구, 최고, 채무승인, 이행각서, 소멸시효, 변제기 재설정 단서가 등장하면 `monetary_claim_profile.requires_money_claim_timeline=true`로 둔다. + - 영업양도, 상호 계속 사용, 사업 양수인, 채무승계 부인, 책임배제 등기 단서가 등장하면 `commercial_successor_liability_hint.requires_article_42_review=true`로 둔다. + + + + - 부동산 사건에서 여러 대여금, 여러 근저당권, 지분별 담보, 변제, 경매, 후행 등기가 함께 등장하면 이를 흩어진 issue로 분리하지 말고 `secured_property_dispute_profile`에 묶는다. + - 변제금이 복수 채무에 영향을 줄 수 있으면 `requires_payment_allocation=true`로 표시한다. + - 전체 부동산이 아니라 지분별 유효/무효가 문제될 수 있으면 `requires_fractional_validity_analysis=true`로 표시한다. + - 원고 주장 지분, 피고 측 유효 가능 지분, 전체 부동산 말소 금지 필요성을 `fractional_title_issue_hint`에 남긴다. + - 미등기 건물, 건물 신축, 건물 매매, 대지 임대차가 함께 등장하면 `land_use_gain_hint`를 생성한다. + - 임대차보증금, 월 차임, 건물 매매대금은 각각 법적 성격이 다르므로 `lease_payment_semantics_hint`에 별도 표시한다. + - 임대차보증금은 토지소유자의 부당이득 원금으로 자동 산정하지 말라는 watchpoint를 남긴다. + + + + - 채무자가 부동산을 수익자에게 이전하고, 원고가 사해행위취소를 희망하는 사건군을 발견하면 `actio_property_transfer_profile`을 생성한다. + - 부동산에 근저당권 등 부담이 있고, 그 부담이 사해행위 이후 변제·말소되었다는 단서가 있으면 `requires_value_compensation_review=true`로 표시한다. + - 같은 채권자와 채무자 사이에 복수 대여금이 등장하면 단순 후보채권 목록으로 흩뜨리지 말고 `preserved_claim_bundle_hint`로 묶는다. + - 피고가 명의신탁, 충분 담보, 상계, 선의 등을 주장하면 이를 `defense_hints`에 남기고, 청구원인 성립 또는 범위에 직접 영향을 주는 항변이면 `requires_stage2_rebuttal=true`로 표시한다. + + + + - 현재 점유자와 현재 권리자가 분리되는 자산군 또는 사건군이 입력자료에서 직접 확인되면 `possession_lien_succession_profile`을 생성한다. + - 상속이 개입되면 current right holder를 final로 확정하지 말고 `succession_resolution_needed=true`와 `standing_watchpoints`를 남긴다. + - 유치권, 제3자 임대, 직접 거주, 피담보채권 충당, 후소유자 소멸청구권 부정 주장이 보이면 `possession_basis_asserted`, `notice_lifecycle_hints`, `legal_sustainability_watchpoints`를 구조화한다. + - 통지서 발송일과 답변서상 수령 인정일이 함께 보이면 발송일과 도달일을 분리하여 `notice_lifecycle_hints`에 남긴다. + - 상속·승계가 미해결인 cluster에서는 입력자료상 공동 통지 발신자, 친족 후보, 직계존속·직계비속 후보를 그 지위의 후보로만 보존하고, 상속 해결 전 `preferred_plaintiffs_for_complaint` 확정자로 두지 않는다. + + + + + - optional additive field `current_control_map_4axis`를 허용한다. + - legacy `current_control_map` row 구조는 유지하되, 목적물별 4축 요약이 가능한 경우 `current_control_map_4axis`를 추가한다. + - 각 item은 아래 구조를 따른다. + { + "object": "string", + "cluster_id": "IC-01", + "current_right_holder_candidate": "string|null", + "current_possessor": "string|null", + "current_user": "string|null", + "current_benefit_holder": "string|null", + "possession_basis_asserted": "string|null", + "possession_basis_transition_needed": true, + "certainty": "final|provisional|불명" + } + - 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속을 하나의 generic holder로 뭉개지 않는다. + - 상속·승계형 사건군에서 현재 권리귀속이 decedent의 과거 권리사실과 친족관계로만 추론되는 경우 `certainty="provisional"`로 둔다. + + + + - optional additive field `plaintiff_capacity_matrix`를 허용한다. + - 이 필드는 사건군별 실질 원고적격을 later stage로 넘기기 위한 standing map이다. + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않는다. + - 각 원소는 아래 구조를 따른다. + { + "cluster_id": "IC-01", + "plaintiff_name": "string", + "capacity_basis": "소유자|공유자|상속인|임차인|채권자|점유권자|불명", + "source_basis_text": "meeting에서 직접 읽히는 짧은 근거", + "certainty": "final|provisional" + } + - 사건군별 직접 권리주체로 읽히는 사람만 넣는다. + - `capacity_basis="상속인"` row라도, 근거가 친족관계, 사망, 공동 통지, 공동 상담 같은 succession trigger 수준이면 `certainty`는 반드시 `"provisional"`로 둔다. + - 상속·승계형 사건군에서 decedent의 과거 소유사실, 매수사실, 등기명의사실만 보일 뿐 living plaintiff의 승계 완료 또는 현재 권리귀속이 meeting에서 직접 해소되지 않으면, living plaintiff를 `capacity_basis="소유자"` 또는 `capacity_basis="공유자"` + `certainty="final"`로 두지 않는다. + - 위 경우에는 원칙적으로 `capacity_basis`를 `"상속인"` 또는 `"불명"`으로 두고, `certainty`를 `"provisional"`로 둔다. + - meeting이 특정 사건군에 관하여 직접 계약당사자, 직접 채권자, 직접 임차인, 직접 소유·공유 관계 또는 승계 완료 상태를 직접 보여줄 때만 `certainty="final"`을 허용한다. + - 원고적격이 불명확하거나 later stage 재검토가 필요하면 `certainty`는 `"provisional"`로 둔다. + - `preferred_plaintiffs_for_complaint`가 별도로 존재하더라도, 이를 맞추기 위해 `plaintiff_capacity_matrix`를 `소유자/final`로 과잉 보정하지 않는다. + + + + - optional additive field `current_control_map`를 허용한다. + - 이 필드는 사건군별 현재 지배상태를 later stage로 넘기기 위한 authority map이다. + - 사건군에 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속 사실이 직접 읽히면 가능한 한 `current_control_map`을 생성한다. + - 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속은 가능한 한 분리한다. + - 각 원소는 아래 구조를 따른다. + { + "cluster_id": "IC-01", + "holder_name": "string", + "holder_role": "current_right_holder|current_possessor|current_user|current_benefit_holder", + "basis_text": "meeting에서 직접 읽히는 짧은 근거", + "certainty": "final|provisional" + } + - 같은 사람이 복수 role을 가지면 중복 기재할 수 있다. + - meeting에 직접 없는 role은 추정하지 않는다. + - 상속·승계형 사건군에서 current right holder가 decedent의 과거 권리사실 + 친족관계만으로 유도되는 경우, `holder_role="current_right_holder"` row는 원칙적으로 `certainty="provisional"`로 둔다. + - decedent의 과거 소유사실만으로 living plaintiff를 `current_right_holder/final`로 기재하지 않는다. + - 비상속형 사건군에서 meeting이 현재 소유자, 현재 점유자, 현재 사용자, 현재 수익귀속자를 직접 적시하면 해당 row는 `certainty="final"`을 우선 검토한다. + + + + - optional additive field `standing_watchpoints`를 허용한다. + - later stage가 반드시 재검토해야 하는 원고적격/권리귀속 리스크를 짧은 경고문으로 남긴다. + - 상속, 상속포기, 사망, 경매, 공동소유, 현재 권리주체 불명, 현재 점유자와 권리주체 불일치가 보이면 우선 검토한다. + - 경고문은 가능한 한 자산군 또는 사건군을 특정하는 asset-specific trigger 형태로 쓴다. + - 예시 형식: + - "상속·승계 자료 추가 확인 후 해당 자산군 원고적격 재확인 필요" + - "현재 권리귀속과 현재 점유자 분리 검토 필요" + - "사망한 종전 권리주체 관련 succession resolution 필요" + - meeting에 직접 읽히는 범위를 넘는 장문 법률판단은 쓰지 않는다. + + + + - `summary_key_incidents`, `key_facts`, `constraints`는 exact-match 중복 제거 후 최초 순서를 유지한다. + - `aliases`는 명백한 경우에만 기록한다. + - `claim_type_candidates`는 중복 제거 후 최초 순서를 유지한다. + - `defendant_target_matrix`를 사용하면 same `name` + `target_status` 조합 기준 exact-match 중복 제거 후 최초 순서를 유지한다. + - 비nullable string 외에는 `불명` 사용을 피하고 `null` 또는 빈 배열을 우선한다. + + + + 기존 필수 schema는 유지하고, 아래 optional additive field를 추가할 수 있다. + v.3 schema registry 기준 `IssueClusterDomainProfile`, `LegalEntitlementCandidate`, `CurrentControlMap4Axis`를 반영한다. + + { + "primary_goal": "string|null", + "constraints": [], + "summary_key_incidents": [], + "key_facts": [], + "claim_type_candidates": ["금전","물건인도","행위","확인","형성","보전(가처분/가압류)"], + "parties": { + "plaintiffs": [{"name":"string","type":"법인|자연인|기관"}], + "defendants": [{"name":"string","type":"법인|자연인|기관|미확정","asset_status":"string|null"}], + "third_parties": [{"name":"string","relationship":"string"}] + }, + "aliases": {}, + + "defendant_target_matrix": [ + { + "name": "string", + "target_status": "target_now|exclude_now|secondary_target|review", + "basis_text": "string", + "execution_risk": "low|medium|high|unknown", + "future_recovery_signal": "none|possible|explicit" + } + ], + + "issue_clusters": [ + { + "cluster_id": "IC-01", + "cluster_label": "string", + "candidate_plaintiffs": ["string"], + "legal_entitlement_candidates": ["string"], + "preferred_plaintiffs_for_complaint": ["string"], + "preferred_plaintiff_selection_basis": "direct_right_holder|resolved_succession|resolved_succession_needed|drafting_preference|review|불명", + "succession_resolution_needed": true, + "candidate_defendants_or_counterparties": ["string"], + "desired_relief_keywords": ["string"], + "domain_profiles": { + "monetary_claim_profile": { + "underlying_claim_type": "string|null", + "principal_amount_candidate": "string|null", + "principal_debtor_candidates": ["string"], + "successor_or_secondary_liability_candidates": [], + "default_or_limitation_issue_expected": true, + "known_demand_or_notice_signals": ["string"], + "requires_money_claim_timeline": true + }, + "secured_property_dispute_profile": { + "has_multiple_secured_debts": true, + "requires_payment_allocation": true, + "requires_fractional_validity_analysis": true, + "secured_object_candidates": ["string"], + "auction_or_registry_invalidity_expected": true + }, + "actio_property_transfer_profile": { + "debtor": "string|null", + "creditor": "string|null", + "beneficiary": "string|null", + "fraudulent_act_asset": "string|null", + "fraudulent_act_type": "string|null", + "fraudulent_act_date_hint": "string|null", + "is_encumbered_real_estate": true, + "requires_value_compensation_review": true + }, + "possession_lien_succession_profile": { + "possession_basis_asserted": "string|null", + "succession_resolution_needed": true, + "notice_lifecycle_hints": [], + "lien_violation_or_extinction_hints": [] + } + }, + "client_requested_relief": ["string"], + "legal_sustainability_watchpoints": ["string"], + "liability_group_hint": {}, + "commercial_successor_liability_hint": {}, + "fractional_title_issue_hint": {}, + "land_use_gain_hint": {}, + "lease_payment_semantics_hint": {}, + "preserved_claim_bundle_hint": {}, + "value_compensation_hint": {}, + "defense_hints": [] + } + ], + + "plaintiff_capacity_matrix": [ + { + "cluster_id": "IC-01", + "plaintiff_name": "string", + "capacity_basis": "소유자|공유자|상속인|임차인|채권자|점유권자|불명", + "source_basis_text": "string", + "certainty": "final|provisional" + } + ], + + "current_control_map": [ + { + "cluster_id": "IC-01", + "holder_name": "string", + "holder_role": "current_right_holder|current_possessor|current_user|current_benefit_holder", + "basis_text": "string", + "certainty": "final|provisional" + } + ], + + "current_control_map_4axis": [ + { + "object": "string", + "cluster_id": "IC-01", + "current_right_holder_candidate": "string|null", + "current_possessor": "string|null", + "current_user": "string|null", + "current_benefit_holder": "string|null", + "possession_basis_asserted": "string|null", + "possession_basis_transition_needed": true, + "certainty": "final|provisional|불명" + } + ], + + "standing_watchpoints": ["string"] + } + + 추가 제약: + - `defendant_target_matrix`, `issue_clusters`, `plaintiff_capacity_matrix`, `current_control_map`, `standing_watchpoints`는 optional이다. + - `domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints`, `liability_group_hint`, `commercial_successor_liability_hint`, `fractional_title_issue_hint`, `land_use_gain_hint`, `lease_payment_semantics_hint`, `preserved_claim_bundle_hint`, `value_compensation_hint`, `defense_hints`는 `issue_clusters` 내부 optional additive field다. + - `current_control_map_4axis`는 top-level optional additive field이며 legacy `current_control_map`을 대체하지 않는다. + - `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`, `preferred_plaintiff_selection_basis`, `succession_resolution_needed`는 `issue_clusters` 내부 optional additive field다. + - 기존 필수 필드는 모두 유지한다. + - 허용된 additive field 외 임의 추가 필드 금지. + + + + - `parties.plaintiffs`에는 실제 의뢰인만 남았는지 점검한다. + - `parties.plaintiffs`가 client universe로만 쓰였고, 사건군별 원고적격 확정값처럼 오염되지 않았는지 점검한다. + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않았는지 점검한다. + - `parties.defendants`와 `third_parties`가 뒤바뀌지 않았는지 점검한다. + - `summary_key_incidents`가 사건군별로 과잉 병합되지 않았는지 점검한다. + - `key_facts`가 later stage에 중요한 standing/ownership/current control fact를 누락하지 않았는지 점검한다. + - `issue_clusters`를 썼다면 각 cluster의 당사자와 desired relief가 meeting 직접 서술 범위를 넘지 않는지 점검한다. + - 상속·승계형 사건군에서 `candidate_plaintiffs`가 친족관계, 공동 통지, 공동 상담 같은 간접 신호만으로 확장되지 않았는지 점검한다. + - `issue_clusters`를 썼다면 unresolved succession cluster에서 `legal_entitlement_candidates`와 `preferred_plaintiffs_for_complaint`가 자동 동일시되지 않았는지 점검한다. + - `preferred_plaintiffs_for_complaint`를 썼다면 그 basis가 `preferred_plaintiff_selection_basis`에 남아 있고, meeting 직접 서술 또는 direct right-holder 구조를 넘지 않는지 점검한다. + - `succession_resolution_needed=true`인데도 해당 cluster가 direct owner/final cluster처럼 오염되지 않았는지 점검한다. + - `plaintiff_capacity_matrix`를 썼다면 각 row의 `capacity_basis`와 `source_basis_text`가 meeting 직접 서술 범위를 넘지 않는지 점검한다. + - `capacity_basis="상속인"` row가 친족관계나 공동행동 사실만으로 `certainty="final"`로 오염되지 않았는지 점검한다. + - 사망한 종전 권리주체의 과거 소유사실, 매수사실, 등기명의사실이 living plaintiff의 `소유자/final` row로 잘못 치환되지 않았는지 점검한다. + - `current_control_map`을 썼다면 `holder_role`이 `current_right_holder|current_possessor|current_user|current_benefit_holder` 중 하나인지와, basis가 meeting 직접 문장에 근거하는지 점검한다. + - 상속·승계형 사건군에서 `current_right_holder` row가 있으면, meeting만으로 승계가 해소되지 않은 경우 `certainty="provisional"`인지 점검한다. + - 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속이 서로 다른데 하나의 generic holder row로 뭉개지지 않았는지 점검한다. + - 사망한 종전 권리주체가 보이는 사건군이 있으면 `standing_watchpoints`에 succession-resolution trigger가 남았는지 점검한다. + - `standing_watchpoints`를 썼다면 장문 법률판단이 아니라 later stage 재검토용 짧은 경고인지 점검한다. + - meeting이 피고별로 현재 집행가능성과 장래 회수가능성을 구별하여 서술하면, 그 차이가 `constraints`, `primary_goal`, `summary_key_incidents`, `parties.defendants.asset_status`, `defendant_target_matrix` 중 어디엔가 비대칭적으로 보존되었는지 점검한다. + - "현재 무자력"과 "장래 재산취득 가능성"이 같은 사람에게 함께 서술되면, 그 사람을 자동 배제 대상으로 압축하지 않았는지 점검한다. + - `constraints`에 current collectability나 단순 회수곤란 사실이 잘못 들어가 있지 않은지 점검한다. + - `asset_status`가 제소 배제 신호처럼 쓰이지 않았는지 점검한다. + - `defendant_target_matrix`를 썼다면 각 `name`이 `parties.defendants`와 모순되지 않는지 점검한다. + - `defendant_target_matrix`에서 `target_status="exclude_now"`가 붙은 사람에게는 meeting상 명시적 no-sue 지시가 있는지 점검한다. + - `defendant_target_matrix`에서 `execution_risk="high"`와 `target_status="target_now"`가 함께 존재할 수 있음을 허용하되, 그 경우 `basis_text` 또는 `future_recovery_signal`이 비어 있지 않은지 점검한다. + - 장래 상속/재산취득 가능성이 직접 서술된 피고가 `exclude_now`로 잘못 내려가지 않았는지 점검한다. + - 금전채권 cluster에 최고ㆍ채무승인ㆍ이행각서ㆍ시효 단서가 있는데 `monetary_claim_profile.requires_money_claim_timeline`이 없거나 false이면 warning으로 본다. + - 동일 사건군에 원채무자와 승계ㆍ양수ㆍ보증ㆍ상호속용 책임 후보가 함께 있는데 `liability_group_hint`가 비어 있으면 warning으로 본다. + - 영업양도ㆍ상호속용 단서가 있는데 `commercial_successor_liability_hint.requires_article_42_review`가 없으면 warning으로 본다. + - 대여금ㆍ근저당권ㆍ경매ㆍ지분 단서가 같은 cluster에 있는데 `secured_property_dispute_profile.requires_payment_allocation`이 없으면 warning으로 본다. + - 상담문서에 지분별 권리상태가 보이는데 `fractional_title_issue_hint`가 없으면 warning으로 본다. + - 미등기 건물ㆍ임대차ㆍ건물 매매ㆍ토지 사용 단서가 있는데 `land_use_gain_hint`가 없으면 warning으로 본다. + - 사해행위취소ㆍ부담부 부동산ㆍ사후 담보권 말소 단서가 있는데 `actio_property_transfer_profile.requires_value_compensation_review`가 false이면 error로 본다. + - 같은 채권자와 채무자 사이 복수 대여금이 보이는데 `preserved_claim_bundle_hint`가 없으면 warning으로 본다. + - 명의신탁ㆍ충분담보ㆍ상계ㆍ선의 등 항변 단서가 있는데 `defense_hints`가 없으면 warning으로 본다. + - 유치권ㆍ상속ㆍ승계 사건군에서 `succession_resolution_needed=true`인데도 입력자료상 공동 통지 발신자, 친족 후보, 직계존속·직계비속 후보가 `preferred_plaintiffs_for_complaint` 확정자로 들어가면 error로 본다. + - 통지 발송일과 도달일 단서가 모두 있는데 `notice_lifecycle_hints`가 없으면 warning으로 본다. + - `current_control_map_4axis`를 썼다면 4축 중 직접 읽히는 축이 generic holder 하나로 합쳐지지 않았는지 점검한다. + - 출력 파일명과 성공 메시지가 정확한지 점검한다. + + + + + + [ ] 1) Preflight: `list_docs` 사용해서 `client_meeting.md` 확인, `read_docs` 사용해서 `client_meeting.md` 읽기 + [ ] 2) `write_file` 사용해서 `client_goal.json` 생성 + [ ] 3) `list_docs` 사용해서 `client_goal.json` 파일 존재만 확인(절대 읽기 금지) + [ ] 4) `"고객의사 초안 생성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `client_meeting.md` + + + - `client_goal.json` + + + + + - `client_goal.json`은 절대 재읽지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"고객의사 초안 생성 완료"` 한 줄만 사용한다. + + + + - task_name: Task_Evidence_shard_planner + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import hashlib + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + def _mid(): return next(MSG_ID) + def _init(): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"task-evidence-shard-planner","version":"1.0","user_id":"{{__user_hash__}}","workspace_id":"{{__workspace_hash__}}"}}}, headers=MCP_HEADERS) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post(LOCALDOCS_URL, json={"jsonrpc":"2.0","method":"notifications/initialized"}, headers=MCP_HEADERS).raise_for_status() + def _parse(text): + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: return json.loads(line[6:]) + except Exception: pass + try: return json.loads(text) + except Exception: return None + def _call(name, args): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc":"2.0","id":_mid(),"method":"tools/call","params":{"name":name,"arguments":args}}, headers=MCP_HEADERS) + r.raise_for_status() + p = _parse(r.text) + if not p or "result" not in p: raise RuntimeError(f"MCP {name} failed") + return p + def _unwrap(p, name): + text = (p["result"].get("content") or [{}])[0].get("text", "") + if not text: raise RuntimeError(f"Empty: {name}") + try: outer = json.loads(text) + except Exception: return text + if isinstance(outer, dict) and "results" in outer: + r0 = (outer.get("results") or [{}])[0] + inner = r0.get("content") or r0.get("text") or "" + if not inner: raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return json.dumps(outer, ensure_ascii=False) if not isinstance(outer, str) else outer + def read_json(name): + raw = _unwrap(_call("read_docs", {"doc_names": [name]}), name) + if not isinstance(raw, str): return raw + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + obj, _ = decoder.raw_decode(raw.strip()) + return obj + def write_doc(path, content): + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def _items(obj: Any) -> list[Any]: + if isinstance(obj, list): + return obj + if isinstance(obj, dict): + for key in ("items", "documents", "evidences", "evidence", "docs"): + val = obj.get(key) + if isinstance(val, list): + return val + raise RuntimeError("evidence_all.json must be a JSON array or an object containing a document array") + + def _text(v: Any) -> str | None: + if v is None: + return None + s = str(v).strip() + return s or None + + def _doc_title(doc: Any, ordinal: int) -> str | None: + if not isinstance(doc, dict): + return f"evidence_all[{ordinal}]" + for key in ("title", "name", "filename", "file_name", "document_title", "doc_title"): + val = _text(doc.get(key)) + if val: + return val + return f"evidence_all[{ordinal}]" + + def _doc_uid(doc: Any, ordinal: int) -> str: + if isinstance(doc, dict): + for key in ("doc_uid", "uid", "id", "document_id", "source_id"): + val = _text(doc.get(key)) + if val: + return val + return f"DOC-{ordinal:03d}" + + def _stable_hash(doc: Any) -> str: + raw = json.dumps(doc, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _list_doc_names(args) -> list[str]: + payload = _unwrap(_call("list_docs", args), "list_docs") + if isinstance(payload, str): + try: + payload = json.loads(payload) + except Exception: + return re.findall(r"[\w./-]+\.json", payload) + vals: list[str] = [] + if isinstance(payload, dict): + for value in payload.values(): + if isinstance(value, list): + for item in value: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + elif isinstance(value, str): + vals.append(value) + elif isinstance(payload, list): + for item in payload: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + return [v for v in vals if v] + + def _read_existing_doc_names() -> list[str]: + preferred = [ + {"path": "evidence_indexed_parts", "pattern": "E-*.json"}, + {"path": "evidence_event_candidate_parts", "pattern": "E-*.json"}, + ] + preferred_errors = [] + preferred_names: list[str] = [] + for args in preferred: + try: + folder = str(args["path"]).rstrip("/") + preferred_names.extend(f"{folder}/{name.split('/')[-1]}" for name in _list_doc_names(args)) + except Exception as exc: + preferred_errors.append(f"{args['path']}: {exc}") + if not preferred_errors: + return sorted(set(preferred_names)) + + fallback_errors = [] + for args in ({}, {"doc_names": []}): + try: + return sorted(set(preferred_names + _list_doc_names(args))) + except Exception as exc: + fallback_errors.append(f"{args}: {exc}") + raise RuntimeError("list_docs failed for preferred schemas and fallback schemas; stale part detection cannot safely continue: " + " | ".join(preferred_errors + fallback_errors)) + + def _stale_parts(existing_names: list[str], n: int, folder: str) -> list[str]: + stale: list[str] = [] + pattern = re.compile(rf"(?:^|/){re.escape(folder)}/E-(\d{{3}})\.json$") + for name in existing_names: + m = pattern.search(name) + if not m: + continue + ordinal = int(m.group(1)) + if ordinal < 1 or ordinal > n: + stale.append(name) + return sorted(set(stale)) + + def main(): + _init() + evidence_all = read_json("evidence_all.json") + docs = _items(evidence_all) + n = len(docs) + if n <= 0: + raise RuntimeError("evidence_all.json contains no evidence documents") + + existing_names = _read_existing_doc_names() + stale_b1 = _stale_parts(existing_names, n, "evidence_indexed_parts") + stale_b2 = _stale_parts(existing_names, n, "evidence_event_candidate_parts") + + shards = [] + streaming_edges = [] + b1_waits = [] + b2_waits = [] + b1_files = [] + b2_files = [] + for idx, doc in enumerate(docs, 1): + label = f"{idx:03d}" + evidence_id = f"E-{label}" + b1_task_id = f"Task_B1_map_doc_{label}" + b2_task_id = f"Task_B2_map_events_{label}" + b1_output = f"evidence_indexed_parts/{evidence_id}.json" + b2_output = f"evidence_event_candidate_parts/{evidence_id}.json" + # 문서별 shard 파일 — B1/B2 인스턴스가 evidence_all.json 풀 대신 자기 shard만 읽도록. + shard_doc_path = f"evidence_shards/{evidence_id}.json" + write_doc(shard_doc_path, json.dumps(doc, ensure_ascii=False, indent=2)) + b1_waits.append(b1_task_id) + b2_waits.append(b2_task_id) + b1_files.append(b1_output) + b2_files.append(b2_output) + shards.append({ + "assigned_ordinal": idx, + "ordinal_label": label, + "evidence_index_candidate": evidence_id, + "b1_task_id": b1_task_id, + "b2_task_id": b2_task_id, + "b1_output_path": b1_output, + "b2_output_path": b2_output, + "shard_doc_path": shard_doc_path, + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": idx, + "array_index": idx - 1, + "doc_uid_candidate": _doc_uid(doc, idx), + "title_candidate": _doc_title(doc, idx), + "content_sha256": _stable_hash(doc), + "shard_doc_path": shard_doc_path + }, + "runtime_bindings": { + "__assigned_ordinal__": idx, + "__ordinal_label__": label, + "__evidence_index_candidate__": evidence_id, + "__b1_part_path__": b1_output, + "__b2_part_path__": b2_output, + "__shard_doc_path__": shard_doc_path + } + }) + streaming_edges.append({ + "from": b1_task_id, + "to": b2_task_id, + "condition": {"file_exists": b1_output}, + "bind": {"__assigned_ordinal__": idx} + }) + + plan = { + "schema_version": "evidence_shard_plan.v1", + "planner_task": "Task_Evidence_shard_planner", + "created_at_utc": datetime.now(timezone.utc).isoformat(), + "source_file": "evidence_all.json", + "source_selector": "$.items || $", + "shard_count": n, + "expected_ordinals": [f"{i:03d}" for i in range(1, n + 1)], + "expected_evidence_ids": [f"E-{i:03d}" for i in range(1, n + 1)], + "ordinal_policy": { + "base": 1, + "assignment": "array_order", + "format": "%03d", + "assigned_ordinal_var": "__assigned_ordinal__", + "evidence_index_candidate": "E-{ordinal:03d}", + "no_legal_sorting": True, + "no_content_based_reordering": True + }, + "shards": shards, + # 백엔드 wildcard fan-out: dynamic_fanout 리스트의 각 항목이 한 인스턴스 컨텍스트({{item.*}}로 접근). + # shards를 그대로 사용 — 디스크립터 메타(id/count/task_template 등)는 fanout_meta로 분리 보존. + "dynamic_fanout": shards, + "fanout_meta": [ + { + "id": "B1_evidence_doc_fanout", + "count": n, + "task_template": "Task_B1_map_doc", + "instance_id_template": "Task_B1_map_doc_{ordinal:03d}", + "output_path_template": "evidence_indexed_parts/E-{ordinal:03d}.json" + }, + { + "id": "B2_event_candidate_fanout", + "count": n, + "task_template": "Task_B2_map_events", + "instance_id_template": "Task_B2_map_events_{ordinal:03d}", + "output_path_template": "evidence_event_candidate_parts/E-{ordinal:03d}.json", + "wait_until_template": "Task_B1_map_doc_{ordinal:03d}" + } + ], + "streaming_edges": streaming_edges, + "barriers": { + "B1_all_parts_barrier": { + "wait_for_tasks": b1_waits, + "require_files": b1_files, + "next": "Task_B1_quality_gate_evidence_indexed" + }, + "B2_all_parts_barrier": { + "wait_for_tasks": b2_waits, + "require_files": b2_files, + "plus": ["Task_B1_quality_gate_evidence_indexed"], + "next": "Task_B2_quality_gate_event_candidates" + } + }, + "stale_part_policy": { + "mode": "block", + "allowed_ordinals": [f"{i:03d}" for i in range(1, n + 1)], + "checked_directories": ["evidence_indexed_parts", "evidence_event_candidate_parts"], + "detected_stale_parts": { + "evidence_indexed_parts": stale_b1, + "evidence_event_candidate_parts": stale_b2 + }, + "downstream_gate_instruction": "B1/B2 quality gates must BLOCK if detected_stale_parts is non-empty or if files outside expected_ordinals are present." + }, + "non_legal_assertions": { + "does_not_classify_document_type": True, + "does_not_extract_facts": True, + "does_not_evaluate_evidence": True, + "does_not_calculate_amounts": True, + "does_not_decide_legal_effect": True, + "does_not_finalize_evidence_index": True, + "does_not_finalize_candidate_id": True + } + } + + if stale_b1 or stale_b2: + plan["planner_status"] = "BLOCKED_BY_STALE_PARTS" + plan["mapper_execution_allowed"] = False + plan["blocked_reason"] = "stale mapper part detected; B1/B2 mapper fanout must not execute and existing parts must not be reused" + else: + plan["planner_status"] = "READY" + plan["mapper_execution_allowed"] = True + plan["blocked_reason"] = None + + write_doc("evidence_shard_plan.json", json.dumps(plan, ensure_ascii=False, indent=2)) + # 백엔드 wildcard fan-out 확장이 stdout JSON의 dynamic_fanout 키를 읽음. + # 전체 plan은 너무 커서 dynamic_fanout만 출력한다. 단 planner_status가 READY가 아니면 mapper fanout은 반드시 비운다. + fanout_payload = plan["dynamic_fanout"] if plan["planner_status"] == "READY" else [] + print(json.dumps({"dynamic_fanout": fanout_payload, "planner_status": plan["planner_status"], "mapper_execution_allowed": plan["mapper_execution_allowed"]}, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B1_map_doc_* + max_concurrency: 12 + preflight_files: + - "{{item.shard_doc_path}}" + - client_meeting.md + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + + Task_B1_map_doc + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. Your role in this task is a **per-document mapper** within the v.3 Step D streaming map-reduce pipeline. + + + 본 task는 v.3 Step D의 B1 mapper다. orchestrator 가 runtime parameter (`{{item.*}}`) 로 주입한 assigned_ordinal 단일 증거문서만 처리하여, 그 문서의 authority component·document component envelope·legal component envelope을 추출한 part 파일을 작성한다. + 본 task는 증거 문서 수 N을 계산하지 않고, ordinal을 배정하지 않고, B1/B2 mapper 인스턴스를 생성하지 않는다. 이러한 orchestration 책임은 전부 `Task_Evidence_shard_planner`와 orchestrator의 권한이다. + 최종 `evidence_indexed.json`은 본 task가 작성하지 않는다. 본 task는 `evidence_indexed_parts/E-{ordinal:03d}.json` part 파일만 작성하며, finalize는 `Task_B1_quality_gate_evidence_indexed`가 수행한다. + 본 task는 자기 문서 안에서 직접 확인되는 component만 작성한다. 다른 문서의 사실을 자기 part에 주입하면 quality gate의 authority drift check에서 BLOCK 또는 HARD_WARNING으로 검출된다. + `evidence_index`는 planner가 부여한 `E-{ordinal:03d}` 형식의 deterministic candidate를 그대로 propose 하되, final 부여는 quality gate가 수행한다. 본 task는 candidate id를 자기 part의 `evidence_index_proposed` 필드에 기록한다. + 본 task의 추출 깊이는 v.2 원본 Task_B1과 동일하다. document component envelope의 모든 하위 구조(claim_document_components, business_transfer_components, registry_row_components, secured_debt_components, valuation_rows, mixed_contract_payment_semantics, loan_components, valuation_matrix, registry_row_role_map, defense_assertions, admission_components, defense_components, notice_components, party_assertion_components, schedule_items, registry_object_display, succession_document_components)를 자기 문서 범위 안에서 가능한 한 분리 추출한다. + 변호사 관점에서 본 단계의 핵심은 등기부 row, 차용증 component, 통지서 발송/도달일 분리, 가족관계·상속포기 component, 별지목록 등의 직접 보존이다. 이는 청구원인 timeline·청구취지 별지목록·금액 계산의 기초가 된다. + + + + + + - runtime parameters: `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.source_document_ref}}`, `{{item.b1_output_path}}` (planner 가 부여한 단일 source of truth — 별도 파일 read 불요) + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 문서 1건 — planner가 evidence_all.json에서 분리해 둔 단일 shard 파일) + - `client_meeting.md` (보조 사용만 허용) + + + - `evidence_indexed_parts/E-{ordinal:03d}.json` (본 mapper 인스턴스의 part 파일 1개만) + + + - 다른 ordinal의 문서를 처리하기 + - 다른 ordinal의 사실을 자기 part에 주입하기 (authority drift) + - 증거 문서 수 N을 재계산하여 planner 결과를 대체하기 + - assigned_ordinal을 새로 생성하거나 재정렬하기 + - B1/B2 mapper fan-out 또는 streaming edge를 설계하기 + - 최종 `evidence_indexed.json` 작성 (이는 quality gate의 단독 권한) + - 다른 mapper의 part 파일 읽기/덮어쓰기 + - root 전체 탐색 + - 이전 task 결과 또는 backend 자동 로드 데이터 사용 + - 자기 part 재읽기 + - cross-document legal conclusion 확정 + - 입력에 없는 사실 추가 + + + + + 1. `list_docs`로 `{{item.shard_doc_path}}`, `client_meeting.md` 존재만 확인한다. + 2. `read_docs`로 위 두 파일을 읽는다. `{{item.shard_doc_path}}`는 이 인스턴스의 단일 evidence 문서로 그대로 사용한다 (별도의 ordinal 추출 불필요). assigned_ordinal·evidence_index_candidate·b1_output_path 는 runtime parameter (`{{item.*}}`) 그대로 사용한다. + 3. 메모리에서 part item 객체를 생성한다(items 배열이 아닌 단일 item object 또는 1-element items 형태). + 4. `write_file(overwrite=true)`로 runtime parameter 가 지정한 `evidence_indexed_parts/E-{ordinal:03d}.json`을 저장한다. + 5. `list_docs`로 자기 part 파일 존재만 확인한다. 절대 읽지 않는다. + 6. 정확히 `"E-{ordinal:03d} mapper part 작성 완료"`만 출력하고 종료한다. + + + + - 입력 누락, 빈 파일, runtime parameter 누락, ordinal 매핑 실패, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. part 파일은 쓰지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + - tool로 읽은 내용만 사실로 취급한다. + + + + - 본 mapper 인스턴스는 정확히 runtime parameter (`{{item.assigned_ordinal}}`) 에 해당하는 1개 문서만 처리한다. + - runtime parameters (`{{item.*}}`) 는 ordinal/binding 의 단일 source of truth 다 (별도 plan 파일 read 불요). + - `evidence_all.json` 배열 순서를 절대 바꾸지 않고, 배열 순서를 근거로 새 plan을 만들지도 않는다. + - planner의 `source_document_ref.content_sha256` 또는 `doc_uid_candidate`가 현재 문서와 명백히 충돌하면 part 파일을 쓰지 않는다. + - 본 task는 planner가 확정한 `evidence_index_candidate`, `b1_output_path`, assigned_ordinal을 소비할 뿐 재작성하지 않는다. + - 다른 ordinal의 문서 내용을 현재 문서에 보충하지 않는다(authority drift 금지). + - unknown 처리: + - nullable scalar → `null` + - 배열 → `[]` + - 비nullable string만 `"불명"` 사용 + - 배열은 exact-match 중복 제거 후 최초 순서를 유지한다. + - part 파일은 단일 item object 형태이며, 기존 필수 필드(`evidence_index_proposed`, `doc_uid`, `title`, `title_normalized`, `doc_type`, `key_*`, `source_pointer`)를 모두 포함한다. + - optional additive field는 downstream(quality gate)이 무시해도 안전해야 한다. + - `evidence_index_proposed`는 deterministic 규칙 `E-{ordinal:03d}`로만 생성한다. 임의 변형 금지. + + + + 이 인스턴스의 `{{item.shard_doc_path}}`(planner가 evidence_all.json에서 분리한 단일 evidence 1건) + `client_meeting.md` (보조만 허용) + + + `client_meeting.md`는 아래 목적에만 사용할 수 있다. + - `asset_id` + - `property_label_for_schedule` + - `transaction_type` + - `transaction_date` + - `market_value_at_act` + - `market_value_at_close` + - `consideration_breakdown` + - `property_cluster_id` + - 현재 문서의 축약 명칭이 어떤 자산군을 가리키는지에 관한 shorthand 해석 + + + + - 현재 문서와 `client_meeting.md`가 충돌하면 현재 문서가 우선한다. + - 예외: 등기부상 형식적 원인이 `매매`여도, 현재 문서 특약 또는 `client_meeting.md`가 실질적으로 채무갈음/대물변제를 직접 드러내면 `transaction_type`은 `"대물변제"`로 할 수 있다. + - 단, 이 예외는 현재 문서 또는 meeting이 실질 원인을 직접적으로 드러내는 경우에만 적용한다. + + + + + - `ordinal` = `evidence_all.json`의 1-based 배열 위치 (assigned_ordinal과 동일) + - `evidence_index_proposed` = `E-{ordinal:03d}` (mapper propose; quality gate가 finalize) + - `title` = 현재 문서 제목 원문 그대로 + - `title_normalized` = 앞뒤 공백 제거 + 내부 연속 공백/줄바꿈을 1칸으로 축약 + - `doc_uid` = `DOC-{ordinal:03d}-{title_normalized의 공백을 "_"로 치환한 값}` + - `source_pointer` = `{"source":"evidence_all.json","ordinal":ordinal}` + + + + 다음 순서로만 핵심을 읽는다. + 1. `title` + 2. `heading` + 3. `key_value` / `form` + 4. 관련 `table` 행 + 5. `list` + 6. `paragraph` + + 보일러플레이트(바코드 안내, 발급확인 문구, 반복 footer/page text)는 무시한다. + 단, `관할등기소`, `접수번호`, `등기원인일`, `발행일` 등 필요한 사실이 있으면 사용한다. + + + + `doc_type` enum은 v.2 원본과 동일하다. nuance는 `doc_semantic_flags`로 보강한다. + 1. `판결|결정|배당표|가압류|가처분|경매|타경` → `"판결/결정"` + 2. `등기사항전부증명서|주민등록|가족관계증명서|사업자등록증` → `"공문서"` + 3. `대출|명세|거래|계산서` → `"거래기록"` + 4. `카톡|문자|이메일|통화` → `"통신기록"` + 5. `계약서|약정서|확인서|영수증|차용증|각서` → `"처분문서"` + 6. 그 외 → `"기타"` + + + + - `key_facts`: 최대 3개, 각 60자 이하 + - `key_dates`: 최대 3개 + - `key_amounts`: 최대 3개 + - `key_parties`: 최대 5개 + - 현재 문서에서 직접 읽히는 핵심만 남긴다. + - 장문 요약, 계산식, 추정 보정 금지. + - 가능하면 heading / key_value / 표의 핵심 행만 사용한다. + + + + + - `property_cluster_id`는 optional field다. + - 현재 문서만으로 특정 자산군이 명백하거나, 현재 문서의 표기와 `client_meeting.md`가 정확히 대응할 때만 채운다. + - 예: `"해당 대지/건물"`, `"해당 상가"`, `"해당 주거용 부동산"`, `"해당 임야"`, `"해당 영업양도"`, `"해당 물품대금"`. + - 명백하지 않으면 `null`. + + + + `doc_semantic_flags`는 optional field다. 아래 값만 사용한다. + - `multi_event_doc` + - `registry_doc` + - `mixed_contract_doc` + - `valuation_doc` + - `claim_doc` + - `status_doc` + - `notice_doc` + - `response_doc` + - `business_transfer_doc` + - `supporting_reference_doc` + - `admission_doc` + - `defense_doc` + - `notice_receipt_admission_doc` + - `lien_assertion_doc` + - `schedule_doc` + - `relief_object_reference_doc` + - `succession_doc` + + 부여 규칙: + - 등기부처럼 복수 row가 서로 다른 법적 효과를 가지면 `multi_event_doc` + - 갑구/을구 등기행위가 있으면 `registry_doc` + - 임대차와 매매 등 서로 다른 계약 구성요소가 같은 문서에 공존하면 `mixed_contract_doc` + - 시가, 차임시세, 사용이익 가치가 표로 제시되면 `valuation_doc` + - 차용증, 보증서, 대여약정 등 채권 발생 문서면 `claim_doc` + - 답변서, 사업자등록증, 현재 상태 확인서처럼 상태를 보여주면 `status_doc` + - 내용증명, 통지서, 최고서, 해제통지서면 `notice_doc` + - 상대방 답신, 답변서, 반박문이면 `response_doc` + - 영업양도 문서면 `business_transfer_doc` + - 별지 목록처럼 단독 계산객체는 없으나 다른 문서 식별을 돕는 문서면 `supporting_reference_doc` + - 상대방이 통지 수령, 임대, 차임, 점유, 채무, 변제 등을 인정하면 `admission_doc` + - 명의신탁, 충분담보, 상계, 선의, 후소유자 소멸청구권 부정, 피담보채권 충당 등 항변이 있으면 `defense_doc` + - 유치권 소멸청구 통지의 수령일을 인정하면 `notice_receipt_admission_doc` + - 유치권 주장, 공사잔금, 유치권 목적물 사용·임대가 있으면 `lien_assertion_doc` + - 별지목록, 목적물 목록, 청구취지에서 그대로 사용할 문구가 있으면 `schedule_doc` 및 `relief_object_reference_doc` + - 가족관계증명서, 상속포기 심판문, 사망·상속 관련 공문서이면 `succession_doc` + + + + - 등기부 문서에서 갑구/을구 여러 row가 서로 다른 법적 효과를 가지면 `multi_event_registry=true`를 optional field로 기록한다. + - `multi_event_registry=true`인 문서는 단일 snapshot 요약을 강요하지 않는다. + + + + + - 문서가 여러 법률효과를 담고 있으면 component 단위로 분해한다. + - 날짜는 작성일, 발송일, 도달일, 법률효과 발생일, 변제기, 기산일 후보로 의미별 분리한다. + - 금액은 원금, 이자, 보증금, 차임, 사용이익, 채권최고액, 실제 변제액, 가액배상 공제액으로 성격별 분리한다. + - 아래 component field들은 optional additive field이며, 현재 문서에 직접 읽히는 범위에서만 사용한다. + + + - 문서가 이행각서, 채무승인서, 최고서, 내용증명, 지급요구서, 변제약정서, 차용증, 납품대금 확인서이면 `claim_document_components`를 생성한다. + - 단순 `key_dates`에 문서작성일만 넣고 끝내지 말고, 문서 안에 지급기일, 새 변제기, 최고 도달일, 채무승인일, 청구금액, 채권자, 채무자가 직접 읽히면 component field로 분해한다. + - 이행각서에서 "언제까지 지급한다"는 문구가 있으면 반드시 `new_due_date` 후보로 분리한다. + - 최고서에서 발송일과 수령일/도달일이 모두 있으면 `dispatch_date`와 `arrival_date`를 분리한다. + - claim component의 `component_kind` 허용값은 `underlying_claim|debt_acknowledgement|demand_notice|notice_dispatch|notice_arrival|new_due_date|payment_promise|limitation_relevance|other`다. + + + + - 영업양도계약서, 상호 계속 사용, 사업 양수인, 영업자산 이전, 고객·직원 승계, 채무불승계 조항, 책임배제 등기 단서가 있으면 `business_transfer_components`를 생성한다. + - `business_name`, `transferor_candidates`, `transferee_candidates`, `business_assets_transferred`, `trade_name_continuation_clause`, `customer_or_employee_succession_clause`, `pre_transfer_debt_exclusion_clause`, `liability_exclusion_registration_clause`를 직접 읽히는 범위에서 채운다. + - 기존 `contract_components`가 있어도 영업양도 책임 검토용 구조는 `business_transfer_components`에 별도 보존한다. + + + + - 등기부, 등기사항전부증명서, 등기내역 문서는 반드시 row 단위로 쪼갠다. + - 각 row마다 갑구/을구, 순위번호, 등기목적, 접수일, 접수번호, 원인, 권리자, 채무자, 채권최고액, 객체 범위를 추출하여 `registry_row_components`에 둔다. + - 청구취지 특정에 필요한 접수번호는 `key_facts` 요약에만 두지 말고 `registry_row_components[*].receipt_no`로 보존한다. + - 차용증, 근저당권 설정계약, 등기부 row가 연결되면 `secured_debt_components`를 생성한다. + - 복수 채무의 원금, 이율, 변제기, 담보 지분이 다르면 법정변제충당 계산 대상이므로 모두 구조화한다. + - 사해행위취소 사건의 등기부 row는 필요하면 `registry_row_role_map`에 actio role을 추가로 남긴다. + - `registry_row_components`의 각 row에 `registry_ref`를 부여한다. 형식: `REG-{ordinal:03d}-{row_seq:02d}` (갑구/을구·순위번호 순 deterministic). + - row에 지분 표시(예: "2분의 1", "1/2 지분")가 직접 읽히면 `registry_row_components[*].fraction_display`(원문 문자열)와 `registry_row_components[*].fraction_ref`를 채운다. + - `fraction_ref`는 `FR-{ordinal:03d}-{row_seq:02d}` 형식의 deterministic propose 값이다. 같은 row에서 파생되는 모든 component는 같은 `fraction_ref`를 공유한다. + - 지분 표시의 분자·분모가 직접 읽히는 경우에만 optional 필드 `fraction_numerator`/`fraction_denominator`를 채운다(예: "2분의 1" → 1/2). 환산·약분·보간 금지. 직접 읽히지 않으면 null. + + + + - 감정표는 금액만 추출하지 말고 `valuation_rows`에 row별 전제조건, 금액, 단위, 법률상 사용 가능성을 함께 추출한다. + - 사해행위취소 valuation 문서가 복수 자산 또는 복수 시점을 담으면 `valuation_matrix`를 생성하고, 각 row의 `legal_uses`를 표시한다. + - 임대차보증금, 월 차임, 매매대금이 한 문서에 같이 있으면 `mixed_contract_payment_semantics`에서 각각 별도 payment semantic으로 분리한다. + - 보증금은 원칙적으로 반환 예정 담보성 금원인지 검토하고, 원고가 직접 수령하지 않았다면 원고의 부당이득 원금으로 자동 전환하지 않는다. + - 월 차임은 피고들 사이의 계약상 차임과 객관적 토지 사용이익을 구분한다. + + + + - 사해행위취소 사건의 증거문서는 일반 key_facts 요약만으로 충분하지 않다. + - 차용증에 복수 대여금이 있으면 `loan_components`로 분리하고, 각 원금·변제기·이자율·담보 목적물·청구 내 역할을 추출한다. + - 감정평가서는 시점별 `valuation_matrix`로 보존하고, 각 row의 legal use를 표시한다. + - 등기부는 갑구/을구 row별로 쪼개고, 각 row의 `actio_roles`를 부여한다. + - 내용증명 답신에 명의신탁, 충분담보, 상계, 선의 주장이 있으면 `defense_assertions`로 추출한다. + - 사후 말소된 담보권과 그 말소를 유발한 변제액은 가액배상 계산의 필수 입력으로 표시한다. + + + + - `loan_components`의 각 항목은 아래 named 필드를 가진다. 직접 읽히지 않는 필드는 null. + { + "loan_component_id": "E-{ordinal:03d}-LC-{seq:02d}", + "principal_amount": null, + "loan_date": null, + "due_date": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "rate_text": null, + "rate_source_locator": null, + "interest_accrual_start_hint": null, + "collateral_object_hint": null, + "role_in_claim_hint": null + } + - `agreed_interest_rate`는 원문 그대로의 문자열("연 5%", "월 1%" 등)만 허용한다. + - `agreed_delay_damage_rate`는 약정 지연손해금율이다. 없으면 null로 둔다(법정이율로 보충 금지). + - `rate_text`는 이율 관련 원문 문자열 그대로, `rate_source_locator`는 그 이율이 읽힌 조항 locator(예: "제2조", "특약")다. + - `agreed_interest_rate`/`agreed_delay_damage_rate`는 계산·환산·보간 금지. 원문 문자열만 허용. + - `secured_debt_components`의 각 항목에도 동일한 optional rate 필드(`agreed_interest_rate`, `agreed_delay_damage_rate`, `rate_text`, `rate_source_locator`, `due_date`)를 직접 읽히는 범위에서 둘 수 있다(근저당권 설정계약서·차용증 겸용 문서의 이율 보존용). + - 이율이 문서에 있는데 두 rate 필드가 모두 null이면 self-warning으로 본다. + + + + - 답변서에 통지 수령 인정, 제3자 임대 자인, 월 차임 자인, 유치권자 사용 허용 주장, 후소유자 소멸청구권 부정 주장, 피담보채권 충당 주장이 있으면 `admission_components`와 `defense_components`로 분리한다. + - 통지서에는 `notice_components`를 생성하고, 통지 유형, 발송일, 도달 필요 여부, 수령자, 목적물, 법률효과 후보를 분리한다. + - `notice_components`의 각 항목은 아래 named 필드를 가진다. 직접 읽히지 않는 필드는 null. + { + "notice_id_proposed": "E-{ordinal:03d}-NT-{seq:02d}", + "notice_type": "최고|해제통지|유치권소멸청구통지|기타", + "drafted_date": null, + "dispatch_date": null, + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [], + "receiver_candidates": [], + "target_object_hint": null, + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "legal_effect_candidate_hint": null + } + - 5개 lifecycle 날짜의 의미: `drafted_date`=작성일, `dispatch_date`=발송일, `arrival_date`=도달일(민법 제111조 도달주의 — 도달≠수령을 구분), `received_date`=수령일(수령 인정 포함; 도달일과 다르면 별도 보존), `legal_effect_date_candidate`=법률효과 발생 후보일(계산 금지, 문서 직접 기재만). + - `requires_receipt_for_effect`는 true|false|null — 문서 유형상 도달/수령이 효력요건인지의 후보 표시다. + - `receipt_evidence_refs`는 수령을 뒷받침하는 같은 문서 내 단서 locator다. + - `recipient_admission_text`는 통지 수령을 인정하는 원문 문자열이며, 답변서 문서의 component에서만 채운다. + - 수령일이 상대방 답변서에서 인정되는 경우 그 답변서의 `admission_components`에 남기고, 본 통지서 문서의 `received_date`에 타 문서 사실을 주입하지 않는다(authority drift 금지). cross-document 연결은 Part 2 B5가 수행한다. + - 당사자가 요구한 기간이나 금액이 법률상 기산일로 자동 사용되면 안 되는 경우 `party_assertion_components.should_not_auto_use_as_legal_start_date=true`를 남긴다. + - 별지목록에는 `schedule_items`를 생성하고, 청구취지에서 그대로 사용할 수 있는 문구와 asset alias, 전유부분, 대지권, 도로명주소를 보존한다. + - `schedule_items`의 각 항목에 `schedule_ref`를 부여한다. 형식: `SCH-{ordinal:03d}-{seq:02d}` (별지 기재 순서 deterministic). asset alias 문자열은 기존대로 함께 보존한다. + - `schedule_items`의 각 항목에 optional 필드 `canonical_label_candidate`(별지 표제 문구 원문)와 `usable_in_relief`("yes|no|unknown" — 청구취지에서 그대로 사용할 수 있는 문구인지)를 직접 읽히는 범위에서 둘 수 있다. + - 등기부가 목적물 특정 자료로 쓰이면 `registry_object_display`를 생성한다. + - 가족관계증명서, 상속포기 심판문, 사망·상속 자료에는 `succession_document_components`를 생성하고 피상속인, 사망일, 배우자 후보, 직계비속 후보, 직계존속 후보, 상속포기자, 신고일·수리일, 연결 asset을 보존한다. + + + + + + - 기존 optional field `legal_calculation_object`는 backward-compatible summary field로 유지한다. + - 그러나 이 필드는 안전할 때만 사용한다. + - 문서가 `multi_event_doc` 또는 `multi_event_registry=true`이면 원칙적으로 `legal_calculation_object`를 생성하지 않는다. + - 문서에 서로 다른 법적 효과의 slot이 2개 이상 공존하면 단일 summary `legal_calculation_object`로 압축하지 않는다. + - 단일 효과가 명백한 문서, 또는 서로 다른 값이 경쟁하지 않는 문서에서만 `legal_calculation_object`를 생성한다. + + + + - multi-event 또는 mixed-contract 문서를 위해 optional field `legal_calculation_objects`를 추가할 수 있다. + - `legal_calculation_objects`는 현재 문서 안의 개별 slot을 권위 있게 분해한 배열이다. + - 문서당 0개 이상 허용한다. + - 단일 slot 문서라면 `legal_calculation_objects`를 생략할 수 있다. + - 각 slot은 현재 문서의 직접 기재만 사용한다. 계산, 합산, 차감 금지. + + + + 개별 slot은 아래 경우에 생성한다. + - 소유권이전 / 매매 / 증여 / 대물변제 / 경매매각 / 경매개시결정 / 근저당권설정 / 근저당권말소 / 가압류 / 임대차 / 영업양도 / 대여 / 담보제공 + - valuation 문서는 시가표/차임표/사용이익표를 slot이 아니라 `valuation_object`로 처리한다. + - 등기부는 갑구/을구의 의미 있는 row마다 1 slot씩 생성한다. + - 혼합계약서는 계약 조항상 독립된 법적 효과마다 1 slot씩 생성한다. + + 각 slot은 아래 구조를 따른다. + { + "slot_id": "E-{ordinal:03d}-LCO-01", + "slot_kind": "ownership_transfer|gift|sale|dation|mortgage_setting|mortgage_cancellation|auction_commencement|auction_sale|lease|building_sale|loan|business_transfer|other", + "source_locator": { + "locator_type": "heading|key_value|table_row|paragraph|unknown", + "locator_hint": "갑구4|을구1|제1조|특약 등", + "excerpt": "짧은 단서" + }, + "asset_id": null, + "property_label_for_schedule": null, + "transaction_type": "매매|증여|대물변제|null", + "transaction_date": null, + "registration_date": null, + "registry_office": null, + "registry_receipt_no": null, + "registry_recorded_transfer_date": null, + "market_value_at_act": null, + "market_value_at_close": null, + "sale_price": null, + "monthly_rent": null, + "consideration_breakdown": [], + "parties": [] + } + - slot이 등기부 row 또는 별지 항목에서 직접 파생되면 optional 필드 `registry_ref_hint`/`schedule_ref_hint`를 남긴다. + + + + - 임대차와 매매가 함께 있는 혼합계약서, 영업양도와 채무불인수 조항이 함께 있는 문서 등은 optional field `contract_components`를 추가할 수 있다. + - `contract_components`는 현재 문서의 조항별 법적 효과 분해 메모다. + - 각 component는 아래 구조를 따른다. + { + "component_id": "E-{ordinal:03d}-CC-01", + "component_kind": "lease|sale|delivery|management_use_shift|permit_cooperation|business_transfer|debt_non_assumption|other", + "object_spec": "짧은 목적물", + "amount": null, + "date": null, + "payer_candidates": [], + "recipient_candidates": [], + "possession_recipient_candidates": [], + "benefit_shift_to_candidates": [], + "source_locator": { + "locator_type": "paragraph|table_row|key_value|unknown", + "locator_hint": "제1조|제2조|특약 등", + "excerpt": "짧은 단서" + } + } + - 혼합계약서에서 component가 2개 이상 명백하면 2개 이상 생성한다. + - mixed contract에 임대차, 매매, 인도, 관리·사용 귀속이 직접 적혀 있으면 이를 lease/sale 1개로 뭉개지지 말고 가능한 한 분리한다. + - 조항에서 누가 대가를 지급하고 누가 수령했는지가 직접 읽히면 `payer_candidates`, `recipient_candidates`를 채운다. + - 인도받는 자가 직접 읽히면 `possession_recipient_candidates`를 채운다. + - 관리·사용 또는 사용수익 귀속 주체가 직접 읽히면 `benefit_shift_to_candidates`를 채운다. + - 위 directionality field는 현재 문서에 직접 적힌 경우에만 채우고, 추정으로 보충하지 않는다. + + + + - 시가, 차임시세, 사용이익 평가 문서는 optional field `valuation_object`를 추가할 수 있다. + - `valuation_object`는 아래 구조를 따른다. + { + "valuation_kind": "market_value|rent_value|use_gain_value", + "target_object": "짧은 목적물", + "basis_date_or_period": "원문 기간 또는 기준일", + "value_rows": [ + { + "label": "2022.1.1.~2022.12.31.|보증금 3억 원인 경우 등", + "amount": "금액 문자열", + "unit": "총액|월액|기타" + } + ] + } + - valuation 문서는 기존 `doc_type`를 바꾸지 않고 `doc_semantic_flags`와 `valuation_object`로 구체화한다. + - 차임표/사용이익표가 있으면 `market_value_*`를 억지로 채우지 않는다. + + + + + - 날짜: 정확한 일자가 있으면 `YYYY-MM-DD`, 없으면 `null` + - 단, `key_dates`와 `valuation_object.basis_date_or_period`는 짧은 원문 텍스트 허용 + - 금액: 입력에 명시된 값만 `"300,000,000원"` 형식 문자열로 정규화 + - 이름/당사자: 원문 우선, 명백히 동일할 때만 canonicalize + - `consideration_breakdown`는 직접 읽히는 짧은 대가 항목만 저장, 최대 4개, 각 40자 이하 + + + + part 파일은 단일 item object 형태이며, top-level에 mapper가 propose하는 모든 component field를 둔다. + + { + "evidence_index_proposed": "E-{ordinal:03d}", + "doc_uid": "DOC-{ordinal:03d}-...", + "title": "...", + "title_normalized": "...", + "doc_type": "판결/결정|공문서|거래기록|통신기록|처분문서|기타", + "key_facts": [], + "key_dates": [], + "key_amounts": [], + "key_parties": [], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": ordinal + }, + "property_cluster_id": null, + "doc_semantic_flags": [], + "multi_event_registry": null, + "contract_components": [], + "valuation_object": null, + "legal_calculation_object": null, + "legal_calculation_objects": [], + "claim_document_components": [], + "business_transfer_components": [], + "registry_row_components": [], + "secured_debt_components": [], + "valuation_rows": [], + "mixed_contract_payment_semantics": {}, + "loan_components": [], + "valuation_matrix": {}, + "registry_row_role_map": {}, + "defense_assertions": [], + "admission_components": [], + "defense_components": [], + "notice_components": [], + "party_assertion_components": [], + "schedule_items": [], + "registry_object_display": {}, + "succession_document_components": null, + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } + } + + 추가 제약: + - top-level은 단일 object다. envelope이 아닌 단일 item. + - `evidence_index_proposed`는 mapper의 propose 식별자이며, finalize는 quality gate가 한다. + - `mapper_meta.self_isolation_attested=true`는 본 mapper가 자기 문서 외 사실을 주입하지 않았음을 self-attest 한다는 의미다. + - optional component field는 직접 읽히는 범위에서만 채우고, 빈 배열/객체/`null`로 두는 것을 우선한다. + + + + - part 파일이 단일 object이고 `evidence_index_proposed`, `doc_uid`, `title`, `title_normalized`, `doc_type`, `source_pointer`를 모두 포함하는지 점검한다. + - `planner_status=="READY"` 및 `mapper_execution_allowed=true`가 확인되지 않았으면 part 파일이 작성되지 않았는지 점검한다. + - `evidence_index_proposed`가 정확히 `E-{ordinal:03d}` 형식인지 점검한다. + - 자기 문서 외 ordinal의 사실이 part에 주입되지 않았는지 self-check 한다(authority drift 방지). + - `registry_doc`이면서 서로 다른 법적 효과 row가 2개 이상 읽히면 `doc_semantic_flags`에 `multi_event_doc`를 넣고, 가능한 경우 `legal_calculation_objects`를 생성한다. + - `multi_event_registry=true` 또는 `doc_semantic_flags`에 `multi_event_doc`가 있으면 단일 `legal_calculation_object`를 억지로 채우지 않는다. + - `mixed_contract_doc`인데 명백한 독립 component가 2개 이상이면 `contract_components`를 2개 이상 생성한다. + - `mixed_contract_doc`에 인도 또는 관리·사용 귀속이 직접 적혀 있으면 `delivery` 또는 `management_use_shift` component가 누락되지 않았는지 점검한다. + - 혼합계약 조항에서 지급자·수령자 또는 점유 이전 상대방이 직접 읽히면 `payer_candidates`, `recipient_candidates`, `possession_recipient_candidates`, `benefit_shift_to_candidates`가 비어 있지 않은지 점검한다. + - `valuation_doc`인데 가치표/차임표가 직접 보이면 `valuation_object`를 생성한다. + - 같은 문서에서 복수 행위가 보이는데도 단일 snapshot `legal_calculation_object`만으로 압축하지 않는다. + - `claim_doc`인데 이행각서ㆍ채무승인ㆍ최고ㆍ변제기ㆍ청구금액 단서가 직접 보이고 `claim_document_components`가 없으면 self-warning으로 본다. + - 이행각서에 새 변제기가 있는데 `new_due_date`가 없으면 self-warning으로 본다. + - 최고서 또는 통지서에 도달·수령 단서가 있는데 `arrival_date` 또는 notice receipt component가 없으면 self-warning으로 본다. + - `business_transfer_doc`에 상호ㆍ영업명ㆍ영업자산ㆍ채무불승계 단서가 있는데 `business_transfer_components`가 없으면 self-warning으로 본다. + - 등기부에 접수번호가 있는데 `registry_row_components[*].receipt_no`가 없으면 self-error로 본다. + - 차용금 문서에 이율 또는 변제기가 있는데 `secured_debt_components` 또는 `loan_components`가 없으면 self-warning으로 본다. + - `loan_components`가 있는데 이율 문구가 문서에 보이면서 `agreed_interest_rate`가 null이면 self-warning으로 본다. + - valuation 문서에 복수 시점 또는 복수 row가 있는데 `valuation_rows[*].legal_use_case` 또는 `valuation_matrix.rows[*].legal_uses`가 없으면 self-warning으로 본다. + - 임대차보증금이 추출되었는데 `mixed_contract_payment_semantics`에서 반환성 보증금과 사용이익 원금 배제 여부가 구분되지 않으면 self-warning으로 본다. + - 등기부 문서에 사후 말소 row가 있는데 `registry_row_role_map` 또는 `registry_row_components`에 release role이 없으면 self-error로 본다. + - 답변서에 수령 인정ㆍ제3자 임대ㆍ항변이 있는데 `admission_components` 또는 `defense_components`가 없으면 self-warning으로 본다. + - 별지목록에 목적물 항목이 있는데 `schedule_items`가 없으면 self-error로 본다. + - 상속자료가 있는데 `succession_document_components`가 없으면 self-warning으로 본다. + - self-warning/self-error는 mapper가 part 파일에 그대로 남겨도 되며, quality gate가 최종 severity를 판정한다. + + + + + {{item.assigned_ordinal}} + + + [ ] 1) Preflight: `list_docs` 사용해서 `{{item.shard_doc_path}}`, `client_meeting.md` 만 확인, `read_docs` 사용해서 두 파일만 읽기 + [ ] 2) runtime parameter `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.b1_output_path}}` 그대로 소비. 별도 검증 read 불요. + [ ] 3) `{{item.shard_doc_path}}`의 단일 evidence 문서를 메모리에서 사용하고, 주입된 shard 밖으로 나가지 않기 + [ ] 4) `write_file` 사용해서 `evidence_indexed_parts/E-{ordinal:03d}.json` 생성 (ordinal은 `{{item.assigned_ordinal}}`) + [ ] 5) `list_docs` 사용해서 자기 part 파일만 존재 확인(절대 읽기 금지) + [ ] 6) `"E-{ordinal:03d} mapper part 작성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 1건; planner가 evidence_all.json에서 분리) + - `client_meeting.md` + + + - `evidence_indexed_parts/E-{ordinal:03d}.json` + + + + + - `evidence_index_proposed`는 정확히 `E-{ordinal:03d}` 형식만 허용한다. + - `ordinal`과 output path는 runtime parameter (`{{item.*}}`) 를 그대로 따른다. + - 증거 문서 수 N, assigned_ordinal, mapper fan-out, streaming edge는 본 task에서 만들지 않는다. + - 자기 part 파일은 절대 재읽지 않는다. + - 다른 ordinal 문서를 처리하지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"E-{ordinal:03d} mapper part 작성 완료"` 한 줄만 사용한다. + + + + - task_name: Task_B2_map_events_* + max_concurrency: 12 + preflight_files: + - "{{item.shard_doc_path}}" + - "{{item.b1_output_path}}" + - client_meeting.md + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + + Task_B2_map_events + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. Your role in this task is a **per-document event/state candidate mapper** within the v.3 Step D streaming map-reduce pipeline. + + + 본 task는 v.3 Step D의 B2 mapper다. orchestrator 가 runtime parameter (`{{item.*}}`) 로 주입한 assigned_ordinal 단일 문서의 event/state candidate(raw event + legal-effect event 포함)를 추출하여 part 파일을 작성한다. + 본 mapper 인스턴스는 orchestrator가 같은 ordinal의 B1 mapper(Task_B1_map_doc) 완료와 `evidence_indexed_parts/E-{ordinal:03d}.json` 생성을 확인한 뒤 시작한다. 본 task는 streaming edge를 설계하지 않고, planner/orchestrator가 이미 결정한 same-ordinal edge를 소비한다. + 최종 `evidence_event_candidates.json`은 본 task가 작성하지 않는다. 본 task는 `evidence_event_candidate_parts/E-{ordinal:03d}.json` part 파일만 작성하며, finalize는 `Task_B2_quality_gate_event_candidates`가 수행한다. + 본 task는 같은 ordinal의 B1 part 파일과 planner shard row를 authority metadata로 참조하고, planner가 분리해 둔 `{{item.shard_doc_path}}` 의 raw 문서 1건을 메모리에서 사용한다. + 같은 문서 내 복수 legal-effect event는 분리한다. 문서 밖 사실을 candidate payload에 넣지 않는다(authority drift 금지). + `candidate_id`는 `EVT-{ordinal:03d}-{seq:02d}` 형식의 deterministic candidate로 propose 하되, final 부여 및 collision 처리는 quality gate가 수행한다. + 변호사 관점에서 본 단계의 핵심은: (a) 등기부 갑구/을구 row별 분리, (b) 혼합계약서의 계약성립/대가수수/인도/관리·사용 귀속 4종 분리, (c) 통지 발송일과 도달일의 분리 candidate, (d) 답변서의 admission/defense 분리 candidate, (e) valuation 문서의 시점별/지표별 candidate, (f) 가족관계·상속포기 문서의 succession candidate 분리다. + legal-effect event는 raw event와 함께 추출하며, payment, notice, auction, lien, succession, valuation, defense assertion, commercial successor liability에 대해 v.2 원본의 REQUIRED_EVENT_SUBKINDS를 그대로 따른다. + + + + + + - runtime parameters: `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.b1_output_path}}`, `{{item.b2_output_path}}` (planner 가 부여한 단일 source of truth — 별도 plan 파일 read 불요) + - `{{item.b1_output_path}}` (같은 ordinal 의 B1 mapper part — runtime parameter 그대로 사용) + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 문서 1건; planner가 evidence_all.json에서 분리해 둔 단일 shard 파일) + - `client_meeting.md` (보조 사용만 허용) + + + - `evidence_event_candidate_parts/E-{ordinal:03d}.json` (본 mapper 인스턴스의 part 파일 1개만) + + + - 다른 ordinal의 B1 part 또는 raw 문서 처리 + - 다른 ordinal의 사실을 자기 part에 주입하기 (authority drift) + - 증거 문서 수 N을 재계산하여 planner 결과를 대체하기 + - assigned_ordinal을 새로 생성하거나 재정렬하기 + - B1/B2 mapper fan-out 또는 streaming edge를 설계하기 + - 최종 `evidence_event_candidates.json` 작성 (이는 quality gate의 단독 권한) + - 다른 mapper의 part 파일 읽기/덮어쓰기 + - 자기 part 재읽기 + - 문서 summary를 다시 쓰는 작업으로 변질시키기 + - meeting만으로 문서에 없는 사건행위를 새로 만들기 + - 입력에 없는 날짜, 당사자, 금액, 목적물을 창작하기 + + + + + 1. `list_docs`로 `{{item.b1_output_path}}`, `{{item.shard_doc_path}}`, `client_meeting.md` 존재만 확인한다. + 2. `read_docs`로 위 세 파일을 읽는다. `{{item.shard_doc_path}}`는 이 인스턴스의 단일 evidence 문서로 그대로 사용한다 (별도의 ordinal 추출 불필요). assigned_ordinal·evidence_index_candidate·b2_output_path 는 runtime parameter (`{{item.*}}`) 그대로 사용한다. + 3. 메모리에서 part 객체를 생성한다. 단일 item object 또는 1-element items 형태이며, event 후보가 0건이어도 파일을 생략하지 말고 `zero_event_reason`을 반드시 가진다. + 4. `write_file` 전에 아래 을 수행한다. 불량이면 먼저 내부 보정을 시도한다. + 5. 보정 후에도 schema invariant가 불량이면 part 파일을 쓰지 않고 `FAILED: E-{ordinal:03d} event candidate part schema invariant violation`만 보고하고 종료한다. + 6. 통과한 경우에만 `write_file(overwrite=true)`로 runtime parameter 가 지정한 `evidence_event_candidate_parts/E-{ordinal:03d}.json`을 저장한다. + 7. `list_docs`로 자기 part 파일 존재만 확인한다. 절대 읽지 않는다. + 8. 정확히 `"E-{ordinal:03d} event candidates part 작성 완료"`만 출력하고 종료한다. + + + + - 입력 누락, 빈 파일, runtime parameter 누락, B1 part 부재, ordinal 매핑 실패, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. part 파일은 쓰지 않는다. + - event 후보가 0건인 것은 실패가 아니다. 이 경우에도 runtime parameter 가 지정한 output path에 `zero_event_reason`을 포함한 schema-valid part 파일을 쓴다. + - `event_candidates: []`와 `zero_event_reason` 누락 또는 null이 함께 존재하는 것은 정상 zero-event가 아니라 schema invariant 위반이다. + - part schema invariant 위반이 내부 보정 후에도 남으면 part 파일은 쓰지 않는다. 불완전한 part를 생성하여 quality gate에 넘기지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + + - 본 mapper 인스턴스는 정확히 runtime parameter (`{{item.assigned_ordinal}}`) 에 해당하는 1개 문서만 처리한다. + - runtime parameters (`{{item.*}}`) 는 ordinal/binding 의 단일 source of truth 다 (별도 plan 파일 read 불요). + - B1 part의 `evidence_index_proposed`, `title`, `doc_type`, `source_pointer.ordinal`을 authority로 사용한다. + - `{{item.shard_doc_path}}`의 단일 evidence 문서(planner가 evidence_all.json에서 분리)를 직접 참조하여 candidate를 추출한다. + - 본 task는 planner가 확정한 `evidence_index_candidate`, `b2_output_path`, assigned_ordinal을 소비할 뿐 재작성하지 않는다. + - 다른 ordinal의 사실을 자기 part에 주입하지 않는다(authority drift 금지). + - candidate layer에 머문다. 최종 BO, 최종 JuristicAct, 최종 청구권, 최종 사해행위 판단을 확정하지 않는다. + - 문서 단위 1요약으로 뭉개지 않는다. 한 문서에 복수의 법적 중요 행위가 있으면 반드시 여러 candidate로 분리한다. + - support가 희박한 후보를 억지로 채우지 않는다. 불명확한 필드는 `null` 또는 빈 배열을 사용할 수 있다. + - 잘못된 과잉 특정화보다 보수적 분해가 낫다. 다만 자기 문서 커버리지를 포기해서는 안 된다. + - `candidate_id`는 deterministic 규칙 `EVT-{ordinal:03d}-{seq:02d}`로 생성한다(seq는 자기 part 내 1-based 순번). + + + + 이 인스턴스의 `{{item.shard_doc_path}}`(planner가 evidence_all.json에서 분리한 단일 evidence 1건) + `evidence_indexed_parts/E-{ordinal:03d}.json`의 authority metadata + `client_meeting.md` + + + `client_meeting.md`는 아래 목적에만 사용할 수 있다. + - alias 정규화 + - 동일인 식별 보조 + - property cluster shorthand 해석 + - 현재 문서가 어느 상담 조항을 뒷받침하는지에 대한 `meeting_clause_refs` 보조 + - 현재 문서에서 직접 읽히는 내용의 중요도 보조 + - 사망자와 특정 자산군의 연결이 meeting에서 직접 읽히는 경우 succession 관련 candidate의 `affected_asset_cluster_ids` 보조 + 금지: + - meeting만으로 문서에 없는 사건행위를 candidate로 생성 + - meeting만으로 현재 문서의 당사자/금액/날짜를 보충해 direct evidence처럼 쓰기 + + + + + - 자기 part는 정확히 1개 item을 가진다. + - 그 item은 아래 둘 중 하나를 반드시 가진다. + - `event_candidates`가 1개 이상 + - `zero_event_reason`가 non-null + - `zero_event_reason` 허용값: + - `"NO_LEGAL_EVENT_FOUND"` + - `"REFERENCE_ONLY_DOC"` + - `"PARSE_INSUFFICIENT"` + - `"ORDINAL_MATCH_FAILED"` + - 등기부, 계약서, 통지서, 답변서, 감정/시세 문서처럼 통상 event-bearing 문서에 대해서는 `zero_event_reason`를 매우 예외적으로만 사용한다. + + + + `write_file` 전에 반드시 메모리의 part object를 검증한다. 이 검증은 생성 지점 안전장치이며 자동 강제 재실행을 의미하지 않는다. + + 필수 invariant: + 1. 메모리상 top-level은 단일 item object 또는 1-element `items` envelope일 수 있으나, `write_file` 직전 serialization 대상은 단일 item object여야 하며 실질 item은 정확히 1개다. + 2. item의 `evidence_index_proposed`는 runtime parameter `{{item.evidence_index_candidate}}` 또는 그와 동일한 `E-{ordinal:03d}` 값이어야 한다. + 3. item은 아래 둘 중 하나를 반드시 만족해야 한다. + - `event_candidates`가 non-empty list + - `zero_event_reason`이 non-null이며 허용값 중 하나 + 4. `event_candidates`가 빈 배열이면 `zero_event_reason`은 반드시 non-null 허용값이어야 한다. + 5. candidate가 없어 `zero_event_reason`이 non-null이면 schema 안정성을 위해 `event_candidates`는 빈 배열로 둔다. + 6. candidate가 있으면 각 candidate는 object이며, `candidate_id_proposed`는 `EVT-{ordinal:03d}-{seq:02d}` 형식이고 `source_evidence_index_proposed`는 자기 `evidence_index_proposed`와 일치해야 한다. + + 내부 보정 원칙: + - 문서 검토 결과 법적 중요 event/state 후보가 없는 것이 맞으면 `zero_event_reason`을 허용값 중 하나로 명시한다. + - event/state candidate 생성 조건이 충족되는데 빈 배열이면 extraction routines로 되돌아가 candidate를 재작성한다. + - 통상 event-bearing 문서에 `zero_event_reason`을 사용하는 경우, 원문상 후보 부재 또는 파싱 불충분 이유가 허용값과 맞는지 재확인한다. + + 보정 실패: + - 위 invariant 중 하나라도 충족하지 못하면 `write_file`을 호출하지 않는다. + - 마지막 채팅 출력은 정확히 `FAILED: E-{ordinal:03d} event candidate part schema invariant violation`로 한다. + + + + 아래 범주의 법적 중요 행위를 후보로 추출한다. + - 소유권 이전, 매매, 증여, 대물변제, 처분 + - 근저당/저당/전세권/가압류/압류/담보 설정 및 말소 + - 대여, 보증, 신용보증, 대위변제, 구상권 발생, 변제, 배당 + - 임대차, 점유, 인도, 현재 점유, 현재 사용자, 현재 사용수익, 현재 수익귀속, 현재 권리귀속, 현재 관리 상태 + - 경매신청, 경매개시결정, 경매매각, 인도명령, 집행 관련 사실 + - 통지, 최고, 해제/해지의사표시, 도달, 채무승인, 감액요구, 책임부인, 현재상태 주장 + - 판결, 결정, 지급명령, 배당표 등 소송/집행 관련 법적 중요 사실 + - 신축, 영업양도, 사업자등록, 현재 영업상태 등 사건핵심 상태 형성 행위 + - 위 범주와 직접 연결되는 기타 법적 중요 행위 또는 현재상태 + + 제외: + - 순수 배경설명만 있는 문장 + - 정보 증가 없는 반복 진술 + - 법적 효과와 관련 없는 주변사정 + + + + + - 갑구/을구의 각 의미 있는 row를 개별 candidate로 본다. + - 최소 추출 대상: 소유권보존, 소유권이전, 증여, 매매, 담보권실행을 위한 경매개시결정, 담보권실행을 위한 경매로 인한 매각, 근저당권설정, 근저당권설정등기말소, 가압류. + - `support_locators.locator_hint`에는 가능하면 `갑구4`, `을구1`, `순위번호 3` 같은 row identifier를 남긴다. + - 말소표시가 있더라도 법적 의미가 있으면 candidate로 남길 수 있다. + - row에 지분 표시가 직접 읽히면 candidate에 같은 ordinal B1 part의 `registry_row_components[*].fraction_ref`와 동일한 `fraction_ref`를 남긴다. 지분 표시가 없으면 null. + + + + - 하나의 문서에 서로 다른 계약 요소가 공존하면 clause별로 분해한다. + - 예: 대지 임대차 + 건물 매매 + 인도 + 관리사용 귀속 + 명의변경 협력은 가능한 한 별개 candidate로 분리한다. + - B1 part의 `contract_components`가 있으면 그것을 우선 참고하되, raw 문서 확인을 생략하지 않는다. + - 혼합계약서가 계약성립, 대가수수, 인도, 관리·사용 귀속을 직접 보여주면 아래 후보군을 가능한 한 독립 candidate로 생성한다. + 1. 계약성립 candidate + 2. 대가수수 candidate + 3. 인도 candidate + 4. 관리·사용 또는 현재 수익귀속 state candidate + - 위 4종 중 raw 문서가 직접 보여주는 것은 formation candidate 1개로 뭉개지지 않도록 반드시 분리한다. + - 대가수수 candidate는 적절한 기존 enum이 없으면 `event_kind="기타"`, `event_subkind="대가수수"`로 둘 수 있다. + - 대가수수 candidate에는 가능한 한 지급자, 수령자, 금액, 목적물, 해당 조항 locator를 함께 남긴다. + - 인도 candidate에는 가능한 한 인도자, 인도수령자, 목적물, 개시일, locator를 함께 남긴다. + - 관리·사용 귀속 candidate는 `event_or_state="state"`를 우선 검토하고, `current_state_candidate` 하위 4축과 interval field를 함께 남긴다. + - 문서가 계약일로부터 현재 점유/사용 귀속을 직접 보여주면 `state_start_date`를 그 개시일로 남기고, 뒤에 다른 점유/사용 candidate가 있으면 선행 state를 open-ended로 두지 않는다(같은 문서 내). + + + + - 내용증명, 답신, 답변서, 최고서, 해제통지서, 소멸청구 통지서에서는 아래를 개별 candidate로 검토한다. + - 지급 요구 + - 해제/해지 통지 + - 도달 + - 책임 부인 + - 현재 점유/현재 거주/현재 운영 주장 + - 사용수익 귀속 주장 + - 현재 권리귀속 주장 + - 채무승인 또는 시효항변 주장 + - 감액요구, 명도요구 + - 문서에 현재 상태가 직접 적혀 있으면 `event_or_state="state"` candidate를 생성할 수 있다. + - 현재 상태가 후행 시점에서 선행 상태를 대체하는 구조가 직접 보이면 interval field를 함께 남긴다. + + + + - 감정평가서, 감정평가 의견서, 임료 시세 확인서 등은 `event_candidates`가 0개라고 판단하지 않는다. + - 시가표, 차임표, 사용이익표, 시장가치표가 있으면 최소 1개의 factual/state candidate를 생성한다. + - rent/use-gain valuation은 `action_type_candidate="사실행위(factual acts)"`로 둘 수 있다. + - 문서가 특정 현재 수익귀속자 또는 현재 권리귀속자를 직접 보여주면, 해당 정보는 `current_state_candidate`의 대응 배열에 분리해 둔다. + + + + - 사업자등록증, 가족관계증명서, 현재상태 확인 문서, 심판문 등은 상태형 후보 또는 법적 지위 관련 후보를 생성할 수 있다. + - 예: 사업자등록은 현재 영업상태/점포 운영 개시의 간접 또는 직접 후보가 된다. + - 가족관계증명서, 상속포기 심판문, 상속 관련 공문서가 사망자와 친족구성 또는 상속포기를 직접 보여주면 succession 관련 candidate를 생성한다. + - sparse status 문서라도 later stage linker가 사용할 수 있도록 가능한 범위에서 `participants`, `object_spec`, `event_date` 또는 `current_state_candidate.state_as_of`를 채운다. + - family/renunciation/status 문서를 generic status로만 남기지 않는다. + + + + - 사망, 가족관계, 상속포기, 재산상속회복, 승계 관련 문서는 가능한 한 succession 관련 candidate를 생성한다. + - succession 관련 candidate에는 optional field `affected_asset_cluster_ids`를 둘 수 있다. + - `affected_asset_cluster_ids`는 meeting이나 B1 part metadata가 사망자와 특정 자산군의 연결을 직접 또는 명백하게 보여줄 때만 채운다. + - succession 관련 candidate에는 optional field `succession_candidate`를 둘 수 있다. + { + "deceased_candidate": null, + "spouse_candidates": [], + "lineal_ascendant_candidates": [], + "lineal_descendant_candidates": [], + "renunciant_candidates": [] + } + - 가족관계증명서 같은 status doc은 direct 내용이 보이면 `deceased_candidate`, `spouse_candidates`, `lineal_ascendant_candidates`, `lineal_descendant_candidates`를 가능한 한 채우고, 자산 연결이 직접 또는 명백하면 `affected_asset_cluster_ids`도 함께 채운다. + - 상속포기 문서는 상속포기 그 자체를 direct candidate로 남기되, direct names가 보이면 `succession_candidate.renunciant_candidates`를 반드시 채운다. + - 상속포기 문서가 특정 사망자와 자산 cluster를 직접 또는 명백하게 연결하면 동일한 `affected_asset_cluster_ids`를 함께 남긴다. + - direct names가 있는 succession/status 문서에서는 가능한 범위에서 `participants`와 `object_spec`도 함께 남겨 later stage overlap scorer가 sparse document를 놓치지 않게 한다. + - 이 단계는 최종 상속분, 단독상속, complaint plaintiff narrowing을 확정하지 않는다. 다만 downstream이 succession bundle을 읽을 수 있을 정도의 구조는 반드시 남긴다. + + + + + - raw event는 문서가 직접 보여주는 사실행위·법률행위·준법률행위다. + - legal-effect event는 raw event에서 downstream 청구권 식별에 필요한 법률효과 후보를 도출한 candidate다. + - raw event와 legal-effect event는 같은 문서에서 함께 생성할 수 있으며, 서로 다른 `candidate_id`를 가진다. + - legal-effect event는 최종 법률판단이 아니라 Stage 2가 검토할 구조화된 후보이므로 `confidence`와 `review_flags`를 함께 남긴다. + + + 금전채권: + - `underlying_claim_arising` + - `first_demand` + - `demand_notice_dispatch` + - `demand_notice_arrival` + - `debt_acknowledgement` + - `new_due_date` + - `business_transfer` + - `trade_name_continuation_candidate` + - `commercial_successor_liability_candidate` + + 담보부 부동산: + - `secured_debt_creation` + - `mortgage_setting_by_fraction` + - `undesignated_payment` + - `payment_allocation_effect` + - `mortgage_extinguishment_by_fraction` + - `auction_application_after_extinguishment` + - `auction_sale_and_registry_transfer` + - `derivative_mortgage_setting_by_invalid_title_holder` + - `unregistered_building_original_acquisition` + - `de_facto_disposition_holder_acquisition` + - `land_use_gain_start` + - `liability_overlap_start` + + 사해행위취소: + - `preserved_claim_arising` + - `collateral_setting_for_preserved_claim` + - `fraudulent_transfer` + - `asset_value_at_fraudulent_act` + - `asset_value_at_close_proxy` + - `collateral_insufficiency_at_act` + - `encumbrance_release_after_act` + - `value_compensation_exception_trigger` + - `value_compensation_cap_calculation_candidate` + - `defense_assertion_title_trust` + - `defense_assertion_sufficient_collateral` + - `defense_assertion_setoff` + + 유치권ㆍ상속ㆍ통지: + - `lien_assertion` + - `third_party_lease_by_lien_holder` + - `lien_violation_candidate` + - `lien_extinction_notice_dispatch` + - `lien_extinction_notice_receipt` + - `lien_extinction_effect_candidate` + - `ongoing_physical_possession` + - `succession_renunciation_filing` + - `succession_renunciation_acceptance` + - `spouse_sole_succession_effect_candidate` + - `defense_assertion_subsequent_owner_no_extinction_right` + - `defense_assertion_benefit_applied_to_secured_claim` + + + + - claim_doc 문서가 채무승인, 최고, 변제기, 변제약정, 지급기일을 함께 포함하면 하나의 generic event로 압축하지 말고 legal effect별 candidate를 분리한다. + - 이행각서에는 최소한 `debt_acknowledgement`, `payment_promise`, `new_due_date` 후보가 있는지 검토한다. + - 최고서에는 `demand_notice_dispatch`, `demand_notice_arrival`, `limitation_preservation` 관련 후보가 있는지 검토한다. + - 발송일과 도달일이 다르면 각각 별도 `event_date`로 보존한다. + - 채무승인 event에는 `legal_effect_candidate.acknowledgement_of_debt=true`, `limitation_interruption_candidate=true`, `related_claim_type`을 직접 읽히는 범위에서 남긴다. + + + + - business_transfer_doc 문서는 단순 영업양도 formation event로만 끝내지 않는다. + - 문서 또는 상담내용에서 상호, 영업명, 영업자산, 고객관계, 고용관계, 채무불승계 조항, 책임배제 등기 필요성이 보이면 `commercial_successor_candidates`로 분해한다. + - 상법 제42조 책임을 최종 확정하지는 않되, downstream이 요건판단을 할 수 있도록 transferor, transferee, business_name, trade_name_continuation_candidate, debt_exclusion_clause_candidate를 남긴다. + + + + - 원자료 event가 곧바로 법률효과를 발생시키는 경우 raw event와 legal-effect event를 모두 생성한다. + - 복수 채무에 대한 지정 없는 변제가 등장하면 `undesignated_payment` event와 별도로 `payment_allocation_effect` 후보를 생성한다. + - 등기부상 경매 매각과 후행 근저당권이 등장하면, 그 경매가 어떤 담보권에 기초했는지, 그 담보권의 피담보채무가 이미 소멸했는지 검토하는 event chain을 만든다. + - 미등기 건물 신축일과 미등기 건물 매수일은 점유·사용이익 발생 시점을 정하므로 `land_use_gain_start` 및 `liability_overlap_start` event 후보를 생성한다. + - `mortgage_setting_by_fraction`/`mortgage_extinguishment_by_fraction` 등 `*_by_fraction` subkind candidate는 지분 표시가 문서에 직접 읽히는 한 `fraction_ref`를 반드시 가진다. + + + + - 사해행위취소 사건에서 raw event와 legal-effect event를 구분한다. + - 사해행위 당시 존재하던 담보권이 사후 말소되면 `value_compensation_exception_trigger` event를 생성한다. + - valuation row와 사후 말소 부담, 존속 부담이 모두 있으면 `value_compensation_cap_calculation_candidate`를 생성한다. + - 피고가 항변을 명시한 내용증명은 `defense_assertion_*` event로 만들고, Stage 2 claim description에 반영해야 하는지 표시한다. + + + + - 통지 문서와 답변서가 서로 대응하고, 답변서가 특정 통지의 수령일을 인정하면 별도의 receipt event candidate를 생성한다. + - receipt event candidate는 발송 event와 병합하지 않는다. + - 발송일과 도달일이 다르면 도달일을 법률효과 후보일로 별도 보존한다. + - 문서에 유치권, 공사잔금, 점유, 임대, 직접 거주, 피담보채권 충당, 후소유자 항변이 함께 나타나면 일반 current possession event가 아니라 lien_event 후보군으로 분해한다. + - 제3자 임대는 `lien_violation_candidate`로, 직접 거주는 ongoing use state로, 피담보채권 충당 주장은 `defense_candidate`로, 소멸청구 수령은 `lien_extinction_effect_candidate`로 각각 분리한다. + - 상속포기 수리 event가 있고 배우자와 자녀 전부 상속포기 구조가 직접 보이면 `spouse_sole_succession_effect_candidate`를 생성하되, 최종 소장 원고 확정은 Task_C 이후로 미룬다. + + + + + `action_type_candidate`는 아래 중 1개를 선택한다. + - `"법률행위(legal acts)"` + - `"준법률행위(quasi-legal acts)"` + - `"사실행위(factual acts)"` + - `"위법행위(unlawful acts)"` + - `"소송행위(litigation acts)"` + - `"불명"` + + + + `event_kind`는 아래 값 중 가장 가까운 것을 선택한다. + - `"처분"`, `"소유권이전"`, `"증여"`, `"대물변제"`, `"담보설정"`, `"담보말소"`, `"채권발생"`, `"보증"`, `"대위변제"`, `"변제"`, `"배당"`, `"임대차"`, `"점유"`, `"인도"`, `"가압류/압류"`, `"경매신청"`, `"경매개시"`, `"경매매각"`, `"통지"`, `"해제/해지"`, `"판결/결정"`, `"영업양도"`, `"사업자등록"`, `"신축"`, `"상속개시"`, `"상속포기"`, `"채권관리"`, `"도달"`, `"항변"`, `"가치평가"`, `"법률효과"`, `"기타"` + + 필요하면 optional field `event_subkind`에 더 구체한 라벨(REQUIRED_EVENT_SUBKINDS 참조)을 남길 수 있다. + + + + - `participants.actor_candidates`는 직접 행위를 한 사람/법인 + - `participants.counterparty_candidates`는 직접 상대방 + - `participants.beneficiary_candidates`는 행위로 이익을 직접 취득한 사람/법인 + - `participants.third_party_candidates`는 법원, 피상속인, 기존 권리자 등 제3자 + - `object_spec`는 짧은 목적물/채권/담보/통지대상/상태 대상 명사구 + - `amount`는 현재 candidate와 직접 연결되는 대표 금액 1개만 사용한다 + - state candidate의 현재 점유/사용/수익/권리 귀속은 `participants`에 뭉개지지 않고 `current_state_candidate` 하위 배열에 분리한다 + - succession/status 문서처럼 sparse한 문서라도 direct names와 direct relation이 보이면 `participants`와 `object_spec`를 불필요하게 비워두지 않는다. + - 잘못된 당사자 확정보다 `[]` 또는 `null`이 낫다 + + + + - `event_date`는 가능하면 `YYYY-MM-DD` + - 불명확하면 `null` + - `time_text`는 짧은 원문 표현을 유지할 수 있다 + - `time_precision` 허용값: `"exact"`, `"approximate"`, `"range"`, `"unknown"` + - 현재상태 candidate의 경우 `state_as_of` 개념은 optional `current_state_candidate.state_as_of`에 남기고, `event_date`는 상태 개시 시점이 직접 보일 때만 채운다. + - state interval이 직접 보이면 `current_state_candidate.state_start_date`, `current_state_candidate.state_end_date_candidate`를 함께 채운다. + - 후행 상태가 선행 상태를 대체하는 것이 직접 보이면 `current_state_candidate.superseded_by_candidate_id`를 사용한다. + + + + - optional field `event_or_state`를 추가할 수 있다. 허용값은 `"event"` 또는 `"state"`. + - 문서가 단발 사건이 아니라 `현재까지`, `현재도`, `계속`, `현재 운영`, `현재 점유`, `현재 거주`, `부지로만 사용`, `현재 명의`, `현재 소유`, `차임을 받고 있다` 같은 계속상태를 직접 보여주면 `event_or_state="state"` candidate를 생성할 수 있다. + - state candidate에는 optional field `current_state_candidate`를 둘 수 있다. + { + "state_summary": "짧은 상태 설명", + "state_as_of": "현재|2024-10-31 등", + "state_start_date": null, + "state_end_date_candidate": null, + "superseded_by_candidate_id": null, + "is_ongoing": true, + "current_possessor_candidates": [], + "current_user_candidates": [], + "current_benefit_holder_candidates": [], + "current_right_holder_candidates": [], + "possession_legal_cause_asserted": null, + "possession_legal_cause_asserted_by": [] + } + - `current_state_candidate`는 가능한 한 아래 4축으로 분해한다. + - 점유자 -> `current_possessor_candidates` + - 사용자/운영자 -> `current_user_candidates` + - 차임·사용이익·영업수익 등 현재 수익귀속자 -> `current_benefit_holder_candidates` + - 현재 소유자·등기명의인·권리귀속자 -> `current_right_holder_candidates` + - 같은 사람이 복수 role을 가지면 각 배열에 중복 기재할 수 있다. + - 문서에 직접 읽히지 않는 축은 빈 배열로 둔다. 추정으로 채우지 않는다. + - event candidate와 state candidate는 동일 문서에서 함께 존재할 수 있다. + - 동일 문서에 과거 행위와 현재 상태가 함께 있으면 분리한다. + - 동일 문서에 현재 점유자와 현재 권리귀속자가 다르게 읽히면 하나로 뭉개지지 않도록 분리 기재한다. + - 상태 개시 시점이 직접 보이면 `state_start_date`를 채운다. + - 후행 점유/사용/수익귀속 candidate가 선행 상태를 대체하는 것이 직접 보이면, 선행 state에 `state_end_date_candidate`와 `superseded_by_candidate_id`를 남기고 open-ended current state로 방치하지 않는다. + - `state_end_date_candidate`가 직접 보이면 `is_ongoing=false`를 우선 검토한다. + - 혼합계약서가 인도, 관리·사용 귀속, 사용수익 귀속을 직접 보여주면 그 상대방에 대한 state candidate를 high confidence로 생성할 수 있다. + - raw 문서가 현재 점유, 현재 사용, 현재 수익귀속, 현재 권리귀속을 직접 적시하면 candidate `confidence`는 원칙적으로 `high`를 우선 검토한다. + - meeting 보조나 간접 정황에 크게 의존한 상태 후보는 `medium` 또는 `low`로 둔다. + - `possession_legal_cause_asserted`는 optional 필드다. 허용값: "유치권행사중|임대차|사용대차|물권|동시이행항변|명도후잔류|무권원주장|미상". `possession_legal_cause_asserted_by`는 그 권원을 주장한 주체(원고측/피고측/제3자 이름)이며 문서에 직접 읽힐 때만 채운다. + - 점유 사실과 권원 주장은 같은 candidate 안에서 축을 분리해 남긴다(사실=4축 배열, 권원=`possession_legal_cause_asserted`). 권원 주장이 문서에 없으면 `미상`이 아니라 null로 둔다. + - 서로 다른 문서가 동일 자산에 대해 다른 권원을 주장하는 것은 본 mapper의 검출 대상이 아니다(authority drift 금지). 각 문서의 주장을 그대로 보존한다. + + + + - optional field `meeting_clause_refs`를 추가할 수 있다. + - 형식은 `1-가`, `2-라`, `3-다`, `4-가`, `5-나`처럼 `client_meeting.md`의 본문 번호체계를 사용한다. + - 현재 문서가 특정 상담조항을 직접 뒷받침할 때만 기록한다. + - meeting clause ref는 보조 recall 장치일 뿐, 문서에 없는 사실을 가져오는 수단이 아니다. + + + + 이 단계는 최종 사해행위 판단을 하지 않는다. 다만 후속 단계의 recall 강화를 위해 candidate tag를 남긴다. + `actio_relevance_candidates.candidate_role_tags` 허용값: + - `"사해행위목적물후보"`, `"원고채권후보"`, `"선순위담보후보"`, `"존속담보후보"`, `"임차권후보"`, `"가압류후보"`, `"수익자이익후보"`, `"현재점유후보"`, `"현재사용수익후보"` + + `fraudulent_act_date_candidate`는 다음 경우에만 채운다. + - 현재 candidate가 직접 목적물의 처분, 이전, 이전등기, 매매일, 증여일, 대물변제일, 경매매각일과 연결되는 경우 + - 그 외에는 쓰지 않는다 + + + + downstream Task_C BO와 evidence 기반 candidate가 중복 제거할 수 있도록, 각 candidate마다 아래 정규화 문자열을 생성한다. + + `(event_or_state + event_kind + 핵심 actor + 핵심 counterparty + 핵심 object_spec/amount + 핵심 date_or_state_as_of)` + + 규칙: + - 불명확한 요소는 생략하되, 최소한 `event_or_state` 또는 `event_kind`와 1개 이상의 핵심 요소는 남긴다. + - 중복 제거를 위해 whitespace, 중복 조사, 과도한 수식어는 제거한다. + - title이나 evidence_index는 넣지 않는다. + - event와 state는 같은 signature가 되지 않도록 구분한다. + - mapper가 propose하는 candidate_id는 `EVT-{ordinal:03d}-{seq:02d}`이며 quality gate가 collision을 처리한다. + + + + part 파일은 단일 item object 형태이며, top-level item은 non-empty `event_candidates` 또는 non-null `zero_event_reason` 중 하나를 반드시 가진다. `event_candidates: []` 단독은 금지된다. + + { + "evidence_index_proposed": "E-{ordinal:03d}", + "title": "문서 제목", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": ordinal + }, + "property_cluster_id": null, + "doc_semantic_flags": [], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-{ordinal:03d}-01", + "event_or_state": "event|state", + "event_kind": "소유권이전", + "event_subkind": null, + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "짧은 1문장 요약", + "source_evidence_index_proposed": "E-{ordinal:03d}", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [], + "counterparty_candidates": [], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": null, + "fraction_ref": null, + "amount": null, + "event_date": null, + "event_end_date": null, + "time_text": null, + "time_precision": "exact|approximate|range|unknown", + "location": null, + "legal_keywords": [], + "support_locators": [ + { + "locator_type": "heading|key_value|table_row|paragraph|caption|unknown", + "locator_hint": "갑구4|제2조 등", + "excerpt": "짧은 단서", + "directness": "직접|간접|불명" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "string|null", + "related_claim_type": "string|null", + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "state|점유|...", + "confidence": "high|medium|low|unknown" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } + } + + 추가 제약: + - top-level은 단일 object다. envelope이 아닌 단일 item. + - `event_candidates`가 빈 배열이면 `zero_event_reason`은 반드시 non-null 허용값이어야 한다. + - `event_candidates: []`와 `zero_event_reason` 누락/null이 결합된 part는 작성 금지되는 schema invariant violation이다. + - `candidate_id_proposed`는 mapper의 propose 식별자(`EVT-{ordinal:03d}-{seq:02d}`)이며, finalize는 quality gate가 한다. + - `source_evidence_index_proposed`는 mapper가 propose 한 자기 ordinal의 candidate evidence_index다. + - `mapper_meta.self_isolation_attested=true`는 본 mapper가 자기 문서 외 사실을 주입하지 않았음을 self-attest 한다는 의미다. + + + + - `write_file` 전 을 수행하고, 불량이면 내부 보정을 먼저 시도했는지 점검한다. + - 자기 part object가 단일 item이고 `evidence_index_proposed`, non-empty `event_candidates` 또는 non-null `zero_event_reason`을 가지는지 점검한다. + - `event_candidates: []`와 `zero_event_reason` 누락/null이 함께 있으면 내부 보정 대상이며, 보정 불가 시 part 파일을 쓰지 않고 FAILED로 종료한다. + - `planner_status=="READY"` 및 `mapper_execution_allowed=true`가 확인되지 않았으면 part 파일이 작성되지 않았는지 점검한다. + - 모든 candidate의 `candidate_id_proposed`가 `EVT-{ordinal:03d}-{seq:02d}` 형식인지 점검한다. + - 자기 문서 외 ordinal의 사실이 part에 주입되지 않았는지 self-check 한다(authority drift 방지). + - `registry_doc` 또는 `multi_event_doc`인데 `event_candidates`가 0개면 다시 점검한다. + - `mixed_contract_doc`인데 서로 다른 법적 효과가 2개 이상 보이면 candidate도 최소 2개 이상 검토한다. + - 혼합계약서가 계약성립, 대가지급·수령, 인도, 관리·사용 귀속을 직접 보여주면 formation candidate만 남기지 말고 consideration/delivery/state candidate가 함께 검토되었는지 점검한다. + - 문서가 위 네 요소를 모두 직접 보여주면 최소 3개 이상, 가능하면 4개까지 독립 candidate로 분리되었는지 점검한다. + - `valuation_doc`인데 가치표/차임표가 직접 보이면 candidate를 최소 1개 생성한다. + - 문서가 상담일지의 명시 조항을 직접 뒷받침하는데도 `meeting_clause_refs`가 전혀 없으면 다시 점검한다. + - event와 state는 구분해 생성한다. 현재상태를 과거 단발행위로 뭉개지 않는다. + - state candidate에서 현재 점유자/사용자/수익귀속자/권리귀속자가 직접 읽히는데도 하나의 participant 배열로 뭉개졌다면 다시 점검한다. + - 현재 점유자와 현재 권리귀속자가 다르게 읽히는 문서는 `current_state_candidate` 하위 배열에서 분리되었는지 다시 점검한다. + - 상태 개시와 종료가 직접 보이거나 후행 상태가 선행 상태를 대체하는 것이 직접 보이면 `state_start_date`, `state_end_date_candidate`, `superseded_by_candidate_id` 검토가 이루어졌는지 점검한다. + - 가족관계증명서가 사망자와 친족구성을 직접 보여주면 `succession_candidate.deceased_candidate`, `spouse_candidates`, `lineal_descendant_candidates` 또는 `lineal_ascendant_candidates`가 필요한 범위에서 채워졌는지 점검한다. + - 상속포기 심판문이 direct names를 보여주면 `succession_candidate.renunciant_candidates`가 비어 있지 않은지 점검한다. + - 가족관계증명서, 상속포기 심판문, 상속 관련 공문서가 사망자와 특정 자산군의 연결을 직접 또는 명백하게 보여주면 `affected_asset_cluster_ids` 검토가 이루어졌는지 점검한다. + - succession/status 문서처럼 sparse한 문서라도 direct names, direct relation, direct date가 보이는데 `participants`, `object_spec`, `event_date` 또는 `state_as_of`가 불필요하게 비어 있지 않은지 점검한다. + - raw 문서가 현재상태를 직접 적시한 경우 candidate `confidence`가 불필요하게 low로 떨어지지 않았는지 점검한다. + - claim doc에 채무승인ㆍ최고ㆍ변제기 재설정 단서가 있는데 `legal_effect_candidate`가 있는 event가 전혀 없으면 self-warning으로 본다. + - 이행각서에 채무승인일과 새 변제기가 함께 있는데 `debt_acknowledgement`와 `new_due_date`가 분리되지 않으면 self-warning으로 본다. + - business transfer 문서에 상호속용 후보 event 또는 `commercial_successor_candidates`가 없으면 self-warning으로 본다. + - 복수 피담보채무와 변제가 있는데 `payment_allocation_effect`가 없으면 self-warning으로 본다. + - 경매 event가 있는데 기초 담보권 및 피담보채무 chain과 연결되지 않으면 `AUCTION_BASIS_CHAIN_MISSING` self-warning으로 본다. + - 미등기 건물 매수 event가 있는데 원시취득 event가 없으면 `UNREGISTERED_BUILDING_ORIGINAL_ACQUISITION_MISSING` self-warning으로 본다. + - 사후 담보권 말소 row가 있는데 `value_compensation_exception_trigger` event가 없으면 self-error로 본다. + - valuation matrix와 released encumbrance가 있는데 `value_compensation_cap_calculation_candidate`가 없으면 self-warning으로 본다. + - defense assertion event가 있는데 `defense_candidate.requires_rebuttal_in_stage2` 검토가 없으면 self-warning으로 본다. + - 유치권 소멸 통지서와 수령 인정 답변서가 있는데 도달 event 또는 `lien_extinction_notice_receipt`가 없으면 self-error로 본다. + - 제3자 임대 event가 있는데 `lien_violation_candidate`가 없으면 self-warning으로 본다. + - 상속포기 수리 event가 있는데 `spouse_sole_succession_effect_candidate` 후보가 없으면 self-warning으로 본다. + - self-warning/self-error는 mapper가 part 파일에 그대로 남겨도 되며, quality gate가 최종 severity를 판정한다. + + + + + {{item.assigned_ordinal}} + + + [ ] 1) Preflight: `list_docs` 사용해서 `{{item.b1_output_path}}`, `{{item.shard_doc_path}}`, `client_meeting.md` 만 확인. `read_docs` 사용해서 동일 파일만 읽기 + [ ] 2) runtime parameter `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.b1_output_path}}`, `{{item.b2_output_path}}` 그대로 소비. 별도 검증 read 불요. + [ ] 3) `{{item.shard_doc_path}}`의 단일 evidence 문서를 메모리에서 사용하고, 같은 ordinal의 B1 part(`{{item.b1_output_path}}`)만 authority metadata로 사용 + [ ] 4) `write_file` 전 수행. 불량이면 내부 보정 시도 + [ ] 5) 보정 후에도 불량이면 part 파일을 쓰지 않고 `FAILED: E-{ordinal:03d} event candidate part schema invariant violation` 출력 후 종료 + [ ] 6) 통과한 경우에만 `write_file` 사용해서 `evidence_event_candidate_parts/E-{ordinal:03d}.json` 생성. event 후보가 0건이어도 non-null `zero_event_reason` part 파일을 작성 + [ ] 7) `list_docs` 사용해서 자기 part 파일만 존재 확인(절대 읽기 금지) + [ ] 8) `"E-{ordinal:03d} event candidates part 작성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `{{item.b1_output_path}}` (같은 ordinal 의 B1 part) + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 1건; planner가 evidence_all.json에서 분리) + - `client_meeting.md` + + + - `evidence_event_candidate_parts/E-{ordinal:03d}.json` + + + + + - `candidate_id_proposed`는 정확히 `EVT-{ordinal:03d}-{seq:02d}` 형식만 허용한다. + - `ordinal`과 output path는 runtime parameter (`{{item.*}}`) 를 그대로 따른다. + - 증거 문서 수 N, assigned_ordinal, mapper fan-out, streaming edge는 본 task에서 만들지 않는다. + - 자기 part 파일은 절대 재읽지 않는다. + - 다른 ordinal 문서를 처리하지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"E-{ordinal:03d} event candidates part 작성 완료"` 한 줄만 사용한다. + + + + - task_name: Task_B1_quality_gate_evidence_indexed + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 240 + code: | + #!/usr/bin/env python3 + # Task_B1_quality_gate_evidence_indexed (v3) - deterministic B1 gate reducer (GA-1) + # Spec: Part_1_Improvement_Strategy_Claude_v1.md §4 + from __future__ import annotations + import itertools + import json + import re + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + + PLAN_PATH = "evidence_shard_plan.json" + PART_DIR = "evidence_indexed_parts" + FINAL_PATH = "evidence_indexed.json" + MANIFEST_PATH = "stage1_tmp/quality_gate/finalized_evidence_id_manifest.json" + PRECHECK_PATH = "stage1_tmp/quality_gate/B1_precheck.json" + EXCERPT_LIMIT = 1500 + EVIDENCE_ID_RE = re.compile(r"^E-\d{3}$") + WARNING_KEY_RE = re.compile(r"warn|review|risk|note", re.IGNORECASE) + DRIFT_TEXT_RE = re.compile(r"drift|출처|다른\s*문서|타\s*문서", re.IGNORECASE) + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-b1-quality-gate-evidence-indexed", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _mid(), + "method": "tools/call", + "params": {"name": name, "arguments": args}, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: Any, name: str) -> str: + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError(f"Empty: {name}") + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, dict) and "results" in outer: + result0 = (outer.get("results") or [{}])[0] + inner = result0.get("content") or result0.get("text") or "" + if not inner: + raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer if isinstance(outer, str) else json.dumps(outer, ensure_ascii=False) + + def read_raw(path: str) -> str: + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str) or not raw.strip(): + raise RuntimeError(f"Missing or empty required input: {path}") + return raw + + def parse_json(raw: str, path: str) -> Any: + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + try: + obj, _ = decoder.raw_decode(raw.strip()) + return obj + except Exception as exc: + raise RuntimeError(f"Invalid JSON input: {path}") from exc + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def clip(value: Any, limit: int = 200) -> str: + text = " ".join(str(value or "").split()) + if len(text) <= limit: + return text + return text[:limit].rstrip() + "..." + + def list_part_names(folder: str) -> list[str] | None: + # planner-compatible list_docs enumeration; None => enumeration unavailable (recorded, non-fatal) + for args in ({"path": folder, "pattern": "E-*.json"}, {}, {"doc_names": []}): + try: + payload = _unwrap(_call("list_docs", args), "list_docs") + except Exception: + continue + if isinstance(payload, str): + try: + payload = json.loads(payload) + except Exception: + return re.findall(r"[\w./-]+\.json", payload) + vals: list[str] = [] + if isinstance(payload, dict): + for value in payload.values(): + if isinstance(value, list): + for item in value: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + elif isinstance(value, str): + vals.append(value) + elif isinstance(payload, list): + for item in payload: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + return [v for v in vals if v] + return None + + def get_single_item(part: Any, path: str) -> dict[str, Any]: + if isinstance(part, dict) and isinstance(part.get("items"), list): + items = part["items"] + if len(items) != 1 or not isinstance(items[0], dict): + raise RuntimeError(f"part must contain exactly one item object: {path}") + return items[0] + if isinstance(part, dict): + return part + raise RuntimeError(f"part top-level is not an object: {path}") + + def nonempty(value: Any) -> bool: + return value not in (None, "", [], {}) + + def flags_of(item: dict[str, Any]) -> list[str]: + return [str(f) for f in (item.get("doc_semantic_flags") or []) if f] + + def norm_title(value: Any) -> str: + return re.sub(r"\s+", "", str(value or "")).lower() + + # v.2 CHECK_4 rule table: (rule_id, flag token, required-nonempty fields (any-of)) + COVERAGE_RULES = ( + ("registry_rows", "registry", ("registry_row_components",)), + ("claim_components", "claim", ("claim_document_components",)), + ("schedule_items", "schedule", ("schedule_items",)), + ("business_transfer", "business_transfer", ("business_transfer_components",)), + ("valuation", "valuation", ("valuation_object", "valuation_rows")), + ("succession", "succession", ("succession_document_components",)), + ("defense", "defense", ("defense_assertions", "defense_components", "admission_components")), + ) + + def main() -> None: + _init() + plan = parse_json(read_raw(PLAN_PATH), PLAN_PATH) + if not isinstance(plan, dict): + raise RuntimeError("evidence_shard_plan.json is not an object") + planner_status = plan.get("planner_status") + if planner_status not in (None, "READY"): + print(json.dumps({"status": "FAILED", "gate": "B1", + "message": "B1 quality gate BLOCK 발생: downstream 진행 금지", + "reason": f"planner_status is not READY: {planner_status}"}, ensure_ascii=False)) + raise RuntimeError(f"planner_status is not READY: {planner_status}") + + expected_ordinals = [str(o) for o in (plan.get("expected_ordinals") or [])] + if not expected_ordinals: + raise RuntimeError("plan.expected_ordinals is empty") + expected_ids = [str(x) for x in (plan.get("expected_evidence_ids") or [f"E-{o}" for o in expected_ordinals])] + shard_count = int(plan.get("shard_count") or len(expected_ordinals)) + stale_policy = (plan.get("stale_part_policy") or {}).get("detected_stale_parts") or {} + if isinstance(stale_policy, dict): + stale_parts = [str(x) for x in (stale_policy.get(PART_DIR) or [])] + else: + stale_parts = [str(x) for x in (stale_policy or [])] + shard_paths: dict[str, str] = {} + for sh in plan.get("shards") or []: + if isinstance(sh, dict) and sh.get("assigned_ordinal") is not None: + try: + shard_paths[f"{int(sh['assigned_ordinal']):03d}"] = str(sh.get("shard_doc_path") or "") + except Exception: + continue + + hard_findings: list[dict[str, Any]] = [] + + def hard(issue_type: str, detail: str, **extra: Any) -> None: + hard_findings.append({"issue_type": issue_type, "detail": clip(detail), **extra}) + + # -------- CHECK 1: shard completeness (deterministic) -------- + if stale_parts: + hard("stale_part", f"stale parts detected by planner policy: {stale_parts}") + missing_ordinals: list[str] = [] + parse_failures: list[dict[str, str]] = [] + parts: dict[str, dict[str, Any]] = {} + for o in expected_ordinals: + path = f"{PART_DIR}/E-{o}.json" + try: + raw = read_raw(path) + except Exception: + missing_ordinals.append(o) + continue + try: + parts[o] = get_single_item(parse_json(raw, path), path) + except Exception as exc: + parse_failures.append({"ordinal": o, "reason": clip(exc)}) + if missing_ordinals or parse_failures: + # FAILURE_POLICY: input-level failure -> report only, write no final files + # SKILL.md §3.3: stdout = task output — 단일 JSON 라인만 출력한다 + print(json.dumps({ + "status": "FAILED", + "gate": "B1", + "message": "B1 quality gate BLOCK 발생: downstream 진행 금지", + "missing_ordinals": missing_ordinals, + "parse_failures": parse_failures, + }, ensure_ascii=False)) + raise RuntimeError("B1 input parts missing or unparsable; final files not written") + extra_parts: list[str] = [] + listed = list_part_names(PART_DIR) + extra_detection = "done" if listed is not None else "unavailable" + if listed is not None: + pat = re.compile(r"(?:^|/)E-(\d{3})\.json$") + expected_set = set(expected_ordinals) + for name in listed: + m = pat.search(str(name)) + if m and m.group(1) not in expected_set: + extra_parts.append(str(name)) + if extra_parts: + hard("unexpected_ordinal", f"unexpected part files outside planner plan: {sorted(set(extra_parts))}") + + # -------- CHECK 2: evidence index determinism (deterministic) -------- + determinism_violations: list[dict[str, str]] = [] + for o in expected_ordinals: + proposed = str(parts[o].get("evidence_index_proposed") or "") + if not EVIDENCE_ID_RE.match(proposed) or proposed != f"E-{o}": + determinism_violations.append({"ordinal": o, "proposed": proposed}) + if determinism_violations: + hard("evidence_index_format_mismatch", f"evidence_index_proposed violations: {determinism_violations}") + + # -------- CHECK 3: duplicate document alias (deterministic exact + suspect pairs) -------- + suspects: list[dict[str, Any]] = [] + dropped_aliases: list[dict[str, Any]] = [] + duplicate_groups: list[dict[str, Any]] = [] + canonical_choices: list[dict[str, Any]] = [] + # 자동 dedup은 강한 동시 일치(v.2 CHECK_3의 "동시 일치성")에서만 수행한다. + # 제목만 같은 서로 다른 문서(예: 서로 다른 채무의 차용증 2건)는 절대 자동 삭제하지 않는다 — R-ALIAS suspect로만 등록. + exact_groups: dict[tuple[str, str], list[str]] = {} + for o in expected_ordinals: + item = parts[o] + uid = str(item.get("doc_uid") or "") + tnorm = str(item.get("title_normalized") or "") + if uid: + exact_groups.setdefault(("doc_uid", uid), []).append(o) + if tnorm: + joint = json.dumps([tnorm, item.get("key_dates"), item.get("key_amounts"), item.get("key_parties")], + ensure_ascii=False, sort_keys=True) + exact_groups.setdefault(("joint_identity", joint), []).append(o) + dropped_set: set[str] = set() + for (kind, value), ords in sorted(exact_groups.items()): + ords = sorted(set(ords)) + if len(ords) < 2: + continue + canonical = ords[0] # v.2 canonical rule (i): smaller ordinal + duplicate_groups.append({"match_kind": kind, "match_value": value, "ordinals": ords}) + canonical_choices.append({"match_value": value, "canonical_ordinal": canonical}) + for dropped in ords[1:]: + if dropped in dropped_set: + continue + dropped_set.add(dropped) + dropped_aliases.append({ + "dropped_ordinal": dropped, + "dropped_evidence_index": f"E-{dropped}", + "canonical_ordinal": canonical, + "canonical_evidence_index": f"E-{canonical}", + "reason": f"exact {kind} match", + }) + suspects.append({ + "exception_id": f"B1-SUS-{len(suspects) + 1:03d}", + "source_gate": "B1", + "rule_id": "R-ALIAS", + "check_id": "duplicate_document_alias", + "ordinals": ords, + "evidence_indexes": [f"E-{o}" for o in ords], + "candidate_ids": [], + "payload": {"confirmed_exact": True, "match_kind": kind, "canonical_ordinal": canonical}, + "excerpt": "", + "escalation_flag": False, + }) + # near-duplicate suspect pairs (not dropped) + seen_pairs: set[tuple[str, str]] = set() + ords_list = [o for o in expected_ordinals if o not in dropped_set] + for i, oa in enumerate(ords_list): + for ob in ords_list[i + 1:]: + a, b = parts[oa], parts[ob] + ta, tb = str(a.get("title_normalized") or ""), str(b.get("title_normalized") or "") + joint_identical = ( + ta == tb + and a.get("key_dates") == b.get("key_dates") + and a.get("key_amounts") == b.get("key_amounts") + and a.get("key_parties") == b.get("key_parties") + ) + near_title = bool(ta and tb and norm_title(ta) == norm_title(tb) and not joint_identical) + triple = ( + nonempty(a.get("key_dates")) and a.get("key_dates") == b.get("key_dates") + and nonempty(a.get("key_amounts")) and a.get("key_amounts") == b.get("key_amounts") + and nonempty(a.get("key_parties")) and a.get("key_parties") == b.get("key_parties") + and ta != tb + ) + if (near_title or triple) and (oa, ob) not in seen_pairs: + seen_pairs.add((oa, ob)) + suspects.append({ + "exception_id": f"B1-SUS-{len(suspects) + 1:03d}", + "source_gate": "B1", + "rule_id": "R-ALIAS", + "check_id": "duplicate_document_alias", + "ordinals": [oa, ob], + "evidence_indexes": [f"E-{oa}", f"E-{ob}"], + "candidate_ids": [], + "payload": { + "confirmed_exact": False, + "titles": [ta, tb], + "key_dates": [a.get("key_dates"), b.get("key_dates")], + "key_amounts": [a.get("key_amounts"), b.get("key_amounts")], + "key_parties": [a.get("key_parties"), b.get("key_parties")], + }, + "excerpt": "", + "escalation_flag": False, + }) + + # -------- CHECK 4: document component coverage (deterministic rule table -> suspects) -------- + missing_components: list[dict[str, Any]] = [] + for o in ords_list: + item = parts[o] + fl = flags_of(item) + for rule_id, token, fields in COVERAGE_RULES: + if any(token in f for f in fl) and not any(nonempty(item.get(f)) for f in fields): + missing_components.append({"ordinal": o, "rule": rule_id, "fields": list(fields)}) + suspects.append({ + "exception_id": f"B1-SUS-{len(suspects) + 1:03d}", + "source_gate": "B1", + "rule_id": "R-COVERAGE", + "check_id": "document_component_coverage", + "ordinals": [o], + "evidence_indexes": [f"E-{o}"], + "candidate_ids": [], + "payload": { + "coverage_rule": rule_id, + "doc_type": item.get("doc_type"), + "doc_semantic_flags": fl, + "empty_fields": list(fields), + "key_facts": item.get("key_facts"), + }, + "excerpt": "", + "escalation_flag": False, + }) + notice_flag = any("notice" in f for f in fl) + ncomps = item.get("notice_components") or [] + if notice_flag and (not ncomps or all( + not nonempty(n.get("dispatch_date")) and not nonempty(n.get("arrival_date")) + for n in ncomps if isinstance(n, dict) + )): + missing_components.append({"ordinal": o, "rule": "notice_dates", "fields": ["notice_components"]}) + suspects.append({ + "exception_id": f"B1-SUS-{len(suspects) + 1:03d}", + "source_gate": "B1", + "rule_id": "R-COVERAGE", + "check_id": "document_component_coverage", + "ordinals": [o], + "evidence_indexes": [f"E-{o}"], + "candidate_ids": [], + "payload": {"coverage_rule": "notice_dates", "notice_components": ncomps, "key_dates": item.get("key_dates")}, + "excerpt": "", + "escalation_flag": False, + }) + loans = [ln for ln in (item.get("loan_components") or []) if isinstance(ln, dict)] + if loans and all( + not nonempty(ln.get("agreed_interest_rate")) and not nonempty(ln.get("agreed_delay_damage_rate")) + for ln in loans + ): + missing_components.append({"ordinal": o, "rule": "loan_rates", "fields": ["loan_components"]}) + suspects.append({ + "exception_id": f"B1-SUS-{len(suspects) + 1:03d}", + "source_gate": "B1", + "rule_id": "R-COVERAGE", + "check_id": "document_component_coverage", + "ordinals": [o], + "evidence_indexes": [f"E-{o}"], + "candidate_ids": [], + "payload": {"coverage_rule": "loan_rates", "loan_components": loans}, + "excerpt": "", + "escalation_flag": False, + }) + + # -------- CHECK 5: authority drift (demoted to suspicion-signal spot-check) -------- + review_candidates: list[dict[str, Any]] = [] + drift_signal_ordinals: list[dict[str, Any]] = [] + for o in ords_list: + item = parts[o] + fl = flags_of(item) + signals: list[str] = [] + if not nonempty(item.get("key_dates")) and not nonempty(item.get("key_amounts")) and any( + t in f for f in fl for t in ("registry", "claim", "notice") + ): + signals.append("empty_key_dates_and_amounts_for_core_doc_type") + sp = item.get("source_pointer") + if isinstance(sp, dict) and sp.get("ordinal") is not None: + try: + if int(sp["ordinal"]) != int(o): + signals.append("source_pointer_ordinal_mismatch") + except Exception: + signals.append("source_pointer_ordinal_unparsable") + mm = item.get("mapper_meta") or {} + for k, v in (mm.items() if isinstance(mm, dict) else []): + if WARNING_KEY_RE.search(str(k)) and nonempty(v): + review_candidates.append({"ordinal": o, "evidence_index": f"E-{o}", "field": str(k), "value": v}) + if DRIFT_TEXT_RE.search(json.dumps(v, ensure_ascii=False)): + signals.append(f"mapper_self_warning:{k}") + if signals: + excerpt = "" + shard_path = shard_paths.get(o) or "" + if shard_path: + try: + excerpt = read_raw(shard_path)[:EXCERPT_LIMIT] + except Exception: + excerpt = "" + drift_signal_ordinals.append({"ordinal": o, "signals": signals}) + suspects.append({ + "exception_id": f"B1-SUS-{len(suspects) + 1:03d}", + "source_gate": "B1", + "rule_id": "R-DRIFT", + "check_id": "authority_drift_detection", + "ordinals": [o], + "evidence_indexes": [f"E-{o}"], + "candidate_ids": [], + "payload": { + "signals": signals, + "source_pointer": sp, + "key_dates": item.get("key_dates"), + "key_amounts": item.get("key_amounts"), + "doc_semantic_flags": fl, + }, + "excerpt": excerpt, + "escalation_flag": "source_pointer_ordinal_mismatch" in signals, + }) + + # -------- finalize & merge (conservative pass-through; v.2 CONSOLIDATION_RULES) -------- + final_items: list[dict[str, Any]] = [] + for o in expected_ordinals: + if o in dropped_set: + continue + item = parts[o] + out: dict[str, Any] = {} + placed = False + for k, v in item.items(): + if k == "evidence_index_proposed": + out["evidence_index"] = f"E-{o}" + placed = True + elif k == "mapper_meta": + continue + else: + out[k] = v + if not placed: + out = {"evidence_index": f"E-{o}", **out} + final_items.append(out) + + envelope = {"schema_contract_version": "evidence_indexed.v3", "items": final_items} + # STEP 0-3 확정: 하류(Part 2/3/4)는 items 키를 우선 소비 — 중복 alias 배열은 두지 않는다 (Q-3 해소). + + # -------- conservation invariants (pre-write) -------- + conservation_errors: list[str] = [] + if len(final_items) != shard_count - len(dropped_set): + conservation_errors.append(f"item count {len(final_items)} != shard_count {shard_count} - dropped {len(dropped_set)}") + final_ids = [it["evidence_index"] for it in final_items] + want_ids = [i for i in expected_ids if i not in {f"E-{o}" for o in dropped_set}] + if set(final_ids) != set(want_ids) or len(final_ids) != len(set(final_ids)): + conservation_errors.append("final evidence_index set mismatch vs planner expected ids minus dropped aliases") + if sorted(final_ids) != final_ids: + conservation_errors.append("final items not sorted by ordinal") + if conservation_errors: + print(json.dumps({"status": "FAILED", "gate": "B1", + "message": "B1 quality gate BLOCK 발생: downstream 진행 금지", + "conservation_errors": conservation_errors}, ensure_ascii=False)) + raise RuntimeError("B1 conservation invariant violated; final file not written") + + content = json.dumps(envelope, ensure_ascii=False, indent=2) + # post-serialization verification (write 전·후 동일 검증 - 자기 파일 재읽기 없이 문자열 기준) + verify = json.loads(content) + if len(verify["items"]) != len(final_items) or [i["evidence_index"] for i in verify["items"]] != final_ids: + raise RuntimeError("post-serialization verification failed for evidence_indexed.json") + write_doc(FINAL_PATH, content) + + manifest = { + "schema_version": "finalized_evidence_id_manifest.v1", + "source_task": "Task_B1_quality_gate_evidence_indexed", + "shard_count": shard_count, + "finalized_evidence_indexes": final_ids, + "dropped_aliases": dropped_aliases, + } + write_doc(MANIFEST_PATH, json.dumps(manifest, ensure_ascii=False, indent=2)) + + block = bool(hard_findings) + completeness_sev = "BLOCK" if (stale_parts or extra_parts) else "PASS" + determinism_sev = "BLOCK" if determinism_violations else "PASS" + precheck = { + "schema_version": "B1_precheck.v1", + "gate_id": "B1_evidence_indexed", + "planner_source": PLAN_PATH, + "planner_shard_count": shard_count, + "expected_ordinals": expected_ordinals, + "received_ordinals": expected_ordinals, + "input_parts_count": len(expected_ordinals), + "extra_part_detection": extra_detection, + "deterministic_checks": [ + {"check_id": "shard_completeness", "severity": completeness_sev, + "detail": "expected/received/stale set 비교 (결정적)", "missing_ordinals": missing_ordinals}, + {"check_id": "evidence_index_determinism", "severity": determinism_sev, + "detail": "E-{ordinal:03d} 형식·ordinal 일치 검사 (결정적)", "violations": determinism_violations}, + {"check_id": "duplicate_document_alias", "severity": "PASS", + "detail": "exact 중복은 canonical 정리, 유사쌍은 GB 판정 대기", "duplicate_groups": duplicate_groups, + "canonical_choices": canonical_choices}, + {"check_id": "document_component_coverage", "severity": "PASS", + "detail": "rule table 위반은 GB 판정 대기", "missing_components": missing_components}, + {"check_id": "authority_drift_detection", "severity": "PASS", + "detail": "상시 전역 검사 삭제, 의심신호 spot-check", "drift_findings": drift_signal_ordinals}, + ], + "hard_gate_findings": hard_findings, + "review_candidates": review_candidates, + "suspects": suspects, + "dropped_aliases": dropped_aliases, + "conservation": {"input_parts": len(expected_ordinals), "final_items": len(final_items), "dropped": len(dropped_set)}, + "block": block, + } + write_doc(PRECHECK_PATH, json.dumps(precheck, ensure_ascii=False, indent=2)) + + overall = "BLOCK" if block else "PASS" + # SKILL.md §3.3: stdout = task output — 단일 JSON 라인. 상태 메시지는 message 필드에 보존. + message = ("B1 quality gate BLOCK 발생: downstream 진행 금지" if block + else f"증거문서 authority catalog finalize 완료 (severity={overall})") + print(json.dumps({ + "status": "B1_DETERMINISTIC_GATE_DONE", + "message": message, + "overall_severity_provisional": overall, + "pending_adjudication": "PENDING_ADJUDICATION" if suspects else "NO_SUSPECTS", + "final_items": len(final_items), + "dropped_aliases": len(dropped_aliases), + "suspect_count": len(suspects), + "review_candidate_count": len(review_candidates), + }, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B2_quality_gate_event_candidates + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 240 + code: | + #!/usr/bin/env python3 + # Task_B2_quality_gate_event_candidates (v3) - deterministic B2 gate reducer (GA-2) + # Spec: Part_1_Improvement_Strategy_Claude_v1.md §5 + from __future__ import annotations + import itertools + import json + import re + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + + PLAN_PATH = "evidence_shard_plan.json" + PART_DIR = "evidence_event_candidate_parts" + FINAL_PATH = "evidence_event_candidates.json" + MANIFEST_PATH = "stage1_tmp/quality_gate/finalized_evidence_id_manifest.json" + B1_PRECHECK_PATH = "stage1_tmp/quality_gate/B1_precheck.json" + PRECHECK_PATH = "stage1_tmp/quality_gate/B2_precheck.json" + PACK_PATH = "stage1_tmp/quality_gate/B12_exception_pack.json" + EXCERPT_LIMIT = 1500 + MAX_ITEMS_PER_EXCEPTION = 2 + MAX_CANDIDATES_PER_EXCEPTION = 8 + CANDIDATE_ID_RE = re.compile(r"^EVT-(\d{3})-(\d{2})$") + DATE_RE = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-b2-quality-gate-event-candidates", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _mid(), + "method": "tools/call", + "params": {"name": name, "arguments": args}, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: Any, name: str) -> str: + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError(f"Empty: {name}") + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, dict) and "results" in outer: + result0 = (outer.get("results") or [{}])[0] + inner = result0.get("content") or result0.get("text") or "" + if not inner: + raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer if isinstance(outer, str) else json.dumps(outer, ensure_ascii=False) + + def read_raw(path: str) -> str: + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str) or not raw.strip(): + raise RuntimeError(f"Missing or empty required input: {path}") + return raw + + def parse_json(raw: str, path: str) -> Any: + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + try: + obj, _ = decoder.raw_decode(raw.strip()) + return obj + except Exception as exc: + raise RuntimeError(f"Invalid JSON input: {path}") from exc + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def clip(value: Any, limit: int = 200) -> str: + text = " ".join(str(value or "").split()) + if len(text) <= limit: + return text + return text[:limit].rstrip() + "..." + + def list_part_names(folder: str) -> list[str] | None: + for args in ({"path": folder, "pattern": "E-*.json"}, {}, {"doc_names": []}): + try: + payload = _unwrap(_call("list_docs", args), "list_docs") + except Exception: + continue + if isinstance(payload, str): + try: + payload = json.loads(payload) + except Exception: + return re.findall(r"[\w./-]+\.json", payload) + vals: list[str] = [] + if isinstance(payload, dict): + for value in payload.values(): + if isinstance(value, list): + for item in value: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + elif isinstance(value, str): + vals.append(value) + elif isinstance(payload, list): + for item in payload: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + return [v for v in vals if v] + return None + + def get_single_item(part: Any, path: str) -> dict[str, Any]: + if isinstance(part, dict) and isinstance(part.get("items"), list): + items = part["items"] + if len(items) != 1 or not isinstance(items[0], dict): + raise RuntimeError(f"part must contain exactly one item object: {path}") + return items[0] + if isinstance(part, dict): + return part + raise RuntimeError(f"part top-level is not an object: {path}") + + def nonempty(value: Any) -> bool: + return value not in (None, "", [], {}) + + def flags_of(item: dict[str, Any]) -> list[str]: + return [str(f) for f in (item.get("doc_semantic_flags") or []) if f] + + def parse_date(value: Any) -> str | None: + m = DATE_RE.match(str(value or "").strip()) + return f"{m.group(1)}-{m.group(2)}-{m.group(3)}" if m else None + + def confidence_rank(value: Any) -> int: + order = {"high": 0, "medium": 1, "mid": 1, "low": 2} + return order.get(str(value or "").lower(), 3) + + def canonical_rank(cand: dict[str, Any], ordinal: str) -> tuple[int, int, int, int]: + # v.2 CHECK_3 canonical priority: (i) raw event > derived legal-effect event, + # (ii) higher confidence, (iii) more support_locators, (iv) earlier ordinal + kind_text = " ".join(str(cand.get(k) or "") for k in ("event_kind", "event_or_state", "action_type_candidate")).lower() + derived = 1 if "derived" in kind_text or "legal_effect" in kind_text else 0 + return (derived, confidence_rank(cand.get("confidence")), -len(cand.get("support_locators") or []), int(ordinal)) + + # v.2 CHECK_4 chronological chain rule table: (earlier subkind, later subkind, allow_equal) + CHRONO_RULES = ( + ("underlying_claim_arising", "first_demand", False), + ("first_demand", "demand_notice_dispatch", False), + ("demand_notice_dispatch", "demand_notice_arrival", True), + ("demand_notice_arrival", "debt_acknowledgement", False), + ("lien_extinction_notice_dispatch", "lien_extinction_notice_receipt", True), + ("lien_extinction_notice_receipt", "lien_extinction_effect_candidate", False), + ("secured_debt_creation", "mortgage_setting_by_fraction", False), + ("mortgage_setting_by_fraction", "auction_application_after_extinguishment", False), + ("auction_application_after_extinguishment", "auction_sale_and_registry_transfer", False), + ("preserved_claim_arising", "fraudulent_transfer", False), + ("fraudulent_transfer", "encumbrance_release_after_act", False), + ) + + def is_defense_relay(cand: dict[str, Any]) -> bool: + # 7/10 실측에서 확인된 mapper 항변 신호의 결정적 재현 규칙 (R-SELFWARN) + if nonempty(cand.get("defense_candidate")): + return True + if "defense_assertion" in str(cand.get("event_subkind") or ""): + return True + lec = cand.get("legal_effect_candidate") + if isinstance(lec, dict) and str(lec.get("effect_type") or "") == "defense_assertion": + return True + return False + + def main() -> None: + _init() + plan = parse_json(read_raw(PLAN_PATH), PLAN_PATH) + if not isinstance(plan, dict): + raise RuntimeError("evidence_shard_plan.json is not an object") + if plan.get("planner_status") not in (None, "READY"): + print(json.dumps({"status": "FAILED", "gate": "B2", + "message": "B2 quality gate BLOCK 발생: downstream 진행 금지", + "reason": f"planner_status is not READY: {plan.get('planner_status')}"}, ensure_ascii=False)) + raise RuntimeError(f"planner_status is not READY: {plan.get('planner_status')}") + + expected_ordinals = [str(o) for o in (plan.get("expected_ordinals") or [])] + if not expected_ordinals: + raise RuntimeError("plan.expected_ordinals is empty") + expected_ids = [str(x) for x in (plan.get("expected_evidence_ids") or [f"E-{o}" for o in expected_ordinals])] + shard_count = int(plan.get("shard_count") or len(expected_ordinals)) + stale_policy = (plan.get("stale_part_policy") or {}).get("detected_stale_parts") or {} + if isinstance(stale_policy, dict): + stale_parts = [str(x) for x in (stale_policy.get(PART_DIR) or [])] + else: + stale_parts = [str(x) for x in (stale_policy or [])] + + manifest = parse_json(read_raw(MANIFEST_PATH), MANIFEST_PATH) + finalized_ids = {str(x) for x in (manifest.get("finalized_evidence_indexes") or [])} + if not finalized_ids: + raise RuntimeError("finalized_evidence_id_manifest.json has no finalized ids (B1 final absent)") + b1_dropped_ordinals = {str(d.get("dropped_ordinal")) for d in (manifest.get("dropped_aliases") or []) if isinstance(d, dict)} + + b1_precheck = parse_json(read_raw(B1_PRECHECK_PATH), B1_PRECHECK_PATH) + b1_suspects = list(b1_precheck.get("suspects") or []) + b1_review_candidates = list(b1_precheck.get("review_candidates") or []) + + hard_findings: list[dict[str, Any]] = [] + + def hard(issue_type: str, detail: str, **extra: Any) -> None: + hard_findings.append({"issue_type": issue_type, "detail": clip(detail), **extra}) + + # -------- schema invariant gate (v.2 PART_ITEM_SCHEMA_INVARIANT_GATE, deterministic) -------- + schema_invariant_findings: list[dict[str, Any]] = [] + + def invariant_block(ordinal: str, reason: str) -> None: + schema_invariant_findings.append({ + "ordinal": ordinal, + "part_path": f"{PART_DIR}/E-{ordinal}.json", + "reason": clip(reason), + "severity": "BLOCK", + "rerun_target_task": f"Task_B2_map_events_{ordinal}", + }) + + if stale_parts: + hard("stale_part", f"stale parts detected by planner policy: {stale_parts}") + missing_ordinals: list[str] = [] + parse_failures: list[dict[str, str]] = [] + parts: dict[str, dict[str, Any]] = {} + for o in expected_ordinals: + path = f"{PART_DIR}/E-{o}.json" + try: + raw = read_raw(path) + except Exception: + missing_ordinals.append(o) + continue + try: + parts[o] = get_single_item(parse_json(raw, path), path) + except Exception as exc: + parse_failures.append({"ordinal": o, "reason": clip(exc)}) + if missing_ordinals or parse_failures: + # SKILL.md §3.3: stdout = task output — 단일 JSON 라인만 출력한다 + print(json.dumps({ + "status": "FAILED", + "gate": "B2", + "message": "B2 quality gate BLOCK 발생: downstream 진행 금지", + "missing_ordinals": missing_ordinals, + "parse_failures": parse_failures, + }, ensure_ascii=False)) + raise RuntimeError("B2 input parts missing or unparsable; final files not written") + extra_parts: list[str] = [] + listed = list_part_names(PART_DIR) + extra_detection = "done" if listed is not None else "unavailable" + if listed is not None: + pat = re.compile(r"(?:^|/)E-(\d{3})\.json$") + expected_set = set(expected_ordinals) + for name in listed: + m = pat.search(str(name)) + if m and m.group(1) not in expected_set: + extra_parts.append(str(name)) + if extra_parts: + hard("unexpected_ordinal", f"unexpected part files outside planner plan: {sorted(set(extra_parts))}") + + for o in expected_ordinals: + item = parts[o] + proposed = str(item.get("evidence_index_proposed") or item.get("evidence_index") or "") + if proposed != f"E-{o}" or proposed not in set(expected_ids): + invariant_block(o, f"evidence_index_proposed mismatch: {proposed}") + candidates = item.get("event_candidates") + zero_reason = item.get("zero_event_reason") + has_candidates = isinstance(candidates, list) and len(candidates) > 0 + if not has_candidates and not nonempty(zero_reason): + invariant_block(o, "event_candidates empty without non-null zero_event_reason") + if has_candidates: + for c in candidates: + if not isinstance(c, dict): + invariant_block(o, "event candidate is not an object") + continue + for req in ("candidate_id_proposed", "source_evidence_index_proposed", "identity_signature"): + if not nonempty(c.get(req)): + invariant_block(o, f"candidate missing required field: {req}") + + # -------- CHECK 2: evidence reference integrity (manifest set membership) -------- + reference_violations: list[dict[str, str]] = [] + for o in expected_ordinals: + for c in parts[o].get("event_candidates") or []: + if not isinstance(c, dict): + continue + ref = str(c.get("source_evidence_index_proposed") or "") + if ref not in finalized_ids: + reference_violations.append({"ordinal": o, "candidate_id": str(c.get("candidate_id_proposed")), "ref": ref}) + if reference_violations: + hard("invalid_evidence_reference", f"candidate references outside finalized evidence id set: {reference_violations}") + + # -------- CHECK 3: candidate id format/uniqueness + exact identity_signature dedup -------- + format_violations: list[dict[str, str]] = [] + for o in expected_ordinals: + seen_seq: set[str] = set() + for c in parts[o].get("event_candidates") or []: + if not isinstance(c, dict): + continue + cid = str(c.get("candidate_id_proposed") or "") + m = CANDIDATE_ID_RE.match(cid) + if not m or m.group(1) != o: + format_violations.append({"ordinal": o, "candidate_id": cid, "reason": "format or ordinal mismatch"}) + elif cid in seen_seq: + format_violations.append({"ordinal": o, "candidate_id": cid, "reason": "duplicate seq in ordinal"}) + else: + seen_seq.add(cid) + if format_violations: + hard("candidate_id_format_violation", f"candidate_id_proposed violations: {format_violations}") + + by_signature: dict[str, list[tuple[str, dict[str, Any]]]] = {} + for o in expected_ordinals: + for c in parts[o].get("event_candidates") or []: + if isinstance(c, dict) and nonempty(c.get("identity_signature")): + by_signature.setdefault(str(c["identity_signature"]), []).append((o, c)) + dedup_findings: list[dict[str, Any]] = [] + dropped_candidate_keys: set[tuple[str, str]] = set() + for sig, entries in sorted(by_signature.items()): + cross = {o for o, _ in entries} + if len(entries) < 2 or len(cross) < 2: + continue # cross-document duplicates only (v.2 CHECK_3) + ranked = sorted(entries, key=lambda oc: canonical_rank(oc[1], oc[0])) + keep_o, keep_c = ranked[0] + dropped = [] + for o, c in ranked[1:]: + dropped_candidate_keys.add((o, str(c.get("candidate_id_proposed")))) + dropped.append({ + "ordinal": o, + "candidate_id": str(c.get("candidate_id_proposed")), + "source_evidence_index": str(c.get("source_evidence_index_proposed")), + "dropped_candidate": c, + }) + dedup_findings.append({ + "identity_signature": sig, + "kept_ordinal": keep_o, + "kept_candidate_id": str(keep_c.get("candidate_id_proposed")), + "dropped": dropped, + }) + + # -------- suspects (semantic exceptions only; GB adjudicates) -------- + suspects: list[dict[str, Any]] = [] + + def add_suspect(rule_id: str, check_id: str, ordinals: list[str], candidate_ids: list[str], + payload: dict[str, Any], escalation: bool = False, excerpt: str = "") -> None: + suspects.append({ + "exception_id": f"B2-SUS-{len(suspects) + 1:03d}", + "source_gate": "B2", + "rule_id": rule_id, + "check_id": check_id, + "ordinals": sorted(set(ordinals)), + "evidence_indexes": [f"E-{o}" for o in sorted(set(ordinals))], + "candidate_ids": candidate_ids[:MAX_CANDIDATES_PER_EXCEPTION], + "payload": payload, + "excerpt": excerpt[:EXCERPT_LIMIT], + "escalation_flag": escalation, + }) + + # CHECK 4: chronological ordering — deterministic date-reversal scan on explicit chain rules + subkind_dates: dict[str, list[tuple[str, str, str | None]]] = {} + for o in expected_ordinals: + for c in parts[o].get("event_candidates") or []: + if not isinstance(c, dict): + continue + sk = str(c.get("event_subkind") or "") + if sk: + subkind_dates.setdefault(sk, []).append((o, str(c.get("candidate_id_proposed")), parse_date(c.get("event_date")))) + ordering_suspect_pairs: list[dict[str, Any]] = [] + null_date_chain_refs: list[dict[str, Any]] = [] + for earlier, later, allow_equal in CHRONO_RULES: + for oe, ce, de in subkind_dates.get(earlier, []): + for ol, cl, dl in subkind_dates.get(later, []): + if de is None or dl is None: + continue + if (de > dl) or (de == dl and not allow_equal and earlier != later and False): + if de > dl: + ordering_suspect_pairs.append({ + "rule": f"{earlier} < {later}" + ("=" if allow_equal else ""), + "earlier": {"ordinal": oe, "candidate_id": ce, "event_date": de}, + "later": {"ordinal": ol, "candidate_id": cl, "event_date": dl}, + }) + for sk in (earlier, later): + for o, cid, d in subkind_dates.get(sk, []): + if d is None: + null_date_chain_refs.append({"ordinal": o, "candidate_id": cid, "event_subkind": sk}) + if ordering_suspect_pairs: + pairs = ordering_suspect_pairs[:MAX_CANDIDATES_PER_EXCEPTION] + add_suspect("R-CHRONO", "chronological_ordering", + [p["earlier"]["ordinal"] for p in pairs] + [p["later"]["ordinal"] for p in pairs], + [p["earlier"]["candidate_id"] for p in pairs] + [p["later"]["candidate_id"] for p in pairs], + {"reversal_pairs": pairs, "total_pairs": len(ordering_suspect_pairs)}, escalation=True) + dedup_null_refs = [] + seen_null = set() + for r in null_date_chain_refs: + key = (r["ordinal"], r["candidate_id"]) + if key not in seen_null: + seen_null.add(key) + dedup_null_refs.append(r) + if dedup_null_refs: + add_suspect("R-CHRONO", "chronological_ordering", + [r["ordinal"] for r in dedup_null_refs][:MAX_CANDIDATES_PER_EXCEPTION], + [r["candidate_id"] for r in dedup_null_refs][:MAX_CANDIDATES_PER_EXCEPTION], + {"null_or_unparsable_dates_in_chain": dedup_null_refs[:MAX_CANDIDATES_PER_EXCEPTION], + "total": len(dedup_null_refs)}, escalation=False) + + # CHECK 5: legal-effect event coverage — REQUIRED_EVENT_SUBKINDS rule table + all_subkinds: set[str] = set(subkind_dates.keys()) + global_text = json.dumps({o: parts[o] for o in expected_ordinals}, ensure_ascii=False) + + def part_subkinds(o: str) -> set[str]: + return {str(c.get("event_subkind") or "") for c in parts[o].get("event_candidates") or [] if isinstance(c, dict)} + + for o in expected_ordinals: + fl = flags_of(parts[o]) + sks = part_subkinds(o) + if any("business_transfer" in f for f in fl) and "commercial_successor_liability_candidate" not in sks: + add_suspect("R-EVTCOV", "legal_effect_event_coverage", [o], + [str(c.get("candidate_id_proposed")) for c in (parts[o].get("event_candidates") or [])[:MAX_CANDIDATES_PER_EXCEPTION] if isinstance(c, dict)], + {"coverage_rule": "business_transfer_without_successor_liability", "doc_semantic_flags": fl}) + if any("claim" in f for f in fl) and not sks & {"debt_acknowledgement", "new_due_date", "demand_notice_dispatch", "demand_notice_arrival"}: + kw = json.dumps(parts[o], ensure_ascii=False) + if any(t in kw for t in ("변제기", "최고", "채무승인", "이행각서")): + add_suspect("R-EVTCOV", "legal_effect_event_coverage", [o], + [str(c.get("candidate_id_proposed")) for c in (parts[o].get("event_candidates") or [])[:MAX_CANDIDATES_PER_EXCEPTION] if isinstance(c, dict)], + {"coverage_rule": "claim_doc_without_demand_or_acknowledgement_subkinds", "doc_semantic_flags": fl}) + if ({"encumbrance_release_after_act", "mortgage_extinguishment_by_fraction"} & all_subkinds) and "value_compensation_exception_trigger" not in all_subkinds: + add_suspect("R-EVTCOV", "legal_effect_event_coverage", + sorted({o for sk in ("encumbrance_release_after_act", "mortgage_extinguishment_by_fraction") for o, _, _ in subkind_dates.get(sk, [])}), + [], {"coverage_rule": "post_act_release_without_value_compensation_exception_trigger"}) + if "auction_sale_and_registry_transfer" in all_subkinds and not {"auction_application_after_extinguishment", "mortgage_extinguishment_by_fraction"} <= all_subkinds: + add_suspect("R-EVTCOV", "legal_effect_event_coverage", + sorted({o for o, _, _ in subkind_dates.get("auction_sale_and_registry_transfer", [])}), + [], {"coverage_rule": "auction_chain_incomplete", + "missing": sorted({"auction_application_after_extinguishment", "mortgage_extinguishment_by_fraction"} - all_subkinds)}) + if "미등기" in global_text and not {"unregistered_building_original_acquisition", "de_facto_disposition_holder_acquisition"} & all_subkinds: + add_suspect("R-EVTCOV", "legal_effect_event_coverage", [], [], + {"coverage_rule": "unregistered_building_cue_without_acquisition_subkinds"}) + if "lien_extinction_notice_dispatch" in all_subkinds and "lien_extinction_notice_receipt" not in all_subkinds: + add_suspect("R-EVTCOV", "legal_effect_event_coverage", + sorted({o for o, _, _ in subkind_dates.get("lien_extinction_notice_dispatch", [])}), + [], {"coverage_rule": "lien_notice_dispatch_without_receipt"}) + succession_flag_ords = [o for o in expected_ordinals if any("succession" in f for f in flags_of(parts[o]))] + if succession_flag_ords and not {"succession_renunciation_acceptance", "spouse_sole_succession_effect_candidate"} & all_subkinds: + add_suspect("R-EVTCOV", "legal_effect_event_coverage", succession_flag_ords[:MAX_ITEMS_PER_EXCEPTION], [], + {"coverage_rule": "succession_doc_without_succession_subkinds"}) + if ({"fraudulent_transfer"} <= all_subkinds and "encumbrance_release_after_act" in all_subkinds + and "value_compensation_cap_calculation_candidate" not in all_subkinds): + add_suspect("R-EVTCOV", "legal_effect_event_coverage", [], [], + {"coverage_rule": "fraudulent_transfer_with_release_but_no_cap_calculation"}) + fraction_refs = [] + for o in expected_ordinals: + for c in parts[o].get("event_candidates") or []: + if isinstance(c, dict) and str(c.get("event_subkind") or "") in ("mortgage_setting_by_fraction", "mortgage_extinguishment_by_fraction") and not nonempty(c.get("fraction_ref")): + fraction_refs.append({"ordinal": o, "candidate_id": str(c.get("candidate_id_proposed"))}) + if fraction_refs: + add_suspect("R-EVTCOV", "legal_effect_event_coverage", + [r["ordinal"] for r in fraction_refs][:MAX_CANDIDATES_PER_EXCEPTION], + [r["candidate_id"] for r in fraction_refs][:MAX_CANDIDATES_PER_EXCEPTION], + {"coverage_rule": "fraction_subkind_with_null_fraction_ref", "refs": fraction_refs[:MAX_CANDIDATES_PER_EXCEPTION]}) + + # R-SELFWARN: mapper 항변/재반박 신호의 승격 판정 대상 (7/10 review finding 재현 경로) + defense_refs: list[dict[str, str]] = [] + for o in expected_ordinals: + for c in parts[o].get("event_candidates") or []: + if isinstance(c, dict) and is_defense_relay(c): + defense_refs.append({"ordinal": o, "candidate_id": str(c.get("candidate_id_proposed")), + "event_subkind": str(c.get("event_subkind") or ""), + "summary": clip(c.get("action_summary"), 120)}) + if defense_refs: + add_suspect("R-SELFWARN", "review_findings_in_mapper", + [r["ordinal"] for r in defense_refs], + [r["candidate_id"] for r in defense_refs], + {"relay_kind": "defense_assertion_requires_stage2_review", "refs": defense_refs}) + + # -------- finalize & merge (dict-copy pass-through; Q-1/Q-2 구조적 해소) -------- + block_ordinals = {f["ordinal"] for f in schema_invariant_findings} + final_items: list[dict[str, Any]] = [] + dropped_by_b1_alias: list[str] = [] + input_candidate_total = 0 + final_candidate_total = 0 + field_count_errors: list[str] = [] + for o in expected_ordinals: + item = parts[o] + input_candidate_total += len(item.get("event_candidates") or []) + if o in b1_dropped_ordinals: + dropped_by_b1_alias.append(o) + continue + if o in block_ordinals: + # schema invariant BLOCK part는 정상 item으로 승격하지 않는다 (명시적 BLOCK finding 대응) + continue + out: dict[str, Any] = {} + placed = False + for k, v in item.items(): + if k == "evidence_index_proposed": + out["evidence_index"] = f"E-{o}" + placed = True + elif k == "mapper_meta": + continue + elif k == "event_candidates": + new_cands = [] + for c in v or []: + if not isinstance(c, dict): + continue + if (o, str(c.get("candidate_id_proposed"))) in dropped_candidate_keys: + continue + nc: dict[str, Any] = {} + for ck, cv in c.items(): + if ck == "candidate_id_proposed": + nc["candidate_id"] = str(cv) + elif ck == "source_evidence_index_proposed": + nc["source_evidence_index"] = str(cv) + else: + nc[ck] = cv + if len(nc) != len(c): + field_count_errors.append(f"{o}:{c.get('candidate_id_proposed')} field count {len(nc)} != {len(c)}") + new_cands.append(nc) + final_candidate_total += 1 + out["event_candidates"] = new_cands + else: + out[k] = v + if not placed: + out = {"evidence_index": f"E-{o}", **out} + final_items.append(out) + + envelope = {"schema_version": "evidence_event_candidates.v1", "items": final_items} + + # -------- conservation invariants (write 전) -------- + dropped_cand_count = sum(len(d["dropped"]) for d in dedup_findings) + conservation_errors: list[str] = list(field_count_errors) + expected_item_count = shard_count - len(b1_dropped_ordinals) - len(block_ordinals) + if len(final_items) != expected_item_count: + conservation_errors.append(f"item count {len(final_items)} != shard_count {shard_count} - b1_dropped {len(b1_dropped_ordinals)} - schema_block {len(block_ordinals)}") + want_ids = {i for i in expected_ids if i not in {f"E-{o}" for o in b1_dropped_ordinals} and i not in {f"E-{o}" for o in block_ordinals}} + got_ids = [it["evidence_index"] for it in final_items] + if set(got_ids) != want_ids or len(got_ids) != len(set(got_ids)): + conservation_errors.append("final evidence_index set mismatch vs planner expected ids minus explicit drops") + blocked_cand_count = sum(len(parts[o].get("event_candidates") or []) for o in block_ordinals) + b1_dropped_cand_count = sum(len(parts[o].get("event_candidates") or []) for o in b1_dropped_ordinals) + if input_candidate_total != final_candidate_total + dropped_cand_count + blocked_cand_count + b1_dropped_cand_count: + conservation_errors.append( + f"candidate conservation failed: input {input_candidate_total} != final {final_candidate_total} + dedup {dropped_cand_count} + blocked {blocked_cand_count} + b1_dropped {b1_dropped_cand_count}") + if not dedup_findings and not block_ordinals and not b1_dropped_ordinals and final_candidate_total < input_candidate_total: + conservation_errors.append("candidate count decreased without any recorded dedup group (silent loss)") + if sorted(got_ids) != got_ids: + conservation_errors.append("final items not sorted by ordinal") + if conservation_errors: + print(json.dumps({"status": "FAILED", "gate": "B2", + "message": "B2 quality gate BLOCK 발생: downstream 진행 금지", + "conservation_errors": conservation_errors}, ensure_ascii=False)) + raise RuntimeError("B2 conservation invariant violated; final file not written") + + content = json.dumps(envelope, ensure_ascii=False, indent=2) + verify = json.loads(content) + if (len(verify["items"]) != len(final_items) + or [i["evidence_index"] for i in verify["items"]] != got_ids + or sum(len(i.get("event_candidates") or []) for i in verify["items"]) != final_candidate_total): + raise RuntimeError("post-serialization verification failed for evidence_event_candidates.json") + write_doc(FINAL_PATH, content) + + block = bool(hard_findings) or bool(schema_invariant_findings) + precheck = { + "schema_version": "B2_precheck.v1", + "gate_id": "B2_event_candidates", + "planner_source": PLAN_PATH, + "planner_shard_count": shard_count, + "expected_ordinals": expected_ordinals, + "received_ordinals": expected_ordinals, + "input_parts_count": len(expected_ordinals), + "extra_part_detection": extra_detection, + "schema_invariant_findings": schema_invariant_findings, + "deterministic_checks": [ + {"check_id": "event_candidate_completeness", + "severity": "BLOCK" if (stale_parts or extra_parts) else "PASS", + "detail": "expected/received/stale set 비교 (결정적)", "missing_ordinals": missing_ordinals}, + {"check_id": "evidence_reference_integrity", + "severity": "BLOCK" if reference_violations else "PASS", + "detail": "finalized evidence id manifest 집합 멤버십 검사 (결정적)", "violations": reference_violations}, + {"check_id": "candidate_id_collision", + "severity": "BLOCK" if format_violations else "PASS", + "detail": "EVT-{ordinal:03d}-{seq:02d} 형식·유일성 + exact identity_signature dedup (결정적)", + "format_violations": format_violations, "dedup_findings": dedup_findings}, + {"check_id": "chronological_ordering", "severity": "PASS", + "detail": "명시 날짜 chain rule 역전 스캔, 의심건은 GB 판정 대기", "ordering_violations": ordering_suspect_pairs}, + {"check_id": "legal_effect_event_coverage", "severity": "PASS", + "detail": "REQUIRED_EVENT_SUBKINDS rule table 검사, 의심건은 GB 판정 대기", + "missing_subkinds_by_ordinal": [s["payload"].get("coverage_rule") for s in suspects if s["rule_id"] == "R-EVTCOV"]}, + ], + "hard_gate_findings": hard_findings, + "suspects": suspects, + "dedup_findings": dedup_findings, + "dropped_by_b1_alias": dropped_by_b1_alias, + "conservation": { + "input_parts": len(expected_ordinals), + "final_items": len(final_items), + "input_candidates": input_candidate_total, + "final_candidates": final_candidate_total, + "dedup_dropped_candidates": dropped_cand_count, + }, + "block": block, + } + write_doc(PRECHECK_PATH, json.dumps(precheck, ensure_ascii=False, indent=2)) + + # -------- B12 exception pack (B1 suspects + B1 review candidates + B2 suspects) -------- + pack_exceptions: list[dict[str, Any]] = [] + overflow_items: list[dict[str, Any]] = [] + for src in itertools.chain(b1_suspects, ( + { + "exception_id": f"B1-RC-{idx:03d}", + "source_gate": "B1", + "rule_id": "R-SELFWARN", + "check_id": "review_findings_in_mapper", + "ordinals": [rc.get("ordinal")], + "evidence_indexes": [rc.get("evidence_index")], + "candidate_ids": [], + "payload": rc, + "excerpt": "", + "escalation_flag": False, + } for idx, rc in enumerate(b1_review_candidates, 1) + ), suspects): + entry = dict(src) + ev = list(entry.get("evidence_indexes") or []) + cands = list(entry.get("candidate_ids") or []) + if len(ev) > MAX_ITEMS_PER_EXCEPTION and entry.get("rule_id") != "R-SELFWARN": + overflow_items.append({"exception_id": entry.get("exception_id"), "overflow_evidence_indexes": ev[MAX_ITEMS_PER_EXCEPTION:]}) + entry["evidence_indexes"] = ev[:MAX_ITEMS_PER_EXCEPTION] + if len(cands) > MAX_CANDIDATES_PER_EXCEPTION: + overflow_items.append({"exception_id": entry.get("exception_id"), "overflow_candidate_ids": cands[MAX_CANDIDATES_PER_EXCEPTION:]}) + entry["candidate_ids"] = cands[:MAX_CANDIDATES_PER_EXCEPTION] + entry["excerpt"] = str(entry.get("excerpt") or "")[:EXCERPT_LIMIT] + pack_exceptions.append(entry) + pack = { + "schema_version": "B12_exception_pack.v1", + "exception_count": len(pack_exceptions), + "exceptions": pack_exceptions, + "overflow_items": overflow_items, + "budget": { + "max_items_per_exception": MAX_ITEMS_PER_EXCEPTION, + "max_candidates": MAX_CANDIDATES_PER_EXCEPTION, + "max_excerpt_chars": EXCERPT_LIMIT, + }, + } + write_doc(PACK_PATH, json.dumps(pack, ensure_ascii=False, indent=2)) + + overall = "BLOCK" if block else "PASS" + # SKILL.md §3.3: stdout = task output — 단일 JSON 라인. 상태 메시지는 message 필드에 보존. + message = ("B2 quality gate BLOCK 발생: downstream 진행 금지" if block + else f"증거문서 사건행위 후보 finalize 완료 (severity={overall})") + print(json.dumps({ + "status": "B2_DETERMINISTIC_GATE_DONE", + "message": message, + "overall_severity_provisional": overall, + "pending_adjudication": "PENDING_ADJUDICATION" if pack_exceptions else "NO_SUSPECTS", + "final_items": len(final_items), + "input_candidates": input_candidate_total, + "final_candidates": final_candidate_total, + "dedup_findings": len(dedup_findings), + "exception_count": len(pack_exceptions), + }, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B12_gate_llm_adjudicator + preflight_files: + - stage1_tmp/quality_gate/B12_exception_pack.json + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: low + llm_verbosity: low + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Validate only the artifact, input bundle, IDs, coverage, severity, and handoff condition assigned to the current task. + - Identify contradictions only within assigned inputs: date, amount, party, object, registration, payment, possession, notice, valuation, legal role, causal link, source identity, or required support chain. + - Distinguish contradiction, incompleteness, ambiguity, multiplicity, schema failure, writer-boundary breach, and review-only legal uncertainty. + - Preserve all material conflicting source IDs. Do not average, normalize, or repair unless the current task expressly authorizes repair. + - Treat PASS, SOFT_WARNING, HARD_WARNING, BLOCK, READY, READY_WITH_REVIEW, FAILED, rollback, and signoff statuses exactly as the current schema defines them. + - Do not downgrade severity for convenience, and do not block progression unless the current gate criteria require it. + - Validators report issues in the authorized validation, audit, gate, review, or rollback schema. They do not write final artifacts unless explicitly assigned. + + + + + Task_B12_gate_llm_adjudicator + + You are an MCP-enabled LLM-backed **exception adjudicator** assisting plaintiff-side Korean civil/commercial litigation counsel. You judge only the compact suspect items that the deterministic B1/B2 gate reducers extracted. You are not a merger, not a final writer, and not a fact creator. + + + 본 task는 v3 아키텍처의 조건부 국소 판정기(GB)다. 결정적 reducer(`Task_B1_quality_gate_evidence_indexed`, `Task_B2_quality_gate_event_candidates`)가 기계적 검사로 좁혀낸 의심 항목만 `stage1_tmp/quality_gate/B12_exception_pack.json`에서 읽어, 항목별 판정을 `stage1_tmp/quality_gate/B12_adjudication_decisions.json`에 기록한다. + 최종 파일(`evidence_indexed.json`, `evidence_event_candidates.json`)과 audit(`quality_gates/*.json`)은 본 task가 쓰지 않는다. audit 확정은 후속 결정적 task `Task_B12_gate_audit_finalizer`가, sidecar는 `Task_B2_SHA256_soft_gate_handoff_writer`가 전담한다. + 변호사 관점의 판정 원칙: 법률적 불확실성(suspected alias, component coverage 의문, chronology 모호성, 항변 사항의 Stage 2 검토 필요성)은 review 판정으로 남기고, provenance 오염·source trace 상실·final evidence identity collision처럼 downstream traceability를 실제로 깨는 경우에만 `CONFIRM_BLOCK`을 사용한다. review 판정은 사실 확정이 아니라 검토 큐다. + + + + + + - `stage1_tmp/quality_gate/B12_exception_pack.json` (유일한 입력 파일. 각 exception의 payload·excerpt가 이미 내장되어 있다) + + + - `stage1_tmp/quality_gate/B12_adjudication_decisions.json` + + + - `evidence_indexed.json`, `evidence_event_candidates.json`, `quality_gates/` 아래 어떤 파일도 작성·재작성 + - exception pack 외의 어떤 파일도 read (full planner/final/meeting/evidence 재독해 금지 — 필요한 발췌는 pack에 이미 포함됨) + - mapper part 수정 또는 새 candidate·새 사실 창작 + - pack에 없는 exception_id 창작, 또는 pack의 exception_id 중 판정 누락 + - `source_refs`를 배열로 출력 (반드시 named object: `evidence_indexes`/`event_candidate_ids`/`meeting_clause_refs`/`doc_titles`) + - count·통계·요약 필드 창작 (집계는 후속 결정적 task 소관) + + + + + 1. `read_docs`로 `stage1_tmp/quality_gate/B12_exception_pack.json` 1개 파일만 읽는다. `list_docs` 사전 확인은 이 파일 1건에 한한다. + 2. `exception_count == 0`이면: `write_file(overwrite=true)`로 `{"schema_version": "B12_adjudication_decisions.v1", "decisions": [], "no_suspects": true}`를 `stage1_tmp/quality_gate/B12_adjudication_decisions.json`에 저장하고 `"NO_SUSPECTS"`만 출력한 뒤 즉시 종료한다(terminate). 다른 어떤 파일도 읽지 않는다. + 3. `exception_count > 0`이면: 각 exception을 pack에 포함된 `payload`·`excerpt`만으로 판정한다. 추가 `read_docs` 호출은 금지된다. + 4. 각 exception에 대해 decision을 하나 결정한다: + - `DISMISS`: 기계적 신호였을 뿐 실질 문제가 없음 (예: 정상적인 rule-table 공백, 문서 성격상 자연스러운 component 부재) + - `SOFT_WARNING_REVIEW`: Stage 2 검토 큐에 남길 경미한 법률적 불확실성 + - `HARD_WARNING_REVIEW`: 청구원인·소권 판단에 영향 가능성이 있는 검토 필요 사항 + - `CONFIRM_BLOCK`: provenance 오염, source trace 상실, final evidence identity collision이 실제로 확인되는 경우만 + - `ESCALATE_TO_REVIEW`: `escalation_flag`가 true이고 pack 정보만으로 확정할 수 없는 경우 — 서로 다른 source가 같은 법률행위의 날짜·당사자·목적물을 실질적으로 충돌 표시하거나, authority drift 여부가 source trace 유효성을 좌우하거나, chronology 판단이 hard block 여부를 좌우하는 경우. 이 판정은 인간 변호사 검토 큐로 라우팅된다. + 5. `write_file(overwrite=true)`로 decisions 파일을 저장한다. 스키마: + {"schema_version": "B12_adjudication_decisions.v1", "no_suspects": false, "decisions": [ {"exception_id": "...", "decision": "DISMISS|SOFT_WARNING_REVIEW|HARD_WARNING_REVIEW|CONFIRM_BLOCK|ESCALATE_TO_REVIEW", "severity": "PASS|SOFT_WARNING|HARD_WARNING|BLOCK", "issue_type": "snake_case", "source_refs": {"evidence_indexes": [], "event_candidate_ids": [], "meeting_clause_refs": [], "doc_titles": []}, "review_instruction": "≤120자 한국어", "candidate_use_policy": "prioritize_if_supported|preserve_as_needs_review|do_not_generate_without_support", "concise_basis": "≤120자 한국어"} ] } + 6. `list_docs`로 decisions 파일 존재만 확인한다. 절대 읽지 않는다. + 7. `"B12 예외 판정 완료 (decisions=<건수>)"`만 출력하고 작업을 끝낸다(terminate). + + + + - exception pack 부재·빈 파일·파싱 불가: 즉시 중단하고 채팅으로만 보고한다. decisions 파일은 쓰지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + + - `hard`(BLOCK 상당)는 downstream 오염·출처·무결성 실패만: stale/provenance 오염, source trace 상실, final evidence identity collision. 그 외는 전부 review다. + - review 판정은 사실 확정이 아니며 자동 차단 사유가 아니다. `rule_id`별 기준: + - `R-ALIAS`: 두 문서가 실제 동일 문서의 alias인지. 동일 확정 + source trace 위험이면 CONFIRM_BLOCK, 동일 의심이면 HARD_WARNING_REVIEW, 별개 문서면 DISMISS. + - `R-COVERAGE`: 문서 유형상 필수 component가 실제로 원문에 존재할 개연성이 payload/excerpt에서 보이는데 비어 있으면 HARD_WARNING_REVIEW, 문서 성격상 자연스러운 부재면 DISMISS. + - `R-DRIFT`: excerpt(해당 shard 원문 발췌)와 part 필드를 대조하여 다른 문서 고유 사실의 주입이 보이면 CONFIRM_BLOCK, 판단 불가면 ESCALATE_TO_REVIEW, 근거 없으면 DISMISS. + - `R-CHRONO`: 날짜 역전이 같은 법률행위 chain의 실질 모순이면(이행기·시효·도달일·경매 chain) HARD_WARNING_REVIEW 또는 ESCALATE_TO_REVIEW, 서로 다른 채무·물건의 우연한 교차면 DISMISS. + - `R-EVTCOV`: payment/notice/auction/lien/succession/valuation/defense/상호속용 등 legal-effect event 누락 의심이 실질적이면 SOFT/HARD_WARNING_REVIEW, rule-table의 기계적 공백이면 DISMISS. + - `R-SELFWARN`: mapper가 남긴 항변·재반박 신호(상계 주장 등)는 원칙적으로 SOFT_WARNING_REVIEW로 승격하여 Stage 2 검토 큐에 보존한다. `candidate_use_policy="preserve_as_needs_review"`. + - `source_refs`는 pack의 `evidence_indexes`/`candidate_ids`를 named object로 재배치한다. 임의 ref 창작 금지. + + + + + + [ ] 1) `read_docs` 사용해서 `stage1_tmp/quality_gate/B12_exception_pack.json` 1개 파일만 읽기 + [ ] 2) `exception_count == 0`이면 빈 decisions 파일(`no_suspects: true`) 저장 후 `"NO_SUSPECTS"` 출력하고 즉시 종료 + [ ] 3) 각 exception을 pack 내장 payload/excerpt만으로 판정 (추가 read 금지) + [ ] 4) 모든 exception_id에 정확히 1개 decision, `source_refs`는 named object + [ ] 5) `write_file` 사용해서 `stage1_tmp/quality_gate/B12_adjudication_decisions.json` 저장 + [ ] 6) `list_docs`로 파일 존재만 확인(절대 읽기 금지) + [ ] 7) `"B12 예외 판정 완료 (decisions=<건수>)"` 출력하고 작업을 끝낸다(terminate) + + + + + - `stage1_tmp/quality_gate/B12_exception_pack.json` + + + - `stage1_tmp/quality_gate/B12_adjudication_decisions.json` + + + + + - 본 task는 판정만 한다. 병합·직렬화·집계·최종 파일 작성은 전부 결정적 task의 소관이다. + - pack 외 파일을 읽는 순간 read-diet 위반이다. 필요한 발췌는 이미 pack 안에 있다. + - review 판정은 검토 큐이지 사실 확정이 아니다. 확신 없는 BLOCK보다 ESCALATE_TO_REVIEW가 항상 안전하다. + + + + - task_name: Task_B12_gate_audit_finalizer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 120 + code: | + #!/usr/bin/env python3 + # Task_B12_gate_audit_finalizer (v3) - deterministic audit finalizer (GC) + # Spec: Part_1_Improvement_Strategy_Claude_v1.md §7 + from __future__ import annotations + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + + B1_PRECHECK_PATH = "stage1_tmp/quality_gate/B1_precheck.json" + B2_PRECHECK_PATH = "stage1_tmp/quality_gate/B2_precheck.json" + DECISIONS_PATH = "stage1_tmp/quality_gate/B12_adjudication_decisions.json" + PACK_PATH = "stage1_tmp/quality_gate/B12_exception_pack.json" + B1_AUDIT_PATH = "quality_gates/B1_evidence_indexed_gate.json" + B2_AUDIT_PATH = "quality_gates/B2_event_candidates_gate.json" + MAX_REVIEW_ITEMS = 40 + SOURCE_REF_KEYS = ("evidence_indexes", "event_candidate_ids", "meeting_clause_refs", "doc_titles") + SEVERITY_ORDER = {"PASS": 0, "SOFT_WARNING": 1, "HARD_WARNING": 2, "BLOCK": 3} + ALLOWED_POLICIES = ("prioritize_if_supported", "preserve_as_needs_review", "do_not_generate_without_support") + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-b12-gate-audit-finalizer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _mid(), + "method": "tools/call", + "params": {"name": name, "arguments": args}, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: Any, name: str) -> str: + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError(f"Empty: {name}") + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, dict) and "results" in outer: + result0 = (outer.get("results") or [{}])[0] + inner = result0.get("content") or result0.get("text") or "" + if not inner: + raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer if isinstance(outer, str) else json.dumps(outer, ensure_ascii=False) + + def read_raw(path: str) -> str: + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str) or not raw.strip(): + raise RuntimeError(f"Missing or empty required input: {path}") + return raw + + def parse_json(raw: str, path: str) -> Any: + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + try: + obj, _ = decoder.raw_decode(raw.strip()) + return obj + except Exception as exc: + raise RuntimeError(f"Invalid JSON input: {path}") from exc + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def clip(value: Any, limit: int = 120) -> str: + text = " ".join(str(value or "").split()) + if len(text) <= limit: + return text + return text[:limit].rstrip() + "..." + + def clean_enum(value: Any, fallback: str) -> str: + text = str(value or fallback).strip().lower() + text = re.sub(r"[^a-z0-9_]+", "_", text) + text = re.sub(r"_+", "_", text).strip("_") + return text or fallback + + def named_source_refs(value: Any, exception: dict[str, Any] | None) -> dict[str, list[str]]: + # source_refs는 반드시 named object — 배열형은 schema failure로 강제 정규화하지 않고 pack 기반으로 재구성 + refs = {key: [] for key in SOURCE_REF_KEYS} + if isinstance(value, dict): + for key in SOURCE_REF_KEYS: + vals = value.get(key) + if isinstance(vals, list): + refs[key].extend(str(v).strip() for v in vals if v) + if exception: + refs["evidence_indexes"].extend(str(v) for v in (exception.get("evidence_indexes") or []) if v) + refs["event_candidate_ids"].extend(str(v) for v in (exception.get("candidate_ids") or []) if v) + return {key: sorted(set(v for v in vals if v)) for key, vals in refs.items()} + + def worst(*severities: str) -> str: + top = "PASS" + for s in severities: + if SEVERITY_ORDER.get(s, 0) > SEVERITY_ORDER.get(top, 0): + top = s + return top + + DECISION_SEVERITY = { + "DISMISS": "PASS", + "SOFT_WARNING_REVIEW": "SOFT_WARNING", + "HARD_WARNING_REVIEW": "HARD_WARNING", + "ESCALATE_TO_REVIEW": "HARD_WARNING", + "CONFIRM_BLOCK": "BLOCK", + } + + def build_audit(gate: str, precheck: dict[str, Any], decisions_by_exc: dict[str, dict[str, Any]], + exceptions_by_id: dict[str, dict[str, Any]], executed_at: str) -> dict[str, Any]: + gate_id = "B1_evidence_indexed" if gate == "B1" else "B2_event_candidates" + checks: list[dict[str, Any]] = [] + check_sev: dict[str, str] = {} + for chk in precheck.get("deterministic_checks") or []: + entry = dict(chk) + check_sev[str(entry.get("check_id"))] = str(entry.get("severity") or "PASS") + checks.append(entry) + + hard_findings: list[dict[str, Any]] = [] + for idx, hf in enumerate(precheck.get("hard_gate_findings") or [], 1): + item = dict(hf) if isinstance(hf, dict) else {"detail": str(hf)} + item.setdefault("gate_finding_id", f"{gate}-HARD-{idx:03d}") + hard_findings.append(item) + + review_findings: list[dict[str, Any]] = [] + drift_confirmed: list[dict[str, Any]] = [] + for exc_id, exc in exceptions_by_id.items(): + if str(exc.get("source_gate")) != gate: + continue + dec = decisions_by_exc.get(exc_id) + if dec is None: + # 판정 누락 exception은 보수적으로 review로 승격 (silent pass 금지) + dec = {"exception_id": exc_id, "decision": "ESCALATE_TO_REVIEW", "severity": "HARD_WARNING", + "issue_type": clean_enum(exc.get("rule_id"), "review_finding"), + "source_refs": {}, "concise_basis": "adjudication decision missing for extracted suspect", + "candidate_use_policy": "preserve_as_needs_review"} + decision = str(dec.get("decision") or "ESCALATE_TO_REVIEW") + mapped = DECISION_SEVERITY.get(decision, "HARD_WARNING") + check_id = str(exc.get("check_id") or "review_finding") + if decision == "DISMISS": + continue + if decision == "CONFIRM_BLOCK": + item = { + "gate_finding_id": f"{gate}-HARD-{len(hard_findings) + 1:03d}", + "issue_type": clean_enum(dec.get("issue_type") or exc.get("rule_id"), "confirmed_block"), + "source_check": clean_enum(check_id, "gate_check"), + "detail": clip(dec.get("concise_basis") or "adjudicator confirmed hard gate violation"), + "source_refs": named_source_refs(dec.get("source_refs"), exc), + } + hard_findings.append(item) + check_sev[check_id] = worst(check_sev.get(check_id, "PASS"), "BLOCK") + if check_id == "authority_drift_detection": + drift_confirmed.append({"ordinals": exc.get("ordinals"), "basis": item["detail"]}) + continue + severity = str(dec.get("severity") or mapped) + if severity not in ("SOFT_WARNING", "HARD_WARNING"): + severity = mapped + policy = dec.get("candidate_use_policy") + if policy not in ALLOWED_POLICIES: + policy = "preserve_as_needs_review" + review_findings.append({ + "gate_finding_id": f"{gate}-REV-{len(review_findings) + 1:03d}", + "source_check": clean_enum(check_id, "review_finding"), + "severity": severity, + "issue_type": clean_enum(dec.get("issue_type") or exc.get("rule_id"), "review_finding"), + "domain_hints": [], + "target_stage_hints": ["Stage_2"], + "target_task_hints": [], + "source_refs": named_source_refs(dec.get("source_refs"), exc), + "review_instruction": clip(dec.get("review_instruction") + or "후속 검토 단계에서 해당 review finding의 증거와 법률상 의미를 재검토한다."), + "candidate_use_policy": policy, + "concise_basis": clip(dec.get("concise_basis") or exc.get("rule_id")), + }) + check_sev[check_id] = worst(check_sev.get(check_id, "PASS"), severity) + review_findings = review_findings[:MAX_REVIEW_ITEMS] + + for chk in checks: + cid = str(chk.get("check_id")) + chk["severity"] = check_sev.get(cid, str(chk.get("severity") or "PASS")) + if cid == "authority_drift_detection" and drift_confirmed: + chk["drift_findings"] = (chk.get("drift_findings") or []) + drift_confirmed + + schema_invariant = list(precheck.get("schema_invariant_findings") or []) if gate == "B2" else None + block = bool(hard_findings) or bool(schema_invariant) + overall = "BLOCK" if block else worst(*[str(c.get("severity") or "PASS") for c in checks], + *[str(r.get("severity")) for r in review_findings]) + if overall == "BLOCK" and not block: + overall = "HARD_WARNING" + progression = "BLOCKED" if block else ("ALLOWED" if overall == "PASS" else "ALLOWED_WITH_WARNING") + auto = not block + highest_review = "PASS" + for r in review_findings: + highest_review = worst(highest_review, str(r.get("severity"))) + handoff_status = "BLOCKED" if block else ("READY_WITH_REVIEW" if review_findings else "READY_NO_REVIEW") + + audit: dict[str, Any] = { + "gate_id": gate_id, + "executed_at": executed_at, + "input_parts_count": precheck.get("input_parts_count"), + "expected_ordinals": precheck.get("expected_ordinals") or [], + "planner_source": precheck.get("planner_source") or "evidence_shard_plan.json", + "planner_shard_count": precheck.get("planner_shard_count"), + "received_ordinals": precheck.get("received_ordinals") or [], + } + if gate == "B2": + audit["schema_invariant_findings"] = schema_invariant or [] + audit.update({ + "checks": checks, + "overall_severity": overall, + "block_reasons": [clip(h.get("detail") or h.get("issue_type")) for h in hard_findings] + + ([clip(f"{s.get('reason')} (rerun: {s.get('rerun_target_task')})") for s in (schema_invariant or [])]), + "warning_reasons": [clip(r.get("concise_basis")) for r in review_findings], + "hard_gate_findings": hard_findings, + "review_findings": review_findings, + "rerun_targets": sorted({str(s.get("rerun_target_task")) for s in (schema_invariant or []) if s.get("rerun_target_task")}), + "stage2_risk_notes": [clip(r.get("concise_basis")) for r in review_findings], + "review_gate_summary": { + "review_gate_count": len(review_findings), + "highest_review_severity": highest_review, + }, + "handoff_export_summary": { + "is_soft_handoff_writer": False, + "handoff_writer_task": "Task_B2_SHA256_soft_gate_handoff_writer", + "handoff_path": "quality_gates/stage1_part1_soft_gate_handoff.json", + "exported_review_count": len(review_findings), + "handoff_status_candidate": handoff_status, + }, + "downstream_progression": progression, + "stage2_auto_progression_allowed": auto, + }) + if gate == "B1": + audit["dropped_aliases"] = precheck.get("dropped_aliases") or [] + audit["schema_contract_version"] = "B1_quality_gate.v3" + else: + audit["dedup_findings"] = precheck.get("dedup_findings") or [] + audit["dropped_by_b1_alias"] = precheck.get("dropped_by_b1_alias") or [] + audit["conservation"] = precheck.get("conservation") or {} + audit["schema_contract_version"] = "B2_quality_gate.v3" + return audit + + def main() -> None: + _init() + b1_precheck = parse_json(read_raw(B1_PRECHECK_PATH), B1_PRECHECK_PATH) + b2_precheck = parse_json(read_raw(B2_PRECHECK_PATH), B2_PRECHECK_PATH) + try: + pack = parse_json(read_raw(PACK_PATH), PACK_PATH) + except Exception: + pack = {"exceptions": []} + try: + decisions_doc = parse_json(read_raw(DECISIONS_PATH), DECISIONS_PATH) + except Exception: + decisions_doc = {"decisions": [], "no_suspects": True} + + exceptions_by_id: dict[str, dict[str, Any]] = {} + for exc in pack.get("exceptions") or []: + if isinstance(exc, dict) and exc.get("exception_id"): + exceptions_by_id[str(exc["exception_id"])] = exc + decisions_by_exc: dict[str, dict[str, Any]] = {} + for dec in decisions_doc.get("decisions") or []: + if isinstance(dec, dict) and dec.get("exception_id"): + decisions_by_exc[str(dec["exception_id"])] = dec + + b2_review_summary_split = {"b1": 0, "b2": 0} + executed_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") + b1_audit = build_audit("B1", b1_precheck, decisions_by_exc, exceptions_by_id, executed_at) + b2_audit = build_audit("B2", b2_precheck, decisions_by_exc, exceptions_by_id, executed_at) + b2_review_summary_split["b1"] = b1_audit["review_gate_summary"]["review_gate_count"] + b2_review_summary_split["b2"] = b2_audit["review_gate_summary"]["review_gate_count"] + # v.2 B2 audit review_gate_summary는 B1/B2 review count를 함께 요약한다 + b2_audit["review_gate_summary"]["b1_review_count"] = b2_review_summary_split["b1"] + b2_audit["review_gate_summary"]["b2_review_count"] = b2_review_summary_split["b2"] + + write_doc(B1_AUDIT_PATH, json.dumps(b1_audit, ensure_ascii=False, indent=2)) + write_doc(B2_AUDIT_PATH, json.dumps(b2_audit, ensure_ascii=False, indent=2)) + + # SKILL.md §3.3: stdout = task output — 단일 JSON 라인. 상태 메시지는 message 필드에 보존. + messages = [] + if b1_audit["overall_severity"] == "BLOCK": + messages.append("B1 quality gate BLOCK 발생: downstream 진행 금지") + if b2_audit["overall_severity"] == "BLOCK": + messages.append("B2 quality gate BLOCK 발생: downstream 진행 금지") + print(json.dumps({ + "status": "GATE_AUDITS_FINALIZED", + "message": " | ".join(messages) if messages else "gate audit finalize 완료", + "b1_overall": b1_audit["overall_severity"], + "b2_overall": b2_audit["overall_severity"], + "review_count": b2_review_summary_split["b1"] + b2_review_summary_split["b2"], + "hard_count": len(b1_audit["hard_gate_findings"]) + len(b2_audit["hard_gate_findings"]) + + len(b2_audit.get("schema_invariant_findings") or []), + "no_suspects": bool(decisions_doc.get("no_suspects")), + }, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B2_SHA256_soft_gate_handoff_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import hashlib + import itertools + import json + import re + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + SIDE_CAR_PATH = "quality_gates/stage1_part1_soft_gate_handoff.json" + INPUT_PATHS = { + "evidence_indexed_sha256": "evidence_indexed.json", + "evidence_event_candidates_sha256": "evidence_event_candidates.json", + "b1_gate_sha256": "quality_gates/B1_evidence_indexed_gate.json", + "b2_gate_sha256": "quality_gates/B2_event_candidates_gate.json", + } + WORKFLOW_ROLES = ( + "claim_right_identification", + "cause_of_action_element_build", + "evidence_weight_and_gap_check", + "chronology_and_state_split", + "asset_registry_succession_check", + "defense_and_obstacle_check", + "relief_scope_and_quantification", + "actio_value_compensation_check", + ) + SOURCE_REF_KEYS = ( + "evidence_indexes", + "event_candidate_ids", + "meeting_clause_refs", + "doc_titles", + ) + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-b2-sha256-soft-gate-handoff-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _mid(), + "method": "tools/call", + "params": {"name": name, "arguments": args}, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: Any, name: str) -> str: + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError(f"Empty: {name}") + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, dict) and "results" in outer: + result0 = (outer.get("results") or [{}])[0] + inner = result0.get("content") or result0.get("text") or "" + if not inner: + raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer if isinstance(outer, str) else json.dumps(outer, ensure_ascii=False) + + def read_raw(path: str) -> str: + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str) or not raw.strip(): + raise RuntimeError(f"Missing or empty required input: {path}") + return raw + + def parse_json(raw: str, path: str) -> Any: + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + try: + obj, _ = decoder.raw_decode(raw.strip()) + return obj + except Exception as exc: + raise RuntimeError(f"Invalid JSON input: {path}") from exc + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def digest(raw: str) -> str: + value = hashlib.sha256(raw.encode("utf-8")).hexdigest() + if not re.fullmatch(r"[0-9a-f]{64}", value): + raise RuntimeError("digest_guard produced a non-SHA256 value") + return value + + def as_list(value: Any) -> list[Any]: + if value is None: + return [] + if isinstance(value, list): + return value + return [value] + + def as_str_list(value: Any) -> list[str]: + out: list[str] = [] + for item in as_list(value): + if item is None: + continue + if isinstance(item, dict): + for key in ("id", "value", "name", "title", "ref"): + if item.get(key): + out.append(str(item[key]).strip()) + break + else: + out.append(str(item).strip()) + return [x for x in out if x] + + def clean_enum(value: Any, fallback: str) -> str: + text = str(value or fallback).strip().lower() + text = re.sub(r"[^a-z0-9_]+", "_", text) + text = re.sub(r"_+", "_", text).strip("_") + return text or fallback + + def clip(value: Any, limit: int = 120) -> str: + text = " ".join(str(value or "").split()) + if len(text) <= limit: + return text + return text[:limit].rstrip() + "..." + + def normalize_severity(value: Any) -> str: + text = str(value or "").upper() + if "HARD" in text: + return "HARD_WARNING" + return "SOFT_WARNING" + + def role_for(issue_type: str, finding: dict[str, Any]) -> str: + explicit = finding.get("lawyer_workflow_role") + if explicit in WORKFLOW_ROLES: + return str(explicit) + text = " ".join([ + issue_type, + str(finding.get("source_check") or ""), + str(finding.get("check_id") or ""), + str(finding.get("detail") or finding.get("reason") or ""), + ]).lower() + if any(k in text for k in ("chrono", "date", "timeline", "state", "order")): + return "chronology_and_state_split" + if any(k in text for k in ("evidence", "proof", "source", "locator")): + return "evidence_weight_and_gap_check" + if any(k in text for k in ("registry", "secured", "lien", "succession", "asset", "auction", "mortgage")): + return "asset_registry_succession_check" + if any(k in text for k in ("defense", "obstacle", "setoff", "limitation")): + return "defense_and_obstacle_check" + if any(k in text for k in ("relief", "amount", "scope", "valuation", "damage")): + return "relief_scope_and_quantification" + if "actio" in text or "fraudulent" in text or "value_compensation" in text: + return "actio_value_compensation_check" + if any(k in text for k in ("claim", "right", "element", "legal_effect")): + return "claim_right_identification" + return "cause_of_action_element_build" + + def source_refs(finding: dict[str, Any]) -> dict[str, list[str]]: + refs = {key: [] for key in SOURCE_REF_KEYS} + nested = finding.get("source_refs") + if isinstance(nested, dict): + for key in SOURCE_REF_KEYS: + refs[key].extend(as_str_list(nested.get(key))) + refs["evidence_indexes"].extend(as_str_list(finding.get("evidence_indexes"))) + refs["evidence_indexes"].extend(as_str_list(finding.get("evidence_index"))) + refs["evidence_indexes"].extend(as_str_list(finding.get("source_evidence_index"))) + refs["event_candidate_ids"].extend(as_str_list(finding.get("event_candidate_ids"))) + refs["event_candidate_ids"].extend(as_str_list(finding.get("candidate_id"))) + refs["event_candidate_ids"].extend(as_str_list(finding.get("candidate_ids"))) + refs["meeting_clause_refs"].extend(as_str_list(finding.get("meeting_clause_refs"))) + refs["meeting_clause_refs"].extend(as_str_list(finding.get("meeting_clause_ref"))) + refs["doc_titles"].extend(as_str_list(finding.get("doc_titles"))) + refs["doc_titles"].extend(as_str_list(finding.get("doc_title"))) + return {key: sorted(set(value)) for key, value in refs.items()} + + def extract_findings(gate: Any, key: str) -> list[Any]: + findings: list[Any] = [] + if isinstance(gate, dict): + findings.extend(as_list(gate.get(key))) + for check in as_list(gate.get("checks")): + if isinstance(check, dict): + findings.extend(as_list(check.get(key))) + return findings + + def hard_findings(gate: Any, source_gate: str) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for item in extract_findings(gate, "hard_gate_findings"): + out.append(item if isinstance(item, dict) else {"detail": str(item)}) + if isinstance(gate, dict): + blocked = gate.get("overall_severity") == "BLOCK" or gate.get("downstream_progression") == "BLOCKED" or gate.get("stage2_auto_progression_allowed") is False + if blocked and not out: + reasons = as_list(gate.get("block_reasons")) + if not reasons: + reasons = [f"{source_gate} gate blocked without explicit hard_gate_findings"] + for reason in reasons: + out.append({"issue_type": "block_reason", "detail": str(reason)}) + for index, item in enumerate(out, 1): + item.setdefault("gate_finding_id", f"{source_gate}-HARD-{index:03d}") + return out + + def compact_review_items(b1_gate: Any, b2_gate: Any) -> list[dict[str, Any]]: + compact: list[dict[str, Any]] = [] + ordered = [("B1", b1_gate), ("B2", b2_gate)] + seq = 1 + for source_gate, gate in ordered: + for raw in extract_findings(gate, "review_findings"): + finding = raw if isinstance(raw, dict) else {"detail": str(raw)} + issue_type = clean_enum( + finding.get("issue_type") or finding.get("check_id") or finding.get("source_check") or "review_finding", + "review_finding", + ) + role = role_for(issue_type, finding) + refs = source_refs(finding) + policy = finding.get("candidate_use_policy") + if policy not in ("prioritize_if_supported", "preserve_as_needs_review", "do_not_generate_without_support"): + policy = "preserve_as_needs_review" + item = { + "review_id": f"P1RG-{seq:04d}", + "source_gate": source_gate, + "source_check": clean_enum(finding.get("source_check") or finding.get("check_id") or issue_type, issue_type), + "severity": normalize_severity(finding.get("severity")), + "issue_type": issue_type, + "lawyer_workflow_role": role, + "domain_hints": as_str_list(finding.get("domain_hints") or finding.get("domain_hint") or finding.get("domains")), + "target_stage_hints": as_str_list(finding.get("target_stage_hints") or finding.get("target_stage_hint")), + "target_task_hints": as_str_list(finding.get("target_task_hints") or finding.get("target_task_hint")), + "source_refs": refs, + "review_instruction": clip(finding.get("review_instruction") or finding.get("instruction") or "후속 검토 단계에서 해당 review finding의 증거와 법률상 의미를 재검토한다.", 120), + "negative_instruction": "사실 확정 또는 자동 차단으로 취급하지 말 것", + "candidate_use_policy": policy, + "concise_basis": clip(finding.get("concise_basis") or finding.get("detail") or finding.get("reason") or issue_type, 120), + "created_from": { + "gate_file": "quality_gates/B1_evidence_indexed_gate.json" if source_gate == "B1" else "quality_gates/B2_event_candidates_gate.json", + "gate_finding_id": finding.get("gate_finding_id") or finding.get("finding_id") or finding.get("id"), + }, + } + compact.append(item) + seq += 1 + if len(compact) >= 40: + return compact + return compact + + def add_index(index: dict[str, dict[str, list[str]]], bucket: str, key: str, review_id: str) -> None: + if not key: + return + index[bucket].setdefault(key, []) + if review_id not in index[bucket][key]: + index[bucket][key].append(review_id) + + def build_routing(review_items: list[dict[str, Any]]) -> tuple[dict[str, dict[str, list[str]]], dict[str, list[str]]]: + routing = { + "by_evidence_index": {}, + "by_event_candidate_id": {}, + "by_meeting_clause_ref": {}, + "by_issue_type": {}, + "by_domain_hint": {}, + "by_target_stage": {}, + "by_lawyer_workflow_role": {}, + } + queues = {role: [] for role in WORKFLOW_ROLES} + for item in review_items: + rid = item["review_id"] + refs = item.get("source_refs") or {} + for value in refs.get("evidence_indexes", []): + add_index(routing, "by_evidence_index", value, rid) + for value in refs.get("event_candidate_ids", []): + add_index(routing, "by_event_candidate_id", value, rid) + for value in refs.get("meeting_clause_refs", []): + add_index(routing, "by_meeting_clause_ref", value, rid) + add_index(routing, "by_issue_type", item.get("issue_type", ""), rid) + for value in item.get("domain_hints", []): + add_index(routing, "by_domain_hint", value, rid) + for value in item.get("target_stage_hints", []): + add_index(routing, "by_target_stage", value, rid) + role = item.get("lawyer_workflow_role", "cause_of_action_element_build") + add_index(routing, "by_lawyer_workflow_role", role, rid) + queues.setdefault(role, []).append(rid) + return routing, {role: queues.get(role, []) for role in WORKFLOW_ROLES} + + def gate_blocked(gate: Any) -> bool: + return isinstance(gate, dict) and ( + gate.get("overall_severity") == "BLOCK" + or gate.get("downstream_progression") == "BLOCKED" + or gate.get("stage2_auto_progression_allowed") is False + ) + + def main() -> None: + _init() + raw_by_digest_key = {key: read_raw(path) for key, path in INPUT_PATHS.items()} + parsed = {key: parse_json(raw, INPUT_PATHS[key]) for key, raw in raw_by_digest_key.items()} + + digest_guard = {key: digest(raw) for key, raw in raw_by_digest_key.items()} + b1_gate = parsed["b1_gate_sha256"] + b2_gate = parsed["b2_gate_sha256"] + review_items = compact_review_items(b1_gate, b2_gate) + routing_index, lawyer_workflow_queues = build_routing(review_items) + + hard_items = hard_findings(b1_gate, "B1") + hard_findings(b2_gate, "B2") + hard_ids = [ + str(item.get("gate_finding_id") or item.get("finding_id") or item.get("id") or f"HARD-{idx:03d}") + for idx, item in enumerate(hard_items, 1) + ] + blocked = bool(hard_items) or gate_blocked(b1_gate) or gate_blocked(b2_gate) + highest_review = "PASS" + if any(item["severity"] == "HARD_WARNING" for item in review_items): + highest_review = "HARD_WARNING" + elif review_items: + highest_review = "SOFT_WARNING" + b1_count = sum(1 for item in review_items if item["source_gate"] == "B1") + b2_count = sum(1 for item in review_items if item["source_gate"] == "B2") + + if blocked: + handoff_status = "BLOCKED" + elif review_items: + handoff_status = "READY_WITH_REVIEW" + else: + handoff_status = "READY_NO_REVIEW" + + sidecar = { + "schema_version": "stage1_part1_soft_gate_handoff.v1", + "source_stage": "Stage_1.Part_1", + "source_gate_files": [ + "quality_gates/B1_evidence_indexed_gate.json", + "quality_gates/B2_event_candidates_gate.json", + ], + "handoff_status": handoff_status, + "stage2_auto_progression_allowed": not blocked, + "hard_gate_summary": { + "hard_gate_count": len(hard_items), + "hard_gate_ids": hard_ids, + }, + "review_gate_summary": { + "review_gate_count": len(review_items), + "highest_review_severity": highest_review, + "b1_review_count": b1_count, + "b2_review_count": b2_count, + }, + "digest_guard": digest_guard, + "review_items": review_items, + "routing_index": routing_index, + "lawyer_workflow_queues": lawyer_workflow_queues, + } + + content = json.dumps(sidecar, ensure_ascii=False, indent=2) + write_doc(SIDE_CAR_PATH, content) + print(json.dumps({ + "status": "SOFT_GATE_HANDOFF_WRITTEN", + "path": SIDE_CAR_PATH, + "handoff_status": handoff_status, + "review_items": len(review_items), + "digest_guard_valid": all(re.fullmatch(r"[0-9a-f]{64}", v) for v in digest_guard.values()), + }, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + task_procedure: + IN: + nexts: ["Task_A_client_goal", "Task_Evidence_shard_planner"] + wait_until: [] + + Task_A_client_goal: + nexts: [] + wait_until: ["IN"] + + Task_Evidence_shard_planner: + nexts: ["Task_B1_map_doc_*"] + wait_until: ["IN"] + + Task_B1_map_doc_*: + nexts: + - "Task_B2_map_events_{same_ordinal}" + - "Task_B1_quality_gate_evidence_indexed" + wait_until: ["Task_Evidence_shard_planner"] + + Task_B2_map_events_*: + nexts: ["Task_B2_quality_gate_event_candidates"] + wait_until: ["Task_B1_map_doc_{same_ordinal}"] + + Task_B1_quality_gate_evidence_indexed: + nexts: ["Task_B2_quality_gate_event_candidates"] + wait_until: ["all Task_B1_map_doc_*"] + + Task_B2_quality_gate_event_candidates: + nexts: ["Task_B12_gate_llm_adjudicator"] + wait_until: ["all Task_B2_map_events_*", "Task_B1_quality_gate_evidence_indexed"] + + Task_B12_gate_llm_adjudicator: + nexts: ["Task_B12_gate_audit_finalizer"] + wait_until: ["Task_B2_quality_gate_event_candidates"] + + Task_B12_gate_audit_finalizer: + nexts: ["Task_B2_SHA256_soft_gate_handoff_writer"] + wait_until: ["Task_B12_gate_llm_adjudicator"] + + Task_B2_SHA256_soft_gate_handoff_writer: + nexts: ["OUT"] + wait_until: ["Task_B12_gate_audit_finalizer"] + + OUT: + nexts: [] + wait_until: ["Task_B2_SHA256_soft_gate_handoff_writer"] + + prevs: [] + nexts: [] diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_1_Codex_v3.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_1_Codex_v3.yml new file mode 100644 index 00000000..9de1a4f5 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_1_Codex_v3.yml @@ -0,0 +1,3937 @@ +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_1 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Part 1 기초작업 + version: v3.0 + + Stages: + - name: stage1_사건개요파악_기초작업 + description: 사건개요 파악, 기초정보 추출 + llm_provider: openai + llm_model: gpt-4o-2024-08-06 + tools: + mcpServers: + localdocs: + type: streamable-http + url: "http://mcp-localdocs:8012/mcp" + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + + tasks: + - task_name: Task_A_client_goal + llm_provider: google + llm_model: 'gemini-3.1-flash-lite' + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + preflight: false + preflight_files: + - client_meeting.md + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + - Aggregate, compile, publish, commit, or write only the inputs and final/derived file authorized by the current task. + - Preserve stable order, source IDs, support IDs, BO_ID, fact_id, structure_id, claim_group_id, liability_group_id, and required enum semantics exactly. + - Create or finalize IDs only from the current task's specified pattern and deterministic ordering rule. + - Do not merge, deduplicate, supersede, normalize, repair, or drop candidates unless the current reducer/writer schema authorizes that action. + - If validation or source support is insufficient, use the current failure, rollback, warning, or blocked path instead of fabricating a passing final artifact. + - Treat downstream authority and handoff fields as machine contracts; do not add prose, alternative files, or extra writer attestations outside the schema. + + + + + Task_A_client_goal + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. + + + `client_meeting.md`만 읽고 `client_goal.json`을 생성한다. + 이 파일은 고객의 소송 목적, 명시적 제약, 핵심 사건군, 당사자 universe를 Stage 1의 가장 앞단에서 안정적으로 구조화한 요약본이어야 한다. + v.3에서도 기존 필수 schema는 유지하되, 복수 원고와 복수 분쟁군이 뒤섞여 later stage가 공동원고를 과잉 일반화하지 않도록 사건군 단위의 optional 구조, 사건군별 원고적격 구조, 현재 지배상태 구조를 함께 남긴다. + 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않는다. + 특히 상속·승계형 사건군에서는 meeting만으로 확정되지 않은 친족관계 또는 공동행동 정보를 direct standing anchor처럼 과잉 일반화하지 않는다. + 상속이 개입된 사건군에서는 `legal entitlement layer`와 `complaint drafting preference layer`를 분리하여 기록한다. + 사망한 종전 권리주체의 과거 소유사실, 매수사실, 등기명의사실을 living plaintiff의 현재 `소유자/final`로 치환하지 않는다. + 필요하면 사건군별 optional additive field `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`, `preferred_plaintiff_selection_basis`, `succession_resolution_needed`를 사용하여, later stage가 실체법상 가능한 권리자 후보층과 실제 소장에 세울 우선 원고층을 혼동하지 않도록 한다. + 또한 `current_control_map`은 보조 메모가 아니라 later stage의 부당이득·인도·철거 청구 식별에 직접 쓰이는 authority map이므로, 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속을 가능한 한 4축으로 분해 저장한다. + v.3 기준으로 Task_A_client_goal은 모든 issue cluster에 사건군별 법률효과 구조의 seed를 남겨야 한다. 단순 사건 요약이 아니라 금전채권 timeline, 상호속용 책임, 담보부 부동산·지분별 권리상태, 사해행위취소 가액배상, 유치권·상속·통지 lifecycle을 downstream이 구조적으로 읽을 수 있게 `domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints`, `legal_entitlement_candidates`, `current_control_map_4axis`를 additive하게 생성한다. + `client_requested_relief`는 의뢰인이 원하는 청구 기간·금액·상대방을 보존하는 층이고, `legal_sustainability_watchpoints`는 법률상 기산일·원고적격·청구범위 재검토 필요성을 보존하는 층이다. 두 층을 절대 하나로 합치지 않는다. + 상속·승계형 사건군에서 current right holder가 meeting만으로 확정되지 않으면 `current_control_map`의 해당 row는 `certainty="provisional"`로 두고 `standing_watchpoints`에 succession-resolution trigger를 반드시 남긴다. + 또한 현재 무자력/집행난과 실제 제소 유지 의사를 혼동하지 않으며, 특정 상대방을 현재 무자력이라는 이유만으로 소 제기 대상에서 소거하지 않는다. + 필요하면 optional additive field `defendant_target_matrix`를 사용하여 피고별 소제기 유지/제외 의사, 현재 회수위험, 장래 회수신호를 분리 보존한다. + 본 task는 v.3 architecture에서 final writer로서 `client_goal.json`을 직접 작성한다(Task_A는 island/mapper가 없으므로 propose 단계 없이 직접 finalize 가능). + + + + + - In this task, legal/domain fields are meeting-grounded routing seeds and watchpoints for downstream stages. + - They are not final legal conclusions, norm search results, merits judgments, or complaint-drafting advice. + - The EXTRACTION overlay's prohibition on norm search and final legal-effect classification does not prohibit schema-authorized `domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints`, or entitlement/watchpoint seed fields. + + + + + - `client_meeting.md` + + + - `client_goal.json` + + + - 다른 파일 확인/읽기 + - 중간 파일 생성 + - `client_goal.json` 재읽기 + - meeting에 없는 당사자, 사건군, 제약, 희망사항, 청구유형을 창작하기 + - 사건군별 당사자 범위를 무시하고 전 사건 공통 사실처럼 과잉 압축하기 + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하기 + + + + + 1. `list_docs`로 `client_meeting.md` 존재만 확인한다. + 2. `read_docs`로 `client_meeting.md`만 읽는다. + 3. 메모리에서 `client_goal.json` 객체를 생성한다. + 4. `write_file(overwrite=true)`로 `client_goal.json`을 저장한다. + 5. `list_docs`로 `client_goal.json` 존재만 확인한다. 절대 읽지 않는다. + 6. 정확히 `"고객의사 초안 생성 완료"`만 출력하고 종료한다. + + + + - 필수 입력 누락, 빈 파일, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. 파일은 쓰지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + + - `client_meeting.md`만 사실 원천이다. + - 상대방 주장, 의뢰인 희망사항, 상담서두의 인적 관계, 본문 1~5항의 사건군을 모두 읽되, meeting에 직접 나타난 범위만 구조화한다. + - meeting의 문장들 사이를 조합해 새로운 사실을 만들지 않는다. + + + + + - `primary_goal`은 사건 전체를 한 문장으로 요약하되, 개별 분쟁군을 완전히 뭉개지 않을 정도의 수준으로만 추상화한다. + - `희망사항`이 복수이면 공통 분모를 중심으로 요약하되, 특정 핵심 구제수단이 반복되면 반영할 수 있다. + - 희망사항이 피고별 비대칭 전략을 직접 드러내면, 그 비대칭을 보존한다. + - 현재 회수곤란 사실이 있더라도 장래 재산취득, 상속, 보험금 회수, 급여압류 가능성 등으로 특정 피고를 제소 대상으로 유지하려는 의사가 직접 서술되면, 이를 `primary_goal` 또는 `summary_key_incidents`에 반영한다. + - 직접 금전청구 대상과 사해행위취소 대상이 다르면 한쪽으로 뭉개지 않는다. + - 불명확하면 `null`. + + + + - `constraints`에는 의뢰인이 명시적으로 원하지 않는 소, 특정 대상 제외, 가능하면/다만/아울러와 같은 제한적 요청, 상속·경매 공신력 등 사건 진행상의 명시적 제약을 넣는다. + - `constraints`는 소송전략상 하지 말 것, 제한할 것, 절차상·법률상 한계만 담는다. + - 단순한 회수곤란 사실, 현재 무자력, 현재 재산 부족, 집행난은 `constraints`에 넣지 않는다. + - 현재 무자력과 장래 재산취득 가능성이 함께 서술되면, 이를 부정적 제약으로 압축하지 않는다. + - 특정 상대방을 제외하라는 명시적 no-sue 지시가 없는 한, current collectability만으로 대상 제외 제약을 만들지 않는다. + - 해석상 추정 제약은 넣지 않는다. + - 배열 순서는 meeting 서술 순서를 최대한 따른다. + + + + - `summary_key_incidents`는 최대 4문장, 각 100자 이하. + - 각 문장은 가능한 한 서로 다른 사건군을 대표하도록 구성한다. + - 사건군 라벨은 입력자료에 직접 나타난 분쟁 대상, 거래유형, 자산군, 청구 방향을 짧게 추상화하여 만든다. 특정 사건의 지명·상호·인명·자산명을 예시값처럼 재사용하지 않는다. + - 하나의 문장에 서로 다른 사건군을 과도하게 병합하지 않는다. + - 직접 서술된 날짜/당사자/핵심 행위만 사용한다. + - 원고가 특정 피고를 직접 금전청구 대상으로 유지하려는 의사가 직접 서술되면, 사건군 대표 문장 중 하나에 그 방향성을 반영할 수 있다. + - 현재 회수곤란 사실과 장래 회수가능성 신호가 같은 사람에 관하여 함께 직접 서술되면, 둘 중 하나만 선택적으로 삭제하지 않는다. + + + + - `key_facts`는 최대 5개, 각 60자 이하. + - 가능한 한 사건군별 핵심 fact를 1개씩 대표시킨다. + - later stage의 원고적격 판단에 중요할 수 있는 소유관계, 상속관계, 현재 권리귀속, 현재 점유/사용/수익귀속 상태, 특정 처분/경매/통지 사실을 우선한다. + - 직접 서술이 없는 금액·날짜·상태를 보충하지 않는다. + + + + - `claim_type_candidates`는 의뢰인의 희망사항과 사건군 구조를 바탕으로 선택한다. + - 허용 enum만 사용한다: `"금전"|"물건인도"|"행위"|"확인"|"형성"|"보전(가처분/가압류)"`. + - later stage 분류기를 돕기 위한 상위 유형 수준만 남기고, 세부 청구명을 쓰지 않는다. + + + + + + - 이름은 원문 우선, 명백히 동일인/동일법인인 경우만 canonicalize 한다. + - 별칭/변형은 `aliases`에 기록한다. + - 당사자 배열은 사건 전체 participant universe다. 모든 분쟁군의 실질 원고적격/피고적격을 의미하지 않는다. + + + + - `parties.plaintiffs`에는 실제 의뢰인 또는 meeting 서두와 희망사항에서 원고 측 client로 직접 읽히는 사람만 넣는다. + - `parties.plaintiffs`는 client universe일 뿐, 사건군별 실질 원고적격을 의미하지 않는다. + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않는다. + - 사건군별 원고 범위 차이는 `issue_clusters`, `plaintiff_capacity_matrix`, `standing_watchpoints`에서 보정한다. + + + + - `parties.defendants`에는 meeting 본문, 상대방 주장, 희망사항에서 분쟁 상대방 또는 소송상 직접 대상으로 명시된 자만 넣는다. + - `asset_status`는 meeting에 직접 나온 재산 상태, 현재 소유/점유/담보권 설정 상태 등 later stage에 직접 도움이 되는 짧은 상태만 넣는다. + - `asset_status`는 현재 재산 상태에 관한 사실기술일 뿐, 소 제기 대상 제외 신호가 아니다. + - meeting이 "현재 무자력이나 장래 상속/재산취득 가능성 있음"을 직접 서술하면, `asset_status`는 그 둘을 함께 보존하는 방향으로 적는다. + - 현재 무자력만을 이유로 defendants에서 제외하지 않는다. 제외는 명시적 no-sue 지시가 있을 때만 허용한다. + - meeting에 없는 재산상태 추정 금지. + + + + - `parties.third_parties`에는 사건군에 중요하지만 즉시 원고/피고로 확정되지 않은 자를 넣는다. + - `relationship`은 later stage가 읽을 수 있도록 짧고 직접적으로 적는다. + + + + + - optional additive field `defendant_target_matrix`를 허용한다. + - 이 필드는 피고별 소제기 유지/제외 의사와 현재 회수위험을 분리 기록하는 전략 매트릭스다. + - `target_status`는 제소 유지 여부를 나타내고, `execution_risk`는 current collectability를 나타낸다. + - 현재 무자력은 `execution_risk`에 반영하고, 제소 유지 여부는 `target_status`에 별도로 반영한다. + - 장래 상속, 장래 재산취득, 보험금 회수 가능성, 급여압류 가능성 등 미래 회수신호가 직접 서술되면 `future_recovery_signal`에 반영한다. + - 명시적 no-sue 지시가 없는 한, 단순한 current uncollectability만으로 `exclude_now`를 부여하지 않는다. + - 각 원소는 아래 구조를 따른다. + { + "name": "string", + "target_status": "target_now|exclude_now|secondary_target|review", + "basis_text": "meeting 직접 문장에 근거한 짧은 사유", + "execution_risk": "low|medium|high|unknown", + "future_recovery_signal": "none|possible|explicit" + } + - `basis_text`는 반드시 meeting 직접 문장에 근거한 짧은 근거여야 한다. + - 이 필드는 optional이다. 명시적 피고별 타기팅 차이가 없으면 생략할 수 있다. + + + + - optional additive field `issue_clusters`를 허용한다. + - 각 cluster는 meeting의 독립 분쟁군을 1개씩 반영한다. + - 이 필드는 later stage가 전 사건 공통 parties 배열만 보고 과잉 일반화하는 것을 줄이기 위한 보조 구조다. + - `candidate_plaintiffs`는 사건군별 잠정 plaintiff anchor일 뿐, 최종 원고적격 확정값이 아니다. + - `legal_entitlement_candidates`는 해당 사건군에서 meeting 직접 서술상 실체법상 권리자일 가능성이 있는 의뢰인 후보층이다. + - `preferred_plaintiffs_for_complaint`는 later stage가 실제 소장 원고로 우선 검토할 수 있는 narrower drafting layer다. + - 상속·승계형 사건군에서는 `candidate_plaintiffs`, `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`를 서로 자동 동일시하지 않는다. + - `candidate_plaintiffs`에는 meeting에서 해당 사건군의 직접 권리주체, 직접 채권자, 직접 임차인, 직접 점유권자 등으로 읽히는 의뢰인만 넣는다. + - 친족관계, 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 `candidate_plaintiffs`를 확장하지 않는다. + - 사망한 자가 해당 사건군의 종전 권리주체로 보이는데 meeting만으로 승계가 해소되지 않으면, 친족관계나 공동 통지 사실만으로 `candidate_plaintiffs`를 채우지 않는다. + - 위와 같은 unresolved succession cluster에서는 meeting이 직접 보여주는 친족관계와 자산 연결 범위 안에서만 `legal_entitlement_candidates`를 별도 기록할 수 있다. + - `preferred_plaintiffs_for_complaint`는 meeting이 특정 의뢰인을 직접 권리주체 또는 직접 소송유지 주체로 좁혀 보여주거나, 비상속형 사건군에서 direct right-holder가 명백할 때만 채운다. + - meeting만으로 drafting preference가 명백하지 않은 unresolved succession cluster에서는 `preferred_plaintiffs_for_complaint`를 빈 배열로 둘 수 있다. + - `preferred_plaintiff_selection_basis` 허용값은 `"direct_right_holder|resolved_succession|resolved_succession_needed|drafting_preference|review|불명"`이다. + - `succession_resolution_needed`는 사망한 종전 권리주체와 사건군의 연결은 보이지만 meeting만으로 현재 권리귀속 또는 complaint plaintiff narrowing이 해소되지 않을 때 `true`로 둔다. + - meeting이 직접 금전청구 대상 피고와 사해행위 상대방/수익자를 구별해 보여주면, 이를 별도 cluster로 분리할 수 있다. + - 직접 금전회수 대상이 특정 상대방으로 서술되면, property transfer cluster와 별도의 monetary recovery cluster를 허용한다. + - cluster 수는 1개 이상 6개 이하로 제한한다. + - 각 cluster는 아래 구조를 따른다. + { + "cluster_id": "IC-01", + "cluster_label": "짧은 사건군명", + "candidate_plaintiffs": ["이 사건군에서 직접 권리주체로 읽히는 의뢰인"], + "legal_entitlement_candidates": ["실체법상 권리자 후보로 읽히는 의뢰인"], + "preferred_plaintiffs_for_complaint": ["실제 소장 원고로 우선 검토할 의뢰인"], + "preferred_plaintiff_selection_basis": "direct_right_holder|resolved_succession|resolved_succession_needed|drafting_preference|review|불명", + "succession_resolution_needed": true, + "candidate_defendants_or_counterparties": ["직접 상대방"], + "desired_relief_keywords": ["금전", "명도", "퇴거", "확인", "보전" 등 짧은 키워드] + } + - 비상속형 사건군에서 direct right-holder가 명백하면 `candidate_plaintiffs`, `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`가 동일할 수 있다. + - `issue_clusters`는 optional이다. 명확하지 않으면 생략 가능하다. + - 추가 사실 창작 없이 meeting 본문과 희망사항에서 직접 읽히는 범위만 기록한다. + + + + - optional additive field `domain_profiles`를 각 `issue_clusters[*]` 내부에 허용한다. + - `domain_profiles`는 아래 네 하위 profile을 담는 object다. 해당하지 않는 profile은 `null`로 둔다. + - `monetary_claim_profile` + - `secured_property_dispute_profile` + - `actio_property_transfer_profile` + - `possession_lien_succession_profile` + - `client_requested_relief`와 `legal_sustainability_watchpoints`를 각 cluster 내부에 둘 수 있다. + - 의뢰인이 원하는 청구취지·기간·금액은 `client_requested_relief`에 보존하고, 법률상 기산일·원고적격·범위가 불명확한 점은 `legal_sustainability_watchpoints`에 분리한다. + - 당사자가 원하는 기간이나 금액을 법률상 청구취지에 바로 사용할 수 있다고 단정하지 않는다. + + + - 금전청구 사건군에서 원채무자 외 보증인, 채무인수자, 영업양수인, 상호속용자, 공동채무자 후보가 함께 등장하면 이를 별도 후보 피고 목록에만 두지 말고 `monetary_claim_profile`, `liability_group_hint`, `commercial_successor_liability_hint`로 묶어 보존한다. + - 동일 금액, 동일 원채권, 동일 원고를 기준으로 복수 피고가 책임질 가능성이 있으면 `liability_group_hint.same_underlying_debt=true`로 둔다. + - 지급기일, 이행청구, 최고, 채무승인, 이행각서, 소멸시효, 변제기 재설정 단서가 등장하면 `monetary_claim_profile.requires_money_claim_timeline=true`로 둔다. + - 영업양도, 상호 계속 사용, 사업 양수인, 채무승계 부인, 책임배제 등기 단서가 등장하면 `commercial_successor_liability_hint.requires_article_42_review=true`로 둔다. + + + + - 부동산 사건에서 여러 대여금, 여러 근저당권, 지분별 담보, 변제, 경매, 후행 등기가 함께 등장하면 이를 흩어진 issue로 분리하지 말고 `secured_property_dispute_profile`에 묶는다. + - 변제금이 복수 채무에 영향을 줄 수 있으면 `requires_payment_allocation=true`로 표시한다. + - 전체 부동산이 아니라 지분별 유효/무효가 문제될 수 있으면 `requires_fractional_validity_analysis=true`로 표시한다. + - 원고 주장 지분, 피고 측 유효 가능 지분, 전체 부동산 말소 금지 필요성을 `fractional_title_issue_hint`에 남긴다. + - 미등기 건물, 건물 신축, 건물 매매, 대지 임대차가 함께 등장하면 `land_use_gain_hint`를 생성한다. + - 임대차보증금, 월 차임, 건물 매매대금은 각각 법적 성격이 다르므로 `lease_payment_semantics_hint`에 별도 표시한다. + - 임대차보증금은 토지소유자의 부당이득 원금으로 자동 산정하지 말라는 watchpoint를 남긴다. + + + + - 채무자가 부동산을 수익자에게 이전하고, 원고가 사해행위취소를 희망하는 사건군을 발견하면 `actio_property_transfer_profile`을 생성한다. + - 부동산에 근저당권 등 부담이 있고, 그 부담이 사해행위 이후 변제·말소되었다는 단서가 있으면 `requires_value_compensation_review=true`로 표시한다. + - 같은 채권자와 채무자 사이에 복수 대여금이 등장하면 단순 후보채권 목록으로 흩뜨리지 말고 `preserved_claim_bundle_hint`로 묶는다. + - 피고가 명의신탁, 충분 담보, 상계, 선의 등을 주장하면 이를 `defense_hints`에 남기고, 청구원인 성립 또는 범위에 직접 영향을 주는 항변이면 `requires_stage2_rebuttal=true`로 표시한다. + + + + - 현재 점유자와 현재 권리자가 분리되는 자산군 또는 사건군이 입력자료에서 직접 확인되면 `possession_lien_succession_profile`을 생성한다. + - 상속이 개입되면 current right holder를 final로 확정하지 말고 `succession_resolution_needed=true`와 `standing_watchpoints`를 남긴다. + - 유치권, 제3자 임대, 직접 거주, 피담보채권 충당, 후소유자 소멸청구권 부정 주장이 보이면 `possession_basis_asserted`, `notice_lifecycle_hints`, `legal_sustainability_watchpoints`를 구조화한다. + - 통지서 발송일과 답변서상 수령 인정일이 함께 보이면 발송일과 도달일을 분리하여 `notice_lifecycle_hints`에 남긴다. + - 상속·승계가 미해결인 cluster에서는 입력자료상 공동 통지 발신자, 친족 후보, 직계존속·직계비속 후보를 그 지위의 후보로만 보존하고, 상속 해결 전 `preferred_plaintiffs_for_complaint` 확정자로 두지 않는다. + + + + + - optional additive field `current_control_map_4axis`를 허용한다. + - legacy `current_control_map` row 구조는 유지하되, 목적물별 4축 요약이 가능한 경우 `current_control_map_4axis`를 추가한다. + - 각 item은 아래 구조를 따른다. + { + "object": "string", + "cluster_id": "IC-01", + "current_right_holder_candidate": "string|null", + "current_possessor": "string|null", + "current_user": "string|null", + "current_benefit_holder": "string|null", + "possession_basis_asserted": "string|null", + "possession_basis_transition_needed": true, + "certainty": "final|provisional|불명" + } + - 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속을 하나의 generic holder로 뭉개지 않는다. + - 상속·승계형 사건군에서 현재 권리귀속이 decedent의 과거 권리사실과 친족관계로만 추론되는 경우 `certainty="provisional"`로 둔다. + + + + - optional additive field `plaintiff_capacity_matrix`를 허용한다. + - 이 필드는 사건군별 실질 원고적격을 later stage로 넘기기 위한 standing map이다. + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않는다. + - 각 원소는 아래 구조를 따른다. + { + "cluster_id": "IC-01", + "plaintiff_name": "string", + "capacity_basis": "소유자|공유자|상속인|임차인|채권자|점유권자|불명", + "source_basis_text": "meeting에서 직접 읽히는 짧은 근거", + "certainty": "final|provisional" + } + - 사건군별 직접 권리주체로 읽히는 사람만 넣는다. + - `capacity_basis="상속인"` row라도, 근거가 친족관계, 사망, 공동 통지, 공동 상담 같은 succession trigger 수준이면 `certainty`는 반드시 `"provisional"`로 둔다. + - 상속·승계형 사건군에서 decedent의 과거 소유사실, 매수사실, 등기명의사실만 보일 뿐 living plaintiff의 승계 완료 또는 현재 권리귀속이 meeting에서 직접 해소되지 않으면, living plaintiff를 `capacity_basis="소유자"` 또는 `capacity_basis="공유자"` + `certainty="final"`로 두지 않는다. + - 위 경우에는 원칙적으로 `capacity_basis`를 `"상속인"` 또는 `"불명"`으로 두고, `certainty`를 `"provisional"`로 둔다. + - meeting이 특정 사건군에 관하여 직접 계약당사자, 직접 채권자, 직접 임차인, 직접 소유·공유 관계 또는 승계 완료 상태를 직접 보여줄 때만 `certainty="final"`을 허용한다. + - 원고적격이 불명확하거나 later stage 재검토가 필요하면 `certainty`는 `"provisional"`로 둔다. + - `preferred_plaintiffs_for_complaint`가 별도로 존재하더라도, 이를 맞추기 위해 `plaintiff_capacity_matrix`를 `소유자/final`로 과잉 보정하지 않는다. + + + + - optional additive field `current_control_map`를 허용한다. + - 이 필드는 사건군별 현재 지배상태를 later stage로 넘기기 위한 authority map이다. + - 사건군에 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속 사실이 직접 읽히면 가능한 한 `current_control_map`을 생성한다. + - 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속은 가능한 한 분리한다. + - 각 원소는 아래 구조를 따른다. + { + "cluster_id": "IC-01", + "holder_name": "string", + "holder_role": "current_right_holder|current_possessor|current_user|current_benefit_holder", + "basis_text": "meeting에서 직접 읽히는 짧은 근거", + "certainty": "final|provisional" + } + - 같은 사람이 복수 role을 가지면 중복 기재할 수 있다. + - meeting에 직접 없는 role은 추정하지 않는다. + - 상속·승계형 사건군에서 current right holder가 decedent의 과거 권리사실 + 친족관계만으로 유도되는 경우, `holder_role="current_right_holder"` row는 원칙적으로 `certainty="provisional"`로 둔다. + - decedent의 과거 소유사실만으로 living plaintiff를 `current_right_holder/final`로 기재하지 않는다. + - 비상속형 사건군에서 meeting이 현재 소유자, 현재 점유자, 현재 사용자, 현재 수익귀속자를 직접 적시하면 해당 row는 `certainty="final"`을 우선 검토한다. + + + + - optional additive field `standing_watchpoints`를 허용한다. + - later stage가 반드시 재검토해야 하는 원고적격/권리귀속 리스크를 짧은 경고문으로 남긴다. + - 상속, 상속포기, 사망, 경매, 공동소유, 현재 권리주체 불명, 현재 점유자와 권리주체 불일치가 보이면 우선 검토한다. + - 경고문은 가능한 한 자산군 또는 사건군을 특정하는 asset-specific trigger 형태로 쓴다. + - 예시 형식: + - "상속·승계 자료 추가 확인 후 해당 자산군 원고적격 재확인 필요" + - "현재 권리귀속과 현재 점유자 분리 검토 필요" + - "사망한 종전 권리주체 관련 succession resolution 필요" + - meeting에 직접 읽히는 범위를 넘는 장문 법률판단은 쓰지 않는다. + + + + - `summary_key_incidents`, `key_facts`, `constraints`는 exact-match 중복 제거 후 최초 순서를 유지한다. + - `aliases`는 명백한 경우에만 기록한다. + - `claim_type_candidates`는 중복 제거 후 최초 순서를 유지한다. + - `defendant_target_matrix`를 사용하면 same `name` + `target_status` 조합 기준 exact-match 중복 제거 후 최초 순서를 유지한다. + - 비nullable string 외에는 `불명` 사용을 피하고 `null` 또는 빈 배열을 우선한다. + + + + 기존 필수 schema는 유지하고, 아래 optional additive field를 추가할 수 있다. + v.3 schema registry 기준 `IssueClusterDomainProfile`, `LegalEntitlementCandidate`, `CurrentControlMap4Axis`를 반영한다. + + { + "primary_goal": "string|null", + "constraints": [], + "summary_key_incidents": [], + "key_facts": [], + "claim_type_candidates": ["금전","물건인도","행위","확인","형성","보전(가처분/가압류)"], + "parties": { + "plaintiffs": [{"name":"string","type":"법인|자연인|기관"}], + "defendants": [{"name":"string","type":"법인|자연인|기관|미확정","asset_status":"string|null"}], + "third_parties": [{"name":"string","relationship":"string"}] + }, + "aliases": {}, + + "defendant_target_matrix": [ + { + "name": "string", + "target_status": "target_now|exclude_now|secondary_target|review", + "basis_text": "string", + "execution_risk": "low|medium|high|unknown", + "future_recovery_signal": "none|possible|explicit" + } + ], + + "issue_clusters": [ + { + "cluster_id": "IC-01", + "cluster_label": "string", + "candidate_plaintiffs": ["string"], + "legal_entitlement_candidates": ["string"], + "preferred_plaintiffs_for_complaint": ["string"], + "preferred_plaintiff_selection_basis": "direct_right_holder|resolved_succession|resolved_succession_needed|drafting_preference|review|불명", + "succession_resolution_needed": true, + "candidate_defendants_or_counterparties": ["string"], + "desired_relief_keywords": ["string"], + "domain_profiles": { + "monetary_claim_profile": { + "underlying_claim_type": "string|null", + "principal_amount_candidate": "string|null", + "principal_debtor_candidates": ["string"], + "successor_or_secondary_liability_candidates": [], + "default_or_limitation_issue_expected": true, + "known_demand_or_notice_signals": ["string"], + "requires_money_claim_timeline": true + }, + "secured_property_dispute_profile": { + "has_multiple_secured_debts": true, + "requires_payment_allocation": true, + "requires_fractional_validity_analysis": true, + "secured_object_candidates": ["string"], + "auction_or_registry_invalidity_expected": true + }, + "actio_property_transfer_profile": { + "debtor": "string|null", + "creditor": "string|null", + "beneficiary": "string|null", + "fraudulent_act_asset": "string|null", + "fraudulent_act_type": "string|null", + "fraudulent_act_date_hint": "string|null", + "is_encumbered_real_estate": true, + "requires_value_compensation_review": true + }, + "possession_lien_succession_profile": { + "possession_basis_asserted": "string|null", + "succession_resolution_needed": true, + "notice_lifecycle_hints": [], + "lien_violation_or_extinction_hints": [] + } + }, + "client_requested_relief": ["string"], + "legal_sustainability_watchpoints": ["string"], + "liability_group_hint": {}, + "commercial_successor_liability_hint": {}, + "fractional_title_issue_hint": {}, + "land_use_gain_hint": {}, + "lease_payment_semantics_hint": {}, + "preserved_claim_bundle_hint": {}, + "value_compensation_hint": {}, + "defense_hints": [] + } + ], + + "plaintiff_capacity_matrix": [ + { + "cluster_id": "IC-01", + "plaintiff_name": "string", + "capacity_basis": "소유자|공유자|상속인|임차인|채권자|점유권자|불명", + "source_basis_text": "string", + "certainty": "final|provisional" + } + ], + + "current_control_map": [ + { + "cluster_id": "IC-01", + "holder_name": "string", + "holder_role": "current_right_holder|current_possessor|current_user|current_benefit_holder", + "basis_text": "string", + "certainty": "final|provisional" + } + ], + + "current_control_map_4axis": [ + { + "object": "string", + "cluster_id": "IC-01", + "current_right_holder_candidate": "string|null", + "current_possessor": "string|null", + "current_user": "string|null", + "current_benefit_holder": "string|null", + "possession_basis_asserted": "string|null", + "possession_basis_transition_needed": true, + "certainty": "final|provisional|불명" + } + ], + + "standing_watchpoints": ["string"] + } + + 추가 제약: + - `defendant_target_matrix`, `issue_clusters`, `plaintiff_capacity_matrix`, `current_control_map`, `standing_watchpoints`는 optional이다. + - `domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints`, `liability_group_hint`, `commercial_successor_liability_hint`, `fractional_title_issue_hint`, `land_use_gain_hint`, `lease_payment_semantics_hint`, `preserved_claim_bundle_hint`, `value_compensation_hint`, `defense_hints`는 `issue_clusters` 내부 optional additive field다. + - `current_control_map_4axis`는 top-level optional additive field이며 legacy `current_control_map`을 대체하지 않는다. + - `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`, `preferred_plaintiff_selection_basis`, `succession_resolution_needed`는 `issue_clusters` 내부 optional additive field다. + - 기존 필수 필드는 모두 유지한다. + - 허용된 additive field 외 임의 추가 필드 금지. + + + + - `parties.plaintiffs`에는 실제 의뢰인만 남았는지 점검한다. + - `parties.plaintiffs`가 client universe로만 쓰였고, 사건군별 원고적격 확정값처럼 오염되지 않았는지 점검한다. + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않았는지 점검한다. + - `parties.defendants`와 `third_parties`가 뒤바뀌지 않았는지 점검한다. + - `summary_key_incidents`가 사건군별로 과잉 병합되지 않았는지 점검한다. + - `key_facts`가 later stage에 중요한 standing/ownership/current control fact를 누락하지 않았는지 점검한다. + - `issue_clusters`를 썼다면 각 cluster의 당사자와 desired relief가 meeting 직접 서술 범위를 넘지 않는지 점검한다. + - 상속·승계형 사건군에서 `candidate_plaintiffs`가 친족관계, 공동 통지, 공동 상담 같은 간접 신호만으로 확장되지 않았는지 점검한다. + - `issue_clusters`를 썼다면 unresolved succession cluster에서 `legal_entitlement_candidates`와 `preferred_plaintiffs_for_complaint`가 자동 동일시되지 않았는지 점검한다. + - `preferred_plaintiffs_for_complaint`를 썼다면 그 basis가 `preferred_plaintiff_selection_basis`에 남아 있고, meeting 직접 서술 또는 direct right-holder 구조를 넘지 않는지 점검한다. + - `succession_resolution_needed=true`인데도 해당 cluster가 direct owner/final cluster처럼 오염되지 않았는지 점검한다. + - `plaintiff_capacity_matrix`를 썼다면 각 row의 `capacity_basis`와 `source_basis_text`가 meeting 직접 서술 범위를 넘지 않는지 점검한다. + - `capacity_basis="상속인"` row가 친족관계나 공동행동 사실만으로 `certainty="final"`로 오염되지 않았는지 점검한다. + - 사망한 종전 권리주체의 과거 소유사실, 매수사실, 등기명의사실이 living plaintiff의 `소유자/final` row로 잘못 치환되지 않았는지 점검한다. + - `current_control_map`을 썼다면 `holder_role`이 `current_right_holder|current_possessor|current_user|current_benefit_holder` 중 하나인지와, basis가 meeting 직접 문장에 근거하는지 점검한다. + - 상속·승계형 사건군에서 `current_right_holder` row가 있으면, meeting만으로 승계가 해소되지 않은 경우 `certainty="provisional"`인지 점검한다. + - 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속이 서로 다른데 하나의 generic holder row로 뭉개지지 않았는지 점검한다. + - 사망한 종전 권리주체가 보이는 사건군이 있으면 `standing_watchpoints`에 succession-resolution trigger가 남았는지 점검한다. + - `standing_watchpoints`를 썼다면 장문 법률판단이 아니라 later stage 재검토용 짧은 경고인지 점검한다. + - meeting이 피고별로 현재 집행가능성과 장래 회수가능성을 구별하여 서술하면, 그 차이가 `constraints`, `primary_goal`, `summary_key_incidents`, `parties.defendants.asset_status`, `defendant_target_matrix` 중 어디엔가 비대칭적으로 보존되었는지 점검한다. + - "현재 무자력"과 "장래 재산취득 가능성"이 같은 사람에게 함께 서술되면, 그 사람을 자동 배제 대상으로 압축하지 않았는지 점검한다. + - `constraints`에 current collectability나 단순 회수곤란 사실이 잘못 들어가 있지 않은지 점검한다. + - `asset_status`가 제소 배제 신호처럼 쓰이지 않았는지 점검한다. + - `defendant_target_matrix`를 썼다면 각 `name`이 `parties.defendants`와 모순되지 않는지 점검한다. + - `defendant_target_matrix`에서 `target_status="exclude_now"`가 붙은 사람에게는 meeting상 명시적 no-sue 지시가 있는지 점검한다. + - `defendant_target_matrix`에서 `execution_risk="high"`와 `target_status="target_now"`가 함께 존재할 수 있음을 허용하되, 그 경우 `basis_text` 또는 `future_recovery_signal`이 비어 있지 않은지 점검한다. + - 장래 상속/재산취득 가능성이 직접 서술된 피고가 `exclude_now`로 잘못 내려가지 않았는지 점검한다. + - 금전채권 cluster에 최고ㆍ채무승인ㆍ이행각서ㆍ시효 단서가 있는데 `monetary_claim_profile.requires_money_claim_timeline`이 없거나 false이면 warning으로 본다. + - 동일 사건군에 원채무자와 승계ㆍ양수ㆍ보증ㆍ상호속용 책임 후보가 함께 있는데 `liability_group_hint`가 비어 있으면 warning으로 본다. + - 영업양도ㆍ상호속용 단서가 있는데 `commercial_successor_liability_hint.requires_article_42_review`가 없으면 warning으로 본다. + - 대여금ㆍ근저당권ㆍ경매ㆍ지분 단서가 같은 cluster에 있는데 `secured_property_dispute_profile.requires_payment_allocation`이 없으면 warning으로 본다. + - 상담문서에 지분별 권리상태가 보이는데 `fractional_title_issue_hint`가 없으면 warning으로 본다. + - 미등기 건물ㆍ임대차ㆍ건물 매매ㆍ토지 사용 단서가 있는데 `land_use_gain_hint`가 없으면 warning으로 본다. + - 사해행위취소ㆍ부담부 부동산ㆍ사후 담보권 말소 단서가 있는데 `actio_property_transfer_profile.requires_value_compensation_review`가 false이면 error로 본다. + - 같은 채권자와 채무자 사이 복수 대여금이 보이는데 `preserved_claim_bundle_hint`가 없으면 warning으로 본다. + - 명의신탁ㆍ충분담보ㆍ상계ㆍ선의 등 항변 단서가 있는데 `defense_hints`가 없으면 warning으로 본다. + - 유치권ㆍ상속ㆍ승계 사건군에서 `succession_resolution_needed=true`인데도 입력자료상 공동 통지 발신자, 친족 후보, 직계존속·직계비속 후보가 `preferred_plaintiffs_for_complaint` 확정자로 들어가면 error로 본다. + - 통지 발송일과 도달일 단서가 모두 있는데 `notice_lifecycle_hints`가 없으면 warning으로 본다. + - `current_control_map_4axis`를 썼다면 4축 중 직접 읽히는 축이 generic holder 하나로 합쳐지지 않았는지 점검한다. + - 출력 파일명과 성공 메시지가 정확한지 점검한다. + + + + + + [ ] 1) Preflight: `list_docs` 사용해서 `client_meeting.md` 확인, `read_docs` 사용해서 `client_meeting.md` 읽기 + [ ] 2) `write_file` 사용해서 `client_goal.json` 생성 + [ ] 3) `list_docs` 사용해서 `client_goal.json` 파일 존재만 확인(절대 읽기 금지) + [ ] 4) `"고객의사 초안 생성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `client_meeting.md` + + + - `client_goal.json` + + + + + - `client_goal.json`은 절대 재읽지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"고객의사 초안 생성 완료"` 한 줄만 사용한다. + + + + - task_name: Task_Evidence_shard_planner + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import hashlib + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + def _mid(): return next(MSG_ID) + def _init(): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"task-evidence-shard-planner","version":"1.0","user_id":"{{__user_hash__}}","workspace_id":"{{__workspace_hash__}}"}}}, headers=MCP_HEADERS) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post(LOCALDOCS_URL, json={"jsonrpc":"2.0","method":"notifications/initialized"}, headers=MCP_HEADERS).raise_for_status() + def _parse(text): + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: return json.loads(line[6:]) + except Exception: pass + try: return json.loads(text) + except Exception: return None + def _call(name, args): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc":"2.0","id":_mid(),"method":"tools/call","params":{"name":name,"arguments":args}}, headers=MCP_HEADERS) + r.raise_for_status() + p = _parse(r.text) + if not p or "result" not in p: raise RuntimeError(f"MCP {name} failed") + return p + def _unwrap(p, name): + text = (p["result"].get("content") or [{}])[0].get("text", "") + if not text: raise RuntimeError(f"Empty: {name}") + try: outer = json.loads(text) + except Exception: return text + if isinstance(outer, dict) and "results" in outer: + r0 = (outer.get("results") or [{}])[0] + inner = r0.get("content") or r0.get("text") or "" + if not inner: raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return json.dumps(outer, ensure_ascii=False) if not isinstance(outer, str) else outer + def read_json(name): + raw = _unwrap(_call("read_docs", {"doc_names": [name]}), name) + if not isinstance(raw, str): return raw + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + obj, _ = decoder.raw_decode(raw.strip()) + return obj + def write_doc(path, content): + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def _items(obj: Any) -> list[Any]: + if isinstance(obj, list): + return obj + if isinstance(obj, dict): + for key in ("items", "documents", "evidences", "evidence", "docs"): + val = obj.get(key) + if isinstance(val, list): + return val + raise RuntimeError("evidence_all.json must be a JSON array or an object containing a document array") + + def _text(v: Any) -> str | None: + if v is None: + return None + s = str(v).strip() + return s or None + + def _doc_title(doc: Any, ordinal: int) -> str | None: + if not isinstance(doc, dict): + return f"evidence_all[{ordinal}]" + for key in ("title", "name", "filename", "file_name", "document_title", "doc_title"): + val = _text(doc.get(key)) + if val: + return val + return f"evidence_all[{ordinal}]" + + def _doc_uid(doc: Any, ordinal: int) -> str: + if isinstance(doc, dict): + for key in ("doc_uid", "uid", "id", "document_id", "source_id"): + val = _text(doc.get(key)) + if val: + return val + return f"DOC-{ordinal:03d}" + + def _stable_hash(doc: Any) -> str: + raw = json.dumps(doc, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _list_doc_names(args) -> list[str]: + payload = _unwrap(_call("list_docs", args), "list_docs") + if isinstance(payload, str): + try: + payload = json.loads(payload) + except Exception: + return re.findall(r"[\w./-]+\.json", payload) + vals: list[str] = [] + if isinstance(payload, dict): + for value in payload.values(): + if isinstance(value, list): + for item in value: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + elif isinstance(value, str): + vals.append(value) + elif isinstance(payload, list): + for item in payload: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + return [v for v in vals if v] + + def _read_existing_doc_names() -> list[str]: + preferred = [ + {"path": "evidence_indexed_parts", "pattern": "E-*.json"}, + {"path": "evidence_event_candidate_parts", "pattern": "E-*.json"}, + ] + preferred_errors = [] + preferred_names: list[str] = [] + for args in preferred: + try: + folder = str(args["path"]).rstrip("/") + preferred_names.extend(f"{folder}/{name.split('/')[-1]}" for name in _list_doc_names(args)) + except Exception as exc: + preferred_errors.append(f"{args['path']}: {exc}") + if not preferred_errors: + return sorted(set(preferred_names)) + + fallback_errors = [] + for args in ({}, {"doc_names": []}): + try: + return sorted(set(preferred_names + _list_doc_names(args))) + except Exception as exc: + fallback_errors.append(f"{args}: {exc}") + raise RuntimeError("list_docs failed for preferred schemas and fallback schemas; stale part detection cannot safely continue: " + " | ".join(preferred_errors + fallback_errors)) + + def _stale_parts(existing_names: list[str], n: int, folder: str) -> list[str]: + stale: list[str] = [] + pattern = re.compile(rf"(?:^|/){re.escape(folder)}/E-(\d{{3}})\.json$") + for name in existing_names: + m = pattern.search(name) + if not m: + continue + ordinal = int(m.group(1)) + if ordinal < 1 or ordinal > n: + stale.append(name) + return sorted(set(stale)) + + def main(): + _init() + evidence_all = read_json("evidence_all.json") + docs = _items(evidence_all) + n = len(docs) + if n <= 0: + raise RuntimeError("evidence_all.json contains no evidence documents") + + existing_names = _read_existing_doc_names() + stale_b1 = _stale_parts(existing_names, n, "evidence_indexed_parts") + stale_b2 = _stale_parts(existing_names, n, "evidence_event_candidate_parts") + + shards = [] + streaming_edges = [] + b1_waits = [] + b2_waits = [] + b1_files = [] + b2_files = [] + for idx, doc in enumerate(docs, 1): + label = f"{idx:03d}" + evidence_id = f"E-{label}" + b1_task_id = f"Task_B1_map_doc_{label}" + b2_task_id = f"Task_B2_map_events_{label}" + b1_output = f"evidence_indexed_parts/{evidence_id}.json" + b2_output = f"evidence_event_candidate_parts/{evidence_id}.json" + # 문서별 shard 파일 — B1/B2 인스턴스가 evidence_all.json 풀 대신 자기 shard만 읽도록. + shard_doc_path = f"evidence_shards/{evidence_id}.json" + write_doc(shard_doc_path, json.dumps(doc, ensure_ascii=False, indent=2)) + b1_waits.append(b1_task_id) + b2_waits.append(b2_task_id) + b1_files.append(b1_output) + b2_files.append(b2_output) + shards.append({ + "assigned_ordinal": idx, + "ordinal_label": label, + "evidence_index_candidate": evidence_id, + "b1_task_id": b1_task_id, + "b2_task_id": b2_task_id, + "b1_output_path": b1_output, + "b2_output_path": b2_output, + "shard_doc_path": shard_doc_path, + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": idx, + "array_index": idx - 1, + "doc_uid_candidate": _doc_uid(doc, idx), + "title_candidate": _doc_title(doc, idx), + "content_sha256": _stable_hash(doc), + "shard_doc_path": shard_doc_path + }, + "runtime_bindings": { + "__assigned_ordinal__": idx, + "__ordinal_label__": label, + "__evidence_index_candidate__": evidence_id, + "__b1_part_path__": b1_output, + "__b2_part_path__": b2_output, + "__shard_doc_path__": shard_doc_path + } + }) + streaming_edges.append({ + "from": b1_task_id, + "to": b2_task_id, + "condition": {"file_exists": b1_output}, + "bind": {"__assigned_ordinal__": idx} + }) + + plan = { + "schema_version": "evidence_shard_plan.v1", + "planner_task": "Task_Evidence_shard_planner", + "created_at_utc": datetime.now(timezone.utc).isoformat(), + "source_file": "evidence_all.json", + "source_selector": "$.items || $", + "shard_count": n, + "expected_ordinals": [f"{i:03d}" for i in range(1, n + 1)], + "expected_evidence_ids": [f"E-{i:03d}" for i in range(1, n + 1)], + "ordinal_policy": { + "base": 1, + "assignment": "array_order", + "format": "%03d", + "assigned_ordinal_var": "__assigned_ordinal__", + "evidence_index_candidate": "E-{ordinal:03d}", + "no_legal_sorting": True, + "no_content_based_reordering": True + }, + "shards": shards, + # 백엔드 wildcard fan-out: dynamic_fanout 리스트의 각 항목이 한 인스턴스 컨텍스트({{item.*}}로 접근). + # shards를 그대로 사용 — 디스크립터 메타(id/count/task_template 등)는 fanout_meta로 분리 보존. + "dynamic_fanout": shards, + "fanout_meta": [ + { + "id": "B1_evidence_doc_fanout", + "count": n, + "task_template": "Task_B1_map_doc", + "instance_id_template": "Task_B1_map_doc_{ordinal:03d}", + "output_path_template": "evidence_indexed_parts/E-{ordinal:03d}.json" + }, + { + "id": "B2_event_candidate_fanout", + "count": n, + "task_template": "Task_B2_map_events", + "instance_id_template": "Task_B2_map_events_{ordinal:03d}", + "output_path_template": "evidence_event_candidate_parts/E-{ordinal:03d}.json", + "wait_until_template": "Task_B1_map_doc_{ordinal:03d}" + } + ], + "streaming_edges": streaming_edges, + "barriers": { + "B1_all_parts_barrier": { + "wait_for_tasks": b1_waits, + "require_files": b1_files, + "next": "Task_B1_gate_merge_deterministic" + }, + "B2_all_parts_barrier": { + "wait_for_tasks": b2_waits, + "require_files": b2_files, + "plus": ["Task_B1_gate_merge_deterministic"], + "next": "Task_B2_gate_merge_deterministic" + } + }, + "stale_part_policy": { + "mode": "block", + "allowed_ordinals": [f"{i:03d}" for i in range(1, n + 1)], + "checked_directories": ["evidence_indexed_parts", "evidence_event_candidate_parts"], + "detected_stale_parts": { + "evidence_indexed_parts": stale_b1, + "evidence_event_candidate_parts": stale_b2 + }, + "downstream_gate_instruction": "B1/B2 quality gates must BLOCK if detected_stale_parts is non-empty or if files outside expected_ordinals are present." + }, + "non_legal_assertions": { + "does_not_classify_document_type": True, + "does_not_extract_facts": True, + "does_not_evaluate_evidence": True, + "does_not_calculate_amounts": True, + "does_not_decide_legal_effect": True, + "does_not_finalize_evidence_index": True, + "does_not_finalize_candidate_id": True + } + } + + if stale_b1 or stale_b2: + plan["planner_status"] = "BLOCKED_BY_STALE_PARTS" + plan["mapper_execution_allowed"] = False + plan["blocked_reason"] = "stale mapper part detected; B1/B2 mapper fanout must not execute and existing parts must not be reused" + else: + plan["planner_status"] = "READY" + plan["mapper_execution_allowed"] = True + plan["blocked_reason"] = None + + write_doc("evidence_shard_plan.json", json.dumps(plan, ensure_ascii=False, indent=2)) + # 백엔드 wildcard fan-out 확장이 stdout JSON의 dynamic_fanout 키를 읽음. + # 전체 plan은 너무 커서 dynamic_fanout만 출력한다. 단 planner_status가 READY가 아니면 mapper fanout은 반드시 비운다. + fanout_payload = plan["dynamic_fanout"] if plan["planner_status"] == "READY" else [] + print(json.dumps({"dynamic_fanout": fanout_payload, "planner_status": plan["planner_status"], "mapper_execution_allowed": plan["mapper_execution_allowed"]}, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B1_map_doc_* + max_concurrency: 12 + preflight_files: + - "{{item.shard_doc_path}}" + - client_meeting.md + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + + Task_B1_map_doc + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. Your role in this task is a **per-document mapper** within the v.3 Step D streaming map-reduce pipeline. + + + 본 task는 v.3 Step D의 B1 mapper다. orchestrator 가 runtime parameter (`{{item.*}}`) 로 주입한 assigned_ordinal 단일 증거문서만 처리하여, 그 문서의 authority component·document component envelope·legal component envelope을 추출한 part 파일을 작성한다. + 본 task는 증거 문서 수 N을 계산하지 않고, ordinal을 배정하지 않고, B1/B2 mapper 인스턴스를 생성하지 않는다. 이러한 orchestration 책임은 전부 `Task_Evidence_shard_planner`와 orchestrator의 권한이다. + 최종 `evidence_indexed.json`은 본 task가 작성하지 않는다. 본 task는 `evidence_indexed_parts/E-{ordinal:03d}.json` part 파일만 작성하며, finalize는 `Task_B1_gate_merge_deterministic`가 수행한다. + 본 task는 자기 문서 안에서 직접 확인되는 component만 작성한다. 다른 문서의 사실을 자기 part에 주입하면 quality gate의 authority drift check에서 BLOCK 또는 HARD_WARNING으로 검출된다. + `evidence_index`는 planner가 부여한 `E-{ordinal:03d}` 형식의 deterministic candidate를 그대로 propose 하되, final 부여는 quality gate가 수행한다. 본 task는 candidate id를 자기 part의 `evidence_index_proposed` 필드에 기록한다. + 본 task의 추출 깊이는 v.2 원본 Task_B1과 동일하다. document component envelope의 모든 하위 구조(claim_document_components, business_transfer_components, registry_row_components, secured_debt_components, valuation_rows, mixed_contract_payment_semantics, loan_components, valuation_matrix, registry_row_role_map, defense_assertions, admission_components, defense_components, notice_components, party_assertion_components, schedule_items, registry_object_display, succession_document_components)를 자기 문서 범위 안에서 가능한 한 분리 추출한다. + 변호사 관점에서 본 단계의 핵심은 등기부 row, 차용증 component, 통지서 발송/도달일 분리, 가족관계·상속포기 component, 별지목록 등의 직접 보존이다. 이는 청구원인 timeline·청구취지 별지목록·금액 계산의 기초가 된다. + + + + + + - runtime parameters: `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.source_document_ref}}`, `{{item.b1_output_path}}` (planner 가 부여한 단일 source of truth — 별도 파일 read 불요) + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 문서 1건 — planner가 evidence_all.json에서 분리해 둔 단일 shard 파일) + - `client_meeting.md` (보조 사용만 허용) + + + - `evidence_indexed_parts/E-{ordinal:03d}.json` (본 mapper 인스턴스의 part 파일 1개만) + + + - 다른 ordinal의 문서를 처리하기 + - 다른 ordinal의 사실을 자기 part에 주입하기 (authority drift) + - 증거 문서 수 N을 재계산하여 planner 결과를 대체하기 + - assigned_ordinal을 새로 생성하거나 재정렬하기 + - B1/B2 mapper fan-out 또는 streaming edge를 설계하기 + - 최종 `evidence_indexed.json` 작성 (이는 quality gate의 단독 권한) + - 다른 mapper의 part 파일 읽기/덮어쓰기 + - root 전체 탐색 + - 이전 task 결과 또는 backend 자동 로드 데이터 사용 + - 자기 part 재읽기 + - cross-document legal conclusion 확정 + - 입력에 없는 사실 추가 + + + + + 1. `list_docs`로 `{{item.shard_doc_path}}`, `client_meeting.md` 존재만 확인한다. + 2. `read_docs`로 위 두 파일을 읽는다. `{{item.shard_doc_path}}`는 이 인스턴스의 단일 evidence 문서로 그대로 사용한다 (별도의 ordinal 추출 불필요). assigned_ordinal·evidence_index_candidate·b1_output_path 는 runtime parameter (`{{item.*}}`) 그대로 사용한다. + 3. 메모리에서 part item 객체를 생성한다(items 배열이 아닌 단일 item object 또는 1-element items 형태). + 4. `write_file(overwrite=true)`로 runtime parameter 가 지정한 `evidence_indexed_parts/E-{ordinal:03d}.json`을 저장한다. + 5. `list_docs`로 자기 part 파일 존재만 확인한다. 절대 읽지 않는다. + 6. 정확히 `"E-{ordinal:03d} mapper part 작성 완료"`만 출력하고 종료한다. + + + + - 입력 누락, 빈 파일, runtime parameter 누락, ordinal 매핑 실패, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. part 파일은 쓰지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + - tool로 읽은 내용만 사실로 취급한다. + + + + - 본 mapper 인스턴스는 정확히 runtime parameter (`{{item.assigned_ordinal}}`) 에 해당하는 1개 문서만 처리한다. + - runtime parameters (`{{item.*}}`) 는 ordinal/binding 의 단일 source of truth 다 (별도 plan 파일 read 불요). + - `evidence_all.json` 배열 순서를 절대 바꾸지 않고, 배열 순서를 근거로 새 plan을 만들지도 않는다. + - planner의 `source_document_ref.content_sha256` 또는 `doc_uid_candidate`가 현재 문서와 명백히 충돌하면 part 파일을 쓰지 않는다. + - 본 task는 planner가 확정한 `evidence_index_candidate`, `b1_output_path`, assigned_ordinal을 소비할 뿐 재작성하지 않는다. + - 다른 ordinal의 문서 내용을 현재 문서에 보충하지 않는다(authority drift 금지). + - unknown 처리: + - nullable scalar → `null` + - 배열 → `[]` + - 비nullable string만 `"불명"` 사용 + - 배열은 exact-match 중복 제거 후 최초 순서를 유지한다. + - part 파일은 단일 item object 형태이며, 기존 필수 필드(`evidence_index_proposed`, `doc_uid`, `title`, `title_normalized`, `doc_type`, `key_*`, `source_pointer`)를 모두 포함한다. + - optional additive field는 downstream(quality gate)이 무시해도 안전해야 한다. + - `evidence_index_proposed`는 deterministic 규칙 `E-{ordinal:03d}`로만 생성한다. 임의 변형 금지. + + + + 이 인스턴스의 `{{item.shard_doc_path}}`(planner가 evidence_all.json에서 분리한 단일 evidence 1건) + `client_meeting.md` (보조만 허용) + + + `client_meeting.md`는 아래 목적에만 사용할 수 있다. + - `asset_id` + - `property_label_for_schedule` + - `transaction_type` + - `transaction_date` + - `market_value_at_act` + - `market_value_at_close` + - `consideration_breakdown` + - `property_cluster_id` + - 현재 문서의 축약 명칭이 어떤 자산군을 가리키는지에 관한 shorthand 해석 + + + + - 현재 문서와 `client_meeting.md`가 충돌하면 현재 문서가 우선한다. + - 예외: 등기부상 형식적 원인이 `매매`여도, 현재 문서 특약 또는 `client_meeting.md`가 실질적으로 채무갈음/대물변제를 직접 드러내면 `transaction_type`은 `"대물변제"`로 할 수 있다. + - 단, 이 예외는 현재 문서 또는 meeting이 실질 원인을 직접적으로 드러내는 경우에만 적용한다. + + + + + - `ordinal` = `evidence_all.json`의 1-based 배열 위치 (assigned_ordinal과 동일) + - `evidence_index_proposed` = `E-{ordinal:03d}` (mapper propose; quality gate가 finalize) + - `title` = 현재 문서 제목 원문 그대로 + - `title_normalized` = 앞뒤 공백 제거 + 내부 연속 공백/줄바꿈을 1칸으로 축약 + - `doc_uid` = `DOC-{ordinal:03d}-{title_normalized의 공백을 "_"로 치환한 값}` + - `source_pointer` = `{"source":"evidence_all.json","ordinal":ordinal}` + + + + 다음 순서로만 핵심을 읽는다. + 1. `title` + 2. `heading` + 3. `key_value` / `form` + 4. 관련 `table` 행 + 5. `list` + 6. `paragraph` + + 보일러플레이트(바코드 안내, 발급확인 문구, 반복 footer/page text)는 무시한다. + 단, `관할등기소`, `접수번호`, `등기원인일`, `발행일` 등 필요한 사실이 있으면 사용한다. + + + + `doc_type` enum은 v.2 원본과 동일하다. nuance는 `doc_semantic_flags`로 보강한다. + 1. `판결|결정|배당표|가압류|가처분|경매|타경` → `"판결/결정"` + 2. `등기사항전부증명서|주민등록|가족관계증명서|사업자등록증` → `"공문서"` + 3. `대출|명세|거래|계산서` → `"거래기록"` + 4. `카톡|문자|이메일|통화` → `"통신기록"` + 5. `계약서|약정서|확인서|영수증|차용증|각서` → `"처분문서"` + 6. 그 외 → `"기타"` + + + + - `key_facts`: 최대 3개, 각 60자 이하 + - `key_dates`: 최대 3개 + - `key_amounts`: 최대 3개 + - `key_parties`: 최대 5개 + - 현재 문서에서 직접 읽히는 핵심만 남긴다. + - 장문 요약, 계산식, 추정 보정 금지. + - 가능하면 heading / key_value / 표의 핵심 행만 사용한다. + + + + + - `property_cluster_id`는 optional field다. + - 현재 문서만으로 특정 자산군이 명백하거나, 현재 문서의 표기와 `client_meeting.md`가 정확히 대응할 때만 채운다. + - 예: `"해당 대지/건물"`, `"해당 상가"`, `"해당 주거용 부동산"`, `"해당 임야"`, `"해당 영업양도"`, `"해당 물품대금"`. + - 명백하지 않으면 `null`. + + + + `doc_semantic_flags`는 optional field다. 아래 값만 사용한다. + - `multi_event_doc` + - `registry_doc` + - `mixed_contract_doc` + - `valuation_doc` + - `claim_doc` + - `status_doc` + - `notice_doc` + - `response_doc` + - `business_transfer_doc` + - `supporting_reference_doc` + - `admission_doc` + - `defense_doc` + - `notice_receipt_admission_doc` + - `lien_assertion_doc` + - `schedule_doc` + - `relief_object_reference_doc` + - `succession_doc` + + 부여 규칙: + - 등기부처럼 복수 row가 서로 다른 법적 효과를 가지면 `multi_event_doc` + - 갑구/을구 등기행위가 있으면 `registry_doc` + - 임대차와 매매 등 서로 다른 계약 구성요소가 같은 문서에 공존하면 `mixed_contract_doc` + - 시가, 차임시세, 사용이익 가치가 표로 제시되면 `valuation_doc` + - 차용증, 보증서, 대여약정 등 채권 발생 문서면 `claim_doc` + - 답변서, 사업자등록증, 현재 상태 확인서처럼 상태를 보여주면 `status_doc` + - 내용증명, 통지서, 최고서, 해제통지서면 `notice_doc` + - 상대방 답신, 답변서, 반박문이면 `response_doc` + - 영업양도 문서면 `business_transfer_doc` + - 별지 목록처럼 단독 계산객체는 없으나 다른 문서 식별을 돕는 문서면 `supporting_reference_doc` + - 상대방이 통지 수령, 임대, 차임, 점유, 채무, 변제 등을 인정하면 `admission_doc` + - 명의신탁, 충분담보, 상계, 선의, 후소유자 소멸청구권 부정, 피담보채권 충당 등 항변이 있으면 `defense_doc` + - 유치권 소멸청구 통지의 수령일을 인정하면 `notice_receipt_admission_doc` + - 유치권 주장, 공사잔금, 유치권 목적물 사용·임대가 있으면 `lien_assertion_doc` + - 별지목록, 목적물 목록, 청구취지에서 그대로 사용할 문구가 있으면 `schedule_doc` 및 `relief_object_reference_doc` + - 가족관계증명서, 상속포기 심판문, 사망·상속 관련 공문서이면 `succession_doc` + + + + - 등기부 문서에서 갑구/을구 여러 row가 서로 다른 법적 효과를 가지면 `multi_event_registry=true`를 optional field로 기록한다. + - `multi_event_registry=true`인 문서는 단일 snapshot 요약을 강요하지 않는다. + + + + + - 문서가 여러 법률효과를 담고 있으면 component 단위로 분해한다. + - 날짜는 작성일, 발송일, 도달일, 법률효과 발생일, 변제기, 기산일 후보로 의미별 분리한다. + - 금액은 원금, 이자, 보증금, 차임, 사용이익, 채권최고액, 실제 변제액, 가액배상 공제액으로 성격별 분리한다. + - 아래 component field들은 optional additive field이며, 현재 문서에 직접 읽히는 범위에서만 사용한다. + + + - 문서가 이행각서, 채무승인서, 최고서, 내용증명, 지급요구서, 변제약정서, 차용증, 납품대금 확인서이면 `claim_document_components`를 생성한다. + - 단순 `key_dates`에 문서작성일만 넣고 끝내지 말고, 문서 안에 지급기일, 새 변제기, 최고 도달일, 채무승인일, 청구금액, 채권자, 채무자가 직접 읽히면 component field로 분해한다. + - 이행각서에서 "언제까지 지급한다"는 문구가 있으면 반드시 `new_due_date` 후보로 분리한다. + - 최고서에서 발송일과 수령일/도달일이 모두 있으면 `dispatch_date`와 `arrival_date`를 분리한다. + - claim component의 `component_kind` 허용값은 `underlying_claim|debt_acknowledgement|demand_notice|notice_dispatch|notice_arrival|new_due_date|payment_promise|limitation_relevance|other`다. + + + + - 영업양도계약서, 상호 계속 사용, 사업 양수인, 영업자산 이전, 고객·직원 승계, 채무불승계 조항, 책임배제 등기 단서가 있으면 `business_transfer_components`를 생성한다. + - `business_name`, `transferor_candidates`, `transferee_candidates`, `business_assets_transferred`, `trade_name_continuation_clause`, `customer_or_employee_succession_clause`, `pre_transfer_debt_exclusion_clause`, `liability_exclusion_registration_clause`를 직접 읽히는 범위에서 채운다. + - 기존 `contract_components`가 있어도 영업양도 책임 검토용 구조는 `business_transfer_components`에 별도 보존한다. + + + + - 등기부, 등기사항전부증명서, 등기내역 문서는 반드시 row 단위로 쪼갠다. + - 각 row마다 갑구/을구, 순위번호, 등기목적, 접수일, 접수번호, 원인, 권리자, 채무자, 채권최고액, 객체 범위를 추출하여 `registry_row_components`에 둔다. + - 청구취지 특정에 필요한 접수번호는 `key_facts` 요약에만 두지 말고 `registry_row_components[*].receipt_no`로 보존한다. + - 차용증, 근저당권 설정계약, 등기부 row가 연결되면 `secured_debt_components`를 생성한다. + - 복수 채무의 원금, 이율, 변제기, 담보 지분이 다르면 법정변제충당 계산 대상이므로 모두 구조화한다. + - 사해행위취소 사건의 등기부 row는 필요하면 `registry_row_role_map`에 actio role을 추가로 남긴다. + - `registry_row_components`의 각 row에 `registry_ref`를 부여한다. 형식: `REG-{ordinal:03d}-{row_seq:02d}` (갑구/을구·순위번호 순 deterministic). + - row에 지분 표시(예: "2분의 1", "1/2 지분")가 직접 읽히면 `registry_row_components[*].fraction_display`(원문 문자열)와 `registry_row_components[*].fraction_ref`를 채운다. + - `fraction_ref`는 `FR-{ordinal:03d}-{row_seq:02d}` 형식의 deterministic propose 값이다. 같은 row에서 파생되는 모든 component는 같은 `fraction_ref`를 공유한다. + - 지분 표시의 분자·분모가 직접 읽히는 경우에만 optional 필드 `fraction_numerator`/`fraction_denominator`를 채운다(예: "2분의 1" → 1/2). 환산·약분·보간 금지. 직접 읽히지 않으면 null. + + + + - 감정표는 금액만 추출하지 말고 `valuation_rows`에 row별 전제조건, 금액, 단위, 법률상 사용 가능성을 함께 추출한다. + - 사해행위취소 valuation 문서가 복수 자산 또는 복수 시점을 담으면 `valuation_matrix`를 생성하고, 각 row의 `legal_uses`를 표시한다. + - 임대차보증금, 월 차임, 매매대금이 한 문서에 같이 있으면 `mixed_contract_payment_semantics`에서 각각 별도 payment semantic으로 분리한다. + - 보증금은 원칙적으로 반환 예정 담보성 금원인지 검토하고, 원고가 직접 수령하지 않았다면 원고의 부당이득 원금으로 자동 전환하지 않는다. + - 월 차임은 피고들 사이의 계약상 차임과 객관적 토지 사용이익을 구분한다. + + + + - 사해행위취소 사건의 증거문서는 일반 key_facts 요약만으로 충분하지 않다. + - 차용증에 복수 대여금이 있으면 `loan_components`로 분리하고, 각 원금·변제기·이자율·담보 목적물·청구 내 역할을 추출한다. + - 감정평가서는 시점별 `valuation_matrix`로 보존하고, 각 row의 legal use를 표시한다. + - 등기부는 갑구/을구 row별로 쪼개고, 각 row의 `actio_roles`를 부여한다. + - 내용증명 답신에 명의신탁, 충분담보, 상계, 선의 주장이 있으면 `defense_assertions`로 추출한다. + - 사후 말소된 담보권과 그 말소를 유발한 변제액은 가액배상 계산의 필수 입력으로 표시한다. + + + + - `loan_components`의 각 항목은 아래 named 필드를 가진다. 직접 읽히지 않는 필드는 null. + { + "loan_component_id": "E-{ordinal:03d}-LC-{seq:02d}", + "principal_amount": null, + "loan_date": null, + "due_date": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "rate_text": null, + "rate_source_locator": null, + "interest_accrual_start_hint": null, + "collateral_object_hint": null, + "role_in_claim_hint": null + } + - `agreed_interest_rate`는 원문 그대로의 문자열("연 5%", "월 1%" 등)만 허용한다. + - `agreed_delay_damage_rate`는 약정 지연손해금율이다. 없으면 null로 둔다(법정이율로 보충 금지). + - `rate_text`는 이율 관련 원문 문자열 그대로, `rate_source_locator`는 그 이율이 읽힌 조항 locator(예: "제2조", "특약")다. + - `agreed_interest_rate`/`agreed_delay_damage_rate`는 계산·환산·보간 금지. 원문 문자열만 허용. + - `secured_debt_components`의 각 항목에도 동일한 optional rate 필드(`agreed_interest_rate`, `agreed_delay_damage_rate`, `rate_text`, `rate_source_locator`, `due_date`)를 직접 읽히는 범위에서 둘 수 있다(근저당권 설정계약서·차용증 겸용 문서의 이율 보존용). + - 이율이 문서에 있는데 두 rate 필드가 모두 null이면 self-warning으로 본다. + + + + - 답변서에 통지 수령 인정, 제3자 임대 자인, 월 차임 자인, 유치권자 사용 허용 주장, 후소유자 소멸청구권 부정 주장, 피담보채권 충당 주장이 있으면 `admission_components`와 `defense_components`로 분리한다. + - 통지서에는 `notice_components`를 생성하고, 통지 유형, 발송일, 도달 필요 여부, 수령자, 목적물, 법률효과 후보를 분리한다. + - `notice_components`의 각 항목은 아래 named 필드를 가진다. 직접 읽히지 않는 필드는 null. + { + "notice_id_proposed": "E-{ordinal:03d}-NT-{seq:02d}", + "notice_type": "최고|해제통지|유치권소멸청구통지|기타", + "drafted_date": null, + "dispatch_date": null, + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [], + "receiver_candidates": [], + "target_object_hint": null, + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "legal_effect_candidate_hint": null + } + - 5개 lifecycle 날짜의 의미: `drafted_date`=작성일, `dispatch_date`=발송일, `arrival_date`=도달일(민법 제111조 도달주의 — 도달≠수령을 구분), `received_date`=수령일(수령 인정 포함; 도달일과 다르면 별도 보존), `legal_effect_date_candidate`=법률효과 발생 후보일(계산 금지, 문서 직접 기재만). + - `requires_receipt_for_effect`는 true|false|null — 문서 유형상 도달/수령이 효력요건인지의 후보 표시다. + - `receipt_evidence_refs`는 수령을 뒷받침하는 같은 문서 내 단서 locator다. + - `recipient_admission_text`는 통지 수령을 인정하는 원문 문자열이며, 답변서 문서의 component에서만 채운다. + - 수령일이 상대방 답변서에서 인정되는 경우 그 답변서의 `admission_components`에 남기고, 본 통지서 문서의 `received_date`에 타 문서 사실을 주입하지 않는다(authority drift 금지). cross-document 연결은 Part 2 B5가 수행한다. + - 당사자가 요구한 기간이나 금액이 법률상 기산일로 자동 사용되면 안 되는 경우 `party_assertion_components.should_not_auto_use_as_legal_start_date=true`를 남긴다. + - 별지목록에는 `schedule_items`를 생성하고, 청구취지에서 그대로 사용할 수 있는 문구와 asset alias, 전유부분, 대지권, 도로명주소를 보존한다. + - `schedule_items`의 각 항목에 `schedule_ref`를 부여한다. 형식: `SCH-{ordinal:03d}-{seq:02d}` (별지 기재 순서 deterministic). asset alias 문자열은 기존대로 함께 보존한다. + - `schedule_items`의 각 항목에 optional 필드 `canonical_label_candidate`(별지 표제 문구 원문)와 `usable_in_relief`("yes|no|unknown" — 청구취지에서 그대로 사용할 수 있는 문구인지)를 직접 읽히는 범위에서 둘 수 있다. + - 등기부가 목적물 특정 자료로 쓰이면 `registry_object_display`를 생성한다. + - 가족관계증명서, 상속포기 심판문, 사망·상속 자료에는 `succession_document_components`를 생성하고 피상속인, 사망일, 배우자 후보, 직계비속 후보, 직계존속 후보, 상속포기자, 신고일·수리일, 연결 asset을 보존한다. + + + + + + - 기존 optional field `legal_calculation_object`는 backward-compatible summary field로 유지한다. + - 그러나 이 필드는 안전할 때만 사용한다. + - 문서가 `multi_event_doc` 또는 `multi_event_registry=true`이면 원칙적으로 `legal_calculation_object`를 생성하지 않는다. + - 문서에 서로 다른 법적 효과의 slot이 2개 이상 공존하면 단일 summary `legal_calculation_object`로 압축하지 않는다. + - 단일 효과가 명백한 문서, 또는 서로 다른 값이 경쟁하지 않는 문서에서만 `legal_calculation_object`를 생성한다. + + + + - multi-event 또는 mixed-contract 문서를 위해 optional field `legal_calculation_objects`를 추가할 수 있다. + - `legal_calculation_objects`는 현재 문서 안의 개별 slot을 권위 있게 분해한 배열이다. + - 문서당 0개 이상 허용한다. + - 단일 slot 문서라면 `legal_calculation_objects`를 생략할 수 있다. + - 각 slot은 현재 문서의 직접 기재만 사용한다. 계산, 합산, 차감 금지. + + + + 개별 slot은 아래 경우에 생성한다. + - 소유권이전 / 매매 / 증여 / 대물변제 / 경매매각 / 경매개시결정 / 근저당권설정 / 근저당권말소 / 가압류 / 임대차 / 영업양도 / 대여 / 담보제공 + - valuation 문서는 시가표/차임표/사용이익표를 slot이 아니라 `valuation_object`로 처리한다. + - 등기부는 갑구/을구의 의미 있는 row마다 1 slot씩 생성한다. + - 혼합계약서는 계약 조항상 독립된 법적 효과마다 1 slot씩 생성한다. + + 각 slot은 아래 구조를 따른다. + { + "slot_id": "E-{ordinal:03d}-LCO-01", + "slot_kind": "ownership_transfer|gift|sale|dation|mortgage_setting|mortgage_cancellation|auction_commencement|auction_sale|lease|building_sale|loan|business_transfer|other", + "source_locator": { + "locator_type": "heading|key_value|table_row|paragraph|unknown", + "locator_hint": "갑구4|을구1|제1조|특약 등", + "excerpt": "짧은 단서" + }, + "asset_id": null, + "property_label_for_schedule": null, + "transaction_type": "매매|증여|대물변제|null", + "transaction_date": null, + "registration_date": null, + "registry_office": null, + "registry_receipt_no": null, + "registry_recorded_transfer_date": null, + "market_value_at_act": null, + "market_value_at_close": null, + "sale_price": null, + "monthly_rent": null, + "consideration_breakdown": [], + "parties": [] + } + - slot이 등기부 row 또는 별지 항목에서 직접 파생되면 optional 필드 `registry_ref_hint`/`schedule_ref_hint`를 남긴다. + + + + - 임대차와 매매가 함께 있는 혼합계약서, 영업양도와 채무불인수 조항이 함께 있는 문서 등은 optional field `contract_components`를 추가할 수 있다. + - `contract_components`는 현재 문서의 조항별 법적 효과 분해 메모다. + - 각 component는 아래 구조를 따른다. + { + "component_id": "E-{ordinal:03d}-CC-01", + "component_kind": "lease|sale|delivery|management_use_shift|permit_cooperation|business_transfer|debt_non_assumption|other", + "object_spec": "짧은 목적물", + "amount": null, + "date": null, + "payer_candidates": [], + "recipient_candidates": [], + "possession_recipient_candidates": [], + "benefit_shift_to_candidates": [], + "source_locator": { + "locator_type": "paragraph|table_row|key_value|unknown", + "locator_hint": "제1조|제2조|특약 등", + "excerpt": "짧은 단서" + } + } + - 혼합계약서에서 component가 2개 이상 명백하면 2개 이상 생성한다. + - mixed contract에 임대차, 매매, 인도, 관리·사용 귀속이 직접 적혀 있으면 이를 lease/sale 1개로 뭉개지지 말고 가능한 한 분리한다. + - 조항에서 누가 대가를 지급하고 누가 수령했는지가 직접 읽히면 `payer_candidates`, `recipient_candidates`를 채운다. + - 인도받는 자가 직접 읽히면 `possession_recipient_candidates`를 채운다. + - 관리·사용 또는 사용수익 귀속 주체가 직접 읽히면 `benefit_shift_to_candidates`를 채운다. + - 위 directionality field는 현재 문서에 직접 적힌 경우에만 채우고, 추정으로 보충하지 않는다. + + + + - 시가, 차임시세, 사용이익 평가 문서는 optional field `valuation_object`를 추가할 수 있다. + - `valuation_object`는 아래 구조를 따른다. + { + "valuation_kind": "market_value|rent_value|use_gain_value", + "target_object": "짧은 목적물", + "basis_date_or_period": "원문 기간 또는 기준일", + "value_rows": [ + { + "label": "2022.1.1.~2022.12.31.|보증금 3억 원인 경우 등", + "amount": "금액 문자열", + "unit": "총액|월액|기타" + } + ] + } + - valuation 문서는 기존 `doc_type`를 바꾸지 않고 `doc_semantic_flags`와 `valuation_object`로 구체화한다. + - 차임표/사용이익표가 있으면 `market_value_*`를 억지로 채우지 않는다. + + + + + - 날짜: 정확한 일자가 있으면 `YYYY-MM-DD`, 없으면 `null` + - 단, `key_dates`와 `valuation_object.basis_date_or_period`는 짧은 원문 텍스트 허용 + - 금액: 입력에 명시된 값만 `"300,000,000원"` 형식 문자열로 정규화 + - 이름/당사자: 원문 우선, 명백히 동일할 때만 canonicalize + - `consideration_breakdown`는 직접 읽히는 짧은 대가 항목만 저장, 최대 4개, 각 40자 이하 + + + + part 파일은 단일 item object 형태이며, top-level에 mapper가 propose하는 모든 component field를 둔다. + + { + "evidence_index_proposed": "E-{ordinal:03d}", + "doc_uid": "DOC-{ordinal:03d}-...", + "title": "...", + "title_normalized": "...", + "doc_type": "판결/결정|공문서|거래기록|통신기록|처분문서|기타", + "key_facts": [], + "key_dates": [], + "key_amounts": [], + "key_parties": [], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": ordinal + }, + "property_cluster_id": null, + "doc_semantic_flags": [], + "multi_event_registry": null, + "contract_components": [], + "valuation_object": null, + "legal_calculation_object": null, + "legal_calculation_objects": [], + "claim_document_components": [], + "business_transfer_components": [], + "registry_row_components": [], + "secured_debt_components": [], + "valuation_rows": [], + "mixed_contract_payment_semantics": {}, + "loan_components": [], + "valuation_matrix": {}, + "registry_row_role_map": {}, + "defense_assertions": [], + "admission_components": [], + "defense_components": [], + "notice_components": [], + "party_assertion_components": [], + "schedule_items": [], + "registry_object_display": {}, + "succession_document_components": null, + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } + } + + 추가 제약: + - top-level은 단일 object다. envelope이 아닌 단일 item. + - `evidence_index_proposed`는 mapper의 propose 식별자이며, finalize는 quality gate가 한다. + - `mapper_meta.self_isolation_attested=true`는 본 mapper가 자기 문서 외 사실을 주입하지 않았음을 self-attest 한다는 의미다. + - optional component field는 직접 읽히는 범위에서만 채우고, 빈 배열/객체/`null`로 두는 것을 우선한다. + + + + - part 파일이 단일 object이고 `evidence_index_proposed`, `doc_uid`, `title`, `title_normalized`, `doc_type`, `source_pointer`를 모두 포함하는지 점검한다. + - `planner_status=="READY"` 및 `mapper_execution_allowed=true`가 확인되지 않았으면 part 파일이 작성되지 않았는지 점검한다. + - `evidence_index_proposed`가 정확히 `E-{ordinal:03d}` 형식인지 점검한다. + - 자기 문서 외 ordinal의 사실이 part에 주입되지 않았는지 self-check 한다(authority drift 방지). + - `registry_doc`이면서 서로 다른 법적 효과 row가 2개 이상 읽히면 `doc_semantic_flags`에 `multi_event_doc`를 넣고, 가능한 경우 `legal_calculation_objects`를 생성한다. + - `multi_event_registry=true` 또는 `doc_semantic_flags`에 `multi_event_doc`가 있으면 단일 `legal_calculation_object`를 억지로 채우지 않는다. + - `mixed_contract_doc`인데 명백한 독립 component가 2개 이상이면 `contract_components`를 2개 이상 생성한다. + - `mixed_contract_doc`에 인도 또는 관리·사용 귀속이 직접 적혀 있으면 `delivery` 또는 `management_use_shift` component가 누락되지 않았는지 점검한다. + - 혼합계약 조항에서 지급자·수령자 또는 점유 이전 상대방이 직접 읽히면 `payer_candidates`, `recipient_candidates`, `possession_recipient_candidates`, `benefit_shift_to_candidates`가 비어 있지 않은지 점검한다. + - `valuation_doc`인데 가치표/차임표가 직접 보이면 `valuation_object`를 생성한다. + - 같은 문서에서 복수 행위가 보이는데도 단일 snapshot `legal_calculation_object`만으로 압축하지 않는다. + - `claim_doc`인데 이행각서ㆍ채무승인ㆍ최고ㆍ변제기ㆍ청구금액 단서가 직접 보이고 `claim_document_components`가 없으면 self-warning으로 본다. + - 이행각서에 새 변제기가 있는데 `new_due_date`가 없으면 self-warning으로 본다. + - 최고서 또는 통지서에 도달·수령 단서가 있는데 `arrival_date` 또는 notice receipt component가 없으면 self-warning으로 본다. + - `business_transfer_doc`에 상호ㆍ영업명ㆍ영업자산ㆍ채무불승계 단서가 있는데 `business_transfer_components`가 없으면 self-warning으로 본다. + - 등기부에 접수번호가 있는데 `registry_row_components[*].receipt_no`가 없으면 self-error로 본다. + - 차용금 문서에 이율 또는 변제기가 있는데 `secured_debt_components` 또는 `loan_components`가 없으면 self-warning으로 본다. + - `loan_components`가 있는데 이율 문구가 문서에 보이면서 `agreed_interest_rate`가 null이면 self-warning으로 본다. + - valuation 문서에 복수 시점 또는 복수 row가 있는데 `valuation_rows[*].legal_use_case` 또는 `valuation_matrix.rows[*].legal_uses`가 없으면 self-warning으로 본다. + - 임대차보증금이 추출되었는데 `mixed_contract_payment_semantics`에서 반환성 보증금과 사용이익 원금 배제 여부가 구분되지 않으면 self-warning으로 본다. + - 등기부 문서에 사후 말소 row가 있는데 `registry_row_role_map` 또는 `registry_row_components`에 release role이 없으면 self-error로 본다. + - 답변서에 수령 인정ㆍ제3자 임대ㆍ항변이 있는데 `admission_components` 또는 `defense_components`가 없으면 self-warning으로 본다. + - 별지목록에 목적물 항목이 있는데 `schedule_items`가 없으면 self-error로 본다. + - 상속자료가 있는데 `succession_document_components`가 없으면 self-warning으로 본다. + - self-warning/self-error는 mapper가 part 파일에 그대로 남겨도 되며, quality gate가 최종 severity를 판정한다. + + + + + {{item.assigned_ordinal}} + + + [ ] 1) Preflight: `list_docs` 사용해서 `{{item.shard_doc_path}}`, `client_meeting.md` 만 확인, `read_docs` 사용해서 두 파일만 읽기 + [ ] 2) runtime parameter `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.b1_output_path}}` 그대로 소비. 별도 검증 read 불요. + [ ] 3) `{{item.shard_doc_path}}`의 단일 evidence 문서를 메모리에서 사용하고, 주입된 shard 밖으로 나가지 않기 + [ ] 4) `write_file` 사용해서 `evidence_indexed_parts/E-{ordinal:03d}.json` 생성 (ordinal은 `{{item.assigned_ordinal}}`) + [ ] 5) `list_docs` 사용해서 자기 part 파일만 존재 확인(절대 읽기 금지) + [ ] 6) `"E-{ordinal:03d} mapper part 작성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 1건; planner가 evidence_all.json에서 분리) + - `client_meeting.md` + + + - `evidence_indexed_parts/E-{ordinal:03d}.json` + + + + + - `evidence_index_proposed`는 정확히 `E-{ordinal:03d}` 형식만 허용한다. + - `ordinal`과 output path는 runtime parameter (`{{item.*}}`) 를 그대로 따른다. + - 증거 문서 수 N, assigned_ordinal, mapper fan-out, streaming edge는 본 task에서 만들지 않는다. + - 자기 part 파일은 절대 재읽지 않는다. + - 다른 ordinal 문서를 처리하지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"E-{ordinal:03d} mapper part 작성 완료"` 한 줄만 사용한다. + + + + - task_name: Task_B2_map_events_* + max_concurrency: 12 + preflight_files: + - "{{item.shard_doc_path}}" + - "{{item.b1_output_path}}" + - client_meeting.md + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + + Task_B2_map_events + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. Your role in this task is a **per-document event/state candidate mapper** within the v.3 Step D streaming map-reduce pipeline. + + + 본 task는 v.3 Step D의 B2 mapper다. orchestrator 가 runtime parameter (`{{item.*}}`) 로 주입한 assigned_ordinal 단일 문서의 event/state candidate(raw event + legal-effect event 포함)를 추출하여 part 파일을 작성한다. + 본 mapper 인스턴스는 orchestrator가 같은 ordinal의 B1 mapper(Task_B1_map_doc) 완료와 `evidence_indexed_parts/E-{ordinal:03d}.json` 생성을 확인한 뒤 시작한다. 본 task는 streaming edge를 설계하지 않고, planner/orchestrator가 이미 결정한 same-ordinal edge를 소비한다. + 최종 `evidence_event_candidates.json`은 본 task가 작성하지 않는다. 본 task는 `evidence_event_candidate_parts/E-{ordinal:03d}.json` part 파일만 작성하며, finalize는 `Task_B2_gate_merge_deterministic`가 수행한다. + 본 task는 같은 ordinal의 B1 part 파일과 planner shard row를 authority metadata로 참조하고, planner가 분리해 둔 `{{item.shard_doc_path}}` 의 raw 문서 1건을 메모리에서 사용한다. + 같은 문서 내 복수 legal-effect event는 분리한다. 문서 밖 사실을 candidate payload에 넣지 않는다(authority drift 금지). + `candidate_id`는 `EVT-{ordinal:03d}-{seq:02d}` 형식의 deterministic candidate로 propose 하되, final 부여 및 collision 처리는 quality gate가 수행한다. + 변호사 관점에서 본 단계의 핵심은: (a) 등기부 갑구/을구 row별 분리, (b) 혼합계약서의 계약성립/대가수수/인도/관리·사용 귀속 4종 분리, (c) 통지 발송일과 도달일의 분리 candidate, (d) 답변서의 admission/defense 분리 candidate, (e) valuation 문서의 시점별/지표별 candidate, (f) 가족관계·상속포기 문서의 succession candidate 분리다. + legal-effect event는 raw event와 함께 추출하며, payment, notice, auction, lien, succession, valuation, defense assertion, commercial successor liability에 대해 v.2 원본의 REQUIRED_EVENT_SUBKINDS를 그대로 따른다. + + + + + + - runtime parameters: `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.b1_output_path}}`, `{{item.b2_output_path}}` (planner 가 부여한 단일 source of truth — 별도 plan 파일 read 불요) + - `{{item.b1_output_path}}` (같은 ordinal 의 B1 mapper part — runtime parameter 그대로 사용) + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 문서 1건; planner가 evidence_all.json에서 분리해 둔 단일 shard 파일) + - `client_meeting.md` (보조 사용만 허용) + + + - `evidence_event_candidate_parts/E-{ordinal:03d}.json` (본 mapper 인스턴스의 part 파일 1개만) + + + - 다른 ordinal의 B1 part 또는 raw 문서 처리 + - 다른 ordinal의 사실을 자기 part에 주입하기 (authority drift) + - 증거 문서 수 N을 재계산하여 planner 결과를 대체하기 + - assigned_ordinal을 새로 생성하거나 재정렬하기 + - B1/B2 mapper fan-out 또는 streaming edge를 설계하기 + - 최종 `evidence_event_candidates.json` 작성 (이는 quality gate의 단독 권한) + - 다른 mapper의 part 파일 읽기/덮어쓰기 + - 자기 part 재읽기 + - 문서 summary를 다시 쓰는 작업으로 변질시키기 + - meeting만으로 문서에 없는 사건행위를 새로 만들기 + - 입력에 없는 날짜, 당사자, 금액, 목적물을 창작하기 + + + + + 1. `list_docs`로 `{{item.b1_output_path}}`, `{{item.shard_doc_path}}`, `client_meeting.md` 존재만 확인한다. + 2. `read_docs`로 위 세 파일을 읽는다. `{{item.shard_doc_path}}`는 이 인스턴스의 단일 evidence 문서로 그대로 사용한다 (별도의 ordinal 추출 불필요). assigned_ordinal·evidence_index_candidate·b2_output_path 는 runtime parameter (`{{item.*}}`) 그대로 사용한다. + 3. 메모리에서 part 객체를 생성한다. 단일 item object 또는 1-element items 형태이며, event 후보가 0건이어도 파일을 생략하지 말고 `zero_event_reason`을 반드시 가진다. + 4. `write_file` 전에 아래 을 수행한다. 불량이면 먼저 내부 보정을 시도한다. + 5. 보정 후에도 schema invariant가 불량이면 part 파일을 쓰지 않고 `FAILED: E-{ordinal:03d} event candidate part schema invariant violation`만 보고하고 종료한다. + 6. 통과한 경우에만 `write_file(overwrite=true)`로 runtime parameter 가 지정한 `evidence_event_candidate_parts/E-{ordinal:03d}.json`을 저장한다. + 7. `list_docs`로 자기 part 파일 존재만 확인한다. 절대 읽지 않는다. + 8. 정확히 `"E-{ordinal:03d} event candidates part 작성 완료"`만 출력하고 종료한다. + + + + - 입력 누락, 빈 파일, runtime parameter 누락, B1 part 부재, ordinal 매핑 실패, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. part 파일은 쓰지 않는다. + - event 후보가 0건인 것은 실패가 아니다. 이 경우에도 runtime parameter 가 지정한 output path에 `zero_event_reason`을 포함한 schema-valid part 파일을 쓴다. + - `event_candidates: []`와 `zero_event_reason` 누락 또는 null이 함께 존재하는 것은 정상 zero-event가 아니라 schema invariant 위반이다. + - part schema invariant 위반이 내부 보정 후에도 남으면 part 파일은 쓰지 않는다. 불완전한 part를 생성하여 quality gate에 넘기지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + + - 본 mapper 인스턴스는 정확히 runtime parameter (`{{item.assigned_ordinal}}`) 에 해당하는 1개 문서만 처리한다. + - runtime parameters (`{{item.*}}`) 는 ordinal/binding 의 단일 source of truth 다 (별도 plan 파일 read 불요). + - B1 part의 `evidence_index_proposed`, `title`, `doc_type`, `source_pointer.ordinal`을 authority로 사용한다. + - `{{item.shard_doc_path}}`의 단일 evidence 문서(planner가 evidence_all.json에서 분리)를 직접 참조하여 candidate를 추출한다. + - 본 task는 planner가 확정한 `evidence_index_candidate`, `b2_output_path`, assigned_ordinal을 소비할 뿐 재작성하지 않는다. + - 다른 ordinal의 사실을 자기 part에 주입하지 않는다(authority drift 금지). + - candidate layer에 머문다. 최종 BO, 최종 JuristicAct, 최종 청구권, 최종 사해행위 판단을 확정하지 않는다. + - 문서 단위 1요약으로 뭉개지 않는다. 한 문서에 복수의 법적 중요 행위가 있으면 반드시 여러 candidate로 분리한다. + - support가 희박한 후보를 억지로 채우지 않는다. 불명확한 필드는 `null` 또는 빈 배열을 사용할 수 있다. + - 잘못된 과잉 특정화보다 보수적 분해가 낫다. 다만 자기 문서 커버리지를 포기해서는 안 된다. + - `candidate_id`는 deterministic 규칙 `EVT-{ordinal:03d}-{seq:02d}`로 생성한다(seq는 자기 part 내 1-based 순번). + + + + 이 인스턴스의 `{{item.shard_doc_path}}`(planner가 evidence_all.json에서 분리한 단일 evidence 1건) + `evidence_indexed_parts/E-{ordinal:03d}.json`의 authority metadata + `client_meeting.md` + + + `client_meeting.md`는 아래 목적에만 사용할 수 있다. + - alias 정규화 + - 동일인 식별 보조 + - property cluster shorthand 해석 + - 현재 문서가 어느 상담 조항을 뒷받침하는지에 대한 `meeting_clause_refs` 보조 + - 현재 문서에서 직접 읽히는 내용의 중요도 보조 + - 사망자와 특정 자산군의 연결이 meeting에서 직접 읽히는 경우 succession 관련 candidate의 `affected_asset_cluster_ids` 보조 + 금지: + - meeting만으로 문서에 없는 사건행위를 candidate로 생성 + - meeting만으로 현재 문서의 당사자/금액/날짜를 보충해 direct evidence처럼 쓰기 + + + + + - 자기 part는 정확히 1개 item을 가진다. + - 그 item은 아래 둘 중 하나를 반드시 가진다. + - `event_candidates`가 1개 이상 + - `zero_event_reason`가 non-null + - `zero_event_reason` 허용값: + - `"NO_LEGAL_EVENT_FOUND"` + - `"REFERENCE_ONLY_DOC"` + - `"PARSE_INSUFFICIENT"` + - `"ORDINAL_MATCH_FAILED"` + - 등기부, 계약서, 통지서, 답변서, 감정/시세 문서처럼 통상 event-bearing 문서에 대해서는 `zero_event_reason`를 매우 예외적으로만 사용한다. + + + + `write_file` 전에 반드시 메모리의 part object를 검증한다. 이 검증은 생성 지점 안전장치이며 자동 강제 재실행을 의미하지 않는다. + + 필수 invariant: + 1. 메모리상 top-level은 단일 item object 또는 1-element `items` envelope일 수 있으나, `write_file` 직전 serialization 대상은 단일 item object여야 하며 실질 item은 정확히 1개다. + 2. item의 `evidence_index_proposed`는 runtime parameter `{{item.evidence_index_candidate}}` 또는 그와 동일한 `E-{ordinal:03d}` 값이어야 한다. + 3. item은 아래 둘 중 하나를 반드시 만족해야 한다. + - `event_candidates`가 non-empty list + - `zero_event_reason`이 non-null이며 허용값 중 하나 + 4. `event_candidates`가 빈 배열이면 `zero_event_reason`은 반드시 non-null 허용값이어야 한다. + 5. candidate가 없어 `zero_event_reason`이 non-null이면 schema 안정성을 위해 `event_candidates`는 빈 배열로 둔다. + 6. candidate가 있으면 각 candidate는 object이며, `candidate_id_proposed`는 `EVT-{ordinal:03d}-{seq:02d}` 형식이고 `source_evidence_index_proposed`는 자기 `evidence_index_proposed`와 일치해야 한다. + + 내부 보정 원칙: + - 문서 검토 결과 법적 중요 event/state 후보가 없는 것이 맞으면 `zero_event_reason`을 허용값 중 하나로 명시한다. + - event/state candidate 생성 조건이 충족되는데 빈 배열이면 extraction routines로 되돌아가 candidate를 재작성한다. + - 통상 event-bearing 문서에 `zero_event_reason`을 사용하는 경우, 원문상 후보 부재 또는 파싱 불충분 이유가 허용값과 맞는지 재확인한다. + + 보정 실패: + - 위 invariant 중 하나라도 충족하지 못하면 `write_file`을 호출하지 않는다. + - 마지막 채팅 출력은 정확히 `FAILED: E-{ordinal:03d} event candidate part schema invariant violation`로 한다. + + + + 아래 범주의 법적 중요 행위를 후보로 추출한다. + - 소유권 이전, 매매, 증여, 대물변제, 처분 + - 근저당/저당/전세권/가압류/압류/담보 설정 및 말소 + - 대여, 보증, 신용보증, 대위변제, 구상권 발생, 변제, 배당 + - 임대차, 점유, 인도, 현재 점유, 현재 사용자, 현재 사용수익, 현재 수익귀속, 현재 권리귀속, 현재 관리 상태 + - 경매신청, 경매개시결정, 경매매각, 인도명령, 집행 관련 사실 + - 통지, 최고, 해제/해지의사표시, 도달, 채무승인, 감액요구, 책임부인, 현재상태 주장 + - 판결, 결정, 지급명령, 배당표 등 소송/집행 관련 법적 중요 사실 + - 신축, 영업양도, 사업자등록, 현재 영업상태 등 사건핵심 상태 형성 행위 + - 위 범주와 직접 연결되는 기타 법적 중요 행위 또는 현재상태 + + 제외: + - 순수 배경설명만 있는 문장 + - 정보 증가 없는 반복 진술 + - 법적 효과와 관련 없는 주변사정 + + + + + - 갑구/을구의 각 의미 있는 row를 개별 candidate로 본다. + - 최소 추출 대상: 소유권보존, 소유권이전, 증여, 매매, 담보권실행을 위한 경매개시결정, 담보권실행을 위한 경매로 인한 매각, 근저당권설정, 근저당권설정등기말소, 가압류. + - `support_locators.locator_hint`에는 가능하면 `갑구4`, `을구1`, `순위번호 3` 같은 row identifier를 남긴다. + - 말소표시가 있더라도 법적 의미가 있으면 candidate로 남길 수 있다. + - row에 지분 표시가 직접 읽히면 candidate에 같은 ordinal B1 part의 `registry_row_components[*].fraction_ref`와 동일한 `fraction_ref`를 남긴다. 지분 표시가 없으면 null. + + + + - 하나의 문서에 서로 다른 계약 요소가 공존하면 clause별로 분해한다. + - 예: 대지 임대차 + 건물 매매 + 인도 + 관리사용 귀속 + 명의변경 협력은 가능한 한 별개 candidate로 분리한다. + - B1 part의 `contract_components`가 있으면 그것을 우선 참고하되, raw 문서 확인을 생략하지 않는다. + - 혼합계약서가 계약성립, 대가수수, 인도, 관리·사용 귀속을 직접 보여주면 아래 후보군을 가능한 한 독립 candidate로 생성한다. + 1. 계약성립 candidate + 2. 대가수수 candidate + 3. 인도 candidate + 4. 관리·사용 또는 현재 수익귀속 state candidate + - 위 4종 중 raw 문서가 직접 보여주는 것은 formation candidate 1개로 뭉개지지 않도록 반드시 분리한다. + - 대가수수 candidate는 적절한 기존 enum이 없으면 `event_kind="기타"`, `event_subkind="대가수수"`로 둘 수 있다. + - 대가수수 candidate에는 가능한 한 지급자, 수령자, 금액, 목적물, 해당 조항 locator를 함께 남긴다. + - 인도 candidate에는 가능한 한 인도자, 인도수령자, 목적물, 개시일, locator를 함께 남긴다. + - 관리·사용 귀속 candidate는 `event_or_state="state"`를 우선 검토하고, `current_state_candidate` 하위 4축과 interval field를 함께 남긴다. + - 문서가 계약일로부터 현재 점유/사용 귀속을 직접 보여주면 `state_start_date`를 그 개시일로 남기고, 뒤에 다른 점유/사용 candidate가 있으면 선행 state를 open-ended로 두지 않는다(같은 문서 내). + + + + - 내용증명, 답신, 답변서, 최고서, 해제통지서, 소멸청구 통지서에서는 아래를 개별 candidate로 검토한다. + - 지급 요구 + - 해제/해지 통지 + - 도달 + - 책임 부인 + - 현재 점유/현재 거주/현재 운영 주장 + - 사용수익 귀속 주장 + - 현재 권리귀속 주장 + - 채무승인 또는 시효항변 주장 + - 감액요구, 명도요구 + - 문서에 현재 상태가 직접 적혀 있으면 `event_or_state="state"` candidate를 생성할 수 있다. + - 현재 상태가 후행 시점에서 선행 상태를 대체하는 구조가 직접 보이면 interval field를 함께 남긴다. + + + + - 감정평가서, 감정평가 의견서, 임료 시세 확인서 등은 `event_candidates`가 0개라고 판단하지 않는다. + - 시가표, 차임표, 사용이익표, 시장가치표가 있으면 최소 1개의 factual/state candidate를 생성한다. + - rent/use-gain valuation은 `action_type_candidate="사실행위(factual acts)"`로 둘 수 있다. + - 문서가 특정 현재 수익귀속자 또는 현재 권리귀속자를 직접 보여주면, 해당 정보는 `current_state_candidate`의 대응 배열에 분리해 둔다. + + + + - 사업자등록증, 가족관계증명서, 현재상태 확인 문서, 심판문 등은 상태형 후보 또는 법적 지위 관련 후보를 생성할 수 있다. + - 예: 사업자등록은 현재 영업상태/점포 운영 개시의 간접 또는 직접 후보가 된다. + - 가족관계증명서, 상속포기 심판문, 상속 관련 공문서가 사망자와 친족구성 또는 상속포기를 직접 보여주면 succession 관련 candidate를 생성한다. + - sparse status 문서라도 later stage linker가 사용할 수 있도록 가능한 범위에서 `participants`, `object_spec`, `event_date` 또는 `current_state_candidate.state_as_of`를 채운다. + - family/renunciation/status 문서를 generic status로만 남기지 않는다. + + + + - 사망, 가족관계, 상속포기, 재산상속회복, 승계 관련 문서는 가능한 한 succession 관련 candidate를 생성한다. + - succession 관련 candidate에는 optional field `affected_asset_cluster_ids`를 둘 수 있다. + - `affected_asset_cluster_ids`는 meeting이나 B1 part metadata가 사망자와 특정 자산군의 연결을 직접 또는 명백하게 보여줄 때만 채운다. + - succession 관련 candidate에는 optional field `succession_candidate`를 둘 수 있다. + { + "deceased_candidate": null, + "spouse_candidates": [], + "lineal_ascendant_candidates": [], + "lineal_descendant_candidates": [], + "renunciant_candidates": [] + } + - 가족관계증명서 같은 status doc은 direct 내용이 보이면 `deceased_candidate`, `spouse_candidates`, `lineal_ascendant_candidates`, `lineal_descendant_candidates`를 가능한 한 채우고, 자산 연결이 직접 또는 명백하면 `affected_asset_cluster_ids`도 함께 채운다. + - 상속포기 문서는 상속포기 그 자체를 direct candidate로 남기되, direct names가 보이면 `succession_candidate.renunciant_candidates`를 반드시 채운다. + - 상속포기 문서가 특정 사망자와 자산 cluster를 직접 또는 명백하게 연결하면 동일한 `affected_asset_cluster_ids`를 함께 남긴다. + - direct names가 있는 succession/status 문서에서는 가능한 범위에서 `participants`와 `object_spec`도 함께 남겨 later stage overlap scorer가 sparse document를 놓치지 않게 한다. + - 이 단계는 최종 상속분, 단독상속, complaint plaintiff narrowing을 확정하지 않는다. 다만 downstream이 succession bundle을 읽을 수 있을 정도의 구조는 반드시 남긴다. + + + + + - raw event는 문서가 직접 보여주는 사실행위·법률행위·준법률행위다. + - legal-effect event는 raw event에서 downstream 청구권 식별에 필요한 법률효과 후보를 도출한 candidate다. + - raw event와 legal-effect event는 같은 문서에서 함께 생성할 수 있으며, 서로 다른 `candidate_id`를 가진다. + - legal-effect event는 최종 법률판단이 아니라 Stage 2가 검토할 구조화된 후보이므로 `confidence`와 `review_flags`를 함께 남긴다. + + + 금전채권: + - `underlying_claim_arising` + - `first_demand` + - `demand_notice_dispatch` + - `demand_notice_arrival` + - `debt_acknowledgement` + - `new_due_date` + - `business_transfer` + - `trade_name_continuation_candidate` + - `commercial_successor_liability_candidate` + + 담보부 부동산: + - `secured_debt_creation` + - `mortgage_setting_by_fraction` + - `undesignated_payment` + - `payment_allocation_effect` + - `mortgage_extinguishment_by_fraction` + - `auction_application_after_extinguishment` + - `auction_sale_and_registry_transfer` + - `derivative_mortgage_setting_by_invalid_title_holder` + - `unregistered_building_original_acquisition` + - `de_facto_disposition_holder_acquisition` + - `land_use_gain_start` + - `liability_overlap_start` + + 사해행위취소: + - `preserved_claim_arising` + - `collateral_setting_for_preserved_claim` + - `fraudulent_transfer` + - `asset_value_at_fraudulent_act` + - `asset_value_at_close_proxy` + - `collateral_insufficiency_at_act` + - `encumbrance_release_after_act` + - `value_compensation_exception_trigger` + - `value_compensation_cap_calculation_candidate` + - `defense_assertion_title_trust` + - `defense_assertion_sufficient_collateral` + - `defense_assertion_setoff` + + 유치권ㆍ상속ㆍ통지: + - `lien_assertion` + - `third_party_lease_by_lien_holder` + - `lien_violation_candidate` + - `lien_extinction_notice_dispatch` + - `lien_extinction_notice_receipt` + - `lien_extinction_effect_candidate` + - `ongoing_physical_possession` + - `succession_renunciation_filing` + - `succession_renunciation_acceptance` + - `spouse_sole_succession_effect_candidate` + - `defense_assertion_subsequent_owner_no_extinction_right` + - `defense_assertion_benefit_applied_to_secured_claim` + + + + - claim_doc 문서가 채무승인, 최고, 변제기, 변제약정, 지급기일을 함께 포함하면 하나의 generic event로 압축하지 말고 legal effect별 candidate를 분리한다. + - 이행각서에는 최소한 `debt_acknowledgement`, `payment_promise`, `new_due_date` 후보가 있는지 검토한다. + - 최고서에는 `demand_notice_dispatch`, `demand_notice_arrival`, `limitation_preservation` 관련 후보가 있는지 검토한다. + - 발송일과 도달일이 다르면 각각 별도 `event_date`로 보존한다. + - 채무승인 event에는 `legal_effect_candidate.acknowledgement_of_debt=true`, `limitation_interruption_candidate=true`, `related_claim_type`을 직접 읽히는 범위에서 남긴다. + + + + - business_transfer_doc 문서는 단순 영업양도 formation event로만 끝내지 않는다. + - 문서 또는 상담내용에서 상호, 영업명, 영업자산, 고객관계, 고용관계, 채무불승계 조항, 책임배제 등기 필요성이 보이면 `commercial_successor_candidates`로 분해한다. + - 상법 제42조 책임을 최종 확정하지는 않되, downstream이 요건판단을 할 수 있도록 transferor, transferee, business_name, trade_name_continuation_candidate, debt_exclusion_clause_candidate를 남긴다. + + + + - 원자료 event가 곧바로 법률효과를 발생시키는 경우 raw event와 legal-effect event를 모두 생성한다. + - 복수 채무에 대한 지정 없는 변제가 등장하면 `undesignated_payment` event와 별도로 `payment_allocation_effect` 후보를 생성한다. + - 등기부상 경매 매각과 후행 근저당권이 등장하면, 그 경매가 어떤 담보권에 기초했는지, 그 담보권의 피담보채무가 이미 소멸했는지 검토하는 event chain을 만든다. + - 미등기 건물 신축일과 미등기 건물 매수일은 점유·사용이익 발생 시점을 정하므로 `land_use_gain_start` 및 `liability_overlap_start` event 후보를 생성한다. + - `mortgage_setting_by_fraction`/`mortgage_extinguishment_by_fraction` 등 `*_by_fraction` subkind candidate는 지분 표시가 문서에 직접 읽히는 한 `fraction_ref`를 반드시 가진다. + + + + - 사해행위취소 사건에서 raw event와 legal-effect event를 구분한다. + - 사해행위 당시 존재하던 담보권이 사후 말소되면 `value_compensation_exception_trigger` event를 생성한다. + - valuation row와 사후 말소 부담, 존속 부담이 모두 있으면 `value_compensation_cap_calculation_candidate`를 생성한다. + - 피고가 항변을 명시한 내용증명은 `defense_assertion_*` event로 만들고, Stage 2 claim description에 반영해야 하는지 표시한다. + + + + - 통지 문서와 답변서가 서로 대응하고, 답변서가 특정 통지의 수령일을 인정하면 별도의 receipt event candidate를 생성한다. + - receipt event candidate는 발송 event와 병합하지 않는다. + - 발송일과 도달일이 다르면 도달일을 법률효과 후보일로 별도 보존한다. + - 문서에 유치권, 공사잔금, 점유, 임대, 직접 거주, 피담보채권 충당, 후소유자 항변이 함께 나타나면 일반 current possession event가 아니라 lien_event 후보군으로 분해한다. + - 제3자 임대는 `lien_violation_candidate`로, 직접 거주는 ongoing use state로, 피담보채권 충당 주장은 `defense_candidate`로, 소멸청구 수령은 `lien_extinction_effect_candidate`로 각각 분리한다. + - 상속포기 수리 event가 있고 배우자와 자녀 전부 상속포기 구조가 직접 보이면 `spouse_sole_succession_effect_candidate`를 생성하되, 최종 소장 원고 확정은 Task_C 이후로 미룬다. + + + + + `action_type_candidate`는 아래 중 1개를 선택한다. + - `"법률행위(legal acts)"` + - `"준법률행위(quasi-legal acts)"` + - `"사실행위(factual acts)"` + - `"위법행위(unlawful acts)"` + - `"소송행위(litigation acts)"` + - `"불명"` + + + + `event_kind`는 아래 값 중 가장 가까운 것을 선택한다. + - `"처분"`, `"소유권이전"`, `"증여"`, `"대물변제"`, `"담보설정"`, `"담보말소"`, `"채권발생"`, `"보증"`, `"대위변제"`, `"변제"`, `"배당"`, `"임대차"`, `"점유"`, `"인도"`, `"가압류/압류"`, `"경매신청"`, `"경매개시"`, `"경매매각"`, `"통지"`, `"해제/해지"`, `"판결/결정"`, `"영업양도"`, `"사업자등록"`, `"신축"`, `"상속개시"`, `"상속포기"`, `"채권관리"`, `"도달"`, `"항변"`, `"가치평가"`, `"법률효과"`, `"기타"` + + 필요하면 optional field `event_subkind`에 더 구체한 라벨(REQUIRED_EVENT_SUBKINDS 참조)을 남길 수 있다. + + + + - `participants.actor_candidates`는 직접 행위를 한 사람/법인 + - `participants.counterparty_candidates`는 직접 상대방 + - `participants.beneficiary_candidates`는 행위로 이익을 직접 취득한 사람/법인 + - `participants.third_party_candidates`는 법원, 피상속인, 기존 권리자 등 제3자 + - `object_spec`는 짧은 목적물/채권/담보/통지대상/상태 대상 명사구 + - `amount`는 현재 candidate와 직접 연결되는 대표 금액 1개만 사용한다 + - state candidate의 현재 점유/사용/수익/권리 귀속은 `participants`에 뭉개지지 않고 `current_state_candidate` 하위 배열에 분리한다 + - succession/status 문서처럼 sparse한 문서라도 direct names와 direct relation이 보이면 `participants`와 `object_spec`를 불필요하게 비워두지 않는다. + - 잘못된 당사자 확정보다 `[]` 또는 `null`이 낫다 + + + + - `event_date`는 가능하면 `YYYY-MM-DD` + - 불명확하면 `null` + - `time_text`는 짧은 원문 표현을 유지할 수 있다 + - `time_precision` 허용값: `"exact"`, `"approximate"`, `"range"`, `"unknown"` + - 현재상태 candidate의 경우 `state_as_of` 개념은 optional `current_state_candidate.state_as_of`에 남기고, `event_date`는 상태 개시 시점이 직접 보일 때만 채운다. + - state interval이 직접 보이면 `current_state_candidate.state_start_date`, `current_state_candidate.state_end_date_candidate`를 함께 채운다. + - 후행 상태가 선행 상태를 대체하는 것이 직접 보이면 `current_state_candidate.superseded_by_candidate_id`를 사용한다. + + + + - optional field `event_or_state`를 추가할 수 있다. 허용값은 `"event"` 또는 `"state"`. + - 문서가 단발 사건이 아니라 `현재까지`, `현재도`, `계속`, `현재 운영`, `현재 점유`, `현재 거주`, `부지로만 사용`, `현재 명의`, `현재 소유`, `차임을 받고 있다` 같은 계속상태를 직접 보여주면 `event_or_state="state"` candidate를 생성할 수 있다. + - state candidate에는 optional field `current_state_candidate`를 둘 수 있다. + { + "state_summary": "짧은 상태 설명", + "state_as_of": "현재|2024-10-31 등", + "state_start_date": null, + "state_end_date_candidate": null, + "superseded_by_candidate_id": null, + "is_ongoing": true, + "current_possessor_candidates": [], + "current_user_candidates": [], + "current_benefit_holder_candidates": [], + "current_right_holder_candidates": [], + "possession_legal_cause_asserted": null, + "possession_legal_cause_asserted_by": [] + } + - `current_state_candidate`는 가능한 한 아래 4축으로 분해한다. + - 점유자 -> `current_possessor_candidates` + - 사용자/운영자 -> `current_user_candidates` + - 차임·사용이익·영업수익 등 현재 수익귀속자 -> `current_benefit_holder_candidates` + - 현재 소유자·등기명의인·권리귀속자 -> `current_right_holder_candidates` + - 같은 사람이 복수 role을 가지면 각 배열에 중복 기재할 수 있다. + - 문서에 직접 읽히지 않는 축은 빈 배열로 둔다. 추정으로 채우지 않는다. + - event candidate와 state candidate는 동일 문서에서 함께 존재할 수 있다. + - 동일 문서에 과거 행위와 현재 상태가 함께 있으면 분리한다. + - 동일 문서에 현재 점유자와 현재 권리귀속자가 다르게 읽히면 하나로 뭉개지지 않도록 분리 기재한다. + - 상태 개시 시점이 직접 보이면 `state_start_date`를 채운다. + - 후행 점유/사용/수익귀속 candidate가 선행 상태를 대체하는 것이 직접 보이면, 선행 state에 `state_end_date_candidate`와 `superseded_by_candidate_id`를 남기고 open-ended current state로 방치하지 않는다. + - `state_end_date_candidate`가 직접 보이면 `is_ongoing=false`를 우선 검토한다. + - 혼합계약서가 인도, 관리·사용 귀속, 사용수익 귀속을 직접 보여주면 그 상대방에 대한 state candidate를 high confidence로 생성할 수 있다. + - raw 문서가 현재 점유, 현재 사용, 현재 수익귀속, 현재 권리귀속을 직접 적시하면 candidate `confidence`는 원칙적으로 `high`를 우선 검토한다. + - meeting 보조나 간접 정황에 크게 의존한 상태 후보는 `medium` 또는 `low`로 둔다. + - `possession_legal_cause_asserted`는 optional 필드다. 허용값: "유치권행사중|임대차|사용대차|물권|동시이행항변|명도후잔류|무권원주장|미상". `possession_legal_cause_asserted_by`는 그 권원을 주장한 주체(원고측/피고측/제3자 이름)이며 문서에 직접 읽힐 때만 채운다. + - 점유 사실과 권원 주장은 같은 candidate 안에서 축을 분리해 남긴다(사실=4축 배열, 권원=`possession_legal_cause_asserted`). 권원 주장이 문서에 없으면 `미상`이 아니라 null로 둔다. + - 서로 다른 문서가 동일 자산에 대해 다른 권원을 주장하는 것은 본 mapper의 검출 대상이 아니다(authority drift 금지). 각 문서의 주장을 그대로 보존한다. + + + + - optional field `meeting_clause_refs`를 추가할 수 있다. + - 형식은 `1-가`, `2-라`, `3-다`, `4-가`, `5-나`처럼 `client_meeting.md`의 본문 번호체계를 사용한다. + - 현재 문서가 특정 상담조항을 직접 뒷받침할 때만 기록한다. + - meeting clause ref는 보조 recall 장치일 뿐, 문서에 없는 사실을 가져오는 수단이 아니다. + + + + 이 단계는 최종 사해행위 판단을 하지 않는다. 다만 후속 단계의 recall 강화를 위해 candidate tag를 남긴다. + `actio_relevance_candidates.candidate_role_tags` 허용값: + - `"사해행위목적물후보"`, `"원고채권후보"`, `"선순위담보후보"`, `"존속담보후보"`, `"임차권후보"`, `"가압류후보"`, `"수익자이익후보"`, `"현재점유후보"`, `"현재사용수익후보"` + + `fraudulent_act_date_candidate`는 다음 경우에만 채운다. + - 현재 candidate가 직접 목적물의 처분, 이전, 이전등기, 매매일, 증여일, 대물변제일, 경매매각일과 연결되는 경우 + - 그 외에는 쓰지 않는다 + + + + downstream Task_C BO와 evidence 기반 candidate가 중복 제거할 수 있도록, 각 candidate마다 아래 정규화 문자열을 생성한다. + + `(event_or_state + event_kind + 핵심 actor + 핵심 counterparty + 핵심 object_spec/amount + 핵심 date_or_state_as_of)` + + 규칙: + - 불명확한 요소는 생략하되, 최소한 `event_or_state` 또는 `event_kind`와 1개 이상의 핵심 요소는 남긴다. + - 중복 제거를 위해 whitespace, 중복 조사, 과도한 수식어는 제거한다. + - title이나 evidence_index는 넣지 않는다. + - event와 state는 같은 signature가 되지 않도록 구분한다. + - mapper가 propose하는 candidate_id는 `EVT-{ordinal:03d}-{seq:02d}`이며 quality gate가 collision을 처리한다. + + + + part 파일은 단일 item object 형태이며, top-level item은 non-empty `event_candidates` 또는 non-null `zero_event_reason` 중 하나를 반드시 가진다. `event_candidates: []` 단독은 금지된다. + + { + "evidence_index_proposed": "E-{ordinal:03d}", + "title": "문서 제목", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": ordinal + }, + "property_cluster_id": null, + "doc_semantic_flags": [], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-{ordinal:03d}-01", + "event_or_state": "event|state", + "event_kind": "소유권이전", + "event_subkind": null, + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "짧은 1문장 요약", + "source_evidence_index_proposed": "E-{ordinal:03d}", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [], + "counterparty_candidates": [], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": null, + "fraction_ref": null, + "amount": null, + "event_date": null, + "event_end_date": null, + "time_text": null, + "time_precision": "exact|approximate|range|unknown", + "location": null, + "legal_keywords": [], + "support_locators": [ + { + "locator_type": "heading|key_value|table_row|paragraph|caption|unknown", + "locator_hint": "갑구4|제2조 등", + "excerpt": "짧은 단서", + "directness": "직접|간접|불명" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "string|null", + "related_claim_type": "string|null", + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "state|점유|...", + "confidence": "high|medium|low|unknown" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } + } + + 추가 제약: + - top-level은 단일 object다. envelope이 아닌 단일 item. + - `event_candidates`가 빈 배열이면 `zero_event_reason`은 반드시 non-null 허용값이어야 한다. + - `event_candidates: []`와 `zero_event_reason` 누락/null이 결합된 part는 작성 금지되는 schema invariant violation이다. + - `candidate_id_proposed`는 mapper의 propose 식별자(`EVT-{ordinal:03d}-{seq:02d}`)이며, finalize는 quality gate가 한다. + - `source_evidence_index_proposed`는 mapper가 propose 한 자기 ordinal의 candidate evidence_index다. + - `mapper_meta.self_isolation_attested=true`는 본 mapper가 자기 문서 외 사실을 주입하지 않았음을 self-attest 한다는 의미다. + + + + - `write_file` 전 을 수행하고, 불량이면 내부 보정을 먼저 시도했는지 점검한다. + - 자기 part object가 단일 item이고 `evidence_index_proposed`, non-empty `event_candidates` 또는 non-null `zero_event_reason`을 가지는지 점검한다. + - `event_candidates: []`와 `zero_event_reason` 누락/null이 함께 있으면 내부 보정 대상이며, 보정 불가 시 part 파일을 쓰지 않고 FAILED로 종료한다. + - `planner_status=="READY"` 및 `mapper_execution_allowed=true`가 확인되지 않았으면 part 파일이 작성되지 않았는지 점검한다. + - 모든 candidate의 `candidate_id_proposed`가 `EVT-{ordinal:03d}-{seq:02d}` 형식인지 점검한다. + - 자기 문서 외 ordinal의 사실이 part에 주입되지 않았는지 self-check 한다(authority drift 방지). + - `registry_doc` 또는 `multi_event_doc`인데 `event_candidates`가 0개면 다시 점검한다. + - `mixed_contract_doc`인데 서로 다른 법적 효과가 2개 이상 보이면 candidate도 최소 2개 이상 검토한다. + - 혼합계약서가 계약성립, 대가지급·수령, 인도, 관리·사용 귀속을 직접 보여주면 formation candidate만 남기지 말고 consideration/delivery/state candidate가 함께 검토되었는지 점검한다. + - 문서가 위 네 요소를 모두 직접 보여주면 최소 3개 이상, 가능하면 4개까지 독립 candidate로 분리되었는지 점검한다. + - `valuation_doc`인데 가치표/차임표가 직접 보이면 candidate를 최소 1개 생성한다. + - 문서가 상담일지의 명시 조항을 직접 뒷받침하는데도 `meeting_clause_refs`가 전혀 없으면 다시 점검한다. + - event와 state는 구분해 생성한다. 현재상태를 과거 단발행위로 뭉개지 않는다. + - state candidate에서 현재 점유자/사용자/수익귀속자/권리귀속자가 직접 읽히는데도 하나의 participant 배열로 뭉개졌다면 다시 점검한다. + - 현재 점유자와 현재 권리귀속자가 다르게 읽히는 문서는 `current_state_candidate` 하위 배열에서 분리되었는지 다시 점검한다. + - 상태 개시와 종료가 직접 보이거나 후행 상태가 선행 상태를 대체하는 것이 직접 보이면 `state_start_date`, `state_end_date_candidate`, `superseded_by_candidate_id` 검토가 이루어졌는지 점검한다. + - 가족관계증명서가 사망자와 친족구성을 직접 보여주면 `succession_candidate.deceased_candidate`, `spouse_candidates`, `lineal_descendant_candidates` 또는 `lineal_ascendant_candidates`가 필요한 범위에서 채워졌는지 점검한다. + - 상속포기 심판문이 direct names를 보여주면 `succession_candidate.renunciant_candidates`가 비어 있지 않은지 점검한다. + - 가족관계증명서, 상속포기 심판문, 상속 관련 공문서가 사망자와 특정 자산군의 연결을 직접 또는 명백하게 보여주면 `affected_asset_cluster_ids` 검토가 이루어졌는지 점검한다. + - succession/status 문서처럼 sparse한 문서라도 direct names, direct relation, direct date가 보이는데 `participants`, `object_spec`, `event_date` 또는 `state_as_of`가 불필요하게 비어 있지 않은지 점검한다. + - raw 문서가 현재상태를 직접 적시한 경우 candidate `confidence`가 불필요하게 low로 떨어지지 않았는지 점검한다. + - claim doc에 채무승인ㆍ최고ㆍ변제기 재설정 단서가 있는데 `legal_effect_candidate`가 있는 event가 전혀 없으면 self-warning으로 본다. + - 이행각서에 채무승인일과 새 변제기가 함께 있는데 `debt_acknowledgement`와 `new_due_date`가 분리되지 않으면 self-warning으로 본다. + - business transfer 문서에 상호속용 후보 event 또는 `commercial_successor_candidates`가 없으면 self-warning으로 본다. + - 복수 피담보채무와 변제가 있는데 `payment_allocation_effect`가 없으면 self-warning으로 본다. + - 경매 event가 있는데 기초 담보권 및 피담보채무 chain과 연결되지 않으면 `AUCTION_BASIS_CHAIN_MISSING` self-warning으로 본다. + - 미등기 건물 매수 event가 있는데 원시취득 event가 없으면 `UNREGISTERED_BUILDING_ORIGINAL_ACQUISITION_MISSING` self-warning으로 본다. + - 사후 담보권 말소 row가 있는데 `value_compensation_exception_trigger` event가 없으면 self-error로 본다. + - valuation matrix와 released encumbrance가 있는데 `value_compensation_cap_calculation_candidate`가 없으면 self-warning으로 본다. + - defense assertion event가 있는데 `defense_candidate.requires_rebuttal_in_stage2` 검토가 없으면 self-warning으로 본다. + - 유치권 소멸 통지서와 수령 인정 답변서가 있는데 도달 event 또는 `lien_extinction_notice_receipt`가 없으면 self-error로 본다. + - 제3자 임대 event가 있는데 `lien_violation_candidate`가 없으면 self-warning으로 본다. + - 상속포기 수리 event가 있는데 `spouse_sole_succession_effect_candidate` 후보가 없으면 self-warning으로 본다. + - self-warning/self-error는 mapper가 part 파일에 그대로 남겨도 되며, quality gate가 최종 severity를 판정한다. + + + + + {{item.assigned_ordinal}} + + + [ ] 1) Preflight: `list_docs` 사용해서 `{{item.b1_output_path}}`, `{{item.shard_doc_path}}`, `client_meeting.md` 만 확인. `read_docs` 사용해서 동일 파일만 읽기 + [ ] 2) runtime parameter `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.b1_output_path}}`, `{{item.b2_output_path}}` 그대로 소비. 별도 검증 read 불요. + [ ] 3) `{{item.shard_doc_path}}`의 단일 evidence 문서를 메모리에서 사용하고, 같은 ordinal의 B1 part(`{{item.b1_output_path}}`)만 authority metadata로 사용 + [ ] 4) `write_file` 전 수행. 불량이면 내부 보정 시도 + [ ] 5) 보정 후에도 불량이면 part 파일을 쓰지 않고 `FAILED: E-{ordinal:03d} event candidate part schema invariant violation` 출력 후 종료 + [ ] 6) 통과한 경우에만 `write_file` 사용해서 `evidence_event_candidate_parts/E-{ordinal:03d}.json` 생성. event 후보가 0건이어도 non-null `zero_event_reason` part 파일을 작성 + [ ] 7) `list_docs` 사용해서 자기 part 파일만 존재 확인(절대 읽기 금지) + [ ] 8) `"E-{ordinal:03d} event candidates part 작성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `{{item.b1_output_path}}` (같은 ordinal 의 B1 part) + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 1건; planner가 evidence_all.json에서 분리) + - `client_meeting.md` + + + - `evidence_event_candidate_parts/E-{ordinal:03d}.json` + + + + + - `candidate_id_proposed`는 정확히 `EVT-{ordinal:03d}-{seq:02d}` 형식만 허용한다. + - `ordinal`과 output path는 runtime parameter (`{{item.*}}`) 를 그대로 따른다. + - 증거 문서 수 N, assigned_ordinal, mapper fan-out, streaming edge는 본 task에서 만들지 않는다. + - 자기 part 파일은 절대 재읽지 않는다. + - 다른 ordinal 문서를 처리하지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"E-{ordinal:03d} event candidates part 작성 완료"` 한 줄만 사용한다. + + + + - task_name: Task_B1_gate_merge_deterministic + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import copy + import hashlib + import itertools + import json + import re + import time + from collections import defaultdict + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + PLAN_PATH = "evidence_shard_plan.json" + PART_DIR = "evidence_indexed_parts" + FINAL_PATH = "evidence_indexed.json" + MANIFEST_PATH = "finalized_evidence_id_manifest.json" + PRECHECK_PATH = "quality_gates/B1_gate_precheck.json" + ID_RE = re.compile(r"^E-(\d{3})$") + + def _mid(): return next(MSG_ID) + + def _init(): + r = CLIENT.post(LOCALDOCS_URL, json={ + "jsonrpc": "2.0", "id": 1, "method": "initialize", + "params": {"protocolVersion": "2025-03-26", "capabilities": {}, + "clientInfo": {"name": "task-b1-gate-merge-deterministic", "version": "1.0", + "user_id": "{{__user_hash__}}", "workspace_id": "{{__workspace_hash__}}"}} + }, headers=MCP_HEADERS) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post(LOCALDOCS_URL, json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS).raise_for_status() + + def _parse_response(text: str): + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: return json.loads(line[6:]) + except Exception: pass + try: return json.loads(text) + except Exception: return None + + def _call(name: str, args: dict[str, Any]): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc": "2.0", "id": _mid(), "method": "tools/call", + "params": {"name": name, "arguments": args}}, + headers=MCP_HEADERS) + r.raise_for_status() + payload = _parse_response(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: dict[str, Any], name: str): + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: raise RuntimeError(f"Empty MCP result: {name}") + try: outer = json.loads(text) + except Exception: return text + if isinstance(outer, dict) and "results" in outer: + row = (outer.get("results") or [{}])[0] + inner = row.get("content") or row.get("text") or "" + if not inner: raise RuntimeError(f"Empty document content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer + + def read_json(path: str): + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str): return raw + try: return json.loads(raw) + except json.JSONDecodeError: + obj, _ = json.JSONDecoder().raw_decode(raw.strip()) + return obj + + def write_doc(path: str, obj: Any): + text = json.dumps(obj, ensure_ascii=False, indent=2) + _call("write_file", {"path": path, "content": text, "overwrite": True}) + return text + + def _walk_strings(value: Any): + if isinstance(value, str): + yield value + elif isinstance(value, list): + for item in value: yield from _walk_strings(item) + elif isinstance(value, dict): + for item in value.values(): yield from _walk_strings(item) + + def list_part_names(folder: str): + payload = None + errors = [] + for args in ({"path": folder, "pattern": "E-*.json"}, {}): + try: + payload = _unwrap(_call("list_docs", args), "list_docs") + break + except Exception as exc: + errors.append(str(exc)) + if payload is None: + raise RuntimeError("list_docs failed: " + " | ".join(errors)) + if isinstance(payload, str): + try: payload = json.loads(payload) + except Exception: pass + names = [] + for value in _walk_strings(payload): + match = re.search(r"(?:^|/)(E-\d{3}\.json)$", value) + if match: + names.append(f"{folder}/{match.group(1)}") + return sorted(set(names)) + + def single_item(obj: Any, path: str): + if isinstance(obj, dict) and isinstance(obj.get("items"), list): + if len(obj["items"]) != 1 or not isinstance(obj["items"][0], dict): + raise ValueError(f"{path}: envelope must contain exactly one object") + return obj["items"][0] + if isinstance(obj, dict): + return obj + raise ValueError(f"{path}: part must be an object") + + def refs(evidence_indexes=None, candidate_ids=None, meeting_refs=None, titles=None): + return { + "evidence_indexes": sorted(set(str(x) for x in (evidence_indexes or []) if x)), + "event_candidate_ids": sorted(set(str(x) for x in (candidate_ids or []) if x)), + "meeting_clause_refs": sorted(set(str(x) for x in (meeting_refs or []) if x)), + "doc_titles": sorted(set(str(x) for x in (titles or []) if x)), + } + + def finding(code: str, detail: str, evidence_indexes=None, rerun=None): + row = {"issue_type": code.lower(), "source_check": code.lower(), "severity": "BLOCK", + "detail": detail, "source_refs": refs(evidence_indexes)} + if rerun: row["rerun_target_task"] = rerun + return row + + def suspect(kind: str, basis: str, evidence_indexes=None, titles=None, context=None): + return {"origin_gate": "B1", "exception_type": kind, "severity_candidate": "HARD_WARNING", + "source_refs": refs(evidence_indexes, titles=titles), "concise_basis": basis, + "compact_context": context or {}} + + def warning_records(node: Any, path: str = "$"): + out = [] + if isinstance(node, dict): + for key, value in node.items(): + child = f"{path}.{key}" + if re.search(r"(self_?)?(warning|error)", str(key), re.I) and value not in (None, False, "", [], {}): + out.append({"path": child, "value": value}) + out.extend(warning_records(value, child)) + elif isinstance(node, list): + for i, value in enumerate(node): out.extend(warning_records(value, f"{path}[{i}]")) + return out + + def main(): + started = time.monotonic() + _init() + plan = read_json(PLAN_PATH) + if not isinstance(plan, dict): raise RuntimeError("planner must be an object") + expected_ordinals = [str(x).zfill(3) for x in plan.get("expected_ordinals") or []] + expected_ids = [str(x) for x in plan.get("expected_evidence_ids") or + [f"E-{x}" for x in expected_ordinals]] + shard_count = int(plan.get("shard_count") or len(expected_ordinals)) + hard = [] + reviews = [] + parse_failures = [] + if plan.get("planner_status") != "READY" or plan.get("mapper_execution_allowed") is not True: + hard.append(finding("PLANNER_NOT_READY", "planner did not authorize mapper output consumption")) + actual_names = list_part_names(PART_DIR) + actual_ordinals = sorted({re.search(r"E-(\d{3})\.json$", x).group(1) for x in actual_names + if re.search(r"E-(\d{3})\.json$", x)}) + missing = sorted(set(expected_ordinals) - set(actual_ordinals)) + unexpected = sorted(set(actual_ordinals) - set(expected_ordinals)) + stale = sorted(set((plan.get("stale_part_policy") or {}).get("detected_stale_parts", {}) + .get("evidence_indexed_parts") or []) | + {f"{PART_DIR}/E-{x}.json" for x in unexpected}) + for ordinal in missing: + hard.append(finding("MISSING_B1_PART", f"missing B1 mapper part E-{ordinal}", + [f"E-{ordinal}"], f"Task_B1_map_doc_{ordinal}")) + for ordinal in unexpected: + hard.append(finding("UNEXPECTED_B1_PART", f"unexpected B1 mapper part E-{ordinal}", + [f"E-{ordinal}"])) + for path in stale: + hard.append(finding("STALE_B1_PART", f"stale B1 mapper part: {path}")) + + shards = {str((row.get("ordinal_label") or row.get("assigned_ordinal") or "")).zfill(3): row + for row in (plan.get("shards") or []) if isinstance(row, dict)} + items = [] + input_bytes = 0 + uid_rows = defaultdict(list) + title_rows = defaultdict(list) + for ordinal in expected_ordinals: + path = f"{PART_DIR}/E-{ordinal}.json" + if ordinal in missing: continue + try: + raw = read_json(path) + input_bytes += len(json.dumps(raw, ensure_ascii=False).encode("utf-8")) + item = single_item(raw, path) + proposed = str(item.get("evidence_index_proposed") or "") + expected = f"E-{ordinal}" + if proposed != expected or not ID_RE.fullmatch(proposed): + raise ValueError(f"evidence_index_proposed {proposed!r} != {expected}") + pointer = item.get("source_pointer") + if not isinstance(pointer, dict) or int(pointer.get("ordinal") or -1) != int(ordinal): + raise ValueError("source_pointer.ordinal mismatch") + meta = item.get("mapper_meta") + if not isinstance(meta, dict) or meta.get("self_isolation_attested") is not True: + raise ValueError("mapper self-isolation attestation missing or false") + final_item = copy.deepcopy(item) + final_item["evidence_index"] = final_item.pop("evidence_index_proposed") + final_item.pop("mapper_meta", None) + items.append(final_item) + shard = shards.get(ordinal) or {} + source_ref = shard.get("source_document_ref") or {} + uid = str(item.get("doc_uid") or source_ref.get("doc_uid_candidate") or "") + content_hash = str(source_ref.get("content_sha256") or "") + title_norm = str(item.get("title_normalized") or "").strip().lower() + row = {"ordinal": ordinal, "evidence_index": expected, "title": item.get("title"), + "title_normalized": title_norm, "content_sha256": content_hash} + if uid: uid_rows[uid].append(row) + if title_norm: title_rows[title_norm].append({**row, "doc_uid": uid}) + for flag in warning_records(item): + reviews.append(suspect("mapper_self_warning", "B1 mapper emitted an explicit warning or error", + [expected], [item.get("title")], + {"warning": flag, "doc_type": item.get("doc_type")})) + semantic_flags = set(str(x) for x in (item.get("doc_semantic_flags") or [])) + title_text = str(item.get("title") or "") + coverage_gaps = [] + if "registry_doc" in semantic_flags and not (item.get("registry_row_components") or []): + coverage_gaps.append("registry_row_components") + if ("schedule_doc" in semantic_flags and re.search(r"별지|목록", title_text) + and not (item.get("schedule_items") or [])): + coverage_gaps.append("schedule_items") + if ("notice_doc" in semantic_flags and re.search(r"통지|내용증명|최고", title_text) + and not (item.get("notice_components") or [])): + coverage_gaps.append("notice_components") + if ("claim_doc" in semantic_flags and re.search(r"차용|금전소비대차|이행각서|각서", title_text) + and not (item.get("claim_document_components") or []) + and not (item.get("loan_components") or [])): + coverage_gaps.append("claim_or_loan_components") + if coverage_gaps: + reviews.append(suspect( + "component_coverage_concern", + "document type and title indicate an empty required component container", + [expected], [item.get("title")], + {"missing_components": coverage_gaps, "doc_type": item.get("doc_type"), + "doc_semantic_flags": sorted(semantic_flags)}, + )) + except Exception as exc: + parse_failures.append({"ordinal": ordinal, "part_path": path, "reason": str(exc), + "rerun_target_task": f"Task_B1_map_doc_{ordinal}"}) + hard.append(finding("B1_PART_SCHEMA_OR_PARSE_FAILURE", str(exc), [f"E-{ordinal}"], + f"Task_B1_map_doc_{ordinal}")) + + for uid, rows in sorted(uid_rows.items()): + if len(rows) < 2: continue + hashes = {row["content_sha256"] for row in rows if row["content_sha256"]} + ids = [row["evidence_index"] for row in rows] + if len(hashes) > 1: + hard.append(finding("EVIDENCE_IDENTITY_COLLISION", + f"doc_uid {uid} maps to different source hashes", ids)) + else: + reviews.append(suspect("suspected_duplicate_alias", + f"doc_uid {uid} appears in multiple evidence ordinals", ids, + [row["title"] for row in rows], {"doc_uid": uid})) + # A generic normalized title such as "내용증명" or "차용증" is not an alias signal by itself. + # Alias review is emitted only by the doc_uid/source-hash rule above. + + items.sort(key=lambda row: int(str(row.get("evidence_index")).split("-")[-1])) + final_ids = [str(row.get("evidence_index")) for row in items] + if set(final_ids) != set(expected_ids) or len(items) != shard_count: + hard.append(finding("EVIDENCE_CONSERVATION_FAILURE", + f"expected {shard_count} items/{len(expected_ids)} IDs, got {len(items)} items/{len(final_ids)} IDs", + sorted(set(expected_ids) ^ set(final_ids)))) + if len(final_ids) != len(set(final_ids)): + hard.append(finding("DUPLICATE_FINAL_EVIDENCE_ID", "final evidence IDs are not unique", final_ids)) + + final_obj = {"schema_contract_version": "evidence_indexed.v3", "items": items} + final_text = json.dumps(final_obj, ensure_ascii=False, indent=2) + parsed_final = json.loads(final_text) + if [x.get("evidence_index") for x in parsed_final["items"]] != final_ids: + raise RuntimeError("post-serialization B1 ID verification failed") + set_material = json.dumps(sorted(final_ids), ensure_ascii=False, separators=(",", ":")) + blocked = bool(hard) + manifest = { + "schema_version": "finalized_evidence_id_manifest.v1", + "status": "BLOCKED" if blocked else "READY", + "source_artifact": FINAL_PATH, + "shard_count": shard_count, + "expected_evidence_ids": expected_ids, + "finalized_evidence_ids": final_ids, + "missing_evidence_ids": sorted(set(expected_ids) - set(final_ids)), + "unexpected_evidence_ids": sorted(set(final_ids) - set(expected_ids)), + "evidence_id_set_sha256": hashlib.sha256(set_material.encode("utf-8")).hexdigest(), + } + checks = [ + {"check_id": "shard_completeness", "severity": "BLOCK" if missing or unexpected or stale else "PASS", + "detail": f"missing={len(missing)}, unexpected={len(unexpected)}, stale={len(stale)}"}, + {"check_id": "evidence_index_determinism", "severity": "BLOCK" if parse_failures else "PASS", + "detail": f"parse_or_schema_failures={len(parse_failures)}"}, + {"check_id": "duplicate_document_alias", "severity": "HARD_WARNING" if any(x["exception_type"] == "suspected_duplicate_alias" for x in reviews) else "PASS", + "detail": "duplicate aliases are preserved and routed for review"}, + {"check_id": "document_component_coverage", "severity": "HARD_WARNING" if any(x["exception_type"] in ("mapper_self_warning", "component_coverage_concern") for x in reviews) else "PASS", + "detail": "named component containers and explicit mapper warnings checked without raw-evidence re-read"}, + {"check_id": "authority_drift_detection", "severity": "BLOCK" if any(x["issue_type"] in ("b1_part_schema_or_parse_failure", "evidence_identity_collision") for x in hard) else "PASS", + "detail": "ordinal, source pointer and self-isolation attestation checked"}, + ] + status = "BLOCKED" if blocked else ("READY_WITH_SUSPECTS" if reviews else "READY") + precheck = { + "schema_version": "B1_gate_precheck.v1", "status": status, + "expected_ordinals": expected_ordinals, "received_ordinals": actual_ordinals, + "missing_ordinals": missing, "unexpected_ordinals": unexpected, "stale_parts": stale, + "parse_failures": parse_failures, "hard_gate_findings": hard, + "review_candidates": reviews, "checks": checks, + "metrics": {"input_parts_count": len(actual_ordinals), "final_items_count": len(items), + "input_bytes": input_bytes, "output_bytes": len(final_text.encode("utf-8")), + "elapsed_ms": int((time.monotonic() - started) * 1000)} + } + write_doc(FINAL_PATH, final_obj) + write_doc(MANIFEST_PATH, manifest) + write_doc(PRECHECK_PATH, precheck) + print(json.dumps({"status": status, "final_items": len(items), "review_candidates": len(reviews), + "hard_findings": len(hard)}, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B2_gate_merge_deterministic + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import copy + import hashlib + import itertools + import json + import re + import time + from collections import defaultdict + from datetime import date + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + PLAN_PATH = "evidence_shard_plan.json" + PART_DIR = "evidence_event_candidate_parts" + MANIFEST_PATH = "finalized_evidence_id_manifest.json" + B1_PRECHECK_PATH = "quality_gates/B1_gate_precheck.json" + FINAL_PATH = "evidence_event_candidates.json" + PRECHECK_PATH = "quality_gates/B2_gate_precheck.json" + EXCEPTION_PATH = "quality_gates/B12_exception_pack.json" + ZERO_REASONS = {"NO_LEGAL_EVENT_FOUND", "REFERENCE_ONLY_DOC", "PARSE_INSUFFICIENT", "ORDINAL_MATCH_FAILED"} + CANDIDATE_RE = re.compile(r"^EVT-(\d{3})-(\d{2})$") + CHAIN_ORDER = { + "underlying_claim_arising": ("claim", 0), "first_demand": ("claim", 1), + "demand_notice_dispatch": ("claim", 2), "demand_notice_arrival": ("claim", 3), + "debt_acknowledgement": ("claim", 4), + "lien_extinction_notice_dispatch": ("lien", 0), "lien_extinction_notice_receipt": ("lien", 1), + "lien_extinction_effect_candidate": ("lien", 2), + "secured_debt_creation": ("auction", 0), "mortgage_setting_by_fraction": ("auction", 1), + "auction_application_after_extinguishment": ("auction", 2), + "auction_sale_and_registry_transfer": ("auction", 3), + "preserved_claim_arising": ("actio", 0), "fraudulent_transfer": ("actio", 1), + "encumbrance_release_after_act": ("actio", 2), + } + + def _mid(): return next(MSG_ID) + + def _init(): + r = CLIENT.post(LOCALDOCS_URL, json={ + "jsonrpc": "2.0", "id": 1, "method": "initialize", + "params": {"protocolVersion": "2025-03-26", "capabilities": {}, + "clientInfo": {"name": "task-b2-gate-merge-deterministic", "version": "1.0", + "user_id": "{{__user_hash__}}", "workspace_id": "{{__workspace_hash__}}"}} + }, headers=MCP_HEADERS) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post(LOCALDOCS_URL, json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS).raise_for_status() + + def _parse_response(text: str): + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: return json.loads(line[6:]) + except Exception: pass + try: return json.loads(text) + except Exception: return None + + def _call(name: str, args: dict[str, Any]): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc": "2.0", "id": _mid(), "method": "tools/call", + "params": {"name": name, "arguments": args}}, + headers=MCP_HEADERS) + r.raise_for_status() + payload = _parse_response(r.text) + if not payload or "result" not in payload: raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: dict[str, Any], name: str): + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: raise RuntimeError(f"Empty MCP result: {name}") + try: outer = json.loads(text) + except Exception: return text + if isinstance(outer, dict) and "results" in outer: + row = (outer.get("results") or [{}])[0] + inner = row.get("content") or row.get("text") or "" + if not inner: raise RuntimeError(f"Empty document content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer + + def read_json(path: str): + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str): return raw + try: return json.loads(raw) + except json.JSONDecodeError: + obj, _ = json.JSONDecoder().raw_decode(raw.strip()) + return obj + + def write_doc(path: str, obj: Any): + text = json.dumps(obj, ensure_ascii=False, indent=2) + _call("write_file", {"path": path, "content": text, "overwrite": True}) + return text + + def _walk_strings(value: Any): + if isinstance(value, str): yield value + elif isinstance(value, list): + for item in value: yield from _walk_strings(item) + elif isinstance(value, dict): + for item in value.values(): yield from _walk_strings(item) + + def list_part_names(folder: str): + payload = None + errors = [] + for args in ({"path": folder, "pattern": "E-*.json"}, {}): + try: + payload = _unwrap(_call("list_docs", args), "list_docs") + break + except Exception as exc: errors.append(str(exc)) + if payload is None: raise RuntimeError("list_docs failed: " + " | ".join(errors)) + if isinstance(payload, str): + try: payload = json.loads(payload) + except Exception: pass + names = [] + for value in _walk_strings(payload): + match = re.search(r"(?:^|/)(E-\d{3}\.json)$", value) + if match: names.append(f"{folder}/{match.group(1)}") + return sorted(set(names)) + + def single_item(obj: Any, path: str): + if isinstance(obj, dict) and isinstance(obj.get("items"), list): + if len(obj["items"]) != 1 or not isinstance(obj["items"][0], dict): + raise ValueError(f"{path}: envelope must contain exactly one object") + return obj["items"][0] + if isinstance(obj, dict): return obj + raise ValueError(f"{path}: part must be an object") + + def refs(evidence_indexes=None, candidate_ids=None, meeting_refs=None, titles=None): + return { + "evidence_indexes": sorted(set(str(x) for x in (evidence_indexes or []) if x)), + "event_candidate_ids": sorted(set(str(x) for x in (candidate_ids or []) if x)), + "meeting_clause_refs": sorted(set(str(x) for x in (meeting_refs or []) if x)), + "doc_titles": sorted(set(str(x) for x in (titles or []) if x)), + } + + def hard_finding(code: str, detail: str, evidence_indexes=None, candidate_ids=None, rerun=None): + row = {"issue_type": code.lower(), "source_check": code.lower(), "severity": "BLOCK", + "detail": detail, "source_refs": refs(evidence_indexes, candidate_ids)} + if rerun: row["rerun_target_task"] = rerun + return row + + def review_candidate(kind: str, basis: str, evidence_indexes=None, candidate_ids=None, + titles=None, context=None): + return {"origin_gate": "B2", "exception_type": kind, "severity_candidate": "HARD_WARNING", + "source_refs": refs(evidence_indexes, candidate_ids, titles=titles), + "concise_basis": basis, "compact_context": context or {}} + + def warning_records(node: Any, path: str = "$"): + out = [] + if isinstance(node, dict): + for key, value in node.items(): + child = f"{path}.{key}" + if re.search(r"(self_?)?(warning|error)", str(key), re.I) and value not in (None, False, "", [], {}): + out.append({"path": child, "value": value}) + out.extend(warning_records(value, child)) + elif isinstance(node, list): + for i, value in enumerate(node): out.extend(warning_records(value, f"{path}[{i}]")) + return out + + def parse_date(value: Any): + if not value: return None + text = str(value) + m = re.search(r"(20\d{2}|19\d{2})[-./년]\s*(\d{1,2})[-./월]\s*(\d{1,2})", text) + if not m: return None + try: return date(int(m.group(1)), int(m.group(2)), int(m.group(3))) + except ValueError: return None + + def relation_key(candidate: dict[str, Any]): + legal = candidate.get("legal_effect_candidate") + if not isinstance(legal, dict): legal = {} + explicit = legal.get("related_claim_type") or candidate.get("claim_id") or candidate.get("lien_id") + if not explicit: return None + return json.dumps({"explicit": explicit, "object": candidate.get("object_spec"), + "fraction": candidate.get("fraction_ref")}, ensure_ascii=False, + sort_keys=True, separators=(",", ":")) + + def candidate_rank(row: dict[str, Any]): + candidate = row["candidate"] + subkind = str(candidate.get("event_subkind") or "") + action = str(candidate.get("action_type_candidate") or "").lower() + derived = subkind.endswith("_candidate") or "derived" in action + confidence = {"high": 0, "medium": 1, "low": 2, "unknown": 3}.get(str(candidate.get("confidence")), 4) + supports = candidate.get("support_locators") + support_count = len(supports) if isinstance(supports, list) else 0 + return (1 if derived else 0, confidence, -support_count, row["ordinal"], row["candidate_id"]) + + def build_packs(reviews: list[dict[str, Any]], blocked: bool): + def clipped_context(value: Any, remaining: list[int]): + if isinstance(value, str): + take = max(0, min(len(value), remaining[0])) + remaining[0] -= take + return value[:take] + if isinstance(value, list): + return [clipped_context(x, remaining) for x in value] + if isinstance(value, dict): + return {str(k): clipped_context(v, remaining) for k, v in value.items()} + return value + + prepared = [] + for raw in reviews: + item = copy.deepcopy(raw) + item["compact_context"] = clipped_context(item.get("compact_context") or {}, [1500]) + prepared.append(item) + ordered = sorted(prepared, key=lambda x: (x.get("origin_gate", ""), x.get("exception_type", ""), + json.dumps(x.get("source_refs") or {}, ensure_ascii=False, sort_keys=True), + x.get("concise_basis", ""))) + for i, item in enumerate(ordered, 1): item["exception_id"] = f"B12-EXC-{i:04d}" + if blocked: + return [], [{**item, "reason": "hard_gate_blocked_before_adjudication"} for item in ordered], False + groups = defaultdict(list) + for item in ordered: groups[item["exception_type"]].append(item) + packs = [] + overflow = [] + for exception_type in sorted(groups): + current = [] + for item in groups[exception_type]: + item_refs = item.get("source_refs") or {} + item_evidence = set(item_refs.get("evidence_indexes") or []) + item_candidates = set(item_refs.get("event_candidate_ids") or []) + item_chars = len(json.dumps([item], ensure_ascii=False)) + if len(item_evidence) > 2 or len(item_candidates) > 8 or item_chars > 6000: + overflow.append({**item, "reason": "BUDGET_OVERFLOW_BLOCK_REVIEW"}) + continue + trial = current + [item] + ev = {x for row in trial for x in (row.get("source_refs") or {}).get("evidence_indexes", [])} + cand = {x for row in trial for x in (row.get("source_refs") or {}).get("event_candidate_ids", [])} + chars = len(json.dumps(trial, ensure_ascii=False)) + if current and (len(ev) > 2 or len(cand) > 8 or chars > 6000): + packs.append({"exception_type": exception_type, "items": current}) + current = [item] + else: + current = trial + if current: packs.append({"exception_type": exception_type, "items": current}) + if len(packs) > 8: + for pack in packs[8:]: + overflow.extend({**item, "reason": "BUDGET_OVERFLOW_BLOCK_REVIEW"} for item in pack["items"]) + packs = packs[:8] + descriptors = [] + for i, pack in enumerate(packs, 1): + pack_id = f"PACK-{i:03d}" + descriptors.append({ + "pack_id": pack_id, + "output_path": f"quality_gates/B12_adjudication_parts/{pack_id}.json", + "exception_type": pack["exception_type"], + "exception_ids": [x["exception_id"] for x in pack["items"]], + "items": pack["items"], + }) + return descriptors, overflow, bool(overflow) + + def main(): + started = time.monotonic() + _init() + plan = read_json(PLAN_PATH) + manifest = read_json(MANIFEST_PATH) + b1_precheck = read_json(B1_PRECHECK_PATH) + expected_ordinals = [str(x).zfill(3) for x in plan.get("expected_ordinals") or []] + expected_ids = [str(x) for x in manifest.get("finalized_evidence_ids") or []] + shard_count = int(plan.get("shard_count") or len(expected_ordinals)) + hard = [] + reviews = list(b1_precheck.get("review_candidates") or []) + b2_reviews = [] + schema_findings = [] + dedup_findings = [] + field_failures = [] + if manifest.get("status") != "READY" or b1_precheck.get("status") == "BLOCKED": + hard.append(hard_finding("UPSTREAM_B1_BLOCKED", "B1 deterministic gate is blocked")) + actual_names = list_part_names(PART_DIR) + actual_ordinals = sorted({re.search(r"E-(\d{3})\.json$", x).group(1) for x in actual_names + if re.search(r"E-(\d{3})\.json$", x)}) + missing = sorted(set(expected_ordinals) - set(actual_ordinals)) + unexpected = sorted(set(actual_ordinals) - set(expected_ordinals)) + stale = sorted(set((plan.get("stale_part_policy") or {}).get("detected_stale_parts", {}) + .get("evidence_event_candidate_parts") or []) | + {f"{PART_DIR}/E-{x}.json" for x in unexpected}) + for ordinal in missing: + hard.append(hard_finding("MISSING_B2_PART", f"missing B2 mapper part E-{ordinal}", + [f"E-{ordinal}"], rerun=f"Task_B2_map_events_{ordinal}")) + for ordinal in unexpected: + hard.append(hard_finding("UNEXPECTED_B2_PART", f"unexpected B2 mapper part E-{ordinal}", + [f"E-{ordinal}"])) + for path in stale: hard.append(hard_finding("STALE_B2_PART", f"stale B2 mapper part: {path}")) + + final_items = [] + candidate_rows = [] + all_candidate_ids = [] + input_candidate_count = 0 + input_bytes = 0 + for ordinal in expected_ordinals: + path = f"{PART_DIR}/E-{ordinal}.json" + if ordinal in missing: continue + evidence_id = f"E-{ordinal}" + try: + raw = read_json(path) + input_bytes += len(json.dumps(raw, ensure_ascii=False).encode("utf-8")) + item = single_item(raw, path) + if str(item.get("evidence_index_proposed") or "") != evidence_id: + raise ValueError("evidence_index_proposed mismatch") + candidates = item.get("event_candidates") + zero_reason = item.get("zero_event_reason") + if not isinstance(candidates, list): raise ValueError("event_candidates must be an array") + if not candidates and zero_reason not in ZERO_REASONS: + raise ValueError("empty event_candidates requires an allowed non-null zero_event_reason") + final_item = copy.deepcopy(item) + final_item["evidence_index"] = final_item.pop("evidence_index_proposed") + final_item.pop("mapper_meta", None) + final_candidates = [] + input_candidate_count += len(candidates) + for seq, candidate in enumerate(candidates, 1): + if not isinstance(candidate, dict): raise ValueError(f"candidate {seq} is not an object") + proposed = str(candidate.get("candidate_id_proposed") or "") + source = str(candidate.get("source_evidence_index_proposed") or "") + match = CANDIDATE_RE.fullmatch(proposed) + if not match or match.group(1) != ordinal: + raise ValueError(f"invalid candidate_id_proposed: {proposed}") + if source != evidence_id or source not in expected_ids: + raise ValueError(f"invalid source evidence reference: {source}") + if not candidate.get("identity_signature"): + raise ValueError(f"{proposed}: identity_signature missing") + projected = copy.deepcopy(candidate) + projected["candidate_id"] = projected.pop("candidate_id_proposed") + projected["source_evidence_index"] = projected.pop("source_evidence_index_proposed") + expected_keys = (set(candidate) - {"candidate_id_proposed", "source_evidence_index_proposed"} | + {"candidate_id", "source_evidence_index"}) + if set(projected) != expected_keys: + field_failures.append({"candidate_id": proposed, "reason": "key set mismatch"}) + restored = copy.deepcopy(projected) + restored["candidate_id_proposed"] = restored.pop("candidate_id") + restored["source_evidence_index_proposed"] = restored.pop("source_evidence_index") + if restored != candidate: + field_failures.append({"candidate_id": proposed, "reason": "deep value mismatch"}) + final_candidates.append(projected) + all_candidate_ids.append(proposed) + candidate_rows.append({"ordinal": int(ordinal), "evidence_index": evidence_id, + "candidate_id": proposed, "candidate": projected, + "title": item.get("title")}) + for flag in warning_records(candidate): + b2_reviews.append(review_candidate("mapper_self_warning", + "B2 mapper emitted an explicit warning or error", + [evidence_id], [proposed], [item.get("title")], + {"warning": flag})) + final_item["event_candidates"] = final_candidates + final_items.append(final_item) + except Exception as exc: + row = {"ordinal": ordinal, "part_path": path, "reason": str(exc), "severity": "BLOCK", + "rerun_target_task": f"Task_B2_map_events_{ordinal}"} + schema_findings.append(row) + hard.append(hard_finding("B2_PART_SCHEMA_OR_PARSE_FAILURE", str(exc), [evidence_id], + rerun=f"Task_B2_map_events_{ordinal}")) + + if len(all_candidate_ids) != len(set(all_candidate_ids)): + hard.append(hard_finding("DUPLICATE_FINAL_CANDIDATE_ID", "candidate IDs are not globally unique", + candidate_ids=all_candidate_ids)) + if field_failures: + hard.append(hard_finding("CANDIDATE_FIELD_CONSERVATION_FAILURE", + f"{len(field_failures)} candidate projection failures", + candidate_ids=[x["candidate_id"] for x in field_failures])) + + by_signature = defaultdict(list) + for row in candidate_rows: by_signature[str(row["candidate"].get("identity_signature"))].append(row) + for signature, rows in sorted(by_signature.items()): + if not signature or len(rows) < 2: continue + canonical = sorted(rows, key=candidate_rank)[0] + ids = [row["candidate_id"] for row in rows] + evs = [row["evidence_index"] for row in rows] + dedup_findings.append({"identity_signature": signature, "candidate_ids": ids, + "canonical_candidate_id": canonical["candidate_id"], + "preservation_policy": "all_candidates_preserved"}) + b2_reviews.append(review_candidate("suspected_duplicate_alias", + "same identity signature appears in multiple evidence candidates", + evs, ids, [row["title"] for row in rows], + {"canonical_candidate_id": canonical["candidate_id"]})) + + chronology_groups = defaultdict(list) + for row in candidate_rows: + subkind = str(row["candidate"].get("event_subkind") or "") + if subkind not in CHAIN_ORDER: continue + key = relation_key(row["candidate"]) + event_date = parse_date(row["candidate"].get("event_date")) + if key and event_date: + chain, position = CHAIN_ORDER[subkind] + chronology_groups[(chain, key)].append((position, event_date, row)) + for (chain, _), rows in chronology_groups.items(): + rows.sort(key=lambda x: x[0]) + for left, right in zip(rows, rows[1:]): + if left[1] > right[1]: + involved = [left[2], right[2]] + b2_reviews.append(review_candidate("chronology_conflict", + f"deterministic date inversion in {chain} chain", + [x["evidence_index"] for x in involved], + [x["candidate_id"] for x in involved], + [x["title"] for x in involved], + {"left_date": str(left[1]), "right_date": str(right[1])})) + + # Defense assertions are explicit routing facts, not ambiguity exceptions. + # Relay them as one deterministic Stage 2 review item without an LLM call. + defense_rows = [] + for row in candidate_rows: + candidate = row["candidate"] + defense = candidate.get("defense_candidate") + legal = candidate.get("legal_effect_candidate") + subkind = str(candidate.get("event_subkind") or "") + needs_rebuttal = isinstance(defense, dict) and defense.get("requires_rebuttal_in_stage2") is True + explicit_defense = subkind.startswith("defense_assertion") + legal_defense = isinstance(legal, dict) and legal.get("effect_type") == "defense_assertion" + if needs_rebuttal or explicit_defense or legal_defense: + defense_rows.append(row) + auto_review_findings = [] + if defense_rows: + auto_review_findings.append({ + "origin_gate": "B2", + "exception_type": "defense_and_obstacle_review", + "severity": "SOFT_WARNING", + "source_check": "deterministic_defense_assertion_relay", + "source_refs": refs( + [row["evidence_index"] for row in defense_rows], + [row["candidate_id"] for row in defense_rows], + titles=[row["title"] for row in defense_rows], + ), + "candidate_use_policy": "preserve_as_needs_review", + "review_instruction": "Stage 2에서 상계, 책임부인 및 기타 방어 주장의 성립과 반박 필요성을 검토한다.", + "concise_basis": "구조화된 defense assertion 후보를 후속 방어·장애사유 검토로 전달", + }) + + final_items.sort(key=lambda row: int(str(row.get("evidence_index")).split("-")[-1])) + final_candidate_count = sum(len(row.get("event_candidates") or []) for row in final_items) + final_ids = [str(row.get("evidence_index")) for row in final_items] + if set(final_ids) != set(expected_ids) or len(final_items) != shard_count: + hard.append(hard_finding("EVENT_ITEM_CONSERVATION_FAILURE", + f"expected {shard_count} items, got {len(final_items)}", + sorted(set(expected_ids) ^ set(final_ids)))) + if input_candidate_count != final_candidate_count: + hard.append(hard_finding("CANDIDATE_CONSERVATION_FAILURE", + f"input={input_candidate_count}, final={final_candidate_count}")) + + final_obj = {"schema_version": "evidence_event_candidates.v1", "items": final_items} + final_text = json.dumps(final_obj, ensure_ascii=False, indent=2) + reparsed = json.loads(final_text) + if sum(len(x.get("event_candidates") or []) for x in reparsed["items"]) != input_candidate_count: + raise RuntimeError("post-serialization candidate conservation failed") + reviews.extend(b2_reviews) + blocked = bool(hard) + packs, auto_blocked, overflow = build_packs(reviews, blocked) + exception_status = "BLOCKED" if blocked else ("READY" if packs else "READY_NO_EXCEPTIONS") + exception_obj = { + "schema_version": "stage1_part1_gate_exception_pack.v1", + "status": exception_status, "exception_count": len(reviews), "pack_count": len(packs), + "budget_gate": {"max_evidence_items_per_pack": 2, "max_event_candidates_per_pack": 8, + "max_excerpt_chars_per_pack": 1500, "max_serialized_chars_per_pack": 6000, + "max_pack_count": 8, "overflow_detected": overflow}, + "packs": packs, "auto_blocked_review_items": auto_blocked, + } + checks = [ + {"check_id": "event_candidate_completeness", "severity": "BLOCK" if missing or unexpected or stale else "PASS", + "detail": f"missing={len(missing)}, unexpected={len(unexpected)}, stale={len(stale)}"}, + {"check_id": "evidence_reference_integrity", "severity": "BLOCK" if any(x["issue_type"] == "b2_part_schema_or_parse_failure" for x in hard) else "PASS", + "detail": "all candidate evidence references checked against compact manifest"}, + {"check_id": "candidate_id_collision", "severity": "BLOCK" if len(all_candidate_ids) != len(set(all_candidate_ids)) else ("HARD_WARNING" if dedup_findings else "PASS"), + "detail": f"duplicate_signature_groups={len(dedup_findings)}; candidates are not deleted"}, + {"check_id": "chronological_ordering", "severity": "HARD_WARNING" if any(x["exception_type"] == "chronology_conflict" for x in b2_reviews) else "PASS", + "detail": "only exact dates with an explicit relation key are compared"}, + {"check_id": "legal_effect_event_coverage", "severity": "HARD_WARNING" if any(x["exception_type"] == "mapper_self_warning" for x in b2_reviews) else "PASS", + "detail": "explicit mapper warnings are escalated without re-reading raw evidence"}, + ] + status = "BLOCKED" if blocked else ( + "READY_WITH_SUSPECTS" if reviews or auto_review_findings else "READY") + precheck = { + "schema_version": "B2_gate_precheck.v1", "status": status, + "upstream_b1_status": b1_precheck.get("status"), + "expected_ordinals": expected_ordinals, "received_ordinals": actual_ordinals, + "missing_ordinals": missing, "unexpected_ordinals": unexpected, "stale_parts": stale, + "schema_invariant_findings": schema_findings, "hard_gate_findings": hard, + "review_candidates": b2_reviews, "auto_review_findings": auto_review_findings, + "dedup_findings": dedup_findings, + "conservation": {"expected_items": shard_count, "final_items": len(final_items), + "input_candidates": input_candidate_count, "final_candidates": final_candidate_count, + "candidate_field_failures": field_failures}, + "checks": checks, + "metrics": {"input_bytes": input_bytes, "output_bytes": len(final_text.encode("utf-8")), + "elapsed_ms": int((time.monotonic() - started) * 1000)} + } + write_doc(FINAL_PATH, final_obj) + write_doc(PRECHECK_PATH, precheck) + write_doc(EXCEPTION_PATH, exception_obj) + print(json.dumps({"dynamic_fanout": packs, "exception_status": exception_status, + "exception_pack_count": len(packs)}, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B1B2_gate_llm_adjudicator_* + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: medium + llm_verbosity: low + max_iterations: 2 + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Execute only this compact Stage 1 exception-adjudication sub-task. + - The assigned pack, output schema, and writer boundary override broader instructions. + - Use only the assigned pack embedded below. + - Do not read or reconstruct any full planner, evidence, event, meeting, BO, or downstream file. + - Do not create facts, dates, amounts, parties, IDs, or source references. + - Write only the assigned adjudication output path. + - Do not modify any final evidence/event artifact, mapper part, precheck, or audit. + - Return only short basis fields; keep chain-of-thought private. + - Do not draft complaint prose or decide final merits. + + + + { + "pack_id": "{{item.pack_id}}", + "output_path": "{{item.output_path}}", + "exception_type": "{{item.exception_type}}", + "exception_ids": {{item.exception_ids}}, + "items": {{item.items}} + } + + + + You are a compact exception-only adjudicator assisting a top-tier Korean civil-litigation lawyer. + Validate pack_id, exception_type, exception_ids, items, and output_path. + Produce exactly one decision for every assigned exception_id and no others. + Allowed decisions are PASS_NO_ACTION, REVIEW_SOFT, REVIEW_HARD, BLOCK_SOURCE_INTEGRITY, BLOCK_REVIEW. + Use BLOCK_SOURCE_INTEGRITY only when source contamination or identity failure is explicit in the pack. + Use BLOCK_REVIEW when the compact record is insufficient. Legal ambiguity is not a Stage 1 integrity BLOCK. + Source refs in each decision must be a subset of that exception item's supplied source_refs. + Severity mapping is exact: + PASS_NO_ACTION -> PASS + REVIEW_SOFT -> SOFT_WARNING + REVIEW_HARD -> HARD_WARNING + BLOCK_REVIEW -> HARD_WARNING + BLOCK_SOURCE_INTEGRITY -> BLOCK + + + + { + "schema_version": "stage1_part1_gate_adjudication_part.v1", + "pack_id": "{{item.pack_id}}", + "status": "READY|READY_WITH_BLOCK_REVIEW|FAILED", + "decisions": [ + { + "exception_id": "B12-EXC-0001", + "exception_type": "assigned exception type", + "decision": "PASS_NO_ACTION|REVIEW_SOFT|REVIEW_HARD|BLOCK_SOURCE_INTEGRITY|BLOCK_REVIEW", + "severity": "PASS|SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "assigned exception type", + "source_refs": { + "evidence_indexes": [], + "event_candidate_ids": [], + "meeting_clause_refs": [], + "doc_titles": [] + }, + "candidate_use_policy": "prioritize_if_supported|preserve_as_needs_review|do_not_generate_without_support", + "review_instruction": "120 characters or fewer", + "concise_basis": "240 characters or fewer" + } + ] + } + + + + 1. Build the exact JSON object in memory. + 2. Call localdocs write_file with path={{item.output_path}}, overwrite=true, and the raw JSON object serialized as JSON. + 3. Do not write any other file. + 4. End with exactly: {{item.pack_id}} adjudication written + + + - task_name: Task_B12_gate_audit_finalizer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import itertools + import json + from datetime import datetime, timezone + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + B1_PRECHECK = "quality_gates/B1_gate_precheck.json" + B2_PRECHECK = "quality_gates/B2_gate_precheck.json" + EXCEPTION_PACK = "quality_gates/B12_exception_pack.json" + B1_AUDIT = "quality_gates/B1_evidence_indexed_gate.json" + B2_AUDIT = "quality_gates/B2_event_candidates_gate.json" + REF_KEYS = ("evidence_indexes", "event_candidate_ids", "meeting_clause_refs", "doc_titles") + ALLOWED_DECISIONS = {"PASS_NO_ACTION", "REVIEW_SOFT", "REVIEW_HARD", + "BLOCK_SOURCE_INTEGRITY", "BLOCK_REVIEW"} + DECISION_SEVERITY = {"PASS_NO_ACTION": "PASS", "REVIEW_SOFT": "SOFT_WARNING", + "REVIEW_HARD": "HARD_WARNING", "BLOCK_REVIEW": "HARD_WARNING", + "BLOCK_SOURCE_INTEGRITY": "BLOCK"} + + def _mid(): return next(MSG_ID) + + def _init(): + r = CLIENT.post(LOCALDOCS_URL, json={ + "jsonrpc": "2.0", "id": 1, "method": "initialize", + "params": {"protocolVersion": "2025-03-26", "capabilities": {}, + "clientInfo": {"name": "task-b12-gate-audit-finalizer", "version": "1.0", + "user_id": "{{__user_hash__}}", "workspace_id": "{{__workspace_hash__}}"}} + }, headers=MCP_HEADERS) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post(LOCALDOCS_URL, json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS).raise_for_status() + + def _parse_response(text: str): + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: return json.loads(line[6:]) + except Exception: pass + try: return json.loads(text) + except Exception: return None + + def _call(name: str, args: dict[str, Any]): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc": "2.0", "id": _mid(), "method": "tools/call", + "params": {"name": name, "arguments": args}}, + headers=MCP_HEADERS) + r.raise_for_status() + payload = _parse_response(r.text) + if not payload or "result" not in payload: raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: dict[str, Any], name: str): + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: raise RuntimeError(f"Empty MCP result: {name}") + try: outer = json.loads(text) + except Exception: return text + if isinstance(outer, dict) and "results" in outer: + row = (outer.get("results") or [{}])[0] + inner = row.get("content") or row.get("text") or "" + if not inner: raise RuntimeError(f"Empty document content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer + + def read_json(path: str): + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str): return raw + try: return json.loads(raw) + except json.JSONDecodeError: + obj, _ = json.JSONDecoder().raw_decode(raw.strip()) + return obj + + def write_doc(path: str, obj: Any): + text = json.dumps(obj, ensure_ascii=False, indent=2) + json.loads(text) + _call("write_file", {"path": path, "content": text, "overwrite": True}) + + def norm_refs(value: Any): + obj = value if isinstance(value, dict) else {} + return {key: sorted(set(str(x) for x in (obj.get(key) or []) if x)) for key in REF_KEYS} + + def source_subset(candidate: dict[str, list[str]], allowed: dict[str, list[str]]): + return all(set(candidate[key]).issubset(set(allowed[key])) for key in REF_KEYS) + + def normalized_hard(raw: Any, origin: str, seq: int): + row = raw if isinstance(raw, dict) else {"detail": str(raw)} + return { + "gate_finding_id": str(row.get("gate_finding_id") or f"{origin}-HARD-{seq:03d}"), + "source_check": str(row.get("source_check") or row.get("issue_type") or "integrity_failure"), + "severity": "BLOCK", + "issue_type": str(row.get("issue_type") or "integrity_failure"), + "source_refs": norm_refs(row.get("source_refs")), + "detail": str(row.get("detail") or row.get("reason") or "integrity failure"), + **({"rerun_target_task": row.get("rerun_target_task")} if row.get("rerun_target_task") else {}), + } + + def review_from_item(item: dict[str, Any], severity: str, basis: str, decision: str): + return { + "source_check": str(item.get("source_check") or item.get("exception_type") or "review_finding"), + "severity": severity, + "issue_type": str(item.get("exception_type") or "review_finding"), + "domain_hints": list(item.get("domain_hints") or []), + "target_stage_hints": list(item.get("target_stage_hints") or []), + "target_task_hints": list(item.get("target_task_hints") or []), + "source_refs": norm_refs(item.get("source_refs")), + "review_instruction": str(item.get("review_instruction") or "후속 검토 단계에서 증거와 법률상 의미를 재검토한다.")[:120], + "candidate_use_policy": str(item.get("candidate_use_policy") or "preserve_as_needs_review"), + "concise_basis": str(basis or item.get("concise_basis") or decision)[:240], + "decision": decision, + } + + def severity_of(hard: list[Any], reviews: list[dict[str, Any]]): + if hard: return "BLOCK" + if any(x.get("severity") == "HARD_WARNING" for x in reviews): return "HARD_WARNING" + if reviews: return "SOFT_WARNING" + return "PASS" + + def progression(severity: str): + return "BLOCKED" if severity == "BLOCK" else ("ALLOWED" if severity == "PASS" else "ALLOWED_WITH_WARNING") + + def clean_checks(value: Any): + out = [] + for row in value if isinstance(value, list) else []: + if not isinstance(row, dict): continue + out.append({k: v for k, v in row.items() if k not in ("hard_gate_findings", "review_findings")}) + return out + + def main(): + _init() + b1 = read_json(B1_PRECHECK) + b2 = read_json(B2_PRECHECK) + pack = read_json(EXCEPTION_PACK) + b1_hard = [normalized_hard(x, "B1", i) for i, x in enumerate(b1.get("hard_gate_findings") or [], 1)] + b2_hard = [normalized_hard(x, "B2", i) for i, x in enumerate(b2.get("hard_gate_findings") or [], 1)] + exception_map = {} + pack_by_id = {} + for descriptor in pack.get("packs") or []: + pack_id = str(descriptor.get("pack_id") or "") + pack_by_id[pack_id] = descriptor + for item in descriptor.get("items") or []: + if isinstance(item, dict): exception_map[str(item.get("exception_id"))] = item + + decisions = {} + for pack_id, descriptor in sorted(pack_by_id.items()): + output_path = str(descriptor.get("output_path") or "") + try: + adjudication = read_json(output_path) + if adjudication.get("schema_version") != "stage1_part1_gate_adjudication_part.v1": + raise ValueError("schema_version mismatch") + if adjudication.get("pack_id") != pack_id: raise ValueError("pack_id mismatch") + expected = set(str(x) for x in descriptor.get("exception_ids") or []) + seen = [] + for decision in adjudication.get("decisions") or []: + if not isinstance(decision, dict): raise ValueError("decision must be an object") + exception_id = str(decision.get("exception_id") or "") + if exception_id not in expected or exception_id in decisions: + raise ValueError(f"unexpected or duplicate exception_id {exception_id}") + code = str(decision.get("decision") or "") + if code not in ALLOWED_DECISIONS: raise ValueError(f"invalid decision {code}") + if decision.get("severity") != DECISION_SEVERITY[code]: + raise ValueError(f"severity mismatch for {exception_id}") + item = exception_map[exception_id] + decision_refs = norm_refs(decision.get("source_refs")) + if not source_subset(decision_refs, norm_refs(item.get("source_refs"))): + raise ValueError(f"ADJUDICATION_SOURCE_REF_OUT_OF_SCOPE: {exception_id}") + decision = {**decision, "source_refs": decision_refs} + decisions[exception_id] = decision + seen.append(exception_id) + if set(seen) != expected: raise ValueError("decision set does not equal expected exception set") + except Exception as exc: + origins = {str(exception_map[x].get("origin_gate") or "B2") for x in descriptor.get("exception_ids") or [] + if x in exception_map} + target = b1_hard if origins == {"B1"} else b2_hard + target.append(normalized_hard({"issue_type": "adjudication_part_missing_or_invalid", + "detail": f"{output_path}: {exc}"}, "B1" if target is b1_hard else "B2", + len(target) + 1)) + + b1_reviews = [] + b2_reviews = [] + for item in b2.get("auto_review_findings") or []: + if not isinstance(item, dict): continue + b2_reviews.append(review_from_item( + item, + str(item.get("severity") or "SOFT_WARNING"), + str(item.get("concise_basis") or "deterministic review relay"), + "AUTO_REVIEW", + )) + for exception_id, item in sorted(exception_map.items()): + decision = decisions.get(exception_id) + if not decision: continue + code = decision["decision"] + origin = str(item.get("origin_gate") or "B2") + if code == "PASS_NO_ACTION": continue + if code == "BLOCK_SOURCE_INTEGRITY": + target = b1_hard if origin == "B1" else b2_hard + target.append(normalized_hard({"issue_type": item.get("exception_type"), + "source_refs": decision.get("source_refs"), + "detail": decision.get("concise_basis")}, origin, + len(target) + 1)) + continue + row = review_from_item({**item, **decision}, DECISION_SEVERITY[code], + str(decision.get("concise_basis") or ""), code) + (b1_reviews if origin == "B1" else b2_reviews).append(row) + + for item in pack.get("auto_blocked_review_items") or []: + if not isinstance(item, dict): continue + origin = str(item.get("origin_gate") or "B2") + row = review_from_item(item, "HARD_WARNING", str(item.get("reason") or "BLOCK_REVIEW"), + "BLOCK_REVIEW") + (b1_reviews if origin == "B1" else b2_reviews).append(row) + + all_reviews = b1_reviews + b2_reviews + if len(all_reviews) > 40: + overflow = all_reviews[40:] + b1_count = min(len(b1_reviews), 40) + b1_reviews = all_reviews[:b1_count] + b2_reviews = all_reviews[b1_count:40] + b2_hard.append(normalized_hard({ + "issue_type": "review_queue_overflow", + "detail": f"review findings exceed SHA writer limit: total={len(all_reviews)}, overflow={len(overflow)}", + "source_refs": {"evidence_indexes": sorted({x for row in overflow for x in row["source_refs"]["evidence_indexes"]})} + }, "B2", len(b2_hard) + 1)) + + for origin, rows in (("B1", b1_reviews), ("B2", b2_reviews)): + for i, row in enumerate(rows, 1): row["gate_finding_id"] = f"{origin}-REVIEW-{i:03d}" + b1_severity = severity_of(b1_hard, b1_reviews) + b2_severity = severity_of(b2_hard, b2_reviews) + highest_global = "HARD_WARNING" if any(x.get("severity") == "HARD_WARNING" for x in b1_reviews + b2_reviews) else ( + "SOFT_WARNING" if b1_reviews or b2_reviews else "PASS") + now = datetime.now(timezone.utc).isoformat() + + def risk_notes(rows): + return [str(x.get("concise_basis") or x.get("issue_type"))[:120] for x in rows] + + b1_audit = { + "gate_id": "B1_evidence_indexed", "executed_at": now, + "checks": clean_checks(b1.get("checks")), "overall_severity": b1_severity, + "block_reasons": [x["detail"] for x in b1_hard], + "warning_reasons": risk_notes(b1_reviews), + "hard_gate_findings": b1_hard, "review_findings": b1_reviews, + "rerun_targets": sorted({x.get("rerun_target_task") for x in b1_hard if x.get("rerun_target_task")}), + "stage2_risk_notes": risk_notes(b1_reviews), + "review_gate_summary": {"review_gate_count": len(b1_reviews), + "highest_review_severity": severity_of([], b1_reviews)}, + "handoff_export_summary": {"is_soft_handoff_writer": False, + "handoff_writer_task": "Task_B2_SHA256_soft_gate_handoff_writer", + "exportable_review_count": len(b1_reviews)}, + "downstream_progression": progression(b1_severity), + "stage2_auto_progression_allowed": b1_severity != "BLOCK", + "schema_contract_version": "B1_quality_gate.v4", + } + b2_audit = { + "gate_id": "B2_event_candidates", "executed_at": now, + "schema_invariant_findings": b2.get("schema_invariant_findings") or [], + "checks": clean_checks(b2.get("checks")), "overall_severity": b2_severity, + "block_reasons": [x["detail"] for x in b2_hard], + "warning_reasons": risk_notes(b2_reviews), + "hard_gate_findings": b2_hard, "review_findings": b2_reviews, + "rerun_targets": sorted({x.get("rerun_target_task") for x in b2_hard if x.get("rerun_target_task")}), + "stage2_risk_notes": risk_notes(b2_reviews), + "review_gate_summary": {"review_gate_count": len(b2_reviews), + "highest_review_severity": severity_of([], b2_reviews), + "b1_review_count": len(b1_reviews), "b2_review_count": len(b2_reviews)}, + "handoff_export_summary": {"is_soft_handoff_writer": False, + "handoff_writer_task": "Task_B2_SHA256_soft_gate_handoff_writer", + "handoff_path": "quality_gates/stage1_part1_soft_gate_handoff.json", + "exported_review_count": len(b2_reviews), + "handoff_status_candidate": "BLOCKED" if b1_hard or b2_hard else ( + "READY_WITH_REVIEW" if b1_reviews or b2_reviews else "READY_NO_REVIEW")}, + "downstream_progression": progression(b2_severity), + "stage2_auto_progression_allowed": b2_severity != "BLOCK", + "schema_contract_version": "B2_quality_gate.v4", + } + if b1_audit["review_gate_summary"]["review_gate_count"] != len(b1_audit["review_findings"]): + raise RuntimeError("B1 review count mismatch") + if b2_audit["review_gate_summary"]["review_gate_count"] != len(b2_audit["review_findings"]): + raise RuntimeError("B2 review count mismatch") + for audit in (b1_audit, b2_audit): + for row in audit["hard_gate_findings"] + audit["review_findings"]: + if set((row.get("source_refs") or {}).keys()) != set(REF_KEYS): + raise RuntimeError("source_refs named-object schema violation") + write_doc(B1_AUDIT, b1_audit) + write_doc(B2_AUDIT, b2_audit) + print(json.dumps({"status": "AUDITS_FINALIZED", "b1_severity": b1_severity, + "b2_severity": b2_severity, "review_count": len(b1_reviews) + len(b2_reviews), + "highest_review_severity": highest_global}, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B2_SHA256_soft_gate_handoff_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import hashlib + import itertools + import json + import re + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + SIDE_CAR_PATH = "quality_gates/stage1_part1_soft_gate_handoff.json" + INPUT_PATHS = { + "evidence_indexed_sha256": "evidence_indexed.json", + "evidence_event_candidates_sha256": "evidence_event_candidates.json", + "b1_gate_sha256": "quality_gates/B1_evidence_indexed_gate.json", + "b2_gate_sha256": "quality_gates/B2_event_candidates_gate.json", + } + WORKFLOW_ROLES = ( + "claim_right_identification", + "cause_of_action_element_build", + "evidence_weight_and_gap_check", + "chronology_and_state_split", + "asset_registry_succession_check", + "defense_and_obstacle_check", + "relief_scope_and_quantification", + "actio_value_compensation_check", + ) + SOURCE_REF_KEYS = ( + "evidence_indexes", + "event_candidate_ids", + "meeting_clause_refs", + "doc_titles", + ) + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-b2-sha256-soft-gate-handoff-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _mid(), + "method": "tools/call", + "params": {"name": name, "arguments": args}, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: Any, name: str) -> str: + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError(f"Empty: {name}") + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, dict) and "results" in outer: + result0 = (outer.get("results") or [{}])[0] + inner = result0.get("content") or result0.get("text") or "" + if not inner: + raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer if isinstance(outer, str) else json.dumps(outer, ensure_ascii=False) + + def read_raw(path: str) -> str: + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str) or not raw.strip(): + raise RuntimeError(f"Missing or empty required input: {path}") + return raw + + def parse_json(raw: str, path: str) -> Any: + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + try: + obj, _ = decoder.raw_decode(raw.strip()) + return obj + except Exception as exc: + raise RuntimeError(f"Invalid JSON input: {path}") from exc + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def digest(raw: str) -> str: + value = hashlib.sha256(raw.encode("utf-8")).hexdigest() + if not re.fullmatch(r"[0-9a-f]{64}", value): + raise RuntimeError("digest_guard produced a non-SHA256 value") + return value + + def as_list(value: Any) -> list[Any]: + if value is None: + return [] + if isinstance(value, list): + return value + return [value] + + def as_str_list(value: Any) -> list[str]: + out: list[str] = [] + for item in as_list(value): + if item is None: + continue + if isinstance(item, dict): + for key in ("id", "value", "name", "title", "ref"): + if item.get(key): + out.append(str(item[key]).strip()) + break + else: + out.append(str(item).strip()) + return [x for x in out if x] + + def clean_enum(value: Any, fallback: str) -> str: + text = str(value or fallback).strip().lower() + text = re.sub(r"[^a-z0-9_]+", "_", text) + text = re.sub(r"_+", "_", text).strip("_") + return text or fallback + + def clip(value: Any, limit: int = 120) -> str: + text = " ".join(str(value or "").split()) + if len(text) <= limit: + return text + return text[:limit].rstrip() + "..." + + def normalize_severity(value: Any) -> str: + text = str(value or "").upper() + if "HARD" in text: + return "HARD_WARNING" + return "SOFT_WARNING" + + def role_for(issue_type: str, finding: dict[str, Any]) -> str: + explicit = finding.get("lawyer_workflow_role") + if explicit in WORKFLOW_ROLES: + return str(explicit) + text = " ".join([ + issue_type, + str(finding.get("source_check") or ""), + str(finding.get("check_id") or ""), + str(finding.get("detail") or finding.get("reason") or ""), + ]).lower() + if any(k in text for k in ("chrono", "date", "timeline", "state", "order")): + return "chronology_and_state_split" + if any(k in text for k in ("evidence", "proof", "source", "locator")): + return "evidence_weight_and_gap_check" + if any(k in text for k in ("registry", "secured", "lien", "succession", "asset", "auction", "mortgage")): + return "asset_registry_succession_check" + if any(k in text for k in ("defense", "obstacle", "setoff", "limitation")): + return "defense_and_obstacle_check" + if any(k in text for k in ("relief", "amount", "scope", "valuation", "damage")): + return "relief_scope_and_quantification" + if "actio" in text or "fraudulent" in text or "value_compensation" in text: + return "actio_value_compensation_check" + if any(k in text for k in ("claim", "right", "element", "legal_effect")): + return "claim_right_identification" + return "cause_of_action_element_build" + + def source_refs(finding: dict[str, Any]) -> dict[str, list[str]]: + refs = {key: [] for key in SOURCE_REF_KEYS} + nested = finding.get("source_refs") + if isinstance(nested, dict): + for key in SOURCE_REF_KEYS: + refs[key].extend(as_str_list(nested.get(key))) + refs["evidence_indexes"].extend(as_str_list(finding.get("evidence_indexes"))) + refs["evidence_indexes"].extend(as_str_list(finding.get("evidence_index"))) + refs["evidence_indexes"].extend(as_str_list(finding.get("source_evidence_index"))) + refs["event_candidate_ids"].extend(as_str_list(finding.get("event_candidate_ids"))) + refs["event_candidate_ids"].extend(as_str_list(finding.get("candidate_id"))) + refs["event_candidate_ids"].extend(as_str_list(finding.get("candidate_ids"))) + refs["meeting_clause_refs"].extend(as_str_list(finding.get("meeting_clause_refs"))) + refs["meeting_clause_refs"].extend(as_str_list(finding.get("meeting_clause_ref"))) + refs["doc_titles"].extend(as_str_list(finding.get("doc_titles"))) + refs["doc_titles"].extend(as_str_list(finding.get("doc_title"))) + return {key: sorted(set(value)) for key, value in refs.items()} + + def extract_findings(gate: Any, key: str) -> list[Any]: + findings: list[Any] = [] + if isinstance(gate, dict): + findings.extend(as_list(gate.get(key))) + for check in as_list(gate.get("checks")): + if isinstance(check, dict): + findings.extend(as_list(check.get(key))) + return findings + + def hard_findings(gate: Any, source_gate: str) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for item in extract_findings(gate, "hard_gate_findings"): + out.append(item if isinstance(item, dict) else {"detail": str(item)}) + if isinstance(gate, dict): + blocked = gate.get("overall_severity") == "BLOCK" or gate.get("downstream_progression") == "BLOCKED" or gate.get("stage2_auto_progression_allowed") is False + if blocked and not out: + reasons = as_list(gate.get("block_reasons")) + if not reasons: + reasons = [f"{source_gate} gate blocked without explicit hard_gate_findings"] + for reason in reasons: + out.append({"issue_type": "block_reason", "detail": str(reason)}) + for index, item in enumerate(out, 1): + item.setdefault("gate_finding_id", f"{source_gate}-HARD-{index:03d}") + return out + + def compact_review_items(b1_gate: Any, b2_gate: Any) -> list[dict[str, Any]]: + compact: list[dict[str, Any]] = [] + ordered = [("B1", b1_gate), ("B2", b2_gate)] + seq = 1 + for source_gate, gate in ordered: + for raw in extract_findings(gate, "review_findings"): + finding = raw if isinstance(raw, dict) else {"detail": str(raw)} + issue_type = clean_enum( + finding.get("issue_type") or finding.get("check_id") or finding.get("source_check") or "review_finding", + "review_finding", + ) + role = role_for(issue_type, finding) + refs = source_refs(finding) + policy = finding.get("candidate_use_policy") + if policy not in ("prioritize_if_supported", "preserve_as_needs_review", "do_not_generate_without_support"): + policy = "preserve_as_needs_review" + item = { + "review_id": f"P1RG-{seq:04d}", + "source_gate": source_gate, + "source_check": clean_enum(finding.get("source_check") or finding.get("check_id") or issue_type, issue_type), + "severity": normalize_severity(finding.get("severity")), + "issue_type": issue_type, + "lawyer_workflow_role": role, + "domain_hints": as_str_list(finding.get("domain_hints") or finding.get("domain_hint") or finding.get("domains")), + "target_stage_hints": as_str_list(finding.get("target_stage_hints") or finding.get("target_stage_hint")), + "target_task_hints": as_str_list(finding.get("target_task_hints") or finding.get("target_task_hint")), + "source_refs": refs, + "review_instruction": clip(finding.get("review_instruction") or finding.get("instruction") or "후속 검토 단계에서 해당 review finding의 증거와 법률상 의미를 재검토한다.", 120), + "negative_instruction": "사실 확정 또는 자동 차단으로 취급하지 말 것", + "candidate_use_policy": policy, + "concise_basis": clip(finding.get("concise_basis") or finding.get("detail") or finding.get("reason") or issue_type, 120), + "created_from": { + "gate_file": "quality_gates/B1_evidence_indexed_gate.json" if source_gate == "B1" else "quality_gates/B2_event_candidates_gate.json", + "gate_finding_id": finding.get("gate_finding_id") or finding.get("finding_id") or finding.get("id"), + }, + } + compact.append(item) + seq += 1 + if len(compact) >= 40: + return compact + return compact + + def add_index(index: dict[str, dict[str, list[str]]], bucket: str, key: str, review_id: str) -> None: + if not key: + return + index[bucket].setdefault(key, []) + if review_id not in index[bucket][key]: + index[bucket][key].append(review_id) + + def build_routing(review_items: list[dict[str, Any]]) -> tuple[dict[str, dict[str, list[str]]], dict[str, list[str]]]: + routing = { + "by_evidence_index": {}, + "by_event_candidate_id": {}, + "by_meeting_clause_ref": {}, + "by_issue_type": {}, + "by_domain_hint": {}, + "by_target_stage": {}, + "by_lawyer_workflow_role": {}, + } + queues = {role: [] for role in WORKFLOW_ROLES} + for item in review_items: + rid = item["review_id"] + refs = item.get("source_refs") or {} + for value in refs.get("evidence_indexes", []): + add_index(routing, "by_evidence_index", value, rid) + for value in refs.get("event_candidate_ids", []): + add_index(routing, "by_event_candidate_id", value, rid) + for value in refs.get("meeting_clause_refs", []): + add_index(routing, "by_meeting_clause_ref", value, rid) + add_index(routing, "by_issue_type", item.get("issue_type", ""), rid) + for value in item.get("domain_hints", []): + add_index(routing, "by_domain_hint", value, rid) + for value in item.get("target_stage_hints", []): + add_index(routing, "by_target_stage", value, rid) + role = item.get("lawyer_workflow_role", "cause_of_action_element_build") + add_index(routing, "by_lawyer_workflow_role", role, rid) + queues.setdefault(role, []).append(rid) + return routing, {role: queues.get(role, []) for role in WORKFLOW_ROLES} + + def gate_blocked(gate: Any) -> bool: + return isinstance(gate, dict) and ( + gate.get("overall_severity") == "BLOCK" + or gate.get("downstream_progression") == "BLOCKED" + or gate.get("stage2_auto_progression_allowed") is False + ) + + def main() -> None: + _init() + raw_by_digest_key = {key: read_raw(path) for key, path in INPUT_PATHS.items()} + parsed = {key: parse_json(raw, INPUT_PATHS[key]) for key, raw in raw_by_digest_key.items()} + + digest_guard = {key: digest(raw) for key, raw in raw_by_digest_key.items()} + b1_gate = parsed["b1_gate_sha256"] + b2_gate = parsed["b2_gate_sha256"] + review_items = compact_review_items(b1_gate, b2_gate) + routing_index, lawyer_workflow_queues = build_routing(review_items) + + hard_items = hard_findings(b1_gate, "B1") + hard_findings(b2_gate, "B2") + hard_ids = [ + str(item.get("gate_finding_id") or item.get("finding_id") or item.get("id") or f"HARD-{idx:03d}") + for idx, item in enumerate(hard_items, 1) + ] + blocked = bool(hard_items) or gate_blocked(b1_gate) or gate_blocked(b2_gate) + highest_review = "PASS" + if any(item["severity"] == "HARD_WARNING" for item in review_items): + highest_review = "HARD_WARNING" + elif review_items: + highest_review = "SOFT_WARNING" + b1_count = sum(1 for item in review_items if item["source_gate"] == "B1") + b2_count = sum(1 for item in review_items if item["source_gate"] == "B2") + + if blocked: + handoff_status = "BLOCKED" + elif review_items: + handoff_status = "READY_WITH_REVIEW" + else: + handoff_status = "READY_NO_REVIEW" + + sidecar = { + "schema_version": "stage1_part1_soft_gate_handoff.v1", + "source_stage": "Stage_1.Part_1", + "source_gate_files": [ + "quality_gates/B1_evidence_indexed_gate.json", + "quality_gates/B2_event_candidates_gate.json", + ], + "handoff_status": handoff_status, + "stage2_auto_progression_allowed": not blocked, + "hard_gate_summary": { + "hard_gate_count": len(hard_items), + "hard_gate_ids": hard_ids, + }, + "review_gate_summary": { + "review_gate_count": len(review_items), + "highest_review_severity": highest_review, + "b1_review_count": b1_count, + "b2_review_count": b2_count, + }, + "digest_guard": digest_guard, + "review_items": review_items, + "routing_index": routing_index, + "lawyer_workflow_queues": lawyer_workflow_queues, + } + + content = json.dumps(sidecar, ensure_ascii=False, indent=2) + write_doc(SIDE_CAR_PATH, content) + print(json.dumps({ + "status": "SOFT_GATE_HANDOFF_WRITTEN", + "path": SIDE_CAR_PATH, + "handoff_status": handoff_status, + "review_items": len(review_items), + "digest_guard_valid": all(re.fullmatch(r"[0-9a-f]{64}", v) for v in digest_guard.values()), + }, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + task_procedure: + IN: + nexts: ["Task_A_client_goal", "Task_Evidence_shard_planner"] + wait_until: [] + + Task_A_client_goal: + nexts: [] + wait_until: ["IN"] + + Task_Evidence_shard_planner: + nexts: ["Task_B1_map_doc_*"] + wait_until: ["IN"] + + Task_B1_map_doc_*: + nexts: + - "Task_B2_map_events_{same_ordinal}" + - "Task_B1_gate_merge_deterministic" + wait_until: ["Task_Evidence_shard_planner"] + + Task_B2_map_events_*: + nexts: ["Task_B2_gate_merge_deterministic"] + wait_until: ["Task_B1_map_doc_{same_ordinal}"] + + Task_B1_gate_merge_deterministic: + nexts: ["Task_B2_gate_merge_deterministic"] + wait_until: ["all Task_B1_map_doc_*"] + + Task_B2_gate_merge_deterministic: + nexts: + - "Task_B1B2_gate_llm_adjudicator_*" + - "Task_B12_gate_audit_finalizer" + wait_until: + - "all Task_B2_map_events_*" + - "Task_B1_gate_merge_deterministic" + + Task_B1B2_gate_llm_adjudicator_*: + nexts: ["Task_B12_gate_audit_finalizer"] + wait_until: ["Task_B2_gate_merge_deterministic"] + + Task_B12_gate_audit_finalizer: + nexts: ["Task_B2_SHA256_soft_gate_handoff_writer"] + wait_until: + - "Task_B2_gate_merge_deterministic" + - "all Task_B1B2_gate_llm_adjudicator_*" + + Task_B2_SHA256_soft_gate_handoff_writer: + nexts: ["OUT"] + wait_until: ["Task_B12_gate_audit_finalizer"] + + OUT: + nexts: [] + wait_until: ["Task_B2_SHA256_soft_gate_handoff_writer"] + + prevs: [] + nexts: [] diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_1_v2.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_1_v2.yml new file mode 100644 index 00000000..8dd6907a --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_1_v2.yml @@ -0,0 +1,3404 @@ +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_1 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Part 1 기초작업 + version: v.2 + + Stages: + - name: stage1_사건개요파악_기초작업 + description: 사건개요 파악, 기초정보 추출 + llm_provider: openai + llm_model: gpt-4o-2024-08-06 + tools: + mcpServers: + localdocs: + type: streamable-http + url: "http://mcp-localdocs:8012/mcp" + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + + tasks: + - task_name: Task_A_client_goal + llm_provider: google + llm_model: 'gemini-3.1-flash-lite' + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + preflight: false + preflight_files: + - client_meeting.md + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + - Aggregate, compile, publish, commit, or write only the inputs and final/derived file authorized by the current task. + - Preserve stable order, source IDs, support IDs, BO_ID, fact_id, structure_id, claim_group_id, liability_group_id, and required enum semantics exactly. + - Create or finalize IDs only from the current task's specified pattern and deterministic ordering rule. + - Do not merge, deduplicate, supersede, normalize, repair, or drop candidates unless the current reducer/writer schema authorizes that action. + - If validation or source support is insufficient, use the current failure, rollback, warning, or blocked path instead of fabricating a passing final artifact. + - Treat downstream authority and handoff fields as machine contracts; do not add prose, alternative files, or extra writer attestations outside the schema. + + + + + Task_A_client_goal + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. + + + `client_meeting.md`만 읽고 `client_goal.json`을 생성한다. + 이 파일은 고객의 소송 목적, 명시적 제약, 핵심 사건군, 당사자 universe를 Stage 1의 가장 앞단에서 안정적으로 구조화한 요약본이어야 한다. + v.3에서도 기존 필수 schema는 유지하되, 복수 원고와 복수 분쟁군이 뒤섞여 later stage가 공동원고를 과잉 일반화하지 않도록 사건군 단위의 optional 구조, 사건군별 원고적격 구조, 현재 지배상태 구조를 함께 남긴다. + 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않는다. + 특히 상속·승계형 사건군에서는 meeting만으로 확정되지 않은 친족관계 또는 공동행동 정보를 direct standing anchor처럼 과잉 일반화하지 않는다. + 상속이 개입된 사건군에서는 `legal entitlement layer`와 `complaint drafting preference layer`를 분리하여 기록한다. + 사망한 종전 권리주체의 과거 소유사실, 매수사실, 등기명의사실을 living plaintiff의 현재 `소유자/final`로 치환하지 않는다. + 필요하면 사건군별 optional additive field `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`, `preferred_plaintiff_selection_basis`, `succession_resolution_needed`를 사용하여, later stage가 실체법상 가능한 권리자 후보층과 실제 소장에 세울 우선 원고층을 혼동하지 않도록 한다. + 또한 `current_control_map`은 보조 메모가 아니라 later stage의 부당이득·인도·철거 청구 식별에 직접 쓰이는 authority map이므로, 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속을 가능한 한 4축으로 분해 저장한다. + v.3 기준으로 Task_A_client_goal은 모든 issue cluster에 사건군별 법률효과 구조의 seed를 남겨야 한다. 단순 사건 요약이 아니라 금전채권 timeline, 상호속용 책임, 담보부 부동산·지분별 권리상태, 사해행위취소 가액배상, 유치권·상속·통지 lifecycle을 downstream이 구조적으로 읽을 수 있게 `domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints`, `legal_entitlement_candidates`, `current_control_map_4axis`를 additive하게 생성한다. + `client_requested_relief`는 의뢰인이 원하는 청구 기간·금액·상대방을 보존하는 층이고, `legal_sustainability_watchpoints`는 법률상 기산일·원고적격·청구범위 재검토 필요성을 보존하는 층이다. 두 층을 절대 하나로 합치지 않는다. + 상속·승계형 사건군에서 current right holder가 meeting만으로 확정되지 않으면 `current_control_map`의 해당 row는 `certainty="provisional"`로 두고 `standing_watchpoints`에 succession-resolution trigger를 반드시 남긴다. + 또한 현재 무자력/집행난과 실제 제소 유지 의사를 혼동하지 않으며, 특정 상대방을 현재 무자력이라는 이유만으로 소 제기 대상에서 소거하지 않는다. + 필요하면 optional additive field `defendant_target_matrix`를 사용하여 피고별 소제기 유지/제외 의사, 현재 회수위험, 장래 회수신호를 분리 보존한다. + 본 task는 v.3 architecture에서 final writer로서 `client_goal.json`을 직접 작성한다(Task_A는 island/mapper가 없으므로 propose 단계 없이 직접 finalize 가능). + + + + + - In this task, legal/domain fields are meeting-grounded routing seeds and watchpoints for downstream stages. + - They are not final legal conclusions, norm search results, merits judgments, or complaint-drafting advice. + - The EXTRACTION overlay's prohibition on norm search and final legal-effect classification does not prohibit schema-authorized `domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints`, or entitlement/watchpoint seed fields. + + + + + - `client_meeting.md` + + + - `client_goal.json` + + + - 다른 파일 확인/읽기 + - 중간 파일 생성 + - `client_goal.json` 재읽기 + - meeting에 없는 당사자, 사건군, 제약, 희망사항, 청구유형을 창작하기 + - 사건군별 당사자 범위를 무시하고 전 사건 공통 사실처럼 과잉 압축하기 + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하기 + + + + + 1. `list_docs`로 `client_meeting.md` 존재만 확인한다. + 2. `read_docs`로 `client_meeting.md`만 읽는다. + 3. 메모리에서 `client_goal.json` 객체를 생성한다. + 4. `write_file(overwrite=true)`로 `client_goal.json`을 저장한다. + 5. `list_docs`로 `client_goal.json` 존재만 확인한다. 절대 읽지 않는다. + 6. 정확히 `"고객의사 초안 생성 완료"`만 출력하고 종료한다. + + + + - 필수 입력 누락, 빈 파일, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. 파일은 쓰지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + + - `client_meeting.md`만 사실 원천이다. + - 상대방 주장, 의뢰인 희망사항, 상담서두의 인적 관계, 본문 1~5항의 사건군을 모두 읽되, meeting에 직접 나타난 범위만 구조화한다. + - meeting의 문장들 사이를 조합해 새로운 사실을 만들지 않는다. + + + + + - `primary_goal`은 사건 전체를 한 문장으로 요약하되, 개별 분쟁군을 완전히 뭉개지 않을 정도의 수준으로만 추상화한다. + - `희망사항`이 복수이면 공통 분모를 중심으로 요약하되, 특정 핵심 구제수단이 반복되면 반영할 수 있다. + - 희망사항이 피고별 비대칭 전략을 직접 드러내면, 그 비대칭을 보존한다. + - 현재 회수곤란 사실이 있더라도 장래 재산취득, 상속, 보험금 회수, 급여압류 가능성 등으로 특정 피고를 제소 대상으로 유지하려는 의사가 직접 서술되면, 이를 `primary_goal` 또는 `summary_key_incidents`에 반영한다. + - 직접 금전청구 대상과 사해행위취소 대상이 다르면 한쪽으로 뭉개지 않는다. + - 불명확하면 `null`. + + + + - `constraints`에는 의뢰인이 명시적으로 원하지 않는 소, 특정 대상 제외, 가능하면/다만/아울러와 같은 제한적 요청, 상속·경매 공신력 등 사건 진행상의 명시적 제약을 넣는다. + - `constraints`는 소송전략상 하지 말 것, 제한할 것, 절차상·법률상 한계만 담는다. + - 단순한 회수곤란 사실, 현재 무자력, 현재 재산 부족, 집행난은 `constraints`에 넣지 않는다. + - 현재 무자력과 장래 재산취득 가능성이 함께 서술되면, 이를 부정적 제약으로 압축하지 않는다. + - 특정 상대방을 제외하라는 명시적 no-sue 지시가 없는 한, current collectability만으로 대상 제외 제약을 만들지 않는다. + - 해석상 추정 제약은 넣지 않는다. + - 배열 순서는 meeting 서술 순서를 최대한 따른다. + + + + - `summary_key_incidents`는 최대 4문장, 각 100자 이하. + - 각 문장은 가능한 한 서로 다른 사건군을 대표하도록 구성한다. + - 사건군 라벨은 입력자료에 직접 나타난 분쟁 대상, 거래유형, 자산군, 청구 방향을 짧게 추상화하여 만든다. 특정 사건의 지명·상호·인명·자산명을 예시값처럼 재사용하지 않는다. + - 하나의 문장에 서로 다른 사건군을 과도하게 병합하지 않는다. + - 직접 서술된 날짜/당사자/핵심 행위만 사용한다. + - 원고가 특정 피고를 직접 금전청구 대상으로 유지하려는 의사가 직접 서술되면, 사건군 대표 문장 중 하나에 그 방향성을 반영할 수 있다. + - 현재 회수곤란 사실과 장래 회수가능성 신호가 같은 사람에 관하여 함께 직접 서술되면, 둘 중 하나만 선택적으로 삭제하지 않는다. + + + + - `key_facts`는 최대 5개, 각 60자 이하. + - 가능한 한 사건군별 핵심 fact를 1개씩 대표시킨다. + - later stage의 원고적격 판단에 중요할 수 있는 소유관계, 상속관계, 현재 권리귀속, 현재 점유/사용/수익귀속 상태, 특정 처분/경매/통지 사실을 우선한다. + - 직접 서술이 없는 금액·날짜·상태를 보충하지 않는다. + + + + - `claim_type_candidates`는 의뢰인의 희망사항과 사건군 구조를 바탕으로 선택한다. + - 허용 enum만 사용한다: `"금전"|"물건인도"|"행위"|"확인"|"형성"|"보전(가처분/가압류)"`. + - later stage 분류기를 돕기 위한 상위 유형 수준만 남기고, 세부 청구명을 쓰지 않는다. + + + + + + - 이름은 원문 우선, 명백히 동일인/동일법인인 경우만 canonicalize 한다. + - 별칭/변형은 `aliases`에 기록한다. + - 당사자 배열은 사건 전체 participant universe다. 모든 분쟁군의 실질 원고적격/피고적격을 의미하지 않는다. + + + + - `parties.plaintiffs`에는 실제 의뢰인 또는 meeting 서두와 희망사항에서 원고 측 client로 직접 읽히는 사람만 넣는다. + - `parties.plaintiffs`는 client universe일 뿐, 사건군별 실질 원고적격을 의미하지 않는다. + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않는다. + - 사건군별 원고 범위 차이는 `issue_clusters`, `plaintiff_capacity_matrix`, `standing_watchpoints`에서 보정한다. + + + + - `parties.defendants`에는 meeting 본문, 상대방 주장, 희망사항에서 분쟁 상대방 또는 소송상 직접 대상으로 명시된 자만 넣는다. + - `asset_status`는 meeting에 직접 나온 재산 상태, 현재 소유/점유/담보권 설정 상태 등 later stage에 직접 도움이 되는 짧은 상태만 넣는다. + - `asset_status`는 현재 재산 상태에 관한 사실기술일 뿐, 소 제기 대상 제외 신호가 아니다. + - meeting이 "현재 무자력이나 장래 상속/재산취득 가능성 있음"을 직접 서술하면, `asset_status`는 그 둘을 함께 보존하는 방향으로 적는다. + - 현재 무자력만을 이유로 defendants에서 제외하지 않는다. 제외는 명시적 no-sue 지시가 있을 때만 허용한다. + - meeting에 없는 재산상태 추정 금지. + + + + - `parties.third_parties`에는 사건군에 중요하지만 즉시 원고/피고로 확정되지 않은 자를 넣는다. + - `relationship`은 later stage가 읽을 수 있도록 짧고 직접적으로 적는다. + + + + + - optional additive field `defendant_target_matrix`를 허용한다. + - 이 필드는 피고별 소제기 유지/제외 의사와 현재 회수위험을 분리 기록하는 전략 매트릭스다. + - `target_status`는 제소 유지 여부를 나타내고, `execution_risk`는 current collectability를 나타낸다. + - 현재 무자력은 `execution_risk`에 반영하고, 제소 유지 여부는 `target_status`에 별도로 반영한다. + - 장래 상속, 장래 재산취득, 보험금 회수 가능성, 급여압류 가능성 등 미래 회수신호가 직접 서술되면 `future_recovery_signal`에 반영한다. + - 명시적 no-sue 지시가 없는 한, 단순한 current uncollectability만으로 `exclude_now`를 부여하지 않는다. + - 각 원소는 아래 구조를 따른다. + { + "name": "string", + "target_status": "target_now|exclude_now|secondary_target|review", + "basis_text": "meeting 직접 문장에 근거한 짧은 사유", + "execution_risk": "low|medium|high|unknown", + "future_recovery_signal": "none|possible|explicit" + } + - `basis_text`는 반드시 meeting 직접 문장에 근거한 짧은 근거여야 한다. + - 이 필드는 optional이다. 명시적 피고별 타기팅 차이가 없으면 생략할 수 있다. + + + + - optional additive field `issue_clusters`를 허용한다. + - 각 cluster는 meeting의 독립 분쟁군을 1개씩 반영한다. + - 이 필드는 later stage가 전 사건 공통 parties 배열만 보고 과잉 일반화하는 것을 줄이기 위한 보조 구조다. + - `candidate_plaintiffs`는 사건군별 잠정 plaintiff anchor일 뿐, 최종 원고적격 확정값이 아니다. + - `legal_entitlement_candidates`는 해당 사건군에서 meeting 직접 서술상 실체법상 권리자일 가능성이 있는 의뢰인 후보층이다. + - `preferred_plaintiffs_for_complaint`는 later stage가 실제 소장 원고로 우선 검토할 수 있는 narrower drafting layer다. + - 상속·승계형 사건군에서는 `candidate_plaintiffs`, `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`를 서로 자동 동일시하지 않는다. + - `candidate_plaintiffs`에는 meeting에서 해당 사건군의 직접 권리주체, 직접 채권자, 직접 임차인, 직접 점유권자 등으로 읽히는 의뢰인만 넣는다. + - 친족관계, 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 `candidate_plaintiffs`를 확장하지 않는다. + - 사망한 자가 해당 사건군의 종전 권리주체로 보이는데 meeting만으로 승계가 해소되지 않으면, 친족관계나 공동 통지 사실만으로 `candidate_plaintiffs`를 채우지 않는다. + - 위와 같은 unresolved succession cluster에서는 meeting이 직접 보여주는 친족관계와 자산 연결 범위 안에서만 `legal_entitlement_candidates`를 별도 기록할 수 있다. + - `preferred_plaintiffs_for_complaint`는 meeting이 특정 의뢰인을 직접 권리주체 또는 직접 소송유지 주체로 좁혀 보여주거나, 비상속형 사건군에서 direct right-holder가 명백할 때만 채운다. + - meeting만으로 drafting preference가 명백하지 않은 unresolved succession cluster에서는 `preferred_plaintiffs_for_complaint`를 빈 배열로 둘 수 있다. + - `preferred_plaintiff_selection_basis` 허용값은 `"direct_right_holder|resolved_succession|resolved_succession_needed|drafting_preference|review|불명"`이다. + - `succession_resolution_needed`는 사망한 종전 권리주체와 사건군의 연결은 보이지만 meeting만으로 현재 권리귀속 또는 complaint plaintiff narrowing이 해소되지 않을 때 `true`로 둔다. + - meeting이 직접 금전청구 대상 피고와 사해행위 상대방/수익자를 구별해 보여주면, 이를 별도 cluster로 분리할 수 있다. + - 직접 금전회수 대상이 특정 상대방으로 서술되면, property transfer cluster와 별도의 monetary recovery cluster를 허용한다. + - cluster 수는 1개 이상 6개 이하로 제한한다. + - 각 cluster는 아래 구조를 따른다. + { + "cluster_id": "IC-01", + "cluster_label": "짧은 사건군명", + "candidate_plaintiffs": ["이 사건군에서 직접 권리주체로 읽히는 의뢰인"], + "legal_entitlement_candidates": ["실체법상 권리자 후보로 읽히는 의뢰인"], + "preferred_plaintiffs_for_complaint": ["실제 소장 원고로 우선 검토할 의뢰인"], + "preferred_plaintiff_selection_basis": "direct_right_holder|resolved_succession|resolved_succession_needed|drafting_preference|review|불명", + "succession_resolution_needed": true, + "candidate_defendants_or_counterparties": ["직접 상대방"], + "desired_relief_keywords": ["금전", "명도", "퇴거", "확인", "보전" 등 짧은 키워드] + } + - 비상속형 사건군에서 direct right-holder가 명백하면 `candidate_plaintiffs`, `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`가 동일할 수 있다. + - `issue_clusters`는 optional이다. 명확하지 않으면 생략 가능하다. + - 추가 사실 창작 없이 meeting 본문과 희망사항에서 직접 읽히는 범위만 기록한다. + + + + - optional additive field `domain_profiles`를 각 `issue_clusters[*]` 내부에 허용한다. + - `domain_profiles`는 아래 네 하위 profile을 담는 object다. 해당하지 않는 profile은 `null`로 둔다. + - `monetary_claim_profile` + - `secured_property_dispute_profile` + - `actio_property_transfer_profile` + - `possession_lien_succession_profile` + - `client_requested_relief`와 `legal_sustainability_watchpoints`를 각 cluster 내부에 둘 수 있다. + - 의뢰인이 원하는 청구취지·기간·금액은 `client_requested_relief`에 보존하고, 법률상 기산일·원고적격·범위가 불명확한 점은 `legal_sustainability_watchpoints`에 분리한다. + - 당사자가 원하는 기간이나 금액을 법률상 청구취지에 바로 사용할 수 있다고 단정하지 않는다. + + + - 금전청구 사건군에서 원채무자 외 보증인, 채무인수자, 영업양수인, 상호속용자, 공동채무자 후보가 함께 등장하면 이를 별도 후보 피고 목록에만 두지 말고 `monetary_claim_profile`, `liability_group_hint`, `commercial_successor_liability_hint`로 묶어 보존한다. + - 동일 금액, 동일 원채권, 동일 원고를 기준으로 복수 피고가 책임질 가능성이 있으면 `liability_group_hint.same_underlying_debt=true`로 둔다. + - 지급기일, 이행청구, 최고, 채무승인, 이행각서, 소멸시효, 변제기 재설정 단서가 등장하면 `monetary_claim_profile.requires_money_claim_timeline=true`로 둔다. + - 영업양도, 상호 계속 사용, 사업 양수인, 채무승계 부인, 책임배제 등기 단서가 등장하면 `commercial_successor_liability_hint.requires_article_42_review=true`로 둔다. + + + + - 부동산 사건에서 여러 대여금, 여러 근저당권, 지분별 담보, 변제, 경매, 후행 등기가 함께 등장하면 이를 흩어진 issue로 분리하지 말고 `secured_property_dispute_profile`에 묶는다. + - 변제금이 복수 채무에 영향을 줄 수 있으면 `requires_payment_allocation=true`로 표시한다. + - 전체 부동산이 아니라 지분별 유효/무효가 문제될 수 있으면 `requires_fractional_validity_analysis=true`로 표시한다. + - 원고 주장 지분, 피고 측 유효 가능 지분, 전체 부동산 말소 금지 필요성을 `fractional_title_issue_hint`에 남긴다. + - 미등기 건물, 건물 신축, 건물 매매, 대지 임대차가 함께 등장하면 `land_use_gain_hint`를 생성한다. + - 임대차보증금, 월 차임, 건물 매매대금은 각각 법적 성격이 다르므로 `lease_payment_semantics_hint`에 별도 표시한다. + - 임대차보증금은 토지소유자의 부당이득 원금으로 자동 산정하지 말라는 watchpoint를 남긴다. + + + + - 채무자가 부동산을 수익자에게 이전하고, 원고가 사해행위취소를 희망하는 사건군을 발견하면 `actio_property_transfer_profile`을 생성한다. + - 부동산에 근저당권 등 부담이 있고, 그 부담이 사해행위 이후 변제·말소되었다는 단서가 있으면 `requires_value_compensation_review=true`로 표시한다. + - 같은 채권자와 채무자 사이에 복수 대여금이 등장하면 단순 후보채권 목록으로 흩뜨리지 말고 `preserved_claim_bundle_hint`로 묶는다. + - 피고가 명의신탁, 충분 담보, 상계, 선의 등을 주장하면 이를 `defense_hints`에 남기고, 청구원인 성립 또는 범위에 직접 영향을 주는 항변이면 `requires_stage2_rebuttal=true`로 표시한다. + + + + - 현재 점유자와 현재 권리자가 분리되는 자산군 또는 사건군이 입력자료에서 직접 확인되면 `possession_lien_succession_profile`을 생성한다. + - 상속이 개입되면 current right holder를 final로 확정하지 말고 `succession_resolution_needed=true`와 `standing_watchpoints`를 남긴다. + - 유치권, 제3자 임대, 직접 거주, 피담보채권 충당, 후소유자 소멸청구권 부정 주장이 보이면 `possession_basis_asserted`, `notice_lifecycle_hints`, `legal_sustainability_watchpoints`를 구조화한다. + - 통지서 발송일과 답변서상 수령 인정일이 함께 보이면 발송일과 도달일을 분리하여 `notice_lifecycle_hints`에 남긴다. + - 상속·승계가 미해결인 cluster에서는 입력자료상 공동 통지 발신자, 친족 후보, 직계존속·직계비속 후보를 그 지위의 후보로만 보존하고, 상속 해결 전 `preferred_plaintiffs_for_complaint` 확정자로 두지 않는다. + + + + + - optional additive field `current_control_map_4axis`를 허용한다. + - legacy `current_control_map` row 구조는 유지하되, 목적물별 4축 요약이 가능한 경우 `current_control_map_4axis`를 추가한다. + - 각 item은 아래 구조를 따른다. + { + "object": "string", + "cluster_id": "IC-01", + "current_right_holder_candidate": "string|null", + "current_possessor": "string|null", + "current_user": "string|null", + "current_benefit_holder": "string|null", + "possession_basis_asserted": "string|null", + "possession_basis_transition_needed": true, + "certainty": "final|provisional|불명" + } + - 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속을 하나의 generic holder로 뭉개지 않는다. + - 상속·승계형 사건군에서 현재 권리귀속이 decedent의 과거 권리사실과 친족관계로만 추론되는 경우 `certainty="provisional"`로 둔다. + + + + - optional additive field `plaintiff_capacity_matrix`를 허용한다. + - 이 필드는 사건군별 실질 원고적격을 later stage로 넘기기 위한 standing map이다. + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않는다. + - 각 원소는 아래 구조를 따른다. + { + "cluster_id": "IC-01", + "plaintiff_name": "string", + "capacity_basis": "소유자|공유자|상속인|임차인|채권자|점유권자|불명", + "source_basis_text": "meeting에서 직접 읽히는 짧은 근거", + "certainty": "final|provisional" + } + - 사건군별 직접 권리주체로 읽히는 사람만 넣는다. + - `capacity_basis="상속인"` row라도, 근거가 친족관계, 사망, 공동 통지, 공동 상담 같은 succession trigger 수준이면 `certainty`는 반드시 `"provisional"`로 둔다. + - 상속·승계형 사건군에서 decedent의 과거 소유사실, 매수사실, 등기명의사실만 보일 뿐 living plaintiff의 승계 완료 또는 현재 권리귀속이 meeting에서 직접 해소되지 않으면, living plaintiff를 `capacity_basis="소유자"` 또는 `capacity_basis="공유자"` + `certainty="final"`로 두지 않는다. + - 위 경우에는 원칙적으로 `capacity_basis`를 `"상속인"` 또는 `"불명"`으로 두고, `certainty`를 `"provisional"`로 둔다. + - meeting이 특정 사건군에 관하여 직접 계약당사자, 직접 채권자, 직접 임차인, 직접 소유·공유 관계 또는 승계 완료 상태를 직접 보여줄 때만 `certainty="final"`을 허용한다. + - 원고적격이 불명확하거나 later stage 재검토가 필요하면 `certainty`는 `"provisional"`로 둔다. + - `preferred_plaintiffs_for_complaint`가 별도로 존재하더라도, 이를 맞추기 위해 `plaintiff_capacity_matrix`를 `소유자/final`로 과잉 보정하지 않는다. + + + + - optional additive field `current_control_map`를 허용한다. + - 이 필드는 사건군별 현재 지배상태를 later stage로 넘기기 위한 authority map이다. + - 사건군에 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속 사실이 직접 읽히면 가능한 한 `current_control_map`을 생성한다. + - 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속은 가능한 한 분리한다. + - 각 원소는 아래 구조를 따른다. + { + "cluster_id": "IC-01", + "holder_name": "string", + "holder_role": "current_right_holder|current_possessor|current_user|current_benefit_holder", + "basis_text": "meeting에서 직접 읽히는 짧은 근거", + "certainty": "final|provisional" + } + - 같은 사람이 복수 role을 가지면 중복 기재할 수 있다. + - meeting에 직접 없는 role은 추정하지 않는다. + - 상속·승계형 사건군에서 current right holder가 decedent의 과거 권리사실 + 친족관계만으로 유도되는 경우, `holder_role="current_right_holder"` row는 원칙적으로 `certainty="provisional"`로 둔다. + - decedent의 과거 소유사실만으로 living plaintiff를 `current_right_holder/final`로 기재하지 않는다. + - 비상속형 사건군에서 meeting이 현재 소유자, 현재 점유자, 현재 사용자, 현재 수익귀속자를 직접 적시하면 해당 row는 `certainty="final"`을 우선 검토한다. + + + + - optional additive field `standing_watchpoints`를 허용한다. + - later stage가 반드시 재검토해야 하는 원고적격/권리귀속 리스크를 짧은 경고문으로 남긴다. + - 상속, 상속포기, 사망, 경매, 공동소유, 현재 권리주체 불명, 현재 점유자와 권리주체 불일치가 보이면 우선 검토한다. + - 경고문은 가능한 한 자산군 또는 사건군을 특정하는 asset-specific trigger 형태로 쓴다. + - 예시 형식: + - "상속·승계 자료 추가 확인 후 해당 자산군 원고적격 재확인 필요" + - "현재 권리귀속과 현재 점유자 분리 검토 필요" + - "사망한 종전 권리주체 관련 succession resolution 필요" + - meeting에 직접 읽히는 범위를 넘는 장문 법률판단은 쓰지 않는다. + + + + - `summary_key_incidents`, `key_facts`, `constraints`는 exact-match 중복 제거 후 최초 순서를 유지한다. + - `aliases`는 명백한 경우에만 기록한다. + - `claim_type_candidates`는 중복 제거 후 최초 순서를 유지한다. + - `defendant_target_matrix`를 사용하면 same `name` + `target_status` 조합 기준 exact-match 중복 제거 후 최초 순서를 유지한다. + - 비nullable string 외에는 `불명` 사용을 피하고 `null` 또는 빈 배열을 우선한다. + + + + 기존 필수 schema는 유지하고, 아래 optional additive field를 추가할 수 있다. + v.3 schema registry 기준 `IssueClusterDomainProfile`, `LegalEntitlementCandidate`, `CurrentControlMap4Axis`를 반영한다. + + { + "primary_goal": "string|null", + "constraints": [], + "summary_key_incidents": [], + "key_facts": [], + "claim_type_candidates": ["금전","물건인도","행위","확인","형성","보전(가처분/가압류)"], + "parties": { + "plaintiffs": [{"name":"string","type":"법인|자연인|기관"}], + "defendants": [{"name":"string","type":"법인|자연인|기관|미확정","asset_status":"string|null"}], + "third_parties": [{"name":"string","relationship":"string"}] + }, + "aliases": {}, + + "defendant_target_matrix": [ + { + "name": "string", + "target_status": "target_now|exclude_now|secondary_target|review", + "basis_text": "string", + "execution_risk": "low|medium|high|unknown", + "future_recovery_signal": "none|possible|explicit" + } + ], + + "issue_clusters": [ + { + "cluster_id": "IC-01", + "cluster_label": "string", + "candidate_plaintiffs": ["string"], + "legal_entitlement_candidates": ["string"], + "preferred_plaintiffs_for_complaint": ["string"], + "preferred_plaintiff_selection_basis": "direct_right_holder|resolved_succession|resolved_succession_needed|drafting_preference|review|불명", + "succession_resolution_needed": true, + "candidate_defendants_or_counterparties": ["string"], + "desired_relief_keywords": ["string"], + "domain_profiles": { + "monetary_claim_profile": { + "underlying_claim_type": "string|null", + "principal_amount_candidate": "string|null", + "principal_debtor_candidates": ["string"], + "successor_or_secondary_liability_candidates": [], + "default_or_limitation_issue_expected": true, + "known_demand_or_notice_signals": ["string"], + "requires_money_claim_timeline": true + }, + "secured_property_dispute_profile": { + "has_multiple_secured_debts": true, + "requires_payment_allocation": true, + "requires_fractional_validity_analysis": true, + "secured_object_candidates": ["string"], + "auction_or_registry_invalidity_expected": true + }, + "actio_property_transfer_profile": { + "debtor": "string|null", + "creditor": "string|null", + "beneficiary": "string|null", + "fraudulent_act_asset": "string|null", + "fraudulent_act_type": "string|null", + "fraudulent_act_date_hint": "string|null", + "is_encumbered_real_estate": true, + "requires_value_compensation_review": true + }, + "possession_lien_succession_profile": { + "possession_basis_asserted": "string|null", + "succession_resolution_needed": true, + "notice_lifecycle_hints": [], + "lien_violation_or_extinction_hints": [] + } + }, + "client_requested_relief": ["string"], + "legal_sustainability_watchpoints": ["string"], + "liability_group_hint": {}, + "commercial_successor_liability_hint": {}, + "fractional_title_issue_hint": {}, + "land_use_gain_hint": {}, + "lease_payment_semantics_hint": {}, + "preserved_claim_bundle_hint": {}, + "value_compensation_hint": {}, + "defense_hints": [] + } + ], + + "plaintiff_capacity_matrix": [ + { + "cluster_id": "IC-01", + "plaintiff_name": "string", + "capacity_basis": "소유자|공유자|상속인|임차인|채권자|점유권자|불명", + "source_basis_text": "string", + "certainty": "final|provisional" + } + ], + + "current_control_map": [ + { + "cluster_id": "IC-01", + "holder_name": "string", + "holder_role": "current_right_holder|current_possessor|current_user|current_benefit_holder", + "basis_text": "string", + "certainty": "final|provisional" + } + ], + + "current_control_map_4axis": [ + { + "object": "string", + "cluster_id": "IC-01", + "current_right_holder_candidate": "string|null", + "current_possessor": "string|null", + "current_user": "string|null", + "current_benefit_holder": "string|null", + "possession_basis_asserted": "string|null", + "possession_basis_transition_needed": true, + "certainty": "final|provisional|불명" + } + ], + + "standing_watchpoints": ["string"] + } + + 추가 제약: + - `defendant_target_matrix`, `issue_clusters`, `plaintiff_capacity_matrix`, `current_control_map`, `standing_watchpoints`는 optional이다. + - `domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints`, `liability_group_hint`, `commercial_successor_liability_hint`, `fractional_title_issue_hint`, `land_use_gain_hint`, `lease_payment_semantics_hint`, `preserved_claim_bundle_hint`, `value_compensation_hint`, `defense_hints`는 `issue_clusters` 내부 optional additive field다. + - `current_control_map_4axis`는 top-level optional additive field이며 legacy `current_control_map`을 대체하지 않는다. + - `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`, `preferred_plaintiff_selection_basis`, `succession_resolution_needed`는 `issue_clusters` 내부 optional additive field다. + - 기존 필수 필드는 모두 유지한다. + - 허용된 additive field 외 임의 추가 필드 금지. + + + + - `parties.plaintiffs`에는 실제 의뢰인만 남았는지 점검한다. + - `parties.plaintiffs`가 client universe로만 쓰였고, 사건군별 원고적격 확정값처럼 오염되지 않았는지 점검한다. + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않았는지 점검한다. + - `parties.defendants`와 `third_parties`가 뒤바뀌지 않았는지 점검한다. + - `summary_key_incidents`가 사건군별로 과잉 병합되지 않았는지 점검한다. + - `key_facts`가 later stage에 중요한 standing/ownership/current control fact를 누락하지 않았는지 점검한다. + - `issue_clusters`를 썼다면 각 cluster의 당사자와 desired relief가 meeting 직접 서술 범위를 넘지 않는지 점검한다. + - 상속·승계형 사건군에서 `candidate_plaintiffs`가 친족관계, 공동 통지, 공동 상담 같은 간접 신호만으로 확장되지 않았는지 점검한다. + - `issue_clusters`를 썼다면 unresolved succession cluster에서 `legal_entitlement_candidates`와 `preferred_plaintiffs_for_complaint`가 자동 동일시되지 않았는지 점검한다. + - `preferred_plaintiffs_for_complaint`를 썼다면 그 basis가 `preferred_plaintiff_selection_basis`에 남아 있고, meeting 직접 서술 또는 direct right-holder 구조를 넘지 않는지 점검한다. + - `succession_resolution_needed=true`인데도 해당 cluster가 direct owner/final cluster처럼 오염되지 않았는지 점검한다. + - `plaintiff_capacity_matrix`를 썼다면 각 row의 `capacity_basis`와 `source_basis_text`가 meeting 직접 서술 범위를 넘지 않는지 점검한다. + - `capacity_basis="상속인"` row가 친족관계나 공동행동 사실만으로 `certainty="final"`로 오염되지 않았는지 점검한다. + - 사망한 종전 권리주체의 과거 소유사실, 매수사실, 등기명의사실이 living plaintiff의 `소유자/final` row로 잘못 치환되지 않았는지 점검한다. + - `current_control_map`을 썼다면 `holder_role`이 `current_right_holder|current_possessor|current_user|current_benefit_holder` 중 하나인지와, basis가 meeting 직접 문장에 근거하는지 점검한다. + - 상속·승계형 사건군에서 `current_right_holder` row가 있으면, meeting만으로 승계가 해소되지 않은 경우 `certainty="provisional"`인지 점검한다. + - 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속이 서로 다른데 하나의 generic holder row로 뭉개지지 않았는지 점검한다. + - 사망한 종전 권리주체가 보이는 사건군이 있으면 `standing_watchpoints`에 succession-resolution trigger가 남았는지 점검한다. + - `standing_watchpoints`를 썼다면 장문 법률판단이 아니라 later stage 재검토용 짧은 경고인지 점검한다. + - meeting이 피고별로 현재 집행가능성과 장래 회수가능성을 구별하여 서술하면, 그 차이가 `constraints`, `primary_goal`, `summary_key_incidents`, `parties.defendants.asset_status`, `defendant_target_matrix` 중 어디엔가 비대칭적으로 보존되었는지 점검한다. + - "현재 무자력"과 "장래 재산취득 가능성"이 같은 사람에게 함께 서술되면, 그 사람을 자동 배제 대상으로 압축하지 않았는지 점검한다. + - `constraints`에 current collectability나 단순 회수곤란 사실이 잘못 들어가 있지 않은지 점검한다. + - `asset_status`가 제소 배제 신호처럼 쓰이지 않았는지 점검한다. + - `defendant_target_matrix`를 썼다면 각 `name`이 `parties.defendants`와 모순되지 않는지 점검한다. + - `defendant_target_matrix`에서 `target_status="exclude_now"`가 붙은 사람에게는 meeting상 명시적 no-sue 지시가 있는지 점검한다. + - `defendant_target_matrix`에서 `execution_risk="high"`와 `target_status="target_now"`가 함께 존재할 수 있음을 허용하되, 그 경우 `basis_text` 또는 `future_recovery_signal`이 비어 있지 않은지 점검한다. + - 장래 상속/재산취득 가능성이 직접 서술된 피고가 `exclude_now`로 잘못 내려가지 않았는지 점검한다. + - 금전채권 cluster에 최고ㆍ채무승인ㆍ이행각서ㆍ시효 단서가 있는데 `monetary_claim_profile.requires_money_claim_timeline`이 없거나 false이면 warning으로 본다. + - 동일 사건군에 원채무자와 승계ㆍ양수ㆍ보증ㆍ상호속용 책임 후보가 함께 있는데 `liability_group_hint`가 비어 있으면 warning으로 본다. + - 영업양도ㆍ상호속용 단서가 있는데 `commercial_successor_liability_hint.requires_article_42_review`가 없으면 warning으로 본다. + - 대여금ㆍ근저당권ㆍ경매ㆍ지분 단서가 같은 cluster에 있는데 `secured_property_dispute_profile.requires_payment_allocation`이 없으면 warning으로 본다. + - 상담문서에 지분별 권리상태가 보이는데 `fractional_title_issue_hint`가 없으면 warning으로 본다. + - 미등기 건물ㆍ임대차ㆍ건물 매매ㆍ토지 사용 단서가 있는데 `land_use_gain_hint`가 없으면 warning으로 본다. + - 사해행위취소ㆍ부담부 부동산ㆍ사후 담보권 말소 단서가 있는데 `actio_property_transfer_profile.requires_value_compensation_review`가 false이면 error로 본다. + - 같은 채권자와 채무자 사이 복수 대여금이 보이는데 `preserved_claim_bundle_hint`가 없으면 warning으로 본다. + - 명의신탁ㆍ충분담보ㆍ상계ㆍ선의 등 항변 단서가 있는데 `defense_hints`가 없으면 warning으로 본다. + - 유치권ㆍ상속ㆍ승계 사건군에서 `succession_resolution_needed=true`인데도 입력자료상 공동 통지 발신자, 친족 후보, 직계존속·직계비속 후보가 `preferred_plaintiffs_for_complaint` 확정자로 들어가면 error로 본다. + - 통지 발송일과 도달일 단서가 모두 있는데 `notice_lifecycle_hints`가 없으면 warning으로 본다. + - `current_control_map_4axis`를 썼다면 4축 중 직접 읽히는 축이 generic holder 하나로 합쳐지지 않았는지 점검한다. + - 출력 파일명과 성공 메시지가 정확한지 점검한다. + + + + + + [ ] 1) Preflight: `list_docs` 사용해서 `client_meeting.md` 확인, `read_docs` 사용해서 `client_meeting.md` 읽기 + [ ] 2) `write_file` 사용해서 `client_goal.json` 생성 + [ ] 3) `list_docs` 사용해서 `client_goal.json` 파일 존재만 확인(절대 읽기 금지) + [ ] 4) `"고객의사 초안 생성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `client_meeting.md` + + + - `client_goal.json` + + + + + - `client_goal.json`은 절대 재읽지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"고객의사 초안 생성 완료"` 한 줄만 사용한다. + + + + - task_name: Task_Evidence_shard_planner + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import hashlib + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + def _mid(): return next(MSG_ID) + def _init(): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"task-evidence-shard-planner","version":"1.0","user_id":"{{__user_hash__}}","workspace_id":"{{__workspace_hash__}}"}}}, headers=MCP_HEADERS) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post(LOCALDOCS_URL, json={"jsonrpc":"2.0","method":"notifications/initialized"}, headers=MCP_HEADERS).raise_for_status() + def _parse(text): + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: return json.loads(line[6:]) + except Exception: pass + try: return json.loads(text) + except Exception: return None + def _call(name, args): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc":"2.0","id":_mid(),"method":"tools/call","params":{"name":name,"arguments":args}}, headers=MCP_HEADERS) + r.raise_for_status() + p = _parse(r.text) + if not p or "result" not in p: raise RuntimeError(f"MCP {name} failed") + return p + def _unwrap(p, name): + text = (p["result"].get("content") or [{}])[0].get("text", "") + if not text: raise RuntimeError(f"Empty: {name}") + try: outer = json.loads(text) + except Exception: return text + if isinstance(outer, dict) and "results" in outer: + r0 = (outer.get("results") or [{}])[0] + inner = r0.get("content") or r0.get("text") or "" + if not inner: raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return json.dumps(outer, ensure_ascii=False) if not isinstance(outer, str) else outer + def read_json(name): + raw = _unwrap(_call("read_docs", {"doc_names": [name]}), name) + if not isinstance(raw, str): return raw + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + obj, _ = decoder.raw_decode(raw.strip()) + return obj + def write_doc(path, content): + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def _items(obj: Any) -> list[Any]: + if isinstance(obj, list): + return obj + if isinstance(obj, dict): + for key in ("items", "documents", "evidences", "evidence", "docs"): + val = obj.get(key) + if isinstance(val, list): + return val + raise RuntimeError("evidence_all.json must be a JSON array or an object containing a document array") + + def _text(v: Any) -> str | None: + if v is None: + return None + s = str(v).strip() + return s or None + + def _doc_title(doc: Any, ordinal: int) -> str | None: + if not isinstance(doc, dict): + return f"evidence_all[{ordinal}]" + for key in ("title", "name", "filename", "file_name", "document_title", "doc_title"): + val = _text(doc.get(key)) + if val: + return val + return f"evidence_all[{ordinal}]" + + def _doc_uid(doc: Any, ordinal: int) -> str: + if isinstance(doc, dict): + for key in ("doc_uid", "uid", "id", "document_id", "source_id"): + val = _text(doc.get(key)) + if val: + return val + return f"DOC-{ordinal:03d}" + + def _stable_hash(doc: Any) -> str: + raw = json.dumps(doc, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _list_doc_names(args) -> list[str]: + payload = _unwrap(_call("list_docs", args), "list_docs") + if isinstance(payload, str): + try: + payload = json.loads(payload) + except Exception: + return re.findall(r"[\w./-]+\.json", payload) + vals: list[str] = [] + if isinstance(payload, dict): + for value in payload.values(): + if isinstance(value, list): + for item in value: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + elif isinstance(value, str): + vals.append(value) + elif isinstance(payload, list): + for item in payload: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + return [v for v in vals if v] + + def _read_existing_doc_names() -> list[str]: + preferred = [ + {"path": "evidence_indexed_parts", "pattern": "E-*.json"}, + {"path": "evidence_event_candidate_parts", "pattern": "E-*.json"}, + ] + preferred_errors = [] + preferred_names: list[str] = [] + for args in preferred: + try: + folder = str(args["path"]).rstrip("/") + preferred_names.extend(f"{folder}/{name.split('/')[-1]}" for name in _list_doc_names(args)) + except Exception as exc: + preferred_errors.append(f"{args['path']}: {exc}") + if not preferred_errors: + return sorted(set(preferred_names)) + + fallback_errors = [] + for args in ({}, {"doc_names": []}): + try: + return sorted(set(preferred_names + _list_doc_names(args))) + except Exception as exc: + fallback_errors.append(f"{args}: {exc}") + raise RuntimeError("list_docs failed for preferred schemas and fallback schemas; stale part detection cannot safely continue: " + " | ".join(preferred_errors + fallback_errors)) + + def _stale_parts(existing_names: list[str], n: int, folder: str) -> list[str]: + stale: list[str] = [] + pattern = re.compile(rf"(?:^|/){re.escape(folder)}/E-(\d{{3}})\.json$") + for name in existing_names: + m = pattern.search(name) + if not m: + continue + ordinal = int(m.group(1)) + if ordinal < 1 or ordinal > n: + stale.append(name) + return sorted(set(stale)) + + def main(): + _init() + evidence_all = read_json("evidence_all.json") + docs = _items(evidence_all) + n = len(docs) + if n <= 0: + raise RuntimeError("evidence_all.json contains no evidence documents") + + existing_names = _read_existing_doc_names() + stale_b1 = _stale_parts(existing_names, n, "evidence_indexed_parts") + stale_b2 = _stale_parts(existing_names, n, "evidence_event_candidate_parts") + + shards = [] + streaming_edges = [] + b1_waits = [] + b2_waits = [] + b1_files = [] + b2_files = [] + for idx, doc in enumerate(docs, 1): + label = f"{idx:03d}" + evidence_id = f"E-{label}" + b1_task_id = f"Task_B1_map_doc_{label}" + b2_task_id = f"Task_B2_map_events_{label}" + b1_output = f"evidence_indexed_parts/{evidence_id}.json" + b2_output = f"evidence_event_candidate_parts/{evidence_id}.json" + # 문서별 shard 파일 — B1/B2 인스턴스가 evidence_all.json 풀 대신 자기 shard만 읽도록. + shard_doc_path = f"evidence_shards/{evidence_id}.json" + write_doc(shard_doc_path, json.dumps(doc, ensure_ascii=False, indent=2)) + b1_waits.append(b1_task_id) + b2_waits.append(b2_task_id) + b1_files.append(b1_output) + b2_files.append(b2_output) + shards.append({ + "assigned_ordinal": idx, + "ordinal_label": label, + "evidence_index_candidate": evidence_id, + "b1_task_id": b1_task_id, + "b2_task_id": b2_task_id, + "b1_output_path": b1_output, + "b2_output_path": b2_output, + "shard_doc_path": shard_doc_path, + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": idx, + "array_index": idx - 1, + "doc_uid_candidate": _doc_uid(doc, idx), + "title_candidate": _doc_title(doc, idx), + "content_sha256": _stable_hash(doc), + "shard_doc_path": shard_doc_path + }, + "runtime_bindings": { + "__assigned_ordinal__": idx, + "__ordinal_label__": label, + "__evidence_index_candidate__": evidence_id, + "__b1_part_path__": b1_output, + "__b2_part_path__": b2_output, + "__shard_doc_path__": shard_doc_path + } + }) + streaming_edges.append({ + "from": b1_task_id, + "to": b2_task_id, + "condition": {"file_exists": b1_output}, + "bind": {"__assigned_ordinal__": idx} + }) + + plan = { + "schema_version": "evidence_shard_plan.v1", + "planner_task": "Task_Evidence_shard_planner", + "created_at_utc": datetime.now(timezone.utc).isoformat(), + "source_file": "evidence_all.json", + "source_selector": "$.items || $", + "shard_count": n, + "expected_ordinals": [f"{i:03d}" for i in range(1, n + 1)], + "expected_evidence_ids": [f"E-{i:03d}" for i in range(1, n + 1)], + "ordinal_policy": { + "base": 1, + "assignment": "array_order", + "format": "%03d", + "assigned_ordinal_var": "__assigned_ordinal__", + "evidence_index_candidate": "E-{ordinal:03d}", + "no_legal_sorting": True, + "no_content_based_reordering": True + }, + "shards": shards, + # 백엔드 wildcard fan-out: dynamic_fanout 리스트의 각 항목이 한 인스턴스 컨텍스트({{item.*}}로 접근). + # shards를 그대로 사용 — 디스크립터 메타(id/count/task_template 등)는 fanout_meta로 분리 보존. + "dynamic_fanout": shards, + "fanout_meta": [ + { + "id": "B1_evidence_doc_fanout", + "count": n, + "task_template": "Task_B1_map_doc", + "instance_id_template": "Task_B1_map_doc_{ordinal:03d}", + "output_path_template": "evidence_indexed_parts/E-{ordinal:03d}.json" + }, + { + "id": "B2_event_candidate_fanout", + "count": n, + "task_template": "Task_B2_map_events", + "instance_id_template": "Task_B2_map_events_{ordinal:03d}", + "output_path_template": "evidence_event_candidate_parts/E-{ordinal:03d}.json", + "wait_until_template": "Task_B1_map_doc_{ordinal:03d}" + } + ], + "streaming_edges": streaming_edges, + "barriers": { + "B1_all_parts_barrier": { + "wait_for_tasks": b1_waits, + "require_files": b1_files, + "next": "Task_B1_quality_gate_evidence_indexed" + }, + "B2_all_parts_barrier": { + "wait_for_tasks": b2_waits, + "require_files": b2_files, + "plus": ["Task_B1_quality_gate_evidence_indexed"], + "next": "Task_B2_quality_gate_event_candidates" + } + }, + "stale_part_policy": { + "mode": "block", + "allowed_ordinals": [f"{i:03d}" for i in range(1, n + 1)], + "checked_directories": ["evidence_indexed_parts", "evidence_event_candidate_parts"], + "detected_stale_parts": { + "evidence_indexed_parts": stale_b1, + "evidence_event_candidate_parts": stale_b2 + }, + "downstream_gate_instruction": "B1/B2 quality gates must BLOCK if detected_stale_parts is non-empty or if files outside expected_ordinals are present." + }, + "non_legal_assertions": { + "does_not_classify_document_type": True, + "does_not_extract_facts": True, + "does_not_evaluate_evidence": True, + "does_not_calculate_amounts": True, + "does_not_decide_legal_effect": True, + "does_not_finalize_evidence_index": True, + "does_not_finalize_candidate_id": True + } + } + + if stale_b1 or stale_b2: + plan["planner_status"] = "BLOCKED_BY_STALE_PARTS" + plan["mapper_execution_allowed"] = False + plan["blocked_reason"] = "stale mapper part detected; B1/B2 mapper fanout must not execute and existing parts must not be reused" + else: + plan["planner_status"] = "READY" + plan["mapper_execution_allowed"] = True + plan["blocked_reason"] = None + + write_doc("evidence_shard_plan.json", json.dumps(plan, ensure_ascii=False, indent=2)) + # 백엔드 wildcard fan-out 확장이 stdout JSON의 dynamic_fanout 키를 읽음. + # 전체 plan은 너무 커서 dynamic_fanout만 출력한다. 단 planner_status가 READY가 아니면 mapper fanout은 반드시 비운다. + fanout_payload = plan["dynamic_fanout"] if plan["planner_status"] == "READY" else [] + print(json.dumps({"dynamic_fanout": fanout_payload, "planner_status": plan["planner_status"], "mapper_execution_allowed": plan["mapper_execution_allowed"]}, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B1_map_doc_* + max_concurrency: 12 + preflight_files: + - "{{item.shard_doc_path}}" + - client_meeting.md + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + + Task_B1_map_doc + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. Your role in this task is a **per-document mapper** within the v.3 Step D streaming map-reduce pipeline. + + + 본 task는 v.3 Step D의 B1 mapper다. orchestrator 가 runtime parameter (`{{item.*}}`) 로 주입한 assigned_ordinal 단일 증거문서만 처리하여, 그 문서의 authority component·document component envelope·legal component envelope을 추출한 part 파일을 작성한다. + 본 task는 증거 문서 수 N을 계산하지 않고, ordinal을 배정하지 않고, B1/B2 mapper 인스턴스를 생성하지 않는다. 이러한 orchestration 책임은 전부 `Task_Evidence_shard_planner`와 orchestrator의 권한이다. + 최종 `evidence_indexed.json`은 본 task가 작성하지 않는다. 본 task는 `evidence_indexed_parts/E-{ordinal:03d}.json` part 파일만 작성하며, finalize는 `Task_B1_quality_gate_evidence_indexed`가 수행한다. + 본 task는 자기 문서 안에서 직접 확인되는 component만 작성한다. 다른 문서의 사실을 자기 part에 주입하면 quality gate의 authority drift check에서 BLOCK 또는 HARD_WARNING으로 검출된다. + `evidence_index`는 planner가 부여한 `E-{ordinal:03d}` 형식의 deterministic candidate를 그대로 propose 하되, final 부여는 quality gate가 수행한다. 본 task는 candidate id를 자기 part의 `evidence_index_proposed` 필드에 기록한다. + 본 task의 추출 깊이는 v.2 원본 Task_B1과 동일하다. document component envelope의 모든 하위 구조(claim_document_components, business_transfer_components, registry_row_components, secured_debt_components, valuation_rows, mixed_contract_payment_semantics, loan_components, valuation_matrix, registry_row_role_map, defense_assertions, admission_components, defense_components, notice_components, party_assertion_components, schedule_items, registry_object_display, succession_document_components)를 자기 문서 범위 안에서 가능한 한 분리 추출한다. + 변호사 관점에서 본 단계의 핵심은 등기부 row, 차용증 component, 통지서 발송/도달일 분리, 가족관계·상속포기 component, 별지목록 등의 직접 보존이다. 이는 청구원인 timeline·청구취지 별지목록·금액 계산의 기초가 된다. + + + + + + - runtime parameters: `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.source_document_ref}}`, `{{item.b1_output_path}}` (planner 가 부여한 단일 source of truth — 별도 파일 read 불요) + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 문서 1건 — planner가 evidence_all.json에서 분리해 둔 단일 shard 파일) + - `client_meeting.md` (보조 사용만 허용) + + + - `evidence_indexed_parts/E-{ordinal:03d}.json` (본 mapper 인스턴스의 part 파일 1개만) + + + - 다른 ordinal의 문서를 처리하기 + - 다른 ordinal의 사실을 자기 part에 주입하기 (authority drift) + - 증거 문서 수 N을 재계산하여 planner 결과를 대체하기 + - assigned_ordinal을 새로 생성하거나 재정렬하기 + - B1/B2 mapper fan-out 또는 streaming edge를 설계하기 + - 최종 `evidence_indexed.json` 작성 (이는 quality gate의 단독 권한) + - 다른 mapper의 part 파일 읽기/덮어쓰기 + - root 전체 탐색 + - 이전 task 결과 또는 backend 자동 로드 데이터 사용 + - 자기 part 재읽기 + - cross-document legal conclusion 확정 + - 입력에 없는 사실 추가 + + + + + 1. `list_docs`로 `{{item.shard_doc_path}}`, `client_meeting.md` 존재만 확인한다. + 2. `read_docs`로 위 두 파일을 읽는다. `{{item.shard_doc_path}}`는 이 인스턴스의 단일 evidence 문서로 그대로 사용한다 (별도의 ordinal 추출 불필요). assigned_ordinal·evidence_index_candidate·b1_output_path 는 runtime parameter (`{{item.*}}`) 그대로 사용한다. + 3. 메모리에서 part item 객체를 생성한다(items 배열이 아닌 단일 item object 또는 1-element items 형태). + 4. `write_file(overwrite=true)`로 runtime parameter 가 지정한 `evidence_indexed_parts/E-{ordinal:03d}.json`을 저장한다. + 5. `list_docs`로 자기 part 파일 존재만 확인한다. 절대 읽지 않는다. + 6. 정확히 `"E-{ordinal:03d} mapper part 작성 완료"`만 출력하고 종료한다. + + + + - 입력 누락, 빈 파일, runtime parameter 누락, ordinal 매핑 실패, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. part 파일은 쓰지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + - tool로 읽은 내용만 사실로 취급한다. + + + + - 본 mapper 인스턴스는 정확히 runtime parameter (`{{item.assigned_ordinal}}`) 에 해당하는 1개 문서만 처리한다. + - runtime parameters (`{{item.*}}`) 는 ordinal/binding 의 단일 source of truth 다 (별도 plan 파일 read 불요). + - `evidence_all.json` 배열 순서를 절대 바꾸지 않고, 배열 순서를 근거로 새 plan을 만들지도 않는다. + - planner의 `source_document_ref.content_sha256` 또는 `doc_uid_candidate`가 현재 문서와 명백히 충돌하면 part 파일을 쓰지 않는다. + - 본 task는 planner가 확정한 `evidence_index_candidate`, `b1_output_path`, assigned_ordinal을 소비할 뿐 재작성하지 않는다. + - 다른 ordinal의 문서 내용을 현재 문서에 보충하지 않는다(authority drift 금지). + - unknown 처리: + - nullable scalar → `null` + - 배열 → `[]` + - 비nullable string만 `"불명"` 사용 + - 배열은 exact-match 중복 제거 후 최초 순서를 유지한다. + - part 파일은 단일 item object 형태이며, 기존 필수 필드(`evidence_index_proposed`, `doc_uid`, `title`, `title_normalized`, `doc_type`, `key_*`, `source_pointer`)를 모두 포함한다. + - optional additive field는 downstream(quality gate)이 무시해도 안전해야 한다. + - `evidence_index_proposed`는 deterministic 규칙 `E-{ordinal:03d}`로만 생성한다. 임의 변형 금지. + + + + 이 인스턴스의 `{{item.shard_doc_path}}`(planner가 evidence_all.json에서 분리한 단일 evidence 1건) + `client_meeting.md` (보조만 허용) + + + `client_meeting.md`는 아래 목적에만 사용할 수 있다. + - `asset_id` + - `property_label_for_schedule` + - `transaction_type` + - `transaction_date` + - `market_value_at_act` + - `market_value_at_close` + - `consideration_breakdown` + - `property_cluster_id` + - 현재 문서의 축약 명칭이 어떤 자산군을 가리키는지에 관한 shorthand 해석 + + + + - 현재 문서와 `client_meeting.md`가 충돌하면 현재 문서가 우선한다. + - 예외: 등기부상 형식적 원인이 `매매`여도, 현재 문서 특약 또는 `client_meeting.md`가 실질적으로 채무갈음/대물변제를 직접 드러내면 `transaction_type`은 `"대물변제"`로 할 수 있다. + - 단, 이 예외는 현재 문서 또는 meeting이 실질 원인을 직접적으로 드러내는 경우에만 적용한다. + + + + + - `ordinal` = `evidence_all.json`의 1-based 배열 위치 (assigned_ordinal과 동일) + - `evidence_index_proposed` = `E-{ordinal:03d}` (mapper propose; quality gate가 finalize) + - `title` = 현재 문서 제목 원문 그대로 + - `title_normalized` = 앞뒤 공백 제거 + 내부 연속 공백/줄바꿈을 1칸으로 축약 + - `doc_uid` = `DOC-{ordinal:03d}-{title_normalized의 공백을 "_"로 치환한 값}` + - `source_pointer` = `{"source":"evidence_all.json","ordinal":ordinal}` + + + + 다음 순서로만 핵심을 읽는다. + 1. `title` + 2. `heading` + 3. `key_value` / `form` + 4. 관련 `table` 행 + 5. `list` + 6. `paragraph` + + 보일러플레이트(바코드 안내, 발급확인 문구, 반복 footer/page text)는 무시한다. + 단, `관할등기소`, `접수번호`, `등기원인일`, `발행일` 등 필요한 사실이 있으면 사용한다. + + + + `doc_type` enum은 v.2 원본과 동일하다. nuance는 `doc_semantic_flags`로 보강한다. + 1. `판결|결정|배당표|가압류|가처분|경매|타경` → `"판결/결정"` + 2. `등기사항전부증명서|주민등록|가족관계증명서|사업자등록증` → `"공문서"` + 3. `대출|명세|거래|계산서` → `"거래기록"` + 4. `카톡|문자|이메일|통화` → `"통신기록"` + 5. `계약서|약정서|확인서|영수증|차용증|각서` → `"처분문서"` + 6. 그 외 → `"기타"` + + + + - `key_facts`: 최대 3개, 각 60자 이하 + - `key_dates`: 최대 3개 + - `key_amounts`: 최대 3개 + - `key_parties`: 최대 5개 + - 현재 문서에서 직접 읽히는 핵심만 남긴다. + - 장문 요약, 계산식, 추정 보정 금지. + - 가능하면 heading / key_value / 표의 핵심 행만 사용한다. + + + + + - `property_cluster_id`는 optional field다. + - 현재 문서만으로 특정 자산군이 명백하거나, 현재 문서의 표기와 `client_meeting.md`가 정확히 대응할 때만 채운다. + - 예: `"해당 대지/건물"`, `"해당 상가"`, `"해당 주거용 부동산"`, `"해당 임야"`, `"해당 영업양도"`, `"해당 물품대금"`. + - 명백하지 않으면 `null`. + + + + `doc_semantic_flags`는 optional field다. 아래 값만 사용한다. + - `multi_event_doc` + - `registry_doc` + - `mixed_contract_doc` + - `valuation_doc` + - `claim_doc` + - `status_doc` + - `notice_doc` + - `response_doc` + - `business_transfer_doc` + - `supporting_reference_doc` + - `admission_doc` + - `defense_doc` + - `notice_receipt_admission_doc` + - `lien_assertion_doc` + - `schedule_doc` + - `relief_object_reference_doc` + - `succession_doc` + + 부여 규칙: + - 등기부처럼 복수 row가 서로 다른 법적 효과를 가지면 `multi_event_doc` + - 갑구/을구 등기행위가 있으면 `registry_doc` + - 임대차와 매매 등 서로 다른 계약 구성요소가 같은 문서에 공존하면 `mixed_contract_doc` + - 시가, 차임시세, 사용이익 가치가 표로 제시되면 `valuation_doc` + - 차용증, 보증서, 대여약정 등 채권 발생 문서면 `claim_doc` + - 답변서, 사업자등록증, 현재 상태 확인서처럼 상태를 보여주면 `status_doc` + - 내용증명, 통지서, 최고서, 해제통지서면 `notice_doc` + - 상대방 답신, 답변서, 반박문이면 `response_doc` + - 영업양도 문서면 `business_transfer_doc` + - 별지 목록처럼 단독 계산객체는 없으나 다른 문서 식별을 돕는 문서면 `supporting_reference_doc` + - 상대방이 통지 수령, 임대, 차임, 점유, 채무, 변제 등을 인정하면 `admission_doc` + - 명의신탁, 충분담보, 상계, 선의, 후소유자 소멸청구권 부정, 피담보채권 충당 등 항변이 있으면 `defense_doc` + - 유치권 소멸청구 통지의 수령일을 인정하면 `notice_receipt_admission_doc` + - 유치권 주장, 공사잔금, 유치권 목적물 사용·임대가 있으면 `lien_assertion_doc` + - 별지목록, 목적물 목록, 청구취지에서 그대로 사용할 문구가 있으면 `schedule_doc` 및 `relief_object_reference_doc` + - 가족관계증명서, 상속포기 심판문, 사망·상속 관련 공문서이면 `succession_doc` + + + + - 등기부 문서에서 갑구/을구 여러 row가 서로 다른 법적 효과를 가지면 `multi_event_registry=true`를 optional field로 기록한다. + - `multi_event_registry=true`인 문서는 단일 snapshot 요약을 강요하지 않는다. + + + + + - 문서가 여러 법률효과를 담고 있으면 component 단위로 분해한다. + - 날짜는 작성일, 발송일, 도달일, 법률효과 발생일, 변제기, 기산일 후보로 의미별 분리한다. + - 금액은 원금, 이자, 보증금, 차임, 사용이익, 채권최고액, 실제 변제액, 가액배상 공제액으로 성격별 분리한다. + - 아래 component field들은 optional additive field이며, 현재 문서에 직접 읽히는 범위에서만 사용한다. + + + - 문서가 이행각서, 채무승인서, 최고서, 내용증명, 지급요구서, 변제약정서, 차용증, 납품대금 확인서이면 `claim_document_components`를 생성한다. + - 단순 `key_dates`에 문서작성일만 넣고 끝내지 말고, 문서 안에 지급기일, 새 변제기, 최고 도달일, 채무승인일, 청구금액, 채권자, 채무자가 직접 읽히면 component field로 분해한다. + - 이행각서에서 "언제까지 지급한다"는 문구가 있으면 반드시 `new_due_date` 후보로 분리한다. + - 최고서에서 발송일과 수령일/도달일이 모두 있으면 `dispatch_date`와 `arrival_date`를 분리한다. + - claim component의 `component_kind` 허용값은 `underlying_claim|debt_acknowledgement|demand_notice|notice_dispatch|notice_arrival|new_due_date|payment_promise|limitation_relevance|other`다. + + + + - 영업양도계약서, 상호 계속 사용, 사업 양수인, 영업자산 이전, 고객·직원 승계, 채무불승계 조항, 책임배제 등기 단서가 있으면 `business_transfer_components`를 생성한다. + - `business_name`, `transferor_candidates`, `transferee_candidates`, `business_assets_transferred`, `trade_name_continuation_clause`, `customer_or_employee_succession_clause`, `pre_transfer_debt_exclusion_clause`, `liability_exclusion_registration_clause`를 직접 읽히는 범위에서 채운다. + - 기존 `contract_components`가 있어도 영업양도 책임 검토용 구조는 `business_transfer_components`에 별도 보존한다. + + + + - 등기부, 등기사항전부증명서, 등기내역 문서는 반드시 row 단위로 쪼갠다. + - 각 row마다 갑구/을구, 순위번호, 등기목적, 접수일, 접수번호, 원인, 권리자, 채무자, 채권최고액, 객체 범위를 추출하여 `registry_row_components`에 둔다. + - 청구취지 특정에 필요한 접수번호는 `key_facts` 요약에만 두지 말고 `registry_row_components[*].receipt_no`로 보존한다. + - 차용증, 근저당권 설정계약, 등기부 row가 연결되면 `secured_debt_components`를 생성한다. + - 복수 채무의 원금, 이율, 변제기, 담보 지분이 다르면 법정변제충당 계산 대상이므로 모두 구조화한다. + - 사해행위취소 사건의 등기부 row는 필요하면 `registry_row_role_map`에 actio role을 추가로 남긴다. + - `registry_row_components`의 각 row에 `registry_ref`를 부여한다. 형식: `REG-{ordinal:03d}-{row_seq:02d}` (갑구/을구·순위번호 순 deterministic). + - row에 지분 표시(예: "2분의 1", "1/2 지분")가 직접 읽히면 `registry_row_components[*].fraction_display`(원문 문자열)와 `registry_row_components[*].fraction_ref`를 채운다. + - `fraction_ref`는 `FR-{ordinal:03d}-{row_seq:02d}` 형식의 deterministic propose 값이다. 같은 row에서 파생되는 모든 component는 같은 `fraction_ref`를 공유한다. + - 지분 표시의 분자·분모가 직접 읽히는 경우에만 optional 필드 `fraction_numerator`/`fraction_denominator`를 채운다(예: "2분의 1" → 1/2). 환산·약분·보간 금지. 직접 읽히지 않으면 null. + + + + - 감정표는 금액만 추출하지 말고 `valuation_rows`에 row별 전제조건, 금액, 단위, 법률상 사용 가능성을 함께 추출한다. + - 사해행위취소 valuation 문서가 복수 자산 또는 복수 시점을 담으면 `valuation_matrix`를 생성하고, 각 row의 `legal_uses`를 표시한다. + - 임대차보증금, 월 차임, 매매대금이 한 문서에 같이 있으면 `mixed_contract_payment_semantics`에서 각각 별도 payment semantic으로 분리한다. + - 보증금은 원칙적으로 반환 예정 담보성 금원인지 검토하고, 원고가 직접 수령하지 않았다면 원고의 부당이득 원금으로 자동 전환하지 않는다. + - 월 차임은 피고들 사이의 계약상 차임과 객관적 토지 사용이익을 구분한다. + + + + - 사해행위취소 사건의 증거문서는 일반 key_facts 요약만으로 충분하지 않다. + - 차용증에 복수 대여금이 있으면 `loan_components`로 분리하고, 각 원금·변제기·이자율·담보 목적물·청구 내 역할을 추출한다. + - 감정평가서는 시점별 `valuation_matrix`로 보존하고, 각 row의 legal use를 표시한다. + - 등기부는 갑구/을구 row별로 쪼개고, 각 row의 `actio_roles`를 부여한다. + - 내용증명 답신에 명의신탁, 충분담보, 상계, 선의 주장이 있으면 `defense_assertions`로 추출한다. + - 사후 말소된 담보권과 그 말소를 유발한 변제액은 가액배상 계산의 필수 입력으로 표시한다. + + + + - `loan_components`의 각 항목은 아래 named 필드를 가진다. 직접 읽히지 않는 필드는 null. + { + "loan_component_id": "E-{ordinal:03d}-LC-{seq:02d}", + "principal_amount": null, + "loan_date": null, + "due_date": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "rate_text": null, + "rate_source_locator": null, + "interest_accrual_start_hint": null, + "collateral_object_hint": null, + "role_in_claim_hint": null + } + - `agreed_interest_rate`는 원문 그대로의 문자열("연 5%", "월 1%" 등)만 허용한다. + - `agreed_delay_damage_rate`는 약정 지연손해금율이다. 없으면 null로 둔다(법정이율로 보충 금지). + - `rate_text`는 이율 관련 원문 문자열 그대로, `rate_source_locator`는 그 이율이 읽힌 조항 locator(예: "제2조", "특약")다. + - `agreed_interest_rate`/`agreed_delay_damage_rate`는 계산·환산·보간 금지. 원문 문자열만 허용. + - `secured_debt_components`의 각 항목에도 동일한 optional rate 필드(`agreed_interest_rate`, `agreed_delay_damage_rate`, `rate_text`, `rate_source_locator`, `due_date`)를 직접 읽히는 범위에서 둘 수 있다(근저당권 설정계약서·차용증 겸용 문서의 이율 보존용). + - 이율이 문서에 있는데 두 rate 필드가 모두 null이면 self-warning으로 본다. + + + + - 답변서에 통지 수령 인정, 제3자 임대 자인, 월 차임 자인, 유치권자 사용 허용 주장, 후소유자 소멸청구권 부정 주장, 피담보채권 충당 주장이 있으면 `admission_components`와 `defense_components`로 분리한다. + - 통지서에는 `notice_components`를 생성하고, 통지 유형, 발송일, 도달 필요 여부, 수령자, 목적물, 법률효과 후보를 분리한다. + - `notice_components`의 각 항목은 아래 named 필드를 가진다. 직접 읽히지 않는 필드는 null. + { + "notice_id_proposed": "E-{ordinal:03d}-NT-{seq:02d}", + "notice_type": "최고|해제통지|유치권소멸청구통지|기타", + "drafted_date": null, + "dispatch_date": null, + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [], + "receiver_candidates": [], + "target_object_hint": null, + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "legal_effect_candidate_hint": null + } + - 5개 lifecycle 날짜의 의미: `drafted_date`=작성일, `dispatch_date`=발송일, `arrival_date`=도달일(민법 제111조 도달주의 — 도달≠수령을 구분), `received_date`=수령일(수령 인정 포함; 도달일과 다르면 별도 보존), `legal_effect_date_candidate`=법률효과 발생 후보일(계산 금지, 문서 직접 기재만). + - `requires_receipt_for_effect`는 true|false|null — 문서 유형상 도달/수령이 효력요건인지의 후보 표시다. + - `receipt_evidence_refs`는 수령을 뒷받침하는 같은 문서 내 단서 locator다. + - `recipient_admission_text`는 통지 수령을 인정하는 원문 문자열이며, 답변서 문서의 component에서만 채운다. + - 수령일이 상대방 답변서에서 인정되는 경우 그 답변서의 `admission_components`에 남기고, 본 통지서 문서의 `received_date`에 타 문서 사실을 주입하지 않는다(authority drift 금지). cross-document 연결은 Part 2 B5가 수행한다. + - 당사자가 요구한 기간이나 금액이 법률상 기산일로 자동 사용되면 안 되는 경우 `party_assertion_components.should_not_auto_use_as_legal_start_date=true`를 남긴다. + - 별지목록에는 `schedule_items`를 생성하고, 청구취지에서 그대로 사용할 수 있는 문구와 asset alias, 전유부분, 대지권, 도로명주소를 보존한다. + - `schedule_items`의 각 항목에 `schedule_ref`를 부여한다. 형식: `SCH-{ordinal:03d}-{seq:02d}` (별지 기재 순서 deterministic). asset alias 문자열은 기존대로 함께 보존한다. + - `schedule_items`의 각 항목에 optional 필드 `canonical_label_candidate`(별지 표제 문구 원문)와 `usable_in_relief`("yes|no|unknown" — 청구취지에서 그대로 사용할 수 있는 문구인지)를 직접 읽히는 범위에서 둘 수 있다. + - 등기부가 목적물 특정 자료로 쓰이면 `registry_object_display`를 생성한다. + - 가족관계증명서, 상속포기 심판문, 사망·상속 자료에는 `succession_document_components`를 생성하고 피상속인, 사망일, 배우자 후보, 직계비속 후보, 직계존속 후보, 상속포기자, 신고일·수리일, 연결 asset을 보존한다. + + + + + + - 기존 optional field `legal_calculation_object`는 backward-compatible summary field로 유지한다. + - 그러나 이 필드는 안전할 때만 사용한다. + - 문서가 `multi_event_doc` 또는 `multi_event_registry=true`이면 원칙적으로 `legal_calculation_object`를 생성하지 않는다. + - 문서에 서로 다른 법적 효과의 slot이 2개 이상 공존하면 단일 summary `legal_calculation_object`로 압축하지 않는다. + - 단일 효과가 명백한 문서, 또는 서로 다른 값이 경쟁하지 않는 문서에서만 `legal_calculation_object`를 생성한다. + + + + - multi-event 또는 mixed-contract 문서를 위해 optional field `legal_calculation_objects`를 추가할 수 있다. + - `legal_calculation_objects`는 현재 문서 안의 개별 slot을 권위 있게 분해한 배열이다. + - 문서당 0개 이상 허용한다. + - 단일 slot 문서라면 `legal_calculation_objects`를 생략할 수 있다. + - 각 slot은 현재 문서의 직접 기재만 사용한다. 계산, 합산, 차감 금지. + + + + 개별 slot은 아래 경우에 생성한다. + - 소유권이전 / 매매 / 증여 / 대물변제 / 경매매각 / 경매개시결정 / 근저당권설정 / 근저당권말소 / 가압류 / 임대차 / 영업양도 / 대여 / 담보제공 + - valuation 문서는 시가표/차임표/사용이익표를 slot이 아니라 `valuation_object`로 처리한다. + - 등기부는 갑구/을구의 의미 있는 row마다 1 slot씩 생성한다. + - 혼합계약서는 계약 조항상 독립된 법적 효과마다 1 slot씩 생성한다. + + 각 slot은 아래 구조를 따른다. + { + "slot_id": "E-{ordinal:03d}-LCO-01", + "slot_kind": "ownership_transfer|gift|sale|dation|mortgage_setting|mortgage_cancellation|auction_commencement|auction_sale|lease|building_sale|loan|business_transfer|other", + "source_locator": { + "locator_type": "heading|key_value|table_row|paragraph|unknown", + "locator_hint": "갑구4|을구1|제1조|특약 등", + "excerpt": "짧은 단서" + }, + "asset_id": null, + "property_label_for_schedule": null, + "transaction_type": "매매|증여|대물변제|null", + "transaction_date": null, + "registration_date": null, + "registry_office": null, + "registry_receipt_no": null, + "registry_recorded_transfer_date": null, + "market_value_at_act": null, + "market_value_at_close": null, + "sale_price": null, + "monthly_rent": null, + "consideration_breakdown": [], + "parties": [] + } + - slot이 등기부 row 또는 별지 항목에서 직접 파생되면 optional 필드 `registry_ref_hint`/`schedule_ref_hint`를 남긴다. + + + + - 임대차와 매매가 함께 있는 혼합계약서, 영업양도와 채무불인수 조항이 함께 있는 문서 등은 optional field `contract_components`를 추가할 수 있다. + - `contract_components`는 현재 문서의 조항별 법적 효과 분해 메모다. + - 각 component는 아래 구조를 따른다. + { + "component_id": "E-{ordinal:03d}-CC-01", + "component_kind": "lease|sale|delivery|management_use_shift|permit_cooperation|business_transfer|debt_non_assumption|other", + "object_spec": "짧은 목적물", + "amount": null, + "date": null, + "payer_candidates": [], + "recipient_candidates": [], + "possession_recipient_candidates": [], + "benefit_shift_to_candidates": [], + "source_locator": { + "locator_type": "paragraph|table_row|key_value|unknown", + "locator_hint": "제1조|제2조|특약 등", + "excerpt": "짧은 단서" + } + } + - 혼합계약서에서 component가 2개 이상 명백하면 2개 이상 생성한다. + - mixed contract에 임대차, 매매, 인도, 관리·사용 귀속이 직접 적혀 있으면 이를 lease/sale 1개로 뭉개지지 말고 가능한 한 분리한다. + - 조항에서 누가 대가를 지급하고 누가 수령했는지가 직접 읽히면 `payer_candidates`, `recipient_candidates`를 채운다. + - 인도받는 자가 직접 읽히면 `possession_recipient_candidates`를 채운다. + - 관리·사용 또는 사용수익 귀속 주체가 직접 읽히면 `benefit_shift_to_candidates`를 채운다. + - 위 directionality field는 현재 문서에 직접 적힌 경우에만 채우고, 추정으로 보충하지 않는다. + + + + - 시가, 차임시세, 사용이익 평가 문서는 optional field `valuation_object`를 추가할 수 있다. + - `valuation_object`는 아래 구조를 따른다. + { + "valuation_kind": "market_value|rent_value|use_gain_value", + "target_object": "짧은 목적물", + "basis_date_or_period": "원문 기간 또는 기준일", + "value_rows": [ + { + "label": "2022.1.1.~2022.12.31.|보증금 3억 원인 경우 등", + "amount": "금액 문자열", + "unit": "총액|월액|기타" + } + ] + } + - valuation 문서는 기존 `doc_type`를 바꾸지 않고 `doc_semantic_flags`와 `valuation_object`로 구체화한다. + - 차임표/사용이익표가 있으면 `market_value_*`를 억지로 채우지 않는다. + + + + + - 날짜: 정확한 일자가 있으면 `YYYY-MM-DD`, 없으면 `null` + - 단, `key_dates`와 `valuation_object.basis_date_or_period`는 짧은 원문 텍스트 허용 + - 금액: 입력에 명시된 값만 `"300,000,000원"` 형식 문자열로 정규화 + - 이름/당사자: 원문 우선, 명백히 동일할 때만 canonicalize + - `consideration_breakdown`는 직접 읽히는 짧은 대가 항목만 저장, 최대 4개, 각 40자 이하 + + + + part 파일은 단일 item object 형태이며, top-level에 mapper가 propose하는 모든 component field를 둔다. + + { + "evidence_index_proposed": "E-{ordinal:03d}", + "doc_uid": "DOC-{ordinal:03d}-...", + "title": "...", + "title_normalized": "...", + "doc_type": "판결/결정|공문서|거래기록|통신기록|처분문서|기타", + "key_facts": [], + "key_dates": [], + "key_amounts": [], + "key_parties": [], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": ordinal + }, + "property_cluster_id": null, + "doc_semantic_flags": [], + "multi_event_registry": null, + "contract_components": [], + "valuation_object": null, + "legal_calculation_object": null, + "legal_calculation_objects": [], + "claim_document_components": [], + "business_transfer_components": [], + "registry_row_components": [], + "secured_debt_components": [], + "valuation_rows": [], + "mixed_contract_payment_semantics": {}, + "loan_components": [], + "valuation_matrix": {}, + "registry_row_role_map": {}, + "defense_assertions": [], + "admission_components": [], + "defense_components": [], + "notice_components": [], + "party_assertion_components": [], + "schedule_items": [], + "registry_object_display": {}, + "succession_document_components": null, + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } + } + + 추가 제약: + - top-level은 단일 object다. envelope이 아닌 단일 item. + - `evidence_index_proposed`는 mapper의 propose 식별자이며, finalize는 quality gate가 한다. + - `mapper_meta.self_isolation_attested=true`는 본 mapper가 자기 문서 외 사실을 주입하지 않았음을 self-attest 한다는 의미다. + - optional component field는 직접 읽히는 범위에서만 채우고, 빈 배열/객체/`null`로 두는 것을 우선한다. + + + + - part 파일이 단일 object이고 `evidence_index_proposed`, `doc_uid`, `title`, `title_normalized`, `doc_type`, `source_pointer`를 모두 포함하는지 점검한다. + - `planner_status=="READY"` 및 `mapper_execution_allowed=true`가 확인되지 않았으면 part 파일이 작성되지 않았는지 점검한다. + - `evidence_index_proposed`가 정확히 `E-{ordinal:03d}` 형식인지 점검한다. + - 자기 문서 외 ordinal의 사실이 part에 주입되지 않았는지 self-check 한다(authority drift 방지). + - `registry_doc`이면서 서로 다른 법적 효과 row가 2개 이상 읽히면 `doc_semantic_flags`에 `multi_event_doc`를 넣고, 가능한 경우 `legal_calculation_objects`를 생성한다. + - `multi_event_registry=true` 또는 `doc_semantic_flags`에 `multi_event_doc`가 있으면 단일 `legal_calculation_object`를 억지로 채우지 않는다. + - `mixed_contract_doc`인데 명백한 독립 component가 2개 이상이면 `contract_components`를 2개 이상 생성한다. + - `mixed_contract_doc`에 인도 또는 관리·사용 귀속이 직접 적혀 있으면 `delivery` 또는 `management_use_shift` component가 누락되지 않았는지 점검한다. + - 혼합계약 조항에서 지급자·수령자 또는 점유 이전 상대방이 직접 읽히면 `payer_candidates`, `recipient_candidates`, `possession_recipient_candidates`, `benefit_shift_to_candidates`가 비어 있지 않은지 점검한다. + - `valuation_doc`인데 가치표/차임표가 직접 보이면 `valuation_object`를 생성한다. + - 같은 문서에서 복수 행위가 보이는데도 단일 snapshot `legal_calculation_object`만으로 압축하지 않는다. + - `claim_doc`인데 이행각서ㆍ채무승인ㆍ최고ㆍ변제기ㆍ청구금액 단서가 직접 보이고 `claim_document_components`가 없으면 self-warning으로 본다. + - 이행각서에 새 변제기가 있는데 `new_due_date`가 없으면 self-warning으로 본다. + - 최고서 또는 통지서에 도달·수령 단서가 있는데 `arrival_date` 또는 notice receipt component가 없으면 self-warning으로 본다. + - `business_transfer_doc`에 상호ㆍ영업명ㆍ영업자산ㆍ채무불승계 단서가 있는데 `business_transfer_components`가 없으면 self-warning으로 본다. + - 등기부에 접수번호가 있는데 `registry_row_components[*].receipt_no`가 없으면 self-error로 본다. + - 차용금 문서에 이율 또는 변제기가 있는데 `secured_debt_components` 또는 `loan_components`가 없으면 self-warning으로 본다. + - `loan_components`가 있는데 이율 문구가 문서에 보이면서 `agreed_interest_rate`가 null이면 self-warning으로 본다. + - valuation 문서에 복수 시점 또는 복수 row가 있는데 `valuation_rows[*].legal_use_case` 또는 `valuation_matrix.rows[*].legal_uses`가 없으면 self-warning으로 본다. + - 임대차보증금이 추출되었는데 `mixed_contract_payment_semantics`에서 반환성 보증금과 사용이익 원금 배제 여부가 구분되지 않으면 self-warning으로 본다. + - 등기부 문서에 사후 말소 row가 있는데 `registry_row_role_map` 또는 `registry_row_components`에 release role이 없으면 self-error로 본다. + - 답변서에 수령 인정ㆍ제3자 임대ㆍ항변이 있는데 `admission_components` 또는 `defense_components`가 없으면 self-warning으로 본다. + - 별지목록에 목적물 항목이 있는데 `schedule_items`가 없으면 self-error로 본다. + - 상속자료가 있는데 `succession_document_components`가 없으면 self-warning으로 본다. + - self-warning/self-error는 mapper가 part 파일에 그대로 남겨도 되며, quality gate가 최종 severity를 판정한다. + + + + + {{item.assigned_ordinal}} + + + [ ] 1) Preflight: `list_docs` 사용해서 `{{item.shard_doc_path}}`, `client_meeting.md` 만 확인, `read_docs` 사용해서 두 파일만 읽기 + [ ] 2) runtime parameter `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.b1_output_path}}` 그대로 소비. 별도 검증 read 불요. + [ ] 3) `{{item.shard_doc_path}}`의 단일 evidence 문서를 메모리에서 사용하고, 주입된 shard 밖으로 나가지 않기 + [ ] 4) `write_file` 사용해서 `evidence_indexed_parts/E-{ordinal:03d}.json` 생성 (ordinal은 `{{item.assigned_ordinal}}`) + [ ] 5) `list_docs` 사용해서 자기 part 파일만 존재 확인(절대 읽기 금지) + [ ] 6) `"E-{ordinal:03d} mapper part 작성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 1건; planner가 evidence_all.json에서 분리) + - `client_meeting.md` + + + - `evidence_indexed_parts/E-{ordinal:03d}.json` + + + + + - `evidence_index_proposed`는 정확히 `E-{ordinal:03d}` 형식만 허용한다. + - `ordinal`과 output path는 runtime parameter (`{{item.*}}`) 를 그대로 따른다. + - 증거 문서 수 N, assigned_ordinal, mapper fan-out, streaming edge는 본 task에서 만들지 않는다. + - 자기 part 파일은 절대 재읽지 않는다. + - 다른 ordinal 문서를 처리하지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"E-{ordinal:03d} mapper part 작성 완료"` 한 줄만 사용한다. + + + + - task_name: Task_B2_map_events_* + max_concurrency: 12 + preflight_files: + - "{{item.shard_doc_path}}" + - "{{item.b1_output_path}}" + - client_meeting.md + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + + Task_B2_map_events + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. Your role in this task is a **per-document event/state candidate mapper** within the v.3 Step D streaming map-reduce pipeline. + + + 본 task는 v.3 Step D의 B2 mapper다. orchestrator 가 runtime parameter (`{{item.*}}`) 로 주입한 assigned_ordinal 단일 문서의 event/state candidate(raw event + legal-effect event 포함)를 추출하여 part 파일을 작성한다. + 본 mapper 인스턴스는 orchestrator가 같은 ordinal의 B1 mapper(Task_B1_map_doc) 완료와 `evidence_indexed_parts/E-{ordinal:03d}.json` 생성을 확인한 뒤 시작한다. 본 task는 streaming edge를 설계하지 않고, planner/orchestrator가 이미 결정한 same-ordinal edge를 소비한다. + 최종 `evidence_event_candidates.json`은 본 task가 작성하지 않는다. 본 task는 `evidence_event_candidate_parts/E-{ordinal:03d}.json` part 파일만 작성하며, finalize는 `Task_B2_quality_gate_event_candidates`가 수행한다. + 본 task는 같은 ordinal의 B1 part 파일과 planner shard row를 authority metadata로 참조하고, planner가 분리해 둔 `{{item.shard_doc_path}}` 의 raw 문서 1건을 메모리에서 사용한다. + 같은 문서 내 복수 legal-effect event는 분리한다. 문서 밖 사실을 candidate payload에 넣지 않는다(authority drift 금지). + `candidate_id`는 `EVT-{ordinal:03d}-{seq:02d}` 형식의 deterministic candidate로 propose 하되, final 부여 및 collision 처리는 quality gate가 수행한다. + 변호사 관점에서 본 단계의 핵심은: (a) 등기부 갑구/을구 row별 분리, (b) 혼합계약서의 계약성립/대가수수/인도/관리·사용 귀속 4종 분리, (c) 통지 발송일과 도달일의 분리 candidate, (d) 답변서의 admission/defense 분리 candidate, (e) valuation 문서의 시점별/지표별 candidate, (f) 가족관계·상속포기 문서의 succession candidate 분리다. + legal-effect event는 raw event와 함께 추출하며, payment, notice, auction, lien, succession, valuation, defense assertion, commercial successor liability에 대해 v.2 원본의 REQUIRED_EVENT_SUBKINDS를 그대로 따른다. + + + + + + - runtime parameters: `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.b1_output_path}}`, `{{item.b2_output_path}}` (planner 가 부여한 단일 source of truth — 별도 plan 파일 read 불요) + - `{{item.b1_output_path}}` (같은 ordinal 의 B1 mapper part — runtime parameter 그대로 사용) + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 문서 1건; planner가 evidence_all.json에서 분리해 둔 단일 shard 파일) + - `client_meeting.md` (보조 사용만 허용) + + + - `evidence_event_candidate_parts/E-{ordinal:03d}.json` (본 mapper 인스턴스의 part 파일 1개만) + + + - 다른 ordinal의 B1 part 또는 raw 문서 처리 + - 다른 ordinal의 사실을 자기 part에 주입하기 (authority drift) + - 증거 문서 수 N을 재계산하여 planner 결과를 대체하기 + - assigned_ordinal을 새로 생성하거나 재정렬하기 + - B1/B2 mapper fan-out 또는 streaming edge를 설계하기 + - 최종 `evidence_event_candidates.json` 작성 (이는 quality gate의 단독 권한) + - 다른 mapper의 part 파일 읽기/덮어쓰기 + - 자기 part 재읽기 + - 문서 summary를 다시 쓰는 작업으로 변질시키기 + - meeting만으로 문서에 없는 사건행위를 새로 만들기 + - 입력에 없는 날짜, 당사자, 금액, 목적물을 창작하기 + + + + + 1. `list_docs`로 `{{item.b1_output_path}}`, `{{item.shard_doc_path}}`, `client_meeting.md` 존재만 확인한다. + 2. `read_docs`로 위 세 파일을 읽는다. `{{item.shard_doc_path}}`는 이 인스턴스의 단일 evidence 문서로 그대로 사용한다 (별도의 ordinal 추출 불필요). assigned_ordinal·evidence_index_candidate·b2_output_path 는 runtime parameter (`{{item.*}}`) 그대로 사용한다. + 3. 메모리에서 part 객체를 생성한다. 단일 item object 또는 1-element items 형태이며, event 후보가 0건이어도 파일을 생략하지 말고 `zero_event_reason`을 반드시 가진다. + 4. `write_file` 전에 아래 을 수행한다. 불량이면 먼저 내부 보정을 시도한다. + 5. 보정 후에도 schema invariant가 불량이면 part 파일을 쓰지 않고 `FAILED: E-{ordinal:03d} event candidate part schema invariant violation`만 보고하고 종료한다. + 6. 통과한 경우에만 `write_file(overwrite=true)`로 runtime parameter 가 지정한 `evidence_event_candidate_parts/E-{ordinal:03d}.json`을 저장한다. + 7. `list_docs`로 자기 part 파일 존재만 확인한다. 절대 읽지 않는다. + 8. 정확히 `"E-{ordinal:03d} event candidates part 작성 완료"`만 출력하고 종료한다. + + + + - 입력 누락, 빈 파일, runtime parameter 누락, B1 part 부재, ordinal 매핑 실패, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. part 파일은 쓰지 않는다. + - event 후보가 0건인 것은 실패가 아니다. 이 경우에도 runtime parameter 가 지정한 output path에 `zero_event_reason`을 포함한 schema-valid part 파일을 쓴다. + - `event_candidates: []`와 `zero_event_reason` 누락 또는 null이 함께 존재하는 것은 정상 zero-event가 아니라 schema invariant 위반이다. + - part schema invariant 위반이 내부 보정 후에도 남으면 part 파일은 쓰지 않는다. 불완전한 part를 생성하여 quality gate에 넘기지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + + - 본 mapper 인스턴스는 정확히 runtime parameter (`{{item.assigned_ordinal}}`) 에 해당하는 1개 문서만 처리한다. + - runtime parameters (`{{item.*}}`) 는 ordinal/binding 의 단일 source of truth 다 (별도 plan 파일 read 불요). + - B1 part의 `evidence_index_proposed`, `title`, `doc_type`, `source_pointer.ordinal`을 authority로 사용한다. + - `{{item.shard_doc_path}}`의 단일 evidence 문서(planner가 evidence_all.json에서 분리)를 직접 참조하여 candidate를 추출한다. + - 본 task는 planner가 확정한 `evidence_index_candidate`, `b2_output_path`, assigned_ordinal을 소비할 뿐 재작성하지 않는다. + - 다른 ordinal의 사실을 자기 part에 주입하지 않는다(authority drift 금지). + - candidate layer에 머문다. 최종 BO, 최종 JuristicAct, 최종 청구권, 최종 사해행위 판단을 확정하지 않는다. + - 문서 단위 1요약으로 뭉개지 않는다. 한 문서에 복수의 법적 중요 행위가 있으면 반드시 여러 candidate로 분리한다. + - support가 희박한 후보를 억지로 채우지 않는다. 불명확한 필드는 `null` 또는 빈 배열을 사용할 수 있다. + - 잘못된 과잉 특정화보다 보수적 분해가 낫다. 다만 자기 문서 커버리지를 포기해서는 안 된다. + - `candidate_id`는 deterministic 규칙 `EVT-{ordinal:03d}-{seq:02d}`로 생성한다(seq는 자기 part 내 1-based 순번). + + + + 이 인스턴스의 `{{item.shard_doc_path}}`(planner가 evidence_all.json에서 분리한 단일 evidence 1건) + `evidence_indexed_parts/E-{ordinal:03d}.json`의 authority metadata + `client_meeting.md` + + + `client_meeting.md`는 아래 목적에만 사용할 수 있다. + - alias 정규화 + - 동일인 식별 보조 + - property cluster shorthand 해석 + - 현재 문서가 어느 상담 조항을 뒷받침하는지에 대한 `meeting_clause_refs` 보조 + - 현재 문서에서 직접 읽히는 내용의 중요도 보조 + - 사망자와 특정 자산군의 연결이 meeting에서 직접 읽히는 경우 succession 관련 candidate의 `affected_asset_cluster_ids` 보조 + 금지: + - meeting만으로 문서에 없는 사건행위를 candidate로 생성 + - meeting만으로 현재 문서의 당사자/금액/날짜를 보충해 direct evidence처럼 쓰기 + + + + + - 자기 part는 정확히 1개 item을 가진다. + - 그 item은 아래 둘 중 하나를 반드시 가진다. + - `event_candidates`가 1개 이상 + - `zero_event_reason`가 non-null + - `zero_event_reason` 허용값: + - `"NO_LEGAL_EVENT_FOUND"` + - `"REFERENCE_ONLY_DOC"` + - `"PARSE_INSUFFICIENT"` + - `"ORDINAL_MATCH_FAILED"` + - 등기부, 계약서, 통지서, 답변서, 감정/시세 문서처럼 통상 event-bearing 문서에 대해서는 `zero_event_reason`를 매우 예외적으로만 사용한다. + + + + `write_file` 전에 반드시 메모리의 part object를 검증한다. 이 검증은 생성 지점 안전장치이며 자동 강제 재실행을 의미하지 않는다. + + 필수 invariant: + 1. 메모리상 top-level은 단일 item object 또는 1-element `items` envelope일 수 있으나, `write_file` 직전 serialization 대상은 단일 item object여야 하며 실질 item은 정확히 1개다. + 2. item의 `evidence_index_proposed`는 runtime parameter `{{item.evidence_index_candidate}}` 또는 그와 동일한 `E-{ordinal:03d}` 값이어야 한다. + 3. item은 아래 둘 중 하나를 반드시 만족해야 한다. + - `event_candidates`가 non-empty list + - `zero_event_reason`이 non-null이며 허용값 중 하나 + 4. `event_candidates`가 빈 배열이면 `zero_event_reason`은 반드시 non-null 허용값이어야 한다. + 5. candidate가 없어 `zero_event_reason`이 non-null이면 schema 안정성을 위해 `event_candidates`는 빈 배열로 둔다. + 6. candidate가 있으면 각 candidate는 object이며, `candidate_id_proposed`는 `EVT-{ordinal:03d}-{seq:02d}` 형식이고 `source_evidence_index_proposed`는 자기 `evidence_index_proposed`와 일치해야 한다. + + 내부 보정 원칙: + - 문서 검토 결과 법적 중요 event/state 후보가 없는 것이 맞으면 `zero_event_reason`을 허용값 중 하나로 명시한다. + - event/state candidate 생성 조건이 충족되는데 빈 배열이면 extraction routines로 되돌아가 candidate를 재작성한다. + - 통상 event-bearing 문서에 `zero_event_reason`을 사용하는 경우, 원문상 후보 부재 또는 파싱 불충분 이유가 허용값과 맞는지 재확인한다. + + 보정 실패: + - 위 invariant 중 하나라도 충족하지 못하면 `write_file`을 호출하지 않는다. + - 마지막 채팅 출력은 정확히 `FAILED: E-{ordinal:03d} event candidate part schema invariant violation`로 한다. + + + + 아래 범주의 법적 중요 행위를 후보로 추출한다. + - 소유권 이전, 매매, 증여, 대물변제, 처분 + - 근저당/저당/전세권/가압류/압류/담보 설정 및 말소 + - 대여, 보증, 신용보증, 대위변제, 구상권 발생, 변제, 배당 + - 임대차, 점유, 인도, 현재 점유, 현재 사용자, 현재 사용수익, 현재 수익귀속, 현재 권리귀속, 현재 관리 상태 + - 경매신청, 경매개시결정, 경매매각, 인도명령, 집행 관련 사실 + - 통지, 최고, 해제/해지의사표시, 도달, 채무승인, 감액요구, 책임부인, 현재상태 주장 + - 판결, 결정, 지급명령, 배당표 등 소송/집행 관련 법적 중요 사실 + - 신축, 영업양도, 사업자등록, 현재 영업상태 등 사건핵심 상태 형성 행위 + - 위 범주와 직접 연결되는 기타 법적 중요 행위 또는 현재상태 + + 제외: + - 순수 배경설명만 있는 문장 + - 정보 증가 없는 반복 진술 + - 법적 효과와 관련 없는 주변사정 + + + + + - 갑구/을구의 각 의미 있는 row를 개별 candidate로 본다. + - 최소 추출 대상: 소유권보존, 소유권이전, 증여, 매매, 담보권실행을 위한 경매개시결정, 담보권실행을 위한 경매로 인한 매각, 근저당권설정, 근저당권설정등기말소, 가압류. + - `support_locators.locator_hint`에는 가능하면 `갑구4`, `을구1`, `순위번호 3` 같은 row identifier를 남긴다. + - 말소표시가 있더라도 법적 의미가 있으면 candidate로 남길 수 있다. + - row에 지분 표시가 직접 읽히면 candidate에 같은 ordinal B1 part의 `registry_row_components[*].fraction_ref`와 동일한 `fraction_ref`를 남긴다. 지분 표시가 없으면 null. + + + + - 하나의 문서에 서로 다른 계약 요소가 공존하면 clause별로 분해한다. + - 예: 대지 임대차 + 건물 매매 + 인도 + 관리사용 귀속 + 명의변경 협력은 가능한 한 별개 candidate로 분리한다. + - B1 part의 `contract_components`가 있으면 그것을 우선 참고하되, raw 문서 확인을 생략하지 않는다. + - 혼합계약서가 계약성립, 대가수수, 인도, 관리·사용 귀속을 직접 보여주면 아래 후보군을 가능한 한 독립 candidate로 생성한다. + 1. 계약성립 candidate + 2. 대가수수 candidate + 3. 인도 candidate + 4. 관리·사용 또는 현재 수익귀속 state candidate + - 위 4종 중 raw 문서가 직접 보여주는 것은 formation candidate 1개로 뭉개지지 않도록 반드시 분리한다. + - 대가수수 candidate는 적절한 기존 enum이 없으면 `event_kind="기타"`, `event_subkind="대가수수"`로 둘 수 있다. + - 대가수수 candidate에는 가능한 한 지급자, 수령자, 금액, 목적물, 해당 조항 locator를 함께 남긴다. + - 인도 candidate에는 가능한 한 인도자, 인도수령자, 목적물, 개시일, locator를 함께 남긴다. + - 관리·사용 귀속 candidate는 `event_or_state="state"`를 우선 검토하고, `current_state_candidate` 하위 4축과 interval field를 함께 남긴다. + - 문서가 계약일로부터 현재 점유/사용 귀속을 직접 보여주면 `state_start_date`를 그 개시일로 남기고, 뒤에 다른 점유/사용 candidate가 있으면 선행 state를 open-ended로 두지 않는다(같은 문서 내). + + + + - 내용증명, 답신, 답변서, 최고서, 해제통지서, 소멸청구 통지서에서는 아래를 개별 candidate로 검토한다. + - 지급 요구 + - 해제/해지 통지 + - 도달 + - 책임 부인 + - 현재 점유/현재 거주/현재 운영 주장 + - 사용수익 귀속 주장 + - 현재 권리귀속 주장 + - 채무승인 또는 시효항변 주장 + - 감액요구, 명도요구 + - 문서에 현재 상태가 직접 적혀 있으면 `event_or_state="state"` candidate를 생성할 수 있다. + - 현재 상태가 후행 시점에서 선행 상태를 대체하는 구조가 직접 보이면 interval field를 함께 남긴다. + + + + - 감정평가서, 감정평가 의견서, 임료 시세 확인서 등은 `event_candidates`가 0개라고 판단하지 않는다. + - 시가표, 차임표, 사용이익표, 시장가치표가 있으면 최소 1개의 factual/state candidate를 생성한다. + - rent/use-gain valuation은 `action_type_candidate="사실행위(factual acts)"`로 둘 수 있다. + - 문서가 특정 현재 수익귀속자 또는 현재 권리귀속자를 직접 보여주면, 해당 정보는 `current_state_candidate`의 대응 배열에 분리해 둔다. + + + + - 사업자등록증, 가족관계증명서, 현재상태 확인 문서, 심판문 등은 상태형 후보 또는 법적 지위 관련 후보를 생성할 수 있다. + - 예: 사업자등록은 현재 영업상태/점포 운영 개시의 간접 또는 직접 후보가 된다. + - 가족관계증명서, 상속포기 심판문, 상속 관련 공문서가 사망자와 친족구성 또는 상속포기를 직접 보여주면 succession 관련 candidate를 생성한다. + - sparse status 문서라도 later stage linker가 사용할 수 있도록 가능한 범위에서 `participants`, `object_spec`, `event_date` 또는 `current_state_candidate.state_as_of`를 채운다. + - family/renunciation/status 문서를 generic status로만 남기지 않는다. + + + + - 사망, 가족관계, 상속포기, 재산상속회복, 승계 관련 문서는 가능한 한 succession 관련 candidate를 생성한다. + - succession 관련 candidate에는 optional field `affected_asset_cluster_ids`를 둘 수 있다. + - `affected_asset_cluster_ids`는 meeting이나 B1 part metadata가 사망자와 특정 자산군의 연결을 직접 또는 명백하게 보여줄 때만 채운다. + - succession 관련 candidate에는 optional field `succession_candidate`를 둘 수 있다. + { + "deceased_candidate": null, + "spouse_candidates": [], + "lineal_ascendant_candidates": [], + "lineal_descendant_candidates": [], + "renunciant_candidates": [] + } + - 가족관계증명서 같은 status doc은 direct 내용이 보이면 `deceased_candidate`, `spouse_candidates`, `lineal_ascendant_candidates`, `lineal_descendant_candidates`를 가능한 한 채우고, 자산 연결이 직접 또는 명백하면 `affected_asset_cluster_ids`도 함께 채운다. + - 상속포기 문서는 상속포기 그 자체를 direct candidate로 남기되, direct names가 보이면 `succession_candidate.renunciant_candidates`를 반드시 채운다. + - 상속포기 문서가 특정 사망자와 자산 cluster를 직접 또는 명백하게 연결하면 동일한 `affected_asset_cluster_ids`를 함께 남긴다. + - direct names가 있는 succession/status 문서에서는 가능한 범위에서 `participants`와 `object_spec`도 함께 남겨 later stage overlap scorer가 sparse document를 놓치지 않게 한다. + - 이 단계는 최종 상속분, 단독상속, complaint plaintiff narrowing을 확정하지 않는다. 다만 downstream이 succession bundle을 읽을 수 있을 정도의 구조는 반드시 남긴다. + + + + + - raw event는 문서가 직접 보여주는 사실행위·법률행위·준법률행위다. + - legal-effect event는 raw event에서 downstream 청구권 식별에 필요한 법률효과 후보를 도출한 candidate다. + - raw event와 legal-effect event는 같은 문서에서 함께 생성할 수 있으며, 서로 다른 `candidate_id`를 가진다. + - legal-effect event는 최종 법률판단이 아니라 Stage 2가 검토할 구조화된 후보이므로 `confidence`와 `review_flags`를 함께 남긴다. + + + 금전채권: + - `underlying_claim_arising` + - `first_demand` + - `demand_notice_dispatch` + - `demand_notice_arrival` + - `debt_acknowledgement` + - `new_due_date` + - `business_transfer` + - `trade_name_continuation_candidate` + - `commercial_successor_liability_candidate` + + 담보부 부동산: + - `secured_debt_creation` + - `mortgage_setting_by_fraction` + - `undesignated_payment` + - `payment_allocation_effect` + - `mortgage_extinguishment_by_fraction` + - `auction_application_after_extinguishment` + - `auction_sale_and_registry_transfer` + - `derivative_mortgage_setting_by_invalid_title_holder` + - `unregistered_building_original_acquisition` + - `de_facto_disposition_holder_acquisition` + - `land_use_gain_start` + - `liability_overlap_start` + + 사해행위취소: + - `preserved_claim_arising` + - `collateral_setting_for_preserved_claim` + - `fraudulent_transfer` + - `asset_value_at_fraudulent_act` + - `asset_value_at_close_proxy` + - `collateral_insufficiency_at_act` + - `encumbrance_release_after_act` + - `value_compensation_exception_trigger` + - `value_compensation_cap_calculation_candidate` + - `defense_assertion_title_trust` + - `defense_assertion_sufficient_collateral` + - `defense_assertion_setoff` + + 유치권ㆍ상속ㆍ통지: + - `lien_assertion` + - `third_party_lease_by_lien_holder` + - `lien_violation_candidate` + - `lien_extinction_notice_dispatch` + - `lien_extinction_notice_receipt` + - `lien_extinction_effect_candidate` + - `ongoing_physical_possession` + - `succession_renunciation_filing` + - `succession_renunciation_acceptance` + - `spouse_sole_succession_effect_candidate` + - `defense_assertion_subsequent_owner_no_extinction_right` + - `defense_assertion_benefit_applied_to_secured_claim` + + + + - claim_doc 문서가 채무승인, 최고, 변제기, 변제약정, 지급기일을 함께 포함하면 하나의 generic event로 압축하지 말고 legal effect별 candidate를 분리한다. + - 이행각서에는 최소한 `debt_acknowledgement`, `payment_promise`, `new_due_date` 후보가 있는지 검토한다. + - 최고서에는 `demand_notice_dispatch`, `demand_notice_arrival`, `limitation_preservation` 관련 후보가 있는지 검토한다. + - 발송일과 도달일이 다르면 각각 별도 `event_date`로 보존한다. + - 채무승인 event에는 `legal_effect_candidate.acknowledgement_of_debt=true`, `limitation_interruption_candidate=true`, `related_claim_type`을 직접 읽히는 범위에서 남긴다. + + + + - business_transfer_doc 문서는 단순 영업양도 formation event로만 끝내지 않는다. + - 문서 또는 상담내용에서 상호, 영업명, 영업자산, 고객관계, 고용관계, 채무불승계 조항, 책임배제 등기 필요성이 보이면 `commercial_successor_candidates`로 분해한다. + - 상법 제42조 책임을 최종 확정하지는 않되, downstream이 요건판단을 할 수 있도록 transferor, transferee, business_name, trade_name_continuation_candidate, debt_exclusion_clause_candidate를 남긴다. + + + + - 원자료 event가 곧바로 법률효과를 발생시키는 경우 raw event와 legal-effect event를 모두 생성한다. + - 복수 채무에 대한 지정 없는 변제가 등장하면 `undesignated_payment` event와 별도로 `payment_allocation_effect` 후보를 생성한다. + - 등기부상 경매 매각과 후행 근저당권이 등장하면, 그 경매가 어떤 담보권에 기초했는지, 그 담보권의 피담보채무가 이미 소멸했는지 검토하는 event chain을 만든다. + - 미등기 건물 신축일과 미등기 건물 매수일은 점유·사용이익 발생 시점을 정하므로 `land_use_gain_start` 및 `liability_overlap_start` event 후보를 생성한다. + - `mortgage_setting_by_fraction`/`mortgage_extinguishment_by_fraction` 등 `*_by_fraction` subkind candidate는 지분 표시가 문서에 직접 읽히는 한 `fraction_ref`를 반드시 가진다. + + + + - 사해행위취소 사건에서 raw event와 legal-effect event를 구분한다. + - 사해행위 당시 존재하던 담보권이 사후 말소되면 `value_compensation_exception_trigger` event를 생성한다. + - valuation row와 사후 말소 부담, 존속 부담이 모두 있으면 `value_compensation_cap_calculation_candidate`를 생성한다. + - 피고가 항변을 명시한 내용증명은 `defense_assertion_*` event로 만들고, Stage 2 claim description에 반영해야 하는지 표시한다. + + + + - 통지 문서와 답변서가 서로 대응하고, 답변서가 특정 통지의 수령일을 인정하면 별도의 receipt event candidate를 생성한다. + - receipt event candidate는 발송 event와 병합하지 않는다. + - 발송일과 도달일이 다르면 도달일을 법률효과 후보일로 별도 보존한다. + - 문서에 유치권, 공사잔금, 점유, 임대, 직접 거주, 피담보채권 충당, 후소유자 항변이 함께 나타나면 일반 current possession event가 아니라 lien_event 후보군으로 분해한다. + - 제3자 임대는 `lien_violation_candidate`로, 직접 거주는 ongoing use state로, 피담보채권 충당 주장은 `defense_candidate`로, 소멸청구 수령은 `lien_extinction_effect_candidate`로 각각 분리한다. + - 상속포기 수리 event가 있고 배우자와 자녀 전부 상속포기 구조가 직접 보이면 `spouse_sole_succession_effect_candidate`를 생성하되, 최종 소장 원고 확정은 Task_C 이후로 미룬다. + + + + + `action_type_candidate`는 아래 중 1개를 선택한다. + - `"법률행위(legal acts)"` + - `"준법률행위(quasi-legal acts)"` + - `"사실행위(factual acts)"` + - `"위법행위(unlawful acts)"` + - `"소송행위(litigation acts)"` + - `"불명"` + + + + `event_kind`는 아래 값 중 가장 가까운 것을 선택한다. + - `"처분"`, `"소유권이전"`, `"증여"`, `"대물변제"`, `"담보설정"`, `"담보말소"`, `"채권발생"`, `"보증"`, `"대위변제"`, `"변제"`, `"배당"`, `"임대차"`, `"점유"`, `"인도"`, `"가압류/압류"`, `"경매신청"`, `"경매개시"`, `"경매매각"`, `"통지"`, `"해제/해지"`, `"판결/결정"`, `"영업양도"`, `"사업자등록"`, `"신축"`, `"상속개시"`, `"상속포기"`, `"채권관리"`, `"도달"`, `"항변"`, `"가치평가"`, `"법률효과"`, `"기타"` + + 필요하면 optional field `event_subkind`에 더 구체한 라벨(REQUIRED_EVENT_SUBKINDS 참조)을 남길 수 있다. + + + + - `participants.actor_candidates`는 직접 행위를 한 사람/법인 + - `participants.counterparty_candidates`는 직접 상대방 + - `participants.beneficiary_candidates`는 행위로 이익을 직접 취득한 사람/법인 + - `participants.third_party_candidates`는 법원, 피상속인, 기존 권리자 등 제3자 + - `object_spec`는 짧은 목적물/채권/담보/통지대상/상태 대상 명사구 + - `amount`는 현재 candidate와 직접 연결되는 대표 금액 1개만 사용한다 + - state candidate의 현재 점유/사용/수익/권리 귀속은 `participants`에 뭉개지지 않고 `current_state_candidate` 하위 배열에 분리한다 + - succession/status 문서처럼 sparse한 문서라도 direct names와 direct relation이 보이면 `participants`와 `object_spec`를 불필요하게 비워두지 않는다. + - 잘못된 당사자 확정보다 `[]` 또는 `null`이 낫다 + + + + - `event_date`는 가능하면 `YYYY-MM-DD` + - 불명확하면 `null` + - `time_text`는 짧은 원문 표현을 유지할 수 있다 + - `time_precision` 허용값: `"exact"`, `"approximate"`, `"range"`, `"unknown"` + - 현재상태 candidate의 경우 `state_as_of` 개념은 optional `current_state_candidate.state_as_of`에 남기고, `event_date`는 상태 개시 시점이 직접 보일 때만 채운다. + - state interval이 직접 보이면 `current_state_candidate.state_start_date`, `current_state_candidate.state_end_date_candidate`를 함께 채운다. + - 후행 상태가 선행 상태를 대체하는 것이 직접 보이면 `current_state_candidate.superseded_by_candidate_id`를 사용한다. + + + + - optional field `event_or_state`를 추가할 수 있다. 허용값은 `"event"` 또는 `"state"`. + - 문서가 단발 사건이 아니라 `현재까지`, `현재도`, `계속`, `현재 운영`, `현재 점유`, `현재 거주`, `부지로만 사용`, `현재 명의`, `현재 소유`, `차임을 받고 있다` 같은 계속상태를 직접 보여주면 `event_or_state="state"` candidate를 생성할 수 있다. + - state candidate에는 optional field `current_state_candidate`를 둘 수 있다. + { + "state_summary": "짧은 상태 설명", + "state_as_of": "현재|2024-10-31 등", + "state_start_date": null, + "state_end_date_candidate": null, + "superseded_by_candidate_id": null, + "is_ongoing": true, + "current_possessor_candidates": [], + "current_user_candidates": [], + "current_benefit_holder_candidates": [], + "current_right_holder_candidates": [], + "possession_legal_cause_asserted": null, + "possession_legal_cause_asserted_by": [] + } + - `current_state_candidate`는 가능한 한 아래 4축으로 분해한다. + - 점유자 -> `current_possessor_candidates` + - 사용자/운영자 -> `current_user_candidates` + - 차임·사용이익·영업수익 등 현재 수익귀속자 -> `current_benefit_holder_candidates` + - 현재 소유자·등기명의인·권리귀속자 -> `current_right_holder_candidates` + - 같은 사람이 복수 role을 가지면 각 배열에 중복 기재할 수 있다. + - 문서에 직접 읽히지 않는 축은 빈 배열로 둔다. 추정으로 채우지 않는다. + - event candidate와 state candidate는 동일 문서에서 함께 존재할 수 있다. + - 동일 문서에 과거 행위와 현재 상태가 함께 있으면 분리한다. + - 동일 문서에 현재 점유자와 현재 권리귀속자가 다르게 읽히면 하나로 뭉개지지 않도록 분리 기재한다. + - 상태 개시 시점이 직접 보이면 `state_start_date`를 채운다. + - 후행 점유/사용/수익귀속 candidate가 선행 상태를 대체하는 것이 직접 보이면, 선행 state에 `state_end_date_candidate`와 `superseded_by_candidate_id`를 남기고 open-ended current state로 방치하지 않는다. + - `state_end_date_candidate`가 직접 보이면 `is_ongoing=false`를 우선 검토한다. + - 혼합계약서가 인도, 관리·사용 귀속, 사용수익 귀속을 직접 보여주면 그 상대방에 대한 state candidate를 high confidence로 생성할 수 있다. + - raw 문서가 현재 점유, 현재 사용, 현재 수익귀속, 현재 권리귀속을 직접 적시하면 candidate `confidence`는 원칙적으로 `high`를 우선 검토한다. + - meeting 보조나 간접 정황에 크게 의존한 상태 후보는 `medium` 또는 `low`로 둔다. + - `possession_legal_cause_asserted`는 optional 필드다. 허용값: "유치권행사중|임대차|사용대차|물권|동시이행항변|명도후잔류|무권원주장|미상". `possession_legal_cause_asserted_by`는 그 권원을 주장한 주체(원고측/피고측/제3자 이름)이며 문서에 직접 읽힐 때만 채운다. + - 점유 사실과 권원 주장은 같은 candidate 안에서 축을 분리해 남긴다(사실=4축 배열, 권원=`possession_legal_cause_asserted`). 권원 주장이 문서에 없으면 `미상`이 아니라 null로 둔다. + - 서로 다른 문서가 동일 자산에 대해 다른 권원을 주장하는 것은 본 mapper의 검출 대상이 아니다(authority drift 금지). 각 문서의 주장을 그대로 보존한다. + + + + - optional field `meeting_clause_refs`를 추가할 수 있다. + - 형식은 `1-가`, `2-라`, `3-다`, `4-가`, `5-나`처럼 `client_meeting.md`의 본문 번호체계를 사용한다. + - 현재 문서가 특정 상담조항을 직접 뒷받침할 때만 기록한다. + - meeting clause ref는 보조 recall 장치일 뿐, 문서에 없는 사실을 가져오는 수단이 아니다. + + + + 이 단계는 최종 사해행위 판단을 하지 않는다. 다만 후속 단계의 recall 강화를 위해 candidate tag를 남긴다. + `actio_relevance_candidates.candidate_role_tags` 허용값: + - `"사해행위목적물후보"`, `"원고채권후보"`, `"선순위담보후보"`, `"존속담보후보"`, `"임차권후보"`, `"가압류후보"`, `"수익자이익후보"`, `"현재점유후보"`, `"현재사용수익후보"` + + `fraudulent_act_date_candidate`는 다음 경우에만 채운다. + - 현재 candidate가 직접 목적물의 처분, 이전, 이전등기, 매매일, 증여일, 대물변제일, 경매매각일과 연결되는 경우 + - 그 외에는 쓰지 않는다 + + + + downstream Task_C BO와 evidence 기반 candidate가 중복 제거할 수 있도록, 각 candidate마다 아래 정규화 문자열을 생성한다. + + `(event_or_state + event_kind + 핵심 actor + 핵심 counterparty + 핵심 object_spec/amount + 핵심 date_or_state_as_of)` + + 규칙: + - 불명확한 요소는 생략하되, 최소한 `event_or_state` 또는 `event_kind`와 1개 이상의 핵심 요소는 남긴다. + - 중복 제거를 위해 whitespace, 중복 조사, 과도한 수식어는 제거한다. + - title이나 evidence_index는 넣지 않는다. + - event와 state는 같은 signature가 되지 않도록 구분한다. + - mapper가 propose하는 candidate_id는 `EVT-{ordinal:03d}-{seq:02d}`이며 quality gate가 collision을 처리한다. + + + + part 파일은 단일 item object 형태이며, top-level item은 non-empty `event_candidates` 또는 non-null `zero_event_reason` 중 하나를 반드시 가진다. `event_candidates: []` 단독은 금지된다. + + { + "evidence_index_proposed": "E-{ordinal:03d}", + "title": "문서 제목", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": ordinal + }, + "property_cluster_id": null, + "doc_semantic_flags": [], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-{ordinal:03d}-01", + "event_or_state": "event|state", + "event_kind": "소유권이전", + "event_subkind": null, + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "짧은 1문장 요약", + "source_evidence_index_proposed": "E-{ordinal:03d}", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [], + "counterparty_candidates": [], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": null, + "fraction_ref": null, + "amount": null, + "event_date": null, + "event_end_date": null, + "time_text": null, + "time_precision": "exact|approximate|range|unknown", + "location": null, + "legal_keywords": [], + "support_locators": [ + { + "locator_type": "heading|key_value|table_row|paragraph|caption|unknown", + "locator_hint": "갑구4|제2조 등", + "excerpt": "짧은 단서", + "directness": "직접|간접|불명" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "string|null", + "related_claim_type": "string|null", + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "state|점유|...", + "confidence": "high|medium|low|unknown" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } + } + + 추가 제약: + - top-level은 단일 object다. envelope이 아닌 단일 item. + - `event_candidates`가 빈 배열이면 `zero_event_reason`은 반드시 non-null 허용값이어야 한다. + - `event_candidates: []`와 `zero_event_reason` 누락/null이 결합된 part는 작성 금지되는 schema invariant violation이다. + - `candidate_id_proposed`는 mapper의 propose 식별자(`EVT-{ordinal:03d}-{seq:02d}`)이며, finalize는 quality gate가 한다. + - `source_evidence_index_proposed`는 mapper가 propose 한 자기 ordinal의 candidate evidence_index다. + - `mapper_meta.self_isolation_attested=true`는 본 mapper가 자기 문서 외 사실을 주입하지 않았음을 self-attest 한다는 의미다. + + + + - `write_file` 전 을 수행하고, 불량이면 내부 보정을 먼저 시도했는지 점검한다. + - 자기 part object가 단일 item이고 `evidence_index_proposed`, non-empty `event_candidates` 또는 non-null `zero_event_reason`을 가지는지 점검한다. + - `event_candidates: []`와 `zero_event_reason` 누락/null이 함께 있으면 내부 보정 대상이며, 보정 불가 시 part 파일을 쓰지 않고 FAILED로 종료한다. + - `planner_status=="READY"` 및 `mapper_execution_allowed=true`가 확인되지 않았으면 part 파일이 작성되지 않았는지 점검한다. + - 모든 candidate의 `candidate_id_proposed`가 `EVT-{ordinal:03d}-{seq:02d}` 형식인지 점검한다. + - 자기 문서 외 ordinal의 사실이 part에 주입되지 않았는지 self-check 한다(authority drift 방지). + - `registry_doc` 또는 `multi_event_doc`인데 `event_candidates`가 0개면 다시 점검한다. + - `mixed_contract_doc`인데 서로 다른 법적 효과가 2개 이상 보이면 candidate도 최소 2개 이상 검토한다. + - 혼합계약서가 계약성립, 대가지급·수령, 인도, 관리·사용 귀속을 직접 보여주면 formation candidate만 남기지 말고 consideration/delivery/state candidate가 함께 검토되었는지 점검한다. + - 문서가 위 네 요소를 모두 직접 보여주면 최소 3개 이상, 가능하면 4개까지 독립 candidate로 분리되었는지 점검한다. + - `valuation_doc`인데 가치표/차임표가 직접 보이면 candidate를 최소 1개 생성한다. + - 문서가 상담일지의 명시 조항을 직접 뒷받침하는데도 `meeting_clause_refs`가 전혀 없으면 다시 점검한다. + - event와 state는 구분해 생성한다. 현재상태를 과거 단발행위로 뭉개지 않는다. + - state candidate에서 현재 점유자/사용자/수익귀속자/권리귀속자가 직접 읽히는데도 하나의 participant 배열로 뭉개졌다면 다시 점검한다. + - 현재 점유자와 현재 권리귀속자가 다르게 읽히는 문서는 `current_state_candidate` 하위 배열에서 분리되었는지 다시 점검한다. + - 상태 개시와 종료가 직접 보이거나 후행 상태가 선행 상태를 대체하는 것이 직접 보이면 `state_start_date`, `state_end_date_candidate`, `superseded_by_candidate_id` 검토가 이루어졌는지 점검한다. + - 가족관계증명서가 사망자와 친족구성을 직접 보여주면 `succession_candidate.deceased_candidate`, `spouse_candidates`, `lineal_descendant_candidates` 또는 `lineal_ascendant_candidates`가 필요한 범위에서 채워졌는지 점검한다. + - 상속포기 심판문이 direct names를 보여주면 `succession_candidate.renunciant_candidates`가 비어 있지 않은지 점검한다. + - 가족관계증명서, 상속포기 심판문, 상속 관련 공문서가 사망자와 특정 자산군의 연결을 직접 또는 명백하게 보여주면 `affected_asset_cluster_ids` 검토가 이루어졌는지 점검한다. + - succession/status 문서처럼 sparse한 문서라도 direct names, direct relation, direct date가 보이는데 `participants`, `object_spec`, `event_date` 또는 `state_as_of`가 불필요하게 비어 있지 않은지 점검한다. + - raw 문서가 현재상태를 직접 적시한 경우 candidate `confidence`가 불필요하게 low로 떨어지지 않았는지 점검한다. + - claim doc에 채무승인ㆍ최고ㆍ변제기 재설정 단서가 있는데 `legal_effect_candidate`가 있는 event가 전혀 없으면 self-warning으로 본다. + - 이행각서에 채무승인일과 새 변제기가 함께 있는데 `debt_acknowledgement`와 `new_due_date`가 분리되지 않으면 self-warning으로 본다. + - business transfer 문서에 상호속용 후보 event 또는 `commercial_successor_candidates`가 없으면 self-warning으로 본다. + - 복수 피담보채무와 변제가 있는데 `payment_allocation_effect`가 없으면 self-warning으로 본다. + - 경매 event가 있는데 기초 담보권 및 피담보채무 chain과 연결되지 않으면 `AUCTION_BASIS_CHAIN_MISSING` self-warning으로 본다. + - 미등기 건물 매수 event가 있는데 원시취득 event가 없으면 `UNREGISTERED_BUILDING_ORIGINAL_ACQUISITION_MISSING` self-warning으로 본다. + - 사후 담보권 말소 row가 있는데 `value_compensation_exception_trigger` event가 없으면 self-error로 본다. + - valuation matrix와 released encumbrance가 있는데 `value_compensation_cap_calculation_candidate`가 없으면 self-warning으로 본다. + - defense assertion event가 있는데 `defense_candidate.requires_rebuttal_in_stage2` 검토가 없으면 self-warning으로 본다. + - 유치권 소멸 통지서와 수령 인정 답변서가 있는데 도달 event 또는 `lien_extinction_notice_receipt`가 없으면 self-error로 본다. + - 제3자 임대 event가 있는데 `lien_violation_candidate`가 없으면 self-warning으로 본다. + - 상속포기 수리 event가 있는데 `spouse_sole_succession_effect_candidate` 후보가 없으면 self-warning으로 본다. + - self-warning/self-error는 mapper가 part 파일에 그대로 남겨도 되며, quality gate가 최종 severity를 판정한다. + + + + + {{item.assigned_ordinal}} + + + [ ] 1) Preflight: `list_docs` 사용해서 `{{item.b1_output_path}}`, `{{item.shard_doc_path}}`, `client_meeting.md` 만 확인. `read_docs` 사용해서 동일 파일만 읽기 + [ ] 2) runtime parameter `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.b1_output_path}}`, `{{item.b2_output_path}}` 그대로 소비. 별도 검증 read 불요. + [ ] 3) `{{item.shard_doc_path}}`의 단일 evidence 문서를 메모리에서 사용하고, 같은 ordinal의 B1 part(`{{item.b1_output_path}}`)만 authority metadata로 사용 + [ ] 4) `write_file` 전 수행. 불량이면 내부 보정 시도 + [ ] 5) 보정 후에도 불량이면 part 파일을 쓰지 않고 `FAILED: E-{ordinal:03d} event candidate part schema invariant violation` 출력 후 종료 + [ ] 6) 통과한 경우에만 `write_file` 사용해서 `evidence_event_candidate_parts/E-{ordinal:03d}.json` 생성. event 후보가 0건이어도 non-null `zero_event_reason` part 파일을 작성 + [ ] 7) `list_docs` 사용해서 자기 part 파일만 존재 확인(절대 읽기 금지) + [ ] 8) `"E-{ordinal:03d} event candidates part 작성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `{{item.b1_output_path}}` (같은 ordinal 의 B1 part) + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 1건; planner가 evidence_all.json에서 분리) + - `client_meeting.md` + + + - `evidence_event_candidate_parts/E-{ordinal:03d}.json` + + + + + - `candidate_id_proposed`는 정확히 `EVT-{ordinal:03d}-{seq:02d}` 형식만 허용한다. + - `ordinal`과 output path는 runtime parameter (`{{item.*}}`) 를 그대로 따른다. + - 증거 문서 수 N, assigned_ordinal, mapper fan-out, streaming edge는 본 task에서 만들지 않는다. + - 자기 part 파일은 절대 재읽지 않는다. + - 다른 ordinal 문서를 처리하지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"E-{ordinal:03d} event candidates part 작성 완료"` 한 줄만 사용한다. + + + + - task_name: Task_B1_quality_gate_evidence_indexed + preflight_files: + - evidence_shard_plan.json + - "evidence_indexed_parts/E-*.json" + - evidence_all.json + - client_meeting.md + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Validate only the artifact, input bundle, IDs, coverage, severity, and handoff condition assigned to the current task. + - Identify contradictions only within assigned inputs: date, amount, party, object, registration, payment, possession, notice, valuation, legal role, causal link, source identity, or required support chain. + - Distinguish contradiction, incompleteness, ambiguity, multiplicity, schema failure, writer-boundary breach, and review-only legal uncertainty. + - Preserve all material conflicting source IDs. Do not average, normalize, or repair unless the current task expressly authorizes repair. + - Treat PASS, SOFT_WARNING, HARD_WARNING, BLOCK, READY, READY_WITH_REVIEW, FAILED, rollback, and signoff statuses exactly as the current schema defines them. + - Do not downgrade severity for convenience, and do not block progression unless the current gate criteria require it. + - Validators report issues in the authorized validation, audit, gate, review, or rollback schema. They do not write final artifacts unless explicitly assigned. + + + + + + Task_B1_quality_gate_evidence_indexed + + You are an MCP-enabled LLM-backed **quality gate** assisting plaintiff-side Korean civil/commercial litigation counsel. Your role is the final writer of `evidence_indexed.json` and the governance gate that decides whether downstream Stage 1 tasks may proceed. + + + 본 task는 v.3 Step D의 B1 quality gate다. `Task_Evidence_shard_planner`가 생성한 `evidence_shard_plan.json`의 `expected_ordinals`와 `shard_count`를 기준으로 모든 B1 mapper part(`evidence_indexed_parts/E-*.json`)를 통합하여 최종 `evidence_indexed.json`을 단일 writer로 작성하고, 동시에 audit report `quality_gates/B1_evidence_indexed_gate.json`을 작성한다. + 본 task는 simple merge가 아니다. governance gate로서 5개 검증 항목(shard completeness, evidence index determinism, duplicate document alias, document component coverage, **authority drift detection**)을 LLM 추론으로 수행하고, 각 항목별 severity(`BLOCK`/`HARD_WARNING`/`SOFT_WARNING`/`PASS`)와 `hard_gate_findings[]`/`review_findings[]` 분류를 결정한다. + `BLOCK`은 stale/provenance/schema/ordinal/clear authority drift 등 hard gate finding이 있을 때만 발생한다. 법률적 불확실성, suspected duplicate, component coverage concern은 review gate finding으로 남기고 downstream은 `ALLOWED_WITH_WARNING`으로 보존한다. + B1은 `quality_gates/stage1_part1_soft_gate_handoff.json`의 writer가 아니다. B1 review finding은 자기 audit report에만 기록하고, 최종 compact handoff sidecar 작성은 후속 deterministic task `Task_B2_SHA256_soft_gate_handoff_writer`가 수행한다. + 본 task만이 `evidence_index`(`E-###`)를 finalize 한다. mapper가 propose 한 `evidence_index_proposed`(`E-{ordinal:03d}`)를 검증하고 collision-free한 canonical `evidence_index`로 부여한다. + 최종 출력은 v.2 envelope schema(`schema_contract_version="evidence_indexed.v2"`, top-level `items` + `evidence_indexed` alias)와 호환되어 Stage 2 호환성을 유지한다. v.3에서는 `schema_contract_version="evidence_indexed.v3"`로 격상하되 v.2 alias도 함께 둔다. + 변호사 관점에서 본 gate의 핵심은: (a) 모든 증거문서가 누락 없이 catalog 되었는가(완전성), (b) 같은 문서가 다른 alias로 중복 등재되지 않았는가, (c) 등기부 row·이행각서 component·통지서 도달일·별지목록·상속자료 등 청구원인 작성에 필수인 component가 누락되지 않았는가, (d) mapper가 다른 문서의 사실을 자기 part에 끌어오지 않았는가(authority drift)다. + + + + + + - `evidence_shard_plan.json` (expected_ordinals, shard_count, assigned_document_refs, stale_part_policy) + - `evidence_indexed_parts/E-*.json` (모든 B1 mapper part 파일) + - `evidence_all.json` (planner cross-check·authority drift 검증용 metadata) + - `client_meeting.md` (alias deduplication 보조) + + + - `evidence_indexed.json` (final authority catalog) + - `quality_gates/B1_evidence_indexed_gate.json` (audit report) + + + - mapper part 파일을 덮어쓰기 + - `evidence_shard_plan.json`을 덮어쓰기 또는 재작성하기 + - planner의 expected_ordinals를 무시하고 독자적 shard plan을 재구성하기 + - 다른 quality gate의 audit report 작성 + - `quality_gates/stage1_part1_soft_gate_handoff.json` 작성(`Task_B2_SHA256_soft_gate_handoff_writer` single writer 책임) + - mapper가 추출하지 않은 새 사실 창작 (gate는 검증·통합만) + - 자기 final 파일 또는 audit report 재읽기 + - simple concat으로 작동(반드시 LLM 검증 수행) + + + + + 1. `list_docs`로 `evidence_shard_plan.json`, `evidence_indexed_parts/` 디렉토리 안의 part 파일 목록과 `evidence_all.json`, `client_meeting.md` 존재를 확인한다. + 2. `read_docs`로 `evidence_shard_plan.json`, 모든 B1 mapper part 파일, `evidence_all.json` metadata, `client_meeting.md`를 읽는다. + 3. planner의 `expected_ordinals`, `expected_evidence_ids`, `barriers.B1_all_parts_barrier.require_files`, `stale_part_policy.detected_stale_parts`를 completeness 기준으로 고정한다. `evidence_all.json`은 planner cross-check와 authority drift 검증에만 사용한다. + 4. 메모리에서 5개 검증을 LLM 추론으로 수행한다(아래 GATE_CHECKS 참조). + 5. canonical `evidence_index` finalize: planner ordinal 순으로 정렬하여 `E-{ordinal:03d}` 형식으로 부여한다(propose된 candidate가 정확하면 그대로 유지). + 6. envelope object를 메모리에서 생성한다(`schema_contract_version`, `items`, `evidence_indexed` alias). + 7. audit report object를 메모리에서 생성한다(아래 AUDIT_REPORT_SCHEMA 참조). 이때 `hard_gate_findings[]`, `review_findings[]`, `stage2_risk_notes[]`, `review_gate_summary`, `handoff_export_summary`를 optional append field로 포함한다. + 8. `write_file(overwrite=true)`로 `evidence_indexed.json`을 저장한다. + 9. `write_file(overwrite=true)`로 `quality_gates/B1_evidence_indexed_gate.json`을 저장한다. + 10. `list_docs`로 두 파일 존재만 확인한다. 절대 읽지 않는다. + 11. `hard_gate_findings`가 1개 이상이면 `overall_severity="BLOCK"`으로 두고 `"B1 quality gate BLOCK 발생: downstream 진행 금지"`만 출력한다. hard gate가 없고 review finding만 있으면 `overall_severity="HARD_WARNING"` 또는 `"SOFT_WARNING"`, `downstream_progression="ALLOWED_WITH_WARNING"`, `stage2_auto_progression_allowed=true`로 두고 `"증거문서 authority catalog finalize 완료 (severity=)"`를 출력한다. + + + + - 입력 누락(planner 파일 또는 part 파일 1개라도 빠짐), 빈 파일, planner_status BLOCKED_BY_STALE_PARTS, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. final 파일은 쓰지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + + - `hard_gate_findings[]`는 downstream을 오염시키는 기계적/출처/무결성 실패만 담는다: stale part, unexpected ordinal, missing ordinal, parser/schema failure, evidence_index format mismatch, planner barrier mismatch, clear evidence identity collision, clear authority drift, writer-boundary violation. + - `review_findings[]`는 인간 변호사 또는 다음 Stage가 검토할 법률적 불확실성만 담는다: suspected duplicate alias, component coverage concern, legal component ambiguity, weak but valid evidence relevance, claim/registry/schedule/valuation/succession/defense 분류 의문. + - review finding은 사실 확정이 아니며, 자동 BLOCK 사유가 아니다. 반드시 `stage2_risk_notes[]`에 짧게 남겨 후속 deterministic writer가 compact handoff로 변환할 수 있게 한다. + - review finding이 provenance 오염, source trace 상실, final evidence identity collision로 승격되는 경우에만 hard gate로 재분류한다. + - `overall_severity="BLOCK"`은 hard gate finding이 1개 이상일 때만 허용한다. hard gate가 없고 review finding만 있으면 `downstream_progression="ALLOWED_WITH_WARNING"`과 `stage2_auto_progression_allowed=true`를 유지한다. + + + + 본 quality gate는 다음 5개 검증을 수행한다. 각 검증은 LLM 추론으로 진행하고 severity를 결정한다. + + + - 검증 대상: 모든 evidence document에 mapper part가 존재하는가 + - 비교 기준: `evidence_shard_plan.json.expected_ordinals`, `barriers.B1_all_parts_barrier.require_files`, `evidence_indexed_parts/` 디렉토리의 part 파일 ordinal 매칭 + - `evidence_shard_plan.json.stale_part_policy.detected_stale_parts`가 비어 있지 않으면 severity=`BLOCK` + - `evidence_all.json`의 실제 문서 수와 planner `shard_count`가 불일치하면 severity=`BLOCK` + - 누락된 ordinal이 1개라도 있으면 severity=`BLOCK` + - 모든 ordinal이 매칭되면 severity=`PASS` + + + + - 검증 대상: 각 part의 `evidence_index_proposed`가 정확히 `E-{ordinal:03d}` 형식이고 ordinal과 일치하는가 + - propose 형식 위반(예: `E-1`, `E-001-A` 등) 또는 ordinal mismatch가 1개라도 있으면 severity=`BLOCK` + - 모두 정확하면 severity=`PASS` + + + + - 검증 대상: 서로 다른 ordinal에 같은 문서가 다른 alias로 중복 등재되었는가 + - 비교 기준: `title_normalized`, `doc_uid`, `key_facts`, `key_dates`, `key_parties`, `key_amounts`의 동시 일치성 + - 명확한 evidence identity collision으로 final catalog의 source trace가 불가능하면 severity=`BLOCK` 및 `hard_gate_findings[]`에 기록 + - 동일 문서 alias 중복이 의심되거나 canonical alias 선택으로 안전하게 정리 가능한 경우는 severity=`HARD_WARNING` 또는 `SOFT_WARNING` 및 `review_findings[]`에 기록 + - 의심 없음이면 severity=`PASS` + - canonical alias 결정은 다음 우선순위로 한다: (i) ordinal이 작은 것, (ii) `title_normalized`가 더 명시적인 것, (iii) `key_dates`/`key_amounts`가 더 구체적인 것 + + + + - 검증 대상: 핵심 문서군의 component envelope 누락 + - 등기부(`registry_doc`)에 row가 명백히 보이는데 `registry_row_components`가 비어 있으면 severity=`HARD_WARNING` 및 `review_findings[]`에 기록 + - 차용증·이행각서·내용증명 등 `claim_doc`이 보이는데 `claim_document_components`가 비어 있으면 severity=`HARD_WARNING` + - 별지목록(`schedule_doc`)인데 `schedule_items`가 비어 있으면 severity=`HARD_WARNING` 및 `review_findings[]`에 기록 + - 영업양도 문서인데 `business_transfer_components`가 비어 있으면 severity=`HARD_WARNING` + - 감정평가서·차임표인데 `valuation_object` 또는 `valuation_rows`가 비어 있으면 severity=`HARD_WARNING` + - 가족관계증명서·상속포기 문서인데 `succession_document_components`가 비어 있으면 severity=`HARD_WARNING` + - 답변서에 항변 단서가 보이는데 `defense_assertions`/`defense_components`/`admission_components`가 모두 비어 있으면 severity=`HARD_WARNING` + - 차용증·변제약정서 등 `claim_doc`에 이율 기재가 보이는데 `loan_components[*].agreed_interest_rate`와 `agreed_delay_damage_rate`가 모두 비어 있으면 severity=`HARD_WARNING` 및 `review_findings[]`에 기록 + - 통지서(`notice_doc` 계열)인데 `notice_components[*].dispatch_date`와 `arrival_date`가 모두 null이면 severity=`HARD_WARNING` 및 `review_findings[]`에 기록 + - 위 누락이 없으면 severity=`PASS` + + + + - 검증 대상: mapper가 자기 문서 외 ordinal의 사실을 자기 part에 주입했는가(authority drift) + - 검증 방법: + - 각 part의 `key_dates`/`key_amounts`/`key_parties`/`registry_row_components`/`claim_document_components` 등의 사실이 raw 문서(자기 ordinal의 `evidence_all.json` 항목) 안에서 직접 확인 가능한지 LLM 추론으로 검증 + - 다른 ordinal의 raw 문서에만 등장하는 고유 사실이 자기 part에 등장했다면 authority drift + - 명확한 drift(다른 문서의 고유 사실 직접 인용)는 severity=`BLOCK` 및 `hard_gate_findings[]`에 기록 + - 의심되지만 확정 어려운 경우(공통 alias·일반적 표현)는 severity=`HARD_WARNING` 및 `review_findings[]`에 기록 + - 의심 없음은 severity=`PASS` + - drift가 발견된 part는 audit report에 ordinal과 drift 내용을 명시한다. + + + + + - canonical `evidence_index`는 ordinal 순으로 부여한다(`E-001`, `E-002`, ..., `E-{N:03d}`). + - mapper가 propose 한 candidate가 정확하면 그대로 유지한다(propose=finalize). + - duplicate alias가 confirmed면 canonical 1개만 `items`에 두고, dropped된 part의 ordinal과 사유를 audit report `dropped_aliases`에 기록한다. + - mapper part의 component field(claim_document_components, registry_row_components 등)는 그대로 보존한다. 임의 요약 금지. + - mapper part의 `mapper_meta` field는 final `items`에서 제외한다(audit report에만 기록). + - 최종 `items` 배열 순서는 ordinal 오름차순으로 정렬한다. + - bare array serialization 금지. 반드시 envelope object. + + + + - 최종 파일 `evidence_indexed.json`의 top-level shape는 반드시 object여야 한다. + - top-level 필수 키: `schema_contract_version`, `items`. + - top-level alias `evidence_indexed`를 함께 둔다(`items`와 exact same order / exact same item contents). + - `schema_contract_version` 권장값은 `"evidence_indexed.v3"`다(v.2 alias 호환을 위해 `evidence_indexed` alias 함께 보존). + - 각 item은 v.2 원본의 schema(`evidence_index`, `doc_uid`, `title`, `title_normalized`, `doc_type`, `key_*`, `source_pointer`, optional component fields)와 호환된다. + + + + audit report는 `quality_gates/B1_evidence_indexed_gate.json` 경로에 저장하며 다음 구조를 따른다. + + { + "gate_id": "B1_evidence_indexed", + "executed_at": "ISO-8601 datetime", + "input_parts_count": N, + "expected_ordinals": ["001", "002", "003"], + "planner_source": "evidence_shard_plan.json", + "planner_shard_count": N, + "received_ordinals": ["001", "002", "003"], + "checks": [ + { + "check_id": "shard_completeness", + "severity": "PASS|HARD_WARNING|SOFT_WARNING|BLOCK", + "detail": "짧은 설명", + "missing_ordinals": [] + }, + { + "check_id": "evidence_index_determinism", + "severity": "...", + "detail": "...", + "violations": [] + }, + { + "check_id": "duplicate_document_alias", + "severity": "...", + "detail": "...", + "duplicate_groups": [], + "canonical_choices": [] + }, + { + "check_id": "document_component_coverage", + "severity": "...", + "detail": "...", + "missing_components": [] + }, + { + "check_id": "authority_drift_detection", + "severity": "...", + "detail": "...", + "drift_findings": [] + } + ], + "overall_severity": "PASS|HARD_WARNING|SOFT_WARNING|BLOCK", + "block_reasons": [], + "warning_reasons": [], + "hard_gate_findings": [], + "review_findings": [], + "rerun_targets": [], + "stage2_risk_notes": [], + "review_gate_summary": { + "review_gate_count": 0, + "highest_review_severity": "PASS|SOFT_WARNING|HARD_WARNING" + }, + "handoff_export_summary": { + "is_soft_handoff_writer": false, + "handoff_writer_task": "Task_B2_SHA256_soft_gate_handoff_writer", + "exportable_review_count": 0 + }, + "downstream_progression": "ALLOWED|ALLOWED_WITH_WARNING|BLOCKED", + "stage2_auto_progression_allowed": true, + "dropped_aliases": [], + "schema_contract_version": "B1_quality_gate.v3" + } + + 추가 제약: + - `overall_severity`는 5개 check 중 가장 높은 severity와 동일하다(우선순위: BLOCK > HARD_WARNING > SOFT_WARNING > PASS). + - `downstream_progression`은 `overall_severity`로 결정한다. + - BLOCK -> "BLOCKED" + - HARD_WARNING -> "ALLOWED_WITH_WARNING" + - SOFT_WARNING -> "ALLOWED_WITH_WARNING" + - PASS -> "ALLOWED" + - `stage2_auto_progression_allowed=false` ⇔ `downstream_progression="BLOCKED"`. B1 BLOCK은 Stage 1 downstream 전체를 중단하므로 Stage 2 auto-progression도 불허한다. + - `block_reasons`/`warning_reasons`는 사람이 읽을 수 있는 짧은 한국어 설명. + - `overall_severity="BLOCK"`은 `hard_gate_findings[]`가 1개 이상일 때만 허용한다. `review_findings[]`만 존재하면 `overall_severity`는 `HARD_WARNING` 또는 `SOFT_WARNING`이고 `downstream_progression="ALLOWED_WITH_WARNING"`이다. + - `hard_gate_findings[]`가 비어 있으면 `stage2_auto_progression_allowed=true`를 유지한다. + - 기존 `block_reasons`/`warning_reasons`는 삭제하지 않고 `hard_gate_findings[]`/`review_findings[]`의 사람이 읽는 mirror로 유지한다. + - B1은 `quality_gates/stage1_part1_soft_gate_handoff.json`을 쓰지 않는다. B1 review finding은 `Task_B2_SHA256_soft_gate_handoff_writer`가 통합할 수 있도록 compact source refs와 함께 audit에 남긴다. + + + + - final `evidence_indexed.json`이 envelope object이고 `schema_contract_version`, `items`, `evidence_indexed` alias를 모두 포함하는지 점검한다. + - 모든 mapper part가 `items`에 정확히 1번 반영되었는지(또는 dropped_aliases에 명시되었는지) 점검한다. + - canonical `evidence_index`가 ordinal 오름차순으로 정렬되었는지 점검한다. + - `mapper_meta` field가 final `items`에서 제거되었는지 점검한다. + - audit report가 5개 check 모두를 포함하는지 점검한다. + - `overall_severity`와 `downstream_progression`이 일치하는지 점검한다. + - audit report가 `downstream_progression`과 `stage2_auto_progression_allowed`를 모두 포함하고, 두 필드가 일관되는지 점검한다. + - `BLOCK` severity가 발생한 경우 final `evidence_indexed.json`도 작성하되, 채팅 메시지에 BLOCK을 명시하여 orchestrator가 downstream 진행을 중단하도록 한다. + - audit report에 `hard_gate_findings[]`, `review_findings[]`, `stage2_risk_notes[]`, `review_gate_summary`, `handoff_export_summary`가 optional append field로 포함되어 있고 기존 field가 삭제되지 않았는지 점검한다. + - B1이 `quality_gates/stage1_part1_soft_gate_handoff.json`을 작성하지 않았는지 점검한다(`Task_B2_SHA256_soft_gate_handoff_writer` single writer). + + + + + + [ ] 1) Preflight: `list_docs` 사용해서 `evidence_shard_plan.json`, `evidence_indexed_parts/E-*.json`, `evidence_all.json`, `client_meeting.md` 확인. `read_docs` 사용해서 동일 파일들 모두 읽기 + [ ] 2) planner의 `expected_ordinals`, `barriers.B1_all_parts_barrier.require_files`, `stale_part_policy`를 기준으로 B1 part completeness와 stale part를 우선 검증 + [ ] 3) 5개 GATE_CHECKS LLM 추론 수행 + severity 및 hard/review gate classification 결정 + [ ] 4) `hard_gate_findings[]`, `review_findings[]`, `stage2_risk_notes[]`, `review_gate_summary`, `handoff_export_summary`를 B1 audit에 optional append + [ ] 5) `write_file` 사용해서 `evidence_indexed.json` 생성 (envelope object) + [ ] 6) `write_file` 사용해서 `quality_gates/B1_evidence_indexed_gate.json` 생성 (audit report) + [ ] 7) `quality_gates/stage1_part1_soft_gate_handoff.json`은 쓰지 않는다(`Task_B2_SHA256_soft_gate_handoff_writer` single writer) + [ ] 8) `list_docs` 사용해서 두 파일 존재만 확인(절대 읽기 금지) + [ ] 9) hard gate가 있으면 `"B1 quality gate BLOCK 발생: downstream 진행 금지"`, 그 외는 `"증거문서 authority catalog finalize 완료 (severity=)"` 출력하고 작업을 끝낸다(terminate) + + + + + - `evidence_shard_plan.json` + - `evidence_indexed_parts/E-*.json` (모든 B1 mapper part) + - `evidence_all.json` + - `client_meeting.md` + + + - `evidence_indexed.json` (final) + - `quality_gates/B1_evidence_indexed_gate.json` (audit report) + + + + + - 본 task만이 final `evidence_indexed.json`을 작성한다. 다른 task가 작성하면 단일 writer 위반이다. + - completeness 기준은 `evidence_shard_plan.json.expected_ordinals`이며, `evidence_all.json`은 cross-check로만 사용한다. + - planner의 stale part 감지 또는 expected ordinal 외 part 발견은 `BLOCK`이다. + - mapper part는 절대 덮어쓰지 않는다. + - `BLOCK`이 발생하면 채팅 메시지에 명시하여 orchestrator가 downstream 진행을 중단하도록 한다. + - audit report는 사람이 검토할 수 있도록 한국어로 짧게 작성한다. + + + + - task_name: Task_B2_quality_gate_event_candidates + preflight_files: + - evidence_shard_plan.json + - "evidence_event_candidate_parts/E-*.json" + - evidence_indexed.json + - quality_gates/B1_evidence_indexed_gate.json + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Validate only the artifact, input bundle, IDs, coverage, severity, and handoff condition assigned to the current task. + - Identify contradictions only within assigned inputs: date, amount, party, object, registration, payment, possession, notice, valuation, legal role, causal link, source identity, or required support chain. + - Distinguish contradiction, incompleteness, ambiguity, multiplicity, schema failure, writer-boundary breach, and review-only legal uncertainty. + - Preserve all material conflicting source IDs. Do not average, normalize, or repair unless the current task expressly authorizes repair. + - Treat PASS, SOFT_WARNING, HARD_WARNING, BLOCK, READY, READY_WITH_REVIEW, FAILED, rollback, and signoff statuses exactly as the current schema defines them. + - Do not downgrade severity for convenience, and do not block progression unless the current gate criteria require it. + - Validators report issues in the authorized validation, audit, gate, review, or rollback schema. They do not write final artifacts unless explicitly assigned. + + + + + + Task_B2_quality_gate_event_candidates + + You are an MCP-enabled LLM-backed **quality gate** assisting plaintiff-side Korean civil/commercial litigation counsel. Your role is the final writer of `evidence_event_candidates.json` and the governance gate that decides whether downstream Stage 1 tasks may proceed. + + + 본 task는 v.3 Step D의 B2 quality gate다. `Task_Evidence_shard_planner`가 생성한 `evidence_shard_plan.json`의 `expected_ordinals`와 `shard_count`를 기준으로 모든 B2 mapper part(`evidence_event_candidate_parts/E-*.json`)를 통합하여 최종 `evidence_event_candidates.json`을 단일 writer로 작성하고, 동시에 audit report `quality_gates/B2_event_candidates_gate.json`을 작성한다. + 본 task는 simple merge가 아니다. governance gate로서 5개 검증 항목(event candidate completeness, evidence reference integrity, candidate id collision, chronological ordering, legal-effect event coverage)을 LLM 추론으로 수행하고, 각 항목별 severity와 `hard_gate_findings[]`/`review_findings[]` 분류를 결정한다. B2는 B1/B2 review finding이 deterministic soft-gate handoff writer에 의해 손실 없이 변환될 수 있도록 `quality_gates/B2_event_candidates_gate.json` 안에 compact source fields(`review_findings[]`, `review_gate_summary`, `handoff_export_summary`)를 남긴다. + 본 task는 기존 5개 검증 전에 모든 mapper part item의 schema invariant를 검사하는 최종 통합 지점 안전장치를 수행한다. `event_candidates`가 empty이면서 `zero_event_reason`이 없거나 null인 part는 정상 zero-event가 아니라 BLOCK 사유다. + 본 task만이 `candidate_id`(`EVT-{ordinal:03d}-{seq:02d}`)를 finalize 한다. mapper가 propose 한 `candidate_id_proposed`를 검증하고 cross-document collision-free 한 canonical `candidate_id`로 부여한다. + 변호사 관점에서 본 gate의 핵심은: (a) 모든 mapper part가 통합되었는가, (b) 각 candidate가 존재하는 `evidence_index`를 참조하는가(B1 final과 정합), (c) candidate_id 충돌·`identity_signature` 중복 처리, (d) timeline 안정성(이행기·시효·통지 도달일·경매 chain 등의 시간순 정합), (e) payment, notice, auction, lien, succession, valuation, defense assertion, commercial successor liability 누락 검출이다. + + + + + + - `evidence_shard_plan.json` (expected_ordinals, shard_count, assigned_document_refs, B2 barrier, stale_part_policy) + - `evidence_event_candidate_parts/E-*.json` (모든 B2 mapper part 파일) + - `evidence_indexed.json` (B1 final, evidence_index reference integrity 검증용) + - `quality_gates/B1_evidence_indexed_gate.json` (B1 review finding import for soft handoff consolidation) + + + - `evidence_event_candidates.json` (final authority ledger) + - `quality_gates/B2_event_candidates_gate.json` (audit report) + + + - `localdocs`는 preflight, input read, final file write, existence-only postcheck에만 사용한다. + - 본 task는 LLM-backed quality gate로서 `code-executor`를 사용하지 않는다. SHA256 계산과 soft-gate handoff sidecar 작성은 후속 deterministic task `Task_B2_SHA256_soft_gate_handoff_writer`가 전담한다. + + + - mapper part 파일을 덮어쓰기 + - `evidence_shard_plan.json`을 덮어쓰기 또는 재작성하기 + - planner의 expected_ordinals를 무시하고 독자적 shard plan을 재구성하기 + - 다른 quality gate의 audit report 작성 + - mapper가 추출하지 않은 새 candidate 창작 + - 자기 final 파일 또는 audit report 재읽기 + - simple concat으로 작동(반드시 LLM 검증 수행) + - B1 final이 부재하면 진행 + - `quality_gates/stage1_part1_soft_gate_handoff.json` 작성 또는 덮어쓰기 + - `digest_guard` 값을 추측, placeholder, 설명문, `"n/a"`, `"sha256"`로 생성하기 + + + + + 1. `list_docs`로 `evidence_shard_plan.json`, `evidence_event_candidate_parts/` 디렉토리 안의 part 파일 목록, `evidence_indexed.json`, `quality_gates/B1_evidence_indexed_gate.json` 존재를 확인한다. + 2. `read_docs`로 `evidence_shard_plan.json`, 모든 B2 mapper part 파일, `evidence_indexed.json`, `quality_gates/B1_evidence_indexed_gate.json`을 읽는다. + 3. planner의 `expected_ordinals`, `expected_evidence_ids`, `barriers.B2_all_parts_barrier.require_files`, `stale_part_policy.detected_stale_parts`를 completeness 기준으로 고정한다. + 4. 메모리에서 를 먼저 수행하여 모든 mapper part item의 schema invariant를 검사한다. + 5. 메모리에서 기존 5개 검증을 LLM 추론으로 수행하고 각 finding을 hard/review gate로 분류한다. + 6. schema invariant 위반이 1개라도 있으면 `overall_severity=BLOCK`, `downstream_progression="BLOCKED"`, `stage2_auto_progression_allowed=false`로 고정하고 audit report에 `rerun_target_task`를 기록한다. 자동 강제 재실행은 수행하지 않는다. + 7. canonical `candidate_id` finalize: 각 planner ordinal 내 seq 순으로 `EVT-{ordinal:03d}-{seq:02d}` 형식 부여(propose된 candidate가 정확하면 그대로 유지). identity_signature 중복은 canonical 1개만 남기고 dedup. + 8. envelope object와 `evidence_event_candidates.json`에 write할 정확한 serialized JSON string을 메모리에서 생성한다(`schema_version="evidence_event_candidates.v1"`, top-level `items` 또는 v.2 호환 구조). schema invariant BLOCK part는 정상 item으로 조용히 보정하거나 승격하지 않는다. + 9. audit report object와 `quality_gates/B2_event_candidates_gate.json`에 write할 정확한 serialized JSON string을 메모리에서 생성한다. 이때 `hard_gate_findings[]`, `review_findings[]`, `stage2_risk_notes[]`, `review_gate_summary`, `handoff_export_summary`를 optional append field로 포함한다. `handoff_export_summary`는 후속 `Task_B2_SHA256_soft_gate_handoff_writer`가 사용할 source metadata이며, 본 task가 sidecar를 썼다는 attestation이 아니다. + 10. `write_file(overwrite=true)`로 `evidence_event_candidates.json`을 저장한다. + 11. `write_file(overwrite=true)`로 `quality_gates/B2_event_candidates_gate.json`을 저장한다. + 12. `list_docs`로 두 파일 존재만 확인한다. 절대 읽지 않는다. + 13. hard gate가 있으면 `"B2 quality gate BLOCK 발생: downstream 진행 금지"`만 출력한다. hard gate가 없고 review finding만 있으면 `"증거문서 사건행위 후보 finalize 완료 (severity=)"`를 출력한다. + + + + - 입력 누락(planner 파일 또는 part 파일 1개라도 빠짐), 빈 파일, planner_status BLOCKED_BY_STALE_PARTS, 파싱 불가, B1 final 부재: 즉시 중단하고 채팅으로만 보고한다. final 파일은 쓰지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + + - `hard_gate_findings[]`는 stale part, missing ordinal, parse/schema failure, schema invariant failure, invalid evidence reference, candidate id format violation, final writer side effect처럼 Stage 2 traceability를 깨는 문제만 담는다. + - `review_findings[]`는 cross-document identity duplicate, chronology ambiguity, event coverage concern, legal relevance uncertainty, zero_event_reason review, legal-effect subkind coverage concern처럼 인간 변호사 또는 Stage C/D/E가 검토할 문제만 담는다. + - review finding은 사실 확정이 아니며, 자동 BLOCK 사유가 아니다. hard gate가 없고 review finding만 있으면 `downstream_progression="ALLOWED_WITH_WARNING"`과 `stage2_auto_progression_allowed=true`를 유지한다. + - review finding이 provenance 오염, evidence reference invalidity, source trace 상실로 승격되는 경우에만 hard gate로 재분류한다. + + + + B2는 Part 1 soft gate handoff sidecar의 writer가 아니다. `quality_gates/stage1_part1_soft_gate_handoff.json`은 후속 deterministic task `Task_B2_SHA256_soft_gate_handoff_writer`가 작성하는 compact review queue이며, final fact artifact가 아니다. + + Required B2 audit source fields: + - `review_findings[]`: B2가 직접 판단한 soft/hard warning review findings. 각 finding은 가능한 한 `source_check`, `severity`, `issue_type`, `domain_hints`, `target_stage_hints`, `target_task_hints`, `source_refs`, `review_instruction`, `candidate_use_policy`, `concise_basis`를 포함한다. + - `review_gate_summary`: B1/B2 review count와 highest review severity를 compact하게 요약한다. + - `handoff_export_summary`: sidecar writer가 아니라 downstream deterministic writer를 지정하는 source metadata다. + + Downstream deterministic writer rule, for writer-boundary awareness only: + - `Task_B2_SHA256_soft_gate_handoff_writer`가 `quality_gates/stage1_part1_soft_gate_handoff.json`의 단일 writer다. + - B2는 sidecar object나 `digest_guard` 값을 생성하지 않는다. B2 audit은 B1/B2 review findings를 deterministic writer가 읽을 수 있도록 `review_findings[]`, `review_gate_summary`, `handoff_export_summary`를 제공한다. + - `handoff_export_summary.is_soft_handoff_writer`는 반드시 `false`다. + - `handoff_export_summary.handoff_writer_task`는 반드시 `"Task_B2_SHA256_soft_gate_handoff_writer"`다. + - `handoff_export_summary.handoff_path`는 반드시 `"quality_gates/stage1_part1_soft_gate_handoff.json"`이다. + - review finding은 사실이 아니라 검토 큐다. B2 audit의 review finding 또는 summary에 이 원칙을 보존한다. + - hard gate finding이 1개 이상이면 `handoff_export_summary.handoff_status_candidate="BLOCKED"`, `stage2_auto_progression_allowed=false`로 기록한다. + - hard gate가 없고 review finding이 1개 이상이면 `handoff_export_summary.handoff_status_candidate="READY_WITH_REVIEW"`, review finding이 없으면 `"READY_NO_REVIEW"`로 기록한다. + - source refs는 가능한 한 `evidence_index`, `candidate_id`, `meeting_clause_ref`로 분리하고, 다음 단계에서 Stage A membership 검증이 가능하게 한다. + + + + 본 gate는 모든 mapper part item에 대한 최종 통합 지점 안전장치다. 자동 강제 재실행을 수행하지 않으며, 문제가 발견되면 audit report에 재실행 대상만 명시한다. + + 검사 대상: + - `evidence_shard_plan.json.expected_ordinals`에 대응하는 모든 `evidence_event_candidate_parts/E-{ordinal}.json` + - 각 part의 실질 item object + - final envelope에 반영될 item object + + 필수 invariant: + 1. 각 part는 단일 item object 또는 1-element `items` envelope이며, 실질 item은 정확히 1개다. + 2. item의 `evidence_index_proposed` 또는 `evidence_index`는 planner의 expected evidence id 및 B1 final의 finalized evidence_index와 일치해야 한다. + 3. item은 non-empty `event_candidates` 또는 non-null `zero_event_reason` 중 하나를 반드시 가진다. + 4. `event_candidates`가 빈 배열이면 `zero_event_reason`은 반드시 non-null 허용값이어야 한다. + 5. `event_candidates`와 `zero_event_reason`이 모두 없거나, `event_candidates: []`와 `zero_event_reason: null`이 결합된 경우 severity=`BLOCK`. + 6. candidate가 있으면 각 candidate는 object이며 `candidate_id_proposed`, `source_evidence_index_proposed`, `identity_signature`를 가진다. + + BLOCK 처리: + - 위반 항목마다 audit report의 `schema_invariant_findings`에 `ordinal`, `part_path`, `reason`, `severity="BLOCK"`, `rerun_target_task="Task_B2_map_events_{ordinal}"`을 기록한다. + - `block_reasons`에도 사람이 읽을 수 있는 한국어 설명과 `rerun_target_task`를 포함한다. + - mapper part를 수정하거나 덮어쓰지 않는다. + - 새 candidate나 `zero_event_reason`을 quality gate가 창작하여 보정하지 않는다. + - downstream 진행은 반드시 금지한다. + + + + 본 quality gate는 이후 다음 5개 검증을 수행한다. 각 검증은 LLM 추론으로 진행하고 severity를 결정한다. + + + - 검증 대상: 모든 B1 evidence item에 대응하는 B2 mapper part가 존재하는가 + - 비교 기준: `evidence_shard_plan.json.expected_ordinals`, `barriers.B2_all_parts_barrier.require_files`, `evidence_indexed.json`의 `items[*].evidence_index`와 `evidence_event_candidate_parts/` 디렉토리의 part 파일 ordinal 매칭 + - `evidence_shard_plan.json.stale_part_policy.detected_stale_parts.evidence_event_candidate_parts`가 비어 있지 않으면 severity=`BLOCK` + - 누락된 ordinal이 1개라도 있으면 severity=`BLOCK` + - 모두 매칭되면 severity=`PASS` + + + + - 검증 대상: 각 candidate의 `source_evidence_index_proposed`가 `evidence_indexed.json`의 finalized `evidence_index`에 존재하는가 + - 부정확한 reference(존재하지 않는 evidence_index 참조)가 1개라도 있으면 severity=`BLOCK` + - 모두 정확하면 severity=`PASS` + + + + - 검증 대상: + - `candidate_id_proposed`가 `EVT-{ordinal:03d}-{seq:02d}` 형식이고 같은 ordinal 내 seq가 unique 한가 + - 서로 다른 ordinal이 같은 `identity_signature`를 가진 candidate를 가지는가(cross-document duplicate) + - 형식 위반은 severity=`BLOCK` + - cross-document `identity_signature` 중복은 canonical 1개만 남기고 dedup, severity=`HARD_WARNING` 또는 `SOFT_WARNING` 및 `review_findings[]`에 기록(상황에 따라) + - canonical 결정은 다음 우선순위: (i) raw event > derived legal-effect event, (ii) higher confidence, (iii) more support_locators, (iv) earlier ordinal + - dedup된 candidate는 audit report `dedup_findings`에 기록한다. + + + + - 검증 대상: timeline 정합성 + - 같은 채무에 대한 `underlying_claim_arising` < `first_demand` < `demand_notice_dispatch` <= `demand_notice_arrival` < `debt_acknowledgement` 순서 + - `lien_extinction_notice_dispatch` <= `lien_extinction_notice_receipt` < `lien_extinction_effect_candidate` + - 경매 chain: `secured_debt_creation` < `mortgage_setting_by_fraction` < `auction_application_after_extinguishment` < `auction_sale_and_registry_transfer` + - 사해행위 chain: `preserved_claim_arising` < `fraudulent_transfer`, `encumbrance_release_after_act` > `fraudulent_transfer` + - 시간순 모순이 source trace 자체를 깨뜨리면 severity=`BLOCK` 및 `hard_gate_findings[]`에 기록 + - 법률적 chronology ambiguity 또는 일부 날짜 `null` 문제는 severity=`HARD_WARNING` 또는 `SOFT_WARNING` 및 `review_findings[]`에 기록 + - 모두 정합하면 severity=`PASS` + + + + - 검증 대상: REQUIRED_EVENT_SUBKINDS 누락 + - claim_doc에 채무승인·최고·변제기 단서가 있는데 `debt_acknowledgement`/`new_due_date`/`demand_notice_dispatch`/`demand_notice_arrival`이 모두 누락 → `HARD_WARNING` 및 `review_findings[]` + - business_transfer_doc에 상호속용 단서가 있는데 `commercial_successor_liability_candidate`가 누락 → `HARD_WARNING` + - 등기부에 사후 말소 row가 있는데 `value_compensation_exception_trigger`가 없음 → `HARD_WARNING` 및 `review_findings[]` + - 경매 chain이 등장하는데 `auction_application_after_extinguishment` 또는 `mortgage_extinguishment_by_fraction`이 누락 → `HARD_WARNING` + - 미등기 건물 매수 단서가 있는데 `unregistered_building_original_acquisition` 또는 `de_facto_disposition_holder_acquisition`이 누락 → `HARD_WARNING` + - 유치권 소멸 통지서/수령 답변서가 있는데 `lien_extinction_notice_dispatch`+`lien_extinction_notice_receipt` 양쪽이 모두 없음 → `HARD_WARNING` 및 `review_findings[]` + - 상속포기 수리가 있는데 `succession_renunciation_acceptance`/`spouse_sole_succession_effect_candidate`가 누락 → `HARD_WARNING` + - valuation matrix와 사후 말소 부담이 함께 있는데 `value_compensation_cap_calculation_candidate`가 없음 → `HARD_WARNING` + - `mortgage_setting_by_fraction` 또는 `mortgage_extinguishment_by_fraction` candidate에 지분 표시가 원문에 보이는데 `fraction_ref`가 null → `HARD_WARNING` 및 `review_findings[]` + - 누락이 없으면 severity=`PASS` + + + + + - canonical `candidate_id`는 ordinal 오름차순 → 같은 ordinal 내 seq 오름차순으로 부여한다. + - mapper가 propose 한 candidate가 정확하면 그대로 유지한다(propose=finalize). + - cross-document `identity_signature` 중복은 canonical 1개만 남기고 dedup한다. dropped는 audit report `dedup_findings`에 기록. + - mapper part의 모든 component field(participants, current_state_candidate, succession_candidate, legal_effect_candidate 등)는 그대로 보존한다. + - mapper part의 `mapper_meta` field는 final `items`에서 제외한다(audit report에만 기록). + - 최종 `items` 배열은 ordinal 오름차순으로 정렬한다. + - bare array serialization 금지. 반드시 envelope object. + + + + - 최종 파일 `evidence_event_candidates.json`의 top-level shape는 반드시 object여야 한다. + - top-level 필수 키: `schema_version`, `items`. + - `schema_version`은 `"evidence_event_candidates.v1"`을 유지한다(Stage 2 호환). v.3 메타는 audit report에만 둔다. + - 각 item은 v.2 원본의 schema(`evidence_index`, `title`, `doc_type`, `source_pointer`, `event_candidates` 또는 `zero_event_reason`)와 호환된다. + - 각 item은 non-empty `event_candidates` 또는 non-null `zero_event_reason` 중 하나를 반드시 가진다. `event_candidates: []`와 `zero_event_reason` 누락/null의 결합은 final 통합 금지 및 BLOCK 사유다. + - 각 candidate의 `candidate_id`는 finalized 값이며, `source_evidence_index`도 finalized 값이다. + + + + audit report는 `quality_gates/B2_event_candidates_gate.json` 경로에 저장하며 다음 구조를 따른다. + + { + "gate_id": "B2_event_candidates", + "executed_at": "ISO-8601 datetime", + "input_parts_count": N, + "expected_ordinals": ["001", "002", "003"], + "planner_source": "evidence_shard_plan.json", + "planner_shard_count": N, + "received_ordinals": ["001", "002", "003"], + "schema_invariant_findings": [], + "checks": [ + { + "check_id": "event_candidate_completeness", + "severity": "...", + "detail": "...", + "missing_ordinals": [] + }, + { + "check_id": "evidence_reference_integrity", + "severity": "...", + "detail": "...", + "violations": [] + }, + { + "check_id": "candidate_id_collision", + "severity": "...", + "detail": "...", + "format_violations": [], + "dedup_findings": [] + }, + { + "check_id": "chronological_ordering", + "severity": "...", + "detail": "...", + "ordering_violations": [] + }, + { + "check_id": "legal_effect_event_coverage", + "severity": "...", + "detail": "...", + "missing_subkinds_by_ordinal": [] + } + ], + "overall_severity": "PASS|HARD_WARNING|SOFT_WARNING|BLOCK", + "block_reasons": [], + "warning_reasons": [], + "hard_gate_findings": [], + "review_findings": [], + "rerun_targets": [], + "stage2_risk_notes": [], + "review_gate_summary": { + "review_gate_count": 0, + "highest_review_severity": "PASS|SOFT_WARNING|HARD_WARNING", + "b1_review_count": 0, + "b2_review_count": 0 + }, + "handoff_export_summary": { + "is_soft_handoff_writer": false, + "handoff_writer_task": "Task_B2_SHA256_soft_gate_handoff_writer", + "handoff_path": "quality_gates/stage1_part1_soft_gate_handoff.json", + "exported_review_count": 0, + "handoff_status_candidate": "READY_WITH_REVIEW|READY_NO_REVIEW|BLOCKED" + }, + "downstream_progression": "ALLOWED|ALLOWED_WITH_WARNING|BLOCKED", + "stage2_auto_progression_allowed": true, + "schema_contract_version": "B2_quality_gate.v3" + } + + 추가 제약: + - `schema_invariant_findings`는 필수 배열이다. 위반이 없으면 빈 배열 `[]`로 둔다. + - `schema_invariant_findings`의 위반 item은 `{ "ordinal": "001", "part_path": "evidence_event_candidate_parts/E-001.json", "reason": "event_candidates empty without non-null zero_event_reason", "severity": "BLOCK", "rerun_target_task": "Task_B2_map_events_001" }` 형태를 따른다. + - `overall_severity`는 schema invariant gate 및 5개 check 중 가장 높은 severity와 동일하다(우선순위: BLOCK > HARD_WARNING > SOFT_WARNING > PASS). + - `schema_invariant_findings`가 1개 이상이면 `overall_severity="BLOCK"`, `downstream_progression="BLOCKED"`, `stage2_auto_progression_allowed=false`로 고정한다. + - `downstream_progression`은 `overall_severity`로 결정한다. + - `stage2_auto_progression_allowed=false` ⇔ `downstream_progression="BLOCKED"`. B2 BLOCK은 Stage 1 downstream 전체를 중단하므로 Stage 2 auto-progression도 불허한다. + - `block_reasons`/`warning_reasons`는 사람이 읽을 수 있는 짧은 한국어 설명이며, schema invariant BLOCK의 경우 `rerun_target_task`를 포함한다. + - `overall_severity="BLOCK"`은 `schema_invariant_findings[]` 또는 `hard_gate_findings[]`가 1개 이상일 때만 허용한다. + - hard gate가 없고 `review_findings[]`만 존재하면 `overall_severity`는 `HARD_WARNING` 또는 `SOFT_WARNING`, `downstream_progression="ALLOWED_WITH_WARNING"`, `stage2_auto_progression_allowed=true`로 둔다. + - 기존 `block_reasons`/`warning_reasons`는 삭제하지 않고 `hard_gate_findings[]`/`review_findings[]`의 사람이 읽는 mirror로 유지한다. + - B2는 `quality_gates/stage1_part1_soft_gate_handoff.json`을 쓰지 않는다. B2는 B1/B2 review finding을 후속 deterministic writer가 변환할 수 있도록 `review_findings[]`, `review_gate_summary`, `handoff_export_summary` source fields를 제공한다. + + + + - 모든 mapper part item에 대해 를 수행했는지 점검한다. + - `event_candidates: []`와 `zero_event_reason` 누락/null이 결합된 part가 1개라도 있으면 BLOCK으로 판정하고 `schema_invariant_findings[*].rerun_target_task`를 기록했는지 점검한다. + - final `evidence_event_candidates.json`이 envelope object이고 `schema_version="evidence_event_candidates.v1"`, `items`를 모두 포함하는지 점검한다. + - schema invariant PASS인 모든 mapper part가 `items`에 정확히 1번 반영되었는지 점검한다. + - 모든 candidate의 `candidate_id`가 `EVT-{ordinal:03d}-{seq:02d}` 형식이고 unique 한지 점검한다. + - 모든 candidate의 `source_evidence_index`가 `evidence_indexed.json`에 존재하는지 점검한다. + - `mapper_meta` field가 final `items`에서 제거되었는지 점검한다. + - audit report가 5개 check 모두를 포함하는지 점검한다. + - audit report가 필수 배열 `schema_invariant_findings`를 포함하는지 점검한다. + - `overall_severity`와 `downstream_progression`이 일치하는지 점검한다. + - audit report가 `downstream_progression`과 `stage2_auto_progression_allowed`를 모두 포함하고, 두 필드가 일관되는지 점검한다. + - `BLOCK` severity가 발생한 경우 final 파일도 작성하되, 채팅 메시지에 BLOCK을 명시하여 orchestrator가 downstream 진행을 중단하도록 한다. + - B2 audit의 `handoff_export_summary.is_soft_handoff_writer`가 false이고 `handoff_writer_task="Task_B2_SHA256_soft_gate_handoff_writer"`인지 점검한다. + - B2가 `quality_gates/stage1_part1_soft_gate_handoff.json`을 작성하거나 `digest_guard` 값을 생성하지 않았는지 점검한다. + - B2 audit의 `review_findings[]`, `review_gate_summary`, `handoff_export_summary`가 후속 deterministic writer의 source material로 충분한지 점검한다. + - review finding이 final fact로 표현되지 않고 lawyer workflow review queue source로만 표현되는지 점검한다. + + + + + + [ ] 1) Preflight: `list_docs` 사용해서 `evidence_shard_plan.json`, `evidence_event_candidate_parts/E-*.json`, `evidence_indexed.json` 확인. `read_docs` 사용해서 동일 파일들 모두 읽기 + [ ] 2) planner의 `expected_ordinals`, `barriers.B2_all_parts_barrier.require_files`, `stale_part_policy`를 기준으로 B2 part completeness와 stale part를 우선 검증 + [ ] 3) 수행: 모든 part item schema 검사, empty `event_candidates` + missing/null `zero_event_reason`이면 BLOCK 및 `rerun_target_task` 기록 + [ ] 4) 기존 5개 GATE_CHECKS LLM 추론 수행 + severity 및 hard/review gate classification 결정 + [ ] 5) `evidence_event_candidates.json`과 `quality_gates/B2_event_candidates_gate.json`에 write할 정확한 serialized JSON string을 확정하고, B2 audit에 `review_findings[]`, `review_gate_summary`, `handoff_export_summary` source fields를 포함 + [ ] 6) B2 audit의 `handoff_export_summary.is_soft_handoff_writer=false`, `handoff_writer_task="Task_B2_SHA256_soft_gate_handoff_writer"`, `handoff_path="quality_gates/stage1_part1_soft_gate_handoff.json"` 확인 + [ ] 7) `write_file` 사용해서 `evidence_event_candidates.json` 생성 (envelope object, schema_version="evidence_event_candidates.v1") + [ ] 8) `write_file` 사용해서 `quality_gates/B2_event_candidates_gate.json` 생성 (audit report, `schema_invariant_findings` 포함) + [ ] 9) `list_docs` 사용해서 작성한 두 파일 존재만 확인(절대 읽기 금지) + [ ] 10) hard gate가 있으면 `"B2 quality gate BLOCK 발생: downstream 진행 금지"`, 그 외는 `"증거문서 사건행위 후보 finalize 완료 (severity=)"` 출력하고 작업을 끝낸다(terminate) + + + + + - `evidence_shard_plan.json` + - `evidence_event_candidate_parts/E-*.json` (모든 B2 mapper part) + - `evidence_indexed.json` + - `quality_gates/B1_evidence_indexed_gate.json` + + + - `evidence_event_candidates.json` (final) + - `quality_gates/B2_event_candidates_gate.json` (audit report) + + + + + - 본 task만이 final `evidence_event_candidates.json`을 작성한다. 다른 task가 작성하면 단일 writer 위반이다. + - completeness 기준은 `evidence_shard_plan.json.expected_ordinals`이며, B1 final은 evidence reference integrity 검증에 사용한다. + - planner의 stale part 감지 또는 expected ordinal 외 part 발견은 `BLOCK`이다. + - `schema_version="evidence_event_candidates.v1"`을 유지하여 Stage 2 호환성을 보장한다. + - mapper part는 절대 덮어쓰지 않는다. + - `quality_gates/stage1_part1_soft_gate_handoff.json`은 절대 쓰지 않는다. 이 sidecar는 후속 deterministic task `Task_B2_SHA256_soft_gate_handoff_writer`만 쓴다. + - `BLOCK`이 발생하면 채팅 메시지에 명시하여 orchestrator가 downstream 진행을 중단하도록 한다. + + + + - task_name: Task_B2_SHA256_soft_gate_handoff_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import hashlib + import itertools + import json + import re + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + SIDE_CAR_PATH = "quality_gates/stage1_part1_soft_gate_handoff.json" + INPUT_PATHS = { + "evidence_indexed_sha256": "evidence_indexed.json", + "evidence_event_candidates_sha256": "evidence_event_candidates.json", + "b1_gate_sha256": "quality_gates/B1_evidence_indexed_gate.json", + "b2_gate_sha256": "quality_gates/B2_event_candidates_gate.json", + } + WORKFLOW_ROLES = ( + "claim_right_identification", + "cause_of_action_element_build", + "evidence_weight_and_gap_check", + "chronology_and_state_split", + "asset_registry_succession_check", + "defense_and_obstacle_check", + "relief_scope_and_quantification", + "actio_value_compensation_check", + ) + SOURCE_REF_KEYS = ( + "evidence_indexes", + "event_candidate_ids", + "meeting_clause_refs", + "doc_titles", + ) + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-b2-sha256-soft-gate-handoff-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _mid(), + "method": "tools/call", + "params": {"name": name, "arguments": args}, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: Any, name: str) -> str: + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError(f"Empty: {name}") + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, dict) and "results" in outer: + result0 = (outer.get("results") or [{}])[0] + inner = result0.get("content") or result0.get("text") or "" + if not inner: + raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer if isinstance(outer, str) else json.dumps(outer, ensure_ascii=False) + + def read_raw(path: str) -> str: + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str) or not raw.strip(): + raise RuntimeError(f"Missing or empty required input: {path}") + return raw + + def parse_json(raw: str, path: str) -> Any: + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + try: + obj, _ = decoder.raw_decode(raw.strip()) + return obj + except Exception as exc: + raise RuntimeError(f"Invalid JSON input: {path}") from exc + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def digest(raw: str) -> str: + value = hashlib.sha256(raw.encode("utf-8")).hexdigest() + if not re.fullmatch(r"[0-9a-f]{64}", value): + raise RuntimeError("digest_guard produced a non-SHA256 value") + return value + + def as_list(value: Any) -> list[Any]: + if value is None: + return [] + if isinstance(value, list): + return value + return [value] + + def as_str_list(value: Any) -> list[str]: + out: list[str] = [] + for item in as_list(value): + if item is None: + continue + if isinstance(item, dict): + for key in ("id", "value", "name", "title", "ref"): + if item.get(key): + out.append(str(item[key]).strip()) + break + else: + out.append(str(item).strip()) + return [x for x in out if x] + + def clean_enum(value: Any, fallback: str) -> str: + text = str(value or fallback).strip().lower() + text = re.sub(r"[^a-z0-9_]+", "_", text) + text = re.sub(r"_+", "_", text).strip("_") + return text or fallback + + def clip(value: Any, limit: int = 120) -> str: + text = " ".join(str(value or "").split()) + if len(text) <= limit: + return text + return text[:limit].rstrip() + "..." + + def normalize_severity(value: Any) -> str: + text = str(value or "").upper() + if "HARD" in text: + return "HARD_WARNING" + return "SOFT_WARNING" + + def role_for(issue_type: str, finding: dict[str, Any]) -> str: + explicit = finding.get("lawyer_workflow_role") + if explicit in WORKFLOW_ROLES: + return str(explicit) + text = " ".join([ + issue_type, + str(finding.get("source_check") or ""), + str(finding.get("check_id") or ""), + str(finding.get("detail") or finding.get("reason") or ""), + ]).lower() + if any(k in text for k in ("chrono", "date", "timeline", "state", "order")): + return "chronology_and_state_split" + if any(k in text for k in ("evidence", "proof", "source", "locator")): + return "evidence_weight_and_gap_check" + if any(k in text for k in ("registry", "secured", "lien", "succession", "asset", "auction", "mortgage")): + return "asset_registry_succession_check" + if any(k in text for k in ("defense", "obstacle", "setoff", "limitation")): + return "defense_and_obstacle_check" + if any(k in text for k in ("relief", "amount", "scope", "valuation", "damage")): + return "relief_scope_and_quantification" + if "actio" in text or "fraudulent" in text or "value_compensation" in text: + return "actio_value_compensation_check" + if any(k in text for k in ("claim", "right", "element", "legal_effect")): + return "claim_right_identification" + return "cause_of_action_element_build" + + def source_refs(finding: dict[str, Any]) -> dict[str, list[str]]: + refs = {key: [] for key in SOURCE_REF_KEYS} + nested = finding.get("source_refs") + if isinstance(nested, dict): + for key in SOURCE_REF_KEYS: + refs[key].extend(as_str_list(nested.get(key))) + refs["evidence_indexes"].extend(as_str_list(finding.get("evidence_indexes"))) + refs["evidence_indexes"].extend(as_str_list(finding.get("evidence_index"))) + refs["evidence_indexes"].extend(as_str_list(finding.get("source_evidence_index"))) + refs["event_candidate_ids"].extend(as_str_list(finding.get("event_candidate_ids"))) + refs["event_candidate_ids"].extend(as_str_list(finding.get("candidate_id"))) + refs["event_candidate_ids"].extend(as_str_list(finding.get("candidate_ids"))) + refs["meeting_clause_refs"].extend(as_str_list(finding.get("meeting_clause_refs"))) + refs["meeting_clause_refs"].extend(as_str_list(finding.get("meeting_clause_ref"))) + refs["doc_titles"].extend(as_str_list(finding.get("doc_titles"))) + refs["doc_titles"].extend(as_str_list(finding.get("doc_title"))) + return {key: sorted(set(value)) for key, value in refs.items()} + + def extract_findings(gate: Any, key: str) -> list[Any]: + findings: list[Any] = [] + if isinstance(gate, dict): + findings.extend(as_list(gate.get(key))) + for check in as_list(gate.get("checks")): + if isinstance(check, dict): + findings.extend(as_list(check.get(key))) + return findings + + def hard_findings(gate: Any, source_gate: str) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for item in extract_findings(gate, "hard_gate_findings"): + out.append(item if isinstance(item, dict) else {"detail": str(item)}) + if isinstance(gate, dict): + blocked = gate.get("overall_severity") == "BLOCK" or gate.get("downstream_progression") == "BLOCKED" or gate.get("stage2_auto_progression_allowed") is False + if blocked and not out: + reasons = as_list(gate.get("block_reasons")) + if not reasons: + reasons = [f"{source_gate} gate blocked without explicit hard_gate_findings"] + for reason in reasons: + out.append({"issue_type": "block_reason", "detail": str(reason)}) + for index, item in enumerate(out, 1): + item.setdefault("gate_finding_id", f"{source_gate}-HARD-{index:03d}") + return out + + def compact_review_items(b1_gate: Any, b2_gate: Any) -> list[dict[str, Any]]: + compact: list[dict[str, Any]] = [] + ordered = [("B1", b1_gate), ("B2", b2_gate)] + seq = 1 + for source_gate, gate in ordered: + for raw in extract_findings(gate, "review_findings"): + finding = raw if isinstance(raw, dict) else {"detail": str(raw)} + issue_type = clean_enum( + finding.get("issue_type") or finding.get("check_id") or finding.get("source_check") or "review_finding", + "review_finding", + ) + role = role_for(issue_type, finding) + refs = source_refs(finding) + policy = finding.get("candidate_use_policy") + if policy not in ("prioritize_if_supported", "preserve_as_needs_review", "do_not_generate_without_support"): + policy = "preserve_as_needs_review" + item = { + "review_id": f"P1RG-{seq:04d}", + "source_gate": source_gate, + "source_check": clean_enum(finding.get("source_check") or finding.get("check_id") or issue_type, issue_type), + "severity": normalize_severity(finding.get("severity")), + "issue_type": issue_type, + "lawyer_workflow_role": role, + "domain_hints": as_str_list(finding.get("domain_hints") or finding.get("domain_hint") or finding.get("domains")), + "target_stage_hints": as_str_list(finding.get("target_stage_hints") or finding.get("target_stage_hint")), + "target_task_hints": as_str_list(finding.get("target_task_hints") or finding.get("target_task_hint")), + "source_refs": refs, + "review_instruction": clip(finding.get("review_instruction") or finding.get("instruction") or "후속 검토 단계에서 해당 review finding의 증거와 법률상 의미를 재검토한다.", 120), + "negative_instruction": "사실 확정 또는 자동 차단으로 취급하지 말 것", + "candidate_use_policy": policy, + "concise_basis": clip(finding.get("concise_basis") or finding.get("detail") or finding.get("reason") or issue_type, 120), + "created_from": { + "gate_file": "quality_gates/B1_evidence_indexed_gate.json" if source_gate == "B1" else "quality_gates/B2_event_candidates_gate.json", + "gate_finding_id": finding.get("gate_finding_id") or finding.get("finding_id") or finding.get("id"), + }, + } + compact.append(item) + seq += 1 + if len(compact) >= 40: + return compact + return compact + + def add_index(index: dict[str, dict[str, list[str]]], bucket: str, key: str, review_id: str) -> None: + if not key: + return + index[bucket].setdefault(key, []) + if review_id not in index[bucket][key]: + index[bucket][key].append(review_id) + + def build_routing(review_items: list[dict[str, Any]]) -> tuple[dict[str, dict[str, list[str]]], dict[str, list[str]]]: + routing = { + "by_evidence_index": {}, + "by_event_candidate_id": {}, + "by_meeting_clause_ref": {}, + "by_issue_type": {}, + "by_domain_hint": {}, + "by_target_stage": {}, + "by_lawyer_workflow_role": {}, + } + queues = {role: [] for role in WORKFLOW_ROLES} + for item in review_items: + rid = item["review_id"] + refs = item.get("source_refs") or {} + for value in refs.get("evidence_indexes", []): + add_index(routing, "by_evidence_index", value, rid) + for value in refs.get("event_candidate_ids", []): + add_index(routing, "by_event_candidate_id", value, rid) + for value in refs.get("meeting_clause_refs", []): + add_index(routing, "by_meeting_clause_ref", value, rid) + add_index(routing, "by_issue_type", item.get("issue_type", ""), rid) + for value in item.get("domain_hints", []): + add_index(routing, "by_domain_hint", value, rid) + for value in item.get("target_stage_hints", []): + add_index(routing, "by_target_stage", value, rid) + role = item.get("lawyer_workflow_role", "cause_of_action_element_build") + add_index(routing, "by_lawyer_workflow_role", role, rid) + queues.setdefault(role, []).append(rid) + return routing, {role: queues.get(role, []) for role in WORKFLOW_ROLES} + + def gate_blocked(gate: Any) -> bool: + return isinstance(gate, dict) and ( + gate.get("overall_severity") == "BLOCK" + or gate.get("downstream_progression") == "BLOCKED" + or gate.get("stage2_auto_progression_allowed") is False + ) + + def main() -> None: + _init() + raw_by_digest_key = {key: read_raw(path) for key, path in INPUT_PATHS.items()} + parsed = {key: parse_json(raw, INPUT_PATHS[key]) for key, raw in raw_by_digest_key.items()} + + digest_guard = {key: digest(raw) for key, raw in raw_by_digest_key.items()} + b1_gate = parsed["b1_gate_sha256"] + b2_gate = parsed["b2_gate_sha256"] + review_items = compact_review_items(b1_gate, b2_gate) + routing_index, lawyer_workflow_queues = build_routing(review_items) + + hard_items = hard_findings(b1_gate, "B1") + hard_findings(b2_gate, "B2") + hard_ids = [ + str(item.get("gate_finding_id") or item.get("finding_id") or item.get("id") or f"HARD-{idx:03d}") + for idx, item in enumerate(hard_items, 1) + ] + blocked = bool(hard_items) or gate_blocked(b1_gate) or gate_blocked(b2_gate) + highest_review = "PASS" + if any(item["severity"] == "HARD_WARNING" for item in review_items): + highest_review = "HARD_WARNING" + elif review_items: + highest_review = "SOFT_WARNING" + b1_count = sum(1 for item in review_items if item["source_gate"] == "B1") + b2_count = sum(1 for item in review_items if item["source_gate"] == "B2") + + if blocked: + handoff_status = "BLOCKED" + elif review_items: + handoff_status = "READY_WITH_REVIEW" + else: + handoff_status = "READY_NO_REVIEW" + + sidecar = { + "schema_version": "stage1_part1_soft_gate_handoff.v1", + "source_stage": "Stage_1.Part_1", + "source_gate_files": [ + "quality_gates/B1_evidence_indexed_gate.json", + "quality_gates/B2_event_candidates_gate.json", + ], + "handoff_status": handoff_status, + "stage2_auto_progression_allowed": not blocked, + "hard_gate_summary": { + "hard_gate_count": len(hard_items), + "hard_gate_ids": hard_ids, + }, + "review_gate_summary": { + "review_gate_count": len(review_items), + "highest_review_severity": highest_review, + "b1_review_count": b1_count, + "b2_review_count": b2_count, + }, + "digest_guard": digest_guard, + "review_items": review_items, + "routing_index": routing_index, + "lawyer_workflow_queues": lawyer_workflow_queues, + } + + content = json.dumps(sidecar, ensure_ascii=False, indent=2) + write_doc(SIDE_CAR_PATH, content) + print(json.dumps({ + "status": "SOFT_GATE_HANDOFF_WRITTEN", + "path": SIDE_CAR_PATH, + "handoff_status": handoff_status, + "review_items": len(review_items), + "digest_guard_valid": all(re.fullmatch(r"[0-9a-f]{64}", v) for v in digest_guard.values()), + }, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + task_procedure: + IN: + nexts: ["Task_A_client_goal", "Task_Evidence_shard_planner"] + wait_until: [] + + Task_A_client_goal: + nexts: [] + wait_until: ["IN"] + + Task_Evidence_shard_planner: + nexts: ["Task_B1_map_doc_*"] + wait_until: ["IN"] + + Task_B1_map_doc_*: + nexts: + - "Task_B2_map_events_{same_ordinal}" + - "Task_B1_quality_gate_evidence_indexed" + wait_until: ["Task_Evidence_shard_planner"] + + Task_B2_map_events_*: + nexts: ["Task_B2_quality_gate_event_candidates"] + wait_until: ["Task_B1_map_doc_{same_ordinal}"] + + Task_B1_quality_gate_evidence_indexed: + nexts: ["Task_B2_quality_gate_event_candidates"] + wait_until: ["all Task_B1_map_doc_*"] + + Task_B2_quality_gate_event_candidates: + nexts: ["Task_B2_SHA256_soft_gate_handoff_writer"] + wait_until: ["all Task_B2_map_events_*", "Task_B1_quality_gate_evidence_indexed"] + + Task_B2_SHA256_soft_gate_handoff_writer: + nexts: ["OUT"] + wait_until: ["Task_B2_quality_gate_event_candidates"] + + OUT: + nexts: [] + wait_until: ["Task_B2_SHA256_soft_gate_handoff_writer"] + + prevs: [] + nexts: [] diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_2_Claude_v3.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_2_Claude_v3.yml new file mode 100644 index 00000000..a514060a --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_2_Claude_v3.yml @@ -0,0 +1,5280 @@ +--- +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_2 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Part 2 + version: v.2 + Stages: + - name: stage1_BO_시그널_생성 + description: BO 생성, 시그널 생성 + llm_provider: openai + llm_model: gpt-4o-2024-08-06 + tools: + mcpServers: + localdocs: + type: streamable-http + url: http://mcp-localdocs:8012/mcp + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + tasks: + - task_name: Task_C_BO_A0_context_and_domain_slice_compiler + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: |- + #!/usr/bin/env python3 + from __future__ import annotations + import hashlib + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + + REQUIRED_FILES = [ + "client_meeting.md", + "evidence_event_candidates.json", + "evidence_indexed.json", + "Default_Agent/Juristic_Act.md", + ] + PART1_SOFT_GATE_HANDOFF_FILE = "quality_gates/stage1_part1_soft_gate_handoff.json" + + CURRENT_STATE_PATTERNS = [ + "현재", "현재까지", "현재도", "계속", "거주", "점유", "사용", "운영", + "부지로만 사용", "인도받아", "사업자등록", "유치권", "임대", "수익", + ] + CONFLICT_PATTERNS = [ + "하였음에도", "그럼에도", "불구하고", "거절", "응하지", "주장", + "항변", "다툼", "무효", "취소", "해제", "해지", "말소", "신뢰", + ] + SUCCESSION_PATTERNS = [ + "사망", "상속", "상속포기", "배우자", "직계비속", "직계존속", + "피상속인", "상속인", "수리", "가족관계", + ] + MIXED_CONTRACT_PATTERNS = [ + "임대차", "매매", "보증금", "매매대금", "인도", "관리", "사용", + "사용수익", "영업양도", "채무불승계", "대물변제", + ] + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-c-bo-a0-context-and-domain-slice-compiler", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _mid(), + "method": "tools/call", + "params": {"name": name, "arguments": args}, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + p = _parse(r.text) + if not p or "result" not in p: + raise RuntimeError(f"MCP {name} failed") + return p + + def _unwrap(p: Any, name: str) -> Any: + text = (p["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError(f"Empty: {name}") + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, dict) and "results" in outer: + return outer + return outer + + def _stable_hash(value: Any) -> str: + if isinstance(value, str): + raw = value + else: + raw = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _text(value: Any) -> str | None: + if value is None: + return None + s = str(value).strip() + return s or None + + def _norm_space(value: Any) -> str: + return re.sub(r"\s+", " ", str(value or "")).strip() + + def _norm_key(value: Any) -> str: + text = str(value or "").strip().lower() + return "".join(ch for ch in text if ch.isalnum()) + + def _short_excerpt(value: Any, limit: int = 120) -> str: + text = _norm_space(value) + return text if len(text) <= limit else text[: limit - 1] + "…" + + def _section_key_from_heading(heading: str, seq: int) -> str: + normalized = _norm_key(heading) + if "상담내용" in normalized: + suffix = "consultation" + elif "사건관계인의주장" in normalized or ("사건관계인" in normalized and "주장" in normalized): + suffix = "party_assertions" + elif "희망사항" in normalized or "의뢰인희망" in normalized: + suffix = "client_requests" + elif normalized: + suffix = normalized[:24] + else: + suffix = "unnamed" + return f"s{seq:02d}_{suffix}" + + def _uniq(values: list[Any]) -> list[Any]: + seen: set[str] = set() + out: list[Any] = [] + for value in values: + marker = json.dumps(value, ensure_ascii=False, sort_keys=True) if isinstance(value, (dict, list)) else str(value) + if marker not in seen: + seen.add(marker) + out.append(value) + return out + + def _extract_doc_names(payload: Any) -> list[str]: + names: list[str] = [] + def walk(v: Any) -> None: + if isinstance(v, str): + if "." in v or "/" in v: + names.append(v) + names.extend(re.findall(r"[\w./()가-힣ᄀ-ᇿ -]+\.[A-Za-z0-9]+", v)) + elif isinstance(v, list): + for item in v: + walk(item) + elif isinstance(v, dict): + for key in ("name", "path", "doc_name", "document_name", "file", "filename"): + if v.get(key): + names.append(str(v[key])) + for val in v.values(): + if isinstance(val, (list, dict)): + walk(val) + walk(payload) + return sorted(set(names)) + + def _doc_name_matches(found_names: list[str], required: str) -> bool: + required_norm = required.strip("/") + required_base = required_norm.split("/")[-1] + for name in found_names: + n = name.strip("/") + if n == required_norm or n.endswith("/" + required_norm) or n.split("/")[-1] == required_base: + return True + if required_norm in n or required_base in n: + return True + return False + + def _preflight_required_files() -> dict[str, Any]: + found_names_set: set[str] = set() + list_docs_args = ( + {"path": "/", "pattern": "*", "recursive": True}, + {"path": ".", "pattern": "*", "recursive": True}, + {"path": "/", "pattern": "*"}, + {"path": ".", "pattern": "*"}, + {"path": "Default_Agent", "pattern": "*"}, + {"path": "quality_gates", "pattern": "*.json"}, + ) + for args in list_docs_args: + try: + payload = _unwrap(_call("list_docs", args), "list_docs") + found_names_set.update(_extract_doc_names(payload)) + except Exception: + continue + found_names_now = sorted(found_names_set) + required_ready = all(_doc_name_matches(found_names_now, path) for path in REQUIRED_FILES) + sidecar_checked = _doc_name_matches(found_names_now, PART1_SOFT_GATE_HANDOFF_FILE) or args.get("path") == "quality_gates" + if required_ready and sidecar_checked: + break + found_names = sorted(found_names_set) + if not found_names: + raise RuntimeError("list_docs returned no recognizable document names for required inputs") + missing = [path for path in REQUIRED_FILES if not _doc_name_matches(found_names, path)] + if missing: + raise RuntimeError(f"Missing required input files by list_docs: {missing}") + return {"found_names": found_names, "required_files": REQUIRED_FILES} + + def _read_required_docs_once() -> dict[str, str]: + payload = _unwrap(_call("read_docs", {"doc_names": REQUIRED_FILES}), "read_docs") + docs: dict[str, str] = {} + + def assign(name: str | None, content: Any) -> None: + if not name: + return + text = content if isinstance(content, str) else json.dumps(content, ensure_ascii=False) + for required in REQUIRED_FILES: + if name == required or name.endswith("/" + required) or name.split("/")[-1] == required.split("/")[-1]: + docs[required] = text + + if isinstance(payload, dict) and isinstance(payload.get("results"), list): + for item in payload["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + content = item.get("content") or item.get("text") or item.get("markdown") or item.get("raw") + assign(str(name) if name else None, content) + elif isinstance(payload, dict): + for required in REQUIRED_FILES: + if required in payload: + assign(required, payload[required]) + elif isinstance(payload, str): + if len(REQUIRED_FILES) == 1: + assign(REQUIRED_FILES[0], payload) + + missing = [name for name in REQUIRED_FILES if name not in docs] + if missing: + # Some Localdocs runtimes return one JSON wrapper inside the text field. + raw = payload if isinstance(payload, str) else json.dumps(payload, ensure_ascii=False) + try: + outer = json.loads(raw) + if isinstance(outer, dict) and isinstance(outer.get("results"), list): + for item in outer["results"]: + if isinstance(item, dict): + name = item.get("name") or item.get("path") or item.get("doc_name") + content = item.get("content") or item.get("text") + assign(str(name) if name else None, content) + except Exception: + pass + missing = [name for name in REQUIRED_FILES if name not in docs] + if missing: + raise RuntimeError(f"read_docs did not return required inputs: {missing}") + for name, content in docs.items(): + if not str(content).strip(): + raise RuntimeError(f"Required input is empty: {name}") + return docs + + def _parse_json_doc(name: str, raw: str) -> Any: + try: + return json.loads(raw.strip()) + except json.JSONDecodeError as exc: + try: + obj, _ = json.JSONDecoder().raw_decode(raw.strip()) + return obj + except json.JSONDecodeError: + raise RuntimeError(f"Strict JSON parse failed: {name}: {exc.msg} at line {exc.lineno} column {exc.colno}") from exc + + def _items(obj: Any, *keys: str) -> list[Any]: + if isinstance(obj, list): + return obj + if isinstance(obj, dict): + for key in keys + ("items", "evidence_indexed", "documents", "results"): + val = obj.get(key) + if isinstance(val, list): + return val + return [] + + def _extract_keywords(text: str, patterns: list[str]) -> list[str]: + return [p for p in patterns if p in text] + + def _build_meeting_clause_map(meeting_text: str) -> dict[str, Any]: + clauses: list[dict[str, Any]] = [] + current_major: str | None = None + current: dict[str, Any] | None = None + section_seq = 0 + current_section_key = "preamble" + current_section_label = "preamble" + + section_re = re.compile(r"^\s*#?\s*【\s*(.+?)\s*】\s*$") + major_re = re.compile(r"^\s*(\d+)\.\s+(.+?)\s*$") + sub_re = re.compile(r"^\s*([가-힣])\.\s+(.+?)\s*$") + + def flush(end_line: int) -> None: + nonlocal current + if not current: + return + current["line_end"] = end_line + current["text"] = _norm_space(current.get("text", "")) + text = current["text"] + current["tags"] = { + "current_state": _extract_keywords(text, CURRENT_STATE_PATTERNS), + "conflict_pattern": _extract_keywords(text, CONFLICT_PATTERNS), + "succession": _extract_keywords(text, SUCCESSION_PATTERNS), + "mixed_contract": _extract_keywords(text, MIXED_CONTRACT_PATTERNS), + } + current["text_excerpt"] = _short_excerpt(text) + clauses.append(current) + current = None + + lines = meeting_text.splitlines() + for idx, line in enumerate(lines, 1): + stripped = line.strip() + if not stripped or stripped.startswith("|") or stripped.startswith("---"): + continue + m_section = section_re.match(stripped) + m_major = major_re.match(stripped) + m_sub = sub_re.match(stripped) + if m_section: + flush(idx - 1) + section_seq += 1 + current_section_label = _norm_space(m_section.group(1)) + current_section_key = _section_key_from_heading(current_section_label, section_seq) + current_major = None + elif m_major: + flush(idx - 1) + current_major = m_major.group(1) + current = { + "clause_id": f"{current_section_key}:{current_major}", + "local_clause_id": current_major, + "section_key": current_section_key, + "section_label": current_section_label, + "major_no": current_major, + "sub_no": None, + "heading": _norm_space(m_major.group(2)), + "line_start": idx, + "text": _norm_space(m_major.group(2)), + } + elif m_sub and current_major: + flush(idx - 1) + sub_no = m_sub.group(1) + current = { + "clause_id": f"{current_section_key}:{current_major}-{sub_no}", + "local_clause_id": f"{current_major}-{sub_no}", + "section_key": current_section_key, + "section_label": current_section_label, + "major_no": current_major, + "sub_no": sub_no, + "heading": None, + "line_start": idx, + "text": _norm_space(m_sub.group(2)), + } + elif current: + current["text"] = _norm_space(str(current.get("text", "")) + " " + stripped) + flush(len(lines)) + + if not clauses: + raise RuntimeError("client_meeting.md parse produced no numbered meeting clauses") + + by_id: dict[str, Any] = {} + duplicate_clause_ids: list[str] = [] + for clause in clauses: + cid = clause["clause_id"] + if cid in by_id: + duplicate_clause_ids.append(cid) + by_id[cid] = clause + if duplicate_clause_ids: + raise RuntimeError(f"Duplicate meeting clause ids after section normalization: {duplicate_clause_ids}") + return { + "schema_version": "meeting_clause_map.v1", + "clause_order": [c["clause_id"] for c in clauses], + "by_clause_id": by_id, + "sections": _uniq([{"section_key": c["section_key"], "section_label": c["section_label"]} for c in clauses]), + "current_state_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["current_state"]], + "conflict_pattern_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["conflict_pattern"]], + "succession_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["succession"]], + "mixed_contract_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["mixed_contract"]], + } + + def _candidate_id(candidate: dict[str, Any], evidence_index: str, seq: int) -> str: + return str(candidate.get("candidate_id") or candidate.get("candidate_id_proposed") or f"internal_missing_candidate_id::{evidence_index}::{seq:02d}") + + def _evidence_index_from_item(item: dict[str, Any]) -> str | None: + return _text(item.get("evidence_index") or item.get("evidence_index_proposed") or item.get("source_evidence_index")) + + def _build_event_candidate_map(event_obj: Any) -> dict[str, Any]: + items = _items(event_obj) + if not items: + raise RuntimeError("evidence_event_candidates.json parse produced no document-level items") + by_candidate_id: dict[str, Any] = {} + by_evidence_index: dict[str, list[str]] = {} + by_meeting_clause_ref: dict[str, list[str]] = {} + state_candidate_ids: list[str] = [] + succession_candidate_ids: list[str] = [] + mixed_contract_candidate_ids: list[str] = [] + zero_event_evidence_indexes: list[str] = [] + asset_cluster_values: list[str] = [] + malformed_item_refs: list[str] = [] + + for item_seq, item in enumerate(items, 1): + if not isinstance(item, dict): + malformed_item_refs.append(f"item#{item_seq}: non-object") + continue + evidence_index = _evidence_index_from_item(item) + if not evidence_index: + malformed_item_refs.append(f"item#{item_seq}: missing evidence_index") + continue + item_ref = evidence_index + event_candidates_raw = item.get("event_candidates") + has_event_candidates_list = isinstance(event_candidates_raw, list) + has_event_candidates = has_event_candidates_list and len(event_candidates_raw) > 0 + has_zero_event_reason = bool(item.get("zero_event_reason")) + if event_candidates_raw is not None and not has_event_candidates_list: + malformed_item_refs.append(f"{item_ref}: event_candidates must be a list when present") + continue + if not has_event_candidates and not has_zero_event_reason: + malformed_item_refs.append(f"{item_ref}: requires non-empty event_candidates or zero_event_reason") + continue + if item.get("zero_event_reason"): + zero_event_evidence_indexes.append(evidence_index) + item_flags = item.get("doc_semantic_flags") if isinstance(item.get("doc_semantic_flags"), list) else [] + property_cluster_id = _text(item.get("property_cluster_id")) + if property_cluster_id: + asset_cluster_values.append(property_cluster_id) + for asset in item.get("affected_asset_cluster_ids") or []: + if _text(asset): + asset_cluster_values.append(str(asset)) + for seq, cand in enumerate(event_candidates_raw or [], 1): + if not isinstance(cand, dict): + malformed_item_refs.append(f"{item_ref}: event_candidates[{seq}] must be an object") + continue + cid = _candidate_id(cand, evidence_index, seq) + refs = cand.get("meeting_clause_refs") if isinstance(cand.get("meeting_clause_refs"), list) else [] + cand_flags = cand.get("doc_semantic_flags") if isinstance(cand.get("doc_semantic_flags"), list) else [] + candidate_flags = _uniq(item_flags + cand_flags) + record = { + "candidate_id": cid, + "source_evidence_index": _text(cand.get("source_evidence_index") or cand.get("source_evidence_index_proposed")) or evidence_index, + "title": item.get("title"), + "doc_type": item.get("doc_type"), + "event_or_state": cand.get("event_or_state"), + "event_kind": cand.get("event_kind"), + "event_subkind": cand.get("event_subkind"), + "action_type_candidate": cand.get("action_type_candidate"), + "action_summary": cand.get("action_summary"), + "participants": cand.get("participants") or {}, + "object_spec": cand.get("object_spec"), + "amount": cand.get("amount"), + "event_date": cand.get("event_date"), + "event_end_date": cand.get("event_end_date"), + "time_text": cand.get("time_text"), + "time_precision": cand.get("time_precision"), + "current_state_candidate": cand.get("current_state_candidate"), + "succession_candidate": cand.get("succession_candidate"), + "legal_effect_candidate": cand.get("legal_effect_candidate"), + "defense_candidate": cand.get("defense_candidate"), + "commercial_successor_candidates": cand.get("commercial_successor_candidates"), + "meeting_clause_refs": refs, + "actio_relevance_candidates": cand.get("actio_relevance_candidates"), + "identity_signature": cand.get("identity_signature"), + "property_cluster_id": property_cluster_id, + "affected_asset_cluster_ids": item.get("affected_asset_cluster_ids") or [], + "doc_semantic_flags": candidate_flags, + "fraction_ref": cand.get("fraction_ref"), + "confidence": cand.get("confidence"), + } + by_candidate_id[cid] = record + by_evidence_index.setdefault(evidence_index, []).append(cid) + for ref in refs: + by_meeting_clause_ref.setdefault(str(ref), []).append(cid) + if cand.get("event_or_state") == "state" or cand.get("current_state_candidate"): + state_candidate_ids.append(cid) + if cand.get("succession_candidate"): + succession_candidate_ids.append(cid) + if "mixed_contract_doc" in candidate_flags or cand.get("commercial_successor_candidates"): + mixed_contract_candidate_ids.append(cid) + if _text(cand.get("object_spec")): + asset_cluster_values.append(str(cand.get("object_spec"))) + + if malformed_item_refs: + raise RuntimeError(f"Malformed evidence_event_candidates document items: {malformed_item_refs[:20]}") + + return { + "schema_version": "event_candidate_map.v2", + "candidate_count": len(by_candidate_id), + "candidate_id_set": sorted(by_candidate_id.keys()), + "by_candidate_id": by_candidate_id, + "by_evidence_index": {k: _uniq(v) for k, v in by_evidence_index.items()}, + "by_meeting_clause_ref": {k: _uniq(v) for k, v in by_meeting_clause_ref.items()}, + "state_candidate_ids": _uniq(state_candidate_ids), + "succession_candidate_ids": _uniq(succession_candidate_ids), + "mixed_contract_candidate_ids": _uniq(mixed_contract_candidate_ids), + "zero_event_evidence_indexes": _uniq(zero_event_evidence_indexes), + "asset_cluster_values": _uniq([v for v in asset_cluster_values if _text(v)]), + } + + def _component_count(item: dict[str, Any], key: str) -> int: + val = item.get(key) + if isinstance(val, list): + return len(val) + if isinstance(val, dict): + return len(val) + return 1 if val not in (None, "", []) else 0 + + COMPONENT_DETAIL_PROJECTION = { + "loan_components": ["loan_component_id", "principal_amount", "loan_date", "due_date", + "agreed_interest_rate", "agreed_delay_damage_rate", "rate_text", + "rate_source_locator", "interest_accrual_start_hint"], + "secured_debt_components": ["agreed_interest_rate", "agreed_delay_damage_rate", "rate_text", + "rate_source_locator", "due_date"], + "notice_components": ["notice_id_proposed", "notice_type", "drafted_date", "dispatch_date", + "arrival_date", "received_date", "legal_effect_date_candidate", + "requires_receipt_for_effect", "receipt_evidence_refs", + "recipient_admission_text", "sender_candidates", "receiver_candidates"], + "registry_row_components": ["registry_ref", "fraction_ref", "fraction_display", + "fraction_numerator", "fraction_denominator", "receipt_no"], + "schedule_items": ["schedule_ref", "canonical_label_candidate", "usable_in_relief"], + } + + def _component_details(item: dict[str, Any]) -> dict[str, Any]: + details: dict[str, Any] = {} + for key, fields in COMPONENT_DETAIL_PROJECTION.items(): + rows = item.get(key) + if isinstance(rows, dict): + rows = [rows] + if not isinstance(rows, list): + continue + projected = [{field: row.get(field) for field in fields if field in row} for row in rows if isinstance(row, dict)] + projected = [p for p in projected if p] + if projected: + details[key] = projected + return details + + def _build_evidence_authority_map(evidence_obj: Any) -> dict[str, Any]: + items = _items(evidence_obj, "evidence_indexed") + if not items: + raise RuntimeError("evidence_indexed.json parse produced no evidence authority items") + by_evidence_index: dict[str, Any] = {} + by_doc_type: dict[str, list[str]] = {} + by_title_norm: dict[str, list[str]] = {} + semantic_flag_index: dict[str, list[str]] = {} + evidence_index_set: list[str] = [] + malformed_item_refs: list[str] = [] + duplicate_evidence_indexes: list[str] = [] + + component_keys = [ + "contract_components", "valuation_object", "legal_calculation_object", + "legal_calculation_objects", "claim_document_components", + "business_transfer_components", "registry_row_components", + "secured_debt_components", "valuation_rows", "mixed_contract_payment_semantics", + "loan_components", "valuation_matrix", "registry_row_role_map", + "defense_assertions", "admission_components", "defense_components", + "notice_components", "party_assertion_components", "schedule_items", + "registry_object_display", "succession_document_components", + ] + + for item_seq, item in enumerate(items, 1): + if not isinstance(item, dict): + malformed_item_refs.append(f"item#{item_seq}: non-object") + continue + evidence_index = _text(item.get("evidence_index") or item.get("evidence_index_proposed")) + if not evidence_index: + malformed_item_refs.append(f"item#{item_seq}: missing finalized evidence_index") + continue + if evidence_index in by_evidence_index: + duplicate_evidence_indexes.append(evidence_index) + continue + flags = item.get("doc_semantic_flags") if isinstance(item.get("doc_semantic_flags"), list) else [] + component_summary = {key: _component_count(item, key) for key in component_keys} + record = { + "evidence_index": evidence_index, + "doc_uid": item.get("doc_uid"), + "title": item.get("title"), + "title_normalized": item.get("title_normalized") or _norm_space(item.get("title")), + "doc_type": item.get("doc_type"), + "key_facts": item.get("key_facts") or [], + "key_dates": item.get("key_dates") or [], + "key_amounts": item.get("key_amounts") or [], + "key_parties": item.get("key_parties") or [], + "source_pointer": item.get("source_pointer") or {}, + "property_cluster_id": item.get("property_cluster_id"), + "doc_semantic_flags": flags, + "multi_event_registry": item.get("multi_event_registry"), + "component_summary": component_summary, + "component_details": _component_details(item), + } + by_evidence_index[evidence_index] = record + evidence_index_set.append(evidence_index) + by_doc_type.setdefault(str(item.get("doc_type") or "unknown"), []).append(evidence_index) + title_key = _norm_key(record["title_normalized"]) + if title_key: + by_title_norm.setdefault(title_key, []).append(evidence_index) + for flag in flags: + semantic_flag_index.setdefault(str(flag), []).append(evidence_index) + + if malformed_item_refs: + raise RuntimeError(f"Malformed evidence_indexed authority items: {malformed_item_refs[:20]}") + if duplicate_evidence_indexes: + raise RuntimeError(f"Duplicate evidence_index values in evidence_indexed.json: {_uniq(duplicate_evidence_indexes)}") + + return { + "schema_version": "evidence_authority_map.v2", + "evidence_index_set": _sort_evidence_indexes(_uniq(evidence_index_set)), + "by_evidence_index": by_evidence_index, + "by_doc_type": {k: _sort_evidence_indexes(_uniq(v)) for k, v in by_doc_type.items()}, + "by_title_norm": {k: _sort_evidence_indexes(_uniq(v)) for k, v in by_title_norm.items()}, + "semantic_flag_index": {k: _sort_evidence_indexes(_uniq(v)) for k, v in semantic_flag_index.items()}, + } + + def _sort_evidence_indexes(values: list[str]) -> list[str]: + def key(v: str) -> tuple[int, str]: + m = re.search(r"E-(\d+)", str(v)) + return (int(m.group(1)) if m else 10**9, str(v)) + return sorted(values, key=key) + + def _split_examples(value: str) -> list[str]: + parts = re.split(r"[,,、]| / |/|ㆍ|·", value or "") + return [_norm_space(p) for p in parts if _norm_space(p)] + + def _build_juristic_act_map(markdown: str) -> dict[str, Any]: + categories: dict[str, Any] = {} + example_to_gubun: dict[str, list[str]] = {} + for line in markdown.splitlines(): + if "|" not in line: + continue + cells = [_norm_space(c) for c in line.strip().strip("|").split("|")] + if len(cells) < 3: + continue + if cells[0] in ("구분", "---") or set(cells[0]) <= {"-"}: + continue + gubun, content, examples = cells[0], cells[1], cells[2] + if not gubun or gubun.startswith("-"): + continue + example_list = _split_examples(examples) + categories[gubun] = {"content": content, "examples": example_list} + for ex in example_list: + example_to_gubun.setdefault(_norm_key(ex), []).append(gubun) + + axis_groups = { + "structure": ["단독행위", "계약행위", "합동행위"], + "form": ["요식(要式)행위", "불요식행위"], + "time_effect": ["생전 행위", "사후(또는 사인) 행위"], + "legal_object": ["채권행위", "물권행위", "준물권행위"], + "contribution": ["출연행위", "비출연행위"], + "dependency": ["주(主)된 행위", "종(從)된 행위", "유인 행위", "무인 행위"], + "defect_or_condition": ["무효 행위", "취소가능한 법률 행위", "조건부 법률행위", "기한부 법률행위"], + "controversy": ["견해대립있는 행위"], + } + + return { + "schema_version": "juristic_act_taxonomy_map.v1", + "categories": categories, + "example_to_gubun": {k: _uniq(v) for k, v in example_to_gubun.items()}, + "axis_groups": axis_groups, + "source_digest_sha256": _stable_hash(markdown), + } + + def _validate_juristic_act_map(juristic_map: dict[str, Any]) -> None: + categories = juristic_map.get("categories") or {} + example_to_gubun = juristic_map.get("example_to_gubun") or {} + required = ["단독행위", "계약행위", "채권행위", "물권행위"] + missing = [name for name in required if name not in categories] + if not categories or not example_to_gubun: + raise RuntimeError("JuristicAct taxonomy parse produced empty categories or examples") + if missing: + raise RuntimeError(f"JuristicAct taxonomy missing required categories: {missing}") + + def _collect_asset_clusters(meeting_map: dict[str, Any], event_map: dict[str, Any], evidence_map: dict[str, Any]) -> list[str]: + values: list[str] = [] + asset_hint_re = re.compile(r"([가-힣A-Za-z0-9·ㆍ() \-]{1,30}(?:대지|건물|상가|빌라|임야|부동산|지분|영업|상호|채권|대금))") + for clause in meeting_map.get("by_clause_id", {}).values(): + text = str(clause.get("text") or "") + values.extend([_norm_space(m.group(1)) for m in asset_hint_re.finditer(text)]) + values.extend(event_map.get("asset_cluster_values") or []) + for ev in evidence_map.get("by_evidence_index", {}).values(): + if _text(ev.get("property_cluster_id")): + values.append(str(ev.get("property_cluster_id"))) + for fact in ev.get("key_facts") or []: + values.extend([_norm_space(m.group(1)) for m in asset_hint_re.finditer(str(fact))]) + return _uniq([v for v in values if _text(v)]) + + def _build_global_hazard_seed(meeting_map: dict[str, Any], event_map: dict[str, Any], evidence_map: dict[str, Any]) -> dict[str, Any]: + evidence_index_set = evidence_map.get("evidence_index_set") or [] + current_state_ids = event_map.get("state_candidate_ids") or [] + succession_candidate_ids = event_map.get("succession_candidate_ids") or [] + mixed_contract_candidate_ids = event_map.get("mixed_contract_candidate_ids") or [] + + def meeting_expression_records(clause_ids: list[str], tag_key: str) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + by_clause = meeting_map.get("by_clause_id", {}) + for cid in clause_ids: + clause = by_clause.get(cid) or {} + matched = (clause.get("tags") or {}).get(tag_key) or [] + out.append({ + "source": "client_meeting.md", + "clause_id": cid, + "local_clause_id": clause.get("local_clause_id"), + "section_key": clause.get("section_key"), + "matched_patterns": matched, + "text_excerpt": clause.get("text_excerpt") or _short_excerpt(clause.get("text")), + }) + return out + + def event_expression_records(candidate_ids: list[str]) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + by_candidate = event_map.get("by_candidate_id", {}) + for cid in candidate_ids: + cand = by_candidate.get(cid) or {} + text = " ".join(str(cand.get(k) or "") for k in ("action_summary", "event_kind", "event_subkind", "identity_signature")) + matched = _uniq( + _extract_keywords(text, CURRENT_STATE_PATTERNS) + + _extract_keywords(text, CONFLICT_PATTERNS) + + _extract_keywords(text, SUCCESSION_PATTERNS) + + _extract_keywords(text, MIXED_CONTRACT_PATTERNS) + ) + out.append({ + "source": "evidence_event_candidates.json", + "candidate_id": cid, + "source_evidence_index": cand.get("source_evidence_index"), + "event_or_state": cand.get("event_or_state"), + "event_kind": cand.get("event_kind"), + "matched_patterns": matched, + "text_excerpt": _short_excerpt(cand.get("action_summary") or cand.get("identity_signature")), + }) + return out + + conflict_candidate_ids: list[str] = [] + for cid, cand in event_map.get("by_candidate_id", {}).items(): + combined = " ".join( + str(cand.get(k) or "") + for k in ("action_summary", "event_kind", "event_subkind", "identity_signature") + ) + if _extract_keywords(combined, CONFLICT_PATTERNS): + conflict_candidate_ids.append(cid) + + succession_evidence_indexes = [] + mixed_contract_evidence_indexes = [] + for eidx, ev in evidence_map.get("by_evidence_index", {}).items(): + flags = ev.get("doc_semantic_flags") or [] + title_and_facts = " ".join([str(ev.get("title") or "")] + [str(x) for x in ev.get("key_facts") or []]) + if "succession_doc" in flags or _extract_keywords(title_and_facts, SUCCESSION_PATTERNS): + succession_evidence_indexes.append(eidx) + if "mixed_contract_doc" in flags or _extract_keywords(title_and_facts, MIXED_CONTRACT_PATTERNS): + mixed_contract_evidence_indexes.append(eidx) + + return { + "schema_version": "global_hazard_seed.v1", + "meeting_clause_ids": meeting_map.get("clause_order") or [], + "current_state_expressions": { + "meeting_clause_ids": meeting_map.get("current_state_clause_ids") or [], + "event_candidate_ids": current_state_ids, + "meeting_expressions": meeting_expression_records(meeting_map.get("current_state_clause_ids") or [], "current_state"), + "event_expressions": event_expression_records(current_state_ids), + }, + "conflict_pattern_expressions": { + "meeting_clause_ids": meeting_map.get("conflict_pattern_clause_ids") or [], + "event_candidate_ids": _uniq(conflict_candidate_ids), + "meeting_expressions": meeting_expression_records(meeting_map.get("conflict_pattern_clause_ids") or [], "conflict_pattern"), + "event_expressions": event_expression_records(_uniq(conflict_candidate_ids)), + }, + "asset_clusters": _collect_asset_clusters(meeting_map, event_map, evidence_map), + "succession_related_candidates": { + "meeting_clause_ids": meeting_map.get("succession_clause_ids") or [], + "event_candidate_ids": succession_candidate_ids, + "evidence_indexes": _sort_evidence_indexes(_uniq(succession_evidence_indexes)), + "meeting_expressions": meeting_expression_records(meeting_map.get("succession_clause_ids") or [], "succession"), + "event_expressions": event_expression_records(succession_candidate_ids), + }, + "mixed_contract_candidates": { + "meeting_clause_ids": meeting_map.get("mixed_contract_clause_ids") or [], + "event_candidate_ids": mixed_contract_candidate_ids, + "evidence_indexes": _sort_evidence_indexes(_uniq(mixed_contract_evidence_indexes)), + "meeting_expressions": meeting_expression_records(meeting_map.get("mixed_contract_clause_ids") or [], "mixed_contract"), + "event_expressions": event_expression_records(mixed_contract_candidate_ids), + }, + "evidence_index_set": evidence_index_set, + } + + def _read_optional_doc_once(doc_name: str) -> str | None: + try: + payload = _unwrap(_call("read_docs", {"doc_names": [doc_name]}), "read_docs_optional") + except Exception: + return None + if isinstance(payload, dict) and isinstance(payload.get("results"), list): + for item in payload["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + content = item.get("content") or item.get("text") or item.get("markdown") or item.get("raw") + if name and (str(name) == doc_name or str(name).endswith("/" + doc_name) or str(name).split("/")[-1] == doc_name.split("/")[-1]): + return content if isinstance(content, str) else json.dumps(content, ensure_ascii=False) + if isinstance(payload, dict) and doc_name in payload: + val = payload[doc_name] + return val if isinstance(val, str) else json.dumps(val, ensure_ascii=False) + if isinstance(payload, str) and payload.strip(): + return payload + return None + + def _compact_part1_soft_gate_handoff(existence_check: dict[str, Any], input_digests: dict[str, str], meeting_map: dict[str, Any], event_map: dict[str, Any], evidence_map: dict[str, Any]) -> dict[str, Any]: + found_names = existence_check.get("found_names") or [] + base = { + "schema_version": "stage1_part1_soft_gate_handoff.v1", + "status": "ABSENT", + "stage2_auto_progression_allowed": True, + "review_gate_count": 0, + "highest_review_severity": "PASS", + "review_items_compact": [], + "routing_index": {}, + "lawyer_workflow_queues": {}, + "dropped_review_items": [], + "usage_rules": { + "not_factual_authority": True, + "must_not_create_fact_without_stage_a_source": True, + "must_preserve_as_needs_review_if_used": True, + "must_not_override_stage_e_final_progression": True, + "must_not_override_stage_f_single_writer_gate": True, + }, + } + if not _doc_name_matches(found_names, PART1_SOFT_GATE_HANDOFF_FILE): + return base + raw = _read_optional_doc_once(PART1_SOFT_GATE_HANDOFF_FILE) + if not raw: + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": "sidecar listed but unreadable"}) + return base + try: + sidecar = json.loads(raw.strip()) + except Exception as exc: + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": f"sidecar JSON parse failed: {exc}"}) + return base + if not isinstance(sidecar, dict) or sidecar.get("schema_version") != "stage1_part1_soft_gate_handoff.v1": + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": "wrong or missing schema_version"}) + return base + if sidecar.get("handoff_status") == "BLOCKED" or sidecar.get("stage2_auto_progression_allowed") is False: + raise RuntimeError("Part 1 soft gate handoff is BLOCKED; Part 2-6 cannot proceed") + guard = sidecar.get("digest_guard") if isinstance(sidecar.get("digest_guard"), dict) else {} + invalid_guard_fields = [] + stale = [] + for guard_key, doc_name in [("evidence_indexed_sha256", "evidence_indexed.json"), ("evidence_event_candidates_sha256", "evidence_event_candidates.json")]: + val = guard.get(guard_key) + if not isinstance(val, str) or not re.fullmatch(r"[0-9a-f]{64}", val): + invalid_guard_fields.append(guard_key) + elif val != input_digests.get(doc_name): + stale.append(guard_key) + if invalid_guard_fields: + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": "digest_guard missing real sha256", "fields": invalid_guard_fields}) + return base + if stale: + base["status"] = "IGNORED_STALE" + base["dropped_review_items"].append({"reason": "digest_guard mismatch", "fields": stale}) + return base + evidence_set = set(evidence_map.get("evidence_index_set") or []) + event_set = set(event_map.get("candidate_id_set") or []) + meeting_set = set(meeting_map.get("clause_order") or []) + compact = [] + dropped = [] + for idx, item in enumerate(sidecar.get("review_items") or []): + if len(compact) >= 40: + dropped.append({"index": idx, "reason": "review_items_compact max 40 reached"}) + continue + if not isinstance(item, dict): + dropped.append({"index": idx, "reason": "non-object review item"}) + continue + refs = item.get("source_refs") if isinstance(item.get("source_refs"), dict) else {} + bad_e = [x for x in refs.get("evidence_indexes", []) if x not in evidence_set] + bad_ev = [x for x in refs.get("event_candidate_ids", []) if x not in event_set] + bad_m = [x for x in refs.get("meeting_clause_refs", []) if x not in meeting_set] + if bad_e or bad_ev or bad_m: + dropped.append({"review_id": item.get("review_id"), "reason": "source_refs membership failed", "bad_evidence_indexes": bad_e, "bad_event_candidate_ids": bad_ev, "bad_meeting_clause_refs": bad_m}) + continue + compact.append({ + "review_id": item.get("review_id"), + "severity": item.get("severity"), + "issue_type": item.get("issue_type"), + "lawyer_workflow_role": item.get("lawyer_workflow_role"), + "domain_hints": item.get("domain_hints") or [], + "target_stage_hints": item.get("target_stage_hints") or [], + "target_task_hints": item.get("target_task_hints") or [], + "source_refs": refs, + "candidate_use_policy": item.get("candidate_use_policy"), + "review_instruction": item.get("review_instruction"), + "negative_instruction": item.get("negative_instruction"), + }) + summary = sidecar.get("review_gate_summary") if isinstance(sidecar.get("review_gate_summary"), dict) else {} + base.update({ + "status": sidecar.get("handoff_status") if sidecar.get("handoff_status") in {"READY_WITH_REVIEW", "READY_NO_REVIEW"} else "READY_WITH_REVIEW", + "stage2_auto_progression_allowed": sidecar.get("stage2_auto_progression_allowed") is not False, + "review_gate_count": len(compact), + "highest_review_severity": summary.get("highest_review_severity") or ("HARD_WARNING" if any(i.get("severity") == "HARD_WARNING" for i in compact) else ("SOFT_WARNING" if compact else "PASS")), + "review_items_compact": compact, + "routing_index": sidecar.get("routing_index") if isinstance(sidecar.get("routing_index"), dict) else {}, + "lawyer_workflow_queues": sidecar.get("lawyer_workflow_queues") if isinstance(sidecar.get("lawyer_workflow_queues"), dict) else {}, + "dropped_review_items": dropped, + }) + return base + + + + # ==================== A0 domain slicing (B0 x5 통합; 3등급 라우팅 = 개선전략서 X-1) ==================== + A0_DOMAIN_CONFIGS = { + "B1_Money_Successor": { + "domain_id": "B1_Money_Successor", + "domain_label": "금전채권 및 상호속용 책임", + "allowed_legal_effect_bo_types": [ + "money_claim_chain_BO", + "commercial_successor_liability_chain_BO", + "liability_group_hint_BO", + ], + "terms": [ + "대여금", "차용", "매매대금", "물품대금", "보증금", "정산금", "공사대금", + "원금", "이자", "지연손해금", "변제기", "이행기", "일부변제", "미변제", + "채무승인", "지급각서", "확인서", "최고", "통지", "상호속용", "영업양도", + "양수인", "채무불승계", "business_transfer", "loan", "payment", "admission", + "notice", "claim_document", "mixed_contract_payment", + ], + "meeting_sources": ["mixed_contract_clause_ids", "conflict_pattern_clause_ids"], + "event_sources": ["mixed_contract_candidate_ids"], + "hazard_keys": ["mixed_contract_candidates", "conflict_pattern_expressions", "asset_clusters"], + "taxonomy_axes": ["legal_object", "structure", "dependency"], + }, + "B2_Secured_Registry": { + "domain_id": "B2_Secured_Registry", + "domain_label": "담보채무, 변제충당, 등기무효, 경매", + "allowed_legal_effect_bo_types": [ + "secured_debt_chain_BO", + "payment_allocation_and_mortgage_extinguishment_BO", + "auction_invalidity_BO", + "derivative_bank_mortgage_invalidity_BO", + ], + "terms": [ + "근저당", "저당", "담보", "피담보", "채무액", "변제", "변제충당", + "법정변제충당", "지분", "말소", "소유권이전등기", "등기", "등기원인", + "등기무효", "경매", "담보권실행경매", "경매개시", "매각", "배당", + "후행 담보권", "registry", "mortgage", "secured", "auction", "dividend", + "payment_allocation", "registry_row", "legal_calculation", + ], + "meeting_sources": ["conflict_pattern_clause_ids", "mixed_contract_clause_ids"], + "event_sources": [], + "hazard_keys": ["conflict_pattern_expressions", "asset_clusters", "mixed_contract_candidates"], + "taxonomy_axes": ["legal_object", "form", "dependency", "defect_or_condition"], + }, + "B3_Land_Valuation": { + "domain_id": "B3_Land_Valuation", + "domain_label": "토지 사용이익, 점유, 사용수익, 가치평가", + "allowed_legal_effect_bo_types": ["land_use_gain_timeline_BO", "liability_overlap_BO"], + "terms": [ + "토지", "건물", "부지", "인도", "점유", "사용", "사용수익", "현재 사용", + "현재 수익", "거주", "운영", "임대", "차임", "감정", "시가", "가액", + "미등기 건물", "사실상 처분권", "부진정연대", "valuation", "valuation_object", + "valuation_rows", "valuation_matrix", "registry_object_display", "possession", "land", "building", + ], + "meeting_sources": ["current_state_clause_ids", "conflict_pattern_clause_ids", "mixed_contract_clause_ids"], + "event_sources": ["state_candidate_ids"], + "hazard_keys": ["current_state_expressions", "asset_clusters", "mixed_contract_candidates", "conflict_pattern_expressions"], + "taxonomy_axes": ["legal_object", "contribution", "structure"], + }, + "B4_Actio": { + "domain_id": "B4_Actio", + "domain_label": "사해행위취소, 피보전채권, 책임재산, 가액배상", + "allowed_legal_effect_bo_types": [ + "preserved_claim_bundle_BO", + "fraudulent_transfer_BO", + "encumbrance_timeline_BO", + "valuation_matrix_BO", + "remedy_mode_decision_BO", + "value_compensation_cap_BO", + "defense_rebuttal_matrix_BO", + "actio_balance_sheet_BO", + ], + "terms": [ + "사해행위", "피보전채권", "채무초과", "책임재산", "공동담보", "매매", + "증여", "담보설정", "처분행위", "수익자", "전득자", "사해성", "악의", + "선의", "원상회복", "가액배상", "선순위담보", "말소", "가액 산정", + "항변", "actio", "fraudulent", "transfer", "preserved_claim", "encumbrance", + "valuation", "remedy", "compensation", "defense", "claim_document", + ], + "meeting_sources": ["conflict_pattern_clause_ids", "mixed_contract_clause_ids", "current_state_clause_ids", "succession_clause_ids"], + "event_sources": [], + "hazard_keys": ["conflict_pattern_expressions", "asset_clusters", "mixed_contract_candidates", "current_state_expressions", "succession_related_candidates"], + "taxonomy_axes": ["legal_object", "structure", "contribution", "defect_or_condition", "dependency", "form"], + }, + "B5_Succession_Notice_Lien_Asset_Defense": { + "domain_id": "B5_Succession_Notice_Lien_Asset_Defense", + "domain_label": "상속, 통지, 유치권, 목적물 특정, 항변", + "allowed_legal_effect_bo_types": [ + "succession_resolution_BO", + "notice_lifecycle_BO", + "lien_state_timeline_BO", + "asset_alias_BO", + "defense_rebuttal_BO", + "delivery_use_gain_claim_group_BO", + ], + "terms": [ + "사망", "상속", "상속포기", "배우자", "직계비속", "가족관계", "피상속인", + "상속인", "통지", "내용증명", "도달", "수령", "최고", "유치권", "점유", + "인도", "거절", "별지목록", "등기부 표시", "목적물", "동일성", "피고 답변", + "항변", "권리남용", "신의칙", "동시이행", "succession", "notice", "lien", + "possession", "asset", "defense", "party_assertion", "registry_object_display", + ], + "meeting_sources": ["succession_clause_ids", "current_state_clause_ids", "conflict_pattern_clause_ids", "mixed_contract_clause_ids"], + "event_sources": ["succession_candidate_ids", "state_candidate_ids"], + "hazard_keys": ["succession_related_candidates", "current_state_expressions", "conflict_pattern_expressions", "asset_clusters", "mixed_contract_candidates"], + "taxonomy_axes": ["structure", "time_effect", "legal_object", "defect_or_condition"], + }, + } + + # 3등급 라우팅 구조화 셀렉터 (개선전략서 §5.4 X-1): primary 승격은 구조화 키 + # (event_kind/event_subkind/component_summary)와 named-component strong term만 가능. + # broad term(등기·변제·항변·점유·목적물 등)의 단독 blob 매칭은 support 등급에 머문다. + A0_STRUCTURED_ROUTING = { + "B1_Money_Successor": { + "event_kinds": ["채권발생", "변제", "채권관리", "영업양도", "사업자등록"], + "event_subkinds": ["contract_formation", "대가수수", "payment_consideration", + "debt_acknowledgement", "new_due_date", "underlying_claim_arising", + "demand_notice_dispatch", "demand_notice_arrival", + "commercial_successor_liability_candidate", "defense_assertion_setoff", + "defense_assertion_benefit_applied_to_secured_claim"], + "evidence_component_keys": ["claim_document_components", "loan_components", + "business_transfer_components", "admission_components", + "mixed_contract_payment_semantics", "notice_components", "defense_assertions"], + "strong_terms": ["차용", "금전소비대차", "대여", "공사대금", "물품대금", "매매대금", + "지급각서", "이행각서", "채무승인", "상호속용", "영업양도", "사업자등록", "상계", + "답변서", "도급"], + }, + "B2_Secured_Registry": { + "event_kinds": ["담보설정", "담보말소", "경매개시", "경매매각", "소유권이전", "변제", + "가압류/압류", "증여"], + "event_subkinds": ["secured_debt_creation", "mortgage_extinguishment_by_fraction", + "auction_application_after_extinguishment", "auction_sale_and_registry_transfer", + "소유권보존", "매매", "defense_assertion_benefit_applied_to_secured_claim", + "defense_assertion_setoff", "defense_assertion_subsequent_owner_no_extinction_right"], + "evidence_component_keys": ["registry_row_components", "secured_debt_components", + "legal_calculation_object", "legal_calculation_objects", "registry_object_display", + "registry_row_role_map", "loan_components", "claim_document_components"], + "strong_terms": ["근저당", "저당권", "말소", "소유권이전등기", "등기부", "등기사항", + "경매", "배당", "변제충당", "법정변제충당", "채권최고액", "유치권", "상계", + "답변서", "차용", "도급"], + }, + "B3_Land_Valuation": { + "event_kinds": ["가치평가", "임대차", "인도", "점유", "사업자등록"], + "event_subkinds": ["ongoing_physical_possession", "해제/해지의사표시", "책임부인", + "defense_assertion_benefit_applied_to_secured_claim"], + "evidence_component_keys": ["valuation_object", "valuation_rows", "valuation_matrix", + "contract_components"], + "strong_terms": ["감정", "임료", "시세", "사용수익", "사용이익", "명도", "임대차", + "매매계약", "사업자등록", "답변서", "해제", "차임"], + }, + "B4_Actio": { + "event_kinds": ["처분", "증여", "소유권이전", "담보설정", "담보말소", "채권발생", + "가치평가", "가압류/압류"], + "event_subkinds": ["대가수수", "매매", "mortgage_extinguishment_by_fraction", + "underlying_claim_arising", "secured_debt_creation", "defense_assertion_setoff"], + "evidence_component_keys": ["claim_document_components", "loan_components", + "valuation_object", "valuation_matrix", "registry_row_components", + "defense_assertions"], + "strong_terms": ["사해", "피보전", "채무초과", "책임재산", "명의신탁", "수익자", + "전득자", "가액배상", "증여", "감정", "답변서"], + }, + "B5_Succession_Notice_Lien_Asset_Defense": { + "event_kinds": ["상속포기", "상속개시", "통지", "도달", "항변", "점유", "인도", + "사업자등록", "판결/결정", "영업양도"], + "event_subkinds": ["succession_renunciation_filing", "succession_renunciation_acceptance", + "demand_notice_dispatch", "demand_notice_arrival", "해제/해지의사표시", + "해제/해지 통지", "책임부인", "ongoing_physical_possession", + "defense_assertion_setoff", "defense_assertion_benefit_applied_to_secured_claim", + "defense_assertion_subsequent_owner_no_extinction_right"], + "evidence_component_keys": ["succession_document_components", "notice_components", + "defense_assertions", "schedule_items", "business_transfer_components", + "contract_components", "registry_row_components"], + "strong_terms": ["상속", "유치권", "소멸청구", "가족관계", "사업자등록", "별지", + "상호속용", "해제", "심판", "최고"], + }, + } + + A0_MEETING_FIELDS = ["clause_id", "local_clause_id", "section_key", "section_label", "major_no", + "sub_no", "heading", "line_start", "line_end", "text_excerpt", "tags"] + A0_EVENT_FIELDS = ["candidate_id", "source_evidence_index", "title", "doc_type", "event_or_state", "event_kind", "event_subkind", "action_type_candidate", "action_summary", "participants", "object_spec", "amount", "event_date", "event_end_date", "time_text", "time_precision", "current_state_candidate", "succession_candidate", "legal_effect_candidate", "defense_candidate", "commercial_successor_candidates", "meeting_clause_refs", "actio_relevance_candidates", "identity_signature", "property_cluster_id", "affected_asset_cluster_ids", "doc_semantic_flags", "confidence", "fraction_ref"] + A0_EVIDENCE_FIELDS = ["evidence_index", "doc_uid", "title", "title_normalized", "doc_type", "key_facts", "key_dates", "key_amounts", "key_parties", "source_pointer", "property_cluster_id", "doc_semantic_flags", "multi_event_registry", "component_summary", "component_details"] + A0_STAGE_A_PATH = "stage1_tmp/task_c_bo/stage_a_context.json" + A0_MANIFEST_PATH = "stage1_tmp/task_c_bo/source_universe_manifest.json" + A0_SLICE_PATHS = {dom: f"stage1_tmp/task_c_bo/domain_slices/{dom.split('_')[0]}.json" for dom in A0_DOMAIN_CONFIGS} + A0_BROADNESS_WARN_RATIO = 0.6 + + def _a0_write(path, content): + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def _a0_natural_key(value): + return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", str(value))] + + def _a0_sorted_ids(values): + return sorted([str(v) for v in values if str(v)], key=_a0_natural_key) + + def _a0_list(value): + return value if isinstance(value, list) else [] + + def _a0_blob(value): + return json.dumps(value, ensure_ascii=False, sort_keys=True).lower() + + def _a0_matches(value, terms): + haystack = _a0_blob(value) + return any(str(term).lower() in haystack for term in terms if str(term).strip()) + + def _a0_copy(source, fields): + return {f: source.get(f) for f in fields if f in source} + + def _a0_matched_domains(record): + return [d for d, cfg in A0_DOMAIN_CONFIGS.items() if _a0_matches(record, cfg["terms"])] + + def _a0_event_primary(record, routing): + if str(record.get("event_kind")) in routing["event_kinds"]: + return True + if str(record.get("event_subkind")) in routing["event_subkinds"]: + return True + return _a0_matches(record, routing["strong_terms"]) + + def _a0_evidence_primary(record, routing): + summary = record.get("component_summary") or {} + if any(summary.get(key) for key in routing["evidence_component_keys"]): + return True + return _a0_matches(record, routing["strong_terms"]) + + def _a0_clause_primary(record, routing): + return _a0_matches(record, routing["strong_terms"]) + + def _a0_select(stage_a, config): + """3등급 라우팅(X-1): 구조화 키/named-component 매칭 = primary, + broad term 단독 매칭·closure 유입 = support, 잔여 참조 = review_only(id만).""" + routing = A0_STRUCTURED_ROUTING[config["domain_id"]] + meeting_map = stage_a.get("meeting_clause_map") or {} + event_map = stage_a.get("event_candidate_map") or {} + evidence_map = stage_a.get("evidence_authority_map") or {} + by_clause = meeting_map.get("by_clause_id") or {} + by_event = event_map.get("by_candidate_id") or {} + by_evidence = evidence_map.get("by_evidence_index") or {} + + # 1) primary: 구조화 키(event_kind/event_subkind/component_summary) + named-component strong term + # + hazard 유래 구조화 id 목록(meeting_sources/event_sources). broad term 단독으로는 승격 불가. + primary = {"clause": set(), "event": set(), "evidence": set()} + for key in config["meeting_sources"]: + for value in _a0_list(meeting_map.get(key)): + if str(value) in by_clause: + primary["clause"].add(str(value)) + for key in config["event_sources"]: + for value in _a0_list(event_map.get(key)): + if str(value) in by_event: + primary["event"].add(str(value)) + for cid, record in by_clause.items(): + if _a0_clause_primary(record, routing): + primary["clause"].add(str(cid)) + for eid, record in by_event.items(): + if _a0_event_primary(record, routing): + primary["event"].add(str(eid)) + for vid, record in by_evidence.items(): + if _a0_evidence_primary(record, routing): + primary["evidence"].add(str(vid)) + + # 2) support: broad term 매칭(비-primary) + primary의 source-ref closure 유입분 + clause_ids = set(primary["clause"]) + event_ids = set(primary["event"]) + evidence_ids = set(primary["evidence"]) + for cid, record in by_clause.items(): + if _a0_matches(record, config["terms"]): + clause_ids.add(str(cid)) + for eid, record in by_event.items(): + if _a0_matches(record, config["terms"]): + event_ids.add(str(eid)) + for vid, record in by_evidence.items(): + if _a0_matches(record, config["terms"]): + evidence_ids.add(str(vid)) + for _ in range(2): + for evidence_id in list(evidence_ids): + event_ids.update(str(v) for v in _a0_list((event_map.get("by_evidence_index") or {}).get(evidence_id))) + for clause_id in list(clause_ids): + event_ids.update(str(v) for v in _a0_list((event_map.get("by_meeting_clause_ref") or {}).get(clause_id))) + for event_id in list(event_ids): + event = by_event.get(event_id) or {} + if event.get("source_evidence_index"): + evidence_ids.add(str(event.get("source_evidence_index"))) + for ref in _a0_list(event.get("meeting_clause_refs")): + clause_ids.add(str(ref)) + + clause_ids = {v for v in clause_ids if v in by_clause} + event_ids = {v for v in event_ids if v in by_event} + evidence_ids = {v for v in evidence_ids if v in by_evidence} + support = {"clause": clause_ids - primary["clause"], "event": event_ids - primary["event"], + "evidence": evidence_ids - primary["evidence"]} + # 3) review_only: universe 잔여 참조 (id만 — 존재 확인용) + review_only = {"clause": set(by_clause) - clause_ids, "event": set(by_event) - event_ids, + "evidence": set(by_evidence) - evidence_ids} + + def _label(source, item_id, keys): + rec = source.get(item_id) or {} + for k in keys: + v = rec.get(k) + if v: + return _short_excerpt(v, 80) + return item_id + + shared = [] + for item_type, ids_all, source in [("meeting_clause", clause_ids, by_clause), + ("event_candidate", event_ids, by_event), + ("evidence", evidence_ids, by_evidence)]: + for item_id in _a0_sorted_ids(ids_all): + domains = _a0_matched_domains(source.get(item_id) or {}) + if len(domains) > 1: + shared.append({"item_type": item_type, "id": item_id, "matched_domain_ids": domains}) + + support_refs = ( + [{"item_type": "meeting_clause", "id": i, "label": _label(by_clause, i, ["heading", "text_excerpt"])} for i in _a0_sorted_ids(support["clause"])] + + [{"item_type": "event_candidate", "id": i, "label": _label(by_event, i, ["action_summary", "title"])} for i in _a0_sorted_ids(support["event"])] + + [{"item_type": "evidence", "id": i, "label": _label(by_evidence, i, ["title"])} for i in _a0_sorted_ids(support["evidence"])] + ) + review_only_refs = ( + [{"item_type": "meeting_clause", "id": i} for i in _a0_sorted_ids(review_only["clause"])] + + [{"item_type": "event_candidate", "id": i} for i in _a0_sorted_ids(review_only["event"])] + + [{"item_type": "evidence", "id": i} for i in _a0_sorted_ids(review_only["evidence"])] + ) + return { + "meeting_clause_ids": _a0_sorted_ids(clause_ids), + "event_candidate_ids": _a0_sorted_ids(event_ids), + "evidence_indexes": _a0_sorted_ids(evidence_ids), + "primary": primary, + "selected_meetings": [_a0_copy(by_clause[v], A0_MEETING_FIELDS) for v in _a0_sorted_ids(primary["clause"])], + "selected_events": [_a0_copy(by_event[v], A0_EVENT_FIELDS) for v in _a0_sorted_ids(primary["event"])], + "selected_evidence": [_a0_copy(by_evidence[v], A0_EVIDENCE_FIELDS) for v in _a0_sorted_ids(primary["evidence"])], + "support_refs": support_refs, + "review_only_refs": review_only_refs, + "shared": shared, + } + + def _a0_taxonomy_subset(stage_a, config): + taxonomy = stage_a.get("juristic_act_taxonomy_map") or {} + axes = taxonomy.get("axis_groups") or {} + return { + "schema_version": taxonomy.get("schema_version"), + "categories": taxonomy.get("categories") or {}, + "example_to_gubun": taxonomy.get("example_to_gubun") or {}, + "axis_groups": {key: axes.get(key) for key in config["taxonomy_axes"] if key in axes}, + "source_digest_sha256": taxonomy.get("source_digest_sha256"), + } + + def _a0_hazard_subset(stage_a, config): + hazard = stage_a.get("global_hazard_seed") or {} + return {key: hazard.get(key) for key in config["hazard_keys"] if key in hazard} + + def _a0_part1_review(stage_a, selected, config): + handoff = stage_a.get("part1_soft_gate_handoff") or {} + guard = { + "schema_version": handoff.get("schema_version"), + "status": handoff.get("status"), + "stage2_auto_progression_allowed": handoff.get("stage2_auto_progression_allowed"), + "usage_rules": handoff.get("usage_rules") or {}, + } + se, sev, sm = set(selected["evidence_indexes"]), set(selected["event_candidate_ids"]), set(selected["meeting_clause_ids"]) + relevant = [] + for item in _a0_list(handoff.get("review_items_compact")): + refs = item.get("source_refs") if isinstance(item, dict) else {} + domain_text = _a0_blob({ + "domain_hints": item.get("domain_hints"), + "target_task_hints": item.get("target_task_hints"), + "issue_type": item.get("issue_type"), + "review_instruction": item.get("review_instruction"), + }) + ref_hit = bool( + se.intersection(str(v) for v in _a0_list((refs or {}).get("evidence_indexes"))) + or sev.intersection(str(v) for v in _a0_list((refs or {}).get("event_candidate_ids"))) + or sm.intersection(str(v) for v in _a0_list((refs or {}).get("meeting_clause_refs"))) + ) + if ref_hit or any(str(term).lower() in domain_text for term in config["terms"]): + relevant.append(item) + return guard, relevant + + def _a0_emit(stage_a_context): + import hashlib as _hl + _a0_write(A0_STAGE_A_PATH, json.dumps({"stage_a_context": stage_a_context}, ensure_ascii=False, indent=2)) + meeting_map = stage_a_context.get("meeting_clause_map") or {} + event_map = stage_a_context.get("event_candidate_map") or {} + evidence_map = stage_a_context.get("evidence_authority_map") or {} + manifest = { + "schema_version": "source_universe_manifest.v1", + "input_digests_sha256": stage_a_context.get("input_digests_sha256") or {}, + "evidence_index_set": evidence_map.get("evidence_index_set") or [], + "event_candidate_ids": event_map.get("candidate_id_set") or [], + "meeting_clause_ids": meeting_map.get("clause_order") or [], + "counts": { + "evidence": len(evidence_map.get("evidence_index_set") or []), + "event": len(event_map.get("candidate_id_set") or []), + "meeting": len(meeting_map.get("clause_order") or []), + }, + } + _a0_write(A0_MANIFEST_PATH, json.dumps(manifest, ensure_ascii=False, indent=2)) + digests_summary = _hl.sha256(json.dumps(stage_a_context.get("input_digests_sha256") or {}, + ensure_ascii=False, sort_keys=True).encode("utf-8")).hexdigest() + worker_contract = stage_a_context.get("stage_b_worker_contract") or {} + universe_events = len(event_map.get("candidate_id_set") or []) or 1 + universe_evidence = len(evidence_map.get("evidence_index_set") or []) or 1 + routing_summary = {} + for domain_id, config in A0_DOMAIN_CONFIGS.items(): + selected = _a0_select(stage_a_context, config) + part1_guard, part1_items = _a0_part1_review(stage_a_context, selected, config) + primary = selected["primary"] + broadness = (len(primary["event"]) + len(primary["evidence"])) / (universe_events + universe_evidence) + warnings = ["ROUTING_BROADNESS_WARNING"] if broadness > A0_BROADNESS_WARN_RATIO else [] + slice_obj = { + "stage_b_domain_slice": { + "schema_version": "task_c_bo_stage_b_domain_slice.v1", + "stage": "Task_C_BO.Stage_B0.DomainInputRouter", + "router_task": "Task_C_BO_A0_context_and_domain_slice_compiler", + "router_status": "READY", + "domain_id": domain_id, + "domain_label": config["domain_label"], + "allowed_legal_effect_bo_types": config["allowed_legal_effect_bo_types"], + "source_stage_a_schema_version": stage_a_context.get("schema_version"), + "source_stage_a_status": stage_a_context.get("status"), + "source_stage_a_created_at_utc": stage_a_context.get("created_at_utc"), + "input_digests_sha256": stage_a_context.get("input_digests_sha256") or {}, + "slice_guard_echo": { + "source_stage_a_created_at_utc": stage_a_context.get("created_at_utc"), + "input_digests_summary": digests_summary, + }, + "stage_b_worker_contract": { + "context_is_read_only": worker_contract.get("context_is_read_only"), + "bo_id_finalization_allowed": worker_contract.get("bo_id_finalization_allowed"), + "candidate_generation_allowed": worker_contract.get("candidate_generation_allowed"), + "must_use_evidence_index_set": worker_contract.get("must_use_evidence_index_set") or [], + }, + "source_universe": { + "meeting_clause_ids": meeting_map.get("clause_order") or [], + "event_candidate_ids": event_map.get("candidate_id_set") or [], + "evidence_indexes": evidence_map.get("evidence_index_set") or [], + }, + "selected_meeting_clauses": selected["selected_meetings"], + "selected_event_candidates": selected["selected_events"], + "selected_evidence_authority": selected["selected_evidence"], + "support_refs": selected["support_refs"], + "review_only_refs": selected["review_only_refs"], + "juristic_act_taxonomy_subset": _a0_taxonomy_subset(stage_a_context, config), + "hazard_subset": _a0_hazard_subset(stage_a_context, config), + "part1_review_guard": part1_guard, + "part1_review_items_relevant": part1_items, + "shared_or_ambiguous_items": selected["shared"], + "routing_audit": { + "primary_ids": { + "meeting_clause_ids": _a0_sorted_ids(primary["clause"]), + "event_candidate_ids": _a0_sorted_ids(primary["event"]), + "evidence_indexes": _a0_sorted_ids(primary["evidence"]), + }, + "support_ids": [r["id"] for r in selected["support_refs"]], + "review_only_ids": [r["id"] for r in selected["review_only_refs"]], + "selected_meeting_clause_ids": selected["meeting_clause_ids"], + "selected_event_candidate_ids": selected["event_candidate_ids"], + "selected_evidence_indexes": selected["evidence_indexes"], + "multi_domain_item_refs": selected["shared"], + "unrouted_but_preserved_refs": [], + "broadness_ratio": round(broadness, 4), + "warnings": warnings, + "selection_basis": "deterministic keyword/flag/doc_type routing; closure-joined items demoted to support tier", + "field_policy": "primary records are full compact authority projections; support items are id+label refs only.", + }, + } + } + _a0_write(A0_SLICE_PATHS[domain_id], json.dumps(slice_obj, ensure_ascii=False, indent=2)) + routing_summary[domain_id] = { + "primary": {"meeting": len(primary["clause"]), "event": len(primary["event"]), "evidence": len(primary["evidence"])}, + "support": len(selected["support_refs"]), + "broadness_ratio": round(broadness, 4), + "warnings": warnings, + } + print(json.dumps({ + "status": "READY", + "message": "A0 context + domain slice compile 완료", + "stage_a_path": A0_STAGE_A_PATH, + "source_manifest_path": A0_MANIFEST_PATH, + "domain_slice_paths": A0_SLICE_PATHS, + "input_digests_sha256": stage_a_context.get("input_digests_sha256") or {}, + "routing_summary": routing_summary, + }, ensure_ascii=False)) + + + def main() -> None: + _init() + + # Stage A.1-A.2: allowed input guard + existence check. + existence_check = _preflight_required_files() + + # Stage A.3-A.4: read exactly four inputs once, then parse/fail fast. + docs = _read_required_docs_once() + event_candidates = _parse_json_doc("evidence_event_candidates.json", docs["evidence_event_candidates.json"]) + evidence_indexed = _parse_json_doc("evidence_indexed.json", docs["evidence_indexed.json"]) + meeting_text = docs["client_meeting.md"] + juristic_text = docs["Default_Agent/Juristic_Act.md"] + + # Stage A.5-A.8: build normalized maps and the global hazard seed. + meeting_clause_map = _build_meeting_clause_map(meeting_text) + event_candidate_map = _build_event_candidate_map(event_candidates) + evidence_authority_map = _build_evidence_authority_map(evidence_indexed) + juristic_act_taxonomy_map = _build_juristic_act_map(juristic_text) + _validate_juristic_act_map(juristic_act_taxonomy_map) + global_hazard_seed = _build_global_hazard_seed(meeting_clause_map, event_candidate_map, evidence_authority_map) + + if not evidence_authority_map["evidence_index_set"]: + raise RuntimeError("No finalized evidence_index values available") + + input_digests_sha256 = { + name: _stable_hash(docs[name]) + for name in REQUIRED_FILES + } + part1_soft_gate_handoff = _compact_part1_soft_gate_handoff( + existence_check, + input_digests_sha256, + meeting_clause_map, + event_candidate_map, + evidence_authority_map, + ) + + stage_a_context = { + "schema_version": "task_c_bo_stage_a_context.v1", + "stage": "Task_C_BO.Stage_A", + "status": "READY", + "created_at_utc": datetime.now(timezone.utc).isoformat(), + "allowed_input_files": REQUIRED_FILES, + "forbidden_files_attested": ["evidence_all.json", "BO.json", "actio_case_signals.json", "case_liability_signals.json", "legal_effect_signals.json"], + "file_io_policy": { + "list_docs_called_for": REQUIRED_FILES, + "read_docs_called_once_for": REQUIRED_FILES, + "write_file_called": False, + "raw_evidence_all_read": False, + "output_file_written": False, + }, + "existence_check": existence_check, + "input_digests_sha256": input_digests_sha256, + "meeting_clause_map": meeting_clause_map, + "event_candidate_map": event_candidate_map, + "evidence_authority_map": evidence_authority_map, + "juristic_act_taxonomy_map": juristic_act_taxonomy_map, + "global_hazard_seed": global_hazard_seed, + "part1_soft_gate_handoff": part1_soft_gate_handoff, + "stage_b_worker_contract": { + "context_is_read_only": True, + "bo_id_finalization_allowed": False, + "candidate_generation_allowed": True, + "must_use_evidence_index_set": evidence_authority_map["evidence_index_set"], + }, + } + + _a0_emit(stage_a_context) + + if __name__ == "__main__": + try: + main() + except Exception as exc: + print(json.dumps({"stage": "Task_C_BO.Stage_A", "status": "FAILED", "error": str(exc)}, ensure_ascii=False)) + raise + - task_name: Task_C_BO_Stage_B_B1_Money_Successor + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: true + preflight_files: + - stage1_tmp/task_c_bo/domain_slices/B1.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + 본 task의 유일한 입력은 preflight로 제공된 도메인 slice 파일 `stage1_tmp/task_c_bo/domain_slices/B1.json`의 내용이다. 그 안의 `stage_b_domain_slice` 객체만 사용한다. 다른 파일 요청·재구성은 금지된다. + + + + + - You MUST use only the `stage_b_domain_slice` object provided via the preflight file `stage1_tmp/task_c_bo/domain_slices/B1.json`. + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B1_Money_Successor"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B1_Money_Successor + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for money claim, payment, notice, admission, and commercial successor liability. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B1:001`, `B1:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - money claim origin: loan, sale price, deposit, settlement, construction price, unpaid balance + - due date, acceleration, demand, notice, payment deadline, or delayed performance + - partial payment, debt admission, payment memorandum, balance confirmation, account statement, or settlement confirmation + - business transfer, trade-name continuation, transferee liability, debt non-assumption clause, or commercial successor liability + - uncertain creditor/debtor/successor linkage that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - `support_refs` (id+label 참조 전용 — 사실 인용 근거로 사용 금지) + - `review_only_refs` + - `slice_guard_echo` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use B0 routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the current B0 slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split money claim origin, maturity/demand, payment/admission, and successor liability when they are distinct legal-effect seeds. + 4. Preserve payment history, demand/notice hints, admission hints, successor liability hints, and successor-link uncertainty in `extensions.domain_payload`. Preserve agreed interest rate and agreed delay damage rate as literal strings from B1 `loan_components`/`secured_debt_components` (`component_details` of `selected_evidence_authority`) when present; never substitute statutory rates, and keep the supporting evidence indexes in `interest_rate_source_evidence_refs`. + 5. Use `domain_review_queue` for missing amount/date, uncertain creditor/debtor identity, conflicting payment status, or cross-domain successor merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Keep all unsupported or uncertain values as null or [] and route review needs to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + - 값이 null, [], {}, 빈 문자열인 키는 출력하지 않는다. 의미 있는 false와 0은 유지한다. 후속 결정적 reducer(R0)가 canonical schema로 확장하며 키 부재를 null로 해석한다. + - 다음 필드는 출력하지 않는다 (R0가 결정적으로 재구성/복원한다): `transport_candidate_refs`, `Reason`, `PriorAct`, `ReasonRefs`, `downstream_seed_refs`(단, 선행행위 후보가 실재할 때의 `prior_candidate_refs`/`reason_refs_candidate_refs`는 예외적으로 출력 가능), `slice_guard`의 `source_universe`/`input_digests_sha256`/`stage_b_worker_contract` 전체 에코. + - `transport_metadata.slice_guard`에는 slice의 `slice_guard_echo` 값 2개(`source_stage_a_created_at_utc`, `input_digests_summary`)만 문자 그대로 복사한다. + - `domain_review_queue` 항목은 `review_id`, `severity`, `issue_type`, `source_event_candidate_ids`, `source_evidence_indexes`, `source_meeting_clause_ids`, `recommended_downstream_owner`만 출력한다. 서술형 `description`은 출력하지 않는다(고정 문구는 R0가 부여한다). + - candidate의 실질 내용(BOType, ActionType, JuristicAct, Action, core_field_base, amount, source refs, domain_payload의 유의미 값, 도메인 named fields)은 생략 대상이 아니다 — 값이 있으면 반드시 출력한다. + + + + Return exactly one JSON object: + { + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B1_Money_Successor", + "status": "READY|READY_WITH_REVIEW|FAILED", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B1_Money_Successor", + "domain_label": "금전채권 및 상호속용 책임", + "slice_guard": { + "source_stage_a_created_at_utc": "slice_guard_echo에서 문자 그대로 복사", + "input_digests_summary": "slice_guard_echo에서 문자 그대로 복사" + } + }, + "bo_seed_candidates": [ + { + "candidate_ref": "B1:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "JuristicAct": {"label": "string|null"}, + "Action": "short neutral factual/legal-action draft", + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [], + "asset_refs": [], + "legal_effect_summary": null + }, + "amount": { + "value": null, + "currency": "KRW|null", + "basis": null + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": null, + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": {"claim_type_candidate": null, "principal_amount_hint": null, "interest_hint": null, "delay_damage_hint": null, "agreed_interest_rate": null, "agreed_delay_damage_rate": null, "interest_accrual_start_hint": null, "interest_rate_source_evidence_refs": [], "unpaid_balance_hint": null, "creditor_candidate": null, "debtor_candidate": null}, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": "string|null", + "conflict_flags": [] + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B1:review:001", + "severity": "SOFT_WARNING|HARD_WARNING", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "publisher|domain_join|C0|C1|C2|C3|C5|D|E" + } + ] + } + } + + Status rule: use `FAILED` when any input acceptance or `stage_b_worker_contract` guard fails; use `READY_WITH_REVIEW` when `domain_review_queue` is non-empty and no hard gate failed; otherwise use `READY`. + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B2_Secured_Registry + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: true + preflight_files: + - stage1_tmp/task_c_bo/domain_slices/B2.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + 본 task의 유일한 입력은 preflight로 제공된 도메인 slice 파일 `stage1_tmp/task_c_bo/domain_slices/B2.json`의 내용이다. 그 안의 `stage_b_domain_slice` 객체만 사용한다. 다른 파일 요청·재구성은 금지된다. + + + + + - You MUST use only the `stage_b_domain_slice` object provided via the preflight file `stage1_tmp/task_c_bo/domain_slices/B2.json`. + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B2_Secured_Registry"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B2_Secured_Registry + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for secured debt, registry, payment allocation, auction, distribution, and derivative invalidity issues. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B2:001`, `B2:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - secured debt creation, change, repayment, payment allocation, and secured obligation balance + - mortgage, pledge, collateral security, registration, cancellation, correction, or invalidity chain + - auction, distribution, dividend, satisfaction, or residual balance facts + - derivative invalidity, registry error, title defect, or secured property dispute + - uncertain registry sequence or security/right-holder linkage that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - `support_refs` (id+label 참조 전용 — 사실 인용 근거로 사용 금지) + - `review_only_refs` + - `slice_guard_echo` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use B0 routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the current B0 slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split secured debt chain, registry act, payment allocation, auction step, dividend distribution, and derivative invalidity when they are distinct legal-effect seeds. + 4. Preserve registry rows, secured debt chain, payment allocation, auction step, dividend distribution hints, derivative invalidity hints, and registry display values in `extensions.domain_payload`. Carry `fraction_ref` from source event candidates unchanged; do not merge or renumber. Populate `fractional_effect_events` as an event-reference list only (fraction-bearing setting/extinguishment/transfer events); never fold fraction states or normalize/reduce fractions (that is Stage 2 work). + 5. Use `domain_review_queue` for missing registry date, unclear secured obligation balance, conflicting registration sequence, or cross-domain property merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Keep all unsupported or uncertain values as null or [] and route review needs to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + - 값이 null, [], {}, 빈 문자열인 키는 출력하지 않는다. 의미 있는 false와 0은 유지한다. 후속 결정적 reducer(R0)가 canonical schema로 확장하며 키 부재를 null로 해석한다. + - 다음 필드는 출력하지 않는다 (R0가 결정적으로 재구성/복원한다): `transport_candidate_refs`, `Reason`, `PriorAct`, `ReasonRefs`, `downstream_seed_refs`(단, 선행행위 후보가 실재할 때의 `prior_candidate_refs`/`reason_refs_candidate_refs`는 예외적으로 출력 가능), `slice_guard`의 `source_universe`/`input_digests_sha256`/`stage_b_worker_contract` 전체 에코. + - `transport_metadata.slice_guard`에는 slice의 `slice_guard_echo` 값 2개(`source_stage_a_created_at_utc`, `input_digests_summary`)만 문자 그대로 복사한다. + - `domain_review_queue` 항목은 `review_id`, `severity`, `issue_type`, `source_event_candidate_ids`, `source_evidence_indexes`, `source_meeting_clause_ids`, `recommended_downstream_owner`만 출력한다. 서술형 `description`은 출력하지 않는다(고정 문구는 R0가 부여한다). + - candidate의 실질 내용(BOType, ActionType, JuristicAct, Action, core_field_base, amount, source refs, domain_payload의 유의미 값, 도메인 named fields)은 생략 대상이 아니다 — 값이 있으면 반드시 출력한다. + + + + Return exactly one JSON object: + { + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B2_Secured_Registry", + "status": "READY|READY_WITH_REVIEW|FAILED", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B2_Secured_Registry", + "domain_label": "담보채무, 변제충당, 등기무효, 경매", + "slice_guard": { + "source_stage_a_created_at_utc": "slice_guard_echo에서 문자 그대로 복사", + "input_digests_summary": "slice_guard_echo에서 문자 그대로 복사" + } + }, + "bo_seed_candidates": [ + { + "candidate_ref": "B2:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "JuristicAct": {"label": "string|null"}, + "Action": "short neutral factual/legal-action draft", + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [], + "asset_refs": [], + "legal_effect_summary": null + }, + "amount": { + "value": null, + "currency": "KRW|null", + "basis": null + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "title_hint": null, + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "registry_rows": [ + {"registry_ref": null, "fraction_ref": null, "fraction_display": null, "fraction_numerator": null, "fraction_denominator": null, "receipt_no": null} + ], + "fractional_effect_events": [ + {"fraction_ref": null, "fraction_display": null, "fraction_numerator": null, "fraction_denominator": null, "effect_kind_hint": "mortgage_setting|mortgage_extinguishment|transfer|other", "source_event_candidate_ids": [], "registry_refs": [], "evidence_indexes": []} + ], + "secured_debt_chain": [], + "payment_allocation": [], + "mortgage_extinguishment_hints": [], + "auction_steps": [], + "dividend_distribution_hints": [], + "derivative_invalidity_hints": [], + "registry_object_display": "string|null" + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B2:review:001", + "severity": "SOFT_WARNING|HARD_WARNING", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "publisher|domain_join|C0|C1|C2|C3|C5|D|E" + } + ] + } + } + + Status rule: use `FAILED` when any input acceptance or `stage_b_worker_contract` guard fails; use `READY_WITH_REVIEW` when `domain_review_queue` is non-empty and no hard gate failed; otherwise use `READY`. + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B3_Land_Valuation + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: true + preflight_files: + - stage1_tmp/task_c_bo/domain_slices/B3.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + 본 task의 유일한 입력은 preflight로 제공된 도메인 slice 파일 `stage1_tmp/task_c_bo/domain_slices/B3.json`의 내용이다. 그 안의 `stage_b_domain_slice` 객체만 사용한다. 다른 파일 요청·재구성은 금지된다. + + + + + - You MUST use only the `stage_b_domain_slice` object provided via the preflight file `stage1_tmp/task_c_bo/domain_slices/B3.json`. + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B3_Land_Valuation"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B3_Land_Valuation + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for land use gain, possession/use, valuation, and asset-value facts. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B3:001`, `B3:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - land or building possession, use, occupation, benefit, rent-equivalent gain, or loss of use + - valuation fact, appraisal, market value, rent value, area, period, and calculation base + - delivery, possession start/end, unauthorized use, co-occupancy, or liability overlap + - asset identity, address, parcel, unit, area, or other value-bearing object description + - uncertain use period, value basis, possession status, or asset binding that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - `support_refs` (id+label 참조 전용 — 사실 인용 근거로 사용 금지) + - `review_only_refs` + - `slice_guard_echo` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use B0 routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the current B0 slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split possession/use state, use-gain period, valuation fact, and liability overlap when they are distinct legal-effect seeds. + 4. Preserve valuation rows, valuation matrix, use-gain facts, possession state, asset binding hints, and liability overlap hints in `extensions.domain_payload`. Populate the `possession_state` two-track structure (possession facts vs. asserted legal causes) from selected event candidates only; never resolve which legal cause prevails (that is Stage 2 work); preserve conflicting assertions side by side. + 5. Use `domain_review_queue` for missing period, missing value base, unclear possession state, or cross-domain asset merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Keep all unsupported or uncertain values as null or [] and route review needs to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + - 값이 null, [], {}, 빈 문자열인 키는 출력하지 않는다. 의미 있는 false와 0은 유지한다. 후속 결정적 reducer(R0)가 canonical schema로 확장하며 키 부재를 null로 해석한다. + - 다음 필드는 출력하지 않는다 (R0가 결정적으로 재구성/복원한다): `transport_candidate_refs`, `Reason`, `PriorAct`, `ReasonRefs`, `downstream_seed_refs`(단, 선행행위 후보가 실재할 때의 `prior_candidate_refs`/`reason_refs_candidate_refs`는 예외적으로 출력 가능), `slice_guard`의 `source_universe`/`input_digests_sha256`/`stage_b_worker_contract` 전체 에코. + - `transport_metadata.slice_guard`에는 slice의 `slice_guard_echo` 값 2개(`source_stage_a_created_at_utc`, `input_digests_summary`)만 문자 그대로 복사한다. + - `domain_review_queue` 항목은 `review_id`, `severity`, `issue_type`, `source_event_candidate_ids`, `source_evidence_indexes`, `source_meeting_clause_ids`, `recommended_downstream_owner`만 출력한다. 서술형 `description`은 출력하지 않는다(고정 문구는 R0가 부여한다). + - candidate의 실질 내용(BOType, ActionType, JuristicAct, Action, core_field_base, amount, source refs, domain_payload의 유의미 값, 도메인 named fields)은 생략 대상이 아니다 — 값이 있으면 반드시 출력한다. + + + + Return exactly one JSON object: + { + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B3_Land_Valuation", + "status": "READY|READY_WITH_REVIEW|FAILED", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B3_Land_Valuation", + "domain_label": "토지 사용이익, 점유, 사용수익, 가치평가", + "slice_guard": { + "source_stage_a_created_at_utc": "slice_guard_echo에서 문자 그대로 복사", + "input_digests_summary": "slice_guard_echo에서 문자 그대로 복사" + } + }, + "bo_seed_candidates": [ + { + "candidate_ref": "B3:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "JuristicAct": {"label": "string|null"}, + "Action": "short neutral factual/legal-action draft", + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [], + "asset_refs": [], + "legal_effect_summary": null + }, + "amount": { + "value": null, + "currency": "KRW|null", + "basis": null + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "title_hint": null, + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "valuation_rows": [], + "valuation_matrix": [], + "valuation_object": "string|null", + "use_gain_facts": [], + "possession_state": { + "asset_ref": null, + "possession_events": [ + {"possessor_candidates": [], "start_date": null, "end_date": null, "source_event_candidate_ids": [], "evidence_indexes": []} + ], + "legal_cause_assertions": [ + {"cause_asserted": "유치권행사중|임대차|사용대차|물권|동시이행항변|명도후잔류|무권원주장|미상", "asserted_by": null, "assertion_date": null, "source_event_candidate_ids": [], "evidence_indexes": []} + ] + }, + "delivery_or_possession_start_hints": [], + "unregistered_building_hints": [], + "liability_overlap_hints": [] + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B3:review:001", + "severity": "SOFT_WARNING|HARD_WARNING", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "publisher|domain_join|C0|C1|C2|C3|C5|D|E" + } + ] + } + } + + Status rule: use `FAILED` when any input acceptance or `stage_b_worker_contract` guard fails; use `READY_WITH_REVIEW` when `domain_review_queue` is non-empty and no hard gate failed; otherwise use `READY`. + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B4_Actio + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: true + preflight_files: + - stage1_tmp/task_c_bo/domain_slices/B4.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + 본 task의 유일한 입력은 preflight로 제공된 도메인 slice 파일 `stage1_tmp/task_c_bo/domain_slices/B4.json`의 내용이다. 그 안의 `stage_b_domain_slice` 객체만 사용한다. 다른 파일 요청·재구성은 금지된다. + + + + + - You MUST use only the `stage_b_domain_slice` object provided via the preflight file `stage1_tmp/task_c_bo/domain_slices/B4.json`. + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B4_Actio"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B4_Actio + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for actio pauliana, preserved claim, debtor fraudulent act, beneficiary/transferee, remedy, cap, and defenses. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B4:001`, `B4:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - preserved claim existence, claim date, creditor/debtor identity, and claim amount basis + - debtor act, asset decrease, insolvency, common collateral impairment, and harmfulness + - beneficiary, transferee, bad faith/good faith, notice, value compensation, restitution, and remedy cap + - encumbrance, valuation, property transfer, mortgage, sale, gift, collateral provision, or registration act used as fraudulent act basis + - uncertain fraudulent act date, beneficiary chain, asset value, remedy mode, or defense rebuttal that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - `support_refs` (id+label 참조 전용 — 사실 인용 근거로 사용 금지) + - `review_only_refs` + - `slice_guard_echo` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use B0 routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the current B0 slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split preserved claim, fraudulent transfer, balance-sheet impairment, encumbrance/valuation, beneficiary/transferee state, remedy/cap, and defense rebuttal when they are distinct legal-effect seeds. + 4. Preserve preserved claim bundle, fraudulent transfer facts, balance sheet, encumbrance timeline, valuation matrix, remedy/cap hints, and defense rebuttal in `extensions.domain_payload`. + 5. Use `domain_review_queue` for missing claim date, uncertain fraudulent act date, conflicting value basis, unclear beneficiary chain, or cross-domain asset/claim merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Keep all unsupported or uncertain values as null or [] and route review needs to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + - 값이 null, [], {}, 빈 문자열인 키는 출력하지 않는다. 의미 있는 false와 0은 유지한다. 후속 결정적 reducer(R0)가 canonical schema로 확장하며 키 부재를 null로 해석한다. + - 다음 필드는 출력하지 않는다 (R0가 결정적으로 재구성/복원한다): `transport_candidate_refs`, `Reason`, `PriorAct`, `ReasonRefs`, `downstream_seed_refs`(단, 선행행위 후보가 실재할 때의 `prior_candidate_refs`/`reason_refs_candidate_refs`는 예외적으로 출력 가능), `slice_guard`의 `source_universe`/`input_digests_sha256`/`stage_b_worker_contract` 전체 에코. + - `transport_metadata.slice_guard`에는 slice의 `slice_guard_echo` 값 2개(`source_stage_a_created_at_utc`, `input_digests_summary`)만 문자 그대로 복사한다. + - `domain_review_queue` 항목은 `review_id`, `severity`, `issue_type`, `source_event_candidate_ids`, `source_evidence_indexes`, `source_meeting_clause_ids`, `recommended_downstream_owner`만 출력한다. 서술형 `description`은 출력하지 않는다(고정 문구는 R0가 부여한다). + - candidate의 실질 내용(BOType, ActionType, JuristicAct, Action, core_field_base, amount, source refs, domain_payload의 유의미 값, 도메인 named fields)은 생략 대상이 아니다 — 값이 있으면 반드시 출력한다. + + + + Return exactly one JSON object: + { + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B4_Actio", + "status": "READY|READY_WITH_REVIEW|FAILED", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B4_Actio", + "domain_label": "사해행위취소, 피보전채권, 책임재산, 가액배상", + "slice_guard": { + "source_stage_a_created_at_utc": "slice_guard_echo에서 문자 그대로 복사", + "input_digests_summary": "slice_guard_echo에서 문자 그대로 복사" + } + }, + "bo_seed_candidates": [ + { + "candidate_ref": "B4:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "JuristicAct": {"label": "string|null"}, + "Action": "short neutral factual/legal-action draft", + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [], + "asset_refs": [], + "legal_effect_summary": null + }, + "amount": { + "value": null, + "currency": "KRW|null", + "basis": null + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "actio_fraudulent_transfer", + "title_hint": null, + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "preserved_claim_bundle": {}, + "fraudulent_transfer_facts": [], + "debtor_act_hints": [], + "balance_sheet": {"asset_decrease_hints": [], "insolvency_hints": [], "common_collateral_impairment_hints": []}, + "encumbrance_timeline": [], + "valuation_matrix": [], + "remedy_mode_hints": [], + "value_compensation_cap_hints": [], + "beneficiary_transferee_hints": [], + "bad_faith_good_faith_hints": [], + "defense_rebuttal": [] + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B4:review:001", + "severity": "SOFT_WARNING|HARD_WARNING", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "publisher|domain_join|C0|C1|C2|C3|C5|D|E" + } + ] + } + } + + Status rule: use `FAILED` when any input acceptance or `stage_b_worker_contract` guard fails; use `READY_WITH_REVIEW` when `domain_review_queue` is non-empty and no hard gate failed; otherwise use `READY`. + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: true + preflight_files: + - stage1_tmp/task_c_bo/domain_slices/B5.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + 본 task의 유일한 입력은 preflight로 제공된 도메인 slice 파일 `stage1_tmp/task_c_bo/domain_slices/B5.json`의 내용이다. 그 안의 `stage_b_domain_slice` 객체만 사용한다. 다른 파일 요청·재구성은 금지된다. + + + + + - You MUST use only the `stage_b_domain_slice` object provided via the preflight file `stage1_tmp/task_c_bo/domain_slices/B5.json`. + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B5_Succession_Notice_Lien_Asset_Defense"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B5_Succession_Notice_Lien_Asset_Defense + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for succession, notice lifecycle, lien, asset identity, possession, and major defenses. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B5:001`, `B5:002`, ... in deterministic order of the candidate seed list. + - If multiple lien seeds exist, assign `lien_id` values `B5:LIEN:001`, `B5:LIEN:002`, ... ordered by the earliest related event date (ties broken by `candidate_ref` lexicographic order). This id is transport-scope only, not a final BO id. + - Do not produce `violation_candidates` or `status_after_notice_receipt`; route such needs to `domain_review_queue` with `issue_type="legal_theory_required"`. + - `legal_sender_after_resolution.party` must always remain null in Stage 1 (value finalization is Stage 2 legal work); fill only its `status` and `succession_resolution_ref` from directly-read sources. + + + + Create separate seeds when supported for: + - general succession, specific succession, inheritance, assignment, acquisition, transfer, or party-status succession + - notice dispatch, arrival, receipt, refusal, content, timing, and legal effect of notice + - lien, possession, retained object, claim-object connection, asset alias, and asset identity + - simultaneous performance, setoff, payment, prescription, delivery, possession, or other major defense facts + - uncertain notice arrival, lien possession continuity, asset identity, succession scope, or defense linkage that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - `support_refs` (id+label 참조 전용 — 사실 인용 근거로 사용 금지) + - `review_only_refs` + - `slice_guard_echo` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use B0 routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the current B0 slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split succession state, notice lifecycle, lien/possession state, asset identity, and defense rebuttal when they are distinct legal-effect seeds. + 4. Preserve succession resolution, notice lifecycle, lien state, possession state, asset alias, simultaneous-performance hints, setoff/payment defense hints, and defense rebuttal in `extensions.domain_payload`. Populate the `possession_state` two-track structure (possession facts vs. asserted legal causes) from selected event candidates only; never resolve which legal cause prevails (that is Stage 2 work); preserve conflicting assertions side by side. Assign `lien_id` deterministically and reference it from possession/notice payloads of the same seed group. Inherit `notice_id` from B1 `notice_components[*].notice_id_proposed` (`component_details` of `selected_evidence_authority`); when multiple notices exist, split seeds so each seed carries one notice lifecycle. Inherit `schedule_refs`/`registry_refs` (SCH-*/REG-* keys) and `canonical_label_candidate`/`usable_in_relief` into `asset_alias` unchanged. + 5. Use `domain_review_queue` for missing arrival evidence, uncertain asset identity, unclear possession continuity, conflicting succession scope, or cross-domain defense merge need. When `received_date` cannot be established from selected sources, leave it null and add a `domain_review_queue` item with `issue_type="missing_source"` (do not fabricate); the sealed drafting gate is applied downstream at FL. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Keep all unsupported or uncertain values as null or [] and route review needs to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + - 값이 null, [], {}, 빈 문자열인 키는 출력하지 않는다. 의미 있는 false와 0은 유지한다. 후속 결정적 reducer(R0)가 canonical schema로 확장하며 키 부재를 null로 해석한다. + - 다음 필드는 출력하지 않는다 (R0가 결정적으로 재구성/복원한다): `transport_candidate_refs`, `Reason`, `PriorAct`, `ReasonRefs`, `downstream_seed_refs`(단, 선행행위 후보가 실재할 때의 `prior_candidate_refs`/`reason_refs_candidate_refs`는 예외적으로 출력 가능), `slice_guard`의 `source_universe`/`input_digests_sha256`/`stage_b_worker_contract` 전체 에코. + - `transport_metadata.slice_guard`에는 slice의 `slice_guard_echo` 값 2개(`source_stage_a_created_at_utc`, `input_digests_summary`)만 문자 그대로 복사한다. + - `domain_review_queue` 항목은 `review_id`, `severity`, `issue_type`, `source_event_candidate_ids`, `source_evidence_indexes`, `source_meeting_clause_ids`, `recommended_downstream_owner`만 출력한다. 서술형 `description`은 출력하지 않는다(고정 문구는 R0가 부여한다). + - candidate의 실질 내용(BOType, ActionType, JuristicAct, Action, core_field_base, amount, source refs, domain_payload의 유의미 값, 도메인 named fields)은 생략 대상이 아니다 — 값이 있으면 반드시 출력한다. + + + + Return exactly one JSON object: + { + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B5_Succession_Notice_Lien_Asset_Defense", + "status": "READY|READY_WITH_REVIEW|FAILED", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B5_Succession_Notice_Lien_Asset_Defense", + "domain_label": "상속, 통지, 유치권, 목적물 특정, 항변", + "slice_guard": { + "source_stage_a_created_at_utc": "slice_guard_echo에서 문자 그대로 복사", + "input_digests_summary": "slice_guard_echo에서 문자 그대로 복사" + } + }, + "bo_seed_candidates": [ + { + "candidate_ref": "B5:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "JuristicAct": {"label": "string|null"}, + "Action": "short neutral factual/legal-action draft", + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [], + "asset_refs": [], + "legal_effect_summary": null + }, + "amount": { + "value": null, + "currency": "KRW|null", + "basis": null + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "title_hint": null, + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "succession_resolution": {}, + "general_succession_status": {}, + "asset_specific_succession_resolution": {}, + "notice_lifecycle": { + "notice_id": null, + "notice_type": null, + "drafted_date": null, + "dispatch_date": null, + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [], + "legal_sender_after_resolution": {"status": "resolved|unresolved|disputed|not_required", "party": null, "succession_resolution_ref": null}, + "receiver_candidates": [], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + }, + "lien_state": { + "lien_id": null, + "asset_ref": null, + "lien_holder_candidates": [], + "secured_claim_hint": {"debtor_candidate": null, "basis_text": null, "claimed_remaining_amount_hint": null, "evidence_indexes": []}, + "possession_event_refs": [], + "assertion_events": [], + "extinction_notice_refs": [], + "status_raw_hint": null + }, + "possession_state": { + "asset_ref": null, + "possession_events": [ + {"possessor_candidates": [], "start_date": null, "end_date": null, "source_event_candidate_ids": [], "evidence_indexes": []} + ], + "legal_cause_assertions": [ + {"cause_asserted": "유치권행사중|임대차|사용대차|물권|동시이행항변|명도후잔류|무권원주장|미상", "asserted_by": null, "assertion_date": null, "source_event_candidate_ids": [], "evidence_indexes": []} + ] + }, + "asset_alias": [ + {"asset_ref": null, "alias_strings": [], "canonical_label_candidate": null, "usable_in_relief": "yes|no|unknown", "schedule_refs": [], "registry_refs": [{"registry_ref": null, "receipt_no_refs": []}], "source_evidence_indexes": []} + ], + "defense_rebuttal": [], + "simultaneous_performance_hints": [], + "setoff_or_payment_defense_hints": [] + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B5:review:001", + "severity": "SOFT_WARNING|HARD_WARNING", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "publisher|domain_join|C0|C1|C2|C3|C5|D|E" + } + ] + } + } + + Status rule: use `FAILED` when any input acceptance or `stage_b_worker_contract` guard fails; use `READY_WITH_REVIEW` when `domain_review_queue` is non-empty and no hard gate failed; otherwise use `READY`. + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_R0_seed_reducer_and_exception_planner + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: |- + #!/usr/bin/env python3 + # Task_C_BO_R0_seed_reducer_and_exception_planner (v3) + # publisher + domain_join + PostB_1 통합 결정적 reducer. + # Spec: Part_2_Improvement_Strategy_Claude_v1.md §7 (defer policy = 개선전략서 X-2, pack 조건 = X-3) + from __future__ import annotations + import itertools + import json + import re + import sys + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + WORKER_RAW_FIELDS = { + "B1_Money_Successor": r"""{{prev.Task_C_BO_Stage_B_B1_Money_Successor.json_output}}""", + "B2_Secured_Registry": r"""{{prev.Task_C_BO_Stage_B_B2_Secured_Registry.json_output}}""", + "B3_Land_Valuation": r"""{{prev.Task_C_BO_Stage_B_B3_Land_Valuation.json_output}}""", + "B4_Actio": r"""{{prev.Task_C_BO_Stage_B_B4_Actio.json_output}}""", + "B5_Succession_Notice_Lien_Asset_Defense": r"""{{prev.Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense.json_output}}""", + } + SEED_DOCS = { + "B1_Money_Successor": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B1_Money_Successor.json", + "B2_Secured_Registry": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B2_Secured_Registry.json", + "B3_Land_Valuation": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B3_Land_Valuation.json", + "B4_Actio": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B4_Actio.json", + "B5_Succession_Notice_Lien_Asset_Defense": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B5_Succession_Notice_Lien_Asset_Defense.json", + } + SEED_TASK_NAMES = { + "B1_Money_Successor": "Task_C_BO_Stage_B_B1_Money_Successor", + "B2_Secured_Registry": "Task_C_BO_Stage_B_B2_Secured_Registry", + "B3_Land_Valuation": "Task_C_BO_Stage_B_B3_Land_Valuation", + "B4_Actio": "Task_C_BO_Stage_B_B4_Actio", + "B5_Succession_Notice_Lien_Asset_Defense": "Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense", + } + DOMAIN_ORDER = list(SEED_DOCS.keys()) + DOMAIN_PREFIX = { + "B1_Money_Successor": "B1", + "B2_Secured_Registry": "B2", + "B3_Land_Valuation": "B3", + "B4_Actio": "B4", + "B5_Succession_Notice_Lien_Asset_Defense": "B5", + } + DOMAIN_LABELS = { + "B1_Money_Successor": "금전채권 및 상호속용 책임", + "B2_Secured_Registry": "담보채무, 변제충당, 등기무효, 경매", + "B3_Land_Valuation": "토지 사용이익, 점유, 사용수익, 가치평가", + "B4_Actio": "사해행위취소, 피보전채권, 책임재산, 가액배상", + "B5_Succession_Notice_Lien_Asset_Defense": "상속, 통지, 유치권, 목적물 특정, 항변", + } + STAGE_A_PATH = "stage1_tmp/task_c_bo/stage_a_context.json" + MANIFEST_PATH = "stage1_tmp/task_c_bo/source_universe_manifest.json" + LEDGER_PATH = "stage1_tmp/task_c_bo/postb_seed_ledger.json" + PACK_PATH = "quality_gates/stage1_part2_exception_pack.json" + REVIEW_HANDOFF_PATH = "quality_gates/stage1_part2_review_handoff.json" + CANDIDATE_REF_RE = re.compile(r"^B[1-5]:[0-9]{3}$") + REVIEW_ISSUE_ENUM = { + "missing_source", "source_conflict", "cross_domain_merge_needed", + "amount_or_date_uncertain", "legal_effect_uncertain", "review_required", + "legal_theory_required", "near_duplicate_kept_separate", + "meeting_only_evidence_gap", "schema_field_fallback", "prior_link_ambiguous", + } + DOWNSTREAM_OWNER_ENUM = {"publisher", "domain_join", "C0", "C1", "C2", "C3", "C5", "D", "E", "Stage2"} + ALLOWED_SEED_KEYS = { + "candidate_ref", "source_domain", "BOType", "ActionType", "JuristicAct", "Action", + "core_field_base", "amount", "source_evidence_indexes", "provenance", + "downstream_seed_refs", "extensions", "Legal_Keywords", "transport_audit", + "Reason", "PriorAct", "ReasonRefs", + } + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-c-bo-r0-seed-reducer-and-exception-planner", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse_mcp(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "id": _mid(), "method": "tools/call", + "params": {"name": name, "arguments": args}}, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse_mcp(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def read_json_doc(path: str) -> Any: + result = _call("read_docs", {"doc_names": [path]}) + content = result["result"].get("content", []) + text = content[0].get("text", "") if content else "" + try: + parsed = json.loads(text) + except json.JSONDecodeError as exc: + raise RuntimeError(f"Invalid read_docs envelope for {path}") from exc + if isinstance(parsed, dict) and "results" in parsed: + rows = parsed.get("results") or [] + if not rows or rows[0].get("error"): + raise RuntimeError(f"read_docs failed for {path}: {rows[:1]}") + inner = rows[0].get("content") or "" + if isinstance(inner, str): + try: + return json.loads(inner) + except json.JSONDecodeError: + obj, end = JSON_DECODER.raw_decode(inner.strip()) + if inner.strip()[end:].strip(): + raise RuntimeError(f"{path}: non-json tail") + return obj + return inner + return parsed + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def clip(value: Any, limit: int = 120) -> str: + text = " ".join(str(value or "").split()) + return text if len(text) <= limit else text[:limit].rstrip() + "..." + + # ---------- 관용 파서 (SKILL.md §6.7 + v2 publisher unwrap 계승; Y-3) ---------- + def parse_llm_json(raw: str, label: str, salvage_notes: list[dict[str, Any]]) -> dict[str, Any]: + text = (raw or "").strip() + if not text or text.startswith("{{"): + raise ValueError(f"{label}: no injected JSON body") + if text.startswith("```"): + salvage_notes.append({"label": label, "salvage": "stripped_code_fence"}) + parts = text.split("```") + for part in parts: + part = part.strip() + if part.startswith("json"): + part = part[4:].strip() + if part.startswith("{"): + text = part + break + start = text.find("{") + if start < 0: + raise ValueError(f"{label}: JSON object start not found") + if start > 0: + salvage_notes.append({"label": label, "salvage": "stripped_leading_prose"}) + obj, end = JSON_DECODER.raw_decode(text[start:].strip()) + tail = text[start:].strip()[end:].strip() + if tail: + salvage_notes.append({"label": label, "salvage": "stripped_trailing_tail", "tail_preview": tail[:80]}) + if not isinstance(obj, dict): + raise ValueError(f"{label}: JSON root must be object") + if obj.get("type") == "text" and isinstance(obj.get("text"), str): + return parse_llm_json(obj["text"], f"{label}.text", salvage_notes) + if obj.get("success") is True and isinstance(obj.get("output"), str): + return parse_llm_json(obj["output"], f"{label}.output", salvage_notes) + return obj + + # ---------- sparse -> canonical schema expander (사실 창작 없이 default만 복원) ---------- + # v2 워커 OUTPUT_CONTRACT의 domain_payload 상위 키·순서·기본형 (B5는 후보 유형별 가변 스키마이므로 제외) + DOMAIN_PAYLOAD_CANON: dict[str, list[tuple[str, Any]]] = { + "B1_Money_Successor": [ + ("domain_legal_effect_tag", None), ("title_hint", None), ("summary_hint", None), + ("conflict_patterns", []), ("asset_bindings", []), ("parties_hint", []), + ("temporal_hint", None), ("hazard_flags", []), ("legal_effect_tags", []), + ("event_state_type", None), ("action_summary", None), ("time_interval_hint", None), + ("asset_binding_hint", None), ("mixed_contract_split_hint", None), ("conflict_pattern_hint", None), + ("candidate_details", {}), ("facts", {}), ("money_claim_details", {}), + ("payment_history", []), ("demand_or_notice_hints", []), ("admission_hints", []), + ("successor_liability_hints", []), ("successor_link_uncertainty", None), ("conflict_flags", []), + ], + "B2_Secured_Registry": [ + ("domain_legal_effect_tag", None), ("title_hint", None), ("summary_hint", None), + ("conflict_patterns", []), ("asset_bindings", []), ("parties_hint", []), + ("temporal_hint", None), ("hazard_flags", []), ("legal_effect_tags", []), + ("event_state_type", None), ("action_summary", None), ("time_interval_hint", None), + ("asset_binding_hint", None), ("mixed_contract_split_hint", None), ("conflict_pattern_hint", None), + ("candidate_details", {}), ("facts", {}), ("registry_rows", []), + ("fractional_effect_events", []), ("secured_debt_chain", []), ("payment_allocation", []), + ("mortgage_extinguishment_hints", []), ("auction_steps", []), ("dividend_distribution_hints", []), + ("derivative_invalidity_hints", []), ("registry_object_display", None), + ], + "B3_Land_Valuation": [ + ("domain_legal_effect_tag", None), ("title_hint", None), ("summary_hint", None), + ("conflict_patterns", []), ("asset_bindings", []), ("parties_hint", []), + ("temporal_hint", None), ("hazard_flags", []), ("legal_effect_tags", []), + ("event_state_type", None), ("action_summary", None), ("time_interval_hint", None), + ("asset_binding_hint", None), ("mixed_contract_split_hint", None), ("conflict_pattern_hint", None), + ("candidate_details", {}), ("facts", {}), ("valuation_rows", []), + ("valuation_matrix", []), ("valuation_object", None), ("use_gain_facts", []), + ("possession_state", {}), ("delivery_or_possession_start_hints", []), ("unregistered_building_hints", []), + ("liability_overlap_hints", []), + ], + "B4_Actio": [ + ("domain_legal_effect_tag", None), ("title_hint", None), ("summary_hint", None), + ("conflict_patterns", []), ("asset_bindings", []), ("parties_hint", []), + ("temporal_hint", None), ("hazard_flags", []), ("legal_effect_tags", []), + ("event_state_type", None), ("action_summary", None), ("time_interval_hint", None), + ("asset_binding_hint", None), ("mixed_contract_split_hint", None), ("conflict_pattern_hint", None), + ("candidate_details", {}), ("facts", {}), ("preserved_claim_bundle", {}), + ("fraudulent_transfer_facts", []), ("debtor_act_hints", []), ("balance_sheet", {}), + ("encumbrance_timeline", []), ("valuation_matrix", []), ("remedy_mode_hints", []), + ("value_compensation_cap_hints", []), ("beneficiary_transferee_hints", []), ("bad_faith_good_faith_hints", []), + ("defense_rebuttal", []), + ], + } + + def _canon_payload(payload: dict[str, Any], domain_id: str) -> dict[str, Any]: + canon = DOMAIN_PAYLOAD_CANON.get(domain_id) + if canon is None: + return payload # B5: 후보 유형별 가변 스키마 — 워커 발행 형태 유지 (v2 동작 동일) + out: dict[str, Any] = {} + for key, default in canon: + if key in payload: + out[key] = payload[key] + else: + out[key] = [] if isinstance(default, list) else {} if isinstance(default, dict) else None + for key, value in payload.items(): + if key not in out: + out[key] = value + return out + + def expand_candidate(cand: dict[str, Any], domain_id: str) -> dict[str, Any]: + out = dict(cand) + out.setdefault("Reason", None) + out.setdefault("PriorAct", None) + out.setdefault("ReasonRefs", []) + out.setdefault("Legal_Keywords", []) + out.setdefault("core_field_base", {}) + if out.get("JuristicAct") is None: + out["JuristicAct"] = {"label": None} + if out.get("amount") is None: + out["amount"] = {} + out.setdefault("source_evidence_indexes", []) + prov = _dict(out.get("provenance")) + prov.setdefault("source_event_candidate_ids", []) + prov.setdefault("source_meeting_clause_ids", []) + prov.setdefault("source_evidence_indexes", list(out.get("source_evidence_indexes") or [])) + out["provenance"] = prov + out.setdefault("downstream_seed_refs", {}) + ext = _dict(out.get("extensions")) + ext["domain_payload"] = _canon_payload(_dict(ext.get("domain_payload")), domain_id) + out["extensions"] = ext + return out + + def expand_review_item(item: dict[str, Any], domain_id: str, idx: int) -> dict[str, Any]: + out = dict(item) + out.setdefault("review_id", f"{DOMAIN_PREFIX[domain_id]}:review:{idx:03d}") + out.setdefault("severity", "SOFT_WARNING") + out.setdefault("issue_type", "review_required") + out.setdefault("source_event_candidate_ids", []) + out.setdefault("source_evidence_indexes", []) + out.setdefault("source_meeting_clause_ids", []) + out.setdefault("recommended_downstream_owner", "Stage2") + return out + + def digest_summary(input_digests: dict[str, Any]) -> str: + import hashlib + return hashlib.sha256(json.dumps(input_digests, ensure_ascii=False, sort_keys=True).encode("utf-8")).hexdigest() + + # ---------- 워커 출력 수용 검증 (publisher 이식, sparse 인지형: expander 후 강검증) ---------- + def validate_seed_object(seed_obj: dict[str, Any], domain_id: str, stage_a: dict[str, Any], warnings: list[dict[str, Any]]) -> None: + if seed_obj.get("schema_version") != "task_c_bo_stage_b_domain_bo_seed.v2": + raise ValueError(f"{domain_id}: seed schema_version mismatch") + if seed_obj.get("domain_id") != domain_id: + raise ValueError(f"{domain_id}: seed domain_id mismatch") + if seed_obj.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError(f"{domain_id}: seed status must be READY or READY_WITH_REVIEW") + guard = _dict(_dict(seed_obj.get("transport_metadata")).get("slice_guard")) + created = guard.get("source_stage_a_created_at_utc") + if created and created != stage_a.get("created_at_utc"): + raise ValueError(f"{domain_id}: stale seed output: Stage A created_at_utc mismatch") + summary = guard.get("input_digests_summary") + full = guard.get("input_digests_sha256") + expected = digest_summary(stage_a.get("input_digests_sha256") or {}) + if isinstance(full, dict) and full and full != stage_a.get("input_digests_sha256"): + raise ValueError(f"{domain_id}: stale seed output: input digest mismatch") + if isinstance(summary, str) and summary and summary != expected: + raise ValueError(f"{domain_id}: stale seed output: input digest summary mismatch") + if not created and not summary and not full: + warnings.append({"domain_id": domain_id, "warning": "seed output carries no freshness guard"}) + + def validate_candidate(cand: dict[str, Any], domain_id: str, idx: int) -> None: + prefix = DOMAIN_PREFIX[domain_id] + ref = cand.get("candidate_ref") + if not isinstance(ref, str) or not CANDIDATE_REF_RE.fullmatch(ref): + raise ValueError(f"{domain_id}.bo_seed_candidates[{idx}].candidate_ref invalid") + if not ref.startswith(prefix + ":"): + raise ValueError(f"{domain_id}.bo_seed_candidates[{idx}].candidate_ref prefix mismatch") + for forbidden in ("BO_ID", "id", "Evidence", "EvidenceTitles"): + if forbidden in cand: + raise ValueError(f"{domain_id}.{ref}: final field {forbidden} is prohibited") + if cand.get("Reason") is not None: + raise ValueError(f"{domain_id}.{ref}: Reason must be null/absent") + if cand.get("PriorAct") is not None: + raise ValueError(f"{domain_id}.{ref}: PriorAct must be null/absent") + if cand.get("ReasonRefs") not in ([], None): + raise ValueError(f"{domain_id}.{ref}: ReasonRefs must be []/absent") + + # ---------- PostB_1 이식: sort key / duplicate keys / schema risk ---------- + def _source_refs(seed: dict[str, Any]) -> dict[str, list[str]]: + provenance = _dict(seed.get("provenance")) + return { + "source_evidence_indexes": _strings(seed.get("source_evidence_indexes") or provenance.get("source_evidence_indexes")), + "source_event_candidate_ids": _strings(provenance.get("source_event_candidate_ids")), + "source_meeting_clause_ids": _strings(provenance.get("source_meeting_clause_ids")), + } + + def _sort_key(seed: dict[str, Any]) -> dict[str, Any]: + core = _dict(seed.get("core_field_base")) + domain = seed.get("source_domain") + juristic = _dict(seed.get("JuristicAct")) + return { + "BehaviorTime": core.get("BehaviorTime"), + "domain_order": DOMAIN_ORDER.index(domain) if domain in DOMAIN_ORDER else 99, + "BOType": seed.get("BOType"), + "ActionType": seed.get("ActionType"), + "JuristicActLabel": juristic.get("label"), + "Action": seed.get("Action"), + "candidate_ref": seed.get("candidate_ref"), + } + + def _duplicate_key(seed: dict[str, Any]) -> tuple[Any, ...]: + core = _dict(seed.get("core_field_base")) + juristic = _dict(seed.get("JuristicAct")) + refs = _source_refs(seed) + return ( + tuple(sorted(refs["source_evidence_indexes"])), + tuple(sorted(refs["source_event_candidate_ids"])), + seed.get("BOType"), + seed.get("ActionType"), + juristic.get("label"), + str(seed.get("Action") or "").strip(), + str(core.get("BehaviorTime") or "").strip(), + str(core.get("Object") or "").strip(), + ) + + def _normalize_juristic(value: Any) -> Any: + if value is None: + return None + if isinstance(value, dict): + label = value.get("label") + return {"label": str(label).strip()} if label not in (None, "") else {"label": None} + text = str(value).strip() + return {"label": text} if text else None + + def _seed_payload(cand: dict[str, Any], domain_id: str) -> dict[str, Any]: + payload: dict[str, Any] = {} + for key in ALLOWED_SEED_KEYS: + if key in cand: + payload[key] = cand[key] + payload["candidate_ref"] = cand.get("candidate_ref") + payload["source_domain"] = domain_id + payload["JuristicAct"] = _normalize_juristic(payload.get("JuristicAct")) + provenance = _dict(payload.get("provenance")) + payload["provenance"] = { + "source_event_candidate_ids": _strings(provenance.get("source_event_candidate_ids")), + "source_meeting_clause_ids": _strings(provenance.get("source_meeting_clause_ids")), + "source_evidence_indexes": _strings(cand.get("source_evidence_indexes") or provenance.get("source_evidence_indexes")), + "source_domain": domain_id, + } + payload["source_evidence_indexes"] = _strings(cand.get("source_evidence_indexes") or payload["provenance"]["source_evidence_indexes"]) + payload["Reason"] = None + payload["PriorAct"] = None + payload["ReasonRefs"] = [] + payload["core_field_base"] = _dict(payload.get("core_field_base")) + payload["downstream_seed_refs"] = _dict(payload.get("downstream_seed_refs")) + payload["extensions"] = _dict(payload.get("extensions")) + if "domain_payload" not in payload["extensions"]: + payload["extensions"]["domain_payload"] = {} + return payload + + def _compact_exception_payload(seeds: list[dict[str, Any]]) -> list[dict[str, Any]]: + compact = [] + for seed in seeds: + compact.append({ + "candidate_ref": seed.get("candidate_ref"), + "source_domain": seed.get("source_domain"), + "BOType": seed.get("BOType"), + "ActionType": seed.get("ActionType"), + "JuristicAct": seed.get("JuristicAct"), + "Action": seed.get("Action"), + "core_field_base": seed.get("core_field_base"), + "amount": seed.get("amount"), + "source_refs": _source_refs(seed), + }) + return compact + + def main() -> None: + _init() + salvage_notes: list[dict[str, Any]] = [] + guard_warnings: list[dict[str, Any]] = [] + stage_a_outer = read_json_doc(STAGE_A_PATH) + stage_a = _dict(_dict(stage_a_outer).get("stage_a_context") or stage_a_outer) + if stage_a.get("schema_version") != "task_c_bo_stage_a_context.v1" or stage_a.get("status") != "READY": + raise RuntimeError("Stage A context must be READY task_c_bo_stage_a_context.v1") + manifest = _dict(read_json_doc(MANIFEST_PATH)) + universe = { + "source_event_candidate_ids": set(_strings(manifest.get("event_candidate_ids"))), + "source_evidence_indexes": set(_strings(manifest.get("evidence_index_set"))), + "source_meeting_clause_ids": set(_strings(manifest.get("meeting_clause_ids"))), + } + if not universe["source_evidence_indexes"]: + raise RuntimeError("source universe manifest has no evidence indexes") + + # 1) 워커 출력 수용: 관용 파싱 -> sparse 검증 -> expander -> seed 파일 발행 + seed_objects: dict[str, dict[str, Any]] = {} + review_handoff_items: list[dict[str, Any]] = [] + for domain_id in DOMAIN_ORDER: + outer = parse_llm_json(WORKER_RAW_FIELDS[domain_id], domain_id, salvage_notes) + seed_obj = _dict(outer.get("stage_b_domain_bo_seed_output")) + if not seed_obj: + raise RuntimeError(f"{domain_id}: stage_b_domain_bo_seed_output missing") + validate_seed_object(seed_obj, domain_id, stage_a, guard_warnings) + cands = _list(seed_obj.get("bo_seed_candidates")) + expanded: list[dict[str, Any]] = [] + for idx, cand in enumerate(cands): + if not isinstance(cand, dict): + raise RuntimeError(f"{domain_id}.bo_seed_candidates[{idx}] must be object") + validate_candidate(cand, domain_id, idx) + full = expand_candidate(cand, domain_id) + # membership 검사 (PostB_1 이식 + 개정 defer 정책 §7): universe 밖 source ref는 hard BLOCK + prov = full["provenance"] + for key, allowed in ( + ("source_event_candidate_ids", universe["source_event_candidate_ids"]), + ("source_meeting_clause_ids", universe["source_meeting_clause_ids"]), + ("source_evidence_indexes", universe["source_evidence_indexes"]), + ): + if not allowed: + continue + bad = [v for v in _strings(prov.get(key)) if v not in allowed] + if bad: + raise RuntimeError(f"BLOCK: {domain_id}.{cand.get('candidate_ref')}: {key} outside Stage A universe: {bad}") + top_bad = [v for v in _strings(full.get("source_evidence_indexes")) if v not in universe["source_evidence_indexes"]] + if top_bad: + raise RuntimeError(f"BLOCK: {domain_id}.{cand.get('candidate_ref')}: source_evidence_indexes outside Stage A universe: {top_bad}") + expanded.append(full) + seed_obj = dict(seed_obj) + seed_obj["bo_seed_candidates"] = expanded + reviews = [expand_review_item(_dict(it), domain_id, idx + 1) for idx, it in enumerate(_list(seed_obj.get("domain_review_queue")))] + seed_obj["domain_review_queue"] = reviews + # transport 재구성 (워커 출력 금지 필드 — R0가 provenance에서 결정적으로 재구성) + seed_obj["transport_candidate_refs"] = [ + { + "candidate_ref": c.get("candidate_ref"), + "payload_index": i, + "source_event_candidate_ids": _strings(_dict(c.get("provenance")).get("source_event_candidate_ids")), + "source_evidence_indexes": _strings(c.get("source_evidence_indexes")), + "source_meeting_clause_ids": _strings(_dict(c.get("provenance")).get("source_meeting_clause_ids")), + } + for i, c in enumerate(expanded) + ] + metadata = _dict(seed_obj.get("transport_metadata")) + metadata.setdefault("stage", "Task_C_BO.Stage_B.DomainSeed") + metadata.setdefault("source_task", domain_id) + metadata.setdefault("domain_label", DOMAIN_LABELS[domain_id]) + metadata["slice_guard"] = { + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": stage_a.get("created_at_utc"), + "input_digests_sha256": stage_a.get("input_digests_sha256"), + "source_universe": { + "meeting_clause_ids": sorted(universe["source_meeting_clause_ids"]), + "event_candidate_ids": sorted(universe["source_event_candidate_ids"]), + "evidence_indexes": sorted(universe["source_evidence_indexes"]), + }, + "stage_b_worker_contract": { + "context_is_read_only": True, + "bo_id_finalization_allowed": False, + "candidate_generation_allowed": True, + }, + } + seed_obj["transport_metadata"] = metadata + seed_obj["handoff_guard"] = { + "schema_version": "task_c_bo_stage_b_seed_handoff_guard.v1", + "handoff_target": SEED_DOCS[domain_id], + "publisher_task": "Task_C_BO_R0_seed_reducer_and_exception_planner", + "source_worker_task": SEED_TASK_NAMES[domain_id], + "source_task": domain_id, + "source_contract": "direct_prev_task_json_output.stage_b_domain_bo_seed_output", + "source_prev_path": f"prev.{SEED_TASK_NAMES[domain_id]}.json_output", + "domain_id": domain_id, + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": stage_a.get("created_at_utc"), + "input_digests_sha256": stage_a.get("input_digests_sha256"), + } + write_doc(SEED_DOCS[domain_id], json.dumps({"stage_b_domain_bo_seed_output": seed_obj}, ensure_ascii=False, indent=2)) + seed_objects[domain_id] = seed_obj + for idx, item in enumerate(reviews, start=1): + issue = item.get("issue_type") if item.get("issue_type") in REVIEW_ISSUE_ENUM else "review_required" + owner = item.get("recommended_downstream_owner") + review_handoff_items.append({ + "review_id": item.get("review_id"), + "source_domain": domain_id, + "severity": item.get("severity") if item.get("severity") in ("SOFT_WARNING", "HARD_WARNING") else "SOFT_WARNING", + "issue_type": issue, + "source_event_candidate_ids": _strings(item.get("source_event_candidate_ids")), + "source_evidence_indexes": _strings(item.get("source_evidence_indexes")), + "source_meeting_clause_ids": _strings(item.get("source_meeting_clause_ids")), + "downstream_owner": owner if owner in DOWNSTREAM_OWNER_ENUM else "Stage2", + "template_note": "후속 단계에서 해당 review 항목의 증거와 법률상 의미를 재검토한다.", + }) + + # 2) ledger 구성 (PostB_1 이식) + input_candidate_total = 0 + seeds: list[dict[str, Any]] = [] + for domain_id in DOMAIN_ORDER: + cands = seed_objects[domain_id]["bo_seed_candidates"] + input_candidate_total += len(cands) + for cand in cands: + seeds.append(_seed_payload(cand, domain_id)) + if not seeds: + raise RuntimeError("no seed candidate from Stage B workers") + + seen_refs: set[str] = set() + ledger_candidates: list[dict[str, Any]] = [] + deterministic_decisions: list[dict[str, Any]] = [] + exceptions: list[dict[str, Any]] = [] + duplicate_buckets: dict[tuple[Any, ...], list[dict[str, Any]]] = {} + policy_review_counter = 0 + + def add_policy_review(domain_id: str, issue_type: str, refs: dict[str, list[str]], severity: str = "SOFT_WARNING") -> None: + nonlocal policy_review_counter + policy_review_counter += 1 + review_handoff_items.append({ + "review_id": f"R0:policy:{policy_review_counter:03d}", + "source_domain": domain_id, + "severity": severity, + "issue_type": issue_type, + "source_event_candidate_ids": refs.get("source_event_candidate_ids", []), + "source_evidence_indexes": refs.get("source_evidence_indexes", []), + "source_meeting_clause_ids": refs.get("source_meeting_clause_ids", []), + "downstream_owner": "Stage2", + "template_note": "결정적 defer 정책에 의해 보존된 검토 항목이다.", + }) + + for seed in seeds: + ref = seed.get("candidate_ref") + if not isinstance(ref, str) or not CANDIDATE_REF_RE.fullmatch(ref): + raise RuntimeError(f"invalid candidate_ref: {ref!r}") + if ref in seen_refs: + raise RuntimeError(f"duplicate candidate_ref: {ref}") + seen_refs.add(ref) + refs = _source_refs(seed) + # hard BLOCK: universe 밖 ref는 soft-strip 이후에도 남아있으면 안 된다 (방어적 재검사) + for key, values in refs.items(): + allowed = universe.get(key, set()) + outside = [v for v in values if allowed and v not in allowed] + if outside: + raise RuntimeError(f"{ref}: {key} outside Stage A universe: {outside}") + flags: list[str] = [] + # 결정적 defer 정책 (개선전략서 X-2): + if not refs["source_evidence_indexes"]: + flags.append("meeting_only_evidence_gap") + add_policy_review(seed.get("source_domain"), "meeting_only_evidence_gap", refs) + if seed.get("BOType") not in {"event", "state"} or not seed.get("ActionType") or not ( + seed.get("Action") or _dict(_dict(seed.get("extensions")).get("domain_payload")).get("action_summary") + ): + flags.append("schema_field_fallback") + add_policy_review(seed.get("source_domain"), "schema_field_fallback", refs) + link_candidates = _strings(_dict(seed.get("downstream_seed_refs")).get("prior_candidate_refs")) + if len(link_candidates) > 1: + flags.append("prior_link_ambiguous") + add_policy_review(seed.get("source_domain"), "prior_link_ambiguous", refs) + duplicate_buckets.setdefault(_duplicate_key(seed), []).append(seed) + ledger_candidates.append({ + "candidate_ref": ref, + "source_domain": seed.get("source_domain"), + "seed_payload": seed, + "source_refs": refs, + "deterministic_sort_key": _sort_key(seed), + "flags": flags, + }) + + # exact duplicate: provenance union 무손실이므로 canonical merge (v2 규칙 계승) + for bucket in duplicate_buckets.values(): + if len(bucket) <= 1: + continue + canonical = bucket[0].get("candidate_ref") + duplicates = [item.get("candidate_ref") for item in bucket[1:]] + deterministic_decisions.append({ + "decision_type": "EXACT_DUPLICATE_MERGE", + "canonical_candidate_ref": canonical, + "duplicate_candidate_refs": duplicates, + "basis": "exact duplicate deterministic rule (provenance-lossless union)", + }) + + # near duplicate: KEEP_SEPARATE + cluster id + review (LLM 금지 — defer 정책) + near_buckets: dict[tuple[Any, ...], list[dict[str, Any]]] = {} + for seed in seeds: + refs = _source_refs(seed) + key = ( + tuple(sorted(refs["source_evidence_indexes"])), + tuple(sorted(refs["source_event_candidate_ids"])), + seed.get("BOType"), + seed.get("ActionType"), + ) + near_buckets.setdefault(key, []).append(seed) + near_cluster_count = 0 + pack_field_conflicts: list[dict[str, Any]] = [] + for bucket in near_buckets.values(): + if len(bucket) <= 1 or len({_duplicate_key(s) for s in bucket}) <= 1: + continue + near_cluster_count += 1 + cluster_id = f"near-dup-{near_cluster_count:03d}" + cluster_refs = [str(s.get("candidate_ref")) for s in bucket] + for item in ledger_candidates: + if item["candidate_ref"] in cluster_refs: + item.setdefault("near_dup_cluster_id", cluster_id) + if "near_duplicate_kept_separate" not in item["flags"]: + item["flags"].append("near_duplicate_kept_separate") + add_policy_review(bucket[0].get("source_domain"), "near_duplicate_kept_separate", + {"source_evidence_indexes": _source_refs(bucket[0])["source_evidence_indexes"], + "source_event_candidate_ids": _source_refs(bucket[0])["source_event_candidate_ids"], + "source_meeting_clause_ids": []}) + # non-deferrable 판정(X-3 4중 조건): 같은 near cluster에서 BehaviorTime 또는 amount가 + # 서로 다른 non-null 값으로 충돌하면 writer가 단일 값을 고를 수 없으므로 pack에 수록 + times = {str(_dict(s.get("core_field_base")).get("BehaviorTime")) for s in bucket if _dict(s.get("core_field_base")).get("BehaviorTime")} + amounts = set() + for s in bucket: + av = s.get("amount") + if isinstance(av, dict) and av.get("value_text"): + amounts.add(str(av.get("value_text"))) + elif isinstance(av, str) and av.strip(): + amounts.add(av.strip()) + if len(times) > 1 or len(amounts) > 1: + pack_field_conflicts.append({ + "exception_id": f"EX-FIELD-{len(pack_field_conflicts) + 1:03d}", + "exception_type": "field_conflict", + "candidate_refs": cluster_refs, + "reason": "same-source candidates carry conflicting BehaviorTime/amount values", + "conflicting_values": {"BehaviorTime": sorted(times), "amount": sorted(amounts)}, + "compact_candidate_payload": _compact_exception_payload(bucket), + "allowed_decisions": ["KEEP_SEPARATE", "MERGE", "SPLIT", "DROP", "BLOCK_REVIEW"], + "escalation_flag": True, + }) + + exceptions.extend(pack_field_conflicts) + has_exceptions = bool(exceptions) + + # 3) conservation invariant (write 전) + merged_absorbed = sum(len(_strings(d.get("duplicate_candidate_refs"))) for d in deterministic_decisions) + if len(ledger_candidates) != input_candidate_total: + raise RuntimeError(f"ledger candidate count {len(ledger_candidates)} != input candidates {input_candidate_total}") + if len(seen_refs) != input_candidate_total: + raise RuntimeError("candidate_ref conservation failed") + + ledger = { + "postb_seed_ledger": { + "schema_version": "task_c_bo_postb_seed_ledger.v1", + "status": "READY", + "source_stage_a_created_at_utc": stage_a.get("created_at_utc"), + "input_digests_sha256": stage_a.get("input_digests_sha256"), + "source_universe": { + "source_event_candidate_ids": sorted(universe["source_event_candidate_ids"]), + "source_evidence_indexes": sorted(universe["source_evidence_indexes"]), + "source_meeting_clause_ids": sorted(universe["source_meeting_clause_ids"]), + }, + "stage_b_source_contract": { + "schema_version": "task_c_bo_stage_b_bo_seed_universe.compat_from_r0.v1", + "status": "READY", + "compatibility_source": "r0_seed_reducer.direct_worker_outputs", + }, + "ledger_candidates": sorted(ledger_candidates, key=lambda item: ( + item["deterministic_sort_key"].get("BehaviorTime") is None, + item["deterministic_sort_key"].get("BehaviorTime") or "", + item["deterministic_sort_key"].get("domain_order", 99), + item["deterministic_sort_key"].get("BOType") or "", + item["deterministic_sort_key"].get("ActionType") or "", + item["deterministic_sort_key"].get("JuristicActLabel") or "", + item["deterministic_sort_key"].get("Action") or "", + item["deterministic_sort_key"].get("candidate_ref") or "", + )), + "deterministic_decisions": deterministic_decisions, + "exception_pack": { + "has_exceptions": has_exceptions, + "clusters": [], + "field_conflicts": pack_field_conflicts, + "link_ambiguities": [], + "schema_risks": [], + }, + "audit_trace": { + "removed_or_sidecar_fields": [], + "source_membership_policy": "outside-universe source ref => hard BLOCK (defer 정책 §7)", + "normalization_notes": salvage_notes + guard_warnings, + }, + } + } + write_doc(LEDGER_PATH, json.dumps(ledger, ensure_ascii=False, indent=2)) + + pack = { + "schema_version": "stage1_part2_exception_pack.v1", + "has_exceptions": has_exceptions, + "exception_count": len(exceptions), + "exceptions": exceptions, + "budget": {"max_candidates_per_exception": 8, "max_payload_chars_per_candidate": 2000}, + } + write_doc(PACK_PATH, json.dumps(pack, ensure_ascii=False, indent=2)) + + handoff = { + "schema_version": "stage1_part2_review_handoff.v1", + "status": "PENDING_FINALIZE", + "review_items": review_handoff_items, + } + write_doc(REVIEW_HANDOFF_PATH, json.dumps(handoff, ensure_ascii=False, indent=2)) + + print(json.dumps({ + "status": "READY", + "message": "R0 seed reducer 완료: ledger/exception pack/review handoff 생성", + "ledger_path": LEDGER_PATH, + "exception_pack_path": PACK_PATH, + "review_handoff_path": REVIEW_HANDOFF_PATH, + "has_exceptions": has_exceptions, + "exception_count": len(exceptions), + "candidate_counts": { + "input": input_candidate_total, + "ledger": len(ledger_candidates), + "exact_duplicate_absorbed": merged_absorbed, + "near_dup_clusters": near_cluster_count, + }, + "review_item_count": len(review_handoff_items), + "salvage_count": len(salvage_notes), + }, ensure_ascii=False)) + + if __name__ == "__main__": + try: + main() + except Exception as exc: + print(json.dumps({"status": "FAILED", "message": "R0 seed reducer 실패: downstream 진행 금지", + "reason": str(exc)}, ensure_ascii=False)) + raise + - task_name: Task_C_BO_R1_exception_adjudicator + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: low + llm_verbosity: low + max_iterations: 1 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 20m + preflight: true + preflight_files: + - quality_gates/stage1_part2_exception_pack.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + + TASK_NAME: Task_C_BO_R1_exception_adjudicator + STAGE: PostB conditional exception adjudicator (Part 1 v3 GB 패턴) + MISSION: 결정적 reducer(R0)가 non-deferrable로 판정한 compact exception만 판정한다. 병합·최종 파일 작성·사실 창작은 하지 않는다. + + + + - 유일한 입력은 preflight로 제공된 `quality_gates/stage1_part2_exception_pack.json`이다. + - Stage A context, seed ledger 전문, raw evidence, meeting 원문을 읽거나 요청하지 않는다. + - pack에 없는 exception_id·candidate_ref·bh# id를 창작하지 않는다. + - BO.json, ledger, review handoff, signal 파일을 작성하지 않는다. + - 출력 파일은 `stage1_tmp/task_c_bo/postb_adjudication_decisions.json` 하나뿐이다. + + + + 1. preflight로 제공된 exception pack의 `has_exceptions`를 확인한다. + 2. `has_exceptions == false`이면: `write_file(overwrite=true)`로 아래 no-exception 객체를 `stage1_tmp/task_c_bo/postb_adjudication_decisions.json`에 저장하고 `"NO_EXCEPTIONS"`만 출력한 뒤 즉시 종료한다(terminate). 다른 어떤 파일도 읽지 않는다. + {"postb_exception_adjudication": {"schema_version": "task_c_bo_postb_exception_adjudication.v1", "status": "READY_NO_EXCEPTIONS", "exception_count": 0, "canonical_decisions": [], "field_decisions": [], "link_decisions": [], "semantic_gate_decisions": [], "blocked_review_items": []}} + 3. `has_exceptions == true`이면: 각 exception을 `compact_candidate_payload`만으로 판정한다. 추가 read는 금지된다. + 4. 판정 규칙: + - canonical decision: KEEP_SEPARATE, MERGE, SPLIT, DROP, BLOCK_REVIEW 중 하나. MERGE는 `input_candidate_refs`와 `merge_target_ref`를 명시한다. + - field conflict: 제공된 conflicting_values 중 하나를 `selected_value`로 선택하거나 BLOCK_REVIEW. 임의 값 창작 금지. + - link ambiguity: exception에 나열된 candidate ref 중 선택, NO_LINK, 또는 BLOCK_REVIEW. + - semantic risk: PASS, WARNING, BLOCK_REVIEW. + - compact payload로 확정할 수 없으면 반드시 `blocked_review_items`에 넣는다(확신 없는 확정 금지 — 인간 검토 라우팅). + 5. `write_file(overwrite=true)`로 결과를 저장한다. root는 `postb_exception_adjudication`이며 schema_version은 `task_c_bo_postb_exception_adjudication.v1`, status는 `READY`, `exception_count`는 판정한 exception 수다. 모든 decision은 pack의 `exception_id`를 인용한다. + 6. `"R1 예외 판정 완료 (decisions=<건수>)"`만 출력하고 작업을 끝낸다(terminate). + + + + - Stage 1은 법률효과·청구원인을 확정하지 않는다. 두 값을 모두 보존하거나 Stage 2로 defer할 수 있는 사안은 이미 R0가 결정적으로 처리했으므로, 여기 도달한 항목은 final writer가 단일 값을 선택해야만 진행되는 사안이다. + - 같은 source에 근거한 상충 값(BehaviorTime·amount)은: 원문 근거가 더 구체적인 쪽(payload의 core_field_base·amount 기재가 더 완전한 후보)을 선택하고, 우열을 가릴 수 없으면 BLOCK_REVIEW. + - KEEP_SEPARATE가 provenance를 보존하는 기본값이다. MERGE는 provenance 합집합이 무손실일 때만 선택한다. + - DROP은 어떤 경우에도 source 유일 후보에 적용하지 않는다. + + + + - exception pack 부재·파싱 불가: 즉시 중단하고 채팅으로만 보고한다. decisions 파일은 쓰지 않는다. + - tool 오류: 1회만 재시도. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + - task_name: Task_C_BO_F0_final_bo_compiler_gate_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: |- + #!/usr/bin/env python3 + # Task_C_BO_F0_final_bo_compiler_gate_writer (v3) + # PostB_3(final compiler) + PostB_4(final gate/writer) 통합. 입력은 파일 계약(ledger/decisions/stage_a). + # Spec: Part_2_Improvement_Strategy_Claude_v1.md §9 (bh# 규칙 N-6, Reason/PriorAct 정책 R-5) + from __future__ import annotations + import itertools + import json + import re + import sys + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + STAGE_A_PATH = "stage1_tmp/task_c_bo/stage_a_context.json" + LEDGER_PATH = "stage1_tmp/task_c_bo/postb_seed_ledger.json" + DECISIONS_PATH = "stage1_tmp/task_c_bo/postb_adjudication_decisions.json" + REVIEW_HANDOFF_PATH = "quality_gates/stage1_part2_review_handoff.json" + BUNDLE_COMPACT_PATH = "stage1_tmp/task_c_bo/postb_compiled_bundle_compact.json" + TARGET_NAME = "BO.json" + BH_ID_RE = re.compile(r"^bh[1-9][0-9]*$") + ACTION_TYPE_ENUM = { + "법률행위(legal acts)", + "준법률행위(quasi-legal acts)", + "사실행위(factual acts)", + "위법행위(unlawful acts)", + "소송행위(litigation acts)", + } + ALLOWED_TOP_LEVEL = { + "BO_ID", "id", "BOType", "ActionType", "JuristicAct", "Action", "Reason", + "PriorAct", "ReasonRefs", "Legal_Keywords", "core_field_base", "amount", + "EvidenceTitles", "Evidence", "source_evidence_indexes", "provenance", + "downstream_seed_refs", "extensions", + } + REQUIRED_TOP_LEVEL = { + "BO_ID", "id", "BOType", "ActionType", "JuristicAct", "Action", "Reason", + "PriorAct", "ReasonRefs", "Legal_Keywords", "core_field_base", + "EvidenceTitles", "Evidence", "source_evidence_indexes", "provenance", + "downstream_seed_refs", + } + CORE_KEYS = [ + "Performer", "PerformerType", "Action_proposal", "Subject", "Object", + "BehaviorTime", "TimeText", "TimePrecision", "StatementType", "Perspective", + ] + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-c-bo-f0-final-bo-compiler-gate-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse_mcp(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "id": _mid(), "method": "tools/call", + "params": {"name": name, "arguments": args}}, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse_mcp(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def read_json_doc(path: str) -> Any: + result = _call("read_docs", {"doc_names": [path]}) + content = result["result"].get("content", []) + text = content[0].get("text", "") if content else "" + try: + parsed = json.loads(text) + except json.JSONDecodeError as exc: + raise RuntimeError(f"Invalid read_docs envelope for {path}") from exc + if isinstance(parsed, dict) and "results" in parsed: + rows = parsed.get("results") or [] + if not rows or rows[0].get("error"): + raise RuntimeError(f"read_docs failed for {path}: {rows[:1]}") + inner = rows[0].get("content") or "" + if isinstance(inner, str): + try: + return json.loads(inner) + except json.JSONDecodeError: + obj, end = JSON_DECODER.raw_decode(inner.strip()) + if inner.strip()[end:].strip(): + raise RuntimeError(f"{path}: non-json tail") + return obj + return inner + return parsed + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + # ---------- PostB_3 이식 ---------- + def _field_decision_map(adj: dict[str, Any]) -> dict[tuple[str, str], Any]: + out: dict[tuple[str, str], Any] = {} + for item in _list(adj.get("field_decisions")): + if isinstance(item, dict) and item.get("candidate_ref") and item.get("field") and item.get("selected_value") != "BLOCK_REVIEW": + out[(str(item["candidate_ref"]), str(item["field"]))] = item.get("selected_value") + return out + + def _link_decision_map(adj: dict[str, Any]) -> dict[str, dict[str, Any]]: + out: dict[str, dict[str, Any]] = {} + for item in _list(adj.get("link_decisions")): + if isinstance(item, dict) and item.get("candidate_ref"): + out[str(item["candidate_ref"])] = item + return out + + def _decision_sets(ledger: dict[str, Any], adj: dict[str, Any], blockers: list[Any]) -> tuple[set[str], dict[str, str]]: + dropped: set[str] = set() + merge_into: dict[str, str] = {} + for decision in _list(ledger.get("deterministic_decisions")): + if not isinstance(decision, dict) or decision.get("decision_type") != "EXACT_DUPLICATE_MERGE": + continue + canonical = decision.get("canonical_candidate_ref") + for dup in _strings(decision.get("duplicate_candidate_refs")): + if canonical: + merge_into[dup] = str(canonical) + dropped.add(dup) + for decision in _list(adj.get("canonical_decisions")): + if not isinstance(decision, dict): + continue + kind = decision.get("decision") + refs = _strings(decision.get("input_candidate_refs")) + if kind == "DROP": + dropped.update(_strings(decision.get("drop_candidate_refs")) or refs) + elif kind == "MERGE": + target = decision.get("merge_target_ref") or (refs[0] if refs else None) + if target: + for ref in refs: + if ref != target: + merge_into[ref] = str(target) + dropped.add(ref) + elif kind == "BLOCK_REVIEW": + blockers.append(decision) + return dropped, merge_into + + def _sort_tuple(item: dict[str, Any]) -> tuple[Any, ...]: + key = _dict(item.get("deterministic_sort_key")) + return ( + key.get("BehaviorTime") is None, + key.get("BehaviorTime") or "", + key.get("domain_order", 99), + key.get("BOType") or "", + key.get("ActionType") or "", + key.get("JuristicActLabel") or "", + key.get("Action") or "", + key.get("candidate_ref") or "", + ) + + def _juristic(value: Any) -> dict[str, Any] | None: + if value is None: + return None + if isinstance(value, dict): + label = value.get("label") + return {"label": str(label).strip()} if label not in (None, "") else {"label": None} + text = str(value).strip() + return {"label": text} if text else None + + def _core(seed: dict[str, Any]) -> dict[str, Any]: + src = _dict(seed.get("core_field_base")) + out = {key: src.get(key) for key in CORE_KEYS} + if out.get("Action_proposal") is None and seed.get("Action"): + out["Action_proposal"] = seed.get("Action") + if out.get("StatementType") is None: + out["StatementType"] = seed.get("BOType") + if out.get("Perspective") is None: + out["Perspective"] = "plaintiff" + return out + + def _amount(value: Any) -> dict[str, Any] | None: + if value is None: + return None + if isinstance(value, dict): + return { + "value_text": value.get("value_text") or value.get("text"), + "numeric_value": value.get("numeric_value"), + "currency": value.get("currency"), + } + text = str(value).strip() + return {"value_text": text, "numeric_value": None, "currency": None} if text else None + + def _evidence_item(index: str, source: Any, gaps: list[Any], bo_id: str) -> dict[str, Any]: + obj = _dict(source) + title = obj.get("source_title") or obj.get("title") or obj.get("evidence_title") or obj.get("document_title") or index + relevant = obj.get("relevant_content") or obj.get("excerpt") or obj.get("summary") or obj.get("content") + if relevant in (None, ""): + gaps.append({"BO_ID": bo_id, "evidence_index": index, "gap": "missing_relevant_content"}) + relevant = None + return { + "evidence_index": index, + "source_title": str(title), + "priority_class": obj.get("priority_class") or obj.get("priority") or None, + "relevant_content": relevant, + "authentication_status": obj.get("authentication_status") or obj.get("auth_status") or None, + "corroboration": obj.get("corroboration") or None, + "selection_basis": "source_evidence_indexes membership", + } + + def _downstream_refs(seed: dict[str, Any]) -> dict[str, Any]: + src = _dict(seed.get("downstream_seed_refs")) + return { + "claim_group_seed_refs_proposed": _strings(src.get("claim_group_seed_refs_proposed") or src.get("claim_group_seed_refs")), + "canonical_theory_graph_seed_ref_proposed": src.get("canonical_theory_graph_seed_ref_proposed") or src.get("canonical_theory_graph_seed_ref"), + "legal_effect_structure_seed_ref_proposed": src.get("legal_effect_structure_seed_ref_proposed") or src.get("legal_effect_structure_seed_ref"), + } + + def _keywords(seed: dict[str, Any], juristic: dict[str, Any] | None) -> list[str]: + out = _strings(seed.get("Legal_Keywords")) + domain_payload = _dict(_dict(seed.get("extensions")).get("domain_payload")) + out.extend(_strings(domain_payload.get("legal_effect_tags"))) + if isinstance(juristic, dict) and juristic.get("label"): + out.append(str(juristic["label"])) + deduped: list[str] = [] + for item in out: + if item not in deduped: + deduped.append(item) + return deduped + + def _evidence_map_from_stage_a(stage_a: dict[str, Any]) -> dict[str, Any]: + evidence_map = _dict(stage_a.get("evidence_authority_map")) + by_index = _dict(evidence_map.get("by_evidence_index")) + if by_index: + return by_index + out: dict[str, Any] = {} + for item in _list(evidence_map.get("items")): + if isinstance(item, dict): + idx = item.get("evidence_index") or item.get("index") or item.get("id") + if idx is not None: + out[str(idx)] = item + return out + + def _stage_a_universe(stage_a: dict[str, Any]) -> dict[str, set[str]]: + event_map = _dict(stage_a.get("event_candidate_map")) + evidence_map = _dict(stage_a.get("evidence_authority_map")) + meeting_map = _dict(stage_a.get("meeting_clause_map")) + event_ids = set(_strings(event_map.get("candidate_id_set"))) + evidence_ids = set(_strings(evidence_map.get("evidence_index_set"))) + meeting_ids = set(_strings(meeting_map.get("clause_order"))) + event_ids.update(str(k) for k in _dict(event_map.get("by_event_candidate_id")).keys()) + evidence_ids.update(str(k) for k in _dict(evidence_map.get("by_evidence_index")).keys()) + meeting_ids.update(str(k) for k in _dict(meeting_map.get("by_clause_id")).keys()) + return { + "source_event_candidate_ids": event_ids, + "source_evidence_indexes": evidence_ids, + "source_meeting_clause_ids": meeting_ids, + } + + # ---------- PostB_4 이식: 게이트 ---------- + def _add_gate(gates: list[dict[str, Any]], key: str, passed: bool, detail: str) -> None: + gates.append({"gate_key": key, "status": "PASS" if passed else "FAILED", "detail": detail}) + + def _validate_item(item: Any, idx: int, ids: set[str], universe: dict[str, set[str]]) -> list[str]: + errors: list[str] = [] + if not isinstance(item, dict): + return [f"item {idx} must be object"] + extra = sorted(set(item.keys()) - ALLOWED_TOP_LEVEL) + missing = sorted(REQUIRED_TOP_LEVEL - set(item.keys())) + if extra: + errors.append(f"{item.get('BO_ID', idx)} additional fields: {extra}") + if missing: + errors.append(f"{item.get('BO_ID', idx)} missing fields: {missing}") + bo_id = item.get("BO_ID") + expected = f"bh{idx}" + if bo_id != expected or item.get("id") != bo_id or not isinstance(bo_id, str) or not BH_ID_RE.fullmatch(bo_id): + errors.append(f"BO_ID/id sequence mismatch: expected {expected}") + if item.get("BOType") not in {"event", "state"}: + errors.append(f"{bo_id}.BOType invalid") + if item.get("ActionType") not in ACTION_TYPE_ENUM: + errors.append(f"{bo_id}.ActionType invalid") + juristic = item.get("JuristicAct") + if juristic is not None and (not isinstance(juristic, dict) or set(juristic.keys()) != {"label"}): + errors.append(f"{bo_id}.JuristicAct invalid") + for key in ("Action", "Reason"): + if not isinstance(item.get(key), str) or not item.get(key).strip(): + errors.append(f"{bo_id}.{key} must be non-empty string") + prior = item.get("PriorAct") + if prior is not None and prior not in ids: + errors.append(f"{bo_id}.PriorAct references missing BO_ID") + for ref in _list(item.get("ReasonRefs")): + if ref not in ids: + errors.append(f"{bo_id}.ReasonRefs references missing BO_ID {ref}") + core = item.get("core_field_base") + if not isinstance(core, dict) or set(core.keys()) != set(CORE_KEYS): + errors.append(f"{bo_id}.core_field_base keys invalid") + amount = item.get("amount") + if amount is not None and (not isinstance(amount, dict) or set(amount.keys()) - {"value_text", "numeric_value", "currency"}): + errors.append(f"{bo_id}.amount invalid") + evidence = _list(item.get("Evidence")) + evidence_indexes = _strings(item.get("source_evidence_indexes")) + evidence_index_set: set[str] = set() + titles: list[str] = [] + for ev in evidence: + if not isinstance(ev, dict): + errors.append(f"{bo_id}.Evidence item must be object") + continue + required_ev = {"evidence_index", "source_title", "priority_class", "relevant_content", "authentication_status", "corroboration", "selection_basis"} + if set(ev.keys()) != required_ev: + errors.append(f"{bo_id}.Evidence item keys invalid") + if isinstance(ev.get("evidence_index"), str): + evidence_index_set.add(ev["evidence_index"]) + if isinstance(ev.get("source_title"), str) and ev.get("source_title") not in titles: + titles.append(ev["source_title"]) + if item.get("EvidenceTitles") != titles: + errors.append(f"{bo_id}.EvidenceTitles mismatch") + if set(evidence_indexes) != evidence_index_set: + errors.append(f"{bo_id}.source_evidence_indexes must equal Evidence[].evidence_index") + if universe["source_evidence_indexes"] and not set(evidence_indexes).issubset(universe["source_evidence_indexes"]): + errors.append(f"{bo_id}.source_evidence_indexes outside Stage A universe") + provenance = item.get("provenance") + if not isinstance(provenance, dict) or set(provenance.keys()) != {"source_event_candidate_ids", "source_meeting_clause_ids", "source_domain"}: + errors.append(f"{bo_id}.provenance invalid") + else: + if universe["source_event_candidate_ids"] and not set(_strings(provenance.get("source_event_candidate_ids"))).issubset(universe["source_event_candidate_ids"]): + errors.append(f"{bo_id}.provenance.source_event_candidate_ids outside Stage A universe") + if universe["source_meeting_clause_ids"] and not set(_strings(provenance.get("source_meeting_clause_ids"))).issubset(universe["source_meeting_clause_ids"]): + errors.append(f"{bo_id}.provenance.source_meeting_clause_ids outside Stage A universe") + downstream = item.get("downstream_seed_refs") + if not isinstance(downstream, dict) or set(downstream.keys()) != { + "claim_group_seed_refs_proposed", "canonical_theory_graph_seed_ref_proposed", "legal_effect_structure_seed_ref_proposed", + }: + errors.append(f"{bo_id}.downstream_seed_refs invalid") + extensions = item.get("extensions", {"domain_payload": {}}) + if extensions is not None and (not isinstance(extensions, dict) or set(extensions.keys()) - {"domain_payload"} or not isinstance(extensions.get("domain_payload", {}), dict)): + errors.append(f"{bo_id}.extensions invalid") + return errors + + def _fail(message: str, gates: list[dict[str, Any]], reasons: list[str]) -> None: + print(json.dumps({ + "status": "FAILED", + "message": message, + "write_target": TARGET_NAME, + "gate_results": gates, + "failure_reasons": reasons[:40], + }, ensure_ascii=False)) + sys.exit(1) + + def main() -> None: + _init() + gates: list[dict[str, Any]] = [] + stage_a = _dict(_dict(read_json_doc(STAGE_A_PATH)).get("stage_a_context") or read_json_doc(STAGE_A_PATH)) + if stage_a.get("schema_version") != "task_c_bo_stage_a_context.v1" or stage_a.get("status") != "READY": + _fail("Stage A freshness guard failed", gates, ["stage_a not READY"]) + universe = _stage_a_universe(stage_a) + evidence_map = _evidence_map_from_stage_a(stage_a) + ledger = _dict(_dict(read_json_doc(LEDGER_PATH)).get("postb_seed_ledger")) + if ledger.get("schema_version") != "task_c_bo_postb_seed_ledger.v1" or ledger.get("status") != "READY": + _fail("R0 seed ledger not READY", gates, [str(ledger.get("status"))]) + try: + adj_doc = read_json_doc(DECISIONS_PATH) + except Exception: + adj_doc = {"postb_exception_adjudication": {"schema_version": "task_c_bo_postb_exception_adjudication.v1", + "status": "READY_NO_EXCEPTIONS", "exception_count": 0, + "canonical_decisions": [], "field_decisions": [], + "link_decisions": [], "semantic_gate_decisions": [], + "blocked_review_items": []}} + adj = _dict(_dict(adj_doc).get("postb_exception_adjudication")) + if adj.get("schema_version") != "task_c_bo_postb_exception_adjudication.v1": + _fail("R1 adjudication schema mismatch", gates, [str(adj.get("schema_version"))]) + if adj.get("status") not in {"READY", "READY_NO_EXCEPTIONS"}: + _fail("R1 adjudication status invalid", gates, [str(adj.get("status"))]) + blocked = _list(adj.get("blocked_review_items")) + block_decisions: list[Any] = [] + field_decisions = _field_decision_map(adj) + link_decisions = _link_decision_map(adj) + dropped, merge_into = _decision_sets(ledger, adj, block_decisions) + if blocked or block_decisions: + _fail("R1 returned BLOCK_REVIEW items: 인간 검토 필요", gates, + [json.dumps(x, ensure_ascii=False)[:200] for x in (blocked + block_decisions)]) + + candidates = [item for item in _list(ledger.get("ledger_candidates")) if isinstance(item, dict)] + survivors = [item for item in candidates if item.get("candidate_ref") not in dropped] + survivors.sort(key=_sort_tuple) + if not survivors: + _fail("no surviving BO candidates after decisions", gates, []) + + candidate_ref_to_bo_id: dict[str, str] = {} + for idx, item in enumerate(survivors, start=1): + candidate_ref_to_bo_id[str(item["candidate_ref"])] = f"bh{idx}" + for source_ref, target_ref in merge_into.items(): + if target_ref in candidate_ref_to_bo_id: + candidate_ref_to_bo_id[source_ref] = candidate_ref_to_bo_id[target_ref] + + bo_items: list[dict[str, Any]] = [] + normalization_notes: list[dict[str, Any]] = [] + evidence_gaps: list[Any] = [] + prior_link_notes: list[dict[str, Any]] = [] + + for idx, ledger_item in enumerate(survivors, start=1): + seed = _dict(ledger_item.get("seed_payload")) + candidate_ref = str(ledger_item.get("candidate_ref")) + bo_id = f"bh{idx}" + bo_type = field_decisions.get((candidate_ref, "BOType"), seed.get("BOType")) + action_type = field_decisions.get((candidate_ref, "ActionType"), seed.get("ActionType")) + juristic = _juristic(field_decisions.get((candidate_ref, "JuristicAct.label"), seed.get("JuristicAct"))) + domain_payload = _dict(_dict(seed.get("extensions")).get("domain_payload")) + action = field_decisions.get((candidate_ref, "Action"), seed.get("Action") or domain_payload.get("action_summary") or _dict(seed.get("core_field_base")).get("Action_proposal")) + if bo_type not in {"event", "state"}: + normalization_notes.append({"candidate_ref": candidate_ref, "field": "BOType", "received": bo_type, "fallback": "event"}) + bo_type = "event" + if action_type not in ACTION_TYPE_ENUM: + normalization_notes.append({"candidate_ref": candidate_ref, "field": "ActionType", "received": action_type, "fallback": "사실행위(factual acts)"}) + action_type = "사실행위(factual acts)" + if not isinstance(action, str) or not action.strip(): + normalization_notes.append({"candidate_ref": candidate_ref, "field": "Action", "fallback": "source-backed BO"}) + action = f"{candidate_ref} source-backed BO" + + refs = _dict(ledger_item.get("source_refs")) + source_evidence_indexes = _strings(refs.get("source_evidence_indexes")) + evidence_items = [_evidence_item(eidx, evidence_map.get(eidx), evidence_gaps, bo_id) for eidx in source_evidence_indexes] + evidence_titles: list[str] = [] + for ev in evidence_items: + title = ev["source_title"] + if title not in evidence_titles: + evidence_titles.append(title) + + link = link_decisions.get(candidate_ref, {}) + reason_ref_candidates = _strings(link.get("reason_refs_candidate_refs")) + prior_candidate = link.get("prior_candidate_ref") + if prior_candidate == "NO_LINK": + prior_candidate = None + explicit_refs = _dict(seed.get("downstream_seed_refs")) + if not reason_ref_candidates: + reason_ref_candidates = _strings(explicit_refs.get("reason_refs_candidate_refs")) + if not prior_candidate: + prior_list = _strings(explicit_refs.get("prior_candidate_refs")) + if len(prior_list) == 1: + prior_candidate = prior_list[0] + elif len(prior_list) > 1: + # 결정적 defer 정책 (R-5): PriorAct 불명은 null 유지 + review note (blocker 아님) + prior_candidate = None + prior_link_notes.append({"candidate_ref": candidate_ref, "prior_candidates": prior_list, + "policy": "prior_link_ambiguous_kept_null"}) + reason_refs = [candidate_ref_to_bo_id[ref] for ref in reason_ref_candidates if ref in candidate_ref_to_bo_id and candidate_ref_to_bo_id[ref] != bo_id] + if prior_candidate and prior_candidate in candidate_ref_to_bo_id: + prior_act = candidate_ref_to_bo_id[prior_candidate] + elif reason_refs: + prior_act = reason_refs[0] + else: + prior_act = None + reason = "ReasonRefs에 기재된 선행 BO와 source evidence/event chain으로 연결됨" if reason_refs else "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO" + + bo_items.append({ + "BO_ID": bo_id, + "id": bo_id, + "BOType": bo_type, + "ActionType": action_type, + "JuristicAct": juristic, + "Action": str(action).strip(), + "Reason": reason, + "PriorAct": prior_act, + "ReasonRefs": reason_refs, + "Legal_Keywords": _keywords(seed, juristic), + "core_field_base": _core(seed), + "amount": _amount(seed.get("amount")), + "EvidenceTitles": evidence_titles, + "Evidence": evidence_items, + "source_evidence_indexes": source_evidence_indexes, + "provenance": { + "source_event_candidate_ids": _strings(refs.get("source_event_candidate_ids")), + "source_meeting_clause_ids": _strings(refs.get("source_meeting_clause_ids")), + "source_domain": seed.get("source_domain"), + }, + "downstream_seed_refs": _downstream_refs(seed), + "extensions": {"domain_payload": domain_payload}, + }) + + # ---------- conservation + 게이트 (PostB_4 이식) ---------- + total_ledger = len(candidates) + absorbed = len(dropped) + _add_gate(gates, "candidate_conservation", len(bo_items) + absorbed == total_ledger, + f"BO {len(bo_items)} + absorbed {absorbed} == ledger {total_ledger}") + _add_gate(gates, "bo_items_array_non_empty", len(bo_items) > 0, "bo_items must be non-empty array") + ids = {item["BO_ID"] for item in bo_items} + errors: list[str] = [] + for idx, item in enumerate(bo_items, start=1): + errors.extend(_validate_item(item, idx, ids, universe)) + _add_gate(gates, "bo_schema_and_reference_validation", not errors, "BO_JSON_Schema target validation") + if any(g["status"] != "PASS" for g in gates) or errors: + _fail("pre-write gate failed", gates, errors) + + payload = json.dumps(bo_items, ensure_ascii=False, indent=2) + "\n" + write_doc(TARGET_NAME, payload) + reread = read_json_doc(TARGET_NAME) + _add_gate(gates, "post_write_json_parse", isinstance(reread, list) and len(reread) == len(bo_items), "BO.json reread JSON parse") + if not isinstance(reread, list) or len(reread) != len(bo_items): + _fail("post-write verification failed", gates, ["reread mismatch"]) + + write_doc(BUNDLE_COMPACT_PATH, json.dumps({ + "schema_version": "task_c_bo_postb_compiled_bundle_compact.v1", + "status": "READY", + "candidate_ref_to_bo_id": candidate_ref_to_bo_id, + "bo_item_count": len(bo_items), + "normalization_notes": normalization_notes, + "evidence_gap_items": evidence_gaps, + "prior_link_notes": prior_link_notes, + }, ensure_ascii=False, indent=2)) + + # review handoff 최종 status 갱신 + try: + handoff = _dict(read_json_doc(REVIEW_HANDOFF_PATH)) + except Exception: + handoff = {"schema_version": "stage1_part2_review_handoff.v1", "review_items": []} + handoff["status"] = "FINALIZED" + handoff["bo_item_count"] = len(bo_items) + for note in prior_link_notes: + handoff.setdefault("review_items", []).append({ + "review_id": f"F0:prior:{note['candidate_ref']}", + "source_domain": None, + "severity": "SOFT_WARNING", + "issue_type": "prior_link_ambiguous", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Stage2", + "template_note": "선행행위 후보가 복수여서 PriorAct를 null로 보존하였다.", + }) + write_doc(REVIEW_HANDOFF_PATH, json.dumps(handoff, ensure_ascii=False, indent=2)) + + print(json.dumps({ + "status": "PASS", + "message": f"BO.json 작성 완료 (BO {len(bo_items)}건)", + "write_target": TARGET_NAME, + "bo_item_count": len(bo_items), + "absorbed_by_merge": absorbed, + "gate_results": gates, + "bundle_compact_path": BUNDLE_COMPACT_PATH, + }, ensure_ascii=False)) + + if __name__ == "__main__": + main() + - task_name: Task_C_BO_S0_signal_bundle_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 180 + code: |- + #!/usr/bin/env python3 + # Task_C_BO_S0_signal_bundle_writer (v3) + # Middle_Input 3개 task 통합: BO.json 1회 read로 signal 3파일 생성. + # 각 signal 빌더는 v2 원본 함수를 모듈 소스 그대로 exec하여 사용한다 (검증 로직 포함 무손실 이식). + # Spec: Part_2_Improvement_Strategy_Claude_v1.md §10 + from __future__ import annotations + import itertools + import json + import re + import sys + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + STAGE_A_PATH = "stage1_tmp/task_c_bo/stage_a_context.json" + LEDGER_PATH = "stage1_tmp/task_c_bo/postb_seed_ledger.json" + BUNDLE_COMPACT_PATH = "stage1_tmp/task_c_bo/postb_compiled_bundle_compact.json" + B4_SEED_PATH = "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B4_Actio.json" + BO_PATH = "BO.json" + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-c-bo-s0-signal-bundle-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse_mcp(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "id": _mid(), "method": "tools/call", + "params": {"name": name, "arguments": args}}, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse_mcp(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def read_json_doc(path: str) -> Any: + result = _call("read_docs", {"doc_names": [path]}) + content = result["result"].get("content", []) + text = content[0].get("text", "") if content else "" + try: + parsed = json.loads(text) + except json.JSONDecodeError as exc: + raise RuntimeError(f"Invalid read_docs envelope for {path}") from exc + if isinstance(parsed, dict) and "results" in parsed: + rows = parsed.get("results") or [] + if not rows or rows[0].get("error"): + raise RuntimeError(f"read_docs failed for {path}: {rows[:1]}") + inner = rows[0].get("content") or "" + if isinstance(inner, str): + try: + return json.loads(inner) + except json.JSONDecodeError: + obj, end = JSON_DECODER.raw_decode(inner.strip()) + if inner.strip()[end:].strip(): + raise RuntimeError(f"{path}: non-json tail") + return obj + return inner + return parsed + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + _MOD_ACTIO_SRC = r''' + ACTIO_SCOPE_ENUM = { + "preserved_claim", "fraudulent_act", "beneficiary_or_transferee", + "target_asset", "encumbrance", "remedy_or_cap", "defense", + } + + SUSPICION_LEVEL_ENUM = {"NONE", "LOW", "MEDIUM", "HIGH", "CRITICAL"} + + ASSET_KIND_ENUM = { + "real_property", "movable", "claim", "security_right", + "cash_or_deposit", "business_asset", "other", "unknown", + } + + ROLE_ENUM = { + "fraudulent_act_date", "debtor_act", "beneficiary", "transferee", + "remedy_or_cap", "defense", "encumbrance", + } + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def _first_string(*values: Any) -> str | None: + for value in values: + for item in _strings(value): + return item + return None + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + def _bo_by_id(bo_items: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: + return {str(item.get("BO_ID") or item.get("id")): item for item in bo_items if item.get("BO_ID") or item.get("id")} + + def _candidate_mapping(bundle: dict[str, Any]) -> dict[str, str]: + mapping = _dict(bundle.get("candidate_ref_to_bo_id")) + return {str(k): str(v) for k, v in mapping.items() if k and v} + + def _candidate_to_bo(ledger: dict[str, Any], mapping: dict[str, str]) -> dict[str, dict[str, Any]]: + out: dict[str, dict[str, Any]] = {} + for item in _list(ledger.get("ledger_candidates")): + if isinstance(item, dict) and item.get("candidate_ref") in mapping: + out[str(item["candidate_ref"])] = item + return out + + def _source_evidence(value: Any) -> list[str]: + if isinstance(value, dict): + refs = _dict(value.get("source_refs")) + if refs: + return _strings(refs.get("source_evidence_indexes")) + return _strings(value.get("source_evidence_indexes")) + return [] + + def _seed_payload(candidate: dict[str, Any]) -> dict[str, Any]: + return _dict(candidate.get("seed_payload")) + + def _domain_payload_from_seed(seed: dict[str, Any]) -> dict[str, Any]: + return _dict(_dict(seed.get("extensions")).get("domain_payload")) + + def _domain_payload_from_bo(bo: dict[str, Any]) -> dict[str, Any]: + return _dict(_dict(bo.get("extensions")).get("domain_payload")) + + def _flatten_strings(value: Any, limit: int = 10) -> list[str]: + out: list[str] = [] + + def visit(node: Any) -> None: + if len(out) >= limit: + return + if node is None: + return + if isinstance(node, (str, int, float, bool)): + text = str(node).strip() + if text and text not in out: + out.append(text) + return + if isinstance(node, list): + for child in node: + visit(child) + return + if isinstance(node, dict): + for key in ( + "asset_label", "asset", "property", "object", "Object", "description", + "value", "name", "party", "beneficiary", "transferee", "debtor_act", + "remedy", "defense", "encumbrance", "date", "claim_type", + ): + if key in node: + visit(node.get(key)) + visit(value) + return out[:limit] + + def _asset_kind_from_text(text: str) -> str: + lowered = text.lower() + if any(key in text for key in ["부동산", "토지", "건물", "아파트", "대지", "등기", "근저당"]): + return "real_property" + if any(key in text for key in ["채권", "대여금", "매매대금", "임대차보증금"]): + return "claim" + if any(key in text for key in ["근저당", "담보", "저당", "질권", "유치권"]): + return "security_right" + if any(key in text for key in ["예금", "현금", "계좌", "deposit", "cash"]) or "bank" in lowered: + return "cash_or_deposit" + if any(key in text for key in ["영업", "사업", "상호", "business"]) or "company" in lowered: + return "business_asset" + if any(key in text for key in ["동산", "차량", "기계", "movable"]): + return "movable" + return "unknown" + + def _target_property_candidates(payload: dict[str, Any], bo_ids: list[str], evidence: list[str]) -> list[dict[str, Any]]: + sources = [ + payload.get("asset_bindings"), + payload.get("asset_binding_hint"), + payload.get("valuation_matrix"), + payload.get("fraudulent_transfer_facts"), + payload.get("encumbrance_timeline"), + payload.get("target_property_candidates"), + ] + out: list[dict[str, Any]] = [] + for source in sources: + for item in _as_list(source): + labels = _flatten_strings(item, limit=4) + if not labels and isinstance(item, dict): + labels = _strings(item.get("asset_label") or item.get("property_label")) + for label in labels: + candidate = { + "asset_label": label, + "asset_kind": _asset_kind_from_text(label), + "related_bo_ids": bo_ids, + "related_evidence_indexes": evidence, + } + key = json.dumps(candidate, ensure_ascii=False, sort_keys=True) + if key not in {json.dumps(x, ensure_ascii=False, sort_keys=True) for x in out}: + out.append(candidate) + return out + + def _preserved_claim_candidates(payload: dict[str, Any], mapping: dict[str, str], bo_ids: list[str], evidence: list[str]) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for item in _as_list(payload.get("preserved_claim_bundle") or payload.get("preserved_claim_candidates")): + obj = _dict(item) + ref = _first_string(obj.get("candidate_ref"), obj.get("bo_candidate_ref")) + bo_id = mapping.get(ref) if ref else _first_string(obj.get("bo_id")) + if bo_id not in bo_ids and bo_ids: + bo_id = bo_id or None + out.append({ + "bo_id": bo_id, + "claim_type_candidate": _first_string(obj.get("claim_type_candidate"), obj.get("claim_type"), obj.get("type"), obj.get("label")), + "creditor_candidate": _first_string(obj.get("creditor_candidate"), obj.get("creditor"), obj.get("plaintiff")), + "debtor_candidate": _first_string(obj.get("debtor_candidate"), obj.get("debtor"), obj.get("defendant")), + "claim_date_candidate": _first_string(obj.get("claim_date_candidate"), obj.get("claim_date"), obj.get("date"), obj.get("BehaviorTime")), + "related_evidence_indexes": _dedupe(evidence + _strings(obj.get("related_evidence_indexes") or obj.get("source_evidence_indexes"))), + }) + if not out and bo_ids: + out.append({ + "bo_id": bo_ids[0], + "claim_type_candidate": None, + "creditor_candidate": None, + "debtor_candidate": None, + "claim_date_candidate": None, + "related_evidence_indexes": evidence, + }) + return out + + def _role_hint(role: str, value: Any, date: Any, bo_ids: list[str], evidence: list[str]) -> dict[str, Any] | None: + if role not in ROLE_ENUM: + return None + value_candidate = _first_string(value) + date_candidate = _first_string(date) + if value_candidate is None and date_candidate is None: + return None + return { + "role": role, + "value_candidate": value_candidate, + "date_candidate": date_candidate, + "related_bo_ids": bo_ids, + "related_evidence_indexes": evidence, + } + + def _actio_role_hints(payload: dict[str, Any], seed: dict[str, Any], bo_ids: list[str], evidence: list[str]) -> list[dict[str, Any]]: + hints: list[dict[str, Any]] = [] + for fact in _as_list(payload.get("fraudulent_transfer_facts")): + obj = _dict(fact) + for hint in ( + _role_hint("fraudulent_act_date", obj.get("date") or obj.get("act_date"), obj.get("date") or obj.get("act_date"), bo_ids, evidence), + _role_hint("debtor_act", obj.get("debtor_act") or obj.get("action") or obj.get("description"), obj.get("date"), bo_ids, evidence), + ): + if hint: + hints.append(hint) + for party in _as_list(payload.get("beneficiary_transferee_hints")): + obj = _dict(party) + role_text = "transferee" if "transferee" in str(obj).lower() or "양수" in str(obj) else "beneficiary" + hint = _role_hint(role_text, obj.get("party") or obj.get("name") or obj.get("value") or obj, obj.get("date"), bo_ids, evidence) + if hint: + hints.append(hint) + for role, key in [ + ("remedy_or_cap", "remedy_mode_hints"), + ("defense", "defense_rebuttal"), + ("encumbrance", "encumbrance_timeline"), + ]: + for item in _as_list(payload.get(key)): + obj = _dict(item) + hint = _role_hint(role, obj or item, obj.get("date") if obj else None, bo_ids, evidence) + if hint: + hints.append(hint) + if not hints: + action = _first_string(seed.get("Action"), _dict(seed.get("core_field_base")).get("Action_proposal")) + hint = _role_hint("debtor_act", action, _dict(seed.get("core_field_base")).get("BehaviorTime"), bo_ids, evidence) + if hint: + hints.append(hint) + deduped: list[dict[str, Any]] = [] + seen: set[str] = set() + for hint in hints: + key = json.dumps(hint, ensure_ascii=False, sort_keys=True) + if key not in seen: + seen.add(key) + deduped.append(hint) + return deduped + + def _scopes(payload: dict[str, Any]) -> list[str]: + scopes: list[str] = [] + if payload.get("preserved_claim_bundle") or payload.get("preserved_claim_candidates"): + scopes.append("preserved_claim") + if payload.get("fraudulent_transfer_facts") or "actio" in " ".join(_strings(payload.get("legal_effect_tags"))).lower(): + scopes.append("fraudulent_act") + if payload.get("beneficiary_transferee_hints"): + scopes.append("beneficiary_or_transferee") + if payload.get("asset_bindings") or payload.get("asset_binding_hint") or payload.get("valuation_matrix"): + scopes.append("target_asset") + if payload.get("encumbrance_timeline"): + scopes.append("encumbrance") + if payload.get("remedy_mode_hints"): + scopes.append("remedy_or_cap") + if payload.get("defense_rebuttal"): + scopes.append("defense") + return [scope for scope in _dedupe(scopes) if scope in ACTIO_SCOPE_ENUM] + + def _suspicion_level(scopes: list[str]) -> str: + core = {"preserved_claim", "fraudulent_act", "beneficiary_or_transferee"} + if core.issubset(set(scopes)): + return "HIGH" + if {"preserved_claim", "fraudulent_act"}.issubset(set(scopes)): + return "MEDIUM" + if scopes: + return "LOW" + return "NONE" + + def _reasons(scopes: list[str], payload: dict[str, Any], b4_review_queue: list[Any]) -> str: + fragments: list[str] = [] + if "preserved_claim" in scopes: + fragments.append("피보전채권 후보가 B4 seed/domain payload에서 확인됨") + if "fraudulent_act" in scopes: + fragments.append("채무자 처분행위 또는 사해행위 후보 사실이 확인됨") + if "beneficiary_or_transferee" in scopes: + fragments.append("수익자 또는 전득자 후보가 확인됨") + if "target_asset" in scopes: + fragments.append("취소·원상회복 대상 재산 후보가 확인됨") + if "encumbrance" in scopes: + fragments.append("담보권·가압류 등 부담 관련 단서가 확인됨") + if "remedy_or_cap" in scopes: + fragments.append("원상회복 방법 또는 가액배상 한도 단서가 확인됨") + if "defense" in scopes: + fragments.append("항변 또는 선의·악의 관련 반박 단서가 확인됨") + for item in b4_review_queue[:2]: + if isinstance(item, dict): + reason = _first_string(item.get("reason"), item.get("issue"), item.get("review_reason")) + if reason: + fragments.append(f"B4 review queue: {reason}") + return ", ".join(fragments) if fragments else "사해행위취소 관련 구조화 단서가 충분히 확인되지 않음" + + def _validate(payload: dict[str, Any]) -> None: + if payload.get("schema_version") != "actio_case_signals.v1": + raise ValueError("schema_version mismatch") + if payload.get("status") not in {"READY", "READY_WITH_REVIEW", "EMPTY", "FAILED"}: + raise ValueError("invalid status") + signals = payload.get("actio_case_signals") + if not isinstance(signals, list): + raise ValueError("actio_case_signals must be array") + for idx, item in enumerate(signals): + if not isinstance(item, dict): + raise ValueError(f"signal[{idx}] must be object") + for key in [ + "signal_id", "actio_scope", "suspicion_level", "suspicion_reasons", + "related_bo_ids", "related_evidence_indexes", "target_property_candidates", + "preserved_claim_candidates", "actio_role_hints", + ]: + if key not in item: + raise ValueError(f"signal[{idx}] missing {key}") + if not isinstance(item["signal_id"], str): + raise ValueError(f"signal[{idx}].signal_id must be string") + if item["suspicion_level"] not in SUSPICION_LEVEL_ENUM: + raise ValueError(f"signal[{idx}].suspicion_level invalid") + if not isinstance(item["suspicion_reasons"], str): + raise ValueError(f"signal[{idx}].suspicion_reasons must be string") + if any(scope not in ACTIO_SCOPE_ENUM for scope in _list(item["actio_scope"])): + raise ValueError(f"signal[{idx}].actio_scope invalid") + for field in ["related_bo_ids", "related_evidence_indexes"]: + if not all(isinstance(x, str) for x in _list(item[field])): + raise ValueError(f"signal[{idx}].{field} must be string array") + for prop in _list(item["target_property_candidates"]): + if not isinstance(prop, dict): + raise ValueError(f"signal[{idx}].target_property_candidates item must be object") + for key in ["asset_label", "asset_kind", "related_bo_ids", "related_evidence_indexes"]: + if key not in prop: + raise ValueError(f"target_property_candidate missing {key}") + if prop["asset_kind"] not in ASSET_KIND_ENUM: + raise ValueError("target_property_candidate.asset_kind invalid") + for claim in _list(item["preserved_claim_candidates"]): + if not isinstance(claim, dict): + raise ValueError(f"signal[{idx}].preserved_claim_candidates item must be object") + for key in [ + "bo_id", "claim_type_candidate", "creditor_candidate", + "debtor_candidate", "claim_date_candidate", "related_evidence_indexes", + ]: + if key not in claim: + raise ValueError(f"preserved_claim_candidate missing {key}") + for hint in _list(item["actio_role_hints"]): + if not isinstance(hint, dict): + raise ValueError(f"signal[{idx}].actio_role_hints item must be object") + for key in ["role", "value_candidate", "date_candidate", "related_bo_ids", "related_evidence_indexes"]: + if key not in hint: + raise ValueError(f"actio_role_hint missing {key}") + if hint["role"] not in ROLE_ENUM: + raise ValueError("actio_role_hint.role invalid") + + + def build_signals(ledger, compiled, b4, bo_items): + bo_by_id = _bo_by_id(bo_items) + mapping = _candidate_mapping(compiled) + candidates_by_ref = _candidate_to_bo(ledger, mapping) + signals = [] + review_needed = False + for seed_candidate in _list(b4.get("bo_seed_candidates")): + if not isinstance(seed_candidate, dict): + continue + candidate_ref = str(seed_candidate.get("candidate_ref") or "") + bo_id = mapping.get(candidate_ref) + bo_ids = [bo_id] if bo_id else [] + ledger_item = candidates_by_ref.get(candidate_ref, {}) + ledger_seed = _seed_payload(ledger_item) + seed = seed_candidate if seed_candidate else ledger_seed + payload = _domain_payload_from_seed(seed) + if not payload and ledger_seed: + payload = _domain_payload_from_seed(ledger_seed) + if bo_id and bo_id in bo_by_id: + payload = {**payload, **_domain_payload_from_bo(bo_by_id[bo_id])} + bo_evidence = _source_evidence(bo_by_id.get(bo_id, {})) if bo_id else [] + evidence = _dedupe(_source_evidence(seed_candidate) + _source_evidence(ledger_item) + bo_evidence) + scopes = _scopes(payload) + if not scopes and candidate_ref: + scopes = ["fraudulent_act"] + review_needed = True + related_bo_ids = _dedupe(bo_ids + [x for x in _strings(seed_candidate.get("related_bo_ids")) if x in bo_by_id]) + if not related_bo_ids: + review_needed = True + target_properties = _target_property_candidates(payload, related_bo_ids, evidence) + preserved_claims = _preserved_claim_candidates(payload, mapping, related_bo_ids, evidence) + role_hints = _actio_role_hints(payload, seed, related_bo_ids, evidence) + if not target_properties or not preserved_claims or not role_hints: + review_needed = True + if scopes or related_bo_ids or target_properties or preserved_claims or role_hints: + signals.append({ + "signal_id": f"actio-signal-{len(signals) + 1:03d}", + "actio_scope": scopes, + "suspicion_level": _suspicion_level(scopes), + "suspicion_reasons": _reasons(scopes, payload, _list(b4.get("domain_review_queue"))), + "related_bo_ids": related_bo_ids, + "related_evidence_indexes": evidence, + "target_property_candidates": target_properties, + "preserved_claim_candidates": preserved_claims, + "actio_role_hints": role_hints, + }) + status = "EMPTY" if not signals else ("READY_WITH_REVIEW" if review_needed or b4.get("status") == "READY_WITH_REVIEW" else "READY") + payload = {"schema_version": "actio_case_signals.v1", "status": status, "actio_case_signals": signals} + _validate(payload) + return payload + + ''' + + _MOD_LIABILITY_SRC = r''' + LIABILITY_ENUM = { + "money_claim", "commercial_successor", "secured_debt", "registry_invalidity", + "land_use_gain", "actio_related", "succession_related", "notice_or_lien_related", + "defense_or_setoff", "other", + } + + PARTY_ROLE_ENUM = {"plaintiff", "defendant", "creditor", "debtor", "liable_party"} + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def _first_string(*values: Any) -> str | None: + for value in values: + for item in _strings(value): + return item + return None + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + def _source_evidence(bo: dict[str, Any]) -> list[str]: + return _strings(bo.get("source_evidence_indexes")) + + def _domain_payload(bo: dict[str, Any]) -> dict[str, Any]: + return _dict(_dict(bo.get("extensions")).get("domain_payload")) + + def _candidate_mapping(bundle: dict[str, Any]) -> dict[str, str]: + mapping = _dict(bundle.get("candidate_ref_to_bo_id")) + return {str(k): str(v) for k, v in mapping.items() if k and v} + + def _candidate_ref_for_bo(bo_id: str, mapping: dict[str, str]) -> str | None: + for ref, mapped in mapping.items(): + if mapped == bo_id: + return ref + return None + + def _liability_type(bo: dict[str, Any]) -> str: + payload = _domain_payload(bo) + source_domain = str(_dict(bo.get("provenance")).get("source_domain") or "").lower() + text = " ".join([ + source_domain, + str(bo.get("Action") or ""), + str(bo.get("ActionType") or ""), + str(_dict(bo.get("JuristicAct")).get("label") or ""), + " ".join(_strings(bo.get("Legal_Keywords"))), + " ".join(_strings(payload.get("legal_effect_tags"))), + str(payload.get("domain_legal_effect_tag") or ""), + ]).lower() + if "b4" in source_domain or "actio" in text or "사해" in text: + return "actio_related" + if "successor" in text or "영업양수" in text or "상호속용" in text: + return "commercial_successor" + if "b1" in source_domain or "대여금" in text or "매매대금" in text or "채권" in text: + return "money_claim" + if "registry_invalid" in text or "등기무효" in text or "말소" in text: + return "registry_invalidity" + if "secured" in text or "근저당" in text or "담보" in text or "유치권" in text: + return "secured_debt" + if "land" in text or "valuation" in text or "사용이익" in text or "부당이득" in text: + return "land_use_gain" + if "succession" in text or "상속" in text or "승계" in text: + return "succession_related" + if "notice" in text or "lien" in text or "통지" in text or "유치권" in text: + return "notice_or_lien_related" + if "defense" in text or "항변" in text or "상계" in text: + return "defense_or_setoff" + return "other" + + def _normalize_role(value: Any) -> str | None: + text = str(value or "").strip().lower() + if not text: + return None + if text in PARTY_ROLE_ENUM: + return text + if "plaintiff" in text or "원고" in text: + return "plaintiff" + if "defendant" in text or "피고" in text: + return "defendant" + if "creditor" in text or "채권자" in text: + return "creditor" + if "debtor" in text or "채무자" in text: + return "debtor" + if "liable" in text or "책임" in text or "이행" in text: + return "liable_party" + return None + + def _party_roles(bo: dict[str, Any]) -> list[dict[str, Any]]: + bo_id = str(bo.get("BO_ID") or bo.get("id") or "") + payload = _domain_payload(bo) + roles: list[dict[str, Any]] = [] + for item in _as_list(payload.get("party_roles") or payload.get("parties_hint") or payload.get("liability_party_hints")): + obj = _dict(item) + party = _first_string(obj.get("party"), obj.get("name"), obj.get("value")) + role = _normalize_role(obj.get("role") or obj.get("party_role")) + if party and role: + roles.append({"party": party, "role": role, "bo_ids": [bo_id] if bo_id else []}) + core = _dict(bo.get("core_field_base")) + performer = _first_string(core.get("Performer"), bo.get("Performer")) + subject = _first_string(core.get("Subject"), bo.get("Subject")) + if performer and not any(item["party"] == performer for item in roles): + roles.append({"party": performer, "role": "liable_party", "bo_ids": [bo_id] if bo_id else []}) + if subject and not any(item["party"] == subject for item in roles): + role = "creditor" if _liability_type(bo) in {"money_claim", "actio_related"} else "liable_party" + roles.append({"party": subject, "role": role, "bo_ids": [bo_id] if bo_id else []}) + deduped: list[dict[str, Any]] = [] + seen: set[str] = set() + for item in roles: + key = json.dumps(item, ensure_ascii=False, sort_keys=True) + if key not in seen: + seen.add(key) + deduped.append(item) + return deduped + + def _claim_group_id(bo: dict[str, Any]) -> str | None: + refs = _dict(bo.get("downstream_seed_refs")) + return _first_string(refs.get("claim_group_seed_refs_proposed") or refs.get("claim_group_seed_refs")) + + def _liability_group_id(bo: dict[str, Any], candidate_ref: str | None) -> str | None: + payload = _domain_payload(bo) + return _first_string(payload.get("liability_group_id"), payload.get("liability_group_ref"), candidate_ref) + + def _linked_structure_ids(bo: dict[str, Any]) -> list[str]: + refs = _dict(bo.get("downstream_seed_refs")) + return _strings(refs.get("legal_effect_structure_seed_ref_proposed") or refs.get("linked_structure_ids")) + + def _validate(payload: dict[str, Any]) -> None: + if payload.get("schema_version") != "case_liability_signals.v1": + raise ValueError("schema_version mismatch") + if payload.get("status") not in {"READY", "READY_WITH_REVIEW", "EMPTY", "FAILED"}: + raise ValueError("invalid status") + signals = payload.get("case_liability_signals") + if not isinstance(signals, list): + raise ValueError("case_liability_signals must be array") + for idx, item in enumerate(signals): + if not isinstance(item, dict): + raise ValueError(f"signal[{idx}] must be object") + for key in [ + "liability_candidate_type", "related_bo_ids", "related_evidence_indexes", + "party_roles", "claim_group_id", "liability_group_id", "linked_structure_ids", + ]: + if key not in item: + raise ValueError(f"signal[{idx}] missing {key}") + if item["liability_candidate_type"] not in LIABILITY_ENUM: + raise ValueError(f"signal[{idx}].liability_candidate_type invalid") + for field in ["related_bo_ids", "related_evidence_indexes", "linked_structure_ids"]: + if not isinstance(item[field], list) or not all(isinstance(x, str) for x in item[field]): + raise ValueError(f"signal[{idx}].{field} must be string array") + for role in _list(item["party_roles"]): + if not isinstance(role, dict): + raise ValueError(f"signal[{idx}].party_roles item must be object") + for key in ["party", "role", "bo_ids"]: + if key not in role: + raise ValueError(f"party_role missing {key}") + if not isinstance(role["party"], str) or role["role"] not in PARTY_ROLE_ENUM: + raise ValueError("party_role type invalid") + if not isinstance(role["bo_ids"], list) or not all(isinstance(x, str) for x in role["bo_ids"]): + raise ValueError("party_role.bo_ids must be string array") + + + def build_signals(ledger, compiled, b4, bo_items): + mapping = _candidate_mapping(compiled) + signals = [] + review_needed = b4.get("status") == "READY_WITH_REVIEW" + for bo in bo_items: + bo_id = str(bo.get("BO_ID") or bo.get("id") or "") + if not bo_id: + review_needed = True + continue + candidate_ref = _candidate_ref_for_bo(bo_id, mapping) + party_roles = _party_roles(bo) + if not party_roles: + review_needed = True + liability_group_id = _liability_group_id(bo, candidate_ref) + if liability_group_id is None: + review_needed = True + signals.append({ + "liability_candidate_type": _liability_type(bo), + "related_bo_ids": [bo_id], + "related_evidence_indexes": _source_evidence(bo), + "party_roles": party_roles, + "claim_group_id": _claim_group_id(bo), + "liability_group_id": liability_group_id, + "linked_structure_ids": _linked_structure_ids(bo), + }) + status = "EMPTY" if not signals else ("READY_WITH_REVIEW" if review_needed else "READY") + payload = {"schema_version": "case_liability_signals.v1", "status": status, "case_liability_signals": signals} + _validate(payload) + return payload + + ''' + + _MOD_LEGAL_EFFECT_SRC = r''' + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + def _domain_payload(bo: dict[str, Any]) -> dict[str, Any]: + return _dict(_dict(bo.get("extensions")).get("domain_payload")) + + def _source_evidence(bo: dict[str, Any]) -> list[str]: + return _strings(bo.get("source_evidence_indexes")) + + def _candidate_structure_types(bo: dict[str, Any]) -> list[str]: + payload = _domain_payload(bo) + juristic = _dict(bo.get("JuristicAct")) + candidates: list[str] = [] + candidates.extend(_strings(payload.get("candidate_structure_types"))) + candidates.extend(_strings(payload.get("legal_effect_tags"))) + candidates.extend(_strings(payload.get("domain_legal_effect_tag"))) + candidates.extend(_strings(bo.get("Legal_Keywords"))) + candidates.extend(_strings(bo.get("BOType"))) + candidates.extend(_strings(bo.get("ActionType"))) + candidates.extend(_strings(juristic.get("label"))) + source_domain = _dict(bo.get("provenance")).get("source_domain") + if source_domain: + candidates.append(str(source_domain)) + return _dedupe(candidates) or ["generic_legal_effect"] + + def _linked_structure_ids(bo: dict[str, Any]) -> list[str]: + refs = _dict(bo.get("downstream_seed_refs")) + payload = _domain_payload(bo) + return _dedupe( + _strings(refs.get("legal_effect_structure_seed_ref_proposed")) + + _strings(refs.get("linked_structure_ids")) + + _strings(payload.get("linked_structure_ids")) + ) + + def _validate(payload: dict[str, Any]) -> None: + if payload.get("schema_version") != "legal_effect_signals.v1": + raise ValueError("schema_version mismatch") + if payload.get("status") not in {"READY", "READY_WITH_REVIEW", "EMPTY", "FAILED"}: + raise ValueError("invalid status") + routes = payload.get("bo_legal_effect_routes") + if not isinstance(routes, list): + raise ValueError("bo_legal_effect_routes must be array") + for idx, item in enumerate(routes): + if not isinstance(item, dict): + raise ValueError(f"route[{idx}] must be object") + for key in ["bo_id", "candidate_structure_types", "linked_structure_ids", "related_evidence_indexes"]: + if key not in item: + raise ValueError(f"route[{idx}] missing {key}") + if not isinstance(item["bo_id"], str): + raise ValueError(f"route[{idx}].bo_id must be string") + for field in ["candidate_structure_types", "linked_structure_ids", "related_evidence_indexes"]: + if not isinstance(item[field], list) or not all(isinstance(x, str) for x in item[field]): + raise ValueError(f"route[{idx}].{field} must be string array") + + + def build_signals(ledger, compiled, b4, bo_items): + routes = [] + review_needed = b4.get("status") == "READY_WITH_REVIEW" + for bo in bo_items: + bo_id = str(bo.get("BO_ID") or bo.get("id") or "") + if not bo_id: + review_needed = True + continue + linked = _linked_structure_ids(bo) + if not linked: + review_needed = True + routes.append({ + "bo_id": bo_id, + "candidate_structure_types": _candidate_structure_types(bo), + "linked_structure_ids": linked, + "related_evidence_indexes": _source_evidence(bo), + }) + status = "EMPTY" if not routes else ("READY_WITH_REVIEW" if review_needed else "READY") + payload = {"schema_version": "legal_effect_signals.v1", "status": status, "bo_legal_effect_routes": routes} + _validate(payload) + return payload + + ''' + + + def _load_module(src: str) -> dict[str, Any]: + ns: dict[str, Any] = {"json": json, "re": re, "Any": Any} + exec(compile(src, "", "exec"), ns) + return ns + + def main() -> None: + _init() + stage_a_doc = read_json_doc(STAGE_A_PATH) + stage_a = stage_a_doc.get("stage_a_context") if isinstance(stage_a_doc, dict) else None + if not isinstance(stage_a, dict) or stage_a.get("status") != "READY": + raise RuntimeError("Stage A context must be READY") + ledger = (read_json_doc(LEDGER_PATH) or {}).get("postb_seed_ledger") + if not isinstance(ledger, dict) or ledger.get("status") != "READY": + raise RuntimeError("R0 seed ledger must be READY") + compiled = read_json_doc(BUNDLE_COMPACT_PATH) + if not isinstance(compiled, dict) or compiled.get("status") != "READY": + raise RuntimeError("F0 compiled bundle compact must be READY") + b4_doc = read_json_doc(B4_SEED_PATH) + b4 = b4_doc.get("stage_b_domain_bo_seed_output") if isinstance(b4_doc, dict) else None + if not isinstance(b4, dict) or b4.get("domain_id") != "B4_Actio": + raise RuntimeError("B4 seed artifact invalid") + if b4.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise RuntimeError("B4 seed status invalid") + bo_items = read_json_doc(BO_PATH) + if not isinstance(bo_items, list) or not bo_items: + raise RuntimeError("BO.json root must be non-empty array") + bo_items = [item for item in bo_items if isinstance(item, dict)] + + results = [] + for target, src in ( + ("actio_case_signals.json", _MOD_ACTIO_SRC), + ("case_liability_signals.json", _MOD_LIABILITY_SRC), + ("legal_effect_signals.json", _MOD_LEGAL_EFFECT_SRC), + ): + ns = _load_module(src) + payload = ns["build_signals"](ledger, compiled, b4, bo_items) + write_doc(target, json.dumps(payload, ensure_ascii=False, indent=2) + "\n") + count_key = next(k for k in payload if isinstance(payload[k], list)) + results.append({"written_file": target, "payload_status": payload.get("status"), + "item_count": len(payload[count_key])}) + + print(json.dumps({ + "status": "READY", + "message": "signal 3종 생성 완료", + "schema_version": "stage1_middle_input_signal_writer.v2", + "results": results, + }, ensure_ascii=False)) + + if __name__ == "__main__": + try: + main() + except Exception as exc: + print(json.dumps({"status": "FAILED", "message": "S0 signal bundle writer 실패", + "reason": str(exc)}, ensure_ascii=False)) + raise + task_procedure: + IN: + nexts: + - Task_C_BO_A0_context_and_domain_slice_compiler + wait_until: [] + Task_C_BO_A0_context_and_domain_slice_compiler: + nexts: + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + wait_until: + - IN + Task_C_BO_Stage_B_B1_Money_Successor: + nexts: + - Task_C_BO_R0_seed_reducer_and_exception_planner + wait_until: + - Task_C_BO_A0_context_and_domain_slice_compiler + Task_C_BO_Stage_B_B2_Secured_Registry: + nexts: + - Task_C_BO_R0_seed_reducer_and_exception_planner + wait_until: + - Task_C_BO_A0_context_and_domain_slice_compiler + Task_C_BO_Stage_B_B3_Land_Valuation: + nexts: + - Task_C_BO_R0_seed_reducer_and_exception_planner + wait_until: + - Task_C_BO_A0_context_and_domain_slice_compiler + Task_C_BO_Stage_B_B4_Actio: + nexts: + - Task_C_BO_R0_seed_reducer_and_exception_planner + wait_until: + - Task_C_BO_A0_context_and_domain_slice_compiler + Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense: + nexts: + - Task_C_BO_R0_seed_reducer_and_exception_planner + wait_until: + - Task_C_BO_A0_context_and_domain_slice_compiler + Task_C_BO_R0_seed_reducer_and_exception_planner: + nexts: + - Task_C_BO_R1_exception_adjudicator + wait_until: + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + Task_C_BO_R1_exception_adjudicator: + nexts: + - Task_C_BO_F0_final_bo_compiler_gate_writer + wait_until: + - Task_C_BO_R0_seed_reducer_and_exception_planner + Task_C_BO_F0_final_bo_compiler_gate_writer: + nexts: + - Task_C_BO_S0_signal_bundle_writer + wait_until: + - Task_C_BO_R1_exception_adjudicator + Task_C_BO_S0_signal_bundle_writer: + nexts: + - OUT + wait_until: + - Task_C_BO_F0_final_bo_compiler_gate_writer + OUT: + nexts: [] + wait_until: + - Task_C_BO_S0_signal_bundle_writer diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_2_Codex_v3.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_2_Codex_v3.yml new file mode 100644 index 00000000..ea89beb1 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_2_Codex_v3.yml @@ -0,0 +1,4013 @@ +--- +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_2 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Part 2 + version: v.3 + Stages: + - name: stage1_BO_시그널_생성 + description: BO 생성, 시그널 생성 + tools: + mcpServers: + localdocs: + type: streamable-http + url: http://mcp-localdocs:8012/mcp + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + tasks: + - task_name: Task_C_BO_A0_context_domain_slice_compiler + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_IDS = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + def _next_id() -> int: + return next(MSG_IDS) + + def _parse_json_first(raw: str, label: str, allow_trailing: bool = False) -> Any: + text = (raw or "").lstrip("\ufeff").strip() + if not text: + raise RuntimeError(f"{label}: empty JSON text") + try: + return json.loads(text) + except json.JSONDecodeError as first_error: + try: + value, end = JSON_DECODER.raw_decode(text) + except json.JSONDecodeError as second_error: + raise RuntimeError(f"{label}: invalid JSON") from second_error + if not allow_trailing and text[end:].strip(): + raise RuntimeError(f"{label}: trailing non-JSON data") from first_error + return value + + def _parse_rpc(raw: str) -> dict[str, Any]: + events: list[dict[str, Any]] = [] + for line in (raw or "").splitlines(): + if not line.startswith("data:"): + continue + candidate = line[5:].strip() + if not candidate or candidate == "[DONE]": + continue + try: + parsed = json.loads(candidate) + except json.JSONDecodeError: + continue + if isinstance(parsed, dict): + events.append(parsed) + if events: + return events[-1] + parsed = _parse_json_first(raw, "MCP response", allow_trailing=True) + if not isinstance(parsed, dict): + raise RuntimeError("MCP response root must be object") + return parsed + + def _init(task_name: str) -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": task_name, + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _call(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_rpc(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP {name} failed: {payload['error']}") + result = payload.get("result") + if not isinstance(result, dict): + raise RuntimeError(f"MCP {name} returned no result") + if result.get("isError") is True: + raise RuntimeError(f"MCP {name} returned isError") + return payload + + def _unwrap(payload: dict[str, Any], label: str) -> Any: + result = payload.get("result") or {} + content = result.get("content") or [] + texts = [block.get("text") for block in content if isinstance(block, dict) and block.get("text")] + if not texts: + raise RuntimeError(f"{label}: empty MCP content") + raw = texts[0] + try: + return _parse_json_first(raw, label, allow_trailing=True) + except RuntimeError: + return raw + + def _extract_doc(payload: dict[str, Any], doc_name: str) -> str: + outer = _unwrap(payload, f"read_docs:{doc_name}") + if isinstance(outer, dict) and isinstance(outer.get("results"), list): + for item in outer["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + if name and not (str(name) == doc_name or str(name).endswith("/" + doc_name)): + continue + value = item.get("content") + if value is None: + value = item.get("text") + if value is None: + value = item.get("raw") + if value is not None: + return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False) + if isinstance(outer, dict) and doc_name in outer: + value = outer[doc_name] + return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False) + if isinstance(outer, str): + return outer + return json.dumps(outer, ensure_ascii=False) + + def _read_text(doc_name: str) -> str: + return _extract_doc(_call("read_docs", {"doc_names": [doc_name]}), doc_name) + + def _read_json(doc_name: str) -> Any: + raw = _read_text(doc_name) + return _parse_json_first(raw, doc_name, allow_trailing=True) + + def _canonical(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + def _sha(value: Any) -> str: + raw = value if isinstance(value, str) else _canonical(value) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _write_json_verified(doc_name: str, value: Any) -> str: + content = json.dumps(value, ensure_ascii=False, indent=2) + _call("write_file", {"path": doc_name, "content": content, "overwrite": True}) + reread = _read_json(doc_name) + if _canonical(reread) != _canonical(value): + raise RuntimeError(f"{doc_name}: post-write verification mismatch") + return _sha(content) + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _uniq(values: list[Any]) -> list[Any]: + seen: set[str] = set() + output: list[Any] = [] + for value in values: + marker = _canonical(value) if isinstance(value, (dict, list)) else str(value) + if marker in seen: + continue + seen.add(marker) + output.append(value) + return output + + def _natural_key(value: Any) -> list[Any]: + return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", str(value))] + + REQUIRED_FILES = [ + "client_meeting.md", + "evidence_event_candidates.json", + "evidence_indexed.json", + "Default_Agent/Juristic_Act.md", + ] + PART1_SOFT_GATE_HANDOFF_FILE = "quality_gates/stage1_part1_soft_gate_handoff.json" + + CURRENT_STATE_PATTERNS = [ + "현재", "현재까지", "현재도", "계속", "거주", "점유", "사용", "운영", + "부지로만 사용", "인도받아", "사업자등록", "유치권", "임대", "수익", + ] + CONFLICT_PATTERNS = [ + "하였음에도", "그럼에도", "불구하고", "거절", "응하지", "주장", + "항변", "다툼", "무효", "취소", "해제", "해지", "말소", "신뢰", + ] + SUCCESSION_PATTERNS = [ + "사망", "상속", "상속포기", "배우자", "직계비속", "직계존속", + "피상속인", "상속인", "수리", "가족관계", + ] + MIXED_CONTRACT_PATTERNS = [ + "임대차", "매매", "보증금", "매매대금", "인도", "관리", "사용", + "사용수익", "영업양도", "채무불승계", "대물변제", + ] + + + DOMAIN_CONFIGS = [ + { + "domain_id": "B1_Money_Successor", + "domain_label": "금전채권 및 상호속용 책임", + "allowed_legal_effect_bo_types": [ + "money_claim_chain_BO", + "commercial_successor_liability_chain_BO", + "liability_group_hint_BO", + ], + "terms": [ + "대여금", "차용", "매매대금", "물품대금", "보증금", "정산금", "공사대금", + "원금", "이자", "지연손해금", "변제기", "이행기", "일부변제", "미변제", + "채무승인", "지급각서", "확인서", "최고", "통지", "상호속용", "영업양도", + "양수인", "채무불승계", "business_transfer", "loan", "payment", "admission", + "notice", "claim_document", "mixed_contract_payment", + ], + "meeting_sources": ["mixed_contract_clause_ids", "conflict_pattern_clause_ids"], + "event_sources": ["mixed_contract_candidate_ids"], + "hazard_keys": ["mixed_contract_candidates", "conflict_pattern_expressions", "asset_clusters"], + "taxonomy_axes": ["legal_object", "structure", "dependency"], + }, + { + "domain_id": "B2_Secured_Registry", + "domain_label": "담보채무, 변제충당, 등기무효, 경매", + "allowed_legal_effect_bo_types": [ + "secured_debt_chain_BO", + "payment_allocation_and_mortgage_extinguishment_BO", + "auction_invalidity_BO", + "derivative_bank_mortgage_invalidity_BO", + ], + "terms": [ + "근저당", "저당", "담보", "피담보", "채무액", "변제", "변제충당", + "법정변제충당", "지분", "말소", "소유권이전등기", "등기", "등기원인", + "등기무효", "경매", "담보권실행경매", "경매개시", "매각", "배당", + "후행 담보권", "registry", "mortgage", "secured", "auction", "dividend", + "payment_allocation", "registry_row", "legal_calculation", + ], + "meeting_sources": ["conflict_pattern_clause_ids", "mixed_contract_clause_ids"], + "event_sources": [], + "hazard_keys": ["conflict_pattern_expressions", "asset_clusters", "mixed_contract_candidates"], + "taxonomy_axes": ["legal_object", "form", "dependency", "defect_or_condition"], + }, + { + "domain_id": "B3_Land_Valuation", + "domain_label": "토지 사용이익, 점유, 사용수익, 가치평가", + "allowed_legal_effect_bo_types": ["land_use_gain_timeline_BO", "liability_overlap_BO"], + "terms": [ + "토지", "건물", "부지", "인도", "점유", "사용", "사용수익", "현재 사용", + "현재 수익", "거주", "운영", "임대", "차임", "감정", "시가", "가액", + "미등기 건물", "사실상 처분권", "부진정연대", "valuation", "valuation_object", + "valuation_rows", "valuation_matrix", "registry_object_display", "possession", "land", "building", + ], + "meeting_sources": ["current_state_clause_ids", "conflict_pattern_clause_ids", "mixed_contract_clause_ids"], + "event_sources": ["state_candidate_ids"], + "hazard_keys": ["current_state_expressions", "asset_clusters", "mixed_contract_candidates", "conflict_pattern_expressions"], + "taxonomy_axes": ["legal_object", "contribution", "structure"], + }, + { + "domain_id": "B4_Actio", + "domain_label": "사해행위취소, 피보전채권, 책임재산, 가액배상", + "allowed_legal_effect_bo_types": [ + "preserved_claim_bundle_BO", + "fraudulent_transfer_BO", + "encumbrance_timeline_BO", + "valuation_matrix_BO", + "remedy_mode_decision_BO", + "value_compensation_cap_BO", + "defense_rebuttal_matrix_BO", + "actio_balance_sheet_BO", + ], + "terms": [ + "사해행위", "피보전채권", "채무초과", "책임재산", "공동담보", "매매", + "증여", "담보설정", "처분행위", "수익자", "전득자", "사해성", "악의", + "선의", "원상회복", "가액배상", "선순위담보", "말소", "가액 산정", + "항변", "actio", "fraudulent", "transfer", "preserved_claim", "encumbrance", + "valuation", "remedy", "compensation", "defense", "claim_document", + ], + "meeting_sources": ["conflict_pattern_clause_ids", "mixed_contract_clause_ids", "current_state_clause_ids", "succession_clause_ids"], + "event_sources": [], + "hazard_keys": ["conflict_pattern_expressions", "asset_clusters", "mixed_contract_candidates", "current_state_expressions", "succession_related_candidates"], + "taxonomy_axes": ["legal_object", "structure", "contribution", "defect_or_condition", "dependency", "form"], + }, + { + "domain_id": "B5_Succession_Notice_Lien_Asset_Defense", + "domain_label": "상속, 통지, 유치권, 목적물 특정, 항변", + "allowed_legal_effect_bo_types": [ + "succession_resolution_BO", + "notice_lifecycle_BO", + "lien_state_timeline_BO", + "asset_alias_BO", + "defense_rebuttal_BO", + "delivery_use_gain_claim_group_BO", + ], + "terms": [ + "사망", "상속", "상속포기", "배우자", "직계비속", "가족관계", "피상속인", + "상속인", "통지", "내용증명", "도달", "수령", "최고", "유치권", "점유", + "인도", "거절", "별지목록", "등기부 표시", "목적물", "동일성", "피고 답변", + "항변", "권리남용", "신의칙", "동시이행", "succession", "notice", "lien", + "possession", "asset", "defense", "party_assertion", "registry_object_display", + ], + "meeting_sources": ["succession_clause_ids", "current_state_clause_ids", "conflict_pattern_clause_ids", "mixed_contract_clause_ids"], + "event_sources": ["succession_candidate_ids", "state_candidate_ids"], + "hazard_keys": ["succession_related_candidates", "current_state_expressions", "conflict_pattern_expressions", "asset_clusters", "mixed_contract_candidates"], + "taxonomy_axes": ["structure", "time_effect", "legal_object", "defect_or_condition"], + } + ] + + def _stable_hash(value: Any) -> str: + if isinstance(value, str): + raw = value + else: + raw = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _text(value: Any) -> str | None: + if value is None: + return None + s = str(value).strip() + return s or None + + def _norm_space(value: Any) -> str: + return re.sub(r"\s+", " ", str(value or "")).strip() + + def _norm_key(value: Any) -> str: + text = str(value or "").strip().lower() + return "".join(ch for ch in text if ch.isalnum()) + + def _short_excerpt(value: Any, limit: int = 120) -> str: + text = _norm_space(value) + return text if len(text) <= limit else text[: limit - 1] + "…" + + def _section_key_from_heading(heading: str, seq: int) -> str: + normalized = _norm_key(heading) + if "상담내용" in normalized: + suffix = "consultation" + elif "사건관계인의주장" in normalized or ("사건관계인" in normalized and "주장" in normalized): + suffix = "party_assertions" + elif "희망사항" in normalized or "의뢰인희망" in normalized: + suffix = "client_requests" + elif normalized: + suffix = normalized[:24] + else: + suffix = "unnamed" + return f"s{seq:02d}_{suffix}" + + def _uniq(values: list[Any]) -> list[Any]: + seen: set[str] = set() + out: list[Any] = [] + for value in values: + marker = json.dumps(value, ensure_ascii=False, sort_keys=True) if isinstance(value, (dict, list)) else str(value) + if marker not in seen: + seen.add(marker) + out.append(value) + return out + + def _extract_doc_names(payload: Any) -> list[str]: + names: list[str] = [] + def walk(v: Any) -> None: + if isinstance(v, str): + if "." in v or "/" in v: + names.append(v) + names.extend(re.findall(r"[\w./()가-힣ᄀ-ᇿ -]+\.[A-Za-z0-9]+", v)) + elif isinstance(v, list): + for item in v: + walk(item) + elif isinstance(v, dict): + for key in ("name", "path", "doc_name", "document_name", "file", "filename"): + if v.get(key): + names.append(str(v[key])) + for val in v.values(): + if isinstance(val, (list, dict)): + walk(val) + walk(payload) + return sorted(set(names)) + + def _doc_name_matches(found_names: list[str], required: str) -> bool: + required_norm = required.strip("/") + required_base = required_norm.split("/")[-1] + for name in found_names: + n = name.strip("/") + if n == required_norm or n.endswith("/" + required_norm) or n.split("/")[-1] == required_base: + return True + if required_norm in n or required_base in n: + return True + return False + + def _preflight_required_files() -> dict[str, Any]: + found_names_set: set[str] = set() + list_docs_args = ( + {"path": "/", "pattern": "*", "recursive": True}, + {"path": ".", "pattern": "*", "recursive": True}, + {"path": "/", "pattern": "*"}, + {"path": ".", "pattern": "*"}, + {"path": "Default_Agent", "pattern": "*"}, + {"path": "quality_gates", "pattern": "*.json"}, + ) + for args in list_docs_args: + try: + payload = _unwrap(_call("list_docs", args), "list_docs") + found_names_set.update(_extract_doc_names(payload)) + except Exception: + continue + found_names_now = sorted(found_names_set) + required_ready = all(_doc_name_matches(found_names_now, path) for path in REQUIRED_FILES) + sidecar_checked = _doc_name_matches(found_names_now, PART1_SOFT_GATE_HANDOFF_FILE) or args.get("path") == "quality_gates" + if required_ready and sidecar_checked: + break + found_names = sorted(found_names_set) + if not found_names: + raise RuntimeError("list_docs returned no recognizable document names for required inputs") + missing = [path for path in REQUIRED_FILES if not _doc_name_matches(found_names, path)] + if missing: + raise RuntimeError(f"Missing required input files by list_docs: {missing}") + return {"found_names": found_names, "required_files": REQUIRED_FILES} + + def _read_required_docs_once() -> dict[str, str]: + payload = _unwrap(_call("read_docs", {"doc_names": REQUIRED_FILES}), "read_docs") + docs: dict[str, str] = {} + + def assign(name: str | None, content: Any) -> None: + if not name: + return + text = content if isinstance(content, str) else json.dumps(content, ensure_ascii=False) + for required in REQUIRED_FILES: + if name == required or name.endswith("/" + required) or name.split("/")[-1] == required.split("/")[-1]: + docs[required] = text + + if isinstance(payload, dict) and isinstance(payload.get("results"), list): + for item in payload["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + content = item.get("content") or item.get("text") or item.get("markdown") or item.get("raw") + assign(str(name) if name else None, content) + elif isinstance(payload, dict): + for required in REQUIRED_FILES: + if required in payload: + assign(required, payload[required]) + elif isinstance(payload, str): + if len(REQUIRED_FILES) == 1: + assign(REQUIRED_FILES[0], payload) + + missing = [name for name in REQUIRED_FILES if name not in docs] + if missing: + # Some Localdocs runtimes return one JSON wrapper inside the text field. + raw = payload if isinstance(payload, str) else json.dumps(payload, ensure_ascii=False) + try: + outer = json.loads(raw) + if isinstance(outer, dict) and isinstance(outer.get("results"), list): + for item in outer["results"]: + if isinstance(item, dict): + name = item.get("name") or item.get("path") or item.get("doc_name") + content = item.get("content") or item.get("text") + assign(str(name) if name else None, content) + except Exception: + raw = "" + missing = [name for name in REQUIRED_FILES if name not in docs] + if missing: + raise RuntimeError(f"read_docs did not return required inputs: {missing}") + for name, content in docs.items(): + if not str(content).strip(): + raise RuntimeError(f"Required input is empty: {name}") + return docs + + def _parse_json_doc(name: str, raw: str) -> Any: + try: + return json.loads(raw.strip()) + except json.JSONDecodeError as exc: + raise RuntimeError(f"Strict JSON parse failed: {name}: {exc.msg} at line {exc.lineno} column {exc.colno}") from exc + + def _items(obj: Any, *keys: str) -> list[Any]: + if isinstance(obj, list): + return obj + if isinstance(obj, dict): + for key in keys + ("items", "evidence_indexed", "documents", "results"): + val = obj.get(key) + if isinstance(val, list): + return val + return [] + + def _extract_keywords(text: str, patterns: list[str]) -> list[str]: + return [p for p in patterns if p in text] + + def _build_meeting_clause_map(meeting_text: str) -> dict[str, Any]: + clauses: list[dict[str, Any]] = [] + current_major: str | None = None + current: dict[str, Any] | None = None + section_seq = 0 + current_section_key = "preamble" + current_section_label = "preamble" + + section_re = re.compile(r"^\s*#?\s*【\s*(.+?)\s*】\s*$") + major_re = re.compile(r"^\s*(\d+)\.\s+(.+?)\s*$") + sub_re = re.compile(r"^\s*([가-힣])\.\s+(.+?)\s*$") + + def flush(end_line: int) -> None: + nonlocal current + if not current: + return + current["line_end"] = end_line + current["text"] = _norm_space(current.get("text", "")) + text = current["text"] + current["tags"] = { + "current_state": _extract_keywords(text, CURRENT_STATE_PATTERNS), + "conflict_pattern": _extract_keywords(text, CONFLICT_PATTERNS), + "succession": _extract_keywords(text, SUCCESSION_PATTERNS), + "mixed_contract": _extract_keywords(text, MIXED_CONTRACT_PATTERNS), + } + current["text_excerpt"] = _short_excerpt(text) + clauses.append(current) + current = None + + lines = meeting_text.splitlines() + for idx, line in enumerate(lines, 1): + stripped = line.strip() + if not stripped or stripped.startswith("|") or stripped.startswith("---"): + continue + m_section = section_re.match(stripped) + m_major = major_re.match(stripped) + m_sub = sub_re.match(stripped) + if m_section: + flush(idx - 1) + section_seq += 1 + current_section_label = _norm_space(m_section.group(1)) + current_section_key = _section_key_from_heading(current_section_label, section_seq) + current_major = None + elif m_major: + flush(idx - 1) + current_major = m_major.group(1) + current = { + "clause_id": f"{current_section_key}:{current_major}", + "local_clause_id": current_major, + "section_key": current_section_key, + "section_label": current_section_label, + "major_no": current_major, + "sub_no": None, + "heading": _norm_space(m_major.group(2)), + "line_start": idx, + "text": _norm_space(m_major.group(2)), + } + elif m_sub and current_major: + flush(idx - 1) + sub_no = m_sub.group(1) + current = { + "clause_id": f"{current_section_key}:{current_major}-{sub_no}", + "local_clause_id": f"{current_major}-{sub_no}", + "section_key": current_section_key, + "section_label": current_section_label, + "major_no": current_major, + "sub_no": sub_no, + "heading": None, + "line_start": idx, + "text": _norm_space(m_sub.group(2)), + } + elif current: + current["text"] = _norm_space(str(current.get("text", "")) + " " + stripped) + flush(len(lines)) + + if not clauses: + raise RuntimeError("client_meeting.md parse produced no numbered meeting clauses") + + by_id: dict[str, Any] = {} + duplicate_clause_ids: list[str] = [] + for clause in clauses: + cid = clause["clause_id"] + if cid in by_id: + duplicate_clause_ids.append(cid) + by_id[cid] = clause + if duplicate_clause_ids: + raise RuntimeError(f"Duplicate meeting clause ids after section normalization: {duplicate_clause_ids}") + return { + "schema_version": "meeting_clause_map.v1", + "clause_order": [c["clause_id"] for c in clauses], + "by_clause_id": by_id, + "sections": _uniq([{"section_key": c["section_key"], "section_label": c["section_label"]} for c in clauses]), + "current_state_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["current_state"]], + "conflict_pattern_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["conflict_pattern"]], + "succession_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["succession"]], + "mixed_contract_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["mixed_contract"]], + } + + def _candidate_id(candidate: dict[str, Any], evidence_index: str, seq: int) -> str: + return str(candidate.get("candidate_id") or candidate.get("candidate_id_proposed") or f"internal_missing_candidate_id::{evidence_index}::{seq:02d}") + + def _evidence_index_from_item(item: dict[str, Any]) -> str | None: + return _text(item.get("evidence_index") or item.get("evidence_index_proposed") or item.get("source_evidence_index")) + + def _build_event_candidate_map(event_obj: Any) -> dict[str, Any]: + items = _items(event_obj) + if not items: + raise RuntimeError("evidence_event_candidates.json parse produced no document-level items") + by_candidate_id: dict[str, Any] = {} + by_evidence_index: dict[str, list[str]] = {} + by_meeting_clause_ref: dict[str, list[str]] = {} + state_candidate_ids: list[str] = [] + succession_candidate_ids: list[str] = [] + mixed_contract_candidate_ids: list[str] = [] + zero_event_evidence_indexes: list[str] = [] + asset_cluster_values: list[str] = [] + malformed_item_refs: list[str] = [] + + for item_seq, item in enumerate(items, 1): + if not isinstance(item, dict): + malformed_item_refs.append(f"item#{item_seq}: non-object") + continue + evidence_index = _evidence_index_from_item(item) + if not evidence_index: + malformed_item_refs.append(f"item#{item_seq}: missing evidence_index") + continue + item_ref = evidence_index + event_candidates_raw = item.get("event_candidates") + has_event_candidates_list = isinstance(event_candidates_raw, list) + has_event_candidates = has_event_candidates_list and len(event_candidates_raw) > 0 + has_zero_event_reason = bool(item.get("zero_event_reason")) + if event_candidates_raw is not None and not has_event_candidates_list: + malformed_item_refs.append(f"{item_ref}: event_candidates must be a list when present") + continue + if not has_event_candidates and not has_zero_event_reason: + malformed_item_refs.append(f"{item_ref}: requires non-empty event_candidates or zero_event_reason") + continue + if item.get("zero_event_reason"): + zero_event_evidence_indexes.append(evidence_index) + item_flags = item.get("doc_semantic_flags") if isinstance(item.get("doc_semantic_flags"), list) else [] + property_cluster_id = _text(item.get("property_cluster_id")) + if property_cluster_id: + asset_cluster_values.append(property_cluster_id) + for asset in item.get("affected_asset_cluster_ids") or []: + if _text(asset): + asset_cluster_values.append(str(asset)) + for seq, cand in enumerate(event_candidates_raw or [], 1): + if not isinstance(cand, dict): + malformed_item_refs.append(f"{item_ref}: event_candidates[{seq}] must be an object") + continue + cid = _candidate_id(cand, evidence_index, seq) + refs = cand.get("meeting_clause_refs") if isinstance(cand.get("meeting_clause_refs"), list) else [] + cand_flags = cand.get("doc_semantic_flags") if isinstance(cand.get("doc_semantic_flags"), list) else [] + candidate_flags = _uniq(item_flags + cand_flags) + record = { + "candidate_id": cid, + "source_evidence_index": _text(cand.get("source_evidence_index") or cand.get("source_evidence_index_proposed")) or evidence_index, + "title": item.get("title"), + "doc_type": item.get("doc_type"), + "event_or_state": cand.get("event_or_state"), + "event_kind": cand.get("event_kind"), + "event_subkind": cand.get("event_subkind"), + "action_type_candidate": cand.get("action_type_candidate"), + "action_summary": cand.get("action_summary"), + "participants": cand.get("participants") or {}, + "object_spec": cand.get("object_spec"), + "amount": cand.get("amount"), + "event_date": cand.get("event_date"), + "event_end_date": cand.get("event_end_date"), + "time_text": cand.get("time_text"), + "time_precision": cand.get("time_precision"), + "current_state_candidate": cand.get("current_state_candidate"), + "succession_candidate": cand.get("succession_candidate"), + "legal_effect_candidate": cand.get("legal_effect_candidate"), + "defense_candidate": cand.get("defense_candidate"), + "commercial_successor_candidates": cand.get("commercial_successor_candidates"), + "meeting_clause_refs": refs, + "actio_relevance_candidates": cand.get("actio_relevance_candidates"), + "identity_signature": cand.get("identity_signature"), + "property_cluster_id": property_cluster_id, + "affected_asset_cluster_ids": item.get("affected_asset_cluster_ids") or [], + "doc_semantic_flags": candidate_flags, + "fraction_ref": cand.get("fraction_ref"), + "confidence": cand.get("confidence"), + } + by_candidate_id[cid] = record + by_evidence_index.setdefault(evidence_index, []).append(cid) + for ref in refs: + by_meeting_clause_ref.setdefault(str(ref), []).append(cid) + if cand.get("event_or_state") == "state" or cand.get("current_state_candidate"): + state_candidate_ids.append(cid) + if cand.get("succession_candidate"): + succession_candidate_ids.append(cid) + if "mixed_contract_doc" in candidate_flags or cand.get("commercial_successor_candidates"): + mixed_contract_candidate_ids.append(cid) + if _text(cand.get("object_spec")): + asset_cluster_values.append(str(cand.get("object_spec"))) + + if malformed_item_refs: + raise RuntimeError(f"Malformed evidence_event_candidates document items: {malformed_item_refs[:20]}") + + return { + "schema_version": "event_candidate_map.v2", + "candidate_count": len(by_candidate_id), + "candidate_id_set": sorted(by_candidate_id.keys()), + "by_candidate_id": by_candidate_id, + "by_evidence_index": {k: _uniq(v) for k, v in by_evidence_index.items()}, + "by_meeting_clause_ref": {k: _uniq(v) for k, v in by_meeting_clause_ref.items()}, + "state_candidate_ids": _uniq(state_candidate_ids), + "succession_candidate_ids": _uniq(succession_candidate_ids), + "mixed_contract_candidate_ids": _uniq(mixed_contract_candidate_ids), + "zero_event_evidence_indexes": _uniq(zero_event_evidence_indexes), + "asset_cluster_values": _uniq([v for v in asset_cluster_values if _text(v)]), + } + + def _component_count(item: dict[str, Any], key: str) -> int: + val = item.get(key) + if isinstance(val, list): + return len(val) + if isinstance(val, dict): + return len(val) + return 1 if val not in (None, "", []) else 0 + + COMPONENT_DETAIL_PROJECTION = { + "loan_components": ["loan_component_id", "principal_amount", "loan_date", "due_date", + "agreed_interest_rate", "agreed_delay_damage_rate", "rate_text", + "rate_source_locator", "interest_accrual_start_hint"], + "secured_debt_components": ["agreed_interest_rate", "agreed_delay_damage_rate", "rate_text", + "rate_source_locator", "due_date"], + "notice_components": ["notice_id_proposed", "notice_type", "drafted_date", "dispatch_date", + "arrival_date", "received_date", "legal_effect_date_candidate", + "requires_receipt_for_effect", "receipt_evidence_refs", + "recipient_admission_text", "sender_candidates", "receiver_candidates"], + "registry_row_components": ["registry_ref", "fraction_ref", "fraction_display", + "fraction_numerator", "fraction_denominator", "receipt_no"], + "schedule_items": ["schedule_ref", "canonical_label_candidate", "usable_in_relief"], + } + + def _component_details(item: dict[str, Any]) -> dict[str, Any]: + details: dict[str, Any] = {} + for key, fields in COMPONENT_DETAIL_PROJECTION.items(): + rows = item.get(key) + if isinstance(rows, dict): + rows = [rows] + if not isinstance(rows, list): + continue + projected = [{field: row.get(field) for field in fields if field in row} for row in rows if isinstance(row, dict)] + projected = [p for p in projected if p] + if projected: + details[key] = projected + return details + + def _build_evidence_authority_map(evidence_obj: Any) -> dict[str, Any]: + items = _items(evidence_obj, "evidence_indexed") + if not items: + raise RuntimeError("evidence_indexed.json parse produced no evidence authority items") + by_evidence_index: dict[str, Any] = {} + by_doc_type: dict[str, list[str]] = {} + by_title_norm: dict[str, list[str]] = {} + semantic_flag_index: dict[str, list[str]] = {} + evidence_index_set: list[str] = [] + malformed_item_refs: list[str] = [] + duplicate_evidence_indexes: list[str] = [] + + component_keys = [ + "contract_components", "valuation_object", "legal_calculation_object", + "legal_calculation_objects", "claim_document_components", + "business_transfer_components", "registry_row_components", + "secured_debt_components", "valuation_rows", "mixed_contract_payment_semantics", + "loan_components", "valuation_matrix", "registry_row_role_map", + "defense_assertions", "admission_components", "defense_components", + "notice_components", "party_assertion_components", "schedule_items", + "registry_object_display", "succession_document_components", + ] + + for item_seq, item in enumerate(items, 1): + if not isinstance(item, dict): + malformed_item_refs.append(f"item#{item_seq}: non-object") + continue + evidence_index = _text(item.get("evidence_index") or item.get("evidence_index_proposed")) + if not evidence_index: + malformed_item_refs.append(f"item#{item_seq}: missing finalized evidence_index") + continue + if evidence_index in by_evidence_index: + duplicate_evidence_indexes.append(evidence_index) + continue + flags = item.get("doc_semantic_flags") if isinstance(item.get("doc_semantic_flags"), list) else [] + component_summary = {key: _component_count(item, key) for key in component_keys} + record = { + "evidence_index": evidence_index, + "doc_uid": item.get("doc_uid"), + "title": item.get("title"), + "title_normalized": item.get("title_normalized") or _norm_space(item.get("title")), + "doc_type": item.get("doc_type"), + "key_facts": item.get("key_facts") or [], + "key_dates": item.get("key_dates") or [], + "key_amounts": item.get("key_amounts") or [], + "key_parties": item.get("key_parties") or [], + "source_pointer": item.get("source_pointer") or {}, + "property_cluster_id": item.get("property_cluster_id"), + "doc_semantic_flags": flags, + "multi_event_registry": item.get("multi_event_registry"), + "component_summary": component_summary, + "component_details": _component_details(item), + } + by_evidence_index[evidence_index] = record + evidence_index_set.append(evidence_index) + by_doc_type.setdefault(str(item.get("doc_type") or "unknown"), []).append(evidence_index) + title_key = _norm_key(record["title_normalized"]) + if title_key: + by_title_norm.setdefault(title_key, []).append(evidence_index) + for flag in flags: + semantic_flag_index.setdefault(str(flag), []).append(evidence_index) + + if malformed_item_refs: + raise RuntimeError(f"Malformed evidence_indexed authority items: {malformed_item_refs[:20]}") + if duplicate_evidence_indexes: + raise RuntimeError(f"Duplicate evidence_index values in evidence_indexed.json: {_uniq(duplicate_evidence_indexes)}") + + return { + "schema_version": "evidence_authority_map.v2", + "evidence_index_set": _sort_evidence_indexes(_uniq(evidence_index_set)), + "by_evidence_index": by_evidence_index, + "by_doc_type": {k: _sort_evidence_indexes(_uniq(v)) for k, v in by_doc_type.items()}, + "by_title_norm": {k: _sort_evidence_indexes(_uniq(v)) for k, v in by_title_norm.items()}, + "semantic_flag_index": {k: _sort_evidence_indexes(_uniq(v)) for k, v in semantic_flag_index.items()}, + } + + def _sort_evidence_indexes(values: list[str]) -> list[str]: + def key(v: str) -> tuple[int, str]: + m = re.search(r"E-(\d+)", str(v)) + return (int(m.group(1)) if m else 10**9, str(v)) + return sorted(values, key=key) + + def _split_examples(value: str) -> list[str]: + parts = re.split(r"[,,、]| / |/|ㆍ|·", value or "") + return [_norm_space(p) for p in parts if _norm_space(p)] + + def _build_juristic_act_map(markdown: str) -> dict[str, Any]: + categories: dict[str, Any] = {} + example_to_gubun: dict[str, list[str]] = {} + for line in markdown.splitlines(): + if "|" not in line: + continue + cells = [_norm_space(c) for c in line.strip().strip("|").split("|")] + if len(cells) < 3: + continue + if cells[0] in ("구분", "---") or set(cells[0]) <= {"-"}: + continue + gubun, content, examples = cells[0], cells[1], cells[2] + if not gubun or gubun.startswith("-"): + continue + example_list = _split_examples(examples) + categories[gubun] = {"content": content, "examples": example_list} + for ex in example_list: + example_to_gubun.setdefault(_norm_key(ex), []).append(gubun) + + axis_groups = { + "structure": ["단독행위", "계약행위", "합동행위"], + "form": ["요식(要式)행위", "불요식행위"], + "time_effect": ["생전 행위", "사후(또는 사인) 행위"], + "legal_object": ["채권행위", "물권행위", "준물권행위"], + "contribution": ["출연행위", "비출연행위"], + "dependency": ["주(主)된 행위", "종(從)된 행위", "유인 행위", "무인 행위"], + "defect_or_condition": ["무효 행위", "취소가능한 법률 행위", "조건부 법률행위", "기한부 법률행위"], + "controversy": ["견해대립있는 행위"], + } + + return { + "schema_version": "juristic_act_taxonomy_map.v1", + "categories": categories, + "example_to_gubun": {k: _uniq(v) for k, v in example_to_gubun.items()}, + "axis_groups": axis_groups, + "source_digest_sha256": _stable_hash(markdown), + } + + def _validate_juristic_act_map(juristic_map: dict[str, Any]) -> None: + categories = juristic_map.get("categories") or {} + example_to_gubun = juristic_map.get("example_to_gubun") or {} + required = ["단독행위", "계약행위", "채권행위", "물권행위"] + missing = [name for name in required if name not in categories] + if not categories or not example_to_gubun: + raise RuntimeError("JuristicAct taxonomy parse produced empty categories or examples") + if missing: + raise RuntimeError(f"JuristicAct taxonomy missing required categories: {missing}") + + def _collect_asset_clusters(meeting_map: dict[str, Any], event_map: dict[str, Any], evidence_map: dict[str, Any]) -> list[str]: + values: list[str] = [] + asset_hint_re = re.compile(r"([가-힣A-Za-z0-9·ㆍ() \-]{1,30}(?:대지|건물|상가|빌라|임야|부동산|지분|영업|상호|채권|대금))") + for clause in meeting_map.get("by_clause_id", {}).values(): + text = str(clause.get("text") or "") + values.extend([_norm_space(m.group(1)) for m in asset_hint_re.finditer(text)]) + values.extend(event_map.get("asset_cluster_values") or []) + for ev in evidence_map.get("by_evidence_index", {}).values(): + if _text(ev.get("property_cluster_id")): + values.append(str(ev.get("property_cluster_id"))) + for fact in ev.get("key_facts") or []: + values.extend([_norm_space(m.group(1)) for m in asset_hint_re.finditer(str(fact))]) + return _uniq([v for v in values if _text(v)]) + + def _build_global_hazard_seed(meeting_map: dict[str, Any], event_map: dict[str, Any], evidence_map: dict[str, Any]) -> dict[str, Any]: + evidence_index_set = evidence_map.get("evidence_index_set") or [] + current_state_ids = event_map.get("state_candidate_ids") or [] + succession_candidate_ids = event_map.get("succession_candidate_ids") or [] + mixed_contract_candidate_ids = event_map.get("mixed_contract_candidate_ids") or [] + + def meeting_expression_records(clause_ids: list[str], tag_key: str) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + by_clause = meeting_map.get("by_clause_id", {}) + for cid in clause_ids: + clause = by_clause.get(cid) or {} + matched = (clause.get("tags") or {}).get(tag_key) or [] + out.append({ + "source": "client_meeting.md", + "clause_id": cid, + "local_clause_id": clause.get("local_clause_id"), + "section_key": clause.get("section_key"), + "matched_patterns": matched, + "text_excerpt": clause.get("text_excerpt") or _short_excerpt(clause.get("text")), + }) + return out + + def event_expression_records(candidate_ids: list[str]) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + by_candidate = event_map.get("by_candidate_id", {}) + for cid in candidate_ids: + cand = by_candidate.get(cid) or {} + text = " ".join(str(cand.get(k) or "") for k in ("action_summary", "event_kind", "event_subkind", "identity_signature")) + matched = _uniq( + _extract_keywords(text, CURRENT_STATE_PATTERNS) + + _extract_keywords(text, CONFLICT_PATTERNS) + + _extract_keywords(text, SUCCESSION_PATTERNS) + + _extract_keywords(text, MIXED_CONTRACT_PATTERNS) + ) + out.append({ + "source": "evidence_event_candidates.json", + "candidate_id": cid, + "source_evidence_index": cand.get("source_evidence_index"), + "event_or_state": cand.get("event_or_state"), + "event_kind": cand.get("event_kind"), + "matched_patterns": matched, + "text_excerpt": _short_excerpt(cand.get("action_summary") or cand.get("identity_signature")), + }) + return out + + conflict_candidate_ids: list[str] = [] + for cid, cand in event_map.get("by_candidate_id", {}).items(): + combined = " ".join( + str(cand.get(k) or "") + for k in ("action_summary", "event_kind", "event_subkind", "identity_signature") + ) + if _extract_keywords(combined, CONFLICT_PATTERNS): + conflict_candidate_ids.append(cid) + + succession_evidence_indexes = [] + mixed_contract_evidence_indexes = [] + for eidx, ev in evidence_map.get("by_evidence_index", {}).items(): + flags = ev.get("doc_semantic_flags") or [] + title_and_facts = " ".join([str(ev.get("title") or "")] + [str(x) for x in ev.get("key_facts") or []]) + if "succession_doc" in flags or _extract_keywords(title_and_facts, SUCCESSION_PATTERNS): + succession_evidence_indexes.append(eidx) + if "mixed_contract_doc" in flags or _extract_keywords(title_and_facts, MIXED_CONTRACT_PATTERNS): + mixed_contract_evidence_indexes.append(eidx) + + return { + "schema_version": "global_hazard_seed.v1", + "meeting_clause_ids": meeting_map.get("clause_order") or [], + "current_state_expressions": { + "meeting_clause_ids": meeting_map.get("current_state_clause_ids") or [], + "event_candidate_ids": current_state_ids, + "meeting_expressions": meeting_expression_records(meeting_map.get("current_state_clause_ids") or [], "current_state"), + "event_expressions": event_expression_records(current_state_ids), + }, + "conflict_pattern_expressions": { + "meeting_clause_ids": meeting_map.get("conflict_pattern_clause_ids") or [], + "event_candidate_ids": _uniq(conflict_candidate_ids), + "meeting_expressions": meeting_expression_records(meeting_map.get("conflict_pattern_clause_ids") or [], "conflict_pattern"), + "event_expressions": event_expression_records(_uniq(conflict_candidate_ids)), + }, + "asset_clusters": _collect_asset_clusters(meeting_map, event_map, evidence_map), + "succession_related_candidates": { + "meeting_clause_ids": meeting_map.get("succession_clause_ids") or [], + "event_candidate_ids": succession_candidate_ids, + "evidence_indexes": _sort_evidence_indexes(_uniq(succession_evidence_indexes)), + "meeting_expressions": meeting_expression_records(meeting_map.get("succession_clause_ids") or [], "succession"), + "event_expressions": event_expression_records(succession_candidate_ids), + }, + "mixed_contract_candidates": { + "meeting_clause_ids": meeting_map.get("mixed_contract_clause_ids") or [], + "event_candidate_ids": mixed_contract_candidate_ids, + "evidence_indexes": _sort_evidence_indexes(_uniq(mixed_contract_evidence_indexes)), + "meeting_expressions": meeting_expression_records(meeting_map.get("mixed_contract_clause_ids") or [], "mixed_contract"), + "event_expressions": event_expression_records(mixed_contract_candidate_ids), + }, + "evidence_index_set": evidence_index_set, + } + + def _read_optional_doc_once(doc_name: str) -> str | None: + try: + payload = _unwrap(_call("read_docs", {"doc_names": [doc_name]}), "read_docs_optional") + except Exception: + return None + if isinstance(payload, dict) and isinstance(payload.get("results"), list): + for item in payload["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + content = item.get("content") or item.get("text") or item.get("markdown") or item.get("raw") + if name and (str(name) == doc_name or str(name).endswith("/" + doc_name) or str(name).split("/")[-1] == doc_name.split("/")[-1]): + return content if isinstance(content, str) else json.dumps(content, ensure_ascii=False) + if isinstance(payload, dict) and doc_name in payload: + val = payload[doc_name] + return val if isinstance(val, str) else json.dumps(val, ensure_ascii=False) + if isinstance(payload, str) and payload.strip(): + return payload + return None + + def _compact_part1_soft_gate_handoff(existence_check: dict[str, Any], input_digests: dict[str, str], meeting_map: dict[str, Any], event_map: dict[str, Any], evidence_map: dict[str, Any]) -> dict[str, Any]: + found_names = existence_check.get("found_names") or [] + base = { + "schema_version": "stage1_part1_soft_gate_handoff.v1", + "status": "ABSENT", + "stage2_auto_progression_allowed": True, + "review_gate_count": 0, + "highest_review_severity": "PASS", + "review_items_compact": [], + "routing_index": {}, + "lawyer_workflow_queues": {}, + "dropped_review_items": [], + "usage_rules": { + "not_factual_authority": True, + "must_not_create_fact_without_stage_a_source": True, + "must_preserve_as_needs_review_if_used": True, + "must_not_override_stage_e_final_progression": True, + "must_not_override_stage_f_single_writer_gate": True, + }, + } + if not _doc_name_matches(found_names, PART1_SOFT_GATE_HANDOFF_FILE): + return base + raw = _read_optional_doc_once(PART1_SOFT_GATE_HANDOFF_FILE) + if not raw: + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": "sidecar listed but unreadable"}) + return base + try: + sidecar = json.loads(raw.strip()) + except Exception as exc: + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": f"sidecar JSON parse failed: {exc}"}) + return base + if not isinstance(sidecar, dict) or sidecar.get("schema_version") != "stage1_part1_soft_gate_handoff.v1": + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": "wrong or missing schema_version"}) + return base + if sidecar.get("handoff_status") == "BLOCKED" or sidecar.get("stage2_auto_progression_allowed") is False: + raise RuntimeError("Part 1 soft gate handoff is BLOCKED; Part 2-6 cannot proceed") + guard = sidecar.get("digest_guard") if isinstance(sidecar.get("digest_guard"), dict) else {} + invalid_guard_fields = [] + stale = [] + for guard_key, doc_name in [("evidence_indexed_sha256", "evidence_indexed.json"), ("evidence_event_candidates_sha256", "evidence_event_candidates.json")]: + val = guard.get(guard_key) + if not isinstance(val, str) or not re.fullmatch(r"[0-9a-f]{64}", val): + invalid_guard_fields.append(guard_key) + elif val != input_digests.get(doc_name): + stale.append(guard_key) + if invalid_guard_fields: + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": "digest_guard missing real sha256", "fields": invalid_guard_fields}) + return base + if stale: + base["status"] = "IGNORED_STALE" + base["dropped_review_items"].append({"reason": "digest_guard mismatch", "fields": stale}) + return base + evidence_set = set(evidence_map.get("evidence_index_set") or []) + event_set = set(event_map.get("candidate_id_set") or []) + meeting_set = set(meeting_map.get("clause_order") or []) + compact = [] + dropped = [] + for idx, item in enumerate(sidecar.get("review_items") or []): + if len(compact) >= 40: + dropped.append({"index": idx, "reason": "review_items_compact max 40 reached"}) + continue + if not isinstance(item, dict): + dropped.append({"index": idx, "reason": "non-object review item"}) + continue + refs = item.get("source_refs") if isinstance(item.get("source_refs"), dict) else {} + bad_e = [x for x in refs.get("evidence_indexes", []) if x not in evidence_set] + bad_ev = [x for x in refs.get("event_candidate_ids", []) if x not in event_set] + bad_m = [x for x in refs.get("meeting_clause_refs", []) if x not in meeting_set] + if bad_e or bad_ev or bad_m: + dropped.append({"review_id": item.get("review_id"), "reason": "source_refs membership failed", "bad_evidence_indexes": bad_e, "bad_event_candidate_ids": bad_ev, "bad_meeting_clause_refs": bad_m}) + continue + compact.append({ + "review_id": item.get("review_id"), + "severity": item.get("severity"), + "issue_type": item.get("issue_type"), + "lawyer_workflow_role": item.get("lawyer_workflow_role"), + "domain_hints": item.get("domain_hints") or [], + "target_stage_hints": item.get("target_stage_hints") or [], + "target_task_hints": item.get("target_task_hints") or [], + "source_refs": refs, + "candidate_use_policy": item.get("candidate_use_policy"), + "review_instruction": item.get("review_instruction"), + "negative_instruction": item.get("negative_instruction"), + }) + summary = sidecar.get("review_gate_summary") if isinstance(sidecar.get("review_gate_summary"), dict) else {} + base.update({ + "status": sidecar.get("handoff_status") if sidecar.get("handoff_status") in {"READY_WITH_REVIEW", "READY_NO_REVIEW"} else "READY_WITH_REVIEW", + "stage2_auto_progression_allowed": sidecar.get("stage2_auto_progression_allowed") is not False, + "review_gate_count": len(compact), + "highest_review_severity": summary.get("highest_review_severity") or ("HARD_WARNING" if any(i.get("severity") == "HARD_WARNING" for i in compact) else ("SOFT_WARNING" if compact else "PASS")), + "review_items_compact": compact, + "routing_index": sidecar.get("routing_index") if isinstance(sidecar.get("routing_index"), dict) else {}, + "lawyer_workflow_queues": sidecar.get("lawyer_workflow_queues") if isinstance(sidecar.get("lawyer_workflow_queues"), dict) else {}, + "dropped_review_items": dropped, + }) + return base + + + ALL_DOMAIN_TERMS = {config["domain_id"]: config["terms"] for config in DOMAIN_CONFIGS} + PRIMARY_TERMS = { + "B1_Money_Successor": ["대여금", "차용", "물품대금", "공사대금", "원금", "이자", "지연손해금", "변제기", "일부변제", "미변제", "채무승인", "지급각서", "상호속용", "영업양도", "양수인", "채무불승계", "business_transfer", "loan", "admission"], + "B2_Secured_Registry": ["근저당", "저당", "담보", "피담보", "변제충당", "말소", "경매", "배당", "registry", "mortgage", "secured", "auction", "dividend"], + "B3_Land_Valuation": ["토지", "건물", "점유", "수익", "인도", "차임", "감정", "시가", "valuation", "possession", "land", "building"], + "B4_Actio": ["사해행위", "피보전채권", "채무초과", "책임재산", "공동담보", "처분행위", "수익자", "전득자", "가액배상", "actio", "fraudulent"], + "B5_Succession_Notice_Lien_Asset_Defense": ["사망", "상속", "상속포기", "내용증명", "유치권", "별지목록", "동시이행", "상계", "succession", "notice", "lien", "defense"], + } + + MEETING_FIELDS = [ + "clause_id", "local_clause_id", "section_key", "section_label", "major_no", + "sub_no", "heading", "line_start", "line_end", "text_excerpt", "tags", + ] + EVENT_FIELDS = ["candidate_id", "source_evidence_index", "title", "doc_type", "event_or_state", "event_kind", "event_subkind", "action_type_candidate", "action_summary", "participants", "object_spec", "amount", "event_date", "event_end_date", "time_text", "time_precision", "current_state_candidate", "succession_candidate", "legal_effect_candidate", "defense_candidate", "commercial_successor_candidates", "meeting_clause_refs", "actio_relevance_candidates", "identity_signature", "property_cluster_id", "affected_asset_cluster_ids", "doc_semantic_flags", "confidence", "fraction_ref"] + EVIDENCE_FIELDS = ["evidence_index", "doc_uid", "title", "title_normalized", "doc_type", "key_facts", "key_dates", "key_amounts", "key_parties", "source_pointer", "property_cluster_id", "doc_semantic_flags", "multi_event_registry", "component_summary", "component_details"] + + def _parse_json(raw: str, label: str) -> dict[str, Any]: + text = (raw or "").strip() + if not text or text == "null": + raise RuntimeError(f"{label}: empty injected JSON") + decoder = json.JSONDecoder() + obj, idx = decoder.raw_decode(text) + if text[idx:].strip(): + raise RuntimeError(f"{label}: trailing data after JSON object") + if not isinstance(obj, dict): + raise RuntimeError(f"{label}: root must be object") + return obj + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _uniq(values: list[Any]) -> list[Any]: + seen: set[str] = set() + out: list[Any] = [] + for value in values: + marker = json.dumps(value, ensure_ascii=False, sort_keys=True) if isinstance(value, (dict, list)) else str(value) + if marker not in seen: + seen.add(marker) + out.append(value) + return out + + def _natural_key(value: Any) -> list[Any]: + return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", str(value))] + + def _sorted_ids(values: set[str] | list[str]) -> list[str]: + return sorted([str(v) for v in values if str(v)], key=_natural_key) + + def _blob(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True).lower() + + def _matches(value: Any, terms: list[str]) -> bool: + haystack = _blob(value) + return any(str(term).lower() in haystack for term in terms if str(term).strip()) + + def _copy_fields(source: dict[str, Any], fields: list[str]) -> dict[str, Any]: + out: dict[str, Any] = {} + for field in fields: + if field in source: + out[field] = source.get(field) + return out + + def _project_meeting(record: dict[str, Any]) -> dict[str, Any]: + out = _copy_fields(record, MEETING_FIELDS) + if not out.get("text_excerpt") and record.get("text"): + out["text_excerpt"] = str(record.get("text"))[:240] + return out + + def _project_event(record: dict[str, Any]) -> dict[str, Any]: + return _copy_fields(record, EVENT_FIELDS) + + def _project_evidence(record: dict[str, Any]) -> dict[str, Any]: + return _copy_fields(record, EVIDENCE_FIELDS) + + def _accept_stage_a(stage_a: dict[str, Any]) -> None: + checks = [ + ("stage_a_context.schema_version", stage_a.get("schema_version") == "task_c_bo_stage_a_context.v1"), + ("stage_a_context.status", stage_a.get("status") == "READY"), + ("meeting_clause_map.schema_version", (stage_a.get("meeting_clause_map") or {}).get("schema_version") == "meeting_clause_map.v1"), + ("event_candidate_map.schema_version", (stage_a.get("event_candidate_map") or {}).get("schema_version") == "event_candidate_map.v2"), + ("evidence_authority_map.schema_version", (stage_a.get("evidence_authority_map") or {}).get("schema_version") == "evidence_authority_map.v2"), + ("juristic_act_taxonomy_map.schema_version", (stage_a.get("juristic_act_taxonomy_map") or {}).get("schema_version") == "juristic_act_taxonomy_map.v1"), + ("global_hazard_seed.schema_version", (stage_a.get("global_hazard_seed") or {}).get("schema_version") == "global_hazard_seed.v1"), + ("stage_b_worker_contract.context_is_read_only", (stage_a.get("stage_b_worker_contract") or {}).get("context_is_read_only") is True), + ("stage_b_worker_contract.bo_id_finalization_allowed", (stage_a.get("stage_b_worker_contract") or {}).get("bo_id_finalization_allowed") is False), + ("stage_b_worker_contract.candidate_generation_allowed", (stage_a.get("stage_b_worker_contract") or {}).get("candidate_generation_allowed") is True), + ] + failed = [name for name, ok in checks if not ok] + if failed: + raise RuntimeError(f"Stage A acceptance failed: {failed}") + + def _matched_domains(record: dict[str, Any]) -> list[str]: + return [domain_id for domain_id, terms in ALL_DOMAIN_TERMS.items() if _matches(record, terms)] + + def _select(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + meeting_map = stage_a.get("meeting_clause_map") or {} + event_map = stage_a.get("event_candidate_map") or {} + evidence_map = stage_a.get("evidence_authority_map") or {} + by_clause = meeting_map.get("by_clause_id") or {} + by_event = event_map.get("by_candidate_id") or {} + by_evidence = evidence_map.get("by_evidence_index") or {} + + clause_ids: set[str] = set() + event_ids: set[str] = set() + evidence_ids: set[str] = set() + + for key in config["meeting_sources"]: + for value in _as_list(meeting_map.get(key)): + value_id = str(value) + if _matches(by_clause.get(value_id) or {}, config["terms"]): + clause_ids.add(value_id) + for key in config["event_sources"]: + for value in _as_list(event_map.get(key)): + value_id = str(value) + if _matches(by_event.get(value_id) or {}, config["terms"]): + event_ids.add(value_id) + + for clause_id, record in by_clause.items(): + if _matches(record, config["terms"]): + clause_ids.add(str(clause_id)) + for event_id, record in by_event.items(): + if _matches(record, config["terms"]): + event_ids.add(str(event_id)) + for evidence_id, record in by_evidence.items(): + if _matches(record, config["terms"]): + evidence_ids.add(str(evidence_id)) + + for _ in range(2): + for evidence_id in list(evidence_ids): + event_ids.update(str(v) for v in _as_list((event_map.get("by_evidence_index") or {}).get(evidence_id))) + for clause_id in list(clause_ids): + event_ids.update(str(v) for v in _as_list((event_map.get("by_meeting_clause_ref") or {}).get(clause_id))) + for event_id in list(event_ids): + event = by_event.get(event_id) or {} + source_evidence = event.get("source_evidence_index") + if source_evidence: + evidence_ids.add(str(source_evidence)) + for ref in _as_list(event.get("meeting_clause_refs")): + clause_ids.add(str(ref)) + + clause_ids = {v for v in clause_ids if v in by_clause} + event_ids = {v for v in event_ids if v in by_event} + evidence_ids = {v for v in evidence_ids if v in by_evidence} + + selected_meetings = [_project_meeting(by_clause[v]) for v in _sorted_ids(clause_ids)] + selected_events = [_project_event(by_event[v]) for v in _sorted_ids(event_ids)] + selected_evidence = [_project_evidence(by_evidence[v]) for v in _sorted_ids(evidence_ids)] + + shared: list[dict[str, Any]] = [] + for item_type, id_values, source in [ + ("meeting_clause", clause_ids, by_clause), + ("event_candidate", event_ids, by_event), + ("evidence", evidence_ids, by_evidence), + ]: + for item_id in _sorted_ids(id_values): + domains = _matched_domains(source.get(item_id) or {}) + if len(domains) > 1: + shared.append({"item_type": item_type, "id": item_id, "matched_domain_ids": domains}) + + return { + "meeting_clause_ids": _sorted_ids(clause_ids), + "event_candidate_ids": _sorted_ids(event_ids), + "evidence_indexes": _sorted_ids(evidence_ids), + "selected_meetings": selected_meetings, + "selected_events": selected_events, + "selected_evidence": selected_evidence, + "shared": shared, + } + + def _taxonomy_subset(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + taxonomy = stage_a.get("juristic_act_taxonomy_map") or {} + axes = taxonomy.get("axis_groups") or {} + return { + "schema_version": taxonomy.get("schema_version"), + "categories": taxonomy.get("categories") or {}, + "example_to_gubun": taxonomy.get("example_to_gubun") or {}, + "axis_groups": {key: axes.get(key) for key in config["taxonomy_axes"] if key in axes}, + "source_digest_sha256": taxonomy.get("source_digest_sha256"), + } + + def _hazard_subset(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + hazard = stage_a.get("global_hazard_seed") or {} + return {key: hazard.get(key) for key in config["hazard_keys"] if key in hazard} + + def _part1_review(stage_a: dict[str, Any], selected: dict[str, Any], config: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]: + handoff = stage_a.get("part1_soft_gate_handoff") or {} + guard = { + "schema_version": handoff.get("schema_version"), + "status": handoff.get("status"), + "stage2_auto_progression_allowed": handoff.get("stage2_auto_progression_allowed"), + "usage_rules": handoff.get("usage_rules") or {}, + } + selected_e = set(selected["evidence_indexes"]) + selected_ev = set(selected["event_candidate_ids"]) + selected_m = set(selected["meeting_clause_ids"]) + relevant: list[dict[str, Any]] = [] + for item in _as_list(handoff.get("review_items_compact")): + refs = item.get("source_refs") if isinstance(item, dict) else {} + domain_text = _blob({ + "domain_hints": item.get("domain_hints"), + "target_task_hints": item.get("target_task_hints"), + "issue_type": item.get("issue_type"), + "review_instruction": item.get("review_instruction"), + }) + ref_hit = bool( + selected_e.intersection(str(v) for v in _as_list(refs.get("evidence_indexes"))) + or selected_ev.intersection(str(v) for v in _as_list(refs.get("event_candidate_ids"))) + or selected_m.intersection(str(v) for v in _as_list(refs.get("meeting_clause_refs"))) + ) + if ref_hit or any(str(term).lower() in domain_text for term in config["terms"]): + relevant.append(item) + return guard, relevant + + + def _build_stage_a_context() -> dict[str, Any]: + existence_check = _preflight_required_files() + docs = _read_required_docs_once() + event_candidates = _parse_json_doc("evidence_event_candidates.json", docs["evidence_event_candidates.json"]) + evidence_indexed = _parse_json_doc("evidence_indexed.json", docs["evidence_indexed.json"]) + meeting_clause_map = _build_meeting_clause_map(docs["client_meeting.md"]) + event_candidate_map = _build_event_candidate_map(event_candidates) + evidence_authority_map = _build_evidence_authority_map(evidence_indexed) + juristic_act_taxonomy_map = _build_juristic_act_map(docs["Default_Agent/Juristic_Act.md"]) + _validate_juristic_act_map(juristic_act_taxonomy_map) + if not evidence_authority_map["evidence_index_set"]: + raise RuntimeError("No finalized evidence_index values available") + input_digests = {name: _sha(docs[name]) for name in REQUIRED_FILES} + part1_handoff = _compact_part1_soft_gate_handoff( + existence_check, input_digests, meeting_clause_map, event_candidate_map, evidence_authority_map + ) + return { + "schema_version": "task_c_bo_stage_a_context.v1", + "stage": "Task_C_BO.Stage_A", + "status": "READY", + "created_at_utc": datetime.now(timezone.utc).isoformat(), + "allowed_input_files": REQUIRED_FILES, + "forbidden_files_attested": ["evidence_all.json", "BO.json", "actio_case_signals.json", "case_liability_signals.json", "legal_effect_signals.json"], + "file_io_policy": { + "list_docs_called_for": REQUIRED_FILES, + "read_docs_called_once_for": REQUIRED_FILES, + "raw_evidence_all_read": False, + }, + "existence_check": existence_check, + "input_digests_sha256": input_digests, + "meeting_clause_map": meeting_clause_map, + "event_candidate_map": event_candidate_map, + "evidence_authority_map": evidence_authority_map, + "juristic_act_taxonomy_map": juristic_act_taxonomy_map, + "global_hazard_seed": _build_global_hazard_seed(meeting_clause_map, event_candidate_map, evidence_authority_map), + "part1_soft_gate_handoff": part1_handoff, + "stage_b_worker_contract": { + "context_is_read_only": True, + "bo_id_finalization_allowed": False, + "candidate_generation_allowed": True, + "must_use_evidence_index_set": evidence_authority_map["evidence_index_set"], + }, + } + + def _tier_ids(selected: dict[str, Any], config: dict[str, Any]) -> dict[str, list[str]]: + primary: list[str] = [] + support: list[str] = [] + review_only: list[str] = [] + records = [ + ("meeting", "clause_id", selected["selected_meetings"]), + ("event", "candidate_id", selected["selected_events"]), + ("evidence", "evidence_index", selected["selected_evidence"]), + ] + primary_terms = PRIMARY_TERMS[config["domain_id"]] + for kind, id_key, items in records: + for item in items: + item_id = str(item.get(id_key) or "") + if kind == "meeting": + direct_value = {key: item.get(key) for key in ("heading", "text_excerpt", "tags")} + elif kind == "event": + direct_value = {key: item.get(key) for key in ("event_kind", "event_subkind", "action_summary", "object_spec", "participants", "amount", "fraction_ref", "doc_semantic_flags")} + else: + direct_value = {key: item.get(key) for key in ("title", "doc_type", "key_facts", "key_dates", "key_amounts", "key_parties", "doc_semantic_flags", "component_summary", "component_details")} + direct_blob = _blob(direct_value) + strong_hits = [term for term in primary_terms if str(term).lower() in direct_blob] + broad_hits = [term for term in config["terms"] if str(term).lower() in direct_blob] + if strong_hits: + primary.append(item_id) + elif broad_hits: + review_only.append(item_id) + else: + support.append(item_id) + return {"primary_ids": _uniq(primary), "support_ids": _uniq(support), "review_only_ids": _uniq(review_only)} + + def _tier_project(items: list[dict[str, Any]], id_key: str, kind: str, tiers: dict[str, list[str]], config: dict[str, Any]) -> list[dict[str, Any]]: + primary = set(tiers["primary_ids"]) + review = set(tiers["review_only_ids"]) + projected: list[dict[str, Any]] = [] + support_fields = { + "meeting": ["clause_id", "section_key", "heading", "text_excerpt", "tags"], + "event": ["candidate_id", "source_evidence_index", "event_or_state", "event_kind", "event_subkind", "action_summary", "participants", "object_spec", "amount", "event_date", "time_text", "meeting_clause_refs", "fraction_ref"], + "evidence": ["evidence_index", "title", "doc_type", "key_dates", "key_amounts", "key_parties", "source_pointer", "doc_semantic_flags", "component_summary"], + } + for item in items: + item_id = str(item.get(id_key) or "") + if item_id in primary: + projected.append({**item, "routing_tier": "primary"}) + continue + if item_id in review: + projected.append({ + id_key: item_id, + "routing_tier": "review_only", + "source_type": kind, + "match_reason": [term for term in config["terms"] if str(term).lower() in _blob(item)][:4], + **({"source_evidence_index": item.get("source_evidence_index")} if kind == "event" else {}), + }) + continue + projected.append({ + **{key: item.get(key) for key in support_fields[kind] if key in item}, + "routing_tier": "support", + }) + return projected + + def _build_slice(stage_a: dict[str, Any], config: dict[str, Any], run_fingerprint: str) -> dict[str, Any]: + _accept_stage_a(stage_a) + selected = _select(stage_a, config) + all_clauses = _as_dict(_as_dict(stage_a.get("meeting_clause_map")).get("by_clause_id")) + selected_clause_ids = {str(item.get("clause_id")) for item in selected["selected_meetings"]} + for clause_id in _as_list(_as_dict(stage_a.get("meeting_clause_map")).get("clause_order")): + if str(clause_id) in selected_clause_ids: + continue + record = _as_dict(all_clauses.get(str(clause_id))) + selected["selected_meetings"].append(_project_meeting(record)) + selected["meeting_clause_ids"].append(str(clause_id)) + selected["meeting_clause_ids"] = _sorted_ids(set(selected["meeting_clause_ids"])) + part1_guard, part1_items = _part1_review(stage_a, selected, config) + tiers = _tier_ids(selected, config) + selected_meetings = _tier_project(selected["selected_meetings"], "clause_id", "meeting", tiers, config) + selected_events = _tier_project(selected["selected_events"], "candidate_id", "event", tiers, config) + selected_evidence = _tier_project(selected["selected_evidence"], "evidence_index", "evidence", tiers, config) + meeting_map = stage_a["meeting_clause_map"] + event_map = stage_a["event_candidate_map"] + evidence_map = stage_a["evidence_authority_map"] + slice_body = { + "schema_version": "task_c_bo_stage_b_domain_slice.v1", + "router_status": "READY", + "domain_id": config["domain_id"], + "domain_label": config["domain_label"], + "allowed_legal_effect_bo_types": config["allowed_legal_effect_bo_types"], + "source_stage_a_schema_version": stage_a["schema_version"], + "source_stage_a_status": stage_a["status"], + "source_stage_a_created_at_utc": stage_a["created_at_utc"], + "run_fingerprint": run_fingerprint, + "input_digests_sha256": stage_a["input_digests_sha256"], + "stage_b_worker_contract": stage_a["stage_b_worker_contract"], + "source_universe": { + "meeting_clause_ids": meeting_map.get("clause_order") or [], + "event_candidate_ids": event_map.get("candidate_id_set") or [], + "evidence_indexes": evidence_map.get("evidence_index_set") or [], + }, + "selected_meeting_clauses": selected_meetings, + "selected_event_candidates": selected_events, + "selected_evidence_authority": selected_evidence, + "juristic_act_taxonomy_subset": _taxonomy_subset(stage_a, config), + "hazard_subset": _hazard_subset(stage_a, config), + "part1_review_guard": part1_guard, + "part1_review_items_relevant": part1_items, + "shared_or_ambiguous_items": selected["shared"], + "routing_audit": { + **tiers, + "activation_reasons": ["deterministic domain terms", "source-reference closure"], + "source_count": { + "meeting": len(selected_meetings), + "event": len(selected_events), + "evidence": len(selected_evidence), + }, + "broadness_ratio": round(len(tiers["review_only_ids"]) / max(1, sum(len(v) for v in tiers.values())), 4), + }, + } + slice_body["routing_audit"]["slice_bytes"] = len(_canonical(slice_body).encode("utf-8")) + slice_body["slice_digest_sha256"] = _sha(slice_body) + return {"stage_b_domain_slice": slice_body} + + def main() -> None: + _init("task-c-bo-a0-context-domain-slice-compiler") + stage_a = _build_stage_a_context() + run_fingerprint = _sha(stage_a["input_digests_sha256"]) + stage_a["run_fingerprint"] = run_fingerprint + slices = {config["domain_id"]: _build_slice(stage_a, config, run_fingerprint) for config in DOMAIN_CONFIGS} + domain_paths = {domain_id: f"stage1_tmp/task_c_bo/domain_slices/{domain_id}.json" for domain_id in slices} + domain_digests = {domain_id: root["stage_b_domain_slice"]["slice_digest_sha256"] for domain_id, root in slices.items()} + domain_sources = { + domain_id: { + "meeting_clause_ids": root["stage_b_domain_slice"]["routing_audit"]["primary_ids"] + root["stage_b_domain_slice"]["routing_audit"]["support_ids"] + root["stage_b_domain_slice"]["routing_audit"]["review_only_ids"], + "event_candidate_ids": [str(item.get("candidate_id")) for item in root["stage_b_domain_slice"]["selected_event_candidates"] if item.get("candidate_id")], + "evidence_indexes": [str(item.get("evidence_index")) for item in root["stage_b_domain_slice"]["selected_evidence_authority"] if item.get("evidence_index")], + "meeting_clause_refs": [str(item.get("clause_id")) for item in root["stage_b_domain_slice"]["selected_meeting_clauses"] if item.get("clause_id")], + } + for domain_id, root in slices.items() + } + source_manifest = { + "schema_version": "task_c_bo_source_universe_manifest.v1", + "status": "READY", + "run_fingerprint": run_fingerprint, + "input_digests_sha256": stage_a["input_digests_sha256"], + "source_universe": { + "meeting_clause_ids": stage_a["meeting_clause_map"].get("clause_order") or [], + "event_candidate_ids": stage_a["event_candidate_map"].get("candidate_id_set") or [], + "evidence_indexes": stage_a["evidence_authority_map"].get("evidence_index_set") or [], + }, + "domain_source_universes": domain_sources, + "counts": { + "meeting_clauses": len(stage_a["meeting_clause_map"].get("clause_order") or []), + "event_candidates": len(stage_a["event_candidate_map"].get("candidate_id_set") or []), + "evidence_indexes": len(stage_a["evidence_authority_map"].get("evidence_index_set") or []), + }, + "domain_slice_digests_sha256": domain_digests, + } + _write_json_verified("stage1_tmp/task_c_bo/stage_a_context.json", {"stage_a_context": stage_a}) + _write_json_verified("stage1_tmp/task_c_bo/source_universe_manifest.json", source_manifest) + for domain_id, root in slices.items(): + _write_json_verified(domain_paths[domain_id], root) + print(json.dumps({"a0_manifest": { + "schema_version": "task_c_bo_a0_manifest.v1", + "status": "READY", + "run_fingerprint": run_fingerprint, + "stage_a_context_path": "stage1_tmp/task_c_bo/stage_a_context.json", + "source_manifest_path": "stage1_tmp/task_c_bo/source_universe_manifest.json", + "domain_slice_paths": domain_paths, + "input_digests_sha256": stage_a["input_digests_sha256"], + "counts": source_manifest["counts"], + }}, ensure_ascii=False, separators=(",", ":"))) + + if __name__ == "__main__": + try: + main() + except Exception as exc: + print(json.dumps({"a0_manifest": {"schema_version": "task_c_bo_a0_manifest.v1", "status": "FAILED", "error": str(exc)}}, ensure_ascii=False)) + raise + - task_name: Task_C_BO_Stage_B_B1_Money_Successor + llm_provider: google + llm_model: gemini-3.5-flash + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + Use localdocs exactly once to call read_docs for only this path: + stage1_tmp/task_c_bo/domain_slices/B1_Money_Successor.json + Do not call list_docs, do not read another path, and do not write any file. + The read result is the only factual input for this task. + + + + + - You MUST use only the JSON object read from . + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B1_Money_Successor"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B1_Money_Successor + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for money claim, payment, notice, admission, and commercial successor liability. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B1:001`, `B1:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - money claim origin: loan, sale price, deposit, settlement, construction price, unpaid balance + - due date, acceleration, demand, notice, payment deadline, or delayed performance + - partial payment, debt admission, payment memorandum, balance confirmation, account statement, or settlement confirmation + - business transfer, trade-name continuation, transferee liability, debt non-assumption clause, or commercial successor liability + - uncertain creditor/debtor/successor linkage that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use domain routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the assigned domain slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split money claim origin, maturity/demand, payment/admission, and successor liability when they are distinct legal-effect seeds. + 4. Preserve payment history, demand/notice hints, admission hints, successor liability hints, and successor-link uncertainty in `extensions.domain_payload`. Preserve agreed interest rate and agreed delay damage rate as literal strings from B1 `loan_components`/`secured_debt_components` (`component_details` of `selected_evidence_authority`) when present; never substitute statutory rates, and keep the supporting evidence indexes in `interest_rate_source_evidence_refs`. + 5. Use `domain_review_queue` for missing amount/date, uncertain creditor/debtor identity, conflicting payment status, or cross-domain successor merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Omit unsupported optional values and route unresolved extractable facts to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + Return exactly one JSON object with this sparse schema. Omit optional keys that are unsupported; preserve meaningful false and zero values. + { + "stage_b_domain_bo_seed_delta": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed_delta.v1", + "domain_id": "B1_Money_Successor", + "status": "READY|READY_WITH_REVIEW|FAILED", + "run_fingerprint": "sha256", + "slice_digest_sha256": "sha256", + "bo_seed_candidates": [ + { + "candidate_ref": "B1:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "Action": "short neutral source-backed draft", + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability" + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B1:R001", + "severity": "SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "candidate_refs": [], + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Part3|Part4|Stage2|human_review" + } + ] + } + } + Optional candidate keys allowed only when supported: JuristicAct, Legal_Keywords, core_field_base, amount, downstream_seed_refs. + Protected domain_payload keys whose source-backed content must be preserved when present: money_claim_details, payment_history, demand_or_notice_hints, admission_hints, successor_liability_hints, successor_link_uncertainty. + Do not emit transport_metadata, transport_candidate_refs, Reason, PriorAct, ReasonRefs, Evidence, EvidenceTitles, BO_ID, id, null-only scaffolding, or empty optional containers. + candidate_ref values must be contiguous in output order with the domain prefix and three-digit ordinal. + Status is FAILED on input guard failure, READY_WITH_REVIEW when the review queue is non-empty, otherwise READY. + + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B2_Secured_Registry + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + Use localdocs exactly once to call read_docs for only this path: + stage1_tmp/task_c_bo/domain_slices/B2_Secured_Registry.json + Do not call list_docs, do not read another path, and do not write any file. + The read result is the only factual input for this task. + + + + + - You MUST use only the JSON object read from . + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B2_Secured_Registry"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B2_Secured_Registry + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for secured debt, registry, payment allocation, auction, distribution, and derivative invalidity issues. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B2:001`, `B2:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - secured debt creation, change, repayment, payment allocation, and secured obligation balance + - mortgage, pledge, collateral security, registration, cancellation, correction, or invalidity chain + - auction, distribution, dividend, satisfaction, or residual balance facts + - derivative invalidity, registry error, title defect, or secured property dispute + - uncertain registry sequence or security/right-holder linkage that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use domain routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the assigned domain slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split secured debt chain, registry act, payment allocation, auction step, dividend distribution, and derivative invalidity when they are distinct legal-effect seeds. + 4. Preserve registry rows, secured debt chain, payment allocation, auction step, dividend distribution hints, derivative invalidity hints, and registry display values in `extensions.domain_payload`. Carry `fraction_ref` from source event candidates unchanged; do not merge or renumber. Populate `fractional_effect_events` as an event-reference list only (fraction-bearing setting/extinguishment/transfer events); never fold fraction states or normalize/reduce fractions (that is Stage 2 work). + 5. Use `domain_review_queue` for missing registry date, unclear secured obligation balance, conflicting registration sequence, or cross-domain property merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Omit unsupported optional values and route unresolved extractable facts to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + Return exactly one JSON object with this sparse schema. Omit optional keys that are unsupported; preserve meaningful false and zero values. + { + "stage_b_domain_bo_seed_delta": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed_delta.v1", + "domain_id": "B2_Secured_Registry", + "status": "READY|READY_WITH_REVIEW|FAILED", + "run_fingerprint": "sha256", + "slice_digest_sha256": "sha256", + "bo_seed_candidates": [ + { + "candidate_ref": "B2:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "Action": "short neutral source-backed draft", + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction" + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B2:R001", + "severity": "SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "candidate_refs": [], + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Part3|Part4|Stage2|human_review" + } + ] + } + } + Optional candidate keys allowed only when supported: JuristicAct, Legal_Keywords, core_field_base, amount, downstream_seed_refs. + Protected domain_payload keys whose source-backed content must be preserved when present: secured_debt_chain, payment_allocation, registry_rows, fractional_effect_events, mortgage_extinguishment_hints, auction_steps, dividend_distribution_hints, derivative_invalidity_hints. + Do not emit transport_metadata, transport_candidate_refs, Reason, PriorAct, ReasonRefs, Evidence, EvidenceTitles, BO_ID, id, null-only scaffolding, or empty optional containers. + candidate_ref values must be contiguous in output order with the domain prefix and three-digit ordinal. + Status is FAILED on input guard failure, READY_WITH_REVIEW when the review queue is non-empty, otherwise READY. + + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B3_Land_Valuation + llm_provider: google + llm_model: gemini-3.5-flash + llm_reasoning: medium + llm_verbosity: low + max_iterations: 2 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + Use localdocs exactly once to call read_docs for only this path: + stage1_tmp/task_c_bo/domain_slices/B3_Land_Valuation.json + Do not call list_docs, do not read another path, and do not write any file. + The read result is the only factual input for this task. + + + + + - You MUST use only the JSON object read from . + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B3_Land_Valuation"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B3_Land_Valuation + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for land use gain, possession/use, valuation, and asset-value facts. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B3:001`, `B3:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - land or building possession, use, occupation, benefit, rent-equivalent gain, or loss of use + - valuation fact, appraisal, market value, rent value, area, period, and calculation base + - delivery, possession start/end, unauthorized use, co-occupancy, or liability overlap + - asset identity, address, parcel, unit, area, or other value-bearing object description + - uncertain use period, value basis, possession status, or asset binding that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use domain routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the assigned domain slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split possession/use state, use-gain period, valuation fact, and liability overlap when they are distinct legal-effect seeds. + 4. Preserve valuation rows, valuation matrix, use-gain facts, possession state, asset binding hints, and liability overlap hints in `extensions.domain_payload`. Populate the `possession_state` two-track structure (possession facts vs. asserted legal causes) from selected event candidates only; never resolve which legal cause prevails (that is Stage 2 work); preserve conflicting assertions side by side. + 5. Use `domain_review_queue` for missing period, missing value base, unclear possession state, or cross-domain asset merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Omit unsupported optional values and route unresolved extractable facts to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + Return exactly one JSON object with this sparse schema. Omit optional keys that are unsupported; preserve meaningful false and zero values. + { + "stage_b_domain_bo_seed_delta": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed_delta.v1", + "domain_id": "B3_Land_Valuation", + "status": "READY|READY_WITH_REVIEW|FAILED", + "run_fingerprint": "sha256", + "slice_digest_sha256": "sha256", + "bo_seed_candidates": [ + { + "candidate_ref": "B3:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "Action": "short neutral source-backed draft", + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation" + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B3:R001", + "severity": "SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "candidate_refs": [], + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Part3|Part4|Stage2|human_review" + } + ] + } + } + Optional candidate keys allowed only when supported: JuristicAct, Legal_Keywords, core_field_base, amount, downstream_seed_refs. + Protected domain_payload keys whose source-backed content must be preserved when present: use_gain_facts, valuation_object, valuation_rows, valuation_matrix, possession_state, delivery_or_possession_start_hints, unregistered_building_hints, liability_overlap_hints. + Do not emit transport_metadata, transport_candidate_refs, Reason, PriorAct, ReasonRefs, Evidence, EvidenceTitles, BO_ID, id, null-only scaffolding, or empty optional containers. + candidate_ref values must be contiguous in output order with the domain prefix and three-digit ordinal. + Status is FAILED on input guard failure, READY_WITH_REVIEW when the review queue is non-empty, otherwise READY. + + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B4_Actio + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + Use localdocs exactly once to call read_docs for only this path: + stage1_tmp/task_c_bo/domain_slices/B4_Actio.json + Do not call list_docs, do not read another path, and do not write any file. + The read result is the only factual input for this task. + + + + + - You MUST use only the JSON object read from . + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B4_Actio"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B4_Actio + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for actio pauliana, preserved claim, debtor fraudulent act, beneficiary/transferee, remedy, cap, and defenses. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B4:001`, `B4:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - preserved claim existence, claim date, creditor/debtor identity, and claim amount basis + - debtor act, asset decrease, insolvency, common collateral impairment, and harmfulness + - beneficiary, transferee, bad faith/good faith, notice, value compensation, restitution, and remedy cap + - encumbrance, valuation, property transfer, mortgage, sale, gift, collateral provision, or registration act used as fraudulent act basis + - uncertain fraudulent act date, beneficiary chain, asset value, remedy mode, or defense rebuttal that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use domain routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the assigned domain slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split preserved claim, fraudulent transfer, balance-sheet impairment, encumbrance/valuation, beneficiary/transferee state, remedy/cap, and defense rebuttal when they are distinct legal-effect seeds. + 4. Preserve preserved claim bundle, fraudulent transfer facts, balance sheet, encumbrance timeline, valuation matrix, remedy/cap hints, and defense rebuttal in `extensions.domain_payload`. + 5. Use `domain_review_queue` for missing claim date, uncertain fraudulent act date, conflicting value basis, unclear beneficiary chain, or cross-domain asset/claim merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Omit unsupported optional values and route unresolved extractable facts to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + Return exactly one JSON object with this sparse schema. Omit optional keys that are unsupported; preserve meaningful false and zero values. + { + "stage_b_domain_bo_seed_delta": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed_delta.v1", + "domain_id": "B4_Actio", + "status": "READY|READY_WITH_REVIEW|FAILED", + "run_fingerprint": "sha256", + "slice_digest_sha256": "sha256", + "bo_seed_candidates": [ + { + "candidate_ref": "B4:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "Action": "short neutral source-backed draft", + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "actio_fraudulent_transfer" + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B4:R001", + "severity": "SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "candidate_refs": [], + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Part3|Part4|Stage2|human_review" + } + ] + } + } + Optional candidate keys allowed only when supported: JuristicAct, Legal_Keywords, core_field_base, amount, downstream_seed_refs. + Protected domain_payload keys whose source-backed content must be preserved when present: preserved_claim_bundle, fraudulent_transfer_facts, encumbrance_timeline, valuation_matrix, balance_sheet, remedy_mode_hints, value_compensation_cap_hints, defense_rebuttal. + Do not emit transport_metadata, transport_candidate_refs, Reason, PriorAct, ReasonRefs, Evidence, EvidenceTitles, BO_ID, id, null-only scaffolding, or empty optional containers. + candidate_ref values must be contiguous in output order with the domain prefix and three-digit ordinal. + Status is FAILED on input guard failure, READY_WITH_REVIEW when the review queue is non-empty, otherwise READY. + + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + Use localdocs exactly once to call read_docs for only this path: + stage1_tmp/task_c_bo/domain_slices/B5_Succession_Notice_Lien_Asset_Defense.json + Do not call list_docs, do not read another path, and do not write any file. + The read result is the only factual input for this task. + + + + + - You MUST use only the JSON object read from . + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B5_Succession_Notice_Lien_Asset_Defense"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B5_Succession_Notice_Lien_Asset_Defense + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for succession, notice lifecycle, lien, asset identity, possession, and major defenses. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B5:001`, `B5:002`, ... in deterministic order of the candidate seed list. + - If multiple lien seeds exist, assign `lien_id` values `B5:LIEN:001`, `B5:LIEN:002`, ... ordered by the earliest related event date (ties broken by `candidate_ref` lexicographic order). This id is transport-scope only, not a final BO id. + - Do not produce `violation_candidates` or `status_after_notice_receipt`; route such needs to `domain_review_queue` with `issue_type="legal_theory_required"`. + - `legal_sender_after_resolution.party` must always remain null in Stage 1 (value finalization is Stage 2 legal work); fill only its `status` and `succession_resolution_ref` from directly-read sources. + + + + Create separate seeds when supported for: + - general succession, specific succession, inheritance, assignment, acquisition, transfer, or party-status succession + - notice dispatch, arrival, receipt, refusal, content, timing, and legal effect of notice + - lien, possession, retained object, claim-object connection, asset alias, and asset identity + - simultaneous performance, setoff, payment, prescription, delivery, possession, or other major defense facts + - uncertain notice arrival, lien possession continuity, asset identity, succession scope, or defense linkage that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use domain routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the assigned domain slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split succession state, notice lifecycle, lien/possession state, asset identity, and defense rebuttal when they are distinct legal-effect seeds. + 4. Preserve succession resolution, notice lifecycle, lien state, possession state, asset alias, simultaneous-performance hints, setoff/payment defense hints, and defense rebuttal in `extensions.domain_payload`. Populate the `possession_state` two-track structure (possession facts vs. asserted legal causes) from selected event candidates only; never resolve which legal cause prevails (that is Stage 2 work); preserve conflicting assertions side by side. Assign `lien_id` deterministically and reference it from possession/notice payloads of the same seed group. Inherit `notice_id` from B1 `notice_components[*].notice_id_proposed` (`component_details` of `selected_evidence_authority`); when multiple notices exist, split seeds so each seed carries one notice lifecycle. Inherit `schedule_refs`/`registry_refs` (SCH-*/REG-* keys) and `canonical_label_candidate`/`usable_in_relief` into `asset_alias` unchanged. + 5. Use `domain_review_queue` for missing arrival evidence, uncertain asset identity, unclear possession continuity, conflicting succession scope, or cross-domain defense merge need. When `received_date` cannot be established from selected sources, leave it null and add a `domain_review_queue` item with `issue_type="missing_source"` (do not fabricate); the sealed drafting gate is applied downstream at FL. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Omit unsupported optional values and route unresolved extractable facts to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + Return exactly one JSON object with this sparse schema. Omit optional keys that are unsupported; preserve meaningful false and zero values. + { + "stage_b_domain_bo_seed_delta": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed_delta.v1", + "domain_id": "B5_Succession_Notice_Lien_Asset_Defense", + "status": "READY|READY_WITH_REVIEW|FAILED", + "run_fingerprint": "sha256", + "slice_digest_sha256": "sha256", + "bo_seed_candidates": [ + { + "candidate_ref": "B5:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "Action": "short neutral source-backed draft", + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense" + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B5:R001", + "severity": "SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "candidate_refs": [], + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Part3|Part4|Stage2|human_review" + } + ] + } + } + Optional candidate keys allowed only when supported: JuristicAct, Legal_Keywords, core_field_base, amount, downstream_seed_refs. + Protected domain_payload keys whose source-backed content must be preserved when present: succession_resolution, general_succession_status, asset_specific_succession_resolution, notice_lifecycle, lien_state, possession_state, asset_alias, defense_rebuttal, simultaneous_performance_hints, setoff_or_payment_defense_hints. + Do not emit transport_metadata, transport_candidate_refs, Reason, PriorAct, ReasonRefs, Evidence, EvidenceTitles, BO_ID, id, null-only scaffolding, or empty optional containers. + candidate_ref values must be contiguous in output order with the domain prefix and three-digit ordinal. + Status is FAILED on input guard failure, READY_WITH_REVIEW when the review queue is non-empty, otherwise READY. + + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_R0_seed_reducer_exception_planner + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: "#!/usr/bin/env python3\nfrom __future__ import annotations\n\nimport hashlib\nimport + itertools\nimport json\nimport re\nfrom datetime import datetime, timezone\nfrom + typing import Any\n\nimport httpx\n\nLOCALDOCS_URL = \"http://mcp-localdocs:8012/mcp\"\nMCP_HEADERS + = {\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/json, + text/event-stream\",\n}\nCLIENT = httpx.Client(timeout=60)\nMSG_IDS = itertools.count(10)\nJSON_DECODER + = json.JSONDecoder()\n\ndef _next_id() -> int:\n return next(MSG_IDS)\n\ndef + _parse_json_first(raw: str, label: str, allow_trailing: bool = False) -> Any:\n + \ text = (raw or \"\").lstrip(\"\\ufeff\").strip()\n if not text:\n raise + RuntimeError(f\"{label}: empty JSON text\")\n try:\n return json.loads(text)\n + \ except json.JSONDecodeError as first_error:\n try:\n value, + end = JSON_DECODER.raw_decode(text)\n except json.JSONDecodeError as + second_error:\n raise RuntimeError(f\"{label}: invalid JSON\") from + second_error\n if not allow_trailing and text[end:].strip():\n raise + RuntimeError(f\"{label}: trailing non-JSON data\") from first_error\n return + value\n\ndef _parse_rpc(raw: str) -> dict[str, Any]:\n events: list[dict[str, + Any]] = []\n for line in (raw or \"\").splitlines():\n if not line.startswith(\"data:\"):\n + \ continue\n candidate = line[5:].strip()\n if not candidate + or candidate == \"[DONE]\":\n continue\n try:\n parsed + = json.loads(candidate)\n except json.JSONDecodeError:\n continue\n + \ if isinstance(parsed, dict):\n events.append(parsed)\n if + events:\n return events[-1]\n parsed = _parse_json_first(raw, \"MCP + response\", allow_trailing=True)\n if not isinstance(parsed, dict):\n raise + RuntimeError(\"MCP response root must be object\")\n return parsed\n\ndef + _init(task_name: str) -> None:\n response = CLIENT.post(\n LOCALDOCS_URL,\n + \ json={\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n + \ \"method\": \"initialize\",\n \"params\": {\n \"protocolVersion\": + \"2025-03-26\",\n \"capabilities\": {},\n \"clientInfo\": + {\n \"name\": task_name,\n \"version\": + \"1.0\",\n \"user_id\": \"{{__user_hash__}}\",\n \"workspace_id\": + \"{{__workspace_hash__}}\",\n },\n },\n },\n + \ headers=MCP_HEADERS,\n )\n response.raise_for_status()\n session_id + = response.headers.get(\"mcp-session-id\")\n if session_id:\n MCP_HEADERS[\"mcp-session-id\"] + = session_id\n CLIENT.post(\n LOCALDOCS_URL,\n json={\"jsonrpc\": + \"2.0\", \"method\": \"notifications/initialized\"},\n headers=MCP_HEADERS,\n + \ ).raise_for_status()\n\ndef _call(name: str, arguments: dict[str, Any]) + -> dict[str, Any]:\n response = CLIENT.post(\n LOCALDOCS_URL,\n json={\n + \ \"jsonrpc\": \"2.0\",\n \"id\": _next_id(),\n \"method\": + \"tools/call\",\n \"params\": {\"name\": name, \"arguments\": arguments},\n + \ },\n headers=MCP_HEADERS,\n )\n response.raise_for_status()\n + \ payload = _parse_rpc(response.text)\n if payload.get(\"error\"):\n raise + RuntimeError(f\"MCP {name} failed: {payload['error']}\")\n result = payload.get(\"result\")\n + \ if not isinstance(result, dict):\n raise RuntimeError(f\"MCP {name} + returned no result\")\n if result.get(\"isError\") is True:\n raise + RuntimeError(f\"MCP {name} returned isError\")\n return payload\n\ndef _unwrap(payload: + dict[str, Any], label: str) -> Any:\n result = payload.get(\"result\") or + {}\n content = result.get(\"content\") or []\n texts = [block.get(\"text\") + for block in content if isinstance(block, dict) and block.get(\"text\")]\n if + not texts:\n raise RuntimeError(f\"{label}: empty MCP content\")\n raw + = texts[0]\n try:\n return _parse_json_first(raw, label, allow_trailing=True)\n + \ except RuntimeError:\n return raw\n\ndef _extract_doc(payload: dict[str, + Any], doc_name: str) -> str:\n outer = _unwrap(payload, f\"read_docs:{doc_name}\")\n + \ if isinstance(outer, dict) and isinstance(outer.get(\"results\"), list):\n + \ for item in outer[\"results\"]:\n if not isinstance(item, + dict):\n continue\n name = item.get(\"name\") or item.get(\"path\") + or item.get(\"doc_name\") or item.get(\"document_name\")\n if name + and not (str(name) == doc_name or str(name).endswith(\"/\" + doc_name)):\n continue\n + \ value = item.get(\"content\")\n if value is None:\n value + = item.get(\"text\")\n if value is None:\n value = + item.get(\"raw\")\n if value is not None:\n return + value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)\n + \ if isinstance(outer, dict) and doc_name in outer:\n value = outer[doc_name]\n + \ return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)\n + \ if isinstance(outer, str):\n return outer\n return json.dumps(outer, + ensure_ascii=False)\n\ndef _read_text(doc_name: str) -> str:\n return _extract_doc(_call(\"read_docs\", + {\"doc_names\": [doc_name]}), doc_name)\n\ndef _read_json(doc_name: str) -> + Any:\n raw = _read_text(doc_name)\n return _parse_json_first(raw, doc_name, + allow_trailing=True)\n\ndef _canonical(value: Any) -> str:\n return json.dumps(value, + ensure_ascii=False, sort_keys=True, separators=(\",\", \":\"))\n\ndef _sha(value: + Any) -> str:\n raw = value if isinstance(value, str) else _canonical(value)\n + \ return hashlib.sha256(raw.encode(\"utf-8\")).hexdigest()\n\ndef _write_json_verified(doc_name: + str, value: Any) -> str:\n content = json.dumps(value, ensure_ascii=False, + indent=2)\n _call(\"write_file\", {\"path\": doc_name, \"content\": content, + \"overwrite\": True})\n reread = _read_json(doc_name)\n if _canonical(reread) + != _canonical(value):\n raise RuntimeError(f\"{doc_name}: post-write + verification mismatch\")\n return _sha(content)\n\ndef _as_list(value: Any) + -> list[Any]:\n return value if isinstance(value, list) else []\n\ndef _as_dict(value: + Any) -> dict[str, Any]:\n return value if isinstance(value, dict) else {}\n\ndef + _uniq(values: list[Any]) -> list[Any]:\n seen: set[str] = set()\n output: + list[Any] = []\n for value in values:\n marker = _canonical(value) + if isinstance(value, (dict, list)) else str(value)\n if marker in seen:\n + \ continue\n seen.add(marker)\n output.append(value)\n + \ return output\n\ndef _natural_key(value: Any) -> list[Any]:\n return + [int(part) if part.isdigit() else part for part in re.split(r\"(\\d+)\", str(value))]\n\nTASK_NAME + = \"Task_C_BO_R0_seed_reducer_exception_planner\"\nWORKER_RAWS = {\n \"B1_Money_Successor\": + r\"\"\"{{prev.Task_C_BO_Stage_B_B1_Money_Successor.json_output}}\"\"\",\n \"B2_Secured_Registry\": + r\"\"\"{{prev.Task_C_BO_Stage_B_B2_Secured_Registry.json_output}}\"\"\",\n \"B3_Land_Valuation\": + r\"\"\"{{prev.Task_C_BO_Stage_B_B3_Land_Valuation.json_output}}\"\"\",\n \"B4_Actio\": + r\"\"\"{{prev.Task_C_BO_Stage_B_B4_Actio.json_output}}\"\"\",\n \"B5_Succession_Notice_Lien_Asset_Defense\": + r\"\"\"{{prev.Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense.json_output}}\"\"\",\n}\nSEED_PATHS + = {domain: f\"stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/{domain}.json\" + for domain in WORKER_RAWS}\nACTION_TYPES = {\n \"법률행위(legal acts)\", \"준법률행위(quasi-legal + acts)\", \"사실행위(factual acts)\",\n \"위법행위(unlawful acts)\", \"소송행위(litigation + acts)\",\n}\nDOMAIN_PAYLOAD_DEFAULTS = json.loads(r'''{\"B1_Money_Successor\":{\"money_claim_details\":{\"claim_type_candidate\":null,\"principal_amount_hint\":null,\"interest_hint\":null,\"delay_damage_hint\":null,\"agreed_interest_rate\":null,\"agreed_delay_damage_rate\":null,\"interest_accrual_start_hint\":null,\"interest_rate_source_evidence_refs\":[],\"unpaid_balance_hint\":null,\"creditor_candidate\":null,\"debtor_candidate\":null},\"payment_history\":[],\"demand_or_notice_hints\":[],\"admission_hints\":[],\"successor_liability_hints\":[],\"successor_link_uncertainty\":null},\"B2_Secured_Registry\":{\"secured_debt_chain\":[],\"payment_allocation\":[],\"registry_rows\":[],\"fractional_effect_events\":[],\"mortgage_extinguishment_hints\":[],\"auction_steps\":[],\"dividend_distribution_hints\":[],\"derivative_invalidity_hints\":[]},\"B3_Land_Valuation\":{\"use_gain_facts\":[],\"valuation_object\":{},\"valuation_rows\":[],\"valuation_matrix\":{},\"possession_state\":{},\"delivery_or_possession_start_hints\":[],\"unregistered_building_hints\":[],\"liability_overlap_hints\":[]},\"B4_Actio\":{\"preserved_claim_bundle\":{},\"fraudulent_transfer_facts\":{},\"encumbrance_timeline\":[],\"valuation_matrix\":{},\"balance_sheet\":{},\"remedy_mode_hints\":[],\"value_compensation_cap_hints\":[],\"defense_rebuttal\":{}},\"B5_Succession_Notice_Lien_Asset_Defense\":{\"succession_resolution\":{},\"general_succession_status\":{},\"asset_specific_succession_resolution\":{},\"notice_lifecycle\":{},\"lien_state\":{},\"possession_state\":{},\"asset_alias\":{},\"defense_rebuttal\":{},\"simultaneous_performance_hints\":[],\"setoff_or_payment_defense_hints\":[]}}''')\nCOMMON_DOMAIN_DEFAULTS + = {\n \"title_hint\": None, \"summary_hint\": None, \"conflict_patterns\": + [], \"asset_bindings\": [],\n \"parties_hint\": [], \"temporal_hint\": None, + \"hazard_flags\": [], \"legal_effect_tags\": [],\n \"event_state_type\": + None, \"action_summary\": None, \"time_interval_hint\": None,\n \"asset_binding_hint\": + None, \"mixed_contract_split_hint\": None, \"conflict_pattern_hint\": None,\n + \ \"candidate_details\": {}, \"facts\": {}, \"conflict_flags\": [],\n}\n\ndef + _strict_worker_json(raw: str, label: str) -> dict[str, Any]:\n text = (raw + or \"\").lstrip(\"\uFEFF\").strip()\n fence = re.fullmatch(r\"```(?:json)? + *(.*?) *```\", text, flags=re.DOTALL | re.IGNORECASE)\n if fence:\n text + = fence.group(1).strip()\n value = _parse_json_first(text, label, allow_trailing=False)\n + \ for _ in range(3):\n if isinstance(value, dict) and set(value).issubset({\"success\", + \"output\", \"json_output\"}) and any(key in value for key in (\"output\", \"json_output\")):\n + \ value = value.get(\"json_output\", value.get(\"output\"))\n if + isinstance(value, str):\n value = _parse_json_first(value, label, + allow_trailing=False)\n continue\n if isinstance(value, dict) + and set(value).issubset({\"type\", \"text\"}) and isinstance(value.get(\"text\"), + str):\n value = _parse_json_first(value[\"text\"], label, allow_trailing=False)\n + \ continue\n break\n if not isinstance(value, dict):\n raise + RuntimeError(f\"{label}: root must be object\")\n return value\n\ndef _deep_defaults(default: + Any, supplied: Any) -> Any:\n if isinstance(default, dict):\n incoming + = supplied if isinstance(supplied, dict) else {}\n result = {key: _deep_defaults(value, + incoming[key]) if key in incoming else value for key, value in default.items()}\n + \ for key, value in incoming.items():\n if key not in result:\n + \ result[key] = value\n return result\n return supplied\n\ndef + _source_refs(candidate: dict[str, Any]) -> dict[str, list[str]]:\n provenance + = _as_dict(candidate.get(\"provenance\"))\n return {\n \"source_event_candidate_ids\": + _uniq([str(v) for v in _as_list(provenance.get(\"source_event_candidate_ids\")) + if v]),\n \"source_meeting_clause_ids\": _uniq([str(v) for v in _as_list(provenance.get(\"source_meeting_clause_ids\")) + if v]),\n \"source_evidence_indexes\": _uniq([str(v) for v in _as_list(candidate.get(\"source_evidence_indexes\")) + + _as_list(provenance.get(\"source_evidence_indexes\")) if v]),\n }\n\ndef + _expand_candidate(candidate: dict[str, Any], domain: str, manifest: dict[str, + Any]) -> dict[str, Any]:\n refs = _source_refs(candidate)\n domain_payload + = _as_dict(_as_dict(candidate.get(\"extensions\")).get(\"domain_payload\"))\n + \ defaults = {**COMMON_DOMAIN_DEFAULTS, **DOMAIN_PAYLOAD_DEFAULTS[domain]}\n + \ expanded_payload = _deep_defaults(defaults, domain_payload)\n core = + _as_dict(candidate.get(\"core_field_base\"))\n amount = candidate.get(\"amount\") + if isinstance(candidate.get(\"amount\"), dict) else None\n downstream = _as_dict(candidate.get(\"downstream_seed_refs\"))\n + \ return {\n \"candidate_ref\": candidate[\"candidate_ref\"],\n \"BOType\": + candidate[\"BOType\"],\n \"ActionType\": candidate[\"ActionType\"],\n + \ \"JuristicAct\": candidate.get(\"JuristicAct\") if isinstance(candidate.get(\"JuristicAct\"), + dict) else {\"label\": None},\n \"Action\": candidate[\"Action\"],\n + \ \"Reason\": None,\n \"PriorAct\": None,\n \"ReasonRefs\": + [],\n \"Legal_Keywords\": _as_list(candidate.get(\"Legal_Keywords\")),\n + \ \"core_field_base\": {\n \"date\": core.get(\"date\"), \"amount\": + core.get(\"amount\"), \"parties\": _as_list(core.get(\"parties\")),\n \"asset_refs\": + _as_list(core.get(\"asset_refs\")), \"legal_effect_summary\": core.get(\"legal_effect_summary\"),\n + \ },\n \"amount\": amount,\n \"source_evidence_indexes\": + refs[\"source_evidence_indexes\"],\n \"provenance\": refs,\n \"downstream_seed_refs\": + {\n \"legal_effect_structure_seed\": downstream.get(\"legal_effect_structure_seed\"),\n + \ \"claim_group_seed\": downstream.get(\"claim_group_seed\"),\n \"asset_cluster_seed\": + downstream.get(\"asset_cluster_seed\"),\n \"issue_cluster_seed\": + downstream.get(\"issue_cluster_seed\"),\n },\n \"extensions\": + {\"domain_payload\": expanded_payload},\n \"transport_metadata\": {\n + \ \"stage\": \"Task_C_BO.Stage_B.DomainSeed\", \"source_task\": domain,\n + \ \"run_fingerprint\": manifest[\"run_fingerprint\"],\n \"slice_digest_sha256\": + manifest[\"domain_slice_digests_sha256\"][domain],\n },\n \"transport_candidate_refs\": + refs,\n }\n\ndef _validate_refs(candidate: dict[str, Any], domain: str, manifest: + dict[str, Any]) -> list[str]:\n refs = _source_refs(candidate)\n global_u + = _as_dict(manifest.get(\"source_universe\"))\n domain_u = _as_dict(_as_dict(manifest.get(\"domain_source_universes\")).get(domain))\n + \ failures: list[str] = []\n checks = [\n (\"source_event_candidate_ids\", + \"event_candidate_ids\"),\n (\"source_meeting_clause_ids\", \"meeting_clause_refs\"),\n + \ (\"source_evidence_indexes\", \"evidence_indexes\"),\n ]\n for + ref_key, universe_key in checks:\n global_key = \"meeting_clause_ids\" + if ref_key == \"source_meeting_clause_ids\" else universe_key\n for value + in refs[ref_key]:\n if value not in set(str(v) for v in _as_list(global_u.get(global_key))):\n + \ failures.append(f\"{candidate.get('candidate_ref')}:{ref_key}:outside_global:{value}\")\n + \ if value not in set(str(v) for v in _as_list(domain_u.get(universe_key))):\n + \ failures.append(f\"{candidate.get('candidate_ref')}:{ref_key}:outside_slice:{value}\")\n + \ return failures\n\ndef _substantive_key(candidate: dict[str, Any]) -> str:\n + \ return _sha({key: candidate.get(key) for key in (\"BOType\", \"ActionType\", + \"JuristicAct\", \"Action\", \"core_field_base\", \"amount\", \"downstream_seed_refs\", + \"extensions\")})\n\ndef main() -> None:\n _init(\"task-c-bo-r0-seed-reducer-exception-planner\")\n + \ stage_root = _read_json(\"stage1_tmp/task_c_bo/stage_a_context.json\")\n + \ stage_a = _as_dict(_as_dict(stage_root).get(\"stage_a_context\"))\n manifest + = _read_json(\"stage1_tmp/task_c_bo/source_universe_manifest.json\")\n if + stage_a.get(\"status\") != \"READY\" or manifest.get(\"status\") != \"READY\":\n + \ raise RuntimeError(\"A0 artifacts are not READY\")\n if stage_a.get(\"run_fingerprint\") + != manifest.get(\"run_fingerprint\"):\n raise RuntimeError(\"A0 run fingerprint + mismatch\")\n ledger_candidates: list[dict[str, Any]] = []\n reviews: + list[dict[str, Any]] = []\n failures: list[str] = []\n seed_counts: dict[str, + int] = {}\n for domain, raw in WORKER_RAWS.items():\n parsed = _strict_worker_json(raw, + domain)\n delta = _as_dict(parsed.get(\"stage_b_domain_bo_seed_delta\"))\n + \ if delta.get(\"schema_version\") != \"task_c_bo_stage_b_domain_bo_seed_delta.v1\" + or delta.get(\"domain_id\") != domain:\n failures.append(f\"{domain}:delta + schema/domain mismatch\")\n continue\n if delta.get(\"status\") + == \"FAILED\":\n failures.append(f\"{domain}:worker FAILED\")\n if + delta.get(\"run_fingerprint\") != manifest.get(\"run_fingerprint\"):\n failures.append(f\"{domain}:run + fingerprint mismatch\")\n if delta.get(\"slice_digest_sha256\") != manifest.get(\"domain_slice_digests_sha256\", + {}).get(domain):\n failures.append(f\"{domain}:slice digest mismatch\")\n + \ candidates = _as_list(delta.get(\"bo_seed_candidates\"))\n prefix + = domain.split(\"_\", 1)[0]\n expected_refs = [f\"{prefix}:{index:03d}\" + for index in range(1, len(candidates) + 1)]\n actual_refs = [str(_as_dict(candidate).get(\"candidate_ref\") + or \"\") for candidate in candidates]\n if actual_refs != expected_refs:\n + \ failures.append(f\"{domain}:candidate_ref sequence mismatch\")\n + \ expanded: list[dict[str, Any]] = []\n for candidate in candidates:\n + \ if not isinstance(candidate, dict):\n failures.append(f\"{domain}:non-object + candidate\")\n continue\n forbidden_keys = {\"transport_metadata\", + \"transport_candidate_refs\", \"Reason\", \"PriorAct\", \"ReasonRefs\", \"Evidence\", + \"EvidenceTitles\", \"BO_ID\", \"id\"}\n present_forbidden = sorted(forbidden_keys.intersection(candidate))\n + \ if present_forbidden:\n failures.append(f\"{candidate.get('candidate_ref')}:forbidden + sparse keys:{present_forbidden}\")\n continue\n if + candidate.get(\"BOType\") not in {\"event\", \"state\"} or candidate.get(\"ActionType\") + not in ACTION_TYPES or not str(candidate.get(\"Action\") or \"\").strip():\n + \ failures.append(f\"{candidate.get('candidate_ref')}:required + enum/action invalid\")\n continue\n failures.extend(_validate_refs(candidate, + domain, manifest))\n seed = _expand_candidate(candidate, domain, + manifest)\n expanded.append(seed)\n ledger_candidates.append({\"candidate_ref\": + seed[\"candidate_ref\"], \"source_domain\": domain, \"seed\": seed})\n reviews.extend([dict(item, + source_domain=domain) for item in _as_list(delta.get(\"domain_review_queue\")) + if isinstance(item, dict)])\n artifact = {\"stage_b_domain_bo_seed_output\": + {\n \"schema_version\": \"task_c_bo_stage_b_domain_bo_seed.v2\", + \"domain_id\": domain,\n \"status\": \"READY_WITH_REVIEW\" if delta.get(\"domain_review_queue\") + else \"READY\",\n \"run_fingerprint\": manifest[\"run_fingerprint\"], + \"bo_seed_candidates\": expanded,\n \"domain_review_queue\": _as_list(delta.get(\"domain_review_queue\")),\n + \ }}\n _write_json_verified(SEED_PATHS[domain], artifact)\n seed_counts[domain] + = len(expanded)\n\n deterministic: list[dict[str, Any]] = []\n merged_refs: + set[str] = set()\n by_key: dict[str, list[dict[str, Any]]] = {}\n for + item in ledger_candidates:\n by_key.setdefault(_substantive_key(item[\"seed\"]), + []).append(item)\n for group in by_key.values():\n if len(group) < + 2:\n continue\n survivor = group[0][\"candidate_ref\"]\n absorbed + = [item[\"candidate_ref\"] for item in group[1:]]\n merged_refs.update(absorbed)\n + \ deterministic.append({\"decision\": \"MERGE_PROVENANCE_UNION\", \"survivor_ref\": + survivor, \"absorbed_refs\": absorbed})\n for item in ledger_candidates:\n + \ seed = item[\"seed\"]\n refs = _source_refs(seed)\n if + refs[\"source_meeting_clause_ids\"] and not refs[\"source_evidence_indexes\"] + and not refs[\"source_event_candidate_ids\"]:\n reviews.append({\n + \ \"review_id\": f\"R0:{item['candidate_ref']}:MEETING_ONLY\", + \"severity\": \"SOFT_WARNING\",\n \"issue_type\": \"MEETING_ONLY_EVIDENCE_GAP\", + \"candidate_refs\": [item[\"candidate_ref\"]],\n **refs, \"downstream_owner\": + \"human_review\", \"source_domain\": item[\"source_domain\"],\n })\n + \ for left_index, left in enumerate(ledger_candidates):\n for right + in ledger_candidates[left_index + 1:]:\n if left[\"candidate_ref\"] + in merged_refs or right[\"candidate_ref\"] in merged_refs:\n continue\n + \ left_refs = set(_source_refs(left[\"seed\"])[\"source_evidence_indexes\"])\n + \ right_refs = set(_source_refs(right[\"seed\"])[\"source_evidence_indexes\"])\n + \ if left_refs.intersection(right_refs) and left[\"seed\"][\"ActionType\"] + == right[\"seed\"][\"ActionType\"]:\n deterministic.append({\"decision\": + \"KEEP_SEPARATE\", \"cluster_id\": f\"NEAR-{len(deterministic)+1:03d}\", \"candidate_refs\": + [left[\"candidate_ref\"], right[\"candidate_ref\"]]})\n\n packs: list[dict[str, + Any]] = []\n exception_number = 1\n for review in reviews:\n candidate_refs + = [str(value) for value in _as_list(review.get(\"candidate_refs\"))]\n if + review.get(\"issue_type\") != \"cross_domain_merge_needed\" or len(candidate_refs) + < 2:\n continue\n candidates = [item for item in ledger_candidates + if item[\"candidate_ref\"] in candidate_refs][:8]\n exception_id = f\"P2-EXC-{exception_number:03d}\"\n + \ pack_id = f\"P2-PACK-{len(packs)+1:03d}\"\n pack = {\n \"pack_id\": + pack_id, \"exception_type\": \"canonicalization_conflict\", \"exception_ids\": + [exception_id],\n \"items\": [{\n \"exception_id\": + exception_id, \"exception_type\": \"canonicalization_conflict\",\n \"candidate_refs\": + candidate_refs[:8], \"conflicting_field\": \"cross_domain_identity\",\n \"allowed_values\": + [],\n \"compact_candidates\": [{\n \"candidate_ref\": + item[\"candidate_ref\"], \"source_domain\": item[\"source_domain\"],\n \"relevant_fields\": + {key: item[\"seed\"].get(key) for key in (\"BOType\", \"ActionType\", \"Action\")},\n + \ \"source_refs\": _source_refs(item[\"seed\"]),\n } + for item in candidates],\n \"allowed_decisions\": [\"KEEP_SEPARATE\", + \"MERGE_PROVENANCE_UNION\", \"SPLIT\", \"BLOCK_REVIEW\"],\n }],\n + \ \"output_path\": f\"quality_gates/stage1_part2_adjudication_parts/PACK-{len(packs)+1:03d}.json\",\n + \ }\n if len(_canonical(pack).encode(\"utf-8\")) > 20000:\n reviews.append({\"review_id\": + f\"R0:BUDGET:{exception_number:03d}\", \"severity\": \"BLOCK\", \"issue_type\": + \"BUDGET_OVERFLOW_BLOCK_REVIEW\", \"candidate_refs\": candidate_refs, \"downstream_owner\": + \"human_review\"})\n elif len(packs) < 8:\n packs.append(pack)\n + \ exception_number += 1\n\n status = \"BLOCKED\" if failures or any(item.get(\"severity\") + == \"BLOCK\" for item in reviews) else (\"READY_WITH_REVIEW\" if reviews else + \"READY\")\n all_refs = [item[\"candidate_ref\"] for item in ledger_candidates]\n + \ ledger = {\"postb_seed_ledger\": {\n \"schema_version\": \"task_c_bo_postb_seed_ledger.v2\", + \"status\": status,\n \"run_fingerprint\": manifest[\"run_fingerprint\"], + \"source_universe\": manifest[\"source_universe\"],\n \"ledger_candidates\": + ledger_candidates,\n \"candidate_index\": {item[\"candidate_ref\"]: {\"source_domain\": + item[\"source_domain\"], \"ordinal\": index} for index, item in enumerate(ledger_candidates)},\n + \ \"domain_review_queue\": reviews, \"deterministic_decisions\": deterministic,\n + \ \"exception_summary\": {\"pack_count\": len(packs), \"exception_count\": + sum(len(pack[\"items\"]) for pack in packs)},\n \"audit_trace\": {\"failures\": + failures, \"seed_counts\": seed_counts},\n \"conservation\": {\"input_candidate_refs\": + all_refs, \"input_count\": len(all_refs), \"unique_count\": len(set(all_refs))},\n + \ }}\n handoff = {\n \"schema_version\": \"stage1_part2_review_handoff.v1\", + \"status\": status,\n \"run_fingerprint\": manifest[\"run_fingerprint\"], + \"review_count\": len(reviews), \"items\": reviews,\n }\n exception_pack + = {\n \"schema_version\": \"stage1_part2_exception_pack.v1\", \"status\": + status,\n \"run_fingerprint\": manifest[\"run_fingerprint\"], \"pack_count\": + len(packs), \"packs\": packs,\n }\n _write_json_verified(\"stage1_tmp/task_c_bo/postb_seed_ledger.json\", + ledger)\n _write_json_verified(\"quality_gates/stage1_part2_review_handoff.json\", + handoff)\n _write_json_verified(\"quality_gates/stage1_part2_exception_pack.json\", + exception_pack)\n output = {\"r0_manifest\": {\n \"schema_version\": + \"task_c_bo_r0_manifest.v1\", \"status\": status,\n \"run_fingerprint\": + manifest[\"run_fingerprint\"], \"seed_ledger_path\": \"stage1_tmp/task_c_bo/postb_seed_ledger.json\",\n + \ \"review_handoff_path\": \"quality_gates/stage1_part2_review_handoff.json\",\n + \ \"exception_pack_path\": \"quality_gates/stage1_part2_exception_pack.json\",\n + \ \"candidate_count\": len(ledger_candidates), \"review_count\": len(reviews), + \"pack_count\": len(packs),\n }, \"dynamic_fanout\": packs if status != \"BLOCKED\" + else []}\n print(json.dumps(output, ensure_ascii=False, separators=(\",\", + \":\")))\n if status == \"BLOCKED\":\n raise RuntimeError(\"R0 blocked + by schema, source, or budget findings\")\n\nif __name__ == \"__main__\":\n try:\n + \ main()\n except Exception as exc:\n print(json.dumps({\"r0_manifest\": + {\"schema_version\": \"task_c_bo_r0_manifest.v1\", \"status\": \"FAILED\", \"error\": + str(exc)}, \"dynamic_fanout\": []}, ensure_ascii=False))\n raise\n" + - task_name: Task_C_BO_R1_exception_adjudicator_* + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: medium + llm_verbosity: low + max_iterations: 2 + max_concurrency: 2 + cache_control: + mode: auto + ttl: 1h + use_tools: + - localdocs + preflight: false + prompts: + - role: user + content: | + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + You are the conditional, compact exception adjudicator for Stage 1 Part 2. + Use only the item fields below. Do not read any Stage A file, ledger, BO, domain seed, meeting, event, or evidence file. + + pack_id: {{item.pack_id}} + exception_type: {{item.exception_type}} + exception_ids: {{item.exception_ids}} + items: {{item.items}} + output_path: {{item.output_path}} + + For every exception return exactly one decision drawn both from the item's allowed_decisions and this closed enum: + KEEP_SEPARATE, MERGE_PROVENANCE_UNION, SPLIT, DROP_UNSUPPORTED, SELECT_VALUE, SELECT_LINK, NO_LINK, BLOCK_REVIEW. + Never invent a candidate_ref, source ref, exception id, value, or fact. SELECT_VALUE must use allowed_values. When compact data is insufficient, use BLOCK_REVIEW. + Write exactly one JSON file to {{item.output_path}} with localdocs write_file and overwrite=true. Do not write another file. + The file schema is: + { + "schema_version": "stage1_part2_adjudication_part.v1", + "status": "READY|BLOCKED", + "pack_id": "{{item.pack_id}}", + "exception_type": "{{item.exception_type}}", + "decisions": [ + { + "exception_id": "from item", + "decision": "closed enum", + "candidate_refs": [], + "selected_value": null, + "selected_link": null, + "reason_code": "SOURCE_SUPPORT|IDENTITY_MATCH|NO_SAFE_RESOLUTION|INSUFFICIENT_COMPACT_CONTEXT" + } + ] + } + Return only a compact raw JSON object containing pack_id, status, output_path, and decision_count after the write succeeds. + + - task_name: Task_C_BO_F0_final_bo_gate_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_IDS = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + def _next_id() -> int: + return next(MSG_IDS) + + def _parse_json_first(raw: str, label: str, allow_trailing: bool = False) -> Any: + text = (raw or "").lstrip("\ufeff").strip() + if not text: + raise RuntimeError(f"{label}: empty JSON text") + try: + return json.loads(text) + except json.JSONDecodeError as first_error: + try: + value, end = JSON_DECODER.raw_decode(text) + except json.JSONDecodeError as second_error: + raise RuntimeError(f"{label}: invalid JSON") from second_error + if not allow_trailing and text[end:].strip(): + raise RuntimeError(f"{label}: trailing non-JSON data") from first_error + return value + + def _parse_rpc(raw: str) -> dict[str, Any]: + events: list[dict[str, Any]] = [] + for line in (raw or "").splitlines(): + if not line.startswith("data:"): + continue + candidate = line[5:].strip() + if not candidate or candidate == "[DONE]": + continue + try: + parsed = json.loads(candidate) + except json.JSONDecodeError: + continue + if isinstance(parsed, dict): + events.append(parsed) + if events: + return events[-1] + parsed = _parse_json_first(raw, "MCP response", allow_trailing=True) + if not isinstance(parsed, dict): + raise RuntimeError("MCP response root must be object") + return parsed + + def _init(task_name: str) -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": task_name, + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _call(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_rpc(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP {name} failed: {payload['error']}") + result = payload.get("result") + if not isinstance(result, dict): + raise RuntimeError(f"MCP {name} returned no result") + if result.get("isError") is True: + raise RuntimeError(f"MCP {name} returned isError") + return payload + + def _unwrap(payload: dict[str, Any], label: str) -> Any: + result = payload.get("result") or {} + content = result.get("content") or [] + texts = [block.get("text") for block in content if isinstance(block, dict) and block.get("text")] + if not texts: + raise RuntimeError(f"{label}: empty MCP content") + raw = texts[0] + try: + return _parse_json_first(raw, label, allow_trailing=True) + except RuntimeError: + return raw + + def _extract_doc(payload: dict[str, Any], doc_name: str) -> str: + outer = _unwrap(payload, f"read_docs:{doc_name}") + if isinstance(outer, dict) and isinstance(outer.get("results"), list): + for item in outer["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + if name and not (str(name) == doc_name or str(name).endswith("/" + doc_name)): + continue + value = item.get("content") + if value is None: + value = item.get("text") + if value is None: + value = item.get("raw") + if value is not None: + return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False) + if isinstance(outer, dict) and doc_name in outer: + value = outer[doc_name] + return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False) + if isinstance(outer, str): + return outer + return json.dumps(outer, ensure_ascii=False) + + def _read_text(doc_name: str) -> str: + return _extract_doc(_call("read_docs", {"doc_names": [doc_name]}), doc_name) + + def _read_json(doc_name: str) -> Any: + raw = _read_text(doc_name) + return _parse_json_first(raw, doc_name, allow_trailing=True) + + def _canonical(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + def _sha(value: Any) -> str: + raw = value if isinstance(value, str) else _canonical(value) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _write_json_verified(doc_name: str, value: Any) -> str: + content = json.dumps(value, ensure_ascii=False, indent=2) + _call("write_file", {"path": doc_name, "content": content, "overwrite": True}) + reread = _read_json(doc_name) + if _canonical(reread) != _canonical(value): + raise RuntimeError(f"{doc_name}: post-write verification mismatch") + return _sha(content) + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _uniq(values: list[Any]) -> list[Any]: + seen: set[str] = set() + output: list[Any] = [] + for value in values: + marker = _canonical(value) if isinstance(value, (dict, list)) else str(value) + if marker in seen: + continue + seen.add(marker) + output.append(value) + return output + + def _natural_key(value: Any) -> list[Any]: + return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", str(value))] + + TASK_NAME = "Task_C_BO_F0_final_bo_gate_writer" + R0_RAW = r"""{{prev.Task_C_BO_R0_seed_reducer_exception_planner}}""" + BO_KEYS = ["BO_ID", "id", "BOType", "ActionType", "JuristicAct", "Action", "Reason", "PriorAct", "ReasonRefs", "Legal_Keywords", "core_field_base", "amount", "EvidenceTitles", "Evidence", "source_evidence_indexes", "provenance", "downstream_seed_refs", "extensions"] + CORE_KEYS = ["Performer", "PerformerType", "Action_proposal", "Subject", "Object", "BehaviorTime", "TimeText", "TimePrecision", "StatementType", "Perspective"] + DOWNSTREAM_KEYS = ["claim_group_seed_refs_proposed", "canonical_theory_graph_seed_ref_proposed", "legal_effect_structure_seed_ref_proposed"] + EVIDENCE_KEYS = ["evidence_index", "source_title", "priority_class", "relevant_content", "authentication_status", "corroboration", "selection_basis"] + + def _r0_manifest() -> dict[str, Any]: + parsed = _parse_json_first(R0_RAW, "R0_RAW", allow_trailing=False) + if not isinstance(parsed, dict) or not isinstance(parsed.get("r0_manifest"), dict): + raise RuntimeError("R0 stdout manifest missing") + return parsed["r0_manifest"] + + def _decision_parts(exception_pack: dict[str, Any]) -> dict[str, dict[str, Any]]: + output: dict[str, dict[str, Any]] = {} + for pack in _as_list(exception_pack.get("packs")): + path = str(_as_dict(pack).get("output_path") or "") + part = _read_json(path) + if not isinstance(part, dict) or part.get("schema_version") != "stage1_part2_adjudication_part.v1": + raise RuntimeError(f"invalid adjudication part: {path}") + pack_id = str(pack.get("pack_id")) + if part.get("pack_id") != pack_id or pack_id in output: + raise RuntimeError(f"adjudication pack coverage mismatch: {pack_id}") + expected = set(str(v) for v in _as_list(pack.get("exception_ids"))) + actual = set(str(_as_dict(v).get("exception_id")) for v in _as_list(part.get("decisions"))) + if actual != expected: + raise RuntimeError(f"adjudication exception coverage mismatch: {pack_id}") + output[pack_id] = part + return output + + def _merge_refs(target: dict[str, Any], source: dict[str, Any]) -> None: + for key in ("source_event_candidate_ids", "source_meeting_clause_ids", "source_evidence_indexes"): + target["provenance"][key] = _uniq(_as_list(target["provenance"].get(key)) + _as_list(source["provenance"].get(key))) + target["source_evidence_indexes"] = _uniq(_as_list(target.get("source_evidence_indexes")) + _as_list(source.get("source_evidence_indexes"))) + + def _apply_decisions(ledger: dict[str, Any], parts: dict[str, dict[str, Any]]) -> tuple[list[dict[str, Any]], dict[str, Any]]: + candidates = {item["candidate_ref"]: {**item, "seed": json.loads(json.dumps(item["seed"], ensure_ascii=False))} for item in _as_list(ledger.get("ledger_candidates"))} + removed: set[str] = set() + blocked: set[str] = set() + merged: set[str] = set() + for decision in _as_list(ledger.get("deterministic_decisions")): + if decision.get("decision") != "MERGE_PROVENANCE_UNION": + continue + survivor = str(decision.get("survivor_ref")) + if survivor not in candidates: + raise RuntimeError("deterministic merge survivor missing") + for absorbed in _as_list(decision.get("absorbed_refs")): + absorbed = str(absorbed) + if absorbed not in candidates: + raise RuntimeError("deterministic merge absorbed ref missing") + _merge_refs(candidates[survivor]["seed"], candidates[absorbed]["seed"]) + removed.add(absorbed) + merged.add(absorbed) + for part in parts.values(): + for decision in _as_list(part.get("decisions")): + action = decision.get("decision") + refs = [str(v) for v in _as_list(decision.get("candidate_refs"))] + if any(ref not in candidates for ref in refs): + raise RuntimeError("foreign candidate ref in adjudication") + if action == "MERGE_PROVENANCE_UNION" and len(refs) >= 2: + for absorbed in refs[1:]: + _merge_refs(candidates[refs[0]]["seed"], candidates[absorbed]["seed"]) + removed.add(absorbed) + merged.add(absorbed) + elif action == "DROP_UNSUPPORTED": + for ref in refs: + if _as_list(candidates[ref]["seed"].get("source_evidence_indexes")): + raise RuntimeError("source-backed candidate cannot be dropped") + removed.add(ref) + elif action == "SELECT_VALUE": + for ref in refs: + candidates[ref]["seed"]["ActionType"] = decision.get("selected_value") + elif action == "BLOCK_REVIEW": + blocked.update(refs) + elif action not in {"KEEP_SEPARATE", "SPLIT", "SELECT_LINK", "NO_LINK"}: + raise RuntimeError(f"unsupported adjudication decision: {action}") + final_items = [item for ref, item in candidates.items() if ref not in removed and ref not in blocked] + audit = {"input_count": len(candidates), "final_candidate_count": len(final_items), "merged_refs": sorted(merged), "blocked_refs": sorted(blocked), "removed_refs": sorted(removed)} + if audit["input_count"] != audit["final_candidate_count"] + len(set(audit["merged_refs"] + audit["blocked_refs"] + audit["removed_refs"])): + raise RuntimeError("candidate conservation failed") + return final_items, audit + + def _evidence_item(index: str, evidence_map: dict[str, Any]) -> dict[str, Any]: + record = _as_dict(_as_dict(evidence_map.get("by_evidence_index")).get(index)) + return { + "evidence_index": index, "source_title": record.get("title"), "priority_class": record.get("priority_class"), + "relevant_content": record.get("relevant_content"), "authentication_status": record.get("authentication_status"), + "corroboration": record.get("corroboration"), "selection_basis": "source_evidence_indexes membership", + } + + def _final_bo(item: dict[str, Any], ordinal: int, evidence_map: dict[str, Any]) -> dict[str, Any]: + seed = item["seed"] + seed_core = _as_dict(seed.get("core_field_base")) + parties = _as_list(seed_core.get("parties")) + assets = _as_list(seed_core.get("asset_refs")) + source_indexes = sorted(_uniq([str(v) for v in _as_list(seed.get("source_evidence_indexes"))]), key=_natural_key) + evidence = [_evidence_item(index, evidence_map) for index in source_indexes] + amount = _as_dict(seed.get("amount")) + value = amount.get("value") if "value" in amount else amount.get("numeric_value") + value_text = amount.get("basis") if amount.get("basis") is not None else amount.get("value_text") + downstream = _as_dict(seed.get("downstream_seed_refs")) + bo_id = f"bh{ordinal}" + provenance = _as_dict(seed.get("provenance")) + return { + "BO_ID": bo_id, "id": bo_id, "BOType": seed.get("BOType"), "ActionType": seed.get("ActionType"), + "JuristicAct": seed.get("JuristicAct") if isinstance(seed.get("JuristicAct"), dict) else {"label": None}, + "Action": seed.get("Action"), "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": None, "ReasonRefs": [], "Legal_Keywords": _as_list(seed.get("Legal_Keywords")), + "core_field_base": { + "Performer": parties[0] if parties else None, "PerformerType": None, "Action_proposal": seed.get("Action"), + "Subject": parties[1] if len(parties) > 1 else None, "Object": assets[0] if assets else None, + "BehaviorTime": seed_core.get("date"), "TimeText": seed_core.get("time_text"), + "TimePrecision": seed_core.get("time_precision"), "StatementType": seed.get("BOType"), "Perspective": "plaintiff", + }, + "amount": {"value_text": value_text, "numeric_value": value, "currency": amount.get("currency") or "KRW"}, + "EvidenceTitles": _uniq([entry.get("source_title") for entry in evidence if entry.get("source_title")]), + "Evidence": evidence, "source_evidence_indexes": source_indexes, + "provenance": { + "source_event_candidate_ids": _uniq(_as_list(provenance.get("source_event_candidate_ids"))), + "source_meeting_clause_ids": _uniq(_as_list(provenance.get("source_meeting_clause_ids"))), + "source_domain": item.get("source_domain"), + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": _as_list(downstream.get("claim_group_seed")), + "canonical_theory_graph_seed_ref_proposed": downstream.get("issue_cluster_seed"), + "legal_effect_structure_seed_ref_proposed": downstream.get("legal_effect_structure_seed"), + }, + "extensions": seed.get("extensions") if isinstance(seed.get("extensions"), dict) else {"domain_payload": {}}, + } + + def _validate_bo(bo_list: list[dict[str, Any]], source_universe: set[str]) -> None: + for index, bo in enumerate(bo_list, 1): + if list(bo) != BO_KEYS or bo.get("BO_ID") != f"bh{index}" or bo.get("id") != f"bh{index}": + raise RuntimeError(f"BO schema/id mismatch at ordinal {index}") + if list(_as_dict(bo.get("core_field_base"))) != CORE_KEYS: + raise RuntimeError(f"BO core schema mismatch at ordinal {index}") + if list(_as_dict(bo.get("downstream_seed_refs"))) != DOWNSTREAM_KEYS: + raise RuntimeError(f"BO downstream schema mismatch at ordinal {index}") + if any(list(_as_dict(item)) != EVIDENCE_KEYS for item in _as_list(bo.get("Evidence"))): + raise RuntimeError(f"BO Evidence schema mismatch at ordinal {index}") + if any(value not in source_universe for value in _as_list(bo.get("source_evidence_indexes"))): + raise RuntimeError(f"BO source membership mismatch at ordinal {index}") + + def main() -> None: + _init("task-c-bo-f0-final-bo-gate-writer") + r0 = _r0_manifest() + stage_a = _as_dict(_as_dict(_read_json("stage1_tmp/task_c_bo/stage_a_context.json")).get("stage_a_context")) + manifest = _read_json("stage1_tmp/task_c_bo/source_universe_manifest.json") + ledger = _as_dict(_as_dict(_read_json("stage1_tmp/task_c_bo/postb_seed_ledger.json")).get("postb_seed_ledger")) + exception_pack = _read_json("quality_gates/stage1_part2_exception_pack.json") + fingerprints = {stage_a.get("run_fingerprint"), manifest.get("run_fingerprint"), ledger.get("run_fingerprint"), exception_pack.get("run_fingerprint"), r0.get("run_fingerprint")} + if len(fingerprints) != 1 or None in fingerprints: + raise RuntimeError("F0 run fingerprint mismatch") + if ledger.get("status") not in {"READY", "READY_WITH_REVIEW"} or r0.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise RuntimeError("R0 ledger is not finalizable") + if r0.get("pack_count") != exception_pack.get("pack_count"): + raise RuntimeError("R0 pack count mismatch") + parts = _decision_parts(exception_pack) + items, conservation = _apply_decisions(ledger, parts) + items.sort(key=lambda item: (_natural_key(item.get("source_domain")), _natural_key(item.get("candidate_ref")))) + evidence_map = _as_dict(stage_a.get("evidence_authority_map")) + bo_list = [_final_bo(item, ordinal, evidence_map) for ordinal, item in enumerate(items, 1)] + source_universe = set(str(v) for v in _as_list(_as_dict(manifest.get("source_universe")).get("evidence_indexes"))) + _validate_bo(bo_list, source_universe) + bo_text = json.dumps(bo_list, ensure_ascii=False, indent=2) + bo_digest = _sha(bo_text) + compiled = {"postb_compiled_bo_bundle": { + "schema_version": "task_c_bo_postb_compiled_bo_bundle.v1", "status": "READY", + "run_fingerprint": manifest["run_fingerprint"], "bo_count": len(bo_list), "behavior_objects": bo_list, + "candidate_conservation": conservation, + }} + _write_json_verified("stage1_tmp/task_c_bo/postb_compiled_bo_bundle.json", compiled) + _write_json_verified("BO.json", bo_list) + reread_bo = _read_json("BO.json") + _validate_bo(reread_bo, source_universe) + if len(reread_bo) != len(bo_list): + raise RuntimeError("BO post-write count mismatch") + gate = { + "schema_version": "stage1_part2_bo_gate.v1", "status": "PASS", + "run_fingerprint": manifest["run_fingerprint"], "bo_count": len(bo_list), + "candidate_conservation": conservation, + "source_membership": {"status": "PASS", "universe_count": len(source_universe)}, + "adjudication_coverage": {"expected_pack_count": exception_pack.get("pack_count"), "actual_pack_count": len(parts)}, + "schema_gate": {"status": "PASS", "bo_keys": BO_KEYS}, + "post_write": {"file_exists": True, "json_parse_pass": True, "sha256": bo_digest}, + "findings": [], + } + _write_json_verified("quality_gates/stage1_part2_bo_gate.json", gate) + print(json.dumps({"bo_write_report": {"schema_version": "stage1_part2_bo_write_report.v1", "status": "PASS", "run_fingerprint": manifest["run_fingerprint"], "bo_count": len(bo_list), "bo_sha256": bo_digest}}, ensure_ascii=False, separators=(",", ":"))) + + if __name__ == "__main__": + try: + main() + except Exception as exc: + print(json.dumps({"bo_write_report": {"schema_version": "stage1_part2_bo_write_report.v1", "status": "FAILED", "error": str(exc)}}, ensure_ascii=False)) + raise + - task_name: Task_C_BO_S0_signal_bundle_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_IDS = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + def _next_id() -> int: + return next(MSG_IDS) + + def _parse_json_first(raw: str, label: str, allow_trailing: bool = False) -> Any: + text = (raw or "").lstrip("\ufeff").strip() + if not text: + raise RuntimeError(f"{label}: empty JSON text") + try: + return json.loads(text) + except json.JSONDecodeError as first_error: + try: + value, end = JSON_DECODER.raw_decode(text) + except json.JSONDecodeError as second_error: + raise RuntimeError(f"{label}: invalid JSON") from second_error + if not allow_trailing and text[end:].strip(): + raise RuntimeError(f"{label}: trailing non-JSON data") from first_error + return value + + def _parse_rpc(raw: str) -> dict[str, Any]: + events: list[dict[str, Any]] = [] + for line in (raw or "").splitlines(): + if not line.startswith("data:"): + continue + candidate = line[5:].strip() + if not candidate or candidate == "[DONE]": + continue + try: + parsed = json.loads(candidate) + except json.JSONDecodeError: + continue + if isinstance(parsed, dict): + events.append(parsed) + if events: + return events[-1] + parsed = _parse_json_first(raw, "MCP response", allow_trailing=True) + if not isinstance(parsed, dict): + raise RuntimeError("MCP response root must be object") + return parsed + + def _init(task_name: str) -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": task_name, + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _call(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_rpc(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP {name} failed: {payload['error']}") + result = payload.get("result") + if not isinstance(result, dict): + raise RuntimeError(f"MCP {name} returned no result") + if result.get("isError") is True: + raise RuntimeError(f"MCP {name} returned isError") + return payload + + def _unwrap(payload: dict[str, Any], label: str) -> Any: + result = payload.get("result") or {} + content = result.get("content") or [] + texts = [block.get("text") for block in content if isinstance(block, dict) and block.get("text")] + if not texts: + raise RuntimeError(f"{label}: empty MCP content") + raw = texts[0] + try: + return _parse_json_first(raw, label, allow_trailing=True) + except RuntimeError: + return raw + + def _extract_doc(payload: dict[str, Any], doc_name: str) -> str: + outer = _unwrap(payload, f"read_docs:{doc_name}") + if isinstance(outer, dict) and isinstance(outer.get("results"), list): + for item in outer["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + if name and not (str(name) == doc_name or str(name).endswith("/" + doc_name)): + continue + value = item.get("content") + if value is None: + value = item.get("text") + if value is None: + value = item.get("raw") + if value is not None: + return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False) + if isinstance(outer, dict) and doc_name in outer: + value = outer[doc_name] + return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False) + if isinstance(outer, str): + return outer + return json.dumps(outer, ensure_ascii=False) + + def _read_text(doc_name: str) -> str: + return _extract_doc(_call("read_docs", {"doc_names": [doc_name]}), doc_name) + + def _read_json(doc_name: str) -> Any: + raw = _read_text(doc_name) + return _parse_json_first(raw, doc_name, allow_trailing=True) + + def _canonical(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + def _sha(value: Any) -> str: + raw = value if isinstance(value, str) else _canonical(value) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _write_json_verified(doc_name: str, value: Any) -> str: + content = json.dumps(value, ensure_ascii=False, indent=2) + _call("write_file", {"path": doc_name, "content": content, "overwrite": True}) + reread = _read_json(doc_name) + if _canonical(reread) != _canonical(value): + raise RuntimeError(f"{doc_name}: post-write verification mismatch") + return _sha(content) + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _uniq(values: list[Any]) -> list[Any]: + seen: set[str] = set() + output: list[Any] = [] + for value in values: + marker = _canonical(value) if isinstance(value, (dict, list)) else str(value) + if marker in seen: + continue + seen.add(marker) + output.append(value) + return output + + def _natural_key(value: Any) -> list[Any]: + return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", str(value))] + + TASK_NAME = "Task_C_BO_S0_signal_bundle_writer" + LIABILITY_TYPES = { + "B1_Money_Successor": "money_claim", "B2_Secured_Registry": "secured_debt", + "B3_Land_Valuation": "land_use_gain", "B4_Actio": "actio_related", + "B5_Succession_Notice_Lien_Asset_Defense": "notice_or_lien_related", + } + LIABILITY_ENUM = {"money_claim", "commercial_successor", "secured_debt", "registry_invalidity", "land_use_gain", "actio_related", "succession_related", "notice_or_lien_related", "defense_or_setoff", "other"} + ACTIO_SCOPES = {"preserved_claim", "fraudulent_act", "beneficiary_or_transferee", "target_asset", "encumbrance", "remedy_or_cap", "defense"} + SUSPICION_LEVELS = {"NONE", "LOW", "MEDIUM", "HIGH", "CRITICAL"} + ACTIO_SCOPE_ENUM = ACTIO_SCOPES + ROLE_ENUM = {"fraudulent_act_date", "debtor_act", "beneficiary", "transferee", "remedy_or_cap", "defense", "encumbrance"} + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def _first_string(*values: Any) -> str | None: + for value in values: + for item in _strings(value): + return item + return None + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + + def _flatten_strings(value: Any, limit: int = 10) -> list[str]: + out: list[str] = [] + + def visit(node: Any) -> None: + if len(out) >= limit: + return + if node is None: + return + if isinstance(node, (str, int, float, bool)): + text = str(node).strip() + if text and text not in out: + out.append(text) + return + if isinstance(node, list): + for child in node: + visit(child) + return + if isinstance(node, dict): + for key in ( + "asset_label", "asset", "property", "object", "Object", "description", + "value", "name", "party", "beneficiary", "transferee", "debtor_act", + "remedy", "defense", "encumbrance", "date", "claim_type", + ): + if key in node: + visit(node.get(key)) + visit(value) + return out[:limit] + + def _asset_kind_from_text(text: str) -> str: + lowered = text.lower() + if any(key in text for key in ["부동산", "토지", "건물", "아파트", "대지", "등기", "근저당"]): + return "real_property" + if any(key in text for key in ["채권", "대여금", "매매대금", "임대차보증금"]): + return "claim" + if any(key in text for key in ["근저당", "담보", "저당", "질권", "유치권"]): + return "security_right" + if any(key in text for key in ["예금", "현금", "계좌", "deposit", "cash"]) or "bank" in lowered: + return "cash_or_deposit" + if any(key in text for key in ["영업", "사업", "상호", "business"]) or "company" in lowered: + return "business_asset" + if any(key in text for key in ["동산", "차량", "기계", "movable"]): + return "movable" + return "unknown" + + def _target_property_candidates(payload: dict[str, Any], bo_ids: list[str], evidence: list[str]) -> list[dict[str, Any]]: + sources = [ + payload.get("asset_bindings"), + payload.get("asset_binding_hint"), + payload.get("valuation_matrix"), + payload.get("fraudulent_transfer_facts"), + payload.get("encumbrance_timeline"), + payload.get("target_property_candidates"), + ] + out: list[dict[str, Any]] = [] + for source in sources: + for item in _as_list(source): + labels = _flatten_strings(item, limit=4) + if not labels and isinstance(item, dict): + labels = _strings(item.get("asset_label") or item.get("property_label")) + for label in labels: + candidate = { + "asset_label": label, + "asset_kind": _asset_kind_from_text(label), + "related_bo_ids": bo_ids, + "related_evidence_indexes": evidence, + } + key = json.dumps(candidate, ensure_ascii=False, sort_keys=True) + if key not in {json.dumps(x, ensure_ascii=False, sort_keys=True) for x in out}: + out.append(candidate) + return out + + def _preserved_claim_candidates(payload: dict[str, Any], mapping: dict[str, str], bo_ids: list[str], evidence: list[str]) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for item in _as_list(payload.get("preserved_claim_bundle") or payload.get("preserved_claim_candidates")): + obj = _dict(item) + ref = _first_string(obj.get("candidate_ref"), obj.get("bo_candidate_ref")) + bo_id = mapping.get(ref) if ref else _first_string(obj.get("bo_id")) + if bo_id not in bo_ids and bo_ids: + bo_id = bo_id or None + out.append({ + "bo_id": bo_id, + "claim_type_candidate": _first_string(obj.get("claim_type_candidate"), obj.get("claim_type"), obj.get("type"), obj.get("label")), + "creditor_candidate": _first_string(obj.get("creditor_candidate"), obj.get("creditor"), obj.get("plaintiff")), + "debtor_candidate": _first_string(obj.get("debtor_candidate"), obj.get("debtor"), obj.get("defendant")), + "claim_date_candidate": _first_string(obj.get("claim_date_candidate"), obj.get("claim_date"), obj.get("date"), obj.get("BehaviorTime")), + "related_evidence_indexes": _dedupe(evidence + _strings(obj.get("related_evidence_indexes") or obj.get("source_evidence_indexes"))), + }) + if not out and bo_ids: + out.append({ + "bo_id": bo_ids[0], + "claim_type_candidate": None, + "creditor_candidate": None, + "debtor_candidate": None, + "claim_date_candidate": None, + "related_evidence_indexes": evidence, + }) + return out + + def _role_hint(role: str, value: Any, date: Any, bo_ids: list[str], evidence: list[str]) -> dict[str, Any] | None: + if role not in ROLE_ENUM: + return None + value_candidate = _first_string(value) + date_candidate = _first_string(date) + if value_candidate is None and date_candidate is None: + return None + return { + "role": role, + "value_candidate": value_candidate, + "date_candidate": date_candidate, + "related_bo_ids": bo_ids, + "related_evidence_indexes": evidence, + } + + def _actio_role_hints(payload: dict[str, Any], seed: dict[str, Any], bo_ids: list[str], evidence: list[str]) -> list[dict[str, Any]]: + hints: list[dict[str, Any]] = [] + for fact in _as_list(payload.get("fraudulent_transfer_facts")): + obj = _dict(fact) + for hint in ( + _role_hint("fraudulent_act_date", obj.get("date") or obj.get("act_date"), obj.get("date") or obj.get("act_date"), bo_ids, evidence), + _role_hint("debtor_act", obj.get("debtor_act") or obj.get("action") or obj.get("description"), obj.get("date"), bo_ids, evidence), + ): + if hint: + hints.append(hint) + for party in _as_list(payload.get("beneficiary_transferee_hints")): + obj = _dict(party) + role_text = "transferee" if "transferee" in str(obj).lower() or "양수" in str(obj) else "beneficiary" + hint = _role_hint(role_text, obj.get("party") or obj.get("name") or obj.get("value") or obj, obj.get("date"), bo_ids, evidence) + if hint: + hints.append(hint) + for role, key in [ + ("remedy_or_cap", "remedy_mode_hints"), + ("defense", "defense_rebuttal"), + ("encumbrance", "encumbrance_timeline"), + ]: + for item in _as_list(payload.get(key)): + obj = _dict(item) + hint = _role_hint(role, obj or item, obj.get("date") if obj else None, bo_ids, evidence) + if hint: + hints.append(hint) + if not hints: + action = _first_string(seed.get("Action"), _dict(seed.get("core_field_base")).get("Action_proposal")) + hint = _role_hint("debtor_act", action, _dict(seed.get("core_field_base")).get("BehaviorTime"), bo_ids, evidence) + if hint: + hints.append(hint) + deduped: list[dict[str, Any]] = [] + seen: set[str] = set() + for hint in hints: + key = json.dumps(hint, ensure_ascii=False, sort_keys=True) + if key not in seen: + seen.add(key) + deduped.append(hint) + return deduped + + def _scopes(payload: dict[str, Any]) -> list[str]: + scopes: list[str] = [] + if payload.get("preserved_claim_bundle") or payload.get("preserved_claim_candidates"): + scopes.append("preserved_claim") + if payload.get("fraudulent_transfer_facts") or "actio" in " ".join(_strings(payload.get("legal_effect_tags"))).lower(): + scopes.append("fraudulent_act") + if payload.get("beneficiary_transferee_hints"): + scopes.append("beneficiary_or_transferee") + if payload.get("asset_bindings") or payload.get("asset_binding_hint") or payload.get("valuation_matrix"): + scopes.append("target_asset") + if payload.get("encumbrance_timeline"): + scopes.append("encumbrance") + if payload.get("remedy_mode_hints"): + scopes.append("remedy_or_cap") + if payload.get("defense_rebuttal"): + scopes.append("defense") + return [scope for scope in _dedupe(scopes) if scope in ACTIO_SCOPE_ENUM] + + def _suspicion_level(scopes: list[str]) -> str: + core = {"preserved_claim", "fraudulent_act", "beneficiary_or_transferee"} + if core.issubset(set(scopes)): + return "HIGH" + if {"preserved_claim", "fraudulent_act"}.issubset(set(scopes)): + return "MEDIUM" + if scopes: + return "LOW" + return "NONE" + + def _reasons(scopes: list[str], payload: dict[str, Any], b4_review_queue: list[Any]) -> str: + fragments: list[str] = [] + if "preserved_claim" in scopes: + fragments.append("피보전채권 후보가 B4 seed/domain payload에서 확인됨") + if "fraudulent_act" in scopes: + fragments.append("채무자 처분행위 또는 사해행위 후보 사실이 확인됨") + if "beneficiary_or_transferee" in scopes: + fragments.append("수익자 또는 전득자 후보가 확인됨") + if "target_asset" in scopes: + fragments.append("취소·원상회복 대상 재산 후보가 확인됨") + if "encumbrance" in scopes: + fragments.append("담보권·가압류 등 부담 관련 단서가 확인됨") + if "remedy_or_cap" in scopes: + fragments.append("원상회복 방법 또는 가액배상 한도 단서가 확인됨") + if "defense" in scopes: + fragments.append("항변 또는 선의·악의 관련 반박 단서가 확인됨") + for item in b4_review_queue[:2]: + if isinstance(item, dict): + reason = _first_string(item.get("reason"), item.get("issue"), item.get("review_reason")) + if reason: + fragments.append(f"B4 review queue: {reason}") + return ", ".join(fragments) if fragments else "사해행위취소 관련 구조화 단서가 충분히 확인되지 않음" + + + def _payload(bo: dict[str, Any]) -> dict[str, Any]: + return _as_dict(_as_dict(bo.get("extensions")).get("domain_payload")) + + def _actio_scope(payload: dict[str, Any]) -> list[str]: + scopes: list[str] = [] + if payload.get("preserved_claim_bundle"): + scopes.append("preserved_claim") + if payload.get("fraudulent_transfer_facts"): + scopes.append("fraudulent_act") + if payload.get("encumbrance_timeline"): + scopes.append("encumbrance") + if payload.get("remedy_mode_hints") or payload.get("value_compensation_cap_hints"): + scopes.append("remedy_or_cap") + if payload.get("defense_rebuttal"): + scopes.append("defense") + if payload.get("asset_bindings") or payload.get("valuation_matrix"): + scopes.append("target_asset") + return [value for value in _uniq(scopes) if value in ACTIO_SCOPES] + + def _candidate_types(bo: dict[str, Any]) -> list[str]: + payload = _payload(bo) + values = [payload.get("domain_legal_effect_tag"), bo.get("BOType"), bo.get("ActionType"), _as_dict(bo.get("provenance")).get("source_domain")] + values.extend(_as_list(payload.get("legal_effect_tags"))) + label = _as_dict(bo.get("JuristicAct")).get("label") + if label: + values.append(label) + return _uniq([str(value) for value in values if value not in (None, "")]) + + def _liability_type(domain: str, payload: dict[str, Any]) -> str: + if domain == "B1_Money_Successor" and payload.get("successor_liability_hints"): + return "commercial_successor" + if domain == "B2_Secured_Registry" and payload.get("derivative_invalidity_hints"): + return "registry_invalidity" + if domain == "B5_Succession_Notice_Lien_Asset_Defense": + if payload.get("succession_resolution") or payload.get("general_succession_status") or payload.get("asset_specific_succession_resolution"): + return "succession_related" + if payload.get("setoff_or_payment_defense_hints") or payload.get("simultaneous_performance_hints") or payload.get("defense_rebuttal"): + return "defense_or_setoff" + return LIABILITY_TYPES.get(domain, "other") + + def _validate_signal_payloads(actio: dict[str, Any], liability: dict[str, Any], legal: dict[str, Any], bo_ids: set[str]) -> None: + if actio.get("schema_version") != "actio_case_signals.v1" or liability.get("schema_version") != "case_liability_signals.v1" or legal.get("schema_version") != "legal_effect_signals.v1": + raise RuntimeError("signal schema version mismatch") + checks = [ + (actio.get("actio_case_signals"), {"signal_id", "actio_scope", "suspicion_level", "suspicion_reasons", "related_bo_ids", "related_evidence_indexes", "target_property_candidates", "preserved_claim_candidates", "actio_role_hints"}), + (liability.get("case_liability_signals"), {"liability_candidate_type", "related_bo_ids", "related_evidence_indexes", "party_roles", "claim_group_id", "liability_group_id", "linked_structure_ids"}), + (legal.get("bo_legal_effect_routes"), {"bo_id", "candidate_structure_types", "linked_structure_ids", "related_evidence_indexes"}), + ] + for items, keys in checks: + if not isinstance(items, list): + raise RuntimeError("signal item array missing") + for item in items: + if set(_as_dict(item)) != keys: + raise RuntimeError("signal item key mismatch") + related = _as_list(_as_dict(item).get("related_bo_ids")) + if any(value not in bo_ids for value in related): + raise RuntimeError("signal related BO outside BO.json") + if any(item.get("suspicion_level") not in SUSPICION_LEVELS or any(scope not in ACTIO_SCOPES for scope in _as_list(item.get("actio_scope"))) for item in actio["actio_case_signals"]): + raise RuntimeError("actio signal enum mismatch") + if any(item.get("liability_candidate_type") not in LIABILITY_ENUM for item in liability["case_liability_signals"]): + raise RuntimeError("liability signal enum mismatch") + if any(item.get("bo_id") not in bo_ids for item in legal["bo_legal_effect_routes"]): + raise RuntimeError("legal effect route BO outside BO.json") + + def main() -> None: + _init("task-c-bo-s0-signal-bundle-writer") + gate = _read_json("quality_gates/stage1_part2_bo_gate.json") + manifest = _read_json("stage1_tmp/task_c_bo/source_universe_manifest.json") + ledger = _as_dict(_as_dict(_read_json("stage1_tmp/task_c_bo/postb_seed_ledger.json")).get("postb_seed_ledger")) + _read_json("stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B4_Actio.json") + bo_list = _read_json("BO.json") + if gate.get("status") != "PASS" or gate.get("run_fingerprint") != manifest.get("run_fingerprint") or ledger.get("run_fingerprint") != manifest.get("run_fingerprint"): + raise RuntimeError("F0 gate or run fingerprint invalid") + bo_digest = _sha(json.dumps(bo_list, ensure_ascii=False, indent=2)) + if gate.get("post_write", {}).get("sha256") != bo_digest: + raise RuntimeError("BO digest mismatch") + bo_ids = {str(bo.get("BO_ID")) for bo in bo_list} + actio_items: list[dict[str, Any]] = [] + liability_items: list[dict[str, Any]] = [] + legal_items: list[dict[str, Any]] = [] + for bo in bo_list: + bo_id = str(bo["BO_ID"]) + evidence = _as_list(bo.get("source_evidence_indexes")) + domain = str(_as_dict(bo.get("provenance")).get("source_domain") or "") + payload = _payload(bo) + scopes = _actio_scope(payload) + if domain == "B4_Actio": + scopes = _scopes(payload) + actio_items.append({ + "signal_id": f"actio-signal-{len(actio_items)+1:03d}", "actio_scope": scopes or ["fraudulent_act"], + "suspicion_level": _suspicion_level(scopes), "suspicion_reasons": _reasons(scopes, payload, []), + "related_bo_ids": [bo_id], "related_evidence_indexes": evidence, + "target_property_candidates": _target_property_candidates(payload, [bo_id], evidence), + "preserved_claim_candidates": _preserved_claim_candidates(payload, {}, [bo_id], evidence), + "actio_role_hints": _actio_role_hints(payload, bo, [bo_id], evidence), + }) + downstream = _as_dict(bo.get("downstream_seed_refs")) + liability_items.append({ + "liability_candidate_type": _liability_type(domain, payload), "related_bo_ids": [bo_id], + "related_evidence_indexes": evidence, "party_roles": [], + "claim_group_id": (_as_list(downstream.get("claim_group_seed_refs_proposed")) or [None])[0], + "liability_group_id": None, "linked_structure_ids": [value for value in [downstream.get("legal_effect_structure_seed_ref_proposed")] if value], + }) + legal_items.append({ + "bo_id": bo_id, "candidate_structure_types": _candidate_types(bo), + "linked_structure_ids": [value for value in [downstream.get("legal_effect_structure_seed_ref_proposed")] if value], + "related_evidence_indexes": evidence, + }) + actio = {"schema_version": "actio_case_signals.v1", "status": "READY_WITH_REVIEW" if actio_items else "READY", "actio_case_signals": actio_items} + liability = {"schema_version": "case_liability_signals.v1", "status": "READY_WITH_REVIEW", "case_liability_signals": liability_items} + legal = {"schema_version": "legal_effect_signals.v1", "status": "READY_WITH_REVIEW", "bo_legal_effect_routes": legal_items} + _validate_signal_payloads(actio, liability, legal, bo_ids) + payloads = {"actio_case_signals.json": actio, "case_liability_signals.json": liability, "legal_effect_signals.json": legal} + reports: dict[str, Any] = {} + for path, payload in payloads.items(): + digest = _write_json_verified(path, payload) + reread = _read_json(path) + item_key = "actio_case_signals" if path.startswith("actio") else ("case_liability_signals" if path.startswith("case") else "bo_legal_effect_routes") + reports[path] = {"count": len(reread[item_key]), "status": reread["status"], "sha256": digest} + print(json.dumps({"signal_bundle_write_report": { + "schema_version": "stage1_part2_signal_bundle_write_report.v1", "status": "PASS", + "run_fingerprint": manifest["run_fingerprint"], "files": reports, + }}, ensure_ascii=False, separators=(",", ":"))) + + if __name__ == "__main__": + try: + main() + except Exception as exc: + print(json.dumps({"signal_bundle_write_report": {"schema_version": "stage1_part2_signal_bundle_write_report.v1", "status": "FAILED", "error": str(exc)}}, ensure_ascii=False)) + raise + task_procedure: + IN: + nexts: + - Task_C_BO_A0_context_domain_slice_compiler + wait_until: [] + Task_C_BO_A0_context_domain_slice_compiler: + nexts: + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + wait_until: + - IN + Task_C_BO_Stage_B_B1_Money_Successor: + nexts: + - Task_C_BO_R0_seed_reducer_exception_planner + wait_until: + - Task_C_BO_A0_context_domain_slice_compiler + Task_C_BO_Stage_B_B2_Secured_Registry: + nexts: + - Task_C_BO_R0_seed_reducer_exception_planner + wait_until: + - Task_C_BO_A0_context_domain_slice_compiler + Task_C_BO_Stage_B_B3_Land_Valuation: + nexts: + - Task_C_BO_R0_seed_reducer_exception_planner + wait_until: + - Task_C_BO_A0_context_domain_slice_compiler + Task_C_BO_Stage_B_B4_Actio: + nexts: + - Task_C_BO_R0_seed_reducer_exception_planner + wait_until: + - Task_C_BO_A0_context_domain_slice_compiler + Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense: + nexts: + - Task_C_BO_R0_seed_reducer_exception_planner + wait_until: + - Task_C_BO_A0_context_domain_slice_compiler + Task_C_BO_R0_seed_reducer_exception_planner: + nexts: + - Task_C_BO_R1_exception_adjudicator_* + - Task_C_BO_F0_final_bo_gate_writer + wait_until: + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + Task_C_BO_R1_exception_adjudicator_*: + nexts: + - Task_C_BO_F0_final_bo_gate_writer + wait_until: + - Task_C_BO_R0_seed_reducer_exception_planner + Task_C_BO_F0_final_bo_gate_writer: + nexts: + - Task_C_BO_S0_signal_bundle_writer + wait_until: + - Task_C_BO_R0_seed_reducer_exception_planner + - all Task_C_BO_R1_exception_adjudicator_* + Task_C_BO_S0_signal_bundle_writer: + nexts: + - OUT + wait_until: + - Task_C_BO_F0_final_bo_gate_writer + OUT: + nexts: [] + wait_until: + - Task_C_BO_S0_signal_bundle_writer diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_2_v2.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_2_v2.yml new file mode 100644 index 00000000..dc91f140 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_2_v2.yml @@ -0,0 +1,7462 @@ +--- +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_2 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Part 2 + version: v.2 + Stages: + - name: stage1_BO_시그널_생성 + description: BO 생성, 시그널 생성 + llm_provider: openai + llm_model: gpt-4o-2024-08-06 + tools: + mcpServers: + localdocs: + type: streamable-http + url: http://mcp-localdocs:8012/mcp + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + tasks: + - task_name: Task_C_BO_Stage_A_input_normalization + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 180 + code: |- + #!/usr/bin/env python3 + from __future__ import annotations + import hashlib + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + + REQUIRED_FILES = [ + "client_meeting.md", + "evidence_event_candidates.json", + "evidence_indexed.json", + "Default_Agent/Juristic_Act.md", + ] + PART1_SOFT_GATE_HANDOFF_FILE = "quality_gates/stage1_part1_soft_gate_handoff.json" + + CURRENT_STATE_PATTERNS = [ + "현재", "현재까지", "현재도", "계속", "거주", "점유", "사용", "운영", + "부지로만 사용", "인도받아", "사업자등록", "유치권", "임대", "수익", + ] + CONFLICT_PATTERNS = [ + "하였음에도", "그럼에도", "불구하고", "거절", "응하지", "주장", + "항변", "다툼", "무효", "취소", "해제", "해지", "말소", "신뢰", + ] + SUCCESSION_PATTERNS = [ + "사망", "상속", "상속포기", "배우자", "직계비속", "직계존속", + "피상속인", "상속인", "수리", "가족관계", + ] + MIXED_CONTRACT_PATTERNS = [ + "임대차", "매매", "보증금", "매매대금", "인도", "관리", "사용", + "사용수익", "영업양도", "채무불승계", "대물변제", + ] + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-c-bo-stage-a-input-normalization", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _mid(), + "method": "tools/call", + "params": {"name": name, "arguments": args}, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + p = _parse(r.text) + if not p or "result" not in p: + raise RuntimeError(f"MCP {name} failed") + return p + + def _unwrap(p: Any, name: str) -> Any: + text = (p["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError(f"Empty: {name}") + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, dict) and "results" in outer: + return outer + return outer + + def _stable_hash(value: Any) -> str: + if isinstance(value, str): + raw = value + else: + raw = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _text(value: Any) -> str | None: + if value is None: + return None + s = str(value).strip() + return s or None + + def _norm_space(value: Any) -> str: + return re.sub(r"\s+", " ", str(value or "")).strip() + + def _norm_key(value: Any) -> str: + text = str(value or "").strip().lower() + return "".join(ch for ch in text if ch.isalnum()) + + def _short_excerpt(value: Any, limit: int = 120) -> str: + text = _norm_space(value) + return text if len(text) <= limit else text[: limit - 1] + "…" + + def _section_key_from_heading(heading: str, seq: int) -> str: + normalized = _norm_key(heading) + if "상담내용" in normalized: + suffix = "consultation" + elif "사건관계인의주장" in normalized or ("사건관계인" in normalized and "주장" in normalized): + suffix = "party_assertions" + elif "희망사항" in normalized or "의뢰인희망" in normalized: + suffix = "client_requests" + elif normalized: + suffix = normalized[:24] + else: + suffix = "unnamed" + return f"s{seq:02d}_{suffix}" + + def _uniq(values: list[Any]) -> list[Any]: + seen: set[str] = set() + out: list[Any] = [] + for value in values: + marker = json.dumps(value, ensure_ascii=False, sort_keys=True) if isinstance(value, (dict, list)) else str(value) + if marker not in seen: + seen.add(marker) + out.append(value) + return out + + def _extract_doc_names(payload: Any) -> list[str]: + names: list[str] = [] + def walk(v: Any) -> None: + if isinstance(v, str): + if "." in v or "/" in v: + names.append(v) + names.extend(re.findall(r"[\w./()가-힣ᄀ-ᇿ -]+\.[A-Za-z0-9]+", v)) + elif isinstance(v, list): + for item in v: + walk(item) + elif isinstance(v, dict): + for key in ("name", "path", "doc_name", "document_name", "file", "filename"): + if v.get(key): + names.append(str(v[key])) + for val in v.values(): + if isinstance(val, (list, dict)): + walk(val) + walk(payload) + return sorted(set(names)) + + def _doc_name_matches(found_names: list[str], required: str) -> bool: + required_norm = required.strip("/") + required_base = required_norm.split("/")[-1] + for name in found_names: + n = name.strip("/") + if n == required_norm or n.endswith("/" + required_norm) or n.split("/")[-1] == required_base: + return True + if required_norm in n or required_base in n: + return True + return False + + def _preflight_required_files() -> dict[str, Any]: + found_names_set: set[str] = set() + list_docs_args = ( + {"path": "/", "pattern": "*", "recursive": True}, + {"path": ".", "pattern": "*", "recursive": True}, + {"path": "/", "pattern": "*"}, + {"path": ".", "pattern": "*"}, + {"path": "Default_Agent", "pattern": "*"}, + {"path": "quality_gates", "pattern": "*.json"}, + ) + for args in list_docs_args: + try: + payload = _unwrap(_call("list_docs", args), "list_docs") + found_names_set.update(_extract_doc_names(payload)) + except Exception: + continue + found_names_now = sorted(found_names_set) + required_ready = all(_doc_name_matches(found_names_now, path) for path in REQUIRED_FILES) + sidecar_checked = _doc_name_matches(found_names_now, PART1_SOFT_GATE_HANDOFF_FILE) or args.get("path") == "quality_gates" + if required_ready and sidecar_checked: + break + found_names = sorted(found_names_set) + if not found_names: + raise RuntimeError("list_docs returned no recognizable document names for required inputs") + missing = [path for path in REQUIRED_FILES if not _doc_name_matches(found_names, path)] + if missing: + raise RuntimeError(f"Missing required input files by list_docs: {missing}") + return {"found_names": found_names, "required_files": REQUIRED_FILES} + + def _read_required_docs_once() -> dict[str, str]: + payload = _unwrap(_call("read_docs", {"doc_names": REQUIRED_FILES}), "read_docs") + docs: dict[str, str] = {} + + def assign(name: str | None, content: Any) -> None: + if not name: + return + text = content if isinstance(content, str) else json.dumps(content, ensure_ascii=False) + for required in REQUIRED_FILES: + if name == required or name.endswith("/" + required) or name.split("/")[-1] == required.split("/")[-1]: + docs[required] = text + + if isinstance(payload, dict) and isinstance(payload.get("results"), list): + for item in payload["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + content = item.get("content") or item.get("text") or item.get("markdown") or item.get("raw") + assign(str(name) if name else None, content) + elif isinstance(payload, dict): + for required in REQUIRED_FILES: + if required in payload: + assign(required, payload[required]) + elif isinstance(payload, str): + if len(REQUIRED_FILES) == 1: + assign(REQUIRED_FILES[0], payload) + + missing = [name for name in REQUIRED_FILES if name not in docs] + if missing: + # Some Localdocs runtimes return one JSON wrapper inside the text field. + raw = payload if isinstance(payload, str) else json.dumps(payload, ensure_ascii=False) + try: + outer = json.loads(raw) + if isinstance(outer, dict) and isinstance(outer.get("results"), list): + for item in outer["results"]: + if isinstance(item, dict): + name = item.get("name") or item.get("path") or item.get("doc_name") + content = item.get("content") or item.get("text") + assign(str(name) if name else None, content) + except Exception: + pass + missing = [name for name in REQUIRED_FILES if name not in docs] + if missing: + raise RuntimeError(f"read_docs did not return required inputs: {missing}") + for name, content in docs.items(): + if not str(content).strip(): + raise RuntimeError(f"Required input is empty: {name}") + return docs + + def _parse_json_doc(name: str, raw: str) -> Any: + try: + return json.loads(raw.strip()) + except json.JSONDecodeError as exc: + raise RuntimeError(f"Strict JSON parse failed: {name}: {exc.msg} at line {exc.lineno} column {exc.colno}") from exc + + def _items(obj: Any, *keys: str) -> list[Any]: + if isinstance(obj, list): + return obj + if isinstance(obj, dict): + for key in keys + ("items", "evidence_indexed", "documents", "results"): + val = obj.get(key) + if isinstance(val, list): + return val + return [] + + def _extract_keywords(text: str, patterns: list[str]) -> list[str]: + return [p for p in patterns if p in text] + + def _build_meeting_clause_map(meeting_text: str) -> dict[str, Any]: + clauses: list[dict[str, Any]] = [] + current_major: str | None = None + current: dict[str, Any] | None = None + section_seq = 0 + current_section_key = "preamble" + current_section_label = "preamble" + + section_re = re.compile(r"^\s*#?\s*【\s*(.+?)\s*】\s*$") + major_re = re.compile(r"^\s*(\d+)\.\s+(.+?)\s*$") + sub_re = re.compile(r"^\s*([가-힣])\.\s+(.+?)\s*$") + + def flush(end_line: int) -> None: + nonlocal current + if not current: + return + current["line_end"] = end_line + current["text"] = _norm_space(current.get("text", "")) + text = current["text"] + current["tags"] = { + "current_state": _extract_keywords(text, CURRENT_STATE_PATTERNS), + "conflict_pattern": _extract_keywords(text, CONFLICT_PATTERNS), + "succession": _extract_keywords(text, SUCCESSION_PATTERNS), + "mixed_contract": _extract_keywords(text, MIXED_CONTRACT_PATTERNS), + } + current["text_excerpt"] = _short_excerpt(text) + clauses.append(current) + current = None + + lines = meeting_text.splitlines() + for idx, line in enumerate(lines, 1): + stripped = line.strip() + if not stripped or stripped.startswith("|") or stripped.startswith("---"): + continue + m_section = section_re.match(stripped) + m_major = major_re.match(stripped) + m_sub = sub_re.match(stripped) + if m_section: + flush(idx - 1) + section_seq += 1 + current_section_label = _norm_space(m_section.group(1)) + current_section_key = _section_key_from_heading(current_section_label, section_seq) + current_major = None + elif m_major: + flush(idx - 1) + current_major = m_major.group(1) + current = { + "clause_id": f"{current_section_key}:{current_major}", + "local_clause_id": current_major, + "section_key": current_section_key, + "section_label": current_section_label, + "major_no": current_major, + "sub_no": None, + "heading": _norm_space(m_major.group(2)), + "line_start": idx, + "text": _norm_space(m_major.group(2)), + } + elif m_sub and current_major: + flush(idx - 1) + sub_no = m_sub.group(1) + current = { + "clause_id": f"{current_section_key}:{current_major}-{sub_no}", + "local_clause_id": f"{current_major}-{sub_no}", + "section_key": current_section_key, + "section_label": current_section_label, + "major_no": current_major, + "sub_no": sub_no, + "heading": None, + "line_start": idx, + "text": _norm_space(m_sub.group(2)), + } + elif current: + current["text"] = _norm_space(str(current.get("text", "")) + " " + stripped) + flush(len(lines)) + + if not clauses: + raise RuntimeError("client_meeting.md parse produced no numbered meeting clauses") + + by_id: dict[str, Any] = {} + duplicate_clause_ids: list[str] = [] + for clause in clauses: + cid = clause["clause_id"] + if cid in by_id: + duplicate_clause_ids.append(cid) + by_id[cid] = clause + if duplicate_clause_ids: + raise RuntimeError(f"Duplicate meeting clause ids after section normalization: {duplicate_clause_ids}") + return { + "schema_version": "meeting_clause_map.v1", + "clause_order": [c["clause_id"] for c in clauses], + "by_clause_id": by_id, + "sections": _uniq([{"section_key": c["section_key"], "section_label": c["section_label"]} for c in clauses]), + "current_state_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["current_state"]], + "conflict_pattern_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["conflict_pattern"]], + "succession_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["succession"]], + "mixed_contract_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["mixed_contract"]], + } + + def _candidate_id(candidate: dict[str, Any], evidence_index: str, seq: int) -> str: + return str(candidate.get("candidate_id") or candidate.get("candidate_id_proposed") or f"internal_missing_candidate_id::{evidence_index}::{seq:02d}") + + def _evidence_index_from_item(item: dict[str, Any]) -> str | None: + return _text(item.get("evidence_index") or item.get("evidence_index_proposed") or item.get("source_evidence_index")) + + def _build_event_candidate_map(event_obj: Any) -> dict[str, Any]: + items = _items(event_obj) + if not items: + raise RuntimeError("evidence_event_candidates.json parse produced no document-level items") + by_candidate_id: dict[str, Any] = {} + by_evidence_index: dict[str, list[str]] = {} + by_meeting_clause_ref: dict[str, list[str]] = {} + state_candidate_ids: list[str] = [] + succession_candidate_ids: list[str] = [] + mixed_contract_candidate_ids: list[str] = [] + zero_event_evidence_indexes: list[str] = [] + asset_cluster_values: list[str] = [] + malformed_item_refs: list[str] = [] + + for item_seq, item in enumerate(items, 1): + if not isinstance(item, dict): + malformed_item_refs.append(f"item#{item_seq}: non-object") + continue + evidence_index = _evidence_index_from_item(item) + if not evidence_index: + malformed_item_refs.append(f"item#{item_seq}: missing evidence_index") + continue + item_ref = evidence_index + event_candidates_raw = item.get("event_candidates") + has_event_candidates_list = isinstance(event_candidates_raw, list) + has_event_candidates = has_event_candidates_list and len(event_candidates_raw) > 0 + has_zero_event_reason = bool(item.get("zero_event_reason")) + if event_candidates_raw is not None and not has_event_candidates_list: + malformed_item_refs.append(f"{item_ref}: event_candidates must be a list when present") + continue + if not has_event_candidates and not has_zero_event_reason: + malformed_item_refs.append(f"{item_ref}: requires non-empty event_candidates or zero_event_reason") + continue + if item.get("zero_event_reason"): + zero_event_evidence_indexes.append(evidence_index) + item_flags = item.get("doc_semantic_flags") if isinstance(item.get("doc_semantic_flags"), list) else [] + property_cluster_id = _text(item.get("property_cluster_id")) + if property_cluster_id: + asset_cluster_values.append(property_cluster_id) + for asset in item.get("affected_asset_cluster_ids") or []: + if _text(asset): + asset_cluster_values.append(str(asset)) + for seq, cand in enumerate(event_candidates_raw or [], 1): + if not isinstance(cand, dict): + malformed_item_refs.append(f"{item_ref}: event_candidates[{seq}] must be an object") + continue + cid = _candidate_id(cand, evidence_index, seq) + refs = cand.get("meeting_clause_refs") if isinstance(cand.get("meeting_clause_refs"), list) else [] + cand_flags = cand.get("doc_semantic_flags") if isinstance(cand.get("doc_semantic_flags"), list) else [] + candidate_flags = _uniq(item_flags + cand_flags) + record = { + "candidate_id": cid, + "source_evidence_index": _text(cand.get("source_evidence_index") or cand.get("source_evidence_index_proposed")) or evidence_index, + "title": item.get("title"), + "doc_type": item.get("doc_type"), + "event_or_state": cand.get("event_or_state"), + "event_kind": cand.get("event_kind"), + "event_subkind": cand.get("event_subkind"), + "action_type_candidate": cand.get("action_type_candidate"), + "action_summary": cand.get("action_summary"), + "participants": cand.get("participants") or {}, + "object_spec": cand.get("object_spec"), + "amount": cand.get("amount"), + "event_date": cand.get("event_date"), + "event_end_date": cand.get("event_end_date"), + "time_text": cand.get("time_text"), + "time_precision": cand.get("time_precision"), + "current_state_candidate": cand.get("current_state_candidate"), + "succession_candidate": cand.get("succession_candidate"), + "legal_effect_candidate": cand.get("legal_effect_candidate"), + "defense_candidate": cand.get("defense_candidate"), + "commercial_successor_candidates": cand.get("commercial_successor_candidates"), + "meeting_clause_refs": refs, + "actio_relevance_candidates": cand.get("actio_relevance_candidates"), + "identity_signature": cand.get("identity_signature"), + "property_cluster_id": property_cluster_id, + "affected_asset_cluster_ids": item.get("affected_asset_cluster_ids") or [], + "doc_semantic_flags": candidate_flags, + "fraction_ref": cand.get("fraction_ref"), + "confidence": cand.get("confidence"), + } + by_candidate_id[cid] = record + by_evidence_index.setdefault(evidence_index, []).append(cid) + for ref in refs: + by_meeting_clause_ref.setdefault(str(ref), []).append(cid) + if cand.get("event_or_state") == "state" or cand.get("current_state_candidate"): + state_candidate_ids.append(cid) + if cand.get("succession_candidate"): + succession_candidate_ids.append(cid) + if "mixed_contract_doc" in candidate_flags or cand.get("commercial_successor_candidates"): + mixed_contract_candidate_ids.append(cid) + if _text(cand.get("object_spec")): + asset_cluster_values.append(str(cand.get("object_spec"))) + + if malformed_item_refs: + raise RuntimeError(f"Malformed evidence_event_candidates document items: {malformed_item_refs[:20]}") + + return { + "schema_version": "event_candidate_map.v2", + "candidate_count": len(by_candidate_id), + "candidate_id_set": sorted(by_candidate_id.keys()), + "by_candidate_id": by_candidate_id, + "by_evidence_index": {k: _uniq(v) for k, v in by_evidence_index.items()}, + "by_meeting_clause_ref": {k: _uniq(v) for k, v in by_meeting_clause_ref.items()}, + "state_candidate_ids": _uniq(state_candidate_ids), + "succession_candidate_ids": _uniq(succession_candidate_ids), + "mixed_contract_candidate_ids": _uniq(mixed_contract_candidate_ids), + "zero_event_evidence_indexes": _uniq(zero_event_evidence_indexes), + "asset_cluster_values": _uniq([v for v in asset_cluster_values if _text(v)]), + } + + def _component_count(item: dict[str, Any], key: str) -> int: + val = item.get(key) + if isinstance(val, list): + return len(val) + if isinstance(val, dict): + return len(val) + return 1 if val not in (None, "", []) else 0 + + COMPONENT_DETAIL_PROJECTION = { + "loan_components": ["loan_component_id", "principal_amount", "loan_date", "due_date", + "agreed_interest_rate", "agreed_delay_damage_rate", "rate_text", + "rate_source_locator", "interest_accrual_start_hint"], + "secured_debt_components": ["agreed_interest_rate", "agreed_delay_damage_rate", "rate_text", + "rate_source_locator", "due_date"], + "notice_components": ["notice_id_proposed", "notice_type", "drafted_date", "dispatch_date", + "arrival_date", "received_date", "legal_effect_date_candidate", + "requires_receipt_for_effect", "receipt_evidence_refs", + "recipient_admission_text", "sender_candidates", "receiver_candidates"], + "registry_row_components": ["registry_ref", "fraction_ref", "fraction_display", + "fraction_numerator", "fraction_denominator", "receipt_no"], + "schedule_items": ["schedule_ref", "canonical_label_candidate", "usable_in_relief"], + } + + def _component_details(item: dict[str, Any]) -> dict[str, Any]: + details: dict[str, Any] = {} + for key, fields in COMPONENT_DETAIL_PROJECTION.items(): + rows = item.get(key) + if isinstance(rows, dict): + rows = [rows] + if not isinstance(rows, list): + continue + projected = [{field: row.get(field) for field in fields if field in row} for row in rows if isinstance(row, dict)] + projected = [p for p in projected if p] + if projected: + details[key] = projected + return details + + def _build_evidence_authority_map(evidence_obj: Any) -> dict[str, Any]: + items = _items(evidence_obj, "evidence_indexed") + if not items: + raise RuntimeError("evidence_indexed.json parse produced no evidence authority items") + by_evidence_index: dict[str, Any] = {} + by_doc_type: dict[str, list[str]] = {} + by_title_norm: dict[str, list[str]] = {} + semantic_flag_index: dict[str, list[str]] = {} + evidence_index_set: list[str] = [] + malformed_item_refs: list[str] = [] + duplicate_evidence_indexes: list[str] = [] + + component_keys = [ + "contract_components", "valuation_object", "legal_calculation_object", + "legal_calculation_objects", "claim_document_components", + "business_transfer_components", "registry_row_components", + "secured_debt_components", "valuation_rows", "mixed_contract_payment_semantics", + "loan_components", "valuation_matrix", "registry_row_role_map", + "defense_assertions", "admission_components", "defense_components", + "notice_components", "party_assertion_components", "schedule_items", + "registry_object_display", "succession_document_components", + ] + + for item_seq, item in enumerate(items, 1): + if not isinstance(item, dict): + malformed_item_refs.append(f"item#{item_seq}: non-object") + continue + evidence_index = _text(item.get("evidence_index") or item.get("evidence_index_proposed")) + if not evidence_index: + malformed_item_refs.append(f"item#{item_seq}: missing finalized evidence_index") + continue + if evidence_index in by_evidence_index: + duplicate_evidence_indexes.append(evidence_index) + continue + flags = item.get("doc_semantic_flags") if isinstance(item.get("doc_semantic_flags"), list) else [] + component_summary = {key: _component_count(item, key) for key in component_keys} + record = { + "evidence_index": evidence_index, + "doc_uid": item.get("doc_uid"), + "title": item.get("title"), + "title_normalized": item.get("title_normalized") or _norm_space(item.get("title")), + "doc_type": item.get("doc_type"), + "key_facts": item.get("key_facts") or [], + "key_dates": item.get("key_dates") or [], + "key_amounts": item.get("key_amounts") or [], + "key_parties": item.get("key_parties") or [], + "source_pointer": item.get("source_pointer") or {}, + "property_cluster_id": item.get("property_cluster_id"), + "doc_semantic_flags": flags, + "multi_event_registry": item.get("multi_event_registry"), + "component_summary": component_summary, + "component_details": _component_details(item), + } + by_evidence_index[evidence_index] = record + evidence_index_set.append(evidence_index) + by_doc_type.setdefault(str(item.get("doc_type") or "unknown"), []).append(evidence_index) + title_key = _norm_key(record["title_normalized"]) + if title_key: + by_title_norm.setdefault(title_key, []).append(evidence_index) + for flag in flags: + semantic_flag_index.setdefault(str(flag), []).append(evidence_index) + + if malformed_item_refs: + raise RuntimeError(f"Malformed evidence_indexed authority items: {malformed_item_refs[:20]}") + if duplicate_evidence_indexes: + raise RuntimeError(f"Duplicate evidence_index values in evidence_indexed.json: {_uniq(duplicate_evidence_indexes)}") + + return { + "schema_version": "evidence_authority_map.v2", + "evidence_index_set": _sort_evidence_indexes(_uniq(evidence_index_set)), + "by_evidence_index": by_evidence_index, + "by_doc_type": {k: _sort_evidence_indexes(_uniq(v)) for k, v in by_doc_type.items()}, + "by_title_norm": {k: _sort_evidence_indexes(_uniq(v)) for k, v in by_title_norm.items()}, + "semantic_flag_index": {k: _sort_evidence_indexes(_uniq(v)) for k, v in semantic_flag_index.items()}, + } + + def _sort_evidence_indexes(values: list[str]) -> list[str]: + def key(v: str) -> tuple[int, str]: + m = re.search(r"E-(\d+)", str(v)) + return (int(m.group(1)) if m else 10**9, str(v)) + return sorted(values, key=key) + + def _split_examples(value: str) -> list[str]: + parts = re.split(r"[,,、]| / |/|ㆍ|·", value or "") + return [_norm_space(p) for p in parts if _norm_space(p)] + + def _build_juristic_act_map(markdown: str) -> dict[str, Any]: + categories: dict[str, Any] = {} + example_to_gubun: dict[str, list[str]] = {} + for line in markdown.splitlines(): + if "|" not in line: + continue + cells = [_norm_space(c) for c in line.strip().strip("|").split("|")] + if len(cells) < 3: + continue + if cells[0] in ("구분", "---") or set(cells[0]) <= {"-"}: + continue + gubun, content, examples = cells[0], cells[1], cells[2] + if not gubun or gubun.startswith("-"): + continue + example_list = _split_examples(examples) + categories[gubun] = {"content": content, "examples": example_list} + for ex in example_list: + example_to_gubun.setdefault(_norm_key(ex), []).append(gubun) + + axis_groups = { + "structure": ["단독행위", "계약행위", "합동행위"], + "form": ["요식(要式)행위", "불요식행위"], + "time_effect": ["생전 행위", "사후(또는 사인) 행위"], + "legal_object": ["채권행위", "물권행위", "준물권행위"], + "contribution": ["출연행위", "비출연행위"], + "dependency": ["주(主)된 행위", "종(從)된 행위", "유인 행위", "무인 행위"], + "defect_or_condition": ["무효 행위", "취소가능한 법률 행위", "조건부 법률행위", "기한부 법률행위"], + "controversy": ["견해대립있는 행위"], + } + + return { + "schema_version": "juristic_act_taxonomy_map.v1", + "categories": categories, + "example_to_gubun": {k: _uniq(v) for k, v in example_to_gubun.items()}, + "axis_groups": axis_groups, + "source_digest_sha256": _stable_hash(markdown), + } + + def _validate_juristic_act_map(juristic_map: dict[str, Any]) -> None: + categories = juristic_map.get("categories") or {} + example_to_gubun = juristic_map.get("example_to_gubun") or {} + required = ["단독행위", "계약행위", "채권행위", "물권행위"] + missing = [name for name in required if name not in categories] + if not categories or not example_to_gubun: + raise RuntimeError("JuristicAct taxonomy parse produced empty categories or examples") + if missing: + raise RuntimeError(f"JuristicAct taxonomy missing required categories: {missing}") + + def _collect_asset_clusters(meeting_map: dict[str, Any], event_map: dict[str, Any], evidence_map: dict[str, Any]) -> list[str]: + values: list[str] = [] + asset_hint_re = re.compile(r"([가-힣A-Za-z0-9·ㆍ() \-]{1,30}(?:대지|건물|상가|빌라|임야|부동산|지분|영업|상호|채권|대금))") + for clause in meeting_map.get("by_clause_id", {}).values(): + text = str(clause.get("text") or "") + values.extend([_norm_space(m.group(1)) for m in asset_hint_re.finditer(text)]) + values.extend(event_map.get("asset_cluster_values") or []) + for ev in evidence_map.get("by_evidence_index", {}).values(): + if _text(ev.get("property_cluster_id")): + values.append(str(ev.get("property_cluster_id"))) + for fact in ev.get("key_facts") or []: + values.extend([_norm_space(m.group(1)) for m in asset_hint_re.finditer(str(fact))]) + return _uniq([v for v in values if _text(v)]) + + def _build_global_hazard_seed(meeting_map: dict[str, Any], event_map: dict[str, Any], evidence_map: dict[str, Any]) -> dict[str, Any]: + evidence_index_set = evidence_map.get("evidence_index_set") or [] + current_state_ids = event_map.get("state_candidate_ids") or [] + succession_candidate_ids = event_map.get("succession_candidate_ids") or [] + mixed_contract_candidate_ids = event_map.get("mixed_contract_candidate_ids") or [] + + def meeting_expression_records(clause_ids: list[str], tag_key: str) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + by_clause = meeting_map.get("by_clause_id", {}) + for cid in clause_ids: + clause = by_clause.get(cid) or {} + matched = (clause.get("tags") or {}).get(tag_key) or [] + out.append({ + "source": "client_meeting.md", + "clause_id": cid, + "local_clause_id": clause.get("local_clause_id"), + "section_key": clause.get("section_key"), + "matched_patterns": matched, + "text_excerpt": clause.get("text_excerpt") or _short_excerpt(clause.get("text")), + }) + return out + + def event_expression_records(candidate_ids: list[str]) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + by_candidate = event_map.get("by_candidate_id", {}) + for cid in candidate_ids: + cand = by_candidate.get(cid) or {} + text = " ".join(str(cand.get(k) or "") for k in ("action_summary", "event_kind", "event_subkind", "identity_signature")) + matched = _uniq( + _extract_keywords(text, CURRENT_STATE_PATTERNS) + + _extract_keywords(text, CONFLICT_PATTERNS) + + _extract_keywords(text, SUCCESSION_PATTERNS) + + _extract_keywords(text, MIXED_CONTRACT_PATTERNS) + ) + out.append({ + "source": "evidence_event_candidates.json", + "candidate_id": cid, + "source_evidence_index": cand.get("source_evidence_index"), + "event_or_state": cand.get("event_or_state"), + "event_kind": cand.get("event_kind"), + "matched_patterns": matched, + "text_excerpt": _short_excerpt(cand.get("action_summary") or cand.get("identity_signature")), + }) + return out + + conflict_candidate_ids: list[str] = [] + for cid, cand in event_map.get("by_candidate_id", {}).items(): + combined = " ".join( + str(cand.get(k) or "") + for k in ("action_summary", "event_kind", "event_subkind", "identity_signature") + ) + if _extract_keywords(combined, CONFLICT_PATTERNS): + conflict_candidate_ids.append(cid) + + succession_evidence_indexes = [] + mixed_contract_evidence_indexes = [] + for eidx, ev in evidence_map.get("by_evidence_index", {}).items(): + flags = ev.get("doc_semantic_flags") or [] + title_and_facts = " ".join([str(ev.get("title") or "")] + [str(x) for x in ev.get("key_facts") or []]) + if "succession_doc" in flags or _extract_keywords(title_and_facts, SUCCESSION_PATTERNS): + succession_evidence_indexes.append(eidx) + if "mixed_contract_doc" in flags or _extract_keywords(title_and_facts, MIXED_CONTRACT_PATTERNS): + mixed_contract_evidence_indexes.append(eidx) + + return { + "schema_version": "global_hazard_seed.v1", + "meeting_clause_ids": meeting_map.get("clause_order") or [], + "current_state_expressions": { + "meeting_clause_ids": meeting_map.get("current_state_clause_ids") or [], + "event_candidate_ids": current_state_ids, + "meeting_expressions": meeting_expression_records(meeting_map.get("current_state_clause_ids") or [], "current_state"), + "event_expressions": event_expression_records(current_state_ids), + }, + "conflict_pattern_expressions": { + "meeting_clause_ids": meeting_map.get("conflict_pattern_clause_ids") or [], + "event_candidate_ids": _uniq(conflict_candidate_ids), + "meeting_expressions": meeting_expression_records(meeting_map.get("conflict_pattern_clause_ids") or [], "conflict_pattern"), + "event_expressions": event_expression_records(_uniq(conflict_candidate_ids)), + }, + "asset_clusters": _collect_asset_clusters(meeting_map, event_map, evidence_map), + "succession_related_candidates": { + "meeting_clause_ids": meeting_map.get("succession_clause_ids") or [], + "event_candidate_ids": succession_candidate_ids, + "evidence_indexes": _sort_evidence_indexes(_uniq(succession_evidence_indexes)), + "meeting_expressions": meeting_expression_records(meeting_map.get("succession_clause_ids") or [], "succession"), + "event_expressions": event_expression_records(succession_candidate_ids), + }, + "mixed_contract_candidates": { + "meeting_clause_ids": meeting_map.get("mixed_contract_clause_ids") or [], + "event_candidate_ids": mixed_contract_candidate_ids, + "evidence_indexes": _sort_evidence_indexes(_uniq(mixed_contract_evidence_indexes)), + "meeting_expressions": meeting_expression_records(meeting_map.get("mixed_contract_clause_ids") or [], "mixed_contract"), + "event_expressions": event_expression_records(mixed_contract_candidate_ids), + }, + "evidence_index_set": evidence_index_set, + } + + def _read_optional_doc_once(doc_name: str) -> str | None: + try: + payload = _unwrap(_call("read_docs", {"doc_names": [doc_name]}), "read_docs_optional") + except Exception: + return None + if isinstance(payload, dict) and isinstance(payload.get("results"), list): + for item in payload["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + content = item.get("content") or item.get("text") or item.get("markdown") or item.get("raw") + if name and (str(name) == doc_name or str(name).endswith("/" + doc_name) or str(name).split("/")[-1] == doc_name.split("/")[-1]): + return content if isinstance(content, str) else json.dumps(content, ensure_ascii=False) + if isinstance(payload, dict) and doc_name in payload: + val = payload[doc_name] + return val if isinstance(val, str) else json.dumps(val, ensure_ascii=False) + if isinstance(payload, str) and payload.strip(): + return payload + return None + + def _compact_part1_soft_gate_handoff(existence_check: dict[str, Any], input_digests: dict[str, str], meeting_map: dict[str, Any], event_map: dict[str, Any], evidence_map: dict[str, Any]) -> dict[str, Any]: + found_names = existence_check.get("found_names") or [] + base = { + "schema_version": "stage1_part1_soft_gate_handoff.v1", + "status": "ABSENT", + "stage2_auto_progression_allowed": True, + "review_gate_count": 0, + "highest_review_severity": "PASS", + "review_items_compact": [], + "routing_index": {}, + "lawyer_workflow_queues": {}, + "dropped_review_items": [], + "usage_rules": { + "not_factual_authority": True, + "must_not_create_fact_without_stage_a_source": True, + "must_preserve_as_needs_review_if_used": True, + "must_not_override_stage_e_final_progression": True, + "must_not_override_stage_f_single_writer_gate": True, + }, + } + if not _doc_name_matches(found_names, PART1_SOFT_GATE_HANDOFF_FILE): + return base + raw = _read_optional_doc_once(PART1_SOFT_GATE_HANDOFF_FILE) + if not raw: + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": "sidecar listed but unreadable"}) + return base + try: + sidecar = json.loads(raw.strip()) + except Exception as exc: + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": f"sidecar JSON parse failed: {exc}"}) + return base + if not isinstance(sidecar, dict) or sidecar.get("schema_version") != "stage1_part1_soft_gate_handoff.v1": + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": "wrong or missing schema_version"}) + return base + if sidecar.get("handoff_status") == "BLOCKED" or sidecar.get("stage2_auto_progression_allowed") is False: + raise RuntimeError("Part 1 soft gate handoff is BLOCKED; Part 2-6 cannot proceed") + guard = sidecar.get("digest_guard") if isinstance(sidecar.get("digest_guard"), dict) else {} + invalid_guard_fields = [] + stale = [] + for guard_key, doc_name in [("evidence_indexed_sha256", "evidence_indexed.json"), ("evidence_event_candidates_sha256", "evidence_event_candidates.json")]: + val = guard.get(guard_key) + if not isinstance(val, str) or not re.fullmatch(r"[0-9a-f]{64}", val): + invalid_guard_fields.append(guard_key) + elif val != input_digests.get(doc_name): + stale.append(guard_key) + if invalid_guard_fields: + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": "digest_guard missing real sha256", "fields": invalid_guard_fields}) + return base + if stale: + base["status"] = "IGNORED_STALE" + base["dropped_review_items"].append({"reason": "digest_guard mismatch", "fields": stale}) + return base + evidence_set = set(evidence_map.get("evidence_index_set") or []) + event_set = set(event_map.get("candidate_id_set") or []) + meeting_set = set(meeting_map.get("clause_order") or []) + compact = [] + dropped = [] + for idx, item in enumerate(sidecar.get("review_items") or []): + if len(compact) >= 40: + dropped.append({"index": idx, "reason": "review_items_compact max 40 reached"}) + continue + if not isinstance(item, dict): + dropped.append({"index": idx, "reason": "non-object review item"}) + continue + refs = item.get("source_refs") if isinstance(item.get("source_refs"), dict) else {} + bad_e = [x for x in refs.get("evidence_indexes", []) if x not in evidence_set] + bad_ev = [x for x in refs.get("event_candidate_ids", []) if x not in event_set] + bad_m = [x for x in refs.get("meeting_clause_refs", []) if x not in meeting_set] + if bad_e or bad_ev or bad_m: + dropped.append({"review_id": item.get("review_id"), "reason": "source_refs membership failed", "bad_evidence_indexes": bad_e, "bad_event_candidate_ids": bad_ev, "bad_meeting_clause_refs": bad_m}) + continue + compact.append({ + "review_id": item.get("review_id"), + "severity": item.get("severity"), + "issue_type": item.get("issue_type"), + "lawyer_workflow_role": item.get("lawyer_workflow_role"), + "domain_hints": item.get("domain_hints") or [], + "target_stage_hints": item.get("target_stage_hints") or [], + "target_task_hints": item.get("target_task_hints") or [], + "source_refs": refs, + "candidate_use_policy": item.get("candidate_use_policy"), + "review_instruction": item.get("review_instruction"), + "negative_instruction": item.get("negative_instruction"), + }) + summary = sidecar.get("review_gate_summary") if isinstance(sidecar.get("review_gate_summary"), dict) else {} + base.update({ + "status": sidecar.get("handoff_status") if sidecar.get("handoff_status") in {"READY_WITH_REVIEW", "READY_NO_REVIEW"} else "READY_WITH_REVIEW", + "stage2_auto_progression_allowed": sidecar.get("stage2_auto_progression_allowed") is not False, + "review_gate_count": len(compact), + "highest_review_severity": summary.get("highest_review_severity") or ("HARD_WARNING" if any(i.get("severity") == "HARD_WARNING" for i in compact) else ("SOFT_WARNING" if compact else "PASS")), + "review_items_compact": compact, + "routing_index": sidecar.get("routing_index") if isinstance(sidecar.get("routing_index"), dict) else {}, + "lawyer_workflow_queues": sidecar.get("lawyer_workflow_queues") if isinstance(sidecar.get("lawyer_workflow_queues"), dict) else {}, + "dropped_review_items": dropped, + }) + return base + + def main() -> None: + _init() + + # Stage A.1-A.2: allowed input guard + existence check. + existence_check = _preflight_required_files() + + # Stage A.3-A.4: read exactly four inputs once, then parse/fail fast. + docs = _read_required_docs_once() + event_candidates = _parse_json_doc("evidence_event_candidates.json", docs["evidence_event_candidates.json"]) + evidence_indexed = _parse_json_doc("evidence_indexed.json", docs["evidence_indexed.json"]) + meeting_text = docs["client_meeting.md"] + juristic_text = docs["Default_Agent/Juristic_Act.md"] + + # Stage A.5-A.8: build normalized maps and the global hazard seed. + meeting_clause_map = _build_meeting_clause_map(meeting_text) + event_candidate_map = _build_event_candidate_map(event_candidates) + evidence_authority_map = _build_evidence_authority_map(evidence_indexed) + juristic_act_taxonomy_map = _build_juristic_act_map(juristic_text) + _validate_juristic_act_map(juristic_act_taxonomy_map) + global_hazard_seed = _build_global_hazard_seed(meeting_clause_map, event_candidate_map, evidence_authority_map) + + if not evidence_authority_map["evidence_index_set"]: + raise RuntimeError("No finalized evidence_index values available") + + input_digests_sha256 = { + name: _stable_hash(docs[name]) + for name in REQUIRED_FILES + } + part1_soft_gate_handoff = _compact_part1_soft_gate_handoff( + existence_check, + input_digests_sha256, + meeting_clause_map, + event_candidate_map, + evidence_authority_map, + ) + + stage_a_context = { + "schema_version": "task_c_bo_stage_a_context.v1", + "stage": "Task_C_BO.Stage_A", + "status": "READY", + "created_at_utc": datetime.now(timezone.utc).isoformat(), + "allowed_input_files": REQUIRED_FILES, + "forbidden_files_attested": ["evidence_all.json", "BO.json", "actio_case_signals.json", "case_liability_signals.json", "legal_effect_signals.json"], + "file_io_policy": { + "list_docs_called_for": REQUIRED_FILES, + "read_docs_called_once_for": REQUIRED_FILES, + "write_file_called": False, + "raw_evidence_all_read": False, + "output_file_written": False, + }, + "existence_check": existence_check, + "input_digests_sha256": input_digests_sha256, + "meeting_clause_map": meeting_clause_map, + "event_candidate_map": event_candidate_map, + "evidence_authority_map": evidence_authority_map, + "juristic_act_taxonomy_map": juristic_act_taxonomy_map, + "global_hazard_seed": global_hazard_seed, + "part1_soft_gate_handoff": part1_soft_gate_handoff, + "stage_b_worker_contract": { + "context_is_read_only": True, + "bo_id_finalization_allowed": False, + "candidate_generation_allowed": True, + "must_use_evidence_index_set": evidence_authority_map["evidence_index_set"], + }, + } + + print(json.dumps({"stage_a_context": stage_a_context}, ensure_ascii=False)) + + if __name__ == "__main__": + try: + main() + except Exception as exc: + print(json.dumps({"stage": "Task_C_BO.Stage_A", "status": "FAILED", "error": str(exc)}, ensure_ascii=False)) + raise + + - task_name: Task_C_BO_Stage_B0_input_money_successor + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 60 + code: |- + #!/usr/bin/env python3 + from __future__ import annotations + + import json + import re + import sys + from typing import Any + + TASK_NAME = "Task_C_BO_Stage_B0_input_money_successor" + STAGE_A_RAW = r"""{{prev.Task_C_BO_Stage_A_input_normalization}}""" + + DOMAIN_CONFIG = { + "domain_id": "B1_Money_Successor", + "domain_label": "금전채권 및 상호속용 책임", + "allowed_legal_effect_bo_types": [ + "money_claim_chain_BO", + "commercial_successor_liability_chain_BO", + "liability_group_hint_BO", + ], + "terms": [ + "대여금", "차용", "매매대금", "물품대금", "보증금", "정산금", "공사대금", + "원금", "이자", "지연손해금", "변제기", "이행기", "일부변제", "미변제", + "채무승인", "지급각서", "확인서", "최고", "통지", "상호속용", "영업양도", + "양수인", "채무불승계", "business_transfer", "loan", "payment", "admission", + "notice", "claim_document", "mixed_contract_payment", + ], + "meeting_sources": ["mixed_contract_clause_ids", "conflict_pattern_clause_ids"], + "event_sources": ["mixed_contract_candidate_ids"], + "hazard_keys": ["mixed_contract_candidates", "conflict_pattern_expressions", "asset_clusters"], + "taxonomy_axes": ["legal_object", "structure", "dependency"], + } + + ALL_DOMAIN_TERMS = { + "B1_Money_Successor": DOMAIN_CONFIG["terms"], + "B2_Secured_Registry": ["근저당", "저당", "담보", "피담보", "변제충당", "등기", "말소", "경매", "배당", "registry", "mortgage", "secured", "auction", "dividend"], + "B3_Land_Valuation": ["토지", "건물", "점유", "사용", "수익", "인도", "차임", "감정", "시가", "가액", "valuation", "possession", "land", "building"], + "B4_Actio": ["사해행위", "피보전채권", "채무초과", "책임재산", "공동담보", "처분행위", "수익자", "전득자", "가액배상", "actio", "fraudulent", "valuation", "defense"], + "B5_Succession_Notice_Lien_Asset_Defense": ["사망", "상속", "상속포기", "통지", "내용증명", "유치권", "별지목록", "목적물", "항변", "succession", "notice", "lien", "defense"], + } + + MEETING_FIELDS = [ + "clause_id", "local_clause_id", "section_key", "section_label", "major_no", + "sub_no", "heading", "line_start", "line_end", "text_excerpt", "tags", + ] + EVENT_FIELDS = ["candidate_id", "source_evidence_index", "title", "doc_type", "event_or_state", "event_kind", "event_subkind", "action_type_candidate", "action_summary", "participants", "object_spec", "amount", "event_date", "event_end_date", "time_text", "time_precision", "current_state_candidate", "succession_candidate", "legal_effect_candidate", "defense_candidate", "commercial_successor_candidates", "meeting_clause_refs", "actio_relevance_candidates", "identity_signature", "property_cluster_id", "affected_asset_cluster_ids", "doc_semantic_flags", "confidence", "fraction_ref"] + EVIDENCE_FIELDS = ["evidence_index", "doc_uid", "title", "title_normalized", "doc_type", "key_facts", "key_dates", "key_amounts", "key_parties", "source_pointer", "property_cluster_id", "doc_semantic_flags", "multi_event_registry", "component_summary", "component_details"] + + def _parse_json(raw: str, label: str) -> dict[str, Any]: + text = (raw or "").strip() + if not text or text == "null": + raise RuntimeError(f"{label}: empty injected JSON") + decoder = json.JSONDecoder() + obj, idx = decoder.raw_decode(text) + if text[idx:].strip(): + raise RuntimeError(f"{label}: trailing data after JSON object") + if not isinstance(obj, dict): + raise RuntimeError(f"{label}: root must be object") + return obj + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _uniq(values: list[Any]) -> list[Any]: + seen: set[str] = set() + out: list[Any] = [] + for value in values: + marker = json.dumps(value, ensure_ascii=False, sort_keys=True) if isinstance(value, (dict, list)) else str(value) + if marker not in seen: + seen.add(marker) + out.append(value) + return out + + def _natural_key(value: Any) -> list[Any]: + return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", str(value))] + + def _sorted_ids(values: set[str] | list[str]) -> list[str]: + return sorted([str(v) for v in values if str(v)], key=_natural_key) + + def _blob(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True).lower() + + def _matches(value: Any, terms: list[str]) -> bool: + haystack = _blob(value) + return any(str(term).lower() in haystack for term in terms if str(term).strip()) + + def _copy_fields(source: dict[str, Any], fields: list[str]) -> dict[str, Any]: + out: dict[str, Any] = {} + for field in fields: + if field in source: + out[field] = source.get(field) + return out + + def _project_meeting(record: dict[str, Any]) -> dict[str, Any]: + out = _copy_fields(record, MEETING_FIELDS) + if not out.get("text_excerpt") and record.get("text"): + out["text_excerpt"] = str(record.get("text"))[:240] + return out + + def _project_event(record: dict[str, Any]) -> dict[str, Any]: + return _copy_fields(record, EVENT_FIELDS) + + def _project_evidence(record: dict[str, Any]) -> dict[str, Any]: + return _copy_fields(record, EVIDENCE_FIELDS) + + def _accept_stage_a(stage_a: dict[str, Any]) -> None: + checks = [ + ("stage_a_context.schema_version", stage_a.get("schema_version") == "task_c_bo_stage_a_context.v1"), + ("stage_a_context.status", stage_a.get("status") == "READY"), + ("meeting_clause_map.schema_version", (stage_a.get("meeting_clause_map") or {}).get("schema_version") == "meeting_clause_map.v1"), + ("event_candidate_map.schema_version", (stage_a.get("event_candidate_map") or {}).get("schema_version") == "event_candidate_map.v2"), + ("evidence_authority_map.schema_version", (stage_a.get("evidence_authority_map") or {}).get("schema_version") == "evidence_authority_map.v2"), + ("juristic_act_taxonomy_map.schema_version", (stage_a.get("juristic_act_taxonomy_map") or {}).get("schema_version") == "juristic_act_taxonomy_map.v1"), + ("global_hazard_seed.schema_version", (stage_a.get("global_hazard_seed") or {}).get("schema_version") == "global_hazard_seed.v1"), + ("stage_b_worker_contract.context_is_read_only", (stage_a.get("stage_b_worker_contract") or {}).get("context_is_read_only") is True), + ("stage_b_worker_contract.bo_id_finalization_allowed", (stage_a.get("stage_b_worker_contract") or {}).get("bo_id_finalization_allowed") is False), + ("stage_b_worker_contract.candidate_generation_allowed", (stage_a.get("stage_b_worker_contract") or {}).get("candidate_generation_allowed") is True), + ] + failed = [name for name, ok in checks if not ok] + if failed: + raise RuntimeError(f"Stage A acceptance failed: {failed}") + + def _matched_domains(record: dict[str, Any]) -> list[str]: + return [domain_id for domain_id, terms in ALL_DOMAIN_TERMS.items() if _matches(record, terms)] + + def _select(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + meeting_map = stage_a.get("meeting_clause_map") or {} + event_map = stage_a.get("event_candidate_map") or {} + evidence_map = stage_a.get("evidence_authority_map") or {} + by_clause = meeting_map.get("by_clause_id") or {} + by_event = event_map.get("by_candidate_id") or {} + by_evidence = evidence_map.get("by_evidence_index") or {} + + clause_ids: set[str] = set() + event_ids: set[str] = set() + evidence_ids: set[str] = set() + + for key in config["meeting_sources"]: + for value in _as_list(meeting_map.get(key)): + value_id = str(value) + if _matches(by_clause.get(value_id) or {}, config["terms"]): + clause_ids.add(value_id) + for key in config["event_sources"]: + for value in _as_list(event_map.get(key)): + value_id = str(value) + if _matches(by_event.get(value_id) or {}, config["terms"]): + event_ids.add(value_id) + + for clause_id, record in by_clause.items(): + if _matches(record, config["terms"]): + clause_ids.add(str(clause_id)) + for event_id, record in by_event.items(): + if _matches(record, config["terms"]): + event_ids.add(str(event_id)) + for evidence_id, record in by_evidence.items(): + if _matches(record, config["terms"]): + evidence_ids.add(str(evidence_id)) + + for _ in range(2): + for evidence_id in list(evidence_ids): + event_ids.update(str(v) for v in _as_list((event_map.get("by_evidence_index") or {}).get(evidence_id))) + for clause_id in list(clause_ids): + event_ids.update(str(v) for v in _as_list((event_map.get("by_meeting_clause_ref") or {}).get(clause_id))) + for event_id in list(event_ids): + event = by_event.get(event_id) or {} + source_evidence = event.get("source_evidence_index") + if source_evidence: + evidence_ids.add(str(source_evidence)) + for ref in _as_list(event.get("meeting_clause_refs")): + clause_ids.add(str(ref)) + + clause_ids = {v for v in clause_ids if v in by_clause} + event_ids = {v for v in event_ids if v in by_event} + evidence_ids = {v for v in evidence_ids if v in by_evidence} + + selected_meetings = [_project_meeting(by_clause[v]) for v in _sorted_ids(clause_ids)] + selected_events = [_project_event(by_event[v]) for v in _sorted_ids(event_ids)] + selected_evidence = [_project_evidence(by_evidence[v]) for v in _sorted_ids(evidence_ids)] + + shared: list[dict[str, Any]] = [] + for item_type, id_values, source in [ + ("meeting_clause", clause_ids, by_clause), + ("event_candidate", event_ids, by_event), + ("evidence", evidence_ids, by_evidence), + ]: + for item_id in _sorted_ids(id_values): + domains = _matched_domains(source.get(item_id) or {}) + if len(domains) > 1: + shared.append({"item_type": item_type, "id": item_id, "matched_domain_ids": domains}) + + return { + "meeting_clause_ids": _sorted_ids(clause_ids), + "event_candidate_ids": _sorted_ids(event_ids), + "evidence_indexes": _sorted_ids(evidence_ids), + "selected_meetings": selected_meetings, + "selected_events": selected_events, + "selected_evidence": selected_evidence, + "shared": shared, + } + + def _taxonomy_subset(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + taxonomy = stage_a.get("juristic_act_taxonomy_map") or {} + axes = taxonomy.get("axis_groups") or {} + return { + "schema_version": taxonomy.get("schema_version"), + "categories": taxonomy.get("categories") or {}, + "example_to_gubun": taxonomy.get("example_to_gubun") or {}, + "axis_groups": {key: axes.get(key) for key in config["taxonomy_axes"] if key in axes}, + "source_digest_sha256": taxonomy.get("source_digest_sha256"), + } + + def _hazard_subset(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + hazard = stage_a.get("global_hazard_seed") or {} + return {key: hazard.get(key) for key in config["hazard_keys"] if key in hazard} + + def _part1_review(stage_a: dict[str, Any], selected: dict[str, Any], config: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]: + handoff = stage_a.get("part1_soft_gate_handoff") or {} + guard = { + "schema_version": handoff.get("schema_version"), + "status": handoff.get("status"), + "stage2_auto_progression_allowed": handoff.get("stage2_auto_progression_allowed"), + "usage_rules": handoff.get("usage_rules") or {}, + } + selected_e = set(selected["evidence_indexes"]) + selected_ev = set(selected["event_candidate_ids"]) + selected_m = set(selected["meeting_clause_ids"]) + relevant: list[dict[str, Any]] = [] + for item in _as_list(handoff.get("review_items_compact")): + refs = item.get("source_refs") if isinstance(item, dict) else {} + domain_text = _blob({ + "domain_hints": item.get("domain_hints"), + "target_task_hints": item.get("target_task_hints"), + "issue_type": item.get("issue_type"), + "review_instruction": item.get("review_instruction"), + }) + ref_hit = bool( + selected_e.intersection(str(v) for v in _as_list(refs.get("evidence_indexes"))) + or selected_ev.intersection(str(v) for v in _as_list(refs.get("event_candidate_ids"))) + or selected_m.intersection(str(v) for v in _as_list(refs.get("meeting_clause_refs"))) + ) + if ref_hit or any(str(term).lower() in domain_text for term in config["terms"]): + relevant.append(item) + return guard, relevant + + def _build_slice() -> dict[str, Any]: + root = _parse_json(STAGE_A_RAW, "Task_C_BO_Stage_A_input_normalization") + stage_a = root.get("stage_a_context") + if not isinstance(stage_a, dict): + raise RuntimeError("Stage A output must contain top-level key stage_a_context") + _accept_stage_a(stage_a) + + selected = _select(stage_a, DOMAIN_CONFIG) + part1_guard, part1_items = _part1_review(stage_a, selected, DOMAIN_CONFIG) + worker_contract = stage_a.get("stage_b_worker_contract") or {} + meeting_map = stage_a.get("meeting_clause_map") or {} + event_map = stage_a.get("event_candidate_map") or {} + evidence_map = stage_a.get("evidence_authority_map") or {} + + return { + "stage_b_domain_slice": { + "schema_version": "task_c_bo_stage_b_domain_slice.v1", + "stage": "Task_C_BO.Stage_B0.DomainInputRouter", + "router_task": TASK_NAME, + "router_status": "READY", + "domain_id": DOMAIN_CONFIG["domain_id"], + "domain_label": DOMAIN_CONFIG["domain_label"], + "allowed_legal_effect_bo_types": DOMAIN_CONFIG["allowed_legal_effect_bo_types"], + "source_stage_a_schema_version": stage_a.get("schema_version"), + "source_stage_a_status": stage_a.get("status"), + "source_stage_a_created_at_utc": stage_a.get("created_at_utc"), + "input_digests_sha256": stage_a.get("input_digests_sha256") or {}, + "stage_b_worker_contract": { + "context_is_read_only": worker_contract.get("context_is_read_only"), + "bo_id_finalization_allowed": worker_contract.get("bo_id_finalization_allowed"), + "candidate_generation_allowed": worker_contract.get("candidate_generation_allowed"), + "must_use_evidence_index_set": worker_contract.get("must_use_evidence_index_set") or [], + }, + "source_universe": { + "meeting_clause_ids": meeting_map.get("clause_order") or [], + "event_candidate_ids": event_map.get("candidate_id_set") or [], + "evidence_indexes": evidence_map.get("evidence_index_set") or [], + }, + "selected_meeting_clauses": selected["selected_meetings"], + "selected_event_candidates": selected["selected_events"], + "selected_evidence_authority": selected["selected_evidence"], + "juristic_act_taxonomy_subset": _taxonomy_subset(stage_a, DOMAIN_CONFIG), + "hazard_subset": _hazard_subset(stage_a, DOMAIN_CONFIG), + "part1_review_guard": part1_guard, + "part1_review_items_relevant": part1_items, + "shared_or_ambiguous_items": selected["shared"], + "routing_audit": { + "selected_meeting_clause_ids": selected["meeting_clause_ids"], + "selected_event_candidate_ids": selected["event_candidate_ids"], + "selected_evidence_indexes": selected["evidence_indexes"], + "multi_domain_item_refs": selected["shared"], + "unrouted_but_preserved_refs": [], + "selection_basis": "deterministic keyword/flag/doc_type/domain cue routing with source-id join closure", + "field_policy": "Stage A compact authority records are projected; raw meeting/evidence text is not re-read.", + }, + } + } + + if __name__ == "__main__": + try: + print(json.dumps(_build_slice(), ensure_ascii=False, separators=(",", ":"))) + except Exception as exc: + print(json.dumps({ + "stage_b_domain_slice": { + "schema_version": "task_c_bo_stage_b_domain_slice.v1", + "stage": "Task_C_BO.Stage_B0.DomainInputRouter", + "router_task": TASK_NAME, + "router_status": "FAILED", + "domain_id": DOMAIN_CONFIG["domain_id"], + "domain_label": DOMAIN_CONFIG["domain_label"], + "routing_audit": {"failure": str(exc)}, + } + }, ensure_ascii=False, separators=(",", ":"))) + print(f"[{TASK_NAME}] {exc}", file=sys.stderr) + raise + + - task_name: Task_C_BO_Stage_B0_input_secured_registry + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 60 + code: |- + #!/usr/bin/env python3 + from __future__ import annotations + + import json + import re + import sys + from typing import Any + + TASK_NAME = "Task_C_BO_Stage_B0_input_secured_registry" + STAGE_A_RAW = r"""{{prev.Task_C_BO_Stage_A_input_normalization}}""" + + DOMAIN_CONFIG = { + "domain_id": "B2_Secured_Registry", + "domain_label": "담보채무, 변제충당, 등기무효, 경매", + "allowed_legal_effect_bo_types": [ + "secured_debt_chain_BO", + "payment_allocation_and_mortgage_extinguishment_BO", + "auction_invalidity_BO", + "derivative_bank_mortgage_invalidity_BO", + ], + "terms": [ + "근저당", "저당", "담보", "피담보", "채무액", "변제", "변제충당", + "법정변제충당", "지분", "말소", "소유권이전등기", "등기", "등기원인", + "등기무효", "경매", "담보권실행경매", "경매개시", "매각", "배당", + "후행 담보권", "registry", "mortgage", "secured", "auction", "dividend", + "payment_allocation", "registry_row", "legal_calculation", + ], + "meeting_sources": ["conflict_pattern_clause_ids", "mixed_contract_clause_ids"], + "event_sources": [], + "hazard_keys": ["conflict_pattern_expressions", "asset_clusters", "mixed_contract_candidates"], + "taxonomy_axes": ["legal_object", "form", "dependency", "defect_or_condition"], + } + + ALL_DOMAIN_TERMS = { + "B1_Money_Successor": ["대여금", "매매대금", "보증금", "원금", "이자", "변제기", "상호속용", "영업양도", "loan", "payment", "business_transfer"], + "B2_Secured_Registry": DOMAIN_CONFIG["terms"], + "B3_Land_Valuation": ["토지", "건물", "점유", "사용", "수익", "인도", "차임", "감정", "시가", "가액", "valuation", "possession", "land", "building"], + "B4_Actio": ["사해행위", "피보전채권", "채무초과", "책임재산", "공동담보", "처분행위", "수익자", "전득자", "가액배상", "actio", "fraudulent", "valuation", "defense"], + "B5_Succession_Notice_Lien_Asset_Defense": ["사망", "상속", "상속포기", "통지", "내용증명", "유치권", "별지목록", "목적물", "항변", "succession", "notice", "lien", "defense"], + } + + MEETING_FIELDS = ["clause_id", "local_clause_id", "section_key", "section_label", "major_no", "sub_no", "heading", "line_start", "line_end", "text_excerpt", "tags"] + EVENT_FIELDS = ["candidate_id", "source_evidence_index", "title", "doc_type", "event_or_state", "event_kind", "event_subkind", "action_type_candidate", "action_summary", "participants", "object_spec", "amount", "event_date", "event_end_date", "time_text", "time_precision", "current_state_candidate", "succession_candidate", "legal_effect_candidate", "defense_candidate", "commercial_successor_candidates", "meeting_clause_refs", "actio_relevance_candidates", "identity_signature", "property_cluster_id", "affected_asset_cluster_ids", "doc_semantic_flags", "confidence", "fraction_ref"] + EVIDENCE_FIELDS = ["evidence_index", "doc_uid", "title", "title_normalized", "doc_type", "key_facts", "key_dates", "key_amounts", "key_parties", "source_pointer", "property_cluster_id", "doc_semantic_flags", "multi_event_registry", "component_summary", "component_details"] + + def _parse_json(raw: str, label: str) -> dict[str, Any]: + text = (raw or "").strip() + if not text or text == "null": + raise RuntimeError(f"{label}: empty injected JSON") + obj, idx = json.JSONDecoder().raw_decode(text) + if text[idx:].strip(): + raise RuntimeError(f"{label}: trailing data after JSON object") + if not isinstance(obj, dict): + raise RuntimeError(f"{label}: root must be object") + return obj + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _natural_key(value: Any) -> list[Any]: + return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", str(value))] + + def _sorted_ids(values: set[str] | list[str]) -> list[str]: + return sorted([str(v) for v in values if str(v)], key=_natural_key) + + def _blob(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True).lower() + + def _matches(value: Any, terms: list[str]) -> bool: + haystack = _blob(value) + return any(str(term).lower() in haystack for term in terms if str(term).strip()) + + def _copy_fields(source: dict[str, Any], fields: list[str]) -> dict[str, Any]: + return {field: source.get(field) for field in fields if field in source} + + def _project_meeting(record: dict[str, Any]) -> dict[str, Any]: + out = _copy_fields(record, MEETING_FIELDS) + if not out.get("text_excerpt") and record.get("text"): + out["text_excerpt"] = str(record.get("text"))[:240] + return out + + def _project_event(record: dict[str, Any]) -> dict[str, Any]: + return _copy_fields(record, EVENT_FIELDS) + + def _project_evidence(record: dict[str, Any]) -> dict[str, Any]: + return _copy_fields(record, EVIDENCE_FIELDS) + + def _accept_stage_a(stage_a: dict[str, Any]) -> None: + checks = [ + ("stage_a_context.schema_version", stage_a.get("schema_version") == "task_c_bo_stage_a_context.v1"), + ("stage_a_context.status", stage_a.get("status") == "READY"), + ("meeting_clause_map.schema_version", (stage_a.get("meeting_clause_map") or {}).get("schema_version") == "meeting_clause_map.v1"), + ("event_candidate_map.schema_version", (stage_a.get("event_candidate_map") or {}).get("schema_version") == "event_candidate_map.v2"), + ("evidence_authority_map.schema_version", (stage_a.get("evidence_authority_map") or {}).get("schema_version") == "evidence_authority_map.v2"), + ("juristic_act_taxonomy_map.schema_version", (stage_a.get("juristic_act_taxonomy_map") or {}).get("schema_version") == "juristic_act_taxonomy_map.v1"), + ("global_hazard_seed.schema_version", (stage_a.get("global_hazard_seed") or {}).get("schema_version") == "global_hazard_seed.v1"), + ("stage_b_worker_contract.context_is_read_only", (stage_a.get("stage_b_worker_contract") or {}).get("context_is_read_only") is True), + ("stage_b_worker_contract.bo_id_finalization_allowed", (stage_a.get("stage_b_worker_contract") or {}).get("bo_id_finalization_allowed") is False), + ("stage_b_worker_contract.candidate_generation_allowed", (stage_a.get("stage_b_worker_contract") or {}).get("candidate_generation_allowed") is True), + ] + failed = [name for name, ok in checks if not ok] + if failed: + raise RuntimeError(f"Stage A acceptance failed: {failed}") + + def _matched_domains(record: dict[str, Any]) -> list[str]: + return [domain_id for domain_id, terms in ALL_DOMAIN_TERMS.items() if _matches(record, terms)] + + def _select(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + meeting_map = stage_a.get("meeting_clause_map") or {} + event_map = stage_a.get("event_candidate_map") or {} + evidence_map = stage_a.get("evidence_authority_map") or {} + by_clause = meeting_map.get("by_clause_id") or {} + by_event = event_map.get("by_candidate_id") or {} + by_evidence = evidence_map.get("by_evidence_index") or {} + clause_ids: set[str] = set() + event_ids: set[str] = set() + evidence_ids: set[str] = set() + for key in config["meeting_sources"]: + for value in _as_list(meeting_map.get(key)): + value_id = str(value) + if _matches(by_clause.get(value_id) or {}, config["terms"]): + clause_ids.add(value_id) + for key in config["event_sources"]: + for value in _as_list(event_map.get(key)): + value_id = str(value) + if _matches(by_event.get(value_id) or {}, config["terms"]): + event_ids.add(value_id) + for clause_id, record in by_clause.items(): + if _matches(record, config["terms"]): + clause_ids.add(str(clause_id)) + for event_id, record in by_event.items(): + if _matches(record, config["terms"]): + event_ids.add(str(event_id)) + for evidence_id, record in by_evidence.items(): + if _matches(record, config["terms"]): + evidence_ids.add(str(evidence_id)) + for _ in range(2): + for evidence_id in list(evidence_ids): + event_ids.update(str(v) for v in _as_list((event_map.get("by_evidence_index") or {}).get(evidence_id))) + for clause_id in list(clause_ids): + event_ids.update(str(v) for v in _as_list((event_map.get("by_meeting_clause_ref") or {}).get(clause_id))) + for event_id in list(event_ids): + event = by_event.get(event_id) or {} + if event.get("source_evidence_index"): + evidence_ids.add(str(event.get("source_evidence_index"))) + for ref in _as_list(event.get("meeting_clause_refs")): + clause_ids.add(str(ref)) + clause_ids = {v for v in clause_ids if v in by_clause} + event_ids = {v for v in event_ids if v in by_event} + evidence_ids = {v for v in evidence_ids if v in by_evidence} + shared: list[dict[str, Any]] = [] + for item_type, id_values, source in [("meeting_clause", clause_ids, by_clause), ("event_candidate", event_ids, by_event), ("evidence", evidence_ids, by_evidence)]: + for item_id in _sorted_ids(id_values): + domains = _matched_domains(source.get(item_id) or {}) + if len(domains) > 1: + shared.append({"item_type": item_type, "id": item_id, "matched_domain_ids": domains}) + return { + "meeting_clause_ids": _sorted_ids(clause_ids), + "event_candidate_ids": _sorted_ids(event_ids), + "evidence_indexes": _sorted_ids(evidence_ids), + "selected_meetings": [_project_meeting(by_clause[v]) for v in _sorted_ids(clause_ids)], + "selected_events": [_project_event(by_event[v]) for v in _sorted_ids(event_ids)], + "selected_evidence": [_project_evidence(by_evidence[v]) for v in _sorted_ids(evidence_ids)], + "shared": shared, + } + + def _taxonomy_subset(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + taxonomy = stage_a.get("juristic_act_taxonomy_map") or {} + axes = taxonomy.get("axis_groups") or {} + return {"schema_version": taxonomy.get("schema_version"), "categories": taxonomy.get("categories") or {}, "example_to_gubun": taxonomy.get("example_to_gubun") or {}, "axis_groups": {key: axes.get(key) for key in config["taxonomy_axes"] if key in axes}, "source_digest_sha256": taxonomy.get("source_digest_sha256")} + + def _hazard_subset(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + hazard = stage_a.get("global_hazard_seed") or {} + return {key: hazard.get(key) for key in config["hazard_keys"] if key in hazard} + + def _part1_review(stage_a: dict[str, Any], selected: dict[str, Any], config: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]: + handoff = stage_a.get("part1_soft_gate_handoff") or {} + guard = {"schema_version": handoff.get("schema_version"), "status": handoff.get("status"), "stage2_auto_progression_allowed": handoff.get("stage2_auto_progression_allowed"), "usage_rules": handoff.get("usage_rules") or {}} + selected_e = set(selected["evidence_indexes"]) + selected_ev = set(selected["event_candidate_ids"]) + selected_m = set(selected["meeting_clause_ids"]) + relevant: list[dict[str, Any]] = [] + for item in _as_list(handoff.get("review_items_compact")): + refs = item.get("source_refs") if isinstance(item, dict) else {} + domain_text = _blob({"domain_hints": item.get("domain_hints"), "target_task_hints": item.get("target_task_hints"), "issue_type": item.get("issue_type"), "review_instruction": item.get("review_instruction")}) + ref_hit = bool(selected_e.intersection(str(v) for v in _as_list(refs.get("evidence_indexes"))) or selected_ev.intersection(str(v) for v in _as_list(refs.get("event_candidate_ids"))) or selected_m.intersection(str(v) for v in _as_list(refs.get("meeting_clause_refs")))) + if ref_hit or any(str(term).lower() in domain_text for term in config["terms"]): + relevant.append(item) + return guard, relevant + + def _build_slice() -> dict[str, Any]: + root = _parse_json(STAGE_A_RAW, "Task_C_BO_Stage_A_input_normalization") + stage_a = root.get("stage_a_context") + if not isinstance(stage_a, dict): + raise RuntimeError("Stage A output must contain top-level key stage_a_context") + _accept_stage_a(stage_a) + selected = _select(stage_a, DOMAIN_CONFIG) + part1_guard, part1_items = _part1_review(stage_a, selected, DOMAIN_CONFIG) + worker_contract = stage_a.get("stage_b_worker_contract") or {} + meeting_map = stage_a.get("meeting_clause_map") or {} + event_map = stage_a.get("event_candidate_map") or {} + evidence_map = stage_a.get("evidence_authority_map") or {} + return {"stage_b_domain_slice": {"schema_version": "task_c_bo_stage_b_domain_slice.v1", "stage": "Task_C_BO.Stage_B0.DomainInputRouter", "router_task": TASK_NAME, "router_status": "READY", "domain_id": DOMAIN_CONFIG["domain_id"], "domain_label": DOMAIN_CONFIG["domain_label"], "allowed_legal_effect_bo_types": DOMAIN_CONFIG["allowed_legal_effect_bo_types"], "source_stage_a_schema_version": stage_a.get("schema_version"), "source_stage_a_status": stage_a.get("status"), "source_stage_a_created_at_utc": stage_a.get("created_at_utc"), "input_digests_sha256": stage_a.get("input_digests_sha256") or {}, "stage_b_worker_contract": {"context_is_read_only": worker_contract.get("context_is_read_only"), "bo_id_finalization_allowed": worker_contract.get("bo_id_finalization_allowed"), "candidate_generation_allowed": worker_contract.get("candidate_generation_allowed"), "must_use_evidence_index_set": worker_contract.get("must_use_evidence_index_set") or []}, "source_universe": {"meeting_clause_ids": meeting_map.get("clause_order") or [], "event_candidate_ids": event_map.get("candidate_id_set") or [], "evidence_indexes": evidence_map.get("evidence_index_set") or []}, "selected_meeting_clauses": selected["selected_meetings"], "selected_event_candidates": selected["selected_events"], "selected_evidence_authority": selected["selected_evidence"], "juristic_act_taxonomy_subset": _taxonomy_subset(stage_a, DOMAIN_CONFIG), "hazard_subset": _hazard_subset(stage_a, DOMAIN_CONFIG), "part1_review_guard": part1_guard, "part1_review_items_relevant": part1_items, "shared_or_ambiguous_items": selected["shared"], "routing_audit": {"selected_meeting_clause_ids": selected["meeting_clause_ids"], "selected_event_candidate_ids": selected["event_candidate_ids"], "selected_evidence_indexes": selected["evidence_indexes"], "multi_domain_item_refs": selected["shared"], "unrouted_but_preserved_refs": [], "selection_basis": "deterministic keyword/flag/doc_type/domain cue routing with source-id join closure", "field_policy": "Stage A compact authority records are projected; raw meeting/evidence text is not re-read."}}} + + if __name__ == "__main__": + try: + print(json.dumps(_build_slice(), ensure_ascii=False, separators=(",", ":"))) + except Exception as exc: + print(json.dumps({"stage_b_domain_slice": {"schema_version": "task_c_bo_stage_b_domain_slice.v1", "stage": "Task_C_BO.Stage_B0.DomainInputRouter", "router_task": TASK_NAME, "router_status": "FAILED", "domain_id": DOMAIN_CONFIG["domain_id"], "domain_label": DOMAIN_CONFIG["domain_label"], "routing_audit": {"failure": str(exc)}}}, ensure_ascii=False, separators=(",", ":"))) + print(f"[{TASK_NAME}] {exc}", file=sys.stderr) + raise + + - task_name: Task_C_BO_Stage_B0_input_land_valuation + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 60 + code: |- + #!/usr/bin/env python3 + from __future__ import annotations + + import json + import re + import sys + from typing import Any + + TASK_NAME = "Task_C_BO_Stage_B0_input_land_valuation" + STAGE_A_RAW = r"""{{prev.Task_C_BO_Stage_A_input_normalization}}""" + + DOMAIN_CONFIG = { + "domain_id": "B3_Land_Valuation", + "domain_label": "토지 사용이익, 점유, 사용수익, 가치평가", + "allowed_legal_effect_bo_types": ["land_use_gain_timeline_BO", "liability_overlap_BO"], + "terms": [ + "토지", "건물", "부지", "인도", "점유", "사용", "사용수익", "현재 사용", + "현재 수익", "거주", "운영", "임대", "차임", "감정", "시가", "가액", + "미등기 건물", "사실상 처분권", "부진정연대", "valuation", "valuation_object", + "valuation_rows", "valuation_matrix", "registry_object_display", "possession", "land", "building", + ], + "meeting_sources": ["current_state_clause_ids", "conflict_pattern_clause_ids", "mixed_contract_clause_ids"], + "event_sources": ["state_candidate_ids"], + "hazard_keys": ["current_state_expressions", "asset_clusters", "mixed_contract_candidates", "conflict_pattern_expressions"], + "taxonomy_axes": ["legal_object", "contribution", "structure"], + } + + ALL_DOMAIN_TERMS = { + "B1_Money_Successor": ["대여금", "매매대금", "보증금", "원금", "이자", "변제기", "상호속용", "영업양도", "loan", "payment", "business_transfer"], + "B2_Secured_Registry": ["근저당", "저당", "담보", "피담보", "변제충당", "등기", "말소", "경매", "배당", "registry", "mortgage", "secured", "auction", "dividend"], + "B3_Land_Valuation": DOMAIN_CONFIG["terms"], + "B4_Actio": ["사해행위", "피보전채권", "채무초과", "책임재산", "공동담보", "처분행위", "수익자", "전득자", "가액배상", "actio", "fraudulent", "valuation", "defense"], + "B5_Succession_Notice_Lien_Asset_Defense": ["사망", "상속", "상속포기", "통지", "내용증명", "유치권", "별지목록", "목적물", "항변", "succession", "notice", "lien", "defense"], + } + + MEETING_FIELDS = ["clause_id", "local_clause_id", "section_key", "section_label", "major_no", "sub_no", "heading", "line_start", "line_end", "text_excerpt", "tags"] + EVENT_FIELDS = ["candidate_id", "source_evidence_index", "title", "doc_type", "event_or_state", "event_kind", "event_subkind", "action_type_candidate", "action_summary", "participants", "object_spec", "amount", "event_date", "event_end_date", "time_text", "time_precision", "current_state_candidate", "succession_candidate", "legal_effect_candidate", "defense_candidate", "commercial_successor_candidates", "meeting_clause_refs", "actio_relevance_candidates", "identity_signature", "property_cluster_id", "affected_asset_cluster_ids", "doc_semantic_flags", "confidence", "fraction_ref"] + EVIDENCE_FIELDS = ["evidence_index", "doc_uid", "title", "title_normalized", "doc_type", "key_facts", "key_dates", "key_amounts", "key_parties", "source_pointer", "property_cluster_id", "doc_semantic_flags", "multi_event_registry", "component_summary", "component_details"] + + def _parse_json(raw: str, label: str) -> dict[str, Any]: + text = (raw or "").strip() + if not text or text == "null": + raise RuntimeError(f"{label}: empty injected JSON") + obj, idx = json.JSONDecoder().raw_decode(text) + if text[idx:].strip(): + raise RuntimeError(f"{label}: trailing data after JSON object") + if not isinstance(obj, dict): + raise RuntimeError(f"{label}: root must be object") + return obj + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _natural_key(value: Any) -> list[Any]: + return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", str(value))] + + def _sorted_ids(values: set[str] | list[str]) -> list[str]: + return sorted([str(v) for v in values if str(v)], key=_natural_key) + + def _blob(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True).lower() + + def _matches(value: Any, terms: list[str]) -> bool: + haystack = _blob(value) + return any(str(term).lower() in haystack for term in terms if str(term).strip()) + + def _copy_fields(source: dict[str, Any], fields: list[str]) -> dict[str, Any]: + return {field: source.get(field) for field in fields if field in source} + + def _project_meeting(record: dict[str, Any]) -> dict[str, Any]: + out = _copy_fields(record, MEETING_FIELDS) + if not out.get("text_excerpt") and record.get("text"): + out["text_excerpt"] = str(record.get("text"))[:240] + return out + + def _project_event(record: dict[str, Any]) -> dict[str, Any]: + return _copy_fields(record, EVENT_FIELDS) + + def _project_evidence(record: dict[str, Any]) -> dict[str, Any]: + return _copy_fields(record, EVIDENCE_FIELDS) + + def _accept_stage_a(stage_a: dict[str, Any]) -> None: + checks = [ + ("stage_a_context.schema_version", stage_a.get("schema_version") == "task_c_bo_stage_a_context.v1"), + ("stage_a_context.status", stage_a.get("status") == "READY"), + ("meeting_clause_map.schema_version", (stage_a.get("meeting_clause_map") or {}).get("schema_version") == "meeting_clause_map.v1"), + ("event_candidate_map.schema_version", (stage_a.get("event_candidate_map") or {}).get("schema_version") == "event_candidate_map.v2"), + ("evidence_authority_map.schema_version", (stage_a.get("evidence_authority_map") or {}).get("schema_version") == "evidence_authority_map.v2"), + ("juristic_act_taxonomy_map.schema_version", (stage_a.get("juristic_act_taxonomy_map") or {}).get("schema_version") == "juristic_act_taxonomy_map.v1"), + ("global_hazard_seed.schema_version", (stage_a.get("global_hazard_seed") or {}).get("schema_version") == "global_hazard_seed.v1"), + ("stage_b_worker_contract.context_is_read_only", (stage_a.get("stage_b_worker_contract") or {}).get("context_is_read_only") is True), + ("stage_b_worker_contract.bo_id_finalization_allowed", (stage_a.get("stage_b_worker_contract") or {}).get("bo_id_finalization_allowed") is False), + ("stage_b_worker_contract.candidate_generation_allowed", (stage_a.get("stage_b_worker_contract") or {}).get("candidate_generation_allowed") is True), + ] + failed = [name for name, ok in checks if not ok] + if failed: + raise RuntimeError(f"Stage A acceptance failed: {failed}") + + def _matched_domains(record: dict[str, Any]) -> list[str]: + return [domain_id for domain_id, terms in ALL_DOMAIN_TERMS.items() if _matches(record, terms)] + + def _select(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + meeting_map = stage_a.get("meeting_clause_map") or {} + event_map = stage_a.get("event_candidate_map") or {} + evidence_map = stage_a.get("evidence_authority_map") or {} + by_clause = meeting_map.get("by_clause_id") or {} + by_event = event_map.get("by_candidate_id") or {} + by_evidence = evidence_map.get("by_evidence_index") or {} + clause_ids: set[str] = set() + event_ids: set[str] = set() + evidence_ids: set[str] = set() + for key in config["meeting_sources"]: + for value in _as_list(meeting_map.get(key)): + value_id = str(value) + if _matches(by_clause.get(value_id) or {}, config["terms"]): + clause_ids.add(value_id) + for key in config["event_sources"]: + for value in _as_list(event_map.get(key)): + value_id = str(value) + if _matches(by_event.get(value_id) or {}, config["terms"]): + event_ids.add(value_id) + for clause_id, record in by_clause.items(): + if _matches(record, config["terms"]): + clause_ids.add(str(clause_id)) + for event_id, record in by_event.items(): + if _matches(record, config["terms"]): + event_ids.add(str(event_id)) + for evidence_id, record in by_evidence.items(): + if _matches(record, config["terms"]): + evidence_ids.add(str(evidence_id)) + for _ in range(2): + for evidence_id in list(evidence_ids): + event_ids.update(str(v) for v in _as_list((event_map.get("by_evidence_index") or {}).get(evidence_id))) + for clause_id in list(clause_ids): + event_ids.update(str(v) for v in _as_list((event_map.get("by_meeting_clause_ref") or {}).get(clause_id))) + for event_id in list(event_ids): + event = by_event.get(event_id) or {} + if event.get("source_evidence_index"): + evidence_ids.add(str(event.get("source_evidence_index"))) + for ref in _as_list(event.get("meeting_clause_refs")): + clause_ids.add(str(ref)) + clause_ids = {v for v in clause_ids if v in by_clause} + event_ids = {v for v in event_ids if v in by_event} + evidence_ids = {v for v in evidence_ids if v in by_evidence} + shared: list[dict[str, Any]] = [] + for item_type, id_values, source in [("meeting_clause", clause_ids, by_clause), ("event_candidate", event_ids, by_event), ("evidence", evidence_ids, by_evidence)]: + for item_id in _sorted_ids(id_values): + domains = _matched_domains(source.get(item_id) or {}) + if len(domains) > 1: + shared.append({"item_type": item_type, "id": item_id, "matched_domain_ids": domains}) + return { + "meeting_clause_ids": _sorted_ids(clause_ids), + "event_candidate_ids": _sorted_ids(event_ids), + "evidence_indexes": _sorted_ids(evidence_ids), + "selected_meetings": [_project_meeting(by_clause[v]) for v in _sorted_ids(clause_ids)], + "selected_events": [_project_event(by_event[v]) for v in _sorted_ids(event_ids)], + "selected_evidence": [_project_evidence(by_evidence[v]) for v in _sorted_ids(evidence_ids)], + "shared": shared, + } + + def _taxonomy_subset(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + taxonomy = stage_a.get("juristic_act_taxonomy_map") or {} + axes = taxonomy.get("axis_groups") or {} + return {"schema_version": taxonomy.get("schema_version"), "categories": taxonomy.get("categories") or {}, "example_to_gubun": taxonomy.get("example_to_gubun") or {}, "axis_groups": {key: axes.get(key) for key in config["taxonomy_axes"] if key in axes}, "source_digest_sha256": taxonomy.get("source_digest_sha256")} + + def _hazard_subset(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + hazard = stage_a.get("global_hazard_seed") or {} + return {key: hazard.get(key) for key in config["hazard_keys"] if key in hazard} + + def _part1_review(stage_a: dict[str, Any], selected: dict[str, Any], config: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]: + handoff = stage_a.get("part1_soft_gate_handoff") or {} + guard = {"schema_version": handoff.get("schema_version"), "status": handoff.get("status"), "stage2_auto_progression_allowed": handoff.get("stage2_auto_progression_allowed"), "usage_rules": handoff.get("usage_rules") or {}} + selected_e = set(selected["evidence_indexes"]) + selected_ev = set(selected["event_candidate_ids"]) + selected_m = set(selected["meeting_clause_ids"]) + relevant: list[dict[str, Any]] = [] + for item in _as_list(handoff.get("review_items_compact")): + refs = item.get("source_refs") if isinstance(item, dict) else {} + domain_text = _blob({"domain_hints": item.get("domain_hints"), "target_task_hints": item.get("target_task_hints"), "issue_type": item.get("issue_type"), "review_instruction": item.get("review_instruction")}) + ref_hit = bool(selected_e.intersection(str(v) for v in _as_list(refs.get("evidence_indexes"))) or selected_ev.intersection(str(v) for v in _as_list(refs.get("event_candidate_ids"))) or selected_m.intersection(str(v) for v in _as_list(refs.get("meeting_clause_refs")))) + if ref_hit or any(str(term).lower() in domain_text for term in config["terms"]): + relevant.append(item) + return guard, relevant + + def _build_slice() -> dict[str, Any]: + root = _parse_json(STAGE_A_RAW, "Task_C_BO_Stage_A_input_normalization") + stage_a = root.get("stage_a_context") + if not isinstance(stage_a, dict): + raise RuntimeError("Stage A output must contain top-level key stage_a_context") + _accept_stage_a(stage_a) + selected = _select(stage_a, DOMAIN_CONFIG) + part1_guard, part1_items = _part1_review(stage_a, selected, DOMAIN_CONFIG) + worker_contract = stage_a.get("stage_b_worker_contract") or {} + meeting_map = stage_a.get("meeting_clause_map") or {} + event_map = stage_a.get("event_candidate_map") or {} + evidence_map = stage_a.get("evidence_authority_map") or {} + return {"stage_b_domain_slice": {"schema_version": "task_c_bo_stage_b_domain_slice.v1", "stage": "Task_C_BO.Stage_B0.DomainInputRouter", "router_task": TASK_NAME, "router_status": "READY", "domain_id": DOMAIN_CONFIG["domain_id"], "domain_label": DOMAIN_CONFIG["domain_label"], "allowed_legal_effect_bo_types": DOMAIN_CONFIG["allowed_legal_effect_bo_types"], "source_stage_a_schema_version": stage_a.get("schema_version"), "source_stage_a_status": stage_a.get("status"), "source_stage_a_created_at_utc": stage_a.get("created_at_utc"), "input_digests_sha256": stage_a.get("input_digests_sha256") or {}, "stage_b_worker_contract": {"context_is_read_only": worker_contract.get("context_is_read_only"), "bo_id_finalization_allowed": worker_contract.get("bo_id_finalization_allowed"), "candidate_generation_allowed": worker_contract.get("candidate_generation_allowed"), "must_use_evidence_index_set": worker_contract.get("must_use_evidence_index_set") or []}, "source_universe": {"meeting_clause_ids": meeting_map.get("clause_order") or [], "event_candidate_ids": event_map.get("candidate_id_set") or [], "evidence_indexes": evidence_map.get("evidence_index_set") or []}, "selected_meeting_clauses": selected["selected_meetings"], "selected_event_candidates": selected["selected_events"], "selected_evidence_authority": selected["selected_evidence"], "juristic_act_taxonomy_subset": _taxonomy_subset(stage_a, DOMAIN_CONFIG), "hazard_subset": _hazard_subset(stage_a, DOMAIN_CONFIG), "part1_review_guard": part1_guard, "part1_review_items_relevant": part1_items, "shared_or_ambiguous_items": selected["shared"], "routing_audit": {"selected_meeting_clause_ids": selected["meeting_clause_ids"], "selected_event_candidate_ids": selected["event_candidate_ids"], "selected_evidence_indexes": selected["evidence_indexes"], "multi_domain_item_refs": selected["shared"], "unrouted_but_preserved_refs": [], "selection_basis": "deterministic keyword/flag/doc_type/domain cue routing with source-id join closure", "field_policy": "Stage A compact authority records are projected; raw meeting/evidence text is not re-read."}}} + + if __name__ == "__main__": + try: + print(json.dumps(_build_slice(), ensure_ascii=False, separators=(",", ":"))) + except Exception as exc: + print(json.dumps({"stage_b_domain_slice": {"schema_version": "task_c_bo_stage_b_domain_slice.v1", "stage": "Task_C_BO.Stage_B0.DomainInputRouter", "router_task": TASK_NAME, "router_status": "FAILED", "domain_id": DOMAIN_CONFIG["domain_id"], "domain_label": DOMAIN_CONFIG["domain_label"], "routing_audit": {"failure": str(exc)}}}, ensure_ascii=False, separators=(",", ":"))) + print(f"[{TASK_NAME}] {exc}", file=sys.stderr) + raise + + - task_name: Task_C_BO_Stage_B0_input_actio + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 60 + code: |- + #!/usr/bin/env python3 + from __future__ import annotations + + import json + import re + import sys + from typing import Any + + TASK_NAME = "Task_C_BO_Stage_B0_input_actio" + STAGE_A_RAW = r"""{{prev.Task_C_BO_Stage_A_input_normalization}}""" + + DOMAIN_CONFIG = { + "domain_id": "B4_Actio", + "domain_label": "사해행위취소, 피보전채권, 책임재산, 가액배상", + "allowed_legal_effect_bo_types": [ + "preserved_claim_bundle_BO", + "fraudulent_transfer_BO", + "encumbrance_timeline_BO", + "valuation_matrix_BO", + "remedy_mode_decision_BO", + "value_compensation_cap_BO", + "defense_rebuttal_matrix_BO", + "actio_balance_sheet_BO", + ], + "terms": [ + "사해행위", "피보전채권", "채무초과", "책임재산", "공동담보", "매매", + "증여", "담보설정", "처분행위", "수익자", "전득자", "사해성", "악의", + "선의", "원상회복", "가액배상", "선순위담보", "말소", "가액 산정", + "항변", "actio", "fraudulent", "transfer", "preserved_claim", "encumbrance", + "valuation", "remedy", "compensation", "defense", "claim_document", + ], + "meeting_sources": ["conflict_pattern_clause_ids", "mixed_contract_clause_ids", "current_state_clause_ids", "succession_clause_ids"], + "event_sources": [], + "hazard_keys": ["conflict_pattern_expressions", "asset_clusters", "mixed_contract_candidates", "current_state_expressions", "succession_related_candidates"], + "taxonomy_axes": ["legal_object", "structure", "contribution", "defect_or_condition", "dependency", "form"], + } + + ALL_DOMAIN_TERMS = { + "B1_Money_Successor": ["대여금", "매매대금", "보증금", "원금", "이자", "변제기", "상호속용", "영업양도", "loan", "payment", "business_transfer"], + "B2_Secured_Registry": ["근저당", "저당", "담보", "피담보", "변제충당", "등기", "말소", "경매", "배당", "registry", "mortgage", "secured", "auction", "dividend"], + "B3_Land_Valuation": ["토지", "건물", "점유", "사용", "수익", "인도", "차임", "감정", "시가", "가액", "valuation", "possession", "land", "building"], + "B4_Actio": DOMAIN_CONFIG["terms"], + "B5_Succession_Notice_Lien_Asset_Defense": ["사망", "상속", "상속포기", "통지", "내용증명", "유치권", "별지목록", "목적물", "항변", "succession", "notice", "lien", "defense"], + } + + MEETING_FIELDS = ["clause_id", "local_clause_id", "section_key", "section_label", "major_no", "sub_no", "heading", "line_start", "line_end", "text_excerpt", "tags"] + EVENT_FIELDS = ["candidate_id", "source_evidence_index", "title", "doc_type", "event_or_state", "event_kind", "event_subkind", "action_type_candidate", "action_summary", "participants", "object_spec", "amount", "event_date", "event_end_date", "time_text", "time_precision", "current_state_candidate", "succession_candidate", "legal_effect_candidate", "defense_candidate", "commercial_successor_candidates", "meeting_clause_refs", "actio_relevance_candidates", "identity_signature", "property_cluster_id", "affected_asset_cluster_ids", "doc_semantic_flags", "confidence", "fraction_ref"] + EVIDENCE_FIELDS = ["evidence_index", "doc_uid", "title", "title_normalized", "doc_type", "key_facts", "key_dates", "key_amounts", "key_parties", "source_pointer", "property_cluster_id", "doc_semantic_flags", "multi_event_registry", "component_summary", "component_details"] + + def _parse_json(raw: str, label: str) -> dict[str, Any]: + text = (raw or "").strip() + if not text or text == "null": + raise RuntimeError(f"{label}: empty injected JSON") + obj, idx = json.JSONDecoder().raw_decode(text) + if text[idx:].strip(): + raise RuntimeError(f"{label}: trailing data after JSON object") + if not isinstance(obj, dict): + raise RuntimeError(f"{label}: root must be object") + return obj + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _natural_key(value: Any) -> list[Any]: + return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", str(value))] + + def _sorted_ids(values: set[str] | list[str]) -> list[str]: + return sorted([str(v) for v in values if str(v)], key=_natural_key) + + def _blob(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True).lower() + + def _matches(value: Any, terms: list[str]) -> bool: + haystack = _blob(value) + return any(str(term).lower() in haystack for term in terms if str(term).strip()) + + def _copy_fields(source: dict[str, Any], fields: list[str]) -> dict[str, Any]: + return {field: source.get(field) for field in fields if field in source} + + def _project_meeting(record: dict[str, Any]) -> dict[str, Any]: + out = _copy_fields(record, MEETING_FIELDS) + if not out.get("text_excerpt") and record.get("text"): + out["text_excerpt"] = str(record.get("text"))[:240] + return out + + def _project_event(record: dict[str, Any]) -> dict[str, Any]: + return _copy_fields(record, EVENT_FIELDS) + + def _project_evidence(record: dict[str, Any]) -> dict[str, Any]: + return _copy_fields(record, EVIDENCE_FIELDS) + + def _accept_stage_a(stage_a: dict[str, Any]) -> None: + checks = [ + ("stage_a_context.schema_version", stage_a.get("schema_version") == "task_c_bo_stage_a_context.v1"), + ("stage_a_context.status", stage_a.get("status") == "READY"), + ("meeting_clause_map.schema_version", (stage_a.get("meeting_clause_map") or {}).get("schema_version") == "meeting_clause_map.v1"), + ("event_candidate_map.schema_version", (stage_a.get("event_candidate_map") or {}).get("schema_version") == "event_candidate_map.v2"), + ("evidence_authority_map.schema_version", (stage_a.get("evidence_authority_map") or {}).get("schema_version") == "evidence_authority_map.v2"), + ("juristic_act_taxonomy_map.schema_version", (stage_a.get("juristic_act_taxonomy_map") or {}).get("schema_version") == "juristic_act_taxonomy_map.v1"), + ("global_hazard_seed.schema_version", (stage_a.get("global_hazard_seed") or {}).get("schema_version") == "global_hazard_seed.v1"), + ("stage_b_worker_contract.context_is_read_only", (stage_a.get("stage_b_worker_contract") or {}).get("context_is_read_only") is True), + ("stage_b_worker_contract.bo_id_finalization_allowed", (stage_a.get("stage_b_worker_contract") or {}).get("bo_id_finalization_allowed") is False), + ("stage_b_worker_contract.candidate_generation_allowed", (stage_a.get("stage_b_worker_contract") or {}).get("candidate_generation_allowed") is True), + ] + failed = [name for name, ok in checks if not ok] + if failed: + raise RuntimeError(f"Stage A acceptance failed: {failed}") + + def _matched_domains(record: dict[str, Any]) -> list[str]: + return [domain_id for domain_id, terms in ALL_DOMAIN_TERMS.items() if _matches(record, terms)] + + def _select(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + meeting_map = stage_a.get("meeting_clause_map") or {} + event_map = stage_a.get("event_candidate_map") or {} + evidence_map = stage_a.get("evidence_authority_map") or {} + by_clause = meeting_map.get("by_clause_id") or {} + by_event = event_map.get("by_candidate_id") or {} + by_evidence = evidence_map.get("by_evidence_index") or {} + clause_ids: set[str] = set() + event_ids: set[str] = set() + evidence_ids: set[str] = set() + for key in config["meeting_sources"]: + for value in _as_list(meeting_map.get(key)): + value_id = str(value) + if _matches(by_clause.get(value_id) or {}, config["terms"]): + clause_ids.add(value_id) + for key in config["event_sources"]: + for value in _as_list(event_map.get(key)): + value_id = str(value) + if _matches(by_event.get(value_id) or {}, config["terms"]): + event_ids.add(value_id) + for clause_id, record in by_clause.items(): + if _matches(record, config["terms"]): + clause_ids.add(str(clause_id)) + for event_id, record in by_event.items(): + if record.get("actio_relevance_candidates") or _matches(record, config["terms"]): + event_ids.add(str(event_id)) + for evidence_id, record in by_evidence.items(): + if _matches(record, config["terms"]): + evidence_ids.add(str(evidence_id)) + for _ in range(2): + for evidence_id in list(evidence_ids): + event_ids.update(str(v) for v in _as_list((event_map.get("by_evidence_index") or {}).get(evidence_id))) + for clause_id in list(clause_ids): + event_ids.update(str(v) for v in _as_list((event_map.get("by_meeting_clause_ref") or {}).get(clause_id))) + for event_id in list(event_ids): + event = by_event.get(event_id) or {} + if event.get("source_evidence_index"): + evidence_ids.add(str(event.get("source_evidence_index"))) + for ref in _as_list(event.get("meeting_clause_refs")): + clause_ids.add(str(ref)) + clause_ids = {v for v in clause_ids if v in by_clause} + event_ids = {v for v in event_ids if v in by_event} + evidence_ids = {v for v in evidence_ids if v in by_evidence} + shared: list[dict[str, Any]] = [] + for item_type, id_values, source in [("meeting_clause", clause_ids, by_clause), ("event_candidate", event_ids, by_event), ("evidence", evidence_ids, by_evidence)]: + for item_id in _sorted_ids(id_values): + domains = _matched_domains(source.get(item_id) or {}) + if len(domains) > 1: + shared.append({"item_type": item_type, "id": item_id, "matched_domain_ids": domains}) + return { + "meeting_clause_ids": _sorted_ids(clause_ids), + "event_candidate_ids": _sorted_ids(event_ids), + "evidence_indexes": _sorted_ids(evidence_ids), + "selected_meetings": [_project_meeting(by_clause[v]) for v in _sorted_ids(clause_ids)], + "selected_events": [_project_event(by_event[v]) for v in _sorted_ids(event_ids)], + "selected_evidence": [_project_evidence(by_evidence[v]) for v in _sorted_ids(evidence_ids)], + "shared": shared, + } + + def _taxonomy_subset(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + taxonomy = stage_a.get("juristic_act_taxonomy_map") or {} + axes = taxonomy.get("axis_groups") or {} + return {"schema_version": taxonomy.get("schema_version"), "categories": taxonomy.get("categories") or {}, "example_to_gubun": taxonomy.get("example_to_gubun") or {}, "axis_groups": {key: axes.get(key) for key in config["taxonomy_axes"] if key in axes}, "source_digest_sha256": taxonomy.get("source_digest_sha256")} + + def _hazard_subset(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + hazard = stage_a.get("global_hazard_seed") or {} + return {key: hazard.get(key) for key in config["hazard_keys"] if key in hazard} + + def _part1_review(stage_a: dict[str, Any], selected: dict[str, Any], config: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]: + handoff = stage_a.get("part1_soft_gate_handoff") or {} + guard = {"schema_version": handoff.get("schema_version"), "status": handoff.get("status"), "stage2_auto_progression_allowed": handoff.get("stage2_auto_progression_allowed"), "usage_rules": handoff.get("usage_rules") or {}} + selected_e = set(selected["evidence_indexes"]) + selected_ev = set(selected["event_candidate_ids"]) + selected_m = set(selected["meeting_clause_ids"]) + relevant: list[dict[str, Any]] = [] + for item in _as_list(handoff.get("review_items_compact")): + refs = item.get("source_refs") if isinstance(item, dict) else {} + domain_text = _blob({"domain_hints": item.get("domain_hints"), "target_task_hints": item.get("target_task_hints"), "issue_type": item.get("issue_type"), "review_instruction": item.get("review_instruction")}) + ref_hit = bool(selected_e.intersection(str(v) for v in _as_list(refs.get("evidence_indexes"))) or selected_ev.intersection(str(v) for v in _as_list(refs.get("event_candidate_ids"))) or selected_m.intersection(str(v) for v in _as_list(refs.get("meeting_clause_refs")))) + if ref_hit or any(str(term).lower() in domain_text for term in config["terms"]): + relevant.append(item) + return guard, relevant + + def _build_slice() -> dict[str, Any]: + root = _parse_json(STAGE_A_RAW, "Task_C_BO_Stage_A_input_normalization") + stage_a = root.get("stage_a_context") + if not isinstance(stage_a, dict): + raise RuntimeError("Stage A output must contain top-level key stage_a_context") + _accept_stage_a(stage_a) + selected = _select(stage_a, DOMAIN_CONFIG) + part1_guard, part1_items = _part1_review(stage_a, selected, DOMAIN_CONFIG) + worker_contract = stage_a.get("stage_b_worker_contract") or {} + meeting_map = stage_a.get("meeting_clause_map") or {} + event_map = stage_a.get("event_candidate_map") or {} + evidence_map = stage_a.get("evidence_authority_map") or {} + return {"stage_b_domain_slice": {"schema_version": "task_c_bo_stage_b_domain_slice.v1", "stage": "Task_C_BO.Stage_B0.DomainInputRouter", "router_task": TASK_NAME, "router_status": "READY", "domain_id": DOMAIN_CONFIG["domain_id"], "domain_label": DOMAIN_CONFIG["domain_label"], "allowed_legal_effect_bo_types": DOMAIN_CONFIG["allowed_legal_effect_bo_types"], "source_stage_a_schema_version": stage_a.get("schema_version"), "source_stage_a_status": stage_a.get("status"), "source_stage_a_created_at_utc": stage_a.get("created_at_utc"), "input_digests_sha256": stage_a.get("input_digests_sha256") or {}, "stage_b_worker_contract": {"context_is_read_only": worker_contract.get("context_is_read_only"), "bo_id_finalization_allowed": worker_contract.get("bo_id_finalization_allowed"), "candidate_generation_allowed": worker_contract.get("candidate_generation_allowed"), "must_use_evidence_index_set": worker_contract.get("must_use_evidence_index_set") or []}, "source_universe": {"meeting_clause_ids": meeting_map.get("clause_order") or [], "event_candidate_ids": event_map.get("candidate_id_set") or [], "evidence_indexes": evidence_map.get("evidence_index_set") or []}, "selected_meeting_clauses": selected["selected_meetings"], "selected_event_candidates": selected["selected_events"], "selected_evidence_authority": selected["selected_evidence"], "juristic_act_taxonomy_subset": _taxonomy_subset(stage_a, DOMAIN_CONFIG), "hazard_subset": _hazard_subset(stage_a, DOMAIN_CONFIG), "part1_review_guard": part1_guard, "part1_review_items_relevant": part1_items, "shared_or_ambiguous_items": selected["shared"], "routing_audit": {"selected_meeting_clause_ids": selected["meeting_clause_ids"], "selected_event_candidate_ids": selected["event_candidate_ids"], "selected_evidence_indexes": selected["evidence_indexes"], "multi_domain_item_refs": selected["shared"], "unrouted_but_preserved_refs": [], "selection_basis": "deterministic keyword/flag/doc_type/domain cue routing with source-id join closure", "field_policy": "Stage A compact authority records are projected; raw meeting/evidence text is not re-read."}}} + + if __name__ == "__main__": + try: + print(json.dumps(_build_slice(), ensure_ascii=False, separators=(",", ":"))) + except Exception as exc: + print(json.dumps({"stage_b_domain_slice": {"schema_version": "task_c_bo_stage_b_domain_slice.v1", "stage": "Task_C_BO.Stage_B0.DomainInputRouter", "router_task": TASK_NAME, "router_status": "FAILED", "domain_id": DOMAIN_CONFIG["domain_id"], "domain_label": DOMAIN_CONFIG["domain_label"], "routing_audit": {"failure": str(exc)}}}, ensure_ascii=False, separators=(",", ":"))) + print(f"[{TASK_NAME}] {exc}", file=sys.stderr) + raise + + - task_name: Task_C_BO_Stage_B0_input_succession_notice_lien_asset_defense + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 60 + code: |- + #!/usr/bin/env python3 + from __future__ import annotations + + import json + import re + import sys + from typing import Any + + TASK_NAME = "Task_C_BO_Stage_B0_input_succession_notice_lien_asset_defense" + STAGE_A_RAW = r"""{{prev.Task_C_BO_Stage_A_input_normalization}}""" + + DOMAIN_CONFIG = { + "domain_id": "B5_Succession_Notice_Lien_Asset_Defense", + "domain_label": "상속, 통지, 유치권, 목적물 특정, 항변", + "allowed_legal_effect_bo_types": [ + "succession_resolution_BO", + "notice_lifecycle_BO", + "lien_state_timeline_BO", + "asset_alias_BO", + "defense_rebuttal_BO", + "delivery_use_gain_claim_group_BO", + ], + "terms": [ + "사망", "상속", "상속포기", "배우자", "직계비속", "가족관계", "피상속인", + "상속인", "통지", "내용증명", "도달", "수령", "최고", "유치권", "점유", + "인도", "거절", "별지목록", "등기부 표시", "목적물", "동일성", "피고 답변", + "항변", "권리남용", "신의칙", "동시이행", "succession", "notice", "lien", + "possession", "asset", "defense", "party_assertion", "registry_object_display", + ], + "meeting_sources": ["succession_clause_ids", "current_state_clause_ids", "conflict_pattern_clause_ids", "mixed_contract_clause_ids"], + "event_sources": ["succession_candidate_ids", "state_candidate_ids"], + "hazard_keys": ["succession_related_candidates", "current_state_expressions", "conflict_pattern_expressions", "asset_clusters", "mixed_contract_candidates"], + "taxonomy_axes": ["structure", "time_effect", "legal_object", "defect_or_condition"], + } + + ALL_DOMAIN_TERMS = { + "B1_Money_Successor": ["대여금", "매매대금", "보증금", "원금", "이자", "변제기", "상호속용", "영업양도", "loan", "payment", "business_transfer"], + "B2_Secured_Registry": ["근저당", "저당", "담보", "피담보", "변제충당", "등기", "말소", "경매", "배당", "registry", "mortgage", "secured", "auction", "dividend"], + "B3_Land_Valuation": ["토지", "건물", "점유", "사용", "수익", "인도", "차임", "감정", "시가", "가액", "valuation", "possession", "land", "building"], + "B4_Actio": ["사해행위", "피보전채권", "채무초과", "책임재산", "공동담보", "처분행위", "수익자", "전득자", "가액배상", "actio", "fraudulent", "valuation", "defense"], + "B5_Succession_Notice_Lien_Asset_Defense": DOMAIN_CONFIG["terms"], + } + + MEETING_FIELDS = ["clause_id", "local_clause_id", "section_key", "section_label", "major_no", "sub_no", "heading", "line_start", "line_end", "text_excerpt", "tags"] + EVENT_FIELDS = ["candidate_id", "source_evidence_index", "title", "doc_type", "event_or_state", "event_kind", "event_subkind", "action_type_candidate", "action_summary", "participants", "object_spec", "amount", "event_date", "event_end_date", "time_text", "time_precision", "current_state_candidate", "succession_candidate", "legal_effect_candidate", "defense_candidate", "commercial_successor_candidates", "meeting_clause_refs", "actio_relevance_candidates", "identity_signature", "property_cluster_id", "affected_asset_cluster_ids", "doc_semantic_flags", "confidence", "fraction_ref"] + EVIDENCE_FIELDS = ["evidence_index", "doc_uid", "title", "title_normalized", "doc_type", "key_facts", "key_dates", "key_amounts", "key_parties", "source_pointer", "property_cluster_id", "doc_semantic_flags", "multi_event_registry", "component_summary", "component_details"] + + def _parse_json(raw: str, label: str) -> dict[str, Any]: + text = (raw or "").strip() + if not text or text == "null": + raise RuntimeError(f"{label}: empty injected JSON") + obj, idx = json.JSONDecoder().raw_decode(text) + if text[idx:].strip(): + raise RuntimeError(f"{label}: trailing data after JSON object") + if not isinstance(obj, dict): + raise RuntimeError(f"{label}: root must be object") + return obj + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _natural_key(value: Any) -> list[Any]: + return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", str(value))] + + def _sorted_ids(values: set[str] | list[str]) -> list[str]: + return sorted([str(v) for v in values if str(v)], key=_natural_key) + + def _blob(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True).lower() + + def _matches(value: Any, terms: list[str]) -> bool: + haystack = _blob(value) + return any(str(term).lower() in haystack for term in terms if str(term).strip()) + + def _copy_fields(source: dict[str, Any], fields: list[str]) -> dict[str, Any]: + return {field: source.get(field) for field in fields if field in source} + + def _project_meeting(record: dict[str, Any]) -> dict[str, Any]: + out = _copy_fields(record, MEETING_FIELDS) + if not out.get("text_excerpt") and record.get("text"): + out["text_excerpt"] = str(record.get("text"))[:240] + return out + + def _project_event(record: dict[str, Any]) -> dict[str, Any]: + return _copy_fields(record, EVENT_FIELDS) + + def _project_evidence(record: dict[str, Any]) -> dict[str, Any]: + return _copy_fields(record, EVIDENCE_FIELDS) + + def _accept_stage_a(stage_a: dict[str, Any]) -> None: + checks = [ + ("stage_a_context.schema_version", stage_a.get("schema_version") == "task_c_bo_stage_a_context.v1"), + ("stage_a_context.status", stage_a.get("status") == "READY"), + ("meeting_clause_map.schema_version", (stage_a.get("meeting_clause_map") or {}).get("schema_version") == "meeting_clause_map.v1"), + ("event_candidate_map.schema_version", (stage_a.get("event_candidate_map") or {}).get("schema_version") == "event_candidate_map.v2"), + ("evidence_authority_map.schema_version", (stage_a.get("evidence_authority_map") or {}).get("schema_version") == "evidence_authority_map.v2"), + ("juristic_act_taxonomy_map.schema_version", (stage_a.get("juristic_act_taxonomy_map") or {}).get("schema_version") == "juristic_act_taxonomy_map.v1"), + ("global_hazard_seed.schema_version", (stage_a.get("global_hazard_seed") or {}).get("schema_version") == "global_hazard_seed.v1"), + ("stage_b_worker_contract.context_is_read_only", (stage_a.get("stage_b_worker_contract") or {}).get("context_is_read_only") is True), + ("stage_b_worker_contract.bo_id_finalization_allowed", (stage_a.get("stage_b_worker_contract") or {}).get("bo_id_finalization_allowed") is False), + ("stage_b_worker_contract.candidate_generation_allowed", (stage_a.get("stage_b_worker_contract") or {}).get("candidate_generation_allowed") is True), + ] + failed = [name for name, ok in checks if not ok] + if failed: + raise RuntimeError(f"Stage A acceptance failed: {failed}") + + def _matched_domains(record: dict[str, Any]) -> list[str]: + return [domain_id for domain_id, terms in ALL_DOMAIN_TERMS.items() if _matches(record, terms)] + + def _select(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + meeting_map = stage_a.get("meeting_clause_map") or {} + event_map = stage_a.get("event_candidate_map") or {} + evidence_map = stage_a.get("evidence_authority_map") or {} + by_clause = meeting_map.get("by_clause_id") or {} + by_event = event_map.get("by_candidate_id") or {} + by_evidence = evidence_map.get("by_evidence_index") or {} + clause_ids: set[str] = set() + event_ids: set[str] = set() + evidence_ids: set[str] = set() + for key in config["meeting_sources"]: + for value in _as_list(meeting_map.get(key)): + value_id = str(value) + if _matches(by_clause.get(value_id) or {}, config["terms"]): + clause_ids.add(value_id) + for key in config["event_sources"]: + for value in _as_list(event_map.get(key)): + value_id = str(value) + if _matches(by_event.get(value_id) or {}, config["terms"]): + event_ids.add(value_id) + for clause_id, record in by_clause.items(): + if _matches(record, config["terms"]): + clause_ids.add(str(clause_id)) + for event_id, record in by_event.items(): + if _matches(record, config["terms"]): + event_ids.add(str(event_id)) + for evidence_id, record in by_evidence.items(): + if _matches(record, config["terms"]): + evidence_ids.add(str(evidence_id)) + for _ in range(2): + for evidence_id in list(evidence_ids): + event_ids.update(str(v) for v in _as_list((event_map.get("by_evidence_index") or {}).get(evidence_id))) + for clause_id in list(clause_ids): + event_ids.update(str(v) for v in _as_list((event_map.get("by_meeting_clause_ref") or {}).get(clause_id))) + for event_id in list(event_ids): + event = by_event.get(event_id) or {} + if event.get("source_evidence_index"): + evidence_ids.add(str(event.get("source_evidence_index"))) + for ref in _as_list(event.get("meeting_clause_refs")): + clause_ids.add(str(ref)) + clause_ids = {v for v in clause_ids if v in by_clause} + event_ids = {v for v in event_ids if v in by_event} + evidence_ids = {v for v in evidence_ids if v in by_evidence} + shared: list[dict[str, Any]] = [] + for item_type, id_values, source in [("meeting_clause", clause_ids, by_clause), ("event_candidate", event_ids, by_event), ("evidence", evidence_ids, by_evidence)]: + for item_id in _sorted_ids(id_values): + domains = _matched_domains(source.get(item_id) or {}) + if len(domains) > 1: + shared.append({"item_type": item_type, "id": item_id, "matched_domain_ids": domains}) + return { + "meeting_clause_ids": _sorted_ids(clause_ids), + "event_candidate_ids": _sorted_ids(event_ids), + "evidence_indexes": _sorted_ids(evidence_ids), + "selected_meetings": [_project_meeting(by_clause[v]) for v in _sorted_ids(clause_ids)], + "selected_events": [_project_event(by_event[v]) for v in _sorted_ids(event_ids)], + "selected_evidence": [_project_evidence(by_evidence[v]) for v in _sorted_ids(evidence_ids)], + "shared": shared, + } + + def _taxonomy_subset(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + taxonomy = stage_a.get("juristic_act_taxonomy_map") or {} + axes = taxonomy.get("axis_groups") or {} + return {"schema_version": taxonomy.get("schema_version"), "categories": taxonomy.get("categories") or {}, "example_to_gubun": taxonomy.get("example_to_gubun") or {}, "axis_groups": {key: axes.get(key) for key in config["taxonomy_axes"] if key in axes}, "source_digest_sha256": taxonomy.get("source_digest_sha256")} + + def _hazard_subset(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + hazard = stage_a.get("global_hazard_seed") or {} + return {key: hazard.get(key) for key in config["hazard_keys"] if key in hazard} + + def _part1_review(stage_a: dict[str, Any], selected: dict[str, Any], config: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]: + handoff = stage_a.get("part1_soft_gate_handoff") or {} + guard = {"schema_version": handoff.get("schema_version"), "status": handoff.get("status"), "stage2_auto_progression_allowed": handoff.get("stage2_auto_progression_allowed"), "usage_rules": handoff.get("usage_rules") or {}} + selected_e = set(selected["evidence_indexes"]) + selected_ev = set(selected["event_candidate_ids"]) + selected_m = set(selected["meeting_clause_ids"]) + relevant: list[dict[str, Any]] = [] + for item in _as_list(handoff.get("review_items_compact")): + refs = item.get("source_refs") if isinstance(item, dict) else {} + domain_text = _blob({"domain_hints": item.get("domain_hints"), "target_task_hints": item.get("target_task_hints"), "issue_type": item.get("issue_type"), "review_instruction": item.get("review_instruction")}) + ref_hit = bool(selected_e.intersection(str(v) for v in _as_list(refs.get("evidence_indexes"))) or selected_ev.intersection(str(v) for v in _as_list(refs.get("event_candidate_ids"))) or selected_m.intersection(str(v) for v in _as_list(refs.get("meeting_clause_refs")))) + if ref_hit or any(str(term).lower() in domain_text for term in config["terms"]): + relevant.append(item) + return guard, relevant + + def _build_slice() -> dict[str, Any]: + root = _parse_json(STAGE_A_RAW, "Task_C_BO_Stage_A_input_normalization") + stage_a = root.get("stage_a_context") + if not isinstance(stage_a, dict): + raise RuntimeError("Stage A output must contain top-level key stage_a_context") + _accept_stage_a(stage_a) + selected = _select(stage_a, DOMAIN_CONFIG) + part1_guard, part1_items = _part1_review(stage_a, selected, DOMAIN_CONFIG) + worker_contract = stage_a.get("stage_b_worker_contract") or {} + meeting_map = stage_a.get("meeting_clause_map") or {} + event_map = stage_a.get("event_candidate_map") or {} + evidence_map = stage_a.get("evidence_authority_map") or {} + return {"stage_b_domain_slice": {"schema_version": "task_c_bo_stage_b_domain_slice.v1", "stage": "Task_C_BO.Stage_B0.DomainInputRouter", "router_task": TASK_NAME, "router_status": "READY", "domain_id": DOMAIN_CONFIG["domain_id"], "domain_label": DOMAIN_CONFIG["domain_label"], "allowed_legal_effect_bo_types": DOMAIN_CONFIG["allowed_legal_effect_bo_types"], "source_stage_a_schema_version": stage_a.get("schema_version"), "source_stage_a_status": stage_a.get("status"), "source_stage_a_created_at_utc": stage_a.get("created_at_utc"), "input_digests_sha256": stage_a.get("input_digests_sha256") or {}, "stage_b_worker_contract": {"context_is_read_only": worker_contract.get("context_is_read_only"), "bo_id_finalization_allowed": worker_contract.get("bo_id_finalization_allowed"), "candidate_generation_allowed": worker_contract.get("candidate_generation_allowed"), "must_use_evidence_index_set": worker_contract.get("must_use_evidence_index_set") or []}, "source_universe": {"meeting_clause_ids": meeting_map.get("clause_order") or [], "event_candidate_ids": event_map.get("candidate_id_set") or [], "evidence_indexes": evidence_map.get("evidence_index_set") or []}, "selected_meeting_clauses": selected["selected_meetings"], "selected_event_candidates": selected["selected_events"], "selected_evidence_authority": selected["selected_evidence"], "juristic_act_taxonomy_subset": _taxonomy_subset(stage_a, DOMAIN_CONFIG), "hazard_subset": _hazard_subset(stage_a, DOMAIN_CONFIG), "part1_review_guard": part1_guard, "part1_review_items_relevant": part1_items, "shared_or_ambiguous_items": selected["shared"], "routing_audit": {"selected_meeting_clause_ids": selected["meeting_clause_ids"], "selected_event_candidate_ids": selected["event_candidate_ids"], "selected_evidence_indexes": selected["evidence_indexes"], "multi_domain_item_refs": selected["shared"], "unrouted_but_preserved_refs": [], "selection_basis": "deterministic keyword/flag/doc_type/domain cue routing with source-id join closure", "field_policy": "Stage A compact authority records are projected; raw meeting/evidence text is not re-read."}}} + + if __name__ == "__main__": + try: + print(json.dumps(_build_slice(), ensure_ascii=False, separators=(",", ":"))) + except Exception as exc: + print(json.dumps({"stage_b_domain_slice": {"schema_version": "task_c_bo_stage_b_domain_slice.v1", "stage": "Task_C_BO.Stage_B0.DomainInputRouter", "router_task": TASK_NAME, "router_status": "FAILED", "domain_id": DOMAIN_CONFIG["domain_id"], "domain_label": DOMAIN_CONFIG["domain_label"], "routing_audit": {"failure": str(exc)}}}, ensure_ascii=False, separators=(",", ":"))) + print(f"[{TASK_NAME}] {exc}", file=sys.stderr) + raise + + - task_name: Task_C_BO_Stage_B_B1_Money_Successor + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: medium + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + {{prev.Task_C_BO_Stage_B0_input_money_successor}} + + + + + - You MUST use only the JSON object in . + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B1_Money_Successor"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B1_Money_Successor + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for money claim, payment, notice, admission, and commercial successor liability. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B1:001`, `B1:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - money claim origin: loan, sale price, deposit, settlement, construction price, unpaid balance + - due date, acceleration, demand, notice, payment deadline, or delayed performance + - partial payment, debt admission, payment memorandum, balance confirmation, account statement, or settlement confirmation + - business transfer, trade-name continuation, transferee liability, debt non-assumption clause, or commercial successor liability + - uncertain creditor/debtor/successor linkage that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use B0 routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the current B0 slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split money claim origin, maturity/demand, payment/admission, and successor liability when they are distinct legal-effect seeds. + 4. Preserve payment history, demand/notice hints, admission hints, successor liability hints, and successor-link uncertainty in `extensions.domain_payload`. Preserve agreed interest rate and agreed delay damage rate as literal strings from B1 `loan_components`/`secured_debt_components` (`component_details` of `selected_evidence_authority`) when present; never substitute statutory rates, and keep the supporting evidence indexes in `interest_rate_source_evidence_refs`. + 5. Use `domain_review_queue` for missing amount/date, uncertain creditor/debtor identity, conflicting payment status, or cross-domain successor merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Keep all unsupported or uncertain values as null or [] and route review needs to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + Return exactly one JSON object: + { + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B1_Money_Successor", + "status": "READY|READY_WITH_REVIEW|FAILED", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B1_Money_Successor", + "domain_label": "금전채권 및 상호속용 책임", + "slice_guard": { + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": "ISO-8601", + "input_digests_sha256": {}, + "source_universe": { + "meeting_clause_ids": [], + "event_candidate_ids": [], + "evidence_indexes": [] + }, + "stage_b_worker_contract": { + "context_is_read_only": true, + "bo_id_finalization_allowed": false, + "candidate_generation_allowed": true + } + } + }, + "transport_candidate_refs": [ + { + "candidate_ref": "B1:001", + "payload_index": 0, + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [] + } + ], + "bo_seed_candidates": [ + { + "candidate_ref": "B1:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "JuristicAct": {"label": "string|null"}, + "Action": "short neutral factual/legal-action draft", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [], + "asset_refs": [], + "legal_effect_summary": null + }, + "amount": { + "value": null, + "currency": "KRW|null", + "basis": null + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": null, + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": {"claim_type_candidate": null, "principal_amount_hint": null, "interest_hint": null, "delay_damage_hint": null, "agreed_interest_rate": null, "agreed_delay_damage_rate": null, "interest_accrual_start_hint": null, "interest_rate_source_evidence_refs": [], "unpaid_balance_hint": null, "creditor_candidate": null, "debtor_candidate": null}, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": "string|null", + "conflict_flags": [] + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B1:review:001", + "severity": "SOFT_WARNING|HARD_WARNING", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "description": "string", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "publisher|domain_join|C0|C1|C2|C3|C5|D|E" + } + ] + } + } + + Status rule: use `FAILED` when any input acceptance or `stage_b_worker_contract` guard fails; use `READY_WITH_REVIEW` when `domain_review_queue` is non-empty and no hard gate failed; otherwise use `READY`. + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + + - task_name: Task_C_BO_Stage_B_B2_Secured_Registry + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: medium + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + {{prev.Task_C_BO_Stage_B0_input_secured_registry}} + + + + + - You MUST use only the JSON object in . + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B2_Secured_Registry"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B2_Secured_Registry + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for secured debt, registry, payment allocation, auction, distribution, and derivative invalidity issues. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B2:001`, `B2:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - secured debt creation, change, repayment, payment allocation, and secured obligation balance + - mortgage, pledge, collateral security, registration, cancellation, correction, or invalidity chain + - auction, distribution, dividend, satisfaction, or residual balance facts + - derivative invalidity, registry error, title defect, or secured property dispute + - uncertain registry sequence or security/right-holder linkage that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use B0 routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the current B0 slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split secured debt chain, registry act, payment allocation, auction step, dividend distribution, and derivative invalidity when they are distinct legal-effect seeds. + 4. Preserve registry rows, secured debt chain, payment allocation, auction step, dividend distribution hints, derivative invalidity hints, and registry display values in `extensions.domain_payload`. Carry `fraction_ref` from source event candidates unchanged; do not merge or renumber. Populate `fractional_effect_events` as an event-reference list only (fraction-bearing setting/extinguishment/transfer events); never fold fraction states or normalize/reduce fractions (that is Stage 2 work). + 5. Use `domain_review_queue` for missing registry date, unclear secured obligation balance, conflicting registration sequence, or cross-domain property merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Keep all unsupported or uncertain values as null or [] and route review needs to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + Return exactly one JSON object: + { + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B2_Secured_Registry", + "status": "READY|READY_WITH_REVIEW|FAILED", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B2_Secured_Registry", + "domain_label": "담보채무, 변제충당, 등기무효, 경매", + "slice_guard": { + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": "ISO-8601", + "input_digests_sha256": {}, + "source_universe": { + "meeting_clause_ids": [], + "event_candidate_ids": [], + "evidence_indexes": [] + }, + "stage_b_worker_contract": { + "context_is_read_only": true, + "bo_id_finalization_allowed": false, + "candidate_generation_allowed": true + } + } + }, + "transport_candidate_refs": [ + { + "candidate_ref": "B2:001", + "payload_index": 0, + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [] + } + ], + "bo_seed_candidates": [ + { + "candidate_ref": "B2:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "JuristicAct": {"label": "string|null"}, + "Action": "short neutral factual/legal-action draft", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [], + "asset_refs": [], + "legal_effect_summary": null + }, + "amount": { + "value": null, + "currency": "KRW|null", + "basis": null + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "title_hint": null, + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "registry_rows": [ + {"registry_ref": null, "fraction_ref": null, "fraction_display": null, "fraction_numerator": null, "fraction_denominator": null, "receipt_no": null} + ], + "fractional_effect_events": [ + {"fraction_ref": null, "fraction_display": null, "fraction_numerator": null, "fraction_denominator": null, "effect_kind_hint": "mortgage_setting|mortgage_extinguishment|transfer|other", "source_event_candidate_ids": [], "registry_refs": [], "evidence_indexes": []} + ], + "secured_debt_chain": [], + "payment_allocation": [], + "mortgage_extinguishment_hints": [], + "auction_steps": [], + "dividend_distribution_hints": [], + "derivative_invalidity_hints": [], + "registry_object_display": "string|null" + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B2:review:001", + "severity": "SOFT_WARNING|HARD_WARNING", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "description": "string", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "publisher|domain_join|C0|C1|C2|C3|C5|D|E" + } + ] + } + } + + Status rule: use `FAILED` when any input acceptance or `stage_b_worker_contract` guard fails; use `READY_WITH_REVIEW` when `domain_review_queue` is non-empty and no hard gate failed; otherwise use `READY`. + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + + - task_name: Task_C_BO_Stage_B_B3_Land_Valuation + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: medium + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + {{prev.Task_C_BO_Stage_B0_input_land_valuation}} + + + + + - You MUST use only the JSON object in . + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B3_Land_Valuation"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B3_Land_Valuation + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for land use gain, possession/use, valuation, and asset-value facts. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B3:001`, `B3:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - land or building possession, use, occupation, benefit, rent-equivalent gain, or loss of use + - valuation fact, appraisal, market value, rent value, area, period, and calculation base + - delivery, possession start/end, unauthorized use, co-occupancy, or liability overlap + - asset identity, address, parcel, unit, area, or other value-bearing object description + - uncertain use period, value basis, possession status, or asset binding that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use B0 routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the current B0 slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split possession/use state, use-gain period, valuation fact, and liability overlap when they are distinct legal-effect seeds. + 4. Preserve valuation rows, valuation matrix, use-gain facts, possession state, asset binding hints, and liability overlap hints in `extensions.domain_payload`. Populate the `possession_state` two-track structure (possession facts vs. asserted legal causes) from selected event candidates only; never resolve which legal cause prevails (that is Stage 2 work); preserve conflicting assertions side by side. + 5. Use `domain_review_queue` for missing period, missing value base, unclear possession state, or cross-domain asset merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Keep all unsupported or uncertain values as null or [] and route review needs to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + Return exactly one JSON object: + { + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B3_Land_Valuation", + "status": "READY|READY_WITH_REVIEW|FAILED", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B3_Land_Valuation", + "domain_label": "토지 사용이익, 점유, 사용수익, 가치평가", + "slice_guard": { + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": "ISO-8601", + "input_digests_sha256": {}, + "source_universe": { + "meeting_clause_ids": [], + "event_candidate_ids": [], + "evidence_indexes": [] + }, + "stage_b_worker_contract": { + "context_is_read_only": true, + "bo_id_finalization_allowed": false, + "candidate_generation_allowed": true + } + } + }, + "transport_candidate_refs": [ + { + "candidate_ref": "B3:001", + "payload_index": 0, + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [] + } + ], + "bo_seed_candidates": [ + { + "candidate_ref": "B3:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "JuristicAct": {"label": "string|null"}, + "Action": "short neutral factual/legal-action draft", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [], + "asset_refs": [], + "legal_effect_summary": null + }, + "amount": { + "value": null, + "currency": "KRW|null", + "basis": null + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "title_hint": null, + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "valuation_rows": [], + "valuation_matrix": [], + "valuation_object": "string|null", + "use_gain_facts": [], + "possession_state": { + "asset_ref": null, + "possession_events": [ + {"possessor_candidates": [], "start_date": null, "end_date": null, "source_event_candidate_ids": [], "evidence_indexes": []} + ], + "legal_cause_assertions": [ + {"cause_asserted": "유치권행사중|임대차|사용대차|물권|동시이행항변|명도후잔류|무권원주장|미상", "asserted_by": null, "assertion_date": null, "source_event_candidate_ids": [], "evidence_indexes": []} + ] + }, + "delivery_or_possession_start_hints": [], + "unregistered_building_hints": [], + "liability_overlap_hints": [] + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B3:review:001", + "severity": "SOFT_WARNING|HARD_WARNING", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "description": "string", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "publisher|domain_join|C0|C1|C2|C3|C5|D|E" + } + ] + } + } + + Status rule: use `FAILED` when any input acceptance or `stage_b_worker_contract` guard fails; use `READY_WITH_REVIEW` when `domain_review_queue` is non-empty and no hard gate failed; otherwise use `READY`. + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + + - task_name: Task_C_BO_Stage_B_B4_Actio + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: medium + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + {{prev.Task_C_BO_Stage_B0_input_actio}} + + + + + - You MUST use only the JSON object in . + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B4_Actio"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B4_Actio + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for actio pauliana, preserved claim, debtor fraudulent act, beneficiary/transferee, remedy, cap, and defenses. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B4:001`, `B4:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - preserved claim existence, claim date, creditor/debtor identity, and claim amount basis + - debtor act, asset decrease, insolvency, common collateral impairment, and harmfulness + - beneficiary, transferee, bad faith/good faith, notice, value compensation, restitution, and remedy cap + - encumbrance, valuation, property transfer, mortgage, sale, gift, collateral provision, or registration act used as fraudulent act basis + - uncertain fraudulent act date, beneficiary chain, asset value, remedy mode, or defense rebuttal that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use B0 routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the current B0 slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split preserved claim, fraudulent transfer, balance-sheet impairment, encumbrance/valuation, beneficiary/transferee state, remedy/cap, and defense rebuttal when they are distinct legal-effect seeds. + 4. Preserve preserved claim bundle, fraudulent transfer facts, balance sheet, encumbrance timeline, valuation matrix, remedy/cap hints, and defense rebuttal in `extensions.domain_payload`. + 5. Use `domain_review_queue` for missing claim date, uncertain fraudulent act date, conflicting value basis, unclear beneficiary chain, or cross-domain asset/claim merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Keep all unsupported or uncertain values as null or [] and route review needs to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + Return exactly one JSON object: + { + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B4_Actio", + "status": "READY|READY_WITH_REVIEW|FAILED", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B4_Actio", + "domain_label": "사해행위취소, 피보전채권, 책임재산, 가액배상", + "slice_guard": { + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": "ISO-8601", + "input_digests_sha256": {}, + "source_universe": { + "meeting_clause_ids": [], + "event_candidate_ids": [], + "evidence_indexes": [] + }, + "stage_b_worker_contract": { + "context_is_read_only": true, + "bo_id_finalization_allowed": false, + "candidate_generation_allowed": true + } + } + }, + "transport_candidate_refs": [ + { + "candidate_ref": "B4:001", + "payload_index": 0, + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [] + } + ], + "bo_seed_candidates": [ + { + "candidate_ref": "B4:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "JuristicAct": {"label": "string|null"}, + "Action": "short neutral factual/legal-action draft", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [], + "asset_refs": [], + "legal_effect_summary": null + }, + "amount": { + "value": null, + "currency": "KRW|null", + "basis": null + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "actio_fraudulent_transfer", + "title_hint": null, + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "preserved_claim_bundle": {}, + "fraudulent_transfer_facts": [], + "debtor_act_hints": [], + "balance_sheet": {"asset_decrease_hints": [], "insolvency_hints": [], "common_collateral_impairment_hints": []}, + "encumbrance_timeline": [], + "valuation_matrix": [], + "remedy_mode_hints": [], + "value_compensation_cap_hints": [], + "beneficiary_transferee_hints": [], + "bad_faith_good_faith_hints": [], + "defense_rebuttal": [] + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B4:review:001", + "severity": "SOFT_WARNING|HARD_WARNING", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "description": "string", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "publisher|domain_join|C0|C1|C2|C3|C5|D|E" + } + ] + } + } + + Status rule: use `FAILED` when any input acceptance or `stage_b_worker_contract` guard fails; use `READY_WITH_REVIEW` when `domain_review_queue` is non-empty and no hard gate failed; otherwise use `READY`. + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + + - task_name: Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: medium + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + {{prev.Task_C_BO_Stage_B0_input_succession_notice_lien_asset_defense}} + + + + + - You MUST use only the JSON object in . + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B5_Succession_Notice_Lien_Asset_Defense"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B5_Succession_Notice_Lien_Asset_Defense + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for succession, notice lifecycle, lien, asset identity, possession, and major defenses. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B5:001`, `B5:002`, ... in deterministic order of the candidate seed list. + - If multiple lien seeds exist, assign `lien_id` values `B5:LIEN:001`, `B5:LIEN:002`, ... ordered by the earliest related event date (ties broken by `candidate_ref` lexicographic order). This id is transport-scope only, not a final BO id. + - Do not produce `violation_candidates` or `status_after_notice_receipt`; route such needs to `domain_review_queue` with `issue_type="legal_theory_required"`. + - `legal_sender_after_resolution.party` must always remain null in Stage 1 (value finalization is Stage 2 legal work); fill only its `status` and `succession_resolution_ref` from directly-read sources. + + + + Create separate seeds when supported for: + - general succession, specific succession, inheritance, assignment, acquisition, transfer, or party-status succession + - notice dispatch, arrival, receipt, refusal, content, timing, and legal effect of notice + - lien, possession, retained object, claim-object connection, asset alias, and asset identity + - simultaneous performance, setoff, payment, prescription, delivery, possession, or other major defense facts + - uncertain notice arrival, lien possession continuity, asset identity, succession scope, or defense linkage that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use B0 routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the current B0 slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split succession state, notice lifecycle, lien/possession state, asset identity, and defense rebuttal when they are distinct legal-effect seeds. + 4. Preserve succession resolution, notice lifecycle, lien state, possession state, asset alias, simultaneous-performance hints, setoff/payment defense hints, and defense rebuttal in `extensions.domain_payload`. Populate the `possession_state` two-track structure (possession facts vs. asserted legal causes) from selected event candidates only; never resolve which legal cause prevails (that is Stage 2 work); preserve conflicting assertions side by side. Assign `lien_id` deterministically and reference it from possession/notice payloads of the same seed group. Inherit `notice_id` from B1 `notice_components[*].notice_id_proposed` (`component_details` of `selected_evidence_authority`); when multiple notices exist, split seeds so each seed carries one notice lifecycle. Inherit `schedule_refs`/`registry_refs` (SCH-*/REG-* keys) and `canonical_label_candidate`/`usable_in_relief` into `asset_alias` unchanged. + 5. Use `domain_review_queue` for missing arrival evidence, uncertain asset identity, unclear possession continuity, conflicting succession scope, or cross-domain defense merge need. When `received_date` cannot be established from selected sources, leave it null and add a `domain_review_queue` item with `issue_type="missing_source"` (do not fabricate); the sealed drafting gate is applied downstream at FL. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Keep all unsupported or uncertain values as null or [] and route review needs to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + Return exactly one JSON object: + { + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B5_Succession_Notice_Lien_Asset_Defense", + "status": "READY|READY_WITH_REVIEW|FAILED", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B5_Succession_Notice_Lien_Asset_Defense", + "domain_label": "상속, 통지, 유치권, 목적물 특정, 항변", + "slice_guard": { + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": "ISO-8601", + "input_digests_sha256": {}, + "source_universe": { + "meeting_clause_ids": [], + "event_candidate_ids": [], + "evidence_indexes": [] + }, + "stage_b_worker_contract": { + "context_is_read_only": true, + "bo_id_finalization_allowed": false, + "candidate_generation_allowed": true + } + } + }, + "transport_candidate_refs": [ + { + "candidate_ref": "B5:001", + "payload_index": 0, + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [] + } + ], + "bo_seed_candidates": [ + { + "candidate_ref": "B5:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "JuristicAct": {"label": "string|null"}, + "Action": "short neutral factual/legal-action draft", + "Reason": null, + "PriorAct": null, + "ReasonRefs": [], + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [], + "asset_refs": [], + "legal_effect_summary": null + }, + "amount": { + "value": null, + "currency": "KRW|null", + "basis": null + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "downstream_seed_refs": { + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "title_hint": null, + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "succession_resolution": {}, + "general_succession_status": {}, + "asset_specific_succession_resolution": {}, + "notice_lifecycle": { + "notice_id": null, + "notice_type": null, + "drafted_date": null, + "dispatch_date": null, + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [], + "legal_sender_after_resolution": {"status": "resolved|unresolved|disputed|not_required", "party": null, "succession_resolution_ref": null}, + "receiver_candidates": [], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + }, + "lien_state": { + "lien_id": null, + "asset_ref": null, + "lien_holder_candidates": [], + "secured_claim_hint": {"debtor_candidate": null, "basis_text": null, "claimed_remaining_amount_hint": null, "evidence_indexes": []}, + "possession_event_refs": [], + "assertion_events": [], + "extinction_notice_refs": [], + "status_raw_hint": null + }, + "possession_state": { + "asset_ref": null, + "possession_events": [ + {"possessor_candidates": [], "start_date": null, "end_date": null, "source_event_candidate_ids": [], "evidence_indexes": []} + ], + "legal_cause_assertions": [ + {"cause_asserted": "유치권행사중|임대차|사용대차|물권|동시이행항변|명도후잔류|무권원주장|미상", "asserted_by": null, "assertion_date": null, "source_event_candidate_ids": [], "evidence_indexes": []} + ] + }, + "asset_alias": [ + {"asset_ref": null, "alias_strings": [], "canonical_label_candidate": null, "usable_in_relief": "yes|no|unknown", "schedule_refs": [], "registry_refs": [{"registry_ref": null, "receipt_no_refs": []}], "source_evidence_indexes": []} + ], + "defense_rebuttal": [], + "simultaneous_performance_hints": [], + "setoff_or_payment_defense_hints": [] + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B5:review:001", + "severity": "SOFT_WARNING|HARD_WARNING", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "description": "string", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "publisher|domain_join|C0|C1|C2|C3|C5|D|E" + } + ] + } + } + + Status rule: use `FAILED` when any input acceptance or `stage_b_worker_contract` guard fails; use `READY_WITH_REVIEW` when `domain_review_queue` is non-empty and no hard gate failed; otherwise use `READY`. + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + + - task_name: Task_C_BO_Stage_B_worker_output_publisher + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 120 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import itertools + import json + import re + import sys + from copy import deepcopy + from datetime import datetime, timezone + from typing import Any + + import httpx + + STAGE_A_RAW = r"""{{prev.Task_C_BO_Stage_A_input_normalization}}""" + SEED_RAW_FIELDS = { + "B1_Money_Successor": r"""{{prev.Task_C_BO_Stage_B_B1_Money_Successor.json_output}}""", + "B2_Secured_Registry": r"""{{prev.Task_C_BO_Stage_B_B2_Secured_Registry.json_output}}""", + "B3_Land_Valuation": r"""{{prev.Task_C_BO_Stage_B_B3_Land_Valuation.json_output}}""", + "B4_Actio": r"""{{prev.Task_C_BO_Stage_B_B4_Actio.json_output}}""", + "B5_Succession_Notice_Lien_Asset_Defense": r"""{{prev.Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense.json_output}}""", + } + SEED_DOCS = { + "B1_Money_Successor": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B1_Money_Successor.json", + "B2_Secured_Registry": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B2_Secured_Registry.json", + "B3_Land_Valuation": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B3_Land_Valuation.json", + "B4_Actio": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B4_Actio.json", + "B5_Succession_Notice_Lien_Asset_Defense": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B5_Succession_Notice_Lien_Asset_Defense.json", + } + SEED_TASK_NAMES = { + "B1_Money_Successor": "Task_C_BO_Stage_B_B1_Money_Successor", + "B2_Secured_Registry": "Task_C_BO_Stage_B_B2_Secured_Registry", + "B3_Land_Valuation": "Task_C_BO_Stage_B_B3_Land_Valuation", + "B4_Actio": "Task_C_BO_Stage_B_B4_Actio", + "B5_Succession_Notice_Lien_Asset_Defense": "Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense", + } + DOMAIN_PREFIX = { + "B1_Money_Successor": "B1", + "B2_Secured_Registry": "B2", + "B3_Land_Valuation": "B3", + "B4_Actio": "B4", + "B5_Succession_Notice_Lien_Asset_Defense": "B5", + } + CANDIDATE_REF_RE = re.compile(r"^B[1-5]:[0-9]{3}$") + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-c-bo-stage-b-seed-output-publisher", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + + def _write_doc(doc_name: str, content: str, msg_id: int) -> None: + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=msg_id) + + + def _preview(raw: str, limit: int = 160) -> str: + return (raw or "").strip().replace("\n", "\\n")[:limit] + + + def _extract_first_json_object(raw: str, label: str) -> tuple[dict[str, Any], str]: + text = (raw or "").strip() + if not text or text == "null" or text.startswith("{{"): + raise ValueError(f"{label}: no injected JSON body; preview={_preview(raw)!r}") + start = text.find("{") + if start < 0: + raise ValueError(f"{label}: JSON object start not found; preview={_preview(raw)!r}") + candidate = text[start:].strip() + obj, end = JSON_DECODER.raw_decode(candidate) + if not isinstance(obj, dict): + raise ValueError(f"{label}: JSON root must be object") + return obj, candidate[end:].strip() + + + def _parse_complete_json_object(raw: str, label: str) -> dict[str, Any]: + obj, tail = _extract_first_json_object(raw, label) + if tail: + raise ValueError(f"{label}: non-json tail after object; preview={_preview(tail)!r}") + return obj + + + def _unwrap_code_executor_envelope(obj: dict[str, Any], label: str) -> dict[str, Any]: + if obj.get("type") == "text" and isinstance(obj.get("text"), str): + obj = _parse_complete_json_object(obj["text"], f"{label}.text") + if obj.get("success") is True and isinstance(obj.get("output"), str): + obj = _parse_complete_json_object(obj["output"], f"{label}.output") + if not isinstance(obj, dict): + raise ValueError(f"{label}: unwrapped JSON root must be object") + return obj + + + def unwrap(raw: str, label: str) -> dict[str, Any]: + return _unwrap_code_executor_envelope(_parse_complete_json_object(raw, label), label) + + + def _accept_stage_a(raw: str) -> dict[str, Any]: + outer = unwrap(raw, "STAGE_A_RAW") + stage_a = outer.get("stage_a_context") + if not isinstance(stage_a, dict): + raise ValueError("Stage A context missing") + if stage_a.get("schema_version") != "task_c_bo_stage_a_context.v1": + raise ValueError("Stage A schema_version mismatch") + if stage_a.get("status") != "READY": + raise ValueError("Stage A status is not READY") + if not isinstance(stage_a.get("created_at_utc"), str) or not stage_a.get("created_at_utc"): + raise ValueError("Stage A created_at_utc missing") + if not isinstance(stage_a.get("input_digests_sha256"), dict): + raise ValueError("Stage A input_digests_sha256 missing or not object") + return stage_a + + + def _require_bool(obj: dict[str, Any], key: str, expected: bool, label: str) -> None: + if obj.get(key) is not expected: + raise ValueError(f"{label}.{key} must be {expected}") + + + def _list_or_fail(value: Any, label: str) -> list[Any]: + if not isinstance(value, list): + raise ValueError(f"{label} must be list") + return value + + + def _validate_seed_candidate(candidate: dict[str, Any], domain_id: str, idx: int) -> None: + prefix = DOMAIN_PREFIX[domain_id] + candidate_ref = candidate.get("candidate_ref") + if not isinstance(candidate_ref, str) or not CANDIDATE_REF_RE.fullmatch(candidate_ref): + raise ValueError(f"{domain_id}.bo_seed_candidates[{idx}].candidate_ref invalid") + if not candidate_ref.startswith(prefix + ":"): + raise ValueError(f"{domain_id}.bo_seed_candidates[{idx}].candidate_ref prefix mismatch") + for forbidden_key in ("BO_ID", "id", "Evidence", "EvidenceTitles"): + if forbidden_key in candidate: + raise ValueError(f"{domain_id}.{candidate_ref}: final field {forbidden_key} is prohibited") + if candidate.get("Reason") is not None: + raise ValueError(f"{domain_id}.{candidate_ref}: Reason must be null") + if candidate.get("PriorAct") is not None: + raise ValueError(f"{domain_id}.{candidate_ref}: PriorAct must be null") + if candidate.get("ReasonRefs") != []: + raise ValueError(f"{domain_id}.{candidate_ref}: ReasonRefs must be []") + _list_or_fail(candidate.get("source_evidence_indexes", []), f"{domain_id}.{candidate_ref}.source_evidence_indexes") + provenance = candidate.get("provenance") + if not isinstance(provenance, dict): + raise ValueError(f"{domain_id}.{candidate_ref}.provenance must be object") + _list_or_fail(provenance.get("source_event_candidate_ids", []), f"{domain_id}.{candidate_ref}.provenance.source_event_candidate_ids") + _list_or_fail(provenance.get("source_meeting_clause_ids", []), f"{domain_id}.{candidate_ref}.provenance.source_meeting_clause_ids") + _list_or_fail(provenance.get("source_evidence_indexes", []), f"{domain_id}.{candidate_ref}.provenance.source_evidence_indexes") + + + def _strict_direct_seed_json(raw: str, domain_id: str, stage_a: dict[str, Any]) -> dict[str, Any]: + outer = unwrap(raw, domain_id) + seed_obj = outer.get("stage_b_domain_bo_seed_output") + if not isinstance(seed_obj, dict): + raise ValueError(f"{domain_id}: top-level stage_b_domain_bo_seed_output object missing") + if seed_obj.get("schema_version") != "task_c_bo_stage_b_domain_bo_seed.v2": + raise ValueError(f"{domain_id}: seed schema_version mismatch") + if seed_obj.get("domain_id") != domain_id: + raise ValueError(f"{domain_id}: seed domain_id mismatch {seed_obj.get('domain_id')!r}") + if seed_obj.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError(f"{domain_id}: seed status must be READY or READY_WITH_REVIEW") + + metadata = seed_obj.get("transport_metadata") + if not isinstance(metadata, dict): + raise ValueError(f"{domain_id}: transport_metadata must be object") + slice_guard = metadata.get("slice_guard") + if not isinstance(slice_guard, dict): + raise ValueError(f"{domain_id}: transport_metadata.slice_guard must be object") + if slice_guard.get("source_stage_a_schema_version") != "task_c_bo_stage_a_context.v1": + raise ValueError(f"{domain_id}: slice_guard Stage A schema mismatch") + if slice_guard.get("source_stage_a_status") != "READY": + raise ValueError(f"{domain_id}: slice_guard Stage A status mismatch") + if slice_guard.get("source_stage_a_created_at_utc") != stage_a.get("created_at_utc"): + raise ValueError(f"{domain_id}: stale seed output: Stage A created_at_utc mismatch") + if slice_guard.get("input_digests_sha256") != stage_a.get("input_digests_sha256"): + raise ValueError(f"{domain_id}: stale seed output: input digest mismatch") + source_universe = slice_guard.get("source_universe") + if not isinstance(source_universe, dict): + raise ValueError(f"{domain_id}: slice_guard.source_universe must be object") + for key in ("meeting_clause_ids", "event_candidate_ids", "evidence_indexes"): + _list_or_fail(source_universe.get(key, []), f"{domain_id}.slice_guard.source_universe.{key}") + worker_contract = slice_guard.get("stage_b_worker_contract") + if not isinstance(worker_contract, dict): + raise ValueError(f"{domain_id}: slice_guard.stage_b_worker_contract must be object") + _require_bool(worker_contract, "context_is_read_only", True, f"{domain_id}.stage_b_worker_contract") + _require_bool(worker_contract, "bo_id_finalization_allowed", False, f"{domain_id}.stage_b_worker_contract") + _require_bool(worker_contract, "candidate_generation_allowed", True, f"{domain_id}.stage_b_worker_contract") + + candidates = _list_or_fail(seed_obj.get("bo_seed_candidates"), f"{domain_id}.bo_seed_candidates") + for idx, candidate in enumerate(candidates): + if not isinstance(candidate, dict): + raise ValueError(f"{domain_id}.bo_seed_candidates[{idx}] must be object") + _validate_seed_candidate(candidate, domain_id, idx) + _list_or_fail(seed_obj.get("domain_review_queue", []), f"{domain_id}.domain_review_queue") + return outer + + + def _print_result(status: str, published: list[dict[str, Any]] | None = None, failure: dict[str, Any] | None = None) -> None: + print(json.dumps({ + "stage_b_seed_output_publish_result": { + "schema_version": "task_c_bo_stage_b_seed_output_publish_result.v1", + "stage": "Task_C_BO.Stage_B.SeedOutputPublisher", + "status": status, + "created_at_utc": datetime.now(timezone.utc).isoformat(), + "input_contract": { + "source": "direct_prev_task_json_output", + "expected_root": "stage_b_domain_bo_seed_output", + "expected_schema": "task_c_bo_stage_b_domain_bo_seed.v2", + "strict_single_json_object_required": True, + }, + "published_artifacts": published or [], + "failure": failure, + "prohibited_finalization_attested": { + "legal_judgment_performed": False, + "candidate_content_modified": False, + "bo_json_written": False, + "signal_files_written": False, + "handoff_files_written": bool(published), + }, + } + }, ensure_ascii=False)) + sys.exit(0) + + + def main() -> None: + try: + stage_a = _accept_stage_a(STAGE_A_RAW) + seed_outers: dict[str, dict[str, Any]] = {} + for domain_id, raw in SEED_RAW_FIELDS.items(): + seed_outers[domain_id] = _strict_direct_seed_json(raw, domain_id, stage_a) + + _init_mcp_session() + published: list[dict[str, Any]] = [] + for domain_id, outer in seed_outers.items(): + doc_name = SEED_DOCS[domain_id] + seed_obj = deepcopy(outer["stage_b_domain_bo_seed_output"]) + seed_obj["handoff_guard"] = { + "schema_version": "task_c_bo_stage_b_seed_handoff_guard.v1", + "handoff_target": doc_name, + "publisher_task": "Task_C_BO_Stage_B_worker_output_publisher", + "source_worker_task": SEED_TASK_NAMES[domain_id], + "source_task": domain_id, + "source_contract": "direct_prev_task_json_output.stage_b_domain_bo_seed_output", + "source_prev_path": f"prev.{SEED_TASK_NAMES[domain_id]}.json_output", + "domain_id": domain_id, + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": stage_a.get("created_at_utc"), + "input_digests_sha256": stage_a.get("input_digests_sha256"), + } + content = json.dumps({"stage_b_domain_bo_seed_output": seed_obj}, ensure_ascii=False, indent=2) + _write_doc(doc_name, content, _next_msg_id()) + published.append({ + "domain_id": domain_id, + "path": doc_name, + "source_task": SEED_TASK_NAMES[domain_id], + "source_contract": "direct_prev_task_json_output.stage_b_domain_bo_seed_output", + "status": seed_obj.get("status"), + }) + _print_result("READY", published=published) + except Exception as exc: + _print_result("FAILED", failure={"failure_step": "seed_output_publish", "failure_reason": str(exc)}) + + + if __name__ == "__main__": + main() + + - task_name: Task_C_BO_Stage_B_domain_join + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 120 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import itertools + import json + import re + import sys + from datetime import datetime, timezone + from typing import Any + + import httpx + + STAGE_A_RAW = r'''{{prev.Task_C_BO_Stage_A_input_normalization}}''' + PUBLISHER_RAW = r'''{{prev.Task_C_BO_Stage_B_worker_output_publisher}}''' + SEED_DOCS = { + "B1_Money_Successor": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B1_Money_Successor.json", + "B2_Secured_Registry": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B2_Secured_Registry.json", + "B3_Land_Valuation": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B3_Land_Valuation.json", + "B4_Actio": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B4_Actio.json", + "B5_Succession_Notice_Lien_Asset_Defense": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B5_Succession_Notice_Lien_Asset_Defense.json", + } + SEED_TASK_NAMES = { + "B1_Money_Successor": "Task_C_BO_Stage_B_B1_Money_Successor", + "B2_Secured_Registry": "Task_C_BO_Stage_B_B2_Secured_Registry", + "B3_Land_Valuation": "Task_C_BO_Stage_B_B3_Land_Valuation", + "B4_Actio": "Task_C_BO_Stage_B_B4_Actio", + "B5_Succession_Notice_Lien_Asset_Defense": "Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense", + } + DOMAIN_PREFIX = { + "B1_Money_Successor": "B1", + "B2_Secured_Registry": "B2", + "B3_Land_Valuation": "B3", + "B4_Actio": "B4", + "B5_Succession_Notice_Lien_Asset_Defense": "B5", + } + REQUIRED_DOMAINS = list(SEED_DOCS.keys()) + REQUIRED_STAGE_C_ITEMS = [ + "cross_domain_merge_and_dedup", + "timeline_sort", + "bh_id_assignment", + "PriorAct", + "Reason", + "ReasonRefs", + "superseded_by_bo_id", + "cross_domain_state_supersession", + "cross_domain_conflict_resolution", + "prop_id_to_bh_id_mapping", + ] + CANDIDATE_REF_RE = re.compile(r"^(B[1-5]):([0-9]{3})$") + PROP_RE = re.compile(r"^prop_B[1-5]_[0-9]{3}$") + JSON_DECODER = json.JSONDecoder() + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + LOCALDOCS_SESSION_INITIALIZED = False + SEED_ARTIFACTS_READ: list[str] = [] + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _init_mcp_session() -> None: + global LOCALDOCS_SESSION_INITIALIZED + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-c-bo-stage-b-seed-domain-join", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + LOCALDOCS_SESSION_INITIALIZED = True + + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result["result"].get("content", []) + text = content[0].get("text", "") if content else "" + try: + parsed = json.loads(text) + except json.JSONDecodeError as exc: + raise RuntimeError(f"Invalid read_docs JSON envelope for {doc_name}: {text[:300]}") from exc + if isinstance(parsed, dict) and "results" in parsed: + results = parsed.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + first = results[0] + if first.get("error"): + raise RuntimeError(f"{doc_name}: {first['error']}") + inner = first.get("content", "") + if not inner: + raise RuntimeError(f"Empty content for {doc_name}") + if isinstance(inner, str): + try: + return json.loads(inner) + except json.JSONDecodeError: + try: + obj, end = JSON_DECODER.raw_decode(inner.strip()) + tail = inner.strip()[end:].strip() + if tail: + raise RuntimeError(f"Content of {doc_name} has non-json tail after object: {tail[:160]}") + return obj + except json.JSONDecodeError as exc: + raise RuntimeError(f"Content of {doc_name} is not valid JSON: {inner[:300]}") from exc + return inner + return parsed + + + def _read_required_json_doc(doc_name: str, msg_id: int) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=msg_id) + parsed = _extract_json_from_read_docs_result(result, doc_name) + SEED_ARTIFACTS_READ.append(doc_name) + return parsed + + + def _preview(raw: str, limit: int = 160) -> str: + return (raw or "").strip().replace("\n", "\\n")[:limit] + + + def _extract_first_json_object(raw: str, label: str) -> tuple[dict[str, Any], str]: + text = (raw or "").strip() + if not text or text.startswith("{{"): + raise ValueError(f"{label}: missing injected upstream JSON") + start = text.find("{") + if start < 0: + raise ValueError(f"{label}: upstream JSON object start not found; preview={_preview(raw)!r}") + candidate = text[start:].strip() + obj, end = JSON_DECODER.raw_decode(candidate) + if not isinstance(obj, dict): + raise ValueError(f"{label}: upstream JSON root must be object") + return obj, candidate[end:].strip() + + + def _parse_json_object_text(raw: str, label: str) -> dict[str, Any]: + obj, tail = _extract_first_json_object(raw, label) + if tail: + raise ValueError(f"{label}: non-json tail after object; preview={_preview(tail)!r}") + return obj + + + def _unwrap_code_executor_envelope(obj: dict[str, Any], label: str) -> dict[str, Any]: + if obj.get("type") == "text" and isinstance(obj.get("text"), str): + obj = _parse_json_object_text(obj["text"], f"{label}.text") + if obj.get("success") is True and isinstance(obj.get("output"), str): + obj = _parse_json_object_text(obj["output"], f"{label}.output") + if not isinstance(obj, dict): + raise ValueError(f"{label}: unwrapped upstream JSON root must be object") + return obj + + + def unwrap(raw: str, label: str) -> dict[str, Any]: + return _unwrap_code_executor_envelope(_parse_json_object_text(raw, label), label) + + + def _empty_candidate_index() -> dict[str, Any]: + return { + "all_prop_ids": [], + "by_domain": {domain_id: [] for domain_id in REQUIRED_DOMAINS}, + "by_source_event_candidate_id": {}, + "by_source_evidence_index": {}, + "by_meeting_clause_id": {}, + "by_asset_cluster_id_candidate": {}, + "by_legal_effect_bo_type_candidate": {}, + } + + + def _base_stage_c_handoff() -> dict[str, Any]: + return { + "must_finalize_in_stage_c": REQUIRED_STAGE_C_ITEMS, + "merge_review_queue": [], + "state_supersession_queue": [], + "high_risk_hazard_queue": [], + "cross_domain_duplicate_warning_queue": [], + "asset_cluster_review_queue": [], + "legal_effect_routing_review_queue": [], + "part1_review_queue": [], + } + + + def fail(step: str, reason: str, fields: list[Any] | None = None) -> None: + print(json.dumps({ + "stage_b_output": { + "schema_version": "task_c_bo_stage_b_proposals.v1", + "stage": "Task_C_BO.Stage_B", + "status": "FAILED", + "failure": {"failure_step": step, "failure_reason": reason, "missing_or_invalid_fields": fields or []}, + "domain_outputs": {}, + "candidate_index": _empty_candidate_index(), + "stage_c_handoff": _base_stage_c_handoff(), + "prohibited_finalization_attested": { + "tools_used": LOCALDOCS_SESSION_INITIALIZED or bool(SEED_ARTIFACTS_READ), + "files_read": bool(SEED_ARTIFACTS_READ), + "files_written": False, + "bo_json_written": False, + "signal_files_written": False, + "bo_id_finalized": False, + "bh_id_used": False, + "cross_domain_merge_finalized": False, + "final_prioract_reason_finalized": False, + "final_superseded_by_bo_id_finalized": False, + "final_evidence_objects_created": False, + "final_evidence_titles_created": False, + "final_actiontype_juristicact_finalized": False, + }, + "stage_b_artifact_handoff_audit": { + "schema_version": "task_c_bo_stage_b_seed_artifact_handoff_audit.v1", + "handoff_artifact_count": len(SEED_DOCS), + "handoff_artifacts_read": SEED_ARTIFACTS_READ, + "all_handoff_guards_matched_stage_a": False, + "seed_json_loaded_from_prev": False, + }, + } + }, ensure_ascii=False)) + sys.exit(0) + + + def _as_set(value: Any) -> set[str]: + return {str(v) for v in value} if isinstance(value, list) else set() + + + def _stage_a_universe(stage_a: dict[str, Any]) -> dict[str, set[str]]: + return { + "event_candidate_ids": _as_set(((stage_a.get("event_candidate_map") or {}).get("candidate_id_set") or [])), + "evidence_indexes": _as_set(((stage_a.get("evidence_authority_map") or {}).get("evidence_index_set") or [])), + "meeting_clause_ids": _as_set(((stage_a.get("meeting_clause_map") or {}).get("clause_order") or [])), + } + + + # soft normalize: bad id 인용은 fail 대신 strip + warning 으로 흡수. + # LLM 이 자기 slice 외 universe id 를 인용한 경우 그 id 만 떨어내고 candidate 자체는 유지. + _SOURCE_MEMBERSHIP_WARNINGS: list[dict[str, Any]] = [] + def _validate_source_subset(values: list[Any], allowed: set[str], label: str) -> list[Any]: + bad = [str(v) for v in values if str(v) not in allowed] + if bad: + _SOURCE_MEMBERSHIP_WARNINGS.append({"label": label, "stripped_ids": bad}) + return [v for v in values if str(v) in allowed] + return list(values) + + + def _source_values(candidate: dict[str, Any]) -> tuple[list[Any], list[Any], list[Any]]: + provenance = candidate.get("provenance") if isinstance(candidate.get("provenance"), dict) else {} + event_ids = provenance.get("source_event_candidate_ids") or [] + meeting_ids = provenance.get("source_meeting_clause_ids") or [] + evidence_ids = candidate.get("source_evidence_indexes") or provenance.get("source_evidence_indexes") or [] + return event_ids, meeting_ids, evidence_ids + + + def _candidate_ref_to_prop_id(candidate_ref: str, domain_id: str) -> str: + match = CANDIDATE_REF_RE.fullmatch(candidate_ref or "") + if not match: + fail("candidate_ref_mapping", "candidate_ref pattern mismatch", [candidate_ref]) + prefix, ordinal = match.groups() + if prefix != DOMAIN_PREFIX[domain_id]: + fail("candidate_ref_mapping", "candidate_ref prefix does not match domain", [domain_id, candidate_ref]) + return f"prop_{prefix}_{ordinal}" + + + def _project_seed_candidate(seed_candidate: dict[str, Any], prop_id: str) -> dict[str, Any]: + provenance = seed_candidate.get("provenance") if isinstance(seed_candidate.get("provenance"), dict) else {} + payload = { + "prop_id": prop_id, + "source_contract": "stage_b_domain_bo_seed_output.v1", + "source_candidate_ref": seed_candidate.get("candidate_ref"), + "BOType": seed_candidate.get("BOType"), + "ActionType": seed_candidate.get("ActionType"), + "JuristicAct": seed_candidate.get("JuristicAct"), + "Action": seed_candidate.get("Action"), + "Reason": seed_candidate.get("Reason"), + "PriorAct": seed_candidate.get("PriorAct"), + "ReasonRefs": seed_candidate.get("ReasonRefs") or [], + "Legal_Keywords": seed_candidate.get("Legal_Keywords") or [], + "core_field_base": seed_candidate.get("core_field_base") or {}, + "amount": seed_candidate.get("amount"), + "source_evidence_indexes": seed_candidate.get("source_evidence_indexes") or provenance.get("source_evidence_indexes") or [], + "source_event_candidate_ids": provenance.get("source_event_candidate_ids") or [], + "source_meeting_clause_ids": provenance.get("source_meeting_clause_ids") or [], + "provenance": provenance, + "downstream_seed_refs": seed_candidate.get("downstream_seed_refs") or {}, + "extensions": seed_candidate.get("extensions") or {}, + } + domain_payload = ((payload.get("extensions") or {}).get("domain_payload") or {}) if isinstance(payload.get("extensions"), dict) else {} + downstream_refs = payload.get("downstream_seed_refs") if isinstance(payload.get("downstream_seed_refs"), dict) else {} + asset_hint = downstream_refs.get("asset_cluster_seed") or domain_payload.get("asset_binding_hint") + legal_hint = downstream_refs.get("legal_effect_structure_seed") or domain_payload.get("domain_legal_effect_tag") + if asset_hint: + payload["AssetClusterId_candidate"] = asset_hint + if legal_hint: + payload["legal_effect_bo_type_candidate"] = legal_hint + return payload + + + def _add_index(index: dict[str, Any], key: str, subkey: Any, prop_id: str) -> None: + if subkey in (None, ""): + return + index.setdefault(key, {}).setdefault(str(subkey), []).append(prop_id) + + + def _validate_seed_handoff_guard(seed_obj: dict[str, Any], domain_id: str, doc_name: str, stage_a: dict[str, Any]) -> None: + guard = seed_obj.get("handoff_guard") + if not isinstance(guard, dict): + fail("seed_handoff_guard", f"{domain_id} handoff_guard missing", [doc_name]) + expected = { + "schema_version": "task_c_bo_stage_b_seed_handoff_guard.v1", + "handoff_target": doc_name, + "publisher_task": "Task_C_BO_Stage_B_worker_output_publisher", + "source_worker_task": SEED_TASK_NAMES[domain_id], + "source_contract": "direct_prev_task_json_output.stage_b_domain_bo_seed_output", + "domain_id": domain_id, + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + } + for key, expected_value in expected.items(): + if guard.get(key) != expected_value: + fail("seed_handoff_guard", f"{domain_id} guard {key} mismatch", [doc_name, guard.get(key)]) + if guard.get("source_stage_a_created_at_utc") != stage_a.get("created_at_utc"): + fail("seed_handoff_guard", f"{domain_id} stale seed artifact: Stage A created_at_utc mismatch", [doc_name]) + if guard.get("input_digests_sha256") != stage_a.get("input_digests_sha256"): + fail("seed_handoff_guard", f"{domain_id} stale seed artifact: input digest mismatch", [doc_name]) + + + def _read_seed_doc(doc_name: str, domain_id: str, stage_a: dict[str, Any]) -> dict[str, Any]: + try: + outer = _read_required_json_doc(doc_name, _next_msg_id()) + except Exception as exc: + fail("seed_artifact_acceptance", f"{domain_id} seed artifact read_docs failed: {exc}", [doc_name]) + if not isinstance(outer, dict): + fail("seed_artifact_acceptance", f"{domain_id} seed artifact root must be object", [doc_name]) + seed_obj = outer.get("stage_b_domain_bo_seed_output") + if not isinstance(seed_obj, dict): + fail("seed_artifact_acceptance", f"{domain_id} seed artifact has no stage_b_domain_bo_seed_output", [doc_name]) + _validate_seed_handoff_guard(seed_obj, domain_id, doc_name, stage_a) + if seed_obj.get("schema_version") != "task_c_bo_stage_b_domain_bo_seed.v2": + fail("seed_artifact_acceptance", f"{domain_id} seed schema mismatch", [doc_name]) + if seed_obj.get("domain_id") != domain_id: + fail("seed_artifact_acceptance", f"{domain_id} seed domain_id mismatch", [doc_name]) + if seed_obj.get("status") not in {"READY", "READY_WITH_REVIEW"}: + fail("seed_artifact_acceptance", f"{domain_id} seed status not ready", [str(seed_obj.get("status"))]) + if not isinstance(seed_obj.get("bo_seed_candidates"), list): + fail("seed_artifact_acceptance", f"{domain_id} bo_seed_candidates must be list", [doc_name]) + if not isinstance(seed_obj.get("domain_review_queue", []), list): + fail("seed_artifact_acceptance", f"{domain_id} domain_review_queue must be list", [doc_name]) + return seed_obj + + + def _build_domain_output(seed_obj: dict[str, Any], projected_candidates: list[dict[str, Any]], selected: dict[str, set[str]]) -> dict[str, Any]: + metadata = seed_obj.get("transport_metadata") if isinstance(seed_obj.get("transport_metadata"), dict) else {} + return { + "domain_id": seed_obj.get("domain_id"), + "domain_label": metadata.get("domain_label") or seed_obj.get("domain_id"), + "allowed_legal_effect_bo_types": [], + "coverage_inventory": { + "meeting_clause_ids_reviewed": sorted(selected["meeting_clause_ids"]), + "meeting_clause_ids_selected": sorted(selected["meeting_clause_ids"]), + "meeting_clause_ids_no_candidate": [], + "event_candidate_ids_reviewed": sorted(selected["event_candidate_ids"]), + "event_candidate_ids_selected": sorted(selected["event_candidate_ids"]), + "evidence_indexes_touched": sorted(selected["evidence_indexes"]), + "zero_event_evidence_indexes_considered_for_hint_only": [], + }, + "provisional_bo_candidates": projected_candidates, + "domain_self_check": { + "source_contract": "stage_b_domain_bo_seed_output.v1", + "seed_candidate_count": len(projected_candidates), + "prop_id_mapping_generated_by_domain_join": True, + }, + "domain_stage_c_review_queue": [], + } + + + def _route_review_queue(seed_obj: dict[str, Any], domain_id: str, stage_c_handoff: dict[str, Any], domain_output: dict[str, Any]) -> None: + for idx, item in enumerate(seed_obj.get("domain_review_queue") or []): + if not isinstance(item, dict): + row = {"queue_source": f"seed_outputs.{domain_id}.domain_review_queue", "queue_index": idx, "domain_id": domain_id, "queue_item": item} + else: + row = {"queue_source": f"seed_outputs.{domain_id}.domain_review_queue", "queue_index": idx, "domain_id": domain_id, "queue_item": item} + domain_output["domain_stage_c_review_queue"].append(row) + issue_type = item.get("issue_type") if isinstance(item, dict) else None + if issue_type == "cross_domain_merge_needed": + stage_c_handoff["merge_review_queue"].append(row) + elif issue_type == "legal_effect_uncertain": + stage_c_handoff["legal_effect_routing_review_queue"].append(row) + elif issue_type == "source_conflict": + stage_c_handoff["cross_domain_duplicate_warning_queue"].append(row) + elif issue_type in {"amount_or_date_uncertain", "missing_source"}: + stage_c_handoff["merge_review_queue"].append(row) + + + def _accept_stage_a(raw: str) -> dict[str, Any]: + outer = unwrap(raw, "STAGE_A_RAW") + stage_a = outer.get("stage_a_context") + if not isinstance(stage_a, dict): + fail("acceptance_stage_a", "Stage A context missing") + if stage_a.get("schema_version") != "task_c_bo_stage_a_context.v1" or stage_a.get("status") != "READY": + fail("acceptance_stage_a", "Stage A context not READY or wrong schema") + if not isinstance(stage_a.get("input_digests_sha256"), dict): + fail("acceptance_stage_a", "Stage A input_digests_sha256 missing") + return stage_a + + + def _accept_publisher(raw: str) -> dict[str, Any]: + outer = unwrap(raw, "PUBLISHER_RAW") + result = outer.get("stage_b_seed_output_publish_result") + if not isinstance(result, dict): + fail("seed_output_publisher", "Publisher output missing stage_b_seed_output_publish_result") + if result.get("schema_version") != "task_c_bo_stage_b_seed_output_publish_result.v1": + fail("seed_output_publisher", "Publisher schema mismatch") + if result.get("status") != "READY": + fail("seed_output_publisher", "Publisher did not produce READY seed handoff files", [json.dumps(result.get("failure") or {}, ensure_ascii=False)[:800]]) + contract = result.get("input_contract") + if not isinstance(contract, dict): + fail("seed_output_publisher", "Publisher input_contract missing") + if contract.get("expected_root") != "stage_b_domain_bo_seed_output": + fail("seed_output_publisher", "Publisher expected_root mismatch") + if contract.get("expected_schema") != "task_c_bo_stage_b_domain_bo_seed.v2": + fail("seed_output_publisher", "Publisher expected_schema mismatch") + expected_paths = list(SEED_DOCS.values()) + published = [row for row in (result.get("published_artifacts") or []) if isinstance(row, dict)] + published_paths = [row.get("path") for row in published] + if published_paths != expected_paths: + fail("seed_output_publisher", "Publisher artifact path set mismatch", published_paths) + published_contracts = [row.get("source_contract") for row in published] + if published_contracts != ["direct_prev_task_json_output.stage_b_domain_bo_seed_output"] * len(SEED_DOCS): + fail("seed_output_publisher", "Publisher artifact source_contract mismatch", published_contracts) + return result + + + def main() -> None: + try: + stage_a = _accept_stage_a(STAGE_A_RAW) + handoff = stage_a.get("part1_soft_gate_handoff") if isinstance(stage_a.get("part1_soft_gate_handoff"), dict) else {"status": "ABSENT", "review_items_compact": []} + if handoff.get("status") == "BLOCKED_INVALID": + fail("part1_soft_gate_handoff", "Part 1 sidecar blocked progression") + _accept_publisher(PUBLISHER_RAW) + _init_mcp_session() + stage_a_universe = _stage_a_universe(stage_a) + candidate_index = _empty_candidate_index() + stage_c_handoff = _base_stage_c_handoff() + domains: dict[str, dict[str, Any]] = {} + seen_prop_ids: set[str] = set() + worker_statuses: dict[str, str] = {} + + for domain_id in REQUIRED_DOMAINS: + seed_obj = _read_seed_doc(SEED_DOCS[domain_id], domain_id, stage_a) + worker_statuses[domain_id] = seed_obj.get("status", "READY") + metadata = seed_obj.get("transport_metadata") if isinstance(seed_obj.get("transport_metadata"), dict) else {} + slice_guard = metadata.get("slice_guard") if isinstance(metadata.get("slice_guard"), dict) else {} + source_universe = slice_guard.get("source_universe") if isinstance(slice_guard.get("source_universe"), dict) else {} + for key, allowed in stage_a_universe.items(): + subset = _as_set(source_universe.get(key) or []) + if not subset.issubset(allowed): + fail("source_membership", f"{domain_id} slice source_universe.{key} outside Stage A universe", sorted(subset - allowed)) + selected = {"event_candidate_ids": set(), "meeting_clause_ids": set(), "evidence_indexes": set()} + projected_candidates: list[dict[str, Any]] = [] + for idx, seed_candidate in enumerate(seed_obj.get("bo_seed_candidates") or []): + if not isinstance(seed_candidate, dict): + fail("seed_candidate_acceptance", f"{domain_id} bo_seed_candidates[{idx}] must be object") + candidate_ref = seed_candidate.get("candidate_ref") + prop_id = _candidate_ref_to_prop_id(candidate_ref, domain_id) + if prop_id in seen_prop_ids: + fail("candidate_ref_mapping", "duplicate generated prop_id", [prop_id]) + seen_prop_ids.add(prop_id) + event_ids, meeting_ids, evidence_ids = _source_values(seed_candidate) + event_ids = _validate_source_subset(event_ids, _as_set(source_universe.get("event_candidate_ids") or []), f"{domain_id}.{candidate_ref}.event ids") + meeting_ids = _validate_source_subset(meeting_ids, _as_set(source_universe.get("meeting_clause_ids") or []), f"{domain_id}.{candidate_ref}.meeting ids") + evidence_ids = _validate_source_subset(evidence_ids, _as_set(source_universe.get("evidence_indexes") or []), f"{domain_id}.{candidate_ref}.evidence ids") + # candidate 의 source id list 도 strip 결과로 갱신 (downstream projection 이 일관) + _prov = seed_candidate.get("provenance") if isinstance(seed_candidate.get("provenance"), dict) else None + if _prov is not None: + _prov["source_event_candidate_ids"] = list(event_ids) + _prov["source_meeting_clause_ids"] = list(meeting_ids) + if "source_evidence_indexes" in _prov: + _prov["source_evidence_indexes"] = list(evidence_ids) + if "source_evidence_indexes" in seed_candidate: + seed_candidate["source_evidence_indexes"] = list(evidence_ids) + selected["event_candidate_ids"].update(str(v) for v in event_ids) + selected["meeting_clause_ids"].update(str(v) for v in meeting_ids) + selected["evidence_indexes"].update(str(v) for v in evidence_ids) + projected = _project_seed_candidate(seed_candidate, prop_id) + projected_candidates.append(projected) + candidate_index["all_prop_ids"].append(prop_id) + candidate_index["by_domain"].setdefault(domain_id, []).append(prop_id) + for ev in projected.get("source_event_candidate_ids") or []: + _add_index(candidate_index, "by_source_event_candidate_id", ev, prop_id) + for ei in projected.get("source_evidence_indexes") or []: + _add_index(candidate_index, "by_source_evidence_index", ei, prop_id) + for mid in projected.get("source_meeting_clause_ids") or []: + _add_index(candidate_index, "by_meeting_clause_id", mid, prop_id) + _add_index(candidate_index, "by_asset_cluster_id_candidate", projected.get("AssetClusterId_candidate"), prop_id) + _add_index(candidate_index, "by_legal_effect_bo_type_candidate", projected.get("legal_effect_bo_type_candidate"), prop_id) + domain_output = _build_domain_output(seed_obj, projected_candidates, selected) + _route_review_queue(seed_obj, domain_id, stage_c_handoff, domain_output) + domains[domain_id] = domain_output + + for idx, item in enumerate(handoff.get("review_items_compact") or []): + stage_c_handoff["part1_review_queue"].append({ + "queue_source": "stage_a_context.part1_soft_gate_handoff.review_items_compact", + "queue_index": idx, + "queue_item": item, + "must_preserve_as_review": True, + }) + + for key, value in list(candidate_index.items()): + if isinstance(value, list): + candidate_index[key] = sorted(set(value)) + elif isinstance(value, dict): + candidate_index[key] = {subkey: sorted(set(vals)) for subkey, vals in sorted(value.items())} + review_count = sum(len(domain.get("domain_stage_c_review_queue") or []) for domain in domains.values()) + len(stage_c_handoff["part1_review_queue"]) + status = "READY_WITH_REVIEW" if review_count else "READY" + print(json.dumps({ + "stage_b_output": { + "schema_version": "task_c_bo_stage_b_proposals.v1", + "stage": "Task_C_BO.Stage_B", + "status": status, + "failure": None, + "domain_outputs": domains, + "candidate_index": candidate_index, + "stage_c_handoff": stage_c_handoff, + "prohibited_finalization_attested": { + "tools_used": True, + "files_read": True, + "files_written": False, + "bo_json_written": False, + "signal_files_written": False, + "bo_id_finalized": False, + "bh_id_used": False, + "cross_domain_merge_finalized": False, + "final_prioract_reason_finalized": False, + "final_superseded_by_bo_id_finalized": False, + "final_evidence_objects_created": False, + "final_evidence_titles_created": False, + "final_actiontype_juristicact_finalized": False, + }, + "stage_b_artifact_handoff_audit": { + "schema_version": "task_c_bo_stage_b_seed_artifact_handoff_audit.v1", + "handoff_artifact_count": len(SEED_DOCS), + "handoff_artifacts_read": SEED_ARTIFACTS_READ, + "all_handoff_guards_matched_stage_a": True, + "seed_json_loaded_from_prev": False, + "seed_worker_statuses": worker_statuses, + "source_contract": "stage_b_domain_bo_seed_output.v1", + }, + "embedded_validation_summary": { + "stage_b_self_check_status": "PASS_WITH_REVIEW" if review_count else "PASS", + "domain_seed_count_attested": len(REQUIRED_DOMAINS), + "prop_id_mapping_generated_by_domain_join": True, + "prop_id_uniqueness_attested": True, + "source_membership_hard_gates_passed": True, + "merge_finalization_deferred_to_stage_c": True, + "part1_review_items_preserved_as_review_queue": True, + }, + } + }, ensure_ascii=False)) + except Exception as exc: + fail("domain_join_unhandled_exception", str(exc)) + + + if __name__ == "__main__": + main() + + - task_name: Task_C_BO_PostB_1_seed_ledger_compiler + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 120 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import json + import re + import sys + from datetime import datetime, timezone + from typing import Any + + STAGE_A_RAW = r'''{{prev.Task_C_BO_Stage_A_input_normalization}}''' + STAGE_B_RAW = r'''{{prev.Task_C_BO_Stage_B_domain_join}}''' + + DOMAIN_ORDER = [ + "B1_Money_Successor", + "B2_Secured_Registry", + "B3_Land_Valuation", + "B4_Actio", + "B5_Succession_Notice_Lien_Asset_Defense", + ] + DOMAIN_PREFIX = { + "B1_Money_Successor": "B1", + "B2_Secured_Registry": "B2", + "B3_Land_Valuation": "B3", + "B4_Actio": "B4", + "B5_Succession_Notice_Lien_Asset_Defense": "B5", + } + CANDIDATE_REF_RE = re.compile(r"^B[1-5]:[0-9]{3}$") + PROP_ID_RE = re.compile(r"^prop_(B[1-5])_([0-9]{3})$") + JSON_DECODER = json.JSONDecoder() + + FINAL_FORBIDDEN_KEYS = { + "BO_ID", "id", "Evidence", "EvidenceTitles", "prop_id", "source_prop_ids", + "source_domain_ids", "base_prop_id", "temp_canon_id", "canonical_decision_id", + "decision_type", "semantic_decision_basis", "canonical_review_notes", "sort_key", + "tie_breaker_explanation", "stage_d_enrichment_required", + "stage_d_enrichment_source_domain", "d0_domain_batching_review", + "stage_d_review_hint", "review_notes", "AssetClusterId", "MeetingClauseRefs", + "StatusTags", "source_fact_candidates", "mandatory_downstream_roles", + "downstream_module_candidates", "provisional_prior_prop_id_candidates", + "provisional_reason_prop_id_candidates", "provisional_reason_text_candidates", + "provisional_reasonrefs_prop_id_candidates", + "provisional_superseded_by_prop_id_candidates", "superseded_by_bo_id", + "legal_effect_payload_candidates", + } + ALLOWED_SEED_KEYS = { + "candidate_ref", "source_domain", "BOType", "ActionType", "JuristicAct", "Action", + "core_field_base", "amount", "source_evidence_indexes", "provenance", + "downstream_seed_refs", "extensions", "Legal_Keywords", "transport_audit", + "Reason", "PriorAct", "ReasonRefs", + } + CORE_KEYS = [ + "Performer", "PerformerType", "Action_proposal", "Subject", "Object", + "BehaviorTime", "TimeText", "TimePrecision", "StatementType", "Perspective", + ] + + def _preview(raw: str, limit: int = 180) -> str: + return (raw or "").strip().replace("\n", "\\n")[:limit] + + def _extract_json_object(raw: str, label: str) -> dict[str, Any]: + text = (raw or "").strip() + if not text or text.startswith("{{"): + raise ValueError(f"{label}: unresolved or empty upstream payload; preview={_preview(raw)!r}") + start = text.find("{") + if start < 0: + raise ValueError(f"{label}: JSON object start not found; preview={_preview(raw)!r}") + candidate = text[start:].strip() + obj, end = JSON_DECODER.raw_decode(candidate) + tail = candidate[end:].strip() + if tail: + raise ValueError(f"{label}: non-json tail after object; preview={_preview(tail)!r}") + if not isinstance(obj, dict): + raise ValueError(f"{label}: JSON root must be object") + return obj + + def _unwrap(raw: str, label: str) -> dict[str, Any]: + obj = _extract_json_object(raw, label) + if obj.get("type") == "text" and isinstance(obj.get("text"), str): + obj = _extract_json_object(obj["text"], f"{label}.text") + if obj.get("success") is True and isinstance(obj.get("output"), str): + obj = _extract_json_object(obj["output"], f"{label}.output") + if not isinstance(obj, dict): + raise ValueError(f"{label}: unwrapped root must be object") + return obj + + def _fail(step: str, reason: str, details: Any = None) -> None: + print(json.dumps({ + "postb_seed_ledger": { + "schema_version": "task_c_bo_postb_seed_ledger.v1", + "status": "FAILED", + "failure": {"failure_step": step, "failure_reason": reason, "details": details}, + "source_stage_a_created_at_utc": None, + "input_digests_sha256": {}, + "ledger_candidates": [], + "deterministic_decisions": [], + "exception_pack": { + "has_exceptions": True, + "clusters": [], + "field_conflicts": [], + "link_ambiguities": [], + "schema_risks": [{"exception_id": "EX-FAILED", "reason": reason, "details": details}], + }, + "audit_trace": { + "removed_or_sidecar_fields": [], + "source_membership_warnings": [], + "normalization_notes": [], + }, + } + }, ensure_ascii=False)) + sys.exit(0) + + def _stage_a(raw: str) -> dict[str, Any]: + outer = _unwrap(raw, "STAGE_A_RAW") + stage_a = outer.get("stage_a_context") + if not isinstance(stage_a, dict): + raise ValueError("stage_a_context missing") + if stage_a.get("schema_version") != "task_c_bo_stage_a_context.v1": + raise ValueError("Stage A schema mismatch") + if stage_a.get("status") != "READY": + raise ValueError("Stage A status must be READY") + if not isinstance(stage_a.get("input_digests_sha256"), dict): + raise ValueError("Stage A input_digests_sha256 missing") + return stage_a + + def _list_strings(value: Any) -> list[str]: + if not isinstance(value, list): + return [] + out: list[str] = [] + for item in value: + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _stage_a_universe(stage_a: dict[str, Any]) -> dict[str, set[str]]: + event_map = _dict(stage_a.get("event_candidate_map")) + evidence_map = _dict(stage_a.get("evidence_authority_map")) + meeting_map = _dict(stage_a.get("meeting_clause_map")) + event_ids = set(_list_strings(event_map.get("candidate_id_set"))) + evidence_ids = set(_list_strings(evidence_map.get("evidence_index_set"))) + meeting_ids = set(_list_strings(meeting_map.get("clause_order"))) + event_ids.update(str(k) for k in _dict(event_map.get("by_event_candidate_id")).keys()) + evidence_ids.update(str(k) for k in _dict(evidence_map.get("by_evidence_index")).keys()) + meeting_ids.update(str(k) for k in _dict(meeting_map.get("by_clause_id")).keys()) + return { + "source_event_candidate_ids": event_ids, + "source_evidence_indexes": evidence_ids, + "source_meeting_clause_ids": meeting_ids, + } + + def _normalize_ref(candidate: dict[str, Any], domain_id: str, ordinal: int) -> str: + direct = candidate.get("candidate_ref") or candidate.get("source_candidate_ref") + if isinstance(direct, str) and CANDIDATE_REF_RE.fullmatch(direct): + return direct + prop_id = candidate.get("prop_id") + if isinstance(prop_id, str): + match = PROP_ID_RE.fullmatch(prop_id) + if match: + return f"{match.group(1)}:{match.group(2)}" + prefix = DOMAIN_PREFIX[domain_id] + return f"{prefix}:{ordinal:03d}" + + def _normalize_juristic(value: Any) -> Any: + if value is None: + return None + if isinstance(value, dict): + label = value.get("label") + return {"label": str(label).strip()} if label not in (None, "") else {"label": None} + text = str(value).strip() + return {"label": text} if text else None + + def _seed_from_candidate(candidate: dict[str, Any], domain_id: str, ordinal: int, audit: dict[str, list[Any]]) -> dict[str, Any]: + candidate_ref = _normalize_ref(candidate, domain_id, ordinal) + payload: dict[str, Any] = {} + for key in ALLOWED_SEED_KEYS: + if key in candidate: + payload[key] = candidate[key] + payload["candidate_ref"] = candidate_ref + payload["source_domain"] = domain_id + payload["JuristicAct"] = _normalize_juristic(payload.get("JuristicAct")) + provenance = _dict(payload.get("provenance")) + payload["provenance"] = { + "source_event_candidate_ids": _list_strings(candidate.get("source_event_candidate_ids") or provenance.get("source_event_candidate_ids")), + "source_meeting_clause_ids": _list_strings(candidate.get("source_meeting_clause_ids") or provenance.get("source_meeting_clause_ids")), + "source_evidence_indexes": _list_strings(candidate.get("source_evidence_indexes") or provenance.get("source_evidence_indexes")), + "source_domain": domain_id, + } + payload["source_evidence_indexes"] = _list_strings(candidate.get("source_evidence_indexes") or payload["provenance"]["source_evidence_indexes"]) + payload["core_field_base"] = _dict(payload.get("core_field_base")) + payload["downstream_seed_refs"] = _dict(payload.get("downstream_seed_refs")) + payload["extensions"] = _dict(payload.get("extensions")) + if "domain_payload" not in payload["extensions"]: + payload["extensions"]["domain_payload"] = {} + for key in sorted(set(candidate.keys()) - ALLOWED_SEED_KEYS): + if key in FINAL_FORBIDDEN_KEYS: + audit["removed_or_sidecar_fields"].append({ + "candidate_ref": candidate_ref, + "field": key, + "handling": "removed_from_seed_payload", + }) + return payload + + def _extract_seed_candidates(stage_b_outer: dict[str, Any], audit: dict[str, list[Any]]) -> tuple[list[dict[str, Any]], dict[str, Any]]: + if isinstance(stage_b_outer.get("stage_b_bo_seed_universe"), dict): + universe = stage_b_outer["stage_b_bo_seed_universe"] + if universe.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("stage_b_bo_seed_universe status is not READY") + seeds = universe.get("bo_seed_candidates") + if not isinstance(seeds, list): + raise ValueError("stage_b_bo_seed_universe.bo_seed_candidates must be array") + out: list[dict[str, Any]] = [] + counters = {domain: 0 for domain in DOMAIN_ORDER} + for item in seeds: + if not isinstance(item, dict): + continue + domain_id = item.get("source_domain") or item.get("domain_id") + if domain_id not in DOMAIN_ORDER: + raise ValueError(f"invalid source_domain: {domain_id!r}") + counters[domain_id] += 1 + out.append(_seed_from_candidate(item, domain_id, counters[domain_id], audit)) + return out, universe + + if isinstance(stage_b_outer.get("stage_b_output"), dict): + stage_b = stage_b_outer["stage_b_output"] + if stage_b.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("stage_b_output status is not READY") + domain_outputs = _dict(stage_b.get("domain_outputs")) + out = [] + for domain_id in DOMAIN_ORDER: + domain_output = _dict(domain_outputs.get(domain_id)) + candidates = domain_output.get("provisional_bo_candidates") + if not isinstance(candidates, list): + continue + for ordinal, candidate in enumerate(candidates, start=1): + if isinstance(candidate, dict): + out.append(_seed_from_candidate(candidate, domain_id, ordinal, audit)) + return out, { + "schema_version": "task_c_bo_stage_b_bo_seed_universe.compat_from_stage_b_output.v1", + "status": stage_b.get("status"), + "compatibility_source": "stage_b_output.domain_outputs[].provisional_bo_candidates", + "stage_b_artifact_handoff_audit": stage_b.get("stage_b_artifact_handoff_audit"), + } + + raise ValueError("Stage B output must contain stage_b_bo_seed_universe or stage_b_output") + + def _source_refs(seed: dict[str, Any]) -> dict[str, list[str]]: + provenance = _dict(seed.get("provenance")) + return { + "source_evidence_indexes": _list_strings(seed.get("source_evidence_indexes") or provenance.get("source_evidence_indexes")), + "source_event_candidate_ids": _list_strings(provenance.get("source_event_candidate_ids")), + "source_meeting_clause_ids": _list_strings(provenance.get("source_meeting_clause_ids")), + } + + def _check_source_membership(refs: dict[str, list[str]], universe: dict[str, set[str]], candidate_ref: str, warnings: list[Any]) -> None: + for key, values in refs.items(): + allowed = universe.get(key, set()) + if not allowed: + continue + outside = [value for value in values if value not in allowed] + if outside: + raise ValueError(f"{candidate_ref}: {key} outside Stage A universe: {outside}") + if not values and key == "source_evidence_indexes": + warnings.append({"candidate_ref": candidate_ref, "warning": "no_source_evidence_indexes"}) + + def _sort_key(seed: dict[str, Any]) -> dict[str, Any]: + core = _dict(seed.get("core_field_base")) + domain = seed.get("source_domain") + juristic = _dict(seed.get("JuristicAct")) + return { + "BehaviorTime": core.get("BehaviorTime"), + "domain_order": DOMAIN_ORDER.index(domain) if domain in DOMAIN_ORDER else 99, + "BOType": seed.get("BOType"), + "ActionType": seed.get("ActionType"), + "JuristicActLabel": juristic.get("label"), + "Action": seed.get("Action"), + "candidate_ref": seed.get("candidate_ref"), + } + + def _duplicate_key(seed: dict[str, Any]) -> tuple[Any, ...]: + core = _dict(seed.get("core_field_base")) + juristic = _dict(seed.get("JuristicAct")) + refs = _source_refs(seed) + return ( + tuple(sorted(refs["source_evidence_indexes"])), + tuple(sorted(refs["source_event_candidate_ids"])), + seed.get("BOType"), + seed.get("ActionType"), + juristic.get("label"), + str(seed.get("Action") or "").strip(), + str(core.get("BehaviorTime") or "").strip(), + str(core.get("Object") or "").strip(), + ) + + def _schema_risks(seed: dict[str, Any], refs: dict[str, list[str]]) -> list[str]: + risks: list[str] = [] + if seed.get("BOType") not in {"event", "state"}: + risks.append("missing_or_invalid_BOType") + if not seed.get("ActionType"): + risks.append("missing_ActionType") + if not seed.get("Action") and not _dict(_dict(seed.get("extensions")).get("domain_payload")).get("action_summary"): + risks.append("missing_Action") + if not refs["source_evidence_indexes"]: + risks.append("missing_source_evidence_indexes") + return risks + + def _exception_item(exception_id: str, kind: str, candidate_refs: list[str], reason: str, candidates: list[dict[str, Any]]) -> dict[str, Any]: + compact = [] + for seed in candidates: + compact.append({ + "candidate_ref": seed.get("candidate_ref"), + "source_domain": seed.get("source_domain"), + "BOType": seed.get("BOType"), + "ActionType": seed.get("ActionType"), + "JuristicAct": seed.get("JuristicAct"), + "Action": seed.get("Action"), + "core_field_base": seed.get("core_field_base"), + "source_refs": _source_refs(seed), + "downstream_seed_refs": seed.get("downstream_seed_refs"), + "extensions": seed.get("extensions"), + }) + return { + "exception_id": exception_id, + "exception_type": kind, + "candidate_refs": candidate_refs, + "reason": reason, + "compact_candidate_payload": compact, + "allowed_decisions": ["KEEP_SEPARATE", "MERGE", "SPLIT", "DROP", "BLOCK_REVIEW"], + } + + def main() -> None: + try: + stage_a = _stage_a(STAGE_A_RAW) + stage_b_outer = _unwrap(STAGE_B_RAW, "STAGE_B_RAW") + audit = { + "removed_or_sidecar_fields": [], + "source_membership_warnings": [], + "normalization_notes": [], + } + seeds, source_universe = _extract_seed_candidates(stage_b_outer, audit) + if not seeds: + raise ValueError("no seed candidate from Stage B domain_join") + universe = _stage_a_universe(stage_a) + seen_refs: set[str] = set() + ledger_candidates: list[dict[str, Any]] = [] + exception_pack = { + "has_exceptions": False, + "clusters": [], + "field_conflicts": [], + "link_ambiguities": [], + "schema_risks": [], + } + deterministic_decisions: list[dict[str, Any]] = [] + duplicate_buckets: dict[tuple[Any, ...], list[dict[str, Any]]] = {} + + for idx, seed in enumerate(seeds, start=1): + candidate_ref = seed.get("candidate_ref") + if not isinstance(candidate_ref, str) or not CANDIDATE_REF_RE.fullmatch(candidate_ref): + raise ValueError(f"invalid candidate_ref: {candidate_ref!r}") + if candidate_ref in seen_refs: + raise ValueError(f"duplicate candidate_ref: {candidate_ref}") + seen_refs.add(candidate_ref) + domain = seed.get("source_domain") + if domain not in DOMAIN_ORDER: + raise ValueError(f"{candidate_ref}: invalid source_domain {domain!r}") + refs = _source_refs(seed) + _check_source_membership(refs, universe, candidate_ref, audit["source_membership_warnings"]) + risks = _schema_risks(seed, refs) + if risks: + exception_id = f"EX-SCHEMA-{len(exception_pack['schema_risks']) + 1:03d}" + exception_pack["schema_risks"].append(_exception_item(exception_id, "schema_risk", [candidate_ref], ",".join(risks), [seed])) + downstream_refs = _dict(seed.get("downstream_seed_refs")) + link_candidates = _list_strings(downstream_refs.get("prior_candidate_refs") or downstream_refs.get("reason_refs_candidate_refs")) + if len(link_candidates) > 1: + exception_id = f"EX-LINK-{len(exception_pack['link_ambiguities']) + 1:03d}" + exception_pack["link_ambiguities"].append(_exception_item(exception_id, "link_ambiguity", [candidate_ref] + link_candidates, "multiple_link_candidates", [seed])) + duplicate_buckets.setdefault(_duplicate_key(seed), []).append(seed) + ledger_candidates.append({ + "candidate_ref": candidate_ref, + "source_domain": domain, + "seed_payload": seed, + "source_refs": refs, + "deterministic_sort_key": _sort_key(seed), + "flags": risks, + }) + + for bucket in duplicate_buckets.values(): + if len(bucket) <= 1: + continue + canonical = bucket[0].get("candidate_ref") + duplicates = [item.get("candidate_ref") for item in bucket[1:]] + deterministic_decisions.append({ + "decision_type": "EXACT_DUPLICATE_MERGE", + "canonical_candidate_ref": canonical, + "duplicate_candidate_refs": duplicates, + "basis": "exact duplicate rule in Task_C_BO_update_strategy_C.md", + }) + + near_buckets: dict[tuple[Any, ...], list[dict[str, Any]]] = {} + for seed in seeds: + refs = _source_refs(seed) + key = ( + tuple(sorted(refs["source_evidence_indexes"])), + tuple(sorted(refs["source_event_candidate_ids"])), + seed.get("BOType"), + seed.get("ActionType"), + ) + near_buckets.setdefault(key, []).append(seed) + for bucket in near_buckets.values(): + if len(bucket) > 1 and len({_duplicate_key(seed) for seed in bucket}) > 1: + exception_id = f"EX-CLUSTER-{len(exception_pack['clusters']) + 1:03d}" + refs = [str(seed.get("candidate_ref")) for seed in bucket] + exception_pack["clusters"].append(_exception_item(exception_id, "possible_duplicate_or_merge", refs, "shared_source_refs_but_not_exact_duplicate", bucket)) + + exception_pack["has_exceptions"] = any(exception_pack[key] for key in ("clusters", "field_conflicts", "link_ambiguities", "schema_risks")) + print(json.dumps({ + "postb_seed_ledger": { + "schema_version": "task_c_bo_postb_seed_ledger.v1", + "status": "READY", + "source_stage_a_created_at_utc": stage_a.get("created_at_utc"), + "input_digests_sha256": stage_a.get("input_digests_sha256"), + "source_universe": { + "source_event_candidate_ids": sorted(universe["source_event_candidate_ids"]), + "source_evidence_indexes": sorted(universe["source_evidence_indexes"]), + "source_meeting_clause_ids": sorted(universe["source_meeting_clause_ids"]), + }, + "stage_b_source_contract": source_universe, + "ledger_candidates": sorted(ledger_candidates, key=lambda item: ( + item["deterministic_sort_key"].get("BehaviorTime") is None, + item["deterministic_sort_key"].get("BehaviorTime") or "", + item["deterministic_sort_key"].get("domain_order", 99), + item["deterministic_sort_key"].get("BOType") or "", + item["deterministic_sort_key"].get("ActionType") or "", + item["deterministic_sort_key"].get("JuristicActLabel") or "", + item["deterministic_sort_key"].get("Action") or "", + item["deterministic_sort_key"].get("candidate_ref") or "", + )), + "deterministic_decisions": deterministic_decisions, + "exception_pack": exception_pack, + "audit_trace": audit, + } + }, ensure_ascii=False)) + except Exception as exc: + _fail("postb_1_seed_ledger_compile", str(exc)) + + if __name__ == "__main__": + main() + + - task_name: Task_C_BO_PostB_2_single_exception_adjudicator + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 1 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 Task_C_BO after Stage B domain_join. + - This task is the only post-Stage-B LLM task. + - The current static block, assigned input, output schema, and writer boundary control. + - Do not expand the input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + + + + - Use only the assigned exception pack embedded in the upstream PostB_1 output below. + - Do not use Stage A full context, raw evidence maps, raw meeting text, outside law, memory, or unassigned files. + - Do not add new evidence indexes, source event ids, meeting clause ids, candidate refs, BO IDs, or bh# ids. + - If the compact exception payload is insufficient, use BLOCK_REVIEW. + + + + - You are not a BO writer. + - You are not allowed to write BO.json or any file. + - You are not allowed to create final BO_ID or bh# ids. + - Resolve only listed exceptions. + - Return exactly one JSON object and nothing else. + - Do not output markdown fences. + - Do not output explanations outside JSON. + - Do not output terminate, stop marker, or trailing note. + + + + + Task name: Task_C_BO_PostB_2_single_exception_adjudicator + + Input: + - Full upstream object is provided only because some runtimes do not support stable nested field access. + - You must read only `postb_seed_ledger.exception_pack`. + - You may use only compact candidate payloads already embedded inside each exception item. + + Upstream PostB_1 output: + {{prev.Task_C_BO_PostB_1_seed_ledger_compiler}} + + Allowed judgment types: + - canonical decision: KEEP_SEPARATE, MERGE, SPLIT, DROP, BLOCK_REVIEW + - field conflict: selected BOType, ActionType, JuristicAct.label, Action, or BLOCK_REVIEW + - link ambiguity: selected prior/link candidate refs, NO_LINK, or BLOCK_REVIEW + - semantic risk: PASS, WARNING, BLOCK_REVIEW + + If `exception_pack.has_exceptions` is false, return: + { + "postb_exception_adjudication": { + "schema_version": "task_c_bo_postb_exception_adjudication.v1", + "status": "READY_NO_EXCEPTIONS", + "exception_count": 0, + "canonical_decisions": [], + "field_decisions": [], + "link_decisions": [], + "semantic_gate_decisions": [], + "blocked_review_items": [] + } + } + + Otherwise return exactly this root: + { + "postb_exception_adjudication": { + "schema_version": "task_c_bo_postb_exception_adjudication.v1", + "status": "READY", + "exception_count": 0, + "canonical_decisions": [], + "field_decisions": [], + "link_decisions": [], + "semantic_gate_decisions": [], + "blocked_review_items": [] + } + } + + Required rules: + - Every decision must cite an `exception_id` from the provided exception pack. + - Every candidate_ref must already appear in the provided exception pack. + - For schema risks, use semantic_gate_decisions with PASS, WARNING, or BLOCK_REVIEW. + - For field conflicts, select only values already present in compact candidate payload unless the decision is BLOCK_REVIEW. + - For link ambiguities, select only candidate refs already listed in the exception item, or NO_LINK, or BLOCK_REVIEW. + - If any exception is unsafe to resolve from the compact payload, put it in blocked_review_items. + + + - task_name: Task_C_BO_PostB_3_final_bo_compiler + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 120 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import json + import re + import sys + from typing import Any + + STAGE_A_RAW = r'''{{prev.Task_C_BO_Stage_A_input_normalization}}''' + LEDGER_RAW = r'''{{prev.Task_C_BO_PostB_1_seed_ledger_compiler}}''' + ADJUDICATION_RAW = r'''{{prev.Task_C_BO_PostB_2_single_exception_adjudicator}}''' + + JSON_DECODER = json.JSONDecoder() + DOMAIN_ORDER = [ + "B1_Money_Successor", + "B2_Secured_Registry", + "B3_Land_Valuation", + "B4_Actio", + "B5_Succession_Notice_Lien_Asset_Defense", + ] + ACTION_TYPE_ENUM = { + "법률행위(legal acts)", + "준법률행위(quasi-legal acts)", + "사실행위(factual acts)", + "위법행위(unlawful acts)", + "소송행위(litigation acts)", + } + CORE_KEYS = [ + "Performer", "PerformerType", "Action_proposal", "Subject", "Object", + "BehaviorTime", "TimeText", "TimePrecision", "StatementType", "Perspective", + ] + DOWNSTREAM_KEYS = [ + "claim_group_seed_refs_proposed", + "canonical_theory_graph_seed_ref_proposed", + "legal_effect_structure_seed_ref_proposed", + ] + + def _preview(raw: str, limit: int = 180) -> str: + return (raw or "").strip().replace("\n", "\\n")[:limit] + + def _extract_json_object(raw: str, label: str) -> dict[str, Any]: + text = (raw or "").strip() + if not text or text.startswith("{{"): + raise ValueError(f"{label}: unresolved or empty upstream payload; preview={_preview(raw)!r}") + start = text.find("{") + if start < 0: + raise ValueError(f"{label}: JSON object start not found; preview={_preview(raw)!r}") + obj, end = JSON_DECODER.raw_decode(text[start:].strip()) + tail = text[start:].strip()[end:].strip() + if tail: + raise ValueError(f"{label}: non-json tail after object; preview={_preview(tail)!r}") + if not isinstance(obj, dict): + raise ValueError(f"{label}: JSON root must be object") + return obj + + def _unwrap(raw: str, label: str) -> dict[str, Any]: + obj = _extract_json_object(raw, label) + if obj.get("type") == "text" and isinstance(obj.get("text"), str): + obj = _extract_json_object(obj["text"], f"{label}.text") + if obj.get("success") is True and isinstance(obj.get("output"), str): + obj = _extract_json_object(obj["output"], f"{label}.output") + return obj + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def _fail(status: str, reason: str, details: Any = None) -> None: + print(json.dumps({ + "postb_compiled_bo_bundle": { + "schema_version": "task_c_bo_postb_compiled_bo_bundle.v1", + "status": status, + "failure": {"reason": reason, "details": details}, + "bo_items": [], + "candidate_ref_to_bo_id": {}, + "audit_trace": { + "canonical_decisions_applied": [], + "field_decisions_applied": [], + "link_decisions_applied": [], + "sidecar_fields": [], + "normalization_notes": [], + }, + "pre_gate_report": { + "schema_candidate_errors": [reason], + "evidence_gap_items": [], + "unresolved_exception_items": _list(details), + "writer_blockers": [reason], + }, + } + }, ensure_ascii=False)) + sys.exit(0) + + def _stage_a(raw: str) -> dict[str, Any]: + outer = _unwrap(raw, "STAGE_A_RAW") + stage_a = _dict(outer.get("stage_a_context")) + if stage_a.get("schema_version") != "task_c_bo_stage_a_context.v1" or stage_a.get("status") != "READY": + raise ValueError("Stage A must be READY task_c_bo_stage_a_context.v1") + return stage_a + + def _evidence_map(stage_a: dict[str, Any]) -> dict[str, Any]: + evidence_map = _dict(stage_a.get("evidence_authority_map")) + by_index = _dict(evidence_map.get("by_evidence_index")) + if by_index: + return by_index + out: dict[str, Any] = {} + for item in _list(evidence_map.get("items")): + if isinstance(item, dict): + idx = item.get("evidence_index") or item.get("index") or item.get("id") + if idx is not None: + out[str(idx)] = item + return out + + def _ledger(raw: str) -> dict[str, Any]: + outer = _unwrap(raw, "LEDGER_RAW") + ledger = _dict(outer.get("postb_seed_ledger")) + if ledger.get("schema_version") != "task_c_bo_postb_seed_ledger.v1" or ledger.get("status") != "READY": + _upf = ledger.get("failure") or {} + raise ValueError( + f"PostB_1 seed ledger not READY (status={ledger.get('status')!r}); " + f"upstream_step={_upf.get('failure_step')!r}; " + f"upstream_reason={_upf.get('failure_reason')!r}" + ) + return ledger + + def _adjudication(raw: str) -> dict[str, Any]: + outer = _unwrap(raw, "ADJUDICATION_RAW") + adj = _dict(outer.get("postb_exception_adjudication")) + if adj.get("schema_version") != "task_c_bo_postb_exception_adjudication.v1": + raise ValueError("PostB_2 adjudication schema mismatch") + if adj.get("status") not in {"READY", "READY_NO_EXCEPTIONS"}: + raise ValueError("PostB_2 adjudication status is not READY") + if _list(adj.get("blocked_review_items")): + _fail("FAILED_NEEDS_REVIEW", "PostB_2 returned blocked_review_items", adj.get("blocked_review_items")) + return adj + + def _field_decision_map(adj: dict[str, Any]) -> dict[tuple[str, str], Any]: + out: dict[tuple[str, str], Any] = {} + for item in _list(adj.get("field_decisions")): + if isinstance(item, dict) and item.get("candidate_ref") and item.get("field") and item.get("selected_value") != "BLOCK_REVIEW": + out[(str(item["candidate_ref"]), str(item["field"]))] = item.get("selected_value") + return out + + def _link_decision_map(adj: dict[str, Any]) -> dict[str, dict[str, Any]]: + out: dict[str, dict[str, Any]] = {} + for item in _list(adj.get("link_decisions")): + if isinstance(item, dict) and item.get("candidate_ref"): + out[str(item["candidate_ref"])] = item + return out + + def _decision_sets(ledger: dict[str, Any], adj: dict[str, Any]) -> tuple[set[str], dict[str, str]]: + dropped: set[str] = set() + merge_into: dict[str, str] = {} + for decision in _list(ledger.get("deterministic_decisions")): + if not isinstance(decision, dict) or decision.get("decision_type") != "EXACT_DUPLICATE_MERGE": + continue + canonical = decision.get("canonical_candidate_ref") + for dup in _strings(decision.get("duplicate_candidate_refs")): + if canonical: + merge_into[dup] = str(canonical) + dropped.add(dup) + for decision in _list(adj.get("canonical_decisions")): + if not isinstance(decision, dict): + continue + kind = decision.get("decision") + refs = _strings(decision.get("input_candidate_refs")) + if kind == "DROP": + dropped.update(_strings(decision.get("drop_candidate_refs")) or refs) + elif kind == "MERGE": + target = decision.get("merge_target_ref") or (refs[0] if refs else None) + if target: + for ref in refs: + if ref != target: + merge_into[ref] = str(target) + dropped.add(ref) + elif kind == "BLOCK_REVIEW": + _fail("FAILED_NEEDS_REVIEW", "PostB_2 canonical decision BLOCK_REVIEW", decision) + return dropped, merge_into + + def _sort_tuple(item: dict[str, Any]) -> tuple[Any, ...]: + key = _dict(item.get("deterministic_sort_key")) + return ( + key.get("BehaviorTime") is None, + key.get("BehaviorTime") or "", + key.get("domain_order", 99), + key.get("BOType") or "", + key.get("ActionType") or "", + key.get("JuristicActLabel") or "", + key.get("Action") or "", + key.get("candidate_ref") or "", + ) + + def _juristic(value: Any) -> dict[str, Any] | None: + if value is None: + return None + if isinstance(value, dict): + label = value.get("label") + return {"label": str(label).strip()} if label not in (None, "") else {"label": None} + text = str(value).strip() + return {"label": text} if text else None + + def _core(seed: dict[str, Any]) -> dict[str, Any]: + src = _dict(seed.get("core_field_base")) + out = {key: src.get(key) for key in CORE_KEYS} + if out.get("Action_proposal") is None and seed.get("Action"): + out["Action_proposal"] = seed.get("Action") + if out.get("StatementType") is None: + out["StatementType"] = seed.get("BOType") + if out.get("Perspective") is None: + out["Perspective"] = "plaintiff" + return out + + def _amount(value: Any) -> dict[str, Any] | None: + if value is None: + return None + if isinstance(value, dict): + return { + "value_text": value.get("value_text") or value.get("text"), + "numeric_value": value.get("numeric_value"), + "currency": value.get("currency"), + } + text = str(value).strip() + return {"value_text": text, "numeric_value": None, "currency": None} if text else None + + def _evidence_item(index: str, source: Any, gaps: list[Any], bo_id: str) -> dict[str, Any]: + obj = _dict(source) + title = obj.get("source_title") or obj.get("title") or obj.get("evidence_title") or obj.get("document_title") or index + relevant = obj.get("relevant_content") or obj.get("excerpt") or obj.get("summary") or obj.get("content") + if relevant in (None, ""): + gaps.append({"BO_ID": bo_id, "evidence_index": index, "gap": "missing_relevant_content"}) + relevant = None + return { + "evidence_index": index, + "source_title": str(title), + "priority_class": obj.get("priority_class") or obj.get("priority") or None, + "relevant_content": relevant, + "authentication_status": obj.get("authentication_status") or obj.get("auth_status") or None, + "corroboration": obj.get("corroboration") or None, + "selection_basis": "source_evidence_indexes membership", + } + + def _downstream_refs(seed: dict[str, Any]) -> dict[str, Any]: + src = _dict(seed.get("downstream_seed_refs")) + return { + "claim_group_seed_refs_proposed": _strings(src.get("claim_group_seed_refs_proposed") or src.get("claim_group_seed_refs")), + "canonical_theory_graph_seed_ref_proposed": src.get("canonical_theory_graph_seed_ref_proposed") or src.get("canonical_theory_graph_seed_ref"), + "legal_effect_structure_seed_ref_proposed": src.get("legal_effect_structure_seed_ref_proposed") or src.get("legal_effect_structure_seed_ref"), + } + + def _keywords(seed: dict[str, Any], juristic: dict[str, Any] | None) -> list[str]: + out = _strings(seed.get("Legal_Keywords")) + domain_payload = _dict(_dict(seed.get("extensions")).get("domain_payload")) + out.extend(_strings(domain_payload.get("legal_effect_tags"))) + if isinstance(juristic, dict) and juristic.get("label"): + out.append(str(juristic["label"])) + deduped: list[str] = [] + for item in out: + if item not in deduped: + deduped.append(item) + return deduped + + def main() -> None: + try: + stage_a = _stage_a(STAGE_A_RAW) + evidence_map = _evidence_map(stage_a) + ledger = _ledger(LEDGER_RAW) + adjudication = _adjudication(ADJUDICATION_RAW) + field_decisions = _field_decision_map(adjudication) + link_decisions = _link_decision_map(adjudication) + dropped, merge_into = _decision_sets(ledger, adjudication) + + candidates_by_ref: dict[str, dict[str, Any]] = {} + for item in _list(ledger.get("ledger_candidates")): + if isinstance(item, dict) and isinstance(item.get("candidate_ref"), str): + candidates_by_ref[item["candidate_ref"]] = item + survivors = [item for item in _list(ledger.get("ledger_candidates")) if isinstance(item, dict) and item.get("candidate_ref") not in dropped] + survivors.sort(key=_sort_tuple) + if not survivors: + _fail("FAILED", "no surviving BO candidates after decisions") + + candidate_ref_to_bo_id: dict[str, str] = {} + for idx, item in enumerate(survivors, start=1): + candidate_ref_to_bo_id[str(item["candidate_ref"])] = f"bh{idx}" + for source_ref, target_ref in merge_into.items(): + if target_ref in candidate_ref_to_bo_id: + candidate_ref_to_bo_id[source_ref] = candidate_ref_to_bo_id[target_ref] + + bo_items: list[dict[str, Any]] = [] + audit = { + "canonical_decisions_applied": _list(ledger.get("deterministic_decisions")) + _list(adjudication.get("canonical_decisions")), + "field_decisions_applied": _list(adjudication.get("field_decisions")), + "link_decisions_applied": _list(adjudication.get("link_decisions")), + "sidecar_fields": _list(_dict(ledger.get("audit_trace")).get("removed_or_sidecar_fields")), + "normalization_notes": _list(_dict(ledger.get("audit_trace")).get("normalization_notes")), + } + pre_gate = { + "schema_candidate_errors": [], + "evidence_gap_items": [], + "unresolved_exception_items": [], + "writer_blockers": [], + } + + for idx, ledger_item in enumerate(survivors, start=1): + seed = _dict(ledger_item.get("seed_payload")) + candidate_ref = str(ledger_item.get("candidate_ref")) + bo_id = f"bh{idx}" + bo_type = field_decisions.get((candidate_ref, "BOType"), seed.get("BOType")) + action_type = field_decisions.get((candidate_ref, "ActionType"), seed.get("ActionType")) + juristic = _juristic(field_decisions.get((candidate_ref, "JuristicAct.label"), seed.get("JuristicAct"))) + domain_payload = _dict(_dict(seed.get("extensions")).get("domain_payload")) + action = field_decisions.get((candidate_ref, "Action"), seed.get("Action") or domain_payload.get("action_summary") or _dict(seed.get("core_field_base")).get("Action_proposal")) + if bo_type not in {"event", "state"}: + audit["normalization_notes"].append({"candidate_ref": candidate_ref, "field": "BOType", "received": bo_type, "fallback": "event"}) + bo_type = "event" + if action_type not in ACTION_TYPE_ENUM: + audit["normalization_notes"].append({"candidate_ref": candidate_ref, "field": "ActionType", "received": action_type, "fallback": "사실행위(factual acts)"}) + action_type = "사실행위(factual acts)" + if not isinstance(action, str) or not action.strip(): + audit["normalization_notes"].append({"candidate_ref": candidate_ref, "field": "Action", "fallback": "source-backed BO"}) + action = f"{candidate_ref} source-backed BO" + + refs = _dict(ledger_item.get("source_refs")) + source_evidence_indexes = _strings(refs.get("source_evidence_indexes")) + evidence_items = [_evidence_item(eidx, evidence_map.get(eidx), pre_gate["evidence_gap_items"], bo_id) for eidx in source_evidence_indexes] + evidence_titles: list[str] = [] + for ev in evidence_items: + title = ev["source_title"] + if title not in evidence_titles: + evidence_titles.append(title) + + link = link_decisions.get(candidate_ref, {}) + reason_ref_candidates = _strings(link.get("reason_refs_candidate_refs")) + prior_candidate = link.get("prior_candidate_ref") + if prior_candidate == "NO_LINK": + prior_candidate = None + explicit_refs = _dict(seed.get("downstream_seed_refs")) + if not reason_ref_candidates: + reason_ref_candidates = _strings(explicit_refs.get("reason_refs_candidate_refs")) + if not prior_candidate: + prior_list = _strings(explicit_refs.get("prior_candidate_refs")) + prior_candidate = prior_list[0] if len(prior_list) == 1 else None + if len(prior_list) > 1: + pre_gate["unresolved_exception_items"].append({"candidate_ref": candidate_ref, "issue": "multiple_prior_candidate_refs_without_llm_decision"}) + reason_refs = [candidate_ref_to_bo_id[ref] for ref in reason_ref_candidates if ref in candidate_ref_to_bo_id and candidate_ref_to_bo_id[ref] != bo_id] + if prior_candidate and prior_candidate in candidate_ref_to_bo_id: + prior_act = candidate_ref_to_bo_id[prior_candidate] + elif reason_refs: + prior_act = reason_refs[0] + else: + prior_act = None + reason = "ReasonRefs에 기재된 선행 BO와 source evidence/event chain으로 연결됨" if reason_refs else "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO" + + bo_items.append({ + "BO_ID": bo_id, + "id": bo_id, + "BOType": bo_type, + "ActionType": action_type, + "JuristicAct": juristic, + "Action": str(action).strip(), + "Reason": reason, + "PriorAct": prior_act, + "ReasonRefs": reason_refs, + "Legal_Keywords": _keywords(seed, juristic), + "core_field_base": _core(seed), + "amount": _amount(seed.get("amount")), + "EvidenceTitles": evidence_titles, + "Evidence": evidence_items, + "source_evidence_indexes": source_evidence_indexes, + "provenance": { + "source_event_candidate_ids": _strings(refs.get("source_event_candidate_ids")), + "source_meeting_clause_ids": _strings(refs.get("source_meeting_clause_ids")), + "source_domain": seed.get("source_domain"), + }, + "downstream_seed_refs": _downstream_refs(seed), + "extensions": {"domain_payload": domain_payload}, + }) + + if pre_gate["schema_candidate_errors"] or pre_gate["unresolved_exception_items"]: + pre_gate["writer_blockers"].append("schema_or_exception_blockers_present") + + print(json.dumps({ + "postb_compiled_bo_bundle": { + "schema_version": "task_c_bo_postb_compiled_bo_bundle.v1", + "status": "READY" if not pre_gate["writer_blockers"] else "FAILED_NEEDS_REVIEW", + "bo_items": bo_items if not pre_gate["writer_blockers"] else [], + "candidate_ref_to_bo_id": candidate_ref_to_bo_id, + "audit_trace": audit, + "pre_gate_report": pre_gate, + } + }, ensure_ascii=False)) + except Exception as exc: + _fail("FAILED", str(exc)) + + if __name__ == "__main__": + main() + + - task_name: Task_C_BO_PostB_4_final_gate_and_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 120 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import itertools + import json + import re + import sys + from typing import Any + + import httpx + + STAGE_A_RAW = r'''{{prev.Task_C_BO_Stage_A_input_normalization}}''' + COMPILED_RAW = r'''{{prev.Task_C_BO_PostB_3_final_bo_compiler}}''' + TARGET_NAME = "BO.json" + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + BH_ID_RE = re.compile(r"^bh[1-9][0-9]*$") + ACTION_TYPE_ENUM = { + "법률행위(legal acts)", + "준법률행위(quasi-legal acts)", + "사실행위(factual acts)", + "위법행위(unlawful acts)", + "소송행위(litigation acts)", + } + ALLOWED_TOP_LEVEL = { + "BO_ID", "id", "BOType", "ActionType", "JuristicAct", "Action", "Reason", + "PriorAct", "ReasonRefs", "Legal_Keywords", "core_field_base", "amount", + "EvidenceTitles", "Evidence", "source_evidence_indexes", "provenance", + "downstream_seed_refs", "extensions", + } + REQUIRED_TOP_LEVEL = { + "BO_ID", "id", "BOType", "ActionType", "JuristicAct", "Action", "Reason", + "PriorAct", "ReasonRefs", "Legal_Keywords", "core_field_base", + "EvidenceTitles", "Evidence", "source_evidence_indexes", "provenance", + "downstream_seed_refs", + } + FORBIDDEN_TOP_LEVEL = { + "prop_id", "source_prop_ids", "source_domain_ids", "base_prop_id", "temp_canon_id", + "canonical_decision_id", "decision_type", "semantic_decision_basis", + "canonical_review_notes", "sort_key", "tie_breaker_explanation", + "stage_d_enrichment_required", "stage_d_enrichment_source_domain", + "d0_domain_batching_review", "stage_d_review_hint", "review_notes", + "AssetClusterId", "MeetingClauseRefs", "StatusTags", "source_fact_candidates", + "mandatory_downstream_roles", "downstream_module_candidates", + "provisional_prior_prop_id_candidates", "provisional_reason_prop_id_candidates", + "provisional_reason_text_candidates", "provisional_reasonrefs_prop_id_candidates", + "provisional_superseded_by_prop_id_candidates", "superseded_by_bo_id", + "legal_effect_payload_candidates", + } + CORE_KEYS = { + "Performer", "PerformerType", "Action_proposal", "Subject", "Object", + "BehaviorTime", "TimeText", "TimePrecision", "StatementType", "Perspective", + } + DOWNSTREAM_KEYS = { + "claim_group_seed_refs_proposed", + "canonical_theory_graph_seed_ref_proposed", + "legal_effect_structure_seed_ref_proposed", + } + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-c-bo-postb-final-gate-and-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + def _write_doc(doc_name: str, content: str, msg_id: int) -> None: + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=msg_id) + + def _read_doc(doc_name: str, msg_id: int) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=msg_id) + content = result["result"].get("content", []) + text = content[0].get("text", "") if content else "" + parsed = json.loads(text) + if isinstance(parsed, dict) and "results" in parsed: + rows = parsed.get("results") or [] + if not rows or rows[0].get("error"): + raise RuntimeError(f"read_docs failed for {doc_name}: {rows}") + return json.loads(rows[0].get("content") or "") + return parsed + + def _preview(raw: str, limit: int = 180) -> str: + return (raw or "").strip().replace("\n", "\\n")[:limit] + + def _extract_json_object(raw: str, label: str) -> dict[str, Any]: + text = (raw or "").strip() + if not text or text.startswith("{{"): + raise ValueError(f"{label}: unresolved or empty upstream payload; preview={_preview(raw)!r}") + start = text.find("{") + if start < 0: + raise ValueError(f"{label}: JSON object start not found; preview={_preview(raw)!r}") + obj, end = JSON_DECODER.raw_decode(text[start:].strip()) + tail = text[start:].strip()[end:].strip() + if tail: + raise ValueError(f"{label}: non-json tail after object; preview={_preview(tail)!r}") + if not isinstance(obj, dict): + raise ValueError(f"{label}: JSON root must be object") + return obj + + def _unwrap(raw: str, label: str) -> dict[str, Any]: + obj = _extract_json_object(raw, label) + if obj.get("type") == "text" and isinstance(obj.get("text"), str): + obj = _extract_json_object(obj["text"], f"{label}.text") + if obj.get("success") is True and isinstance(obj.get("output"), str): + obj = _extract_json_object(obj["output"], f"{label}.output") + return obj + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def _stage_a_universe(stage_a: dict[str, Any]) -> dict[str, set[str]]: + event_map = _dict(stage_a.get("event_candidate_map")) + evidence_map = _dict(stage_a.get("evidence_authority_map")) + meeting_map = _dict(stage_a.get("meeting_clause_map")) + event_ids = set(_strings(event_map.get("candidate_id_set"))) + evidence_ids = set(_strings(evidence_map.get("evidence_index_set"))) + meeting_ids = set(_strings(meeting_map.get("clause_order"))) + event_ids.update(str(k) for k in _dict(event_map.get("by_event_candidate_id")).keys()) + evidence_ids.update(str(k) for k in _dict(evidence_map.get("by_evidence_index")).keys()) + meeting_ids.update(str(k) for k in _dict(meeting_map.get("by_clause_id")).keys()) + return { + "source_event_candidate_ids": event_ids, + "source_evidence_indexes": evidence_ids, + "source_meeting_clause_ids": meeting_ids, + } + + def _gate(status: str, write_performed: bool, gates: list[dict[str, Any]], bo_count: int, failure_reasons: list[str] | None = None, post_write: dict[str, Any] | None = None) -> None: + payload = { + "postb_final_write_report": { + "schema_version": "task_c_bo_postb_final_write_report.v1", + "status": status, + "write_target": TARGET_NAME, + "bo_item_count": bo_count, + "gate_results": gates, + "write_performed": write_performed, + } + } + if status == "PASS": + payload["postb_final_write_report"]["post_write_check"] = post_write or {"file_exists": True, "json_parse_pass": True} + else: + payload["postb_final_write_report"]["failure_reasons"] = failure_reasons or [] + print(json.dumps(payload, ensure_ascii=False)) + sys.exit(0) + + def _add_gate(gates: list[dict[str, Any]], key: str, passed: bool, detail: str) -> None: + gates.append({"gate_key": key, "status": "PASS" if passed else "FAILED", "detail": detail}) + + def _validate_item(item: Any, idx: int, ids: set[str], universe: dict[str, set[str]], gates: list[dict[str, Any]]) -> list[str]: + errors: list[str] = [] + if not isinstance(item, dict): + return [f"item {idx} must be object"] + extra = sorted(set(item.keys()) - ALLOWED_TOP_LEVEL) + missing = sorted(REQUIRED_TOP_LEVEL - set(item.keys())) + forbidden = sorted(FORBIDDEN_TOP_LEVEL & set(item.keys())) + if extra: + errors.append(f"{item.get('BO_ID', idx)} additional fields: {extra}") + if missing: + errors.append(f"{item.get('BO_ID', idx)} missing fields: {missing}") + if forbidden: + errors.append(f"{item.get('BO_ID', idx)} forbidden fields: {forbidden}") + bo_id = item.get("BO_ID") + expected = f"bh{idx}" + if bo_id != expected or item.get("id") != bo_id or not isinstance(bo_id, str) or not BH_ID_RE.fullmatch(bo_id): + errors.append(f"BO_ID/id sequence mismatch: expected {expected}") + if item.get("BOType") not in {"event", "state"}: + errors.append(f"{bo_id}.BOType invalid") + if item.get("ActionType") not in ACTION_TYPE_ENUM: + errors.append(f"{bo_id}.ActionType invalid") + juristic = item.get("JuristicAct") + if juristic is not None and (not isinstance(juristic, dict) or set(juristic.keys()) != {"label"}): + errors.append(f"{bo_id}.JuristicAct invalid") + for key in ("Action", "Reason"): + if not isinstance(item.get(key), str) or not item.get(key).strip(): + errors.append(f"{bo_id}.{key} must be non-empty string") + prior = item.get("PriorAct") + if prior is not None and prior not in ids: + errors.append(f"{bo_id}.PriorAct references missing BO_ID") + for ref in _list(item.get("ReasonRefs")): + if ref not in ids: + errors.append(f"{bo_id}.ReasonRefs references missing BO_ID {ref}") + if not isinstance(item.get("Legal_Keywords"), list): + errors.append(f"{bo_id}.Legal_Keywords must be array") + core = item.get("core_field_base") + if not isinstance(core, dict) or set(core.keys()) != CORE_KEYS: + errors.append(f"{bo_id}.core_field_base keys invalid") + amount = item.get("amount") + if amount is not None and (not isinstance(amount, dict) or set(amount.keys()) - {"value_text", "numeric_value", "currency"}): + errors.append(f"{bo_id}.amount invalid") + evidence = _list(item.get("Evidence")) + evidence_indexes = _strings(item.get("source_evidence_indexes")) + evidence_index_set = set() + titles = [] + for ev in evidence: + if not isinstance(ev, dict): + errors.append(f"{bo_id}.Evidence item must be object") + continue + required_ev = {"evidence_index", "source_title", "priority_class", "relevant_content", "authentication_status", "corroboration", "selection_basis"} + if set(ev.keys()) != required_ev: + errors.append(f"{bo_id}.Evidence item keys invalid") + if isinstance(ev.get("evidence_index"), str): + evidence_index_set.add(ev["evidence_index"]) + if isinstance(ev.get("source_title"), str) and ev.get("source_title") not in titles: + titles.append(ev["source_title"]) + if item.get("EvidenceTitles") != titles: + errors.append(f"{bo_id}.EvidenceTitles mismatch") + if set(evidence_indexes) != evidence_index_set: + errors.append(f"{bo_id}.source_evidence_indexes must equal Evidence[].evidence_index") + if universe["source_evidence_indexes"] and not set(evidence_indexes).issubset(universe["source_evidence_indexes"]): + errors.append(f"{bo_id}.source_evidence_indexes outside Stage A universe") + provenance = item.get("provenance") + if not isinstance(provenance, dict) or set(provenance.keys()) != {"source_event_candidate_ids", "source_meeting_clause_ids", "source_domain"}: + errors.append(f"{bo_id}.provenance invalid") + else: + if universe["source_event_candidate_ids"] and not set(_strings(provenance.get("source_event_candidate_ids"))).issubset(universe["source_event_candidate_ids"]): + errors.append(f"{bo_id}.provenance.source_event_candidate_ids outside Stage A universe") + if universe["source_meeting_clause_ids"] and not set(_strings(provenance.get("source_meeting_clause_ids"))).issubset(universe["source_meeting_clause_ids"]): + errors.append(f"{bo_id}.provenance.source_meeting_clause_ids outside Stage A universe") + downstream = item.get("downstream_seed_refs") + if not isinstance(downstream, dict) or set(downstream.keys()) != DOWNSTREAM_KEYS: + errors.append(f"{bo_id}.downstream_seed_refs invalid") + extensions = item.get("extensions", {"domain_payload": {}}) + if extensions is not None and (not isinstance(extensions, dict) or set(extensions.keys()) - {"domain_payload"} or not isinstance(extensions.get("domain_payload", {}), dict)): + errors.append(f"{bo_id}.extensions invalid") + return errors + + def main() -> None: + gates: list[dict[str, Any]] = [] + try: + stage_a_outer = _unwrap(STAGE_A_RAW, "STAGE_A_RAW") + stage_a = _dict(stage_a_outer.get("stage_a_context")) + if stage_a.get("schema_version") != "task_c_bo_stage_a_context.v1" or stage_a.get("status") != "READY": + _gate("FAILED", False, gates, 0, ["Stage A freshness guard failed"]) + universe = _stage_a_universe(stage_a) + compiled_outer = _unwrap(COMPILED_RAW, "COMPILED_RAW") + bundle = _dict(compiled_outer.get("postb_compiled_bo_bundle")) + _add_gate(gates, "postb3_status_ready", bundle.get("status") == "READY", "PostB_3 status must be READY") + bo_items = bundle.get("bo_items") + _add_gate(gates, "bo_items_array_non_empty", isinstance(bo_items, list) and len(bo_items) > 0, "bo_items must be non-empty array") + pre_gate = _dict(bundle.get("pre_gate_report")) + blockers = _list(pre_gate.get("writer_blockers")) + _list(pre_gate.get("unresolved_exception_items")) + _list(pre_gate.get("schema_candidate_errors")) + _add_gate(gates, "pre_gate_report_clear", not blockers, "pre_gate_report blockers must be empty") + if any(gate["status"] != "PASS" for gate in gates): + _gate("FAILED", False, gates, len(bo_items) if isinstance(bo_items, list) else 0, ["pre-write gate failed"]) + assert isinstance(bo_items, list) + ids = {item.get("BO_ID") for item in bo_items if isinstance(item, dict)} + errors: list[str] = [] + for idx, item in enumerate(bo_items, start=1): + errors.extend(_validate_item(item, idx, ids, universe, gates)) + _add_gate(gates, "bo_schema_and_reference_validation", not errors, "BO_JSON_Schema target validation") + if errors: + _gate("FAILED", False, gates, len(bo_items), errors) + _init_mcp_session() + payload = json.dumps(bo_items, ensure_ascii=False, indent=2) + "\n" + _write_doc(TARGET_NAME, payload, _next_msg_id()) + reread = _read_doc(TARGET_NAME, _next_msg_id()) + _add_gate(gates, "post_write_json_parse", isinstance(reread, list), "BO.json reread JSON parse") + if not isinstance(reread, list): + _gate("FAILED", True, gates, len(bo_items), ["post-write JSON parse failed"]) + _gate("PASS", True, gates, len(bo_items), post_write={"file_exists": True, "json_parse_pass": True}) + except Exception as exc: + _gate("FAILED", False, gates, 0, [str(exc)]) + + if __name__ == "__main__": + main() + + - task_name: Task_C_BO_Middle_Input_actio_case_signals + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 120 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import itertools + import json + import re + import sys + from typing import Any + + import httpx + + STAGE_A_RAW = r'''{{prev.Task_C_BO_Stage_A_input_normalization}}''' + LEDGER_RAW = r'''{{prev.Task_C_BO_PostB_1_seed_ledger_compiler}}''' + COMPILED_RAW = r'''{{prev.Task_C_BO_PostB_3_final_bo_compiler}}''' + B4_ACTIO_RAW = r'''{{prev.Task_C_BO_Stage_B_B4_Actio}}''' + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + ACTIO_SCOPE_ENUM = { + "preserved_claim", "fraudulent_act", "beneficiary_or_transferee", + "target_asset", "encumbrance", "remedy_or_cap", "defense", + } + SUSPICION_LEVEL_ENUM = {"NONE", "LOW", "MEDIUM", "HIGH", "CRITICAL"} + ASSET_KIND_ENUM = { + "real_property", "movable", "claim", "security_right", + "cash_or_deposit", "business_asset", "other", "unknown", + } + ROLE_ENUM = { + "fraudulent_act_date", "debtor_act", "beneficiary", "transferee", + "remedy_or_cap", "defense", "encumbrance", + } + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + def _preview(raw: str, limit: int = 180) -> str: + return (raw or "").strip().replace("\n", "\\n")[:limit] + + def _extract_json_object(raw: str, label: str) -> dict[str, Any]: + text = (raw or "").strip() + if not text or text.startswith("{{"): + raise ValueError(f"{label}: unresolved or empty upstream payload; preview={_preview(raw)!r}") + start = text.find("{") + if start < 0: + raise ValueError(f"{label}: JSON object start not found; preview={_preview(raw)!r}") + candidate = text[start:].strip() + obj, end = JSON_DECODER.raw_decode(candidate) + tail = candidate[end:].strip() + if tail: + raise ValueError(f"{label}: non-json tail after object; preview={_preview(tail)!r}") + if not isinstance(obj, dict): + raise ValueError(f"{label}: JSON root must be object") + return obj + + def _unwrap(raw: str, label: str) -> dict[str, Any]: + obj = _extract_json_object(raw, label) + if obj.get("type") == "text" and isinstance(obj.get("text"), str): + obj = _extract_json_object(obj["text"], f"{label}.text") + if obj.get("success") is True and isinstance(obj.get("output"), str): + obj = _extract_json_object(obj["output"], f"{label}.output") + if not isinstance(obj, dict): + raise ValueError(f"{label}: unwrapped root must be object") + return obj + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def _first_string(*values: Any) -> str | None: + for value in values: + for item in _strings(value): + return item + return None + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-middle-input-actio-case-signal-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result.get("result", {}).get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + text = content[0].get("text", "") + try: + envelope = json.loads(text) + except Exception as exc: + raise RuntimeError(f"Invalid read_docs JSON envelope for {doc_name}: {text[:300]}") from exc + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + item = results[0] + if isinstance(item, dict) and item.get("error"): + raise RuntimeError(f"read_docs error for {doc_name}: {item.get('error')}") + raw = item.get("content", "") + else: + raw = envelope + if isinstance(raw, str): + return json.loads(raw) + return raw + + def _read_doc(doc_name: str) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + return _extract_json_from_read_docs_result(result, doc_name) + + def _write_doc(doc_name: str, payload: dict[str, Any]) -> None: + content = json.dumps(payload, ensure_ascii=False, indent=2) + "\n" + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=_next_msg_id()) + + def _stage_a(raw: str) -> dict[str, Any]: + outer = _unwrap(raw, "STAGE_A_RAW") + stage_a = _dict(outer.get("stage_a_context")) + if stage_a.get("schema_version") != "task_c_bo_stage_a_context.v1" or stage_a.get("status") != "READY": + raise ValueError("Stage A must be READY task_c_bo_stage_a_context.v1") + return stage_a + + def _ledger(raw: str) -> dict[str, Any]: + outer = _unwrap(raw, "LEDGER_RAW") + ledger = _dict(outer.get("postb_seed_ledger")) + if ledger.get("schema_version") != "task_c_bo_postb_seed_ledger.v1" or ledger.get("status") != "READY": + _upf = ledger.get("failure") or {} + raise ValueError( + f"PostB_1 seed ledger not READY (status={ledger.get('status')!r}); " + f"upstream_step={_upf.get('failure_step')!r}; " + f"upstream_reason={_upf.get('failure_reason')!r}" + ) + return ledger + + def _compiled(raw: str) -> dict[str, Any]: + outer = _unwrap(raw, "COMPILED_RAW") + bundle = _dict(outer.get("postb_compiled_bo_bundle")) + if bundle.get("schema_version") != "task_c_bo_postb_compiled_bo_bundle.v1" or bundle.get("status") != "READY": + raise ValueError("PostB_3 final BO compiler must be READY task_c_bo_postb_compiled_bo_bundle.v1") + return bundle + + def _b4(raw: str) -> dict[str, Any]: + outer = _unwrap(raw, "B4_ACTIO_RAW") + root = _dict(outer.get("stage_b_domain_bo_seed_output")) + if root.get("schema_version") != "task_c_bo_stage_b_domain_bo_seed.v2": + raise ValueError("B4 Actio seed schema mismatch") + if root.get("domain_id") != "B4_Actio": + raise ValueError("B4 Actio domain_id mismatch") + if root.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("B4 Actio status must be READY or READY_WITH_REVIEW") + return root + + def _bo_items_from_localdocs() -> list[dict[str, Any]]: + data = _read_doc("BO.json") + if not isinstance(data, list): + raise ValueError("BO.json root must be array") + return [item for item in data if isinstance(item, dict)] + + def _bo_by_id(bo_items: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: + return {str(item.get("BO_ID") or item.get("id")): item for item in bo_items if item.get("BO_ID") or item.get("id")} + + def _candidate_mapping(bundle: dict[str, Any]) -> dict[str, str]: + mapping = _dict(bundle.get("candidate_ref_to_bo_id")) + return {str(k): str(v) for k, v in mapping.items() if k and v} + + def _candidate_to_bo(ledger: dict[str, Any], mapping: dict[str, str]) -> dict[str, dict[str, Any]]: + out: dict[str, dict[str, Any]] = {} + for item in _list(ledger.get("ledger_candidates")): + if isinstance(item, dict) and item.get("candidate_ref") in mapping: + out[str(item["candidate_ref"])] = item + return out + + def _source_evidence(value: Any) -> list[str]: + if isinstance(value, dict): + refs = _dict(value.get("source_refs")) + if refs: + return _strings(refs.get("source_evidence_indexes")) + return _strings(value.get("source_evidence_indexes")) + return [] + + def _seed_payload(candidate: dict[str, Any]) -> dict[str, Any]: + return _dict(candidate.get("seed_payload")) + + def _domain_payload_from_seed(seed: dict[str, Any]) -> dict[str, Any]: + return _dict(_dict(seed.get("extensions")).get("domain_payload")) + + def _domain_payload_from_bo(bo: dict[str, Any]) -> dict[str, Any]: + return _dict(_dict(bo.get("extensions")).get("domain_payload")) + + def _flatten_strings(value: Any, limit: int = 10) -> list[str]: + out: list[str] = [] + + def visit(node: Any) -> None: + if len(out) >= limit: + return + if node is None: + return + if isinstance(node, (str, int, float, bool)): + text = str(node).strip() + if text and text not in out: + out.append(text) + return + if isinstance(node, list): + for child in node: + visit(child) + return + if isinstance(node, dict): + for key in ( + "asset_label", "asset", "property", "object", "Object", "description", + "value", "name", "party", "beneficiary", "transferee", "debtor_act", + "remedy", "defense", "encumbrance", "date", "claim_type", + ): + if key in node: + visit(node.get(key)) + visit(value) + return out[:limit] + + def _asset_kind_from_text(text: str) -> str: + lowered = text.lower() + if any(key in text for key in ["부동산", "토지", "건물", "아파트", "대지", "등기", "근저당"]): + return "real_property" + if any(key in text for key in ["채권", "대여금", "매매대금", "임대차보증금"]): + return "claim" + if any(key in text for key in ["근저당", "담보", "저당", "질권", "유치권"]): + return "security_right" + if any(key in text for key in ["예금", "현금", "계좌", "deposit", "cash"]) or "bank" in lowered: + return "cash_or_deposit" + if any(key in text for key in ["영업", "사업", "상호", "business"]) or "company" in lowered: + return "business_asset" + if any(key in text for key in ["동산", "차량", "기계", "movable"]): + return "movable" + return "unknown" + + def _target_property_candidates(payload: dict[str, Any], bo_ids: list[str], evidence: list[str]) -> list[dict[str, Any]]: + sources = [ + payload.get("asset_bindings"), + payload.get("asset_binding_hint"), + payload.get("valuation_matrix"), + payload.get("fraudulent_transfer_facts"), + payload.get("encumbrance_timeline"), + payload.get("target_property_candidates"), + ] + out: list[dict[str, Any]] = [] + for source in sources: + for item in _as_list(source): + labels = _flatten_strings(item, limit=4) + if not labels and isinstance(item, dict): + labels = _strings(item.get("asset_label") or item.get("property_label")) + for label in labels: + candidate = { + "asset_label": label, + "asset_kind": _asset_kind_from_text(label), + "related_bo_ids": bo_ids, + "related_evidence_indexes": evidence, + } + key = json.dumps(candidate, ensure_ascii=False, sort_keys=True) + if key not in {json.dumps(x, ensure_ascii=False, sort_keys=True) for x in out}: + out.append(candidate) + return out + + def _preserved_claim_candidates(payload: dict[str, Any], mapping: dict[str, str], bo_ids: list[str], evidence: list[str]) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for item in _as_list(payload.get("preserved_claim_bundle") or payload.get("preserved_claim_candidates")): + obj = _dict(item) + ref = _first_string(obj.get("candidate_ref"), obj.get("bo_candidate_ref")) + bo_id = mapping.get(ref) if ref else _first_string(obj.get("bo_id")) + if bo_id not in bo_ids and bo_ids: + bo_id = bo_id or None + out.append({ + "bo_id": bo_id, + "claim_type_candidate": _first_string(obj.get("claim_type_candidate"), obj.get("claim_type"), obj.get("type"), obj.get("label")), + "creditor_candidate": _first_string(obj.get("creditor_candidate"), obj.get("creditor"), obj.get("plaintiff")), + "debtor_candidate": _first_string(obj.get("debtor_candidate"), obj.get("debtor"), obj.get("defendant")), + "claim_date_candidate": _first_string(obj.get("claim_date_candidate"), obj.get("claim_date"), obj.get("date"), obj.get("BehaviorTime")), + "related_evidence_indexes": _dedupe(evidence + _strings(obj.get("related_evidence_indexes") or obj.get("source_evidence_indexes"))), + }) + if not out and bo_ids: + out.append({ + "bo_id": bo_ids[0], + "claim_type_candidate": None, + "creditor_candidate": None, + "debtor_candidate": None, + "claim_date_candidate": None, + "related_evidence_indexes": evidence, + }) + return out + + def _role_hint(role: str, value: Any, date: Any, bo_ids: list[str], evidence: list[str]) -> dict[str, Any] | None: + if role not in ROLE_ENUM: + return None + value_candidate = _first_string(value) + date_candidate = _first_string(date) + if value_candidate is None and date_candidate is None: + return None + return { + "role": role, + "value_candidate": value_candidate, + "date_candidate": date_candidate, + "related_bo_ids": bo_ids, + "related_evidence_indexes": evidence, + } + + def _actio_role_hints(payload: dict[str, Any], seed: dict[str, Any], bo_ids: list[str], evidence: list[str]) -> list[dict[str, Any]]: + hints: list[dict[str, Any]] = [] + for fact in _as_list(payload.get("fraudulent_transfer_facts")): + obj = _dict(fact) + for hint in ( + _role_hint("fraudulent_act_date", obj.get("date") or obj.get("act_date"), obj.get("date") or obj.get("act_date"), bo_ids, evidence), + _role_hint("debtor_act", obj.get("debtor_act") or obj.get("action") or obj.get("description"), obj.get("date"), bo_ids, evidence), + ): + if hint: + hints.append(hint) + for party in _as_list(payload.get("beneficiary_transferee_hints")): + obj = _dict(party) + role_text = "transferee" if "transferee" in str(obj).lower() or "양수" in str(obj) else "beneficiary" + hint = _role_hint(role_text, obj.get("party") or obj.get("name") or obj.get("value") or obj, obj.get("date"), bo_ids, evidence) + if hint: + hints.append(hint) + for role, key in [ + ("remedy_or_cap", "remedy_mode_hints"), + ("defense", "defense_rebuttal"), + ("encumbrance", "encumbrance_timeline"), + ]: + for item in _as_list(payload.get(key)): + obj = _dict(item) + hint = _role_hint(role, obj or item, obj.get("date") if obj else None, bo_ids, evidence) + if hint: + hints.append(hint) + if not hints: + action = _first_string(seed.get("Action"), _dict(seed.get("core_field_base")).get("Action_proposal")) + hint = _role_hint("debtor_act", action, _dict(seed.get("core_field_base")).get("BehaviorTime"), bo_ids, evidence) + if hint: + hints.append(hint) + deduped: list[dict[str, Any]] = [] + seen: set[str] = set() + for hint in hints: + key = json.dumps(hint, ensure_ascii=False, sort_keys=True) + if key not in seen: + seen.add(key) + deduped.append(hint) + return deduped + + def _scopes(payload: dict[str, Any]) -> list[str]: + scopes: list[str] = [] + if payload.get("preserved_claim_bundle") or payload.get("preserved_claim_candidates"): + scopes.append("preserved_claim") + if payload.get("fraudulent_transfer_facts") or "actio" in " ".join(_strings(payload.get("legal_effect_tags"))).lower(): + scopes.append("fraudulent_act") + if payload.get("beneficiary_transferee_hints"): + scopes.append("beneficiary_or_transferee") + if payload.get("asset_bindings") or payload.get("asset_binding_hint") or payload.get("valuation_matrix"): + scopes.append("target_asset") + if payload.get("encumbrance_timeline"): + scopes.append("encumbrance") + if payload.get("remedy_mode_hints"): + scopes.append("remedy_or_cap") + if payload.get("defense_rebuttal"): + scopes.append("defense") + return [scope for scope in _dedupe(scopes) if scope in ACTIO_SCOPE_ENUM] + + def _suspicion_level(scopes: list[str]) -> str: + core = {"preserved_claim", "fraudulent_act", "beneficiary_or_transferee"} + if core.issubset(set(scopes)): + return "HIGH" + if {"preserved_claim", "fraudulent_act"}.issubset(set(scopes)): + return "MEDIUM" + if scopes: + return "LOW" + return "NONE" + + def _reasons(scopes: list[str], payload: dict[str, Any], b4_review_queue: list[Any]) -> str: + fragments: list[str] = [] + if "preserved_claim" in scopes: + fragments.append("피보전채권 후보가 B4 seed/domain payload에서 확인됨") + if "fraudulent_act" in scopes: + fragments.append("채무자 처분행위 또는 사해행위 후보 사실이 확인됨") + if "beneficiary_or_transferee" in scopes: + fragments.append("수익자 또는 전득자 후보가 확인됨") + if "target_asset" in scopes: + fragments.append("취소·원상회복 대상 재산 후보가 확인됨") + if "encumbrance" in scopes: + fragments.append("담보권·가압류 등 부담 관련 단서가 확인됨") + if "remedy_or_cap" in scopes: + fragments.append("원상회복 방법 또는 가액배상 한도 단서가 확인됨") + if "defense" in scopes: + fragments.append("항변 또는 선의·악의 관련 반박 단서가 확인됨") + for item in b4_review_queue[:2]: + if isinstance(item, dict): + reason = _first_string(item.get("reason"), item.get("issue"), item.get("review_reason")) + if reason: + fragments.append(f"B4 review queue: {reason}") + return ", ".join(fragments) if fragments else "사해행위취소 관련 구조화 단서가 충분히 확인되지 않음" + + def _validate(payload: dict[str, Any]) -> None: + if payload.get("schema_version") != "actio_case_signals.v1": + raise ValueError("schema_version mismatch") + if payload.get("status") not in {"READY", "READY_WITH_REVIEW", "EMPTY", "FAILED"}: + raise ValueError("invalid status") + signals = payload.get("actio_case_signals") + if not isinstance(signals, list): + raise ValueError("actio_case_signals must be array") + for idx, item in enumerate(signals): + if not isinstance(item, dict): + raise ValueError(f"signal[{idx}] must be object") + for key in [ + "signal_id", "actio_scope", "suspicion_level", "suspicion_reasons", + "related_bo_ids", "related_evidence_indexes", "target_property_candidates", + "preserved_claim_candidates", "actio_role_hints", + ]: + if key not in item: + raise ValueError(f"signal[{idx}] missing {key}") + if not isinstance(item["signal_id"], str): + raise ValueError(f"signal[{idx}].signal_id must be string") + if item["suspicion_level"] not in SUSPICION_LEVEL_ENUM: + raise ValueError(f"signal[{idx}].suspicion_level invalid") + if not isinstance(item["suspicion_reasons"], str): + raise ValueError(f"signal[{idx}].suspicion_reasons must be string") + if any(scope not in ACTIO_SCOPE_ENUM for scope in _list(item["actio_scope"])): + raise ValueError(f"signal[{idx}].actio_scope invalid") + for field in ["related_bo_ids", "related_evidence_indexes"]: + if not all(isinstance(x, str) for x in _list(item[field])): + raise ValueError(f"signal[{idx}].{field} must be string array") + for prop in _list(item["target_property_candidates"]): + if not isinstance(prop, dict): + raise ValueError(f"signal[{idx}].target_property_candidates item must be object") + for key in ["asset_label", "asset_kind", "related_bo_ids", "related_evidence_indexes"]: + if key not in prop: + raise ValueError(f"target_property_candidate missing {key}") + if prop["asset_kind"] not in ASSET_KIND_ENUM: + raise ValueError("target_property_candidate.asset_kind invalid") + for claim in _list(item["preserved_claim_candidates"]): + if not isinstance(claim, dict): + raise ValueError(f"signal[{idx}].preserved_claim_candidates item must be object") + for key in [ + "bo_id", "claim_type_candidate", "creditor_candidate", + "debtor_candidate", "claim_date_candidate", "related_evidence_indexes", + ]: + if key not in claim: + raise ValueError(f"preserved_claim_candidate missing {key}") + for hint in _list(item["actio_role_hints"]): + if not isinstance(hint, dict): + raise ValueError(f"signal[{idx}].actio_role_hints item must be object") + for key in ["role", "value_candidate", "date_candidate", "related_bo_ids", "related_evidence_indexes"]: + if key not in hint: + raise ValueError(f"actio_role_hint missing {key}") + if hint["role"] not in ROLE_ENUM: + raise ValueError("actio_role_hint.role invalid") + + def main() -> None: + try: + _init_mcp_session() + _stage_a(STAGE_A_RAW) + ledger = _ledger(LEDGER_RAW) + compiled = _compiled(COMPILED_RAW) + b4 = _b4(B4_ACTIO_RAW) + bo_items = _bo_items_from_localdocs() + bo_by_id = _bo_by_id(bo_items) + mapping = _candidate_mapping(compiled) + candidates_by_ref = _candidate_to_bo(ledger, mapping) + signals: list[dict[str, Any]] = [] + review_needed = False + + for seed_candidate in _list(b4.get("bo_seed_candidates")): + if not isinstance(seed_candidate, dict): + continue + candidate_ref = str(seed_candidate.get("candidate_ref") or "") + bo_id = mapping.get(candidate_ref) + bo_ids = [bo_id] if bo_id else [] + ledger_item = candidates_by_ref.get(candidate_ref, {}) + ledger_seed = _seed_payload(ledger_item) + seed = seed_candidate if seed_candidate else ledger_seed + payload = _domain_payload_from_seed(seed) + if not payload and ledger_seed: + payload = _domain_payload_from_seed(ledger_seed) + if bo_id and bo_id in bo_by_id: + payload = {**payload, **_domain_payload_from_bo(bo_by_id[bo_id])} + bo_evidence = _source_evidence(bo_by_id.get(bo_id, {})) if bo_id else [] + evidence = _dedupe(_source_evidence(seed_candidate) + _source_evidence(ledger_item) + bo_evidence) + scopes = _scopes(payload) + if not scopes and candidate_ref: + scopes = ["fraudulent_act"] + review_needed = True + related_bo_ids = _dedupe(bo_ids + [x for x in _strings(seed_candidate.get("related_bo_ids")) if x in bo_by_id]) + if not related_bo_ids: + review_needed = True + target_properties = _target_property_candidates(payload, related_bo_ids, evidence) + preserved_claims = _preserved_claim_candidates(payload, mapping, related_bo_ids, evidence) + role_hints = _actio_role_hints(payload, seed, related_bo_ids, evidence) + if not target_properties or not preserved_claims or not role_hints: + review_needed = True + if scopes or related_bo_ids or target_properties or preserved_claims or role_hints: + signals.append({ + "signal_id": f"actio-signal-{len(signals) + 1:03d}", + "actio_scope": scopes, + "suspicion_level": _suspicion_level(scopes), + "suspicion_reasons": _reasons(scopes, payload, _list(b4.get("domain_review_queue"))), + "related_bo_ids": related_bo_ids, + "related_evidence_indexes": evidence, + "target_property_candidates": target_properties, + "preserved_claim_candidates": preserved_claims, + "actio_role_hints": role_hints, + }) + + status = "EMPTY" if not signals else ("READY_WITH_REVIEW" if review_needed or b4.get("status") == "READY_WITH_REVIEW" else "READY") + payload = { + "schema_version": "actio_case_signals.v1", + "status": status, + "actio_case_signals": signals, + } + _validate(payload) + _write_doc("actio_case_signals.json", payload) + print(json.dumps({ + "middle_input_signal_writer": { + "schema_version": "stage1_middle_input_signal_writer.v1", + "status": "READY", + "written_file": "actio_case_signals.json", + "signal_count": len(signals), + "payload_status": status, + } + }, ensure_ascii=False)) + except Exception as exc: + print(json.dumps({ + "middle_input_signal_writer": { + "schema_version": "stage1_middle_input_signal_writer.v1", + "status": "FAILED", + "target_file": "actio_case_signals.json", + "failure_reason": str(exc), + } + }, ensure_ascii=False)) + sys.exit(0) + + if __name__ == "__main__": + main() + + - task_name: Task_C_BO_Middle_Input_case_liability_signals + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 120 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import itertools + import json + import sys + from typing import Any + + import httpx + + STAGE_A_RAW = r'''{{prev.Task_C_BO_Stage_A_input_normalization}}''' + LEDGER_RAW = r'''{{prev.Task_C_BO_PostB_1_seed_ledger_compiler}}''' + COMPILED_RAW = r'''{{prev.Task_C_BO_PostB_3_final_bo_compiler}}''' + B4_ACTIO_RAW = r'''{{prev.Task_C_BO_Stage_B_B4_Actio}}''' + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + LIABILITY_ENUM = { + "money_claim", "commercial_successor", "secured_debt", "registry_invalidity", + "land_use_gain", "actio_related", "succession_related", "notice_or_lien_related", + "defense_or_setoff", "other", + } + PARTY_ROLE_ENUM = {"plaintiff", "defendant", "creditor", "debtor", "liable_party"} + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + def _preview(raw: str, limit: int = 180) -> str: + return (raw or "").strip().replace("\n", "\\n")[:limit] + + def _extract_json_object(raw: str, label: str) -> dict[str, Any]: + text = (raw or "").strip() + if not text or text.startswith("{{"): + raise ValueError(f"{label}: unresolved or empty upstream payload; preview={_preview(raw)!r}") + start = text.find("{") + if start < 0: + raise ValueError(f"{label}: JSON object start not found; preview={_preview(raw)!r}") + candidate = text[start:].strip() + obj, end = JSON_DECODER.raw_decode(candidate) + tail = candidate[end:].strip() + if tail: + raise ValueError(f"{label}: non-json tail after object; preview={_preview(tail)!r}") + if not isinstance(obj, dict): + raise ValueError(f"{label}: JSON root must be object") + return obj + + def _unwrap(raw: str, label: str) -> dict[str, Any]: + obj = _extract_json_object(raw, label) + if obj.get("type") == "text" and isinstance(obj.get("text"), str): + obj = _extract_json_object(obj["text"], f"{label}.text") + if obj.get("success") is True and isinstance(obj.get("output"), str): + obj = _extract_json_object(obj["output"], f"{label}.output") + return obj + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def _first_string(*values: Any) -> str | None: + for value in values: + for item in _strings(value): + return item + return None + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-middle-input-case-liability-signal-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result.get("result", {}).get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + text = content[0].get("text", "") + envelope = json.loads(text) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + item = results[0] + if isinstance(item, dict) and item.get("error"): + raise RuntimeError(f"read_docs error for {doc_name}: {item.get('error')}") + raw = item.get("content", "") + else: + raw = envelope + if isinstance(raw, str): + return json.loads(raw) + return raw + + def _read_doc(doc_name: str) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + return _extract_json_from_read_docs_result(result, doc_name) + + def _write_doc(doc_name: str, payload: dict[str, Any]) -> None: + content = json.dumps(payload, ensure_ascii=False, indent=2) + "\n" + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=_next_msg_id()) + + def _stage_a(raw: str) -> dict[str, Any]: + outer = _unwrap(raw, "STAGE_A_RAW") + stage_a = _dict(outer.get("stage_a_context")) + if stage_a.get("schema_version") != "task_c_bo_stage_a_context.v1" or stage_a.get("status") != "READY": + raise ValueError("Stage A must be READY task_c_bo_stage_a_context.v1") + return stage_a + + def _ledger(raw: str) -> dict[str, Any]: + outer = _unwrap(raw, "LEDGER_RAW") + ledger = _dict(outer.get("postb_seed_ledger")) + if ledger.get("schema_version") != "task_c_bo_postb_seed_ledger.v1" or ledger.get("status") != "READY": + _upf = ledger.get("failure") or {} + raise ValueError( + f"PostB_1 seed ledger not READY (status={ledger.get('status')!r}); " + f"upstream_step={_upf.get('failure_step')!r}; " + f"upstream_reason={_upf.get('failure_reason')!r}" + ) + return ledger + + def _compiled(raw: str) -> dict[str, Any]: + outer = _unwrap(raw, "COMPILED_RAW") + bundle = _dict(outer.get("postb_compiled_bo_bundle")) + if bundle.get("schema_version") != "task_c_bo_postb_compiled_bo_bundle.v1" or bundle.get("status") != "READY": + raise ValueError("PostB_3 final BO compiler must be READY task_c_bo_postb_compiled_bo_bundle.v1") + return bundle + + def _b4(raw: str) -> dict[str, Any]: + outer = _unwrap(raw, "B4_ACTIO_RAW") + root = _dict(outer.get("stage_b_domain_bo_seed_output")) + if root.get("schema_version") != "task_c_bo_stage_b_domain_bo_seed.v2": + raise ValueError("B4 Actio seed schema mismatch") + if root.get("domain_id") != "B4_Actio": + raise ValueError("B4 Actio domain_id mismatch") + if root.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("B4 Actio status must be READY or READY_WITH_REVIEW") + return root + + def _bo_items_from_localdocs() -> list[dict[str, Any]]: + data = _read_doc("BO.json") + if not isinstance(data, list): + raise ValueError("BO.json root must be array") + return [item for item in data if isinstance(item, dict)] + + def _source_evidence(bo: dict[str, Any]) -> list[str]: + return _strings(bo.get("source_evidence_indexes")) + + def _domain_payload(bo: dict[str, Any]) -> dict[str, Any]: + return _dict(_dict(bo.get("extensions")).get("domain_payload")) + + def _candidate_mapping(bundle: dict[str, Any]) -> dict[str, str]: + mapping = _dict(bundle.get("candidate_ref_to_bo_id")) + return {str(k): str(v) for k, v in mapping.items() if k and v} + + def _candidate_ref_for_bo(bo_id: str, mapping: dict[str, str]) -> str | None: + for ref, mapped in mapping.items(): + if mapped == bo_id: + return ref + return None + + def _liability_type(bo: dict[str, Any]) -> str: + payload = _domain_payload(bo) + source_domain = str(_dict(bo.get("provenance")).get("source_domain") or "").lower() + text = " ".join([ + source_domain, + str(bo.get("Action") or ""), + str(bo.get("ActionType") or ""), + str(_dict(bo.get("JuristicAct")).get("label") or ""), + " ".join(_strings(bo.get("Legal_Keywords"))), + " ".join(_strings(payload.get("legal_effect_tags"))), + str(payload.get("domain_legal_effect_tag") or ""), + ]).lower() + if "b4" in source_domain or "actio" in text or "사해" in text: + return "actio_related" + if "successor" in text or "영업양수" in text or "상호속용" in text: + return "commercial_successor" + if "b1" in source_domain or "대여금" in text or "매매대금" in text or "채권" in text: + return "money_claim" + if "registry_invalid" in text or "등기무효" in text or "말소" in text: + return "registry_invalidity" + if "secured" in text or "근저당" in text or "담보" in text or "유치권" in text: + return "secured_debt" + if "land" in text or "valuation" in text or "사용이익" in text or "부당이득" in text: + return "land_use_gain" + if "succession" in text or "상속" in text or "승계" in text: + return "succession_related" + if "notice" in text or "lien" in text or "통지" in text or "유치권" in text: + return "notice_or_lien_related" + if "defense" in text or "항변" in text or "상계" in text: + return "defense_or_setoff" + return "other" + + def _normalize_role(value: Any) -> str | None: + text = str(value or "").strip().lower() + if not text: + return None + if text in PARTY_ROLE_ENUM: + return text + if "plaintiff" in text or "원고" in text: + return "plaintiff" + if "defendant" in text or "피고" in text: + return "defendant" + if "creditor" in text or "채권자" in text: + return "creditor" + if "debtor" in text or "채무자" in text: + return "debtor" + if "liable" in text or "책임" in text or "이행" in text: + return "liable_party" + return None + + def _party_roles(bo: dict[str, Any]) -> list[dict[str, Any]]: + bo_id = str(bo.get("BO_ID") or bo.get("id") or "") + payload = _domain_payload(bo) + roles: list[dict[str, Any]] = [] + for item in _as_list(payload.get("party_roles") or payload.get("parties_hint") or payload.get("liability_party_hints")): + obj = _dict(item) + party = _first_string(obj.get("party"), obj.get("name"), obj.get("value")) + role = _normalize_role(obj.get("role") or obj.get("party_role")) + if party and role: + roles.append({"party": party, "role": role, "bo_ids": [bo_id] if bo_id else []}) + core = _dict(bo.get("core_field_base")) + performer = _first_string(core.get("Performer"), bo.get("Performer")) + subject = _first_string(core.get("Subject"), bo.get("Subject")) + if performer and not any(item["party"] == performer for item in roles): + roles.append({"party": performer, "role": "liable_party", "bo_ids": [bo_id] if bo_id else []}) + if subject and not any(item["party"] == subject for item in roles): + role = "creditor" if _liability_type(bo) in {"money_claim", "actio_related"} else "liable_party" + roles.append({"party": subject, "role": role, "bo_ids": [bo_id] if bo_id else []}) + deduped: list[dict[str, Any]] = [] + seen: set[str] = set() + for item in roles: + key = json.dumps(item, ensure_ascii=False, sort_keys=True) + if key not in seen: + seen.add(key) + deduped.append(item) + return deduped + + def _claim_group_id(bo: dict[str, Any]) -> str | None: + refs = _dict(bo.get("downstream_seed_refs")) + return _first_string(refs.get("claim_group_seed_refs_proposed") or refs.get("claim_group_seed_refs")) + + def _liability_group_id(bo: dict[str, Any], candidate_ref: str | None) -> str | None: + payload = _domain_payload(bo) + return _first_string(payload.get("liability_group_id"), payload.get("liability_group_ref"), candidate_ref) + + def _linked_structure_ids(bo: dict[str, Any]) -> list[str]: + refs = _dict(bo.get("downstream_seed_refs")) + return _strings(refs.get("legal_effect_structure_seed_ref_proposed") or refs.get("linked_structure_ids")) + + def _validate(payload: dict[str, Any]) -> None: + if payload.get("schema_version") != "case_liability_signals.v1": + raise ValueError("schema_version mismatch") + if payload.get("status") not in {"READY", "READY_WITH_REVIEW", "EMPTY", "FAILED"}: + raise ValueError("invalid status") + signals = payload.get("case_liability_signals") + if not isinstance(signals, list): + raise ValueError("case_liability_signals must be array") + for idx, item in enumerate(signals): + if not isinstance(item, dict): + raise ValueError(f"signal[{idx}] must be object") + for key in [ + "liability_candidate_type", "related_bo_ids", "related_evidence_indexes", + "party_roles", "claim_group_id", "liability_group_id", "linked_structure_ids", + ]: + if key not in item: + raise ValueError(f"signal[{idx}] missing {key}") + if item["liability_candidate_type"] not in LIABILITY_ENUM: + raise ValueError(f"signal[{idx}].liability_candidate_type invalid") + for field in ["related_bo_ids", "related_evidence_indexes", "linked_structure_ids"]: + if not isinstance(item[field], list) or not all(isinstance(x, str) for x in item[field]): + raise ValueError(f"signal[{idx}].{field} must be string array") + for role in _list(item["party_roles"]): + if not isinstance(role, dict): + raise ValueError(f"signal[{idx}].party_roles item must be object") + for key in ["party", "role", "bo_ids"]: + if key not in role: + raise ValueError(f"party_role missing {key}") + if not isinstance(role["party"], str) or role["role"] not in PARTY_ROLE_ENUM: + raise ValueError("party_role type invalid") + if not isinstance(role["bo_ids"], list) or not all(isinstance(x, str) for x in role["bo_ids"]): + raise ValueError("party_role.bo_ids must be string array") + + def main() -> None: + try: + _init_mcp_session() + _stage_a(STAGE_A_RAW) + _ledger(LEDGER_RAW) + compiled = _compiled(COMPILED_RAW) + b4 = _b4(B4_ACTIO_RAW) + mapping = _candidate_mapping(compiled) + bo_items = _bo_items_from_localdocs() + signals: list[dict[str, Any]] = [] + review_needed = b4.get("status") == "READY_WITH_REVIEW" + for bo in bo_items: + bo_id = str(bo.get("BO_ID") or bo.get("id") or "") + if not bo_id: + review_needed = True + continue + candidate_ref = _candidate_ref_for_bo(bo_id, mapping) + party_roles = _party_roles(bo) + if not party_roles: + review_needed = True + liability_group_id = _liability_group_id(bo, candidate_ref) + if liability_group_id is None: + review_needed = True + signal = { + "liability_candidate_type": _liability_type(bo), + "related_bo_ids": [bo_id], + "related_evidence_indexes": _source_evidence(bo), + "party_roles": party_roles, + "claim_group_id": _claim_group_id(bo), + "liability_group_id": liability_group_id, + "linked_structure_ids": _linked_structure_ids(bo), + } + signals.append(signal) + status = "EMPTY" if not signals else ("READY_WITH_REVIEW" if review_needed else "READY") + payload = { + "schema_version": "case_liability_signals.v1", + "status": status, + "case_liability_signals": signals, + } + _validate(payload) + _write_doc("case_liability_signals.json", payload) + print(json.dumps({ + "middle_input_signal_writer": { + "schema_version": "stage1_middle_input_signal_writer.v1", + "status": "READY", + "written_file": "case_liability_signals.json", + "signal_count": len(signals), + "payload_status": status, + } + }, ensure_ascii=False)) + except Exception as exc: + print(json.dumps({ + "middle_input_signal_writer": { + "schema_version": "stage1_middle_input_signal_writer.v1", + "status": "FAILED", + "target_file": "case_liability_signals.json", + "failure_reason": str(exc), + } + }, ensure_ascii=False)) + sys.exit(0) + + if __name__ == "__main__": + main() + + - task_name: Task_C_BO_Middle_Input_legal_effect_signals + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 120 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import itertools + import json + import sys + from typing import Any + + import httpx + + STAGE_A_RAW = r'''{{prev.Task_C_BO_Stage_A_input_normalization}}''' + LEDGER_RAW = r'''{{prev.Task_C_BO_PostB_1_seed_ledger_compiler}}''' + COMPILED_RAW = r'''{{prev.Task_C_BO_PostB_3_final_bo_compiler}}''' + B4_ACTIO_RAW = r'''{{prev.Task_C_BO_Stage_B_B4_Actio}}''' + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + def _preview(raw: str, limit: int = 180) -> str: + return (raw or "").strip().replace("\n", "\\n")[:limit] + + def _extract_json_object(raw: str, label: str) -> dict[str, Any]: + text = (raw or "").strip() + if not text or text.startswith("{{"): + raise ValueError(f"{label}: unresolved or empty upstream payload; preview={_preview(raw)!r}") + start = text.find("{") + if start < 0: + raise ValueError(f"{label}: JSON object start not found; preview={_preview(raw)!r}") + candidate = text[start:].strip() + obj, end = JSON_DECODER.raw_decode(candidate) + tail = candidate[end:].strip() + if tail: + raise ValueError(f"{label}: non-json tail after object; preview={_preview(tail)!r}") + if not isinstance(obj, dict): + raise ValueError(f"{label}: JSON root must be object") + return obj + + def _unwrap(raw: str, label: str) -> dict[str, Any]: + obj = _extract_json_object(raw, label) + if obj.get("type") == "text" and isinstance(obj.get("text"), str): + obj = _extract_json_object(obj["text"], f"{label}.text") + if obj.get("success") is True and isinstance(obj.get("output"), str): + obj = _extract_json_object(obj["output"], f"{label}.output") + return obj + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-middle-input-legal-effect-signal-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result.get("result", {}).get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + text = content[0].get("text", "") + envelope = json.loads(text) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + item = results[0] + if isinstance(item, dict) and item.get("error"): + raise RuntimeError(f"read_docs error for {doc_name}: {item.get('error')}") + raw = item.get("content", "") + else: + raw = envelope + if isinstance(raw, str): + return json.loads(raw) + return raw + + def _read_doc(doc_name: str) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + return _extract_json_from_read_docs_result(result, doc_name) + + def _write_doc(doc_name: str, payload: dict[str, Any]) -> None: + content = json.dumps(payload, ensure_ascii=False, indent=2) + "\n" + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=_next_msg_id()) + + def _stage_a(raw: str) -> dict[str, Any]: + outer = _unwrap(raw, "STAGE_A_RAW") + stage_a = _dict(outer.get("stage_a_context")) + if stage_a.get("schema_version") != "task_c_bo_stage_a_context.v1" or stage_a.get("status") != "READY": + raise ValueError("Stage A must be READY task_c_bo_stage_a_context.v1") + return stage_a + + def _ledger(raw: str) -> dict[str, Any]: + outer = _unwrap(raw, "LEDGER_RAW") + ledger = _dict(outer.get("postb_seed_ledger")) + if ledger.get("schema_version") != "task_c_bo_postb_seed_ledger.v1" or ledger.get("status") != "READY": + _upf = ledger.get("failure") or {} + raise ValueError( + f"PostB_1 seed ledger not READY (status={ledger.get('status')!r}); " + f"upstream_step={_upf.get('failure_step')!r}; " + f"upstream_reason={_upf.get('failure_reason')!r}" + ) + return ledger + + def _compiled(raw: str) -> dict[str, Any]: + outer = _unwrap(raw, "COMPILED_RAW") + bundle = _dict(outer.get("postb_compiled_bo_bundle")) + if bundle.get("schema_version") != "task_c_bo_postb_compiled_bo_bundle.v1" or bundle.get("status") != "READY": + raise ValueError("PostB_3 final BO compiler must be READY task_c_bo_postb_compiled_bo_bundle.v1") + return bundle + + def _b4(raw: str) -> dict[str, Any]: + outer = _unwrap(raw, "B4_ACTIO_RAW") + root = _dict(outer.get("stage_b_domain_bo_seed_output")) + if root.get("schema_version") != "task_c_bo_stage_b_domain_bo_seed.v2": + raise ValueError("B4 Actio seed schema mismatch") + if root.get("domain_id") != "B4_Actio": + raise ValueError("B4 Actio domain_id mismatch") + if root.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("B4 Actio status must be READY or READY_WITH_REVIEW") + return root + + def _bo_items_from_localdocs() -> list[dict[str, Any]]: + data = _read_doc("BO.json") + if not isinstance(data, list): + raise ValueError("BO.json root must be array") + return [item for item in data if isinstance(item, dict)] + + def _domain_payload(bo: dict[str, Any]) -> dict[str, Any]: + return _dict(_dict(bo.get("extensions")).get("domain_payload")) + + def _source_evidence(bo: dict[str, Any]) -> list[str]: + return _strings(bo.get("source_evidence_indexes")) + + def _candidate_structure_types(bo: dict[str, Any]) -> list[str]: + payload = _domain_payload(bo) + juristic = _dict(bo.get("JuristicAct")) + candidates: list[str] = [] + candidates.extend(_strings(payload.get("candidate_structure_types"))) + candidates.extend(_strings(payload.get("legal_effect_tags"))) + candidates.extend(_strings(payload.get("domain_legal_effect_tag"))) + candidates.extend(_strings(bo.get("Legal_Keywords"))) + candidates.extend(_strings(bo.get("BOType"))) + candidates.extend(_strings(bo.get("ActionType"))) + candidates.extend(_strings(juristic.get("label"))) + source_domain = _dict(bo.get("provenance")).get("source_domain") + if source_domain: + candidates.append(str(source_domain)) + return _dedupe(candidates) or ["generic_legal_effect"] + + def _linked_structure_ids(bo: dict[str, Any]) -> list[str]: + refs = _dict(bo.get("downstream_seed_refs")) + payload = _domain_payload(bo) + return _dedupe( + _strings(refs.get("legal_effect_structure_seed_ref_proposed")) + + _strings(refs.get("linked_structure_ids")) + + _strings(payload.get("linked_structure_ids")) + ) + + def _validate(payload: dict[str, Any]) -> None: + if payload.get("schema_version") != "legal_effect_signals.v1": + raise ValueError("schema_version mismatch") + if payload.get("status") not in {"READY", "READY_WITH_REVIEW", "EMPTY", "FAILED"}: + raise ValueError("invalid status") + routes = payload.get("bo_legal_effect_routes") + if not isinstance(routes, list): + raise ValueError("bo_legal_effect_routes must be array") + for idx, item in enumerate(routes): + if not isinstance(item, dict): + raise ValueError(f"route[{idx}] must be object") + for key in ["bo_id", "candidate_structure_types", "linked_structure_ids", "related_evidence_indexes"]: + if key not in item: + raise ValueError(f"route[{idx}] missing {key}") + if not isinstance(item["bo_id"], str): + raise ValueError(f"route[{idx}].bo_id must be string") + for field in ["candidate_structure_types", "linked_structure_ids", "related_evidence_indexes"]: + if not isinstance(item[field], list) or not all(isinstance(x, str) for x in item[field]): + raise ValueError(f"route[{idx}].{field} must be string array") + + def main() -> None: + try: + _init_mcp_session() + _stage_a(STAGE_A_RAW) + _ledger(LEDGER_RAW) + _compiled(COMPILED_RAW) + b4 = _b4(B4_ACTIO_RAW) + bo_items = _bo_items_from_localdocs() + routes: list[dict[str, Any]] = [] + review_needed = b4.get("status") == "READY_WITH_REVIEW" + for bo in bo_items: + bo_id = str(bo.get("BO_ID") or bo.get("id") or "") + if not bo_id: + review_needed = True + continue + linked = _linked_structure_ids(bo) + if not linked: + review_needed = True + routes.append({ + "bo_id": bo_id, + "candidate_structure_types": _candidate_structure_types(bo), + "linked_structure_ids": linked, + "related_evidence_indexes": _source_evidence(bo), + }) + status = "EMPTY" if not routes else ("READY_WITH_REVIEW" if review_needed else "READY") + payload = { + "schema_version": "legal_effect_signals.v1", + "status": status, + "bo_legal_effect_routes": routes, + } + _validate(payload) + _write_doc("legal_effect_signals.json", payload) + print(json.dumps({ + "middle_input_signal_writer": { + "schema_version": "stage1_middle_input_signal_writer.v1", + "status": "READY", + "written_file": "legal_effect_signals.json", + "route_count": len(routes), + "payload_status": status, + } + }, ensure_ascii=False)) + except Exception as exc: + print(json.dumps({ + "middle_input_signal_writer": { + "schema_version": "stage1_middle_input_signal_writer.v1", + "status": "FAILED", + "target_file": "legal_effect_signals.json", + "failure_reason": str(exc), + } + }, ensure_ascii=False)) + sys.exit(0) + + if __name__ == "__main__": + main() + + task_procedure: + IN: + nexts: + - Task_C_BO_Stage_A_input_normalization + wait_until: [] + Task_C_BO_Stage_A_input_normalization: + nexts: + - Task_C_BO_Stage_B0_input_money_successor + - Task_C_BO_Stage_B0_input_secured_registry + - Task_C_BO_Stage_B0_input_land_valuation + - Task_C_BO_Stage_B0_input_actio + - Task_C_BO_Stage_B0_input_succession_notice_lien_asset_defense + wait_until: + - IN + Task_C_BO_Stage_B0_input_money_successor: + nexts: + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + wait_until: + - Task_C_BO_Stage_A_input_normalization + Task_C_BO_Stage_B0_input_secured_registry: + nexts: + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + wait_until: + - Task_C_BO_Stage_A_input_normalization + Task_C_BO_Stage_B0_input_land_valuation: + nexts: + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + wait_until: + - Task_C_BO_Stage_A_input_normalization + Task_C_BO_Stage_B0_input_actio: + nexts: + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + wait_until: + - Task_C_BO_Stage_A_input_normalization + Task_C_BO_Stage_B0_input_succession_notice_lien_asset_defense: + nexts: + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + wait_until: + - Task_C_BO_Stage_A_input_normalization + Task_C_BO_Stage_B_B1_Money_Successor: + nexts: + - Task_C_BO_Stage_B_worker_output_publisher + wait_until: + - Task_C_BO_Stage_B0_input_money_successor + - Task_C_BO_Stage_B0_input_secured_registry + - Task_C_BO_Stage_B0_input_land_valuation + - Task_C_BO_Stage_B0_input_actio + - Task_C_BO_Stage_B0_input_succession_notice_lien_asset_defense + Task_C_BO_Stage_B_B2_Secured_Registry: + nexts: + - Task_C_BO_Stage_B_worker_output_publisher + wait_until: + - Task_C_BO_Stage_B0_input_money_successor + - Task_C_BO_Stage_B0_input_secured_registry + - Task_C_BO_Stage_B0_input_land_valuation + - Task_C_BO_Stage_B0_input_actio + - Task_C_BO_Stage_B0_input_succession_notice_lien_asset_defense + Task_C_BO_Stage_B_B3_Land_Valuation: + nexts: + - Task_C_BO_Stage_B_worker_output_publisher + wait_until: + - Task_C_BO_Stage_B0_input_money_successor + - Task_C_BO_Stage_B0_input_secured_registry + - Task_C_BO_Stage_B0_input_land_valuation + - Task_C_BO_Stage_B0_input_actio + - Task_C_BO_Stage_B0_input_succession_notice_lien_asset_defense + Task_C_BO_Stage_B_B4_Actio: + nexts: + - Task_C_BO_Stage_B_worker_output_publisher + wait_until: + - Task_C_BO_Stage_B0_input_money_successor + - Task_C_BO_Stage_B0_input_secured_registry + - Task_C_BO_Stage_B0_input_land_valuation + - Task_C_BO_Stage_B0_input_actio + - Task_C_BO_Stage_B0_input_succession_notice_lien_asset_defense + Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense: + nexts: + - Task_C_BO_Stage_B_worker_output_publisher + wait_until: + - Task_C_BO_Stage_B0_input_money_successor + - Task_C_BO_Stage_B0_input_secured_registry + - Task_C_BO_Stage_B0_input_land_valuation + - Task_C_BO_Stage_B0_input_actio + - Task_C_BO_Stage_B0_input_succession_notice_lien_asset_defense + Task_C_BO_Stage_B_worker_output_publisher: + nexts: + - Task_C_BO_Stage_B_domain_join + wait_until: + - Task_C_BO_Stage_A_input_normalization + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + Task_C_BO_Stage_B_domain_join: + nexts: + - Task_C_BO_PostB_1_seed_ledger_compiler + wait_until: + - Task_C_BO_Stage_A_input_normalization + - Task_C_BO_Stage_B_worker_output_publisher + Task_C_BO_PostB_1_seed_ledger_compiler: + nexts: + - Task_C_BO_PostB_2_single_exception_adjudicator + wait_until: + - Task_C_BO_Stage_A_input_normalization + - Task_C_BO_Stage_B_domain_join + Task_C_BO_PostB_2_single_exception_adjudicator: + nexts: + - Task_C_BO_PostB_3_final_bo_compiler + wait_until: + - Task_C_BO_PostB_1_seed_ledger_compiler + Task_C_BO_PostB_3_final_bo_compiler: + nexts: + - Task_C_BO_PostB_4_final_gate_and_writer + wait_until: + - Task_C_BO_Stage_A_input_normalization + - Task_C_BO_PostB_1_seed_ledger_compiler + - Task_C_BO_PostB_2_single_exception_adjudicator + Task_C_BO_PostB_4_final_gate_and_writer: + nexts: + - Task_C_BO_Middle_Input_actio_case_signals + - Task_C_BO_Middle_Input_case_liability_signals + - Task_C_BO_Middle_Input_legal_effect_signals + wait_until: + - Task_C_BO_Stage_A_input_normalization + - Task_C_BO_PostB_3_final_bo_compiler + Task_C_BO_Middle_Input_actio_case_signals: + nexts: + - OUT + wait_until: + - Task_C_BO_Stage_A_input_normalization + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_PostB_1_seed_ledger_compiler + - Task_C_BO_PostB_3_final_bo_compiler + - Task_C_BO_PostB_4_final_gate_and_writer + Task_C_BO_Middle_Input_case_liability_signals: + nexts: + - OUT + wait_until: + - Task_C_BO_Stage_A_input_normalization + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_PostB_1_seed_ledger_compiler + - Task_C_BO_PostB_3_final_bo_compiler + - Task_C_BO_PostB_4_final_gate_and_writer + Task_C_BO_Middle_Input_legal_effect_signals: + nexts: + - OUT + wait_until: + - Task_C_BO_Stage_A_input_normalization + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_PostB_1_seed_ledger_compiler + - Task_C_BO_PostB_3_final_bo_compiler + - Task_C_BO_PostB_4_final_gate_and_writer + OUT: + nexts: [] + wait_until: + - Task_C_BO_Middle_Input_actio_case_signals + - Task_C_BO_Middle_Input_case_liability_signals + - Task_C_BO_Middle_Input_legal_effect_signals diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_3_Claude_v2.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_3_Claude_v2.yml new file mode 100644 index 00000000..38cd3b8a --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_3_Claude_v2.yml @@ -0,0 +1,2250 @@ +--- +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_3 + description: 민사소송 원고 송무 초지능 AI변호사 - Legal Effect 생성 + version: v2.0-claude + Stages: + - name: stage1_legal_effect_생성 + description: Legal Effect Structure 작성 + tools: + mcpServers: + localdocs: + type: streamable-http + url: http://mcp-localdocs:8012/mcp + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + task_procedure: + IN: + nexts: + - Task_L0_structure_seed_reducer + wait_until: [] + Task_L0_structure_seed_reducer: + nexts: + - Task_L1_exception_adjudicator + wait_until: + - IN + Task_L1_exception_adjudicator: + nexts: + - Task_L2_final_structure_index_writer + wait_until: + - Task_L0_structure_seed_reducer + Task_L2_final_structure_index_writer: + nexts: + - OUT + wait_until: + - Task_L1_exception_adjudicator + OUT: + nexts: [] + wait_until: + - Task_L2_final_structure_index_writer + + tasks: + - task_name: Task_L0_structure_seed_reducer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + import sys + from collections import defaultdict + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + INPUT_FILES = [ + "BO.json", + "evidence_indexed.json", + "actio_case_signals.json", + "case_liability_signals.json", + "legal_effect_signals.json", + ] + SEED_OUT_PATH = "stage1_tmp/legal_effect_structure_seed_bundle.json" + PACK_OUT_PATH = "stage1_tmp/legal_effect_structure_exception_pack.json" + AUDIT_OUT_PATH = "stage1_tmp/legal_effect_structure_audit.json" + MAX_EXCERPT = 700 + ISSUE_THRESHOLD = 3 + ISSUE_MARGIN = 2 + STRUCTURE_TYPE_PRIORITY = [ + "money_claim", + "commercial_successor", + "secured_debt", + "registry_invalidity", + "land_use_gain", + "valuation", + "fraudulent_transfer", + "preserved_claim", + "actio_remedy_or_cap", + "succession_notice_lien_asset_defense", + "general_legal_effect", + ] + # X-1: closed canonical routing enum — 이 값만 candidate_structure_types에 들어간다. + CANONICAL_TYPE_SET = set(STRUCTURE_TYPE_PRIORITY) + DOWNSTREAM_ROLE_FAMILY = { + "money_claim": "claim_chain_ref", + "commercial_successor": "claim_chain_ref", + "secured_debt": "linked_structures", + "registry_invalidity": "linked_structures", + "land_use_gain": "legal_calculation_object", + "valuation": "legal_calculation_object", + "fraudulent_transfer": "actio_roles", + "preserved_claim": "claim_chain_ref", + "actio_remedy_or_cap": "linked_actio_structures", + "succession_notice_lien_asset_defense": "linked_structures", + "general_legal_effect": "legal_effect_roles", + } + ISSUE_KEYWORDS = { + "money_claim": ["money", "loan", "대여금", "매매대금", "미지급", "채권", "변제"], + "commercial_successor": ["successor", "상호속용", "영업양수", "승계"], + "secured_debt": ["secured", "담보", "근저당", "피담보", "저당"], + "registry_invalidity": ["registry", "등기", "말소", "무효", "소유권이전"], + "land_use_gain": ["land", "사용이익", "임료", "점유", "인도"], + "valuation": ["valuation", "감정", "평가", "가액", "시가"], + "actio": ["fraudulent", "actio", "사해", "수익자", "전득자", "피보전", "가액배상"], + "asset_defense": ["notice", "lien", "통지", "유치권", "항변", "방어"], + } + HARD_ISSUE_KEY_PRIORITY = ["claim_group_ids", "liability_group_ids", + "canonical_theory_graph_seed_ref", "linked_structure_ids"] + # X-4 방어 상한 (자동 차단 금지 — 초과분은 deterministic_review로 라우팅) + PACK_MAX_ITEMS = 30 + PACK_MAX_BYTES = 40000 + PACK_ITEM_CHAR_LIMIT = 2000 + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-legal-effect-l0-structure-seed-reducer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + + def _loads_tolerant(raw: str, label: str) -> Any: + """SKILL §6.4: strict json.loads 실패 시 raw_decode fallback (read_docs Extra-data 대응).""" + try: + return json.loads(raw) + except json.JSONDecodeError: + try: + obj, _ = JSON_DECODER.raw_decode(raw.strip()) + return obj + except json.JSONDecodeError as exc: + raise RuntimeError(f"Content of {label} is not valid JSON: {raw[:300]}") from exc + + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result.get("result", {}).get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + text = content[0].get("text", "") + envelope = _loads_tolerant(text, doc_name) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + item = results[0] + if isinstance(item, dict) and item.get("error"): + raise RuntimeError(f"read_docs error for {doc_name}: {item.get('error')}") + raw = item.get("content", "") + else: + raw = envelope + if isinstance(raw, str): + return _loads_tolerant(raw, doc_name) + return raw + + + def _read_doc(doc_name: str) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + return _extract_json_from_read_docs_result(result, doc_name) + + + def _write_doc(doc_name: str, payload: dict[str, Any]) -> str: + content = json.dumps(payload, ensure_ascii=False, indent=2) + "\n" + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=_next_msg_id()) + return content + + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + if value is None: + continue + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + + def _first_string(*values: Any) -> str | None: + for value in values: + for item in _strings(value): + return item + return None + + + def _short(value: Any, limit: int = MAX_EXCERPT) -> str | None: + if value is None: + return None + text = re.sub(r"\s+", " ", str(value)).strip() + if not text: + return None + return text[:limit] + + + def _norm_text(text: Any) -> str: + value = re.sub(r"\s+", "", str(text or "").lower()) + value = re.sub(r"[()\[\]{}'\"·,.;:]", "", value) + value = re.sub(r"(에관하여|에대한|및|의|을|를|은|는|이|가)$", "", value) + return value + + + def _asset_norm(label: str) -> str: + text = _norm_text(label) + text = re.sub(r"(지분|전체|소유권|목적물)$", "", text) + return text + + + def _legal_unit_tokens(label: str) -> list[str]: + tokens = [] + for token in ["대지", "토지", "건물", "지분", "근저당", "담보", "점유", "사용이익", "상가", "임야", "채권"]: + if token in str(label): + tokens.append(token) + return tokens + + + def _make_id(prefix: str, ordinal: int) -> str: + return f"{prefix}-{ordinal:03d}" + + + # ---------------------------------------------------------------- 컴파일 (v1 LES0 이식) + def _evidence_index_from_item(item: dict[str, Any]) -> str | None: + return _first_string( + item.get("evidence_index"), + item.get("evidence_id"), + item.get("index"), + item.get("id"), + item.get("source_id"), + ) + + + def _evidence_authority_item(index: str, item: dict[str, Any], source: str) -> dict[str, Any]: + return { + "evidence_index": index, + "source_title": _first_string(item.get("source_title"), item.get("title"), item.get("evidence_title"), item.get("document_title"), index), + "priority_class": _first_string(item.get("priority_class"), item.get("priority"), item.get("evidence_priority")), + "authentication_status": _first_string(item.get("authentication_status"), item.get("auth_status"), item.get("authentication")), + "excerpt": _short(item.get("relevant_content") or item.get("excerpt") or item.get("summary") or item.get("content") or item.get("text")), + "source_file": source, + } + + + def _collect_evidence_authority(evidence_indexed: Any, bo_items: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: + """v1 LES0 이식 — evidence_all 입력 제거판 (포함/제외 산출 동일 실측: 개정 보고서 P3-3).""" + out: dict[str, dict[str, Any]] = {} + + def add(index: str | None, item: dict[str, Any], source: str) -> None: + if not index: + return + new = _evidence_authority_item(index, item, source) + old = out.get(index, {}) + merged = {**new, **{k: v for k, v in old.items() if v not in (None, "", [])}} + if old.get("excerpt") and len(str(old.get("excerpt"))) >= len(str(new.get("excerpt") or "")): + merged["excerpt"] = old.get("excerpt") + out[index] = merged + + def visit(node: Any, source: str) -> None: + if isinstance(node, dict): + idx = _evidence_index_from_item(node) + if idx: + add(idx, node, source) + for key in ("items", "evidence", "documents", "records", "results", "events"): + for child in _list(node.get(key)): + visit(child, source) + elif isinstance(node, list): + for child in node: + visit(child, source) + + visit(evidence_indexed, "evidence_indexed.json") + for bo in bo_items: + for ev in _list(bo.get("Evidence")): + if isinstance(ev, dict): + add(_evidence_index_from_item(ev), ev, "BO.json.Evidence") + return out + + + def _domain_payload(bo: dict[str, Any]) -> dict[str, Any]: + return _dict(_dict(bo.get("extensions")).get("domain_payload")) + + + def _compact_bo(bo: dict[str, Any]) -> dict[str, Any]: + payload = _domain_payload(bo) + core = _dict(bo.get("core_field_base")) + evidence_indexes = _dedupe(_strings(bo.get("source_evidence_indexes")) + [_first_string(ev.get("evidence_index")) for ev in _list(bo.get("Evidence")) if isinstance(ev, dict)]) + evidence_indexes = [idx for idx in evidence_indexes if idx] + return { + "BO_ID": str(bo.get("BO_ID") or bo.get("id") or ""), + "id": str(bo.get("id") or bo.get("BO_ID") or ""), + "BOType": bo.get("BOType"), + "ActionType": bo.get("ActionType"), + "JuristicAct": _dict(bo.get("JuristicAct")), + "Action": bo.get("Action"), + "Reason": bo.get("Reason"), + "PriorAct": bo.get("PriorAct"), + "ReasonRefs": _strings(bo.get("ReasonRefs")), + "Legal_Keywords": _strings(bo.get("Legal_Keywords")), + "core_field_base": core, + "amount": bo.get("amount"), + "source_evidence_indexes": evidence_indexes, + "provenance": _dict(bo.get("provenance")), + "downstream_seed_refs": _dict(bo.get("downstream_seed_refs")), + "domain_payload": { + "domain_legal_effect_tag": payload.get("domain_legal_effect_tag"), + "legal_effect_tags": _strings(payload.get("legal_effect_tags")), + "asset_bindings": _as_list(payload.get("asset_bindings")), + "asset_binding_hint": payload.get("asset_binding_hint"), + "candidate_details": payload.get("candidate_details"), + "facts": payload.get("facts"), + "parties_hint": _as_list(payload.get("parties_hint")), + "action_summary": payload.get("action_summary"), + "title_hint": payload.get("title_hint"), + "summary_hint": payload.get("summary_hint"), + "event_state_type": payload.get("event_state_type"), + "asset_alias": _as_list(payload.get("asset_alias")), + }, + } + + + def _actio_signals_by_bo(data: Any) -> dict[str, list[dict[str, Any]]]: + root = _dict(data) + out: dict[str, list[dict[str, Any]]] = {} + for sig in _list(root.get("actio_case_signals")): + if not isinstance(sig, dict): + continue + compact = { + "signal_id": _first_string(sig.get("signal_id")), + "actio_scope": _strings(sig.get("actio_scope")), + "suspicion_level": sig.get("suspicion_level"), + "related_evidence_indexes": _strings(sig.get("related_evidence_indexes")), + "target_property_candidates": _list(sig.get("target_property_candidates")), + "preserved_claim_candidates": _list(sig.get("preserved_claim_candidates")), + "actio_role_hints": _list(sig.get("actio_role_hints")), + } + for bo_id in _strings(sig.get("related_bo_ids")): + out.setdefault(bo_id, []).append(compact) + for item in _list(sig.get("target_property_candidates")): + if isinstance(item, dict): + for bo_id in _strings(item.get("related_bo_ids")): + out.setdefault(bo_id, []).append(compact) + return out + + + def _case_signals_by_bo(data: Any) -> dict[str, list[dict[str, Any]]]: + root = _dict(data) + out: dict[str, list[dict[str, Any]]] = {} + for idx, sig in enumerate(_list(root.get("case_liability_signals")), start=1): + if not isinstance(sig, dict): + continue + compact = { + "signal_ref": f"case-liability-signal-{idx:03d}", + "liability_candidate_type": sig.get("liability_candidate_type"), + "related_evidence_indexes": _strings(sig.get("related_evidence_indexes")), + "party_roles": _list(sig.get("party_roles")), + "claim_group_id": sig.get("claim_group_id"), + "liability_group_id": sig.get("liability_group_id"), + "linked_structure_ids": _strings(sig.get("linked_structure_ids")), + } + bo_ids = _strings(sig.get("related_bo_ids")) + for role in _list(sig.get("party_roles")): + if isinstance(role, dict): + bo_ids.extend(_strings(role.get("bo_ids"))) + for bo_id in _dedupe(bo_ids): + out.setdefault(bo_id, []).append(compact) + return out + + + def _legal_routes_by_bo(data: Any) -> dict[str, list[dict[str, Any]]]: + root = _dict(data) + out: dict[str, list[dict[str, Any]]] = {} + for route in _list(root.get("bo_legal_effect_routes")): + if not isinstance(route, dict): + continue + bo_id = _first_string(route.get("bo_id")) + if not bo_id: + continue + out.setdefault(bo_id, []).append({ + "candidate_structure_types": _strings(route.get("candidate_structure_types")), + "linked_structure_ids": _strings(route.get("linked_structure_ids")), + "related_evidence_indexes": _strings(route.get("related_evidence_indexes")), + }) + return out + + + def _normalize_structure_type(value: str) -> str: + text = str(value).strip().lower() + if any(x in text for x in ["money", "loan", "대여금", "매매대금", "미지급"]): + return "money_claim" + if any(x in text for x in ["successor", "상호속용", "영업양수"]): + return "commercial_successor" + if any(x in text for x in ["secured", "담보", "근저당", "피담보"]): + return "secured_debt" + if any(x in text for x in ["registry", "등기", "말소", "무효"]): + return "registry_invalidity" + if any(x in text for x in ["land", "사용이익", "임료", "점유"]): + return "land_use_gain" + if any(x in text for x in ["valuation", "감정", "평가", "가액", "cap"]): + return "valuation" + if any(x in text for x in ["fraudulent", "actio", "사해", "수익자", "전득자"]): + return "fraudulent_transfer" + if any(x in text for x in ["preserved", "피보전"]): + return "preserved_claim" + if any(x in text for x in ["remedy", "원상회복", "가액배상"]): + return "actio_remedy_or_cap" + if any(x in text for x in ["succession", "notice", "lien", "asset_defense", "승계", "통지", "유치권", "인도", "항변"]): + return "succession_notice_lien_asset_defense" + return text.replace(" ", "_") if text else "general_legal_effect" + + + def _candidate_structure_types(bo: dict[str, Any], signals: dict[str, list[dict[str, Any]]]) -> tuple[list[str], list[str]]: + """X-1: canonical routing enum만 candidate_structure_types로. 비-canonical은 nonrouting_labels로 보존.""" + types: list[str] = [] + for route in signals.get("legal_effect", []): + types.extend(_strings(route.get("candidate_structure_types"))) + for case_sig in signals.get("case_liability", []): + types.extend(_strings(case_sig.get("liability_candidate_type"))) + for actio_sig in signals.get("actio", []): + for scope in _strings(actio_sig.get("actio_scope")): + mapping = { + "fraudulent_act": "fraudulent_transfer", + "preserved_claim": "preserved_claim", + "remedy_or_cap": "actio_remedy_or_cap", + "target_asset": "actio_asset_target", + "beneficiary_or_transferee": "fraudulent_transfer", + "encumbrance": "secured_debt", + "defense": "succession_notice_lien_asset_defense", + } + if scope in mapping: + types.append(mapping[scope]) + payload = _dict(bo.get("domain_payload")) + types.extend(_strings(payload.get("domain_legal_effect_tag"))) + types.extend(_strings(payload.get("legal_effect_tags"))) + source_domain = _first_string(_dict(bo.get("provenance")).get("source_domain")) + if source_domain: + if "B1" in source_domain: + types.append("money_claim") + elif "B2" in source_domain: + types.append("secured_debt") + elif "B3" in source_domain: + types.append("land_use_gain") + elif "B4" in source_domain: + types.append("fraudulent_transfer") + elif "B5" in source_domain: + types.append("succession_notice_lien_asset_defense") + canonical: list[str] = [] + nonrouting: list[str] = [] + for value in types: + if not value: + continue + norm = _normalize_structure_type(value) + if norm in CANONICAL_TYPE_SET: + canonical.append(norm) + else: + nonrouting.append(norm) + return _dedupe(canonical), _dedupe(nonrouting) + + + def _asset_kind(label: str, fallback: Any = None) -> str: + if fallback: + text = str(fallback) + if text in {"real_property", "movable", "claim", "security_right", "cash_or_deposit", "business_asset", "other", "unknown"}: + return text + if any(x in label for x in ["토지", "대지", "건물", "부동산", "아파트", "상가", "임야", "등기"]): + return "real_property" + if any(x in label for x in ["근저당", "담보", "저당", "유치권", "질권"]): + return "security_right" + if any(x in label for x in ["채권", "대여금", "매매대금", "보증금"]): + return "claim" + if any(x in label for x in ["예금", "현금", "계좌"]): + return "cash_or_deposit" + if any(x in label for x in ["영업", "사업", "상호"]): + return "business_asset" + if any(x in label for x in ["동산", "차량", "기계"]): + return "movable" + return "unknown" + + + def _asset_strings(value: Any) -> list[str]: + out: list[str] = [] + + def visit(node: Any) -> None: + if node is None or len(out) >= 20: + return + if isinstance(node, (str, int, float)): + text = str(node).strip() + if text and len(text) <= 180: + out.append(text) + return + if isinstance(node, list): + for child in node: + visit(child) + return + if isinstance(node, dict): + for key in [ + "asset_label", "asset", "assets", "property", "properties", "object", "objects", + "collateral", "collaterals", "target", "official_designation", "alias", + ]: + if key in node: + visit(node.get(key)) + visit(value) + return _dedupe(out) + + + def _dedupe_assets(items: list[dict[str, Any]]) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + seen: set[str] = set() + for item in items: + key = json.dumps({ + "asset_label": item.get("asset_label"), + "asset_kind": item.get("asset_kind"), + "source": item.get("source"), + # X-3: ref는 자산 identity 정보 — 상이 ref 행을 중복으로 소거하지 않는다 + "asset_ref_keys": sorted(_strings(item.get("asset_ref_keys"))), + }, ensure_ascii=False, sort_keys=True) + if key not in seen: + seen.add(key) + out.append(item) + return out + + + def _extract_asset_candidates(bo: dict[str, Any], signals: dict[str, list[dict[str, Any]]]) -> list[dict[str, Any]]: + bo_id = bo.get("BO_ID") + evidence = _strings(bo.get("source_evidence_indexes")) + out: list[dict[str, Any]] = [] + for sig in signals.get("actio", []): + for item in _list(sig.get("target_property_candidates")): + if isinstance(item, dict): + label = _first_string(item.get("asset_label")) + if label: + out.append({ + "asset_label": label, + "asset_kind": _asset_kind(label, item.get("asset_kind")), + "source": "actio_case_signals.target_property_candidates", + "related_bo_ids": _dedupe([bo_id] + _strings(item.get("related_bo_ids"))), + "related_evidence_indexes": _dedupe(evidence + _strings(item.get("related_evidence_indexes"))), + }) + payload = _dict(bo.get("domain_payload")) + for source_key in ["asset_bindings", "asset_binding_hint", "candidate_details", "facts"]: + values = _asset_strings(payload.get(source_key)) + for label in values: + out.append({ + "asset_label": label, + "asset_kind": _asset_kind(label), + "source": f"BO.extensions.domain_payload.{source_key}", + "related_bo_ids": [bo_id] if bo_id else [], + "related_evidence_indexes": evidence, + }) + for alias in _as_list(payload.get("asset_alias")): + if not isinstance(alias, dict): + continue + ref_keys = _strings(alias.get("schedule_refs")) + for reg in _as_list(alias.get("registry_refs")): + if isinstance(reg, dict): + ref_keys.extend(_strings(reg.get("registry_ref"))) + else: + ref_keys.extend(_strings(reg)) + alias_labels = _strings(alias.get("alias_strings")) + _strings(alias.get("canonical_label_candidate")) + for label in alias_labels or ([ref_keys[0]] if ref_keys else []): + out.append({ + "asset_label": label, + "asset_kind": _asset_kind(label), + "source": "BO.extensions.domain_payload.asset_alias", + "asset_ref_keys": _dedupe(ref_keys), + "related_bo_ids": [bo_id] if bo_id else [], + "related_evidence_indexes": evidence, + }) + core_obj = _first_string(_dict(bo.get("core_field_base")).get("Object")) + if core_obj: + out.append({ + "asset_label": core_obj, + "asset_kind": _asset_kind(core_obj), + "source": "BO.core_field_base.Object", + "related_bo_ids": [bo_id] if bo_id else [], + "related_evidence_indexes": evidence, + }) + return _dedupe_assets(out) + + + def _require_signal_root(data: Any, doc_name: str, schema_version: str, array_key: str) -> None: + root = _dict(data) + if root.get("schema_version") != schema_version: + raise ValueError(f"{doc_name} schema_version must be {schema_version}") + if root.get("status") not in {"READY", "READY_WITH_REVIEW", "EMPTY"}: + raise ValueError(f"{doc_name} status must be READY, READY_WITH_REVIEW, or EMPTY") + if not isinstance(root.get(array_key), list): + raise ValueError(f"{doc_name}.{array_key} must be array") + + + # ---------------------------------------------------------------- 결정 로직 (v1 LES1 이식 + X-2/X-3/X-4/X-5) + def _score_issue(candidate: dict[str, Any]) -> list[dict[str, Any]]: + fields = _dict(candidate.get("keyword_fields")) + text = " ".join( + _strings(fields.get("Legal_Keywords")) + + _strings(fields.get("Action")) + + _strings(fields.get("Reason")) + + _strings(fields.get("JuristicAct_label")) + + _strings(fields.get("ActionType")) + + _strings(fields.get("domain_legal_effect_tag")) + + _strings(fields.get("legal_effect_tags")) + + _strings(candidate.get("candidate_structure_types")) + + _strings(candidate.get("source_domain")) + ).lower() + rows = [] + for family, keywords in ISSUE_KEYWORDS.items(): + score = sum(1 for keyword in keywords if keyword.lower() in text) + if score: + rows.append({"candidate_value": family, "score": score, "basis": [kw for kw in keywords if kw.lower() in text]}) + rows.sort(key=lambda row: (-row["score"], row["candidate_value"])) + return rows + + + def _hard_issue_keys(candidate: dict[str, Any]) -> list[dict[str, Any]]: + hard = _dict(candidate.get("hard_issue_keys")) + rows: list[dict[str, Any]] = [] + for field in ["claim_group_ids", "liability_group_ids", "linked_structure_ids", "canonical_theory_graph_seed_ref"]: + for value in _strings(hard.get(field)): + rows.append({"field": field, "value": value}) + return rows + + + def _issue_decision(candidate: dict[str, Any]) -> tuple[str, dict[str, Any], list[str]]: + """v1 이식 + 개정: llm_required 분기를 deterministic_review로 전환 (예외 생성 없음 — X-4 ④ 불충족 계열).""" + bo_id = candidate["bo_id"] + hard_rows = _hard_issue_keys(candidate) + score_rows = _score_issue(candidate) + conflict_flags: list[str] = [] + hard_by_field = {field: _strings(_dict(candidate.get("hard_issue_keys")).get(field)) + for field in HARD_ISSUE_KEY_PRIORITY} + intra_conflict_fields = [field for field, values in hard_by_field.items() if len(values) > 1] + if intra_conflict_fields: + conflict_flags.append("multiple_hard_issue_keys") + else: + for field in HARD_ISSUE_KEY_PRIORITY: + if hard_by_field[field]: + demoted = {f: hard_by_field[f] for f in HARD_ISSUE_KEY_PRIORITY + if f != field and hard_by_field[f]} + return f"hard:{hard_by_field[field][0]}", { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [field], + "demoted_hard_keys": demoted, + "conflict_flags": [], + }, [] + if score_rows: + top = score_rows[0] + second = score_rows[1]["score"] if len(score_rows) > 1 else 0 + if top["score"] >= ISSUE_THRESHOLD and top["score"] - second >= ISSUE_MARGIN and not conflict_flags: + return f"issue:{top['candidate_value']}", { + "decision": "score_confident", + "confidence": "MEDIUM", + "basis_fields": ["Legal_Keywords", "Action", "Reason", "JuristicAct.label", "domain tags"], + "conflict_flags": [], + }, [] + types = set(_strings(candidate.get("candidate_structure_types"))) + if {"money_claim", "fraudulent_transfer"}.issubset(types): + conflict_flags.append("money_claim_and_actio_same_bo") + if {"registry_invalidity", "secured_debt"}.issubset(types): + conflict_flags.append("registry_invalidity_and_secured_debt_same_bo") + candidate_pool = _dedupe([row["value"] for row in hard_rows] + [row["candidate_value"] for row in score_rows]) + if len(candidate_pool) == 1 and not conflict_flags: + return f"issue:{candidate_pool[0]}", { + "decision": "auto_selected_single_candidate", + "confidence": "MEDIUM", + "basis_fields": ["hard_issue_keys", "score_table"], + "conflict_flags": [], + }, [] + if not hard_rows and not score_rows: + return f"review_issue:{bo_id}", { + "decision": "deterministic_review", + "confidence": "LOW", + "basis_fields": [], + "conflict_flags": ["missing_candidate_values"], + }, ["missing_candidate_values"] + # 개정: compact 근거로 확정 불충분(스코어 저신뢰·다후보)한 issue 모호성은 LLM행이 아니라 + # 보존적 defer — 별도 issue cluster로 유지하고 review code만 남긴다 (X-4 ④ 불충족). + return f"review_issue:{bo_id}", { + "decision": "deterministic_review", + "confidence": "LOW", + "basis_fields": ["hard_issue_keys", "Legal_Keywords", "Action", "Reason", "JuristicAct.label", "candidate_structure_types"], + "conflict_flags": conflict_flags or ["insufficient_issue_confidence"], + }, ["ambiguous_issue_cluster_deferred"] + + + def _structure_type_decision(candidate: dict[str, Any]) -> tuple[str, list[str], dict[str, Any]]: + """X-2: canonical 복수 타입은 예외가 아니다 — 전부 보존 + priority 대표. 예외 생성 경로 없음. + (현행 최종 스키마에는 타입 간 상호 배타 필드가 존재하지 않아 X-4 ① 조건을 충족하는 + type 충돌이 성립하지 않는다 — 개정 전략서 §5-4.)""" + types = _dedupe(_strings(candidate.get("candidate_structure_types"))) or ["general_legal_effect"] + roles = {DOWNSTREAM_ROLE_FAMILY.get(t, "legal_effect_roles") for t in types} + representative = sorted(types, key=lambda t: STRUCTURE_TYPE_PRIORITY.index(t) if t in STRUCTURE_TYPE_PRIORITY else 999)[0] + diag = { + "decision": "deterministic_multi_routing" if len(types) > 1 else "deterministic", + "candidate_types": types, + "downstream_roles": sorted(roles), + "representative_candidate_structure_type": representative, + "basis_fields": ["legal_effect_signals", "case_liability_signals", "actio_case_signals", "BO.downstream_seed_refs", "BO.domain_payload"], + } + return representative, types, diag + + + def _build_asset_alias_map(candidates: dict[str, dict[str, Any]]) -> dict[str, list[list[str]]]: + """X-3(b): label(정규화) ↔ ref 계열(lineage) map. 같은 alias 항목에 함께 기재되었거나 + ref가 겹치는 집합은 하나의 lineage로 통합한다 (등기행 다수 + 별지목록 = 동일 자산의 정상 기재). + 한 라벨이 서로소인 lineage 2개 이상을 주장할 때만 진짜 충돌이다.""" + label_to_refsets: dict[str, list[set[str]]] = defaultdict(list) + for bo_id in sorted(candidates): + for asset in _list(candidates[bo_id].get("asset_candidates")): + if not isinstance(asset, dict): + continue + refs = set(_strings(asset.get("asset_ref_keys"))) + label = asset.get("asset_label") + if not refs or not label: + continue + sets = label_to_refsets[_asset_norm(str(label))] + merged = False + for existing in sets: + if existing & refs: + existing |= refs + merged = True + break + if not merged: + sets.append(set(refs)) + out: dict[str, list[list[str]]] = {} + for key, sets in label_to_refsets.items(): + changed = True + while changed: + changed = False + for i in range(len(sets)): + for j in range(i + 1, len(sets)): + if sets[i] & sets[j]: + sets[i] |= sets[j] + del sets[j] + changed = True + break + if changed: + break + out[key] = sorted((sorted(s, key=_ref_sort) for s in sets), key=lambda s: s[0]) + return out + + + def _ref_sort(ref_key: str) -> tuple[int, str]: + return (0 if ref_key.startswith("REG-") else 1 if ref_key.startswith("SCH-") else 2, ref_key) + + + def _asset_decisions(candidate: dict[str, Any], alias_map: dict[str, set[str]], + key_alias: dict[str, str]) -> tuple[list[str], list[dict[str, Any]], list[dict[str, Any]], list[str], list[dict[str, Any]]]: + """X-3 ref-first 규칙: + (a) 동일 REG-*/SCH-* ref 공유 → 동일 클러스터 (v1 keyed 그룹핑 유지) + (b) unkeyed 라벨이 alias map에서 단일 ref 계열로 연결 → 결정론 병합 (deterministic_resolution) + (c) 한 BO에 서로 다른 ref 복수 → 클러스터 분리 + multi_asset_bundle 보존 (예외 아님) + (d) alias 상충(한 라벨이 2+ 상이 ref 계열 주장) → X-4 4중 조건 충족 → pack 예외 + (e) ref/alias 근거 없는 자유문구 동일성 불명(kind/legal unit 상이) → SEPARATE 보존 + review code + 반환: (cluster_keys, bundle_items, pack_exceptions, review_codes, resolutions)""" + bo_id = candidate["bo_id"] + assets = [item for item in _list(candidate.get("asset_candidates")) if isinstance(item, dict) and item.get("asset_label")] + if not assets: + return [], [], [], [], [] + cluster_keys: list[str] = [] + bundle_items: list[dict[str, Any]] = [] + pack_exceptions: list[dict[str, Any]] = [] + review_codes: list[str] = [] + resolutions: list[dict[str, Any]] = [] + + def _root_for_refs(refs: list[str]) -> str: + existing = next((key_alias[r] for r in refs if r in key_alias), None) + root = existing or f"ref:{sorted(refs, key=_ref_sort)[0]}" + for r in refs: + key_alias[r] = root + return root + + groups: dict[str, list[dict[str, Any]]] = defaultdict(list) + packed_labels: set[str] = set() + for asset in assets: + label_norm = _asset_norm(str(asset.get("asset_label"))) + lineages_all = alias_map.get(label_norm) or [] + if len(lineages_all) > 1: + # (d) keyed/unkeyed 불문: 한 라벨이 서로소 ref lineage 2+를 주장 — non-deferrable pack + if label_norm not in packed_labels: + packed_labels.add(label_norm) + pack_exceptions.append({ + "exception_id": "", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [bo_id], + "candidate_values": _dedupe([str(asset.get("asset_label"))] + [lineage[0] for lineage in lineages_all]), + "basis_fields": ["asset_alias", str(asset.get("source"))], + "score_summary": [{"candidate_value": "|".join(lineage), "basis": "disjoint ref lineage"} for lineage in lineages_all[:3]], + "conflict_flags": ["label_claims_disjoint_ref_lineages"], + "allowed_decisions": ["SAME_CLUSTER", "SEPARATE_CLUSTER", "MULTI_ASSET_BUNDLE", "BLOCK_REVIEW"], + "compact_context": {"lineages": lineages_all, "asset_kind": asset.get("asset_kind")}, + }) + cluster_keys.append(f"review_asset:{bo_id}:{len(pack_exceptions)}") + continue + refs = _strings(asset.get("asset_ref_keys")) + if refs: + groups[_root_for_refs(refs)].append(asset) + continue + if lineages_all: + # (b) 단일 lineage — alias 근거 결정론 병합 + root = _root_for_refs(list(lineages_all[0])) + groups[root].append(asset) + resolutions.append({ + "kind": "alias_merged", + "bo_id": bo_id, + "asset_label": asset.get("asset_label"), + "ref_root": root, + "basis": "asset_alias ref-label linkage (X-3b)", + }) + continue + groups[f"{label_norm}::{asset.get('asset_kind') or 'unknown'}"].append(asset) + + for key, group in groups.items(): + if key.startswith("ref:"): + cluster_keys.append(f"asset:{key}") + continue + labels = _dedupe([str(item.get("asset_label")) for item in group]) + kinds = set(_strings([item.get("asset_kind") for item in group])) + legal_units = {tuple(_legal_unit_tokens(label)) for label in labels} + if len(kinds) == 1 and len(legal_units) <= 1: + cluster_keys.append(f"asset:{key}") + else: + # (e) 개정: v1의 LLM 예외 대신 라벨별 SEPARATE 보존 + review code (fuzzy 병합 금지) + for label in labels: + cluster_keys.append(f"asset:{_asset_norm(label)}::{_asset_kind(label)}") + review_codes.append("asset_identity_unresolved_separate_preserved") + resolutions.append({ + "kind": "separate_preserved", + "bo_id": bo_id, + "labels": labels, + "basis": "kind/legal-unit divergence without ref linkage (X-3e)", + }) + normalized_roots: set[str] = set() + for item in assets: + item_refs = _strings(item.get("asset_ref_keys")) + if item_refs and item_refs[0] in key_alias: + normalized_roots.add(key_alias[item_refs[0]]) + else: + label_norm = _asset_norm(str(item.get("asset_label"))) + lineages = alias_map.get(label_norm) or [] + if len(lineages) == 1 and lineages[0] and lineages[0][0] in key_alias: + normalized_roots.add(key_alias[lineages[0][0]]) + else: + normalized_roots.add(label_norm) + if len(normalized_roots) > 1: + bundle_items.append({ + "bundle_ref": f"bundle:{bo_id}", + "bundle_type": "multi_asset_bundle", + "source_bo_ids": [bo_id], + "member_asset_labels": _dedupe([str(item.get("asset_label")) for item in assets]), + }) + return _dedupe(cluster_keys), bundle_items, pack_exceptions, _dedupe(review_codes), resolutions + + + def _merge_exceptions(exceptions: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + """v1 서명 병합 이식 (단일 리스트 형태).""" + merged: dict[str, dict[str, Any]] = {} + for exc in exceptions: + signature = json.dumps({ + "type": exc.get("exception_type"), + "candidates": sorted(_strings(exc.get("candidate_values"))), + "flags": sorted(_strings(exc.get("conflict_flags"))), + "decisions": sorted(_strings(exc.get("allowed_decisions"))), + }, ensure_ascii=False, sort_keys=True) + if signature in merged: + root = merged[signature] + root["source_bo_ids"] = _dedupe(_strings(root.get("source_bo_ids")) + _strings(exc.get("source_bo_ids"))) + root["merged_from_count"] = root.get("merged_from_count", 1) + 1 + else: + merged[signature] = {**exc, "merged_from_count": 1} + ordered = sorted( + merged.values(), + key=lambda item: ( + min(_strings(item.get("source_bo_ids")) or [""]), + json.dumps(sorted(_strings(item.get("candidate_values"))), ensure_ascii=False), + ), + ) + merge_groups: list[dict[str, Any]] = [] + for ordinal, exc in enumerate(ordered, start=1): + exc["exception_id"] = _make_id("LEX", ordinal) + if exc.get("merged_from_count", 1) > 1: + merge_groups.append({ + "representative_exception_id": exc["exception_id"], + "source_bo_ids": _strings(exc.get("source_bo_ids")), + "merged_from_count": exc["merged_from_count"], + }) + return ordered, merge_groups + + + def _add_issue_seed(target: dict[str, dict[str, Any]], issue_id: str, issue_key: str, item: dict[str, Any], candidate: dict[str, Any], diag: dict[str, Any]) -> None: + row = target.setdefault(issue_id, { + "issue_cluster_id": issue_id, + "issue_key": issue_key, + "issue_type": issue_key.split(":", 1)[-1], + "source_bo_ids": [], + "candidate_structure_types": [], + "claim_group_ids": [], + "liability_group_ids": [], + "evidence_indexes": [], + "asset_cluster_ids": [], + "basis_fields": [], + "pending_exception_ids": [], + "deterministic_review_codes": [], + }) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + item["source_bo_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + item["candidate_structure_types"]) + hard = _dict(candidate.get("hard_issue_keys")) + row["claim_group_ids"] = _dedupe(row["claim_group_ids"] + _strings(hard.get("claim_group_ids"))) + row["liability_group_ids"] = _dedupe(row["liability_group_ids"] + _strings(hard.get("liability_group_ids"))) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + item["evidence_indexes"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + item["asset_cluster_ids"]) + row["basis_fields"] = _dedupe(row["basis_fields"] + _strings(diag.get("basis_fields"))) + row["pending_exception_ids"] = _dedupe(row["pending_exception_ids"] + item["pending_exception_ids"]) + row["deterministic_review_codes"] = _dedupe(row["deterministic_review_codes"] + item["deterministic_review_codes"]) + + + def _add_asset_seeds(target: dict[str, dict[str, Any]], asset_ids: list[str], item: dict[str, Any], candidate: dict[str, Any]) -> None: + assets = [asset for asset in _list(candidate.get("asset_candidates")) if isinstance(asset, dict)] + for asset_id in asset_ids: + row = target.setdefault(asset_id, { + "asset_cluster_id": asset_id, + "canonical_asset_label": None, + "asset_kind": None, + "member_asset_mentions": [], + "source_bo_ids": [], + "evidence_indexes": [], + "issue_cluster_ids": [], + "candidate_structure_types": [], + "basis_fields": [], + "pending_exception_ids": [], + "deterministic_review_codes": [], + }) + related_assets = assets or [] + labels = _dedupe([str(asset.get("asset_label")) for asset in related_assets if asset.get("asset_label")]) + if row["canonical_asset_label"] is None and labels: + row["canonical_asset_label"] = labels[0] + kinds = _dedupe([str(asset.get("asset_kind")) for asset in related_assets if asset.get("asset_kind")]) + if row["asset_kind"] is None and kinds: + row["asset_kind"] = kinds[0] + row["member_asset_mentions"] = _dedupe(row["member_asset_mentions"] + labels) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + item["source_bo_ids"]) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + item["evidence_indexes"]) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + item["issue_cluster_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + item["candidate_structure_types"]) + row["basis_fields"] = _dedupe(row["basis_fields"] + [str(asset.get("source")) for asset in related_assets if asset.get("source")]) + row["pending_exception_ids"] = _dedupe(row["pending_exception_ids"] + item["pending_exception_ids"]) + row["deterministic_review_codes"] = _dedupe(row["deterministic_review_codes"] + item["deterministic_review_codes"]) + + + def _add_evidence_index(target: dict[str, dict[str, Any]], evidence_indexes: list[str], item: dict[str, Any], authority: dict[str, Any]) -> None: + for idx in evidence_indexes: + auth = _dict(authority.get(idx)) + row = target.setdefault(idx, { + "evidence_index": idx, + "source_title": auth.get("source_title"), + "priority_class": auth.get("priority_class"), + "authentication_status": auth.get("authentication_status"), + "seed_ids": [], + "source_bo_ids": [], + "issue_cluster_ids": [], + "asset_cluster_ids": [], + "candidate_structure_types": [], + "gap_flags": [], + }) + row["seed_ids"] = _dedupe(row["seed_ids"] + [item["seed_id"]]) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + item["source_bo_ids"]) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + item["issue_cluster_ids"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + item["asset_cluster_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + item["candidate_structure_types"]) + if not auth: + row["gap_flags"] = _dedupe(row["gap_flags"] + ["missing_evidence_authority"]) + elif not auth.get("excerpt"): + row["gap_flags"] = _dedupe(row["gap_flags"] + ["missing_evidence_excerpt"]) + + + def main() -> None: + try: + _init_mcp_session() + docs = {name: _read_doc(name) for name in INPUT_FILES} + bo_items = docs["BO.json"] + if not isinstance(bo_items, list): + raise ValueError("BO.json root must be array") + _require_signal_root(docs["actio_case_signals.json"], "actio_case_signals.json", "actio_case_signals.v1", "actio_case_signals") + _require_signal_root(docs["case_liability_signals.json"], "case_liability_signals.json", "case_liability_signals.v1", "case_liability_signals") + _require_signal_root(docs["legal_effect_signals.json"], "legal_effect_signals.json", "legal_effect_signals.v1", "bo_legal_effect_routes") + evidence_authority = _collect_evidence_authority(docs["evidence_indexed.json"], bo_items) + compact_bo_items = [_compact_bo(bo) for bo in bo_items if isinstance(bo, dict)] + bo_by_id = {bo["BO_ID"]: bo for bo in compact_bo_items if bo.get("BO_ID")} + actio_by_bo = _actio_signals_by_bo(docs["actio_case_signals.json"]) + case_by_bo = _case_signals_by_bo(docs["case_liability_signals.json"]) + legal_by_bo = _legal_routes_by_bo(docs["legal_effect_signals.json"]) + + candidates: dict[str, dict[str, Any]] = {} + nonrouting_by_bo: dict[str, list[str]] = {} + input_warnings: list[dict[str, Any]] = [] + for bo_id, bo in bo_by_id.items(): + signals = { + "actio": actio_by_bo.get(bo_id, []), + "case_liability": case_by_bo.get(bo_id, []), + "legal_effect": legal_by_bo.get(bo_id, []), + } + evidence_indexes = _dedupe( + _strings(bo.get("source_evidence_indexes")) + + [idx for route in signals["legal_effect"] for idx in _strings(route.get("related_evidence_indexes"))] + + [idx for sig in signals["case_liability"] for idx in _strings(sig.get("related_evidence_indexes"))] + + [idx for sig in signals["actio"] for idx in _strings(sig.get("related_evidence_indexes"))] + ) + missing_evidence = [idx for idx in evidence_indexes if idx not in evidence_authority] + if missing_evidence: + input_warnings.append({"bo_id": bo_id, "warning": "missing_evidence_authority", "evidence_indexes": missing_evidence}) + canonical_types, nonrouting = _candidate_structure_types(bo, signals) + nonrouting_by_bo[bo_id] = nonrouting + candidates[bo_id] = { + "bo_id": bo_id, + "source_domain": _first_string(_dict(bo.get("provenance")).get("source_domain")), + "hard_issue_keys": { + "claim_group_ids": _dedupe([x for sig in signals["case_liability"] for x in _strings(sig.get("claim_group_id"))] + _strings(_dict(bo.get("downstream_seed_refs")).get("claim_group_seed_refs_proposed"))), + "liability_group_ids": _dedupe([x for sig in signals["case_liability"] for x in _strings(sig.get("liability_group_id"))]), + "linked_structure_ids": _dedupe( + [x for route in signals["legal_effect"] for x in _strings(route.get("linked_structure_ids"))] + + [x for sig in signals["case_liability"] for x in _strings(sig.get("linked_structure_ids"))] + + _strings(_dict(bo.get("downstream_seed_refs")).get("legal_effect_structure_seed_ref_proposed")) + ), + "canonical_theory_graph_seed_ref": _strings(_dict(bo.get("downstream_seed_refs")).get("canonical_theory_graph_seed_ref_proposed")), + }, + "candidate_structure_types": canonical_types, + "asset_candidates": _extract_asset_candidates(bo, signals), + "evidence_indexes": evidence_indexes, + "keyword_fields": { + "Legal_Keywords": _strings(bo.get("Legal_Keywords")), + "Action": bo.get("Action"), + "Reason": bo.get("Reason"), + "JuristicAct_label": _dict(bo.get("JuristicAct")).get("label"), + "ActionType": bo.get("ActionType"), + "BOType": bo.get("BOType"), + "domain_legal_effect_tag": _dict(bo.get("domain_payload")).get("domain_legal_effect_tag"), + "legal_effect_tags": _strings(_dict(bo.get("domain_payload")).get("legal_effect_tags")), + }, + "signals": signals, + } + + alias_map = _build_asset_alias_map(candidates) + issue_key_to_id: dict[str, str] = {} + asset_key_to_id: dict[str, str] = {} + key_alias: dict[str, str] = {} + issue_seed_map: dict[str, dict[str, Any]] = {} + asset_seed_map: dict[str, dict[str, Any]] = {} + evidence_index_seed: dict[str, dict[str, Any]] = {} + seed_items: list[dict[str, Any]] = [] + raw_exceptions: list[dict[str, Any]] = [] + deterministic_resolutions: list[dict[str, Any]] = [] + multi_asset_bundles: list[dict[str, Any]] = [] + + for bo_id in sorted(candidates): + candidate = _dict(candidates[bo_id]) + issue_key, issue_diag, issue_codes = _issue_decision(candidate) + if issue_key not in issue_key_to_id: + issue_key_to_id[issue_key] = _make_id("IC", len(issue_key_to_id) + 1) + issue_id = issue_key_to_id[issue_key] + asset_keys, bundles, asset_excs, asset_codes, resolutions = _asset_decisions(candidate, alias_map, key_alias) + asset_ids: list[str] = [] + for key in asset_keys: + if key not in asset_key_to_id: + asset_key_to_id[key] = _make_id("AC", len(asset_key_to_id) + 1) + asset_ids.append(asset_key_to_id[key]) + multi_asset_bundles.extend(bundles) + deterministic_resolutions.extend(resolutions) + raw_exceptions.extend(asset_excs) + representative_type, all_types, type_diag = _structure_type_decision(candidate) + if type_diag.get("decision") == "deterministic_multi_routing": + deterministic_resolutions.append({ + "kind": "multi_routing_preserved", + "bo_id": bo_id, + "candidate_structure_types": all_types, + "representative": representative_type, + "basis": "canonical multi-routing preserved deterministically (X-2)", + }) + + seed_id = _make_id("LESS", len(seed_items) + 1) + evidence_indexes = _strings(candidate.get("evidence_indexes")) + signals = _dict(candidate.get("signals")) + case_signals = _list(signals.get("case_liability")) + actio_signals = _list(signals.get("actio")) + item = { + "seed_id": seed_id, + "source_bo_ids": [bo_id], + "representative_candidate_structure_type": representative_type, + "candidate_structure_types": all_types, + "issue_cluster_ids": [issue_id], + "asset_cluster_ids": asset_ids, + "multi_asset_bundle_refs": _strings([bundle.get("bundle_ref") for bundle in bundles]), + "evidence_indexes": evidence_indexes, + "linked_claim_group_ids": _dedupe([x for sig in case_signals if isinstance(sig, dict) for x in _strings(sig.get("claim_group_id"))]), + "linked_liability_group_ids": _dedupe([x for sig in case_signals if isinstance(sig, dict) for x in _strings(sig.get("liability_group_id"))]), + "linked_actio_signal_ids": _dedupe([x for sig in actio_signals if isinstance(sig, dict) for x in _strings(sig.get("signal_id"))]), + "basis_fields": _dedupe(_strings(issue_diag.get("basis_fields")) + _strings(type_diag.get("basis_fields")) + ["BO.source_evidence_indexes"]), + "pending_exception_ids": [], + "deterministic_review_codes": _dedupe(issue_codes + asset_codes), + "diagnostics": { + "issue": {"decision": issue_diag.get("decision"), "confidence": issue_diag.get("confidence")}, + "candidate_structure_type": {"decision": type_diag.get("decision")}, + }, + } + seed_items.append(item) + _add_issue_seed(issue_seed_map, issue_id, issue_key, item, candidate, issue_diag) + _add_asset_seeds(asset_seed_map, asset_ids, item, candidate) + _add_evidence_index(evidence_index_seed, evidence_indexes, item, evidence_authority) + + merged_exceptions, merge_groups = _merge_exceptions(raw_exceptions) + # 병합 후 LEX id를 seed의 pending_exception_ids로 역결합 + exc_by_signature_bo: dict[str, list[str]] = defaultdict(list) + for exc in merged_exceptions: + for exc_bo in _strings(exc.get("source_bo_ids")): + exc_by_signature_bo[exc_bo].append(str(exc["exception_id"])) + # X-4 방어 상한: 자동 차단 금지 — 초과분은 deterministic_review로 전환 (id 유지, 추적 가능) + deterministic_review_overflow: list[dict[str, Any]] = [] + kept_exceptions: list[dict[str, Any]] = [] + total_bytes = 0 + for exc in merged_exceptions: + serialized = len(json.dumps(exc, ensure_ascii=False).encode("utf-8")) + item_chars = len(json.dumps(exc, ensure_ascii=False)) + if item_chars > PACK_ITEM_CHAR_LIMIT or len(kept_exceptions) >= PACK_MAX_ITEMS or total_bytes + serialized > PACK_MAX_BYTES: + deterministic_review_overflow.append({ + "exception_id": exc["exception_id"], + "exception_type": exc.get("exception_type"), + "source_bo_ids": _strings(exc.get("source_bo_ids")), + "reason": "pack_budget_deferred_to_review", + }) + continue + kept_exceptions.append(exc) + total_bytes += serialized + overflow_ids = {str(item["exception_id"]) for item in deterministic_review_overflow} + for item in seed_items: + bo_id = item["source_bo_ids"][0] + ids = exc_by_signature_bo.get(bo_id, []) + item["pending_exception_ids"] = _dedupe([i for i in ids if i not in overflow_ids]) + if any(i in overflow_ids for i in ids): + item["deterministic_review_codes"] = _dedupe(item["deterministic_review_codes"] + ["pack_budget_deferred_to_review"]) + for issue_row in issue_seed_map.values(): + issue_row["pending_exception_ids"] = _dedupe([i for bo in issue_row["source_bo_ids"] for i in exc_by_signature_bo.get(bo, []) if i not in overflow_ids]) + for asset_row in asset_seed_map.values(): + asset_row["pending_exception_ids"] = _dedupe([i for bo in asset_row["source_bo_ids"] for i in exc_by_signature_bo.get(bo, []) if i not in overflow_ids]) + + exception_pack = { + "schema_version": "stage1_legal_structure_exception_pack.v2", + "has_exceptions": bool(kept_exceptions), + "exception_count": len(kept_exceptions), + "exceptions": kept_exceptions, + "budget": {"max_items": PACK_MAX_ITEMS, "max_bytes": PACK_MAX_BYTES, + "max_chars_per_item": PACK_ITEM_CHAR_LIMIT, + "overflow_policy": "DEFER_TO_DETERMINISTIC_REVIEW"}, + } + pack_content = _write_doc(PACK_OUT_PATH, exception_pack) + pack_sha = hashlib.sha256(pack_content.encode("utf-8")).hexdigest() + + audit = { + "schema_version": "stage1_legal_structure_audit.v1", + "nonrouting_labels_by_bo_id": {k: v for k, v in sorted(nonrouting_by_bo.items()) if v}, + "deterministic_resolutions": deterministic_resolutions, + "deterministic_review_items": deterministic_review_overflow + [ + {"seed_id": item["seed_id"], "source_bo_ids": item["source_bo_ids"], + "codes": item["deterministic_review_codes"]} + for item in seed_items if item["deterministic_review_codes"] + ], + "merge_groups": merge_groups, + "input_warnings": input_warnings, + "review_reduction_trace": [], + } + _write_doc(AUDIT_OUT_PATH, audit) + + seed_bundle = { + "legal_structure_seed_bundle": { + "schema_version": "stage1_legal_structure_seed_bundle.v2", + "status": "READY", + "structure_seed_items": seed_items, + "issue_cluster_seed_map": issue_seed_map, + "asset_cluster_seed_map": asset_seed_map, + "evidence_index_seed": evidence_index_seed, + "multi_asset_bundles": multi_asset_bundles, + "exception_pack_ref": { + "path": PACK_OUT_PATH, + "sha256": pack_sha, + "expected_exception_ids": [str(exc["exception_id"]) for exc in kept_exceptions], + }, + } + } + _write_doc(SEED_OUT_PATH, seed_bundle) + + print(json.dumps({ + "status": "READY", + "message": "L0 structure seed reduce 완료: seed bundle/exception pack/audit 생성", + "seed_bundle_path": SEED_OUT_PATH, + "exception_pack_path": PACK_OUT_PATH, + "audit_path": AUDIT_OUT_PATH, + "seed_count": len(seed_items), + "has_exceptions": exception_pack["has_exceptions"], + "exception_count": exception_pack["exception_count"], + "deterministic_resolution_count": len(deterministic_resolutions), + "nonrouting_label_count": sum(len(v) for v in nonrouting_by_bo.values()), + }, ensure_ascii=False)) + except Exception as exc: + print(json.dumps({ + "status": "FAILED", + "message": "L0 structure seed reducer 실패", + "reason": str(exc), + }, ensure_ascii=False)) + raise + + + if __name__ == "__main__": + main() + - task_name: Task_L1_exception_adjudicator + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: low + llm_verbosity: low + max_iterations: 1 + use_tools: [localdocs] + cache_control: + mode: auto + ttl: 20m + preflight: true + preflight_files: + - stage1_tmp/legal_effect_structure_exception_pack.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + + TASK_NAME: Task_L1_exception_adjudicator + STAGE: Stage 1 legal effect structure/index builder — 조건부 예외 판정 (Part 1 v3 GB / Part 2 v3 R1 패턴) + MISSION: 결정론 reducer(L0)가 non-deferrable로 판정한 compact exception만 판정한다. 파일 병합·최종 파일 작성·사실 창작은 하지 않는다. + + + + - 유일한 입력은 preflight로 제공된 `stage1_tmp/legal_effect_structure_exception_pack.json`이다. + - seed bundle, BO.json, evidence, signal 파일, raw 원문을 읽거나 요청하지 않는다. + - pack에 없는 exception_id·BO ID·evidence index·structure/cluster ID·사실·당사자·일자·금액을 창작하지 않는다. + - `legal_effect_structures.json`·seed bundle·audit 파일을 작성하지 않는다. + - 출력 파일은 `stage1_tmp/legal_effect_structure_adjudication.json` 하나뿐이다. + - markdown fence·prose 없이 raw JSON만 파일에 쓴다. + + + + 1. preflight로 제공된 exception pack의 `has_exceptions`를 확인한다. + 2. `has_exceptions == false`이면: `write_file(overwrite=true)`로 아래 no-exception 객체를 `stage1_tmp/legal_effect_structure_adjudication.json`에 저장하고 `"NO_EXCEPTIONS"`만 출력한 뒤 즉시 종료한다(terminate). 다른 어떤 파일도 읽지 않는다. + {"legal_structure_exception_adjudication": {"schema_version": "stage1_legal_structure_exception_adjudication.v1", "status": "READY_NO_EXCEPTIONS", "source_exception_pack_schema_version": "stage1_legal_structure_exception_pack.v2", "decisions": [], "blocked_review_items": [], "adjudication_gate": {"input_exception_count": 0, "decision_count": 0, "blocked_count": 0, "coverage_pass": true, "json_only_contract_pass": true}}} + 3. `has_exceptions == true`이면: `exceptions[]`의 각 항목을 해당 항목의 `compact_context`·`candidate_values`·`basis_fields`·`score_summary`·`conflict_flags`만으로 판정한다. 추가 read는 금지된다. + 4. 판정 규칙: + - `ambiguous_issue_cluster`: `USE_EXISTING_HARD_KEY`, `SELECT_ISSUE_FAMILY`, `SPLIT_ISSUE_CLUSTER`, `BLOCK_REVIEW` 중 하나. + - `ambiguous_asset_cluster`: `SAME_CLUSTER`, `SEPARATE_CLUSTER`, `MULTI_ASSET_BUNDLE`, `BLOCK_REVIEW` 중 하나. 자산 동일성은 등기행·별지목록 ref가 자유문구보다 우선한다. 병합이 권리객체를 훼손할 수 있으면 SEPARATE_CLUSTER 또는 BLOCK_REVIEW. + - `ambiguous_candidate_structure_type`: `SELECT_REPRESENTATIVE_TYPE`, `PRESERVE_MULTI_ROUTING`, `SPLIT_STRUCTURE_SEED`, `BLOCK_REVIEW` 중 하나. 다중 라우팅 보존이 기본적으로 안전하다. + - 각 항목의 `allowed_decisions` 안에서만 선택한다. `selected_value`/`selected_values`/`preserve_candidate_values`/`split_plan`의 값은 반드시 해당 항목의 `candidate_values` 부분집합이어야 한다. + - compact 근거로 확정할 수 없으면 반드시 `BLOCK_REVIEW`로 판정하고 같은 항목을 `blocked_review_items`에 넣는다(확신 없는 확정 금지 — 인간 검토 라우팅). + 5. `write_file(overwrite=true)`로 결과를 저장한다. 루트는 `legal_structure_exception_adjudication`, `schema_version`은 `stage1_legal_structure_exception_adjudication.v1`, `status`는 전 항목 비차단이면 `READY`, 차단 존재 시 `READY_WITH_BLOCKS`다. decision 항목 필드: `exception_id`, `exception_type`, `source_bo_ids`, `decision`, `selected_value|null`, `selected_values[]`, `preserve_candidate_values[]`, `split_plan[]`(split일 때만), `review_required`, `reason_code`, `basis_fields_used[]`, `conflict_flags_acknowledged[]`. 모든 decision은 pack의 `exception_id`를 인용하며, 각 exception마다 정확히 하나의 decision을 만든다. + 6. `"L1 예외 판정 완료 (decisions=<건수>)"`만 출력하고 작업을 끝낸다(terminate). + + + + - Stage 1은 법률효과·청구원인을 확정하지 않는다. 여기 도달한 항목은 final writer가 단일 join 값을 선택해야만 진행되는 사안뿐이다. + - 하나의 라벨이 서로 다른 등기행/별지목록 ref 계열을 주장하는 경우: 원문 ref 근거가 더 구체적인 쪽을 선택하고, 우열을 가릴 수 없으면 BLOCK_REVIEW. + - SEPARATE_CLUSTER(보존)가 권리객체를 지키는 기본값이다. SAME_CLUSTER 병합은 ref 근거가 무손실로 합치될 때만 선택한다. + - 판정은 라우팅/조인 키 결정일 뿐 소유권·담보권·평가액의 실체 판단이 아니다. + + + + - exception pack 부재·파싱 불가: 즉시 중단하고 채팅으로만 보고한다. adjudication 파일은 쓰지 않는다. + - tool 오류: 1회만 재시도. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + - task_name: Task_L2_final_structure_index_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import sys + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + SEED_PATH = "stage1_tmp/legal_effect_structure_seed_bundle.json" + PACK_PATH = "stage1_tmp/legal_effect_structure_exception_pack.json" + ADJUDICATION_PATH = "stage1_tmp/legal_effect_structure_adjudication.json" + AUDIT_PATH = "stage1_tmp/legal_effect_structure_audit.json" + TARGET_NAME = "legal_effect_structures.json" + STRUCTURE_TYPE_ROLE = { + "money_claim": "claim_chain_ref", + "commercial_successor": "claim_chain_ref", + "secured_debt": "linked_structures", + "registry_invalidity": "linked_structures", + "land_use_gain": "legal_calculation_object", + "valuation": "legal_calculation_object", + "fraudulent_transfer": "actio_roles", + "preserved_claim": "claim_chain_ref", + "actio_remedy_or_cap": "linked_actio_structures", + "succession_notice_lien_asset_defense": "linked_structures", + "general_legal_effect": "legal_effect_roles", + } + ISSUE_DECISIONS = {"USE_EXISTING_HARD_KEY", "SELECT_ISSUE_FAMILY", "SPLIT_ISSUE_CLUSTER", "BLOCK_REVIEW"} + ASSET_DECISIONS = {"SAME_CLUSTER", "SEPARATE_CLUSTER", "MULTI_ASSET_BUNDLE", "BLOCK_REVIEW"} + TYPE_DECISIONS = {"SELECT_REPRESENTATIVE_TYPE", "PRESERVE_MULTI_ROUTING", "SPLIT_STRUCTURE_SEED", "BLOCK_REVIEW"} + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-legal-effect-l2-final-structure-index-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + + def _lenient_json(raw: str, label: str) -> Any: + """SKILL §6.4/§6.7: strict 실패 시 코드펜스 제거 + raw_decode salvage.""" + try: + return json.loads(raw) + except json.JSONDecodeError: + text = raw.strip() + if text.startswith("```"): + first_newline = text.find("\n") + if first_newline >= 0: + text = text[first_newline + 1:] + if text.rstrip().endswith("```"): + text = text.rstrip()[:-3] + start = text.find("{") + if start < 0: + raise RuntimeError(f"Content of {label} is not valid JSON: {raw[:300]}") + try: + obj, _ = JSON_DECODER.raw_decode(text[start:].strip()) + return obj + except json.JSONDecodeError as exc: + raise RuntimeError(f"Content of {label} is not valid JSON: {raw[:300]}") from exc + + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result.get("result", {}).get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + text = content[0].get("text", "") + envelope = _lenient_json(text, doc_name) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + item = results[0] + if isinstance(item, dict) and item.get("error"): + raise RuntimeError(f"read_docs error for {doc_name}: {item.get('error')}") + raw = item.get("content", "") + else: + raw = envelope + if isinstance(raw, str): + return _lenient_json(raw, doc_name) + return raw + + + def _read_doc(doc_name: str) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + return _extract_json_from_read_docs_result(result, doc_name) + + + def _write_doc(doc_name: str, payload: dict[str, Any]) -> None: + content = json.dumps(payload, ensure_ascii=False, indent=2) + "\n" + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=_next_msg_id()) + + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _strings(value: Any) -> list[str]: + if value is None: + return [] + items = value if isinstance(value, list) else [value] + out: list[str] = [] + for item in items: + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + if value is None: + continue + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + + def _make_id(prefix: str, ordinal: int) -> str: + return f"{prefix}-{ordinal:03d}" + + + # ---------------------------------------------------------------- 입력 적재 (파일 채널 일원화 — X-9) + def _load_seed_bundle() -> dict[str, Any]: + outer = _read_doc(SEED_PATH) + root = _dict(_dict(outer).get("legal_structure_seed_bundle")) + if root.get("schema_version") != "stage1_legal_structure_seed_bundle.v2": + raise ValueError("legal_structure_seed_bundle schema mismatch (v2 required)") + if root.get("status") != "READY": + raise ValueError("legal_structure_seed_bundle status invalid") + return root + + + def _load_exception_pack(seed_bundle: dict[str, Any]) -> dict[str, Any]: + ref = _dict(seed_bundle.get("exception_pack_ref")) + if ref.get("path") != PACK_PATH: + raise ValueError("exception_pack_ref path mismatch") + result = _call_tool("read_docs", {"doc_names": [PACK_PATH]}, msg_id=_next_msg_id()) + content = result.get("result", {}).get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {PACK_PATH}") + envelope = _lenient_json(content[0].get("text", ""), PACK_PATH) + raw_text = None + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if results and isinstance(results[0], dict): + raw_text = results[0].get("content") + pack = _lenient_json(raw_text, PACK_PATH) if isinstance(raw_text, str) else (raw_text or envelope) + pack = _dict(pack) + if pack.get("schema_version") != "stage1_legal_structure_exception_pack.v2": + raise ValueError("exception pack schema mismatch (v2 required)") + if isinstance(raw_text, str) and ref.get("sha256"): + digest = hashlib.sha256(raw_text.encode("utf-8")).hexdigest() + if digest != ref.get("sha256"): + raise ValueError("exception pack sha256 mismatch against seed bundle ref") + expected_ids = _strings(ref.get("expected_exception_ids")) + pack_ids = [str(_dict(e).get("exception_id")) for e in _list(pack.get("exceptions"))] + if sorted(expected_ids) != sorted(pack_ids): + raise ValueError("exception pack ids mismatch against seed bundle ref") + return pack + + + def _load_adjudication() -> dict[str, Any]: + outer = _read_doc(ADJUDICATION_PATH) + root = _dict(_dict(outer).get("legal_structure_exception_adjudication")) + if root.get("schema_version") != "stage1_legal_structure_exception_adjudication.v1": + raise ValueError("legal_structure_exception_adjudication schema mismatch") + if root.get("status") not in {"READY", "READY_NO_EXCEPTIONS", "READY_WITH_BLOCKS"}: + raise ValueError("legal_structure_exception_adjudication status is not accepted") + return root + + + # ---------------------------------------------------------------- 판정 검증 (v1 LES3 이식 + coverage 등식) + def _expected_exceptions(pack: dict[str, Any]) -> dict[str, dict[str, Any]]: + out: dict[str, dict[str, Any]] = {} + for item in _list(pack.get("exceptions")): + if not isinstance(item, dict) or not item.get("exception_id"): + raise ValueError("exception item without exception_id in pack") + out[str(item["exception_id"])] = item + return out + + + def _validate_decision_enum(decision: dict[str, Any]) -> None: + exception_type = decision.get("exception_type") + choice = decision.get("decision") + allowed = { + "ambiguous_issue_cluster": ISSUE_DECISIONS, + "ambiguous_asset_cluster": ASSET_DECISIONS, + "ambiguous_candidate_structure_type": TYPE_DECISIONS, + }.get(exception_type) + if allowed is None: + raise ValueError(f"invalid exception_type: {exception_type}") + if choice not in allowed: + raise ValueError(f"invalid decision {choice} for {exception_type}") + + + def _validate_decision_subsets(decision: dict[str, Any], expected: dict[str, Any]) -> None: + candidates = set(_strings(expected.get("candidate_values"))) + basis = set(_strings(expected.get("basis_fields"))) + flags = set(_strings(expected.get("conflict_flags"))) + for field in ["selected_values", "preserve_candidate_values"]: + extras = set(_strings(decision.get(field))) - candidates + if extras: + raise ValueError(f"{decision.get('exception_id')}: {field} contains values outside candidate_values: {sorted(extras)}") + selected = decision.get("selected_value") + if selected not in (None, "") and candidates and str(selected) not in candidates: + raise ValueError(f"{decision.get('exception_id')}: selected_value outside candidate_values") + for split in _list(decision.get("split_plan")): + if isinstance(split, dict): + extras = set(_strings(split.get("candidate_values"))) - candidates + if extras: + raise ValueError(f"{decision.get('exception_id')}: split_plan candidate outside candidate_values") + if basis: + extras = set(_strings(decision.get("basis_fields_used"))) - basis + if extras: + raise ValueError(f"{decision.get('exception_id')}: basis_fields_used outside basis_fields") + if flags: + extras = set(_strings(decision.get("conflict_flags_acknowledged"))) - flags + if extras: + raise ValueError(f"{decision.get('exception_id')}: conflict_flags_acknowledged outside conflict_flags") + + + def _validate_decisions(pack: dict[str, Any], adjudication: dict[str, Any]) -> tuple[dict[str, dict[str, Any]], list[dict[str, Any]], set[str], set[str]]: + """coverage 등식(X-5): expected == resolved ∪ blocked. 반환: (decisions, blocked, resolved_ids, blocked_ids).""" + expected = _expected_exceptions(pack) + decisions: dict[str, dict[str, Any]] = {} + blocked = _list(adjudication.get("blocked_review_items")) + if not expected: + if adjudication.get("status") != "READY_NO_EXCEPTIONS": + raise ValueError("no exceptions expected; adjudication status must be READY_NO_EXCEPTIONS") + if _list(adjudication.get("decisions")): + raise ValueError("no exceptions expected; decisions must be empty") + return decisions, [], set(), set() + for decision in _list(adjudication.get("decisions")): + if not isinstance(decision, dict) or not decision.get("exception_id"): + raise ValueError("decision item missing exception_id") + exception_id = str(decision["exception_id"]) + if exception_id not in expected: + raise ValueError(f"decision references unknown exception_id: {exception_id}") + if exception_id in decisions: + raise ValueError(f"duplicate decision for exception_id: {exception_id}") + _validate_decision_enum(decision) + _validate_decision_subsets(decision, expected[exception_id]) + decisions[exception_id] = decision + missing = sorted(set(expected) - set(decisions)) + if missing: + raise ValueError(f"adjudication missing decisions: {missing}") + blocked_ids = {str(item.get("exception_id")) for item in blocked if isinstance(item, dict)} + for exception_id, decision in decisions.items(): + if decision.get("decision") == "BLOCK_REVIEW" and exception_id not in blocked_ids: + blocked.append({ + "exception_id": exception_id, + "exception_type": decision.get("exception_type"), + "source_bo_ids": _strings(decision.get("source_bo_ids")), + "block_reason": "block_review_decision_without_block_item", + }) + blocked_ids.add(exception_id) + resolved_ids = {eid for eid, d in decisions.items() if d.get("decision") != "BLOCK_REVIEW"} + if resolved_ids | blocked_ids != set(expected): + raise ValueError("coverage equation failed: expected != resolved ∪ blocked") + return decisions, blocked, resolved_ids, blocked_ids + + + # ---------------------------------------------------------------- 구조 빌더 (v1 LES3 이식 + X-5/X-6) + def _decisions_for_bo(decisions: dict[str, dict[str, Any]], bo_id: str, exception_type: str) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for decision in decisions.values(): + if decision.get("exception_type") == exception_type and bo_id in _strings(decision.get("source_bo_ids")): + out.append(decision) + return out + + + def _issue_ids_for_item(item: dict[str, Any], decisions: dict[str, dict[str, Any]], issue_key_ids: dict[str, str]) -> list[str]: + bo_id = _strings(item.get("source_bo_ids"))[0] if _strings(item.get("source_bo_ids")) else "" + issue_decisions = _decisions_for_bo(decisions, bo_id, "ambiguous_issue_cluster") + keys: list[str] = [] + for decision in issue_decisions: + choice = decision.get("decision") + if choice == "BLOCK_REVIEW": + continue + if choice == "SPLIT_ISSUE_CLUSTER": + for split in _list(decision.get("split_plan")): + if isinstance(split, dict): + label = split.get("split_label") or "|".join(_strings(split.get("candidate_values"))) + if label: + keys.append(f"split_issue:{label}") + else: + value = decision.get("selected_value") or (_strings(decision.get("selected_values"))[0] if _strings(decision.get("selected_values")) else None) + if value: + keys.append(f"adjudicated_issue:{value}") + if not keys: + keys = _strings(item.get("issue_cluster_ids")) + out: list[str] = [] + for key in keys: + if key not in issue_key_ids: + issue_key_ids[key] = _make_id("IC", len(issue_key_ids) + 1) + out.append(issue_key_ids[key]) + return _dedupe(out) + + + def _asset_ids_for_item(item: dict[str, Any], decisions: dict[str, dict[str, Any]], asset_key_ids: dict[str, str]) -> tuple[list[str], list[str]]: + bo_id = _strings(item.get("source_bo_ids"))[0] if _strings(item.get("source_bo_ids")) else "" + asset_decisions = _decisions_for_bo(decisions, bo_id, "ambiguous_asset_cluster") + keys = _strings(item.get("asset_cluster_ids")) + bundle_refs = _strings(item.get("multi_asset_bundle_refs")) + for decision in asset_decisions: + choice = decision.get("decision") + if choice == "BLOCK_REVIEW": + continue + if choice == "SAME_CLUSTER": + value = decision.get("selected_value") or "|".join(_strings(decision.get("selected_values")) or _strings(decision.get("preserve_candidate_values"))) + if value: + keys = [f"same_asset:{value}"] + elif choice == "SEPARATE_CLUSTER": + vals = _strings(decision.get("selected_values")) or _strings(decision.get("preserve_candidate_values")) or _strings(decision.get("candidate_values")) + keys = [f"separate_asset:{value}" for value in vals] or keys + elif choice == "MULTI_ASSET_BUNDLE": + value = decision.get("selected_value") or f"bundle:{bo_id}" + if value not in bundle_refs: + bundle_refs.append(str(value)) + out: list[str] = [] + for key in keys: + if key not in asset_key_ids: + asset_key_ids[key] = _make_id("AC", len(asset_key_ids) + 1) + out.append(asset_key_ids[key]) + return _dedupe(out), _dedupe(bundle_refs) + + + def _type_plan_for_item(item: dict[str, Any], decisions: dict[str, dict[str, Any]]) -> list[dict[str, Any]]: + bo_id = _strings(item.get("source_bo_ids"))[0] if _strings(item.get("source_bo_ids")) else "" + original_types = _strings(item.get("candidate_structure_types")) or _strings(item.get("representative_candidate_structure_type")) or ["general_legal_effect"] + representative = item.get("representative_candidate_structure_type") or original_types[0] + type_decisions = _decisions_for_bo(decisions, bo_id, "ambiguous_candidate_structure_type") + if not type_decisions: + return [{"candidate_structure_type": representative, "candidate_structure_types": original_types, "split_label": None}] + plans: list[dict[str, Any]] = [] + for decision in type_decisions: + choice = decision.get("decision") + if choice == "BLOCK_REVIEW": + plans.append({"candidate_structure_type": representative, "candidate_structure_types": original_types, "split_label": None}) + elif choice == "SELECT_REPRESENTATIVE_TYPE": + selected = decision.get("selected_value") or (_strings(decision.get("selected_values"))[0] if _strings(decision.get("selected_values")) else representative) + plans.append({"candidate_structure_type": selected, "candidate_structure_types": _dedupe([selected] + original_types), "split_label": None}) + elif choice == "PRESERVE_MULTI_ROUTING": + vals = _strings(decision.get("selected_values")) or _strings(decision.get("preserve_candidate_values")) or original_types + selected = decision.get("selected_value") or vals[0] + plans.append({"candidate_structure_type": selected, "candidate_structure_types": vals, "split_label": None}) + elif choice == "SPLIT_STRUCTURE_SEED": + for split in _list(decision.get("split_plan")): + if isinstance(split, dict): + vals = _strings(split.get("candidate_values")) or original_types + plans.append({ + "candidate_structure_type": vals[0], + "candidate_structure_types": vals, + "split_label": split.get("split_label"), + }) + return plans or [{"candidate_structure_type": representative, "candidate_structure_types": original_types, "split_label": None}] + + + def _legal_roles(types: list[str]) -> list[str]: + return _dedupe([STRUCTURE_TYPE_ROLE.get(typ, "legal_effect_roles") for typ in types]) + + + def _basis(item: dict[str, Any], types: list[str]) -> dict[str, Any]: + signal_files: list[str] = [] + if _strings(item.get("linked_actio_signal_ids")): + signal_files.append("actio_case_signals.json") + if _strings(item.get("linked_claim_group_ids")) or _strings(item.get("linked_liability_group_ids")): + signal_files.append("case_liability_signals.json") + if types: + signal_files.append("legal_effect_signals.json") + return { + "primary_fields": _strings(item.get("basis_fields")), + "source_signal_files": _dedupe(signal_files), + "source_evidence_indexes": _strings(item.get("evidence_indexes")), + } + + + def _structure_label(candidate_type: str, source_bo_ids: list[str], split_label: Any) -> str: + label = candidate_type.replace("_", " ") + suffix = f" / {split_label}" if split_label else "" + return f"{label} structure for {', '.join(source_bo_ids)}{suffix}" + + + def _build_structures(seed_bundle: dict[str, Any], decisions: dict[str, dict[str, Any]], + blocked_ids: set[str], resolved_ids: set[str]) -> list[dict[str, Any]]: + issue_key_ids: dict[str, str] = {} + asset_key_ids: dict[str, str] = {} + structures: list[dict[str, Any]] = [] + for item in _list(seed_bundle.get("structure_seed_items")): + if not isinstance(item, dict): + continue + source_bo_ids = _strings(item.get("source_bo_ids")) + if not source_bo_ids: + continue + pending = _strings(item.get("pending_exception_ids")) + review_codes = _strings(item.get("deterministic_review_codes")) + item_blocked = sorted(set(pending) & blocked_ids) + item_resolved = sorted(set(pending) & resolved_ids) + issue_ids = _issue_ids_for_item(item, decisions, issue_key_ids) + asset_ids, bundle_refs = _asset_ids_for_item(item, decisions, asset_key_ids) + type_plans = _type_plan_for_item(item, decisions) + for plan in type_plans: + structure_id = _make_id("LES", len(structures) + 1) + types = _strings(plan.get("candidate_structure_types")) or ["general_legal_effect"] + # X-5: 해소된 판정은 review를 발생시키지 않는다 — blocked/deterministic review/split만 review + review_required = bool(item_blocked or review_codes or plan.get("split_label")) + structures.append({ + "structure_id": structure_id, + "candidate_structure_type": str(plan.get("candidate_structure_type") or types[0]), + "candidate_structure_types": types, + "structure_label": _structure_label(str(plan.get("candidate_structure_type") or types[0]), source_bo_ids, plan.get("split_label")), + "source_bo_ids": source_bo_ids, + "issue_cluster_ids": issue_ids, + "asset_cluster_ids": asset_ids, + "evidence_indexes": _strings(item.get("evidence_indexes")), + "linked_claim_group_ids": _strings(item.get("linked_claim_group_ids")), + "linked_liability_group_ids": _strings(item.get("linked_liability_group_ids")), + "linked_actio_signal_ids": _strings(item.get("linked_actio_signal_ids")), + "legal_effect_roles": _legal_roles(types), + "basis": _basis(item, types), + "review_required": review_required, + "pending_exception_state": { + "resolved": item_resolved, + "blocked": item_blocked, + "deterministic_review_codes": review_codes, + }, + "routing_audit": { + "source_seed_id": item.get("seed_id"), + "candidate_structure_types": types, + "multi_asset_bundle_refs": bundle_refs, + }, + }) + return structures + + + def _build_structure_index(structures: list[dict[str, Any]], seed_bundle: dict[str, Any]) -> dict[str, Any]: + by_issue: dict[str, Any] = {} + by_asset: dict[str, Any] = {} + by_evidence: dict[str, Any] = {} + by_type: dict[str, Any] = {} + by_bo: dict[str, Any] = {} + issue_seed_map = _dict(seed_bundle.get("issue_cluster_seed_map")) + asset_seed_map = _dict(seed_bundle.get("asset_cluster_seed_map")) + evidence_seed = _dict(seed_bundle.get("evidence_index_seed")) + + for structure in structures: + sid = structure["structure_id"] + basis = _dict(structure.get("basis")) + routing_audit = _dict(structure.get("routing_audit")) + all_types = _strings(structure.get("candidate_structure_types")) or [structure["candidate_structure_type"]] + for bo_id in _strings(structure.get("source_bo_ids")): + row = by_bo.setdefault(bo_id, { + "bo_id": bo_id, + "structure_ids": [], + "structure_labels": [], + "issue_cluster_ids": [], + "asset_cluster_ids": [], + "evidence_indexes": [], + "candidate_structure_types": [], + "claim_group_ids": [], + "liability_group_ids": [], + "related_actio_signal_ids": [], + "legal_effect_roles": [], + "basis_fields": [], + "multi_asset_bundle_refs": [], + "review_required": False, + }) + row["structure_ids"] = _dedupe(row["structure_ids"] + [sid]) + row["structure_labels"] = _dedupe(row["structure_labels"] + _strings(structure.get("structure_label"))) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + structure["issue_cluster_ids"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + structure["asset_cluster_ids"]) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + structure["evidence_indexes"]) + # X-6: 대표 타입만이 아니라 canonical 전 타입을 인덱스에 반영 (additive) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + all_types) + row["claim_group_ids"] = _dedupe(row["claim_group_ids"] + structure["linked_claim_group_ids"]) + row["liability_group_ids"] = _dedupe(row["liability_group_ids"] + structure["linked_liability_group_ids"]) + row["related_actio_signal_ids"] = _dedupe(row["related_actio_signal_ids"] + structure["linked_actio_signal_ids"]) + row["legal_effect_roles"] = _dedupe(row["legal_effect_roles"] + structure["legal_effect_roles"]) + row["basis_fields"] = _dedupe(row["basis_fields"] + _strings(basis.get("primary_fields"))) + row["multi_asset_bundle_refs"] = _dedupe(row["multi_asset_bundle_refs"] + _strings(routing_audit.get("multi_asset_bundle_refs"))) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + for issue_id in _strings(structure.get("issue_cluster_ids")): + seed = _dict(issue_seed_map.get(issue_id)) + row = by_issue.setdefault(issue_id, { + "issue_cluster_id": issue_id, + "issue_type": seed.get("issue_type"), + "issue_label": seed.get("issue_key") or issue_id, + "source_bo_ids": [], + "candidate_structure_types": [], + "claim_group_ids": [], + "liability_group_ids": [], + "evidence_indexes": [], + "asset_cluster_ids": [], + "structure_ids": [], + "basis_fields": [], + "review_required": False, + }) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + structure["source_bo_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + all_types) + row["claim_group_ids"] = _dedupe(row["claim_group_ids"] + structure["linked_claim_group_ids"]) + row["liability_group_ids"] = _dedupe(row["liability_group_ids"] + structure["linked_liability_group_ids"]) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + structure["evidence_indexes"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + structure["asset_cluster_ids"]) + row["structure_ids"] = _dedupe(row["structure_ids"] + [sid]) + row["basis_fields"] = _dedupe(row["basis_fields"] + _strings(structure.get("basis", {}).get("primary_fields"))) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + for asset_id in _strings(structure.get("asset_cluster_ids")): + seed = _dict(asset_seed_map.get(asset_id)) + row = by_asset.setdefault(asset_id, { + "asset_cluster_id": asset_id, + "canonical_asset_label": seed.get("canonical_asset_label"), + "asset_kind": seed.get("asset_kind"), + "member_asset_mentions": _strings(seed.get("member_asset_mentions")), + "source_bo_ids": [], + "evidence_indexes": [], + "issue_cluster_ids": [], + "candidate_structure_types": [], + "structure_ids": [], + "basis_fields": _strings(seed.get("basis_fields")), + "review_required": bool(_strings(seed.get("deterministic_review_codes"))), + }) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + structure["source_bo_ids"]) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + structure["evidence_indexes"]) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + structure["issue_cluster_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + all_types) + row["structure_ids"] = _dedupe(row["structure_ids"] + [sid]) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + for evidence_index in _strings(structure.get("evidence_indexes")): + seed = _dict(evidence_seed.get(evidence_index)) + row = by_evidence.setdefault(evidence_index, { + "evidence_index": evidence_index, + "source_title": seed.get("source_title"), + "priority_class": seed.get("priority_class"), + "authentication_status": seed.get("authentication_status"), + "source_bo_ids": [], + "structure_ids": [], + "issue_cluster_ids": [], + "asset_cluster_ids": [], + "candidate_structure_types": [], + "related_event_candidate_ids": [], + "related_signal_refs": [], + "support_role": "primary", + "gap_flags": _strings(seed.get("gap_flags")), + }) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + structure["source_bo_ids"]) + row["structure_ids"] = _dedupe(row["structure_ids"] + [sid]) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + structure["issue_cluster_ids"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + structure["asset_cluster_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + all_types) + row["related_signal_refs"] = _dedupe(row["related_signal_refs"] + structure["linked_actio_signal_ids"] + structure["linked_claim_group_ids"] + structure["linked_liability_group_ids"]) + # X-6: by_candidate_structure_type을 전체 canonical 타입에 대해 생성 + for typ in all_types: + row = by_type.setdefault(typ, { + "candidate_structure_type": typ, + "structure_ids": [], + "source_bo_ids": [], + "issue_cluster_ids": [], + "asset_cluster_ids": [], + "evidence_indexes": [], + "claim_group_ids": [], + "liability_group_ids": [], + "related_actio_signal_ids": [], + "downstream_fact_roles": [], + "basis_fields": [], + "review_required": False, + }) + row["structure_ids"] = _dedupe(row["structure_ids"] + [sid]) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + structure["source_bo_ids"]) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + structure["issue_cluster_ids"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + structure["asset_cluster_ids"]) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + structure["evidence_indexes"]) + row["claim_group_ids"] = _dedupe(row["claim_group_ids"] + structure["linked_claim_group_ids"]) + row["liability_group_ids"] = _dedupe(row["liability_group_ids"] + structure["linked_liability_group_ids"]) + row["related_actio_signal_ids"] = _dedupe(row["related_actio_signal_ids"] + structure["linked_actio_signal_ids"]) + row["downstream_fact_roles"] = _dedupe(row["downstream_fact_roles"] + structure["legal_effect_roles"]) + row["basis_fields"] = _dedupe(row["basis_fields"] + _strings(structure.get("basis", {}).get("primary_fields"))) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + + return { + "by_bo_id": by_bo, + "by_issue_cluster_id": by_issue, + "by_asset_cluster_id": by_asset, + "by_evidence_index": by_evidence, + "by_candidate_structure_type": by_type, + } + + + def _quality_gate(structures: list[dict[str, Any]], seed_bundle: dict[str, Any], + blocked: list[dict[str, Any]]) -> dict[str, Any]: + seed_items = [item for item in _list(seed_bundle.get("structure_seed_items")) if isinstance(item, dict)] + seed_bo_ids = sorted({bo for item in seed_items for bo in _strings(item.get("source_bo_ids"))}) + covered_bo_ids = sorted({bo for structure in structures for bo in _strings(structure.get("source_bo_ids"))}) + seed_evidence = sorted({ev for item in seed_items for ev in _strings(item.get("evidence_indexes"))}) + covered_evidence = sorted({ev for structure in structures for ev in _strings(structure.get("evidence_indexes"))}) + review_queue = [] + for structure in structures: + if structure.get("review_required"): + review_queue.append({ + "review_type": "structure_review_required", + "structure_id": structure["structure_id"], + "source_bo_ids": structure["source_bo_ids"], + }) + for item in blocked: + if isinstance(item, dict): + review_queue.append({ + "review_type": "blocked_exception", + "exception_id": item.get("exception_id"), + "exception_type": item.get("exception_type"), + "source_bo_ids": _strings(item.get("source_bo_ids")), + "block_reason": item.get("block_reason"), + }) + # X-5: 자동차단 추적성 복원 — deterministic review 항목을 식별 가능한 큐 항목으로 남긴다 + for item in seed_items: + codes = _strings(item.get("deterministic_review_codes")) + if codes: + review_queue.append({ + "review_type": "deterministic_review", + "seed_id": item.get("seed_id"), + "source_bo_ids": _strings(item.get("source_bo_ids")), + "codes": codes, + }) + return { + "bo_coverage": { + "source_bo_count": len(seed_bo_ids), + "covered_bo_count": len(covered_bo_ids), + "missing_bo_ids": sorted(set(seed_bo_ids) - set(covered_bo_ids)), + "coverage_pass": set(seed_bo_ids) == set(covered_bo_ids), + }, + "evidence_coverage": { + "source_evidence_count": len(seed_evidence), + "covered_evidence_count": len(covered_evidence), + "missing_evidence_indexes": sorted(set(seed_evidence) - set(covered_evidence)), + "coverage_pass": set(seed_evidence).issubset(set(covered_evidence)), + }, + "review_queue": review_queue, + } + + + def _validate_payload(payload: dict[str, Any]) -> None: + if payload.get("schema_version") != "stage1_legal_effect_structures.v1": + raise ValueError("final payload schema_version mismatch") + if payload.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("final payload status invalid") + structures = _list(payload.get("legal_effect_structures")) + index = _dict(payload.get("structure_index")) + for key in ["by_bo_id", "by_issue_cluster_id", "by_asset_cluster_id", "by_evidence_index", "by_candidate_structure_type"]: + if not isinstance(index.get(key), dict): + raise ValueError(f"structure_index.{key} must be object") + structure_ids = {str(item.get("structure_id")) for item in structures if isinstance(item, dict)} + if len(structure_ids) != len(structures): + raise ValueError("structure_id values must be unique") + for item in structures: + if not isinstance(item, dict): + raise ValueError("legal_effect_structures item must be object") + for field in [ + "structure_id", "candidate_structure_type", "candidate_structure_types", "structure_label", + "source_bo_ids", "issue_cluster_ids", "asset_cluster_ids", "evidence_indexes", + "linked_claim_group_ids", "linked_liability_group_ids", "linked_actio_signal_ids", + "legal_effect_roles", "basis", "review_required", "pending_exception_state", + ]: + if field not in item: + raise ValueError(f"structure item missing {field}") + for bo_id in _strings(item.get("source_bo_ids")): + bo_row = _dict(index["by_bo_id"].get(bo_id)) + if not bo_row: + raise ValueError(f"missing BO index for {bo_id}") + if item.get("structure_id") not in _strings(bo_row.get("structure_ids")): + raise ValueError(f"BO index for {bo_id} does not reference {item.get('structure_id')}") + for issue_id in _strings(item.get("issue_cluster_ids")): + if issue_id not in index["by_issue_cluster_id"]: + raise ValueError(f"missing issue index for {issue_id}") + for asset_id in _strings(item.get("asset_cluster_ids")): + if asset_id not in index["by_asset_cluster_id"]: + raise ValueError(f"missing asset index for {asset_id}") + for evidence_index in _strings(item.get("evidence_indexes")): + if evidence_index not in index["by_evidence_index"]: + raise ValueError(f"missing evidence index for {evidence_index}") + for typ in _strings(item.get("candidate_structure_types")) or [item.get("candidate_structure_type")]: + if typ not in index["by_candidate_structure_type"]: + raise ValueError(f"missing candidate type index for {typ}") + gate = _dict(payload.get("quality_gate")) + if not _dict(gate.get("bo_coverage")).get("coverage_pass"): + raise ValueError("BO coverage failed") + if not _dict(gate.get("evidence_coverage")).get("coverage_pass"): + raise ValueError("evidence coverage failed") + + + def _update_audit(resolved_ids: set[str], blocked_ids: set[str], decisions: dict[str, dict[str, Any]]) -> None: + try: + audit = _dict(_read_doc(AUDIT_PATH)) + except Exception: + audit = {"schema_version": "stage1_legal_structure_audit.v1"} + trace = _list(audit.get("review_reduction_trace")) + for eid in sorted(resolved_ids): + decision = _dict(decisions.get(eid)) + trace.append({ + "exception_id": eid, + "resolution": "llm_adjudicated", + "decision": decision.get("decision"), + "reason_code": decision.get("reason_code"), + }) + for eid in sorted(blocked_ids): + trace.append({"exception_id": eid, "resolution": "blocked_to_review"}) + audit["review_reduction_trace"] = trace + _write_doc(AUDIT_PATH, audit) + + + def main() -> None: + try: + _init_mcp_session() + seed_bundle = _load_seed_bundle() + pack = _load_exception_pack(seed_bundle) + adjudication = _load_adjudication() + decisions, blocked, resolved_ids, blocked_ids = _validate_decisions(pack, adjudication) + structures = _build_structures(seed_bundle, decisions, blocked_ids, resolved_ids) + if not structures: + raise ValueError("no legal effect structures generated") + structure_index = _build_structure_index(structures, seed_bundle) + quality_gate = _quality_gate(structures, seed_bundle, blocked) + status = "READY_WITH_REVIEW" if quality_gate["review_queue"] else "READY" + payload = { + "schema_version": "stage1_legal_effect_structures.v1", + "status": status, + "legal_effect_structures": structures, + "structure_index": structure_index, + "quality_gate": quality_gate, + } + _validate_payload(payload) + _write_doc(TARGET_NAME, payload) + reread = _read_doc(TARGET_NAME) + if not isinstance(reread, dict) or reread.get("schema_version") != "stage1_legal_effect_structures.v1": + raise ValueError("post-write reread validation failed") + _update_audit(resolved_ids, blocked_ids, decisions) + print(json.dumps({ + "status": "READY", + "message": "legal_effect_structures.json 작성 완료", + "written_file": TARGET_NAME, + "payload_status": status, + "structure_count": len(structures), + "review_queue_count": len(quality_gate["review_queue"]), + "resolved_exception_count": len(resolved_ids), + "blocked_exception_count": len(blocked_ids), + }, ensure_ascii=False)) + except Exception as exc: + print(json.dumps({ + "status": "FAILED", + "message": "L2 final structure index writer 실패", + "target_file": TARGET_NAME, + "reason": str(exc), + }, ensure_ascii=False)) + raise + + + if __name__ == "__main__": + main() diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_3_Codex_v2.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_3_Codex_v2.yml new file mode 100644 index 00000000..29aa24be --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_3_Codex_v2.yml @@ -0,0 +1,2165 @@ +--- +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_3 + description: 민사소송 원고 송무 초지능 AI변호사 - Legal Effect 생성 + version: v2.0 + Stages: + - name: stage1_legal_effect_생성 + description: Legal Effect Structure 작성 + tools: + mcpServers: + localdocs: + type: streamable-http + url: http://mcp-localdocs:8012/mcp + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + task_procedure: + IN: + nexts: + - Task_LES0_input_pack_compiler + wait_until: [] + Task_LES0_input_pack_compiler: + nexts: + - Task_LES1_deterministic_structure_seed_builder + wait_until: + - IN + Task_LES1_deterministic_structure_seed_builder: + nexts: + - Task_LES2_exception_adjudicator_* + - Task_LES3_final_structure_index_writer + wait_until: + - Task_LES0_input_pack_compiler + Task_LES2_exception_adjudicator_*: + nexts: + - Task_LES3_final_structure_index_writer + wait_until: + - Task_LES1_deterministic_structure_seed_builder + Task_LES3_final_structure_index_writer: + nexts: + - OUT + wait_until: + - Task_LES1_deterministic_structure_seed_builder + - all Task_LES2_exception_adjudicator_* + OUT: + nexts: [] + wait_until: + - Task_LES3_final_structure_index_writer + + tasks: + - task_name: Task_LES0_input_pack_compiler + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: |- + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MESSAGE_IDS = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + INPUT_FILES = [ + "BO.json", + "evidence_indexed.json", + "actio_case_signals.json", + "case_liability_signals.json", + "legal_effect_signals.json", + ] + OUTPUT_PATH = "stage1_tmp/legal_effect_structure_input_pack.json" + CANONICAL_TYPES = [ + "money_claim", + "commercial_successor", + "secured_debt", + "registry_invalidity", + "land_use_gain", + "valuation", + "fraudulent_transfer", + "preserved_claim", + "succession_notice_lien_asset_defense", + "general_legal_effect", + ] + SOURCE_DOMAIN_FALLBACK = { + "B1_Money_Successor": "money_claim", + "B2_Secured_Registry": "secured_debt", + "B3_Land_Valuation": "land_use_gain", + "B4_Actio": "fraudulent_transfer", + "B5_Succession_Notice_Lien_Asset_Defense": "succession_notice_lien_asset_defense", + } + EXACT_TYPE_ALIASES = { + "auction_invalidity": ["registry_invalidity"], + "derivative_bank_mortgage_invalidity": ["secured_debt", "registry_invalidity"], + "mortgage_extinguishment": ["secured_debt", "registry_invalidity"], + "actio_remedy_or_cap": ["fraudulent_transfer"], + "remedy_or_cap": ["fraudulent_transfer"], + "value_compensation": ["fraudulent_transfer", "valuation"], + "insolvency_check": ["fraudulent_transfer"], + "actio_asset_target": ["fraudulent_transfer"], + "target_asset": ["fraudulent_transfer"], + "preserved_claim": ["preserved_claim"], + } + SIGNAL_CONTRACTS = { + "actio_case_signals.json": ("actio_case_signals.v1", "actio_case_signals"), + "case_liability_signals.json": ("case_liability_signals.v1", "case_liability_signals"), + "legal_effect_signals.json": ("legal_effect_signals.v1", "bo_legal_effect_routes"), + } + + def _next_id() -> int: + return next(MESSAGE_IDS) + + def _decode_json(value: Any, label: str) -> Any: + if not isinstance(value, str): + return value + text = value.strip() + if not text: + raise ValueError(f"{label}: empty JSON text") + try: + return json.loads(text) + except json.JSONDecodeError: + try: + obj, _ = JSON_DECODER.raw_decode(text) + return obj + except json.JSONDecodeError as exc: + raise ValueError(f"{label}: invalid JSON: {text[:240]}") from exc + + def _parse_rpc(text: str) -> dict[str, Any]: + candidates = [] + for line in text.splitlines(): + if line.startswith("data:"): + candidates.append(line[5:].strip()) + candidates.append(text.strip()) + for candidate in candidates: + if not candidate: + continue + try: + value = _decode_json(candidate, "MCP response") + except ValueError: + continue + if isinstance(value, dict) and ("result" in value or "error" in value): + return value + raise RuntimeError(f"MCP response could not be parsed: {text[:300]}") + + def _initialize() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage1-part3-les0-v2", + "version": "2.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + initialized = CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ) + initialized.raise_for_status() + + def _call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_rpc(response.text) + if parsed.get("error"): + raise RuntimeError(f"MCP {name} failed: {parsed['error']}") + return parsed + + def _read_doc(path: str) -> Any: + rpc = _call_tool("read_docs", {"doc_names": [path]}) + content = rpc.get("result", {}).get("content", []) + if not content or not isinstance(content[0], dict): + raise RuntimeError(f"{path}: read_docs returned no content") + envelope = _decode_json(content[0].get("text", ""), f"{path} envelope") + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results or not isinstance(results[0], dict): + raise RuntimeError(f"{path}: read_docs results are empty") + if results[0].get("error"): + raise RuntimeError(f"{path}: {results[0]['error']}") + inner = results[0].get("content", "") + else: + inner = envelope + return _decode_json(inner, path) + + def _artifact_text(payload: dict[str, Any]) -> str: + return json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n" + + def _write_doc(path: str, payload: dict[str, Any]) -> None: + _call_tool("write_file", {"path": path, "content": _artifact_text(payload), "overwrite": True}) + + def _digest(value: Any) -> str: + text = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return "sha256:" + hashlib.sha256(text.encode("utf-8")).hexdigest() + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + output = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in output: + output.append(text) + return output + + def _sorted_strings(value: Any) -> list[str]: + return sorted(set(_strings(value))) + + def _first(*values: Any) -> str | None: + for value in values: + items = _strings(value) + if items: + return items[0] + return None + + def _normalized(text: Any) -> str: + return re.sub(r"[^0-9a-zA-Z가-힣]", "", str(text or "").lower()) + + def _evidence_rows(doc: Any) -> list[dict[str, Any]]: + root = _as_dict(doc) + rows = root.get("evidence_indexed") + if not isinstance(rows, list): + rows = root.get("items") + if not isinstance(rows, list): + raise ValueError("evidence_indexed.json must contain evidence_indexed or items array") + return [row for row in rows if isinstance(row, dict)] + + def _canonicalize_label(label: str, source_domain: str | None) -> list[str]: + raw = label.strip() + lower = raw.lower().replace("-", "_").replace(" ", "_") + if lower in CANONICAL_TYPES: + return [lower] + if lower in EXACT_TYPE_ALIASES: + return EXACT_TYPE_ALIASES[lower] + types = [] + text = lower + " " + raw + families = [ + (["money", "loan", "debt_payment", "claim", "대여", "공사대금", "물품대금", "미지급", "채권", "변제"], "money_claim"), + (["successor", "business_transfer", "continued_trade", "상호속용", "영업양수", "승계책임"], "commercial_successor"), + (["secured", "mortgage", "collateral", "담보", "근저당", "피담보", "저당"], "secured_debt"), + (["registry", "auction_invalid", "cancellation", "derivative_invalid", "등기", "말소", "경매무효"], "registry_invalidity"), + (["land_use", "use_gain", "rent", "delivery", "사용이익", "임료", "차임", "인도"], "land_use_gain"), + (["valuation", "appraisal", "value_compensation", "감정", "평가", "가액", "시가"], "valuation"), + (["fraudulent", "beneficiary", "transferee", "insolvency", "remedy_or_cap", "target_asset", "사해", "수익자", "전득자", "무자력", "가액배상"], "fraudulent_transfer"), + (["preserved_claim", "피보전채권"], "preserved_claim"), + (["succession", "notice", "lien", "asset_defense", "상속", "통지", "유치권", "항변"], "succession_notice_lien_asset_defense"), + ] + if "possession" in text or "점유" in text: + if source_domain == "B3_Land_Valuation": + types.append("land_use_gain") + elif source_domain == "B5_Succession_Notice_Lien_Asset_Defense": + types.append("succession_notice_lien_asset_defense") + for needles, canonical in families: + if any(needle in text for needle in needles): + types.append(canonical) + return [item for item in CANONICAL_TYPES if item in types] + + def _signal_rows(doc: Any, name: str) -> list[dict[str, Any]]: + schema, key = SIGNAL_CONTRACTS[name] + root = _as_dict(doc) + if root.get("schema_version") != schema: + raise ValueError(f"{name}: schema_version must be {schema}") + if root.get("status") not in {"READY", "READY_WITH_REVIEW", "EMPTY"}: + raise ValueError(f"{name}: invalid status") + rows = root.get(key) + if not isinstance(rows, list): + raise ValueError(f"{name}.{key} must be array") + return [row for row in rows if isinstance(row, dict)] + + def _signal_bo_ids(row: Any) -> list[str]: + output = [] + if isinstance(row, dict): + for key, value in row.items(): + if key in {"bo_id", "BO_ID"}: + output.extend(_strings(value)) + elif key in {"related_bo_ids", "source_bo_ids"}: + output.extend(_strings(value)) + elif isinstance(value, (dict, list)): + output.extend(_signal_bo_ids(value)) + elif isinstance(row, list): + for item in row: + output.extend(_signal_bo_ids(item)) + return _sorted_strings(output) + + def _group_signals(rows: list[dict[str, Any]], bo_ids: set[str]) -> dict[str, list[dict[str, Any]]]: + output = {bo_id: [] for bo_id in bo_ids} + for row in rows: + refs = _signal_bo_ids(row) + unknown = sorted(set(refs) - bo_ids) + if unknown: + raise ValueError(f"signal references unknown BO IDs: {unknown}") + for bo_id in refs: + output[bo_id].append(row) + return output + + def _add_alias(alias_map: dict[str, dict[str, Any]], ref: str, row: dict[str, Any], review: list[dict[str, Any]]) -> None: + old = alias_map.get(ref) + label = row.get("canonical_label_candidate") + if old and old.get("canonical_label_candidate") and label and old["canonical_label_candidate"] != label: + review.append({ + "review_code": "ASSET_ALIAS_LABEL_CONFLICT", + "source_bo_ids": _sorted_strings(row.get("source_bo_ids")), + "evidence_indexes": _sorted_strings([old.get("evidence_index"), row.get("evidence_index")]), + "related_ref_keys": [ref], + "reason_code": "CONFLICTING_NON_NULL_CANONICAL_LABELS", + }) + return + merged = dict(old or {}) + for key, value in row.items(): + if value not in (None, "", []): + merged[key] = value + merged["source_bo_ids"] = _sorted_strings(_strings((old or {}).get("source_bo_ids")) + _strings(row.get("source_bo_ids"))) + alias_map[ref] = merged + + def _build_evidence_and_aliases(rows: list[dict[str, Any]], bo_items: list[dict[str, Any]]) -> tuple[dict[str, Any], dict[str, Any], list[dict[str, Any]]]: + evidence = {} + aliases = {} + review = [] + for row in rows: + evidence_index = _first(row.get("evidence_index"), row.get("id")) + if not evidence_index: + raise ValueError("evidence row missing evidence_index") + if evidence_index in evidence: + raise ValueError(f"duplicate evidence_index: {evidence_index}") + evidence[evidence_index] = { + "evidence_index": evidence_index, + "source_title": _first(row.get("source_title"), row.get("title")), + "doc_type": _first(row.get("doc_type")), + "source_pointer": _as_dict(row.get("source_pointer")), + "priority_class": _first(row.get("priority_class")), + "authentication_status": _first(row.get("authentication_status")), + } + for schedule in _as_list(row.get("schedule_items")): + if not isinstance(schedule, dict): + continue + ref = _first(schedule.get("schedule_ref")) + if ref: + _add_alias(aliases, ref, { + "ref_type": "schedule", + "canonical_label_candidate": _first(schedule.get("canonical_label_candidate")), + "evidence_index": evidence_index, + "usable_in_relief": _first(schedule.get("usable_in_relief")), + "source_bo_ids": [], + }, review) + for registry in _as_list(row.get("registry_row_components")): + if not isinstance(registry, dict): + continue + ref = _first(registry.get("registry_ref")) + if ref: + _add_alias(aliases, ref, { + "ref_type": "registry_row", + "canonical_label_candidate": None, + "evidence_index": evidence_index, + "section": _first(registry.get("section"), registry.get("kind")), + "row_seq": registry.get("row_seq"), + "source_bo_ids": [], + }, review) + for bo in bo_items: + bo_id = _first(bo.get("BO_ID"), bo.get("id")) + payload = _as_dict(_as_dict(bo.get("extensions")).get("domain_payload")) + for alias in _as_list(payload.get("asset_alias")): + if not isinstance(alias, dict): + continue + labels = _strings(alias.get("canonical_label_candidate")) + _strings(alias.get("alias_strings")) + label = labels[0] if labels else None + for ref in _strings(alias.get("schedule_refs")) + _strings(alias.get("registry_refs")): + _add_alias(aliases, ref, { + "ref_type": "schedule" if ref.startswith("SCH-") else "registry_row", + "canonical_label_candidate": label, + "evidence_index": None, + "source_bo_ids": [bo_id] if bo_id else [], + }, review) + return evidence, aliases, review + + def _asset_kind(label: str, refs: list[str]) -> str: + text = label.lower() + if any(ref.startswith(("REG-", "SCH-")) for ref in refs) or any(word in text for word in ["토지", "대지", "건물", "상가", "빌라", "임야", "부동산"]): + return "real_property" + if any(word in text for word in ["근저당", "저당", "담보권"]): + return "security_right" + if any(word in text for word in ["채권", "대여금", "공사대금", "물품대금"]): + return "claim" + if any(word in text for word in ["예금", "현금", "계좌"]): + return "cash_or_deposit" + return "unknown" + + def _extract_ref_keys(value: Any) -> list[str]: + refs = [] + for text in _strings(value): + refs.extend(re.findall(r"(?:REG|SCH)-\d{3}-\d{2}", text)) + return _sorted_strings(refs) + + def _asset_candidates(bo: dict[str, Any], actio_rows: list[dict[str, Any]], alias_map: dict[str, Any]) -> list[dict[str, Any]]: + bo_id = _first(bo.get("BO_ID"), bo.get("id")) or "" + evidence_indexes = _sorted_strings(bo.get("source_evidence_indexes")) + payload = _as_dict(_as_dict(bo.get("extensions")).get("domain_payload")) + candidates = [] + + def add(label_value: Any, source_code: str, refs_value: Any = None, evidence_value: Any = None) -> None: + for label in _strings(label_value): + if re.fullmatch(r"\d{4}-\d{2}-\d{2}", label): + continue + refs = _sorted_strings(_strings(refs_value) + _extract_ref_keys(label)) + if not refs: + normalized_label = _normalized(label) + matches = [] + if len(normalized_label) >= 4: + for ref, alias in alias_map.items(): + if not ref.startswith("SCH-"): + continue + alias_label = _normalized(_as_dict(alias).get("canonical_label_candidate")) + same_scope = bo_id in _strings(_as_dict(alias).get("source_bo_ids")) or _as_dict(alias).get("evidence_index") in evidence_indexes + if same_scope and normalized_label in alias_label: + matches.append(ref) + if len(matches) == 1: + refs = matches + candidates.append({ + "asset_label": label, + "asset_kind": _asset_kind(label, refs), + "asset_ref_keys": refs, + "source_code": source_code, + "evidence_indexes": _sorted_strings(evidence_value or evidence_indexes), + }) + + add(payload.get("asset_bindings"), "BO_ASSET_BINDINGS") + add(payload.get("asset_binding_hint"), "BO_ASSET_BINDING_HINT") + add(payload.get("registry_object_display"), "BO_REGISTRY_OBJECT_DISPLAY") + add(payload.get("valuation_object"), "BO_VALUATION_OBJECT") + possession_ref = _as_dict(payload.get("possession_state")).get("asset_ref") + if possession_ref: + add(possession_ref, "BO_POSSESSION_ASSET_REF", possession_ref) + for registry in _as_list(payload.get("registry_rows")): + if isinstance(registry, dict) and registry.get("registry_ref"): + add(registry.get("registry_ref"), "BO_REGISTRY_ROW", registry.get("registry_ref")) + for alias in _as_list(payload.get("asset_alias")): + if isinstance(alias, dict): + refs = _strings(alias.get("schedule_refs")) + _strings(alias.get("registry_refs")) + labels = _strings(alias.get("canonical_label_candidate")) + _strings(alias.get("alias_strings")) + add(labels or refs, "BO_EXPLICIT_ASSET_ALIAS", refs) + for signal in actio_rows: + for candidate in _as_list(signal.get("target_property_candidates")): + if isinstance(candidate, dict): + add(candidate.get("asset_label"), "ACTIO_TARGET_PROPERTY", None, candidate.get("related_evidence_indexes")) + output = [] + seen = set() + for item in candidates: + key = (item["asset_label"], tuple(item["asset_ref_keys"]), item["source_code"]) + if key not in seen: + seen.add(key) + output.append(item) + return sorted(output, key=lambda row: (row["asset_ref_keys"], row["asset_label"], row["source_code"])) + + def _candidate_record(bo: dict[str, Any], signals: dict[str, list[dict[str, Any]]], alias_map: dict[str, Any]) -> dict[str, Any]: + bo_id = _first(bo.get("BO_ID"), bo.get("id")) + if not bo_id: + raise ValueError("BO row missing BO_ID and id") + payload = _as_dict(_as_dict(bo.get("extensions")).get("domain_payload")) + source_domain = _first(_as_dict(bo.get("provenance")).get("source_domain")) + raw_labels = [] + for route in signals["legal_effect"]: + raw_labels.extend(_strings(route.get("candidate_structure_types"))) + for row in signals["case_liability"]: + raw_labels.extend(_strings(row.get("liability_candidate_type"))) + for row in signals["actio"]: + raw_labels.extend(_strings(row.get("actio_scope"))) + raw_labels.extend(_strings(payload.get("domain_legal_effect_tag"))) + raw_labels.extend(_strings(payload.get("legal_effect_tags"))) + raw_labels.extend(_strings(bo.get("Legal_Keywords"))) + canonical = [] + nonrouting = [] + for label in _strings(raw_labels): + mapped = _canonicalize_label(label, source_domain) + if mapped: + canonical.extend(mapped) + else: + nonrouting.append(label) + if not canonical: + canonical = [SOURCE_DOMAIN_FALLBACK.get(source_domain, "general_legal_effect")] + canonical = [item for item in CANONICAL_TYPES if item in canonical] + claim_ids = [] + liability_ids = [] + linked_structure_ids = [] + actio_signal_ids = [] + evidence_indexes = _strings(bo.get("source_evidence_indexes")) + for row in signals["case_liability"]: + claim_ids.extend(_strings(row.get("claim_group_id"))) + liability_ids.extend(_strings(row.get("liability_group_id"))) + linked_structure_ids.extend(_strings(row.get("linked_structure_ids"))) + evidence_indexes.extend(_strings(row.get("related_evidence_indexes"))) + for row in signals["legal_effect"]: + linked_structure_ids.extend(_strings(row.get("linked_structure_ids"))) + evidence_indexes.extend(_strings(row.get("related_evidence_indexes"))) + for row in signals["actio"]: + actio_signal_ids.extend(_strings(row.get("signal_id"))) + evidence_indexes.extend(_strings(row.get("related_evidence_indexes"))) + downstream = _as_dict(bo.get("downstream_seed_refs")) + claim_ids.extend(_strings(downstream.get("claim_group_seed_refs_proposed"))) + linked_structure_ids.extend(_strings(downstream.get("legal_effect_structure_seed_ref_proposed"))) + theory_refs = _strings(downstream.get("canonical_theory_graph_seed_ref_proposed")) + basis_codes = ["BO_SOURCE", "EVIDENCE_INDEX", "SOURCE_DOMAIN_FALLBACK"] + if signals["legal_effect"]: + basis_codes.append("LEGAL_EFFECT_SIGNAL") + if signals["case_liability"]: + basis_codes.append("CASE_LIABILITY_SIGNAL") + if signals["actio"]: + basis_codes.append("ACTIO_SIGNAL") + return { + "bo_id": bo_id, + "source_domain": source_domain, + "hard_issue_keys": { + "claim_group_ids": _sorted_strings(claim_ids), + "liability_group_ids": _sorted_strings(liability_ids), + "canonical_theory_graph_seed_refs": _sorted_strings(theory_refs), + "linked_structure_ids": _sorted_strings(linked_structure_ids), + }, + "canonical_structure_types": canonical, + "nonrouting_labels": _sorted_strings(nonrouting), + "asset_candidates": _asset_candidates(bo, signals["actio"], alias_map), + "evidence_indexes": _sorted_strings(evidence_indexes), + "linked_claim_group_ids": _sorted_strings(claim_ids), + "linked_liability_group_ids": _sorted_strings(liability_ids), + "linked_actio_signal_ids": _sorted_strings(actio_signal_ids), + "issue_basis": { + "legal_keywords": _sorted_strings(bo.get("Legal_Keywords")), + "action": _first(bo.get("Action")), + "reason": _first(bo.get("Reason")), + "juristic_act_label": _first(_as_dict(bo.get("JuristicAct")).get("label")), + "action_type": _first(bo.get("ActionType")), + "bo_type": _first(bo.get("BOType")), + }, + "basis_codes": _sorted_strings(basis_codes), + } + + def main() -> None: + fingerprint = None + try: + _initialize() + docs = {name: _read_doc(name) for name in INPUT_FILES} + bo_items = docs["BO.json"] + if not isinstance(bo_items, list): + raise ValueError("BO.json root must be array") + bo_rows = [row for row in bo_items if isinstance(row, dict)] + bo_ids = [_first(row.get("BO_ID"), row.get("id")) for row in bo_rows] + if any(not value for value in bo_ids): + raise ValueError("every BO row must have BO_ID or id") + if len(set(bo_ids)) != len(bo_ids): + raise ValueError("BO IDs must be unique") + bo_universe = set(bo_ids) + evidence_rows = _evidence_rows(docs["evidence_indexed.json"]) + signal_rows = {name: _signal_rows(docs[name], name) for name in SIGNAL_CONTRACTS} + grouped = {name: _group_signals(rows, bo_universe) for name, rows in signal_rows.items()} + evidence, aliases, input_review = _build_evidence_and_aliases(evidence_rows, bo_rows) + records = {} + for bo in sorted(bo_rows, key=lambda row: _first(row.get("BO_ID"), row.get("id")) or ""): + bo_id = _first(bo.get("BO_ID"), bo.get("id")) or "" + signals = { + "actio": grouped["actio_case_signals.json"].get(bo_id, []), + "case_liability": grouped["case_liability_signals.json"].get(bo_id, []), + "legal_effect": grouped["legal_effect_signals.json"].get(bo_id, []), + } + record = _candidate_record(bo, signals, aliases) + missing = sorted(set(record["evidence_indexes"]) - set(evidence)) + if missing: + input_review.append({ + "review_code": "MISSING_EVIDENCE_AUTHORITY", + "source_bo_ids": [bo_id], + "evidence_indexes": missing, + "related_ref_keys": [], + "reason_code": "REFERENCED_EVIDENCE_NOT_INDEXED", + }) + records[bo_id] = record + if set(records) != bo_universe: + raise ValueError("BO-to-candidate conservation failed") + input_digests = {name: _digest(docs[name]) for name in INPUT_FILES} + fingerprint = _digest(input_digests) + status = "READY_WITH_REVIEW" if input_review else "READY" + payload = { + "legal_structure_input_pack": { + "schema_version": "stage1_legal_structure_input_pack.v2", + "status": status, + "run_fingerprint": fingerprint, + "input_digests": input_digests, + "source_counts": { + "bo": len(bo_rows), + "evidence": len(evidence_rows), + "actio_signals": len(signal_rows["actio_case_signals.json"]), + "case_liability_signals": len(signal_rows["case_liability_signals.json"]), + "legal_effect_signals": len(signal_rows["legal_effect_signals.json"]), + }, + "evidence_authority_by_index": evidence, + "asset_alias_by_ref": aliases, + "candidate_records_by_bo_id": records, + "input_review_items": input_review, + } + } + _write_doc(OUTPUT_PATH, payload) + reread = _read_doc(OUTPUT_PATH) + if _digest(reread) != _digest(payload): + raise ValueError("LES0 post-write reread mismatch") + print(json.dumps({ + "les0_manifest": { + "schema_version": "stage1_legal_structure_input_manifest.v2", + "status": status, + "artifact_path": OUTPUT_PATH, + "run_fingerprint": fingerprint, + "bo_count": len(bo_rows), + "candidate_count": len(records), + "review_count": len(input_review), + } + }, ensure_ascii=False, separators=(",", ":"))) + except Exception as exc: + print(json.dumps({ + "les0_manifest": { + "schema_version": "stage1_legal_structure_input_manifest.v2", + "status": "FAILED", + "artifact_path": OUTPUT_PATH, + "run_fingerprint": fingerprint, + "failure_reason": str(exc), + } + }, ensure_ascii=False, separators=(",", ":"))) + raise + + if __name__ == "__main__": + main() + + - task_name: Task_LES1_deterministic_structure_seed_builder + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: |- + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + from collections import defaultdict + from typing import Any + + import httpx + + LES0_MANIFEST_RAW = r'''{{prev.Task_LES0_input_pack_compiler}}''' + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MESSAGE_IDS = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + INPUT_PATH = "stage1_tmp/legal_effect_structure_input_pack.json" + SEED_PATH = "stage1_tmp/legal_effect_structure_seed_bundle.json" + EXCEPTION_PATH = "stage1_tmp/legal_effect_structure_exception_pack.json" + MAX_EXCEPTION_ITEMS_PER_PACK = 4 + TARGET_EXCEPTION_ITEMS_PER_PACK = 3 + MAX_EXCEPTION_PACK_BYTES = 12000 + MAX_EXCEPTION_CANDIDATE_VALUES = 8 + MAX_EXCEPTION_PACKS = 8 + MAX_CONCURRENCY = 2 + CANONICAL_TYPES = [ + "money_claim", + "commercial_successor", + "secured_debt", + "registry_invalidity", + "land_use_gain", + "valuation", + "fraudulent_transfer", + "preserved_claim", + "succession_notice_lien_asset_defense", + "general_legal_effect", + ] + SOURCE_DOMAIN_FALLBACK = { + "B1_Money_Successor": "money_claim", + "B2_Secured_Registry": "secured_debt", + "B3_Land_Valuation": "land_use_gain", + "B4_Actio": "fraudulent_transfer", + "B5_Succession_Notice_Lien_Asset_Defense": "succession_notice_lien_asset_defense", + } + TYPE_ROLES = { + "money_claim": "claim_chain_ref", + "commercial_successor": "claim_chain_ref", + "secured_debt": "linked_structures", + "registry_invalidity": "linked_structures", + "land_use_gain": "legal_calculation_object", + "valuation": "legal_calculation_object", + "fraudulent_transfer": "actio_roles", + "preserved_claim": "claim_chain_ref", + "succession_notice_lien_asset_defense": "linked_structures", + "general_legal_effect": "legal_effect_roles", + } + HARD_ISSUE_KEY_PRIORITY = [ + "claim_group_ids", + "liability_group_ids", + "canonical_theory_graph_seed_refs", + "linked_structure_ids", + ] + ALLOWED_DECISIONS = { + "ambiguous_issue_cluster": ["USE_EXISTING_HARD_KEY", "PRESERVE_MULTI_ISSUE", "SPLIT_ISSUE_CLUSTER", "BLOCK_REVIEW"], + "ambiguous_asset_cluster": ["SAME_CLUSTER", "SEPARATE_CLUSTER", "MULTI_ASSET_BUNDLE", "BLOCK_REVIEW"], + "ambiguous_candidate_structure_type": ["SELECT_REPRESENTATIVE_TYPE", "PRESERVE_MULTI_ROUTING", "SPLIT_STRUCTURE_SEED", "BLOCK_REVIEW"], + } + REVIEW_CODES = { + "ASSET_ALIAS_LABEL_CONFLICT", + "ASSET_IDENTITY_DEFERRED_SEPARATE", + "ISSUE_FAMILY_LOW_MARGIN", + "MISSING_EVIDENCE_AUTHORITY", + "PACK_CONTEXT_TOO_LARGE", + } + + def _next_id() -> int: + return next(MESSAGE_IDS) + + def _decode_json(value: Any, label: str) -> Any: + if not isinstance(value, str): + return value + text = value.strip() + if not text: + raise ValueError(f"{label}: empty JSON text") + try: + return json.loads(text) + except json.JSONDecodeError: + try: + obj, _ = JSON_DECODER.raw_decode(text) + return obj + except json.JSONDecodeError as exc: + raise ValueError(f"{label}: invalid JSON: {text[:240]}") from exc + + def _parse_rpc(text: str) -> dict[str, Any]: + candidates = [line[5:].strip() for line in text.splitlines() if line.startswith("data:")] + candidates.append(text.strip()) + for candidate in candidates: + if not candidate: + continue + try: + value = _decode_json(candidate, "MCP response") + except ValueError: + continue + if isinstance(value, dict) and ("result" in value or "error" in value): + return value + raise RuntimeError(f"MCP response could not be parsed: {text[:300]}") + + def _initialize() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage1-part3-les1-v2", + "version": "2.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_rpc(response.text) + if parsed.get("error"): + raise RuntimeError(f"MCP {name} failed: {parsed['error']}") + return parsed + + def _read_doc(path: str) -> Any: + rpc = _call_tool("read_docs", {"doc_names": [path]}) + content = rpc.get("result", {}).get("content", []) + if not content or not isinstance(content[0], dict): + raise RuntimeError(f"{path}: read_docs returned no content") + envelope = _decode_json(content[0].get("text", ""), f"{path} envelope") + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results or not isinstance(results[0], dict): + raise RuntimeError(f"{path}: read_docs results are empty") + if results[0].get("error"): + raise RuntimeError(f"{path}: {results[0]['error']}") + inner = results[0].get("content", "") + else: + inner = envelope + return _decode_json(inner, path) + + def _artifact_text(payload: dict[str, Any]) -> str: + return json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n" + + def _artifact_hash(payload: dict[str, Any]) -> str: + return "sha256:" + hashlib.sha256(_artifact_text(payload).encode("utf-8")).hexdigest() + + def _write_doc(path: str, payload: dict[str, Any]) -> None: + _call_tool("write_file", {"path": path, "content": _artifact_text(payload), "overwrite": True}) + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + output = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in output: + output.append(text) + return output + + def _sorted_strings(value: Any) -> list[str]: + return sorted(set(_strings(value))) + + def _stable_id(prefix: str, ordinal: int) -> str: + return f"{prefix}-{ordinal:03d}" + + def _normalized(value: Any) -> str: + return re.sub(r"[^0-9a-zA-Z가-힣]", "", str(value or "").lower()) or "unknown" + + def _priority_types(value: Any, source_domain: str | None) -> list[str]: + supplied = _strings(value) + unknown = sorted(set(supplied) - set(CANONICAL_TYPES)) + if unknown: + raise ValueError(f"candidate record contains non-canonical types: {unknown}") + if not supplied: + supplied = [SOURCE_DOMAIN_FALLBACK.get(source_domain, "general_legal_effect")] + return [item for item in CANONICAL_TYPES if item in supplied] + + def _issue_keys(record: dict[str, Any], representative_type: str) -> tuple[list[str], bool]: + hard = _as_dict(record.get("hard_issue_keys")) + keys = [] + for family in HARD_ISSUE_KEY_PRIORITY: + for value in _sorted_strings(hard.get(family)): + keys.append(f"hard:{family}:{value}") + if keys: + return keys, False + return [f"family:{representative_type}"], representative_type == "general_legal_effect" + + def _add_review( + reviews: list[dict[str, Any]], + review_index: dict[tuple[Any, ...], str], + review_by_bo: dict[str, list[str]], + code: str, + bo_ids: Any, + evidence_indexes: Any, + ref_keys: Any, + reason_code: str, + severity: str = "WARNING", + owner: str = "STAGE_2", + ) -> str: + if code not in REVIEW_CODES: + raise ValueError(f"unsupported review code: {code}") + bos = _sorted_strings(bo_ids) + evidence = _sorted_strings(evidence_indexes) + refs = _sorted_strings(ref_keys) + key = (code, tuple(bos), tuple(evidence), tuple(refs), reason_code, severity, owner) + if key in review_index: + return review_index[key] + review_id = _stable_id("LRV", len(reviews) + 1) + item = { + "review_id": review_id, + "review_code": code, + "severity": severity, + "source_bo_ids": bos, + "evidence_indexes": evidence, + "related_ref_keys": refs, + "reason_code": reason_code, + "downstream_owner": owner, + } + reviews.append(item) + review_index[key] = review_id + for bo_id in bos: + review_by_bo[bo_id].append(review_id) + return review_id + + def _explicit_exception_draft(record: dict[str, Any], issue_ids: list[str], asset_ids: list[str], types: list[str]) -> dict[str, Any] | None: + codes = set(_strings(record.get("basis_codes"))) + common = {"FINAL_SINGLE_SELECTION_REQUIRED", "PRESERVE_OR_DEFER_PROHIBITED", "COMPACT_CONTEXT_SUFFICIENT"} + if not common.issubset(codes): + return None + if "AMBIGUOUS_ISSUE_FINAL_REQUIRED" in codes: + exception_type = "ambiguous_issue_cluster" + candidates = issue_ids + elif "AMBIGUOUS_ASSET_FINAL_REQUIRED" in codes: + exception_type = "ambiguous_asset_cluster" + candidates = asset_ids + elif "AMBIGUOUS_TYPE_FINAL_REQUIRED" in codes: + exception_type = "ambiguous_candidate_structure_type" + candidates = types + else: + return None + candidates = _sorted_strings(candidates) + if len(candidates) < 2: + return None + return { + "exception_type": exception_type, + "source_bo_ids": [record["bo_id"]], + "candidate_values": candidates, + "basis_codes": sorted(common | {code for code in codes if code.startswith("AMBIGUOUS_")}), + "score_summary": [], + "conflict_codes": ["FINALIZATION_REQUIRES_IRREVERSIBLE_CHOICE"], + "allowed_decisions": ALLOWED_DECISIONS[exception_type], + "compact_context": { + "bo_id": record["bo_id"], + "source_domain": record.get("source_domain"), + "candidate_values": candidates, + "basis_codes": sorted(common), + }, + } + + def main() -> None: + fingerprint = None + try: + _initialize() + manifest_outer = _decode_json(LES0_MANIFEST_RAW, "LES0 manifest") + manifest = _as_dict(_as_dict(manifest_outer).get("les0_manifest")) + if manifest.get("schema_version") != "stage1_legal_structure_input_manifest.v2": + raise ValueError("LES0 manifest schema mismatch") + if manifest.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("LES0 manifest is not consumable") + if manifest.get("artifact_path") != INPUT_PATH: + raise ValueError("LES0 artifact path mismatch") + input_outer = _read_doc(INPUT_PATH) + input_pack = _as_dict(_as_dict(input_outer).get("legal_structure_input_pack")) + if input_pack.get("schema_version") != "stage1_legal_structure_input_pack.v2": + raise ValueError("input pack schema mismatch") + if input_pack.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("input pack status is not consumable") + fingerprint = input_pack.get("run_fingerprint") + if not fingerprint or fingerprint != manifest.get("run_fingerprint"): + raise ValueError("LES0 fingerprint mismatch") + records = _as_dict(input_pack.get("candidate_records_by_bo_id")) + if len(records) != int(manifest.get("candidate_count", -1)): + raise ValueError("LES0 candidate count mismatch") + evidence_authority = _as_dict(input_pack.get("evidence_authority_by_index")) + alias_map = _as_dict(input_pack.get("asset_alias_by_ref")) + + reviews = [] + review_index = {} + review_by_bo = defaultdict(list) + for source_review in _as_list(input_pack.get("input_review_items")): + if not isinstance(source_review, dict): + continue + code = source_review.get("review_code") + if code not in REVIEW_CODES: + continue + _add_review( + reviews, + review_index, + review_by_bo, + code, + source_review.get("source_bo_ids"), + source_review.get("evidence_indexes"), + source_review.get("related_ref_keys"), + str(source_review.get("reason_code") or "UPSTREAM_REVIEW"), + "HARD_WARNING" if code == "ASSET_ALIAS_LABEL_CONFLICT" else "WARNING", + "HUMAN_REVIEW" if code == "ASSET_ALIAS_LABEL_CONFLICT" else "STAGE_2", + ) + + issue_key_to_id = {} + asset_key_to_id = {} + issue_map = {} + asset_map = {} + seeds = [] + exception_drafts = [] + + for bo_id in sorted(records): + record = _as_dict(records[bo_id]) + if record.get("bo_id") != bo_id: + raise ValueError(f"candidate record key mismatch: {bo_id}") + types = _priority_types(record.get("canonical_structure_types"), record.get("source_domain")) + representative = types[0] + issue_keys, low_margin = _issue_keys(record, representative) + issue_ids = [] + for key in issue_keys: + if key not in issue_key_to_id: + issue_id = _stable_id("IC", len(issue_key_to_id) + 1) + issue_key_to_id[key] = issue_id + issue_map[issue_id] = { + "issue_cluster_id": issue_id, + "cluster_key": key, + "source_bo_ids": [], + "evidence_indexes": [], + "candidate_structure_types": [], + "basis_codes": [], + } + issue_id = issue_key_to_id[key] + issue_ids.append(issue_id) + row = issue_map[issue_id] + row["source_bo_ids"] = _sorted_strings(row["source_bo_ids"] + [bo_id]) + row["evidence_indexes"] = _sorted_strings(row["evidence_indexes"] + _strings(record.get("evidence_indexes"))) + row["candidate_structure_types"] = [item for item in CANONICAL_TYPES if item in row["candidate_structure_types"] + types] + row["basis_codes"] = _sorted_strings(row["basis_codes"] + _strings(record.get("basis_codes"))) + if low_margin: + _add_review( + reviews, review_index, review_by_bo, + "ISSUE_FAMILY_LOW_MARGIN", [bo_id], record.get("evidence_indexes"), [], + "GENERAL_LEGAL_EFFECT_FALLBACK", "WARNING", "STAGE_2", + ) + + asset_ids = [] + free_asset_count = 0 + for candidate in _as_list(record.get("asset_candidates")): + if not isinstance(candidate, dict): + continue + refs = _sorted_strings(candidate.get("asset_ref_keys")) + if refs: + keys = [f"ref:{ref}" for ref in refs] + unknown_refs = sorted(set(refs) - set(alias_map)) + if unknown_refs: + _add_review( + reviews, review_index, review_by_bo, + "ASSET_IDENTITY_DEFERRED_SEPARATE", [bo_id], candidate.get("evidence_indexes"), unknown_refs, + "UNKNOWN_SOURCE_REF", "HARD_WARNING", "HUMAN_REVIEW", + ) + else: + free_asset_count += 1 + keys = [f"free:{bo_id}:{candidate.get('asset_kind') or 'unknown'}:{_normalized(candidate.get('asset_label'))}"] + for key in keys: + if key not in asset_key_to_id: + asset_id = _stable_id("AC", len(asset_key_to_id) + 1) + asset_key_to_id[key] = asset_id + asset_map[asset_id] = { + "asset_cluster_id": asset_id, + "cluster_key": key, + "asset_kind": candidate.get("asset_kind") or "unknown", + "canonical_label_candidate": candidate.get("asset_label"), + "asset_ref_keys": refs, + "source_bo_ids": [], + "evidence_indexes": [], + } + asset_id = asset_key_to_id[key] + asset_ids.append(asset_id) + row = asset_map[asset_id] + row["source_bo_ids"] = _sorted_strings(row["source_bo_ids"] + [bo_id]) + row["evidence_indexes"] = _sorted_strings(row["evidence_indexes"] + _strings(candidate.get("evidence_indexes"))) + asset_ids = _sorted_strings(asset_ids) + if free_asset_count >= 2: + _add_review( + reviews, review_index, review_by_bo, + "ASSET_IDENTITY_DEFERRED_SEPARATE", [bo_id], record.get("evidence_indexes"), [], + "MULTIPLE_REF_FREE_ASSET_MENTIONS", "WARNING", "STAGE_2", + ) + bundle_refs = [_stable_id("BUNDLE", len(seeds) + 1)] if len(asset_ids) > 1 else [] + seed_id = _stable_id("LESS", len(seeds) + 1) + seed = { + "seed_id": seed_id, + "source_bo_ids": [bo_id], + "candidate_structure_type": representative, + "candidate_structure_types": types, + "issue_cluster_ids": _sorted_strings(issue_ids), + "asset_cluster_ids": asset_ids, + "multi_asset_bundle_refs": bundle_refs, + "evidence_indexes": _sorted_strings(record.get("evidence_indexes")), + "linked_claim_group_ids": _sorted_strings(record.get("linked_claim_group_ids")), + "linked_liability_group_ids": _sorted_strings(record.get("linked_liability_group_ids")), + "linked_actio_signal_ids": _sorted_strings(record.get("linked_actio_signal_ids")), + "legal_effect_roles": _sorted_strings([TYPE_ROLES[item] for item in types]), + "basis_codes": _sorted_strings(record.get("basis_codes")), + "confidence": "HIGH", + "pending_exception_ids": [], + "deterministic_review_ids": [], + } + draft = _explicit_exception_draft(record, issue_ids, asset_ids, types) + if draft: + exception_drafts.append((seed, draft)) + seeds.append(seed) + + exceptions = [] + for seed, draft in exception_drafts: + candidates = _strings(draft.get("candidate_values")) + preview = dict(draft) + if len(candidates) > MAX_EXCEPTION_CANDIDATE_VALUES or len(_artifact_text(preview).encode("utf-8")) > MAX_EXCEPTION_PACK_BYTES: + review_id = _add_review( + reviews, review_index, review_by_bo, + "PACK_CONTEXT_TOO_LARGE", draft.get("source_bo_ids"), [], [], + "EXCEPTION_ITEM_EXCEEDS_MICRO_PACK_BUDGET", "HARD_WARNING", "HUMAN_REVIEW", + ) + seed["deterministic_review_ids"].append(review_id) + continue + exception_id = _stable_id("LEX", len(exceptions) + 1) + item = {"exception_id": exception_id, **draft} + exceptions.append(item) + seed["pending_exception_ids"].append(exception_id) + + for seed in seeds: + bo_ids = seed["source_bo_ids"] + seed["deterministic_review_ids"] = _sorted_strings( + seed["deterministic_review_ids"] + [review_id for bo_id in bo_ids for review_id in review_by_bo.get(bo_id, [])] + ) + if seed["deterministic_review_ids"]: + seed["confidence"] = "MEDIUM" + + packs = [] + by_type = defaultdict(list) + for item in exceptions: + by_type[item["exception_type"]].append(item) + for exception_type in sorted(by_type): + rows = by_type[exception_type] + for offset in range(0, len(rows), TARGET_EXCEPTION_ITEMS_PER_PACK): + items = rows[offset:offset + TARGET_EXCEPTION_ITEMS_PER_PACK] + pack_id = _stable_id("LES-PACK", len(packs) + 1) + descriptor = { + "pack_id": pack_id, + "exception_type": exception_type, + "exception_ids": [item["exception_id"] for item in items], + "items": items, + "output_path": f"stage1_tmp/legal_effect_structure_adjudication_parts/PACK-{len(packs) + 1:03d}.json", + "run_fingerprint": fingerprint, + } + if len(_artifact_text(descriptor).encode("utf-8")) > MAX_EXCEPTION_PACK_BYTES: + raise ValueError(f"{pack_id}: serialized micro-pack exceeds {MAX_EXCEPTION_PACK_BYTES} bytes") + packs.append(descriptor) + if len(packs) > MAX_EXCEPTION_PACKS: + blocked_payload = { + "legal_structure_exception_pack": { + "schema_version": "stage1_legal_structure_exception_pack.v2", + "status": "BLOCKED", + "run_fingerprint": fingerprint, + "expected_exception_ids": [item["exception_id"] for item in exceptions], + "exceptions": exceptions, + "deterministic_review_items": reviews, + "pack_count": len(packs), + "packs": packs, + } + } + _write_doc(EXCEPTION_PATH, blocked_payload) + raise ValueError(f"exception pack count {len(packs)} exceeds {MAX_EXCEPTION_PACKS}") + + expected_ids = [item["exception_id"] for item in exceptions] + packed_ids = [exception_id for pack in packs for exception_id in pack["exception_ids"]] + if sorted(expected_ids) != sorted(packed_ids) or len(packed_ids) != len(set(packed_ids)): + raise ValueError("exception-to-pack exact coverage failed") + exception_status = "READY" if exceptions else ("READY_WITH_REVIEW" if reviews else "READY_NO_EXCEPTIONS") + exception_payload = { + "legal_structure_exception_pack": { + "schema_version": "stage1_legal_structure_exception_pack.v2", + "status": exception_status, + "run_fingerprint": fingerprint, + "expected_exception_ids": expected_ids, + "exceptions": exceptions, + "deterministic_review_items": reviews, + "pack_count": len(packs), + "packs": packs, + } + } + exception_hash = _artifact_hash(exception_payload) + referenced_evidence = sorted({evidence_id for seed in seeds for evidence_id in seed["evidence_indexes"]}) + seed_status = "READY_WITH_REVIEW" if reviews else "READY" + seed_payload = { + "legal_structure_seed_bundle": { + "schema_version": "stage1_legal_structure_seed_bundle.v2", + "status": seed_status, + "run_fingerprint": fingerprint, + "source_input_pack": { + "path": INPUT_PATH, + "schema_version": "stage1_legal_structure_input_pack.v2", + "sha256": _artifact_hash(input_outer), + }, + "structure_seed_items": seeds, + "issue_cluster_seed_map": issue_map, + "asset_cluster_seed_map": asset_map, + "evidence_index_seed": {key: evidence_authority[key] for key in referenced_evidence if key in evidence_authority}, + "deterministic_review_items": reviews, + "exception_pack_ref": { + "path": EXCEPTION_PATH, + "schema_version": "stage1_legal_structure_exception_pack.v2", + "sha256": exception_hash, + "expected_exception_ids": expected_ids, + }, + } + } + if {bo_id for seed in seeds for bo_id in seed["source_bo_ids"]} != set(records): + raise ValueError("candidate-to-seed BO conservation failed") + _write_doc(EXCEPTION_PATH, exception_payload) + _write_doc(SEED_PATH, seed_payload) + if _artifact_hash(_read_doc(EXCEPTION_PATH)) != exception_hash: + raise ValueError("exception pack post-write hash mismatch") + if _artifact_hash(_read_doc(SEED_PATH)) != _artifact_hash(seed_payload): + raise ValueError("seed bundle post-write hash mismatch") + manifest_status = "READY" if exceptions and not reviews else ("READY_WITH_REVIEW" if reviews else "READY_NO_EXCEPTIONS") + print(json.dumps({ + "les1_manifest": { + "schema_version": "stage1_legal_structure_les1_manifest.v2", + "status": manifest_status, + "seed_path": SEED_PATH, + "exception_pack_path": EXCEPTION_PATH, + "run_fingerprint": fingerprint, + "seed_count": len(seeds), + "exception_count": len(exceptions), + "review_count": len(reviews), + "pack_count": len(packs), + }, + "dynamic_fanout": packs, + }, ensure_ascii=False, separators=(",", ":"))) + except Exception as exc: + print(json.dumps({ + "les1_manifest": { + "schema_version": "stage1_legal_structure_les1_manifest.v2", + "status": "FAILED", + "seed_path": SEED_PATH, + "exception_pack_path": EXCEPTION_PATH, + "run_fingerprint": fingerprint, + "failure_reason": str(exc), + }, + "dynamic_fanout": [], + }, ensure_ascii=False, separators=(",", ":"))) + raise + + if __name__ == "__main__": + main() + + - task_name: Task_LES2_exception_adjudicator_* + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: medium + llm_verbosity: low + max_iterations: 2 + max_concurrency: 2 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + + TASK_NAME: Task_LES2_exception_adjudicator_* + STAGE: Stage 1 legal effect structure and index builder + ROLE: compact exception-only routing adjudicator + + + + pack_id: {{item.pack_id}} + exception_type: {{item.exception_type}} + exception_ids: {{item.exception_ids}} + items: {{item.items}} + output_path: {{item.output_path}} + run_fingerprint: {{item.run_fingerprint}} + + + + - Act as a senior Korean civil-litigation lawyer and AI systems architect, but make only the narrow routing decision authorized below. + - Use only ASSIGNED_PACK. Do not read, request, or reconstruct BO.json, evidence files, signal files, the LES0 input pack, the LES1 seed bundle, or any other pack. + - Do not create a fact, party, date, amount, BO ID, evidence index, exception ID, candidate value, cluster ID, or structure ID. + - Process every assigned item exactly once. Do not omit, merge, duplicate, or reorder exception IDs. + - Choose only a decision listed in that item's allowed_decisions. + - selected_value, selected_values, preserve_candidate_values, and split_plan candidate_values must be subsets of that item's candidate_values. + - If compact context is insufficient, use BLOCK_REVIEW. Never guess. + - Do not write legal_effect_structures.json, the seed bundle, or the exception pack. + - Write exactly one adjudication part to ASSIGNED_PACK.output_path with localdocs write_file and overwrite=true. + - After the write succeeds, return only the compact manifest JSON specified below. No markdown or prose. + + + + For ambiguous_issue_cluster, use only: + - USE_EXISTING_HARD_KEY when one listed hard-key candidate controls. + - PRESERVE_MULTI_ISSUE when all listed issue routes must remain. + - SPLIT_ISSUE_CLUSTER only when the listed candidates require separate structures and the split is supported by compact context. + - BLOCK_REVIEW otherwise. + + For ambiguous_asset_cluster, use only: + - SAME_CLUSTER when a listed exact ref or explicit alias proves identity. + - SEPARATE_CLUSTER when listed source refs differ or conservative separation is required. + - MULTI_ASSET_BUNDLE when all listed assets remain jointly relevant without identity merger. + - BLOCK_REVIEW otherwise. + + For ambiguous_candidate_structure_type, use only: + - SELECT_REPRESENTATIVE_TYPE when one listed canonical type must control finalization. + - PRESERVE_MULTI_ROUTING when all listed canonical routes remain valid. + - SPLIT_STRUCTURE_SEED only when separate structures are required and supported. + - BLOCK_REVIEW otherwise. + + + + Each decisions[] item must contain exactly: + { + "exception_id": "LEX-001", + "exception_type": "ambiguous_issue_cluster|ambiguous_asset_cluster|ambiguous_candidate_structure_type", + "source_bo_ids": [], + "decision": "one allowed enum", + "selected_value": null, + "selected_values": [], + "preserve_candidate_values": [], + "split_plan": [], + "review_required": false, + "reason_code": "HARD_KEY_CONTROLS|SOURCE_REF_IDENTITY|CONSERVATIVE_SEPARATION|PRESERVE_ALL_ROUTING|SPLIT_REQUIRED|INSUFFICIENT_COMPACT_CONTEXT", + "basis_codes_used": [], + "conflict_codes_acknowledged": [] + } + BLOCK_REVIEW and every split decision must set review_required=true. Other completed decisions set it to false. + + + + Write this raw JSON object to ASSIGNED_PACK.output_path: + { + "schema_version": "stage1_legal_structure_adjudication_part.v2", + "status": "READY|READY_WITH_BLOCKS|FAILED", + "pack_id": "the assigned pack_id", + "run_fingerprint": "the assigned run_fingerprint", + "exception_type": "the assigned exception_type", + "input_exception_ids": ["all assigned exception IDs exactly once"], + "decisions": ["one decision per assigned exception"], + "coverage": { + "expected": 0, + "actual": 0, + "missing_exception_ids": [], + "unknown_exception_ids": [], + "coverage_pass": true + } + } + Use READY_WITH_BLOCKS when any decision is BLOCK_REVIEW; otherwise use READY. + + + + After localdocs confirms the write, return exactly: + { + "pack_id": "the assigned pack_id", + "status": "READY|READY_WITH_BLOCKS|FAILED", + "output_path": "the assigned output_path", + "decision_count": 0 + } + + + + - task_name: Task_LES3_final_structure_index_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: |- + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + from collections import defaultdict + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MESSAGE_IDS = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + SEED_PATH = "stage1_tmp/legal_effect_structure_seed_bundle.json" + EXCEPTION_PATH = "stage1_tmp/legal_effect_structure_exception_pack.json" + TARGET_PATH = "legal_effect_structures.json" + CANONICAL_TYPES = [ + "money_claim", + "commercial_successor", + "secured_debt", + "registry_invalidity", + "land_use_gain", + "valuation", + "fraudulent_transfer", + "preserved_claim", + "succession_notice_lien_asset_defense", + "general_legal_effect", + ] + TYPE_ROLES = { + "money_claim": "claim_chain_ref", + "commercial_successor": "claim_chain_ref", + "secured_debt": "linked_structures", + "registry_invalidity": "linked_structures", + "land_use_gain": "legal_calculation_object", + "valuation": "legal_calculation_object", + "fraudulent_transfer": "actio_roles", + "preserved_claim": "claim_chain_ref", + "succession_notice_lien_asset_defense": "linked_structures", + "general_legal_effect": "legal_effect_roles", + } + ALLOWED_DECISIONS = { + "ambiguous_issue_cluster": {"USE_EXISTING_HARD_KEY", "PRESERVE_MULTI_ISSUE", "SPLIT_ISSUE_CLUSTER", "BLOCK_REVIEW"}, + "ambiguous_asset_cluster": {"SAME_CLUSTER", "SEPARATE_CLUSTER", "MULTI_ASSET_BUNDLE", "BLOCK_REVIEW"}, + "ambiguous_candidate_structure_type": {"SELECT_REPRESENTATIVE_TYPE", "PRESERVE_MULTI_ROUTING", "SPLIT_STRUCTURE_SEED", "BLOCK_REVIEW"}, + } + + def _next_id() -> int: + return next(MESSAGE_IDS) + + def _decode_json(value: Any, label: str) -> Any: + if not isinstance(value, str): + return value + text = value.strip() + if not text: + raise ValueError(f"{label}: empty JSON text") + if text.startswith("```"): + text = re.sub(r"^```(?:json)?\s*", "", text) + text = re.sub(r"\s*```$", "", text) + try: + return json.loads(text) + except json.JSONDecodeError: + try: + obj, _ = JSON_DECODER.raw_decode(text) + return obj + except json.JSONDecodeError as exc: + raise ValueError(f"{label}: invalid JSON: {text[:240]}") from exc + + def _parse_rpc(text: str) -> dict[str, Any]: + candidates = [line[5:].strip() for line in text.splitlines() if line.startswith("data:")] + candidates.append(text.strip()) + for candidate in candidates: + if not candidate: + continue + try: + value = _decode_json(candidate, "MCP response") + except ValueError: + continue + if isinstance(value, dict) and ("result" in value or "error" in value): + return value + raise RuntimeError(f"MCP response could not be parsed: {text[:300]}") + + def _initialize() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage1-part3-les3-v2", + "version": "2.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_rpc(response.text) + if parsed.get("error"): + raise RuntimeError(f"MCP {name} failed: {parsed['error']}") + return parsed + + def _read_doc(path: str) -> Any: + rpc = _call_tool("read_docs", {"doc_names": [path]}) + content = rpc.get("result", {}).get("content", []) + if not content or not isinstance(content[0], dict): + raise RuntimeError(f"{path}: read_docs returned no content") + envelope = _decode_json(content[0].get("text", ""), f"{path} envelope") + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results or not isinstance(results[0], dict): + raise RuntimeError(f"{path}: read_docs results are empty") + if results[0].get("error"): + raise RuntimeError(f"{path}: {results[0]['error']}") + inner = results[0].get("content", "") + else: + inner = envelope + return _decode_json(inner, path) + + def _artifact_text(payload: dict[str, Any]) -> str: + return json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n" + + def _artifact_hash(payload: dict[str, Any]) -> str: + return "sha256:" + hashlib.sha256(_artifact_text(payload).encode("utf-8")).hexdigest() + + def _write_doc(path: str, payload: dict[str, Any]) -> None: + _call_tool("write_file", {"path": path, "content": _artifact_text(payload), "overwrite": True}) + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + output = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in output: + output.append(text) + return output + + def _sorted_strings(value: Any) -> list[str]: + return sorted(set(_strings(value))) + + def _priority_types(value: Any) -> list[str]: + supplied = _strings(value) + unknown = sorted(set(supplied) - set(CANONICAL_TYPES)) + if unknown: + raise ValueError(f"non-canonical structure types: {unknown}") + if not supplied: + supplied = ["general_legal_effect"] + return [item for item in CANONICAL_TYPES if item in supplied] + + def _next_numeric_id(prefix: str, rows: dict[str, Any]) -> str: + numbers = [] + for key in rows: + match = re.fullmatch(re.escape(prefix) + r"-(\d+)", str(key)) + if match: + numbers.append(int(match.group(1))) + return f"{prefix}-{max(numbers, default=0) + 1:03d}" + + def _validate_decision(decision: dict[str, Any], expected: dict[str, Any]) -> None: + if decision.get("exception_id") != expected.get("exception_id"): + raise ValueError("decision exception_id mismatch") + exception_type = expected.get("exception_type") + if decision.get("exception_type") != exception_type: + raise ValueError(f"{expected.get('exception_id')}: exception_type mismatch") + choice = decision.get("decision") + if choice not in ALLOWED_DECISIONS.get(exception_type, set()): + raise ValueError(f"{expected.get('exception_id')}: invalid decision {choice}") + candidates = set(_strings(expected.get("candidate_values"))) + for field in ["selected_values", "preserve_candidate_values"]: + extras = set(_strings(decision.get(field))) - candidates + if extras: + raise ValueError(f"{expected.get('exception_id')}: {field} outside candidates: {sorted(extras)}") + selected = decision.get("selected_value") + if selected is not None and str(selected) not in candidates: + raise ValueError(f"{expected.get('exception_id')}: selected_value outside candidates") + for split in _as_list(decision.get("split_plan")): + values = split.get("candidate_values") if isinstance(split, dict) else split + extras = set(_strings(values)) - candidates + if extras: + raise ValueError(f"{expected.get('exception_id')}: split_plan outside candidates: {sorted(extras)}") + if choice == "BLOCK_REVIEW" and decision.get("review_required") is not True: + raise ValueError(f"{expected.get('exception_id')}: BLOCK_REVIEW must require review") + + def _load_inputs() -> tuple[dict[str, Any], dict[str, Any], dict[str, dict[str, Any]]]: + seed_outer = _read_doc(SEED_PATH) + exception_outer = _read_doc(EXCEPTION_PATH) + seed = _as_dict(_as_dict(seed_outer).get("legal_structure_seed_bundle")) + pack = _as_dict(_as_dict(exception_outer).get("legal_structure_exception_pack")) + if seed.get("schema_version") != "stage1_legal_structure_seed_bundle.v2": + raise ValueError("seed bundle schema mismatch") + if seed.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("seed bundle status is not consumable") + if pack.get("schema_version") != "stage1_legal_structure_exception_pack.v2": + raise ValueError("exception pack schema mismatch") + if pack.get("status") not in {"READY", "READY_NO_EXCEPTIONS", "READY_WITH_REVIEW"}: + raise ValueError("exception pack status is not consumable") + fingerprint = seed.get("run_fingerprint") + if not fingerprint or fingerprint != pack.get("run_fingerprint"): + raise ValueError("seed/exception fingerprint mismatch") + pack_ref = _as_dict(seed.get("exception_pack_ref")) + if pack_ref.get("path") != EXCEPTION_PATH or pack_ref.get("schema_version") != "stage1_legal_structure_exception_pack.v2": + raise ValueError("seed exception_pack_ref path/schema mismatch") + if pack_ref.get("sha256") != _artifact_hash(exception_outer): + raise ValueError("seed exception_pack_ref hash mismatch") + expected_rows = {} + for item in _as_list(pack.get("exceptions")): + if not isinstance(item, dict) or not item.get("exception_id"): + raise ValueError("exception item missing exception_id") + exception_id = str(item["exception_id"]) + if exception_id in expected_rows: + raise ValueError(f"duplicate exception_id: {exception_id}") + expected_rows[exception_id] = item + expected_ids = _strings(pack.get("expected_exception_ids")) + if set(expected_ids) != set(expected_rows) or len(expected_ids) != len(expected_rows): + raise ValueError("expected exception ID set mismatch") + packs = [row for row in _as_list(pack.get("packs")) if isinstance(row, dict)] + if int(pack.get("pack_count", -1)) != len(packs): + raise ValueError("exception pack count mismatch") + packed_ids = [exception_id for row in packs for exception_id in _strings(row.get("exception_ids"))] + if set(packed_ids) != set(expected_ids) or len(packed_ids) != len(set(packed_ids)): + raise ValueError("pack-to-exception exact coverage failed") + decisions = {} + for descriptor in packs: + output_path = descriptor.get("output_path") + if not isinstance(output_path, str) or not output_path: + raise ValueError("pack descriptor missing output_path") + part = _as_dict(_read_doc(output_path)) + if part.get("schema_version") != "stage1_legal_structure_adjudication_part.v2": + raise ValueError(f"{output_path}: adjudication schema mismatch") + if part.get("status") not in {"READY", "READY_WITH_BLOCKS"}: + raise ValueError(f"{output_path}: adjudication status is not consumable") + if part.get("pack_id") != descriptor.get("pack_id") or part.get("run_fingerprint") != fingerprint: + raise ValueError(f"{output_path}: pack ID/fingerprint mismatch") + part_expected = _strings(part.get("input_exception_ids")) + descriptor_expected = _strings(descriptor.get("exception_ids")) + if set(part_expected) != set(descriptor_expected) or len(part_expected) != len(descriptor_expected): + raise ValueError(f"{output_path}: input exception coverage mismatch") + coverage = _as_dict(part.get("coverage")) + if coverage.get("coverage_pass") is not True: + raise ValueError(f"{output_path}: reported coverage failure") + for decision in _as_list(part.get("decisions")): + if not isinstance(decision, dict) or not decision.get("exception_id"): + raise ValueError(f"{output_path}: decision missing exception_id") + exception_id = str(decision["exception_id"]) + if exception_id not in expected_rows: + raise ValueError(f"{output_path}: unknown exception ID {exception_id}") + if exception_id in decisions: + raise ValueError(f"duplicate decision for {exception_id}") + _validate_decision(decision, expected_rows[exception_id]) + decisions[exception_id] = decision + if set(decisions) != set(expected_ids): + missing = sorted(set(expected_ids) - set(decisions)) + unknown = sorted(set(decisions) - set(expected_ids)) + raise ValueError(f"decision exact coverage failed; missing={missing}; unknown={unknown}") + return seed, pack, decisions + + def _decision_groups(seed: dict[str, Any], decisions: dict[str, dict[str, Any]]) -> dict[str, list[dict[str, Any]]]: + output = defaultdict(list) + for exception_id in _strings(seed.get("pending_exception_ids")): + if exception_id not in decisions: + raise ValueError(f"seed references missing decision: {exception_id}") + output[decisions[exception_id]["exception_type"]].append(decisions[exception_id]) + return output + + def _split_values(decision: dict[str, Any]) -> list[list[str]]: + groups = [] + for row in _as_list(decision.get("split_plan")): + values = row.get("candidate_values") if isinstance(row, dict) else row + values = _strings(values) + if values: + groups.append(values) + if not groups: + values = _strings(decision.get("selected_values")) + groups = [[value] for value in values] + return groups + + def _apply_issue_decisions(ids: list[str], rows: list[dict[str, Any]], issue_map: dict[str, Any], bo_ids: list[str]) -> tuple[list[str], bool]: + output = list(ids) + split_review = False + for decision in rows: + choice = decision.get("decision") + if choice == "USE_EXISTING_HARD_KEY": + selected = _strings(decision.get("selected_value")) + _strings(decision.get("selected_values")) + selected = [value for value in selected if value in issue_map] + if selected: + output = selected + elif choice == "SPLIT_ISSUE_CLUSTER": + split_review = True + output = [] + for group in _split_values(decision): + existing = [value for value in group if value in issue_map] + if existing: + output.extend(existing) + else: + issue_id = _next_numeric_id("IC", issue_map) + issue_map[issue_id] = { + "issue_cluster_id": issue_id, + "cluster_key": "adjudicated:" + "|".join(group), + "source_bo_ids": bo_ids, + "evidence_indexes": [], + "candidate_structure_types": [], + "basis_codes": ["LES2_SPLIT_ISSUE_CLUSTER"], + } + output.append(issue_id) + return _sorted_strings(output), split_review + + def _apply_asset_decisions(ids: list[str], rows: list[dict[str, Any]], asset_map: dict[str, Any], bo_ids: list[str]) -> tuple[list[str], list[str], bool]: + output = list(ids) + bundle_refs = [] + split_review = False + for decision in rows: + choice = decision.get("decision") + values = _strings(decision.get("selected_value")) + _strings(decision.get("selected_values")) + existing = [value for value in values if value in asset_map] + if choice == "SAME_CLUSTER" and existing: + output = [existing[0]] + elif choice == "SEPARATE_CLUSTER" and existing: + output = existing + elif choice == "MULTI_ASSET_BUNDLE": + if existing: + output = existing + bundle_refs.append("BUNDLE-ADJ-" + str(decision.get("exception_id"))) + if choice in {"SEPARATE_CLUSTER", "MULTI_ASSET_BUNDLE"}: + split_review = bool(decision.get("review_required")) + return _sorted_strings(output), _sorted_strings(bundle_refs), split_review + + def _type_plans(types: list[str], representative: str, rows: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], bool]: + plans = [{"types": types, "representative": representative, "split_label": None}] + split_review = False + for decision in rows: + choice = decision.get("decision") + if choice == "SELECT_REPRESENTATIVE_TYPE": + selected = str(decision.get("selected_value") or representative) + if selected not in types: + raise ValueError(f"{decision.get('exception_id')}: selected type not in seed types") + plans = [{"types": types, "representative": selected, "split_label": None}] + elif choice == "PRESERVE_MULTI_ROUTING": + plans = [{"types": types, "representative": representative, "split_label": None}] + elif choice == "SPLIT_STRUCTURE_SEED": + split_review = True + groups = _split_values(decision) + plans = [] + for ordinal, group in enumerate(groups, 1): + group_types = _priority_types(group) + plans.append({"types": group_types, "representative": group_types[0], "split_label": f"split-{ordinal}"}) + return plans, split_review + + def _build_structures(seed: dict[str, Any], decisions: dict[str, dict[str, Any]]) -> tuple[list[dict[str, Any]], dict[str, Any], dict[str, Any]]: + issue_map = {key: dict(value) for key, value in _as_dict(seed.get("issue_cluster_seed_map")).items() if isinstance(value, dict)} + asset_map = {key: dict(value) for key, value in _as_dict(seed.get("asset_cluster_seed_map")).items() if isinstance(value, dict)} + structures = [] + for seed_item in _as_list(seed.get("structure_seed_items")): + if not isinstance(seed_item, dict): + raise ValueError("structure seed item must be object") + bo_ids = _sorted_strings(seed_item.get("source_bo_ids")) + if not bo_ids: + raise ValueError("structure seed item missing source BO") + groups = _decision_groups(seed_item, decisions) + issue_ids, issue_split = _apply_issue_decisions( + _strings(seed_item.get("issue_cluster_ids")), groups.get("ambiguous_issue_cluster", []), issue_map, bo_ids + ) + asset_ids, adjudicated_bundles, asset_split = _apply_asset_decisions( + _strings(seed_item.get("asset_cluster_ids")), groups.get("ambiguous_asset_cluster", []), asset_map, bo_ids + ) + types = _priority_types(seed_item.get("candidate_structure_types")) + representative = str(seed_item.get("candidate_structure_type") or types[0]) + if representative not in types: + representative = types[0] + plans, type_split = _type_plans(types, representative, groups.get("ambiguous_candidate_structure_type", [])) + pending_ids = _strings(seed_item.get("pending_exception_ids")) + blocked_ids = [exception_id for exception_id in pending_ids if decisions[exception_id].get("decision") == "BLOCK_REVIEW"] + resolved_ids = [exception_id for exception_id in pending_ids if exception_id not in blocked_ids] + deterministic_review_ids = _sorted_strings(seed_item.get("deterministic_review_ids")) + review_required = bool(deterministic_review_ids or blocked_ids or issue_split or asset_split or type_split) + bundle_refs = _sorted_strings(_strings(seed_item.get("multi_asset_bundle_refs")) + adjudicated_bundles) + for plan in plans: + structure_id = f"LES-{len(structures) + 1:03d}" + plan_types = _priority_types(plan["types"]) + plan_representative = plan["representative"] if plan["representative"] in plan_types else plan_types[0] + suffix = f" ({plan['split_label']})" if plan.get("split_label") else "" + sources = ["BO.json", "legal_effect_signals.json"] + if seed_item.get("linked_claim_group_ids") or seed_item.get("linked_liability_group_ids"): + sources.append("case_liability_signals.json") + if seed_item.get("linked_actio_signal_ids"): + sources.append("actio_case_signals.json") + structures.append({ + "structure_id": structure_id, + "candidate_structure_type": plan_representative, + "candidate_structure_types": plan_types, + "structure_label": f"{plan_representative} structure for {', '.join(bo_ids)}{suffix}", + "source_bo_ids": bo_ids, + "issue_cluster_ids": issue_ids, + "asset_cluster_ids": asset_ids, + "evidence_indexes": _sorted_strings(seed_item.get("evidence_indexes")), + "linked_claim_group_ids": _sorted_strings(seed_item.get("linked_claim_group_ids")), + "linked_liability_group_ids": _sorted_strings(seed_item.get("linked_liability_group_ids")), + "linked_actio_signal_ids": _sorted_strings(seed_item.get("linked_actio_signal_ids")), + "legal_effect_roles": _sorted_strings([TYPE_ROLES[item] for item in plan_types]), + "basis": { + "primary_fields": _sorted_strings(seed_item.get("basis_codes")), + "source_signal_files": sorted(set(sources)), + "source_evidence_indexes": _sorted_strings(seed_item.get("evidence_indexes")), + }, + "review_required": review_required, + "routing_audit": { + "source_seed_id": seed_item.get("seed_id"), + "candidate_structure_types": plan_types, + "multi_asset_bundle_refs": bundle_refs, + "resolved_exception_ids": _sorted_strings(resolved_ids), + "blocked_exception_ids": _sorted_strings(blocked_ids), + "deterministic_review_ids": deterministic_review_ids, + }, + }) + return structures, issue_map, asset_map + + def _merge(row: dict[str, Any], field: str, values: Any) -> None: + row[field] = _sorted_strings(_strings(row.get(field)) + _strings(values)) + + def _build_index(structures: list[dict[str, Any]], issue_map: dict[str, Any], asset_map: dict[str, Any], evidence_seed: dict[str, Any]) -> dict[str, Any]: + by_bo = {} + by_issue = {} + by_asset = {} + by_evidence = {} + by_type = {} + for structure in structures: + sid = structure["structure_id"] + types = structure["candidate_structure_types"] + routing = _as_dict(structure.get("routing_audit")) + basis = _as_dict(structure.get("basis")) + for bo_id in structure["source_bo_ids"]: + row = by_bo.setdefault(bo_id, { + "bo_id": bo_id, "structure_ids": [], "structure_labels": [], "issue_cluster_ids": [], + "asset_cluster_ids": [], "evidence_indexes": [], "candidate_structure_types": [], + "claim_group_ids": [], "liability_group_ids": [], "related_actio_signal_ids": [], + "legal_effect_roles": [], "basis_fields": [], "multi_asset_bundle_refs": [], "review_required": False, + }) + _merge(row, "structure_ids", [sid]) + _merge(row, "structure_labels", [structure["structure_label"]]) + _merge(row, "issue_cluster_ids", structure["issue_cluster_ids"]) + _merge(row, "asset_cluster_ids", structure["asset_cluster_ids"]) + _merge(row, "evidence_indexes", structure["evidence_indexes"]) + row["candidate_structure_types"] = [item for item in CANONICAL_TYPES if item in row["candidate_structure_types"] + types] + _merge(row, "claim_group_ids", structure["linked_claim_group_ids"]) + _merge(row, "liability_group_ids", structure["linked_liability_group_ids"]) + _merge(row, "related_actio_signal_ids", structure["linked_actio_signal_ids"]) + _merge(row, "legal_effect_roles", structure["legal_effect_roles"]) + _merge(row, "basis_fields", basis.get("primary_fields")) + _merge(row, "multi_asset_bundle_refs", routing.get("multi_asset_bundle_refs")) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + for issue_id in structure["issue_cluster_ids"]: + source = _as_dict(issue_map.get(issue_id)) + row = by_issue.setdefault(issue_id, { + "issue_cluster_id": issue_id, "cluster_key": source.get("cluster_key"), "source_bo_ids": [], + "evidence_indexes": [], "candidate_structure_types": [], "structure_ids": [], "review_required": False, + }) + _merge(row, "source_bo_ids", structure["source_bo_ids"]) + _merge(row, "evidence_indexes", structure["evidence_indexes"]) + row["candidate_structure_types"] = [item for item in CANONICAL_TYPES if item in row["candidate_structure_types"] + types] + _merge(row, "structure_ids", [sid]) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + for asset_id in structure["asset_cluster_ids"]: + source = _as_dict(asset_map.get(asset_id)) + row = by_asset.setdefault(asset_id, { + "asset_cluster_id": asset_id, "cluster_key": source.get("cluster_key"), + "asset_kind": source.get("asset_kind"), "canonical_label_candidate": source.get("canonical_label_candidate"), + "asset_ref_keys": _sorted_strings(source.get("asset_ref_keys")), "source_bo_ids": [], + "evidence_indexes": [], "issue_cluster_ids": [], "candidate_structure_types": [], + "structure_ids": [], "review_required": False, + }) + _merge(row, "source_bo_ids", structure["source_bo_ids"]) + _merge(row, "evidence_indexes", structure["evidence_indexes"]) + _merge(row, "issue_cluster_ids", structure["issue_cluster_ids"]) + row["candidate_structure_types"] = [item for item in CANONICAL_TYPES if item in row["candidate_structure_types"] + types] + _merge(row, "structure_ids", [sid]) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + for evidence_index in structure["evidence_indexes"]: + source = _as_dict(evidence_seed.get(evidence_index)) + row = by_evidence.setdefault(evidence_index, { + "evidence_index": evidence_index, "source_title": source.get("source_title"), + "priority_class": source.get("priority_class"), "authentication_status": source.get("authentication_status"), + "source_bo_ids": [], "structure_ids": [], "issue_cluster_ids": [], "asset_cluster_ids": [], + "candidate_structure_types": [], "related_event_candidate_ids": [], "related_signal_refs": [], + "support_role": "primary", "gap_flags": [], + }) + _merge(row, "source_bo_ids", structure["source_bo_ids"]) + _merge(row, "structure_ids", [sid]) + _merge(row, "issue_cluster_ids", structure["issue_cluster_ids"]) + _merge(row, "asset_cluster_ids", structure["asset_cluster_ids"]) + row["candidate_structure_types"] = [item for item in CANONICAL_TYPES if item in row["candidate_structure_types"] + types] + _merge(row, "related_signal_refs", structure["linked_claim_group_ids"] + structure["linked_liability_group_ids"] + structure["linked_actio_signal_ids"]) + for candidate_type in types: + row = by_type.setdefault(candidate_type, { + "candidate_structure_type": candidate_type, "structure_ids": [], "source_bo_ids": [], + "issue_cluster_ids": [], "asset_cluster_ids": [], "evidence_indexes": [], + "claim_group_ids": [], "liability_group_ids": [], "related_actio_signal_ids": [], + "downstream_fact_roles": [], "basis_fields": [], "review_required": False, + }) + _merge(row, "structure_ids", [sid]) + _merge(row, "source_bo_ids", structure["source_bo_ids"]) + _merge(row, "issue_cluster_ids", structure["issue_cluster_ids"]) + _merge(row, "asset_cluster_ids", structure["asset_cluster_ids"]) + _merge(row, "evidence_indexes", structure["evidence_indexes"]) + _merge(row, "claim_group_ids", structure["linked_claim_group_ids"]) + _merge(row, "liability_group_ids", structure["linked_liability_group_ids"]) + _merge(row, "related_actio_signal_ids", structure["linked_actio_signal_ids"]) + _merge(row, "downstream_fact_roles", structure["legal_effect_roles"]) + _merge(row, "basis_fields", basis.get("primary_fields")) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + return { + "by_bo_id": by_bo, + "by_issue_cluster_id": by_issue, + "by_asset_cluster_id": by_asset, + "by_evidence_index": by_evidence, + "by_candidate_structure_type": by_type, + } + + def _quality_gate(structures: list[dict[str, Any]], index: dict[str, Any], seed: dict[str, Any], pack: dict[str, Any], decisions: dict[str, dict[str, Any]]) -> dict[str, Any]: + seed_items = [item for item in _as_list(seed.get("structure_seed_items")) if isinstance(item, dict)] + expected_bo = sorted({bo for item in seed_items for bo in _strings(item.get("source_bo_ids"))}) + covered_bo = sorted(index["by_bo_id"]) + expected_evidence = sorted({evidence for item in seed_items for evidence in _strings(item.get("evidence_indexes"))}) + covered_evidence = sorted(index["by_evidence_index"]) + expected_exception_ids = _strings(pack.get("expected_exception_ids")) + blocked_ids = sorted(exception_id for exception_id, decision in decisions.items() if decision.get("decision") == "BLOCK_REVIEW") + resolved_ids = sorted(set(expected_exception_ids) - set(blocked_ids)) + reviews = [] + for item in _as_list(seed.get("deterministic_review_items")): + if isinstance(item, dict): + reviews.append({ + "review_type": "deterministic_review", + "review_id": item.get("review_id"), + "review_code": item.get("review_code"), + "source_bo_ids": _sorted_strings(item.get("source_bo_ids")), + "evidence_indexes": _sorted_strings(item.get("evidence_indexes")), + "downstream_owner": item.get("downstream_owner"), + }) + for exception_id in blocked_ids: + decision = decisions[exception_id] + reviews.append({ + "review_type": "blocked_exception", + "exception_id": exception_id, + "exception_type": decision.get("exception_type"), + "source_bo_ids": _sorted_strings(decision.get("source_bo_ids")), + "reason_code": decision.get("reason_code"), + }) + split_structure_ids = [ + structure["structure_id"] for structure in structures + if structure.get("review_required") and ( + _as_dict(structure.get("routing_audit")).get("resolved_exception_ids") + and "split" in structure.get("structure_label", "") + ) + ] + for structure_id in split_structure_ids: + reviews.append({"review_type": "review_sensitive_split", "structure_id": structure_id}) + return { + "bo_coverage": { + "source_bo_count": len(expected_bo), "covered_bo_count": len(covered_bo), + "missing_bo_ids": sorted(set(expected_bo) - set(covered_bo)), + "unknown_bo_ids": sorted(set(covered_bo) - set(expected_bo)), + "coverage_pass": expected_bo == covered_bo, + }, + "evidence_coverage": { + "source_evidence_count": len(expected_evidence), "covered_evidence_count": len(covered_evidence), + "missing_evidence_indexes": sorted(set(expected_evidence) - set(covered_evidence)), + "coverage_pass": set(expected_evidence).issubset(set(covered_evidence)), + }, + "exception_coverage": { + "expected_exception_count": len(expected_exception_ids), + "resolved_exception_count": len(resolved_ids), + "blocked_exception_count": len(blocked_ids), + "missing_exception_ids": sorted(set(expected_exception_ids) - set(decisions)), + "coverage_pass": set(expected_exception_ids) == set(decisions), + }, + "run_fingerprint_gate": {"match": seed.get("run_fingerprint") == pack.get("run_fingerprint")}, + "review_summary": { + "structure_review_count": sum(1 for structure in structures if structure.get("review_required")), + "deterministic_review_count": len(_as_list(seed.get("deterministic_review_items"))), + "blocked_exception_count": len(blocked_ids), + }, + "review_queue": reviews, + } + + def _validate_final(payload: dict[str, Any]) -> None: + if payload.get("schema_version") != "stage1_legal_effect_structures.v1": + raise ValueError("final schema mismatch") + if payload.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("final status invalid") + structures = _as_list(payload.get("legal_effect_structures")) + if not structures: + raise ValueError("final structures must be non-empty") + index = _as_dict(payload.get("structure_index")) + required_indexes = ["by_bo_id", "by_issue_cluster_id", "by_asset_cluster_id", "by_evidence_index", "by_candidate_structure_type"] + for key in required_indexes: + if not isinstance(index.get(key), dict): + raise ValueError(f"structure_index.{key} must be object") + structure_ids = [str(_as_dict(row).get("structure_id")) for row in structures] + if len(structure_ids) != len(set(structure_ids)): + raise ValueError("structure IDs must be unique") + for structure in structures: + if not isinstance(structure, dict): + raise ValueError("structure must be object") + types = _priority_types(structure.get("candidate_structure_types")) + if structure.get("candidate_structure_type") not in types: + raise ValueError(f"{structure.get('structure_id')}: representative type missing from type array") + audit_types = _priority_types(_as_dict(structure.get("routing_audit")).get("candidate_structure_types")) + if types != audit_types: + raise ValueError(f"{structure.get('structure_id')}: top-level/audit type mismatch") + sid = structure.get("structure_id") + for bo_id in _strings(structure.get("source_bo_ids")): + if sid not in _strings(_as_dict(index["by_bo_id"].get(bo_id)).get("structure_ids")): + raise ValueError(f"{sid}: missing BO index link {bo_id}") + for issue_id in _strings(structure.get("issue_cluster_ids")): + if issue_id not in index["by_issue_cluster_id"]: + raise ValueError(f"{sid}: missing issue index {issue_id}") + for asset_id in _strings(structure.get("asset_cluster_ids")): + if asset_id not in index["by_asset_cluster_id"]: + raise ValueError(f"{sid}: missing asset index {asset_id}") + for evidence_index in _strings(structure.get("evidence_indexes")): + if evidence_index not in index["by_evidence_index"]: + raise ValueError(f"{sid}: missing evidence index {evidence_index}") + for candidate_type in types: + if sid not in _strings(_as_dict(index["by_candidate_structure_type"].get(candidate_type)).get("structure_ids")): + raise ValueError(f"{sid}: missing type index {candidate_type}") + gate = _as_dict(payload.get("quality_gate")) + for key in ["bo_coverage", "evidence_coverage", "exception_coverage"]: + if _as_dict(gate.get(key)).get("coverage_pass") is not True: + raise ValueError(f"quality gate failed: {key}") + if _as_dict(gate.get("run_fingerprint_gate")).get("match") is not True: + raise ValueError("quality gate failed: run_fingerprint") + + def main() -> None: + try: + _initialize() + seed, pack, decisions = _load_inputs() + structures, issue_map, asset_map = _build_structures(seed, decisions) + index = _build_index(structures, issue_map, asset_map, _as_dict(seed.get("evidence_index_seed"))) + gate = _quality_gate(structures, index, seed, pack, decisions) + status = "READY_WITH_REVIEW" if gate["review_queue"] else "READY" + payload = { + "schema_version": "stage1_legal_effect_structures.v1", + "status": status, + "legal_effect_structures": structures, + "structure_index": index, + "quality_gate": gate, + } + _validate_final(payload) + _write_doc(TARGET_PATH, payload) + reread = _read_doc(TARGET_PATH) + if _artifact_hash(reread) != _artifact_hash(payload): + raise ValueError("final post-write reread mismatch") + _validate_final(reread) + print(json.dumps({ + "legal_effect_structures_writer": { + "schema_version": "stage1_legal_effect_structures_writer.v2", + "status": "READY", + "written_file": TARGET_PATH, + "payload_status": status, + "structure_count": len(structures), + "exception_count": len(_strings(pack.get("expected_exception_ids"))), + "review_queue_count": len(gate["review_queue"]), + "run_fingerprint": seed.get("run_fingerprint"), + } + }, ensure_ascii=False, separators=(",", ":"))) + except Exception as exc: + print(json.dumps({ + "legal_effect_structures_writer": { + "schema_version": "stage1_legal_effect_structures_writer.v2", + "status": "FAILED", + "target_file": TARGET_PATH, + "failure_reason": str(exc), + } + }, ensure_ascii=False, separators=(",", ":"))) + raise + + if __name__ == "__main__": + main() diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_3_v1.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_3_v1.yml new file mode 100644 index 00000000..ec1d12bc --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_3_v1.yml @@ -0,0 +1,2638 @@ +--- +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_3 + description: 민사소송 원고 송무 초지능 AI변호사 - Legal Effect 생성 + version: v1.0 + Stages: + - name: stage1_legal_effect_생성 + description: Legal Effect Structure 작성 + tools: + mcpServers: + localdocs: + type: streamable-http + url: http://mcp-localdocs:8012/mcp + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + task_procedure: + IN: + nexts: + - Task_LES0_input_pack_compiler + wait_until: [] + Task_LES0_input_pack_compiler: + nexts: + - Task_LES1_deterministic_structure_seed_builder + wait_until: + - IN + Task_LES1_deterministic_structure_seed_builder: + nexts: + - Task_LES2_single_cluster_exception_adjudicator + wait_until: + - Task_LES0_input_pack_compiler + Task_LES2_single_cluster_exception_adjudicator: + nexts: + - Task_LES3_final_structure_index_writer + wait_until: + - Task_LES1_deterministic_structure_seed_builder + Task_LES3_final_structure_index_writer: + nexts: + - OUT + wait_until: + - Task_LES1_deterministic_structure_seed_builder + - Task_LES2_single_cluster_exception_adjudicator + OUT: + nexts: [] + wait_until: + - Task_LES3_final_structure_index_writer + + tasks: + - task_name: Task_LES0_input_pack_compiler + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 120 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import itertools + import json + import re + import sys + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + INPUT_FILES = [ + "client_goal.json", + "BO.json", + "evidence_indexed.json", + "evidence_all.json", + "evidence_event_candidates.json", + "actio_case_signals.json", + "case_liability_signals.json", + "legal_effect_signals.json", + ] + OUT_PATH = "stage1_tmp/legal_effect_structure_input_pack.json" + MAX_EXCERPT = 700 + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-legal-effect-les0-input-pack-compiler", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result.get("result", {}).get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + text = content[0].get("text", "") + try: + envelope = json.loads(text) + except Exception as exc: + raise RuntimeError(f"Invalid read_docs envelope for {doc_name}: {text[:300]}") from exc + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + item = results[0] + if isinstance(item, dict) and item.get("error"): + raise RuntimeError(f"read_docs error for {doc_name}: {item.get('error')}") + raw = item.get("content", "") + else: + raw = envelope + if isinstance(raw, str): + return json.loads(raw) + return raw + + def _read_doc(doc_name: str) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + return _extract_json_from_read_docs_result(result, doc_name) + + def _write_doc(doc_name: str, payload: dict[str, Any]) -> None: + content = json.dumps(payload, ensure_ascii=False, indent=2) + "\n" + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=_next_msg_id()) + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def _first_string(*values: Any) -> str | None: + for value in values: + for item in _strings(value): + return item + return None + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + if value is None: + continue + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + def _short(value: Any, limit: int = MAX_EXCERPT) -> str | None: + if value is None: + return None + text = re.sub(r"\s+", " ", str(value)).strip() + if not text: + return None + return text[:limit] + + def _evidence_index_from_item(item: dict[str, Any]) -> str | None: + return _first_string( + item.get("evidence_index"), + item.get("evidence_id"), + item.get("index"), + item.get("id"), + item.get("source_id"), + ) + + def _evidence_authority_item(index: str, item: dict[str, Any], source: str) -> dict[str, Any]: + return { + "evidence_index": index, + "source_title": _first_string(item.get("source_title"), item.get("title"), item.get("evidence_title"), item.get("document_title"), index), + "priority_class": _first_string(item.get("priority_class"), item.get("priority"), item.get("evidence_priority")), + "authentication_status": _first_string(item.get("authentication_status"), item.get("auth_status"), item.get("authentication")), + "excerpt": _short(item.get("relevant_content") or item.get("excerpt") or item.get("summary") or item.get("content") or item.get("text")), + "source_file": source, + } + + def _collect_evidence_authority(evidence_indexed: Any, evidence_all: Any, bo_items: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: + out: dict[str, dict[str, Any]] = {} + + def add(index: str | None, item: dict[str, Any], source: str) -> None: + if not index: + return + new = _evidence_authority_item(index, item, source) + old = out.get(index, {}) + merged = {**new, **{k: v for k, v in old.items() if v not in (None, "", [])}} + if old.get("excerpt") and len(str(old.get("excerpt"))) >= len(str(new.get("excerpt") or "")): + merged["excerpt"] = old.get("excerpt") + out[index] = merged + + def visit(node: Any, source: str) -> None: + if isinstance(node, dict): + idx = _evidence_index_from_item(node) + if idx: + add(idx, node, source) + for key in ("items", "evidence", "documents", "records", "results", "events"): + for child in _list(node.get(key)): + visit(child, source) + elif isinstance(node, list): + for child in node: + visit(child, source) + + visit(evidence_indexed, "evidence_indexed.json") + visit(evidence_all, "evidence_all.json") + for bo in bo_items: + for ev in _list(bo.get("Evidence")): + if isinstance(ev, dict): + add(_evidence_index_from_item(ev), ev, "BO.json.Evidence") + return out + + def _event_candidates_by_evidence(events: Any) -> dict[str, list[dict[str, Any]]]: + out: dict[str, list[dict[str, Any]]] = {} + + def visit(node: Any) -> None: + if isinstance(node, dict): + idxs = _strings(node.get("evidence_index") or node.get("evidence_indexes") or node.get("source_evidence_indexes")) + if idxs: + compact = { + "event_candidate_id": _first_string(node.get("candidate_id"), node.get("event_id"), node.get("id")), + "event_date": _first_string(node.get("event_date"), node.get("date"), node.get("BehaviorTime")), + "action_summary": _short(node.get("action_summary") or node.get("summary") or node.get("description"), 300), + "object": _first_string(node.get("object"), node.get("asset"), node.get("target"), node.get("Object")), + } + for idx in idxs: + out.setdefault(idx, []).append(compact) + for key in ("items", "events", "event_candidates", "candidates", "results"): + for child in _list(node.get(key)): + visit(child) + elif isinstance(node, list): + for child in node: + visit(child) + visit(events) + return out + + def _domain_payload(bo: dict[str, Any]) -> dict[str, Any]: + return _dict(_dict(bo.get("extensions")).get("domain_payload")) + + def _compact_bo(bo: dict[str, Any]) -> dict[str, Any]: + payload = _domain_payload(bo) + core = _dict(bo.get("core_field_base")) + evidence_indexes = _dedupe(_strings(bo.get("source_evidence_indexes")) + [_first_string(ev.get("evidence_index")) for ev in _list(bo.get("Evidence")) if isinstance(ev, dict)]) + evidence_indexes = [idx for idx in evidence_indexes if idx] + return { + "BO_ID": str(bo.get("BO_ID") or bo.get("id") or ""), + "id": str(bo.get("id") or bo.get("BO_ID") or ""), + "BOType": bo.get("BOType"), + "ActionType": bo.get("ActionType"), + "JuristicAct": _dict(bo.get("JuristicAct")), + "Action": bo.get("Action"), + "Reason": bo.get("Reason"), + "PriorAct": bo.get("PriorAct"), + "ReasonRefs": _strings(bo.get("ReasonRefs")), + "Legal_Keywords": _strings(bo.get("Legal_Keywords")), + "core_field_base": core, + "amount": bo.get("amount"), + "source_evidence_indexes": evidence_indexes, + "Evidence": [ + { + "evidence_index": _first_string(ev.get("evidence_index")), + "source_title": _first_string(ev.get("source_title"), ev.get("title")), + "priority_class": _first_string(ev.get("priority_class"), ev.get("priority")), + "authentication_status": _first_string(ev.get("authentication_status"), ev.get("auth_status")), + "excerpt": _short(ev.get("relevant_content") or ev.get("excerpt") or ev.get("summary"), 400), + } + for ev in _list(bo.get("Evidence")) if isinstance(ev, dict) + ], + "provenance": _dict(bo.get("provenance")), + "downstream_seed_refs": _dict(bo.get("downstream_seed_refs")), + "domain_payload": { + "domain_legal_effect_tag": payload.get("domain_legal_effect_tag"), + "legal_effect_tags": _strings(payload.get("legal_effect_tags")), + "asset_bindings": _as_list(payload.get("asset_bindings")), + "asset_binding_hint": payload.get("asset_binding_hint"), + "candidate_details": payload.get("candidate_details"), + "facts": payload.get("facts"), + "parties_hint": _as_list(payload.get("parties_hint")), + "action_summary": payload.get("action_summary"), + "title_hint": payload.get("title_hint"), + "summary_hint": payload.get("summary_hint"), + "event_state_type": payload.get("event_state_type"), + "asset_alias": _as_list(payload.get("asset_alias")), + }, + } + + def _actio_signals_by_bo(data: Any) -> dict[str, list[dict[str, Any]]]: + root = _dict(data) + out: dict[str, list[dict[str, Any]]] = {} + for sig in _list(root.get("actio_case_signals")): + if not isinstance(sig, dict): + continue + compact = { + "signal_id": _first_string(sig.get("signal_id")), + "actio_scope": _strings(sig.get("actio_scope")), + "suspicion_level": sig.get("suspicion_level"), + "related_evidence_indexes": _strings(sig.get("related_evidence_indexes")), + "target_property_candidates": _list(sig.get("target_property_candidates")), + "preserved_claim_candidates": _list(sig.get("preserved_claim_candidates")), + "actio_role_hints": _list(sig.get("actio_role_hints")), + } + for bo_id in _strings(sig.get("related_bo_ids")): + out.setdefault(bo_id, []).append(compact) + for item in _list(sig.get("target_property_candidates")): + if isinstance(item, dict): + for bo_id in _strings(item.get("related_bo_ids")): + out.setdefault(bo_id, []).append(compact) + return out + + def _case_signals_by_bo(data: Any) -> dict[str, list[dict[str, Any]]]: + root = _dict(data) + out: dict[str, list[dict[str, Any]]] = {} + for idx, sig in enumerate(_list(root.get("case_liability_signals")), start=1): + if not isinstance(sig, dict): + continue + compact = { + "signal_ref": f"case-liability-signal-{idx:03d}", + "liability_candidate_type": sig.get("liability_candidate_type"), + "related_evidence_indexes": _strings(sig.get("related_evidence_indexes")), + "party_roles": _list(sig.get("party_roles")), + "claim_group_id": sig.get("claim_group_id"), + "liability_group_id": sig.get("liability_group_id"), + "linked_structure_ids": _strings(sig.get("linked_structure_ids")), + } + bo_ids = _strings(sig.get("related_bo_ids")) + for role in _list(sig.get("party_roles")): + if isinstance(role, dict): + bo_ids.extend(_strings(role.get("bo_ids"))) + for bo_id in _dedupe(bo_ids): + out.setdefault(bo_id, []).append(compact) + return out + + def _legal_routes_by_bo(data: Any) -> dict[str, list[dict[str, Any]]]: + root = _dict(data) + out: dict[str, list[dict[str, Any]]] = {} + for route in _list(root.get("bo_legal_effect_routes")): + if not isinstance(route, dict): + continue + bo_id = _first_string(route.get("bo_id")) + if not bo_id: + continue + out.setdefault(bo_id, []).append({ + "candidate_structure_types": _strings(route.get("candidate_structure_types")), + "linked_structure_ids": _strings(route.get("linked_structure_ids")), + "related_evidence_indexes": _strings(route.get("related_evidence_indexes")), + }) + return out + + def _candidate_structure_types(bo: dict[str, Any], signals: dict[str, list[dict[str, Any]]]) -> list[str]: + types: list[str] = [] + for route in signals.get("legal_effect", []): + types.extend(_strings(route.get("candidate_structure_types"))) + for case_sig in signals.get("case_liability", []): + types.extend(_strings(case_sig.get("liability_candidate_type"))) + for actio_sig in signals.get("actio", []): + for scope in _strings(actio_sig.get("actio_scope")): + mapping = { + "fraudulent_act": "fraudulent_transfer", + "preserved_claim": "preserved_claim", + "remedy_or_cap": "actio_remedy_or_cap", + "target_asset": "actio_asset_target", + "beneficiary_or_transferee": "fraudulent_transfer", + "encumbrance": "secured_debt", + "defense": "succession_notice_lien_asset_defense", + } + if scope in mapping: + types.append(mapping[scope]) + payload = _dict(bo.get("domain_payload")) + types.extend(_strings(payload.get("domain_legal_effect_tag"))) + types.extend(_strings(payload.get("legal_effect_tags"))) + source_domain = _first_string(_dict(bo.get("provenance")).get("source_domain")) + if source_domain: + if "B1" in source_domain: + types.append("money_claim") + elif "B2" in source_domain: + types.append("secured_debt") + elif "B3" in source_domain: + types.append("land_use_gain") + elif "B4" in source_domain: + types.append("fraudulent_transfer") + elif "B5" in source_domain: + types.append("succession_notice_lien_asset_defense") + return _dedupe([_normalize_structure_type(t) for t in types if t]) + + def _normalize_structure_type(value: str) -> str: + text = str(value).strip().lower() + if any(x in text for x in ["money", "loan", "대여금", "매매대금", "미지급"]): + return "money_claim" + if any(x in text for x in ["successor", "상호속용", "영업양수"]): + return "commercial_successor" + if any(x in text for x in ["secured", "담보", "근저당", "피담보"]): + return "secured_debt" + if any(x in text for x in ["registry", "등기", "말소", "무효"]): + return "registry_invalidity" + if any(x in text for x in ["land", "사용이익", "임료", "점유"]): + return "land_use_gain" + if any(x in text for x in ["valuation", "감정", "평가", "가액", "cap"]): + return "valuation" + if any(x in text for x in ["fraudulent", "actio", "사해", "수익자", "전득자"]): + return "fraudulent_transfer" + if any(x in text for x in ["preserved", "피보전"]): + return "preserved_claim" + if any(x in text for x in ["remedy", "원상회복", "가액배상"]): + return "actio_remedy_or_cap" + if any(x in text for x in ["succession", "notice", "lien", "asset_defense", "승계", "통지", "유치권", "인도", "항변"]): + return "succession_notice_lien_asset_defense" + return text.replace(" ", "_") if text else "general_legal_effect" + + def _asset_kind(label: str, fallback: Any = None) -> str: + if fallback: + text = str(fallback) + if text in {"real_property", "movable", "claim", "security_right", "cash_or_deposit", "business_asset", "other", "unknown"}: + return text + if any(x in label for x in ["토지", "대지", "건물", "부동산", "아파트", "상가", "임야", "등기"]): + return "real_property" + if any(x in label for x in ["근저당", "담보", "저당", "유치권", "질권"]): + return "security_right" + if any(x in label for x in ["채권", "대여금", "매매대금", "보증금"]): + return "claim" + if any(x in label for x in ["예금", "현금", "계좌"]): + return "cash_or_deposit" + if any(x in label for x in ["영업", "사업", "상호"]): + return "business_asset" + if any(x in label for x in ["동산", "차량", "기계"]): + return "movable" + return "unknown" + + def _extract_asset_candidates(bo: dict[str, Any], signals: dict[str, list[dict[str, Any]]]) -> list[dict[str, Any]]: + bo_id = bo.get("BO_ID") + evidence = _strings(bo.get("source_evidence_indexes")) + out: list[dict[str, Any]] = [] + for sig in signals.get("actio", []): + for item in _list(sig.get("target_property_candidates")): + if isinstance(item, dict): + label = _first_string(item.get("asset_label")) + if label: + out.append({ + "asset_label": label, + "asset_kind": _asset_kind(label, item.get("asset_kind")), + "source": "actio_case_signals.target_property_candidates", + "related_bo_ids": _dedupe([bo_id] + _strings(item.get("related_bo_ids"))), + "related_evidence_indexes": _dedupe(evidence + _strings(item.get("related_evidence_indexes"))), + }) + payload = _dict(bo.get("domain_payload")) + for source_key in ["asset_bindings", "asset_binding_hint", "candidate_details", "facts"]: + values = _asset_strings(payload.get(source_key)) + for label in values: + out.append({ + "asset_label": label, + "asset_kind": _asset_kind(label), + "source": f"BO.extensions.domain_payload.{source_key}", + "related_bo_ids": [bo_id] if bo_id else [], + "related_evidence_indexes": evidence, + }) + for alias in _as_list(payload.get("asset_alias")): + if not isinstance(alias, dict): + continue + ref_keys = _strings(alias.get("schedule_refs")) + for reg in _as_list(alias.get("registry_refs")): + if isinstance(reg, dict): + ref_keys.extend(_strings(reg.get("registry_ref"))) + else: + ref_keys.extend(_strings(reg)) + alias_labels = _strings(alias.get("alias_strings")) + _strings(alias.get("canonical_label_candidate")) + for label in alias_labels or ([ref_keys[0]] if ref_keys else []): + out.append({ + "asset_label": label, + "asset_kind": _asset_kind(label), + "source": "BO.extensions.domain_payload.asset_alias", + "asset_ref_keys": _dedupe(ref_keys), + "related_bo_ids": [bo_id] if bo_id else [], + "related_evidence_indexes": evidence, + }) + core_obj = _first_string(_dict(bo.get("core_field_base")).get("Object")) + if core_obj: + out.append({ + "asset_label": core_obj, + "asset_kind": _asset_kind(core_obj), + "source": "BO.core_field_base.Object", + "related_bo_ids": [bo_id] if bo_id else [], + "related_evidence_indexes": evidence, + }) + return _dedupe_assets(out) + + def _asset_strings(value: Any) -> list[str]: + out: list[str] = [] + + def visit(node: Any) -> None: + if node is None or len(out) >= 20: + return + if isinstance(node, (str, int, float)): + text = str(node).strip() + if text and len(text) <= 180: + out.append(text) + return + if isinstance(node, list): + for child in node: + visit(child) + return + if isinstance(node, dict): + for key in [ + "asset_label", "asset", "assets", "property", "properties", "object", "objects", + "collateral", "collaterals", "target", "official_designation", "alias", + ]: + if key in node: + visit(node.get(key)) + visit(value) + return _dedupe(out) + + def _dedupe_assets(items: list[dict[str, Any]]) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + seen: set[str] = set() + for item in items: + key = json.dumps({ + "asset_label": item.get("asset_label"), + "asset_kind": item.get("asset_kind"), + "source": item.get("source"), + }, ensure_ascii=False, sort_keys=True) + if key not in seen: + seen.add(key) + out.append(item) + return out + + def _require_signal_root(data: Any, doc_name: str, schema_version: str, array_key: str) -> None: + root = _dict(data) + if root.get("schema_version") != schema_version: + raise ValueError(f"{doc_name} schema_version must be {schema_version}") + if root.get("status") not in {"READY", "READY_WITH_REVIEW", "EMPTY"}: + raise ValueError(f"{doc_name} status must be READY, READY_WITH_REVIEW, or EMPTY") + if not isinstance(root.get(array_key), list): + raise ValueError(f"{doc_name}.{array_key} must be array") + + def main() -> None: + try: + _init_mcp_session() + docs = {name: _read_doc(name) for name in INPUT_FILES} + bo_items = docs["BO.json"] + if not isinstance(bo_items, list): + raise ValueError("BO.json root must be array") + _require_signal_root(docs["actio_case_signals.json"], "actio_case_signals.json", "actio_case_signals.v1", "actio_case_signals") + _require_signal_root(docs["case_liability_signals.json"], "case_liability_signals.json", "case_liability_signals.v1", "case_liability_signals") + _require_signal_root(docs["legal_effect_signals.json"], "legal_effect_signals.json", "legal_effect_signals.v1", "bo_legal_effect_routes") + client_goal = docs["client_goal.json"] + evidence_authority = _collect_evidence_authority(docs["evidence_indexed.json"], docs["evidence_all.json"], bo_items) + events_by_evidence = _event_candidates_by_evidence(docs["evidence_event_candidates.json"]) + compact_bo_items = [_compact_bo(bo) for bo in bo_items if isinstance(bo, dict)] + bo_by_id = {bo["BO_ID"]: bo for bo in compact_bo_items if bo.get("BO_ID")} + actio_by_bo = _actio_signals_by_bo(docs["actio_case_signals.json"]) + case_by_bo = _case_signals_by_bo(docs["case_liability_signals.json"]) + legal_by_bo = _legal_routes_by_bo(docs["legal_effect_signals.json"]) + + candidate_inputs: dict[str, dict[str, Any]] = {} + warnings: list[dict[str, Any]] = [] + for bo_id, bo in bo_by_id.items(): + signals = { + "actio": actio_by_bo.get(bo_id, []), + "case_liability": case_by_bo.get(bo_id, []), + "legal_effect": legal_by_bo.get(bo_id, []), + } + evidence_indexes = _dedupe( + _strings(bo.get("source_evidence_indexes")) + + [idx for route in signals["legal_effect"] for idx in _strings(route.get("related_evidence_indexes"))] + + [idx for sig in signals["case_liability"] for idx in _strings(sig.get("related_evidence_indexes"))] + + [idx for sig in signals["actio"] for idx in _strings(sig.get("related_evidence_indexes"))] + ) + missing_evidence = [idx for idx in evidence_indexes if idx not in evidence_authority] + if missing_evidence: + warnings.append({"bo_id": bo_id, "warning": "missing_evidence_authority", "evidence_indexes": missing_evidence}) + candidate_inputs[bo_id] = { + "bo_id": bo_id, + "source_domain": _first_string(_dict(bo.get("provenance")).get("source_domain")), + "hard_issue_keys": { + "claim_group_ids": _dedupe([x for sig in signals["case_liability"] for x in _strings(sig.get("claim_group_id"))] + _strings(_dict(bo.get("downstream_seed_refs")).get("claim_group_seed_refs_proposed"))), + "liability_group_ids": _dedupe([x for sig in signals["case_liability"] for x in _strings(sig.get("liability_group_id"))]), + "linked_structure_ids": _dedupe( + [x for route in signals["legal_effect"] for x in _strings(route.get("linked_structure_ids"))] + + [x for sig in signals["case_liability"] for x in _strings(sig.get("linked_structure_ids"))] + + _strings(_dict(bo.get("downstream_seed_refs")).get("legal_effect_structure_seed_ref_proposed")) + ), + "canonical_theory_graph_seed_ref": _strings(_dict(bo.get("downstream_seed_refs")).get("canonical_theory_graph_seed_ref_proposed")), + }, + "candidate_structure_types": _candidate_structure_types(bo, signals), + "asset_candidates": _extract_asset_candidates(bo, signals), + "evidence_indexes": evidence_indexes, + "related_event_candidates": [event for idx in evidence_indexes for event in events_by_evidence.get(idx, [])], + "keyword_fields": { + "Legal_Keywords": _strings(bo.get("Legal_Keywords")), + "Action": bo.get("Action"), + "Reason": bo.get("Reason"), + "JuristicAct_label": _dict(bo.get("JuristicAct")).get("label"), + "ActionType": bo.get("ActionType"), + "BOType": bo.get("BOType"), + "domain_legal_effect_tag": _dict(bo.get("domain_payload")).get("domain_legal_effect_tag"), + "legal_effect_tags": _strings(_dict(bo.get("domain_payload")).get("legal_effect_tags")), + }, + "signals": signals, + } + + pack = { + "legal_structure_input_pack": { + "schema_version": "stage1_legal_structure_input_pack.v1", + "status": "READY", + "input_files": INPUT_FILES, + "client_goal_compact": _compact_client_goal(client_goal), + "bo_count": len(bo_by_id), + "bo_by_id": bo_by_id, + "evidence_authority_by_index": evidence_authority, + "events_by_evidence_index": events_by_evidence, + "signals_by_bo_id": { + bo_id: { + "actio": actio_by_bo.get(bo_id, []), + "case_liability": case_by_bo.get(bo_id, []), + "legal_effect": legal_by_bo.get(bo_id, []), + } + for bo_id in bo_by_id + }, + "candidate_inputs_by_bo_id": candidate_inputs, + "input_warnings": warnings, + } + } + _validate_pack(pack) + _write_doc(OUT_PATH, pack) + print(json.dumps(pack, ensure_ascii=False)) + except Exception as exc: + print(json.dumps({ + "legal_structure_input_pack": { + "schema_version": "stage1_legal_structure_input_pack.v1", + "status": "FAILED", + "failure_reason": str(exc), + "input_files": INPUT_FILES, + } + }, ensure_ascii=False)) + sys.exit(0) + + def _compact_client_goal(data: Any) -> dict[str, Any]: + if not isinstance(data, dict): + return {"raw_type": type(data).__name__, "summary": _short(data, 500)} + keys = [ + "client_goal", "goal", "case_goal", "claim_goal", "requested_relief", + "parties", "plaintiff", "defendant", "case_type", "remedy", "summary", + ] + return {key: data.get(key) for key in keys if key in data} + + def _validate_pack(pack: dict[str, Any]) -> None: + root = _dict(pack.get("legal_structure_input_pack")) + if root.get("schema_version") != "stage1_legal_structure_input_pack.v1": + raise ValueError("input pack schema mismatch") + if root.get("status") != "READY": + raise ValueError("input pack must be READY") + for key in ["bo_by_id", "evidence_authority_by_index", "signals_by_bo_id", "candidate_inputs_by_bo_id", "input_warnings"]: + if key not in root: + raise ValueError(f"input pack missing {key}") + + if __name__ == "__main__": + main() + + - task_name: Task_LES1_deterministic_structure_seed_builder + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 120 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import itertools + import json + import re + import sys + from collections import defaultdict + from typing import Any + + import httpx + + LES0_RAW = r'''{{prev.Task_LES0_input_pack_compiler}}''' + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + INPUT_PACK_PATH = "stage1_tmp/legal_effect_structure_input_pack.json" + SEED_OUT_PATH = "stage1_tmp/legal_effect_structure_seed_bundle.json" + EXCEPTION_OUT_PATH = "stage1_tmp/legal_effect_structure_exception_pack.json" + ISSUE_THRESHOLD = 3 + ISSUE_MARGIN = 2 + STRUCTURE_TYPE_PRIORITY = [ + "money_claim", + "commercial_successor", + "secured_debt", + "registry_invalidity", + "land_use_gain", + "valuation", + "fraudulent_transfer", + "preserved_claim", + "actio_remedy_or_cap", + "succession_notice_lien_asset_defense", + "general_legal_effect", + ] + DOWNSTREAM_ROLE_FAMILY = { + "money_claim": "claim_chain_ref", + "commercial_successor": "claim_chain_ref", + "secured_debt": "linked_structures", + "registry_invalidity": "linked_structures", + "land_use_gain": "legal_calculation_object", + "valuation": "legal_calculation_object", + "fraudulent_transfer": "actio_roles", + "preserved_claim": "claim_chain_ref", + "actio_remedy_or_cap": "linked_actio_structures", + "succession_notice_lien_asset_defense": "linked_structures", + "general_legal_effect": "legal_effect_roles", + } + ISSUE_KEYWORDS = { + "money_claim": ["money", "loan", "대여금", "매매대금", "미지급", "채권", "변제"], + "commercial_successor": ["successor", "상호속용", "영업양수", "승계"], + "secured_debt": ["secured", "담보", "근저당", "피담보", "저당"], + "registry_invalidity": ["registry", "등기", "말소", "무효", "소유권이전"], + "land_use_gain": ["land", "사용이익", "임료", "점유", "인도"], + "valuation": ["valuation", "감정", "평가", "가액", "시가"], + "actio": ["fraudulent", "actio", "사해", "수익자", "전득자", "피보전", "가액배상"], + "asset_defense": ["notice", "lien", "통지", "유치권", "항변", "방어"], + } + HARD_ISSUE_KEY_PRIORITY = ["claim_group_ids", "liability_group_ids", + "canonical_theory_graph_seed_ref", "linked_structure_ids"] + ADJUDICATOR_BUDGET = {"max_llm_items": 30, "max_candidate_values_per_item": 8, + "max_chars_per_item": 1200, "overflow_policy": "SPLIT_BATCH_THEN_BLOCK"} + EXCEPTION_TYPE_ORDER = {"ambiguous_issue_cluster": 0, "ambiguous_asset_cluster": 1, + "ambiguous_candidate_structure_type": 2} + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-legal-effect-les1-structure-seed-builder", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result.get("result", {}).get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + text = content[0].get("text", "") + envelope = json.loads(text) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + item = results[0] + if isinstance(item, dict) and item.get("error"): + raise RuntimeError(f"read_docs error for {doc_name}: {item.get('error')}") + raw = item.get("content", "") + else: + raw = envelope + if isinstance(raw, str): + return json.loads(raw) + return raw + + def _read_doc(doc_name: str) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + return _extract_json_from_read_docs_result(result, doc_name) + + def _write_doc(doc_name: str, payload: dict[str, Any]) -> None: + content = json.dumps(payload, ensure_ascii=False, indent=2) + "\n" + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=_next_msg_id()) + + def _preview(raw: str, limit: int = 180) -> str: + return (raw or "").strip().replace("\n", "\\n")[:limit] + + def _extract_json_object(raw: str, label: str) -> dict[str, Any]: + text = (raw or "").strip() + if not text or text.startswith("{{"): + raise ValueError(f"{label}: unresolved or empty payload; preview={_preview(raw)!r}") + start = text.find("{") + if start < 0: + raise ValueError(f"{label}: JSON object start not found; preview={_preview(raw)!r}") + candidate = text[start:].strip() + obj, end = JSON_DECODER.raw_decode(candidate) + tail = candidate[end:].strip() + if tail: + raise ValueError(f"{label}: non-json tail after object; preview={_preview(tail)!r}") + if not isinstance(obj, dict): + raise ValueError(f"{label}: JSON root must be object") + return obj + + def _unwrap(raw: str, label: str) -> dict[str, Any]: + obj = _extract_json_object(raw, label) + if obj.get("type") == "text" and isinstance(obj.get("text"), str): + obj = _extract_json_object(obj["text"], f"{label}.text") + if obj.get("success") is True and isinstance(obj.get("output"), str): + obj = _extract_json_object(obj["output"], f"{label}.output") + return obj + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + if value is None: + continue + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + def _load_input_pack() -> dict[str, Any]: + try: + outer = _unwrap(LES0_RAW, "LES0_RAW") + root = _dict(outer.get("legal_structure_input_pack")) + if root.get("schema_version") == "stage1_legal_structure_input_pack.v1" and root.get("status") == "READY": + return root + except Exception: + pass + outer = _read_doc(INPUT_PACK_PATH) + root = _dict(outer.get("legal_structure_input_pack")) + if root.get("schema_version") != "stage1_legal_structure_input_pack.v1" or root.get("status") != "READY": + raise ValueError("legal_structure_input_pack is not READY") + return root + + def _norm_text(text: Any) -> str: + value = re.sub(r"\s+", "", str(text or "").lower()) + value = re.sub(r"[()\[\]{}'\"·,.;:]", "", value) + value = re.sub(r"(에관하여|에대한|및|의|을|를|은|는|이|가)$", "", value) + return value + + def _score_issue(candidate: dict[str, Any]) -> list[dict[str, Any]]: + fields = _dict(candidate.get("keyword_fields")) + text = " ".join( + _strings(fields.get("Legal_Keywords")) + + _strings(fields.get("Action")) + + _strings(fields.get("Reason")) + + _strings(fields.get("JuristicAct_label")) + + _strings(fields.get("ActionType")) + + _strings(fields.get("domain_legal_effect_tag")) + + _strings(fields.get("legal_effect_tags")) + + _strings(candidate.get("candidate_structure_types")) + + _strings(candidate.get("source_domain")) + ).lower() + rows = [] + for family, keywords in ISSUE_KEYWORDS.items(): + score = sum(1 for keyword in keywords if keyword.lower() in text) + if score: + rows.append({"candidate_value": family, "score": score, "basis": [kw for kw in keywords if kw.lower() in text]}) + rows.sort(key=lambda row: (-row["score"], row["candidate_value"])) + return rows + + def _hard_issue_keys(candidate: dict[str, Any]) -> list[dict[str, Any]]: + hard = _dict(candidate.get("hard_issue_keys")) + rows: list[dict[str, Any]] = [] + for field in ["claim_group_ids", "liability_group_ids", "linked_structure_ids", "canonical_theory_graph_seed_ref"]: + for value in _strings(hard.get(field)): + rows.append({"field": field, "value": value}) + return rows + + def _issue_decision(candidate: dict[str, Any]) -> tuple[str, dict[str, Any], dict[str, Any] | None]: + bo_id = candidate["bo_id"] + hard_rows = _hard_issue_keys(candidate) + score_rows = _score_issue(candidate) + conflict_flags: list[str] = [] + hard_by_field = {field: _strings(_dict(candidate.get("hard_issue_keys")).get(field)) + for field in HARD_ISSUE_KEY_PRIORITY} + intra_conflict_fields = [field for field, values in hard_by_field.items() if len(values) > 1] + if intra_conflict_fields: + conflict_flags.append("multiple_hard_issue_keys") + else: + for field in HARD_ISSUE_KEY_PRIORITY: + if hard_by_field[field]: + demoted = {f: hard_by_field[f] for f in HARD_ISSUE_KEY_PRIORITY + if f != field and hard_by_field[f]} + return f"hard:{hard_by_field[field][0]}", { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [field], + "demoted_hard_keys": demoted, + "score_table": score_rows, + "conflict_flags": [], + }, None + if score_rows: + top = score_rows[0] + second = score_rows[1]["score"] if len(score_rows) > 1 else 0 + if top["score"] >= ISSUE_THRESHOLD and top["score"] - second >= ISSUE_MARGIN and not conflict_flags: + return f"issue:{top['candidate_value']}", { + "decision": "score_confident", + "confidence": "MEDIUM", + "basis_fields": ["Legal_Keywords", "Action", "Reason", "JuristicAct.label", "domain tags"], + "score_table": score_rows, + "conflict_flags": [], + }, None + types = set(_strings(candidate.get("candidate_structure_types"))) + if {"money_claim", "fraudulent_transfer"}.issubset(types): + conflict_flags.append("money_claim_and_actio_same_bo") + if {"registry_invalidity", "secured_debt"}.issubset(types): + conflict_flags.append("registry_invalidity_and_secured_debt_same_bo") + candidate_pool = _dedupe([row["value"] for row in hard_rows] + [row["candidate_value"] for row in score_rows]) + if len(candidate_pool) == 1 and not conflict_flags: + return f"issue:{candidate_pool[0]}", { + "decision": "auto_selected_single_candidate", + "confidence": "MEDIUM", + "basis_fields": ["hard_issue_keys", "score_table"], + "score_table": score_rows, + "conflict_flags": [], + }, None + if not hard_rows and not score_rows: + return f"review_issue:{bo_id}", { + "decision": "auto_blocked", + "confidence": "LOW", + "basis_fields": [], + "score_table": [], + "conflict_flags": ["missing_candidate_values"], + }, None + exception = { + "exception_id": "", + "exception_type": "ambiguous_issue_cluster", + "source_bo_ids": [bo_id], + "candidate_values": [row["value"] for row in hard_rows] + [row["candidate_value"] for row in score_rows], + "basis_fields": ["hard_issue_keys", "Legal_Keywords", "Action", "Reason", "JuristicAct.label", "candidate_structure_types"], + "score_table": score_rows, + "conflict_flags": conflict_flags or ["insufficient_issue_confidence"], + "compact_context_class": sorted(types), + "allowed_decisions": ["USE_EXISTING_HARD_KEY", "SELECT_ISSUE_FAMILY", "SPLIT_ISSUE_CLUSTER", "BLOCK_REVIEW"], + } + fallback = f"review_issue:{bo_id}" + return fallback, { + "decision": "llm_required", + "confidence": "LOW", + "basis_fields": exception["basis_fields"], + "score_table": score_rows, + "conflict_flags": exception["conflict_flags"], + }, exception + + def _asset_norm(label: str) -> str: + text = _norm_text(label) + text = re.sub(r"(지분|전체|소유권|목적물)$", "", text) + return text + + def _legal_unit_tokens(label: str) -> list[str]: + tokens = [] + for token in ["대지", "토지", "건물", "지분", "근저당", "담보", "점유", "사용이익", "상가", "임야", "채권"]: + if token in str(label): + tokens.append(token) + return tokens + + def _asset_decisions(candidate: dict[str, Any]) -> tuple[list[str], list[dict[str, Any]], list[dict[str, Any]]]: + bo_id = candidate["bo_id"] + assets = [item for item in _list(candidate.get("asset_candidates")) if isinstance(item, dict) and item.get("asset_label")] + cluster_keys: list[str] = [] + bundle_items: list[dict[str, Any]] = [] + exceptions: list[dict[str, Any]] = [] + if not assets: + return [], [], [] + def _ref_sort(ref_key: str) -> tuple[int, str]: + return (0 if ref_key.startswith("REG-") else 1 if ref_key.startswith("SCH-") else 2, ref_key) + keyed, unkeyed = [], [] + for asset in assets: + (keyed if _strings(asset.get("asset_ref_keys")) else unkeyed).append(asset) + groups: dict[str, list[dict[str, Any]]] = defaultdict(list) + key_alias: dict[str, str] = {} + for asset in keyed: + refs = _strings(asset.get("asset_ref_keys")) + root = next((key_alias[r] for r in refs if r in key_alias), + f"ref:{sorted(refs, key=_ref_sort)[0]}") + for r in refs: + key_alias[r] = root + groups[root].append(asset) + for asset in unkeyed: + label = str(asset.get("asset_label")) + groups[f"{_asset_norm(label)}::{asset.get('asset_kind') or 'unknown'}"].append(asset) + for key, group in groups.items(): + if key.startswith("ref:"): + cluster_keys.append(f"asset:{key}") + continue + labels = _dedupe([str(item.get("asset_label")) for item in group]) + kinds = set(_strings([item.get("asset_kind") for item in group])) + legal_units = {tuple(_legal_unit_tokens(label)) for label in labels} + if len(kinds) == 1 and len(legal_units) <= 1: + cluster_keys.append(f"asset:{key}") + else: + exceptions.append({ + "exception_id": "", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [bo_id], + "candidate_values": labels, + "basis_fields": [str(item.get("source")) for item in group], + "score_table": [{"candidate_value": label, "normalized_label": _asset_norm(label), "asset_kind": item.get("asset_kind")} for label, item in zip(labels, group)], + "conflict_flags": ["same_label_but_legal_unit_or_kind_differs"], + "compact_context_class": [sorted(_strings(list(kinds))), sorted(str(t) for t in legal_units), key], + "allowed_decisions": ["SAME_CLUSTER", "SEPARATE_CLUSTER", "MULTI_ASSET_BUNDLE", "BLOCK_REVIEW"], + }) + cluster_keys.append(f"review_asset:{bo_id}:{len(exceptions)}") + normalized_labels: set[str] = set() + for item in assets: + item_refs = _strings(item.get("asset_ref_keys")) + if item_refs and item_refs[0] in key_alias: + normalized_labels.add(key_alias[item_refs[0]]) + else: + normalized_labels.add(_asset_norm(str(item.get("asset_label")))) + if len(normalized_labels) > 1: + bundle_items.append({ + "bundle_ref": f"bundle:{bo_id}", + "bundle_type": "multi_asset_bundle", + "source_bo_ids": [bo_id], + "member_asset_labels": _dedupe([str(item.get("asset_label")) for item in assets]), + "review_required": len(normalized_labels) <= 2, + }) + if len(normalized_labels) <= 2: + exceptions.append({ + "exception_id": "", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [bo_id], + "candidate_values": _dedupe([str(item.get("asset_label")) for item in assets]), + "basis_fields": ["asset_candidates"], + "score_table": [{"candidate_value": str(item.get("asset_label")), "normalized_label": _asset_norm(str(item.get("asset_label"))), "asset_kind": item.get("asset_kind")} for item in assets], + "conflict_flags": ["multi_asset_bundle_or_separate_clusters_ambiguous"], + "compact_context_class": [sorted(_dedupe([str(item.get("asset_kind")) for item in assets])), sorted(normalized_labels)], + "allowed_decisions": ["SEPARATE_CLUSTER", "MULTI_ASSET_BUNDLE", "BLOCK_REVIEW"], + }) + return _dedupe(cluster_keys), bundle_items, exceptions + + def _structure_type_decision(candidate: dict[str, Any]) -> tuple[str, list[str], dict[str, Any] | None, dict[str, Any]]: + bo_id = candidate["bo_id"] + types = _dedupe(_strings(candidate.get("candidate_structure_types"))) or ["general_legal_effect"] + roles = {DOWNSTREAM_ROLE_FAMILY.get(t, "legal_effect_roles") for t in types} + representative = sorted(types, key=lambda t: STRUCTURE_TYPE_PRIORITY.index(t) if t in STRUCTURE_TYPE_PRIORITY else 999)[0] + diag = { + "candidate_types": types, + "downstream_roles": sorted(roles), + "representative_candidate_structure_type": representative, + "basis_fields": ["legal_effect_signals", "case_liability_signals", "actio_case_signals", "BO.downstream_seed_refs", "BO.domain_payload"], + } + if len(types) == 1 or len(roles) == 1: + return representative, types, None, {**diag, "decision": "deterministic"} + conflict = { + "exception_id": "", + "exception_type": "ambiguous_candidate_structure_type", + "source_bo_ids": [bo_id], + "candidate_values": types, + "basis_fields": diag["basis_fields"], + "score_table": [{"candidate_value": t, "downstream_role": DOWNSTREAM_ROLE_FAMILY.get(t, "legal_effect_roles")} for t in types], + "conflict_flags": ["candidate_types_map_to_different_downstream_fact_roles"], + "compact_context_class": sorted(types), + "allowed_decisions": ["SELECT_REPRESENTATIVE_TYPE", "PRESERVE_MULTI_ROUTING", "SPLIT_STRUCTURE_SEED", "BLOCK_REVIEW"], + } + return representative, types, conflict, {**diag, "decision": "llm_required"} + + def _make_id(prefix: str, ordinal: int) -> str: + return f"{prefix}-{ordinal:03d}" + + def main() -> None: + try: + _init_mcp_session() + pack = _load_input_pack() + bo_by_id = _dict(pack.get("bo_by_id")) + candidates = _dict(pack.get("candidate_inputs_by_bo_id")) + evidence_authority = _dict(pack.get("evidence_authority_by_index")) + issue_key_to_id: dict[str, str] = {} + asset_key_to_id: dict[str, str] = {} + issue_seed_map: dict[str, dict[str, Any]] = {} + asset_seed_map: dict[str, dict[str, Any]] = {} + type_index_seed: dict[str, dict[str, Any]] = {} + evidence_index_seed: dict[str, dict[str, Any]] = {} + seed_items: list[dict[str, Any]] = [] + diagnostics = { + "issue_cluster_exceptions": [], + "asset_cluster_exceptions": [], + "candidate_structure_type_exceptions": [], + "auto_decided_items": [], + "auto_blocked_items": [], + } + multi_asset_bundles: list[dict[str, Any]] = [] + + for bo_id in sorted(candidates): + candidate = _dict(candidates[bo_id]) + issue_key, issue_diag, issue_exc = _issue_decision(candidate) + if issue_key not in issue_key_to_id: + issue_key_to_id[issue_key] = _make_id("IC", len(issue_key_to_id) + 1) + issue_id = issue_key_to_id[issue_key] + if issue_exc: + diagnostics["issue_cluster_exceptions"].append(issue_exc) + if issue_diag.get("decision") == "auto_blocked": + diagnostics["auto_blocked_items"].append({ + "bo_id": bo_id, + "exception_type": "ambiguous_issue_cluster", + "reason": "missing_candidate_values", + }) + if issue_diag.get("decision") == "auto_selected_single_candidate": + diagnostics["auto_decided_items"].append({ + "bo_id": bo_id, + "exception_type": "ambiguous_issue_cluster", + "selected_value": issue_key.split(":", 1)[-1], + }) + asset_keys, bundles, asset_excs = _asset_decisions(candidate) + asset_ids: list[str] = [] + for key in asset_keys: + if key not in asset_key_to_id: + asset_key_to_id[key] = _make_id("AC", len(asset_key_to_id) + 1) + asset_ids.append(asset_key_to_id[key]) + for bundle in bundles: + multi_asset_bundles.append(bundle) + for exc in asset_excs: + diagnostics["asset_cluster_exceptions"].append(exc) + representative_type, all_types, type_exc, type_diag = _structure_type_decision(candidate) + if type_exc: + diagnostics["candidate_structure_type_exceptions"].append(type_exc) + + seed_id = _make_id("LESS", len(seed_items) + 1) + evidence_indexes = _strings(candidate.get("evidence_indexes")) + signals = _dict(candidate.get("signals")) + case_signals = _list(signals.get("case_liability")) + actio_signals = _list(signals.get("actio")) + linked_claim_group_ids = _dedupe([x for sig in case_signals if isinstance(sig, dict) for x in _strings(sig.get("claim_group_id"))]) + linked_liability_group_ids = _dedupe([x for sig in case_signals if isinstance(sig, dict) for x in _strings(sig.get("liability_group_id"))]) + linked_actio_signal_ids = _dedupe([x for sig in actio_signals if isinstance(sig, dict) for x in _strings(sig.get("signal_id"))]) + review_required = bool(issue_exc or asset_excs or type_exc + or issue_diag.get("decision") == "auto_blocked") + item = { + "seed_id": seed_id, + "source_bo_ids": [bo_id], + "representative_candidate_structure_type": representative_type, + "candidate_structure_types": all_types, + "issue_cluster_ids": [issue_id], + "asset_cluster_ids": asset_ids, + "multi_asset_bundle_refs": _strings([bundle.get("bundle_ref") for bundle in bundles]), + "evidence_indexes": evidence_indexes, + "linked_claim_group_ids": linked_claim_group_ids, + "linked_liability_group_ids": linked_liability_group_ids, + "linked_actio_signal_ids": linked_actio_signal_ids, + "basis_fields": _dedupe(issue_diag.get("basis_fields", []) + type_diag.get("basis_fields", []) + ["BO.source_evidence_indexes", "candidate_inputs_by_bo_id"]), + "confidence": "LOW" if review_required else ("HIGH" if issue_diag.get("confidence") == "HIGH" and len(all_types) == 1 else "MEDIUM"), + "review_required": review_required, + "diagnostics": { + "issue": issue_diag, + "candidate_structure_type": type_diag, + }, + } + seed_items.append(item) + _add_issue_seed(issue_seed_map, issue_id, issue_key, item, candidate, issue_diag) + _add_asset_seeds(asset_seed_map, asset_ids, item, candidate, asset_key_to_id) + _add_type_index(type_index_seed, all_types, item) + _add_evidence_index(evidence_index_seed, evidence_indexes, item, evidence_authority) + + merge_groups = _merge_exceptions(diagnostics) + adjudicator_input = _build_adjudicator_input( + seed_items, diagnostics, merge_groups, + issue_seed_map, asset_seed_map, multi_asset_bundles) + exception_pack = { + "legal_structure_exception_pack": { + "schema_version": "stage1_legal_structure_exception_pack.v1", + "status": "READY", + "issue_cluster_exceptions": diagnostics["issue_cluster_exceptions"], + "asset_cluster_exceptions": diagnostics["asset_cluster_exceptions"], + "candidate_structure_type_exceptions": diagnostics["candidate_structure_type_exceptions"], + } + } + seed_bundle = { + "legal_structure_seed_bundle": { + "schema_version": "stage1_legal_structure_seed_bundle.v1", + "status": "READY_WITH_REVIEW" if _exception_count(diagnostics) or diagnostics["auto_blocked_items"] else "READY", + "source_input_pack_schema": "stage1_legal_structure_input_pack.v1", + "structure_seed_items": seed_items, + "issue_cluster_seed_map": issue_seed_map, + "asset_cluster_seed_map": asset_seed_map, + "multi_asset_bundles": multi_asset_bundles, + "candidate_structure_type_index_seed": type_index_seed, + "evidence_index_seed": evidence_index_seed, + "ambiguity_diagnostics": diagnostics, + "legal_structure_exception_pack": exception_pack["legal_structure_exception_pack"], + } + } + _validate_outputs(seed_bundle, exception_pack) + _write_doc(SEED_OUT_PATH, seed_bundle) + _write_doc(EXCEPTION_OUT_PATH, exception_pack) + print(json.dumps({ + "legal_structure_les0_seed_generation": { + "schema_version": "stage1_legal_structure_les0_seed_generation.v1", + "status": seed_bundle["legal_structure_seed_bundle"]["status"], + "written_files": [SEED_OUT_PATH, EXCEPTION_OUT_PATH], + "seed_count": len(seed_items), + "exception_count": _exception_count(diagnostics), + "llm_exception_count": len(adjudicator_input["llm_exception_items"]), + "auto_decision_count": len(adjudicator_input["auto_decisions"]), + "auto_blocked_count": len(adjudicator_input["auto_blocked_items"]), + "merged_from_count": adjudicator_input["exception_counts"]["merged_from"], + }, + "legal_structure_exception_adjudicator_input": adjudicator_input, + }, ensure_ascii=False)) + except Exception as exc: + print(json.dumps({ + "legal_structure_seed_bundle": { + "schema_version": "stage1_legal_structure_seed_bundle.v1", + "status": "FAILED", + "failure_reason": str(exc), + "structure_seed_items": [], + "ambiguity_diagnostics": { + "issue_cluster_exceptions": [], + "asset_cluster_exceptions": [], + "candidate_structure_type_exceptions": [], + }, + } + }, ensure_ascii=False)) + sys.exit(0) + + def _exception_count(diagnostics: dict[str, Any]) -> int: + return ( + len(_list(diagnostics.get("issue_cluster_exceptions"))) + + len(_list(diagnostics.get("asset_cluster_exceptions"))) + + len(_list(diagnostics.get("candidate_structure_type_exceptions"))) + ) + + def _merge_exceptions(diagnostics: dict[str, Any]) -> list[dict[str, Any]]: + merge_groups: list[dict[str, Any]] = [] + for key in ["issue_cluster_exceptions", "asset_cluster_exceptions", "candidate_structure_type_exceptions"]: + merged: dict[str, dict[str, Any]] = {} + for exc in _list(diagnostics.get(key)): + if not isinstance(exc, dict): + continue + signature = json.dumps({ + "type": exc.get("exception_type"), + "candidates": sorted(_strings(exc.get("candidate_values"))), + "flags": sorted(_strings(exc.get("conflict_flags"))), + "decisions": sorted(_strings(exc.get("allowed_decisions"))), + "context_class": exc.get("compact_context_class"), + }, ensure_ascii=False, sort_keys=True) + if signature in merged: + root = merged[signature] + root["source_bo_ids"] = _dedupe(_strings(root.get("source_bo_ids")) + _strings(exc.get("source_bo_ids"))) + root["merged_from_count"] = root.get("merged_from_count", 1) + 1 + else: + merged[signature] = {**exc, "merged_from_count": 1} + diagnostics[key] = sorted( + merged.values(), + key=lambda item: ( + min(_strings(item.get("source_bo_ids")) or [""]), + json.dumps(sorted(_strings(item.get("candidate_values"))), ensure_ascii=False), + json.dumps(sorted(_strings(item.get("conflict_flags"))), ensure_ascii=False), + ), + ) + lex_ordinal = itertools.count(1) + for key in ["issue_cluster_exceptions", "asset_cluster_exceptions", "candidate_structure_type_exceptions"]: + for exc in diagnostics[key]: + exc["exception_id"] = _make_id("LEX", next(lex_ordinal)) + if exc.get("merged_from_count", 1) > 1: + merge_groups.append({ + "representative_exception_id": exc["exception_id"], + "source_bo_ids": _strings(exc.get("source_bo_ids")), + "merged_from_count": exc["merged_from_count"], + }) + diagnostics["merge_groups"] = merge_groups + return merge_groups + + def _compact_seed_summary(item: dict[str, Any], minimal: bool = False) -> dict[str, Any]: + summary: dict[str, Any] = {"seed_id": item.get("seed_id")} + if minimal: + return summary + summary.update({ + "representative_candidate_structure_type": item.get("representative_candidate_structure_type"), + "candidate_structure_types": item.get("candidate_structure_types"), + "issue_cluster_ids": item.get("issue_cluster_ids"), + "asset_cluster_ids": item.get("asset_cluster_ids"), + "confidence": item.get("confidence"), + "demoted_hard_keys": _dict(_dict(_dict(item.get("diagnostics")).get("issue")).get("demoted_hard_keys")), + }) + return summary + + def _build_adjudicator_input( + seed_items: list[dict[str, Any]], + diagnostics: dict[str, Any], + merge_groups: list[dict[str, Any]], + issue_seed_map: dict[str, dict[str, Any]], + asset_seed_map: dict[str, dict[str, Any]], + multi_asset_bundles: list[dict[str, Any]], + ) -> dict[str, Any]: + representatives: list[dict[str, Any]] = [] + for key in ["issue_cluster_exceptions", "asset_cluster_exceptions", "candidate_structure_type_exceptions"]: + representatives.extend([exc for exc in _list(diagnostics.get(key)) if isinstance(exc, dict)]) + representatives.sort(key=lambda exc: ( + EXCEPTION_TYPE_ORDER.get(str(exc.get("exception_type")), 9), + -len(_strings(exc.get("source_bo_ids"))), + str(exc.get("exception_id")), + )) + seeds_by_bo: dict[str, list[dict[str, Any]]] = {} + for item in seed_items: + for seed_bo_id in _strings(item.get("source_bo_ids")): + seeds_by_bo.setdefault(seed_bo_id, []).append(item) + + def _context_for(exc: dict[str, Any], minimal: bool) -> dict[str, Any]: + related = [_compact_seed_summary(item, minimal) + for exc_bo_id in _strings(exc.get("source_bo_ids")) + for item in seeds_by_bo.get(exc_bo_id, [])] + issue_rows: dict[str, Any] = {} + asset_rows: dict[str, Any] = {} + bundles: list[dict[str, Any]] = [] + if not minimal: + for summary in related: + for issue_id in _strings(summary.get("issue_cluster_ids")): + row = _dict(issue_seed_map.get(issue_id)) + if row: + issue_rows[issue_id] = { + "issue_type": row.get("issue_type"), + "source_bo_ids": row.get("source_bo_ids"), + "candidate_structure_types": row.get("candidate_structure_types"), + } + for asset_id in _strings(summary.get("asset_cluster_ids")): + row = _dict(asset_seed_map.get(asset_id)) + if row: + asset_rows[asset_id] = { + "canonical_asset_label": row.get("canonical_asset_label"), + "asset_kind": row.get("asset_kind"), + "member_asset_mentions": row.get("member_asset_mentions"), + } + exc_bo_ids = set(_strings(exc.get("source_bo_ids"))) + bundles = [bundle for bundle in _list(multi_asset_bundles) + if isinstance(bundle, dict) and exc_bo_ids & set(_strings(bundle.get("source_bo_ids")))] + return {"seed_items": related, "issue_cluster_rows": issue_rows, + "asset_cluster_rows": asset_rows, "multi_asset_bundles": bundles} + + def _micro_item(exc: dict[str, Any], minimal: bool) -> dict[str, Any]: + return { + "exception_id": exc.get("exception_id"), + "exception_type": exc.get("exception_type"), + "source_bo_ids": _strings(exc.get("source_bo_ids")), + "candidate_values": _strings(exc.get("candidate_values")), + "basis_fields": _strings(exc.get("basis_fields")), + "score_summary": _list(exc.get("score_table"))[:3], + "conflict_flags": _strings(exc.get("conflict_flags")), + "allowed_decisions": _strings(exc.get("allowed_decisions")), + "merged_from_count": exc.get("merged_from_count", 1), + "compact_context": _context_for(exc, minimal), + } + + llm_items: list[dict[str, Any]] = [] + blocked_ids: set[str] = set() + overflow = False + for exc in representatives: + block_reason = None + if len(llm_items) >= ADJUDICATOR_BUDGET["max_llm_items"]: + block_reason = "budget_overflow" + elif len(_strings(exc.get("candidate_values"))) > ADJUDICATOR_BUDGET["max_candidate_values_per_item"]: + block_reason = "budget_overflow" + else: + micro = _micro_item(exc, minimal=False) + if len(json.dumps(micro, ensure_ascii=False)) > ADJUDICATOR_BUDGET["max_chars_per_item"]: + micro = _micro_item(exc, minimal=True) + if len(json.dumps(micro, ensure_ascii=False)) > ADJUDICATOR_BUDGET["max_chars_per_item"]: + block_reason = "budget_overflow" + else: + llm_items.append(micro) + if block_reason is not None: + overflow = True + blocked_ids.add(str(exc.get("exception_id"))) + diagnostics["auto_blocked_items"].append({ + "exception_type": exc.get("exception_type"), + "source_bo_ids": _strings(exc.get("source_bo_ids")), + "reason": block_reason, + }) + exc_bo_ids = set(_strings(exc.get("source_bo_ids"))) + for item in seed_items: + if exc_bo_ids & set(_strings(item.get("source_bo_ids"))): + item["review_required"] = True + if blocked_ids: + for key in ["issue_cluster_exceptions", "asset_cluster_exceptions", "candidate_structure_type_exceptions"]: + diagnostics[key] = [exc for exc in _list(diagnostics.get(key)) + if str(exc.get("exception_id")) not in blocked_ids] + auto_decided = _list(diagnostics.get("auto_decided_items")) + auto_blocked = _list(diagnostics.get("auto_blocked_items")) + if not llm_items and not auto_blocked: + status = "READY_NO_EXCEPTIONS" + elif not llm_items: + status = "READY_WITH_AUTOBLOCKS" + else: + status = "READY" + type_counts = {"issue": 0, "asset": 0, "type": 0} + for micro in llm_items: + if micro["exception_type"] == "ambiguous_issue_cluster": + type_counts["issue"] += 1 + elif micro["exception_type"] == "ambiguous_asset_cluster": + type_counts["asset"] += 1 + else: + type_counts["type"] += 1 + return { + "schema_version": "stage1_legal_structure_exception_adjudicator_input.v1", + "status": status, + "source_exception_pack_schema_version": "stage1_legal_structure_exception_pack.v1", + "exception_counts": { + "issue": type_counts["issue"], + "asset": type_counts["asset"], + "type": type_counts["type"], + "auto_decided": len(auto_decided), + "auto_blocked": len(auto_blocked), + "merged_from": sum(int(_dict(group).get("merged_from_count") or 1) - 1 for group in _list(merge_groups)), + }, + "budget_gate": {**ADJUDICATOR_BUDGET, "truncated": overflow, + "overflow_detected": overflow, "phase2_recommended": overflow}, + "auto_decisions": auto_decided, + "auto_blocked_items": auto_blocked, + "llm_exception_items": llm_items, + "merge_groups": _list(merge_groups), + } + + def _add_issue_seed(target: dict[str, dict[str, Any]], issue_id: str, issue_key: str, item: dict[str, Any], candidate: dict[str, Any], diag: dict[str, Any]) -> None: + row = target.setdefault(issue_id, { + "issue_cluster_id": issue_id, + "issue_key": issue_key, + "issue_type": issue_key.split(":", 1)[-1], + "source_bo_ids": [], + "candidate_structure_types": [], + "claim_group_ids": [], + "liability_group_ids": [], + "evidence_indexes": [], + "asset_cluster_ids": [], + "basis_fields": [], + "review_required": False, + }) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + item["source_bo_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + item["candidate_structure_types"]) + hard = _dict(candidate.get("hard_issue_keys")) + row["claim_group_ids"] = _dedupe(row["claim_group_ids"] + _strings(hard.get("claim_group_ids"))) + row["liability_group_ids"] = _dedupe(row["liability_group_ids"] + _strings(hard.get("liability_group_ids"))) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + item["evidence_indexes"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + item["asset_cluster_ids"]) + row["basis_fields"] = _dedupe(row["basis_fields"] + _strings(diag.get("basis_fields"))) + row["review_required"] = bool(row["review_required"] or item["review_required"]) + + def _add_asset_seeds(target: dict[str, dict[str, Any]], asset_ids: list[str], item: dict[str, Any], candidate: dict[str, Any], mapping: dict[str, str]) -> None: + assets = [asset for asset in _list(candidate.get("asset_candidates")) if isinstance(asset, dict)] + for asset_id in asset_ids: + row = target.setdefault(asset_id, { + "asset_cluster_id": asset_id, + "canonical_asset_label": None, + "asset_kind": None, + "member_asset_mentions": [], + "source_bo_ids": [], + "evidence_indexes": [], + "issue_cluster_ids": [], + "candidate_structure_types": [], + "basis_fields": [], + "review_required": asset_id.startswith("AC-") and any(key.startswith("review_asset") and value == asset_id for key, value in mapping.items()), + }) + related_assets = assets or [] + labels = _dedupe([str(asset.get("asset_label")) for asset in related_assets if asset.get("asset_label")]) + if row["canonical_asset_label"] is None and labels: + row["canonical_asset_label"] = labels[0] + kinds = _dedupe([str(asset.get("asset_kind")) for asset in related_assets if asset.get("asset_kind")]) + if row["asset_kind"] is None and kinds: + row["asset_kind"] = kinds[0] + row["member_asset_mentions"] = _dedupe(row["member_asset_mentions"] + labels) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + item["source_bo_ids"]) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + item["evidence_indexes"]) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + item["issue_cluster_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + item["candidate_structure_types"]) + row["basis_fields"] = _dedupe(row["basis_fields"] + [str(asset.get("source")) for asset in related_assets if asset.get("source")]) + + def _add_type_index(target: dict[str, dict[str, Any]], types: list[str], item: dict[str, Any]) -> None: + for typ in types: + row = target.setdefault(typ, { + "candidate_structure_type": typ, + "seed_ids": [], + "source_bo_ids": [], + "issue_cluster_ids": [], + "asset_cluster_ids": [], + "evidence_indexes": [], + "downstream_fact_roles": [], + "basis_fields": [], + "review_required": False, + }) + row["seed_ids"] = _dedupe(row["seed_ids"] + [item["seed_id"]]) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + item["source_bo_ids"]) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + item["issue_cluster_ids"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + item["asset_cluster_ids"]) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + item["evidence_indexes"]) + row["downstream_fact_roles"] = _dedupe(row["downstream_fact_roles"] + [DOWNSTREAM_ROLE_FAMILY.get(typ, "legal_effect_roles")]) + row["basis_fields"] = _dedupe(row["basis_fields"] + item["basis_fields"]) + row["review_required"] = bool(row["review_required"] or item["review_required"]) + + def _add_evidence_index(target: dict[str, dict[str, Any]], evidence_indexes: list[str], item: dict[str, Any], authority: dict[str, Any]) -> None: + for idx in evidence_indexes: + auth = _dict(authority.get(idx)) + row = target.setdefault(idx, { + "evidence_index": idx, + "source_title": auth.get("source_title"), + "priority_class": auth.get("priority_class"), + "authentication_status": auth.get("authentication_status"), + "seed_ids": [], + "source_bo_ids": [], + "issue_cluster_ids": [], + "asset_cluster_ids": [], + "candidate_structure_types": [], + "gap_flags": [], + }) + row["seed_ids"] = _dedupe(row["seed_ids"] + [item["seed_id"]]) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + item["source_bo_ids"]) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + item["issue_cluster_ids"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + item["asset_cluster_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + item["candidate_structure_types"]) + if not auth: + row["gap_flags"] = _dedupe(row["gap_flags"] + ["missing_evidence_authority"]) + elif not auth.get("excerpt"): + row["gap_flags"] = _dedupe(row["gap_flags"] + ["missing_evidence_excerpt"]) + + def _validate_outputs(seed_bundle: dict[str, Any], exception_pack: dict[str, Any]) -> None: + root = _dict(seed_bundle.get("legal_structure_seed_bundle")) + if root.get("schema_version") != "stage1_legal_structure_seed_bundle.v1": + raise ValueError("seed bundle schema mismatch") + if root.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("seed bundle status invalid") + diagnostics = _dict(root.get("ambiguity_diagnostics")) + for key in ["issue_cluster_exceptions", "asset_cluster_exceptions", "candidate_structure_type_exceptions"]: + if not isinstance(diagnostics.get(key), list): + raise ValueError(f"ambiguity_diagnostics.{key} must be array") + pack = _dict(exception_pack.get("legal_structure_exception_pack")) + if pack.get("schema_version") != "stage1_legal_structure_exception_pack.v1" or pack.get("status") != "READY": + raise ValueError("exception pack schema/status mismatch") + for key in ["issue_cluster_exceptions", "asset_cluster_exceptions", "candidate_structure_type_exceptions"]: + if not isinstance(pack.get(key), list): + raise ValueError(f"exception pack missing {key}") + for item in pack.get(key): + for field in ["exception_id", "exception_type", "source_bo_ids", "candidate_values", "basis_fields", "score_table", "conflict_flags", "allowed_decisions"]: + if field not in item: + raise ValueError(f"exception item missing {field}") + + if __name__ == "__main__": + main() + + - task_name: Task_LES2_single_cluster_exception_adjudicator + llm_provider: google + llm_model: gemini-3.5-flash + llm_reasoning: high + llm_verbosity: medium + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + + {{prev.Task_LES1_deterministic_structure_seed_builder}} + + + + + + - You MUST act from the perspective of a top-tier Korean civil-litigation lawyer with more than 20 years of experience and a world-class AI architect. + - You MUST use only . + - You MUST validate that the input contains `legal_structure_exception_adjudicator_input`. + - You MUST validate `legal_structure_exception_adjudicator_input.schema_version == "stage1_legal_structure_exception_adjudicator_input.v1"`. + - You MUST validate that `legal_structure_exception_adjudicator_input.llm_exception_items` is an array. + - You MUST treat `source_exception_pack_schema_version`, `exception_counts`, `budget_gate`, `auto_decisions`, `auto_blocked_items`, and `merge_groups` as optional audit context; ignore unknown additive fields instead of failing validation. + - You MUST NOT adjudicate items in `auto_decisions` or `auto_blocked_items`; they are audit-only records already applied upstream. + - You MAY read each `llm_exception_items[]` item's embedded `compact_context` only as compact supporting context for that item. + - You MUST NOT read, request, or reconstruct `client_goal.json`, `BO.json`, `evidence_indexed.json`, `evidence_all.json`, `evidence_event_candidates.json`, `actio_case_signals.json`, `case_liability_signals.json`, or `legal_effect_signals.json`. + - You MUST NOT create new BO IDs, evidence indexes, structure IDs, issue cluster IDs, asset cluster IDs, facts, parties, dates, amounts, or source references. + - You MUST NOT write `legal_effect_structures.json` or any file. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: Task_LES2_single_cluster_exception_adjudicator + STAGE: Stage 1 legal effect structure/index builder + ROLE: compact exception-only LLM adjudicator before deterministic LES3 final writer + MISSION: Decide only ambiguity exceptions produced by deterministic LES1 so LES3 can finalize legal_effect_structures.json. + + + + - You are not the final writer. + - You may decide only exceptions explicitly listed in `legal_structure_exception_adjudicator_input.llm_exception_items`. + - You may not change or add exception IDs. + - You may not alter non-exception rows. + - You may not add new candidate values outside `candidate_values` or allowed decision targets. + - You may not invent a missing basis. If the listed compact basis is insufficient, return `BLOCK_REVIEW`. + - LES3 will assign stable final `LES-###`, `IC-###`, and `AC-###` IDs and will validate references. + + + + - `ambiguous_issue_cluster`: decide whether BOs or issue candidates belong to the same legal/factual issue family or require split/review. + - `ambiguous_asset_cluster`: decide whether asset mentions represent the same legally relevant asset group, separate clusters, a multi-asset bundle, or review. + - `ambiguous_candidate_structure_type`: decide whether multiple candidate structure types should preserve multi-routing, select one representative type, split structure seed, or review. + + + + For each `llm_exception_items[]` item, read only these fields: + - `exception_id` + - `exception_type` + - `source_bo_ids` + - `candidate_values` + - `basis_fields` + - `score_summary` + - `conflict_flags` + - `allowed_decisions` + - `merged_from_count` + + You may use compact supporting rows only when needed: + - the item's embedded `compact_context.seed_items[]` + - the item's embedded `compact_context.issue_cluster_rows` + - the item's embedded `compact_context.asset_cluster_rows` + - the item's embedded `compact_context.multi_asset_bundles` + + You must ignore all unsupported, raw, or non-compact material if present. + + + + + - Use `USE_EXISTING_HARD_KEY` only if one candidate hard key clearly controls and no conflict flag defeats it. + - Use `SELECT_ISSUE_FAMILY` only when one issue family is clearly supported by candidate values, score table, source BO ids, and basis fields. + - Use `SPLIT_ISSUE_CLUSTER` when source BOs or candidate values point to distinct legal issue families that LES3 should keep separate. + - Use `BLOCK_REVIEW` when the compact record is insufficient to decide whether items share the same legal/factual issue. + - Do not decide a merits-level legal conclusion. This is only a routing/index judgment. + + + + - Use `SAME_CLUSTER` only when the candidate asset mentions appear to refer to the same legally relevant asset unit. + - Use `SEPARATE_CLUSTER` when mentions refer to different assets, different legal units, or different legal roles such as property object versus security right. + - Use `MULTI_ASSET_BUNDLE` when one BO or structure must preserve a bundle of multiple assets without collapsing them into one asset cluster. + - Use `BLOCK_REVIEW` when the compact record cannot distinguish same asset, separate asset, or bundle. + - Treat asset cluster as a routing/join key, not a final title, ownership, encumbrance, or valuation conclusion. + + + + - Use `SELECT_REPRESENTATIVE_TYPE` only when one candidate structure type is clearly primary for Fact Ledger routing. + - Use `PRESERVE_MULTI_ROUTING` when multiple candidate types share compatible downstream roles or the fact row legitimately serves multiple modules. + - Use `SPLIT_STRUCTURE_SEED` when different candidate types imply materially different Fact Ledger roles such as `claim_chain_ref`, `actio_roles`, `linked_structures`, or `legal_calculation_object`. + - Use `BLOCK_REVIEW` when the compact record is insufficient to decide representative, multi-routing, or split. + - Do not create a candidate structure type that is not listed in `candidate_values`. + + + + + 1. Parse and locate `legal_structure_exception_adjudicator_input`. + 2. If `llm_exception_items` is empty, return `status: "READY_NO_EXCEPTIONS"` with empty `decisions` and `blocked_review_items`. + 3. For every item in `llm_exception_items`, produce exactly one decision whose `exception_type` matches the item's `exception_type`. + 4. Use only the item's `allowed_decisions`. + 5. If the item cannot be decided safely, use `decision: "BLOCK_REVIEW"` and add the same item to `blocked_review_items`. + 6. Keep rationales short and basis-code-like. Do not reveal chain-of-thought. + 7. Return exactly one JSON object matching . + + + + Return exactly one JSON object: + { + "legal_structure_exception_adjudication": { + "schema_version": "stage1_legal_structure_exception_adjudication.v1", + "status": "READY|READY_NO_EXCEPTIONS|READY_WITH_BLOCKS|FAILED", + "source_exception_pack_schema_version": "stage1_legal_structure_exception_pack.v1", + "decisions": [ + { + "exception_id": "LEX-001", + "exception_type": "ambiguous_issue_cluster|ambiguous_asset_cluster|ambiguous_candidate_structure_type", + "source_bo_ids": [], + "decision": "USE_EXISTING_HARD_KEY|SELECT_ISSUE_FAMILY|SPLIT_ISSUE_CLUSTER|SAME_CLUSTER|SEPARATE_CLUSTER|MULTI_ASSET_BUNDLE|SELECT_REPRESENTATIVE_TYPE|PRESERVE_MULTI_ROUTING|SPLIT_STRUCTURE_SEED|BLOCK_REVIEW", + "selected_value": "string|null", + "selected_values": [], + "preserve_candidate_values": [], + "split_plan": [ + { + "split_label": "string", + "candidate_values": [], + "source_bo_ids": [] + } + ], + "review_required": true, + "reason_code": "hard_key_controls|score_margin_controls|separate_legal_issue|same_legal_asset_unit|separate_asset_unit|multi_asset_bundle_required|same_downstream_role|different_downstream_roles|insufficient_compact_record|unsupported_candidate", + "basis_fields_used": [], + "conflict_flags_acknowledged": [] + } + ], + "blocked_review_items": [ + { + "exception_id": "LEX-001", + "exception_type": "ambiguous_issue_cluster|ambiguous_asset_cluster|ambiguous_candidate_structure_type", + "source_bo_ids": [], + "block_reason": "insufficient_compact_record|unsupported_candidate|missing_candidate_values|conflicting_allowed_decisions|schema_violation" + } + ], + "adjudication_gate": { + "input_exception_count": 0, + "decision_count": 0, + "blocked_count": 0, + "coverage_pass": true, + "json_only_contract_pass": true + } + } + } + + Status rules: + - `READY_NO_EXCEPTIONS`: `llm_exception_items` is empty. + - `READY`: every input exception has exactly one non-BLOCK_REVIEW decision. + - `READY_WITH_BLOCKS`: every input exception is covered, but at least one decision is `BLOCK_REVIEW`. + - `FAILED`: schema is unreadable, required roots are missing, or coverage cannot be verified. + + Decision enum restrictions by exception type: + - `ambiguous_issue_cluster`: `USE_EXISTING_HARD_KEY`, `SELECT_ISSUE_FAMILY`, `SPLIT_ISSUE_CLUSTER`, `BLOCK_REVIEW`. + - `ambiguous_asset_cluster`: `SAME_CLUSTER`, `SEPARATE_CLUSTER`, `MULTI_ASSET_BUNDLE`, `BLOCK_REVIEW`. + - `ambiguous_candidate_structure_type`: `SELECT_REPRESENTATIVE_TYPE`, `PRESERVE_MULTI_ROUTING`, `SPLIT_STRUCTURE_SEED`, `BLOCK_REVIEW`. + + Field rules: + - `selected_value` is used for single selection decisions. + - `selected_values` is used for multi-selection or preserved multi-routing decisions. + - `preserve_candidate_values` must be a subset of input `candidate_values`. + - `split_plan[].candidate_values` must be subsets of input `candidate_values`. + - `basis_fields_used` must be a subset of input `basis_fields`. + - `conflict_flags_acknowledged` must be a subset of input `conflict_flags`. + - `review_required` is true only for `BLOCK_REVIEW` or split/review-sensitive decisions; otherwise false. + - Never omit `exception_id`, `exception_type`, `source_bo_ids`, or `decision`. Other fields not needed for the decision (e.g., `split_plan` for non-split decisions) may be omitted; include `basis_fields_used` and `conflict_flags_acknowledged` only with actually used entries. + + + - task_name: Task_LES3_final_structure_index_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 120 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import itertools + import json + import sys + from typing import Any + + import httpx + + SEED_RAW = r'''{{prev.Task_LES1_deterministic_structure_seed_builder}}''' + ADJUDICATION_RAW = r'''{{prev.Task_LES2_single_cluster_exception_adjudicator}}''' + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + SEED_FALLBACK_PATH = "stage1_tmp/legal_effect_structure_seed_bundle.json" + TARGET_NAME = "legal_effect_structures.json" + STRUCTURE_TYPE_ROLE = { + "money_claim": "claim_chain_ref", + "commercial_successor": "claim_chain_ref", + "secured_debt": "linked_structures", + "registry_invalidity": "linked_structures", + "land_use_gain": "legal_calculation_object", + "valuation": "legal_calculation_object", + "fraudulent_transfer": "actio_roles", + "preserved_claim": "claim_chain_ref", + "actio_remedy_or_cap": "linked_actio_structures", + "succession_notice_lien_asset_defense": "linked_structures", + "general_legal_effect": "legal_effect_roles", + } + ISSUE_DECISIONS = {"USE_EXISTING_HARD_KEY", "SELECT_ISSUE_FAMILY", "SPLIT_ISSUE_CLUSTER", "BLOCK_REVIEW"} + ASSET_DECISIONS = {"SAME_CLUSTER", "SEPARATE_CLUSTER", "MULTI_ASSET_BUNDLE", "BLOCK_REVIEW"} + TYPE_DECISIONS = {"SELECT_REPRESENTATIVE_TYPE", "PRESERVE_MULTI_ROUTING", "SPLIT_STRUCTURE_SEED", "BLOCK_REVIEW"} + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-legal-effect-les3-final-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result.get("result", {}).get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + text = content[0].get("text", "") + envelope = json.loads(text) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + item = results[0] + if isinstance(item, dict) and item.get("error"): + raise RuntimeError(f"read_docs error for {doc_name}: {item.get('error')}") + raw = item.get("content", "") + else: + raw = envelope + if isinstance(raw, str): + return json.loads(raw) + return raw + + def _read_doc(doc_name: str) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + return _extract_json_from_read_docs_result(result, doc_name) + + def _write_doc(doc_name: str, payload: dict[str, Any]) -> None: + content = json.dumps(payload, ensure_ascii=False, indent=2) + "\n" + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=_next_msg_id()) + + def _preview(raw: str, limit: int = 180) -> str: + return (raw or "").strip().replace("\n", "\\n")[:limit] + + def _extract_json_object(raw: str, label: str) -> dict[str, Any]: + text = (raw or "").strip() + if not text or text.startswith("{{"): + raise ValueError(f"{label}: unresolved or empty payload; preview={_preview(raw)!r}") + start = text.find("{") + if start < 0: + raise ValueError(f"{label}: JSON object start not found; preview={_preview(raw)!r}") + candidate = text[start:].strip() + obj, end = JSON_DECODER.raw_decode(candidate) + tail = candidate[end:].strip() + if tail: + raise ValueError(f"{label}: non-json tail after object; preview={_preview(tail)!r}") + if not isinstance(obj, dict): + raise ValueError(f"{label}: JSON root must be object") + return obj + + def _unwrap(raw: str, label: str) -> dict[str, Any]: + obj = _extract_json_object(raw, label) + if obj.get("type") == "text" and isinstance(obj.get("text"), str): + obj = _extract_json_object(obj["text"], f"{label}.text") + if obj.get("success") is True and isinstance(obj.get("output"), str): + obj = _extract_json_object(obj["output"], f"{label}.output") + return obj + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _strings(value: Any) -> list[str]: + if value is None: + return [] + items = value if isinstance(value, list) else [value] + out: list[str] = [] + for item in items: + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + if value is None: + continue + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + def _make_id(prefix: str, ordinal: int) -> str: + return f"{prefix}-{ordinal:03d}" + + def _load_seed_bundle() -> dict[str, Any]: + try: + outer = _unwrap(SEED_RAW, "SEED_RAW") + root = _dict(outer.get("legal_structure_seed_bundle")) + if root.get("schema_version") == "stage1_legal_structure_seed_bundle.v1": + return root + except Exception: + pass + outer = _read_doc(SEED_FALLBACK_PATH) + root = _dict(outer.get("legal_structure_seed_bundle")) + if root.get("schema_version") != "stage1_legal_structure_seed_bundle.v1": + raise ValueError("legal_structure_seed_bundle schema mismatch") + return root + + def _load_adjudication() -> dict[str, Any]: + outer = _unwrap(ADJUDICATION_RAW, "ADJUDICATION_RAW") + root = _dict(outer.get("legal_structure_exception_adjudication")) + if root.get("schema_version") != "stage1_legal_structure_exception_adjudication.v1": + raise ValueError("legal_structure_exception_adjudication schema mismatch") + if root.get("status") not in {"READY", "READY_NO_EXCEPTIONS", "READY_WITH_BLOCKS"}: + raise ValueError("legal_structure_exception_adjudication status is not accepted") + return root + + def _expected_exceptions(seed_bundle: dict[str, Any]) -> dict[str, dict[str, Any]]: + pack = _dict(seed_bundle.get("legal_structure_exception_pack")) + out: dict[str, dict[str, Any]] = {} + for key in ["issue_cluster_exceptions", "asset_cluster_exceptions", "candidate_structure_type_exceptions"]: + for item in _list(pack.get(key)): + if not isinstance(item, dict) or not item.get("exception_id"): + raise ValueError(f"exception item without exception_id in {key}") + out[str(item["exception_id"])] = item + return out + + def _validate_decisions(seed_bundle: dict[str, Any], adjudication: dict[str, Any]) -> tuple[dict[str, dict[str, Any]], list[dict[str, Any]]]: + expected = _expected_exceptions(seed_bundle) + decisions: dict[str, dict[str, Any]] = {} + blocked = _list(adjudication.get("blocked_review_items")) + if not expected: + if adjudication.get("status") != "READY_NO_EXCEPTIONS": + raise ValueError("no exceptions expected; adjudication status must be READY_NO_EXCEPTIONS") + if _list(adjudication.get("decisions")): + raise ValueError("no exceptions expected; decisions must be empty") + return decisions, [] + for decision in _list(adjudication.get("decisions")): + if not isinstance(decision, dict) or not decision.get("exception_id"): + raise ValueError("decision item missing exception_id") + exception_id = str(decision["exception_id"]) + if exception_id not in expected: + raise ValueError(f"decision references unknown exception_id: {exception_id}") + if exception_id in decisions: + raise ValueError(f"duplicate decision for exception_id: {exception_id}") + _validate_decision_enum(decision) + _validate_decision_subsets(decision, expected[exception_id]) + decisions[exception_id] = decision + missing = sorted(set(expected) - set(decisions)) + if missing: + raise ValueError(f"adjudication missing decisions: {missing}") + blocked_ids = {str(item.get("exception_id")) for item in blocked if isinstance(item, dict)} + for exception_id, decision in decisions.items(): + if decision.get("decision") == "BLOCK_REVIEW" and exception_id not in blocked_ids: + blocked.append({ + "exception_id": exception_id, + "exception_type": decision.get("exception_type"), + "source_bo_ids": _strings(decision.get("source_bo_ids")), + "block_reason": "block_review_decision_without_block_item", + }) + return decisions, blocked + + def _validate_decision_enum(decision: dict[str, Any]) -> None: + exception_type = decision.get("exception_type") + choice = decision.get("decision") + allowed = { + "ambiguous_issue_cluster": ISSUE_DECISIONS, + "ambiguous_asset_cluster": ASSET_DECISIONS, + "ambiguous_candidate_structure_type": TYPE_DECISIONS, + }.get(exception_type) + if allowed is None: + raise ValueError(f"invalid exception_type: {exception_type}") + if choice not in allowed: + raise ValueError(f"invalid decision {choice} for {exception_type}") + + def _validate_decision_subsets(decision: dict[str, Any], expected: dict[str, Any]) -> None: + candidates = set(_strings(expected.get("candidate_values"))) + basis = set(_strings(expected.get("basis_fields"))) + flags = set(_strings(expected.get("conflict_flags"))) + for field in ["selected_values", "preserve_candidate_values"]: + extras = set(_strings(decision.get(field))) - candidates + if extras: + raise ValueError(f"{decision.get('exception_id')}: {field} contains values outside candidate_values: {sorted(extras)}") + selected = decision.get("selected_value") + if selected not in (None, "") and candidates and str(selected) not in candidates: + raise ValueError(f"{decision.get('exception_id')}: selected_value outside candidate_values") + for split in _list(decision.get("split_plan")): + if isinstance(split, dict): + extras = set(_strings(split.get("candidate_values"))) - candidates + if extras: + raise ValueError(f"{decision.get('exception_id')}: split_plan candidate outside candidate_values") + if basis: + extras = set(_strings(decision.get("basis_fields_used"))) - basis + if extras: + raise ValueError(f"{decision.get('exception_id')}: basis_fields_used outside basis_fields") + if flags: + extras = set(_strings(decision.get("conflict_flags_acknowledged"))) - flags + if extras: + raise ValueError(f"{decision.get('exception_id')}: conflict_flags_acknowledged outside conflict_flags") + + def _decisions_for_bo(decisions: dict[str, dict[str, Any]], bo_id: str, exception_type: str) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for decision in decisions.values(): + if decision.get("exception_type") == exception_type and bo_id in _strings(decision.get("source_bo_ids")): + out.append(decision) + return out + + def _issue_ids_for_item(item: dict[str, Any], decisions: dict[str, dict[str, Any]], issue_key_ids: dict[str, str]) -> list[str]: + bo_id = _strings(item.get("source_bo_ids"))[0] if _strings(item.get("source_bo_ids")) else "" + issue_decisions = _decisions_for_bo(decisions, bo_id, "ambiguous_issue_cluster") + keys: list[str] = [] + for decision in issue_decisions: + choice = decision.get("decision") + if choice == "BLOCK_REVIEW": + continue + if choice == "SPLIT_ISSUE_CLUSTER": + for split in _list(decision.get("split_plan")): + if isinstance(split, dict): + label = split.get("split_label") or "|".join(_strings(split.get("candidate_values"))) + if label: + keys.append(f"split_issue:{label}") + else: + value = decision.get("selected_value") or (_strings(decision.get("selected_values"))[0] if _strings(decision.get("selected_values")) else None) + if value: + keys.append(f"adjudicated_issue:{value}") + if not keys: + keys = _strings(item.get("issue_cluster_ids")) + out: list[str] = [] + for key in keys: + if key not in issue_key_ids: + issue_key_ids[key] = _make_id("IC", len(issue_key_ids) + 1) + out.append(issue_key_ids[key]) + return _dedupe(out) + + def _asset_ids_for_item(item: dict[str, Any], decisions: dict[str, dict[str, Any]], asset_key_ids: dict[str, str]) -> tuple[list[str], list[str]]: + bo_id = _strings(item.get("source_bo_ids"))[0] if _strings(item.get("source_bo_ids")) else "" + asset_decisions = _decisions_for_bo(decisions, bo_id, "ambiguous_asset_cluster") + keys = _strings(item.get("asset_cluster_ids")) + bundle_refs = _strings(item.get("multi_asset_bundle_refs")) + for decision in asset_decisions: + choice = decision.get("decision") + if choice == "BLOCK_REVIEW": + continue + if choice == "SAME_CLUSTER": + value = decision.get("selected_value") or "|".join(_strings(decision.get("selected_values")) or _strings(decision.get("preserve_candidate_values"))) + if value: + keys = [f"same_asset:{value}"] + elif choice == "SEPARATE_CLUSTER": + vals = _strings(decision.get("selected_values")) or _strings(decision.get("preserve_candidate_values")) or _strings(decision.get("candidate_values")) + keys = [f"separate_asset:{value}" for value in vals] or keys + elif choice == "MULTI_ASSET_BUNDLE": + value = decision.get("selected_value") or f"bundle:{bo_id}" + if value not in bundle_refs: + bundle_refs.append(str(value)) + out: list[str] = [] + for key in keys: + if key not in asset_key_ids: + asset_key_ids[key] = _make_id("AC", len(asset_key_ids) + 1) + out.append(asset_key_ids[key]) + return _dedupe(out), _dedupe(bundle_refs) + + def _type_plan_for_item(item: dict[str, Any], decisions: dict[str, dict[str, Any]]) -> list[dict[str, Any]]: + bo_id = _strings(item.get("source_bo_ids"))[0] if _strings(item.get("source_bo_ids")) else "" + original_types = _strings(item.get("candidate_structure_types")) or _strings(item.get("representative_candidate_structure_type")) or ["general_legal_effect"] + representative = item.get("representative_candidate_structure_type") or original_types[0] + type_decisions = _decisions_for_bo(decisions, bo_id, "ambiguous_candidate_structure_type") + if not type_decisions: + return [{"candidate_structure_type": representative, "candidate_structure_types": original_types, "split_label": None}] + plans: list[dict[str, Any]] = [] + for decision in type_decisions: + choice = decision.get("decision") + if choice == "BLOCK_REVIEW": + plans.append({"candidate_structure_type": representative, "candidate_structure_types": original_types, "split_label": None}) + elif choice == "SELECT_REPRESENTATIVE_TYPE": + selected = decision.get("selected_value") or (_strings(decision.get("selected_values"))[0] if _strings(decision.get("selected_values")) else representative) + plans.append({"candidate_structure_type": selected, "candidate_structure_types": _dedupe([selected] + original_types), "split_label": None}) + elif choice == "PRESERVE_MULTI_ROUTING": + vals = _strings(decision.get("selected_values")) or _strings(decision.get("preserve_candidate_values")) or original_types + selected = decision.get("selected_value") or vals[0] + plans.append({"candidate_structure_type": selected, "candidate_structure_types": vals, "split_label": None}) + elif choice == "SPLIT_STRUCTURE_SEED": + for split in _list(decision.get("split_plan")): + if isinstance(split, dict): + vals = _strings(split.get("candidate_values")) or original_types + plans.append({ + "candidate_structure_type": vals[0], + "candidate_structure_types": vals, + "split_label": split.get("split_label"), + }) + return plans or [{"candidate_structure_type": representative, "candidate_structure_types": original_types, "split_label": None}] + + def _legal_roles(types: list[str]) -> list[str]: + return _dedupe([STRUCTURE_TYPE_ROLE.get(typ, "legal_effect_roles") for typ in types]) + + def _basis(item: dict[str, Any], types: list[str]) -> dict[str, Any]: + signal_files: list[str] = [] + if _strings(item.get("linked_actio_signal_ids")): + signal_files.append("actio_case_signals.json") + if _strings(item.get("linked_claim_group_ids")) or _strings(item.get("linked_liability_group_ids")): + signal_files.append("case_liability_signals.json") + if types: + signal_files.append("legal_effect_signals.json") + return { + "primary_fields": _strings(item.get("basis_fields")), + "source_signal_files": _dedupe(signal_files), + "source_evidence_indexes": _strings(item.get("evidence_indexes")), + } + + def _structure_label(candidate_type: str, source_bo_ids: list[str], split_label: Any) -> str: + label = candidate_type.replace("_", " ") + suffix = f" / {split_label}" if split_label else "" + return f"{label} structure for {', '.join(source_bo_ids)}{suffix}" + + def _build_structures(seed_bundle: dict[str, Any], decisions: dict[str, dict[str, Any]], blocked: list[dict[str, Any]]) -> list[dict[str, Any]]: + issue_key_ids: dict[str, str] = {} + asset_key_ids: dict[str, str] = {} + structures: list[dict[str, Any]] = [] + blocked_bo_ids = {bo_id for item in blocked if isinstance(item, dict) for bo_id in _strings(item.get("source_bo_ids"))} + for item in _list(seed_bundle.get("structure_seed_items")): + if not isinstance(item, dict): + continue + source_bo_ids = _strings(item.get("source_bo_ids")) + if not source_bo_ids: + continue + issue_ids = _issue_ids_for_item(item, decisions, issue_key_ids) + asset_ids, bundle_refs = _asset_ids_for_item(item, decisions, asset_key_ids) + type_plans = _type_plan_for_item(item, decisions) + for plan in type_plans: + structure_id = _make_id("LES", len(structures) + 1) + types = _strings(plan.get("candidate_structure_types")) or ["general_legal_effect"] + review_required = bool(item.get("review_required") or any(bo in blocked_bo_ids for bo in source_bo_ids)) + if plan.get("split_label"): + review_required = True + structures.append({ + "structure_id": structure_id, + "candidate_structure_type": str(plan.get("candidate_structure_type") or types[0]), + "structure_label": _structure_label(str(plan.get("candidate_structure_type") or types[0]), source_bo_ids, plan.get("split_label")), + "source_bo_ids": source_bo_ids, + "issue_cluster_ids": issue_ids, + "asset_cluster_ids": asset_ids, + "evidence_indexes": _strings(item.get("evidence_indexes")), + "linked_claim_group_ids": _strings(item.get("linked_claim_group_ids")), + "linked_liability_group_ids": _strings(item.get("linked_liability_group_ids")), + "linked_actio_signal_ids": _strings(item.get("linked_actio_signal_ids")), + "legal_effect_roles": _legal_roles(types), + "basis": _basis(item, types), + "review_required": review_required, + "routing_audit": { + "source_seed_id": item.get("seed_id"), + "candidate_structure_types": types, + "multi_asset_bundle_refs": bundle_refs, + }, + }) + return structures + + def _add_index_entry(index: dict[str, Any], key: str, field: str, values: list[str]) -> None: + row = index.setdefault(key, {}) + row[field] = _dedupe(_strings(row.get(field)) + values) + + def _build_structure_index(structures: list[dict[str, Any]], seed_bundle: dict[str, Any]) -> dict[str, Any]: + by_issue: dict[str, Any] = {} + by_asset: dict[str, Any] = {} + by_evidence: dict[str, Any] = {} + by_type: dict[str, Any] = {} + by_bo: dict[str, Any] = {} + issue_seed_map = _dict(seed_bundle.get("issue_cluster_seed_map")) + asset_seed_map = _dict(seed_bundle.get("asset_cluster_seed_map")) + evidence_seed = _dict(seed_bundle.get("evidence_index_seed")) + + for structure in structures: + sid = structure["structure_id"] + basis = _dict(structure.get("basis")) + routing_audit = _dict(structure.get("routing_audit")) + for bo_id in _strings(structure.get("source_bo_ids")): + row = by_bo.setdefault(bo_id, { + "bo_id": bo_id, + "structure_ids": [], + "structure_labels": [], + "issue_cluster_ids": [], + "asset_cluster_ids": [], + "evidence_indexes": [], + "candidate_structure_types": [], + "claim_group_ids": [], + "liability_group_ids": [], + "related_actio_signal_ids": [], + "legal_effect_roles": [], + "basis_fields": [], + "multi_asset_bundle_refs": [], + "review_required": False, + }) + row["structure_ids"] = _dedupe(row["structure_ids"] + [sid]) + row["structure_labels"] = _dedupe(row["structure_labels"] + _strings(structure.get("structure_label"))) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + structure["issue_cluster_ids"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + structure["asset_cluster_ids"]) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + structure["evidence_indexes"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + [structure["candidate_structure_type"]]) + row["claim_group_ids"] = _dedupe(row["claim_group_ids"] + structure["linked_claim_group_ids"]) + row["liability_group_ids"] = _dedupe(row["liability_group_ids"] + structure["linked_liability_group_ids"]) + row["related_actio_signal_ids"] = _dedupe(row["related_actio_signal_ids"] + structure["linked_actio_signal_ids"]) + row["legal_effect_roles"] = _dedupe(row["legal_effect_roles"] + structure["legal_effect_roles"]) + row["basis_fields"] = _dedupe(row["basis_fields"] + _strings(basis.get("primary_fields"))) + row["multi_asset_bundle_refs"] = _dedupe(row["multi_asset_bundle_refs"] + _strings(routing_audit.get("multi_asset_bundle_refs"))) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + for issue_id in _strings(structure.get("issue_cluster_ids")): + seed = _dict(issue_seed_map.get(issue_id)) + row = by_issue.setdefault(issue_id, { + "issue_cluster_id": issue_id, + "issue_type": seed.get("issue_type"), + "issue_label": seed.get("issue_key") or issue_id, + "source_bo_ids": [], + "candidate_structure_types": [], + "claim_group_ids": [], + "liability_group_ids": [], + "evidence_indexes": [], + "asset_cluster_ids": [], + "structure_ids": [], + "basis_fields": [], + "review_required": False, + }) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + structure["source_bo_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + [structure["candidate_structure_type"]]) + row["claim_group_ids"] = _dedupe(row["claim_group_ids"] + structure["linked_claim_group_ids"]) + row["liability_group_ids"] = _dedupe(row["liability_group_ids"] + structure["linked_liability_group_ids"]) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + structure["evidence_indexes"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + structure["asset_cluster_ids"]) + row["structure_ids"] = _dedupe(row["structure_ids"] + [sid]) + row["basis_fields"] = _dedupe(row["basis_fields"] + _strings(structure.get("basis", {}).get("primary_fields"))) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + for asset_id in _strings(structure.get("asset_cluster_ids")): + seed = _dict(asset_seed_map.get(asset_id)) + row = by_asset.setdefault(asset_id, { + "asset_cluster_id": asset_id, + "canonical_asset_label": seed.get("canonical_asset_label"), + "asset_kind": seed.get("asset_kind"), + "member_asset_mentions": _strings(seed.get("member_asset_mentions")), + "source_bo_ids": [], + "evidence_indexes": [], + "issue_cluster_ids": [], + "candidate_structure_types": [], + "structure_ids": [], + "basis_fields": _strings(seed.get("basis_fields")), + "review_required": bool(seed.get("review_required")), + }) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + structure["source_bo_ids"]) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + structure["evidence_indexes"]) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + structure["issue_cluster_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + [structure["candidate_structure_type"]]) + row["structure_ids"] = _dedupe(row["structure_ids"] + [sid]) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + for evidence_index in _strings(structure.get("evidence_indexes")): + seed = _dict(evidence_seed.get(evidence_index)) + row = by_evidence.setdefault(evidence_index, { + "evidence_index": evidence_index, + "source_title": seed.get("source_title"), + "priority_class": seed.get("priority_class"), + "authentication_status": seed.get("authentication_status"), + "source_bo_ids": [], + "structure_ids": [], + "issue_cluster_ids": [], + "asset_cluster_ids": [], + "candidate_structure_types": [], + "related_event_candidate_ids": [], + "related_signal_refs": [], + "support_role": "primary", + "gap_flags": _strings(seed.get("gap_flags")), + }) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + structure["source_bo_ids"]) + row["structure_ids"] = _dedupe(row["structure_ids"] + [sid]) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + structure["issue_cluster_ids"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + structure["asset_cluster_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + [structure["candidate_structure_type"]]) + row["related_signal_refs"] = _dedupe(row["related_signal_refs"] + structure["linked_actio_signal_ids"] + structure["linked_claim_group_ids"] + structure["linked_liability_group_ids"]) + typ = structure["candidate_structure_type"] + row = by_type.setdefault(typ, { + "candidate_structure_type": typ, + "structure_ids": [], + "source_bo_ids": [], + "issue_cluster_ids": [], + "asset_cluster_ids": [], + "evidence_indexes": [], + "claim_group_ids": [], + "liability_group_ids": [], + "related_actio_signal_ids": [], + "downstream_fact_roles": [], + "basis_fields": [], + "review_required": False, + }) + row["structure_ids"] = _dedupe(row["structure_ids"] + [sid]) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + structure["source_bo_ids"]) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + structure["issue_cluster_ids"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + structure["asset_cluster_ids"]) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + structure["evidence_indexes"]) + row["claim_group_ids"] = _dedupe(row["claim_group_ids"] + structure["linked_claim_group_ids"]) + row["liability_group_ids"] = _dedupe(row["liability_group_ids"] + structure["linked_liability_group_ids"]) + row["related_actio_signal_ids"] = _dedupe(row["related_actio_signal_ids"] + structure["linked_actio_signal_ids"]) + row["downstream_fact_roles"] = _dedupe(row["downstream_fact_roles"] + structure["legal_effect_roles"]) + row["basis_fields"] = _dedupe(row["basis_fields"] + _strings(structure.get("basis", {}).get("primary_fields"))) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + + return { + "by_bo_id": by_bo, + "by_issue_cluster_id": by_issue, + "by_asset_cluster_id": by_asset, + "by_evidence_index": by_evidence, + "by_candidate_structure_type": by_type, + } + + def _quality_gate(structures: list[dict[str, Any]], seed_bundle: dict[str, Any], blocked: list[dict[str, Any]]) -> dict[str, Any]: + seed_items = [item for item in _list(seed_bundle.get("structure_seed_items")) if isinstance(item, dict)] + seed_bo_ids = sorted({bo for item in seed_items for bo in _strings(item.get("source_bo_ids"))}) + covered_bo_ids = sorted({bo for structure in structures for bo in _strings(structure.get("source_bo_ids"))}) + seed_evidence = sorted({ev for item in seed_items for ev in _strings(item.get("evidence_indexes"))}) + covered_evidence = sorted({ev for structure in structures for ev in _strings(structure.get("evidence_indexes"))}) + review_queue = [] + for structure in structures: + if structure.get("review_required"): + review_queue.append({ + "review_type": "structure_review_required", + "structure_id": structure["structure_id"], + "source_bo_ids": structure["source_bo_ids"], + }) + for item in blocked: + if isinstance(item, dict): + review_queue.append({ + "review_type": "blocked_exception", + "exception_id": item.get("exception_id"), + "exception_type": item.get("exception_type"), + "source_bo_ids": _strings(item.get("source_bo_ids")), + "block_reason": item.get("block_reason"), + }) + return { + "bo_coverage": { + "source_bo_count": len(seed_bo_ids), + "covered_bo_count": len(covered_bo_ids), + "missing_bo_ids": sorted(set(seed_bo_ids) - set(covered_bo_ids)), + "coverage_pass": set(seed_bo_ids) == set(covered_bo_ids), + }, + "evidence_coverage": { + "source_evidence_count": len(seed_evidence), + "covered_evidence_count": len(covered_evidence), + "missing_evidence_indexes": sorted(set(seed_evidence) - set(covered_evidence)), + "coverage_pass": set(seed_evidence).issubset(set(covered_evidence)), + }, + "review_queue": review_queue, + } + + def _validate_payload(payload: dict[str, Any]) -> None: + if payload.get("schema_version") != "stage1_legal_effect_structures.v1": + raise ValueError("final payload schema_version mismatch") + if payload.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("final payload status invalid") + structures = _list(payload.get("legal_effect_structures")) + index = _dict(payload.get("structure_index")) + for key in ["by_bo_id", "by_issue_cluster_id", "by_asset_cluster_id", "by_evidence_index", "by_candidate_structure_type"]: + if not isinstance(index.get(key), dict): + raise ValueError(f"structure_index.{key} must be object") + structure_ids = {str(item.get("structure_id")) for item in structures if isinstance(item, dict)} + if len(structure_ids) != len(structures): + raise ValueError("structure_id values must be unique") + for item in structures: + if not isinstance(item, dict): + raise ValueError("legal_effect_structures item must be object") + for field in [ + "structure_id", "candidate_structure_type", "structure_label", "source_bo_ids", + "issue_cluster_ids", "asset_cluster_ids", "evidence_indexes", "linked_claim_group_ids", + "linked_liability_group_ids", "linked_actio_signal_ids", "legal_effect_roles", + "basis", "review_required", + ]: + if field not in item: + raise ValueError(f"structure item missing {field}") + for bo_id in _strings(item.get("source_bo_ids")): + bo_row = _dict(index["by_bo_id"].get(bo_id)) + if not bo_row: + raise ValueError(f"missing BO index for {bo_id}") + if item.get("structure_id") not in _strings(bo_row.get("structure_ids")): + raise ValueError(f"BO index for {bo_id} does not reference {item.get('structure_id')}") + for issue_id in _strings(item.get("issue_cluster_ids")): + if issue_id not in index["by_issue_cluster_id"]: + raise ValueError(f"missing issue index for {issue_id}") + for asset_id in _strings(item.get("asset_cluster_ids")): + if asset_id not in index["by_asset_cluster_id"]: + raise ValueError(f"missing asset index for {asset_id}") + for evidence_index in _strings(item.get("evidence_indexes")): + if evidence_index not in index["by_evidence_index"]: + raise ValueError(f"missing evidence index for {evidence_index}") + if item.get("candidate_structure_type") not in index["by_candidate_structure_type"]: + raise ValueError(f"missing candidate type index for {item.get('candidate_structure_type')}") + gate = _dict(payload.get("quality_gate")) + if not _dict(gate.get("bo_coverage")).get("coverage_pass"): + raise ValueError("BO coverage failed") + if not _dict(gate.get("evidence_coverage")).get("coverage_pass"): + raise ValueError("evidence coverage failed") + + def main() -> None: + try: + _init_mcp_session() + seed_bundle = _load_seed_bundle() + if seed_bundle.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("legal_structure_seed_bundle status invalid") + adjudication = _load_adjudication() + decisions, blocked = _validate_decisions(seed_bundle, adjudication) + structures = _build_structures(seed_bundle, decisions, blocked) + if not structures: + raise ValueError("no legal effect structures generated") + structure_index = _build_structure_index(structures, seed_bundle) + quality_gate = _quality_gate(structures, seed_bundle, blocked) + status = "READY_WITH_REVIEW" if blocked or quality_gate["review_queue"] else "READY" + payload = { + "schema_version": "stage1_legal_effect_structures.v1", + "status": status, + "legal_effect_structures": structures, + "structure_index": structure_index, + "quality_gate": quality_gate, + } + _validate_payload(payload) + _write_doc(TARGET_NAME, payload) + reread = _read_doc(TARGET_NAME) + if not isinstance(reread, dict) or reread.get("schema_version") != "stage1_legal_effect_structures.v1": + raise ValueError("post-write reread validation failed") + print(json.dumps({ + "legal_effect_structures_writer": { + "schema_version": "stage1_legal_effect_structures_writer.v1", + "status": "READY", + "written_file": TARGET_NAME, + "payload_status": status, + "structure_count": len(structures), + "review_queue_count": len(quality_gate["review_queue"]), + } + }, ensure_ascii=False)) + except Exception as exc: + print(json.dumps({ + "legal_effect_structures_writer": { + "schema_version": "stage1_legal_effect_structures_writer.v1", + "status": "FAILED", + "target_file": TARGET_NAME, + "failure_reason": str(exc), + } + }, ensure_ascii=False)) + sys.exit(0) + + if __name__ == "__main__": + main() + + diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_4_Claude_v3.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_4_Claude_v3.yml new file mode 100644 index 00000000..8e5d71a1 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_4_Claude_v3.yml @@ -0,0 +1,1963 @@ +--- +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_4 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Fact Ledger Generation + version: v3.0-claude + Stages: + - name: stage1_fact_ledger_generation + description: BO, Legal Effect 사용해서 Fact_Ledger_base.json 생성 + tools: + mcpServers: + localdocs: + type: streamable-http + url: http://mcp-localdocs:8012/mcp + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + task_procedure: + IN: + nexts: + - Task_F0_fact_ledger_reducer + wait_until: [] + Task_F0_fact_ledger_reducer: + nexts: + - Task_F1_fact_exception_adjudicator + wait_until: + - IN + Task_F1_fact_exception_adjudicator: + nexts: + - Task_F2_final_fact_ledger_gate_and_writer + wait_until: + - Task_F0_fact_ledger_reducer + Task_F2_final_fact_ledger_gate_and_writer: + nexts: + - OUT + wait_until: + - Task_F1_fact_exception_adjudicator + OUT: + nexts: [] + wait_until: + - Task_F2_final_fact_ledger_gate_and_writer + + tasks: + - task_name: Task_F0_fact_ledger_reducer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + import sys + from copy import deepcopy + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + # X-4: evidence_all.json / evidence_event_candidates.json / client_goal.json은 기여 0 실측으로 제거. + INPUT_FILES = { + "bo": "BO.json", + "legal_effect_structures": "legal_effect_structures.json", + "actio_case_signals": "actio_case_signals.json", + "case_liability_signals": "case_liability_signals.json", + "legal_effect_signals": "legal_effect_signals.json", + "evidence_indexed": "evidence_indexed.json", + } + REMOVED_INPUTS = ["evidence_all.json", "evidence_event_candidates.json", "client_goal.json"] + CANDIDATE_OUT_PATH = "stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json" + PACK_OUT_PATH = "stage1_tmp/fact_ledger/fact_exception_pack.json" + MAX_TEXT = 900 + MAX_SIGNAL_ITEMS_PER_BO = 12 + # X-3 방어 상한 (자동 차단 금지 — 초과분은 exception_id 유지한 채 deterministic review로 라우팅) + PACK_MAX_ITEMS = 30 + PACK_MAX_BYTES = 40000 + PACK_ITEM_CHAR_LIMIT = 2000 + FINAL_FIELDS = [ + "fact_id", "source_bo_id", "type", "date", "parties", "object_spec", "amount", "action", + "evidence_refs", "credibility", "legal_centrality", "proof_strength", "legal_effect_roles", + "claim_chain_ref", "linked_structures", "must_not_drop_in_claim_types", "must_consider", + "state_context", "money_claim_effect", "commercial_successor_effect", "actio_roles", + "linked_actio_structures", "downstream_module_candidates", "skeleton_validation_codes", + "derived_fact", "derivation_basis", "legal_calculation_object", + ] + STRUCTURE_TYPE_TO_MODULE = { + "money_claim": "money_claim", + "commercial_successor": "commercial_successor", + "secured_debt": "secured_property", + "registry_invalidity": "registry_invalidity", + "land_use_gain": "land_use_gain", + "valuation": "valuation", + "fraudulent_transfer": "actio", + "preserved_claim": "actio", + "succession_notice_lien_asset_defense": "succession_notice_lien_asset_defense", + "general_legal_effect": "legal_effect", + } + STRUCTURE_TYPE_TO_ROLE = { + "money_claim": "claim_chain_ref", + "commercial_successor": "claim_chain_ref", + "secured_debt": "linked_structures", + "registry_invalidity": "linked_structures", + "land_use_gain": "legal_calculation_object", + "valuation": "legal_calculation_object", + "fraudulent_transfer": "actio_roles", + "preserved_claim": "claim_chain_ref", + "succession_notice_lien_asset_defense": "linked_structures", + "general_legal_effect": "legal_effect_roles", + } + PACK_EXCEPTION_TYPES = [ + "legal_calculation_required", + "unresolved_asset_cluster_projection", + "unresolved_structure_role_projection", + ] + REVIEW_CODE_BY_EXCEPTION_TYPE = { + "legal_calculation_required": "CALCULATION_BASIS_INCOMPLETE_REVIEW", + "unresolved_asset_cluster_projection": "ASSET_CLUSTER_PROJECTION_GAP_REVIEW", + "unresolved_structure_role_projection": "STRUCTURE_ROLE_PROJECTION_GAP_REVIEW", + } + DOMAIN_PAYLOAD_KEEP_KEYS = [ + "money_claim_details", "possession_state", "lien_state", + "notice_lifecycle", "asset_alias", "fractional_effect_events", + ] + POSSESSION_INCOMPATIBLE_CAUSE_PAIRS = [ + ("유치권행사중", "무권원주장"), + ] + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-fact-ledger-f0-reducer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + + def _lenient_json(raw: str, label: str) -> Any: + """SKILL §6.4/§6.7: strict 실패 시 raw_decode fallback (envelope 계층 포함 — X-9).""" + text = (raw or "").strip() + if not text: + raise RuntimeError(f"Empty content for {label}") + try: + return json.loads(text) + except json.JSONDecodeError: + start = text.find("{") + alt = text.find("[") + if alt >= 0 and (start < 0 or alt < start): + start = alt + if start < 0: + raise RuntimeError(f"Content of {label} is not valid JSON: {text[:300]}") + try: + obj, _ = JSON_DECODER.raw_decode(text[start:]) + return obj + except json.JSONDecodeError as exc: + raise RuntimeError(f"Content of {label} is not valid JSON: {text[:300]}") from exc + + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result["result"].get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + text = content[0].get("text", "") + envelope = _lenient_json(text, doc_name) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + first = results[0] + if isinstance(first, dict) and first.get("error"): + raise RuntimeError(f"{doc_name}: {first['error']}") + inner = first.get("content", "") if isinstance(first, dict) else "" + if inner in (None, ""): + raise RuntimeError(f"Empty content for {doc_name}") + if isinstance(inner, str): + return _lenient_json(inner, doc_name) + return inner + return envelope + + + def _read_doc(doc_name: str) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + return _extract_json_from_read_docs_result(result, doc_name) + + + def _write_text_doc(doc_name: str, content: str) -> None: + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=_next_msg_id()) + + + def _read_doc_raw_text(doc_name: str) -> str: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + content = result["result"].get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + envelope = _lenient_json(content[0].get("text", ""), doc_name) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if results and isinstance(results[0], dict): + raw = results[0].get("content") + if isinstance(raw, str): + return raw + raise RuntimeError(f"raw text unavailable for {doc_name}") + + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _text(value: Any, limit: int = MAX_TEXT) -> str: + if value is None: + return "" + if isinstance(value, str): + s = value + else: + s = json.dumps(value, ensure_ascii=False, sort_keys=True) + s = re.sub(r"\s+", " ", s).strip() + return s[:limit] + + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + if value is None: + return out + if isinstance(value, (str, int, float)): + s = str(value).strip() + return [s] if s else [] + if isinstance(value, list): + for item in value: + out.extend(_strings(item)) + return _dedupe(out) + + + def _dedupe(values: list[str]) -> list[str]: + seen: set[str] = set() + out: list[str] = [] + for value in values: + if value and value not in seen: + seen.add(value) + out.append(value) + return out + + + def _first_key(d: dict[str, Any], keys: list[str]) -> Any: + for key in keys: + if key in d and d.get(key) not in (None, "", [], {}): + return d.get(key) + return None + + + def _first_value(*values: Any) -> Any: + for value in values: + if value not in (None, "", [], {}): + return value + return None + + + def _items_from_doc(doc: Any, candidate_keys: list[str]) -> list[dict[str, Any]]: + if isinstance(doc, list): + return [x for x in doc if isinstance(x, dict)] + if isinstance(doc, dict): + for key in candidate_keys: + if isinstance(doc.get(key), list): + return [x for x in doc[key] if isinstance(x, dict)] + if any(k in doc for k in ("BO_ID", "bo_id", "id", "fact_id", "evidence_index", "structure_id")): + return [doc] + return [] + + + def _bo_id(bo: dict[str, Any]) -> str: + return str(_first_key(bo, ["BO_ID", "bo_id", "id", "canonical_bo_id", "bh_id"]) or "").strip() + + + def _bo_items(bo_doc: Any) -> list[dict[str, Any]]: + items = _items_from_doc(bo_doc, ["BO", "bo", "items", "bo_items", "canonical_bo_items", "body"]) + valid = [item for item in items if _bo_id(item)] + if not valid: + raise ValueError("BO.json must contain BO items with BO_ID/id") + return valid + + + def _evidence_refs_from_bo(bo: dict[str, Any]) -> list[str]: + refs: list[str] = [] + for key in ("source_evidence_indexes", "evidence_refs", "Evidence", "EvidenceIndexes", "related_evidence_indexes"): + refs.extend(_strings(bo.get(key))) + core = _as_dict(bo.get("core_field_base")) + refs.extend(_strings(core.get("source_evidence_indexes"))) + return _dedupe(refs) + + + def _compact_bo(bo: dict[str, Any]) -> dict[str, Any]: + core = _as_dict(bo.get("core_field_base")) + return { + "bo_id": _bo_id(bo), + "bo_type": _first_key(bo, ["BOType", "bo_type", "type"]), + "action": _first_key(bo, ["Action", "action"]), + "action_type": _first_key(bo, ["ActionType", "action_type"]), + "juristic_act": _first_key(bo, ["JuristicAct", "juristic_act"]), + "reason": _first_key(bo, ["Reason", "reason"]), + "prior_act": _first_key(bo, ["PriorAct", "prior_act"]), + "legal_keywords": _strings(bo.get("Legal_Keywords")), + "amount": _first_key(bo, ["amount", "proposed_amount", "Amount"]), + "date": _first_key(bo, ["date", "proposed_date", "Date"]), + "core_field_base": { + "BehaviorTime": _first_key(core, ["BehaviorTime", "behavior_time", "date", "Date"]), + "Performer": _first_key(core, ["Performer", "Actor", "actor", "performer"]), + "Subject": _first_key(core, ["Subject", "Counterparty", "subject", "counterparty"]), + "Object": _first_key(core, ["Object", "Property", "Asset", "object", "asset"]), + "Amount": _first_key(core, ["Amount", "amount", "Value", "value"]), + "Action": _first_key(core, ["Action", "action"]), + }, + "source_evidence_indexes": _evidence_refs_from_bo(bo), + "domain_payload_compact": { + k: v + for k, v in _as_dict(_as_dict(bo.get("extensions")).get("domain_payload")).items() + if k in DOMAIN_PAYLOAD_KEEP_KEYS and v not in (None, {}, []) + }, + } + + + def _validate_legal_effect_structures(doc: Any, bo_ids: set[str]) -> tuple[list[dict[str, Any]], dict[str, Any], list[str]]: + warnings: list[str] = [] + if not isinstance(doc, dict): + raise ValueError("legal_effect_structures.json root must be object") + if doc.get("schema_version") != "stage1_legal_effect_structures.v1": + raise ValueError("legal_effect_structures.json schema_version mismatch") + if doc.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("legal_effect_structures.json status must be READY or READY_WITH_REVIEW") + structures = _as_list(doc.get("legal_effect_structures")) + index = _as_dict(doc.get("structure_index")) + if not isinstance(index.get("by_bo_id"), dict): + raise ValueError("legal_effect_structures.structure_index.by_bo_id must be object") + for key in ["by_bo_id", "by_issue_cluster_id", "by_asset_cluster_id", "by_evidence_index", "by_candidate_structure_type"]: + if not isinstance(index.get(key), dict): + raise ValueError(f"legal_effect_structures.structure_index.{key} must be object") + structure_ids = {str(s.get("structure_id")) for s in structures if isinstance(s, dict) and s.get("structure_id")} + for structure in structures: + if not isinstance(structure, dict): + continue + sid = str(structure.get("structure_id") or "") + for bo_id in _strings(structure.get("source_bo_ids")): + if bo_id not in bo_ids: + warnings.append(f"STRUCTURE_SOURCE_BO_NOT_IN_BO_JSON:{sid}:{bo_id}") + bo_row = _as_dict(index["by_bo_id"].get(bo_id)) + if sid and sid not in _strings(bo_row.get("structure_ids")): + warnings.append(f"BY_BO_ID_MISSING_STRUCTURE_REF:{bo_id}:{sid}") + for bo_id, row in index["by_bo_id"].items(): + if bo_id not in bo_ids: + warnings.append(f"BY_BO_ID_UNKNOWN_BO:{bo_id}") + for sid in _strings(_as_dict(row).get("structure_ids")): + if sid not in structure_ids: + warnings.append(f"BY_BO_ID_UNKNOWN_STRUCTURE:{bo_id}:{sid}") + return structures, index, warnings + + + def _compact_structure(structure: dict[str, Any]) -> dict[str, Any]: + return { + "structure_id": structure.get("structure_id"), + "candidate_structure_type": structure.get("candidate_structure_type"), + "structure_label": structure.get("structure_label"), + "source_bo_ids": _strings(structure.get("source_bo_ids")), + "issue_cluster_ids": _strings(structure.get("issue_cluster_ids")), + "asset_cluster_ids": _strings(structure.get("asset_cluster_ids")), + "evidence_indexes": _strings(structure.get("evidence_indexes")), + "linked_claim_group_ids": _strings(structure.get("linked_claim_group_ids")), + "linked_liability_group_ids": _strings(structure.get("linked_liability_group_ids")), + "linked_actio_signal_ids": _strings(structure.get("linked_actio_signal_ids")), + "legal_effect_roles": _strings(structure.get("legal_effect_roles")), + "review_required": bool(structure.get("review_required")), + } + + + def _evidence_id(record: dict[str, Any]) -> str: + return str(_first_key(record, ["evidence_index", "evidence_id", "id", "index", "EvidenceIndex", "doc_id"]) or "").strip() + + + def _flatten_records(value: Any, depth: int = 0) -> list[dict[str, Any]]: + if depth > 3: + return [] + if isinstance(value, list): + out: list[dict[str, Any]] = [] + for item in value: + out.extend(_flatten_records(item, depth + 1)) + return out + if isinstance(value, dict): + if any(k in value for k in ("evidence_index", "evidence_id", "id", "related_bo_ids", "source_bo_ids", "signal_id", "route_id")): + return [value] + out: list[dict[str, Any]] = [] + for child in value.values(): + if isinstance(child, (list, dict)): + out.extend(_flatten_records(child, depth + 1)) + return out + return [] + + + def _excerpt_from_indexed(record: dict[str, Any]) -> str: + """X-8: evidence_indexed.json의 실제 필드(key_facts 우선, key_dates/key_amounts 보조)로 발췌 실질화. + 기존 키(excerpt/summary/content/text/body)는 fallback으로 유지.""" + key_facts = _strings(record.get("key_facts")) + if key_facts: + return _text(" / ".join(key_facts)) + parts: list[str] = [] + key_dates = _strings(record.get("key_dates")) + key_amounts = _strings(record.get("key_amounts")) + if key_dates: + parts.append("dates: " + " / ".join(key_dates)) + if key_amounts: + parts.append("amounts: " + " / ".join(key_amounts)) + if parts: + return _text("; ".join(parts)) + return _text(_first_key(record, ["excerpt", "summary", "content", "text", "body"])) + + + def _build_evidence_authority(evidence_indexed: Any) -> dict[str, Any]: + authority: dict[str, Any] = {} + for record in _flatten_records(evidence_indexed): + eid = _evidence_id(record) + if not eid: + continue + authority[eid] = { + "evidence_index": eid, + "title": _first_key(record, ["title", "Title", "name", "doc_title"]), + "excerpt": _excerpt_from_indexed(record), + "priority": _first_key(record, ["priority", "Priority", "weight"]), + "authentication": _first_key(record, ["authentication", "auth", "source_authentication"]), + "source": "evidence_indexed.json", + } + return authority + + + def _collect_bo_refs(value: Any, bo_ids: set[str]) -> list[str]: + refs: list[str] = [] + if isinstance(value, str): + if value in bo_ids: + refs.append(value) + elif isinstance(value, list): + for item in value: + refs.extend(_collect_bo_refs(item, bo_ids)) + elif isinstance(value, dict): + for key in ("related_bo_ids", "source_bo_ids", "bo_ids", "BO_ID", "bo_id", "source_bo_id"): + refs.extend([x for x in _strings(value.get(key)) if x in bo_ids]) + for child in value.values(): + if isinstance(child, (dict, list)): + refs.extend(_collect_bo_refs(child, bo_ids)) + return _dedupe(refs) + + + def _compact_signal(record: dict[str, Any], signal_type: str) -> dict[str, Any]: + return { + "signal_type": signal_type, + "signal_id": _first_key(record, ["signal_id", "route_id", "id", "structure_id"]), + "candidate_structure_types": _strings(record.get("candidate_structure_types")), + "linked_structure_ids": _strings(record.get("linked_structure_ids")), + "related_evidence_indexes": _strings(_first_key(record, ["related_evidence_indexes", "evidence_refs", "source_evidence_indexes"])), + "party_roles": record.get("party_roles") if isinstance(record.get("party_roles"), list) else [], + "actio_role_hints": record.get("actio_role_hints") if isinstance(record.get("actio_role_hints"), list) else [], + "claim_group_candidates": record.get("claim_group_candidates") if isinstance(record.get("claim_group_candidates"), list) else [], + "liability_group_candidates": record.get("liability_group_candidates") if isinstance(record.get("liability_group_candidates"), list) else [], + "summary": _text(record, 500), + } + + + def _build_signal_hints_by_bo_id(docs: dict[str, Any], bo_ids: set[str]) -> dict[str, dict[str, list[dict[str, Any]]]]: + out: dict[str, dict[str, list[dict[str, Any]]]] = { + bo_id: {"actio": [], "case_liability": [], "legal_effect": []} for bo_id in bo_ids + } + for signal_type, doc in docs.items(): + for record in _flatten_records(doc): + bo_refs = _collect_bo_refs(record, bo_ids) + for bo_id in bo_refs: + bucket = out.setdefault(bo_id, {"actio": [], "case_liability": [], "legal_effect": []}) + key = {"actio_case_signals": "actio", "case_liability_signals": "case_liability", "legal_effect_signals": "legal_effect"}[signal_type] + if len(bucket[key]) < MAX_SIGNAL_ITEMS_PER_BO: + bucket[key].append(_compact_signal(record, key)) + return out + + + def _linked_by_bo(bo_id: str, index: dict[str, Any]) -> dict[str, Any]: + row = _as_dict(index.get("by_bo_id", {}).get(bo_id)) + return { + "structure_ids": _strings(row.get("structure_ids")), + "issue_cluster_ids": _strings(row.get("issue_cluster_ids")), + "asset_cluster_ids": _strings(row.get("asset_cluster_ids")), + "evidence_index_ids": _strings(row.get("evidence_index_ids") or row.get("evidence_indexes")), + "candidate_structure_types": _strings(row.get("candidate_structure_types")), + "claim_group_ids": _strings(row.get("claim_group_ids") or row.get("linked_claim_group_ids")), + "liability_group_ids": _strings(row.get("liability_group_ids") or row.get("linked_liability_group_ids")), + "downstream_fact_roles": _strings(row.get("downstream_fact_roles") or row.get("legal_effect_roles")), + } + + + def _score_evidence(evidence_rows: list[dict[str, Any]]) -> tuple[str, str]: + if not evidence_rows: + return "low", "low" + text = " ".join(_text(row, 300).lower() for row in evidence_rows) + documentary_markers = ["등기", "계약", "판결", "공문", "금융", "송금", "계좌", "registry", "contract", "judgment"] + if any(marker.lower() in text for marker in documentary_markers): + return "high", "documentary" + if len(evidence_rows) >= 2: + return "high", "high" + if "meeting" in text or "회의" in text or "면담" in text: + return "medium", "meeting_only_core" + return "medium", "medium" + + + def _legal_roles(types: list[str], structures: list[dict[str, Any]], signals: dict[str, Any]) -> list[str]: + roles: list[str] = [] + for structure in structures: + roles.extend(_strings(structure.get("legal_effect_roles"))) + roles.extend([STRUCTURE_TYPE_TO_ROLE.get(t, "legal_effect_roles") for t in types]) + if signals.get("actio"): + roles.append("actio_roles") + if signals.get("case_liability"): + roles.append("claim_chain_ref") + if signals.get("legal_effect"): + roles.append("legal_effect_roles") + return _dedupe(roles) + + + def _modules(types: list[str], signals: dict[str, Any]) -> list[str]: + modules = [STRUCTURE_TYPE_TO_MODULE.get(t, "legal_effect") for t in types] + if signals.get("actio"): + modules.append("actio") + if signals.get("case_liability"): + modules.append("claim_liability") + if signals.get("legal_effect"): + modules.append("legal_effect") + return _dedupe(modules) + + + def _linked_structures(linked: dict[str, Any]) -> dict[str, Any]: + return { + "structure_ids": _strings(linked.get("structure_ids")), + "issue_cluster_ids": _strings(linked.get("issue_cluster_ids")), + "asset_cluster_ids": _strings(linked.get("asset_cluster_ids")), + "evidence_index_ids": _strings(linked.get("evidence_index_ids")), + "candidate_structure_types": _strings(linked.get("candidate_structure_types")), + "claim_group_ids": _strings(linked.get("claim_group_ids")), + "liability_group_ids": _strings(linked.get("liability_group_ids")), + } + + + def _parties(bo: dict[str, Any], signals: dict[str, Any]) -> list[dict[str, Any]]: + core = _as_dict(bo.get("core_field_base")) + out: list[dict[str, Any]] = [] + for role, key in [("actor", "Performer"), ("counterparty", "Subject")]: + party = core.get(key) + if party: + out.append({"party": _text(party, 160), "role": role, "source": "BO.core_field_base"}) + for signal in _as_list(signals.get("case_liability")): + for item in _as_list(_as_dict(signal).get("party_roles")): + if isinstance(item, dict) and item.get("party"): + out.append({"party": _text(item.get("party"), 160), "role": _text(item.get("role"), 80), "source": "case_liability_signals"}) + seen: set[tuple[str, str]] = set() + deduped: list[dict[str, Any]] = [] + for row in out: + key = (row.get("party", ""), row.get("role", "")) + if key not in seen: + seen.add(key) + deduped.append(row) + return deduped + + + def _derived_fact(bo: dict[str, Any], row: dict[str, Any]) -> str: + date = row.get("date") or "" + actor = "" + for party in row.get("parties", []): + if isinstance(party, dict) and party.get("role") == "actor": + actor = party.get("party") or "" + break + obj = row.get("object_spec") or "" + action = row.get("action") or bo.get("action") or "" + if date and actor and obj and action: + return f"[{date}] {actor}가 {obj}에 관하여 {action}하였다는 사실" + if action: + return str(action) + return "" + + + def _basis(bo_id: str, evidence_refs: list[str], structures: dict[str, Any]) -> str: + parts = [f"source_bo_id={bo_id}"] + if evidence_refs: + parts.append("evidence_refs=" + ",".join(evidence_refs[:8])) + sids = _strings(structures.get("structure_ids")) + if sids: + parts.append("structure_ids=" + ",".join(sids[:8])) + return "; ".join(parts) + + + def _claim_chain_ref(linked: dict[str, Any], signals: dict[str, Any]) -> dict[str, Any]: + return { + "claim_group_ids": _strings(linked.get("claim_group_ids")), + "liability_group_ids": _strings(linked.get("liability_group_ids")), + "signal_count": len(_as_list(signals.get("case_liability"))), + } + + + def _legal_calculation_object(row: dict[str, Any], types: list[str]) -> dict[str, Any]: + needs_calc = any(t in {"money_claim", "secured_debt", "land_use_gain", "valuation", "fraudulent_transfer"} for t in types) + return { + "calculation_required": needs_calc, + "amount_source": "BO.amount" if row.get("amount") else None, + "amount_text": row.get("amount"), + "object_basis": row.get("object_spec"), + "candidate_structure_types": types, + } + + + def _build_row(idx: int, item: dict[str, Any], structure_by_id: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]: + bo_id = item["source_bo_id"] + bo = _as_dict(item.get("bo_compact")) + linked = _linked_structures(_as_dict(item.get("linked_structures"))) + signals = _as_dict(item.get("signal_hints")) + related_structures = [_as_dict(structure_by_id.get(sid)) for sid in linked["structure_ids"] if isinstance(structure_by_id.get(sid), dict)] + types = _dedupe(linked["candidate_structure_types"] + [str(s.get("candidate_structure_type")) for s in related_structures if s.get("candidate_structure_type")]) + evidence_refs = _dedupe(_strings(item.get("evidence_refs")) + linked["evidence_index_ids"]) + evidence_rows = [_as_dict(_as_dict(item.get("evidence_authority")).get(eid)) for eid in evidence_refs] + evidence_rows = [row for row in evidence_rows if row] + credibility, proof_strength = _score_evidence(evidence_rows) + core = _as_dict(bo.get("core_field_base")) + row = { + "fact_id": f"F-{idx:03d}", + "source_bo_id": bo_id, + "type": _first_value(bo.get("bo_type"), types[0] if types else None), + "date": _first_value(core.get("BehaviorTime"), bo.get("date")), + "parties": _parties(bo, signals), + "object_spec": _first_value(core.get("Object"), _text(linked["asset_cluster_ids"], 160)), + "amount": _text(_first_value(bo.get("amount"), core.get("Amount")), 240) or None, + "action": _first_value(bo.get("action"), core.get("Action")), + "evidence_refs": evidence_refs, + "credibility": credibility, + "legal_centrality": "critical" if linked["structure_ids"] or signals.get("actio") else "medium", + "proof_strength": proof_strength, + "legal_effect_roles": _legal_roles(types, related_structures, signals), + "claim_chain_ref": _claim_chain_ref(linked, signals), + "linked_structures": linked, + "must_not_drop_in_claim_types": _modules(types, signals) if linked["structure_ids"] or signals.get("actio") else [], + "must_consider": bool(linked["structure_ids"] or signals.get("actio")), + "state_context": { + "bo_type": bo.get("bo_type"), + "issue_cluster_ids": linked["issue_cluster_ids"], + "asset_cluster_ids": linked["asset_cluster_ids"], + }, + "money_claim_effect": {"enabled": "money_claim" in types, "source_bo_id": bo_id if "money_claim" in types else None}, + "commercial_successor_effect": {"enabled": "commercial_successor" in types, "source_bo_id": bo_id if "commercial_successor" in types else None}, + "actio_roles": _as_list(signals.get("actio")), + "linked_actio_structures": { + "structure_ids": [s.get("structure_id") for s in related_structures if s.get("candidate_structure_type") in {"fraudulent_transfer", "preserved_claim"}], + "actio_signal_count": len(_as_list(signals.get("actio"))), + }, + "downstream_module_candidates": _modules(types, signals), + "skeleton_validation_codes": [], + "derived_fact": "", + "derivation_basis": "", + "legal_calculation_object": {}, + } + row["legal_calculation_object"] = _legal_calculation_object(row, types) + row["derived_fact"] = _derived_fact(bo, row) + row["derivation_basis"] = _basis(bo_id, evidence_refs, linked) + if not row["derived_fact"]: + row["skeleton_validation_codes"].append("EMPTY_DERIVED_FACT") + if not row["evidence_refs"]: + row["skeleton_validation_codes"].append("NO_EVIDENCE_REFS") + if not linked["structure_ids"]: + row["skeleton_validation_codes"].append("NO_LINKED_LEGAL_EFFECT_STRUCTURE") + return row, related_structures + + + def _mark_row_code(row: dict[str, Any], code: str) -> None: + codes = row.setdefault("skeleton_validation_codes", []) + if isinstance(codes, list) and code not in codes: + codes.append(code) + + + def _domain_payload_compact(bo_by_id: dict[str, Any], row: dict[str, Any]) -> dict[str, Any]: + bo = _as_dict(bo_by_id.get(str(row.get("source_bo_id")))) + return _as_dict(bo.get("domain_payload_compact")) + + + def _clean_str(value: Any) -> str: + return str(value).strip() if value not in (None, "") else "" + + + def _periods_overlap(start_a: str, end_a: str, start_b: str, end_b: str) -> bool: + a0, a1 = start_a or "0000-00-00", end_a or "9999-12-31" + b0, b1 = start_b or "0000-00-00", end_b or "9999-12-31" + return a0 <= b1 and b0 <= a1 + + + def _possession_conflict_scan(rows: list[dict[str, Any]], bo_by_id: dict[str, Any]) -> None: + incompatible = {frozenset(pair) for pair in POSSESSION_INCOMPATIBLE_CAUSE_PAIRS} + entries: list[dict[str, Any]] = [] + for row in rows: + possession = _as_dict(_domain_payload_compact(bo_by_id, row).get("possession_state")) + if not possession: + continue + asset_keys = _strings(possession.get("asset_ref")) or _strings(_as_dict(row.get("state_context")).get("asset_cluster_ids")) + events = [x for x in (_as_dict(e) for e in _as_list(possession.get("possession_events"))) if x] + causes = [x for x in (_as_dict(c) for c in _as_list(possession.get("legal_cause_assertions"))) if _clean_str(x.get("cause_asserted"))] + if events and not causes: + _mark_row_code(row, "LEGAL_CAUSE_STATE_UNKNOWN_REVIEW") + starts = sorted([s for s in (_clean_str(e.get("start_date")) for e in events) if s]) + ends = [_clean_str(e.get("end_date")) for e in events] + open_ended = (not events) or any(not e for e in ends) + period_start = starts[0] if starts else "" + period_end = "" if open_ended else max(e for e in ends if e) + for cause in causes: + entries.append({ + "row": row, + "asset_keys": set(asset_keys), + "cause": _clean_str(cause.get("cause_asserted")), + "start": _clean_str(cause.get("assertion_date")) or period_start, + "end": period_end, + }) + for i in range(len(entries)): + for j in range(i + 1, len(entries)): + first, second = entries[i], entries[j] + if first["row"] is second["row"]: + continue + if not (first["asset_keys"] & second["asset_keys"]): + continue + if frozenset((first["cause"], second["cause"])) not in incompatible: + continue + if not _periods_overlap(first["start"], first["end"], second["start"], second["end"]): + continue + for row in (first["row"], second["row"]): + _mark_row_code(row, "POSSESSION_CAUSE_CONFLICT") + row["must_consider"] = True + + + def _notice_receipt_gate_scan(rows: list[dict[str, Any]], bo_by_id: dict[str, Any]) -> None: + for row in rows: + notice = _as_dict(_domain_payload_compact(bo_by_id, row).get("notice_lifecycle")) + if not notice: + continue + if notice.get("requires_receipt_for_effect") is not True: + continue + received = _clean_str(notice.get("received_date")) + arrival = _clean_str(notice.get("arrival_date")) + if not received and not arrival: + _mark_row_code(row, "NOTICE_RECEIPT_UNVERIFIED") + row["must_consider"] = True + if received and not _strings(notice.get("receipt_evidence_refs")) and not _clean_str(notice.get("recipient_admission_text")): + _mark_row_code(row, "NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW") + + + def _lien_extinction_notice_scan(rows: list[dict[str, Any]], bo_by_id: dict[str, Any]) -> None: + receipt_by_notice_id: dict[str, str] = {} + possession_entries: list[dict[str, Any]] = [] + for row in rows: + payload = _domain_payload_compact(bo_by_id, row) + notice = _as_dict(payload.get("notice_lifecycle")) + notice_id = _clean_str(notice.get("notice_id")) + receipt_date = _clean_str(notice.get("received_date")) or _clean_str(notice.get("arrival_date")) + if notice_id and receipt_date: + receipt_by_notice_id[notice_id] = min(receipt_date, receipt_by_notice_id.get(notice_id, "9999-12-31")) + possession = _as_dict(payload.get("possession_state")) + asset_keys = set(_strings(possession.get("asset_ref"))) + for event in (_as_dict(x) for x in _as_list(possession.get("possession_events"))): + start = _clean_str(event.get("start_date")) + if start and asset_keys: + possession_entries.append({"row": row, "asset_keys": asset_keys, "start": start}) + for row in rows: + lien = _as_dict(_domain_payload_compact(bo_by_id, row).get("lien_state")) + if not lien: + continue + receipt_dates = [receipt_by_notice_id[ref] for ref in _strings(lien.get("extinction_notice_refs")) if ref in receipt_by_notice_id] + lien_assets = set(_strings(lien.get("asset_ref"))) + if not receipt_dates or not lien_assets: + continue + receipt_date = min(receipt_dates) + for entry in possession_entries: + if (lien_assets & entry["asset_keys"]) and entry["start"] > receipt_date: + _mark_row_code(row, "LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW") + _mark_row_code(entry["row"], "LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW") + + + # ---------------------------------------------------------------- X-1/X-2/X-3: 예외의 상류 소거 + def _distinct_values(values: list[Any]) -> list[str]: + out: list[str] = [] + for value in values: + s = _text(value, 240) + if s and s not in out: + out.append(s) + return out + + + def _projection_conflicts_for_row(row: dict[str, Any], bo: dict[str, Any]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + """X-3: v2 트리거 잔여 3유형에 4중 조건(단일 값 선택 필수 / source-backed 상충 후보 2+ / + 보존·defer 불가 / compact 판정 충분)을 적용한다. 후보 2개 미만의 단순 누락은 조건 ② 불충족으로 + deterministic review code로 전환한다. X-1(복수 role/module/그룹 = 정상 다중 라우팅)과 + X-2(파생 사실 문자열 길이 기반)의 trigger는 폐지되었다(배열 보존이 기본값). + 반환: (pack_candidates, deterministic_reviews).""" + core = _as_dict(bo.get("core_field_base")) + linked = _as_dict(row.get("linked_structures")) + roles = _strings(row.get("legal_effect_roles")) + modules = _strings(row.get("downstream_module_candidates")) + calc = _as_dict(row.get("legal_calculation_object")) + pack_candidates: list[dict[str, Any]] = [] + reviews: list[dict[str, Any]] = [] + + def _review(review_code: str, exception_type: str, reason: str) -> None: + reviews.append({ + "fact_id": row["fact_id"], + "source_bo_id": row["source_bo_id"], + "review_code": review_code, + "origin_exception_type": exception_type, + "reason": reason, + }) + + def _candidate(exception_type: str, trigger_reason: str, allowed_output_fields: list[str], + compact_context: dict[str, Any], conflict_flags: list[str]) -> None: + pack_candidates.append({ + "exception_type": exception_type, + "trigger_reason": trigger_reason, + "allowed_output_fields": allowed_output_fields, + "compact_context": compact_context, + "conflict_flags": conflict_flags, + "row": row, + }) + + if calc.get("calculation_required"): + conflict_fields: dict[str, list[str]] = {} + # X-3 ①②: 양 원천(BO.amount vs core.Amount)이 실재하며 상충하면 단일 값 선택이 필수인 진짜 예외. + amount_candidates = _distinct_values([bo.get("amount"), core.get("Amount")]) + if len(amount_candidates) >= 2: + conflict_fields["amount"] = amount_candidates + if not row.get("object_spec"): + object_candidates = _distinct_values([core.get("Object"), ", ".join(_strings(linked.get("asset_cluster_ids")))]) + if len(object_candidates) >= 2: + conflict_fields["object_spec"] = object_candidates + if conflict_fields: + _candidate( + "legal_calculation_required", "calculation_source_conflicting_candidates", + ["amount", "object_spec", "legal_calculation_object", "derivation_basis", "skeleton_validation_codes"], + { + "target_fields": sorted(conflict_fields), + "source_backed_candidates": conflict_fields, + "candidate_structure_types": _strings(calc.get("candidate_structure_types")), + "derivation_basis": row.get("derivation_basis"), + }, + ["CALCULATION_BASIS_CONFLICT"], + ) + elif not row.get("amount") or not row.get("object_spec"): + _review("CALCULATION_BASIS_INCOMPLETE_REVIEW", "legal_calculation_required", + "calculation_source_missing_without_competing_candidates") + asset_ids = _strings(linked.get("asset_cluster_ids")) + if asset_ids and not row.get("object_spec"): + if len(asset_ids) >= 2: + _candidate( + "unresolved_asset_cluster_projection", "asset_cluster_projection_requires_single_object_choice", + ["object_spec", "linked_structures", "legal_calculation_object", "derivation_basis"], + { + "target_fields": ["object_spec"], + "source_backed_candidates": {"object_spec": asset_ids}, + "structure_ids": _strings(linked.get("structure_ids")), + "derivation_basis": row.get("derivation_basis"), + }, + ["ASSET_CLUSTER_PROJECTION_GAP"], + ) + else: + _review("ASSET_CLUSTER_PROJECTION_GAP_REVIEW", "unresolved_asset_cluster_projection", + "single_asset_cluster_without_object_spec") + if _strings(linked.get("structure_ids")) and (not roles or not modules): + types = _strings(linked.get("candidate_structure_types")) + role_candidates = _distinct_values([STRUCTURE_TYPE_TO_ROLE.get(t, "legal_effect_roles") for t in types]) if not roles else [] + module_candidates = _distinct_values([STRUCTURE_TYPE_TO_MODULE.get(t, "legal_effect") for t in types]) if not modules else [] + if len(role_candidates) >= 2 or len(module_candidates) >= 2: + _candidate( + "unresolved_structure_role_projection", "structure_role_projection_requires_single_choice", + ["legal_effect_roles", "downstream_module_candidates", "claim_chain_ref", "actio_roles", "linked_structures"], + { + "target_fields": ["legal_effect_roles", "downstream_module_candidates"], + "source_backed_candidates": {"legal_effect_roles": role_candidates, "downstream_module_candidates": module_candidates}, + "structure_ids": _strings(linked.get("structure_ids")), + "candidate_structure_types": types, + }, + ["STRUCTURE_ROLE_PROJECTION_GAP"], + ) + else: + _review("STRUCTURE_ROLE_PROJECTION_GAP_REVIEW", "unresolved_structure_role_projection", + "structure_role_projection_without_competing_candidates") + return pack_candidates, reviews + + + def _exception_item(exception_id: str, candidate: dict[str, Any]) -> dict[str, Any]: + row = candidate["row"] + return { + "exception_id": exception_id, + "exception_type": candidate["exception_type"], + "fact_id": row["fact_id"], + "source_bo_id": row["source_bo_id"], + "severity": "blocking_projection", + "trigger_reason": candidate["trigger_reason"], + "candidate_fact_row_subset": {key: deepcopy(row.get(key)) for key in candidate["allowed_output_fields"] if key in row}, + "compact_context": candidate["compact_context"], + "allowed_output_fields": candidate["allowed_output_fields"], + "allowed_decisions": ["PATCH", "KEEP_AS_IS", "BLOCK_REVIEW"], + "conflict_flags": candidate["conflict_flags"], + } + + + def _assemble_exception_pack(pack_candidates: list[dict[str, Any]], deterministic_reviews: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], int]: + """FX-### 부여 후 방어 상한 적용. legal_theory 표지는 무조건 LLM 금지 → deterministic review. + 상한 초과분은 자동 차단이 아니라 exception_id를 유지한 채 review로 라우팅한다(§5-6).""" + exceptions: list[dict[str, Any]] = [] + overflow = 0 + total_bytes = 0 + counter = itertools.count(1) + for candidate in pack_candidates: + exception_id = f"FX-{next(counter):03d}" + row = candidate["row"] + review_code = REVIEW_CODE_BY_EXCEPTION_TYPE[candidate["exception_type"]] + if any("legal_theory" in flag for flag in candidate["conflict_flags"]): + deterministic_reviews.append({ + "fact_id": row["fact_id"], "source_bo_id": row["source_bo_id"], + "review_code": "LEGAL_THEORY_REQUIRED_REVIEW", + "origin_exception_type": candidate["exception_type"], + "reason": f"legal_theory_marker_routes_to_stage2:{exception_id}", + }) + _mark_row_code(row, "LEGAL_THEORY_REQUIRED_REVIEW") + row["must_consider"] = True + continue + item = _exception_item(exception_id, candidate) + item_text = json.dumps(item, ensure_ascii=False, sort_keys=True) + oversize = len(item_text) > PACK_ITEM_CHAR_LIMIT + over_budget = len(exceptions) >= PACK_MAX_ITEMS or total_bytes + len(item_text.encode("utf-8")) > PACK_MAX_BYTES + if oversize or over_budget: + overflow += 1 + deterministic_reviews.append({ + "fact_id": row["fact_id"], "source_bo_id": row["source_bo_id"], + "review_code": review_code, + "origin_exception_type": candidate["exception_type"], + "reason": ("pack_item_over_char_limit:" if oversize else "pack_budget_overflow:") + exception_id, + }) + _mark_row_code(row, review_code) + row["must_consider"] = True + continue + total_bytes += len(item_text.encode("utf-8")) + exceptions.append(item) + return exceptions, overflow + + + def _les_review_origins(les_doc: Any, structures: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]: + """X-7: LES 실질 review 상태 수집 — (a) review_required=true, (b) pending_exception_state.blocked + 비공백(Part 3 v2 additive 필드, 부재 시 무시), (c) quality_gate.review_queue 실질 항목.""" + origin_by_sid: dict[str, list[dict[str, Any]]] = {} + for structure in structures: + if not isinstance(structure, dict): + continue + sid = str(structure.get("structure_id") or "") + if not sid: + continue + if structure.get("review_required") is True: + origin_by_sid.setdefault(sid, []).append({"origin": "structure_review_required"}) + blocked = _strings(_as_dict(structure.get("pending_exception_state")).get("blocked")) + if blocked: + origin_by_sid.setdefault(sid, []).append({"origin": "pending_exception_blocked", "blocked_exception_ids": blocked}) + for entry in _as_list(_as_dict(_as_dict(les_doc).get("quality_gate")).get("review_queue")): + item = _as_dict(entry) + sid = str(item.get("structure_id") or "") + review_type = _clean_str(item.get("review_type")) + if not sid or not review_type: + continue + origins = origin_by_sid.setdefault(sid, []) + if review_type == "structure_review_required" and any(o.get("origin") == "structure_review_required" for o in origins): + continue + origins.append({"origin": "quality_gate_review_queue", "review_type": review_type}) + return origin_by_sid + + + def _apply_les_inheritance(rows: list[dict[str, Any]], origin_by_sid: dict[str, list[dict[str, Any]]]) -> dict[str, dict[str, Any]]: + by_fact_id: dict[str, dict[str, Any]] = {} + for row in rows: + sids = sorted(set(_strings(_as_dict(row.get("linked_structures")).get("structure_ids"))) & set(origin_by_sid)) + if not sids: + continue + _mark_row_code(row, "LES_REVIEW_REQUIRED") + row["must_consider"] = True + by_fact_id[row["fact_id"]] = { + "fact_id": row["fact_id"], + "source_bo_id": row["source_bo_id"], + "structure_ids": sids, + "origins": {sid: origin_by_sid[sid] for sid in sids}, + } + return by_fact_id + + + def _validate_candidate_bundle(bundle: dict[str, Any]) -> None: + if bundle.get("schema_version") != "stage1_fact_ledger_candidate_bundle.v2": + raise ValueError("candidate bundle schema_version mismatch") + items = _as_list(bundle.get("candidate_items")) + if not items: + raise ValueError("candidate_items is empty") + for item in items: + missing = [field for field in FINAL_FIELDS if field not in item] + if missing: + raise ValueError(f"candidate item missing fields: {missing}") + extra = [field for field in item if field not in FINAL_FIELDS] + if extra: + raise ValueError(f"candidate item has arbitrary fields: {extra}") + manifest = _as_dict(bundle.get("ref_manifest")) + for key in ("bo_ids", "structure_ids", "evidence_ids"): + if not isinstance(manifest.get(key), list): + raise ValueError(f"ref_manifest.{key} must be list") + ref = _as_dict(bundle.get("exception_pack_ref")) + if ref.get("path") != PACK_OUT_PATH or not ref.get("sha256"): + raise ValueError("exception_pack_ref invalid") + if "exception_pack" in bundle or "created_at_utc" in bundle: + raise ValueError("candidate bundle must not embed exception_pack or created_at_utc") + + + def _validate_exception_pack(pack: dict[str, Any]) -> None: + if pack.get("schema_version") != "stage1_fact_ledger_exception_pack.v2": + raise ValueError("exception pack schema_version mismatch") + for exc in _as_list(pack.get("exceptions")): + if exc.get("exception_type") not in PACK_EXCEPTION_TYPES: + raise ValueError(f"unsupported exception_type: {exc.get('exception_type')}") + + + def main() -> None: + try: + _init_mcp_session() + docs = {key: _read_doc(name) for key, name in INPUT_FILES.items()} + bo_items = _bo_items(docs["bo"]) + bo_by_id = {_bo_id(item): _compact_bo(item) for item in bo_items} + bo_ids = set(bo_by_id) + structures, structure_index, structure_warnings = _validate_legal_effect_structures(docs["legal_effect_structures"], bo_ids) + structure_by_id = { + str(item.get("structure_id")): _compact_structure(item) + for item in structures + if isinstance(item, dict) and item.get("structure_id") + } + evidence_authority = _build_evidence_authority(docs["evidence_indexed"]) + signal_hints = _build_signal_hints_by_bo_id({ + "actio_case_signals": docs["actio_case_signals"], + "case_liability_signals": docs["case_liability_signals"], + "legal_effect_signals": docs["legal_effect_signals"], + }, bo_ids) + warnings = list(structure_warnings) + # per-BO fact input 중간 표현은 메모리에서만 구성한다(§5-3 — 어떤 채널에도 물질화 금지). + rows: list[dict[str, Any]] = [] + pack_candidates: list[dict[str, Any]] = [] + deterministic_reviews: list[dict[str, Any]] = [] + for idx, bo_id in enumerate(sorted(bo_by_id), start=1): + bo = bo_by_id[bo_id] + linked = _linked_by_bo(bo_id, structure_index) + evidence_refs = _dedupe(bo["source_evidence_indexes"] + _strings(linked.get("evidence_index_ids"))) + for eid in evidence_refs: + if eid not in evidence_authority: + warnings.append(f"EVIDENCE_REF_NOT_IN_EVIDENCE_UNIVERSE:{bo_id}:{eid}") + item = { + "source_bo_id": bo_id, + "bo_compact": bo, + "linked_structures": linked, + "signal_hints": signal_hints.get(bo_id, {"actio": [], "case_liability": [], "legal_effect": []}), + "evidence_refs": evidence_refs, + "evidence_authority": {eid: evidence_authority.get(eid, {"evidence_index": eid, "missing": True}) for eid in evidence_refs}, + } + row, _related = _build_row(idx, item, structure_by_id) + rows.append(row) + candidates, reviews = _projection_conflicts_for_row(row, bo) + pack_candidates.extend(candidates) + deterministic_reviews.extend(reviews) + bo_compact_by_id = bo_by_id + _possession_conflict_scan(rows, bo_compact_by_id) + _notice_receipt_gate_scan(rows, bo_compact_by_id) + _lien_extinction_notice_scan(rows, bo_compact_by_id) + for review in deterministic_reviews: + for row in rows: + if row["fact_id"] == review["fact_id"]: + _mark_row_code(row, review["review_code"]) + row["must_consider"] = True + exceptions, overflow = _assemble_exception_pack(pack_candidates, deterministic_reviews) + origin_by_sid = _les_review_origins(docs["legal_effect_structures"], structures) + les_by_fact_id = _apply_les_inheritance(rows, origin_by_sid) + # F2 drafting gate 입력(소스팩 재읽기 폐지 대체): NOTICE_RECEIPT_UNVERIFIED 행의 notice_id. + notice_gate_fact_ids: list[str] = [] + notice_gate_ids: list[str] = [] + for row in rows: + if "NOTICE_RECEIPT_UNVERIFIED" in _as_list(row.get("skeleton_validation_codes")): + notice_gate_fact_ids.append(row["fact_id"]) + notice_id = _clean_str(_as_dict(_domain_payload_compact(bo_compact_by_id, row).get("notice_lifecycle")).get("notice_id")) + if notice_id: + notice_gate_ids.append(notice_id) + pack_payload = { + "schema_version": "stage1_fact_ledger_exception_pack.v2", + "status": "READY", + "has_exceptions": bool(exceptions), + "exception_count": len(exceptions), + "exceptions": exceptions, + "budget": { + "max_items": PACK_MAX_ITEMS, + "max_bytes": PACK_MAX_BYTES, + "item_char_limit": PACK_ITEM_CHAR_LIMIT, + "overflow_routed_to_review_count": overflow, + }, + } + _validate_exception_pack(pack_payload) + pack_text = json.dumps(pack_payload, ensure_ascii=False, indent=2) + "\n" + pack_sha = hashlib.sha256(pack_text.encode("utf-8")).hexdigest() + bundle = { + "schema_version": "stage1_fact_ledger_candidate_bundle.v2", + "status": "READY_WITH_EXCEPTIONS" if exceptions else "READY", + "candidate_items": rows, + "ref_manifest": { + "bo_ids": sorted(bo_ids), + "structure_ids": sorted(structure_by_id), + "evidence_ids": sorted(evidence_authority), + }, + "les_review_inheritance": {"by_fact_id": {fact_id: les_by_fact_id[fact_id] for fact_id in sorted(les_by_fact_id)}}, + "exception_pack_ref": { + "path": PACK_OUT_PATH, + "sha256": pack_sha, + "expected_exception_ids": [exc["exception_id"] for exc in exceptions], + }, + "compile_gate": { + "candidate_count": len(rows), + "exception_count": len(exceptions), + "deterministic_review_count": len(deterministic_reviews), + "les_inheritance_count": len(les_by_fact_id), + "deterministic_review_items": deterministic_reviews, + "notice_gate": {"affected_fact_ids": notice_gate_fact_ids, "notice_ids": _dedupe(notice_gate_ids)}, + "source_integrity_warnings": _dedupe(warnings), + "candidate_schema_closed": True, + "exception_schema_closed": True, + }, + } + _validate_candidate_bundle(bundle) + bundle_text = json.dumps({"fact_ledger_candidate_bundle": bundle}, ensure_ascii=False, indent=2) + "\n" + _write_text_doc(PACK_OUT_PATH, pack_text) + _write_text_doc(CANDIDATE_OUT_PATH, bundle_text) + pack_reread = _read_doc_raw_text(PACK_OUT_PATH) + if hashlib.sha256(pack_reread.encode("utf-8")).hexdigest() != pack_sha: + raise ValueError("post-write reread validation failed: exception pack sha mismatch") + bundle_reread = _read_doc(CANDIDATE_OUT_PATH) + if _as_dict(_as_dict(bundle_reread).get("fact_ledger_candidate_bundle")).get("schema_version") != "stage1_fact_ledger_candidate_bundle.v2": + raise ValueError("post-write reread validation failed: candidate bundle") + print(json.dumps({ + "status": bundle["status"], + "message": "F0 fact ledger reducer complete", + "candidate_path": CANDIDATE_OUT_PATH, + "exception_pack_path": PACK_OUT_PATH, + "fact_count": len(rows), + "has_exceptions": bool(exceptions), + "exception_count": len(exceptions), + "deterministic_review_count": len(deterministic_reviews), + "les_inheritance_count": len(les_by_fact_id), + "removed_inputs": REMOVED_INPUTS, + }, ensure_ascii=False)) + except Exception as exc: + print(json.dumps({ + "status": "FAILED", + "message": str(exc)[:500], + "candidate_path": CANDIDATE_OUT_PATH, + "exception_pack_path": PACK_OUT_PATH, + }, ensure_ascii=False)) + raise + + + if __name__ == "__main__": + main() + - task_name: Task_F1_fact_exception_adjudicator + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: low + llm_verbosity: low + max_iterations: 1 + use_tools: [localdocs] + cache_control: + mode: auto + ttl: 20m + preflight: true + preflight_files: + - stage1_tmp/fact_ledger/fact_exception_pack.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + + TASK_NAME: Task_F1_fact_exception_adjudicator + STAGE: Stage 1 Fact Ledger 직접 컴파일러 — 조건부 예외 판정 (Part 1 v3 GB / Part 2 v3 R1 / Part 3 v2 L1 패턴) + MISSION: 결정론 reducer(F0)가 non-deferrable로 판정한 compact 투영 예외만 판정한다. 파일 병합·최종 파일 작성·사실 창작·LES 모호성 재판정은 하지 않는다. + + + + - 20년 이상 경력을 지닌 대한민국 최고 수준 민사송무 변호사이자 세계적 수준의 AI 아키텍트의 관점에서 판정한다. + - 유일한 입력은 preflight로 제공된 `stage1_tmp/fact_ledger/fact_exception_pack.json`이다. + - `Fact_Ledger_base_candidate.json`·`BO.json`·`legal_effect_structures.json`·`client_goal.json`·evidence 파일·signal 파일·raw 원문을 읽거나 요청하지 않는다. + - pack에 없는 exception_id·fact_id·BO ID·evidence index·structure/cluster ID·사실·당사자·일자·금액·출처를 창작하지 않는다. + - LES2가 확정한 issue cluster·asset cluster·candidate structure type 결정을 재판정하지 않는다. BO들이 같은 법적 쟁점·자산군·구조 유형에 속하는지도 판단하지 않는다. + - `Fact_Ledger_base.json`·candidate bundle·writer report를 작성하지 않는다. 출력 파일은 `stage1_tmp/fact_ledger/fact_adjudication.json` 하나뿐이다. + - markdown fence·prose 없이 raw JSON만 파일에 쓴다. + + + + - `unresolved_asset_cluster_projection`은 자산들이 동일한 법적 자산군인지의 판단을 허용하지 않는다. 이미 확정된 asset cluster의 투영 값 선택만 허용한다. + - `unresolved_structure_role_projection`은 candidate structure type의 선택·변경을 허용하지 않는다. 이미 확정된 구조의 role/module 투영 값 선택만 허용한다. + - 판정이 LES2 법적 구조 모호성의 재개를 요구하면 그 항목은 BLOCK_REVIEW로 차단하고 `adjudication_gate.reopened_les_ambiguity`를 true로 표시한다. + - `trigger_reason`·`conflict_flags`에 `legal_theory` 표지가 있으면 PATCH를 금지한다 — BLOCK_REVIEW(하류 법리 모듈 소관). + + + + 1. preflight로 제공된 exception pack의 `has_exceptions`를 확인한다. + 2. `has_exceptions == false`이면: `write_file(overwrite=true)`로 아래 no-exception 판정 객체를 `stage1_tmp/fact_ledger/fact_adjudication.json`에 저장하고 `"NO_EXCEPTIONS"`만 출력한 뒤 즉시 종료한다(terminate). 다른 어떤 파일도 읽지 않는다. + {"fact_exception_adjudication": {"schema_version": "stage1_fact_exception_adjudication.v1", "status": "READY_NO_EXCEPTIONS", "source_exception_pack_schema_version": "stage1_fact_ledger_exception_pack.v2", "decisions": [], "blocked_review_items": [], "adjudication_gate": {"input_exception_count": 0, "decision_count": 0, "blocked_count": 0, "coverage_pass": true, "json_only_contract_pass": true, "reopened_les_ambiguity": false}}} + 3. `has_exceptions == true`이면: `exceptions[]`의 각 항목을 그 항목에 내장된 `candidate_fact_row_subset`·`compact_context`(target_fields·source_backed_candidates)·`allowed_output_fields`·`allowed_decisions`·`conflict_flags`만으로 판정한다. 추가 read는 금지된다. + 4. 판정 규칙: + - `legal_calculation_required`: `compact_context.source_backed_candidates`의 상충 후보 중 문서 근거가 더 구체적인 단일 값을 선택해 PATCH하거나, 현행 행이 적정하면 KEEP_AS_IS. 후보 밖 값 창작 금지. + - `unresolved_asset_cluster_projection`: `source_backed_candidates.object_spec` 후보 중 등기행·별지목록 등 원문 ref 근거가 더 구체적인 값을 선택한다. 우열을 가릴 수 없으면 BLOCK_REVIEW. + - `unresolved_structure_role_projection`: role/module 투영 값을 후보 안에서 선택한다. 배열 보존이 기본값이며 축소 선택을 강제하지 않는다. + - PATCH의 patch 객체 키는 그 항목의 `allowed_output_fields` 안에서만 쓴다(F2가 위반 patch를 hard reject한다). patch 값은 `source_backed_candidates`에 실재하는 값이어야 한다. + - compact 근거로 확정할 수 없으면 반드시 BLOCK_REVIEW로 차단한다(확신 없는 확정 금지 — 인간 검토 라우팅). + - PATCH·KEEP_AS_IS 판정은 `decisions[]`에, BLOCK_REVIEW 판정은 `blocked_review_items[]`에 넣는다. 각 exception_id는 정확히 두 배열 중 한 곳에만 나타난다(F2가 exact coverage 등식으로 검증한다). + 5. `write_file(overwrite=true)`로 결과를 `stage1_tmp/fact_ledger/fact_adjudication.json`에 저장한다. + - 루트는 `fact_exception_adjudication`, `schema_version`은 `stage1_fact_exception_adjudication.v1`, `source_exception_pack_schema_version`은 `stage1_fact_ledger_exception_pack.v2`. + - `status`: 전 항목 비차단이면 `READY`, 차단 존재 시 `READY_WITH_BLOCKS`. + - decision 항목 필드: `exception_id`, `exception_type`(`legal_calculation_required|unresolved_asset_cluster_projection|unresolved_structure_role_projection`), `fact_id`, `source_bo_id`, `decision`(`PATCH|KEEP_AS_IS`), `patch`(PATCH일 때만, allowed_output_fields 키만), `review_required`, `reason_code`(`calculation_basis_selected|asset_projection_selected|structure_role_projected|keep_existing`), `basis_fields_used[]`(항목에 내장된 필드명의 부분집합), `conflict_flags_acknowledged[]`(항목 `conflict_flags`의 부분집합). + - blocked 항목 필드: `exception_id`, `exception_type`, `fact_id`, `source_bo_id`, `block_reason`(`insufficient_compact_record|reopens_les_ambiguity|unsupported_patch|missing_candidate_basis|schema_violation`). + - `adjudication_gate`: `input_exception_count`(pack의 예외 수와 동일), `decision_count`(decisions 배열 길이), `blocked_count`(blocked 배열 길이), `coverage_pass`(모든 exception_id가 정확히 한 곳에 나타날 때만 true), `json_only_contract_pass`, `reopened_les_ambiguity`. + - `exception_id`·`fact_id`·`source_bo_id`·`exception_type`은 pack 항목의 값을 그대로 인용한다(F2가 정체성 불일치를 hard fail 처리한다). + 6. `"F1 예외 판정 완료 (decisions=<건수>)"`만 출력하고 작업을 끝낸다(terminate). + + + + - Stage 1은 법률효과·청구원인을 확정하지 않는다. 여기 도달한 항목은 F2가 진행하려면 단일 투영 값 선택이 필수인 사안뿐이다. + - 판정은 Fact Ledger 행의 투영/조인 값 결정일 뿐 소유권·담보권·평가액·법리의 실체 판단이 아니다. + - 등기행·계약서·판결 등 문서 근거가 자유 문구보다 우선한다. 우열을 가릴 수 없으면 BLOCK_REVIEW. + - 복수 role/module/claim group은 배열형 최종 스키마에 보존되는 정상 상태다. 축소 선택을 강제하지 않는다. + + + + - exception pack 부재·파싱 불가: 즉시 중단하고 `FAILED: <사유>`만 출력한다. 판정 파일은 쓰지 않는다. + - tool 오류: 1회만 재시도. 재실패 시 `FAILED: `만 출력하고 종료한다. + + + - task_name: Task_F2_final_fact_ledger_gate_and_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import sys + from copy import deepcopy + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + CANDIDATE_PATH = "stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json" + PACK_PATH = "stage1_tmp/fact_ledger/fact_exception_pack.json" + ADJUDICATION_PATH = "stage1_tmp/fact_ledger/fact_adjudication.json" + TARGET_PATH = "Fact_Ledger_base.json" + REPORT_PATH = "stage1_tmp/fact_ledger/fact_ledger_writer_report.json" + FINAL_FIELDS = [ + "fact_id", "source_bo_id", "type", "date", "parties", "object_spec", "amount", "action", + "evidence_refs", "credibility", "legal_centrality", "proof_strength", "legal_effect_roles", + "claim_chain_ref", "linked_structures", "must_not_drop_in_claim_types", "must_consider", + "state_context", "money_claim_effect", "commercial_successor_effect", "actio_roles", + "linked_actio_structures", "downstream_module_candidates", "skeleton_validation_codes", + "derived_fact", "derivation_basis", "legal_calculation_object", + ] + FINAL_FIELD_SET = set(FINAL_FIELDS) + FINAL_DEFAULTS = { + "type": None, + "date": None, + "parties": [], + "object_spec": None, + "amount": None, + "action": None, + "evidence_refs": [], + "credibility": "low", + "legal_centrality": "low", + "proof_strength": "low", + "legal_effect_roles": [], + "claim_chain_ref": {}, + "linked_structures": {}, + "must_not_drop_in_claim_types": [], + "must_consider": False, + "state_context": {}, + "money_claim_effect": {}, + "commercial_successor_effect": {}, + "actio_roles": [], + "linked_actio_structures": {}, + "downstream_module_candidates": [], + "skeleton_validation_codes": [], + "derived_fact": "", + "derivation_basis": "", + "legal_calculation_object": {}, + } + GATE_REVIEW_CODES = { + "POSSESSION_CAUSE_CONFLICT", "NOTICE_RECEIPT_UNVERIFIED", "NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW", + "LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW", "LEGAL_CAUSE_STATE_UNKNOWN_REVIEW", + } + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-fact-ledger-f2-final-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + + def _strip_code_fence(text: str) -> str: + stripped = text.strip() + if stripped.startswith("```"): + first_newline = stripped.find("\n") + if first_newline >= 0: + stripped = stripped[first_newline + 1:] + if stripped.rstrip().endswith("```"): + stripped = stripped.rstrip()[:-3] + return stripped.strip() + + + def _lenient_json(raw: str, label: str) -> Any: + """SKILL §6.4/§6.7 관용 파서: strict 실패 시 코드펜스 제거 + raw_decode salvage(envelope 계층 포함).""" + text = (raw or "").strip() + if not text: + raise RuntimeError(f"Empty content for {label}") + try: + return json.loads(text) + except json.JSONDecodeError: + pass + text = _strip_code_fence(text) + try: + return json.loads(text) + except json.JSONDecodeError: + start = text.find("{") + alt = text.find("[") + if alt >= 0 and (start < 0 or alt < start): + start = alt + if start < 0: + raise RuntimeError(f"Content of {label} is not valid JSON: {text[:300]}") + try: + obj, _ = JSON_DECODER.raw_decode(text[start:]) + return obj + except json.JSONDecodeError as exc: + raise RuntimeError(f"Content of {label} is not valid JSON: {text[:300]}") from exc + + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result["result"].get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + text = content[0].get("text", "") + envelope = _lenient_json(text, doc_name) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + first = results[0] + if isinstance(first, dict) and first.get("error"): + raise RuntimeError(f"{doc_name}: {first['error']}") + inner = first.get("content", "") if isinstance(first, dict) else "" + if inner in (None, ""): + raise RuntimeError(f"Empty content for {doc_name}") + if isinstance(inner, str): + return _lenient_json(inner, doc_name) + return inner + return envelope + + + def _read_doc(doc_name: str) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + return _extract_json_from_read_docs_result(result, doc_name) + + + def _read_doc_raw_text(doc_name: str) -> str | None: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + content = result["result"].get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + envelope = _lenient_json(content[0].get("text", ""), doc_name) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if results and isinstance(results[0], dict): + raw = results[0].get("content") + if isinstance(raw, str): + return raw + return None + + + def _write_doc(doc_name: str, content: str) -> None: + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=_next_msg_id()) + + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + if value is None: + return out + if isinstance(value, (str, int, float)): + s = str(value).strip() + return [s] if s else [] + if isinstance(value, list): + for item in value: + out.extend(_strings(item)) + return _dedupe(out) + + + def _dedupe(values: list[str]) -> list[str]: + seen: set[str] = set() + out: list[str] = [] + for value in values: + if value and value not in seen: + seen.add(value) + out.append(value) + return out + + + def _load_candidate_bundle() -> dict[str, Any]: + outer = _read_doc(CANDIDATE_PATH) + bundle = _as_dict(_as_dict(outer).get("fact_ledger_candidate_bundle")) + if bundle.get("schema_version") != "stage1_fact_ledger_candidate_bundle.v2": + raise ValueError("candidate bundle schema_version mismatch (v2 required)") + if bundle.get("status") not in {"READY", "READY_WITH_EXCEPTIONS"}: + raise ValueError("candidate bundle status invalid") + items = _as_list(bundle.get("candidate_items")) + if not items: + raise ValueError("candidate_items is empty") + for item in items: + missing = [field for field in FINAL_FIELDS if field not in item] + if missing: + raise ValueError(f"candidate item missing fields: {missing}") + extra = [field for field in item if field not in FINAL_FIELD_SET] + if extra: + raise ValueError(f"candidate item has arbitrary fields: {extra}") + manifest = _as_dict(bundle.get("ref_manifest")) + for key in ("bo_ids", "structure_ids", "evidence_ids"): + if not isinstance(manifest.get(key), list): + raise ValueError(f"ref_manifest.{key} must be list") + return bundle + + + def _load_exception_pack(bundle: dict[str, Any]) -> dict[str, Any]: + """X-6: pack은 bundle의 exception_pack_ref 경유로만 읽고 sha256·expected id를 대조한다.""" + ref = _as_dict(bundle.get("exception_pack_ref")) + if ref.get("path") != PACK_PATH: + raise ValueError("exception_pack_ref path mismatch") + raw_text = _read_doc_raw_text(PACK_PATH) + if isinstance(raw_text, str) and ref.get("sha256"): + digest = hashlib.sha256(raw_text.encode("utf-8")).hexdigest() + if digest != ref.get("sha256"): + raise ValueError("exception pack sha256 mismatch against candidate bundle ref") + pack = _lenient_json(raw_text, PACK_PATH) if isinstance(raw_text, str) else _read_doc(PACK_PATH) + pack = _as_dict(pack) + if pack.get("schema_version") != "stage1_fact_ledger_exception_pack.v2": + raise ValueError("exception pack schema_version mismatch (v2 required)") + expected_ids = _strings(ref.get("expected_exception_ids")) + pack_ids = [str(_as_dict(e).get("exception_id")) for e in _as_list(pack.get("exceptions"))] + if sorted(expected_ids) != sorted(pack_ids): + raise ValueError("exception pack ids mismatch against candidate bundle ref") + return pack + + + def _load_adjudication() -> dict[str, Any]: + outer = _read_doc(ADJUDICATION_PATH) + adjudication = _as_dict(_as_dict(outer).get("fact_exception_adjudication")) + if adjudication.get("schema_version") != "stage1_fact_exception_adjudication.v1": + raise ValueError("fact_exception_adjudication schema_version mismatch") + if adjudication.get("status") not in {"READY", "READY_NO_EXCEPTIONS", "READY_WITH_BLOCKS"}: + raise ValueError(f"fact_exception_adjudication status invalid: {adjudication.get('status')}") + return adjudication + + + def _gate_int(gate: dict[str, Any], key: str) -> int: + value = gate.get(key) + if isinstance(value, bool) or not isinstance(value, int): + return -1 + return value + + + def _check_decision_identity(entry: dict[str, Any], exception: dict[str, Any], label: str) -> None: + for key in ("exception_type", "fact_id", "source_bo_id"): + if str(entry.get(key) or "") != str(exception.get(key) or ""): + raise ValueError(f"{label} {entry.get('exception_id')}: {key} mismatch against exception pack") + + + def _validate_adjudication_coverage(adjudication: dict[str, Any], pack: dict[str, Any]) -> tuple[dict[str, dict[str, Any]], list[dict[str, Any]]]: + """X-6①: exact coverage 등식 — expected == decided ∪ blocked, decided ∩ blocked == ∅. + 중복 decision·미지 ID·예외 0건의 임의 decision·gate 수치 불일치·정체성 불일치는 전부 hard fail.""" + exceptions = { + str(exc.get("exception_id")): exc + for exc in _as_list(pack.get("exceptions")) + if isinstance(exc, dict) and exc.get("exception_id") + } + expected = set(exceptions) + raw_decisions = _as_list(adjudication.get("decisions")) + raw_blocked = _as_list(adjudication.get("blocked_review_items")) + gate = _as_dict(adjudication.get("adjudication_gate")) + if not expected: + if adjudication.get("status") != "READY_NO_EXCEPTIONS": + raise ValueError("no exceptions expected; adjudication status must be READY_NO_EXCEPTIONS") + if raw_decisions or raw_blocked: + raise ValueError("no exceptions expected; decisions and blocked_review_items must be empty") + if gate.get("input_exception_count") not in (0, None): + raise ValueError("adjudication_gate.input_exception_count must be 0 when no exceptions exist") + return {}, [] + blocked_items: list[dict[str, Any]] = [] + blocked_ids: set[str] = set() + for item in raw_blocked: + entry = _as_dict(item) + exception_id = str(entry.get("exception_id") or "") + if not exception_id: + raise ValueError("blocked_review_item missing exception_id") + if exception_id not in expected: + raise ValueError(f"blocked_review_item references unknown exception_id: {exception_id}") + if exception_id in blocked_ids: + raise ValueError(f"duplicate blocked_review_item for exception_id: {exception_id}") + _check_decision_identity(entry, exceptions[exception_id], "blocked_review_item") + blocked_ids.add(exception_id) + blocked_items.append(entry) + decisions_by_id: dict[str, dict[str, Any]] = {} + for item in raw_decisions: + decision = _as_dict(item) + exception_id = str(decision.get("exception_id") or "") + if not exception_id: + raise ValueError("decision missing exception_id") + if exception_id not in expected: + raise ValueError(f"decision references unknown exception_id: {exception_id}") + if exception_id in decisions_by_id: + raise ValueError(f"duplicate decision for exception_id: {exception_id}") + _check_decision_identity(decision, exceptions[exception_id], "decision") + decision_type = decision.get("decision") + if decision_type == "BLOCK_REVIEW": + if exception_id not in blocked_ids: + blocked_ids.add(exception_id) + blocked_items.append({ + "exception_id": exception_id, + "exception_type": decision.get("exception_type"), + "fact_id": decision.get("fact_id"), + "source_bo_id": decision.get("source_bo_id"), + "block_reason": str(decision.get("reason_code") or "insufficient_compact_record"), + }) + continue + if decision_type not in {"PATCH", "KEEP_AS_IS"}: + raise ValueError(f"unknown decision enum: {decision_type}") + decisions_by_id[exception_id] = decision + decided = set(decisions_by_id) + overlap = decided & blocked_ids + if overlap: + raise ValueError(f"coverage violation: decided and blocked overlap: {sorted(overlap)}") + if expected != (decided | blocked_ids): + missing = sorted(expected - (decided | blocked_ids)) + raise ValueError(f"coverage equation failed: missing={missing}") + if _gate_int(gate, "input_exception_count") != len(expected): + raise ValueError("adjudication_gate.input_exception_count mismatch") + if _gate_int(gate, "decision_count") != len(raw_decisions): + raise ValueError("adjudication_gate.decision_count mismatch") + if _gate_int(gate, "blocked_count") != len(raw_blocked): + raise ValueError("adjudication_gate.blocked_count mismatch") + if not gate.get("coverage_pass"): + raise ValueError("adjudication_gate.coverage_pass must be true when exceptions exist") + return decisions_by_id, blocked_items + + + def _exception_allowed_fields(pack: dict[str, Any]) -> dict[str, list[str]]: + out: dict[str, list[str]] = {} + for exc in _as_list(pack.get("exceptions")): + if isinstance(exc, dict) and exc.get("exception_id"): + out[str(exc["exception_id"])] = _strings(exc.get("allowed_output_fields")) + return out + + + def _add_code(row: dict[str, Any], code: str) -> None: + codes = row.setdefault("skeleton_validation_codes", []) + if isinstance(codes, list) and code not in codes: + codes.append(code) + + + def _append_basis(row: dict[str, Any], text: str) -> None: + basis = str(row.get("derivation_basis") or "") + row["derivation_basis"] = (basis + "; " + text).strip("; ") + + + def _apply_patch(row: dict[str, Any], decision: dict[str, Any], allowed_fields: list[str], report: dict[str, Any]) -> bool: + """X-6②: patch key가 allowed_output_fields ∩ FINAL_FIELD_SET 밖이면 decision 전체를 거부하고 + 해당 fact를 BLOCK 처리한다. '경고 후 적용'(v2 방식)은 금지.""" + patch = _as_dict(decision.get("patch")) + exception_id = str(decision.get("exception_id") or "") + allowed = set(allowed_fields) & FINAL_FIELD_SET + bad = sorted(key for key in patch if key not in allowed) + if bad: + report["rejected_patches"].append({ + "exception_id": exception_id, + "fields": bad, + "reason": "PATCH_FIELD_OUTSIDE_ALLOWED_OUTPUT_FIELDS", + }) + _add_code(row, "BLOCK_REVIEW_PRESENT") + row["must_consider"] = True + _append_basis(row, f"rejected_patch={exception_id}") + return False + for key, value in patch.items(): + row[key] = deepcopy(value) + return True + + + def _validate_refs(row: dict[str, Any], bo_ids: set[str], structure_ids: set[str], evidence_ids: set[str], report: dict[str, Any]) -> None: + if row.get("source_bo_id") not in bo_ids: + raise ValueError(f"unknown source_bo_id: {row.get('source_bo_id')}") + for sid in _strings(_as_dict(row.get("linked_structures")).get("structure_ids")): + if sid not in structure_ids: + report["row_warnings"].append({"fact_id": row.get("fact_id"), "warning": "UNKNOWN_LINKED_STRUCTURE_ID", "value": sid}) + _add_code(row, "UNKNOWN_LINKED_STRUCTURE_ID") + for eid in _strings(row.get("evidence_refs")): + if eid not in evidence_ids: + report["row_warnings"].append({"fact_id": row.get("fact_id"), "warning": "EVIDENCE_REF_GAP", "value": eid}) + _add_code(row, "EVIDENCE_REF_GAP") + + + def _normalize_final_row(row: dict[str, Any], ordinal: int) -> dict[str, Any]: + out = {field: deepcopy(row.get(field, FINAL_DEFAULTS.get(field))) for field in FINAL_FIELDS} + out["fact_id"] = f"F-{ordinal:03d}" + out["source_bo_id"] = str(out.get("source_bo_id") or "") + for key in ("parties", "evidence_refs", "legal_effect_roles", "must_not_drop_in_claim_types", "actio_roles", "downstream_module_candidates", "skeleton_validation_codes"): + out[key] = _as_list(out.get(key)) + for key in ("claim_chain_ref", "linked_structures", "state_context", "money_claim_effect", "commercial_successor_effect", "linked_actio_structures", "legal_calculation_object"): + out[key] = _as_dict(out.get(key)) + out["credibility"] = out.get("credibility") if out.get("credibility") in {"high", "medium", "low"} else "low" + out["legal_centrality"] = out.get("legal_centrality") if out.get("legal_centrality") in {"low", "medium", "critical"} else "low" + out["proof_strength"] = out.get("proof_strength") if out.get("proof_strength") in {"low", "medium", "high", "documentary", "meeting_only_core"} else "low" + if not out.get("derived_fact"): + raise ValueError(f"{out['source_bo_id']}: derived_fact is empty") + return out + + + def main() -> None: + try: + _init_mcp_session() + bundle = _load_candidate_bundle() + candidate_items = [deepcopy(x) for x in _as_list(bundle.get("candidate_items")) if isinstance(x, dict)] + exception_pack = _load_exception_pack(bundle) + adjudication = _load_adjudication() + decisions_by_id, blocked_items = _validate_adjudication_coverage(adjudication, exception_pack) + allowed_fields_by_exception = _exception_allowed_fields(exception_pack) + rows_by_fact_id = {str(row.get("fact_id")): row for row in candidate_items} + compile_gate = _as_dict(bundle.get("compile_gate")) + les_inheritance = _as_dict(_as_dict(bundle.get("les_review_inheritance")).get("by_fact_id")) + deterministic_review_items = _as_list(compile_gate.get("deterministic_review_items")) + report: dict[str, Any] = { + "schema_version": "stage1_fact_ledger_writer_report.v1", + "status": "READY", + "blocked_review_items": blocked_items, + "rejected_patches": [], + "patch_policy_warnings": [], + "unapplied_patch_fields": [], + "row_warnings": [], + "possession_conflict_gate": {"status": "PASS", "conflicts": []}, + "drafting_gates": [], + "gate_review_code_summary": {}, + "les_review_inheritance": [les_inheritance[fact_id] for fact_id in sorted(les_inheritance)], + "deterministic_review_items": deterministic_review_items, + } + for fact_id in sorted(les_inheritance): + if fact_id not in rows_by_fact_id: + raise ValueError(f"les_review_inheritance references unknown fact_id: {fact_id}") + for blocked in blocked_items: + fact_id = str(_as_dict(blocked).get("fact_id") or "") + row = rows_by_fact_id.get(fact_id) + if not row: + raise ValueError(f"blocked_review_item references unknown fact_id: {fact_id}") + _add_code(row, "BLOCK_REVIEW_PRESENT") + row["must_consider"] = True + _append_basis(row, "blocked_review=" + str(_as_dict(blocked).get("block_reason") or "unspecified")) + resolved_count = 0 + for exception_id in sorted(decisions_by_id): + decision = decisions_by_id[exception_id] + fact_id = str(decision.get("fact_id") or "") + row = rows_by_fact_id.get(fact_id) + if not row: + raise ValueError(f"decision references unknown fact_id: {fact_id}") + decision_type = decision.get("decision") + if decision_type == "KEEP_AS_IS": + resolved_count += 1 + continue + applied = _apply_patch(row, decision, allowed_fields_by_exception.get(exception_id, []), report) + if applied: + resolved_count += 1 + if decision.get("review_required"): + _add_code(row, "FL2_REVIEW_REQUIRED") + manifest = _as_dict(bundle.get("ref_manifest")) + bo_ids = set(_strings(manifest.get("bo_ids"))) + structure_ids = set(_strings(manifest.get("structure_ids"))) + evidence_ids = set(_strings(manifest.get("evidence_ids"))) + final_rows: list[dict[str, Any]] = [] + for ordinal, row in enumerate(candidate_items, start=1): + normalized = _normalize_final_row(row, ordinal) + _validate_refs(normalized, bo_ids, structure_ids, evidence_ids, report) + extra = [key for key in normalized if key not in FINAL_FIELD_SET] + if extra: + raise ValueError(f"final row has arbitrary fields: {extra}") + final_rows.append(normalized) + # conservation: 후보 BO 집합 == 최종 행 BO 집합, F-### 연속·유일. + candidate_bo_ids = sorted(str(row.get("source_bo_id") or "") for row in candidate_items) + final_bo_ids = sorted(str(row.get("source_bo_id") or "") for row in final_rows) + if candidate_bo_ids != final_bo_ids: + raise ValueError("BO conservation violated between candidate and final rows") + fact_ids = [row["fact_id"] for row in final_rows] + if fact_ids != [f"F-{i:03d}" for i in range(1, len(final_rows) + 1)]: + raise ValueError("fact_id sequence must be continuous and unique") + gate_code_rows: dict[str, list[dict[str, Any]]] = {} + for row in final_rows: + for code in _as_list(row.get("skeleton_validation_codes")): + if code in GATE_REVIEW_CODES: + gate_code_rows.setdefault(str(code), []).append(row) + conflict_rows = gate_code_rows.get("POSSESSION_CAUSE_CONFLICT", []) + report["possession_conflict_gate"] = { + "status": "HARD_WARNING" if conflict_rows else "PASS", + "conflicts": [{"fact_id": row.get("fact_id"), "source_bo_id": row.get("source_bo_id")} for row in conflict_rows], + } + notice_gate_rows = gate_code_rows.get("NOTICE_RECEIPT_UNVERIFIED", []) + if notice_gate_rows: + notice_gate = _as_dict(compile_gate.get("notice_gate")) + report["drafting_gates"].append({ + "gate": "BLOCK_FINAL_DRAFTING_NOTICE_RECEIPT_MISSING", + "row_code": "NOTICE_RECEIPT_UNVERIFIED", + "notice_ids": _strings(notice_gate.get("notice_ids")), + "affected_fact_ids": [row.get("fact_id") for row in notice_gate_rows], + }) + report["gate_review_code_summary"] = {code: [row.get("fact_id") for row in code_rows] for code, code_rows in sorted(gate_code_rows.items())} + # X-6③ status 3단 규칙: blocked/거부 patch/결정론 review/LES 승계/게이트 review 존재 → READY_WITH_REVIEW. + has_review = bool( + blocked_items + or report["rejected_patches"] + or deterministic_review_items + or les_inheritance + or report["gate_review_code_summary"] + ) + report["status"] = "READY_WITH_REVIEW" if has_review else "READY" + _write_doc(TARGET_PATH, json.dumps(final_rows, ensure_ascii=False, indent=2)) + _write_doc(REPORT_PATH, json.dumps(report, ensure_ascii=False, indent=2)) + reread = _read_doc(TARGET_PATH) + if not isinstance(reread, list) or len(reread) != len(final_rows): + raise ValueError("post-write reread validation failed: target is not the expected top-level array") + print(json.dumps({ + "status": report["status"], + "message": "F2 final fact ledger gate and writer complete", + "written_file": TARGET_PATH, + "report_file": REPORT_PATH, + "fact_count": len(final_rows), + "blocked_review_count": len(blocked_items), + "resolved_exception_count": resolved_count, + "deterministic_review_count": len(deterministic_review_items), + "les_inheritance_count": len(les_inheritance), + "drafting_gate_count": len(report["drafting_gates"]), + }, ensure_ascii=False)) + except Exception as exc: + print(json.dumps({ + "status": "FAILED", + "message": str(exc)[:500], + "written_file": TARGET_PATH, + "report_file": REPORT_PATH, + }, ensure_ascii=False)) + raise + + + if __name__ == "__main__": + main() diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_4_Codex_v3.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_4_Codex_v3.yml new file mode 100644 index 00000000..a0586631 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_4_Codex_v3.yml @@ -0,0 +1,2197 @@ +--- +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_4 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Fact Ledger Generation + version: v2.0 + Stages: + - name: stage1_fact_ledger_generation + description: BO, Legal Effect 사용해서 Fact_Ledger_base.json 생성 + tools: + mcpServers: + localdocs: + type: streamable-http + url: http://mcp-localdocs:8012/mcp + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + + task_procedure: + IN: + nexts: + - Task_FL0_fact_source_pack_compiler + wait_until: [] + Task_FL0_fact_source_pack_compiler: + nexts: + - Task_FL1_deterministic_fact_ledger_candidate_builder + wait_until: + - IN + Task_FL1_deterministic_fact_ledger_candidate_builder: + nexts: + - Task_FL2_fact_exception_adjudicator_* + - Task_FL3_final_fact_ledger_gate_and_writer + wait_until: + - Task_FL0_fact_source_pack_compiler + Task_FL2_fact_exception_adjudicator_*: + nexts: + - Task_FL3_final_fact_ledger_gate_and_writer + wait_until: + - Task_FL1_deterministic_fact_ledger_candidate_builder + Task_FL3_final_fact_ledger_gate_and_writer: + nexts: + - OUT + wait_until: + - Task_FL1_deterministic_fact_ledger_candidate_builder + - all Task_FL2_fact_exception_adjudicator_* + OUT: + nexts: [] + wait_until: + - Task_FL3_final_fact_ledger_gate_and_writer + + tasks: + - task_name: Task_FL0_fact_source_pack_compiler + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import copy + import hashlib + import itertools + import json + import re + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _safe_error(exc: BaseException) -> str: + return re.sub(r"\s+", " ", str(exc)).strip()[:300] + + + def _parse_json_value(raw: Any, label: str, allow_trailing: bool = False) -> Any: + if isinstance(raw, (dict, list)): + return raw + if not isinstance(raw, str): + raise ValueError(f"{label} is not JSON text") + text = raw.lstrip("\ufeff").strip() + if not text: + raise ValueError(f"{label} is empty") + try: + return json.loads(text) + except json.JSONDecodeError: + try: + value, end = JSON_DECODER.raw_decode(text) + except json.JSONDecodeError as exc: + raise ValueError(f"{label} invalid JSON: {text[:300]}") from exc + if not allow_trailing and text[end:].strip(): + raise ValueError(f"{label} has trailing content: {text[end:end + 300]}") + return value + + + def _parse_mcp_response(text: str) -> dict[str, Any]: + parsed: dict[str, Any] | None = None + for line in text.strip().splitlines(): + if not line.startswith("data: "): + continue + try: + item = _parse_json_value(line[6:], "MCP SSE data", True) + except ValueError: + continue + if isinstance(item, dict): + parsed = item + if parsed is None: + item = _parse_json_value(text, "MCP response", True) + if not isinstance(item, dict): + raise RuntimeError("MCP response root is not an object") + parsed = item + return parsed + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-fact-ledger-fl0-source-compiler", + "version": "2.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_mcp_response(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP initialize failed: {payload['error']}") + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + initialized = CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ) + initialized.raise_for_status() + + + def _call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_msg_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_mcp_response(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP {name} failed: {payload['error']}") + result = payload.get("result") + if not isinstance(result, dict) or result.get("isError") is True: + raise RuntimeError(f"MCP {name} returned an invalid result") + return result + + + def _extract_doc(result: dict[str, Any], doc_name: str) -> Any: + blocks = result.get("content") or [] + if not blocks or not isinstance(blocks[0], dict): + raise RuntimeError(f"Empty response: {doc_name}") + text = blocks[0].get("text") + if not isinstance(text, str) or not text.strip(): + raise RuntimeError(f"Empty text response: {doc_name}") + outer = _parse_json_value(text, f"{doc_name} outer envelope", True) + inner: Any = outer + if isinstance(outer, dict) and "results" in outer: + results = outer.get("results") or [] + if not results or not isinstance(results[0], dict): + raise RuntimeError(f"Empty results envelope: {doc_name}") + inner = results[0].get("content") + if inner in (None, ""): + inner = results[0].get("text") + if isinstance(inner, (dict, list)): + return inner + return _parse_json_value(inner, f"{doc_name} inner document", False) + + + def _read_json(path: str) -> Any: + return _extract_doc(_call_tool("read_docs", {"doc_names": [path]}), path) + + + def _write_text(path: str, content: str) -> None: + _call_tool("write_file", {"path": path, "content": content, "overwrite": True}) + + + def _canonical(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + + def _digest(value: Any) -> str: + return "sha256:" + hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest() + + + def _write_json(path: str, value: Any) -> None: + _write_text(path, json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2)) + + + def _verify_reread(path: str, expected: Any) -> None: + actual = _read_json(path) + if _digest(actual) != _digest(expected): + raise RuntimeError(f"Post-write verification failed: {path}") + + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _strings(value: Any) -> list[str]: + values = value if isinstance(value, list) else [value] + out: list[str] = [] + for item in values: + if item is None: + continue + if isinstance(item, str): + text = item.strip() + elif isinstance(item, (int, float)) and not isinstance(item, bool): + text = str(item) + else: + continue + if text and text not in out: + out.append(text) + return out + + + def _sorted_strings(value: Any) -> list[str]: + return sorted(set(_strings(value))) + + + def _text(value: Any, limit: int = 500) -> str: + if value in (None, "", [], {}): + return "" + raw = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, sort_keys=True) + return re.sub(r"\s+", " ", raw).strip()[:limit] + + INPUT_FILES = { + "bo": "BO.json", + "structures": "legal_effect_structures.json", + "actio": "actio_case_signals.json", + "liability": "case_liability_signals.json", + "legal_effect": "legal_effect_signals.json", + "evidence": "evidence_indexed.json", + } + OUT_PATH = "stage1_tmp/fact_ledger/fact_source_pack.json" + STRUCTURE_INDEX_KEYS = ( + "by_bo_id", + "by_issue_cluster_id", + "by_asset_cluster_id", + "by_evidence_index", + "by_candidate_structure_type", + ) + DOMAIN_KEYS = ( + "money_claim_details", + "possession_state", + "lien_state", + "notice_lifecycle", + "asset_alias", + "fractional_effect_events", + ) + + + def _bo_id(bo: dict[str, Any]) -> str: + return _text(bo.get("BO_ID") or bo.get("id"), 120) + + + def _evidence_id(row: dict[str, Any]) -> str: + return _text(row.get("evidence_index") or row.get("evidence_id") or row.get("id"), 120) + + + def _sanitize(value: Any, depth: int = 0) -> Any: + if depth > 8: + return None + if isinstance(value, dict): + return {str(k): _sanitize(v, depth + 1) for k, v in sorted(value.items())} + if isinstance(value, list): + out: list[Any] = [] + seen: set[str] = set() + for item in value: + compact = _sanitize(item, depth + 1) + key = _canonical(compact) + if key not in seen: + seen.add(key) + out.append(compact) + return out + if isinstance(value, str): + return _text(value, 500) + if value is None or isinstance(value, (bool, int, float)): + return value + return _text(value, 500) + + + def _bo_evidence_refs(bo: dict[str, Any]) -> list[str]: + refs = _strings(bo.get("source_evidence_indexes")) + for row in _as_list(bo.get("Evidence")): + if isinstance(row, dict): + refs.extend(_strings(row.get("evidence_index"))) + return sorted(set(refs)) + + + def _compact_bo(bo: dict[str, Any]) -> dict[str, Any]: + core = _as_dict(bo.get("core_field_base")) + payload = _as_dict(_as_dict(bo.get("extensions")).get("domain_payload")) + domain = {key: _sanitize(payload.get(key)) for key in DOMAIN_KEYS if payload.get(key) not in (None, {}, [])} + juristic = bo.get("JuristicAct") + return { + "bo_id": _bo_id(bo), + "bo_type": bo.get("BOType") or bo.get("bo_type"), + "action": bo.get("Action") or bo.get("action"), + "action_type": bo.get("ActionType") or bo.get("action_type"), + "juristic_act": _sanitize(juristic), + "reason": _text(bo.get("Reason") or bo.get("reason"), 500) or None, + "prior_act": _sanitize(bo.get("PriorAct") or bo.get("prior_act")), + "legal_keywords": _sorted_strings(bo.get("Legal_Keywords") or bo.get("legal_keywords")), + "amount": _sanitize(bo.get("amount") if bo.get("amount") is not None else core.get("Amount")), + "date": core.get("BehaviorTime") or bo.get("date"), + "core_field_base": { + key: _sanitize(core.get(key)) + for key in ("BehaviorTime", "Performer", "Subject", "Object", "Amount", "Action") + }, + "source_evidence_indexes": _bo_evidence_refs(bo), + "domain_payload_compact": domain, + } + + + def _compact_structure(row: dict[str, Any]) -> dict[str, Any]: + audit = _as_dict(row.get("routing_audit")) + return { + "structure_id": _text(row.get("structure_id"), 120), + "candidate_structure_type": _text(row.get("candidate_structure_type"), 120) or None, + "candidate_structure_types": _sorted_strings(audit.get("candidate_structure_types")), + "structure_label": _text(row.get("structure_label"), 300), + "source_bo_ids": _sorted_strings(row.get("source_bo_ids")), + "issue_cluster_ids": _sorted_strings(row.get("issue_cluster_ids")), + "asset_cluster_ids": _sorted_strings(row.get("asset_cluster_ids")), + "evidence_indexes": _sorted_strings(row.get("evidence_indexes")), + "linked_claim_group_ids": _sorted_strings(row.get("linked_claim_group_ids")), + "linked_liability_group_ids": _sorted_strings(row.get("linked_liability_group_ids")), + "linked_actio_signal_ids": _sorted_strings(row.get("linked_actio_signal_ids")), + "legal_effect_roles": _sorted_strings(row.get("legal_effect_roles")), + "review_required": row.get("review_required") is True, + "routing_audit": { + "blocked_exception_ids": _sorted_strings(audit.get("blocked_exception_ids")), + "deterministic_review_ids": _sorted_strings(audit.get("deterministic_review_ids")), + }, + } + + + def _compact_evidence(row: dict[str, Any]) -> dict[str, Any]: + def compact_values(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + text = _text(item, 500) + if text and text not in out: + out.append(text) + return out + + return { + "evidence_index": _evidence_id(row), + "title": _text(row.get("title") or row.get("source_title"), 500), + "doc_type": _text(row.get("doc_type"), 160) or None, + "key_facts": compact_values(row.get("key_facts")), + "key_dates": compact_values(row.get("key_dates")), + "key_amounts": compact_values(row.get("key_amounts")), + "source_pointer": _sanitize(row.get("source_pointer")) if isinstance(row.get("source_pointer"), dict) else {}, + "authentication": _sanitize(row.get("authentication") or row.get("authentication_status")), + } + + + def _signal_rows(doc: dict[str, Any], root: str, schema: str) -> list[dict[str, Any]]: + if doc.get("schema_version") != schema: + raise ValueError(f"{root} schema_version mismatch") + if doc.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError(f"{root} status invalid") + rows = doc.get(root) + if not isinstance(rows, list): + raise ValueError(f"{root} must be an array") + return [row for row in rows if isinstance(row, dict)] + + + def _compact_signal(row: dict[str, Any], signal_type: str) -> dict[str, Any]: + return { + "signal_type": signal_type, + "signal_id": _text(row.get("signal_id") or row.get("id"), 120) or None, + "candidate_structure_types": _sorted_strings(row.get("candidate_structure_types") or row.get("candidate_structure_type")), + "linked_structure_ids": _sorted_strings(row.get("linked_structure_ids")), + "related_evidence_indexes": _sorted_strings(row.get("related_evidence_indexes") or row.get("evidence_indexes")), + "party_roles": _sanitize(_as_list(row.get("party_roles"))), + "actio_role_hints": _sanitize(_as_list(row.get("actio_role_hints"))), + "claim_group_candidates": _sanitize(_as_list(row.get("claim_group_candidates")) + _strings(row.get("claim_group_id"))), + "liability_group_candidates": _sanitize(_as_list(row.get("liability_group_candidates")) + _strings(row.get("liability_group_id"))), + } + + + def _signal_bo_ids(row: dict[str, Any], signal_type: str) -> list[str]: + if signal_type == "legal_effect": + return _strings(row.get("bo_id") or row.get("related_bo_ids")) + return _strings(row.get("related_bo_ids") or row.get("source_bo_ids") or row.get("bo_id")) + + + def _review_for_bo(queue_item: dict[str, Any], bo_id: str) -> dict[str, Any]: + raw_type = _text(queue_item.get("review_type"), 120) + if raw_type == "blocked_exception": + review_type = "blocked_exception" + elif raw_type in {"deterministic_review", "legal_structure_deterministic_review"}: + review_type = "deterministic_review" + elif raw_type in {"review_sensitive_split", "split_review"}: + review_type = "review_sensitive_split" + else: + review_type = "structure_review" + structure_ids = _strings(queue_item.get("structure_ids") or queue_item.get("structure_id")) + return { + "review_type": review_type, + "review_id": _text(queue_item.get("review_id"), 120) or None, + "exception_id": _text(queue_item.get("exception_id"), 120) or None, + "review_code": _text(queue_item.get("review_code"), 160) or None, + "reason_code": _text(queue_item.get("reason_code") or queue_item.get("block_reason"), 160) or None, + "downstream_owner": _text(queue_item.get("downstream_owner"), 80) or ("HUMAN_REVIEW" if review_type == "blocked_exception" else "STAGE_2"), + "structure_ids": sorted(set(structure_ids)), + "blocking": review_type == "blocked_exception" or queue_item.get("blocking") is True, + "source_bo_id": bo_id, + } + + + def main() -> None: + _init_mcp_session() + docs = {key: _read_json(path) for key, path in INPUT_FILES.items()} + input_digests = {path: _digest(docs[key]) for key, path in INPUT_FILES.items()} + run_fingerprint = _digest({"contract": "stage1_fact_ledger_run.v3", "input_digests": input_digests}) + + bo_rows = docs["bo"] + if not isinstance(bo_rows, list) or not bo_rows: + raise ValueError("BO.json must be a non-empty array") + bo_by_id: dict[str, Any] = {} + for row in bo_rows: + if not isinstance(row, dict): + raise ValueError("BO.json contains a non-object item") + bo_id = _bo_id(row) + if not bo_id or bo_id in bo_by_id: + raise ValueError(f"BO ID missing or duplicated: {bo_id}") + bo_by_id[bo_id] = _compact_bo(row) + bo_ids = set(bo_by_id) + + structures_doc = _as_dict(docs["structures"]) + if structures_doc.get("schema_version") != "stage1_legal_effect_structures.v1": + raise ValueError("legal_effect_structures schema_version mismatch") + if structures_doc.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("legal_effect_structures status invalid") + structures_raw = structures_doc.get("legal_effect_structures") + indexes = structures_doc.get("structure_index") + quality_gate = structures_doc.get("quality_gate") + if not isinstance(structures_raw, list) or not isinstance(indexes, dict) or not isinstance(quality_gate, dict): + raise ValueError("legal_effect_structures required roots missing") + for key in STRUCTURE_INDEX_KEYS: + if not isinstance(indexes.get(key), dict): + raise ValueError(f"structure_index.{key} missing") + for gate_name in ("bo_coverage", "evidence_coverage"): + if _as_dict(quality_gate.get(gate_name)).get("coverage_pass") is not True: + raise ValueError(f"quality_gate.{gate_name} failed") + + structure_by_id: dict[str, Any] = {} + expected_by_bo: dict[str, list[str]] = {bo_id: [] for bo_id in bo_ids} + for raw in structures_raw: + if not isinstance(raw, dict): + raise ValueError("legal_effect_structures contains a non-object item") + compact = _compact_structure(raw) + sid = compact["structure_id"] + if not sid or sid in structure_by_id: + raise ValueError(f"structure ID missing or duplicated: {sid}") + unknown_bos = sorted(set(compact["source_bo_ids"]) - bo_ids) + if unknown_bos: + raise ValueError(f"structure {sid} has unknown BO refs: {unknown_bos}") + structure_by_id[sid] = compact + for bo_id in compact["source_bo_ids"]: + expected_by_bo[bo_id].append(sid) + + by_bo = _as_dict(indexes.get("by_bo_id")) + for bo_id in sorted(bo_ids): + indexed = _sorted_strings(_as_dict(by_bo.get(bo_id)).get("structure_ids")) + expected = sorted(set(expected_by_bo.get(bo_id, []))) + if indexed != expected: + raise ValueError(f"structure_index.by_bo_id mismatch: {bo_id}") + + evidence_doc = _as_dict(docs["evidence"]) + evidence_rows = evidence_doc.get("items") or evidence_doc.get("evidence_indexed") + if not isinstance(evidence_rows, list) or not evidence_rows: + raise ValueError("evidence_indexed conservation array missing") + evidence_by_id: dict[str, Any] = {} + for raw in evidence_rows: + if not isinstance(raw, dict): + raise ValueError("evidence_indexed contains a non-object item") + compact = _compact_evidence(raw) + eid = compact["evidence_index"] + if not eid or eid in evidence_by_id: + raise ValueError(f"evidence ID missing or duplicated: {eid}") + evidence_by_id[eid] = compact + evidence_ids = set(evidence_by_id) + for sid, structure in structure_by_id.items(): + unknown = sorted(set(structure["evidence_indexes"]) - evidence_ids) + if unknown: + raise ValueError(f"structure {sid} has unknown evidence refs: {unknown}") + + signal_specs = ( + ("actio", _as_dict(docs["actio"]), "actio_case_signals", "actio_case_signals.v1"), + ("case_liability", _as_dict(docs["liability"]), "case_liability_signals", "case_liability_signals.v1"), + ("legal_effect", _as_dict(docs["legal_effect"]), "bo_legal_effect_routes", "legal_effect_signals.v1"), + ) + signal_hints: dict[str, dict[str, list[dict[str, Any]]]] = { + bo_id: {"actio": [], "case_liability": [], "legal_effect": []} for bo_id in bo_ids + } + source_schema_versions = { + "BO.json": "array", + "legal_effect_structures.json": structures_doc.get("schema_version"), + "evidence_indexed.json": evidence_doc.get("schema_contract_version") or evidence_doc.get("schema_version") or "conservation-array", + } + for signal_type, doc, root, schema in signal_specs: + rows = _signal_rows(doc, root, schema) + source_schema_versions[INPUT_FILES[{"actio": "actio", "case_liability": "liability", "legal_effect": "legal_effect"}[signal_type]]] = schema + for raw in rows: + compact = _compact_signal(raw, signal_type) + for bo_id in _signal_bo_ids(raw, signal_type): + if bo_id in signal_hints: + signal_hints[bo_id][signal_type].append(compact) + for bo_id in signal_hints: + for signal_type in signal_hints[bo_id]: + signal_hints[bo_id][signal_type].sort(key=_canonical) + + structure_refs_by_bo: dict[str, Any] = {} + for bo_id in sorted(bo_ids): + row = _as_dict(by_bo.get(bo_id)) + structure_refs_by_bo[bo_id] = { + "structure_ids": _sorted_strings(row.get("structure_ids")), + "issue_cluster_ids": _sorted_strings(row.get("issue_cluster_ids")), + "asset_cluster_ids": _sorted_strings(row.get("asset_cluster_ids")), + "evidence_index_ids": _sorted_strings(row.get("evidence_indexes")), + "candidate_structure_types": _sorted_strings(row.get("candidate_structure_types")), + "claim_group_ids": _sorted_strings(row.get("claim_group_ids")), + "liability_group_ids": _sorted_strings(row.get("liability_group_ids")), + "downstream_fact_roles": _sorted_strings(row.get("legal_effect_roles") or row.get("downstream_fact_roles")), + } + + upstream_reviews: dict[str, list[dict[str, Any]]] = {bo_id: [] for bo_id in bo_ids} + queue = _as_list(quality_gate.get("review_queue")) + for raw in queue: + if not isinstance(raw, dict): + continue + for bo_id in _strings(raw.get("source_bo_ids") or raw.get("source_bo_id")): + if bo_id in upstream_reviews: + upstream_reviews[bo_id].append(_review_for_bo(raw, bo_id)) + for sid, structure in structure_by_id.items(): + if structure.get("review_required") is not True: + continue + for bo_id in structure["source_bo_ids"]: + upstream_reviews[bo_id].append(_review_for_bo({"review_type": "structure_review", "structure_id": sid}, bo_id)) + for bo_id, rows in upstream_reviews.items(): + unique = {_canonical(row): row for row in rows} + upstream_reviews[bo_id] = sorted(unique.values(), key=lambda row: (_text(row.get("review_type")), _text(row.get("review_id") or row.get("exception_id")), _canonical(row))) + + warnings: list[dict[str, Any]] = [] + for bo_id, bo in bo_by_id.items(): + for eid in bo["source_evidence_indexes"]: + if eid not in evidence_ids: + warnings.append({"code": "UNKNOWN_BO_EVIDENCE_REF", "source_bo_id": bo_id, "evidence_index": eid}) + review_count = sum(len(rows) for rows in upstream_reviews.values()) + blocking_count = sum(1 for rows in upstream_reviews.values() for row in rows if row.get("blocking") is True) + status = "READY_WITH_REVIEW" if review_count or warnings else "READY" + source_pack = { + "schema_version": "stage1_fact_source_pack.v3", + "status": status, + "run_fingerprint": run_fingerprint, + "input_manifest": {"files": input_digests, "source_schema_versions": source_schema_versions}, + "bo_by_id": {bo_id: bo_by_id[bo_id] for bo_id in sorted(bo_by_id)}, + "structure_by_id": {sid: structure_by_id[sid] for sid in sorted(structure_by_id)}, + "structure_refs_by_bo_id": structure_refs_by_bo, + "signal_hints_by_bo_id": {bo_id: signal_hints[bo_id] for bo_id in sorted(signal_hints)}, + "evidence_authority_by_index": {eid: evidence_by_id[eid] for eid in sorted(evidence_by_id)}, + "upstream_review_by_bo_id": {bo_id: upstream_reviews[bo_id] for bo_id in sorted(upstream_reviews)}, + "reference_universe": { + "bo_ids": sorted(bo_ids), + "structure_ids": sorted(structure_by_id), + "evidence_indexes": sorted(evidence_ids), + }, + "source_integrity": { + "warnings": sorted(warnings, key=_canonical), + "warning_count": len(warnings), + "reference_validation_pass": True, + }, + "source_counts": { + "bo_count": len(bo_by_id), + "structure_count": len(structure_by_id), + "evidence_count": len(evidence_by_id), + "review_count": review_count, + "blocking_review_count": blocking_count, + }, + } + payload = {"fact_source_pack": source_pack} + _write_json(OUT_PATH, payload) + _verify_reread(OUT_PATH, payload) + print(_canonical({"fact_source_pack_manifest": { + "schema_version": "stage1_fact_source_pack_manifest.v2", + "status": status, + "path": OUT_PATH, + "sha256": _digest(payload), + "run_fingerprint": run_fingerprint, + "bo_count": len(bo_by_id), + "structure_count": len(structure_by_id), + "evidence_count": len(evidence_by_id), + "review_count": review_count, + }})) + + + try: + main() + except Exception as exc: + print(_canonical({"fact_source_pack_manifest": { + "schema_version": "stage1_fact_source_pack_manifest.v2", + "status": "FAILED", + "error": _safe_error(exc), + }})) + raise + + - task_name: Task_FL1_deterministic_fact_ledger_candidate_builder + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import copy + import hashlib + import itertools + import json + import re + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _safe_error(exc: BaseException) -> str: + return re.sub(r"\s+", " ", str(exc)).strip()[:300] + + + def _parse_json_value(raw: Any, label: str, allow_trailing: bool = False) -> Any: + if isinstance(raw, (dict, list)): + return raw + if not isinstance(raw, str): + raise ValueError(f"{label} is not JSON text") + text = raw.lstrip("\ufeff").strip() + if not text: + raise ValueError(f"{label} is empty") + try: + return json.loads(text) + except json.JSONDecodeError: + try: + value, end = JSON_DECODER.raw_decode(text) + except json.JSONDecodeError as exc: + raise ValueError(f"{label} invalid JSON: {text[:300]}") from exc + if not allow_trailing and text[end:].strip(): + raise ValueError(f"{label} has trailing content: {text[end:end + 300]}") + return value + + + def _parse_mcp_response(text: str) -> dict[str, Any]: + parsed: dict[str, Any] | None = None + for line in text.strip().splitlines(): + if not line.startswith("data: "): + continue + try: + item = _parse_json_value(line[6:], "MCP SSE data", True) + except ValueError: + continue + if isinstance(item, dict): + parsed = item + if parsed is None: + item = _parse_json_value(text, "MCP response", True) + if not isinstance(item, dict): + raise RuntimeError("MCP response root is not an object") + parsed = item + return parsed + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-fact-ledger-fl1-candidate-builder", + "version": "2.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_mcp_response(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP initialize failed: {payload['error']}") + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + initialized = CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ) + initialized.raise_for_status() + + + def _call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_msg_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_mcp_response(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP {name} failed: {payload['error']}") + result = payload.get("result") + if not isinstance(result, dict) or result.get("isError") is True: + raise RuntimeError(f"MCP {name} returned an invalid result") + return result + + + def _extract_doc(result: dict[str, Any], doc_name: str) -> Any: + blocks = result.get("content") or [] + if not blocks or not isinstance(blocks[0], dict): + raise RuntimeError(f"Empty response: {doc_name}") + text = blocks[0].get("text") + if not isinstance(text, str) or not text.strip(): + raise RuntimeError(f"Empty text response: {doc_name}") + outer = _parse_json_value(text, f"{doc_name} outer envelope", True) + inner: Any = outer + if isinstance(outer, dict) and "results" in outer: + results = outer.get("results") or [] + if not results or not isinstance(results[0], dict): + raise RuntimeError(f"Empty results envelope: {doc_name}") + inner = results[0].get("content") + if inner in (None, ""): + inner = results[0].get("text") + if isinstance(inner, (dict, list)): + return inner + return _parse_json_value(inner, f"{doc_name} inner document", False) + + + def _read_json(path: str) -> Any: + return _extract_doc(_call_tool("read_docs", {"doc_names": [path]}), path) + + + def _write_text(path: str, content: str) -> None: + _call_tool("write_file", {"path": path, "content": content, "overwrite": True}) + + + def _canonical(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + + def _digest(value: Any) -> str: + return "sha256:" + hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest() + + + def _write_json(path: str, value: Any) -> None: + _write_text(path, json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2)) + + + def _verify_reread(path: str, expected: Any) -> None: + actual = _read_json(path) + if _digest(actual) != _digest(expected): + raise RuntimeError(f"Post-write verification failed: {path}") + + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _strings(value: Any) -> list[str]: + values = value if isinstance(value, list) else [value] + out: list[str] = [] + for item in values: + if item is None: + continue + if isinstance(item, str): + text = item.strip() + elif isinstance(item, (int, float)) and not isinstance(item, bool): + text = str(item) + else: + continue + if text and text not in out: + out.append(text) + return out + + + def _sorted_strings(value: Any) -> list[str]: + return sorted(set(_strings(value))) + + + def _text(value: Any, limit: int = 500) -> str: + if value in (None, "", [], {}): + return "" + raw = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, sort_keys=True) + return re.sub(r"\s+", " ", raw).strip()[:limit] + + SOURCE_PACK_PATH = "stage1_tmp/fact_ledger/fact_source_pack.json" + CANDIDATE_PATH = "stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json" + EXCEPTION_MANIFEST_PATH = "stage1_tmp/fact_ledger/fact_exception_manifest.json" + INPUT_PART_DIR = "stage1_tmp/fact_ledger/fact_exception_input_parts" + OUTPUT_PART_DIR = "stage1_tmp/fact_ledger/fact_exception_adjudication_parts" + MAX_EXCEPTION_ITEMS_PER_PACK = 4 + TARGET_EXCEPTION_ITEMS_PER_PACK = 3 + MAX_EXCEPTION_ITEM_BYTES = 12000 + MAX_EXCEPTION_PACK_BYTES = 25000 + MAX_EXCEPTION_PACKS = 8 + MAX_CONCURRENCY = 2 + FINAL_FIELDS = [ + "fact_id", "source_bo_id", "type", "date", "parties", "object_spec", "amount", "action", + "evidence_refs", "credibility", "legal_centrality", "proof_strength", "legal_effect_roles", + "claim_chain_ref", "linked_structures", "must_not_drop_in_claim_types", "must_consider", + "state_context", "money_claim_effect", "commercial_successor_effect", "actio_roles", + "linked_actio_structures", "downstream_module_candidates", "skeleton_validation_codes", + "derived_fact", "derivation_basis", "legal_calculation_object", + ] + FINAL_FIELD_SET = set(FINAL_FIELDS) + STRUCTURE_TYPE_TO_MODULE = { + "money_claim": "money_claim", + "commercial_successor": "commercial_successor", + "secured_debt": "secured_registry", + "registry_invalidity": "secured_registry", + "land_use_gain": "land_valuation", + "valuation": "land_valuation", + "fraudulent_transfer": "actio", + "preserved_claim": "actio", + "succession_notice_lien_asset_defense": "succession_notice_lien_asset_defense", + "general_legal_effect": "legal_effect", + } + STRUCTURE_TYPE_TO_ROLE = { + "money_claim": "claim_chain_ref", + "commercial_successor": "claim_chain_ref", + "secured_debt": "linked_structures", + "registry_invalidity": "linked_structures", + "land_use_gain": "legal_calculation_object", + "valuation": "legal_calculation_object", + "fraudulent_transfer": "actio_roles", + "preserved_claim": "actio_roles", + "succession_notice_lien_asset_defense": "linked_structures", + "general_legal_effect": "legal_effect_roles", + } + CLOSED_EXCEPTION_TYPES = { + "conflicting_fact_projection", + "conflicting_calculation_projection", + "conflicting_asset_projection", + "conflicting_structure_role_projection", + } + CLOSED_TARGET_FIELDS = { + "date", "amount", "action", "object_spec", "derived_fact", "claim_chain_ref", + "linked_structures", "state_context", "legal_calculation_object", + } + + + def _first(*values: Any) -> Any: + for value in values: + if value not in (None, "", [], {}): + return value + return None + + + def _add_code(row: dict[str, Any], code: str) -> None: + codes = _strings(row.get("skeleton_validation_codes")) + if code not in codes: + codes.append(code) + row["skeleton_validation_codes"] = sorted(codes) + + + def _score_evidence(rows: list[dict[str, Any]]) -> tuple[str, str, str]: + if not rows: + return "low", "low", "NO_EVIDENCE" + texts = " ".join( + " ".join([_text(row.get("title"), 200), _text(row.get("doc_type"), 100)] + _strings(row.get("key_facts"))) + for row in rows + ).lower() + meeting = any(marker in texts for marker in ("meeting", "회의", "면담")) + documentary = any(marker in texts for marker in ("등기", "계약", "판결", "결정", "공문", "금융", "송금", "계좌", "registry", "contract", "judgment")) + has_structured_fact = any(_as_list(row.get("key_facts")) for row in rows) + has_pointer = any(isinstance(row.get("source_pointer"), dict) and row.get("source_pointer") for row in rows) + if meeting and not documentary and all("meeting" in _text(row.get("doc_type"), 100).lower() or "회의" in _text(row.get("doc_type"), 100) or "면담" in _text(row.get("doc_type"), 100) for row in rows): + return "medium", "meeting_only_core", "MEETING_ONLY" + if documentary and has_structured_fact and has_pointer: + return "high", "documentary", "DOCUMENTARY_STRUCTURED" + if len(rows) >= 2 and has_structured_fact: + return "high", "high", "MULTI_SOURCE_STRUCTURED" + return "medium", "medium", "SINGLE_STRUCTURED_SOURCE" + + + def _linked_view(linked: dict[str, Any]) -> dict[str, Any]: + return { + "structure_ids": _sorted_strings(linked.get("structure_ids")), + "issue_cluster_ids": _sorted_strings(linked.get("issue_cluster_ids")), + "asset_cluster_ids": _sorted_strings(linked.get("asset_cluster_ids")), + "evidence_index_ids": _sorted_strings(linked.get("evidence_index_ids")), + "candidate_structure_types": _sorted_strings(linked.get("candidate_structure_types")), + "claim_group_ids": _sorted_strings(linked.get("claim_group_ids")), + "liability_group_ids": _sorted_strings(linked.get("liability_group_ids")), + } + + + def _legal_roles(types: list[str], structures: list[dict[str, Any]], signals: dict[str, Any]) -> list[str]: + roles: list[str] = [] + for structure in structures: + roles.extend(_strings(structure.get("legal_effect_roles"))) + roles.extend(STRUCTURE_TYPE_TO_ROLE.get(item, "legal_effect_roles") for item in types) + if _as_list(signals.get("actio")): + roles.append("actio_roles") + if _as_list(signals.get("case_liability")): + roles.append("claim_chain_ref") + if _as_list(signals.get("legal_effect")): + roles.append("legal_effect_roles") + return sorted(set(roles)) + + + def _modules(types: list[str], signals: dict[str, Any]) -> list[str]: + modules = [STRUCTURE_TYPE_TO_MODULE.get(item, "legal_effect") for item in types] + if _as_list(signals.get("actio")): + modules.append("actio") + if _as_list(signals.get("case_liability")): + modules.append("claim_liability") + return sorted(set(modules)) + + + def _parties(bo: dict[str, Any], signals: dict[str, Any]) -> list[dict[str, Any]]: + core = _as_dict(bo.get("core_field_base")) + out: list[dict[str, Any]] = [] + for role, field in (("actor", "Performer"), ("counterparty", "Subject")): + value = _text(core.get(field), 160) + if value: + out.append({"party": value, "role": role, "source": "BO.core_field_base"}) + for signal in _as_list(signals.get("case_liability")): + for item in _as_list(_as_dict(signal).get("party_roles")): + if not isinstance(item, dict): + continue + party = _text(item.get("party") or item.get("name"), 160) + if party: + out.append({"party": party, "role": _text(item.get("role"), 80), "source": "case_liability_signals"}) + unique = {(row["party"], row["role"]): row for row in out} + return [unique[key] for key in sorted(unique)] + + + def _claim_groups(linked: dict[str, Any], signals: dict[str, Any]) -> dict[str, Any]: + claims = _strings(linked.get("claim_group_ids")) + liabilities = _strings(linked.get("liability_group_ids")) + for signal in _as_list(signals.get("case_liability")): + claims.extend(_strings(_as_dict(signal).get("claim_group_candidates"))) + liabilities.extend(_strings(_as_dict(signal).get("liability_group_candidates"))) + return { + "claim_group_ids": sorted(set(claims)), + "liability_group_ids": sorted(set(liabilities)), + "signal_count": len(_as_list(signals.get("case_liability"))), + } + + + def _fallback_derived(bo: dict[str, Any], row: dict[str, Any]) -> str: + if _text(row.get("action"), 900): + return _text(row.get("action"), 900) + core = _as_dict(bo.get("core_field_base")) + fields = [core.get(key) for key in ("BehaviorTime", "Performer", "Subject", "Object", "Action", "Amount")] + parts = [_text(value, 240) for value in fields] + return " ".join(value for value in parts if value).strip() + + + def _review_template(code: str, row: dict[str, Any], reason: str, severity: str = "WARNING", owner: str = "STAGE_2") -> dict[str, Any]: + return { + "review_id": None, + "review_code": code, + "severity": severity, + "fact_id": row["fact_id"], + "source_bo_id": row["source_bo_id"], + "structure_ids": _strings(_as_dict(row.get("linked_structures")).get("structure_ids")), + "evidence_refs": _strings(row.get("evidence_refs")), + "reason_code": reason, + "downstream_owner": owner, + } + + + def _build_row(index: int, bo_id: str, source: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]], list[dict[str, Any]], str]: + bo = _as_dict(_as_dict(source.get("bo_by_id")).get(bo_id)) + linked = _linked_view(_as_dict(_as_dict(source.get("structure_refs_by_bo_id")).get(bo_id))) + signals = _as_dict(_as_dict(source.get("signal_hints_by_bo_id")).get(bo_id)) + structure_map = _as_dict(source.get("structure_by_id")) + structures = [_as_dict(structure_map.get(sid)) for sid in linked["structure_ids"] if isinstance(structure_map.get(sid), dict)] + types = sorted(set(linked["candidate_structure_types"] + [str(row.get("candidate_structure_type")) for row in structures if row.get("candidate_structure_type")])) + evidence_refs = sorted(set(_strings(bo.get("source_evidence_indexes")) + linked["evidence_index_ids"])) + evidence_map = _as_dict(source.get("evidence_authority_by_index")) + evidence_rows = [_as_dict(evidence_map.get(eid)) for eid in evidence_refs if isinstance(evidence_map.get(eid), dict)] + credibility, proof_strength, score_basis = _score_evidence(evidence_rows) + core = _as_dict(bo.get("core_field_base")) + modules = _modules(types, signals) + row = { + "fact_id": f"F-{index:03d}", + "source_bo_id": bo_id, + "type": _first(bo.get("bo_type"), types[0] if types else None), + "date": _first(core.get("BehaviorTime"), bo.get("date")), + "parties": _parties(bo, signals), + "object_spec": _first(core.get("Object"), json.dumps(linked["asset_cluster_ids"], ensure_ascii=False, sort_keys=True)), + "amount": _text(_first(bo.get("amount"), core.get("Amount")), 500) or None, + "action": _first(bo.get("action"), core.get("Action")), + "evidence_refs": evidence_refs, + "credibility": credibility, + "legal_centrality": "critical" if linked["structure_ids"] or _as_list(signals.get("actio")) else "medium", + "proof_strength": proof_strength, + "legal_effect_roles": _legal_roles(types, structures, signals), + "claim_chain_ref": _claim_groups(linked, signals), + "linked_structures": linked, + "must_not_drop_in_claim_types": modules if linked["structure_ids"] or _as_list(signals.get("actio")) else [], + "must_consider": bool(linked["structure_ids"] or _as_list(signals.get("actio"))), + "state_context": {"bo_type": bo.get("bo_type"), "issue_cluster_ids": linked["issue_cluster_ids"], "asset_cluster_ids": linked["asset_cluster_ids"]}, + "money_claim_effect": {"enabled": "money_claim" in types, "source_bo_id": bo_id if "money_claim" in types else None}, + "commercial_successor_effect": {"enabled": "commercial_successor" in types, "source_bo_id": bo_id if "commercial_successor" in types else None}, + "actio_roles": copy.deepcopy(_as_list(signals.get("actio"))), + "linked_actio_structures": { + "structure_ids": sorted(s["structure_id"] for s in structures if s.get("candidate_structure_type") in {"fraudulent_transfer", "preserved_claim"}), + "actio_signal_count": len(_as_list(signals.get("actio"))), + }, + "downstream_module_candidates": modules, + "skeleton_validation_codes": [], + "derived_fact": "", + "derivation_basis": "", + "legal_calculation_object": {}, + } + needs_calc = any(item in {"money_claim", "secured_debt", "land_use_gain", "valuation", "fraudulent_transfer"} for item in types) + row["legal_calculation_object"] = { + "calculation_required": needs_calc, + "amount_source": "BO.amount" if row.get("amount") else None, + "amount_text": row.get("amount"), + "object_basis": row.get("object_spec"), + "candidate_structure_types": types, + } + row["derived_fact"] = _fallback_derived(bo, row) + basis = [f"source_bo_id={bo_id}"] + if evidence_refs: + basis.append("evidence_refs=" + ",".join(evidence_refs)) + if linked["structure_ids"]: + basis.append("structure_ids=" + ",".join(linked["structure_ids"])) + row["derivation_basis"] = "; ".join(basis) + reviews: list[dict[str, Any]] = [] + if not row["derived_fact"]: + _add_code(row, "EMPTY_DERIVED_FACT") + _add_code(row, "EMPTY_DERIVED_FACT_AFTER_FALLBACK") + row["must_consider"] = True + reviews.append(_review_template("EMPTY_DERIVED_FACT_AFTER_FALLBACK", row, "NO_SOURCE_BACKED_FALLBACK")) + if not evidence_refs: + _add_code(row, "NO_EVIDENCE_REFS") + if not linked["structure_ids"]: + _add_code(row, "NO_LINKED_LEGAL_EFFECT_STRUCTURE") + if needs_calc and not row.get("amount") and not row.get("object_spec"): + _add_code(row, "CALCULATION_BASIS_MISSING_STAGE2") + reviews.append(_review_template("CALCULATION_BASIS_MISSING_STAGE2", row, "CALCULATION_SOURCE_DEFERRED")) + + upstream = _as_list(_as_dict(source.get("upstream_review_by_bo_id")).get(bo_id)) + for item in upstream: + review = _as_dict(item) + review_type = review.get("review_type") + if review_type == "blocked_exception": + code, severity, owner = "LES_BLOCKED_EXCEPTION_REVIEW", "BLOCK_FINAL_DRAFTING", "HUMAN_REVIEW" + elif review_type == "deterministic_review": + code, severity, owner = "LES_DETERMINISTIC_REVIEW", "WARNING", "STAGE_2" + elif review_type == "review_sensitive_split": + code, severity, owner = "LES_REVIEW_SENSITIVE_SPLIT", "WARNING", "STAGE_2" + else: + code, severity, owner = "LES_REVIEW_REQUIRED", "WARNING", "STAGE_2" + _add_code(row, code) + row["must_consider"] = True + reviews.append(_review_template(code, row, _text(review.get("reason_code") or review_type, 160), severity, owner)) + return row, reviews, evidence_rows, score_basis + + + def _clean_date(value: Any) -> str: + return _text(value, 40) + + + def _periods_overlap(a_start: str, a_end: str, b_start: str, b_end: str) -> bool: + a1, a2 = a_start or "0000-00-00", a_end or "9999-99-99" + b1, b2 = b_start or "0000-00-00", b_end or "9999-99-99" + return max(a1, b1) <= min(a2, b2) + + + def _domain_scans(rows: list[dict[str, Any]], source: dict[str, Any]) -> list[dict[str, Any]]: + reviews: list[dict[str, Any]] = [] + bo_map = _as_dict(source.get("bo_by_id")) + possession_entries: list[tuple[dict[str, Any], set[str], str, str, list[dict[str, Any]]]] = [] + receipt_by_notice: dict[str, str] = {} + for row in rows: + payload = _as_dict(_as_dict(bo_map.get(row["source_bo_id"])).get("domain_payload_compact")) + possession = _as_dict(payload.get("possession_state")) + events = [item for item in _as_list(possession.get("possession_events")) if isinstance(item, dict)] + causes = [item for item in _as_list(possession.get("legal_cause_assertions")) if isinstance(item, dict)] + assets = set(_strings(possession.get("asset_ref")) + _strings(_as_dict(row.get("state_context")).get("asset_cluster_ids"))) + if events and not causes: + _add_code(row, "LEGAL_CAUSE_STATE_UNKNOWN_REVIEW") + reviews.append(_review_template("LEGAL_CAUSE_STATE_UNKNOWN_REVIEW", row, "POSSESSION_WITHOUT_LEGAL_CAUSE")) + cause_names = {_text(item.get("cause_asserted"), 120) for item in causes if _text(item.get("cause_asserted"), 120)} + conflict = any("무권원" in left and any(term in right for term in ("유치권", "임대차", "사용대차")) for left in cause_names for right in cause_names if left != right) + if conflict: + _add_code(row, "POSSESSION_CAUSE_CONFLICT") + reviews.append(_review_template("POSSESSION_CAUSE_CONFLICT", row, "INCOMPATIBLE_LEGAL_CAUSE_ASSERTIONS", "HARD_WARNING", "HUMAN_REVIEW")) + for event in events: + possession_entries.append((row, assets, _clean_date(event.get("start_date")), _clean_date(event.get("end_date")), causes)) + + notice = _as_dict(payload.get("notice_lifecycle")) + notice_id = _text(notice.get("notice_id"), 120) + received = _clean_date(notice.get("received_date")) or _clean_date(notice.get("arrival_date")) + if notice_id and received: + receipt_by_notice[notice_id] = min(received, receipt_by_notice.get(notice_id, "9999-99-99")) + if notice.get("requires_receipt_for_effect") is True and not received: + _add_code(row, "NOTICE_RECEIPT_UNVERIFIED") + reviews.append(_review_template("NOTICE_RECEIPT_UNVERIFIED", row, "RECEIPT_DATE_REQUIRED", "BLOCK_FINAL_DRAFTING", "HUMAN_REVIEW")) + if notice.get("requires_receipt_for_effect") is True and received and not _strings(notice.get("receipt_evidence_refs")) and not _text(notice.get("recipient_admission_text"), 300): + _add_code(row, "NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW") + reviews.append(_review_template("NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW", row, "RECEIPT_SUPPORT_WEAK")) + + for row in rows: + payload = _as_dict(_as_dict(bo_map.get(row["source_bo_id"])).get("domain_payload_compact")) + lien = _as_dict(payload.get("lien_state")) + if not lien: + continue + dates = [receipt_by_notice[ref] for ref in _strings(lien.get("extinction_notice_refs")) if ref in receipt_by_notice] + lien_assets = set(_strings(lien.get("asset_ref"))) + if not dates or not lien_assets: + continue + receipt = min(dates) + for possession_row, assets, start, _, _ in possession_entries: + if assets & lien_assets and start and start > receipt: + _add_code(possession_row, "LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW") + reviews.append(_review_template("LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW", possession_row, "POSSESSION_AFTER_NOTICE_RECEIPT", "HARD_WARNING", "HUMAN_REVIEW")) + return reviews + + + def _walk_explicit_candidates(value: Any, path: str = "") -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + if isinstance(value, dict): + for key, child in value.items(): + child_path = f"{path}.{key}" if path else str(key) + if key in {"candidate_values", "projection_candidates", "conflict_candidates"} and isinstance(child, list): + for item in child: + if isinstance(item, dict): + candidate = copy.deepcopy(item) + candidate["_path"] = child_path + out.append(candidate) + else: + out.extend(_walk_explicit_candidates(child, child_path)) + elif isinstance(value, list): + for index, child in enumerate(value): + out.extend(_walk_explicit_candidates(child, f"{path}[{index}]")) + return out + + + def _true_exceptions(row: dict[str, Any], source: dict[str, Any]) -> list[dict[str, Any]]: + bo = _as_dict(_as_dict(source.get("bo_by_id")).get(row["source_bo_id"])) + payload = _as_dict(bo.get("domain_payload_compact")) + explicit = _walk_explicit_candidates(payload) + grouped: dict[str, list[dict[str, Any]]] = {} + for item in explicit: + field = _text(item.get("field") or item.get("target_field"), 120) + refs = sorted(set(_strings(item.get("source_refs")) + _strings(item.get("evidence_refs")) + _strings(item.get("structure_refs")))) + if field not in CLOSED_TARGET_FIELDS or not refs or "value" not in item: + continue + grouped.setdefault(field, []).append({ + "field": field, + "value": copy.deepcopy(item.get("value")), + "basis_codes": _sorted_strings(item.get("basis_codes")) or ["EXPLICIT_SOURCE_BACKED_CANDIDATE"], + "source_refs": refs, + }) + out: list[dict[str, Any]] = [] + for field, candidates in sorted(grouped.items()): + unique_values = {_canonical(item["value"]) for item in candidates} + source_sets = {tuple(item["source_refs"]) for item in candidates} + if len(unique_values) < 2 or len(source_sets) < 2: + continue + if field in {"amount", "legal_calculation_object"}: + exception_type = "conflicting_calculation_projection" + elif field in {"object_spec", "state_context"}: + exception_type = "conflicting_asset_projection" + elif field in {"claim_chain_ref", "linked_structures"}: + exception_type = "conflicting_structure_role_projection" + else: + exception_type = "conflicting_fact_projection" + out.append({ + "exception_id": None, + "exception_type": exception_type, + "fact_id": row["fact_id"], + "source_bo_id": row["source_bo_id"], + "target_fields": [field], + "candidate_values": sorted(candidates, key=lambda item: (_canonical(item["value"]), _canonical(item["source_refs"]))), + "candidate_fact_subset": {field: copy.deepcopy(row.get(field))}, + "structure_refs": _strings(_as_dict(row.get("linked_structures")).get("structure_ids")), + "evidence_facts": [], + "allowed_output_fields": [field], + "allowed_decisions": ["KEEP_AS_IS", "PATCH", "BLOCK_REVIEW"], + "conflict_codes": ["EXPLICIT_SOURCE_BACKED_CONFLICT"], + }) + return out + + + def _assign_reviews(items: list[dict[str, Any]]) -> list[dict[str, Any]]: + unique: dict[str, dict[str, Any]] = {} + for item in items: + key = _canonical({key: value for key, value in item.items() if key != "review_id"}) + unique[key] = item + ordered = sorted(unique.values(), key=lambda item: (item["source_bo_id"], item["review_code"], item["reason_code"], item["fact_id"])) + for index, item in enumerate(ordered, start=1): + item["review_id"] = f"FRV-{index:03d}" + return ordered + + + def _pack_exceptions(exceptions: list[dict[str, Any]], reviews: list[dict[str, Any]], rows_by_fact: dict[str, dict[str, Any]], run_fingerprint: str) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]: + eligible: list[dict[str, Any]] = [] + for item in sorted(exceptions, key=lambda value: (value["source_bo_id"], value["exception_type"], value["target_fields"][0])): + if len(_canonical(item).encode("utf-8")) > MAX_EXCEPTION_ITEM_BYTES: + row = rows_by_fact[item["fact_id"]] + _add_code(row, "FL2_REVIEW_REQUIRED") + reviews.append(_review_template("PACK_CONTEXT_TOO_LARGE", row, "EXCEPTION_ITEM_OVER_12000_BYTES", "BLOCK_FINAL_DRAFTING", "HUMAN_REVIEW")) + else: + eligible.append(item) + for index, item in enumerate(eligible, start=1): + item["exception_id"] = f"FX-{index:03d}" + + groups: list[list[dict[str, Any]]] = [] + for exception_type in sorted(CLOSED_EXCEPTION_TYPES): + typed = [item for item in eligible if item["exception_type"] == exception_type] + current: list[dict[str, Any]] = [] + for item in typed: + trial = current + [item] + trial_bytes = len(_canonical({"items": trial}).encode("utf-8")) + if current and (len(trial) > MAX_EXCEPTION_ITEMS_PER_PACK or trial_bytes > MAX_EXCEPTION_PACK_BYTES or len(current) >= TARGET_EXCEPTION_ITEMS_PER_PACK): + groups.append(current) + current = [item] + else: + current = trial + if current: + groups.append(current) + if len(groups) > MAX_EXCEPTION_PACKS: + raise ValueError(f"exception pack count exceeds {MAX_EXCEPTION_PACKS}") + + pack_rows: list[dict[str, Any]] = [] + fanout: list[dict[str, Any]] = [] + for ordinal, items in enumerate(groups, start=1): + file_id = f"PACK-{ordinal:03d}" + pack_id = f"FL-PACK-{ordinal:03d}" + input_path = f"{INPUT_PART_DIR}/{file_id}.json" + output_path = f"{OUTPUT_PART_DIR}/{file_id}.json" + exception_ids = [item["exception_id"] for item in items] + exception_type = items[0]["exception_type"] + part = { + "schema_version": "stage1_fact_exception_input_part.v2", + "status": "READY", + "pack_id": pack_id, + "run_fingerprint": run_fingerprint, + "exception_type": exception_type, + "input_exception_ids": exception_ids, + "items": items, + "output_path": output_path, + } + serialized_bytes = len(_canonical(part).encode("utf-8")) + if serialized_bytes > MAX_EXCEPTION_PACK_BYTES: + raise ValueError(f"serialized exception pack exceeds budget: {pack_id}") + _write_json(input_path, part) + _verify_reread(input_path, part) + pack_rows.append({ + "pack_id": pack_id, + "exception_type": exception_type, + "exception_ids": exception_ids, + "input_path": input_path, + "output_path": output_path, + "serialized_bytes": serialized_bytes, + }) + fanout.append({ + "pack_id": pack_id, + "exception_type": exception_type, + "exception_ids": exception_ids, + "items": items, + "output_path": output_path, + "run_fingerprint": run_fingerprint, + }) + return eligible, pack_rows, fanout + + + def main() -> None: + _init_mcp_session() + source_outer = _read_json(SOURCE_PACK_PATH) + source = _as_dict(_as_dict(source_outer).get("fact_source_pack")) + if source.get("schema_version") != "stage1_fact_source_pack.v3": + raise ValueError("fact_source_pack schema_version mismatch") + if source.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("fact_source_pack status invalid") + run_fingerprint = _text(source.get("run_fingerprint"), 100) + if not run_fingerprint.startswith("sha256:"): + raise ValueError("fact_source_pack run_fingerprint invalid") + bo_ids = _sorted_strings(_as_dict(source.get("reference_universe")).get("bo_ids")) + if not bo_ids or bo_ids != sorted(_as_dict(source.get("bo_by_id")).keys()): + raise ValueError("source BO universe mismatch") + + rows: list[dict[str, Any]] = [] + reviews: list[dict[str, Any]] = [] + evidence_score_basis: dict[str, str] = {} + exceptions: list[dict[str, Any]] = [] + for index, bo_id in enumerate(bo_ids, start=1): + row, row_reviews, _, score_basis = _build_row(index, bo_id, source) + if set(row) != FINAL_FIELD_SET: + raise ValueError(f"candidate row schema mismatch: {bo_id}") + rows.append(row) + reviews.extend(row_reviews) + evidence_score_basis[row["fact_id"]] = score_basis + exceptions.extend(_true_exceptions(row, source)) + reviews.extend(_domain_scans(rows, source)) + rows_by_fact = {row["fact_id"]: row for row in rows} + exceptions, pack_rows, fanout = _pack_exceptions(exceptions, reviews, rows_by_fact, run_fingerprint) + reviews = _assign_reviews(reviews) + + exception_ids = [item["exception_id"] for item in exceptions] + if sorted(item for pack in pack_rows for item in pack["exception_ids"]) != sorted(exception_ids): + raise ValueError("exception pack conservation failed") + manifest_status = "READY" if exceptions else ("READY_WITH_REVIEW" if reviews else "READY_NO_EXCEPTIONS") + manifest = { + "fact_exception_manifest": { + "schema_version": "stage1_fact_exception_manifest.v2", + "status": manifest_status, + "run_fingerprint": run_fingerprint, + "expected_exception_ids": exception_ids, + "exception_count": len(exceptions), + "deterministic_review_items": reviews, + "pack_count": len(pack_rows), + "packs": pack_rows, + } + } + _write_json(EXCEPTION_MANIFEST_PATH, manifest) + _verify_reread(EXCEPTION_MANIFEST_PATH, manifest) + + candidate_bo_ids = [row["source_bo_id"] for row in rows] + missing = sorted(set(bo_ids) - set(candidate_bo_ids)) + unknown = sorted(set(candidate_bo_ids) - set(bo_ids)) + duplicates = sorted({item for item in candidate_bo_ids if candidate_bo_ids.count(item) > 1}) + if missing or unknown or duplicates or len(rows) != len(bo_ids): + raise ValueError("candidate BO conservation failed") + candidate_status = "READY_WITH_EXCEPTIONS" if exceptions else ("READY_WITH_REVIEW" if reviews else "READY") + bundle = { + "fact_ledger_candidate_bundle": { + "schema_version": "stage1_fact_ledger_candidate_bundle.v2", + "status": candidate_status, + "run_fingerprint": run_fingerprint, + "source_pack_ref": { + "path": SOURCE_PACK_PATH, + "schema_version": "stage1_fact_source_pack.v3", + "sha256": _digest(source_outer), + }, + "candidate_items": rows, + "exception_manifest_ref": { + "path": EXCEPTION_MANIFEST_PATH, + "schema_version": "stage1_fact_exception_manifest.v2", + "sha256": _digest(manifest), + "expected_exception_ids": exception_ids, + }, + "compile_gate": { + "source_bo_count": len(bo_ids), + "candidate_count": len(rows), + "source_bo_ids": bo_ids, + "candidate_bo_ids": candidate_bo_ids, + "missing_bo_ids": missing, + "unknown_bo_ids": unknown, + "duplicate_bo_ids": duplicates, + "bo_conservation_pass": True, + "candidate_schema_closed": True, + "reference_validation_pass": True, + "evidence_score_basis": evidence_score_basis, + }, + } + } + _write_json(CANDIDATE_PATH, bundle) + _verify_reread(CANDIDATE_PATH, bundle) + stdout_status = "READY" if exceptions else ("READY_WITH_REVIEW" if reviews else "READY_NO_EXCEPTIONS") + print(_canonical({ + "fact_ledger_fl1_manifest": { + "schema_version": "stage1_fact_ledger_fl1_manifest.v2", + "status": stdout_status, + "candidate_path": CANDIDATE_PATH, + "exception_manifest_path": EXCEPTION_MANIFEST_PATH, + "run_fingerprint": run_fingerprint, + "candidate_count": len(rows), + "exception_count": len(exceptions), + "review_count": len(reviews), + "pack_count": len(pack_rows), + }, + "dynamic_fanout": fanout, + })) + + + try: + main() + except Exception as exc: + print(_canonical({"fact_ledger_fl1_manifest": { + "schema_version": "stage1_fact_ledger_fl1_manifest.v2", + "status": "FAILED", + "error": _safe_error(exc), + }, "dynamic_fanout": []})) + raise + + - task_name: Task_FL2_fact_exception_adjudicator_* + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: medium + llm_verbosity: low + max_iterations: 2 + max_concurrency: 2 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: | + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + TASK_NAME: Task_FL2_fact_exception_adjudicator_* + STAGE: Stage 1 Fact Ledger generation + ROLE: compact source-backed Fact projection exception adjudicator + + + + pack_id: {{item.pack_id}} + exception_type: {{item.exception_type}} + exception_ids: {{item.exception_ids}} + items: {{item.items}} + output_path: {{item.output_path}} + run_fingerprint: {{item.run_fingerprint}} + + + + - Act as a senior Korean civil-litigation lawyer and AI systems architect, but decide only the assigned compact projection conflicts. + - Use only ASSIGNED_PACK. Do not read or request any file, any other pack, BO, evidence, signal, structure, source pack, candidate bundle, or final ledger. + - Process every assigned exception ID exactly once, without omission, duplication, merge, split, or reorder. + - Select only KEEP_AS_IS, PATCH, or BLOCK_REVIEW when that decision appears in the item's allowed_decisions. + - PATCH keys must be a subset of allowed_output_fields. Every PATCH value must exactly equal one candidate_values[].value for the same field. + - source_refs_used must be a subset of source_refs embedded in that exception item. + - Never create a fact, party, date, amount, asset, BO ID, Fact ID, exception ID, structure ID, evidence ID, candidate value, source ref, or legal theory. + - If compact context is insufficient, use BLOCK_REVIEW. Never guess. + - Do not write Fact_Ledger_base.json or any input, source, candidate, manifest, or other adjudication file. + - The only permitted tool action is localdocs write_file with overwrite=true to ASSIGNED_PACK.output_path. Do not call read_docs, list_docs, or any other tool. + - Write exactly one raw JSON adjudication part, then return only the compact manifest JSON. Do not return markdown, code fences, explanation, or the file body. + + + + - KEEP_AS_IS: preserve the deterministic candidate when its source priority remains supportable; patch must be {}, review_required=false. + - PATCH: choose exactly one source-backed candidate for each patched field; review_required=false. + - BLOCK_REVIEW: use when sources remain materially balanced, compact context is insufficient, or a safe selection requires legal theory; patch must be {}, review_required=true. + - No split decision is allowed. + + + + Each decisions[] item must contain exactly: + { + "exception_id": "FX-001", + "exception_type": "conflicting_fact_projection|conflicting_calculation_projection|conflicting_asset_projection|conflicting_structure_role_projection", + "fact_id": "F-001", + "source_bo_id": "bh1", + "decision": "KEEP_AS_IS|PATCH|BLOCK_REVIEW", + "patch": {}, + "review_required": false, + "reason_code": "KEEP_EXISTING_SOURCE_PRIORITY|SELECT_SOURCE_BACKED_CANDIDATE|INSUFFICIENT_COMPACT_CONTEXT|UNSUPPORTED_PATCH", + "basis_codes_used": [], + "source_refs_used": [], + "conflict_codes_acknowledged": [] + } + + + + Write this raw JSON object to ASSIGNED_PACK.output_path: + { + "schema_version": "stage1_fact_exception_adjudication_part.v2", + "status": "READY|READY_WITH_BLOCKS|FAILED", + "pack_id": "the assigned pack_id", + "run_fingerprint": "the assigned run_fingerprint", + "exception_type": "the assigned exception_type", + "input_exception_ids": ["all assigned exception IDs exactly once"], + "decisions": ["one decision per assigned exception"], + "coverage": { + "expected": 0, + "actual": 0, + "missing_exception_ids": [], + "unknown_exception_ids": [], + "duplicate_exception_ids": [], + "coverage_pass": true + } + } + Use READY_WITH_BLOCKS when any decision is BLOCK_REVIEW; otherwise use READY. expected and actual must equal the assigned exception count. + + + + After localdocs confirms the write, return exactly: + { + "pack_id": "the assigned pack_id", + "status": "READY|READY_WITH_BLOCKS|FAILED", + "output_path": "the assigned output_path", + "decision_count": 0 + } + + + + - task_name: Task_FL3_final_fact_ledger_gate_and_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import copy + import hashlib + import itertools + import json + import re + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _safe_error(exc: BaseException) -> str: + return re.sub(r"\s+", " ", str(exc)).strip()[:300] + + + def _parse_json_value(raw: Any, label: str, allow_trailing: bool = False) -> Any: + if isinstance(raw, (dict, list)): + return raw + if not isinstance(raw, str): + raise ValueError(f"{label} is not JSON text") + text = raw.lstrip("\ufeff").strip() + if not text: + raise ValueError(f"{label} is empty") + try: + return json.loads(text) + except json.JSONDecodeError: + try: + value, end = JSON_DECODER.raw_decode(text) + except json.JSONDecodeError as exc: + raise ValueError(f"{label} invalid JSON: {text[:300]}") from exc + if not allow_trailing and text[end:].strip(): + raise ValueError(f"{label} has trailing content: {text[end:end + 300]}") + return value + + + def _parse_mcp_response(text: str) -> dict[str, Any]: + parsed: dict[str, Any] | None = None + for line in text.strip().splitlines(): + if not line.startswith("data: "): + continue + try: + item = _parse_json_value(line[6:], "MCP SSE data", True) + except ValueError: + continue + if isinstance(item, dict): + parsed = item + if parsed is None: + item = _parse_json_value(text, "MCP response", True) + if not isinstance(item, dict): + raise RuntimeError("MCP response root is not an object") + parsed = item + return parsed + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-fact-ledger-fl3-final-writer", + "version": "2.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_mcp_response(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP initialize failed: {payload['error']}") + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + initialized = CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ) + initialized.raise_for_status() + + + def _call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_msg_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_mcp_response(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP {name} failed: {payload['error']}") + result = payload.get("result") + if not isinstance(result, dict) or result.get("isError") is True: + raise RuntimeError(f"MCP {name} returned an invalid result") + return result + + + def _extract_doc(result: dict[str, Any], doc_name: str) -> Any: + blocks = result.get("content") or [] + if not blocks or not isinstance(blocks[0], dict): + raise RuntimeError(f"Empty response: {doc_name}") + text = blocks[0].get("text") + if not isinstance(text, str) or not text.strip(): + raise RuntimeError(f"Empty text response: {doc_name}") + outer = _parse_json_value(text, f"{doc_name} outer envelope", True) + inner: Any = outer + if isinstance(outer, dict) and "results" in outer: + results = outer.get("results") or [] + if not results or not isinstance(results[0], dict): + raise RuntimeError(f"Empty results envelope: {doc_name}") + inner = results[0].get("content") + if inner in (None, ""): + inner = results[0].get("text") + if isinstance(inner, (dict, list)): + return inner + return _parse_json_value(inner, f"{doc_name} inner document", False) + + + def _read_json(path: str) -> Any: + return _extract_doc(_call_tool("read_docs", {"doc_names": [path]}), path) + + + def _write_text(path: str, content: str) -> None: + _call_tool("write_file", {"path": path, "content": content, "overwrite": True}) + + + def _canonical(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + + def _digest(value: Any) -> str: + return "sha256:" + hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest() + + + def _write_json(path: str, value: Any) -> None: + _write_text(path, json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2)) + + + def _verify_reread(path: str, expected: Any) -> None: + actual = _read_json(path) + if _digest(actual) != _digest(expected): + raise RuntimeError(f"Post-write verification failed: {path}") + + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _strings(value: Any) -> list[str]: + values = value if isinstance(value, list) else [value] + out: list[str] = [] + for item in values: + if item is None: + continue + if isinstance(item, str): + text = item.strip() + elif isinstance(item, (int, float)) and not isinstance(item, bool): + text = str(item) + else: + continue + if text and text not in out: + out.append(text) + return out + + + def _sorted_strings(value: Any) -> list[str]: + return sorted(set(_strings(value))) + + + def _text(value: Any, limit: int = 500) -> str: + if value in (None, "", [], {}): + return "" + raw = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, sort_keys=True) + return re.sub(r"\s+", " ", raw).strip()[:limit] + + SOURCE_PACK_PATH = "stage1_tmp/fact_ledger/fact_source_pack.json" + CANDIDATE_PATH = "stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json" + EXCEPTION_MANIFEST_PATH = "stage1_tmp/fact_ledger/fact_exception_manifest.json" + TARGET_PATH = "Fact_Ledger_base.json" + REPORT_PATH = "stage1_tmp/fact_ledger/fact_ledger_writer_report.json" + FINAL_FIELDS = [ + "fact_id", "source_bo_id", "type", "date", "parties", "object_spec", "amount", "action", + "evidence_refs", "credibility", "legal_centrality", "proof_strength", "legal_effect_roles", + "claim_chain_ref", "linked_structures", "must_not_drop_in_claim_types", "must_consider", + "state_context", "money_claim_effect", "commercial_successor_effect", "actio_roles", + "linked_actio_structures", "downstream_module_candidates", "skeleton_validation_codes", + "derived_fact", "derivation_basis", "legal_calculation_object", + ] + FINAL_FIELD_SET = set(FINAL_FIELDS) + CLOSED_EXCEPTION_TYPES = { + "conflicting_fact_projection", + "conflicting_calculation_projection", + "conflicting_asset_projection", + "conflicting_structure_role_projection", + } + CLOSED_DECISIONS = {"KEEP_AS_IS", "PATCH", "BLOCK_REVIEW"} + + + def _add_code(row: dict[str, Any], code: str) -> None: + row["skeleton_validation_codes"] = sorted(set(_strings(row.get("skeleton_validation_codes")) + [code])) + + + def _validate_rows(bundle: dict[str, Any]) -> list[dict[str, Any]]: + if bundle.get("schema_version") != "stage1_fact_ledger_candidate_bundle.v2": + raise ValueError("candidate bundle schema_version mismatch") + if bundle.get("status") not in {"READY", "READY_WITH_REVIEW", "READY_WITH_EXCEPTIONS"}: + raise ValueError("candidate bundle status invalid") + rows = bundle.get("candidate_items") + if not isinstance(rows, list) or not rows: + raise ValueError("candidate_items missing") + for row in rows: + if not isinstance(row, dict) or set(row) != FINAL_FIELD_SET: + raise ValueError("candidate row schema is not closed") + return copy.deepcopy(rows) + + + def _load_exception_inputs(manifest: dict[str, Any], run_fingerprint: str) -> tuple[dict[str, dict[str, Any]], list[str]]: + items: dict[str, dict[str, Any]] = {} + input_ids: list[str] = [] + packs = _as_list(manifest.get("packs")) + if len(packs) != manifest.get("pack_count"): + raise ValueError("exception manifest pack_count mismatch") + for pack in packs: + row = _as_dict(pack) + part = _as_dict(_read_json(_text(row.get("input_path"), 300))) + if part.get("schema_version") != "stage1_fact_exception_input_part.v2" or part.get("status") != "READY": + raise ValueError("exception input part schema/status invalid") + if part.get("pack_id") != row.get("pack_id") or part.get("run_fingerprint") != run_fingerprint: + raise ValueError("exception input part identity mismatch") + if part.get("output_path") != row.get("output_path") or part.get("exception_type") != row.get("exception_type"): + raise ValueError("exception input part routing mismatch") + part_ids = _strings(part.get("input_exception_ids")) + if part_ids != _strings(row.get("exception_ids")): + raise ValueError("exception input ID mismatch") + for item in _as_list(part.get("items")): + if not isinstance(item, dict): + raise ValueError("exception input item invalid") + exception_id = _text(item.get("exception_id"), 120) + if not exception_id or exception_id in items: + raise ValueError(f"duplicate exception input: {exception_id}") + if item.get("exception_type") not in CLOSED_EXCEPTION_TYPES: + raise ValueError("exception input type invalid") + items[exception_id] = item + input_ids.append(exception_id) + return items, input_ids + + + def _load_decisions(manifest: dict[str, Any], run_fingerprint: str) -> tuple[dict[str, dict[str, Any]], list[str]]: + decisions: dict[str, dict[str, Any]] = {} + decision_ids: list[str] = [] + for pack in _as_list(manifest.get("packs")): + expected_pack = _as_dict(pack) + part = _as_dict(_read_json(_text(expected_pack.get("output_path"), 300))) + if part.get("schema_version") != "stage1_fact_exception_adjudication_part.v2": + raise ValueError("adjudication part schema_version mismatch") + if part.get("status") not in {"READY", "READY_WITH_BLOCKS"}: + raise ValueError("adjudication part failed or has invalid status") + if part.get("pack_id") != expected_pack.get("pack_id") or part.get("run_fingerprint") != run_fingerprint: + raise ValueError("adjudication part identity mismatch") + if part.get("exception_type") != expected_pack.get("exception_type"): + raise ValueError("adjudication part type mismatch") + if _strings(part.get("input_exception_ids")) != _strings(expected_pack.get("exception_ids")): + raise ValueError("adjudication input IDs mismatch") + coverage = _as_dict(part.get("coverage")) + if coverage.get("coverage_pass") is not True or _as_list(coverage.get("missing_exception_ids")) or _as_list(coverage.get("unknown_exception_ids")) or _as_list(coverage.get("duplicate_exception_ids")): + raise ValueError("adjudication part coverage failed") + for decision in _as_list(part.get("decisions")): + if not isinstance(decision, dict): + raise ValueError("adjudication decision invalid") + exception_id = _text(decision.get("exception_id"), 120) + if not exception_id or exception_id in decisions: + raise ValueError(f"duplicate adjudication decision: {exception_id}") + decisions[exception_id] = decision + decision_ids.append(exception_id) + return decisions, decision_ids + + + def _allowed_values(item: dict[str, Any], field: str) -> list[Any]: + return [candidate.get("value") for candidate in _as_list(item.get("candidate_values")) if isinstance(candidate, dict) and candidate.get("field") == field] + + + def _all_source_refs(item: dict[str, Any]) -> set[str]: + refs: set[str] = set() + for candidate in _as_list(item.get("candidate_values")): + if isinstance(candidate, dict): + refs.update(_strings(candidate.get("source_refs"))) + refs.update(_strings(item.get("structure_refs"))) + return refs + + + def _apply_decision(row: dict[str, Any], item: dict[str, Any], decision: dict[str, Any]) -> bool: + for key in ("exception_id", "exception_type", "fact_id", "source_bo_id"): + if decision.get(key) != item.get(key): + raise ValueError(f"decision identity mismatch: {key}") + decision_type = decision.get("decision") + if decision_type not in CLOSED_DECISIONS or decision_type not in _as_list(item.get("allowed_decisions")): + raise ValueError("decision enum invalid") + patch = decision.get("patch") + if not isinstance(patch, dict): + raise ValueError("decision patch must be an object") + used_refs = set(_strings(decision.get("source_refs_used"))) + if not used_refs.issubset(_all_source_refs(item)): + raise ValueError("decision used unknown source refs") + if decision_type == "KEEP_AS_IS": + if patch or decision.get("review_required") is True: + raise ValueError("KEEP_AS_IS contract violated") + return False + if decision_type == "BLOCK_REVIEW": + if patch or decision.get("review_required") is not True: + raise ValueError("BLOCK_REVIEW contract violated") + _add_code(row, "BLOCK_REVIEW_PRESENT") + _add_code(row, "FL2_REVIEW_REQUIRED") + row["must_consider"] = True + row["derivation_basis"] = (str(row.get("derivation_basis") or "") + "; blocked_exception=" + str(item.get("exception_id"))).strip("; ") + return True + if decision.get("review_required") is True or not patch: + raise ValueError("PATCH contract violated") + allowed_fields = set(_strings(item.get("allowed_output_fields"))) + if not set(patch).issubset(allowed_fields) or not set(patch).issubset(FINAL_FIELD_SET): + raise ValueError("PATCH field outside hard whitelist") + for field, value in patch.items(): + if not any(_canonical(value) == _canonical(candidate) for candidate in _allowed_values(item, field)): + raise ValueError("PATCH value is not source-backed") + row[field] = copy.deepcopy(value) + return False + + + def _reference_universes(source: dict[str, Any]) -> dict[str, set[str]]: + structures = _as_dict(source.get("structure_by_id")) + claims: set[str] = set() + liabilities: set[str] = set() + actio_signals: set[str] = set() + for structure in structures.values(): + row = _as_dict(structure) + claims.update(_strings(row.get("linked_claim_group_ids"))) + liabilities.update(_strings(row.get("linked_liability_group_ids"))) + actio_signals.update(_strings(row.get("linked_actio_signal_ids"))) + for by_type in _as_dict(source.get("signal_hints_by_bo_id")).values(): + for signal_type, rows in _as_dict(by_type).items(): + for signal in _as_list(rows): + item = _as_dict(signal) + claims.update(_strings(item.get("claim_group_candidates"))) + liabilities.update(_strings(item.get("liability_group_candidates"))) + if signal_type == "actio": + actio_signals.update(_strings(item.get("signal_id"))) + return {"claim": claims, "liability": liabilities, "actio": actio_signals} + + + def _validate_row_refs(row: dict[str, Any], source: dict[str, Any], group_universes: dict[str, set[str]], report: dict[str, Any]) -> None: + universe = _as_dict(source.get("reference_universe")) + bo_ids = set(_strings(universe.get("bo_ids"))) + structure_ids = set(_strings(universe.get("structure_ids"))) + evidence_ids = set(_strings(universe.get("evidence_indexes"))) + bo_id = _text(row.get("source_bo_id"), 120) + unknown_bo = [] if bo_id in bo_ids else [bo_id] + unknown_structures = sorted(set(_strings(_as_dict(row.get("linked_structures")).get("structure_ids"))) - structure_ids) + unknown_evidence = sorted(set(_strings(row.get("evidence_refs"))) - evidence_ids) + report["reference_gate"]["unknown_bo_refs"].extend(unknown_bo) + report["reference_gate"]["unknown_structure_refs"].extend(unknown_structures) + report["reference_gate"]["unknown_evidence_refs"].extend(unknown_evidence) + claim = _as_dict(row.get("claim_chain_ref")) + if not set(_strings(claim.get("claim_group_ids"))).issubset(group_universes["claim"]): + raise ValueError(f"unknown claim group ref: {bo_id}") + if not set(_strings(claim.get("liability_group_ids"))).issubset(group_universes["liability"]): + raise ValueError(f"unknown liability group ref: {bo_id}") + + + def main() -> None: + _init_mcp_session() + source_outer = _read_json(SOURCE_PACK_PATH) + candidate_outer = _read_json(CANDIDATE_PATH) + manifest_outer = _read_json(EXCEPTION_MANIFEST_PATH) + source = _as_dict(_as_dict(source_outer).get("fact_source_pack")) + bundle = _as_dict(_as_dict(candidate_outer).get("fact_ledger_candidate_bundle")) + manifest = _as_dict(_as_dict(manifest_outer).get("fact_exception_manifest")) + if source.get("schema_version") != "stage1_fact_source_pack.v3" or source.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("fact_source_pack invalid") + if manifest.get("schema_version") != "stage1_fact_exception_manifest.v2" or manifest.get("status") not in {"READY", "READY_NO_EXCEPTIONS", "READY_WITH_REVIEW"}: + raise ValueError("fact_exception_manifest invalid") + rows = _validate_rows(bundle) + run_fingerprint = _text(source.get("run_fingerprint"), 100) + if not run_fingerprint or bundle.get("run_fingerprint") != run_fingerprint or manifest.get("run_fingerprint") != run_fingerprint: + raise ValueError("run_fingerprint mismatch") + source_ref = _as_dict(bundle.get("source_pack_ref")) + manifest_ref = _as_dict(bundle.get("exception_manifest_ref")) + if source_ref.get("path") != SOURCE_PACK_PATH or source_ref.get("schema_version") != "stage1_fact_source_pack.v3" or source_ref.get("sha256") != _digest(source_outer): + raise ValueError("source pack hash/ref mismatch") + if manifest_ref.get("path") != EXCEPTION_MANIFEST_PATH or manifest_ref.get("schema_version") != "stage1_fact_exception_manifest.v2" or manifest_ref.get("sha256") != _digest(manifest_outer): + raise ValueError("exception manifest hash/ref mismatch") + + expected_ids = _strings(manifest.get("expected_exception_ids")) + if expected_ids != _strings(manifest_ref.get("expected_exception_ids")) or len(expected_ids) != manifest.get("exception_count"): + raise ValueError("expected exception IDs mismatch") + if not expected_ids and (_as_list(manifest.get("packs")) or manifest.get("pack_count") != 0): + raise ValueError("no-exception manifest contains packs") + items_by_id, input_ids = _load_exception_inputs(manifest, run_fingerprint) + decisions_by_id, decision_ids = _load_decisions(manifest, run_fingerprint) + missing_ids = sorted(set(expected_ids) - set(decision_ids)) + unknown_ids = sorted(set(decision_ids) - set(expected_ids)) + duplicate_ids = sorted({item for item in decision_ids if decision_ids.count(item) > 1}) + if sorted(expected_ids) != sorted(input_ids) or missing_ids or unknown_ids or duplicate_ids or len(decision_ids) != len(expected_ids): + raise ValueError("exact exception coverage failed") + + rows_by_fact = {row["fact_id"]: row for row in rows} + blocked_items: list[dict[str, Any]] = [] + for exception_id in expected_ids: + item = items_by_id.get(exception_id) + decision = decisions_by_id.get(exception_id) + if not item or not decision: + raise ValueError(f"exception coverage missing: {exception_id}") + row = rows_by_fact.get(_text(item.get("fact_id"), 120)) + if not row or row.get("source_bo_id") != item.get("source_bo_id"): + raise ValueError("exception Fact/BO target mismatch") + blocked = _apply_decision(row, item, decision) + if blocked: + blocked_items.append({ + "exception_id": exception_id, + "exception_type": item.get("exception_type"), + "fact_id": item.get("fact_id"), + "source_bo_id": item.get("source_bo_id"), + "reason_code": decision.get("reason_code"), + }) + + source_bo_ids = _strings(_as_dict(source.get("reference_universe")).get("bo_ids")) + compile_gate = _as_dict(bundle.get("compile_gate")) + candidate_bo_ids = [str(row.get("source_bo_id")) for row in rows] + duplicate_bos = sorted({item for item in candidate_bo_ids if candidate_bo_ids.count(item) > 1}) + missing_bos = sorted(set(source_bo_ids) - set(candidate_bo_ids)) + unknown_bos = sorted(set(candidate_bo_ids) - set(source_bo_ids)) + if source_bo_ids != _strings(compile_gate.get("source_bo_ids")) or candidate_bo_ids != _strings(compile_gate.get("candidate_bo_ids")) or missing_bos or unknown_bos or duplicate_bos or compile_gate.get("bo_conservation_pass") is not True: + raise ValueError("candidate/BO conservation failed") + + deterministic_reviews = _as_list(manifest.get("deterministic_review_items")) + report = { + "schema_version": "stage1_fact_ledger_writer_report.v2", + "status": "READY", + "run_fingerprint": run_fingerprint, + "conservation_gate": { + "source_bo_count": len(source_bo_ids), + "candidate_count": len(candidate_bo_ids), + "final_count": len(rows), + "missing_bo_ids": missing_bos, + "unknown_bo_ids": unknown_bos, + "duplicate_bo_ids": duplicate_bos, + "pass": True, + }, + "exception_gate": { + "expected_count": len(expected_ids), + "decision_count": len(decision_ids), + "blocked_count": len(blocked_items), + "missing_exception_ids": missing_ids, + "unknown_exception_ids": unknown_ids, + "duplicate_exception_ids": duplicate_ids, + "coverage_pass": True, + }, + "reference_gate": {"unknown_bo_refs": [], "unknown_structure_refs": [], "unknown_evidence_refs": [], "pass": True}, + "upstream_les_review_gate": {}, + "blocked_review_items": blocked_items, + "row_warnings": [], + "possession_conflict_gate": {"status": "PASS", "conflicts": []}, + "drafting_gates": [], + "gate_review_code_summary": {}, + "evidence_score_change_summary": [], + } + group_universes = _reference_universes(source) + for row in rows: + _validate_row_refs(row, source, group_universes, report) + for key in ("unknown_bo_refs", "unknown_structure_refs", "unknown_evidence_refs"): + report["reference_gate"][key] = sorted(set(report["reference_gate"][key])) + report["reference_gate"]["pass"] = not any(report["reference_gate"][key] for key in ("unknown_bo_refs", "unknown_structure_refs", "unknown_evidence_refs")) + if not report["reference_gate"]["pass"]: + raise ValueError("reference gate failed") + + code_rows: dict[str, list[str]] = {} + for ordinal, row in enumerate(sorted(rows, key=lambda item: item["source_bo_id"]), start=1): + row["fact_id"] = f"F-{ordinal:03d}" + row["skeleton_validation_codes"] = sorted(set(_strings(row.get("skeleton_validation_codes")))) + for code in row["skeleton_validation_codes"]: + code_rows.setdefault(code, []).append(row["fact_id"]) + rows.sort(key=lambda item: item["fact_id"]) + report["gate_review_code_summary"] = {code: ids for code, ids in sorted(code_rows.items())} + conflicts = code_rows.get("POSSESSION_CAUSE_CONFLICT", []) + report["possession_conflict_gate"] = {"status": "HARD_WARNING" if conflicts else "PASS", "conflicts": conflicts} + + blocking_reviews = [item for item in deterministic_reviews if _as_dict(item).get("severity") == "BLOCK_FINAL_DRAFTING" or _as_dict(item).get("review_code") == "LES_BLOCKED_EXCEPTION_REVIEW"] + affected_facts = sorted(set(_text(_as_dict(item).get("fact_id"), 120) for item in deterministic_reviews if _text(_as_dict(item).get("fact_id"), 120))) + affected_bos = sorted(set(_text(_as_dict(item).get("source_bo_id"), 120) for item in deterministic_reviews if _text(_as_dict(item).get("source_bo_id"), 120))) + upstream_status = "BLOCK_FINAL_DRAFTING" if blocking_reviews else ("REVIEW" if deterministic_reviews else "PASS") + report["upstream_les_review_gate"] = { + "review_count": len(deterministic_reviews), + "blocking_count": len(blocking_reviews), + "affected_fact_ids": affected_facts, + "affected_bo_ids": affected_bos, + "status": upstream_status, + } + if blocking_reviews: + report["drafting_gates"].append({ + "gate": "BLOCK_FINAL_DRAFTING_UPSTREAM_LES_REVIEW", + "affected_fact_ids": sorted(set(_text(_as_dict(item).get("fact_id"), 120) for item in blocking_reviews)), + "affected_bo_ids": sorted(set(_text(_as_dict(item).get("source_bo_id"), 120) for item in blocking_reviews)), + }) + notice_rows = code_rows.get("NOTICE_RECEIPT_UNVERIFIED", []) + if notice_rows: + report["drafting_gates"].append({"gate": "BLOCK_FINAL_DRAFTING_NOTICE_RECEIPT", "affected_fact_ids": notice_rows}) + report["evidence_score_change_summary"] = [ + {"fact_id": fact_id, "basis_code": code} + for fact_id, code in sorted(_as_dict(compile_gate.get("evidence_score_basis")).items()) + ] + has_review = bool(deterministic_reviews or blocked_items or report["drafting_gates"] or conflicts) + report["status"] = "READY_WITH_REVIEW" if has_review else "READY" + + _write_json(TARGET_PATH, rows) + _write_json(REPORT_PATH, report) + _verify_reread(TARGET_PATH, rows) + _verify_reread(REPORT_PATH, report) + print(_canonical({"fact_ledger_final_writer": { + "schema_version": "stage1_fact_ledger_final_writer.v2", + "status": report["status"], + "written_file": TARGET_PATH, + "writer_report_file": REPORT_PATH, + "run_fingerprint": run_fingerprint, + "fact_count": len(rows), + "review_count": len(deterministic_reviews), + "blocked_count": len(blocked_items), + "drafting_gate_count": len(report["drafting_gates"]), + }})) + + + try: + main() + except Exception as exc: + print(_canonical({"fact_ledger_final_writer": { + "schema_version": "stage1_fact_ledger_final_writer.v2", + "status": "FAILED", + "written_file": None, + "error": _safe_error(exc), + }})) + raise + diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_4_v2.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_4_v2.yml new file mode 100644 index 00000000..0d6285bd --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Stage_1_Part_4_v2.yml @@ -0,0 +1,2027 @@ +--- +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_4 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Fact Ledger Generation + version: v1.0 + Stages: + - name: stage1_fact_ledger_generation + description: BO, Legal Effect 사용해서 Fact_Ledger_base.json 생성 + tools: + mcpServers: + localdocs: + type: streamable-http + url: http://mcp-localdocs:8012/mcp + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + + task_procedure: + IN: + nexts: + - Task_FL0_fact_source_pack_compiler + wait_until: [] + Task_FL0_fact_source_pack_compiler: + nexts: + - Task_FL1_deterministic_fact_ledger_candidate_builder + wait_until: + - IN + Task_FL1_deterministic_fact_ledger_candidate_builder: + nexts: + - Task_FL2_single_fact_exception_adjudicator + wait_until: + - Task_FL0_fact_source_pack_compiler + Task_FL2_single_fact_exception_adjudicator: + nexts: + - Task_FL3_final_fact_ledger_gate_and_writer + wait_until: + - Task_FL1_deterministic_fact_ledger_candidate_builder + Task_FL3_final_fact_ledger_gate_and_writer: + nexts: + - OUT + wait_until: + - Task_FL2_single_fact_exception_adjudicator + OUT: + nexts: [] + wait_until: + - Task_FL3_final_fact_ledger_gate_and_writer + + tasks: + - task_name: Task_FL0_fact_source_pack_compiler + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import itertools + import json + import re + import sys + from datetime import datetime, timezone + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + INPUT_FILES = { + "client_goal": "client_goal.json", + "bo": "BO.json", + "legal_effect_structures": "legal_effect_structures.json", + "actio_case_signals": "actio_case_signals.json", + "case_liability_signals": "case_liability_signals.json", + "legal_effect_signals": "legal_effect_signals.json", + "evidence_indexed": "evidence_indexed.json", + "evidence_all": "evidence_all.json", + "evidence_event_candidates": "evidence_event_candidates.json", + } + OUT_PATH = "stage1_tmp/fact_ledger/fact_source_pack.json" + MAX_TEXT = 900 + MAX_SIGNAL_ITEMS_PER_BO = 12 + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-fact-ledger-fl0-source-pack-compiler", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result["result"].get("content", []) + text = content[0].get("text", "") if content else "" + try: + parsed = json.loads(text) + except json.JSONDecodeError as exc: + raise RuntimeError(f"Invalid read_docs JSON envelope for {doc_name}: {text[:300]}") from exc + if isinstance(parsed, dict) and "results" in parsed: + results = parsed.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + first = results[0] + if first.get("error"): + raise RuntimeError(f"{doc_name}: {first['error']}") + inner = first.get("content", "") + if not inner: + raise RuntimeError(f"Empty content for {doc_name}") + if isinstance(inner, str): + try: + return json.loads(inner) + except json.JSONDecodeError: + try: + obj, _ = json.JSONDecoder().raw_decode(inner.strip()) + return obj + except json.JSONDecodeError as exc: + raise RuntimeError(f"Content of {doc_name} is not valid JSON: {inner[:300]}") from exc + return inner + return parsed + + + def _read_required_json_doc(doc_name: str, msg_id: int) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=msg_id) + return _extract_json_from_read_docs_result(result, doc_name) + + + def _write_doc(doc_name: str, content: str, msg_id: int) -> None: + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=msg_id) + + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _text(value: Any, limit: int = MAX_TEXT) -> str: + if value is None: + return "" + if isinstance(value, str): + s = value + else: + s = json.dumps(value, ensure_ascii=False, sort_keys=True) + s = re.sub(r"\s+", " ", s).strip() + return s[:limit] + + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + if value is None: + return out + if isinstance(value, (str, int, float)): + s = str(value).strip() + return [s] if s else [] + if isinstance(value, list): + for item in value: + out.extend(_strings(item)) + return _dedupe(out) + + + def _dedupe(values: list[str]) -> list[str]: + seen: set[str] = set() + out: list[str] = [] + for value in values: + if value and value not in seen: + seen.add(value) + out.append(value) + return out + + + def _first(d: dict[str, Any], keys: list[str]) -> Any: + for key in keys: + if key in d and d.get(key) not in (None, "", [], {}): + return d.get(key) + return None + + + def _items_from_doc(doc: Any, candidate_keys: list[str]) -> list[dict[str, Any]]: + if isinstance(doc, list): + return [x for x in doc if isinstance(x, dict)] + if isinstance(doc, dict): + for key in candidate_keys: + if isinstance(doc.get(key), list): + return [x for x in doc[key] if isinstance(x, dict)] + if any(k in doc for k in ("BO_ID", "bo_id", "id", "fact_id", "evidence_index", "structure_id")): + return [doc] + return [] + + + def _bo_id(bo: dict[str, Any]) -> str: + return str(_first(bo, ["BO_ID", "bo_id", "id", "canonical_bo_id", "bh_id"]) or "").strip() + + + def _bo_items(bo_doc: Any) -> list[dict[str, Any]]: + items = _items_from_doc(bo_doc, ["BO", "bo", "items", "bo_items", "canonical_bo_items", "body"]) + valid = [item for item in items if _bo_id(item)] + if not valid: + raise ValueError("BO.json must contain BO items with BO_ID/id") + return valid + + + def _evidence_refs_from_bo(bo: dict[str, Any]) -> list[str]: + refs: list[str] = [] + for key in ("source_evidence_indexes", "evidence_refs", "Evidence", "EvidenceIndexes", "related_evidence_indexes"): + refs.extend(_strings(bo.get(key))) + core = _as_dict(bo.get("core_field_base")) + refs.extend(_strings(core.get("source_evidence_indexes"))) + return _dedupe(refs) + + + DOMAIN_PAYLOAD_KEEP_KEYS = [ + "money_claim_details", "possession_state", "lien_state", + "notice_lifecycle", "asset_alias", "fractional_effect_events", + ] + + + def _compact_bo(bo: dict[str, Any]) -> dict[str, Any]: + core = _as_dict(bo.get("core_field_base")) + return { + "bo_id": _bo_id(bo), + "bo_type": _first(bo, ["BOType", "bo_type", "type"]), + "action": _first(bo, ["Action", "action"]), + "action_type": _first(bo, ["ActionType", "action_type"]), + "juristic_act": _first(bo, ["JuristicAct", "juristic_act"]), + "reason": _first(bo, ["Reason", "reason"]), + "prior_act": _first(bo, ["PriorAct", "prior_act"]), + "legal_keywords": _strings(bo.get("Legal_Keywords")), + "amount": _first(bo, ["amount", "proposed_amount", "Amount"]), + "date": _first(bo, ["date", "proposed_date", "Date"]), + "core_field_base": { + "BehaviorTime": _first(core, ["BehaviorTime", "behavior_time", "date", "Date"]), + "Performer": _first(core, ["Performer", "Actor", "actor", "performer"]), + "Subject": _first(core, ["Subject", "Counterparty", "subject", "counterparty"]), + "Object": _first(core, ["Object", "Property", "Asset", "object", "asset"]), + "Amount": _first(core, ["Amount", "amount", "Value", "value"]), + "Action": _first(core, ["Action", "action"]), + }, + "source_evidence_indexes": _evidence_refs_from_bo(bo), + "domain_payload_compact": { + k: v + for k, v in _as_dict(_as_dict(bo.get("extensions")).get("domain_payload")).items() + if k in DOMAIN_PAYLOAD_KEEP_KEYS and v not in (None, {}, []) + }, + } + + + def _validate_legal_effect_structures(doc: Any, bo_ids: set[str]) -> tuple[list[dict[str, Any]], dict[str, Any], list[str]]: + warnings: list[str] = [] + if not isinstance(doc, dict): + raise ValueError("legal_effect_structures.json root must be object") + if doc.get("schema_version") != "stage1_legal_effect_structures.v1": + raise ValueError("legal_effect_structures.json schema_version mismatch") + if doc.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("legal_effect_structures.json status must be READY or READY_WITH_REVIEW") + structures = _as_list(doc.get("legal_effect_structures")) + index = _as_dict(doc.get("structure_index")) + if not isinstance(index.get("by_bo_id"), dict): + raise ValueError("legal_effect_structures.structure_index.by_bo_id must be object") + for key in ["by_bo_id", "by_issue_cluster_id", "by_asset_cluster_id", "by_evidence_index", "by_candidate_structure_type"]: + if not isinstance(index.get(key), dict): + raise ValueError(f"legal_effect_structures.structure_index.{key} must be object") + structure_ids = {str(s.get("structure_id")) for s in structures if isinstance(s, dict) and s.get("structure_id")} + for structure in structures: + if not isinstance(structure, dict): + continue + sid = str(structure.get("structure_id") or "") + for bo_id in _strings(structure.get("source_bo_ids")): + if bo_id not in bo_ids: + warnings.append(f"STRUCTURE_SOURCE_BO_NOT_IN_BO_JSON:{sid}:{bo_id}") + bo_row = _as_dict(index["by_bo_id"].get(bo_id)) + if sid and sid not in _strings(bo_row.get("structure_ids")): + warnings.append(f"BY_BO_ID_MISSING_STRUCTURE_REF:{bo_id}:{sid}") + for bo_id, row in index["by_bo_id"].items(): + if bo_id not in bo_ids: + warnings.append(f"BY_BO_ID_UNKNOWN_BO:{bo_id}") + for sid in _strings(_as_dict(row).get("structure_ids")): + if sid not in structure_ids: + warnings.append(f"BY_BO_ID_UNKNOWN_STRUCTURE:{bo_id}:{sid}") + return structures, index, warnings + + + def _compact_structure(structure: dict[str, Any]) -> dict[str, Any]: + return { + "structure_id": structure.get("structure_id"), + "candidate_structure_type": structure.get("candidate_structure_type"), + "structure_label": structure.get("structure_label"), + "source_bo_ids": _strings(structure.get("source_bo_ids")), + "issue_cluster_ids": _strings(structure.get("issue_cluster_ids")), + "asset_cluster_ids": _strings(structure.get("asset_cluster_ids")), + "evidence_indexes": _strings(structure.get("evidence_indexes")), + "linked_claim_group_ids": _strings(structure.get("linked_claim_group_ids")), + "linked_liability_group_ids": _strings(structure.get("linked_liability_group_ids")), + "linked_actio_signal_ids": _strings(structure.get("linked_actio_signal_ids")), + "legal_effect_roles": _strings(structure.get("legal_effect_roles")), + "review_required": bool(structure.get("review_required")), + } + + + def _evidence_id(record: dict[str, Any]) -> str: + return str(_first(record, ["evidence_index", "evidence_id", "id", "index", "EvidenceIndex", "doc_id"]) or "").strip() + + + def _flatten_records(value: Any, depth: int = 0) -> list[dict[str, Any]]: + if depth > 3: + return [] + if isinstance(value, list): + out: list[dict[str, Any]] = [] + for item in value: + out.extend(_flatten_records(item, depth + 1)) + return out + if isinstance(value, dict): + if any(k in value for k in ("evidence_index", "evidence_id", "id", "related_bo_ids", "source_bo_ids", "signal_id", "route_id")): + return [value] + out: list[dict[str, Any]] = [] + for child in value.values(): + if isinstance(child, (list, dict)): + out.extend(_flatten_records(child, depth + 1)) + return out + return [] + + + def _build_evidence_authority(evidence_indexed: Any, evidence_all: Any, event_candidates: Any) -> dict[str, Any]: + authority: dict[str, Any] = {} + for record in _flatten_records(evidence_indexed): + eid = _evidence_id(record) + if not eid: + continue + authority[eid] = { + "evidence_index": eid, + "title": _first(record, ["title", "Title", "name", "doc_title"]), + "excerpt": _text(_first(record, ["excerpt", "summary", "content", "text", "body"])), + "priority": _first(record, ["priority", "Priority", "weight"]), + "authentication": _first(record, ["authentication", "auth", "source_authentication"]), + "source": "evidence_indexed.json", + } + for record in _flatten_records(evidence_all): + eid = _evidence_id(record) + if not eid: + continue + row = authority.setdefault(eid, {"evidence_index": eid, "source": "evidence_all.json"}) + row.setdefault("title", _first(record, ["title", "Title", "name", "doc_title"])) + row.setdefault("excerpt", _text(_first(record, ["excerpt", "summary", "content", "text", "body"]))) + row.setdefault("authentication", _first(record, ["authentication", "auth", "source_authentication"])) + for record in _flatten_records(event_candidates): + for eid in _strings(_first(record, ["evidence_index", "evidence_refs", "related_evidence_indexes", "source_evidence_indexes"])): + row = authority.setdefault(eid, {"evidence_index": eid, "source": "evidence_event_candidates.json"}) + events = row.setdefault("event_candidates", []) + if isinstance(events, list) and len(events) < 8: + events.append({ + "event_date": _first(record, ["event_date", "date", "time"]), + "actor": _first(record, ["actor", "party", "performer"]), + "action": _first(record, ["action", "event_type"]), + "amount": _first(record, ["amount", "value"]), + "object": _first(record, ["object", "asset", "property"]), + }) + return authority + + + def _build_event_by_evidence(event_candidates: Any) -> dict[str, list[dict[str, Any]]]: + out: dict[str, list[dict[str, Any]]] = {} + for record in _flatten_records(event_candidates): + event = { + "event_date": _first(record, ["event_date", "date", "time"]), + "actor": _first(record, ["actor", "party", "performer"]), + "action": _first(record, ["action", "event_type"]), + "amount": _first(record, ["amount", "value"]), + "object": _first(record, ["object", "asset", "property"]), + } + for eid in _strings(_first(record, ["evidence_index", "evidence_refs", "related_evidence_indexes", "source_evidence_indexes"])): + out.setdefault(eid, []).append(event) + return out + + + def _collect_bo_refs(value: Any, bo_ids: set[str]) -> list[str]: + refs: list[str] = [] + if isinstance(value, str): + if value in bo_ids: + refs.append(value) + elif isinstance(value, list): + for item in value: + refs.extend(_collect_bo_refs(item, bo_ids)) + elif isinstance(value, dict): + for key in ("related_bo_ids", "source_bo_ids", "bo_ids", "BO_ID", "bo_id", "source_bo_id"): + refs.extend([x for x in _strings(value.get(key)) if x in bo_ids]) + for child in value.values(): + if isinstance(child, (dict, list)): + refs.extend(_collect_bo_refs(child, bo_ids)) + return _dedupe(refs) + + + def _compact_signal(record: dict[str, Any], signal_type: str) -> dict[str, Any]: + return { + "signal_type": signal_type, + "signal_id": _first(record, ["signal_id", "route_id", "id", "structure_id"]), + "candidate_structure_types": _strings(record.get("candidate_structure_types")), + "linked_structure_ids": _strings(record.get("linked_structure_ids")), + "related_evidence_indexes": _strings(_first(record, ["related_evidence_indexes", "evidence_refs", "source_evidence_indexes"])), + "party_roles": record.get("party_roles") if isinstance(record.get("party_roles"), list) else [], + "actio_role_hints": record.get("actio_role_hints") if isinstance(record.get("actio_role_hints"), list) else [], + "claim_group_candidates": record.get("claim_group_candidates") if isinstance(record.get("claim_group_candidates"), list) else [], + "liability_group_candidates": record.get("liability_group_candidates") if isinstance(record.get("liability_group_candidates"), list) else [], + "summary": _text(record, 500), + } + + + def _build_signal_hints_by_bo_id(docs: dict[str, Any], bo_ids: set[str]) -> dict[str, dict[str, list[dict[str, Any]]]]: + out: dict[str, dict[str, list[dict[str, Any]]]] = { + bo_id: {"actio": [], "case_liability": [], "legal_effect": []} for bo_id in bo_ids + } + for signal_type, doc in docs.items(): + for record in _flatten_records(doc): + bo_refs = _collect_bo_refs(record, bo_ids) + for bo_id in bo_refs: + bucket = out.setdefault(bo_id, {"actio": [], "case_liability": [], "legal_effect": []}) + key = {"actio_case_signals": "actio", "case_liability_signals": "case_liability", "legal_effect_signals": "legal_effect"}[signal_type] + if len(bucket[key]) < MAX_SIGNAL_ITEMS_PER_BO: + bucket[key].append(_compact_signal(record, key)) + return out + + + def _compact_client_goal(doc: Any) -> dict[str, Any]: + if not isinstance(doc, dict): + return {"raw_summary": _text(doc)} + return { + "goal_type": _first(doc, ["goal_type", "case_goal", "claim_goal"]), + "requested_claims": doc.get("requested_claims") if isinstance(doc.get("requested_claims"), list) else [], + "parties": doc.get("parties") if isinstance(doc.get("parties"), (list, dict)) else {}, + "summary": _text(doc, 700), + } + + + def _linked_by_bo(bo_id: str, index: dict[str, Any]) -> dict[str, Any]: + row = _as_dict(index.get("by_bo_id", {}).get(bo_id)) + return { + "structure_ids": _strings(row.get("structure_ids")), + "issue_cluster_ids": _strings(row.get("issue_cluster_ids")), + "asset_cluster_ids": _strings(row.get("asset_cluster_ids")), + "evidence_index_ids": _strings(row.get("evidence_index_ids") or row.get("evidence_indexes")), + "candidate_structure_types": _strings(row.get("candidate_structure_types")), + "claim_group_ids": _strings(row.get("claim_group_ids") or row.get("linked_claim_group_ids")), + "liability_group_ids": _strings(row.get("liability_group_ids") or row.get("linked_liability_group_ids")), + "downstream_fact_roles": _strings(row.get("downstream_fact_roles") or row.get("legal_effect_roles")), + } + + + def main() -> None: + try: + _init_mcp_session() + docs = {key: _read_required_json_doc(name, _next_msg_id()) for key, name in INPUT_FILES.items()} + bo_items = _bo_items(docs["bo"]) + bo_by_id = {_bo_id(item): _compact_bo(item) for item in bo_items} + bo_ids = set(bo_by_id) + structures, structure_index, structure_warnings = _validate_legal_effect_structures(docs["legal_effect_structures"], bo_ids) + structure_by_id = { + str(item.get("structure_id")): _compact_structure(item) + for item in structures + if isinstance(item, dict) and item.get("structure_id") + } + evidence_authority = _build_evidence_authority(docs["evidence_indexed"], docs["evidence_all"], docs["evidence_event_candidates"]) + events_by_evidence = _build_event_by_evidence(docs["evidence_event_candidates"]) + signal_hints = _build_signal_hints_by_bo_id({ + "actio_case_signals": docs["actio_case_signals"], + "case_liability_signals": docs["case_liability_signals"], + "legal_effect_signals": docs["legal_effect_signals"], + }, bo_ids) + warnings = list(structure_warnings) + bo_fact_inputs: list[dict[str, Any]] = [] + for bo_id in sorted(bo_by_id): + bo = bo_by_id[bo_id] + evidence_refs = _dedupe(bo["source_evidence_indexes"] + _strings(_linked_by_bo(bo_id, structure_index).get("evidence_index_ids"))) + missing_evidence = [eid for eid in evidence_refs if eid not in evidence_authority] + for eid in missing_evidence: + warnings.append(f"EVIDENCE_REF_NOT_IN_EVIDENCE_UNIVERSE:{bo_id}:{eid}") + bo_fact_inputs.append({ + "source_bo_id": bo_id, + "bo_compact": bo, + "linked_structures": _linked_by_bo(bo_id, structure_index), + "signal_hints": signal_hints.get(bo_id, {"actio": [], "case_liability": [], "legal_effect": []}), + "evidence_refs": evidence_refs, + "evidence_authority": {eid: evidence_authority.get(eid, {"evidence_index": eid, "missing": True}) for eid in evidence_refs}, + "event_candidates": {eid: events_by_evidence.get(eid, []) for eid in evidence_refs}, + }) + payload = { + "fact_source_pack": { + "schema_version": "stage1_fact_source_pack.v2", + "status": "READY_WITH_REVIEW" if warnings else "READY", + "created_at_utc": datetime.now(timezone.utc).isoformat(), + "input_files": INPUT_FILES, + "client_goal_compact": _compact_client_goal(docs["client_goal"]), + "bo_by_id": bo_by_id, + "legal_effect_structures_compact": { + "schema_version": "stage1_legal_effect_structures.v1", + "status": docs["legal_effect_structures"].get("status"), + "structure_by_id": structure_by_id, + "structure_index": { + "by_bo_id": structure_index["by_bo_id"], + "by_issue_cluster_id": structure_index["by_issue_cluster_id"], + "by_asset_cluster_id": structure_index["by_asset_cluster_id"], + "by_evidence_index": structure_index["by_evidence_index"], + "by_candidate_structure_type": structure_index["by_candidate_structure_type"], + }, + }, + "signal_hints_by_bo_id": signal_hints, + "evidence_authority_by_index": evidence_authority, + "event_candidates_by_evidence_index": events_by_evidence, + "bo_fact_inputs": bo_fact_inputs, + "source_integrity_warnings": _dedupe(warnings), + "source_counts": { + "bo_count": len(bo_by_id), + "structure_count": len(structure_by_id), + "evidence_authority_count": len(evidence_authority), + "bo_fact_input_count": len(bo_fact_inputs), + }, + } + } + _write_doc(OUT_PATH, json.dumps(payload, ensure_ascii=False, indent=2), _next_msg_id()) + reread = _read_required_json_doc(OUT_PATH, _next_msg_id()) + if not isinstance(reread, dict) or _as_dict(reread.get("fact_source_pack")).get("schema_version") != "stage1_fact_source_pack.v2": + raise ValueError("post-write reread validation failed") + print(json.dumps({ + "fact_source_pack_compiler": { + "schema_version": "stage1_fact_source_pack_compiler.v1", + "status": payload["fact_source_pack"]["status"], + "written_file": OUT_PATH, + "source_counts": payload["fact_source_pack"]["source_counts"], + "source_integrity_warning_count": len(payload["fact_source_pack"]["source_integrity_warnings"]), + } + }, ensure_ascii=False)) + except Exception as exc: + print(json.dumps({ + "fact_source_pack_compiler": { + "schema_version": "stage1_fact_source_pack_compiler.v1", + "status": "FAILED", + "target_file": OUT_PATH, + "failure_reason": str(exc), + } + }, ensure_ascii=False)) + sys.exit(0) + + + if __name__ == "__main__": + main() + + - task_name: Task_FL1_deterministic_fact_ledger_candidate_builder + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import itertools + import json + import re + import sys + from copy import deepcopy + from datetime import datetime, timezone + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + SOURCE_PACK_PATH = "stage1_tmp/fact_ledger/fact_source_pack.json" + OUT_PATH = "stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json" + FINAL_FIELDS = [ + "fact_id", "source_bo_id", "type", "date", "parties", "object_spec", "amount", "action", + "evidence_refs", "credibility", "legal_centrality", "proof_strength", "legal_effect_roles", + "claim_chain_ref", "linked_structures", "must_not_drop_in_claim_types", "must_consider", + "state_context", "money_claim_effect", "commercial_successor_effect", "actio_roles", + "linked_actio_structures", "downstream_module_candidates", "skeleton_validation_codes", + "derived_fact", "derivation_basis", "legal_calculation_object", + ] + STRUCTURE_TYPE_TO_MODULE = { + "money_claim": "money_claim", + "commercial_successor": "commercial_successor", + "secured_debt": "secured_property", + "registry_invalidity": "registry_invalidity", + "land_use_gain": "land_use_gain", + "valuation": "valuation", + "fraudulent_transfer": "actio", + "preserved_claim": "actio", + "succession_notice_lien_asset_defense": "succession_notice_lien_asset_defense", + "general_legal_effect": "legal_effect", + } + STRUCTURE_TYPE_TO_ROLE = { + "money_claim": "claim_chain_ref", + "commercial_successor": "claim_chain_ref", + "secured_debt": "linked_structures", + "registry_invalidity": "linked_structures", + "land_use_gain": "legal_calculation_object", + "valuation": "legal_calculation_object", + "fraudulent_transfer": "actio_roles", + "preserved_claim": "claim_chain_ref", + "succession_notice_lien_asset_defense": "linked_structures", + "general_legal_effect": "legal_effect_roles", + } + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-fact-ledger-fl1-candidate-builder", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result["result"].get("content", []) + text = content[0].get("text", "") if content else "" + try: + parsed = json.loads(text) + except json.JSONDecodeError as exc: + raise RuntimeError(f"Invalid read_docs JSON envelope for {doc_name}: {text[:300]}") from exc + if isinstance(parsed, dict) and "results" in parsed: + results = parsed.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + first = results[0] + if first.get("error"): + raise RuntimeError(f"{doc_name}: {first['error']}") + inner = first.get("content", "") + if not inner: + raise RuntimeError(f"Empty content for {doc_name}") + if isinstance(inner, str): + try: + return json.loads(inner) + except json.JSONDecodeError: + try: + obj, _ = json.JSONDecoder().raw_decode(inner.strip()) + return obj + except json.JSONDecodeError as exc: + raise RuntimeError(f"Content of {doc_name} is not valid JSON: {inner[:300]}") from exc + return inner + return parsed + + + def _read_required_json_doc(doc_name: str, msg_id: int) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=msg_id) + return _extract_json_from_read_docs_result(result, doc_name) + + + def _write_doc(doc_name: str, content: str, msg_id: int) -> None: + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=msg_id) + + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + if value is None: + return out + if isinstance(value, (str, int, float)): + s = str(value).strip() + return [s] if s else [] + if isinstance(value, list): + for item in value: + out.extend(_strings(item)) + return _dedupe(out) + + + def _dedupe(values: list[str]) -> list[str]: + seen: set[str] = set() + out: list[str] = [] + for value in values: + if value and value not in seen: + seen.add(value) + out.append(value) + return out + + + def _text(value: Any, limit: int = 900) -> str: + if value is None: + return "" + if isinstance(value, str): + s = value + else: + s = json.dumps(value, ensure_ascii=False, sort_keys=True) + return re.sub(r"\s+", " ", s).strip()[:limit] + + + def _first(*values: Any) -> Any: + for value in values: + if value not in (None, "", [], {}): + return value + return None + + + def _score_evidence(evidence_rows: list[dict[str, Any]]) -> tuple[str, str]: + if not evidence_rows: + return "low", "low" + text = " ".join(_text(row, 300).lower() for row in evidence_rows) + documentary_markers = ["등기", "계약", "판결", "공문", "금융", "송금", "계좌", "registry", "contract", "judgment"] + if any(marker.lower() in text for marker in documentary_markers): + return "high", "documentary" + if len(evidence_rows) >= 2: + return "high", "high" + if "meeting" in text or "회의" in text or "면담" in text: + return "medium", "meeting_only_core" + return "medium", "medium" + + + def _legal_roles(types: list[str], structures: list[dict[str, Any]], signals: dict[str, Any]) -> list[str]: + roles: list[str] = [] + for structure in structures: + roles.extend(_strings(structure.get("legal_effect_roles"))) + roles.extend([STRUCTURE_TYPE_TO_ROLE.get(t, "legal_effect_roles") for t in types]) + if signals.get("actio"): + roles.append("actio_roles") + if signals.get("case_liability"): + roles.append("claim_chain_ref") + if signals.get("legal_effect"): + roles.append("legal_effect_roles") + return _dedupe(roles) + + + def _modules(types: list[str], signals: dict[str, Any]) -> list[str]: + modules = [STRUCTURE_TYPE_TO_MODULE.get(t, "legal_effect") for t in types] + if signals.get("actio"): + modules.append("actio") + if signals.get("case_liability"): + modules.append("claim_liability") + if signals.get("legal_effect"): + modules.append("legal_effect") + return _dedupe(modules) + + + def _linked_structures(linked: dict[str, Any]) -> dict[str, Any]: + return { + "structure_ids": _strings(linked.get("structure_ids")), + "issue_cluster_ids": _strings(linked.get("issue_cluster_ids")), + "asset_cluster_ids": _strings(linked.get("asset_cluster_ids")), + "evidence_index_ids": _strings(linked.get("evidence_index_ids")), + "candidate_structure_types": _strings(linked.get("candidate_structure_types")), + "claim_group_ids": _strings(linked.get("claim_group_ids")), + "liability_group_ids": _strings(linked.get("liability_group_ids")), + } + + + def _parties(bo: dict[str, Any], signals: dict[str, Any]) -> list[dict[str, Any]]: + core = _as_dict(bo.get("core_field_base")) + out: list[dict[str, Any]] = [] + for role, key in [("actor", "Performer"), ("counterparty", "Subject")]: + party = core.get(key) + if party: + out.append({"party": _text(party, 160), "role": role, "source": "BO.core_field_base"}) + for signal in _as_list(signals.get("case_liability")): + for item in _as_list(_as_dict(signal).get("party_roles")): + if isinstance(item, dict) and item.get("party"): + out.append({"party": _text(item.get("party"), 160), "role": _text(item.get("role"), 80), "source": "case_liability_signals"}) + seen: set[tuple[str, str]] = set() + deduped: list[dict[str, Any]] = [] + for row in out: + key = (row.get("party", ""), row.get("role", "")) + if key not in seen: + seen.add(key) + deduped.append(row) + return deduped + + + def _derived_fact(bo: dict[str, Any], row: dict[str, Any]) -> str: + date = row.get("date") or "" + actor = "" + for party in row.get("parties", []): + if isinstance(party, dict) and party.get("role") == "actor": + actor = party.get("party") or "" + break + obj = row.get("object_spec") or "" + action = row.get("action") or bo.get("action") or "" + if date and actor and obj and action: + return f"[{date}] {actor}가 {obj}에 관하여 {action}하였다는 사실" + if action: + return str(action) + return "" + + + def _basis(bo_id: str, evidence_refs: list[str], structures: dict[str, Any]) -> str: + parts = [f"source_bo_id={bo_id}"] + if evidence_refs: + parts.append("evidence_refs=" + ",".join(evidence_refs[:8])) + sids = _strings(structures.get("structure_ids")) + if sids: + parts.append("structure_ids=" + ",".join(sids[:8])) + return "; ".join(parts) + + + def _claim_chain_ref(linked: dict[str, Any], signals: dict[str, Any]) -> dict[str, Any]: + return { + "claim_group_ids": _strings(linked.get("claim_group_ids")), + "liability_group_ids": _strings(linked.get("liability_group_ids")), + "signal_count": len(_as_list(signals.get("case_liability"))), + } + + + def _legal_calculation_object(row: dict[str, Any], types: list[str]) -> dict[str, Any]: + needs_calc = any(t in {"money_claim", "secured_debt", "land_use_gain", "valuation", "fraudulent_transfer"} for t in types) + return { + "calculation_required": needs_calc, + "amount_source": "BO.amount" if row.get("amount") else None, + "amount_text": row.get("amount"), + "object_basis": row.get("object_spec"), + "candidate_structure_types": types, + } + + + def _exception_item( + exception_id: str, + exception_type: str, + row: dict[str, Any], + trigger_reason: str, + related_bo: dict[str, Any], + related_structures: list[dict[str, Any]], + signals: dict[str, Any], + evidence_rows: list[dict[str, Any]], + allowed_output_fields: list[str], + allowed_decisions: list[str], + conflict_flags: list[str], + ) -> dict[str, Any]: + return { + "exception_id": exception_id, + "exception_type": exception_type, + "fact_id": row["fact_id"], + "source_bo_id": row["source_bo_id"], + "severity": "blocking_projection" if exception_type in {"legal_calculation_required", "unresolved_structure_role_projection"} else "review", + "trigger_reason": trigger_reason, + "candidate_fact_row_subset": {key: deepcopy(row.get(key)) for key in allowed_output_fields if key in row}, + "related_bo_compact": related_bo, + "related_structure_snippets": related_structures[:8], + "related_issue_asset_evidence_index_snippets": { + "issue": row["linked_structures"].get("issue_cluster_ids", []), + "asset": row["linked_structures"].get("asset_cluster_ids", []), + "evidence": row["linked_structures"].get("evidence_index_ids", []), + "candidate_structure_type": row["linked_structures"].get("candidate_structure_types", []), + }, + "related_signal_hints": signals, + "related_evidence_excerpts": evidence_rows[:8], + "allowed_output_fields": allowed_output_fields, + "allowed_decisions": allowed_decisions, + "conflict_flags": conflict_flags, + } + + + def _build_row(idx: int, item: dict[str, Any], source_pack: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]: + bo_id = item["source_bo_id"] + bo = _as_dict(item.get("bo_compact")) + linked = _linked_structures(_as_dict(item.get("linked_structures"))) + signals = _as_dict(item.get("signal_hints")) + structures_by_id = _as_dict(_as_dict(source_pack.get("legal_effect_structures_compact")).get("structure_by_id")) + related_structures = [_as_dict(structures_by_id.get(sid)) for sid in linked["structure_ids"] if isinstance(structures_by_id.get(sid), dict)] + types = _dedupe(linked["candidate_structure_types"] + [str(s.get("candidate_structure_type")) for s in related_structures if s.get("candidate_structure_type")]) + evidence_refs = _dedupe(_strings(item.get("evidence_refs")) + linked["evidence_index_ids"]) + evidence_rows = [_as_dict(_as_dict(item.get("evidence_authority")).get(eid)) for eid in evidence_refs] + evidence_rows = [row for row in evidence_rows if row] + credibility, proof_strength = _score_evidence(evidence_rows) + core = _as_dict(bo.get("core_field_base")) + row = { + "fact_id": f"F-{idx:03d}", + "source_bo_id": bo_id, + "type": _first(bo.get("bo_type"), types[0] if types else None), + "date": _first(core.get("BehaviorTime"), bo.get("date")), + "parties": _parties(bo, signals), + "object_spec": _first(core.get("Object"), _text(linked["asset_cluster_ids"], 160)), + "amount": _text(_first(bo.get("amount"), core.get("Amount")), 240) or None, + "action": _first(bo.get("action"), core.get("Action")), + "evidence_refs": evidence_refs, + "credibility": credibility, + "legal_centrality": "critical" if linked["structure_ids"] or signals.get("actio") else "medium", + "proof_strength": proof_strength, + "legal_effect_roles": _legal_roles(types, related_structures, signals), + "claim_chain_ref": _claim_chain_ref(linked, signals), + "linked_structures": linked, + "must_not_drop_in_claim_types": _modules(types, signals) if linked["structure_ids"] or signals.get("actio") else [], + "must_consider": bool(linked["structure_ids"] or signals.get("actio")), + "state_context": { + "bo_type": bo.get("bo_type"), + "issue_cluster_ids": linked["issue_cluster_ids"], + "asset_cluster_ids": linked["asset_cluster_ids"], + }, + "money_claim_effect": {"enabled": "money_claim" in types, "source_bo_id": bo_id if "money_claim" in types else None}, + "commercial_successor_effect": {"enabled": "commercial_successor" in types, "source_bo_id": bo_id if "commercial_successor" in types else None}, + "actio_roles": _as_list(signals.get("actio")), + "linked_actio_structures": { + "structure_ids": [s.get("structure_id") for s in related_structures if s.get("candidate_structure_type") in {"fraudulent_transfer", "preserved_claim"}], + "actio_signal_count": len(_as_list(signals.get("actio"))), + }, + "downstream_module_candidates": _modules(types, signals), + "skeleton_validation_codes": [], + "derived_fact": "", + "derivation_basis": "", + "legal_calculation_object": {}, + } + row["legal_calculation_object"] = _legal_calculation_object(row, types) + row["derived_fact"] = _derived_fact(bo, row) + row["derivation_basis"] = _basis(bo_id, evidence_refs, linked) + if not row["derived_fact"]: + row["skeleton_validation_codes"].append("EMPTY_DERIVED_FACT") + if not row["evidence_refs"]: + row["skeleton_validation_codes"].append("NO_EVIDENCE_REFS") + if not linked["structure_ids"]: + row["skeleton_validation_codes"].append("NO_LINKED_LEGAL_EFFECT_STRUCTURE") + return row, related_structures + + + def _exceptions_for_row(counter: itertools.count, row: dict[str, Any], bo: dict[str, Any], structures: list[dict[str, Any]], signals: dict[str, Any], evidence_rows: list[dict[str, Any]]) -> list[dict[str, Any]]: + exceptions: list[dict[str, Any]] = [] + roles = _strings(row.get("legal_effect_roles")) + modules = _strings(row.get("downstream_module_candidates")) + linked = _as_dict(row.get("linked_structures")) + if len(roles) > 2 and len(modules) > 1: + exceptions.append(_exception_item( + f"FX-{next(counter):03d}", "ambiguous_legal_role", row, "multiple_legal_roles_and_modules", + bo, structures, signals, evidence_rows, + ["legal_effect_roles", "downstream_module_candidates", "must_not_drop_in_claim_types", "must_consider"], + ["PATCH", "KEEP_AS_IS", "BLOCK_REVIEW"], ["MULTIPLE_ROLE_PROJECTION"], + )) + claim_refs = _strings(_as_dict(row.get("claim_chain_ref")).get("claim_group_ids")) + liability_refs = _strings(_as_dict(row.get("claim_chain_ref")).get("liability_group_ids")) + if len(claim_refs) + len(liability_refs) > 2: + exceptions.append(_exception_item( + f"FX-{next(counter):03d}", "ambiguous_claim_chain", row, "multiple_claim_or_liability_groups", + bo, structures, signals, evidence_rows, + ["claim_chain_ref", "must_consider", "derivation_basis"], + ["PATCH", "KEEP_AS_IS", "BLOCK_REVIEW"], ["MULTIPLE_CLAIM_CHAIN_CANDIDATES"], + )) + if len(str(row.get("derived_fact") or "")) < 12 or "EMPTY_DERIVED_FACT" in row.get("skeleton_validation_codes", []): + exceptions.append(_exception_item( + f"FX-{next(counter):03d}", "weak_derived_fact", row, "derived_fact_too_weak", + bo, structures, signals, evidence_rows, + ["derived_fact", "derivation_basis", "skeleton_validation_codes"], + ["PATCH", "KEEP_AS_IS", "BLOCK_REVIEW"], ["WEAK_DERIVED_FACT"], + )) + calc = _as_dict(row.get("legal_calculation_object")) + if calc.get("calculation_required") and (not row.get("amount") or not row.get("object_spec")): + exceptions.append(_exception_item( + f"FX-{next(counter):03d}", "legal_calculation_required", row, "calculation_source_missing_or_incomplete", + bo, structures, signals, evidence_rows, + ["amount", "object_spec", "legal_calculation_object", "derivation_basis", "skeleton_validation_codes"], + ["PATCH", "KEEP_AS_IS", "BLOCK_REVIEW"], ["CALCULATION_BASIS_INCOMPLETE"], + )) + if linked.get("asset_cluster_ids") and not row.get("object_spec"): + exceptions.append(_exception_item( + f"FX-{next(counter):03d}", "unresolved_asset_cluster_projection", row, "asset_cluster_not_projected_to_fact_object", + bo, structures, signals, evidence_rows, + ["object_spec", "linked_structures", "legal_calculation_object", "derivation_basis"], + ["PATCH", "KEEP_AS_IS", "BLOCK_REVIEW"], ["ASSET_CLUSTER_PROJECTION_GAP"], + )) + if linked.get("structure_ids") and (not roles or not modules): + exceptions.append(_exception_item( + f"FX-{next(counter):03d}", "unresolved_structure_role_projection", row, "structure_not_projected_to_fact_role", + bo, structures, signals, evidence_rows, + ["legal_effect_roles", "downstream_module_candidates", "claim_chain_ref", "actio_roles", "linked_structures"], + ["PATCH", "KEEP_AS_IS", "BLOCK_REVIEW"], ["STRUCTURE_ROLE_PROJECTION_GAP"], + )) + return exceptions + + + POSSESSION_INCOMPATIBLE_CAUSE_PAIRS = [ + ("유치권행사중", "무권원주장"), + ] + + + def _mark_row_code(row: dict[str, Any], code: str) -> None: + codes = row.setdefault("skeleton_validation_codes", []) + if isinstance(codes, list) and code not in codes: + codes.append(code) + + + def _domain_payload_compact(bo_by_id: dict[str, Any], row: dict[str, Any]) -> dict[str, Any]: + bo = _as_dict(bo_by_id.get(str(row.get("source_bo_id")))) + return _as_dict(bo.get("domain_payload_compact")) + + + def _clean_str(value: Any) -> str: + return str(value).strip() if value not in (None, "") else "" + + + def _periods_overlap(start_a: str, end_a: str, start_b: str, end_b: str) -> bool: + a0, a1 = start_a or "0000-00-00", end_a or "9999-12-31" + b0, b1 = start_b or "0000-00-00", end_b or "9999-12-31" + return a0 <= b1 and b0 <= a1 + + + def _possession_conflict_scan(rows: list[dict[str, Any]], bo_by_id: dict[str, Any]) -> None: + incompatible = {frozenset(pair) for pair in POSSESSION_INCOMPATIBLE_CAUSE_PAIRS} + entries: list[dict[str, Any]] = [] + for row in rows: + possession = _as_dict(_domain_payload_compact(bo_by_id, row).get("possession_state")) + if not possession: + continue + asset_keys = _strings(possession.get("asset_ref")) or _strings(_as_dict(row.get("state_context")).get("asset_cluster_ids")) + events = [x for x in (_as_dict(e) for e in _as_list(possession.get("possession_events"))) if x] + causes = [x for x in (_as_dict(c) for c in _as_list(possession.get("legal_cause_assertions"))) if _clean_str(x.get("cause_asserted"))] + if events and not causes: + _mark_row_code(row, "LEGAL_CAUSE_STATE_UNKNOWN_REVIEW") + starts = sorted([s for s in (_clean_str(e.get("start_date")) for e in events) if s]) + ends = [_clean_str(e.get("end_date")) for e in events] + open_ended = (not events) or any(not e for e in ends) + period_start = starts[0] if starts else "" + period_end = "" if open_ended else max(e for e in ends if e) + for cause in causes: + entries.append({ + "row": row, + "asset_keys": set(asset_keys), + "cause": _clean_str(cause.get("cause_asserted")), + "start": _clean_str(cause.get("assertion_date")) or period_start, + "end": period_end, + }) + for i in range(len(entries)): + for j in range(i + 1, len(entries)): + first, second = entries[i], entries[j] + if first["row"] is second["row"]: + continue + if not (first["asset_keys"] & second["asset_keys"]): + continue + if frozenset((first["cause"], second["cause"])) not in incompatible: + continue + if not _periods_overlap(first["start"], first["end"], second["start"], second["end"]): + continue + for row in (first["row"], second["row"]): + _mark_row_code(row, "POSSESSION_CAUSE_CONFLICT") + row["must_consider"] = True + + + def _notice_receipt_gate_scan(rows: list[dict[str, Any]], bo_by_id: dict[str, Any]) -> None: + for row in rows: + notice = _as_dict(_domain_payload_compact(bo_by_id, row).get("notice_lifecycle")) + if not notice: + continue + if notice.get("requires_receipt_for_effect") is not True: + continue + received = _clean_str(notice.get("received_date")) + arrival = _clean_str(notice.get("arrival_date")) + if not received and not arrival: + _mark_row_code(row, "NOTICE_RECEIPT_UNVERIFIED") + row["must_consider"] = True + if received and not _strings(notice.get("receipt_evidence_refs")) and not _clean_str(notice.get("recipient_admission_text")): + _mark_row_code(row, "NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW") + + + def _lien_extinction_notice_scan(rows: list[dict[str, Any]], bo_by_id: dict[str, Any]) -> None: + receipt_by_notice_id: dict[str, str] = {} + possession_entries: list[dict[str, Any]] = [] + for row in rows: + payload = _domain_payload_compact(bo_by_id, row) + notice = _as_dict(payload.get("notice_lifecycle")) + notice_id = _clean_str(notice.get("notice_id")) + receipt_date = _clean_str(notice.get("received_date")) or _clean_str(notice.get("arrival_date")) + if notice_id and receipt_date: + receipt_by_notice_id[notice_id] = min(receipt_date, receipt_by_notice_id.get(notice_id, "9999-12-31")) + possession = _as_dict(payload.get("possession_state")) + asset_keys = set(_strings(possession.get("asset_ref"))) + for event in (_as_dict(x) for x in _as_list(possession.get("possession_events"))): + start = _clean_str(event.get("start_date")) + if start and asset_keys: + possession_entries.append({"row": row, "asset_keys": asset_keys, "start": start}) + for row in rows: + lien = _as_dict(_domain_payload_compact(bo_by_id, row).get("lien_state")) + if not lien: + continue + receipt_dates = [receipt_by_notice_id[ref] for ref in _strings(lien.get("extinction_notice_refs")) if ref in receipt_by_notice_id] + lien_assets = set(_strings(lien.get("asset_ref"))) + if not receipt_dates or not lien_assets: + continue + receipt_date = min(receipt_dates) + for entry in possession_entries: + if (lien_assets & entry["asset_keys"]) and entry["start"] > receipt_date: + _mark_row_code(row, "LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW") + _mark_row_code(entry["row"], "LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW") + + + def _validate_candidate_bundle(bundle: dict[str, Any]) -> None: + if bundle.get("schema_version") != "stage1_fact_ledger_candidate_bundle.v1": + raise ValueError("candidate bundle schema_version mismatch") + for item in _as_list(bundle.get("candidate_items")): + missing = [field for field in FINAL_FIELDS if field not in item] + if missing: + raise ValueError(f"candidate item missing fields: {missing}") + extra = [field for field in item if field not in FINAL_FIELDS] + if extra: + raise ValueError(f"candidate item has arbitrary fields: {extra}") + pack = _as_dict(bundle.get("exception_pack")) + if pack.get("schema_version") != "stage1_fact_ledger_exception_pack.v1": + raise ValueError("exception pack schema_version mismatch") + allowed_types = { + "ambiguous_legal_role", "ambiguous_claim_chain", "weak_derived_fact", + "legal_calculation_required", "unresolved_asset_cluster_projection", + "unresolved_structure_role_projection", + } + for exc in _as_list(pack.get("exceptions")): + if exc.get("exception_type") not in allowed_types: + raise ValueError(f"unsupported exception_type: {exc.get('exception_type')}") + + + def main() -> None: + try: + _init_mcp_session() + source_outer = _read_required_json_doc(SOURCE_PACK_PATH, _next_msg_id()) + source_pack = _as_dict(_as_dict(source_outer).get("fact_source_pack")) + if source_pack.get("schema_version") != "stage1_fact_source_pack.v2": + raise ValueError("fact_source_pack schema_version mismatch") + candidate_items: list[dict[str, Any]] = [] + exceptions: list[dict[str, Any]] = [] + counter = itertools.count(1) + source_inputs = _as_list(source_pack.get("bo_fact_inputs")) + for idx, item in enumerate(sorted(source_inputs, key=lambda x: str(_as_dict(x).get("source_bo_id"))), start=1): + row, related_structures = _build_row(idx, _as_dict(item), source_pack) + candidate_items.append(row) + bo = _as_dict(item.get("bo_compact")) + signals = _as_dict(item.get("signal_hints")) + evidence_rows = [_as_dict(_as_dict(item.get("evidence_authority")).get(eid)) for eid in _strings(item.get("evidence_refs"))] + evidence_rows = [x for x in evidence_rows if x] + exceptions.extend(_exceptions_for_row(counter, row, bo, related_structures, signals, evidence_rows)) + bo_compact_by_id = _as_dict(source_pack.get("bo_by_id")) + _possession_conflict_scan(candidate_items, bo_compact_by_id) + _notice_receipt_gate_scan(candidate_items, bo_compact_by_id) + _lien_extinction_notice_scan(candidate_items, bo_compact_by_id) + status = "READY_WITH_EXCEPTIONS" if exceptions else "READY" + exception_pack = { + "schema_version": "stage1_fact_ledger_exception_pack.v1", + "status": "READY", + "has_exceptions": bool(exceptions), + "exception_count": len(exceptions), + "exceptions": exceptions, + "closed_exception_type_enum": [ + "ambiguous_legal_role", + "ambiguous_claim_chain", + "weak_derived_fact", + "legal_calculation_required", + "unresolved_asset_cluster_projection", + "unresolved_structure_role_projection", + ], + } + bundle = { + "schema_version": "stage1_fact_ledger_candidate_bundle.v1", + "status": status, + "created_at_utc": datetime.now(timezone.utc).isoformat(), + "source_pack_file": SOURCE_PACK_PATH, + "candidate_items": candidate_items, + "exception_pack": exception_pack, + "compile_gate": { + "candidate_count": len(candidate_items), + "exception_count": len(exceptions), + "candidate_schema_closed": True, + "exception_schema_closed": True, + }, + } + _validate_candidate_bundle(bundle) + payload = {"fact_ledger_candidate_bundle": bundle} + _write_doc(OUT_PATH, json.dumps(payload, ensure_ascii=False, indent=2), _next_msg_id()) + reread = _read_required_json_doc(OUT_PATH, _next_msg_id()) + if _as_dict(_as_dict(reread).get("fact_ledger_candidate_bundle")).get("schema_version") != "stage1_fact_ledger_candidate_bundle.v1": + raise ValueError("post-write reread validation failed") + print(json.dumps({ + "fact_exception_adjudicator_input": { + "schema_version": "stage1_fact_exception_adjudicator_input.v1", + "status": "READY", + "candidate_file": OUT_PATH, + "exception_count": len(exceptions), + "exception_pack": exception_pack, + "closed_enum_metadata": { + "exception_type_enum": exception_pack["closed_exception_type_enum"], + "decision_enum": ["PATCH", "KEEP_AS_IS", "SPLIT_FACT", "BLOCK_REVIEW"], + "status_enum": ["READY", "READY_NO_EXCEPTIONS", "READY_WITH_BLOCKS", "FAILED"], + }, + "gate_metadata": { + "candidate_file_contains_full_candidate_bundle": True, + "fl2_input_candidate_payload": "excluded", + "fl2_input_scope": "exception_pack_only", + }, + } + }, ensure_ascii=False)) + except Exception as exc: + print(json.dumps({ + "fact_exception_adjudicator_input": { + "schema_version": "stage1_fact_exception_adjudicator_input.v1", + "status": "FAILED", + "target_file": OUT_PATH, + "failure_reason": str(exc), + } + }, ensure_ascii=False)) + sys.exit(0) + + + if __name__ == "__main__": + main() + + - task_name: Task_FL2_single_fact_exception_adjudicator + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: medium + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + + {{prev.Task_FL1_deterministic_fact_ledger_candidate_builder}} + + + + + + - You MUST act from the perspective of a top-tier Korean civil-litigation lawyer with more than 20 years of experience and a world-class AI architect. + - You MUST use only . + - You MUST validate that the input root is `fact_exception_adjudicator_input`. + - You MUST validate `fact_exception_adjudicator_input.schema_version == "stage1_fact_exception_adjudicator_input.v1"`. + - You MUST validate `fact_exception_adjudicator_input.status == "READY"`. + - You MUST validate that `fact_exception_adjudicator_input` contains `candidate_file`, `exception_count`, `exception_pack`, `closed_enum_metadata`, and `gate_metadata`. + - You MUST validate `exception_pack.schema_version == "stage1_fact_ledger_exception_pack.v1"`. + - You MUST validate `exception_pack.status == "READY"`. + - You MUST read only `exception_pack.exceptions[]` and fields embedded inside each exception item. + - You MUST NOT read, infer from, or request `Fact_Ledger_base_candidate.json` or the file named in `candidate_file`. + - You MUST NOT read, request, reconstruct, or rely on full `client_goal.json`, `BO.json`, `evidence_indexed.json`, `evidence_all.json`, `evidence_event_candidates.json`, `actio_case_signals.json`, `case_liability_signals.json`, `legal_effect_signals.json`, or `legal_effect_structures.json`. + - You MUST NOT re-adjudicate LES2 issue cluster, asset cluster, or candidate structure type decisions. + - You MUST NOT decide whether BOs share the same legal issue, same legal asset group, or same candidate structure type. + - You MUST NOT create new BO IDs, evidence indexes, structure IDs, issue cluster IDs, asset cluster IDs, facts, parties, dates, amounts, or source references. + - You MUST NOT write `Fact_Ledger_base.json` or any file. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: Task_FL2_single_fact_exception_adjudicator + STAGE: Stage 1 Fact Ledger direct compiler + ROLE: compact exception-only LLM adjudicator before deterministic FL3 final writer + MISSION: Adjudicate only Fact Ledger projection exceptions produced by deterministic FL1. + + + + - You are not the final writer. + - You may decide only exceptions explicitly listed in `exception_pack.exceptions[]`. + - You may not change or add exception IDs. + - You may not alter non-exception rows. + - You may not add source identifiers outside embedded compact records. + - You may not invent a missing basis. If the compact record is insufficient, return `BLOCK_REVIEW`. + - FL3 will apply or ignore patches, validate refs, preserve blocked review warnings, and write the final file. + + + + - `ambiguous_legal_role`: fact row has multiple legal roles or module routes and needs projection selection. + - `ambiguous_claim_chain`: fact row has multiple claim/liability groups and needs chain projection selection. + - `weak_derived_fact`: deterministic derived_fact is too weak and may be patched from embedded BO/evidence/structure basis. + - `legal_calculation_required`: amount, cap, secured debt, use-gain, valuation, or value-compensation source projection is incomplete. + - `unresolved_asset_cluster_projection`: LES3_v1 asset cluster is already decided, but its projection into object/calculation fields is incomplete. + - `unresolved_structure_role_projection`: LES3_v1 structure/type decision is already decided, but its projection into Fact Ledger role fields is incomplete. + + + + - `unresolved_asset_cluster_projection` does NOT authorize deciding whether assets are the same legal asset group. + - `unresolved_structure_role_projection` does NOT authorize selecting or changing candidate structure type. + - Do not output `ambiguous_asset_cluster` or `ambiguous_structure_type`. + - If an exception would require reopening LES2 legal-structure ambiguity, set `decision: "BLOCK_REVIEW"` and set `reopened_les_ambiguity: true` in `adjudication_gate`. + + + + For each exception item, read only these fields: + - `exception_id` + - `exception_type` + - `fact_id` + - `source_bo_id` + - `severity` + - `trigger_reason` + - `candidate_fact_row_subset` + - `related_bo_compact` + - `related_structure_snippets` + - `related_issue_asset_evidence_index_snippets` + - `related_signal_hints` + - `related_evidence_excerpts` + - `allowed_output_fields` + - `allowed_decisions` + - `conflict_flags` + + You must ignore all unsupported, raw, or non-compact material if present. + + + + - Use `KEEP_AS_IS` when FL1's candidate row is adequate. + - Use `PATCH` only when the patch can be grounded in embedded compact fields. + - Use `SPLIT_FACT` only when the exception's `allowed_decisions` includes `SPLIT_FACT` and the embedded subset already contains separate row bases. + - Use `BLOCK_REVIEW` when the compact record is insufficient, conflicting, or would require reopening LES2 legal-structure ambiguity. + - If an exception item's `trigger_reason`, description text, or `conflict_flags` carry a `legal_theory_required` tag, do not resolve it with `PATCH`; prefer `BLOCK_REVIEW` (it belongs to the downstream legal module). + - Keep patch objects small. Prefer patching only fields listed in `allowed_output_fields`, but if a necessary field is outside that list and present in the final Fact Ledger schema, you may include it with a short `reason_code`. + - Do not patch arbitrary fields outside the final Fact Ledger row schema. + + + + 1. Parse and locate `fact_exception_adjudicator_input.exception_pack`. + 2. If `exception_pack.has_exceptions == false` or `exceptions[]` is empty, return `status: "READY_NO_EXCEPTIONS"` with empty `decisions` and `blocked_review_items`. + 3. For every item in `exceptions[]`, produce exactly one decision or one blocked review item. + 4. Use only the item's `allowed_decisions`. + 5. If the item cannot be decided safely, use `decision: "BLOCK_REVIEW"` and add the same item to `blocked_review_items`. + 6. Keep rationales short and basis-code-like. Do not reveal chain-of-thought. + 7. Return exactly one JSON object matching . + + + + Return exactly one JSON object: + { + "fact_exception_adjudication": { + "schema_version": "stage1_fact_exception_adjudication.v1", + "status": "READY|READY_NO_EXCEPTIONS|READY_WITH_BLOCKS|FAILED", + "source_exception_pack_schema_version": "stage1_fact_ledger_exception_pack.v1", + "decisions": [ + { + "exception_id": "FX-001", + "exception_type": "ambiguous_legal_role|ambiguous_claim_chain|weak_derived_fact|legal_calculation_required|unresolved_asset_cluster_projection|unresolved_structure_role_projection", + "fact_id": "F-001", + "source_bo_id": "string", + "decision": "PATCH|KEEP_AS_IS|SPLIT_FACT|BLOCK_REVIEW", + "patch": { + "legal_effect_roles": [], + "claim_chain_ref": {}, + "linked_structures": {}, + "must_not_drop_in_claim_types": [], + "must_consider": true, + "downstream_module_candidates": [], + "derived_fact": "string", + "derivation_basis": "string", + "legal_calculation_object": {} + }, + "split_rows": [ + { + "split_label": "string", + "patch": {} + } + ], + "review_required": false, + "reason_code": "projection_aligned|claim_chain_selected|derived_fact_repaired|calculation_basis_selected|asset_projection_selected|structure_role_projected|keep_existing|insufficient_compact_record|reopens_les_ambiguity|unsupported_patch", + "basis_fields_used": [], + "conflict_flags_acknowledged": [] + } + ], + "blocked_review_items": [ + { + "exception_id": "FX-001", + "exception_type": "ambiguous_legal_role|ambiguous_claim_chain|weak_derived_fact|legal_calculation_required|unresolved_asset_cluster_projection|unresolved_structure_role_projection", + "fact_id": "F-001", + "source_bo_id": "string", + "block_reason": "insufficient_compact_record|reopens_les_ambiguity|unsupported_patch|missing_candidate_basis|schema_violation" + } + ], + "adjudication_gate": { + "input_exception_count": 0, + "decision_count": 0, + "blocked_count": 0, + "coverage_pass": true, + "json_only_contract_pass": true, + "reopened_les_ambiguity": false + } + } + } + + Status rules: + - `READY_NO_EXCEPTIONS`: input exception list is empty. + - `READY`: every input exception has exactly one non-BLOCK_REVIEW decision. + - `READY_WITH_BLOCKS`: every input exception is covered, but at least one item is `BLOCK_REVIEW`. + - `FAILED`: schema is unreadable, required roots are missing, or coverage cannot be verified. + + Field rules: + - `basis_fields_used` must be a subset of fields embedded in the exception item. + - `conflict_flags_acknowledged` must be a subset of the input exception item's `conflict_flags`. + - `patch` must contain only final Fact Ledger row fields. + - `review_required` is true only for `BLOCK_REVIEW`, split-sensitive decisions, or unresolved projection. + - `adjudication_gate.input_exception_count` must equal the input exception count. + - `coverage_pass` is true only when every input `exception_id` appears in either `decisions[]` or `blocked_review_items[]`. + + + - task_name: Task_FL3_final_fact_ledger_gate_and_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import itertools + import json + import sys + from copy import deepcopy + from datetime import datetime, timezone + from typing import Any + + import httpx + + ADJUDICATION_RAW = r'''{{prev.Task_FL2_single_fact_exception_adjudicator}}''' + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + SOURCE_PACK_PATH = "stage1_tmp/fact_ledger/fact_source_pack.json" + CANDIDATE_PATH = "stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json" + TARGET_PATH = "Fact_Ledger_base.json" + REPORT_PATH = "stage1_tmp/fact_ledger/fact_ledger_writer_report.json" + FINAL_FIELDS = [ + "fact_id", "source_bo_id", "type", "date", "parties", "object_spec", "amount", "action", + "evidence_refs", "credibility", "legal_centrality", "proof_strength", "legal_effect_roles", + "claim_chain_ref", "linked_structures", "must_not_drop_in_claim_types", "must_consider", + "state_context", "money_claim_effect", "commercial_successor_effect", "actio_roles", + "linked_actio_structures", "downstream_module_candidates", "skeleton_validation_codes", + "derived_fact", "derivation_basis", "legal_calculation_object", + ] + FINAL_FIELD_SET = set(FINAL_FIELDS) + FINAL_DEFAULTS = { + "type": None, + "date": None, + "parties": [], + "object_spec": None, + "amount": None, + "action": None, + "evidence_refs": [], + "credibility": "low", + "legal_centrality": "low", + "proof_strength": "low", + "legal_effect_roles": [], + "claim_chain_ref": {}, + "linked_structures": {}, + "must_not_drop_in_claim_types": [], + "must_consider": False, + "state_context": {}, + "money_claim_effect": {}, + "commercial_successor_effect": {}, + "actio_roles": [], + "linked_actio_structures": {}, + "downstream_module_candidates": [], + "skeleton_validation_codes": [], + "derived_fact": "", + "derivation_basis": "", + "legal_calculation_object": {}, + } + JSON_DECODER = json.JSONDecoder() + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-fact-ledger-fl3-final-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result["result"].get("content", []) + text = content[0].get("text", "") if content else "" + try: + parsed = json.loads(text) + except json.JSONDecodeError as exc: + raise RuntimeError(f"Invalid read_docs JSON envelope for {doc_name}: {text[:300]}") from exc + if isinstance(parsed, dict) and "results" in parsed: + results = parsed.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + first = results[0] + if first.get("error"): + raise RuntimeError(f"{doc_name}: {first['error']}") + inner = first.get("content", "") + if not inner: + raise RuntimeError(f"Empty content for {doc_name}") + if isinstance(inner, str): + try: + return json.loads(inner) + except json.JSONDecodeError: + try: + obj, _ = json.JSONDecoder().raw_decode(inner.strip()) + return obj + except json.JSONDecodeError as exc: + raise RuntimeError(f"Content of {doc_name} is not valid JSON: {inner[:300]}") from exc + return inner + return parsed + + + def _read_required_json_doc(doc_name: str, msg_id: int) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=msg_id) + return _extract_json_from_read_docs_result(result, doc_name) + + + def _write_doc(doc_name: str, content: str, msg_id: int) -> None: + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=msg_id) + + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + if value is None: + return out + if isinstance(value, (str, int, float)): + s = str(value).strip() + return [s] if s else [] + if isinstance(value, list): + for item in value: + out.extend(_strings(item)) + return _dedupe(out) + + + def _dedupe(values: list[str]) -> list[str]: + seen: set[str] = set() + out: list[str] = [] + for value in values: + if value and value not in seen: + seen.add(value) + out.append(value) + return out + + + def _parse_complete_json_object(raw: str, label: str) -> dict[str, Any]: + text = (raw or "").strip() + if not text or text == "null" or text.startswith("{{"): + raise ValueError(f"{label}: no injected JSON body") + start = text.find("{") + if start < 0: + raise ValueError(f"{label}: JSON object start not found") + obj, end = JSON_DECODER.raw_decode(text[start:]) + tail = text[start + end:].strip() + if tail: + raise ValueError(f"{label}: non-json tail after object") + if not isinstance(obj, dict): + raise ValueError(f"{label}: JSON root must be object") + return obj + + + def _unwrap_prev(raw: str, label: str) -> dict[str, Any]: + obj = _parse_complete_json_object(raw, label) + if obj.get("type") == "text" and isinstance(obj.get("text"), str): + obj = _parse_complete_json_object(obj["text"], f"{label}.text") + if obj.get("success") is True and isinstance(obj.get("output"), str): + obj = _parse_complete_json_object(obj["output"], f"{label}.output") + return obj + + + def _load_adjudication() -> dict[str, Any]: + outer = _unwrap_prev(ADJUDICATION_RAW, "ADJUDICATION_RAW") + adjudication = _as_dict(outer.get("fact_exception_adjudication")) + if adjudication.get("schema_version") != "stage1_fact_exception_adjudication.v1": + raise ValueError("fact_exception_adjudication schema_version mismatch") + if adjudication.get("status") not in {"READY", "READY_NO_EXCEPTIONS", "READY_WITH_BLOCKS", "FAILED"}: + raise ValueError("fact_exception_adjudication status invalid") + if adjudication.get("status") == "FAILED": + raise ValueError("fact_exception_adjudication status is FAILED") + return adjudication + + + def _validate_candidate_bundle(bundle: dict[str, Any]) -> list[dict[str, Any]]: + if bundle.get("schema_version") != "stage1_fact_ledger_candidate_bundle.v1": + raise ValueError("candidate bundle schema_version mismatch") + items = _as_list(bundle.get("candidate_items")) + if not items: + raise ValueError("candidate_items is empty") + for item in items: + missing = [field for field in FINAL_FIELDS if field not in item] + if missing: + raise ValueError(f"candidate item missing fields: {missing}") + extra = [field for field in item if field not in FINAL_FIELD_SET] + if extra: + raise ValueError(f"candidate item has arbitrary fields: {extra}") + return [deepcopy(x) for x in items if isinstance(x, dict)] + + + def _add_code(row: dict[str, Any], code: str) -> None: + codes = row.setdefault("skeleton_validation_codes", []) + if isinstance(codes, list) and code not in codes: + codes.append(code) + + + def _append_basis(row: dict[str, Any], text: str) -> None: + basis = str(row.get("derivation_basis") or "") + row["derivation_basis"] = (basis + "; " + text).strip("; ") + + + def _apply_patch(row: dict[str, Any], patch: dict[str, Any], allowed_fields: list[str], report: dict[str, Any], exception_id: str) -> None: + for key, value in patch.items(): + if key not in FINAL_FIELD_SET: + report["unapplied_patch_fields"].append({"exception_id": exception_id, "field": key, "reason": "not_in_final_schema"}) + continue + if key not in allowed_fields: + report["patch_policy_warnings"].append({"exception_id": exception_id, "field": key, "warning": "PATCH_FIELD_OUTSIDE_ALLOWED_OUTPUT_FIELDS"}) + row[key] = value + + + def _exception_allowed_fields(exception_pack: dict[str, Any]) -> dict[str, list[str]]: + out: dict[str, list[str]] = {} + for exc in _as_list(exception_pack.get("exceptions")): + if isinstance(exc, dict) and exc.get("exception_id"): + out[str(exc["exception_id"])] = _strings(exc.get("allowed_output_fields")) + return out + + + def _validate_adjudication_coverage(adjudication: dict[str, Any], exception_pack: dict[str, Any]) -> None: + exceptions = _as_list(exception_pack.get("exceptions")) + expected = {str(exc.get("exception_id")) for exc in exceptions if isinstance(exc, dict) and exc.get("exception_id")} + decided = {str(dec.get("exception_id")) for dec in _as_list(adjudication.get("decisions")) if isinstance(dec, dict) and dec.get("exception_id")} + blocked = {str(item.get("exception_id")) for item in _as_list(adjudication.get("blocked_review_items")) if isinstance(item, dict) and item.get("exception_id")} + if expected and not expected.issubset(decided | blocked): + missing = sorted(expected - (decided | blocked)) + raise ValueError(f"adjudication coverage failed: missing={missing}") + gate = _as_dict(adjudication.get("adjudication_gate")) + if expected and not gate.get("coverage_pass"): + raise ValueError("adjudication_gate.coverage_pass must be true when exceptions exist") + + + def _linked_ids_from_source(source_pack: dict[str, Any]) -> tuple[set[str], set[str], set[str]]: + bo_ids = set(_as_dict(source_pack.get("bo_by_id")).keys()) + structures = _as_dict(_as_dict(source_pack.get("legal_effect_structures_compact")).get("structure_by_id")) + structure_ids = set(structures.keys()) + evidence_ids = set(_as_dict(source_pack.get("evidence_authority_by_index")).keys()) + return bo_ids, structure_ids, evidence_ids + + + def _validate_refs(row: dict[str, Any], bo_ids: set[str], structure_ids: set[str], evidence_ids: set[str], report: dict[str, Any]) -> None: + if row.get("source_bo_id") not in bo_ids: + raise ValueError(f"unknown source_bo_id: {row.get('source_bo_id')}") + for sid in _strings(_as_dict(row.get("linked_structures")).get("structure_ids")): + if sid not in structure_ids: + report["row_warnings"].append({"fact_id": row.get("fact_id"), "warning": "UNKNOWN_LINKED_STRUCTURE_ID", "value": sid}) + _add_code(row, "UNKNOWN_LINKED_STRUCTURE_ID") + for eid in _strings(row.get("evidence_refs")): + if eid not in evidence_ids: + report["row_warnings"].append({"fact_id": row.get("fact_id"), "warning": "EVIDENCE_REF_GAP", "value": eid}) + _add_code(row, "EVIDENCE_REF_GAP") + + + def _normalize_final_row(row: dict[str, Any], ordinal: int) -> dict[str, Any]: + out = {field: deepcopy(row.get(field, FINAL_DEFAULTS.get(field))) for field in FINAL_FIELDS} + out["fact_id"] = f"F-{ordinal:03d}" + out["source_bo_id"] = str(out.get("source_bo_id") or "") + for key in ("parties", "evidence_refs", "legal_effect_roles", "must_not_drop_in_claim_types", "actio_roles", "downstream_module_candidates", "skeleton_validation_codes"): + out[key] = _as_list(out.get(key)) + for key in ("claim_chain_ref", "linked_structures", "state_context", "money_claim_effect", "commercial_successor_effect", "linked_actio_structures", "legal_calculation_object"): + out[key] = _as_dict(out.get(key)) + out["credibility"] = out.get("credibility") if out.get("credibility") in {"high", "medium", "low"} else "low" + out["legal_centrality"] = out.get("legal_centrality") if out.get("legal_centrality") in {"low", "medium", "critical"} else "low" + out["proof_strength"] = out.get("proof_strength") if out.get("proof_strength") in {"low", "medium", "high", "documentary", "meeting_only_core"} else "low" + if not out.get("derived_fact"): + raise ValueError(f"{out['source_bo_id']}: derived_fact is empty") + return out + + + def main() -> None: + try: + _init_mcp_session() + source_outer = _read_required_json_doc(SOURCE_PACK_PATH, _next_msg_id()) + source_pack = _as_dict(_as_dict(source_outer).get("fact_source_pack")) + if source_pack.get("schema_version") != "stage1_fact_source_pack.v2": + raise ValueError("fact_source_pack schema_version mismatch") + candidate_outer = _read_required_json_doc(CANDIDATE_PATH, _next_msg_id()) + candidate_bundle = _as_dict(_as_dict(candidate_outer).get("fact_ledger_candidate_bundle")) + candidate_items = _validate_candidate_bundle(candidate_bundle) + exception_pack = _as_dict(candidate_bundle.get("exception_pack")) + adjudication = _load_adjudication() + _validate_adjudication_coverage(adjudication, exception_pack) + allowed_fields_by_exception = _exception_allowed_fields(exception_pack) + rows_by_fact_id = {str(row.get("fact_id")): row for row in candidate_items} + report: dict[str, Any] = { + "schema_version": "stage1_fact_ledger_writer_report.v1", + "created_at_utc": datetime.now(timezone.utc).isoformat(), + "blocked_review_items": _as_list(adjudication.get("blocked_review_items")), + "patch_policy_warnings": [], + "unapplied_patch_fields": [], + "row_warnings": [], + "possession_conflict_gate": {"status": "PASS", "conflicts": []}, + "drafting_gates": [], + "gate_review_code_summary": {}, + } + for blocked in _as_list(adjudication.get("blocked_review_items")): + fact_id = str(_as_dict(blocked).get("fact_id") or "") + row = rows_by_fact_id.get(fact_id) + if row: + _add_code(row, "BLOCK_REVIEW_PRESENT") + row["must_consider"] = True + _append_basis(row, "blocked_review=" + str(_as_dict(blocked).get("block_reason") or "unspecified")) + for decision in _as_list(adjudication.get("decisions")): + if not isinstance(decision, dict): + continue + fact_id = str(decision.get("fact_id") or "") + row = rows_by_fact_id.get(fact_id) + if not row: + report["row_warnings"].append({"fact_id": fact_id, "warning": "DECISION_FACT_ID_NOT_FOUND"}) + continue + decision_type = decision.get("decision") + exception_id = str(decision.get("exception_id") or "") + if decision_type == "KEEP_AS_IS": + continue + if decision_type == "BLOCK_REVIEW": + _add_code(row, "BLOCK_REVIEW_PRESENT") + row["must_consider"] = True + continue + if decision_type == "PATCH": + _apply_patch(row, _as_dict(decision.get("patch")), allowed_fields_by_exception.get(exception_id, []), report, exception_id) + if decision.get("review_required"): + _add_code(row, "FL2_REVIEW_REQUIRED") + elif decision_type == "SPLIT_FACT": + report["patch_policy_warnings"].append({"exception_id": exception_id, "warning": "SPLIT_FACT_RECORDED_FOR_REVIEW_NOT_AUTOSPLIT"}) + _add_code(row, "SPLIT_FACT_REQUIRES_REVIEW") + row["must_consider"] = True + else: + report["patch_policy_warnings"].append({"exception_id": exception_id, "warning": "UNKNOWN_DECISION_TYPE"}) + bo_ids, structure_ids, evidence_ids = _linked_ids_from_source(source_pack) + final_rows: list[dict[str, Any]] = [] + for ordinal, row in enumerate(candidate_items, start=1): + normalized = _normalize_final_row(row, ordinal) + _validate_refs(normalized, bo_ids, structure_ids, evidence_ids, report) + extra = [key for key in normalized if key not in FINAL_FIELD_SET] + if extra: + raise ValueError(f"final row has arbitrary fields: {extra}") + final_rows.append(normalized) + gate_code_rows: dict[str, list[dict[str, Any]]] = {} + for row in final_rows: + for code in _as_list(row.get("skeleton_validation_codes")): + if code in {"POSSESSION_CAUSE_CONFLICT", "NOTICE_RECEIPT_UNVERIFIED", "NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW", "LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW", "LEGAL_CAUSE_STATE_UNKNOWN_REVIEW"}: + gate_code_rows.setdefault(str(code), []).append(row) + conflict_rows = gate_code_rows.get("POSSESSION_CAUSE_CONFLICT", []) + report["possession_conflict_gate"] = { + "status": "HARD_WARNING" if conflict_rows else "PASS", + "conflicts": [{"fact_id": row.get("fact_id"), "source_bo_id": row.get("source_bo_id")} for row in conflict_rows], + } + notice_gate_rows = gate_code_rows.get("NOTICE_RECEIPT_UNVERIFIED", []) + if notice_gate_rows: + bo_compact_by_id = _as_dict(source_pack.get("bo_by_id")) + notice_ids = _dedupe([ + str(_as_dict(_as_dict(_as_dict(bo_compact_by_id.get(str(row.get("source_bo_id")))).get("domain_payload_compact")).get("notice_lifecycle")).get("notice_id") or "") + for row in notice_gate_rows + ]) + report["drafting_gates"].append({ + "gate": "BLOCK_FINAL_DRAFTING_NOTICE_RECEIPT_MISSING", + "row_code": "NOTICE_RECEIPT_UNVERIFIED", + "notice_ids": [nid for nid in notice_ids if nid], + "affected_fact_ids": [row.get("fact_id") for row in notice_gate_rows], + }) + report["gate_review_code_summary"] = {code: [row.get("fact_id") for row in code_rows] for code, code_rows in sorted(gate_code_rows.items())} + _write_doc(TARGET_PATH, json.dumps(final_rows, ensure_ascii=False, indent=2), _next_msg_id()) + _write_doc(REPORT_PATH, json.dumps(report, ensure_ascii=False, indent=2), _next_msg_id()) + reread = _read_required_json_doc(TARGET_PATH, _next_msg_id()) + if not isinstance(reread, list): + raise ValueError("post-write reread validation failed: target is not top-level array") + print(json.dumps({ + "fact_ledger_final_writer": { + "schema_version": "stage1_fact_ledger_final_writer.v1", + "status": "READY", + "written_file": TARGET_PATH, + "writer_report_file": REPORT_PATH, + "fact_count": len(final_rows), + "blocked_review_count": len(report["blocked_review_items"]), + "patch_policy_warning_count": len(report["patch_policy_warnings"]), + "row_warning_count": len(report["row_warnings"]), + "possession_conflict_count": len(report["possession_conflict_gate"]["conflicts"]), + "drafting_gate_count": len(report["drafting_gates"]), + } + }, ensure_ascii=False)) + except Exception as exc: + print(json.dumps({ + "fact_ledger_final_writer": { + "schema_version": "stage1_fact_ledger_final_writer.v1", + "status": "FAILED", + "target_file": TARGET_PATH, + "failure_reason": str(exc), + } + }, ensure_ascii=False)) + sys.exit(0) + + + if __name__ == "__main__": + main() diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Untitled 3.txt b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Untitled 3.txt new file mode 100644 index 00000000..0fa28c3c --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.6/Untitled 3.txt @@ -0,0 +1,114 @@ + +Stage 1 최적화 + +Sub-Tasks 개정 목표 + +- B2_quality_gate_event_indexed: 작업 경량화 (반드시 필요한 작업만 남기기) +- B2_quality_gate_event_candidates: 작업 경량화 (반드시 필요한 작업만 남기기) + +- Stage_B_B1-B5 추론 작업: 1) 작업 경량화, 2) LLM을 Gemini 3.5 Flash로 바꿔도 되는가? + +- LES2 추론 작업: 1) 작업 경량화, 2) LLM을 Gemini 3.5 Flash로 바꿔도 되는가? + +- FL2_exception_adjudicator 추론 작업: 1) 작업 경량화, 2) LLM을 Gemini 3.5 Flash로 바꿔도 되는가? + +------------------------------------------------------------------------------------------------- + + +Stage 1 4개 yaml 작업 명세서 4개 +1) +2) +3) +4) +를 Liti-agent를 통해 순차적으로 실행하였다. + +순차 실행 후 얻은 결과물은 폴더에 저장되어 있다. + +에이전트가 stage 1 4개 yaml 파일들을 실행했을 때의 런타임(run-time)과 cost는 아래와 같다: +{{ +* Stage 1 Part 1 +런타임: 8min 45sec +비용: $2.09 + +* Stage 1 Part 2 +런타임: 7min 40sec +비용: $4.28 + +* Stage 1 Part 3 +런타임: 3min 40sec +비용: $0 + +* Stage 1 Part 4 +런타임: 3min 6sec +비용: $0.58 +}} + +Stage 1 yaml 개정 전 작업 명세 +- - - - +을 실행했을 때보다 런타임은 더 길어졌고, 비용도 더 상승했다. + +그래서 Stage 1 yaml 작업 명세서를 지금보다도 훨씬 더 최적화(필요 작업만 남기고 군더더기 작업들을 없애는 경량화 / 최고 품질의 추론이 필요 없을 때는 약간 낮은 성능의 LLM을 사용하여 추론 시간 단축 등)하려고 한다. + + + +Stage 1 Part 1에서 "B2_quality_gate_event_indexed"와 "B2_quality_gate_event_candidates" 작업들을 최적화하는 개정 방안(혹은 전략)을 작성한다. + + + +1. 에 제시된 v.6 폴더의 Stage 1 작업 명세서 4개를 모두 읽고, 세부 작업 내용을 숙지한다. +2. 에 제시된 폴더에 있는 Stage 1 실행 결과물들을 파악한다. +3. Stage 1 Part 1 yaml 파일을 정확하고 심도 깊게 이해하고, +4. "B2_quality_gate_event_indexed"와 "B2_quality_gate_event_candidates" 작업들 내용을 파악하고, +5. 두 개 sub-task들의 downstream 작업과의 연계성을 감안하여, 반드시 필요하지 않은 작업들을 파악하고 그것들을 삭제해도 문제가 없는지 판단한다. +6. 특히, "B2_quality_gate_event_indexed" 작업 후 "B2_quality_gate_event_candidates" 작업이 어떻게 실행되는지 파악하여, +7. 두 개 작업 중 선행 작업의 개별 작업들의 결과물들을 모두 모아서 후행 작업들에 한 번에 넘겨줘서 런타임을 증폭시키는 비효율성이 초래되는지, 아니면 어떤 비효율성이 존재하는지 파악한다. +8. 1~7까지의 작업을 마친 후 결과 보고서를 로 생성하고 저장한다. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Analysis_Stage_1_Claude_v1.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Analysis_Stage_1_Claude_v1.md new file mode 100644 index 00000000..39841a77 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Analysis_Stage_1_Claude_v1.md @@ -0,0 +1,584 @@ +# Analysis of whole Tasks in Stage 1 + +- 분석 대상: `YAML_Prompts/1. Stage_1/v.6/` 최종 개정판 4종 + - Part 1: `Stage_1_Part_1_Claude_v3.yml` (4,504행, task 9종 — 동적 fan-out 포함) + - Part 2: `Stage_1_Part_2_Claude_v3.yml` (5,280행, task 10종) + - Part 3: `Stage_1_Part_3_Claude_v2.yml` (2,250행, task 3종) + - Part 4: `Stage_1_Part_4_Claude_v3.yml` (1,963행, task 3종) +- 파이프라인 성격: 민사소송 원고(사해행위취소 및 대여금청구) 소장 생성 에이전트 "Liti-agent"의 Stage 1이다. Stage 1은 소장 프로즈를 쓰지 않는다. 의뢰인 미팅 원문과 증거 전집에서 출발하여, Stage 2(법리·기안 단계)가 소비할 검증된 구조화 산출물 — `client_goal.json`, `evidence_indexed.json`, `evidence_event_candidates.json`, `BO.json`, signal 3종, `legal_effect_structures.json`, `Fact_Ledger_base.json` — 을 결정론적으로 제조하는 공장이다. +- 공통 설계 문법: 네 Part 전부가 동일한 3박자 문법을 따른다. **① 결정론 reducer/컴파일러(Python code-executor)가 기계적으로 판정 가능한 모든 것을 처리하고, 판정 불가능한 잔여만 compact exception pack으로 좁힌다 → ② 조건부 LLM adjudicator(gemini-3.1-flash-lite, reasoning low)가 pack이 비어 있으면 즉시 종료하고, 있으면 pack 항목만 국소 판정한다 → ③ 결정론 writer/gate(Python)가 판정을 검증·적용하고 최종 파일을 단독 작성한다.** 창의적 추출이 실제로 필요한 곳(Part 1의 문서 인덱싱 mapper, Part 2의 도메인 BO seed worker)에만 고성능 LLM을 배치하고, 병합·검증·번호부여·인덱싱 같은 100% 결정론 작업은 전부 Python으로 수행한다. +- 공통 인프라: 모든 task는 localdocs MCP(`read_docs`/`write_file`)로 파일 IO를 수행하고, Python task는 code-executor MCP(`run_code`, httpx JSON-RPC 보일러플레이트)로 실행된다. LLM task의 프롬프트는 `COMMON_CACHE_PREFIX_STAGE_1`로 시작하며, Part 2 R1·Part 3 L1·Part 4 F1의 프리픽스는 3,829바이트가 바이트 단위로 동일하여 flash-lite 계열의 프롬프트 캐시를 공유한다. 실패 정책도 공통이다: 입력 누락·파싱 불가 시 최종 파일을 쓰지 않고 FAILED를 단일 JSON 라인으로 출력한 뒤 예외를 올려 후속 진행을 차단한다. +- 실행 순서: Part 1 → Part 2 → Part 3 → Part 4 순차 실행이며, 각 Part의 산출물이 다음 Part의 입력이 된다. + +``` +[Part 1] client_meeting.md + evidence_all.json + → client_goal.json, evidence_indexed.json, evidence_event_candidates.json, soft gate handoff +[Part 2] Part 1 산출물 + Juristic_Act.md + → BO.json + actio/case_liability/legal_effect signal 3종 +[Part 3] BO.json + evidence_indexed.json + signal 3종 + → legal_effect_structures.json +[Part 4] BO.json + legal_effect_structures.json + signal 3종 + evidence_indexed.json + → Fact_Ledger_base.json (Stage 2 인계) +``` + +--- + +# Section 1: Analysis of Stage 1 — Part 1 + +## 1.1 Part 1의 궁극적 목표 (Ultimate Goal) + +Part 1은 Stage 1 전체의 **증거 기반(evidence substrate)을 구축하는 단계**다. 궁극적 목표는 세 가지다. + +첫째, 의뢰인 미팅 원문(`client_meeting.md`)만으로 의뢰인의 목표·청구 후보·쟁점 클러스터를 구조화한 `client_goal.json`을 생성한다. 단순 요약이 아니라 금전채권 timeline, 상호속용 책임, 담보부 부동산 권리상태, 사해행위취소 가액배상, 유치권·상속·통지 lifecycle을 downstream이 구조적으로 읽을 수 있는 seed(`domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints` 등)로 남긴다. 의뢰인이 원하는 것(relief)과 법률상 재검토가 필요한 것(watchpoints)을 절대 하나로 합치지 않는 층 분리가 핵심 법률 요건이다. + +둘째, 증거 전집(`evidence_all.json`)의 문서 N건 각각을 **문서 단위로 정밀 인덱싱**하여 `evidence_indexed.json`(문서 메타 + key_facts/key_dates/key_amounts/key_parties + 도메인별 component 분해)과 `evidence_event_candidates.json`(문서에서 추출한 사건 후보)을 생성한다. 문서당 1개의 LLM mapper 인스턴스를 동적 fan-out하여 병렬 처리하되, 각 인스턴스는 자기 shard 1건 밖으로 절대 나가지 못하게 봉인한다. + +셋째, 이 산출물의 품질을 **결정론 게이트 + 조건부 LLM 판정**으로 검증하고, 그 결과를 감사 파일(`quality_gates/*.json`)과 SHA-256 digest guard가 붙은 soft gate handoff로 Part 2에 인계한다. 게이트는 산출물을 차단하기보다 변호사 검토 큐(review queue)로 라우팅하는 soft gate 철학을 따른다. + +## 1.2 전체 워크플로우와 DAG + +Part 1은 정적 task 7종 + 동적 템플릿 task 2종(`Task_B1_map_doc_*`, `Task_B2_map_events_*`)으로 구성된다. planner가 증거 문서 수 N을 세어 shard를 만들면 orchestrator가 mapper 인스턴스를 N쌍 생성한다(7/10 실행 기준 N=60). + +``` +IN ─┬─► Task_A_client_goal ──► client_goal.json (독립 분기 — LLM) + │ + └─► Task_Evidence_shard_planner (결정론 planner) + │ evidence_all.json → evidence_shards/E-###.json × N + │ + evidence_shard_plan.json (dynamic_fanout 디스크립터) + │ + │ ┌───────────── 문서별 병렬 N쌍 ─────────────┐ + ├──► Task_B1_map_doc_001 ──► Task_B2_map_events_001 ─┤ + ├──► Task_B1_map_doc_002 ──► Task_B2_map_events_002 ─┤ (LLM mapper, + │ ⋮ (같은 ordinal 직렬) ⋮ │ shard 1건 봉인) + └──► Task_B1_map_doc_N ───► Task_B2_map_events_N ────┤ + │(all B1 완료 대기) │(all B2 완료 대기) + ▼ ▼ + Task_B1_quality_gate_evidence_indexed ──► Task_B2_quality_gate_event_candidates + (결정론 병합·검증 → 최종 2파일) (+ B12_exception_pack 생성) + │ + ▼ + Task_B12_gate_llm_adjudicator + (조건부 flash-lite — 의심 항목만 판정) + │ + ▼ + Task_B12_gate_audit_finalizer + (결정론 audit 확정 → quality_gates/ 2파일) + │ + ▼ + Task_B2_SHA256_soft_gate_handoff_writer ──► OUT + (digest guard + handoff sidecar) +``` + +## 1.3 IO 구조 + +**IN 파일** + +| 파일 | 소비 task | 성격 | +|---|---|---| +| `client_meeting.md` | Task_A, B1/B2 mapper 전 인스턴스 | 의뢰인 미팅 원문 (사건의 유일한 서사 원천) | +| `evidence_all.json` | Task_Evidence_shard_planner (유일 독점 reader) | 증거 문서 전집 — planner가 shard로 분해한 뒤에는 어떤 task도 전집을 직접 읽지 않는다 | + +**OUT 파일** + +| 파일 | writer | 소비자 | +|---|---|---| +| `client_goal.json` | Task_A_client_goal | Stage 2 (Part 2 A0의 직접 입력은 아니며, 목표·쟁점 층은 Stage 2가 소비한다) | +| `evidence_shard_plan.json` + `evidence_shards/E-###.json` ×N | shard planner | B1/B2 mapper, 양 quality gate | +| `evidence_indexed_parts/E-###.json` ×N | 각 B1 mapper | B1 quality gate | +| `evidence_event_candidate_parts/E-###.json` ×N | 각 B2 mapper | B2 quality gate | +| `evidence_indexed.json` | B1 quality gate | Part 2·3·4, Stage 2 | +| `evidence_event_candidates.json` | B2 quality gate | Part 2 | +| `stage1_tmp/quality_gate/` (manifest·B1/B2_precheck·B12_exception_pack·B12_adjudication_decisions) | 게이트 체인 | 게이트 체인 내부 | +| `quality_gates/B1_evidence_indexed_gate.json`, `quality_gates/B2_event_candidates_gate.json` | audit finalizer | 변호사 검토, handoff writer | +| `quality_gates/stage1_part1_soft_gate_handoff.json` | handoff writer | Part 2 A0 (진입 게이트) | + +## 1.4 Sub-task별 상세 설명 + +### (1) Task_A_client_goal — 의뢰인 목표 구조화 (LLM) + +- **목표**: `client_meeting.md`만 읽고 `client_goal.json`을 직접 finalize한다. Part 1에서 유일하게 mapper/게이트 없이 최종 파일을 쓰는 LLM task다(island 구조가 없어 propose 단계가 불필요하다). +- **수행 방식**: gemini-3.1-flash-lite(reasoning high, verbosity medium, cache ttl 20m)가 미팅 원문을 조항 단위로 훑어 primary_goal·issue_clusters·claim_type_candidates·parties를 추출하고, v3에서 추가된 additive 층 — `domain_profiles`(사건군별 법률효과 seed), `client_requested_relief`(의뢰인 요구 보존층), `legal_sustainability_watchpoints`(기산일·원고적격 재검토층), `current_control_map_4axis`, `legal_entitlement_candidates` — 을 생성한다. 규범 검색·최종 법률효과 분류는 금지된다. +- **IO 정렬**: IN `client_meeting.md` → OUT `client_goal.json` (write 후 재읽기 검증). 다른 어떤 파일도 읽지도 쓰지도 않는다. + +### (2) Task_Evidence_shard_planner — 증거 분해·fan-out 설계 (Python) + +- **목표**: 증거 전집을 문서 단위 shard로 물리 분해하고, mapper 인스턴스 생성에 필요한 모든 runtime binding을 결정론적으로 확정한다. LLM이 문서 수를 세거나 ordinal을 배정하는 일을 원천 차단한다. +- **수행 방식**: `evidence_all.json`을 읽어 문서마다 `evidence_shards/E-{ordinal:03d}.json`을 쓰고, shard마다 `{assigned_ordinal, evidence_index_candidate(E-###), b1_output_path, b2_output_path, shard_doc_path, source_document_ref(content_sha256 포함), runtime_bindings}` 디스크립터를 만들어 `evidence_shard_plan.json`의 `dynamic_fanout` 배열에 기록한다. 잔존 stale part 검사(`evidence_indexed_parts`/`evidence_event_candidate_parts`)도 수행한다. +- **IO 정렬**: IN `evidence_all.json` → OUT shard ×N + `evidence_shard_plan.json`. 이후 어떤 task도 `evidence_all.json`을 직접 읽지 않는다는 read-diet 원칙의 물리적 토대다. + +### (3) Task_B1_map_doc_* — 문서 인덱싱 mapper (LLM ×N, 템플릿) + +- **목표**: 자기 인스턴스에 배정된 증거 문서 1건을 법률 문서 유형별 component까지 분해 추출한 part 파일 1개를 만든다. +- **수행 방식**: preflight로 `{{item.shard_doc_path}}`가 주입되는 flash-lite(reasoning high) 인스턴스가 shard 1건 + `client_meeting.md`만 읽고, title/doc_type/key_facts/key_dates/key_amounts/key_parties/source_pointer와 문서 유형별 component envelope(claim_document_components, registry_row_components, secured_debt_components, loan_components, notice_components, valuation_rows, succession_document_components 등 17종)를 자기 문서 범위 안에서 최대 깊이로 분리 추출한다. `evidence_index`는 planner가 부여한 candidate를 `evidence_index_proposed`로 제안만 하고 final 부여는 게이트가 한다. +- **IO 정렬**: IN shard 1건 + meeting → OUT `evidence_indexed_parts/E-###.json` 1개. 최종 `evidence_indexed.json` 작성은 금지(게이트 단독 권한). 같은 ordinal의 B2 인스턴스와 B1 게이트가 이 part를 기다린다. + +### (4) Task_B2_map_events_* — 사건 후보 mapper (LLM ×N, 템플릿) + +- **목표**: 같은 문서에서 소송상 의미 있는 사건 후보(event candidate: 일자·행위자·행위·금액·목적물)를 추출한다. +- **수행 방식**: 같은 ordinal의 B1 part를 authority metadata로, shard raw 문서를 provenance 원천으로 삼아 사건 후보를 추출한다. B1이 이미 확정한 문서 메타를 재작성하지 않는다. +- **IO 정렬**: IN `{{item.b1_output_path}}` + shard + meeting → OUT `evidence_event_candidate_parts/E-###.json` 1개. B1 part → B2 part의 ordinal 단위 직렬 체인이 문서별로 독립 병렬 실행된다. + +### (5) Task_B1_quality_gate_evidence_indexed — 인덱스 병합·검증 게이트 (Python) + +- **목표**: B1 part N개를 결정론적으로 병합해 최종 `evidence_indexed.json`을 만들고, 기계 검사로 의심 항목(suspect)을 좁힌다. +- **수행 방식**: `evidence_shard_plan.json`의 expected_ordinals와 part 존재·스키마·ID 일치를 대조하고, part를 병합해 최종 파일과 `finalized_evidence_id_manifest.json`을 쓴다. 커버리지 결손·ID 충돌·스키마 이형 등은 `B1-SUS-###` suspect로 `B1_precheck.json`에 기록한다(차단이 아니라 후속 판정 대상으로 라우팅). +- **IO 정렬**: IN plan + parts ×N → OUT `evidence_indexed.json` + manifest + `stage1_tmp/quality_gate/B1_precheck.json`. + +### (6) Task_B2_quality_gate_event_candidates — 사건 후보 병합·예외 수집 게이트 (Python) + +- **목표**: B2 part를 병합해 `evidence_event_candidates.json`을 만들고, B1/B2 양쪽 precheck의 suspect를 하나의 `B12_exception_pack.json`으로 통합한다. +- **수행 방식**: B1 게이트와 동일한 병합·검증을 사건 후보에 수행하고(`B2-SUS-###`), 양 게이트 suspect를 exception pack으로 만들어 LLM 판정 대상을 명시적으로 한정한다. +- **IO 정렬**: IN plan + B2 parts + B1_precheck → OUT `evidence_event_candidates.json` + `B2_precheck.json` + `stage1_tmp/quality_gate/B12_exception_pack.json`. + +### (7) Task_B12_gate_llm_adjudicator — 조건부 게이트 판정기 (LLM) + +- **목표**: 결정론 게이트가 좁혀낸 suspect만 국소 판정한다. 산출물 자체를 다시 검사하지 않는다. +- **수행 방식**: preflight로 exception pack이 주입되는 flash-lite(reasoning low, verbosity low). `exception_count == 0`이면 `{"schema_version": "B12_adjudication_decisions.v1", "decisions": [], "no_suspects": true}`를 쓰고 `"NO_SUSPECTS"`만 출력한 뒤 즉시 종료한다. 항목이 있으면 suspect별로 `DISMISS | SOFT_WARNING_REVIEW | HARD_WARNING_REVIEW | CONFIRM_BLOCK | ESCALATE_TO_REVIEW` 판정과 severity, 한국어 review_instruction(≤120자), candidate_use_policy를 기록한다. 최종 파일·audit 파일 작성은 금지된다. +- **IO 정렬**: IN pack(preflight) → OUT `stage1_tmp/quality_gate/B12_adjudication_decisions.json` 1개. + +### (8) Task_B12_gate_audit_finalizer — 감사 확정기 (Python) + +- **목표**: 게이트 precheck + LLM 판정을 병합해 공식 감사 파일 2종을 확정한다. +- **수행 방식**: suspect별 최종 severity를 산정하고 schema_invariant_findings·rerun_targets를 계산해 `quality_gates/B1_evidence_indexed_gate.json`(schema_contract `B1_quality_gate.v3`)과 `B2_event_candidates_gate.json`을 쓴다. handoff 후보 상태(`BLOCKED`/`READY_WITH_REVIEW`/`READY_NO_REVIEW`)를 계산하되 handoff 파일 작성은 다음 task에 위임한다. +- **IO 정렬**: IN precheck 2종 + decisions + pack → OUT `quality_gates/` 감사 2파일. + +### (9) Task_B2_SHA256_soft_gate_handoff_writer — 인계 사이드카 작성기 (Python) + +- **목표**: Part 2가 신뢰할 수 있는 단일 진입 계약을 만든다. +- **수행 방식**: 감사 2파일의 SHA-256을 digest_guard로 봉인하고, hard/review gate 요약, 변호사 워크플로우 큐, Stage 2 자동 진행 허용 여부를 담은 sidecar를 쓴다. 7/10 실행 기준 handoff_status는 `READY_WITH_REVIEW`였다. +- **IO 정렬**: IN 감사 2파일 → OUT `quality_gates/stage1_part1_soft_gate_handoff.json`. + +## 1.5 출력 JSON 형식 + +```jsonc +// evidence_shard_plan.json (발췌) +{ + "schema_version": "evidence_shard_plan.v1", + "planner_task": "Task_Evidence_shard_planner", + "shard_count": 60, + "expected_ordinals": [1, 2, "..."], + "expected_evidence_ids": ["E-001", "E-002", "..."], + "shards": [{ + "assigned_ordinal": 1, + "evidence_index_candidate": "E-001", + "b1_output_path": "evidence_indexed_parts/E-001.json", + "b2_output_path": "evidence_event_candidate_parts/E-001.json", + "shard_doc_path": "evidence_shards/E-001.json", + "source_document_ref": {"source": "evidence_all.json", "content_sha256": "...", "...": "..."} + }], + "dynamic_fanout": ["(shards 동일 — orchestrator 인스턴스 생성용)"], + "mapper_execution_allowed": true +} + +// evidence_indexed.json — 항목 레코드 (레코드당 문서 1건) +{ + "evidence_index": "E-001", "doc_uid": "...", "title": "가정법원심판_강호연", + "title_normalized": "...", "doc_type": "...", + "key_facts": ["2024느단52199 상속포기", "..."], "key_dates": ["2024. 8. 10.", "..."], + "key_amounts": [], "key_parties": ["..."], "source_pointer": {"...": "..."}, + "notice_components": ["문서 유형별 component envelope — 존재 시"], "...": "..." +} + +// evidence_event_candidates.json +{ "schema_version": "evidence_event_candidates.v1", "items": [ {"event_date": "...", "actor": "...", "action": "...", "amount": "...", "object": "...", "evidence_index": "E-###", "...": "..."} ] } + +// B12_adjudication_decisions.json +{ "schema_version": "B12_adjudication_decisions.v1", "no_suspects": false, + "decisions": [{ "exception_id": "B1-SUS-001", + "decision": "DISMISS|SOFT_WARNING_REVIEW|HARD_WARNING_REVIEW|CONFIRM_BLOCK|ESCALATE_TO_REVIEW", + "severity": "PASS|SOFT_WARNING|HARD_WARNING|BLOCK", "issue_type": "snake_case", + "source_refs": {"evidence_indexes": [], "event_candidate_ids": [], "meeting_clause_refs": [], "doc_titles": []}, + "review_instruction": "≤120자 한국어", "candidate_use_policy": "prioritize_if_supported|preserve_as_needs_review|do_not_generate_without_support", + "concise_basis": "≤120자 한국어" }] } + +// quality_gates/stage1_part1_soft_gate_handoff.json (발췌) +{ "schema_version": "stage1_part1_soft_gate_handoff.v1", "source_stage": "Stage_1.Part_1", + "source_gate_files": ["quality_gates/B1_evidence_indexed_gate.json", "quality_gates/B2_event_candidates_gate.json"], + "handoff_status": "READY_NO_REVIEW|READY_WITH_REVIEW|BLOCKED", + "stage2_auto_progression_allowed": true, + "hard_gate_summary": {"hard_gate_count": 0, "hard_gate_ids": []}, + "review_gate_summary": {"review_gate_count": 0, "highest_review_severity": "...", "b1_review_count": 0, "b2_review_count": 0}, + "digest_guard": {"b1_gate_sha256": "64hex", "b2_gate_sha256": "64hex"}, + "review_items": [], "routing_index": {}, "lawyer_workflow_queues": {} } +``` + +--- + +# Section 2: Analysis of Stage 1 — Part 2 + +## 2.1 Part 2의 궁극적 목표 + +Part 2는 **사건의 법적 행위 원자(BO: Behavior Object)를 컴파일하는 단계**다. Part 1이 만든 증거 인덱스·사건 후보·의뢰인 목표를 다섯 개 법률 도메인의 렌즈로 나누어 읽고, 도메인별 전문 LLM worker가 BO seed를 생성하면, 결정론 reducer가 이를 단일 원장으로 병합·검증하여 최종 `BO.json`(7/10 기준 50건)과 하류 라우팅용 signal 3종(`actio_case_signals.json`, `case_liability_signals.json`, `legal_effect_signals.json`)을 확정한다. + +법률적으로 이 단계의 요체는 두 가지다. 첫째, 대여금(금전채권)·상호속용 영업양수 책임·담보/등기·토지 사용이익/감정·사해행위취소·상속/통지/유치권 방어라는 이질적 법리 영역을 **도메인 분리 후 병합** 구조로 처리하여, 각 worker가 자기 법리에만 집중하되 전 우주(universe)와의 정합은 결정론 reducer가 전담하게 한 것이다. 둘째, BO는 Stage 1 후반부(Part 3·4)와 Stage 2 전체의 원자 단위이므로, ID 규율(`bh#`)·출처 규율(source_evidence_indexes)·필드 규율(core_field_base 6종)을 게이트로 봉인하는 것이다. + +## 2.2 전체 워크플로우와 DAG + +``` +IN ──► Task_C_BO_A0_context_and_domain_slice_compiler (결정론 컨텍스트 컴파일러) + │ stage_a_context + source_universe_manifest + │ + domain_slices/B1..B5.json (구조적 3-tier 라우팅) + │ + ├────────────┬────────────┬────────────┬────────────┐ + ▼ ▼ ▼ ▼ ▼ + B1_Money_ B2_Secured_ B3_Land_ B4_Actio B5_Succession_ + Successor Registry Valuation (사해행위) Notice_Lien_ + (대여금· (담보·등기) (토지·감정) Asset_Defense + 상호속용) (상속·통지·유치권) + │ │ │ │ │ + │ [gemini-3.1-pro-preview / high — 도메인 BO seed 병렬 생성] + └────────────┴─────┬──────┴────────────┴────────────┘ + ▼ (5개 seed 전부 대기 — barrier) + Task_C_BO_R0_seed_reducer_and_exception_planner (결정론 reducer) + │ postb_seed_ledger + exception pack + ▼ + Task_C_BO_R1_exception_adjudicator (조건부 flash-lite) + │ postb_adjudication_decisions + ▼ + Task_C_BO_F0_final_bo_compiler_gate_writer (결정론 최종 writer) + │ BO.json + review handoff + compact bundle + ▼ + Task_C_BO_S0_signal_bundle_writer (결정론 signal 파생) + │ signal 3종 + ▼ + OUT +``` + +## 2.3 IO 구조 + +**IN 파일** + +| 파일 | 소비 task | 성격 | +|---|---|---| +| `client_meeting.md` | A0 | 미팅 원문 (조항 clause 단위 universe로 변환) | +| `evidence_indexed.json` | A0 | Part 1 증거 인덱스 | +| `evidence_event_candidates.json` | A0 | Part 1 사건 후보 | +| `Default_Agent/Juristic_Act.md` | A0 | 법률행위 유형 사전 (BOType/JuristicAct 어휘 규율) | +| `quality_gates/stage1_part1_soft_gate_handoff.json` | A0 | Part 1 인계 계약 (digest guard 검증 후 진입) | + +**OUT 파일** + +| 파일 | writer | 소비자 | +|---|---|---| +| `stage1_tmp/task_c_bo/stage_a_context.json` | A0 | R0·F0·S0 | +| `stage1_tmp/task_c_bo/source_universe_manifest.json` | A0 | R0 (global-universe 검증 기준) | +| `stage1_tmp/task_c_bo/domain_slices/B1.json`~`B5.json` | A0 | 각 B worker (preflight) | +| `stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B{1..5}_*.json` | 각 B worker | R0 | +| `stage1_tmp/task_c_bo/postb_seed_ledger.json` | R0 | F0·S0 | +| `quality_gates/stage1_part2_exception_pack.json` | R0 | R1 (preflight) | +| `stage1_tmp/task_c_bo/postb_adjudication_decisions.json` | R1 | F0 | +| **`BO.json`** (최상위 배열) | F0 | Part 3·4, Stage 2 | +| `stage1_tmp/task_c_bo/postb_compiled_bundle_compact.json` | F0 | S0 | +| `quality_gates/stage1_part2_review_handoff.json` | F0 | 변호사 검토, Stage 2 | +| **`actio_case_signals.json`·`case_liability_signals.json`·`legal_effect_signals.json`** | S0 | Part 3·4, Stage 2 | + +## 2.4 Sub-task별 상세 설명 + +### (1) Task_C_BO_A0_context_and_domain_slice_compiler (Python) + +- **목표**: Part 1 산출물 전체를 도메인 5개가 소비 가능한 형태로 결정론 분배한다. LLM worker가 전 우주를 읽는 것을 구조적으로 차단하는 read-diet의 핵심이다. +- **수행 방식**: Part 1 handoff의 digest guard를 검증한 뒤, 미팅 조항·증거 인덱스·사건 후보를 ID 체계로 정규화한 `stage_a_context`와 `source_universe_manifest`(전 우주 ID 집합)를 만든다. 이어 도메인별 **구조적 3-tier 라우팅**(event_kind/component key/strong term 기반 primary → 광의 support → 잔여 review_only)으로 slice 5개를 만들어 각 worker의 preflight 입력으로 좁힌다. slice에는 worker 계약(context_is_read_only, must_use_evidence_index_set 등)과 Part 1 review 항목 중 해당 도메인 관련분이 동봉된다. +- **IO 정렬**: IN 5종(§2.3) → OUT stage_a_context + manifest + slice 5개. 이후 worker는 slice만, R0는 manifest+seed만 읽는 계층 구조가 성립한다. + +### (2) Task_C_BO_Stage_B_B1~B5 — 도메인 BO seed worker 5종 (LLM, 병렬) + +- **목표**: 각자 배정된 법률 도메인에서 BO seed(행위 원자 후보 + 도메인 payload)를 생성한다. B1 대여금·상호속용(변제·일부변제·채무승인·영업양수), B2 담보·등기(근저당·소유권이전·말소), B3 토지·감정(사용이익·가액평가), B4 사해행위(피보전채권·사해행위·수익자/전득자), B5 상속·통지·유치권·자산방어(상속포기, 내용증명 lifecycle, 유치권 주장, asset alias)를 담당한다. +- **수행 방식**: gemini-3.1-pro-preview(reasoning high, verbosity low, max_iterations 2)가 자기 slice의 primary 원천에서 BO seed를 추출한다. seed는 `task_c_bo_stage_b_domain_bo_seed.v2` 스키마를 따르며 BO ID를 확정하지 않는다(finalize는 F0 권한). 모든 seed 필드는 slice 내 원천 ID 인용이 강제된다. +- **IO 정렬**: IN 자기 slice 1개(preflight) → OUT `stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B{n}_{도메인}.json` 1개. 5개 worker는 서로를 알지 못하며 전부 완료되어야 R0가 시작된다. + +### (3) Task_C_BO_R0_seed_reducer_and_exception_planner (Python) + +- **목표**: 도메인 seed 5개를 단일 원장(ledger)으로 병합하고, 기계 판정 불가한 진짜 충돌만 exception pack으로 좁힌다. +- **수행 방식**: seed 스키마·도메인 ID·원천 인용을 global universe(manifest) 기준 하드 검증하고, 도메인 간 중복 후보를 결정론 규칙으로 정규화·병합한다. 같은 필드에 원천이 다른 상충 값이 실재하는 경우만 `EX-FIELD-###` 예외로 pack에 수록하고(defer 가능하면 defer — X-2 정책), 나머지는 결정론 해소한다. 산출 ledger에는 BO 후보 전량과 transport_audit이 담긴다. +- **IO 정렬**: IN seed 5개 + stage_a_context + manifest → OUT `postb_seed_ledger.json` + `quality_gates/stage1_part2_exception_pack.json` (+ stdout에 has_exceptions 보고). + +### (4) Task_C_BO_R1_exception_adjudicator (LLM, 조건부) + +- **목표**: R0가 남긴 필드 충돌 예외만 국소 판정한다. +- **수행 방식**: flash-lite(low/low, max_iterations 1, 캐시 프리픽스 3,829B 공유)가 preflight pack의 `has_exceptions`를 확인해 false면 no-exception 판정 파일을 쓰고 `"NO_EXCEPTIONS"`만 출력 후 즉시 종료한다. true면 항목별 compact 근거만으로 값 선택 또는 보류를 판정한다. 원본 파일 read·ID 창작·최종 파일 작성은 금지된다. +- **IO 정렬**: IN pack(preflight) → OUT `stage1_tmp/task_c_bo/postb_adjudication_decisions.json`(루트 `postb_exception_adjudication`, schema `task_c_bo_postb_exception_adjudication.v1`) 1개. + +### (5) Task_C_BO_F0_final_bo_compiler_gate_writer (Python) + +- **목표**: ledger + 판정을 적용해 최종 `BO.json`을 단독 작성하고 게이트를 봉인한다. +- **수행 방식**: R1 판정의 스키마·coverage를 검증해 ledger에 적용하고, BO ID(`bh#`)를 확정하며, 닫힌 필드 규율(BOType/ActionType/JuristicAct/Action/core_field_base/extensions.domain_payload 등)과 출처 규율을 행 단위 검증한다. write 후 재읽기 parse 게이트를 통과해야 성공으로 보고한다. 검토가 필요한 항목은 `stage1_part2_review_handoff.json`으로 라우팅한다. +- **IO 정렬**: IN ledger + decisions + stage_a_context → OUT **`BO.json`**(최상위 배열 50건) + `postb_compiled_bundle_compact.json` + review handoff. + +### (6) Task_C_BO_S0_signal_bundle_writer (Python) + +- **목표**: 확정된 BO에서 하류(Part 3·4, Stage 2) 라우팅용 signal 3종을 결정론 파생한다. +- **수행 방식**: BO.json·ledger·compact bundle·B4 seed를 읽어 사해행위 관련 신호(`actio_case_signals`), 청구/책임 그룹 신호(`case_liability_signals`), 법률효과 후보 신호(`legal_effect_signals`)를 모듈별 결정론 변환으로 생성한다. 각 파일은 `{schema_version, status, }` envelope로 저장된다. +- **IO 정렬**: IN BO.json + ledger + bundle + B4 seed → OUT signal 3종 (각각 write 후 검증). + +## 2.5 출력 JSON 형식 + +```jsonc +// domain_slices/B{n}.json (발췌) +{ "stage_b_domain_slice": { + "schema_version": "task_c_bo_stage_b_domain_slice.v1", + "router_task": "Task_C_BO_A0_context_and_domain_slice_compiler", + "domain_id": "B1_Money_Successor", "allowed_legal_effect_bo_types": ["..."], + "input_digests_sha256": {"...": "..."}, + "stage_b_worker_contract": {"context_is_read_only": true, "bo_id_finalization_allowed": false, "...": "..."}, + "source_universe": {"meeting_clause_ids": [], "event_candidate_ids": [], "evidence_indexes": []}, + "selected_meeting_clauses": ["primary/support/review_only 3-tier"], "selected_event_candidates": ["..."], + "part1_review_items_relevant": ["..."] } } + +// stage1_tmp/task_c_bo/postb_seed_ledger.json (루트) +{ "postb_seed_ledger": { "schema_version": "task_c_bo_postb_seed_ledger.v1", "status": "READY", "...": "..." } } + +// quality_gates/stage1_part2_exception_pack.json +{ "schema_version": "stage1_part2_exception_pack.v1", "has_exceptions": false, "exception_count": 0, + "exceptions": [{ "exception_id": "EX-FIELD-001", "...": "compact 상충 후보·근거" }] } + +// BO.json — 최상위 배열, 항목(발췌) +[ { "BO_ID": "bh1", "id": "bh1", "BOType": "...", "ActionType": "...", "JuristicAct": "...", + "Action": "...", "Reason": "...", "PriorAct": "...", "ReasonRefs": ["..."], + "Legal_Keywords": ["..."], + "core_field_base": {"BehaviorTime": "...", "Performer": "...", "Subject": "...", "Object": "...", "Amount": "...", "Action": "..."}, + "amount": "...", "EvidenceTitles": ["..."], "Evidence": ["..."], "source_evidence_indexes": ["E-###"], + "provenance": {"...": "..."}, "downstream_seed_refs": {"...": "..."}, + "extensions": {"domain_payload": {"money_claim_details|possession_state|lien_state|notice_lifecycle|asset_alias|fractional_effect_events": "..."}} } ] + +// signal 3종 공통 envelope (예: actio_case_signals.json) +{ "schema_version": "...", "status": "READY", "actio_case_signals": [ {"signal_id": "...", "related_bo_ids": ["bh#"], "...": "..."} ] } +``` + +--- + +# Section 3: Analysis of Stage 1 — Part 3 + +## 3.1 Part 3의 궁극적 목표 + +Part 3는 **BO 원자들을 법률효과 구조(legal effect structure)로 조직하는 단계**다. 개별 BO는 "누가 언제 무엇을 했다"는 행위 원자일 뿐이므로, 청구원인으로 쓰이려면 어느 BO들이 같은 쟁점 클러스터(issue cluster)·같은 자산 클러스터(asset cluster)에 속하고 어떤 후보 구조 유형(candidate_structure_type: money_claim, secured_debt, fraudulent_transfer 등 11종 canonical enum)으로 라우팅되는지가 확정되어야 한다. Part 3의 최종 산출물 `legal_effect_structures.json`은 구조 50건(`LES-###`)과 5종 역인덱스(structure_index), 품질 게이트를 담아 Part 4와 Stage 2의 조인 기반이 된다. + +법률적 요체는 **자산 동일성 판정의 보수성**이다. 서로 다른 등기 계보(ref lineage)를 가진 자산 라벨을 함부로 병합하면 권리객체가 훼손되므로, ref 우선(ref-first) 규칙과 "분리 보존이 기본값" 원칙을 결정론으로 강제하고, 진짜 서로소 계보 충돌만 LLM 판정에 회부한다. + +## 3.2 전체 워크플로우와 DAG + +``` +IN ──► Task_L0_structure_seed_reducer (결정론 reducer) + │ BO.json + evidence_indexed + signal 3종 + │ → structure seed 50건 + canonical type 라우팅 + │ → asset alias/lineage 결정론 해소 + │ → seed bundle + exception pack + audit + ▼ + Task_L1_exception_adjudicator (조건부 flash-lite) + │ pack 비어 있으면 NO_EXCEPTIONS 즉시 종료 + │ 서로소 계보 충돌만 SAME/SEPARATE/MULTI 판정 + ▼ + Task_L2_final_structure_index_writer (결정론 최종 writer) + │ coverage 등식 검증 → 판정 적용 + │ → legal_effect_structures.json (LES-### + index 5종 + quality_gate) + ▼ + OUT +``` + +## 3.3 IO 구조 + +**IN 파일**: `BO.json`, `evidence_indexed.json`, `actio_case_signals.json`, `case_liability_signals.json`, `legal_effect_signals.json` (5종 — L0만 읽는다) + +**OUT 파일** + +| 파일 | writer | 소비자 | +|---|---|---| +| `stage1_tmp/legal_effect_structure_seed_bundle.json` | L0 | L2 | +| `stage1_tmp/legal_effect_structure_exception_pack.json` | L0 | L1 (preflight), L2 (sha 대조) | +| `stage1_tmp/legal_effect_structure_audit.json` | L0 | L2 (audit 승계), 감사 | +| `stage1_tmp/legal_effect_structure_adjudication.json` | L1 | L2 | +| **`legal_effect_structures.json`** | L2 (단독 writer) | Part 4, Stage 2 | + +## 3.4 Sub-task별 상세 설명 + +### (1) Task_L0_structure_seed_reducer (Python) + +- **목표**: BO 50건 전부에 대해 구조 seed를 결정론 생성하고, 자산 계보 충돌 등 기계 판정 불가 항목만 pack으로 좁힌다. +- **수행 방식**: BO별 signal·evidence 링크를 수집해 canonical routing enum(11종) 밖의 라벨은 nonrouting으로 분리 보존하고, 대표 유형은 우선순위표(STRUCTURE_TYPE_PRIORITY)로 결정론 선택한다. 자산 라벨은 등기행·별지목록 ref 계보의 entry-level 합집합으로 alias map을 만들어 같은 계보는 결정론 병합(alias_merged), 계보 증거가 없는 것은 분리 보존(separate_preserved)하며, **서로소 계보가 동일 라벨을 주장하는 충돌만** pack에 수록한다(항목당 ≤2,000자, 30건/40KB 방어 상한 — 초과분은 deterministic review로 라우팅). pending 예외 상태는 `pending_exception_ids`로 seed에 기록된다. +- **IO 정렬**: IN 5종 → OUT seed bundle(`legal_structure_seed_bundle.v2`) + exception pack(`stage1_legal_structure_exception_pack.v2`, sha256이 bundle ref에 기록) + audit. stdout은 has_exceptions·결정론 해소 건수를 보고하는 단일 JSON 라인이다. + +### (2) Task_L1_exception_adjudicator (LLM, 조건부) + +- **목표**: 자산 클러스터·쟁점 클러스터·구조 유형의 진짜 모호성만 국소 판정한다. +- **수행 방식**: flash-lite(low/low/1, ttl 20m, 캐시 프리픽스 공유)가 pack의 `has_exceptions == false`면 no-exception 객체를 쓰고 `"NO_EXCEPTIONS"`만 출력 후 종료한다. 항목이 있으면 `ambiguous_issue_cluster`(USE_EXISTING_HARD_KEY/SELECT_ISSUE_FAMILY/SPLIT_ISSUE_CLUSTER/BLOCK_REVIEW), `ambiguous_asset_cluster`(SAME_CLUSTER/SEPARATE_CLUSTER/MULTI_ASSET_BUNDLE/BLOCK_REVIEW — ref 근거 우선, 병합이 권리객체를 훼손할 수 있으면 분리·차단), `ambiguous_candidate_structure_type`(SELECT_REPRESENTATIVE_TYPE/PRESERVE_MULTI_ROUTING/SPLIT_STRUCTURE_SEED/BLOCK_REVIEW — 다중 라우팅 보존이 기본)을 candidate_values 부분집합 안에서만 판정한다. +- **IO 정렬**: IN pack(preflight) → OUT `legal_effect_structure_adjudication.json`(루트 `legal_structure_exception_adjudication`, v1) 1개. + +### (3) Task_L2_final_structure_index_writer (Python) + +- **목표**: seed + 판정을 병합해 최종 `legal_effect_structures.json`을 봉인 작성한다. +- **수행 방식**: bundle의 `exception_pack_ref.sha256`으로 pack 무결성을 대조하고, coverage 등식(expected == resolved ∪ blocked)을 하드 검증한 뒤 판정을 적용한다. structure_index 5종(by_bo_id/by_issue_cluster_id/by_asset_cluster_id/by_evidence_index/by_candidate_structure_type)은 **다중 유형 additive 인덱싱**으로 재계산하고, review_required를 판정 결과 기반으로 재산정하며(무근거 전면 review 방지), audit에 review_reduction_trace를 남긴다. 관용 파서(§6.7 — 코드펜스 salvage)로 LLM 산출을 읽되 strict 검증 후 적용한다. +- **IO 정렬**: IN seed bundle + pack + adjudication (+audit) → OUT **`legal_effect_structures.json`** (write 후 재읽기 검증). + +## 3.5 출력 JSON 형식 + +```jsonc +// stage1_tmp/legal_effect_structure_exception_pack.json (발췌) +{ "schema_version": "stage1_legal_structure_exception_pack.v2", "status": "READY", + "has_exceptions": false, "exception_count": 0, + "exceptions": [{ "exception_id": "LX-001", "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": ["bh#"], "compact_context": {"...": "..."}, "candidate_values": ["..."], + "allowed_decisions": ["SAME_CLUSTER", "SEPARATE_CLUSTER", "MULTI_ASSET_BUNDLE", "BLOCK_REVIEW"], "conflict_flags": ["..."] }], + "budget": {"max_items": 30, "max_bytes": 40000, "item_char_limit": 2000} } + +// stage1_tmp/legal_effect_structure_adjudication.json (no-exception 경로) +{ "legal_structure_exception_adjudication": { + "schema_version": "stage1_legal_structure_exception_adjudication.v1", "status": "READY_NO_EXCEPTIONS", + "source_exception_pack_schema_version": "stage1_legal_structure_exception_pack.v2", + "decisions": [], "blocked_review_items": [], + "adjudication_gate": {"input_exception_count": 0, "decision_count": 0, "blocked_count": 0, + "coverage_pass": true, "json_only_contract_pass": true} } } + +// legal_effect_structures.json (최종 — 루트) +{ "schema_version": "stage1_legal_effect_structures.v1", "status": "READY|READY_WITH_REVIEW", + "legal_effect_structures": [{ + "structure_id": "LES-001", "candidate_structure_type": "money_claim|secured_debt|fraudulent_transfer|...(11종)", + "structure_label": "...", "source_bo_ids": ["bh#"], "issue_cluster_ids": ["..."], "asset_cluster_ids": ["..."], + "evidence_indexes": ["E-###"], "linked_claim_group_ids": ["..."], "linked_liability_group_ids": ["..."], + "linked_actio_signal_ids": ["..."], "legal_effect_roles": ["..."], "basis": "...", + "review_required": false, "routing_audit": {"...": "..."} }], + "structure_index": { "by_bo_id": {"bh#": {"structure_ids": [], "issue_cluster_ids": [], "asset_cluster_ids": [], + "evidence_index_ids": [], "candidate_structure_types": [], "claim_group_ids": [], "liability_group_ids": [], "downstream_fact_roles": []}}, + "by_issue_cluster_id": {}, "by_asset_cluster_id": {}, "by_evidence_index": {}, "by_candidate_structure_type": {} }, + "quality_gate": {"bo_coverage": {"...": "..."}, "evidence_coverage": {"...": "..."}, "review_queue": []} } +``` + +--- + +# Section 4: Analysis of Stage 1 — Part 4 + +## 4.1 Part 4의 궁극적 목표 + +Part 4는 Stage 1의 종착역으로, **소장 기안의 사실관계 원장(Fact Ledger)을 컴파일하는 단계**다. BO 50건 각각을 FINAL_FIELDS 27종의 닫힌 스키마를 가진 fact 행으로 투영하여 최상위 배열 `Fact_Ledger_base.json`을 생성한다. 각 행은 사실의 본문(derived_fact — "[일자] 행위자가 목적물에 관하여 행위하였다는 사실"), 증거 인용(evidence_refs)과 증거력 평가(credibility/proof_strength — 등기·계약·판결 등 documentary 마커 기반), 법률효과 투영(legal_effect_roles/claim_chain_ref/linked_structures/downstream_module_candidates), 검토 표지(skeleton_validation_codes/must_consider)를 담는다. + +동시에 세 가지 결정론 법률 게이트가 자구 수준으로 봉인되어 있다: 점유 권원 상충 스캔(유치권행사중 vs 무권원주장 — `POSSESSION_CAUSE_CONFLICT`), 통지 도달 게이트(도달 요건 통지의 수령 미확인 — `NOTICE_RECEIPT_UNVERIFIED` + 기안 차단 게이트 `BLOCK_FINAL_DRAFTING_NOTICE_RECEIPT_MISSING`, 수령 증거 약함 — `NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW`), 유치권 소멸 통지 후 점유 개시 스캔(`LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW`). 상류(Part 3)의 review 상태는 `LES_REVIEW_REQUIRED`로 결정론 승계된다. 이 원장이 Stage 2가 청구원인 사실관계를 기술할 때의 유일한 사실 원천이다. + +## 4.2 전체 워크플로우와 DAG + +``` +IN ──► Task_F0_fact_ledger_reducer (결정론 reducer — 舊 FL0+FL1 병합) + │ 입력 6종 read → 메모리 내 컴파일 (소스팩 비물질화) + │ → 후보 50행 (27필드 완전형) + 게이트 3종 스캔 + │ → X-3 4중 조건 통과 예외만 pack 수록 + │ → LES review 결정론 승계 + │ → candidate bundle(v2) + fact_exception_pack(v2) + ▼ + Task_F1_fact_exception_adjudicator (조건부 flash-lite) + │ has_exceptions=false → no-exception 파일 쓰고 즉시 종료 + │ true → compact 투영 예외만 PATCH/KEEP_AS_IS/BLOCK_REVIEW + ▼ + Task_F2_final_fact_ledger_gate_and_writer (결정론 최종 gate/writer) + │ exact coverage 등식 + patch hard reject + 보존 검증 + │ → Fact_Ledger_base.json + writer report (status 3단) + ▼ + OUT +``` + +## 4.3 IO 구조 + +**IN 파일** (F0만 읽는다 — 6종): `BO.json`, `legal_effect_structures.json`, `actio_case_signals.json`, `case_liability_signals.json`, `legal_effect_signals.json`, `evidence_indexed.json`. (`evidence_all.json`·`evidence_event_candidates.json`·`client_goal.json`은 기여도 0 실측으로 제거되었고, stdout `removed_inputs`에 고지된다.) + +**OUT 파일** + +| 파일 | writer | 소비자 | +|---|---|---| +| `stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json` | F0 | F2 | +| `stage1_tmp/fact_ledger/fact_exception_pack.json` | F0 | F1 (preflight), F2 (sha 대조) | +| `stage1_tmp/fact_ledger/fact_adjudication.json` | F1 | F2 | +| **`Fact_Ledger_base.json`** (최상위 배열 50행) | F2 (단독 writer) | Stage 2 | +| `stage1_tmp/fact_ledger/fact_ledger_writer_report.json` | F2 | Stage 2·감사 | + +## 4.4 Sub-task별 상세 설명 + +### (1) Task_F0_fact_ledger_reducer (Python) + +- **목표**: 입력 6종에서 후보 fact 50행을 완전형으로 컴파일하고, LLM 판정이 진짜 필요한 투영 충돌만 pack으로 좁힌다. 구판의 602KB 소스팩 물질화와 카디널리티 기반 가짜 예외 42건을 상류에서 소거한 task다. +- **수행 방식**: BO를 compact 정규화하고 `legal_effect_structures.json`의 스키마·인덱스·참조를 하드 검증한 뒤, 증거 발췌를 `evidence_indexed.json`의 실제 필드(key_facts 우선, key_dates/key_amounts 보조, 구키 fallback)로 실질화하여 증거력 스코어링(`_score_evidence`)의 판정 근거를 회복한다. BO id 정렬 순서로 행을 생성하며(F-001~F-050), 게이트 3종 스캔을 자구 이식 그대로 수행한다. 예외는 3유형(`legal_calculation_required`/`unresolved_asset_cluster_projection`/`unresolved_structure_role_projection`)에 한해 **4중 조건**(단일 값 선택 필수 / source-backed 상충 후보 2개 이상 실재 / 보존·defer 불가 / compact 판정 충분)을 전부 통과할 때만 pack에 수록하고(항목 ≤2,000자, 30건/40KB 상한 — 초과분은 exception_id를 유지한 채 review 라우팅), 단순 누락은 `CALCULATION_BASIS_INCOMPLETE_REVIEW` 등 결정론 review code로 전환한다. 복수 role/module/claim group은 배열로 보존한다(축소 선택 강제 금지). LES의 실질 review 상태(review_required, pending_exception_state.blocked, quality_gate.review_queue)는 관련 행에 `LES_REVIEW_REQUIRED` + `must_consider=true`로 승계하고 bundle의 `les_review_inheritance`에 원천을 기록한다. +- **IO 정렬**: IN 6종 → OUT slim candidate bundle(v2 — ref_manifest·les_review_inheritance·exception_pack_ref 포함, 타임스탬프·pack 내장 금지) + exception pack(v2, sha256이 bundle ref에 기록) → stdout 단일 JSON 라인. + +### (2) Task_F1_fact_exception_adjudicator (LLM, 조건부) + +- **목표**: F0가 non-deferrable로 판정한 compact 투영 예외만 판정한다. 정상 경로(7/10 fixture)에서는 예외 0건으로 즉시 종료하여 LLM 추론 비용이 사실상 0이 된다(구판은 상시 gemini-3.1-pro-preview/high로 $0.58). +- **수행 방식**: flash-lite(low/low/1, ttl 20m — 캐시 프리픽스 3,829B는 Part 2 R1·Part 3 L1과 바이트 동일)가 preflight pack의 `has_exceptions`를 확인해 false면 no-exception 판정 객체를 `fact_adjudication.json`에 쓰고 `"NO_EXCEPTIONS"`만 출력 후 종료한다. true면 항목별 `candidate_fact_row_subset`·`compact_context`(source_backed_candidates)만으로 판정한다: PATCH(후보 실재 값으로 단일 선택, patch 키는 `allowed_output_fields` 안에서만 — F2가 위반을 hard reject), KEEP_AS_IS, BLOCK_REVIEW(확신 없는 확정 금지 — 인간 검토 라우팅). PATCH/KEEP은 `decisions[]`에, BLOCK은 `blocked_review_items[]`에 — 각 exception_id는 정확히 한 곳에만 나타난다. LES2 확정 사항(cluster·유형)의 재판정과 legal_theory 표지의 PATCH는 금지된다. +- **IO 정렬**: IN pack(preflight) → OUT `fact_adjudication.json` 1개. 다른 어떤 파일도 읽지도 쓰지도 않는다. + +### (3) Task_F2_final_fact_ledger_gate_and_writer (Python) + +- **목표**: 후보 + 판정을 봉인 규칙으로 검증·적용하여 최종 `Fact_Ledger_base.json`을 단독 작성한다. +- **수행 방식**: candidate bundle(v2)을 닫힌 스키마로 검증하고, exception pack은 bundle의 `exception_pack_ref.sha256`·expected id 대조 후 읽으며, adjudication은 관용 파서(코드펜스 salvage)로 읽되 strict 검증한다. **exact coverage 등식**(expected == decided ∪ blocked, decided ∩ blocked == ∅; 중복·미지 ID·예외 0건의 임의 decision·gate 수치 불일치·정체성 불일치 전부 hard fail)을 통과한 판정만 적용한다. patch 키가 `allowed_output_fields ∩ FINAL_FIELD_SET` 밖이면 해당 decision 전체를 거부하고 그 행을 BLOCK 처리한다(경고 후 적용 금지). 참조 검증은 bundle의 ref_manifest(bo/structure/evidence ID 집합)로 수행하고(소스팩 재읽기 폐지), BO 보존(후보 == 최종)·F-### 연속·27필드 닫힌 스키마·enum을 검증한 뒤에야 write한다(검증 실패 시 기존 최종 파일 overwrite 금지). status는 3단 규칙 — 전 게이트 통과·차단 0·승계 review 0이면 `READY`, ledger 봉인은 가능하나 검토 항목이 있으면 `READY_WITH_REVIEW`, 위반이면 `FAILED` — 로 산정한다. +- **IO 정렬**: IN candidate bundle + pack + adjudication → OUT **`Fact_Ledger_base.json`** + writer report(기존 필드 유지 + status/les_review_inheritance/deterministic_review_items/rejected_patches additive, 타임스탬프 제거로 2-run 바이트 결정론 성립) → write 후 재읽기 검증 → stdout 단일 JSON 라인. + +## 4.5 출력 JSON 형식 + +```jsonc +// stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json (루트) +{ "fact_ledger_candidate_bundle": { + "schema_version": "stage1_fact_ledger_candidate_bundle.v2", "status": "READY|READY_WITH_EXCEPTIONS", + "candidate_items": ["FINAL_FIELDS 27종 완전형 행 × 50"], + "ref_manifest": {"bo_ids": ["bh#"], "structure_ids": ["LES-###"], "evidence_ids": ["E-###"]}, + "les_review_inheritance": {"by_fact_id": {"F-###": {"fact_id": "F-###", "source_bo_id": "bh#", + "structure_ids": ["LES-###"], "origins": {"LES-###": [{"origin": "structure_review_required|pending_exception_blocked|quality_gate_review_queue"}]}}}}, + "exception_pack_ref": {"path": "stage1_tmp/fact_ledger/fact_exception_pack.json", "sha256": "64hex", "expected_exception_ids": ["FX-###"]}, + "compile_gate": {"candidate_count": 50, "exception_count": 0, "deterministic_review_count": 0, + "les_inheritance_count": 0, "deterministic_review_items": [], "notice_gate": {"affected_fact_ids": [], "notice_ids": []}, + "source_integrity_warnings": [], "candidate_schema_closed": true, "exception_schema_closed": true} } } + +// stage1_tmp/fact_ledger/fact_exception_pack.json +{ "schema_version": "stage1_fact_ledger_exception_pack.v2", "status": "READY", + "has_exceptions": false, "exception_count": 0, + "exceptions": [{ "exception_id": "FX-001", + "exception_type": "legal_calculation_required|unresolved_asset_cluster_projection|unresolved_structure_role_projection", + "fact_id": "F-###", "source_bo_id": "bh#", "severity": "blocking_projection", "trigger_reason": "...", + "candidate_fact_row_subset": {"allowed 필드만": "..."}, + "compact_context": {"target_fields": ["amount"], "source_backed_candidates": {"amount": ["300,000,000원", "250,000,000원"]}}, + "allowed_output_fields": ["..."], "allowed_decisions": ["PATCH", "KEEP_AS_IS", "BLOCK_REVIEW"], "conflict_flags": ["..."] }], + "budget": {"max_items": 30, "max_bytes": 40000, "item_char_limit": 2000, "overflow_routed_to_review_count": 0} } + +// stage1_tmp/fact_ledger/fact_adjudication.json (루트) +{ "fact_exception_adjudication": { + "schema_version": "stage1_fact_exception_adjudication.v1", + "status": "READY|READY_WITH_BLOCKS|READY_NO_EXCEPTIONS", + "source_exception_pack_schema_version": "stage1_fact_ledger_exception_pack.v2", + "decisions": [{"exception_id": "FX-###", "exception_type": "...", "fact_id": "F-###", "source_bo_id": "bh#", + "decision": "PATCH|KEEP_AS_IS", "patch": {"allowed 키만": "..."}, "review_required": false, + "reason_code": "calculation_basis_selected|asset_projection_selected|structure_role_projected|keep_existing", + "basis_fields_used": [], "conflict_flags_acknowledged": []}], + "blocked_review_items": [{"exception_id": "FX-###", "exception_type": "...", "fact_id": "F-###", "source_bo_id": "bh#", + "block_reason": "insufficient_compact_record|reopens_les_ambiguity|unsupported_patch|missing_candidate_basis|schema_violation"}], + "adjudication_gate": {"input_exception_count": 0, "decision_count": 0, "blocked_count": 0, + "coverage_pass": true, "json_only_contract_pass": true, "reopened_les_ambiguity": false} } } + +// Fact_Ledger_base.json — 최상위 배열, 행당 FINAL_FIELDS 27종 (키 순서 고정) +[ { "fact_id": "F-001", "source_bo_id": "bh1", "type": "...", "date": "...", + "parties": [{"party": "...", "role": "actor|counterparty|...", "source": "..."}], + "object_spec": "...", "amount": "...", "action": "...", "evidence_refs": ["E-###"], + "credibility": "high|medium|low", "legal_centrality": "critical|medium|low", + "proof_strength": "documentary|high|medium|low|meeting_only_core", + "legal_effect_roles": ["..."], "claim_chain_ref": {"claim_group_ids": [], "liability_group_ids": [], "signal_count": 0}, + "linked_structures": {"structure_ids": ["LES-###"], "issue_cluster_ids": [], "asset_cluster_ids": [], + "evidence_index_ids": [], "candidate_structure_types": [], "claim_group_ids": [], "liability_group_ids": []}, + "must_not_drop_in_claim_types": ["..."], "must_consider": true, + "state_context": {"bo_type": "...", "issue_cluster_ids": [], "asset_cluster_ids": []}, + "money_claim_effect": {"enabled": false, "source_bo_id": null}, + "commercial_successor_effect": {"enabled": false, "source_bo_id": null}, + "actio_roles": ["..."], "linked_actio_structures": {"structure_ids": [], "actio_signal_count": 0}, + "downstream_module_candidates": ["..."], + "skeleton_validation_codes": ["LES_REVIEW_REQUIRED", "NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW", "..."], + "derived_fact": "[일자] 행위자가 목적물에 관하여 행위하였다는 사실", + "derivation_basis": "source_bo_id=bh1; evidence_refs=E-###; structure_ids=LES-###", + "legal_calculation_object": {"calculation_required": false, "amount_source": null, "amount_text": null, + "object_basis": null, "candidate_structure_types": []} } ] + +// stage1_tmp/fact_ledger/fact_ledger_writer_report.json (발췌) +{ "schema_version": "stage1_fact_ledger_writer_report.v1", "status": "READY|READY_WITH_REVIEW", + "blocked_review_items": [], "rejected_patches": [], "patch_policy_warnings": [], "unapplied_patch_fields": [], + "row_warnings": [], "possession_conflict_gate": {"status": "PASS|HARD_WARNING", "conflicts": []}, + "drafting_gates": [{"gate": "BLOCK_FINAL_DRAFTING_NOTICE_RECEIPT_MISSING", "row_code": "NOTICE_RECEIPT_UNVERIFIED", + "notice_ids": ["..."], "affected_fact_ids": ["F-###"]}], + "gate_review_code_summary": {"NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW": ["F-###", "F-###"]}, + "les_review_inheritance": ["bundle 승계 항목"], "deterministic_review_items": [] } +``` + +--- + +## 맺음말: Stage 1 전체를 관통하는 설계 원칙 + +네 Part를 종합하면 Stage 1은 다음 원칙 위에 서 있다. **writer 유일성** — 최종 파일마다 작성 권한을 가진 task가 정확히 하나이고, LLM은 최종 파일을 쓰지 못한다. **read-diet** — 각 task는 자기 판정에 필요한 최소 입력만 읽으며, 전집·소스팩 같은 비대 채널은 planner/reducer가 slice·bundle로 좁혀 배급한다. **예외의 상류 소거** — 기계적으로 판정 가능한 것은 전부 Python이 해소하고, LLM에는 "단일 값 선택이 필수이고 상충 후보가 실재하는" 진짜 예외만 compact pack으로 도달하므로 정상 사건에서 flash-lite 판정기 4형제(Part 1 B12, Part 2 R1, Part 3 L1, Part 4 F1)는 대부분 즉시 종료한다. **봉인과 감사** — coverage 등식, sha256 digest guard, 닫힌 스키마, write 후 재읽기, review code의 silent drop 금지가 각 경계를 지키고, 판정 불가 사안은 자동 진행이 아니라 변호사 검토 큐로 라우팅된다. 이 문법 덕에 Stage 2는 "검증된 사실·구조·증거"라는 전제 위에서 법리 구성과 기안에만 집중할 수 있다. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Claude YAML/Stage_1_Part_1_Claude_v3.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Claude YAML/Stage_1_Part_1_Claude_v3.yml new file mode 100644 index 00000000..50f2e87c --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Claude YAML/Stage_1_Part_1_Claude_v3.yml @@ -0,0 +1,4504 @@ +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_1 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Part 1 기초작업 + version: v.2 + + Stages: + - name: stage1_사건개요파악_기초작업 + description: 사건개요 파악, 기초정보 추출 + llm_provider: openai + llm_model: gpt-4o-2024-08-06 + tools: + mcpServers: + localdocs: + type: streamable-http + url: "http://mcp-localdocs:8012/mcp" + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + + tasks: + - task_name: Task_A_client_goal + llm_provider: google + llm_model: 'gemini-3.1-flash-lite' + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + preflight: false + preflight_files: + - client_meeting.md + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + - Aggregate, compile, publish, commit, or write only the inputs and final/derived file authorized by the current task. + - Preserve stable order, source IDs, support IDs, BO_ID, fact_id, structure_id, claim_group_id, liability_group_id, and required enum semantics exactly. + - Create or finalize IDs only from the current task's specified pattern and deterministic ordering rule. + - Do not merge, deduplicate, supersede, normalize, repair, or drop candidates unless the current reducer/writer schema authorizes that action. + - If validation or source support is insufficient, use the current failure, rollback, warning, or blocked path instead of fabricating a passing final artifact. + - Treat downstream authority and handoff fields as machine contracts; do not add prose, alternative files, or extra writer attestations outside the schema. + + + + + Task_A_client_goal + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. + + + `client_meeting.md`만 읽고 `client_goal.json`을 생성한다. + 이 파일은 고객의 소송 목적, 명시적 제약, 핵심 사건군, 당사자 universe를 Stage 1의 가장 앞단에서 안정적으로 구조화한 요약본이어야 한다. + v.3에서도 기존 필수 schema는 유지하되, 복수 원고와 복수 분쟁군이 뒤섞여 later stage가 공동원고를 과잉 일반화하지 않도록 사건군 단위의 optional 구조, 사건군별 원고적격 구조, 현재 지배상태 구조를 함께 남긴다. + 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않는다. + 특히 상속·승계형 사건군에서는 meeting만으로 확정되지 않은 친족관계 또는 공동행동 정보를 direct standing anchor처럼 과잉 일반화하지 않는다. + 상속이 개입된 사건군에서는 `legal entitlement layer`와 `complaint drafting preference layer`를 분리하여 기록한다. + 사망한 종전 권리주체의 과거 소유사실, 매수사실, 등기명의사실을 living plaintiff의 현재 `소유자/final`로 치환하지 않는다. + 필요하면 사건군별 optional additive field `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`, `preferred_plaintiff_selection_basis`, `succession_resolution_needed`를 사용하여, later stage가 실체법상 가능한 권리자 후보층과 실제 소장에 세울 우선 원고층을 혼동하지 않도록 한다. + 또한 `current_control_map`은 보조 메모가 아니라 later stage의 부당이득·인도·철거 청구 식별에 직접 쓰이는 authority map이므로, 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속을 가능한 한 4축으로 분해 저장한다. + v.3 기준으로 Task_A_client_goal은 모든 issue cluster에 사건군별 법률효과 구조의 seed를 남겨야 한다. 단순 사건 요약이 아니라 금전채권 timeline, 상호속용 책임, 담보부 부동산·지분별 권리상태, 사해행위취소 가액배상, 유치권·상속·통지 lifecycle을 downstream이 구조적으로 읽을 수 있게 `domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints`, `legal_entitlement_candidates`, `current_control_map_4axis`를 additive하게 생성한다. + `client_requested_relief`는 의뢰인이 원하는 청구 기간·금액·상대방을 보존하는 층이고, `legal_sustainability_watchpoints`는 법률상 기산일·원고적격·청구범위 재검토 필요성을 보존하는 층이다. 두 층을 절대 하나로 합치지 않는다. + 상속·승계형 사건군에서 current right holder가 meeting만으로 확정되지 않으면 `current_control_map`의 해당 row는 `certainty="provisional"`로 두고 `standing_watchpoints`에 succession-resolution trigger를 반드시 남긴다. + 또한 현재 무자력/집행난과 실제 제소 유지 의사를 혼동하지 않으며, 특정 상대방을 현재 무자력이라는 이유만으로 소 제기 대상에서 소거하지 않는다. + 필요하면 optional additive field `defendant_target_matrix`를 사용하여 피고별 소제기 유지/제외 의사, 현재 회수위험, 장래 회수신호를 분리 보존한다. + 본 task는 v.3 architecture에서 final writer로서 `client_goal.json`을 직접 작성한다(Task_A는 island/mapper가 없으므로 propose 단계 없이 직접 finalize 가능). + + + + + - In this task, legal/domain fields are meeting-grounded routing seeds and watchpoints for downstream stages. + - They are not final legal conclusions, norm search results, merits judgments, or complaint-drafting advice. + - The EXTRACTION overlay's prohibition on norm search and final legal-effect classification does not prohibit schema-authorized `domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints`, or entitlement/watchpoint seed fields. + + + + + - `client_meeting.md` + + + - `client_goal.json` + + + - 다른 파일 확인/읽기 + - 중간 파일 생성 + - `client_goal.json` 재읽기 + - meeting에 없는 당사자, 사건군, 제약, 희망사항, 청구유형을 창작하기 + - 사건군별 당사자 범위를 무시하고 전 사건 공통 사실처럼 과잉 압축하기 + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하기 + + + + + 1. `list_docs`로 `client_meeting.md` 존재만 확인한다. + 2. `read_docs`로 `client_meeting.md`만 읽는다. + 3. 메모리에서 `client_goal.json` 객체를 생성한다. + 4. `write_file(overwrite=true)`로 `client_goal.json`을 저장한다. + 5. `list_docs`로 `client_goal.json` 존재만 확인한다. 절대 읽지 않는다. + 6. 정확히 `"고객의사 초안 생성 완료"`만 출력하고 종료한다. + + + + - 필수 입력 누락, 빈 파일, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. 파일은 쓰지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + + - `client_meeting.md`만 사실 원천이다. + - 상대방 주장, 의뢰인 희망사항, 상담서두의 인적 관계, 본문 1~5항의 사건군을 모두 읽되, meeting에 직접 나타난 범위만 구조화한다. + - meeting의 문장들 사이를 조합해 새로운 사실을 만들지 않는다. + + + + + - `primary_goal`은 사건 전체를 한 문장으로 요약하되, 개별 분쟁군을 완전히 뭉개지 않을 정도의 수준으로만 추상화한다. + - `희망사항`이 복수이면 공통 분모를 중심으로 요약하되, 특정 핵심 구제수단이 반복되면 반영할 수 있다. + - 희망사항이 피고별 비대칭 전략을 직접 드러내면, 그 비대칭을 보존한다. + - 현재 회수곤란 사실이 있더라도 장래 재산취득, 상속, 보험금 회수, 급여압류 가능성 등으로 특정 피고를 제소 대상으로 유지하려는 의사가 직접 서술되면, 이를 `primary_goal` 또는 `summary_key_incidents`에 반영한다. + - 직접 금전청구 대상과 사해행위취소 대상이 다르면 한쪽으로 뭉개지 않는다. + - 불명확하면 `null`. + + + + - `constraints`에는 의뢰인이 명시적으로 원하지 않는 소, 특정 대상 제외, 가능하면/다만/아울러와 같은 제한적 요청, 상속·경매 공신력 등 사건 진행상의 명시적 제약을 넣는다. + - `constraints`는 소송전략상 하지 말 것, 제한할 것, 절차상·법률상 한계만 담는다. + - 단순한 회수곤란 사실, 현재 무자력, 현재 재산 부족, 집행난은 `constraints`에 넣지 않는다. + - 현재 무자력과 장래 재산취득 가능성이 함께 서술되면, 이를 부정적 제약으로 압축하지 않는다. + - 특정 상대방을 제외하라는 명시적 no-sue 지시가 없는 한, current collectability만으로 대상 제외 제약을 만들지 않는다. + - 해석상 추정 제약은 넣지 않는다. + - 배열 순서는 meeting 서술 순서를 최대한 따른다. + + + + - `summary_key_incidents`는 최대 4문장, 각 100자 이하. + - 각 문장은 가능한 한 서로 다른 사건군을 대표하도록 구성한다. + - 사건군 라벨은 입력자료에 직접 나타난 분쟁 대상, 거래유형, 자산군, 청구 방향을 짧게 추상화하여 만든다. 특정 사건의 지명·상호·인명·자산명을 예시값처럼 재사용하지 않는다. + - 하나의 문장에 서로 다른 사건군을 과도하게 병합하지 않는다. + - 직접 서술된 날짜/당사자/핵심 행위만 사용한다. + - 원고가 특정 피고를 직접 금전청구 대상으로 유지하려는 의사가 직접 서술되면, 사건군 대표 문장 중 하나에 그 방향성을 반영할 수 있다. + - 현재 회수곤란 사실과 장래 회수가능성 신호가 같은 사람에 관하여 함께 직접 서술되면, 둘 중 하나만 선택적으로 삭제하지 않는다. + + + + - `key_facts`는 최대 5개, 각 60자 이하. + - 가능한 한 사건군별 핵심 fact를 1개씩 대표시킨다. + - later stage의 원고적격 판단에 중요할 수 있는 소유관계, 상속관계, 현재 권리귀속, 현재 점유/사용/수익귀속 상태, 특정 처분/경매/통지 사실을 우선한다. + - 직접 서술이 없는 금액·날짜·상태를 보충하지 않는다. + + + + - `claim_type_candidates`는 의뢰인의 희망사항과 사건군 구조를 바탕으로 선택한다. + - 허용 enum만 사용한다: `"금전"|"물건인도"|"행위"|"확인"|"형성"|"보전(가처분/가압류)"`. + - later stage 분류기를 돕기 위한 상위 유형 수준만 남기고, 세부 청구명을 쓰지 않는다. + + + + + + - 이름은 원문 우선, 명백히 동일인/동일법인인 경우만 canonicalize 한다. + - 별칭/변형은 `aliases`에 기록한다. + - 당사자 배열은 사건 전체 participant universe다. 모든 분쟁군의 실질 원고적격/피고적격을 의미하지 않는다. + + + + - `parties.plaintiffs`에는 실제 의뢰인 또는 meeting 서두와 희망사항에서 원고 측 client로 직접 읽히는 사람만 넣는다. + - `parties.plaintiffs`는 client universe일 뿐, 사건군별 실질 원고적격을 의미하지 않는다. + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않는다. + - 사건군별 원고 범위 차이는 `issue_clusters`, `plaintiff_capacity_matrix`, `standing_watchpoints`에서 보정한다. + + + + - `parties.defendants`에는 meeting 본문, 상대방 주장, 희망사항에서 분쟁 상대방 또는 소송상 직접 대상으로 명시된 자만 넣는다. + - `asset_status`는 meeting에 직접 나온 재산 상태, 현재 소유/점유/담보권 설정 상태 등 later stage에 직접 도움이 되는 짧은 상태만 넣는다. + - `asset_status`는 현재 재산 상태에 관한 사실기술일 뿐, 소 제기 대상 제외 신호가 아니다. + - meeting이 "현재 무자력이나 장래 상속/재산취득 가능성 있음"을 직접 서술하면, `asset_status`는 그 둘을 함께 보존하는 방향으로 적는다. + - 현재 무자력만을 이유로 defendants에서 제외하지 않는다. 제외는 명시적 no-sue 지시가 있을 때만 허용한다. + - meeting에 없는 재산상태 추정 금지. + + + + - `parties.third_parties`에는 사건군에 중요하지만 즉시 원고/피고로 확정되지 않은 자를 넣는다. + - `relationship`은 later stage가 읽을 수 있도록 짧고 직접적으로 적는다. + + + + + - optional additive field `defendant_target_matrix`를 허용한다. + - 이 필드는 피고별 소제기 유지/제외 의사와 현재 회수위험을 분리 기록하는 전략 매트릭스다. + - `target_status`는 제소 유지 여부를 나타내고, `execution_risk`는 current collectability를 나타낸다. + - 현재 무자력은 `execution_risk`에 반영하고, 제소 유지 여부는 `target_status`에 별도로 반영한다. + - 장래 상속, 장래 재산취득, 보험금 회수 가능성, 급여압류 가능성 등 미래 회수신호가 직접 서술되면 `future_recovery_signal`에 반영한다. + - 명시적 no-sue 지시가 없는 한, 단순한 current uncollectability만으로 `exclude_now`를 부여하지 않는다. + - 각 원소는 아래 구조를 따른다. + { + "name": "string", + "target_status": "target_now|exclude_now|secondary_target|review", + "basis_text": "meeting 직접 문장에 근거한 짧은 사유", + "execution_risk": "low|medium|high|unknown", + "future_recovery_signal": "none|possible|explicit" + } + - `basis_text`는 반드시 meeting 직접 문장에 근거한 짧은 근거여야 한다. + - 이 필드는 optional이다. 명시적 피고별 타기팅 차이가 없으면 생략할 수 있다. + + + + - optional additive field `issue_clusters`를 허용한다. + - 각 cluster는 meeting의 독립 분쟁군을 1개씩 반영한다. + - 이 필드는 later stage가 전 사건 공통 parties 배열만 보고 과잉 일반화하는 것을 줄이기 위한 보조 구조다. + - `candidate_plaintiffs`는 사건군별 잠정 plaintiff anchor일 뿐, 최종 원고적격 확정값이 아니다. + - `legal_entitlement_candidates`는 해당 사건군에서 meeting 직접 서술상 실체법상 권리자일 가능성이 있는 의뢰인 후보층이다. + - `preferred_plaintiffs_for_complaint`는 later stage가 실제 소장 원고로 우선 검토할 수 있는 narrower drafting layer다. + - 상속·승계형 사건군에서는 `candidate_plaintiffs`, `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`를 서로 자동 동일시하지 않는다. + - `candidate_plaintiffs`에는 meeting에서 해당 사건군의 직접 권리주체, 직접 채권자, 직접 임차인, 직접 점유권자 등으로 읽히는 의뢰인만 넣는다. + - 친족관계, 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 `candidate_plaintiffs`를 확장하지 않는다. + - 사망한 자가 해당 사건군의 종전 권리주체로 보이는데 meeting만으로 승계가 해소되지 않으면, 친족관계나 공동 통지 사실만으로 `candidate_plaintiffs`를 채우지 않는다. + - 위와 같은 unresolved succession cluster에서는 meeting이 직접 보여주는 친족관계와 자산 연결 범위 안에서만 `legal_entitlement_candidates`를 별도 기록할 수 있다. + - `preferred_plaintiffs_for_complaint`는 meeting이 특정 의뢰인을 직접 권리주체 또는 직접 소송유지 주체로 좁혀 보여주거나, 비상속형 사건군에서 direct right-holder가 명백할 때만 채운다. + - meeting만으로 drafting preference가 명백하지 않은 unresolved succession cluster에서는 `preferred_plaintiffs_for_complaint`를 빈 배열로 둘 수 있다. + - `preferred_plaintiff_selection_basis` 허용값은 `"direct_right_holder|resolved_succession|resolved_succession_needed|drafting_preference|review|불명"`이다. + - `succession_resolution_needed`는 사망한 종전 권리주체와 사건군의 연결은 보이지만 meeting만으로 현재 권리귀속 또는 complaint plaintiff narrowing이 해소되지 않을 때 `true`로 둔다. + - meeting이 직접 금전청구 대상 피고와 사해행위 상대방/수익자를 구별해 보여주면, 이를 별도 cluster로 분리할 수 있다. + - 직접 금전회수 대상이 특정 상대방으로 서술되면, property transfer cluster와 별도의 monetary recovery cluster를 허용한다. + - cluster 수는 1개 이상 6개 이하로 제한한다. + - 각 cluster는 아래 구조를 따른다. + { + "cluster_id": "IC-01", + "cluster_label": "짧은 사건군명", + "candidate_plaintiffs": ["이 사건군에서 직접 권리주체로 읽히는 의뢰인"], + "legal_entitlement_candidates": ["실체법상 권리자 후보로 읽히는 의뢰인"], + "preferred_plaintiffs_for_complaint": ["실제 소장 원고로 우선 검토할 의뢰인"], + "preferred_plaintiff_selection_basis": "direct_right_holder|resolved_succession|resolved_succession_needed|drafting_preference|review|불명", + "succession_resolution_needed": true, + "candidate_defendants_or_counterparties": ["직접 상대방"], + "desired_relief_keywords": ["금전", "명도", "퇴거", "확인", "보전" 등 짧은 키워드] + } + - 비상속형 사건군에서 direct right-holder가 명백하면 `candidate_plaintiffs`, `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`가 동일할 수 있다. + - `issue_clusters`는 optional이다. 명확하지 않으면 생략 가능하다. + - 추가 사실 창작 없이 meeting 본문과 희망사항에서 직접 읽히는 범위만 기록한다. + + + + - optional additive field `domain_profiles`를 각 `issue_clusters[*]` 내부에 허용한다. + - `domain_profiles`는 아래 네 하위 profile을 담는 object다. 해당하지 않는 profile은 `null`로 둔다. + - `monetary_claim_profile` + - `secured_property_dispute_profile` + - `actio_property_transfer_profile` + - `possession_lien_succession_profile` + - `client_requested_relief`와 `legal_sustainability_watchpoints`를 각 cluster 내부에 둘 수 있다. + - 의뢰인이 원하는 청구취지·기간·금액은 `client_requested_relief`에 보존하고, 법률상 기산일·원고적격·범위가 불명확한 점은 `legal_sustainability_watchpoints`에 분리한다. + - 당사자가 원하는 기간이나 금액을 법률상 청구취지에 바로 사용할 수 있다고 단정하지 않는다. + + + - 금전청구 사건군에서 원채무자 외 보증인, 채무인수자, 영업양수인, 상호속용자, 공동채무자 후보가 함께 등장하면 이를 별도 후보 피고 목록에만 두지 말고 `monetary_claim_profile`, `liability_group_hint`, `commercial_successor_liability_hint`로 묶어 보존한다. + - 동일 금액, 동일 원채권, 동일 원고를 기준으로 복수 피고가 책임질 가능성이 있으면 `liability_group_hint.same_underlying_debt=true`로 둔다. + - 지급기일, 이행청구, 최고, 채무승인, 이행각서, 소멸시효, 변제기 재설정 단서가 등장하면 `monetary_claim_profile.requires_money_claim_timeline=true`로 둔다. + - 영업양도, 상호 계속 사용, 사업 양수인, 채무승계 부인, 책임배제 등기 단서가 등장하면 `commercial_successor_liability_hint.requires_article_42_review=true`로 둔다. + + + + - 부동산 사건에서 여러 대여금, 여러 근저당권, 지분별 담보, 변제, 경매, 후행 등기가 함께 등장하면 이를 흩어진 issue로 분리하지 말고 `secured_property_dispute_profile`에 묶는다. + - 변제금이 복수 채무에 영향을 줄 수 있으면 `requires_payment_allocation=true`로 표시한다. + - 전체 부동산이 아니라 지분별 유효/무효가 문제될 수 있으면 `requires_fractional_validity_analysis=true`로 표시한다. + - 원고 주장 지분, 피고 측 유효 가능 지분, 전체 부동산 말소 금지 필요성을 `fractional_title_issue_hint`에 남긴다. + - 미등기 건물, 건물 신축, 건물 매매, 대지 임대차가 함께 등장하면 `land_use_gain_hint`를 생성한다. + - 임대차보증금, 월 차임, 건물 매매대금은 각각 법적 성격이 다르므로 `lease_payment_semantics_hint`에 별도 표시한다. + - 임대차보증금은 토지소유자의 부당이득 원금으로 자동 산정하지 말라는 watchpoint를 남긴다. + + + + - 채무자가 부동산을 수익자에게 이전하고, 원고가 사해행위취소를 희망하는 사건군을 발견하면 `actio_property_transfer_profile`을 생성한다. + - 부동산에 근저당권 등 부담이 있고, 그 부담이 사해행위 이후 변제·말소되었다는 단서가 있으면 `requires_value_compensation_review=true`로 표시한다. + - 같은 채권자와 채무자 사이에 복수 대여금이 등장하면 단순 후보채권 목록으로 흩뜨리지 말고 `preserved_claim_bundle_hint`로 묶는다. + - 피고가 명의신탁, 충분 담보, 상계, 선의 등을 주장하면 이를 `defense_hints`에 남기고, 청구원인 성립 또는 범위에 직접 영향을 주는 항변이면 `requires_stage2_rebuttal=true`로 표시한다. + + + + - 현재 점유자와 현재 권리자가 분리되는 자산군 또는 사건군이 입력자료에서 직접 확인되면 `possession_lien_succession_profile`을 생성한다. + - 상속이 개입되면 current right holder를 final로 확정하지 말고 `succession_resolution_needed=true`와 `standing_watchpoints`를 남긴다. + - 유치권, 제3자 임대, 직접 거주, 피담보채권 충당, 후소유자 소멸청구권 부정 주장이 보이면 `possession_basis_asserted`, `notice_lifecycle_hints`, `legal_sustainability_watchpoints`를 구조화한다. + - 통지서 발송일과 답변서상 수령 인정일이 함께 보이면 발송일과 도달일을 분리하여 `notice_lifecycle_hints`에 남긴다. + - 상속·승계가 미해결인 cluster에서는 입력자료상 공동 통지 발신자, 친족 후보, 직계존속·직계비속 후보를 그 지위의 후보로만 보존하고, 상속 해결 전 `preferred_plaintiffs_for_complaint` 확정자로 두지 않는다. + + + + + - optional additive field `current_control_map_4axis`를 허용한다. + - legacy `current_control_map` row 구조는 유지하되, 목적물별 4축 요약이 가능한 경우 `current_control_map_4axis`를 추가한다. + - 각 item은 아래 구조를 따른다. + { + "object": "string", + "cluster_id": "IC-01", + "current_right_holder_candidate": "string|null", + "current_possessor": "string|null", + "current_user": "string|null", + "current_benefit_holder": "string|null", + "possession_basis_asserted": "string|null", + "possession_basis_transition_needed": true, + "certainty": "final|provisional|불명" + } + - 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속을 하나의 generic holder로 뭉개지 않는다. + - 상속·승계형 사건군에서 현재 권리귀속이 decedent의 과거 권리사실과 친족관계로만 추론되는 경우 `certainty="provisional"`로 둔다. + + + + - optional additive field `plaintiff_capacity_matrix`를 허용한다. + - 이 필드는 사건군별 실질 원고적격을 later stage로 넘기기 위한 standing map이다. + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않는다. + - 각 원소는 아래 구조를 따른다. + { + "cluster_id": "IC-01", + "plaintiff_name": "string", + "capacity_basis": "소유자|공유자|상속인|임차인|채권자|점유권자|불명", + "source_basis_text": "meeting에서 직접 읽히는 짧은 근거", + "certainty": "final|provisional" + } + - 사건군별 직접 권리주체로 읽히는 사람만 넣는다. + - `capacity_basis="상속인"` row라도, 근거가 친족관계, 사망, 공동 통지, 공동 상담 같은 succession trigger 수준이면 `certainty`는 반드시 `"provisional"`로 둔다. + - 상속·승계형 사건군에서 decedent의 과거 소유사실, 매수사실, 등기명의사실만 보일 뿐 living plaintiff의 승계 완료 또는 현재 권리귀속이 meeting에서 직접 해소되지 않으면, living plaintiff를 `capacity_basis="소유자"` 또는 `capacity_basis="공유자"` + `certainty="final"`로 두지 않는다. + - 위 경우에는 원칙적으로 `capacity_basis`를 `"상속인"` 또는 `"불명"`으로 두고, `certainty`를 `"provisional"`로 둔다. + - meeting이 특정 사건군에 관하여 직접 계약당사자, 직접 채권자, 직접 임차인, 직접 소유·공유 관계 또는 승계 완료 상태를 직접 보여줄 때만 `certainty="final"`을 허용한다. + - 원고적격이 불명확하거나 later stage 재검토가 필요하면 `certainty`는 `"provisional"`로 둔다. + - `preferred_plaintiffs_for_complaint`가 별도로 존재하더라도, 이를 맞추기 위해 `plaintiff_capacity_matrix`를 `소유자/final`로 과잉 보정하지 않는다. + + + + - optional additive field `current_control_map`를 허용한다. + - 이 필드는 사건군별 현재 지배상태를 later stage로 넘기기 위한 authority map이다. + - 사건군에 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속 사실이 직접 읽히면 가능한 한 `current_control_map`을 생성한다. + - 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속은 가능한 한 분리한다. + - 각 원소는 아래 구조를 따른다. + { + "cluster_id": "IC-01", + "holder_name": "string", + "holder_role": "current_right_holder|current_possessor|current_user|current_benefit_holder", + "basis_text": "meeting에서 직접 읽히는 짧은 근거", + "certainty": "final|provisional" + } + - 같은 사람이 복수 role을 가지면 중복 기재할 수 있다. + - meeting에 직접 없는 role은 추정하지 않는다. + - 상속·승계형 사건군에서 current right holder가 decedent의 과거 권리사실 + 친족관계만으로 유도되는 경우, `holder_role="current_right_holder"` row는 원칙적으로 `certainty="provisional"`로 둔다. + - decedent의 과거 소유사실만으로 living plaintiff를 `current_right_holder/final`로 기재하지 않는다. + - 비상속형 사건군에서 meeting이 현재 소유자, 현재 점유자, 현재 사용자, 현재 수익귀속자를 직접 적시하면 해당 row는 `certainty="final"`을 우선 검토한다. + + + + - optional additive field `standing_watchpoints`를 허용한다. + - later stage가 반드시 재검토해야 하는 원고적격/권리귀속 리스크를 짧은 경고문으로 남긴다. + - 상속, 상속포기, 사망, 경매, 공동소유, 현재 권리주체 불명, 현재 점유자와 권리주체 불일치가 보이면 우선 검토한다. + - 경고문은 가능한 한 자산군 또는 사건군을 특정하는 asset-specific trigger 형태로 쓴다. + - 예시 형식: + - "상속·승계 자료 추가 확인 후 해당 자산군 원고적격 재확인 필요" + - "현재 권리귀속과 현재 점유자 분리 검토 필요" + - "사망한 종전 권리주체 관련 succession resolution 필요" + - meeting에 직접 읽히는 범위를 넘는 장문 법률판단은 쓰지 않는다. + + + + - `summary_key_incidents`, `key_facts`, `constraints`는 exact-match 중복 제거 후 최초 순서를 유지한다. + - `aliases`는 명백한 경우에만 기록한다. + - `claim_type_candidates`는 중복 제거 후 최초 순서를 유지한다. + - `defendant_target_matrix`를 사용하면 same `name` + `target_status` 조합 기준 exact-match 중복 제거 후 최초 순서를 유지한다. + - 비nullable string 외에는 `불명` 사용을 피하고 `null` 또는 빈 배열을 우선한다. + + + + 기존 필수 schema는 유지하고, 아래 optional additive field를 추가할 수 있다. + v.3 schema registry 기준 `IssueClusterDomainProfile`, `LegalEntitlementCandidate`, `CurrentControlMap4Axis`를 반영한다. + + { + "primary_goal": "string|null", + "constraints": [], + "summary_key_incidents": [], + "key_facts": [], + "claim_type_candidates": ["금전","물건인도","행위","확인","형성","보전(가처분/가압류)"], + "parties": { + "plaintiffs": [{"name":"string","type":"법인|자연인|기관"}], + "defendants": [{"name":"string","type":"법인|자연인|기관|미확정","asset_status":"string|null"}], + "third_parties": [{"name":"string","relationship":"string"}] + }, + "aliases": {}, + + "defendant_target_matrix": [ + { + "name": "string", + "target_status": "target_now|exclude_now|secondary_target|review", + "basis_text": "string", + "execution_risk": "low|medium|high|unknown", + "future_recovery_signal": "none|possible|explicit" + } + ], + + "issue_clusters": [ + { + "cluster_id": "IC-01", + "cluster_label": "string", + "candidate_plaintiffs": ["string"], + "legal_entitlement_candidates": ["string"], + "preferred_plaintiffs_for_complaint": ["string"], + "preferred_plaintiff_selection_basis": "direct_right_holder|resolved_succession|resolved_succession_needed|drafting_preference|review|불명", + "succession_resolution_needed": true, + "candidate_defendants_or_counterparties": ["string"], + "desired_relief_keywords": ["string"], + "domain_profiles": { + "monetary_claim_profile": { + "underlying_claim_type": "string|null", + "principal_amount_candidate": "string|null", + "principal_debtor_candidates": ["string"], + "successor_or_secondary_liability_candidates": [], + "default_or_limitation_issue_expected": true, + "known_demand_or_notice_signals": ["string"], + "requires_money_claim_timeline": true + }, + "secured_property_dispute_profile": { + "has_multiple_secured_debts": true, + "requires_payment_allocation": true, + "requires_fractional_validity_analysis": true, + "secured_object_candidates": ["string"], + "auction_or_registry_invalidity_expected": true + }, + "actio_property_transfer_profile": { + "debtor": "string|null", + "creditor": "string|null", + "beneficiary": "string|null", + "fraudulent_act_asset": "string|null", + "fraudulent_act_type": "string|null", + "fraudulent_act_date_hint": "string|null", + "is_encumbered_real_estate": true, + "requires_value_compensation_review": true + }, + "possession_lien_succession_profile": { + "possession_basis_asserted": "string|null", + "succession_resolution_needed": true, + "notice_lifecycle_hints": [], + "lien_violation_or_extinction_hints": [] + } + }, + "client_requested_relief": ["string"], + "legal_sustainability_watchpoints": ["string"], + "liability_group_hint": {}, + "commercial_successor_liability_hint": {}, + "fractional_title_issue_hint": {}, + "land_use_gain_hint": {}, + "lease_payment_semantics_hint": {}, + "preserved_claim_bundle_hint": {}, + "value_compensation_hint": {}, + "defense_hints": [] + } + ], + + "plaintiff_capacity_matrix": [ + { + "cluster_id": "IC-01", + "plaintiff_name": "string", + "capacity_basis": "소유자|공유자|상속인|임차인|채권자|점유권자|불명", + "source_basis_text": "string", + "certainty": "final|provisional" + } + ], + + "current_control_map": [ + { + "cluster_id": "IC-01", + "holder_name": "string", + "holder_role": "current_right_holder|current_possessor|current_user|current_benefit_holder", + "basis_text": "string", + "certainty": "final|provisional" + } + ], + + "current_control_map_4axis": [ + { + "object": "string", + "cluster_id": "IC-01", + "current_right_holder_candidate": "string|null", + "current_possessor": "string|null", + "current_user": "string|null", + "current_benefit_holder": "string|null", + "possession_basis_asserted": "string|null", + "possession_basis_transition_needed": true, + "certainty": "final|provisional|불명" + } + ], + + "standing_watchpoints": ["string"] + } + + 추가 제약: + - `defendant_target_matrix`, `issue_clusters`, `plaintiff_capacity_matrix`, `current_control_map`, `standing_watchpoints`는 optional이다. + - `domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints`, `liability_group_hint`, `commercial_successor_liability_hint`, `fractional_title_issue_hint`, `land_use_gain_hint`, `lease_payment_semantics_hint`, `preserved_claim_bundle_hint`, `value_compensation_hint`, `defense_hints`는 `issue_clusters` 내부 optional additive field다. + - `current_control_map_4axis`는 top-level optional additive field이며 legacy `current_control_map`을 대체하지 않는다. + - `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`, `preferred_plaintiff_selection_basis`, `succession_resolution_needed`는 `issue_clusters` 내부 optional additive field다. + - 기존 필수 필드는 모두 유지한다. + - 허용된 additive field 외 임의 추가 필드 금지. + + + + - `parties.plaintiffs`에는 실제 의뢰인만 남았는지 점검한다. + - `parties.plaintiffs`가 client universe로만 쓰였고, 사건군별 원고적격 확정값처럼 오염되지 않았는지 점검한다. + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않았는지 점검한다. + - `parties.defendants`와 `third_parties`가 뒤바뀌지 않았는지 점검한다. + - `summary_key_incidents`가 사건군별로 과잉 병합되지 않았는지 점검한다. + - `key_facts`가 later stage에 중요한 standing/ownership/current control fact를 누락하지 않았는지 점검한다. + - `issue_clusters`를 썼다면 각 cluster의 당사자와 desired relief가 meeting 직접 서술 범위를 넘지 않는지 점검한다. + - 상속·승계형 사건군에서 `candidate_plaintiffs`가 친족관계, 공동 통지, 공동 상담 같은 간접 신호만으로 확장되지 않았는지 점검한다. + - `issue_clusters`를 썼다면 unresolved succession cluster에서 `legal_entitlement_candidates`와 `preferred_plaintiffs_for_complaint`가 자동 동일시되지 않았는지 점검한다. + - `preferred_plaintiffs_for_complaint`를 썼다면 그 basis가 `preferred_plaintiff_selection_basis`에 남아 있고, meeting 직접 서술 또는 direct right-holder 구조를 넘지 않는지 점검한다. + - `succession_resolution_needed=true`인데도 해당 cluster가 direct owner/final cluster처럼 오염되지 않았는지 점검한다. + - `plaintiff_capacity_matrix`를 썼다면 각 row의 `capacity_basis`와 `source_basis_text`가 meeting 직접 서술 범위를 넘지 않는지 점검한다. + - `capacity_basis="상속인"` row가 친족관계나 공동행동 사실만으로 `certainty="final"`로 오염되지 않았는지 점검한다. + - 사망한 종전 권리주체의 과거 소유사실, 매수사실, 등기명의사실이 living plaintiff의 `소유자/final` row로 잘못 치환되지 않았는지 점검한다. + - `current_control_map`을 썼다면 `holder_role`이 `current_right_holder|current_possessor|current_user|current_benefit_holder` 중 하나인지와, basis가 meeting 직접 문장에 근거하는지 점검한다. + - 상속·승계형 사건군에서 `current_right_holder` row가 있으면, meeting만으로 승계가 해소되지 않은 경우 `certainty="provisional"`인지 점검한다. + - 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속이 서로 다른데 하나의 generic holder row로 뭉개지지 않았는지 점검한다. + - 사망한 종전 권리주체가 보이는 사건군이 있으면 `standing_watchpoints`에 succession-resolution trigger가 남았는지 점검한다. + - `standing_watchpoints`를 썼다면 장문 법률판단이 아니라 later stage 재검토용 짧은 경고인지 점검한다. + - meeting이 피고별로 현재 집행가능성과 장래 회수가능성을 구별하여 서술하면, 그 차이가 `constraints`, `primary_goal`, `summary_key_incidents`, `parties.defendants.asset_status`, `defendant_target_matrix` 중 어디엔가 비대칭적으로 보존되었는지 점검한다. + - "현재 무자력"과 "장래 재산취득 가능성"이 같은 사람에게 함께 서술되면, 그 사람을 자동 배제 대상으로 압축하지 않았는지 점검한다. + - `constraints`에 current collectability나 단순 회수곤란 사실이 잘못 들어가 있지 않은지 점검한다. + - `asset_status`가 제소 배제 신호처럼 쓰이지 않았는지 점검한다. + - `defendant_target_matrix`를 썼다면 각 `name`이 `parties.defendants`와 모순되지 않는지 점검한다. + - `defendant_target_matrix`에서 `target_status="exclude_now"`가 붙은 사람에게는 meeting상 명시적 no-sue 지시가 있는지 점검한다. + - `defendant_target_matrix`에서 `execution_risk="high"`와 `target_status="target_now"`가 함께 존재할 수 있음을 허용하되, 그 경우 `basis_text` 또는 `future_recovery_signal`이 비어 있지 않은지 점검한다. + - 장래 상속/재산취득 가능성이 직접 서술된 피고가 `exclude_now`로 잘못 내려가지 않았는지 점검한다. + - 금전채권 cluster에 최고ㆍ채무승인ㆍ이행각서ㆍ시효 단서가 있는데 `monetary_claim_profile.requires_money_claim_timeline`이 없거나 false이면 warning으로 본다. + - 동일 사건군에 원채무자와 승계ㆍ양수ㆍ보증ㆍ상호속용 책임 후보가 함께 있는데 `liability_group_hint`가 비어 있으면 warning으로 본다. + - 영업양도ㆍ상호속용 단서가 있는데 `commercial_successor_liability_hint.requires_article_42_review`가 없으면 warning으로 본다. + - 대여금ㆍ근저당권ㆍ경매ㆍ지분 단서가 같은 cluster에 있는데 `secured_property_dispute_profile.requires_payment_allocation`이 없으면 warning으로 본다. + - 상담문서에 지분별 권리상태가 보이는데 `fractional_title_issue_hint`가 없으면 warning으로 본다. + - 미등기 건물ㆍ임대차ㆍ건물 매매ㆍ토지 사용 단서가 있는데 `land_use_gain_hint`가 없으면 warning으로 본다. + - 사해행위취소ㆍ부담부 부동산ㆍ사후 담보권 말소 단서가 있는데 `actio_property_transfer_profile.requires_value_compensation_review`가 false이면 error로 본다. + - 같은 채권자와 채무자 사이 복수 대여금이 보이는데 `preserved_claim_bundle_hint`가 없으면 warning으로 본다. + - 명의신탁ㆍ충분담보ㆍ상계ㆍ선의 등 항변 단서가 있는데 `defense_hints`가 없으면 warning으로 본다. + - 유치권ㆍ상속ㆍ승계 사건군에서 `succession_resolution_needed=true`인데도 입력자료상 공동 통지 발신자, 친족 후보, 직계존속·직계비속 후보가 `preferred_plaintiffs_for_complaint` 확정자로 들어가면 error로 본다. + - 통지 발송일과 도달일 단서가 모두 있는데 `notice_lifecycle_hints`가 없으면 warning으로 본다. + - `current_control_map_4axis`를 썼다면 4축 중 직접 읽히는 축이 generic holder 하나로 합쳐지지 않았는지 점검한다. + - 출력 파일명과 성공 메시지가 정확한지 점검한다. + + + + + + [ ] 1) Preflight: `list_docs` 사용해서 `client_meeting.md` 확인, `read_docs` 사용해서 `client_meeting.md` 읽기 + [ ] 2) `write_file` 사용해서 `client_goal.json` 생성 + [ ] 3) `list_docs` 사용해서 `client_goal.json` 파일 존재만 확인(절대 읽기 금지) + [ ] 4) `"고객의사 초안 생성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `client_meeting.md` + + + - `client_goal.json` + + + + + - `client_goal.json`은 절대 재읽지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"고객의사 초안 생성 완료"` 한 줄만 사용한다. + + + + - task_name: Task_Evidence_shard_planner + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import hashlib + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + def _mid(): return next(MSG_ID) + def _init(): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"task-evidence-shard-planner","version":"1.0","user_id":"{{__user_hash__}}","workspace_id":"{{__workspace_hash__}}"}}}, headers=MCP_HEADERS) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post(LOCALDOCS_URL, json={"jsonrpc":"2.0","method":"notifications/initialized"}, headers=MCP_HEADERS).raise_for_status() + def _parse(text): + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: return json.loads(line[6:]) + except Exception: pass + try: return json.loads(text) + except Exception: return None + def _call(name, args): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc":"2.0","id":_mid(),"method":"tools/call","params":{"name":name,"arguments":args}}, headers=MCP_HEADERS) + r.raise_for_status() + p = _parse(r.text) + if not p or "result" not in p: raise RuntimeError(f"MCP {name} failed") + return p + def _unwrap(p, name): + text = (p["result"].get("content") or [{}])[0].get("text", "") + if not text: raise RuntimeError(f"Empty: {name}") + try: outer = json.loads(text) + except Exception: return text + if isinstance(outer, dict) and "results" in outer: + r0 = (outer.get("results") or [{}])[0] + inner = r0.get("content") or r0.get("text") or "" + if not inner: raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return json.dumps(outer, ensure_ascii=False) if not isinstance(outer, str) else outer + def read_json(name): + raw = _unwrap(_call("read_docs", {"doc_names": [name]}), name) + if not isinstance(raw, str): return raw + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + obj, _ = decoder.raw_decode(raw.strip()) + return obj + def write_doc(path, content): + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def _items(obj: Any) -> list[Any]: + if isinstance(obj, list): + return obj + if isinstance(obj, dict): + for key in ("items", "documents", "evidences", "evidence", "docs"): + val = obj.get(key) + if isinstance(val, list): + return val + raise RuntimeError("evidence_all.json must be a JSON array or an object containing a document array") + + def _text(v: Any) -> str | None: + if v is None: + return None + s = str(v).strip() + return s or None + + def _doc_title(doc: Any, ordinal: int) -> str | None: + if not isinstance(doc, dict): + return f"evidence_all[{ordinal}]" + for key in ("title", "name", "filename", "file_name", "document_title", "doc_title"): + val = _text(doc.get(key)) + if val: + return val + return f"evidence_all[{ordinal}]" + + def _doc_uid(doc: Any, ordinal: int) -> str: + if isinstance(doc, dict): + for key in ("doc_uid", "uid", "id", "document_id", "source_id"): + val = _text(doc.get(key)) + if val: + return val + return f"DOC-{ordinal:03d}" + + def _stable_hash(doc: Any) -> str: + raw = json.dumps(doc, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _list_doc_names(args) -> list[str]: + payload = _unwrap(_call("list_docs", args), "list_docs") + if isinstance(payload, str): + try: + payload = json.loads(payload) + except Exception: + return re.findall(r"[\w./-]+\.json", payload) + vals: list[str] = [] + if isinstance(payload, dict): + for value in payload.values(): + if isinstance(value, list): + for item in value: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + elif isinstance(value, str): + vals.append(value) + elif isinstance(payload, list): + for item in payload: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + return [v for v in vals if v] + + def _read_existing_doc_names() -> list[str]: + preferred = [ + {"path": "evidence_indexed_parts", "pattern": "E-*.json"}, + {"path": "evidence_event_candidate_parts", "pattern": "E-*.json"}, + ] + preferred_errors = [] + preferred_names: list[str] = [] + for args in preferred: + try: + folder = str(args["path"]).rstrip("/") + preferred_names.extend(f"{folder}/{name.split('/')[-1]}" for name in _list_doc_names(args)) + except Exception as exc: + preferred_errors.append(f"{args['path']}: {exc}") + if not preferred_errors: + return sorted(set(preferred_names)) + + fallback_errors = [] + for args in ({}, {"doc_names": []}): + try: + return sorted(set(preferred_names + _list_doc_names(args))) + except Exception as exc: + fallback_errors.append(f"{args}: {exc}") + raise RuntimeError("list_docs failed for preferred schemas and fallback schemas; stale part detection cannot safely continue: " + " | ".join(preferred_errors + fallback_errors)) + + def _stale_parts(existing_names: list[str], n: int, folder: str) -> list[str]: + stale: list[str] = [] + pattern = re.compile(rf"(?:^|/){re.escape(folder)}/E-(\d{{3}})\.json$") + for name in existing_names: + m = pattern.search(name) + if not m: + continue + ordinal = int(m.group(1)) + if ordinal < 1 or ordinal > n: + stale.append(name) + return sorted(set(stale)) + + def main(): + _init() + evidence_all = read_json("evidence_all.json") + docs = _items(evidence_all) + n = len(docs) + if n <= 0: + raise RuntimeError("evidence_all.json contains no evidence documents") + + existing_names = _read_existing_doc_names() + stale_b1 = _stale_parts(existing_names, n, "evidence_indexed_parts") + stale_b2 = _stale_parts(existing_names, n, "evidence_event_candidate_parts") + + shards = [] + streaming_edges = [] + b1_waits = [] + b2_waits = [] + b1_files = [] + b2_files = [] + for idx, doc in enumerate(docs, 1): + label = f"{idx:03d}" + evidence_id = f"E-{label}" + b1_task_id = f"Task_B1_map_doc_{label}" + b2_task_id = f"Task_B2_map_events_{label}" + b1_output = f"evidence_indexed_parts/{evidence_id}.json" + b2_output = f"evidence_event_candidate_parts/{evidence_id}.json" + # 문서별 shard 파일 — B1/B2 인스턴스가 evidence_all.json 풀 대신 자기 shard만 읽도록. + shard_doc_path = f"evidence_shards/{evidence_id}.json" + write_doc(shard_doc_path, json.dumps(doc, ensure_ascii=False, indent=2)) + b1_waits.append(b1_task_id) + b2_waits.append(b2_task_id) + b1_files.append(b1_output) + b2_files.append(b2_output) + shards.append({ + "assigned_ordinal": idx, + "ordinal_label": label, + "evidence_index_candidate": evidence_id, + "b1_task_id": b1_task_id, + "b2_task_id": b2_task_id, + "b1_output_path": b1_output, + "b2_output_path": b2_output, + "shard_doc_path": shard_doc_path, + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": idx, + "array_index": idx - 1, + "doc_uid_candidate": _doc_uid(doc, idx), + "title_candidate": _doc_title(doc, idx), + "content_sha256": _stable_hash(doc), + "shard_doc_path": shard_doc_path + }, + "runtime_bindings": { + "__assigned_ordinal__": idx, + "__ordinal_label__": label, + "__evidence_index_candidate__": evidence_id, + "__b1_part_path__": b1_output, + "__b2_part_path__": b2_output, + "__shard_doc_path__": shard_doc_path + } + }) + streaming_edges.append({ + "from": b1_task_id, + "to": b2_task_id, + "condition": {"file_exists": b1_output}, + "bind": {"__assigned_ordinal__": idx} + }) + + plan = { + "schema_version": "evidence_shard_plan.v1", + "planner_task": "Task_Evidence_shard_planner", + "created_at_utc": datetime.now(timezone.utc).isoformat(), + "source_file": "evidence_all.json", + "source_selector": "$.items || $", + "shard_count": n, + "expected_ordinals": [f"{i:03d}" for i in range(1, n + 1)], + "expected_evidence_ids": [f"E-{i:03d}" for i in range(1, n + 1)], + "ordinal_policy": { + "base": 1, + "assignment": "array_order", + "format": "%03d", + "assigned_ordinal_var": "__assigned_ordinal__", + "evidence_index_candidate": "E-{ordinal:03d}", + "no_legal_sorting": True, + "no_content_based_reordering": True + }, + "shards": shards, + # 백엔드 wildcard fan-out: dynamic_fanout 리스트의 각 항목이 한 인스턴스 컨텍스트({{item.*}}로 접근). + # shards를 그대로 사용 — 디스크립터 메타(id/count/task_template 등)는 fanout_meta로 분리 보존. + "dynamic_fanout": shards, + "fanout_meta": [ + { + "id": "B1_evidence_doc_fanout", + "count": n, + "task_template": "Task_B1_map_doc", + "instance_id_template": "Task_B1_map_doc_{ordinal:03d}", + "output_path_template": "evidence_indexed_parts/E-{ordinal:03d}.json" + }, + { + "id": "B2_event_candidate_fanout", + "count": n, + "task_template": "Task_B2_map_events", + "instance_id_template": "Task_B2_map_events_{ordinal:03d}", + "output_path_template": "evidence_event_candidate_parts/E-{ordinal:03d}.json", + "wait_until_template": "Task_B1_map_doc_{ordinal:03d}" + } + ], + "streaming_edges": streaming_edges, + "barriers": { + "B1_all_parts_barrier": { + "wait_for_tasks": b1_waits, + "require_files": b1_files, + "next": "Task_B1_quality_gate_evidence_indexed" + }, + "B2_all_parts_barrier": { + "wait_for_tasks": b2_waits, + "require_files": b2_files, + "plus": ["Task_B1_quality_gate_evidence_indexed"], + "next": "Task_B2_quality_gate_event_candidates" + } + }, + "stale_part_policy": { + "mode": "block", + "allowed_ordinals": [f"{i:03d}" for i in range(1, n + 1)], + "checked_directories": ["evidence_indexed_parts", "evidence_event_candidate_parts"], + "detected_stale_parts": { + "evidence_indexed_parts": stale_b1, + "evidence_event_candidate_parts": stale_b2 + }, + "downstream_gate_instruction": "B1/B2 quality gates must BLOCK if detected_stale_parts is non-empty or if files outside expected_ordinals are present." + }, + "non_legal_assertions": { + "does_not_classify_document_type": True, + "does_not_extract_facts": True, + "does_not_evaluate_evidence": True, + "does_not_calculate_amounts": True, + "does_not_decide_legal_effect": True, + "does_not_finalize_evidence_index": True, + "does_not_finalize_candidate_id": True + } + } + + if stale_b1 or stale_b2: + plan["planner_status"] = "BLOCKED_BY_STALE_PARTS" + plan["mapper_execution_allowed"] = False + plan["blocked_reason"] = "stale mapper part detected; B1/B2 mapper fanout must not execute and existing parts must not be reused" + else: + plan["planner_status"] = "READY" + plan["mapper_execution_allowed"] = True + plan["blocked_reason"] = None + + write_doc("evidence_shard_plan.json", json.dumps(plan, ensure_ascii=False, indent=2)) + # 백엔드 wildcard fan-out 확장이 stdout JSON의 dynamic_fanout 키를 읽음. + # 전체 plan은 너무 커서 dynamic_fanout만 출력한다. 단 planner_status가 READY가 아니면 mapper fanout은 반드시 비운다. + fanout_payload = plan["dynamic_fanout"] if plan["planner_status"] == "READY" else [] + print(json.dumps({"dynamic_fanout": fanout_payload, "planner_status": plan["planner_status"], "mapper_execution_allowed": plan["mapper_execution_allowed"]}, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B1_map_doc_* + max_concurrency: 12 + preflight_files: + - "{{item.shard_doc_path}}" + - client_meeting.md + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + + Task_B1_map_doc + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. Your role in this task is a **per-document mapper** within the v.3 Step D streaming map-reduce pipeline. + + + 본 task는 v.3 Step D의 B1 mapper다. orchestrator 가 runtime parameter (`{{item.*}}`) 로 주입한 assigned_ordinal 단일 증거문서만 처리하여, 그 문서의 authority component·document component envelope·legal component envelope을 추출한 part 파일을 작성한다. + 본 task는 증거 문서 수 N을 계산하지 않고, ordinal을 배정하지 않고, B1/B2 mapper 인스턴스를 생성하지 않는다. 이러한 orchestration 책임은 전부 `Task_Evidence_shard_planner`와 orchestrator의 권한이다. + 최종 `evidence_indexed.json`은 본 task가 작성하지 않는다. 본 task는 `evidence_indexed_parts/E-{ordinal:03d}.json` part 파일만 작성하며, finalize는 `Task_B1_quality_gate_evidence_indexed`가 수행한다. + 본 task는 자기 문서 안에서 직접 확인되는 component만 작성한다. 다른 문서의 사실을 자기 part에 주입하면 quality gate의 authority drift check에서 BLOCK 또는 HARD_WARNING으로 검출된다. + `evidence_index`는 planner가 부여한 `E-{ordinal:03d}` 형식의 deterministic candidate를 그대로 propose 하되, final 부여는 quality gate가 수행한다. 본 task는 candidate id를 자기 part의 `evidence_index_proposed` 필드에 기록한다. + 본 task의 추출 깊이는 v.2 원본 Task_B1과 동일하다. document component envelope의 모든 하위 구조(claim_document_components, business_transfer_components, registry_row_components, secured_debt_components, valuation_rows, mixed_contract_payment_semantics, loan_components, valuation_matrix, registry_row_role_map, defense_assertions, admission_components, defense_components, notice_components, party_assertion_components, schedule_items, registry_object_display, succession_document_components)를 자기 문서 범위 안에서 가능한 한 분리 추출한다. + 변호사 관점에서 본 단계의 핵심은 등기부 row, 차용증 component, 통지서 발송/도달일 분리, 가족관계·상속포기 component, 별지목록 등의 직접 보존이다. 이는 청구원인 timeline·청구취지 별지목록·금액 계산의 기초가 된다. + + + + + + - runtime parameters: `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.source_document_ref}}`, `{{item.b1_output_path}}` (planner 가 부여한 단일 source of truth — 별도 파일 read 불요) + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 문서 1건 — planner가 evidence_all.json에서 분리해 둔 단일 shard 파일) + - `client_meeting.md` (보조 사용만 허용) + + + - `evidence_indexed_parts/E-{ordinal:03d}.json` (본 mapper 인스턴스의 part 파일 1개만) + + + - 다른 ordinal의 문서를 처리하기 + - 다른 ordinal의 사실을 자기 part에 주입하기 (authority drift) + - 증거 문서 수 N을 재계산하여 planner 결과를 대체하기 + - assigned_ordinal을 새로 생성하거나 재정렬하기 + - B1/B2 mapper fan-out 또는 streaming edge를 설계하기 + - 최종 `evidence_indexed.json` 작성 (이는 quality gate의 단독 권한) + - 다른 mapper의 part 파일 읽기/덮어쓰기 + - root 전체 탐색 + - 이전 task 결과 또는 backend 자동 로드 데이터 사용 + - 자기 part 재읽기 + - cross-document legal conclusion 확정 + - 입력에 없는 사실 추가 + + + + + 1. `list_docs`로 `{{item.shard_doc_path}}`, `client_meeting.md` 존재만 확인한다. + 2. `read_docs`로 위 두 파일을 읽는다. `{{item.shard_doc_path}}`는 이 인스턴스의 단일 evidence 문서로 그대로 사용한다 (별도의 ordinal 추출 불필요). assigned_ordinal·evidence_index_candidate·b1_output_path 는 runtime parameter (`{{item.*}}`) 그대로 사용한다. + 3. 메모리에서 part item 객체를 생성한다(items 배열이 아닌 단일 item object 또는 1-element items 형태). + 4. `write_file(overwrite=true)`로 runtime parameter 가 지정한 `evidence_indexed_parts/E-{ordinal:03d}.json`을 저장한다. + 5. `list_docs`로 자기 part 파일 존재만 확인한다. 절대 읽지 않는다. + 6. 정확히 `"E-{ordinal:03d} mapper part 작성 완료"`만 출력하고 종료한다. + + + + - 입력 누락, 빈 파일, runtime parameter 누락, ordinal 매핑 실패, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. part 파일은 쓰지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + - tool로 읽은 내용만 사실로 취급한다. + + + + - 본 mapper 인스턴스는 정확히 runtime parameter (`{{item.assigned_ordinal}}`) 에 해당하는 1개 문서만 처리한다. + - runtime parameters (`{{item.*}}`) 는 ordinal/binding 의 단일 source of truth 다 (별도 plan 파일 read 불요). + - `evidence_all.json` 배열 순서를 절대 바꾸지 않고, 배열 순서를 근거로 새 plan을 만들지도 않는다. + - planner의 `source_document_ref.content_sha256` 또는 `doc_uid_candidate`가 현재 문서와 명백히 충돌하면 part 파일을 쓰지 않는다. + - 본 task는 planner가 확정한 `evidence_index_candidate`, `b1_output_path`, assigned_ordinal을 소비할 뿐 재작성하지 않는다. + - 다른 ordinal의 문서 내용을 현재 문서에 보충하지 않는다(authority drift 금지). + - unknown 처리: + - nullable scalar → `null` + - 배열 → `[]` + - 비nullable string만 `"불명"` 사용 + - 배열은 exact-match 중복 제거 후 최초 순서를 유지한다. + - part 파일은 단일 item object 형태이며, 기존 필수 필드(`evidence_index_proposed`, `doc_uid`, `title`, `title_normalized`, `doc_type`, `key_*`, `source_pointer`)를 모두 포함한다. + - optional additive field는 downstream(quality gate)이 무시해도 안전해야 한다. + - `evidence_index_proposed`는 deterministic 규칙 `E-{ordinal:03d}`로만 생성한다. 임의 변형 금지. + + + + 이 인스턴스의 `{{item.shard_doc_path}}`(planner가 evidence_all.json에서 분리한 단일 evidence 1건) + `client_meeting.md` (보조만 허용) + + + `client_meeting.md`는 아래 목적에만 사용할 수 있다. + - `asset_id` + - `property_label_for_schedule` + - `transaction_type` + - `transaction_date` + - `market_value_at_act` + - `market_value_at_close` + - `consideration_breakdown` + - `property_cluster_id` + - 현재 문서의 축약 명칭이 어떤 자산군을 가리키는지에 관한 shorthand 해석 + + + + - 현재 문서와 `client_meeting.md`가 충돌하면 현재 문서가 우선한다. + - 예외: 등기부상 형식적 원인이 `매매`여도, 현재 문서 특약 또는 `client_meeting.md`가 실질적으로 채무갈음/대물변제를 직접 드러내면 `transaction_type`은 `"대물변제"`로 할 수 있다. + - 단, 이 예외는 현재 문서 또는 meeting이 실질 원인을 직접적으로 드러내는 경우에만 적용한다. + + + + + - `ordinal` = `evidence_all.json`의 1-based 배열 위치 (assigned_ordinal과 동일) + - `evidence_index_proposed` = `E-{ordinal:03d}` (mapper propose; quality gate가 finalize) + - `title` = 현재 문서 제목 원문 그대로 + - `title_normalized` = 앞뒤 공백 제거 + 내부 연속 공백/줄바꿈을 1칸으로 축약 + - `doc_uid` = `DOC-{ordinal:03d}-{title_normalized의 공백을 "_"로 치환한 값}` + - `source_pointer` = `{"source":"evidence_all.json","ordinal":ordinal}` + + + + 다음 순서로만 핵심을 읽는다. + 1. `title` + 2. `heading` + 3. `key_value` / `form` + 4. 관련 `table` 행 + 5. `list` + 6. `paragraph` + + 보일러플레이트(바코드 안내, 발급확인 문구, 반복 footer/page text)는 무시한다. + 단, `관할등기소`, `접수번호`, `등기원인일`, `발행일` 등 필요한 사실이 있으면 사용한다. + + + + `doc_type` enum은 v.2 원본과 동일하다. nuance는 `doc_semantic_flags`로 보강한다. + 1. `판결|결정|배당표|가압류|가처분|경매|타경` → `"판결/결정"` + 2. `등기사항전부증명서|주민등록|가족관계증명서|사업자등록증` → `"공문서"` + 3. `대출|명세|거래|계산서` → `"거래기록"` + 4. `카톡|문자|이메일|통화` → `"통신기록"` + 5. `계약서|약정서|확인서|영수증|차용증|각서` → `"처분문서"` + 6. 그 외 → `"기타"` + + + + - `key_facts`: 최대 3개, 각 60자 이하 + - `key_dates`: 최대 3개 + - `key_amounts`: 최대 3개 + - `key_parties`: 최대 5개 + - 현재 문서에서 직접 읽히는 핵심만 남긴다. + - 장문 요약, 계산식, 추정 보정 금지. + - 가능하면 heading / key_value / 표의 핵심 행만 사용한다. + + + + + - `property_cluster_id`는 optional field다. + - 현재 문서만으로 특정 자산군이 명백하거나, 현재 문서의 표기와 `client_meeting.md`가 정확히 대응할 때만 채운다. + - 예: `"해당 대지/건물"`, `"해당 상가"`, `"해당 주거용 부동산"`, `"해당 임야"`, `"해당 영업양도"`, `"해당 물품대금"`. + - 명백하지 않으면 `null`. + + + + `doc_semantic_flags`는 optional field다. 아래 값만 사용한다. + - `multi_event_doc` + - `registry_doc` + - `mixed_contract_doc` + - `valuation_doc` + - `claim_doc` + - `status_doc` + - `notice_doc` + - `response_doc` + - `business_transfer_doc` + - `supporting_reference_doc` + - `admission_doc` + - `defense_doc` + - `notice_receipt_admission_doc` + - `lien_assertion_doc` + - `schedule_doc` + - `relief_object_reference_doc` + - `succession_doc` + + 부여 규칙: + - 등기부처럼 복수 row가 서로 다른 법적 효과를 가지면 `multi_event_doc` + - 갑구/을구 등기행위가 있으면 `registry_doc` + - 임대차와 매매 등 서로 다른 계약 구성요소가 같은 문서에 공존하면 `mixed_contract_doc` + - 시가, 차임시세, 사용이익 가치가 표로 제시되면 `valuation_doc` + - 차용증, 보증서, 대여약정 등 채권 발생 문서면 `claim_doc` + - 답변서, 사업자등록증, 현재 상태 확인서처럼 상태를 보여주면 `status_doc` + - 내용증명, 통지서, 최고서, 해제통지서면 `notice_doc` + - 상대방 답신, 답변서, 반박문이면 `response_doc` + - 영업양도 문서면 `business_transfer_doc` + - 별지 목록처럼 단독 계산객체는 없으나 다른 문서 식별을 돕는 문서면 `supporting_reference_doc` + - 상대방이 통지 수령, 임대, 차임, 점유, 채무, 변제 등을 인정하면 `admission_doc` + - 명의신탁, 충분담보, 상계, 선의, 후소유자 소멸청구권 부정, 피담보채권 충당 등 항변이 있으면 `defense_doc` + - 유치권 소멸청구 통지의 수령일을 인정하면 `notice_receipt_admission_doc` + - 유치권 주장, 공사잔금, 유치권 목적물 사용·임대가 있으면 `lien_assertion_doc` + - 별지목록, 목적물 목록, 청구취지에서 그대로 사용할 문구가 있으면 `schedule_doc` 및 `relief_object_reference_doc` + - 가족관계증명서, 상속포기 심판문, 사망·상속 관련 공문서이면 `succession_doc` + + + + - 등기부 문서에서 갑구/을구 여러 row가 서로 다른 법적 효과를 가지면 `multi_event_registry=true`를 optional field로 기록한다. + - `multi_event_registry=true`인 문서는 단일 snapshot 요약을 강요하지 않는다. + + + + + - 문서가 여러 법률효과를 담고 있으면 component 단위로 분해한다. + - 날짜는 작성일, 발송일, 도달일, 법률효과 발생일, 변제기, 기산일 후보로 의미별 분리한다. + - 금액은 원금, 이자, 보증금, 차임, 사용이익, 채권최고액, 실제 변제액, 가액배상 공제액으로 성격별 분리한다. + - 아래 component field들은 optional additive field이며, 현재 문서에 직접 읽히는 범위에서만 사용한다. + + + - 문서가 이행각서, 채무승인서, 최고서, 내용증명, 지급요구서, 변제약정서, 차용증, 납품대금 확인서이면 `claim_document_components`를 생성한다. + - 단순 `key_dates`에 문서작성일만 넣고 끝내지 말고, 문서 안에 지급기일, 새 변제기, 최고 도달일, 채무승인일, 청구금액, 채권자, 채무자가 직접 읽히면 component field로 분해한다. + - 이행각서에서 "언제까지 지급한다"는 문구가 있으면 반드시 `new_due_date` 후보로 분리한다. + - 최고서에서 발송일과 수령일/도달일이 모두 있으면 `dispatch_date`와 `arrival_date`를 분리한다. + - claim component의 `component_kind` 허용값은 `underlying_claim|debt_acknowledgement|demand_notice|notice_dispatch|notice_arrival|new_due_date|payment_promise|limitation_relevance|other`다. + + + + - 영업양도계약서, 상호 계속 사용, 사업 양수인, 영업자산 이전, 고객·직원 승계, 채무불승계 조항, 책임배제 등기 단서가 있으면 `business_transfer_components`를 생성한다. + - `business_name`, `transferor_candidates`, `transferee_candidates`, `business_assets_transferred`, `trade_name_continuation_clause`, `customer_or_employee_succession_clause`, `pre_transfer_debt_exclusion_clause`, `liability_exclusion_registration_clause`를 직접 읽히는 범위에서 채운다. + - 기존 `contract_components`가 있어도 영업양도 책임 검토용 구조는 `business_transfer_components`에 별도 보존한다. + + + + - 등기부, 등기사항전부증명서, 등기내역 문서는 반드시 row 단위로 쪼갠다. + - 각 row마다 갑구/을구, 순위번호, 등기목적, 접수일, 접수번호, 원인, 권리자, 채무자, 채권최고액, 객체 범위를 추출하여 `registry_row_components`에 둔다. + - 청구취지 특정에 필요한 접수번호는 `key_facts` 요약에만 두지 말고 `registry_row_components[*].receipt_no`로 보존한다. + - 차용증, 근저당권 설정계약, 등기부 row가 연결되면 `secured_debt_components`를 생성한다. + - 복수 채무의 원금, 이율, 변제기, 담보 지분이 다르면 법정변제충당 계산 대상이므로 모두 구조화한다. + - 사해행위취소 사건의 등기부 row는 필요하면 `registry_row_role_map`에 actio role을 추가로 남긴다. + - `registry_row_components`의 각 row에 `registry_ref`를 부여한다. 형식: `REG-{ordinal:03d}-{row_seq:02d}` (갑구/을구·순위번호 순 deterministic). + - row에 지분 표시(예: "2분의 1", "1/2 지분")가 직접 읽히면 `registry_row_components[*].fraction_display`(원문 문자열)와 `registry_row_components[*].fraction_ref`를 채운다. + - `fraction_ref`는 `FR-{ordinal:03d}-{row_seq:02d}` 형식의 deterministic propose 값이다. 같은 row에서 파생되는 모든 component는 같은 `fraction_ref`를 공유한다. + - 지분 표시의 분자·분모가 직접 읽히는 경우에만 optional 필드 `fraction_numerator`/`fraction_denominator`를 채운다(예: "2분의 1" → 1/2). 환산·약분·보간 금지. 직접 읽히지 않으면 null. + + + + - 감정표는 금액만 추출하지 말고 `valuation_rows`에 row별 전제조건, 금액, 단위, 법률상 사용 가능성을 함께 추출한다. + - 사해행위취소 valuation 문서가 복수 자산 또는 복수 시점을 담으면 `valuation_matrix`를 생성하고, 각 row의 `legal_uses`를 표시한다. + - 임대차보증금, 월 차임, 매매대금이 한 문서에 같이 있으면 `mixed_contract_payment_semantics`에서 각각 별도 payment semantic으로 분리한다. + - 보증금은 원칙적으로 반환 예정 담보성 금원인지 검토하고, 원고가 직접 수령하지 않았다면 원고의 부당이득 원금으로 자동 전환하지 않는다. + - 월 차임은 피고들 사이의 계약상 차임과 객관적 토지 사용이익을 구분한다. + + + + - 사해행위취소 사건의 증거문서는 일반 key_facts 요약만으로 충분하지 않다. + - 차용증에 복수 대여금이 있으면 `loan_components`로 분리하고, 각 원금·변제기·이자율·담보 목적물·청구 내 역할을 추출한다. + - 감정평가서는 시점별 `valuation_matrix`로 보존하고, 각 row의 legal use를 표시한다. + - 등기부는 갑구/을구 row별로 쪼개고, 각 row의 `actio_roles`를 부여한다. + - 내용증명 답신에 명의신탁, 충분담보, 상계, 선의 주장이 있으면 `defense_assertions`로 추출한다. + - 사후 말소된 담보권과 그 말소를 유발한 변제액은 가액배상 계산의 필수 입력으로 표시한다. + + + + - `loan_components`의 각 항목은 아래 named 필드를 가진다. 직접 읽히지 않는 필드는 null. + { + "loan_component_id": "E-{ordinal:03d}-LC-{seq:02d}", + "principal_amount": null, + "loan_date": null, + "due_date": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "rate_text": null, + "rate_source_locator": null, + "interest_accrual_start_hint": null, + "collateral_object_hint": null, + "role_in_claim_hint": null + } + - `agreed_interest_rate`는 원문 그대로의 문자열("연 5%", "월 1%" 등)만 허용한다. + - `agreed_delay_damage_rate`는 약정 지연손해금율이다. 없으면 null로 둔다(법정이율로 보충 금지). + - `rate_text`는 이율 관련 원문 문자열 그대로, `rate_source_locator`는 그 이율이 읽힌 조항 locator(예: "제2조", "특약")다. + - `agreed_interest_rate`/`agreed_delay_damage_rate`는 계산·환산·보간 금지. 원문 문자열만 허용. + - `secured_debt_components`의 각 항목에도 동일한 optional rate 필드(`agreed_interest_rate`, `agreed_delay_damage_rate`, `rate_text`, `rate_source_locator`, `due_date`)를 직접 읽히는 범위에서 둘 수 있다(근저당권 설정계약서·차용증 겸용 문서의 이율 보존용). + - 이율이 문서에 있는데 두 rate 필드가 모두 null이면 self-warning으로 본다. + + + + - 답변서에 통지 수령 인정, 제3자 임대 자인, 월 차임 자인, 유치권자 사용 허용 주장, 후소유자 소멸청구권 부정 주장, 피담보채권 충당 주장이 있으면 `admission_components`와 `defense_components`로 분리한다. + - 통지서에는 `notice_components`를 생성하고, 통지 유형, 발송일, 도달 필요 여부, 수령자, 목적물, 법률효과 후보를 분리한다. + - `notice_components`의 각 항목은 아래 named 필드를 가진다. 직접 읽히지 않는 필드는 null. + { + "notice_id_proposed": "E-{ordinal:03d}-NT-{seq:02d}", + "notice_type": "최고|해제통지|유치권소멸청구통지|기타", + "drafted_date": null, + "dispatch_date": null, + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [], + "receiver_candidates": [], + "target_object_hint": null, + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "legal_effect_candidate_hint": null + } + - 5개 lifecycle 날짜의 의미: `drafted_date`=작성일, `dispatch_date`=발송일, `arrival_date`=도달일(민법 제111조 도달주의 — 도달≠수령을 구분), `received_date`=수령일(수령 인정 포함; 도달일과 다르면 별도 보존), `legal_effect_date_candidate`=법률효과 발생 후보일(계산 금지, 문서 직접 기재만). + - `requires_receipt_for_effect`는 true|false|null — 문서 유형상 도달/수령이 효력요건인지의 후보 표시다. + - `receipt_evidence_refs`는 수령을 뒷받침하는 같은 문서 내 단서 locator다. + - `recipient_admission_text`는 통지 수령을 인정하는 원문 문자열이며, 답변서 문서의 component에서만 채운다. + - 수령일이 상대방 답변서에서 인정되는 경우 그 답변서의 `admission_components`에 남기고, 본 통지서 문서의 `received_date`에 타 문서 사실을 주입하지 않는다(authority drift 금지). cross-document 연결은 Part 2 B5가 수행한다. + - 당사자가 요구한 기간이나 금액이 법률상 기산일로 자동 사용되면 안 되는 경우 `party_assertion_components.should_not_auto_use_as_legal_start_date=true`를 남긴다. + - 별지목록에는 `schedule_items`를 생성하고, 청구취지에서 그대로 사용할 수 있는 문구와 asset alias, 전유부분, 대지권, 도로명주소를 보존한다. + - `schedule_items`의 각 항목에 `schedule_ref`를 부여한다. 형식: `SCH-{ordinal:03d}-{seq:02d}` (별지 기재 순서 deterministic). asset alias 문자열은 기존대로 함께 보존한다. + - `schedule_items`의 각 항목에 optional 필드 `canonical_label_candidate`(별지 표제 문구 원문)와 `usable_in_relief`("yes|no|unknown" — 청구취지에서 그대로 사용할 수 있는 문구인지)를 직접 읽히는 범위에서 둘 수 있다. + - 등기부가 목적물 특정 자료로 쓰이면 `registry_object_display`를 생성한다. + - 가족관계증명서, 상속포기 심판문, 사망·상속 자료에는 `succession_document_components`를 생성하고 피상속인, 사망일, 배우자 후보, 직계비속 후보, 직계존속 후보, 상속포기자, 신고일·수리일, 연결 asset을 보존한다. + + + + + + - 기존 optional field `legal_calculation_object`는 backward-compatible summary field로 유지한다. + - 그러나 이 필드는 안전할 때만 사용한다. + - 문서가 `multi_event_doc` 또는 `multi_event_registry=true`이면 원칙적으로 `legal_calculation_object`를 생성하지 않는다. + - 문서에 서로 다른 법적 효과의 slot이 2개 이상 공존하면 단일 summary `legal_calculation_object`로 압축하지 않는다. + - 단일 효과가 명백한 문서, 또는 서로 다른 값이 경쟁하지 않는 문서에서만 `legal_calculation_object`를 생성한다. + + + + - multi-event 또는 mixed-contract 문서를 위해 optional field `legal_calculation_objects`를 추가할 수 있다. + - `legal_calculation_objects`는 현재 문서 안의 개별 slot을 권위 있게 분해한 배열이다. + - 문서당 0개 이상 허용한다. + - 단일 slot 문서라면 `legal_calculation_objects`를 생략할 수 있다. + - 각 slot은 현재 문서의 직접 기재만 사용한다. 계산, 합산, 차감 금지. + + + + 개별 slot은 아래 경우에 생성한다. + - 소유권이전 / 매매 / 증여 / 대물변제 / 경매매각 / 경매개시결정 / 근저당권설정 / 근저당권말소 / 가압류 / 임대차 / 영업양도 / 대여 / 담보제공 + - valuation 문서는 시가표/차임표/사용이익표를 slot이 아니라 `valuation_object`로 처리한다. + - 등기부는 갑구/을구의 의미 있는 row마다 1 slot씩 생성한다. + - 혼합계약서는 계약 조항상 독립된 법적 효과마다 1 slot씩 생성한다. + + 각 slot은 아래 구조를 따른다. + { + "slot_id": "E-{ordinal:03d}-LCO-01", + "slot_kind": "ownership_transfer|gift|sale|dation|mortgage_setting|mortgage_cancellation|auction_commencement|auction_sale|lease|building_sale|loan|business_transfer|other", + "source_locator": { + "locator_type": "heading|key_value|table_row|paragraph|unknown", + "locator_hint": "갑구4|을구1|제1조|특약 등", + "excerpt": "짧은 단서" + }, + "asset_id": null, + "property_label_for_schedule": null, + "transaction_type": "매매|증여|대물변제|null", + "transaction_date": null, + "registration_date": null, + "registry_office": null, + "registry_receipt_no": null, + "registry_recorded_transfer_date": null, + "market_value_at_act": null, + "market_value_at_close": null, + "sale_price": null, + "monthly_rent": null, + "consideration_breakdown": [], + "parties": [] + } + - slot이 등기부 row 또는 별지 항목에서 직접 파생되면 optional 필드 `registry_ref_hint`/`schedule_ref_hint`를 남긴다. + + + + - 임대차와 매매가 함께 있는 혼합계약서, 영업양도와 채무불인수 조항이 함께 있는 문서 등은 optional field `contract_components`를 추가할 수 있다. + - `contract_components`는 현재 문서의 조항별 법적 효과 분해 메모다. + - 각 component는 아래 구조를 따른다. + { + "component_id": "E-{ordinal:03d}-CC-01", + "component_kind": "lease|sale|delivery|management_use_shift|permit_cooperation|business_transfer|debt_non_assumption|other", + "object_spec": "짧은 목적물", + "amount": null, + "date": null, + "payer_candidates": [], + "recipient_candidates": [], + "possession_recipient_candidates": [], + "benefit_shift_to_candidates": [], + "source_locator": { + "locator_type": "paragraph|table_row|key_value|unknown", + "locator_hint": "제1조|제2조|특약 등", + "excerpt": "짧은 단서" + } + } + - 혼합계약서에서 component가 2개 이상 명백하면 2개 이상 생성한다. + - mixed contract에 임대차, 매매, 인도, 관리·사용 귀속이 직접 적혀 있으면 이를 lease/sale 1개로 뭉개지지 말고 가능한 한 분리한다. + - 조항에서 누가 대가를 지급하고 누가 수령했는지가 직접 읽히면 `payer_candidates`, `recipient_candidates`를 채운다. + - 인도받는 자가 직접 읽히면 `possession_recipient_candidates`를 채운다. + - 관리·사용 또는 사용수익 귀속 주체가 직접 읽히면 `benefit_shift_to_candidates`를 채운다. + - 위 directionality field는 현재 문서에 직접 적힌 경우에만 채우고, 추정으로 보충하지 않는다. + + + + - 시가, 차임시세, 사용이익 평가 문서는 optional field `valuation_object`를 추가할 수 있다. + - `valuation_object`는 아래 구조를 따른다. + { + "valuation_kind": "market_value|rent_value|use_gain_value", + "target_object": "짧은 목적물", + "basis_date_or_period": "원문 기간 또는 기준일", + "value_rows": [ + { + "label": "2022.1.1.~2022.12.31.|보증금 3억 원인 경우 등", + "amount": "금액 문자열", + "unit": "총액|월액|기타" + } + ] + } + - valuation 문서는 기존 `doc_type`를 바꾸지 않고 `doc_semantic_flags`와 `valuation_object`로 구체화한다. + - 차임표/사용이익표가 있으면 `market_value_*`를 억지로 채우지 않는다. + + + + + - 날짜: 정확한 일자가 있으면 `YYYY-MM-DD`, 없으면 `null` + - 단, `key_dates`와 `valuation_object.basis_date_or_period`는 짧은 원문 텍스트 허용 + - 금액: 입력에 명시된 값만 `"300,000,000원"` 형식 문자열로 정규화 + - 이름/당사자: 원문 우선, 명백히 동일할 때만 canonicalize + - `consideration_breakdown`는 직접 읽히는 짧은 대가 항목만 저장, 최대 4개, 각 40자 이하 + + + + part 파일은 단일 item object 형태이며, top-level에 mapper가 propose하는 모든 component field를 둔다. + + { + "evidence_index_proposed": "E-{ordinal:03d}", + "doc_uid": "DOC-{ordinal:03d}-...", + "title": "...", + "title_normalized": "...", + "doc_type": "판결/결정|공문서|거래기록|통신기록|처분문서|기타", + "key_facts": [], + "key_dates": [], + "key_amounts": [], + "key_parties": [], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": ordinal + }, + "property_cluster_id": null, + "doc_semantic_flags": [], + "multi_event_registry": null, + "contract_components": [], + "valuation_object": null, + "legal_calculation_object": null, + "legal_calculation_objects": [], + "claim_document_components": [], + "business_transfer_components": [], + "registry_row_components": [], + "secured_debt_components": [], + "valuation_rows": [], + "mixed_contract_payment_semantics": {}, + "loan_components": [], + "valuation_matrix": {}, + "registry_row_role_map": {}, + "defense_assertions": [], + "admission_components": [], + "defense_components": [], + "notice_components": [], + "party_assertion_components": [], + "schedule_items": [], + "registry_object_display": {}, + "succession_document_components": null, + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } + } + + 추가 제약: + - top-level은 단일 object다. envelope이 아닌 단일 item. + - `evidence_index_proposed`는 mapper의 propose 식별자이며, finalize는 quality gate가 한다. + - `mapper_meta.self_isolation_attested=true`는 본 mapper가 자기 문서 외 사실을 주입하지 않았음을 self-attest 한다는 의미다. + - optional component field는 직접 읽히는 범위에서만 채우고, 빈 배열/객체/`null`로 두는 것을 우선한다. + + + + - part 파일이 단일 object이고 `evidence_index_proposed`, `doc_uid`, `title`, `title_normalized`, `doc_type`, `source_pointer`를 모두 포함하는지 점검한다. + - `planner_status=="READY"` 및 `mapper_execution_allowed=true`가 확인되지 않았으면 part 파일이 작성되지 않았는지 점검한다. + - `evidence_index_proposed`가 정확히 `E-{ordinal:03d}` 형식인지 점검한다. + - 자기 문서 외 ordinal의 사실이 part에 주입되지 않았는지 self-check 한다(authority drift 방지). + - `registry_doc`이면서 서로 다른 법적 효과 row가 2개 이상 읽히면 `doc_semantic_flags`에 `multi_event_doc`를 넣고, 가능한 경우 `legal_calculation_objects`를 생성한다. + - `multi_event_registry=true` 또는 `doc_semantic_flags`에 `multi_event_doc`가 있으면 단일 `legal_calculation_object`를 억지로 채우지 않는다. + - `mixed_contract_doc`인데 명백한 독립 component가 2개 이상이면 `contract_components`를 2개 이상 생성한다. + - `mixed_contract_doc`에 인도 또는 관리·사용 귀속이 직접 적혀 있으면 `delivery` 또는 `management_use_shift` component가 누락되지 않았는지 점검한다. + - 혼합계약 조항에서 지급자·수령자 또는 점유 이전 상대방이 직접 읽히면 `payer_candidates`, `recipient_candidates`, `possession_recipient_candidates`, `benefit_shift_to_candidates`가 비어 있지 않은지 점검한다. + - `valuation_doc`인데 가치표/차임표가 직접 보이면 `valuation_object`를 생성한다. + - 같은 문서에서 복수 행위가 보이는데도 단일 snapshot `legal_calculation_object`만으로 압축하지 않는다. + - `claim_doc`인데 이행각서ㆍ채무승인ㆍ최고ㆍ변제기ㆍ청구금액 단서가 직접 보이고 `claim_document_components`가 없으면 self-warning으로 본다. + - 이행각서에 새 변제기가 있는데 `new_due_date`가 없으면 self-warning으로 본다. + - 최고서 또는 통지서에 도달·수령 단서가 있는데 `arrival_date` 또는 notice receipt component가 없으면 self-warning으로 본다. + - `business_transfer_doc`에 상호ㆍ영업명ㆍ영업자산ㆍ채무불승계 단서가 있는데 `business_transfer_components`가 없으면 self-warning으로 본다. + - 등기부에 접수번호가 있는데 `registry_row_components[*].receipt_no`가 없으면 self-error로 본다. + - 차용금 문서에 이율 또는 변제기가 있는데 `secured_debt_components` 또는 `loan_components`가 없으면 self-warning으로 본다. + - `loan_components`가 있는데 이율 문구가 문서에 보이면서 `agreed_interest_rate`가 null이면 self-warning으로 본다. + - valuation 문서에 복수 시점 또는 복수 row가 있는데 `valuation_rows[*].legal_use_case` 또는 `valuation_matrix.rows[*].legal_uses`가 없으면 self-warning으로 본다. + - 임대차보증금이 추출되었는데 `mixed_contract_payment_semantics`에서 반환성 보증금과 사용이익 원금 배제 여부가 구분되지 않으면 self-warning으로 본다. + - 등기부 문서에 사후 말소 row가 있는데 `registry_row_role_map` 또는 `registry_row_components`에 release role이 없으면 self-error로 본다. + - 답변서에 수령 인정ㆍ제3자 임대ㆍ항변이 있는데 `admission_components` 또는 `defense_components`가 없으면 self-warning으로 본다. + - 별지목록에 목적물 항목이 있는데 `schedule_items`가 없으면 self-error로 본다. + - 상속자료가 있는데 `succession_document_components`가 없으면 self-warning으로 본다. + - self-warning/self-error는 mapper가 part 파일에 그대로 남겨도 되며, quality gate가 최종 severity를 판정한다. + + + + + {{item.assigned_ordinal}} + + + [ ] 1) Preflight: `list_docs` 사용해서 `{{item.shard_doc_path}}`, `client_meeting.md` 만 확인, `read_docs` 사용해서 두 파일만 읽기 + [ ] 2) runtime parameter `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.b1_output_path}}` 그대로 소비. 별도 검증 read 불요. + [ ] 3) `{{item.shard_doc_path}}`의 단일 evidence 문서를 메모리에서 사용하고, 주입된 shard 밖으로 나가지 않기 + [ ] 4) `write_file` 사용해서 `evidence_indexed_parts/E-{ordinal:03d}.json` 생성 (ordinal은 `{{item.assigned_ordinal}}`) + [ ] 5) `list_docs` 사용해서 자기 part 파일만 존재 확인(절대 읽기 금지) + [ ] 6) `"E-{ordinal:03d} mapper part 작성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 1건; planner가 evidence_all.json에서 분리) + - `client_meeting.md` + + + - `evidence_indexed_parts/E-{ordinal:03d}.json` + + + + + - `evidence_index_proposed`는 정확히 `E-{ordinal:03d}` 형식만 허용한다. + - `ordinal`과 output path는 runtime parameter (`{{item.*}}`) 를 그대로 따른다. + - 증거 문서 수 N, assigned_ordinal, mapper fan-out, streaming edge는 본 task에서 만들지 않는다. + - 자기 part 파일은 절대 재읽지 않는다. + - 다른 ordinal 문서를 처리하지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"E-{ordinal:03d} mapper part 작성 완료"` 한 줄만 사용한다. + + + + - task_name: Task_B2_map_events_* + max_concurrency: 12 + preflight_files: + - "{{item.shard_doc_path}}" + - "{{item.b1_output_path}}" + - client_meeting.md + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + + Task_B2_map_events + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. Your role in this task is a **per-document event/state candidate mapper** within the v.3 Step D streaming map-reduce pipeline. + + + 본 task는 v.3 Step D의 B2 mapper다. orchestrator 가 runtime parameter (`{{item.*}}`) 로 주입한 assigned_ordinal 단일 문서의 event/state candidate(raw event + legal-effect event 포함)를 추출하여 part 파일을 작성한다. + 본 mapper 인스턴스는 orchestrator가 같은 ordinal의 B1 mapper(Task_B1_map_doc) 완료와 `evidence_indexed_parts/E-{ordinal:03d}.json` 생성을 확인한 뒤 시작한다. 본 task는 streaming edge를 설계하지 않고, planner/orchestrator가 이미 결정한 same-ordinal edge를 소비한다. + 최종 `evidence_event_candidates.json`은 본 task가 작성하지 않는다. 본 task는 `evidence_event_candidate_parts/E-{ordinal:03d}.json` part 파일만 작성하며, finalize는 `Task_B2_quality_gate_event_candidates`가 수행한다. + 본 task는 같은 ordinal의 B1 part 파일과 planner shard row를 authority metadata로 참조하고, planner가 분리해 둔 `{{item.shard_doc_path}}` 의 raw 문서 1건을 메모리에서 사용한다. + 같은 문서 내 복수 legal-effect event는 분리한다. 문서 밖 사실을 candidate payload에 넣지 않는다(authority drift 금지). + `candidate_id`는 `EVT-{ordinal:03d}-{seq:02d}` 형식의 deterministic candidate로 propose 하되, final 부여 및 collision 처리는 quality gate가 수행한다. + 변호사 관점에서 본 단계의 핵심은: (a) 등기부 갑구/을구 row별 분리, (b) 혼합계약서의 계약성립/대가수수/인도/관리·사용 귀속 4종 분리, (c) 통지 발송일과 도달일의 분리 candidate, (d) 답변서의 admission/defense 분리 candidate, (e) valuation 문서의 시점별/지표별 candidate, (f) 가족관계·상속포기 문서의 succession candidate 분리다. + legal-effect event는 raw event와 함께 추출하며, payment, notice, auction, lien, succession, valuation, defense assertion, commercial successor liability에 대해 v.2 원본의 REQUIRED_EVENT_SUBKINDS를 그대로 따른다. + + + + + + - runtime parameters: `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.b1_output_path}}`, `{{item.b2_output_path}}` (planner 가 부여한 단일 source of truth — 별도 plan 파일 read 불요) + - `{{item.b1_output_path}}` (같은 ordinal 의 B1 mapper part — runtime parameter 그대로 사용) + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 문서 1건; planner가 evidence_all.json에서 분리해 둔 단일 shard 파일) + - `client_meeting.md` (보조 사용만 허용) + + + - `evidence_event_candidate_parts/E-{ordinal:03d}.json` (본 mapper 인스턴스의 part 파일 1개만) + + + - 다른 ordinal의 B1 part 또는 raw 문서 처리 + - 다른 ordinal의 사실을 자기 part에 주입하기 (authority drift) + - 증거 문서 수 N을 재계산하여 planner 결과를 대체하기 + - assigned_ordinal을 새로 생성하거나 재정렬하기 + - B1/B2 mapper fan-out 또는 streaming edge를 설계하기 + - 최종 `evidence_event_candidates.json` 작성 (이는 quality gate의 단독 권한) + - 다른 mapper의 part 파일 읽기/덮어쓰기 + - 자기 part 재읽기 + - 문서 summary를 다시 쓰는 작업으로 변질시키기 + - meeting만으로 문서에 없는 사건행위를 새로 만들기 + - 입력에 없는 날짜, 당사자, 금액, 목적물을 창작하기 + + + + + 1. `list_docs`로 `{{item.b1_output_path}}`, `{{item.shard_doc_path}}`, `client_meeting.md` 존재만 확인한다. + 2. `read_docs`로 위 세 파일을 읽는다. `{{item.shard_doc_path}}`는 이 인스턴스의 단일 evidence 문서로 그대로 사용한다 (별도의 ordinal 추출 불필요). assigned_ordinal·evidence_index_candidate·b2_output_path 는 runtime parameter (`{{item.*}}`) 그대로 사용한다. + 3. 메모리에서 part 객체를 생성한다. 단일 item object 또는 1-element items 형태이며, event 후보가 0건이어도 파일을 생략하지 말고 `zero_event_reason`을 반드시 가진다. + 4. `write_file` 전에 아래 을 수행한다. 불량이면 먼저 내부 보정을 시도한다. + 5. 보정 후에도 schema invariant가 불량이면 part 파일을 쓰지 않고 `FAILED: E-{ordinal:03d} event candidate part schema invariant violation`만 보고하고 종료한다. + 6. 통과한 경우에만 `write_file(overwrite=true)`로 runtime parameter 가 지정한 `evidence_event_candidate_parts/E-{ordinal:03d}.json`을 저장한다. + 7. `list_docs`로 자기 part 파일 존재만 확인한다. 절대 읽지 않는다. + 8. 정확히 `"E-{ordinal:03d} event candidates part 작성 완료"`만 출력하고 종료한다. + + + + - 입력 누락, 빈 파일, runtime parameter 누락, B1 part 부재, ordinal 매핑 실패, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. part 파일은 쓰지 않는다. + - event 후보가 0건인 것은 실패가 아니다. 이 경우에도 runtime parameter 가 지정한 output path에 `zero_event_reason`을 포함한 schema-valid part 파일을 쓴다. + - `event_candidates: []`와 `zero_event_reason` 누락 또는 null이 함께 존재하는 것은 정상 zero-event가 아니라 schema invariant 위반이다. + - part schema invariant 위반이 내부 보정 후에도 남으면 part 파일은 쓰지 않는다. 불완전한 part를 생성하여 quality gate에 넘기지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + + - 본 mapper 인스턴스는 정확히 runtime parameter (`{{item.assigned_ordinal}}`) 에 해당하는 1개 문서만 처리한다. + - runtime parameters (`{{item.*}}`) 는 ordinal/binding 의 단일 source of truth 다 (별도 plan 파일 read 불요). + - B1 part의 `evidence_index_proposed`, `title`, `doc_type`, `source_pointer.ordinal`을 authority로 사용한다. + - `{{item.shard_doc_path}}`의 단일 evidence 문서(planner가 evidence_all.json에서 분리)를 직접 참조하여 candidate를 추출한다. + - 본 task는 planner가 확정한 `evidence_index_candidate`, `b2_output_path`, assigned_ordinal을 소비할 뿐 재작성하지 않는다. + - 다른 ordinal의 사실을 자기 part에 주입하지 않는다(authority drift 금지). + - candidate layer에 머문다. 최종 BO, 최종 JuristicAct, 최종 청구권, 최종 사해행위 판단을 확정하지 않는다. + - 문서 단위 1요약으로 뭉개지 않는다. 한 문서에 복수의 법적 중요 행위가 있으면 반드시 여러 candidate로 분리한다. + - support가 희박한 후보를 억지로 채우지 않는다. 불명확한 필드는 `null` 또는 빈 배열을 사용할 수 있다. + - 잘못된 과잉 특정화보다 보수적 분해가 낫다. 다만 자기 문서 커버리지를 포기해서는 안 된다. + - `candidate_id`는 deterministic 규칙 `EVT-{ordinal:03d}-{seq:02d}`로 생성한다(seq는 자기 part 내 1-based 순번). + + + + 이 인스턴스의 `{{item.shard_doc_path}}`(planner가 evidence_all.json에서 분리한 단일 evidence 1건) + `evidence_indexed_parts/E-{ordinal:03d}.json`의 authority metadata + `client_meeting.md` + + + `client_meeting.md`는 아래 목적에만 사용할 수 있다. + - alias 정규화 + - 동일인 식별 보조 + - property cluster shorthand 해석 + - 현재 문서가 어느 상담 조항을 뒷받침하는지에 대한 `meeting_clause_refs` 보조 + - 현재 문서에서 직접 읽히는 내용의 중요도 보조 + - 사망자와 특정 자산군의 연결이 meeting에서 직접 읽히는 경우 succession 관련 candidate의 `affected_asset_cluster_ids` 보조 + 금지: + - meeting만으로 문서에 없는 사건행위를 candidate로 생성 + - meeting만으로 현재 문서의 당사자/금액/날짜를 보충해 direct evidence처럼 쓰기 + + + + + - 자기 part는 정확히 1개 item을 가진다. + - 그 item은 아래 둘 중 하나를 반드시 가진다. + - `event_candidates`가 1개 이상 + - `zero_event_reason`가 non-null + - `zero_event_reason` 허용값: + - `"NO_LEGAL_EVENT_FOUND"` + - `"REFERENCE_ONLY_DOC"` + - `"PARSE_INSUFFICIENT"` + - `"ORDINAL_MATCH_FAILED"` + - 등기부, 계약서, 통지서, 답변서, 감정/시세 문서처럼 통상 event-bearing 문서에 대해서는 `zero_event_reason`를 매우 예외적으로만 사용한다. + + + + `write_file` 전에 반드시 메모리의 part object를 검증한다. 이 검증은 생성 지점 안전장치이며 자동 강제 재실행을 의미하지 않는다. + + 필수 invariant: + 1. 메모리상 top-level은 단일 item object 또는 1-element `items` envelope일 수 있으나, `write_file` 직전 serialization 대상은 단일 item object여야 하며 실질 item은 정확히 1개다. + 2. item의 `evidence_index_proposed`는 runtime parameter `{{item.evidence_index_candidate}}` 또는 그와 동일한 `E-{ordinal:03d}` 값이어야 한다. + 3. item은 아래 둘 중 하나를 반드시 만족해야 한다. + - `event_candidates`가 non-empty list + - `zero_event_reason`이 non-null이며 허용값 중 하나 + 4. `event_candidates`가 빈 배열이면 `zero_event_reason`은 반드시 non-null 허용값이어야 한다. + 5. candidate가 없어 `zero_event_reason`이 non-null이면 schema 안정성을 위해 `event_candidates`는 빈 배열로 둔다. + 6. candidate가 있으면 각 candidate는 object이며, `candidate_id_proposed`는 `EVT-{ordinal:03d}-{seq:02d}` 형식이고 `source_evidence_index_proposed`는 자기 `evidence_index_proposed`와 일치해야 한다. + + 내부 보정 원칙: + - 문서 검토 결과 법적 중요 event/state 후보가 없는 것이 맞으면 `zero_event_reason`을 허용값 중 하나로 명시한다. + - event/state candidate 생성 조건이 충족되는데 빈 배열이면 extraction routines로 되돌아가 candidate를 재작성한다. + - 통상 event-bearing 문서에 `zero_event_reason`을 사용하는 경우, 원문상 후보 부재 또는 파싱 불충분 이유가 허용값과 맞는지 재확인한다. + + 보정 실패: + - 위 invariant 중 하나라도 충족하지 못하면 `write_file`을 호출하지 않는다. + - 마지막 채팅 출력은 정확히 `FAILED: E-{ordinal:03d} event candidate part schema invariant violation`로 한다. + + + + 아래 범주의 법적 중요 행위를 후보로 추출한다. + - 소유권 이전, 매매, 증여, 대물변제, 처분 + - 근저당/저당/전세권/가압류/압류/담보 설정 및 말소 + - 대여, 보증, 신용보증, 대위변제, 구상권 발생, 변제, 배당 + - 임대차, 점유, 인도, 현재 점유, 현재 사용자, 현재 사용수익, 현재 수익귀속, 현재 권리귀속, 현재 관리 상태 + - 경매신청, 경매개시결정, 경매매각, 인도명령, 집행 관련 사실 + - 통지, 최고, 해제/해지의사표시, 도달, 채무승인, 감액요구, 책임부인, 현재상태 주장 + - 판결, 결정, 지급명령, 배당표 등 소송/집행 관련 법적 중요 사실 + - 신축, 영업양도, 사업자등록, 현재 영업상태 등 사건핵심 상태 형성 행위 + - 위 범주와 직접 연결되는 기타 법적 중요 행위 또는 현재상태 + + 제외: + - 순수 배경설명만 있는 문장 + - 정보 증가 없는 반복 진술 + - 법적 효과와 관련 없는 주변사정 + + + + + - 갑구/을구의 각 의미 있는 row를 개별 candidate로 본다. + - 최소 추출 대상: 소유권보존, 소유권이전, 증여, 매매, 담보권실행을 위한 경매개시결정, 담보권실행을 위한 경매로 인한 매각, 근저당권설정, 근저당권설정등기말소, 가압류. + - `support_locators.locator_hint`에는 가능하면 `갑구4`, `을구1`, `순위번호 3` 같은 row identifier를 남긴다. + - 말소표시가 있더라도 법적 의미가 있으면 candidate로 남길 수 있다. + - row에 지분 표시가 직접 읽히면 candidate에 같은 ordinal B1 part의 `registry_row_components[*].fraction_ref`와 동일한 `fraction_ref`를 남긴다. 지분 표시가 없으면 null. + + + + - 하나의 문서에 서로 다른 계약 요소가 공존하면 clause별로 분해한다. + - 예: 대지 임대차 + 건물 매매 + 인도 + 관리사용 귀속 + 명의변경 협력은 가능한 한 별개 candidate로 분리한다. + - B1 part의 `contract_components`가 있으면 그것을 우선 참고하되, raw 문서 확인을 생략하지 않는다. + - 혼합계약서가 계약성립, 대가수수, 인도, 관리·사용 귀속을 직접 보여주면 아래 후보군을 가능한 한 독립 candidate로 생성한다. + 1. 계약성립 candidate + 2. 대가수수 candidate + 3. 인도 candidate + 4. 관리·사용 또는 현재 수익귀속 state candidate + - 위 4종 중 raw 문서가 직접 보여주는 것은 formation candidate 1개로 뭉개지지 않도록 반드시 분리한다. + - 대가수수 candidate는 적절한 기존 enum이 없으면 `event_kind="기타"`, `event_subkind="대가수수"`로 둘 수 있다. + - 대가수수 candidate에는 가능한 한 지급자, 수령자, 금액, 목적물, 해당 조항 locator를 함께 남긴다. + - 인도 candidate에는 가능한 한 인도자, 인도수령자, 목적물, 개시일, locator를 함께 남긴다. + - 관리·사용 귀속 candidate는 `event_or_state="state"`를 우선 검토하고, `current_state_candidate` 하위 4축과 interval field를 함께 남긴다. + - 문서가 계약일로부터 현재 점유/사용 귀속을 직접 보여주면 `state_start_date`를 그 개시일로 남기고, 뒤에 다른 점유/사용 candidate가 있으면 선행 state를 open-ended로 두지 않는다(같은 문서 내). + + + + - 내용증명, 답신, 답변서, 최고서, 해제통지서, 소멸청구 통지서에서는 아래를 개별 candidate로 검토한다. + - 지급 요구 + - 해제/해지 통지 + - 도달 + - 책임 부인 + - 현재 점유/현재 거주/현재 운영 주장 + - 사용수익 귀속 주장 + - 현재 권리귀속 주장 + - 채무승인 또는 시효항변 주장 + - 감액요구, 명도요구 + - 문서에 현재 상태가 직접 적혀 있으면 `event_or_state="state"` candidate를 생성할 수 있다. + - 현재 상태가 후행 시점에서 선행 상태를 대체하는 구조가 직접 보이면 interval field를 함께 남긴다. + + + + - 감정평가서, 감정평가 의견서, 임료 시세 확인서 등은 `event_candidates`가 0개라고 판단하지 않는다. + - 시가표, 차임표, 사용이익표, 시장가치표가 있으면 최소 1개의 factual/state candidate를 생성한다. + - rent/use-gain valuation은 `action_type_candidate="사실행위(factual acts)"`로 둘 수 있다. + - 문서가 특정 현재 수익귀속자 또는 현재 권리귀속자를 직접 보여주면, 해당 정보는 `current_state_candidate`의 대응 배열에 분리해 둔다. + + + + - 사업자등록증, 가족관계증명서, 현재상태 확인 문서, 심판문 등은 상태형 후보 또는 법적 지위 관련 후보를 생성할 수 있다. + - 예: 사업자등록은 현재 영업상태/점포 운영 개시의 간접 또는 직접 후보가 된다. + - 가족관계증명서, 상속포기 심판문, 상속 관련 공문서가 사망자와 친족구성 또는 상속포기를 직접 보여주면 succession 관련 candidate를 생성한다. + - sparse status 문서라도 later stage linker가 사용할 수 있도록 가능한 범위에서 `participants`, `object_spec`, `event_date` 또는 `current_state_candidate.state_as_of`를 채운다. + - family/renunciation/status 문서를 generic status로만 남기지 않는다. + + + + - 사망, 가족관계, 상속포기, 재산상속회복, 승계 관련 문서는 가능한 한 succession 관련 candidate를 생성한다. + - succession 관련 candidate에는 optional field `affected_asset_cluster_ids`를 둘 수 있다. + - `affected_asset_cluster_ids`는 meeting이나 B1 part metadata가 사망자와 특정 자산군의 연결을 직접 또는 명백하게 보여줄 때만 채운다. + - succession 관련 candidate에는 optional field `succession_candidate`를 둘 수 있다. + { + "deceased_candidate": null, + "spouse_candidates": [], + "lineal_ascendant_candidates": [], + "lineal_descendant_candidates": [], + "renunciant_candidates": [] + } + - 가족관계증명서 같은 status doc은 direct 내용이 보이면 `deceased_candidate`, `spouse_candidates`, `lineal_ascendant_candidates`, `lineal_descendant_candidates`를 가능한 한 채우고, 자산 연결이 직접 또는 명백하면 `affected_asset_cluster_ids`도 함께 채운다. + - 상속포기 문서는 상속포기 그 자체를 direct candidate로 남기되, direct names가 보이면 `succession_candidate.renunciant_candidates`를 반드시 채운다. + - 상속포기 문서가 특정 사망자와 자산 cluster를 직접 또는 명백하게 연결하면 동일한 `affected_asset_cluster_ids`를 함께 남긴다. + - direct names가 있는 succession/status 문서에서는 가능한 범위에서 `participants`와 `object_spec`도 함께 남겨 later stage overlap scorer가 sparse document를 놓치지 않게 한다. + - 이 단계는 최종 상속분, 단독상속, complaint plaintiff narrowing을 확정하지 않는다. 다만 downstream이 succession bundle을 읽을 수 있을 정도의 구조는 반드시 남긴다. + + + + + - raw event는 문서가 직접 보여주는 사실행위·법률행위·준법률행위다. + - legal-effect event는 raw event에서 downstream 청구권 식별에 필요한 법률효과 후보를 도출한 candidate다. + - raw event와 legal-effect event는 같은 문서에서 함께 생성할 수 있으며, 서로 다른 `candidate_id`를 가진다. + - legal-effect event는 최종 법률판단이 아니라 Stage 2가 검토할 구조화된 후보이므로 `confidence`와 `review_flags`를 함께 남긴다. + + + 금전채권: + - `underlying_claim_arising` + - `first_demand` + - `demand_notice_dispatch` + - `demand_notice_arrival` + - `debt_acknowledgement` + - `new_due_date` + - `business_transfer` + - `trade_name_continuation_candidate` + - `commercial_successor_liability_candidate` + + 담보부 부동산: + - `secured_debt_creation` + - `mortgage_setting_by_fraction` + - `undesignated_payment` + - `payment_allocation_effect` + - `mortgage_extinguishment_by_fraction` + - `auction_application_after_extinguishment` + - `auction_sale_and_registry_transfer` + - `derivative_mortgage_setting_by_invalid_title_holder` + - `unregistered_building_original_acquisition` + - `de_facto_disposition_holder_acquisition` + - `land_use_gain_start` + - `liability_overlap_start` + + 사해행위취소: + - `preserved_claim_arising` + - `collateral_setting_for_preserved_claim` + - `fraudulent_transfer` + - `asset_value_at_fraudulent_act` + - `asset_value_at_close_proxy` + - `collateral_insufficiency_at_act` + - `encumbrance_release_after_act` + - `value_compensation_exception_trigger` + - `value_compensation_cap_calculation_candidate` + - `defense_assertion_title_trust` + - `defense_assertion_sufficient_collateral` + - `defense_assertion_setoff` + + 유치권ㆍ상속ㆍ통지: + - `lien_assertion` + - `third_party_lease_by_lien_holder` + - `lien_violation_candidate` + - `lien_extinction_notice_dispatch` + - `lien_extinction_notice_receipt` + - `lien_extinction_effect_candidate` + - `ongoing_physical_possession` + - `succession_renunciation_filing` + - `succession_renunciation_acceptance` + - `spouse_sole_succession_effect_candidate` + - `defense_assertion_subsequent_owner_no_extinction_right` + - `defense_assertion_benefit_applied_to_secured_claim` + + + + - claim_doc 문서가 채무승인, 최고, 변제기, 변제약정, 지급기일을 함께 포함하면 하나의 generic event로 압축하지 말고 legal effect별 candidate를 분리한다. + - 이행각서에는 최소한 `debt_acknowledgement`, `payment_promise`, `new_due_date` 후보가 있는지 검토한다. + - 최고서에는 `demand_notice_dispatch`, `demand_notice_arrival`, `limitation_preservation` 관련 후보가 있는지 검토한다. + - 발송일과 도달일이 다르면 각각 별도 `event_date`로 보존한다. + - 채무승인 event에는 `legal_effect_candidate.acknowledgement_of_debt=true`, `limitation_interruption_candidate=true`, `related_claim_type`을 직접 읽히는 범위에서 남긴다. + + + + - business_transfer_doc 문서는 단순 영업양도 formation event로만 끝내지 않는다. + - 문서 또는 상담내용에서 상호, 영업명, 영업자산, 고객관계, 고용관계, 채무불승계 조항, 책임배제 등기 필요성이 보이면 `commercial_successor_candidates`로 분해한다. + - 상법 제42조 책임을 최종 확정하지는 않되, downstream이 요건판단을 할 수 있도록 transferor, transferee, business_name, trade_name_continuation_candidate, debt_exclusion_clause_candidate를 남긴다. + + + + - 원자료 event가 곧바로 법률효과를 발생시키는 경우 raw event와 legal-effect event를 모두 생성한다. + - 복수 채무에 대한 지정 없는 변제가 등장하면 `undesignated_payment` event와 별도로 `payment_allocation_effect` 후보를 생성한다. + - 등기부상 경매 매각과 후행 근저당권이 등장하면, 그 경매가 어떤 담보권에 기초했는지, 그 담보권의 피담보채무가 이미 소멸했는지 검토하는 event chain을 만든다. + - 미등기 건물 신축일과 미등기 건물 매수일은 점유·사용이익 발생 시점을 정하므로 `land_use_gain_start` 및 `liability_overlap_start` event 후보를 생성한다. + - `mortgage_setting_by_fraction`/`mortgage_extinguishment_by_fraction` 등 `*_by_fraction` subkind candidate는 지분 표시가 문서에 직접 읽히는 한 `fraction_ref`를 반드시 가진다. + + + + - 사해행위취소 사건에서 raw event와 legal-effect event를 구분한다. + - 사해행위 당시 존재하던 담보권이 사후 말소되면 `value_compensation_exception_trigger` event를 생성한다. + - valuation row와 사후 말소 부담, 존속 부담이 모두 있으면 `value_compensation_cap_calculation_candidate`를 생성한다. + - 피고가 항변을 명시한 내용증명은 `defense_assertion_*` event로 만들고, Stage 2 claim description에 반영해야 하는지 표시한다. + + + + - 통지 문서와 답변서가 서로 대응하고, 답변서가 특정 통지의 수령일을 인정하면 별도의 receipt event candidate를 생성한다. + - receipt event candidate는 발송 event와 병합하지 않는다. + - 발송일과 도달일이 다르면 도달일을 법률효과 후보일로 별도 보존한다. + - 문서에 유치권, 공사잔금, 점유, 임대, 직접 거주, 피담보채권 충당, 후소유자 항변이 함께 나타나면 일반 current possession event가 아니라 lien_event 후보군으로 분해한다. + - 제3자 임대는 `lien_violation_candidate`로, 직접 거주는 ongoing use state로, 피담보채권 충당 주장은 `defense_candidate`로, 소멸청구 수령은 `lien_extinction_effect_candidate`로 각각 분리한다. + - 상속포기 수리 event가 있고 배우자와 자녀 전부 상속포기 구조가 직접 보이면 `spouse_sole_succession_effect_candidate`를 생성하되, 최종 소장 원고 확정은 Task_C 이후로 미룬다. + + + + + `action_type_candidate`는 아래 중 1개를 선택한다. + - `"법률행위(legal acts)"` + - `"준법률행위(quasi-legal acts)"` + - `"사실행위(factual acts)"` + - `"위법행위(unlawful acts)"` + - `"소송행위(litigation acts)"` + - `"불명"` + + + + `event_kind`는 아래 값 중 가장 가까운 것을 선택한다. + - `"처분"`, `"소유권이전"`, `"증여"`, `"대물변제"`, `"담보설정"`, `"담보말소"`, `"채권발생"`, `"보증"`, `"대위변제"`, `"변제"`, `"배당"`, `"임대차"`, `"점유"`, `"인도"`, `"가압류/압류"`, `"경매신청"`, `"경매개시"`, `"경매매각"`, `"통지"`, `"해제/해지"`, `"판결/결정"`, `"영업양도"`, `"사업자등록"`, `"신축"`, `"상속개시"`, `"상속포기"`, `"채권관리"`, `"도달"`, `"항변"`, `"가치평가"`, `"법률효과"`, `"기타"` + + 필요하면 optional field `event_subkind`에 더 구체한 라벨(REQUIRED_EVENT_SUBKINDS 참조)을 남길 수 있다. + + + + - `participants.actor_candidates`는 직접 행위를 한 사람/법인 + - `participants.counterparty_candidates`는 직접 상대방 + - `participants.beneficiary_candidates`는 행위로 이익을 직접 취득한 사람/법인 + - `participants.third_party_candidates`는 법원, 피상속인, 기존 권리자 등 제3자 + - `object_spec`는 짧은 목적물/채권/담보/통지대상/상태 대상 명사구 + - `amount`는 현재 candidate와 직접 연결되는 대표 금액 1개만 사용한다 + - state candidate의 현재 점유/사용/수익/권리 귀속은 `participants`에 뭉개지지 않고 `current_state_candidate` 하위 배열에 분리한다 + - succession/status 문서처럼 sparse한 문서라도 direct names와 direct relation이 보이면 `participants`와 `object_spec`를 불필요하게 비워두지 않는다. + - 잘못된 당사자 확정보다 `[]` 또는 `null`이 낫다 + + + + - `event_date`는 가능하면 `YYYY-MM-DD` + - 불명확하면 `null` + - `time_text`는 짧은 원문 표현을 유지할 수 있다 + - `time_precision` 허용값: `"exact"`, `"approximate"`, `"range"`, `"unknown"` + - 현재상태 candidate의 경우 `state_as_of` 개념은 optional `current_state_candidate.state_as_of`에 남기고, `event_date`는 상태 개시 시점이 직접 보일 때만 채운다. + - state interval이 직접 보이면 `current_state_candidate.state_start_date`, `current_state_candidate.state_end_date_candidate`를 함께 채운다. + - 후행 상태가 선행 상태를 대체하는 것이 직접 보이면 `current_state_candidate.superseded_by_candidate_id`를 사용한다. + + + + - optional field `event_or_state`를 추가할 수 있다. 허용값은 `"event"` 또는 `"state"`. + - 문서가 단발 사건이 아니라 `현재까지`, `현재도`, `계속`, `현재 운영`, `현재 점유`, `현재 거주`, `부지로만 사용`, `현재 명의`, `현재 소유`, `차임을 받고 있다` 같은 계속상태를 직접 보여주면 `event_or_state="state"` candidate를 생성할 수 있다. + - state candidate에는 optional field `current_state_candidate`를 둘 수 있다. + { + "state_summary": "짧은 상태 설명", + "state_as_of": "현재|2024-10-31 등", + "state_start_date": null, + "state_end_date_candidate": null, + "superseded_by_candidate_id": null, + "is_ongoing": true, + "current_possessor_candidates": [], + "current_user_candidates": [], + "current_benefit_holder_candidates": [], + "current_right_holder_candidates": [], + "possession_legal_cause_asserted": null, + "possession_legal_cause_asserted_by": [] + } + - `current_state_candidate`는 가능한 한 아래 4축으로 분해한다. + - 점유자 -> `current_possessor_candidates` + - 사용자/운영자 -> `current_user_candidates` + - 차임·사용이익·영업수익 등 현재 수익귀속자 -> `current_benefit_holder_candidates` + - 현재 소유자·등기명의인·권리귀속자 -> `current_right_holder_candidates` + - 같은 사람이 복수 role을 가지면 각 배열에 중복 기재할 수 있다. + - 문서에 직접 읽히지 않는 축은 빈 배열로 둔다. 추정으로 채우지 않는다. + - event candidate와 state candidate는 동일 문서에서 함께 존재할 수 있다. + - 동일 문서에 과거 행위와 현재 상태가 함께 있으면 분리한다. + - 동일 문서에 현재 점유자와 현재 권리귀속자가 다르게 읽히면 하나로 뭉개지지 않도록 분리 기재한다. + - 상태 개시 시점이 직접 보이면 `state_start_date`를 채운다. + - 후행 점유/사용/수익귀속 candidate가 선행 상태를 대체하는 것이 직접 보이면, 선행 state에 `state_end_date_candidate`와 `superseded_by_candidate_id`를 남기고 open-ended current state로 방치하지 않는다. + - `state_end_date_candidate`가 직접 보이면 `is_ongoing=false`를 우선 검토한다. + - 혼합계약서가 인도, 관리·사용 귀속, 사용수익 귀속을 직접 보여주면 그 상대방에 대한 state candidate를 high confidence로 생성할 수 있다. + - raw 문서가 현재 점유, 현재 사용, 현재 수익귀속, 현재 권리귀속을 직접 적시하면 candidate `confidence`는 원칙적으로 `high`를 우선 검토한다. + - meeting 보조나 간접 정황에 크게 의존한 상태 후보는 `medium` 또는 `low`로 둔다. + - `possession_legal_cause_asserted`는 optional 필드다. 허용값: "유치권행사중|임대차|사용대차|물권|동시이행항변|명도후잔류|무권원주장|미상". `possession_legal_cause_asserted_by`는 그 권원을 주장한 주체(원고측/피고측/제3자 이름)이며 문서에 직접 읽힐 때만 채운다. + - 점유 사실과 권원 주장은 같은 candidate 안에서 축을 분리해 남긴다(사실=4축 배열, 권원=`possession_legal_cause_asserted`). 권원 주장이 문서에 없으면 `미상`이 아니라 null로 둔다. + - 서로 다른 문서가 동일 자산에 대해 다른 권원을 주장하는 것은 본 mapper의 검출 대상이 아니다(authority drift 금지). 각 문서의 주장을 그대로 보존한다. + + + + - optional field `meeting_clause_refs`를 추가할 수 있다. + - 형식은 `1-가`, `2-라`, `3-다`, `4-가`, `5-나`처럼 `client_meeting.md`의 본문 번호체계를 사용한다. + - 현재 문서가 특정 상담조항을 직접 뒷받침할 때만 기록한다. + - meeting clause ref는 보조 recall 장치일 뿐, 문서에 없는 사실을 가져오는 수단이 아니다. + + + + 이 단계는 최종 사해행위 판단을 하지 않는다. 다만 후속 단계의 recall 강화를 위해 candidate tag를 남긴다. + `actio_relevance_candidates.candidate_role_tags` 허용값: + - `"사해행위목적물후보"`, `"원고채권후보"`, `"선순위담보후보"`, `"존속담보후보"`, `"임차권후보"`, `"가압류후보"`, `"수익자이익후보"`, `"현재점유후보"`, `"현재사용수익후보"` + + `fraudulent_act_date_candidate`는 다음 경우에만 채운다. + - 현재 candidate가 직접 목적물의 처분, 이전, 이전등기, 매매일, 증여일, 대물변제일, 경매매각일과 연결되는 경우 + - 그 외에는 쓰지 않는다 + + + + downstream Task_C BO와 evidence 기반 candidate가 중복 제거할 수 있도록, 각 candidate마다 아래 정규화 문자열을 생성한다. + + `(event_or_state + event_kind + 핵심 actor + 핵심 counterparty + 핵심 object_spec/amount + 핵심 date_or_state_as_of)` + + 규칙: + - 불명확한 요소는 생략하되, 최소한 `event_or_state` 또는 `event_kind`와 1개 이상의 핵심 요소는 남긴다. + - 중복 제거를 위해 whitespace, 중복 조사, 과도한 수식어는 제거한다. + - title이나 evidence_index는 넣지 않는다. + - event와 state는 같은 signature가 되지 않도록 구분한다. + - mapper가 propose하는 candidate_id는 `EVT-{ordinal:03d}-{seq:02d}`이며 quality gate가 collision을 처리한다. + + + + part 파일은 단일 item object 형태이며, top-level item은 non-empty `event_candidates` 또는 non-null `zero_event_reason` 중 하나를 반드시 가진다. `event_candidates: []` 단독은 금지된다. + + { + "evidence_index_proposed": "E-{ordinal:03d}", + "title": "문서 제목", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": ordinal + }, + "property_cluster_id": null, + "doc_semantic_flags": [], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-{ordinal:03d}-01", + "event_or_state": "event|state", + "event_kind": "소유권이전", + "event_subkind": null, + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "짧은 1문장 요약", + "source_evidence_index_proposed": "E-{ordinal:03d}", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [], + "counterparty_candidates": [], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": null, + "fraction_ref": null, + "amount": null, + "event_date": null, + "event_end_date": null, + "time_text": null, + "time_precision": "exact|approximate|range|unknown", + "location": null, + "legal_keywords": [], + "support_locators": [ + { + "locator_type": "heading|key_value|table_row|paragraph|caption|unknown", + "locator_hint": "갑구4|제2조 등", + "excerpt": "짧은 단서", + "directness": "직접|간접|불명" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "string|null", + "related_claim_type": "string|null", + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "state|점유|...", + "confidence": "high|medium|low|unknown" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } + } + + 추가 제약: + - top-level은 단일 object다. envelope이 아닌 단일 item. + - `event_candidates`가 빈 배열이면 `zero_event_reason`은 반드시 non-null 허용값이어야 한다. + - `event_candidates: []`와 `zero_event_reason` 누락/null이 결합된 part는 작성 금지되는 schema invariant violation이다. + - `candidate_id_proposed`는 mapper의 propose 식별자(`EVT-{ordinal:03d}-{seq:02d}`)이며, finalize는 quality gate가 한다. + - `source_evidence_index_proposed`는 mapper가 propose 한 자기 ordinal의 candidate evidence_index다. + - `mapper_meta.self_isolation_attested=true`는 본 mapper가 자기 문서 외 사실을 주입하지 않았음을 self-attest 한다는 의미다. + + + + - `write_file` 전 을 수행하고, 불량이면 내부 보정을 먼저 시도했는지 점검한다. + - 자기 part object가 단일 item이고 `evidence_index_proposed`, non-empty `event_candidates` 또는 non-null `zero_event_reason`을 가지는지 점검한다. + - `event_candidates: []`와 `zero_event_reason` 누락/null이 함께 있으면 내부 보정 대상이며, 보정 불가 시 part 파일을 쓰지 않고 FAILED로 종료한다. + - `planner_status=="READY"` 및 `mapper_execution_allowed=true`가 확인되지 않았으면 part 파일이 작성되지 않았는지 점검한다. + - 모든 candidate의 `candidate_id_proposed`가 `EVT-{ordinal:03d}-{seq:02d}` 형식인지 점검한다. + - 자기 문서 외 ordinal의 사실이 part에 주입되지 않았는지 self-check 한다(authority drift 방지). + - `registry_doc` 또는 `multi_event_doc`인데 `event_candidates`가 0개면 다시 점검한다. + - `mixed_contract_doc`인데 서로 다른 법적 효과가 2개 이상 보이면 candidate도 최소 2개 이상 검토한다. + - 혼합계약서가 계약성립, 대가지급·수령, 인도, 관리·사용 귀속을 직접 보여주면 formation candidate만 남기지 말고 consideration/delivery/state candidate가 함께 검토되었는지 점검한다. + - 문서가 위 네 요소를 모두 직접 보여주면 최소 3개 이상, 가능하면 4개까지 독립 candidate로 분리되었는지 점검한다. + - `valuation_doc`인데 가치표/차임표가 직접 보이면 candidate를 최소 1개 생성한다. + - 문서가 상담일지의 명시 조항을 직접 뒷받침하는데도 `meeting_clause_refs`가 전혀 없으면 다시 점검한다. + - event와 state는 구분해 생성한다. 현재상태를 과거 단발행위로 뭉개지 않는다. + - state candidate에서 현재 점유자/사용자/수익귀속자/권리귀속자가 직접 읽히는데도 하나의 participant 배열로 뭉개졌다면 다시 점검한다. + - 현재 점유자와 현재 권리귀속자가 다르게 읽히는 문서는 `current_state_candidate` 하위 배열에서 분리되었는지 다시 점검한다. + - 상태 개시와 종료가 직접 보이거나 후행 상태가 선행 상태를 대체하는 것이 직접 보이면 `state_start_date`, `state_end_date_candidate`, `superseded_by_candidate_id` 검토가 이루어졌는지 점검한다. + - 가족관계증명서가 사망자와 친족구성을 직접 보여주면 `succession_candidate.deceased_candidate`, `spouse_candidates`, `lineal_descendant_candidates` 또는 `lineal_ascendant_candidates`가 필요한 범위에서 채워졌는지 점검한다. + - 상속포기 심판문이 direct names를 보여주면 `succession_candidate.renunciant_candidates`가 비어 있지 않은지 점검한다. + - 가족관계증명서, 상속포기 심판문, 상속 관련 공문서가 사망자와 특정 자산군의 연결을 직접 또는 명백하게 보여주면 `affected_asset_cluster_ids` 검토가 이루어졌는지 점검한다. + - succession/status 문서처럼 sparse한 문서라도 direct names, direct relation, direct date가 보이는데 `participants`, `object_spec`, `event_date` 또는 `state_as_of`가 불필요하게 비어 있지 않은지 점검한다. + - raw 문서가 현재상태를 직접 적시한 경우 candidate `confidence`가 불필요하게 low로 떨어지지 않았는지 점검한다. + - claim doc에 채무승인ㆍ최고ㆍ변제기 재설정 단서가 있는데 `legal_effect_candidate`가 있는 event가 전혀 없으면 self-warning으로 본다. + - 이행각서에 채무승인일과 새 변제기가 함께 있는데 `debt_acknowledgement`와 `new_due_date`가 분리되지 않으면 self-warning으로 본다. + - business transfer 문서에 상호속용 후보 event 또는 `commercial_successor_candidates`가 없으면 self-warning으로 본다. + - 복수 피담보채무와 변제가 있는데 `payment_allocation_effect`가 없으면 self-warning으로 본다. + - 경매 event가 있는데 기초 담보권 및 피담보채무 chain과 연결되지 않으면 `AUCTION_BASIS_CHAIN_MISSING` self-warning으로 본다. + - 미등기 건물 매수 event가 있는데 원시취득 event가 없으면 `UNREGISTERED_BUILDING_ORIGINAL_ACQUISITION_MISSING` self-warning으로 본다. + - 사후 담보권 말소 row가 있는데 `value_compensation_exception_trigger` event가 없으면 self-error로 본다. + - valuation matrix와 released encumbrance가 있는데 `value_compensation_cap_calculation_candidate`가 없으면 self-warning으로 본다. + - defense assertion event가 있는데 `defense_candidate.requires_rebuttal_in_stage2` 검토가 없으면 self-warning으로 본다. + - 유치권 소멸 통지서와 수령 인정 답변서가 있는데 도달 event 또는 `lien_extinction_notice_receipt`가 없으면 self-error로 본다. + - 제3자 임대 event가 있는데 `lien_violation_candidate`가 없으면 self-warning으로 본다. + - 상속포기 수리 event가 있는데 `spouse_sole_succession_effect_candidate` 후보가 없으면 self-warning으로 본다. + - self-warning/self-error는 mapper가 part 파일에 그대로 남겨도 되며, quality gate가 최종 severity를 판정한다. + + + + + {{item.assigned_ordinal}} + + + [ ] 1) Preflight: `list_docs` 사용해서 `{{item.b1_output_path}}`, `{{item.shard_doc_path}}`, `client_meeting.md` 만 확인. `read_docs` 사용해서 동일 파일만 읽기 + [ ] 2) runtime parameter `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.b1_output_path}}`, `{{item.b2_output_path}}` 그대로 소비. 별도 검증 read 불요. + [ ] 3) `{{item.shard_doc_path}}`의 단일 evidence 문서를 메모리에서 사용하고, 같은 ordinal의 B1 part(`{{item.b1_output_path}}`)만 authority metadata로 사용 + [ ] 4) `write_file` 전 수행. 불량이면 내부 보정 시도 + [ ] 5) 보정 후에도 불량이면 part 파일을 쓰지 않고 `FAILED: E-{ordinal:03d} event candidate part schema invariant violation` 출력 후 종료 + [ ] 6) 통과한 경우에만 `write_file` 사용해서 `evidence_event_candidate_parts/E-{ordinal:03d}.json` 생성. event 후보가 0건이어도 non-null `zero_event_reason` part 파일을 작성 + [ ] 7) `list_docs` 사용해서 자기 part 파일만 존재 확인(절대 읽기 금지) + [ ] 8) `"E-{ordinal:03d} event candidates part 작성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `{{item.b1_output_path}}` (같은 ordinal 의 B1 part) + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 1건; planner가 evidence_all.json에서 분리) + - `client_meeting.md` + + + - `evidence_event_candidate_parts/E-{ordinal:03d}.json` + + + + + - `candidate_id_proposed`는 정확히 `EVT-{ordinal:03d}-{seq:02d}` 형식만 허용한다. + - `ordinal`과 output path는 runtime parameter (`{{item.*}}`) 를 그대로 따른다. + - 증거 문서 수 N, assigned_ordinal, mapper fan-out, streaming edge는 본 task에서 만들지 않는다. + - 자기 part 파일은 절대 재읽지 않는다. + - 다른 ordinal 문서를 처리하지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"E-{ordinal:03d} event candidates part 작성 완료"` 한 줄만 사용한다. + + + + - task_name: Task_B1_quality_gate_evidence_indexed + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 240 + code: | + #!/usr/bin/env python3 + # Task_B1_quality_gate_evidence_indexed (v3) - deterministic B1 gate reducer (GA-1) + # Spec: Part_1_Improvement_Strategy_Claude_v1.md §4 + from __future__ import annotations + import itertools + import json + import re + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + + PLAN_PATH = "evidence_shard_plan.json" + PART_DIR = "evidence_indexed_parts" + FINAL_PATH = "evidence_indexed.json" + MANIFEST_PATH = "stage1_tmp/quality_gate/finalized_evidence_id_manifest.json" + PRECHECK_PATH = "stage1_tmp/quality_gate/B1_precheck.json" + EXCERPT_LIMIT = 1500 + EVIDENCE_ID_RE = re.compile(r"^E-\d{3}$") + WARNING_KEY_RE = re.compile(r"warn|review|risk|note", re.IGNORECASE) + DRIFT_TEXT_RE = re.compile(r"drift|출처|다른\s*문서|타\s*문서", re.IGNORECASE) + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-b1-quality-gate-evidence-indexed", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _mid(), + "method": "tools/call", + "params": {"name": name, "arguments": args}, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: Any, name: str) -> str: + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError(f"Empty: {name}") + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, dict) and "results" in outer: + result0 = (outer.get("results") or [{}])[0] + inner = result0.get("content") or result0.get("text") or "" + if not inner: + raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer if isinstance(outer, str) else json.dumps(outer, ensure_ascii=False) + + def read_raw(path: str) -> str: + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str) or not raw.strip(): + raise RuntimeError(f"Missing or empty required input: {path}") + return raw + + def parse_json(raw: str, path: str) -> Any: + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + try: + obj, _ = decoder.raw_decode(raw.strip()) + return obj + except Exception as exc: + raise RuntimeError(f"Invalid JSON input: {path}") from exc + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def clip(value: Any, limit: int = 200) -> str: + text = " ".join(str(value or "").split()) + if len(text) <= limit: + return text + return text[:limit].rstrip() + "..." + + def list_part_names(folder: str) -> list[str] | None: + # planner-compatible list_docs enumeration; None => enumeration unavailable (recorded, non-fatal) + for args in ({"path": folder, "pattern": "E-*.json"}, {}, {"doc_names": []}): + try: + payload = _unwrap(_call("list_docs", args), "list_docs") + except Exception: + continue + if isinstance(payload, str): + try: + payload = json.loads(payload) + except Exception: + return re.findall(r"[\w./-]+\.json", payload) + vals: list[str] = [] + if isinstance(payload, dict): + for value in payload.values(): + if isinstance(value, list): + for item in value: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + elif isinstance(value, str): + vals.append(value) + elif isinstance(payload, list): + for item in payload: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + return [v for v in vals if v] + return None + + def get_single_item(part: Any, path: str) -> dict[str, Any]: + if isinstance(part, dict) and isinstance(part.get("items"), list): + items = part["items"] + if len(items) != 1 or not isinstance(items[0], dict): + raise RuntimeError(f"part must contain exactly one item object: {path}") + return items[0] + if isinstance(part, dict): + return part + raise RuntimeError(f"part top-level is not an object: {path}") + + def nonempty(value: Any) -> bool: + return value not in (None, "", [], {}) + + def flags_of(item: dict[str, Any]) -> list[str]: + return [str(f) for f in (item.get("doc_semantic_flags") or []) if f] + + def norm_title(value: Any) -> str: + return re.sub(r"\s+", "", str(value or "")).lower() + + # v.2 CHECK_4 rule table: (rule_id, flag token, required-nonempty fields (any-of)) + COVERAGE_RULES = ( + ("registry_rows", "registry", ("registry_row_components",)), + ("claim_components", "claim", ("claim_document_components",)), + ("schedule_items", "schedule", ("schedule_items",)), + ("business_transfer", "business_transfer", ("business_transfer_components",)), + ("valuation", "valuation", ("valuation_object", "valuation_rows")), + ("succession", "succession", ("succession_document_components",)), + ("defense", "defense", ("defense_assertions", "defense_components", "admission_components")), + ) + + def main() -> None: + _init() + plan = parse_json(read_raw(PLAN_PATH), PLAN_PATH) + if not isinstance(plan, dict): + raise RuntimeError("evidence_shard_plan.json is not an object") + planner_status = plan.get("planner_status") + if planner_status not in (None, "READY"): + print(json.dumps({"status": "FAILED", "gate": "B1", + "message": "B1 quality gate BLOCK 발생: downstream 진행 금지", + "reason": f"planner_status is not READY: {planner_status}"}, ensure_ascii=False)) + raise RuntimeError(f"planner_status is not READY: {planner_status}") + + expected_ordinals = [str(o) for o in (plan.get("expected_ordinals") or [])] + if not expected_ordinals: + raise RuntimeError("plan.expected_ordinals is empty") + expected_ids = [str(x) for x in (plan.get("expected_evidence_ids") or [f"E-{o}" for o in expected_ordinals])] + shard_count = int(plan.get("shard_count") or len(expected_ordinals)) + stale_policy = (plan.get("stale_part_policy") or {}).get("detected_stale_parts") or {} + if isinstance(stale_policy, dict): + stale_parts = [str(x) for x in (stale_policy.get(PART_DIR) or [])] + else: + stale_parts = [str(x) for x in (stale_policy or [])] + shard_paths: dict[str, str] = {} + for sh in plan.get("shards") or []: + if isinstance(sh, dict) and sh.get("assigned_ordinal") is not None: + try: + shard_paths[f"{int(sh['assigned_ordinal']):03d}"] = str(sh.get("shard_doc_path") or "") + except Exception: + continue + + hard_findings: list[dict[str, Any]] = [] + + def hard(issue_type: str, detail: str, **extra: Any) -> None: + hard_findings.append({"issue_type": issue_type, "detail": clip(detail), **extra}) + + # -------- CHECK 1: shard completeness (deterministic) -------- + if stale_parts: + hard("stale_part", f"stale parts detected by planner policy: {stale_parts}") + missing_ordinals: list[str] = [] + parse_failures: list[dict[str, str]] = [] + parts: dict[str, dict[str, Any]] = {} + for o in expected_ordinals: + path = f"{PART_DIR}/E-{o}.json" + try: + raw = read_raw(path) + except Exception: + missing_ordinals.append(o) + continue + try: + parts[o] = get_single_item(parse_json(raw, path), path) + except Exception as exc: + parse_failures.append({"ordinal": o, "reason": clip(exc)}) + if missing_ordinals or parse_failures: + # FAILURE_POLICY: input-level failure -> report only, write no final files + # SKILL.md §3.3: stdout = task output — 단일 JSON 라인만 출력한다 + print(json.dumps({ + "status": "FAILED", + "gate": "B1", + "message": "B1 quality gate BLOCK 발생: downstream 진행 금지", + "missing_ordinals": missing_ordinals, + "parse_failures": parse_failures, + }, ensure_ascii=False)) + raise RuntimeError("B1 input parts missing or unparsable; final files not written") + extra_parts: list[str] = [] + listed = list_part_names(PART_DIR) + extra_detection = "done" if listed is not None else "unavailable" + if listed is not None: + pat = re.compile(r"(?:^|/)E-(\d{3})\.json$") + expected_set = set(expected_ordinals) + for name in listed: + m = pat.search(str(name)) + if m and m.group(1) not in expected_set: + extra_parts.append(str(name)) + if extra_parts: + hard("unexpected_ordinal", f"unexpected part files outside planner plan: {sorted(set(extra_parts))}") + + # -------- CHECK 2: evidence index determinism (deterministic) -------- + determinism_violations: list[dict[str, str]] = [] + for o in expected_ordinals: + proposed = str(parts[o].get("evidence_index_proposed") or "") + if not EVIDENCE_ID_RE.match(proposed) or proposed != f"E-{o}": + determinism_violations.append({"ordinal": o, "proposed": proposed}) + if determinism_violations: + hard("evidence_index_format_mismatch", f"evidence_index_proposed violations: {determinism_violations}") + + # -------- CHECK 3: duplicate document alias (deterministic exact + suspect pairs) -------- + suspects: list[dict[str, Any]] = [] + dropped_aliases: list[dict[str, Any]] = [] + duplicate_groups: list[dict[str, Any]] = [] + canonical_choices: list[dict[str, Any]] = [] + # 자동 dedup은 강한 동시 일치(v.2 CHECK_3의 "동시 일치성")에서만 수행한다. + # 제목만 같은 서로 다른 문서(예: 서로 다른 채무의 차용증 2건)는 절대 자동 삭제하지 않는다 — R-ALIAS suspect로만 등록. + exact_groups: dict[tuple[str, str], list[str]] = {} + for o in expected_ordinals: + item = parts[o] + uid = str(item.get("doc_uid") or "") + tnorm = str(item.get("title_normalized") or "") + if uid: + exact_groups.setdefault(("doc_uid", uid), []).append(o) + if tnorm: + joint = json.dumps([tnorm, item.get("key_dates"), item.get("key_amounts"), item.get("key_parties")], + ensure_ascii=False, sort_keys=True) + exact_groups.setdefault(("joint_identity", joint), []).append(o) + dropped_set: set[str] = set() + for (kind, value), ords in sorted(exact_groups.items()): + ords = sorted(set(ords)) + if len(ords) < 2: + continue + canonical = ords[0] # v.2 canonical rule (i): smaller ordinal + duplicate_groups.append({"match_kind": kind, "match_value": value, "ordinals": ords}) + canonical_choices.append({"match_value": value, "canonical_ordinal": canonical}) + for dropped in ords[1:]: + if dropped in dropped_set: + continue + dropped_set.add(dropped) + dropped_aliases.append({ + "dropped_ordinal": dropped, + "dropped_evidence_index": f"E-{dropped}", + "canonical_ordinal": canonical, + "canonical_evidence_index": f"E-{canonical}", + "reason": f"exact {kind} match", + }) + suspects.append({ + "exception_id": f"B1-SUS-{len(suspects) + 1:03d}", + "source_gate": "B1", + "rule_id": "R-ALIAS", + "check_id": "duplicate_document_alias", + "ordinals": ords, + "evidence_indexes": [f"E-{o}" for o in ords], + "candidate_ids": [], + "payload": {"confirmed_exact": True, "match_kind": kind, "canonical_ordinal": canonical}, + "excerpt": "", + "escalation_flag": False, + }) + # near-duplicate suspect pairs (not dropped) + seen_pairs: set[tuple[str, str]] = set() + ords_list = [o for o in expected_ordinals if o not in dropped_set] + for i, oa in enumerate(ords_list): + for ob in ords_list[i + 1:]: + a, b = parts[oa], parts[ob] + ta, tb = str(a.get("title_normalized") or ""), str(b.get("title_normalized") or "") + joint_identical = ( + ta == tb + and a.get("key_dates") == b.get("key_dates") + and a.get("key_amounts") == b.get("key_amounts") + and a.get("key_parties") == b.get("key_parties") + ) + near_title = bool(ta and tb and norm_title(ta) == norm_title(tb) and not joint_identical) + triple = ( + nonempty(a.get("key_dates")) and a.get("key_dates") == b.get("key_dates") + and nonempty(a.get("key_amounts")) and a.get("key_amounts") == b.get("key_amounts") + and nonempty(a.get("key_parties")) and a.get("key_parties") == b.get("key_parties") + and ta != tb + ) + if (near_title or triple) and (oa, ob) not in seen_pairs: + seen_pairs.add((oa, ob)) + suspects.append({ + "exception_id": f"B1-SUS-{len(suspects) + 1:03d}", + "source_gate": "B1", + "rule_id": "R-ALIAS", + "check_id": "duplicate_document_alias", + "ordinals": [oa, ob], + "evidence_indexes": [f"E-{oa}", f"E-{ob}"], + "candidate_ids": [], + "payload": { + "confirmed_exact": False, + "titles": [ta, tb], + "key_dates": [a.get("key_dates"), b.get("key_dates")], + "key_amounts": [a.get("key_amounts"), b.get("key_amounts")], + "key_parties": [a.get("key_parties"), b.get("key_parties")], + }, + "excerpt": "", + "escalation_flag": False, + }) + + # -------- CHECK 4: document component coverage (deterministic rule table -> suspects) -------- + missing_components: list[dict[str, Any]] = [] + for o in ords_list: + item = parts[o] + fl = flags_of(item) + for rule_id, token, fields in COVERAGE_RULES: + if any(token in f for f in fl) and not any(nonempty(item.get(f)) for f in fields): + missing_components.append({"ordinal": o, "rule": rule_id, "fields": list(fields)}) + suspects.append({ + "exception_id": f"B1-SUS-{len(suspects) + 1:03d}", + "source_gate": "B1", + "rule_id": "R-COVERAGE", + "check_id": "document_component_coverage", + "ordinals": [o], + "evidence_indexes": [f"E-{o}"], + "candidate_ids": [], + "payload": { + "coverage_rule": rule_id, + "doc_type": item.get("doc_type"), + "doc_semantic_flags": fl, + "empty_fields": list(fields), + "key_facts": item.get("key_facts"), + }, + "excerpt": "", + "escalation_flag": False, + }) + notice_flag = any("notice" in f for f in fl) + ncomps = item.get("notice_components") or [] + if notice_flag and (not ncomps or all( + not nonempty(n.get("dispatch_date")) and not nonempty(n.get("arrival_date")) + for n in ncomps if isinstance(n, dict) + )): + missing_components.append({"ordinal": o, "rule": "notice_dates", "fields": ["notice_components"]}) + suspects.append({ + "exception_id": f"B1-SUS-{len(suspects) + 1:03d}", + "source_gate": "B1", + "rule_id": "R-COVERAGE", + "check_id": "document_component_coverage", + "ordinals": [o], + "evidence_indexes": [f"E-{o}"], + "candidate_ids": [], + "payload": {"coverage_rule": "notice_dates", "notice_components": ncomps, "key_dates": item.get("key_dates")}, + "excerpt": "", + "escalation_flag": False, + }) + loans = [ln for ln in (item.get("loan_components") or []) if isinstance(ln, dict)] + if loans and all( + not nonempty(ln.get("agreed_interest_rate")) and not nonempty(ln.get("agreed_delay_damage_rate")) + for ln in loans + ): + missing_components.append({"ordinal": o, "rule": "loan_rates", "fields": ["loan_components"]}) + suspects.append({ + "exception_id": f"B1-SUS-{len(suspects) + 1:03d}", + "source_gate": "B1", + "rule_id": "R-COVERAGE", + "check_id": "document_component_coverage", + "ordinals": [o], + "evidence_indexes": [f"E-{o}"], + "candidate_ids": [], + "payload": {"coverage_rule": "loan_rates", "loan_components": loans}, + "excerpt": "", + "escalation_flag": False, + }) + + # -------- CHECK 5: authority drift (demoted to suspicion-signal spot-check) -------- + review_candidates: list[dict[str, Any]] = [] + drift_signal_ordinals: list[dict[str, Any]] = [] + for o in ords_list: + item = parts[o] + fl = flags_of(item) + signals: list[str] = [] + if not nonempty(item.get("key_dates")) and not nonempty(item.get("key_amounts")) and any( + t in f for f in fl for t in ("registry", "claim", "notice") + ): + signals.append("empty_key_dates_and_amounts_for_core_doc_type") + sp = item.get("source_pointer") + if isinstance(sp, dict) and sp.get("ordinal") is not None: + try: + if int(sp["ordinal"]) != int(o): + signals.append("source_pointer_ordinal_mismatch") + except Exception: + signals.append("source_pointer_ordinal_unparsable") + mm = item.get("mapper_meta") or {} + for k, v in (mm.items() if isinstance(mm, dict) else []): + if WARNING_KEY_RE.search(str(k)) and nonempty(v): + review_candidates.append({"ordinal": o, "evidence_index": f"E-{o}", "field": str(k), "value": v}) + if DRIFT_TEXT_RE.search(json.dumps(v, ensure_ascii=False)): + signals.append(f"mapper_self_warning:{k}") + if signals: + excerpt = "" + shard_path = shard_paths.get(o) or "" + if shard_path: + try: + excerpt = read_raw(shard_path)[:EXCERPT_LIMIT] + except Exception: + excerpt = "" + drift_signal_ordinals.append({"ordinal": o, "signals": signals}) + suspects.append({ + "exception_id": f"B1-SUS-{len(suspects) + 1:03d}", + "source_gate": "B1", + "rule_id": "R-DRIFT", + "check_id": "authority_drift_detection", + "ordinals": [o], + "evidence_indexes": [f"E-{o}"], + "candidate_ids": [], + "payload": { + "signals": signals, + "source_pointer": sp, + "key_dates": item.get("key_dates"), + "key_amounts": item.get("key_amounts"), + "doc_semantic_flags": fl, + }, + "excerpt": excerpt, + "escalation_flag": "source_pointer_ordinal_mismatch" in signals, + }) + + # -------- finalize & merge (conservative pass-through; v.2 CONSOLIDATION_RULES) -------- + final_items: list[dict[str, Any]] = [] + for o in expected_ordinals: + if o in dropped_set: + continue + item = parts[o] + out: dict[str, Any] = {} + placed = False + for k, v in item.items(): + if k == "evidence_index_proposed": + out["evidence_index"] = f"E-{o}" + placed = True + elif k == "mapper_meta": + continue + else: + out[k] = v + if not placed: + out = {"evidence_index": f"E-{o}", **out} + final_items.append(out) + + envelope = {"schema_contract_version": "evidence_indexed.v3", "items": final_items} + # STEP 0-3 확정: 하류(Part 2/3/4)는 items 키를 우선 소비 — 중복 alias 배열은 두지 않는다 (Q-3 해소). + + # -------- conservation invariants (pre-write) -------- + conservation_errors: list[str] = [] + if len(final_items) != shard_count - len(dropped_set): + conservation_errors.append(f"item count {len(final_items)} != shard_count {shard_count} - dropped {len(dropped_set)}") + final_ids = [it["evidence_index"] for it in final_items] + want_ids = [i for i in expected_ids if i not in {f"E-{o}" for o in dropped_set}] + if set(final_ids) != set(want_ids) or len(final_ids) != len(set(final_ids)): + conservation_errors.append("final evidence_index set mismatch vs planner expected ids minus dropped aliases") + if sorted(final_ids) != final_ids: + conservation_errors.append("final items not sorted by ordinal") + if conservation_errors: + print(json.dumps({"status": "FAILED", "gate": "B1", + "message": "B1 quality gate BLOCK 발생: downstream 진행 금지", + "conservation_errors": conservation_errors}, ensure_ascii=False)) + raise RuntimeError("B1 conservation invariant violated; final file not written") + + content = json.dumps(envelope, ensure_ascii=False, indent=2) + # post-serialization verification (write 전·후 동일 검증 - 자기 파일 재읽기 없이 문자열 기준) + verify = json.loads(content) + if len(verify["items"]) != len(final_items) or [i["evidence_index"] for i in verify["items"]] != final_ids: + raise RuntimeError("post-serialization verification failed for evidence_indexed.json") + write_doc(FINAL_PATH, content) + + manifest = { + "schema_version": "finalized_evidence_id_manifest.v1", + "source_task": "Task_B1_quality_gate_evidence_indexed", + "shard_count": shard_count, + "finalized_evidence_indexes": final_ids, + "dropped_aliases": dropped_aliases, + } + write_doc(MANIFEST_PATH, json.dumps(manifest, ensure_ascii=False, indent=2)) + + block = bool(hard_findings) + completeness_sev = "BLOCK" if (stale_parts or extra_parts) else "PASS" + determinism_sev = "BLOCK" if determinism_violations else "PASS" + precheck = { + "schema_version": "B1_precheck.v1", + "gate_id": "B1_evidence_indexed", + "planner_source": PLAN_PATH, + "planner_shard_count": shard_count, + "expected_ordinals": expected_ordinals, + "received_ordinals": expected_ordinals, + "input_parts_count": len(expected_ordinals), + "extra_part_detection": extra_detection, + "deterministic_checks": [ + {"check_id": "shard_completeness", "severity": completeness_sev, + "detail": "expected/received/stale set 비교 (결정적)", "missing_ordinals": missing_ordinals}, + {"check_id": "evidence_index_determinism", "severity": determinism_sev, + "detail": "E-{ordinal:03d} 형식·ordinal 일치 검사 (결정적)", "violations": determinism_violations}, + {"check_id": "duplicate_document_alias", "severity": "PASS", + "detail": "exact 중복은 canonical 정리, 유사쌍은 GB 판정 대기", "duplicate_groups": duplicate_groups, + "canonical_choices": canonical_choices}, + {"check_id": "document_component_coverage", "severity": "PASS", + "detail": "rule table 위반은 GB 판정 대기", "missing_components": missing_components}, + {"check_id": "authority_drift_detection", "severity": "PASS", + "detail": "상시 전역 검사 삭제, 의심신호 spot-check", "drift_findings": drift_signal_ordinals}, + ], + "hard_gate_findings": hard_findings, + "review_candidates": review_candidates, + "suspects": suspects, + "dropped_aliases": dropped_aliases, + "conservation": {"input_parts": len(expected_ordinals), "final_items": len(final_items), "dropped": len(dropped_set)}, + "block": block, + } + write_doc(PRECHECK_PATH, json.dumps(precheck, ensure_ascii=False, indent=2)) + + overall = "BLOCK" if block else "PASS" + # SKILL.md §3.3: stdout = task output — 단일 JSON 라인. 상태 메시지는 message 필드에 보존. + message = ("B1 quality gate BLOCK 발생: downstream 진행 금지" if block + else f"증거문서 authority catalog finalize 완료 (severity={overall})") + print(json.dumps({ + "status": "B1_DETERMINISTIC_GATE_DONE", + "message": message, + "overall_severity_provisional": overall, + "pending_adjudication": "PENDING_ADJUDICATION" if suspects else "NO_SUSPECTS", + "final_items": len(final_items), + "dropped_aliases": len(dropped_aliases), + "suspect_count": len(suspects), + "review_candidate_count": len(review_candidates), + }, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B2_quality_gate_event_candidates + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 240 + code: | + #!/usr/bin/env python3 + # Task_B2_quality_gate_event_candidates (v3) - deterministic B2 gate reducer (GA-2) + # Spec: Part_1_Improvement_Strategy_Claude_v1.md §5 + from __future__ import annotations + import itertools + import json + import re + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + + PLAN_PATH = "evidence_shard_plan.json" + PART_DIR = "evidence_event_candidate_parts" + FINAL_PATH = "evidence_event_candidates.json" + MANIFEST_PATH = "stage1_tmp/quality_gate/finalized_evidence_id_manifest.json" + B1_PRECHECK_PATH = "stage1_tmp/quality_gate/B1_precheck.json" + PRECHECK_PATH = "stage1_tmp/quality_gate/B2_precheck.json" + PACK_PATH = "stage1_tmp/quality_gate/B12_exception_pack.json" + EXCERPT_LIMIT = 1500 + MAX_ITEMS_PER_EXCEPTION = 2 + MAX_CANDIDATES_PER_EXCEPTION = 8 + CANDIDATE_ID_RE = re.compile(r"^EVT-(\d{3})-(\d{2})$") + DATE_RE = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-b2-quality-gate-event-candidates", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _mid(), + "method": "tools/call", + "params": {"name": name, "arguments": args}, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: Any, name: str) -> str: + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError(f"Empty: {name}") + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, dict) and "results" in outer: + result0 = (outer.get("results") or [{}])[0] + inner = result0.get("content") or result0.get("text") or "" + if not inner: + raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer if isinstance(outer, str) else json.dumps(outer, ensure_ascii=False) + + def read_raw(path: str) -> str: + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str) or not raw.strip(): + raise RuntimeError(f"Missing or empty required input: {path}") + return raw + + def parse_json(raw: str, path: str) -> Any: + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + try: + obj, _ = decoder.raw_decode(raw.strip()) + return obj + except Exception as exc: + raise RuntimeError(f"Invalid JSON input: {path}") from exc + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def clip(value: Any, limit: int = 200) -> str: + text = " ".join(str(value or "").split()) + if len(text) <= limit: + return text + return text[:limit].rstrip() + "..." + + def list_part_names(folder: str) -> list[str] | None: + for args in ({"path": folder, "pattern": "E-*.json"}, {}, {"doc_names": []}): + try: + payload = _unwrap(_call("list_docs", args), "list_docs") + except Exception: + continue + if isinstance(payload, str): + try: + payload = json.loads(payload) + except Exception: + return re.findall(r"[\w./-]+\.json", payload) + vals: list[str] = [] + if isinstance(payload, dict): + for value in payload.values(): + if isinstance(value, list): + for item in value: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + elif isinstance(value, str): + vals.append(value) + elif isinstance(payload, list): + for item in payload: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + return [v for v in vals if v] + return None + + def get_single_item(part: Any, path: str) -> dict[str, Any]: + if isinstance(part, dict) and isinstance(part.get("items"), list): + items = part["items"] + if len(items) != 1 or not isinstance(items[0], dict): + raise RuntimeError(f"part must contain exactly one item object: {path}") + return items[0] + if isinstance(part, dict): + return part + raise RuntimeError(f"part top-level is not an object: {path}") + + def nonempty(value: Any) -> bool: + return value not in (None, "", [], {}) + + def flags_of(item: dict[str, Any]) -> list[str]: + return [str(f) for f in (item.get("doc_semantic_flags") or []) if f] + + def parse_date(value: Any) -> str | None: + m = DATE_RE.match(str(value or "").strip()) + return f"{m.group(1)}-{m.group(2)}-{m.group(3)}" if m else None + + def confidence_rank(value: Any) -> int: + order = {"high": 0, "medium": 1, "mid": 1, "low": 2} + return order.get(str(value or "").lower(), 3) + + def canonical_rank(cand: dict[str, Any], ordinal: str) -> tuple[int, int, int, int]: + # v.2 CHECK_3 canonical priority: (i) raw event > derived legal-effect event, + # (ii) higher confidence, (iii) more support_locators, (iv) earlier ordinal + kind_text = " ".join(str(cand.get(k) or "") for k in ("event_kind", "event_or_state", "action_type_candidate")).lower() + derived = 1 if "derived" in kind_text or "legal_effect" in kind_text else 0 + return (derived, confidence_rank(cand.get("confidence")), -len(cand.get("support_locators") or []), int(ordinal)) + + # v.2 CHECK_4 chronological chain rule table: (earlier subkind, later subkind, allow_equal) + CHRONO_RULES = ( + ("underlying_claim_arising", "first_demand", False), + ("first_demand", "demand_notice_dispatch", False), + ("demand_notice_dispatch", "demand_notice_arrival", True), + ("demand_notice_arrival", "debt_acknowledgement", False), + ("lien_extinction_notice_dispatch", "lien_extinction_notice_receipt", True), + ("lien_extinction_notice_receipt", "lien_extinction_effect_candidate", False), + ("secured_debt_creation", "mortgage_setting_by_fraction", False), + ("mortgage_setting_by_fraction", "auction_application_after_extinguishment", False), + ("auction_application_after_extinguishment", "auction_sale_and_registry_transfer", False), + ("preserved_claim_arising", "fraudulent_transfer", False), + ("fraudulent_transfer", "encumbrance_release_after_act", False), + ) + + def is_defense_relay(cand: dict[str, Any]) -> bool: + # 7/10 실측에서 확인된 mapper 항변 신호의 결정적 재현 규칙 (R-SELFWARN) + if nonempty(cand.get("defense_candidate")): + return True + if "defense_assertion" in str(cand.get("event_subkind") or ""): + return True + lec = cand.get("legal_effect_candidate") + if isinstance(lec, dict) and str(lec.get("effect_type") or "") == "defense_assertion": + return True + return False + + def main() -> None: + _init() + plan = parse_json(read_raw(PLAN_PATH), PLAN_PATH) + if not isinstance(plan, dict): + raise RuntimeError("evidence_shard_plan.json is not an object") + if plan.get("planner_status") not in (None, "READY"): + print(json.dumps({"status": "FAILED", "gate": "B2", + "message": "B2 quality gate BLOCK 발생: downstream 진행 금지", + "reason": f"planner_status is not READY: {plan.get('planner_status')}"}, ensure_ascii=False)) + raise RuntimeError(f"planner_status is not READY: {plan.get('planner_status')}") + + expected_ordinals = [str(o) for o in (plan.get("expected_ordinals") or [])] + if not expected_ordinals: + raise RuntimeError("plan.expected_ordinals is empty") + expected_ids = [str(x) for x in (plan.get("expected_evidence_ids") or [f"E-{o}" for o in expected_ordinals])] + shard_count = int(plan.get("shard_count") or len(expected_ordinals)) + stale_policy = (plan.get("stale_part_policy") or {}).get("detected_stale_parts") or {} + if isinstance(stale_policy, dict): + stale_parts = [str(x) for x in (stale_policy.get(PART_DIR) or [])] + else: + stale_parts = [str(x) for x in (stale_policy or [])] + + manifest = parse_json(read_raw(MANIFEST_PATH), MANIFEST_PATH) + finalized_ids = {str(x) for x in (manifest.get("finalized_evidence_indexes") or [])} + if not finalized_ids: + raise RuntimeError("finalized_evidence_id_manifest.json has no finalized ids (B1 final absent)") + b1_dropped_ordinals = {str(d.get("dropped_ordinal")) for d in (manifest.get("dropped_aliases") or []) if isinstance(d, dict)} + + b1_precheck = parse_json(read_raw(B1_PRECHECK_PATH), B1_PRECHECK_PATH) + b1_suspects = list(b1_precheck.get("suspects") or []) + b1_review_candidates = list(b1_precheck.get("review_candidates") or []) + + hard_findings: list[dict[str, Any]] = [] + + def hard(issue_type: str, detail: str, **extra: Any) -> None: + hard_findings.append({"issue_type": issue_type, "detail": clip(detail), **extra}) + + # -------- schema invariant gate (v.2 PART_ITEM_SCHEMA_INVARIANT_GATE, deterministic) -------- + schema_invariant_findings: list[dict[str, Any]] = [] + + def invariant_block(ordinal: str, reason: str) -> None: + schema_invariant_findings.append({ + "ordinal": ordinal, + "part_path": f"{PART_DIR}/E-{ordinal}.json", + "reason": clip(reason), + "severity": "BLOCK", + "rerun_target_task": f"Task_B2_map_events_{ordinal}", + }) + + if stale_parts: + hard("stale_part", f"stale parts detected by planner policy: {stale_parts}") + missing_ordinals: list[str] = [] + parse_failures: list[dict[str, str]] = [] + parts: dict[str, dict[str, Any]] = {} + for o in expected_ordinals: + path = f"{PART_DIR}/E-{o}.json" + try: + raw = read_raw(path) + except Exception: + missing_ordinals.append(o) + continue + try: + parts[o] = get_single_item(parse_json(raw, path), path) + except Exception as exc: + parse_failures.append({"ordinal": o, "reason": clip(exc)}) + if missing_ordinals or parse_failures: + # SKILL.md §3.3: stdout = task output — 단일 JSON 라인만 출력한다 + print(json.dumps({ + "status": "FAILED", + "gate": "B2", + "message": "B2 quality gate BLOCK 발생: downstream 진행 금지", + "missing_ordinals": missing_ordinals, + "parse_failures": parse_failures, + }, ensure_ascii=False)) + raise RuntimeError("B2 input parts missing or unparsable; final files not written") + extra_parts: list[str] = [] + listed = list_part_names(PART_DIR) + extra_detection = "done" if listed is not None else "unavailable" + if listed is not None: + pat = re.compile(r"(?:^|/)E-(\d{3})\.json$") + expected_set = set(expected_ordinals) + for name in listed: + m = pat.search(str(name)) + if m and m.group(1) not in expected_set: + extra_parts.append(str(name)) + if extra_parts: + hard("unexpected_ordinal", f"unexpected part files outside planner plan: {sorted(set(extra_parts))}") + + for o in expected_ordinals: + item = parts[o] + proposed = str(item.get("evidence_index_proposed") or item.get("evidence_index") or "") + if proposed != f"E-{o}" or proposed not in set(expected_ids): + invariant_block(o, f"evidence_index_proposed mismatch: {proposed}") + candidates = item.get("event_candidates") + zero_reason = item.get("zero_event_reason") + has_candidates = isinstance(candidates, list) and len(candidates) > 0 + if not has_candidates and not nonempty(zero_reason): + invariant_block(o, "event_candidates empty without non-null zero_event_reason") + if has_candidates: + for c in candidates: + if not isinstance(c, dict): + invariant_block(o, "event candidate is not an object") + continue + for req in ("candidate_id_proposed", "source_evidence_index_proposed", "identity_signature"): + if not nonempty(c.get(req)): + invariant_block(o, f"candidate missing required field: {req}") + + # -------- CHECK 2: evidence reference integrity (manifest set membership) -------- + reference_violations: list[dict[str, str]] = [] + for o in expected_ordinals: + for c in parts[o].get("event_candidates") or []: + if not isinstance(c, dict): + continue + ref = str(c.get("source_evidence_index_proposed") or "") + if ref not in finalized_ids: + reference_violations.append({"ordinal": o, "candidate_id": str(c.get("candidate_id_proposed")), "ref": ref}) + if reference_violations: + hard("invalid_evidence_reference", f"candidate references outside finalized evidence id set: {reference_violations}") + + # -------- CHECK 3: candidate id format/uniqueness + exact identity_signature dedup -------- + format_violations: list[dict[str, str]] = [] + for o in expected_ordinals: + seen_seq: set[str] = set() + for c in parts[o].get("event_candidates") or []: + if not isinstance(c, dict): + continue + cid = str(c.get("candidate_id_proposed") or "") + m = CANDIDATE_ID_RE.match(cid) + if not m or m.group(1) != o: + format_violations.append({"ordinal": o, "candidate_id": cid, "reason": "format or ordinal mismatch"}) + elif cid in seen_seq: + format_violations.append({"ordinal": o, "candidate_id": cid, "reason": "duplicate seq in ordinal"}) + else: + seen_seq.add(cid) + if format_violations: + hard("candidate_id_format_violation", f"candidate_id_proposed violations: {format_violations}") + + by_signature: dict[str, list[tuple[str, dict[str, Any]]]] = {} + for o in expected_ordinals: + for c in parts[o].get("event_candidates") or []: + if isinstance(c, dict) and nonempty(c.get("identity_signature")): + by_signature.setdefault(str(c["identity_signature"]), []).append((o, c)) + dedup_findings: list[dict[str, Any]] = [] + dropped_candidate_keys: set[tuple[str, str]] = set() + for sig, entries in sorted(by_signature.items()): + cross = {o for o, _ in entries} + if len(entries) < 2 or len(cross) < 2: + continue # cross-document duplicates only (v.2 CHECK_3) + ranked = sorted(entries, key=lambda oc: canonical_rank(oc[1], oc[0])) + keep_o, keep_c = ranked[0] + dropped = [] + for o, c in ranked[1:]: + dropped_candidate_keys.add((o, str(c.get("candidate_id_proposed")))) + dropped.append({ + "ordinal": o, + "candidate_id": str(c.get("candidate_id_proposed")), + "source_evidence_index": str(c.get("source_evidence_index_proposed")), + "dropped_candidate": c, + }) + dedup_findings.append({ + "identity_signature": sig, + "kept_ordinal": keep_o, + "kept_candidate_id": str(keep_c.get("candidate_id_proposed")), + "dropped": dropped, + }) + + # -------- suspects (semantic exceptions only; GB adjudicates) -------- + suspects: list[dict[str, Any]] = [] + + def add_suspect(rule_id: str, check_id: str, ordinals: list[str], candidate_ids: list[str], + payload: dict[str, Any], escalation: bool = False, excerpt: str = "") -> None: + suspects.append({ + "exception_id": f"B2-SUS-{len(suspects) + 1:03d}", + "source_gate": "B2", + "rule_id": rule_id, + "check_id": check_id, + "ordinals": sorted(set(ordinals)), + "evidence_indexes": [f"E-{o}" for o in sorted(set(ordinals))], + "candidate_ids": candidate_ids[:MAX_CANDIDATES_PER_EXCEPTION], + "payload": payload, + "excerpt": excerpt[:EXCERPT_LIMIT], + "escalation_flag": escalation, + }) + + # CHECK 4: chronological ordering — deterministic date-reversal scan on explicit chain rules + subkind_dates: dict[str, list[tuple[str, str, str | None]]] = {} + for o in expected_ordinals: + for c in parts[o].get("event_candidates") or []: + if not isinstance(c, dict): + continue + sk = str(c.get("event_subkind") or "") + if sk: + subkind_dates.setdefault(sk, []).append((o, str(c.get("candidate_id_proposed")), parse_date(c.get("event_date")))) + ordering_suspect_pairs: list[dict[str, Any]] = [] + null_date_chain_refs: list[dict[str, Any]] = [] + for earlier, later, allow_equal in CHRONO_RULES: + for oe, ce, de in subkind_dates.get(earlier, []): + for ol, cl, dl in subkind_dates.get(later, []): + if de is None or dl is None: + continue + if (de > dl) or (de == dl and not allow_equal and earlier != later and False): + if de > dl: + ordering_suspect_pairs.append({ + "rule": f"{earlier} < {later}" + ("=" if allow_equal else ""), + "earlier": {"ordinal": oe, "candidate_id": ce, "event_date": de}, + "later": {"ordinal": ol, "candidate_id": cl, "event_date": dl}, + }) + for sk in (earlier, later): + for o, cid, d in subkind_dates.get(sk, []): + if d is None: + null_date_chain_refs.append({"ordinal": o, "candidate_id": cid, "event_subkind": sk}) + if ordering_suspect_pairs: + pairs = ordering_suspect_pairs[:MAX_CANDIDATES_PER_EXCEPTION] + add_suspect("R-CHRONO", "chronological_ordering", + [p["earlier"]["ordinal"] for p in pairs] + [p["later"]["ordinal"] for p in pairs], + [p["earlier"]["candidate_id"] for p in pairs] + [p["later"]["candidate_id"] for p in pairs], + {"reversal_pairs": pairs, "total_pairs": len(ordering_suspect_pairs)}, escalation=True) + dedup_null_refs = [] + seen_null = set() + for r in null_date_chain_refs: + key = (r["ordinal"], r["candidate_id"]) + if key not in seen_null: + seen_null.add(key) + dedup_null_refs.append(r) + if dedup_null_refs: + add_suspect("R-CHRONO", "chronological_ordering", + [r["ordinal"] for r in dedup_null_refs][:MAX_CANDIDATES_PER_EXCEPTION], + [r["candidate_id"] for r in dedup_null_refs][:MAX_CANDIDATES_PER_EXCEPTION], + {"null_or_unparsable_dates_in_chain": dedup_null_refs[:MAX_CANDIDATES_PER_EXCEPTION], + "total": len(dedup_null_refs)}, escalation=False) + + # CHECK 5: legal-effect event coverage — REQUIRED_EVENT_SUBKINDS rule table + all_subkinds: set[str] = set(subkind_dates.keys()) + global_text = json.dumps({o: parts[o] for o in expected_ordinals}, ensure_ascii=False) + + def part_subkinds(o: str) -> set[str]: + return {str(c.get("event_subkind") or "") for c in parts[o].get("event_candidates") or [] if isinstance(c, dict)} + + for o in expected_ordinals: + fl = flags_of(parts[o]) + sks = part_subkinds(o) + if any("business_transfer" in f for f in fl) and "commercial_successor_liability_candidate" not in sks: + add_suspect("R-EVTCOV", "legal_effect_event_coverage", [o], + [str(c.get("candidate_id_proposed")) for c in (parts[o].get("event_candidates") or [])[:MAX_CANDIDATES_PER_EXCEPTION] if isinstance(c, dict)], + {"coverage_rule": "business_transfer_without_successor_liability", "doc_semantic_flags": fl}) + if any("claim" in f for f in fl) and not sks & {"debt_acknowledgement", "new_due_date", "demand_notice_dispatch", "demand_notice_arrival"}: + kw = json.dumps(parts[o], ensure_ascii=False) + if any(t in kw for t in ("변제기", "최고", "채무승인", "이행각서")): + add_suspect("R-EVTCOV", "legal_effect_event_coverage", [o], + [str(c.get("candidate_id_proposed")) for c in (parts[o].get("event_candidates") or [])[:MAX_CANDIDATES_PER_EXCEPTION] if isinstance(c, dict)], + {"coverage_rule": "claim_doc_without_demand_or_acknowledgement_subkinds", "doc_semantic_flags": fl}) + if ({"encumbrance_release_after_act", "mortgage_extinguishment_by_fraction"} & all_subkinds) and "value_compensation_exception_trigger" not in all_subkinds: + add_suspect("R-EVTCOV", "legal_effect_event_coverage", + sorted({o for sk in ("encumbrance_release_after_act", "mortgage_extinguishment_by_fraction") for o, _, _ in subkind_dates.get(sk, [])}), + [], {"coverage_rule": "post_act_release_without_value_compensation_exception_trigger"}) + if "auction_sale_and_registry_transfer" in all_subkinds and not {"auction_application_after_extinguishment", "mortgage_extinguishment_by_fraction"} <= all_subkinds: + add_suspect("R-EVTCOV", "legal_effect_event_coverage", + sorted({o for o, _, _ in subkind_dates.get("auction_sale_and_registry_transfer", [])}), + [], {"coverage_rule": "auction_chain_incomplete", + "missing": sorted({"auction_application_after_extinguishment", "mortgage_extinguishment_by_fraction"} - all_subkinds)}) + if "미등기" in global_text and not {"unregistered_building_original_acquisition", "de_facto_disposition_holder_acquisition"} & all_subkinds: + add_suspect("R-EVTCOV", "legal_effect_event_coverage", [], [], + {"coverage_rule": "unregistered_building_cue_without_acquisition_subkinds"}) + if "lien_extinction_notice_dispatch" in all_subkinds and "lien_extinction_notice_receipt" not in all_subkinds: + add_suspect("R-EVTCOV", "legal_effect_event_coverage", + sorted({o for o, _, _ in subkind_dates.get("lien_extinction_notice_dispatch", [])}), + [], {"coverage_rule": "lien_notice_dispatch_without_receipt"}) + succession_flag_ords = [o for o in expected_ordinals if any("succession" in f for f in flags_of(parts[o]))] + if succession_flag_ords and not {"succession_renunciation_acceptance", "spouse_sole_succession_effect_candidate"} & all_subkinds: + add_suspect("R-EVTCOV", "legal_effect_event_coverage", succession_flag_ords[:MAX_ITEMS_PER_EXCEPTION], [], + {"coverage_rule": "succession_doc_without_succession_subkinds"}) + if ({"fraudulent_transfer"} <= all_subkinds and "encumbrance_release_after_act" in all_subkinds + and "value_compensation_cap_calculation_candidate" not in all_subkinds): + add_suspect("R-EVTCOV", "legal_effect_event_coverage", [], [], + {"coverage_rule": "fraudulent_transfer_with_release_but_no_cap_calculation"}) + fraction_refs = [] + for o in expected_ordinals: + for c in parts[o].get("event_candidates") or []: + if isinstance(c, dict) and str(c.get("event_subkind") or "") in ("mortgage_setting_by_fraction", "mortgage_extinguishment_by_fraction") and not nonempty(c.get("fraction_ref")): + fraction_refs.append({"ordinal": o, "candidate_id": str(c.get("candidate_id_proposed"))}) + if fraction_refs: + add_suspect("R-EVTCOV", "legal_effect_event_coverage", + [r["ordinal"] for r in fraction_refs][:MAX_CANDIDATES_PER_EXCEPTION], + [r["candidate_id"] for r in fraction_refs][:MAX_CANDIDATES_PER_EXCEPTION], + {"coverage_rule": "fraction_subkind_with_null_fraction_ref", "refs": fraction_refs[:MAX_CANDIDATES_PER_EXCEPTION]}) + + # R-SELFWARN: mapper 항변/재반박 신호의 승격 판정 대상 (7/10 review finding 재현 경로) + defense_refs: list[dict[str, str]] = [] + for o in expected_ordinals: + for c in parts[o].get("event_candidates") or []: + if isinstance(c, dict) and is_defense_relay(c): + defense_refs.append({"ordinal": o, "candidate_id": str(c.get("candidate_id_proposed")), + "event_subkind": str(c.get("event_subkind") or ""), + "summary": clip(c.get("action_summary"), 120)}) + if defense_refs: + add_suspect("R-SELFWARN", "review_findings_in_mapper", + [r["ordinal"] for r in defense_refs], + [r["candidate_id"] for r in defense_refs], + {"relay_kind": "defense_assertion_requires_stage2_review", "refs": defense_refs}) + + # -------- finalize & merge (dict-copy pass-through; Q-1/Q-2 구조적 해소) -------- + block_ordinals = {f["ordinal"] for f in schema_invariant_findings} + final_items: list[dict[str, Any]] = [] + dropped_by_b1_alias: list[str] = [] + input_candidate_total = 0 + final_candidate_total = 0 + field_count_errors: list[str] = [] + for o in expected_ordinals: + item = parts[o] + input_candidate_total += len(item.get("event_candidates") or []) + if o in b1_dropped_ordinals: + dropped_by_b1_alias.append(o) + continue + if o in block_ordinals: + # schema invariant BLOCK part는 정상 item으로 승격하지 않는다 (명시적 BLOCK finding 대응) + continue + out: dict[str, Any] = {} + placed = False + for k, v in item.items(): + if k == "evidence_index_proposed": + out["evidence_index"] = f"E-{o}" + placed = True + elif k == "mapper_meta": + continue + elif k == "event_candidates": + new_cands = [] + for c in v or []: + if not isinstance(c, dict): + continue + if (o, str(c.get("candidate_id_proposed"))) in dropped_candidate_keys: + continue + nc: dict[str, Any] = {} + for ck, cv in c.items(): + if ck == "candidate_id_proposed": + nc["candidate_id"] = str(cv) + elif ck == "source_evidence_index_proposed": + nc["source_evidence_index"] = str(cv) + else: + nc[ck] = cv + if len(nc) != len(c): + field_count_errors.append(f"{o}:{c.get('candidate_id_proposed')} field count {len(nc)} != {len(c)}") + new_cands.append(nc) + final_candidate_total += 1 + out["event_candidates"] = new_cands + else: + out[k] = v + if not placed: + out = {"evidence_index": f"E-{o}", **out} + final_items.append(out) + + envelope = {"schema_version": "evidence_event_candidates.v1", "items": final_items} + + # -------- conservation invariants (write 전) -------- + dropped_cand_count = sum(len(d["dropped"]) for d in dedup_findings) + conservation_errors: list[str] = list(field_count_errors) + expected_item_count = shard_count - len(b1_dropped_ordinals) - len(block_ordinals) + if len(final_items) != expected_item_count: + conservation_errors.append(f"item count {len(final_items)} != shard_count {shard_count} - b1_dropped {len(b1_dropped_ordinals)} - schema_block {len(block_ordinals)}") + want_ids = {i for i in expected_ids if i not in {f"E-{o}" for o in b1_dropped_ordinals} and i not in {f"E-{o}" for o in block_ordinals}} + got_ids = [it["evidence_index"] for it in final_items] + if set(got_ids) != want_ids or len(got_ids) != len(set(got_ids)): + conservation_errors.append("final evidence_index set mismatch vs planner expected ids minus explicit drops") + blocked_cand_count = sum(len(parts[o].get("event_candidates") or []) for o in block_ordinals) + b1_dropped_cand_count = sum(len(parts[o].get("event_candidates") or []) for o in b1_dropped_ordinals) + if input_candidate_total != final_candidate_total + dropped_cand_count + blocked_cand_count + b1_dropped_cand_count: + conservation_errors.append( + f"candidate conservation failed: input {input_candidate_total} != final {final_candidate_total} + dedup {dropped_cand_count} + blocked {blocked_cand_count} + b1_dropped {b1_dropped_cand_count}") + if not dedup_findings and not block_ordinals and not b1_dropped_ordinals and final_candidate_total < input_candidate_total: + conservation_errors.append("candidate count decreased without any recorded dedup group (silent loss)") + if sorted(got_ids) != got_ids: + conservation_errors.append("final items not sorted by ordinal") + if conservation_errors: + print(json.dumps({"status": "FAILED", "gate": "B2", + "message": "B2 quality gate BLOCK 발생: downstream 진행 금지", + "conservation_errors": conservation_errors}, ensure_ascii=False)) + raise RuntimeError("B2 conservation invariant violated; final file not written") + + content = json.dumps(envelope, ensure_ascii=False, indent=2) + verify = json.loads(content) + if (len(verify["items"]) != len(final_items) + or [i["evidence_index"] for i in verify["items"]] != got_ids + or sum(len(i.get("event_candidates") or []) for i in verify["items"]) != final_candidate_total): + raise RuntimeError("post-serialization verification failed for evidence_event_candidates.json") + write_doc(FINAL_PATH, content) + + block = bool(hard_findings) or bool(schema_invariant_findings) + precheck = { + "schema_version": "B2_precheck.v1", + "gate_id": "B2_event_candidates", + "planner_source": PLAN_PATH, + "planner_shard_count": shard_count, + "expected_ordinals": expected_ordinals, + "received_ordinals": expected_ordinals, + "input_parts_count": len(expected_ordinals), + "extra_part_detection": extra_detection, + "schema_invariant_findings": schema_invariant_findings, + "deterministic_checks": [ + {"check_id": "event_candidate_completeness", + "severity": "BLOCK" if (stale_parts or extra_parts) else "PASS", + "detail": "expected/received/stale set 비교 (결정적)", "missing_ordinals": missing_ordinals}, + {"check_id": "evidence_reference_integrity", + "severity": "BLOCK" if reference_violations else "PASS", + "detail": "finalized evidence id manifest 집합 멤버십 검사 (결정적)", "violations": reference_violations}, + {"check_id": "candidate_id_collision", + "severity": "BLOCK" if format_violations else "PASS", + "detail": "EVT-{ordinal:03d}-{seq:02d} 형식·유일성 + exact identity_signature dedup (결정적)", + "format_violations": format_violations, "dedup_findings": dedup_findings}, + {"check_id": "chronological_ordering", "severity": "PASS", + "detail": "명시 날짜 chain rule 역전 스캔, 의심건은 GB 판정 대기", "ordering_violations": ordering_suspect_pairs}, + {"check_id": "legal_effect_event_coverage", "severity": "PASS", + "detail": "REQUIRED_EVENT_SUBKINDS rule table 검사, 의심건은 GB 판정 대기", + "missing_subkinds_by_ordinal": [s["payload"].get("coverage_rule") for s in suspects if s["rule_id"] == "R-EVTCOV"]}, + ], + "hard_gate_findings": hard_findings, + "suspects": suspects, + "dedup_findings": dedup_findings, + "dropped_by_b1_alias": dropped_by_b1_alias, + "conservation": { + "input_parts": len(expected_ordinals), + "final_items": len(final_items), + "input_candidates": input_candidate_total, + "final_candidates": final_candidate_total, + "dedup_dropped_candidates": dropped_cand_count, + }, + "block": block, + } + write_doc(PRECHECK_PATH, json.dumps(precheck, ensure_ascii=False, indent=2)) + + # -------- B12 exception pack (B1 suspects + B1 review candidates + B2 suspects) -------- + pack_exceptions: list[dict[str, Any]] = [] + overflow_items: list[dict[str, Any]] = [] + for src in itertools.chain(b1_suspects, ( + { + "exception_id": f"B1-RC-{idx:03d}", + "source_gate": "B1", + "rule_id": "R-SELFWARN", + "check_id": "review_findings_in_mapper", + "ordinals": [rc.get("ordinal")], + "evidence_indexes": [rc.get("evidence_index")], + "candidate_ids": [], + "payload": rc, + "excerpt": "", + "escalation_flag": False, + } for idx, rc in enumerate(b1_review_candidates, 1) + ), suspects): + entry = dict(src) + ev = list(entry.get("evidence_indexes") or []) + cands = list(entry.get("candidate_ids") or []) + if len(ev) > MAX_ITEMS_PER_EXCEPTION and entry.get("rule_id") != "R-SELFWARN": + overflow_items.append({"exception_id": entry.get("exception_id"), "overflow_evidence_indexes": ev[MAX_ITEMS_PER_EXCEPTION:]}) + entry["evidence_indexes"] = ev[:MAX_ITEMS_PER_EXCEPTION] + if len(cands) > MAX_CANDIDATES_PER_EXCEPTION: + overflow_items.append({"exception_id": entry.get("exception_id"), "overflow_candidate_ids": cands[MAX_CANDIDATES_PER_EXCEPTION:]}) + entry["candidate_ids"] = cands[:MAX_CANDIDATES_PER_EXCEPTION] + entry["excerpt"] = str(entry.get("excerpt") or "")[:EXCERPT_LIMIT] + pack_exceptions.append(entry) + pack = { + "schema_version": "B12_exception_pack.v1", + "exception_count": len(pack_exceptions), + "exceptions": pack_exceptions, + "overflow_items": overflow_items, + "budget": { + "max_items_per_exception": MAX_ITEMS_PER_EXCEPTION, + "max_candidates": MAX_CANDIDATES_PER_EXCEPTION, + "max_excerpt_chars": EXCERPT_LIMIT, + }, + } + write_doc(PACK_PATH, json.dumps(pack, ensure_ascii=False, indent=2)) + + overall = "BLOCK" if block else "PASS" + # SKILL.md §3.3: stdout = task output — 단일 JSON 라인. 상태 메시지는 message 필드에 보존. + message = ("B2 quality gate BLOCK 발생: downstream 진행 금지" if block + else f"증거문서 사건행위 후보 finalize 완료 (severity={overall})") + print(json.dumps({ + "status": "B2_DETERMINISTIC_GATE_DONE", + "message": message, + "overall_severity_provisional": overall, + "pending_adjudication": "PENDING_ADJUDICATION" if pack_exceptions else "NO_SUSPECTS", + "final_items": len(final_items), + "input_candidates": input_candidate_total, + "final_candidates": final_candidate_total, + "dedup_findings": len(dedup_findings), + "exception_count": len(pack_exceptions), + }, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B12_gate_llm_adjudicator + preflight_files: + - stage1_tmp/quality_gate/B12_exception_pack.json + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: low + llm_verbosity: low + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Validate only the artifact, input bundle, IDs, coverage, severity, and handoff condition assigned to the current task. + - Identify contradictions only within assigned inputs: date, amount, party, object, registration, payment, possession, notice, valuation, legal role, causal link, source identity, or required support chain. + - Distinguish contradiction, incompleteness, ambiguity, multiplicity, schema failure, writer-boundary breach, and review-only legal uncertainty. + - Preserve all material conflicting source IDs. Do not average, normalize, or repair unless the current task expressly authorizes repair. + - Treat PASS, SOFT_WARNING, HARD_WARNING, BLOCK, READY, READY_WITH_REVIEW, FAILED, rollback, and signoff statuses exactly as the current schema defines them. + - Do not downgrade severity for convenience, and do not block progression unless the current gate criteria require it. + - Validators report issues in the authorized validation, audit, gate, review, or rollback schema. They do not write final artifacts unless explicitly assigned. + + + + + Task_B12_gate_llm_adjudicator + + You are an MCP-enabled LLM-backed **exception adjudicator** assisting plaintiff-side Korean civil/commercial litigation counsel. You judge only the compact suspect items that the deterministic B1/B2 gate reducers extracted. You are not a merger, not a final writer, and not a fact creator. + + + 본 task는 v3 아키텍처의 조건부 국소 판정기(GB)다. 결정적 reducer(`Task_B1_quality_gate_evidence_indexed`, `Task_B2_quality_gate_event_candidates`)가 기계적 검사로 좁혀낸 의심 항목만 `stage1_tmp/quality_gate/B12_exception_pack.json`에서 읽어, 항목별 판정을 `stage1_tmp/quality_gate/B12_adjudication_decisions.json`에 기록한다. + 최종 파일(`evidence_indexed.json`, `evidence_event_candidates.json`)과 audit(`quality_gates/*.json`)은 본 task가 쓰지 않는다. audit 확정은 후속 결정적 task `Task_B12_gate_audit_finalizer`가, sidecar는 `Task_B2_SHA256_soft_gate_handoff_writer`가 전담한다. + 변호사 관점의 판정 원칙: 법률적 불확실성(suspected alias, component coverage 의문, chronology 모호성, 항변 사항의 Stage 2 검토 필요성)은 review 판정으로 남기고, provenance 오염·source trace 상실·final evidence identity collision처럼 downstream traceability를 실제로 깨는 경우에만 `CONFIRM_BLOCK`을 사용한다. review 판정은 사실 확정이 아니라 검토 큐다. + + + + + + - `stage1_tmp/quality_gate/B12_exception_pack.json` (유일한 입력 파일. 각 exception의 payload·excerpt가 이미 내장되어 있다) + + + - `stage1_tmp/quality_gate/B12_adjudication_decisions.json` + + + - `evidence_indexed.json`, `evidence_event_candidates.json`, `quality_gates/` 아래 어떤 파일도 작성·재작성 + - exception pack 외의 어떤 파일도 read (full planner/final/meeting/evidence 재독해 금지 — 필요한 발췌는 pack에 이미 포함됨) + - mapper part 수정 또는 새 candidate·새 사실 창작 + - pack에 없는 exception_id 창작, 또는 pack의 exception_id 중 판정 누락 + - `source_refs`를 배열로 출력 (반드시 named object: `evidence_indexes`/`event_candidate_ids`/`meeting_clause_refs`/`doc_titles`) + - count·통계·요약 필드 창작 (집계는 후속 결정적 task 소관) + + + + + 1. `read_docs`로 `stage1_tmp/quality_gate/B12_exception_pack.json` 1개 파일만 읽는다. `list_docs` 사전 확인은 이 파일 1건에 한한다. + 2. `exception_count == 0`이면: `write_file(overwrite=true)`로 `{"schema_version": "B12_adjudication_decisions.v1", "decisions": [], "no_suspects": true}`를 `stage1_tmp/quality_gate/B12_adjudication_decisions.json`에 저장하고 `"NO_SUSPECTS"`만 출력한 뒤 즉시 종료한다(terminate). 다른 어떤 파일도 읽지 않는다. + 3. `exception_count > 0`이면: 각 exception을 pack에 포함된 `payload`·`excerpt`만으로 판정한다. 추가 `read_docs` 호출은 금지된다. + 4. 각 exception에 대해 decision을 하나 결정한다: + - `DISMISS`: 기계적 신호였을 뿐 실질 문제가 없음 (예: 정상적인 rule-table 공백, 문서 성격상 자연스러운 component 부재) + - `SOFT_WARNING_REVIEW`: Stage 2 검토 큐에 남길 경미한 법률적 불확실성 + - `HARD_WARNING_REVIEW`: 청구원인·소권 판단에 영향 가능성이 있는 검토 필요 사항 + - `CONFIRM_BLOCK`: provenance 오염, source trace 상실, final evidence identity collision이 실제로 확인되는 경우만 + - `ESCALATE_TO_REVIEW`: `escalation_flag`가 true이고 pack 정보만으로 확정할 수 없는 경우 — 서로 다른 source가 같은 법률행위의 날짜·당사자·목적물을 실질적으로 충돌 표시하거나, authority drift 여부가 source trace 유효성을 좌우하거나, chronology 판단이 hard block 여부를 좌우하는 경우. 이 판정은 인간 변호사 검토 큐로 라우팅된다. + 5. `write_file(overwrite=true)`로 decisions 파일을 저장한다. 스키마: + {"schema_version": "B12_adjudication_decisions.v1", "no_suspects": false, "decisions": [ {"exception_id": "...", "decision": "DISMISS|SOFT_WARNING_REVIEW|HARD_WARNING_REVIEW|CONFIRM_BLOCK|ESCALATE_TO_REVIEW", "severity": "PASS|SOFT_WARNING|HARD_WARNING|BLOCK", "issue_type": "snake_case", "source_refs": {"evidence_indexes": [], "event_candidate_ids": [], "meeting_clause_refs": [], "doc_titles": []}, "review_instruction": "≤120자 한국어", "candidate_use_policy": "prioritize_if_supported|preserve_as_needs_review|do_not_generate_without_support", "concise_basis": "≤120자 한국어"} ] } + 6. `list_docs`로 decisions 파일 존재만 확인한다. 절대 읽지 않는다. + 7. `"B12 예외 판정 완료 (decisions=<건수>)"`만 출력하고 작업을 끝낸다(terminate). + + + + - exception pack 부재·빈 파일·파싱 불가: 즉시 중단하고 채팅으로만 보고한다. decisions 파일은 쓰지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + + - `hard`(BLOCK 상당)는 downstream 오염·출처·무결성 실패만: stale/provenance 오염, source trace 상실, final evidence identity collision. 그 외는 전부 review다. + - review 판정은 사실 확정이 아니며 자동 차단 사유가 아니다. `rule_id`별 기준: + - `R-ALIAS`: 두 문서가 실제 동일 문서의 alias인지. 동일 확정 + source trace 위험이면 CONFIRM_BLOCK, 동일 의심이면 HARD_WARNING_REVIEW, 별개 문서면 DISMISS. + - `R-COVERAGE`: 문서 유형상 필수 component가 실제로 원문에 존재할 개연성이 payload/excerpt에서 보이는데 비어 있으면 HARD_WARNING_REVIEW, 문서 성격상 자연스러운 부재면 DISMISS. + - `R-DRIFT`: excerpt(해당 shard 원문 발췌)와 part 필드를 대조하여 다른 문서 고유 사실의 주입이 보이면 CONFIRM_BLOCK, 판단 불가면 ESCALATE_TO_REVIEW, 근거 없으면 DISMISS. + - `R-CHRONO`: 날짜 역전이 같은 법률행위 chain의 실질 모순이면(이행기·시효·도달일·경매 chain) HARD_WARNING_REVIEW 또는 ESCALATE_TO_REVIEW, 서로 다른 채무·물건의 우연한 교차면 DISMISS. + - `R-EVTCOV`: payment/notice/auction/lien/succession/valuation/defense/상호속용 등 legal-effect event 누락 의심이 실질적이면 SOFT/HARD_WARNING_REVIEW, rule-table의 기계적 공백이면 DISMISS. + - `R-SELFWARN`: mapper가 남긴 항변·재반박 신호(상계 주장 등)는 원칙적으로 SOFT_WARNING_REVIEW로 승격하여 Stage 2 검토 큐에 보존한다. `candidate_use_policy="preserve_as_needs_review"`. + - `source_refs`는 pack의 `evidence_indexes`/`candidate_ids`를 named object로 재배치한다. 임의 ref 창작 금지. + + + + + + [ ] 1) `read_docs` 사용해서 `stage1_tmp/quality_gate/B12_exception_pack.json` 1개 파일만 읽기 + [ ] 2) `exception_count == 0`이면 빈 decisions 파일(`no_suspects: true`) 저장 후 `"NO_SUSPECTS"` 출력하고 즉시 종료 + [ ] 3) 각 exception을 pack 내장 payload/excerpt만으로 판정 (추가 read 금지) + [ ] 4) 모든 exception_id에 정확히 1개 decision, `source_refs`는 named object + [ ] 5) `write_file` 사용해서 `stage1_tmp/quality_gate/B12_adjudication_decisions.json` 저장 + [ ] 6) `list_docs`로 파일 존재만 확인(절대 읽기 금지) + [ ] 7) `"B12 예외 판정 완료 (decisions=<건수>)"` 출력하고 작업을 끝낸다(terminate) + + + + + - `stage1_tmp/quality_gate/B12_exception_pack.json` + + + - `stage1_tmp/quality_gate/B12_adjudication_decisions.json` + + + + + - 본 task는 판정만 한다. 병합·직렬화·집계·최종 파일 작성은 전부 결정적 task의 소관이다. + - pack 외 파일을 읽는 순간 read-diet 위반이다. 필요한 발췌는 이미 pack 안에 있다. + - review 판정은 검토 큐이지 사실 확정이 아니다. 확신 없는 BLOCK보다 ESCALATE_TO_REVIEW가 항상 안전하다. + + + + - task_name: Task_B12_gate_audit_finalizer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 120 + code: | + #!/usr/bin/env python3 + # Task_B12_gate_audit_finalizer (v3) - deterministic audit finalizer (GC) + # Spec: Part_1_Improvement_Strategy_Claude_v1.md §7 + from __future__ import annotations + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + + B1_PRECHECK_PATH = "stage1_tmp/quality_gate/B1_precheck.json" + B2_PRECHECK_PATH = "stage1_tmp/quality_gate/B2_precheck.json" + DECISIONS_PATH = "stage1_tmp/quality_gate/B12_adjudication_decisions.json" + PACK_PATH = "stage1_tmp/quality_gate/B12_exception_pack.json" + B1_AUDIT_PATH = "quality_gates/B1_evidence_indexed_gate.json" + B2_AUDIT_PATH = "quality_gates/B2_event_candidates_gate.json" + MAX_REVIEW_ITEMS = 40 + SOURCE_REF_KEYS = ("evidence_indexes", "event_candidate_ids", "meeting_clause_refs", "doc_titles") + SEVERITY_ORDER = {"PASS": 0, "SOFT_WARNING": 1, "HARD_WARNING": 2, "BLOCK": 3} + ALLOWED_POLICIES = ("prioritize_if_supported", "preserve_as_needs_review", "do_not_generate_without_support") + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-b12-gate-audit-finalizer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _mid(), + "method": "tools/call", + "params": {"name": name, "arguments": args}, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: Any, name: str) -> str: + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError(f"Empty: {name}") + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, dict) and "results" in outer: + result0 = (outer.get("results") or [{}])[0] + inner = result0.get("content") or result0.get("text") or "" + if not inner: + raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer if isinstance(outer, str) else json.dumps(outer, ensure_ascii=False) + + def read_raw(path: str) -> str: + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str) or not raw.strip(): + raise RuntimeError(f"Missing or empty required input: {path}") + return raw + + def parse_json(raw: str, path: str) -> Any: + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + try: + obj, _ = decoder.raw_decode(raw.strip()) + return obj + except Exception as exc: + raise RuntimeError(f"Invalid JSON input: {path}") from exc + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def clip(value: Any, limit: int = 120) -> str: + text = " ".join(str(value or "").split()) + if len(text) <= limit: + return text + return text[:limit].rstrip() + "..." + + def clean_enum(value: Any, fallback: str) -> str: + text = str(value or fallback).strip().lower() + text = re.sub(r"[^a-z0-9_]+", "_", text) + text = re.sub(r"_+", "_", text).strip("_") + return text or fallback + + def named_source_refs(value: Any, exception: dict[str, Any] | None) -> dict[str, list[str]]: + # source_refs는 반드시 named object — 배열형은 schema failure로 강제 정규화하지 않고 pack 기반으로 재구성 + refs = {key: [] for key in SOURCE_REF_KEYS} + if isinstance(value, dict): + for key in SOURCE_REF_KEYS: + vals = value.get(key) + if isinstance(vals, list): + refs[key].extend(str(v).strip() for v in vals if v) + if exception: + refs["evidence_indexes"].extend(str(v) for v in (exception.get("evidence_indexes") or []) if v) + refs["event_candidate_ids"].extend(str(v) for v in (exception.get("candidate_ids") or []) if v) + return {key: sorted(set(v for v in vals if v)) for key, vals in refs.items()} + + def worst(*severities: str) -> str: + top = "PASS" + for s in severities: + if SEVERITY_ORDER.get(s, 0) > SEVERITY_ORDER.get(top, 0): + top = s + return top + + DECISION_SEVERITY = { + "DISMISS": "PASS", + "SOFT_WARNING_REVIEW": "SOFT_WARNING", + "HARD_WARNING_REVIEW": "HARD_WARNING", + "ESCALATE_TO_REVIEW": "HARD_WARNING", + "CONFIRM_BLOCK": "BLOCK", + } + + def build_audit(gate: str, precheck: dict[str, Any], decisions_by_exc: dict[str, dict[str, Any]], + exceptions_by_id: dict[str, dict[str, Any]], executed_at: str) -> dict[str, Any]: + gate_id = "B1_evidence_indexed" if gate == "B1" else "B2_event_candidates" + checks: list[dict[str, Any]] = [] + check_sev: dict[str, str] = {} + for chk in precheck.get("deterministic_checks") or []: + entry = dict(chk) + check_sev[str(entry.get("check_id"))] = str(entry.get("severity") or "PASS") + checks.append(entry) + + hard_findings: list[dict[str, Any]] = [] + for idx, hf in enumerate(precheck.get("hard_gate_findings") or [], 1): + item = dict(hf) if isinstance(hf, dict) else {"detail": str(hf)} + item.setdefault("gate_finding_id", f"{gate}-HARD-{idx:03d}") + hard_findings.append(item) + + review_findings: list[dict[str, Any]] = [] + drift_confirmed: list[dict[str, Any]] = [] + for exc_id, exc in exceptions_by_id.items(): + if str(exc.get("source_gate")) != gate: + continue + dec = decisions_by_exc.get(exc_id) + if dec is None: + # 판정 누락 exception은 보수적으로 review로 승격 (silent pass 금지) + dec = {"exception_id": exc_id, "decision": "ESCALATE_TO_REVIEW", "severity": "HARD_WARNING", + "issue_type": clean_enum(exc.get("rule_id"), "review_finding"), + "source_refs": {}, "concise_basis": "adjudication decision missing for extracted suspect", + "candidate_use_policy": "preserve_as_needs_review"} + decision = str(dec.get("decision") or "ESCALATE_TO_REVIEW") + mapped = DECISION_SEVERITY.get(decision, "HARD_WARNING") + check_id = str(exc.get("check_id") or "review_finding") + if decision == "DISMISS": + continue + if decision == "CONFIRM_BLOCK": + item = { + "gate_finding_id": f"{gate}-HARD-{len(hard_findings) + 1:03d}", + "issue_type": clean_enum(dec.get("issue_type") or exc.get("rule_id"), "confirmed_block"), + "source_check": clean_enum(check_id, "gate_check"), + "detail": clip(dec.get("concise_basis") or "adjudicator confirmed hard gate violation"), + "source_refs": named_source_refs(dec.get("source_refs"), exc), + } + hard_findings.append(item) + check_sev[check_id] = worst(check_sev.get(check_id, "PASS"), "BLOCK") + if check_id == "authority_drift_detection": + drift_confirmed.append({"ordinals": exc.get("ordinals"), "basis": item["detail"]}) + continue + severity = str(dec.get("severity") or mapped) + if severity not in ("SOFT_WARNING", "HARD_WARNING"): + severity = mapped + policy = dec.get("candidate_use_policy") + if policy not in ALLOWED_POLICIES: + policy = "preserve_as_needs_review" + review_findings.append({ + "gate_finding_id": f"{gate}-REV-{len(review_findings) + 1:03d}", + "source_check": clean_enum(check_id, "review_finding"), + "severity": severity, + "issue_type": clean_enum(dec.get("issue_type") or exc.get("rule_id"), "review_finding"), + "domain_hints": [], + "target_stage_hints": ["Stage_2"], + "target_task_hints": [], + "source_refs": named_source_refs(dec.get("source_refs"), exc), + "review_instruction": clip(dec.get("review_instruction") + or "후속 검토 단계에서 해당 review finding의 증거와 법률상 의미를 재검토한다."), + "candidate_use_policy": policy, + "concise_basis": clip(dec.get("concise_basis") or exc.get("rule_id")), + }) + check_sev[check_id] = worst(check_sev.get(check_id, "PASS"), severity) + review_findings = review_findings[:MAX_REVIEW_ITEMS] + + for chk in checks: + cid = str(chk.get("check_id")) + chk["severity"] = check_sev.get(cid, str(chk.get("severity") or "PASS")) + if cid == "authority_drift_detection" and drift_confirmed: + chk["drift_findings"] = (chk.get("drift_findings") or []) + drift_confirmed + + schema_invariant = list(precheck.get("schema_invariant_findings") or []) if gate == "B2" else None + block = bool(hard_findings) or bool(schema_invariant) + overall = "BLOCK" if block else worst(*[str(c.get("severity") or "PASS") for c in checks], + *[str(r.get("severity")) for r in review_findings]) + if overall == "BLOCK" and not block: + overall = "HARD_WARNING" + progression = "BLOCKED" if block else ("ALLOWED" if overall == "PASS" else "ALLOWED_WITH_WARNING") + auto = not block + highest_review = "PASS" + for r in review_findings: + highest_review = worst(highest_review, str(r.get("severity"))) + handoff_status = "BLOCKED" if block else ("READY_WITH_REVIEW" if review_findings else "READY_NO_REVIEW") + + audit: dict[str, Any] = { + "gate_id": gate_id, + "executed_at": executed_at, + "input_parts_count": precheck.get("input_parts_count"), + "expected_ordinals": precheck.get("expected_ordinals") or [], + "planner_source": precheck.get("planner_source") or "evidence_shard_plan.json", + "planner_shard_count": precheck.get("planner_shard_count"), + "received_ordinals": precheck.get("received_ordinals") or [], + } + if gate == "B2": + audit["schema_invariant_findings"] = schema_invariant or [] + audit.update({ + "checks": checks, + "overall_severity": overall, + "block_reasons": [clip(h.get("detail") or h.get("issue_type")) for h in hard_findings] + + ([clip(f"{s.get('reason')} (rerun: {s.get('rerun_target_task')})") for s in (schema_invariant or [])]), + "warning_reasons": [clip(r.get("concise_basis")) for r in review_findings], + "hard_gate_findings": hard_findings, + "review_findings": review_findings, + "rerun_targets": sorted({str(s.get("rerun_target_task")) for s in (schema_invariant or []) if s.get("rerun_target_task")}), + "stage2_risk_notes": [clip(r.get("concise_basis")) for r in review_findings], + "review_gate_summary": { + "review_gate_count": len(review_findings), + "highest_review_severity": highest_review, + }, + "handoff_export_summary": { + "is_soft_handoff_writer": False, + "handoff_writer_task": "Task_B2_SHA256_soft_gate_handoff_writer", + "handoff_path": "quality_gates/stage1_part1_soft_gate_handoff.json", + "exported_review_count": len(review_findings), + "handoff_status_candidate": handoff_status, + }, + "downstream_progression": progression, + "stage2_auto_progression_allowed": auto, + }) + if gate == "B1": + audit["dropped_aliases"] = precheck.get("dropped_aliases") or [] + audit["schema_contract_version"] = "B1_quality_gate.v3" + else: + audit["dedup_findings"] = precheck.get("dedup_findings") or [] + audit["dropped_by_b1_alias"] = precheck.get("dropped_by_b1_alias") or [] + audit["conservation"] = precheck.get("conservation") or {} + audit["schema_contract_version"] = "B2_quality_gate.v3" + return audit + + def main() -> None: + _init() + b1_precheck = parse_json(read_raw(B1_PRECHECK_PATH), B1_PRECHECK_PATH) + b2_precheck = parse_json(read_raw(B2_PRECHECK_PATH), B2_PRECHECK_PATH) + try: + pack = parse_json(read_raw(PACK_PATH), PACK_PATH) + except Exception: + pack = {"exceptions": []} + try: + decisions_doc = parse_json(read_raw(DECISIONS_PATH), DECISIONS_PATH) + except Exception: + decisions_doc = {"decisions": [], "no_suspects": True} + + exceptions_by_id: dict[str, dict[str, Any]] = {} + for exc in pack.get("exceptions") or []: + if isinstance(exc, dict) and exc.get("exception_id"): + exceptions_by_id[str(exc["exception_id"])] = exc + decisions_by_exc: dict[str, dict[str, Any]] = {} + for dec in decisions_doc.get("decisions") or []: + if isinstance(dec, dict) and dec.get("exception_id"): + decisions_by_exc[str(dec["exception_id"])] = dec + + b2_review_summary_split = {"b1": 0, "b2": 0} + executed_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") + b1_audit = build_audit("B1", b1_precheck, decisions_by_exc, exceptions_by_id, executed_at) + b2_audit = build_audit("B2", b2_precheck, decisions_by_exc, exceptions_by_id, executed_at) + b2_review_summary_split["b1"] = b1_audit["review_gate_summary"]["review_gate_count"] + b2_review_summary_split["b2"] = b2_audit["review_gate_summary"]["review_gate_count"] + # v.2 B2 audit review_gate_summary는 B1/B2 review count를 함께 요약한다 + b2_audit["review_gate_summary"]["b1_review_count"] = b2_review_summary_split["b1"] + b2_audit["review_gate_summary"]["b2_review_count"] = b2_review_summary_split["b2"] + + write_doc(B1_AUDIT_PATH, json.dumps(b1_audit, ensure_ascii=False, indent=2)) + write_doc(B2_AUDIT_PATH, json.dumps(b2_audit, ensure_ascii=False, indent=2)) + + # SKILL.md §3.3: stdout = task output — 단일 JSON 라인. 상태 메시지는 message 필드에 보존. + messages = [] + if b1_audit["overall_severity"] == "BLOCK": + messages.append("B1 quality gate BLOCK 발생: downstream 진행 금지") + if b2_audit["overall_severity"] == "BLOCK": + messages.append("B2 quality gate BLOCK 발생: downstream 진행 금지") + print(json.dumps({ + "status": "GATE_AUDITS_FINALIZED", + "message": " | ".join(messages) if messages else "gate audit finalize 완료", + "b1_overall": b1_audit["overall_severity"], + "b2_overall": b2_audit["overall_severity"], + "review_count": b2_review_summary_split["b1"] + b2_review_summary_split["b2"], + "hard_count": len(b1_audit["hard_gate_findings"]) + len(b2_audit["hard_gate_findings"]) + + len(b2_audit.get("schema_invariant_findings") or []), + "no_suspects": bool(decisions_doc.get("no_suspects")), + }, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B2_SHA256_soft_gate_handoff_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import hashlib + import itertools + import json + import re + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + SIDE_CAR_PATH = "quality_gates/stage1_part1_soft_gate_handoff.json" + INPUT_PATHS = { + "evidence_indexed_sha256": "evidence_indexed.json", + "evidence_event_candidates_sha256": "evidence_event_candidates.json", + "b1_gate_sha256": "quality_gates/B1_evidence_indexed_gate.json", + "b2_gate_sha256": "quality_gates/B2_event_candidates_gate.json", + } + WORKFLOW_ROLES = ( + "claim_right_identification", + "cause_of_action_element_build", + "evidence_weight_and_gap_check", + "chronology_and_state_split", + "asset_registry_succession_check", + "defense_and_obstacle_check", + "relief_scope_and_quantification", + "actio_value_compensation_check", + ) + SOURCE_REF_KEYS = ( + "evidence_indexes", + "event_candidate_ids", + "meeting_clause_refs", + "doc_titles", + ) + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-b2-sha256-soft-gate-handoff-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _mid(), + "method": "tools/call", + "params": {"name": name, "arguments": args}, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: Any, name: str) -> str: + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError(f"Empty: {name}") + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, dict) and "results" in outer: + result0 = (outer.get("results") or [{}])[0] + inner = result0.get("content") or result0.get("text") or "" + if not inner: + raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer if isinstance(outer, str) else json.dumps(outer, ensure_ascii=False) + + def read_raw(path: str) -> str: + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str) or not raw.strip(): + raise RuntimeError(f"Missing or empty required input: {path}") + return raw + + def parse_json(raw: str, path: str) -> Any: + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + try: + obj, _ = decoder.raw_decode(raw.strip()) + return obj + except Exception as exc: + raise RuntimeError(f"Invalid JSON input: {path}") from exc + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def digest(raw: str) -> str: + value = hashlib.sha256(raw.encode("utf-8")).hexdigest() + if not re.fullmatch(r"[0-9a-f]{64}", value): + raise RuntimeError("digest_guard produced a non-SHA256 value") + return value + + def as_list(value: Any) -> list[Any]: + if value is None: + return [] + if isinstance(value, list): + return value + return [value] + + def as_str_list(value: Any) -> list[str]: + out: list[str] = [] + for item in as_list(value): + if item is None: + continue + if isinstance(item, dict): + for key in ("id", "value", "name", "title", "ref"): + if item.get(key): + out.append(str(item[key]).strip()) + break + else: + out.append(str(item).strip()) + return [x for x in out if x] + + def clean_enum(value: Any, fallback: str) -> str: + text = str(value or fallback).strip().lower() + text = re.sub(r"[^a-z0-9_]+", "_", text) + text = re.sub(r"_+", "_", text).strip("_") + return text or fallback + + def clip(value: Any, limit: int = 120) -> str: + text = " ".join(str(value or "").split()) + if len(text) <= limit: + return text + return text[:limit].rstrip() + "..." + + def normalize_severity(value: Any) -> str: + text = str(value or "").upper() + if "HARD" in text: + return "HARD_WARNING" + return "SOFT_WARNING" + + def role_for(issue_type: str, finding: dict[str, Any]) -> str: + explicit = finding.get("lawyer_workflow_role") + if explicit in WORKFLOW_ROLES: + return str(explicit) + text = " ".join([ + issue_type, + str(finding.get("source_check") or ""), + str(finding.get("check_id") or ""), + str(finding.get("detail") or finding.get("reason") or ""), + ]).lower() + if any(k in text for k in ("chrono", "date", "timeline", "state", "order")): + return "chronology_and_state_split" + if any(k in text for k in ("evidence", "proof", "source", "locator")): + return "evidence_weight_and_gap_check" + if any(k in text for k in ("registry", "secured", "lien", "succession", "asset", "auction", "mortgage")): + return "asset_registry_succession_check" + if any(k in text for k in ("defense", "obstacle", "setoff", "limitation")): + return "defense_and_obstacle_check" + if any(k in text for k in ("relief", "amount", "scope", "valuation", "damage")): + return "relief_scope_and_quantification" + if "actio" in text or "fraudulent" in text or "value_compensation" in text: + return "actio_value_compensation_check" + if any(k in text for k in ("claim", "right", "element", "legal_effect")): + return "claim_right_identification" + return "cause_of_action_element_build" + + def source_refs(finding: dict[str, Any]) -> dict[str, list[str]]: + refs = {key: [] for key in SOURCE_REF_KEYS} + nested = finding.get("source_refs") + if isinstance(nested, dict): + for key in SOURCE_REF_KEYS: + refs[key].extend(as_str_list(nested.get(key))) + refs["evidence_indexes"].extend(as_str_list(finding.get("evidence_indexes"))) + refs["evidence_indexes"].extend(as_str_list(finding.get("evidence_index"))) + refs["evidence_indexes"].extend(as_str_list(finding.get("source_evidence_index"))) + refs["event_candidate_ids"].extend(as_str_list(finding.get("event_candidate_ids"))) + refs["event_candidate_ids"].extend(as_str_list(finding.get("candidate_id"))) + refs["event_candidate_ids"].extend(as_str_list(finding.get("candidate_ids"))) + refs["meeting_clause_refs"].extend(as_str_list(finding.get("meeting_clause_refs"))) + refs["meeting_clause_refs"].extend(as_str_list(finding.get("meeting_clause_ref"))) + refs["doc_titles"].extend(as_str_list(finding.get("doc_titles"))) + refs["doc_titles"].extend(as_str_list(finding.get("doc_title"))) + return {key: sorted(set(value)) for key, value in refs.items()} + + def extract_findings(gate: Any, key: str) -> list[Any]: + findings: list[Any] = [] + if isinstance(gate, dict): + findings.extend(as_list(gate.get(key))) + for check in as_list(gate.get("checks")): + if isinstance(check, dict): + findings.extend(as_list(check.get(key))) + return findings + + def hard_findings(gate: Any, source_gate: str) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for item in extract_findings(gate, "hard_gate_findings"): + out.append(item if isinstance(item, dict) else {"detail": str(item)}) + if isinstance(gate, dict): + blocked = gate.get("overall_severity") == "BLOCK" or gate.get("downstream_progression") == "BLOCKED" or gate.get("stage2_auto_progression_allowed") is False + if blocked and not out: + reasons = as_list(gate.get("block_reasons")) + if not reasons: + reasons = [f"{source_gate} gate blocked without explicit hard_gate_findings"] + for reason in reasons: + out.append({"issue_type": "block_reason", "detail": str(reason)}) + for index, item in enumerate(out, 1): + item.setdefault("gate_finding_id", f"{source_gate}-HARD-{index:03d}") + return out + + def compact_review_items(b1_gate: Any, b2_gate: Any) -> list[dict[str, Any]]: + compact: list[dict[str, Any]] = [] + ordered = [("B1", b1_gate), ("B2", b2_gate)] + seq = 1 + for source_gate, gate in ordered: + for raw in extract_findings(gate, "review_findings"): + finding = raw if isinstance(raw, dict) else {"detail": str(raw)} + issue_type = clean_enum( + finding.get("issue_type") or finding.get("check_id") or finding.get("source_check") or "review_finding", + "review_finding", + ) + role = role_for(issue_type, finding) + refs = source_refs(finding) + policy = finding.get("candidate_use_policy") + if policy not in ("prioritize_if_supported", "preserve_as_needs_review", "do_not_generate_without_support"): + policy = "preserve_as_needs_review" + item = { + "review_id": f"P1RG-{seq:04d}", + "source_gate": source_gate, + "source_check": clean_enum(finding.get("source_check") or finding.get("check_id") or issue_type, issue_type), + "severity": normalize_severity(finding.get("severity")), + "issue_type": issue_type, + "lawyer_workflow_role": role, + "domain_hints": as_str_list(finding.get("domain_hints") or finding.get("domain_hint") or finding.get("domains")), + "target_stage_hints": as_str_list(finding.get("target_stage_hints") or finding.get("target_stage_hint")), + "target_task_hints": as_str_list(finding.get("target_task_hints") or finding.get("target_task_hint")), + "source_refs": refs, + "review_instruction": clip(finding.get("review_instruction") or finding.get("instruction") or "후속 검토 단계에서 해당 review finding의 증거와 법률상 의미를 재검토한다.", 120), + "negative_instruction": "사실 확정 또는 자동 차단으로 취급하지 말 것", + "candidate_use_policy": policy, + "concise_basis": clip(finding.get("concise_basis") or finding.get("detail") or finding.get("reason") or issue_type, 120), + "created_from": { + "gate_file": "quality_gates/B1_evidence_indexed_gate.json" if source_gate == "B1" else "quality_gates/B2_event_candidates_gate.json", + "gate_finding_id": finding.get("gate_finding_id") or finding.get("finding_id") or finding.get("id"), + }, + } + compact.append(item) + seq += 1 + if len(compact) >= 40: + return compact + return compact + + def add_index(index: dict[str, dict[str, list[str]]], bucket: str, key: str, review_id: str) -> None: + if not key: + return + index[bucket].setdefault(key, []) + if review_id not in index[bucket][key]: + index[bucket][key].append(review_id) + + def build_routing(review_items: list[dict[str, Any]]) -> tuple[dict[str, dict[str, list[str]]], dict[str, list[str]]]: + routing = { + "by_evidence_index": {}, + "by_event_candidate_id": {}, + "by_meeting_clause_ref": {}, + "by_issue_type": {}, + "by_domain_hint": {}, + "by_target_stage": {}, + "by_lawyer_workflow_role": {}, + } + queues = {role: [] for role in WORKFLOW_ROLES} + for item in review_items: + rid = item["review_id"] + refs = item.get("source_refs") or {} + for value in refs.get("evidence_indexes", []): + add_index(routing, "by_evidence_index", value, rid) + for value in refs.get("event_candidate_ids", []): + add_index(routing, "by_event_candidate_id", value, rid) + for value in refs.get("meeting_clause_refs", []): + add_index(routing, "by_meeting_clause_ref", value, rid) + add_index(routing, "by_issue_type", item.get("issue_type", ""), rid) + for value in item.get("domain_hints", []): + add_index(routing, "by_domain_hint", value, rid) + for value in item.get("target_stage_hints", []): + add_index(routing, "by_target_stage", value, rid) + role = item.get("lawyer_workflow_role", "cause_of_action_element_build") + add_index(routing, "by_lawyer_workflow_role", role, rid) + queues.setdefault(role, []).append(rid) + return routing, {role: queues.get(role, []) for role in WORKFLOW_ROLES} + + def gate_blocked(gate: Any) -> bool: + return isinstance(gate, dict) and ( + gate.get("overall_severity") == "BLOCK" + or gate.get("downstream_progression") == "BLOCKED" + or gate.get("stage2_auto_progression_allowed") is False + ) + + def main() -> None: + _init() + raw_by_digest_key = {key: read_raw(path) for key, path in INPUT_PATHS.items()} + parsed = {key: parse_json(raw, INPUT_PATHS[key]) for key, raw in raw_by_digest_key.items()} + + digest_guard = {key: digest(raw) for key, raw in raw_by_digest_key.items()} + b1_gate = parsed["b1_gate_sha256"] + b2_gate = parsed["b2_gate_sha256"] + review_items = compact_review_items(b1_gate, b2_gate) + routing_index, lawyer_workflow_queues = build_routing(review_items) + + hard_items = hard_findings(b1_gate, "B1") + hard_findings(b2_gate, "B2") + hard_ids = [ + str(item.get("gate_finding_id") or item.get("finding_id") or item.get("id") or f"HARD-{idx:03d}") + for idx, item in enumerate(hard_items, 1) + ] + blocked = bool(hard_items) or gate_blocked(b1_gate) or gate_blocked(b2_gate) + highest_review = "PASS" + if any(item["severity"] == "HARD_WARNING" for item in review_items): + highest_review = "HARD_WARNING" + elif review_items: + highest_review = "SOFT_WARNING" + b1_count = sum(1 for item in review_items if item["source_gate"] == "B1") + b2_count = sum(1 for item in review_items if item["source_gate"] == "B2") + + if blocked: + handoff_status = "BLOCKED" + elif review_items: + handoff_status = "READY_WITH_REVIEW" + else: + handoff_status = "READY_NO_REVIEW" + + sidecar = { + "schema_version": "stage1_part1_soft_gate_handoff.v1", + "source_stage": "Stage_1.Part_1", + "source_gate_files": [ + "quality_gates/B1_evidence_indexed_gate.json", + "quality_gates/B2_event_candidates_gate.json", + ], + "handoff_status": handoff_status, + "stage2_auto_progression_allowed": not blocked, + "hard_gate_summary": { + "hard_gate_count": len(hard_items), + "hard_gate_ids": hard_ids, + }, + "review_gate_summary": { + "review_gate_count": len(review_items), + "highest_review_severity": highest_review, + "b1_review_count": b1_count, + "b2_review_count": b2_count, + }, + "digest_guard": digest_guard, + "review_items": review_items, + "routing_index": routing_index, + "lawyer_workflow_queues": lawyer_workflow_queues, + } + + content = json.dumps(sidecar, ensure_ascii=False, indent=2) + write_doc(SIDE_CAR_PATH, content) + print(json.dumps({ + "status": "SOFT_GATE_HANDOFF_WRITTEN", + "path": SIDE_CAR_PATH, + "handoff_status": handoff_status, + "review_items": len(review_items), + "digest_guard_valid": all(re.fullmatch(r"[0-9a-f]{64}", v) for v in digest_guard.values()), + }, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + task_procedure: + IN: + nexts: ["Task_A_client_goal", "Task_Evidence_shard_planner"] + wait_until: [] + + Task_A_client_goal: + nexts: [] + wait_until: ["IN"] + + Task_Evidence_shard_planner: + nexts: ["Task_B1_map_doc_*"] + wait_until: ["IN"] + + Task_B1_map_doc_*: + nexts: + - "Task_B2_map_events_{same_ordinal}" + - "Task_B1_quality_gate_evidence_indexed" + wait_until: ["Task_Evidence_shard_planner"] + + Task_B2_map_events_*: + nexts: ["Task_B2_quality_gate_event_candidates"] + wait_until: ["Task_B1_map_doc_{same_ordinal}"] + + Task_B1_quality_gate_evidence_indexed: + nexts: ["Task_B2_quality_gate_event_candidates"] + wait_until: ["all Task_B1_map_doc_*"] + + Task_B2_quality_gate_event_candidates: + nexts: ["Task_B12_gate_llm_adjudicator"] + wait_until: ["all Task_B2_map_events_*", "Task_B1_quality_gate_evidence_indexed"] + + Task_B12_gate_llm_adjudicator: + nexts: ["Task_B12_gate_audit_finalizer"] + wait_until: ["Task_B2_quality_gate_event_candidates"] + + Task_B12_gate_audit_finalizer: + nexts: ["Task_B2_SHA256_soft_gate_handoff_writer"] + wait_until: ["Task_B12_gate_llm_adjudicator"] + + Task_B2_SHA256_soft_gate_handoff_writer: + nexts: ["OUT"] + wait_until: ["Task_B12_gate_audit_finalizer"] + + OUT: + nexts: [] + wait_until: ["Task_B2_SHA256_soft_gate_handoff_writer"] + + prevs: [] + nexts: [] diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Claude YAML/Stage_1_Part_2_Claude_v3.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Claude YAML/Stage_1_Part_2_Claude_v3.yml new file mode 100644 index 00000000..a514060a --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Claude YAML/Stage_1_Part_2_Claude_v3.yml @@ -0,0 +1,5280 @@ +--- +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_2 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Part 2 + version: v.2 + Stages: + - name: stage1_BO_시그널_생성 + description: BO 생성, 시그널 생성 + llm_provider: openai + llm_model: gpt-4o-2024-08-06 + tools: + mcpServers: + localdocs: + type: streamable-http + url: http://mcp-localdocs:8012/mcp + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + tasks: + - task_name: Task_C_BO_A0_context_and_domain_slice_compiler + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: |- + #!/usr/bin/env python3 + from __future__ import annotations + import hashlib + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + + REQUIRED_FILES = [ + "client_meeting.md", + "evidence_event_candidates.json", + "evidence_indexed.json", + "Default_Agent/Juristic_Act.md", + ] + PART1_SOFT_GATE_HANDOFF_FILE = "quality_gates/stage1_part1_soft_gate_handoff.json" + + CURRENT_STATE_PATTERNS = [ + "현재", "현재까지", "현재도", "계속", "거주", "점유", "사용", "운영", + "부지로만 사용", "인도받아", "사업자등록", "유치권", "임대", "수익", + ] + CONFLICT_PATTERNS = [ + "하였음에도", "그럼에도", "불구하고", "거절", "응하지", "주장", + "항변", "다툼", "무효", "취소", "해제", "해지", "말소", "신뢰", + ] + SUCCESSION_PATTERNS = [ + "사망", "상속", "상속포기", "배우자", "직계비속", "직계존속", + "피상속인", "상속인", "수리", "가족관계", + ] + MIXED_CONTRACT_PATTERNS = [ + "임대차", "매매", "보증금", "매매대금", "인도", "관리", "사용", + "사용수익", "영업양도", "채무불승계", "대물변제", + ] + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-c-bo-a0-context-and-domain-slice-compiler", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _mid(), + "method": "tools/call", + "params": {"name": name, "arguments": args}, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + p = _parse(r.text) + if not p or "result" not in p: + raise RuntimeError(f"MCP {name} failed") + return p + + def _unwrap(p: Any, name: str) -> Any: + text = (p["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError(f"Empty: {name}") + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, dict) and "results" in outer: + return outer + return outer + + def _stable_hash(value: Any) -> str: + if isinstance(value, str): + raw = value + else: + raw = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _text(value: Any) -> str | None: + if value is None: + return None + s = str(value).strip() + return s or None + + def _norm_space(value: Any) -> str: + return re.sub(r"\s+", " ", str(value or "")).strip() + + def _norm_key(value: Any) -> str: + text = str(value or "").strip().lower() + return "".join(ch for ch in text if ch.isalnum()) + + def _short_excerpt(value: Any, limit: int = 120) -> str: + text = _norm_space(value) + return text if len(text) <= limit else text[: limit - 1] + "…" + + def _section_key_from_heading(heading: str, seq: int) -> str: + normalized = _norm_key(heading) + if "상담내용" in normalized: + suffix = "consultation" + elif "사건관계인의주장" in normalized or ("사건관계인" in normalized and "주장" in normalized): + suffix = "party_assertions" + elif "희망사항" in normalized or "의뢰인희망" in normalized: + suffix = "client_requests" + elif normalized: + suffix = normalized[:24] + else: + suffix = "unnamed" + return f"s{seq:02d}_{suffix}" + + def _uniq(values: list[Any]) -> list[Any]: + seen: set[str] = set() + out: list[Any] = [] + for value in values: + marker = json.dumps(value, ensure_ascii=False, sort_keys=True) if isinstance(value, (dict, list)) else str(value) + if marker not in seen: + seen.add(marker) + out.append(value) + return out + + def _extract_doc_names(payload: Any) -> list[str]: + names: list[str] = [] + def walk(v: Any) -> None: + if isinstance(v, str): + if "." in v or "/" in v: + names.append(v) + names.extend(re.findall(r"[\w./()가-힣ᄀ-ᇿ -]+\.[A-Za-z0-9]+", v)) + elif isinstance(v, list): + for item in v: + walk(item) + elif isinstance(v, dict): + for key in ("name", "path", "doc_name", "document_name", "file", "filename"): + if v.get(key): + names.append(str(v[key])) + for val in v.values(): + if isinstance(val, (list, dict)): + walk(val) + walk(payload) + return sorted(set(names)) + + def _doc_name_matches(found_names: list[str], required: str) -> bool: + required_norm = required.strip("/") + required_base = required_norm.split("/")[-1] + for name in found_names: + n = name.strip("/") + if n == required_norm or n.endswith("/" + required_norm) or n.split("/")[-1] == required_base: + return True + if required_norm in n or required_base in n: + return True + return False + + def _preflight_required_files() -> dict[str, Any]: + found_names_set: set[str] = set() + list_docs_args = ( + {"path": "/", "pattern": "*", "recursive": True}, + {"path": ".", "pattern": "*", "recursive": True}, + {"path": "/", "pattern": "*"}, + {"path": ".", "pattern": "*"}, + {"path": "Default_Agent", "pattern": "*"}, + {"path": "quality_gates", "pattern": "*.json"}, + ) + for args in list_docs_args: + try: + payload = _unwrap(_call("list_docs", args), "list_docs") + found_names_set.update(_extract_doc_names(payload)) + except Exception: + continue + found_names_now = sorted(found_names_set) + required_ready = all(_doc_name_matches(found_names_now, path) for path in REQUIRED_FILES) + sidecar_checked = _doc_name_matches(found_names_now, PART1_SOFT_GATE_HANDOFF_FILE) or args.get("path") == "quality_gates" + if required_ready and sidecar_checked: + break + found_names = sorted(found_names_set) + if not found_names: + raise RuntimeError("list_docs returned no recognizable document names for required inputs") + missing = [path for path in REQUIRED_FILES if not _doc_name_matches(found_names, path)] + if missing: + raise RuntimeError(f"Missing required input files by list_docs: {missing}") + return {"found_names": found_names, "required_files": REQUIRED_FILES} + + def _read_required_docs_once() -> dict[str, str]: + payload = _unwrap(_call("read_docs", {"doc_names": REQUIRED_FILES}), "read_docs") + docs: dict[str, str] = {} + + def assign(name: str | None, content: Any) -> None: + if not name: + return + text = content if isinstance(content, str) else json.dumps(content, ensure_ascii=False) + for required in REQUIRED_FILES: + if name == required or name.endswith("/" + required) or name.split("/")[-1] == required.split("/")[-1]: + docs[required] = text + + if isinstance(payload, dict) and isinstance(payload.get("results"), list): + for item in payload["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + content = item.get("content") or item.get("text") or item.get("markdown") or item.get("raw") + assign(str(name) if name else None, content) + elif isinstance(payload, dict): + for required in REQUIRED_FILES: + if required in payload: + assign(required, payload[required]) + elif isinstance(payload, str): + if len(REQUIRED_FILES) == 1: + assign(REQUIRED_FILES[0], payload) + + missing = [name for name in REQUIRED_FILES if name not in docs] + if missing: + # Some Localdocs runtimes return one JSON wrapper inside the text field. + raw = payload if isinstance(payload, str) else json.dumps(payload, ensure_ascii=False) + try: + outer = json.loads(raw) + if isinstance(outer, dict) and isinstance(outer.get("results"), list): + for item in outer["results"]: + if isinstance(item, dict): + name = item.get("name") or item.get("path") or item.get("doc_name") + content = item.get("content") or item.get("text") + assign(str(name) if name else None, content) + except Exception: + pass + missing = [name for name in REQUIRED_FILES if name not in docs] + if missing: + raise RuntimeError(f"read_docs did not return required inputs: {missing}") + for name, content in docs.items(): + if not str(content).strip(): + raise RuntimeError(f"Required input is empty: {name}") + return docs + + def _parse_json_doc(name: str, raw: str) -> Any: + try: + return json.loads(raw.strip()) + except json.JSONDecodeError as exc: + try: + obj, _ = json.JSONDecoder().raw_decode(raw.strip()) + return obj + except json.JSONDecodeError: + raise RuntimeError(f"Strict JSON parse failed: {name}: {exc.msg} at line {exc.lineno} column {exc.colno}") from exc + + def _items(obj: Any, *keys: str) -> list[Any]: + if isinstance(obj, list): + return obj + if isinstance(obj, dict): + for key in keys + ("items", "evidence_indexed", "documents", "results"): + val = obj.get(key) + if isinstance(val, list): + return val + return [] + + def _extract_keywords(text: str, patterns: list[str]) -> list[str]: + return [p for p in patterns if p in text] + + def _build_meeting_clause_map(meeting_text: str) -> dict[str, Any]: + clauses: list[dict[str, Any]] = [] + current_major: str | None = None + current: dict[str, Any] | None = None + section_seq = 0 + current_section_key = "preamble" + current_section_label = "preamble" + + section_re = re.compile(r"^\s*#?\s*【\s*(.+?)\s*】\s*$") + major_re = re.compile(r"^\s*(\d+)\.\s+(.+?)\s*$") + sub_re = re.compile(r"^\s*([가-힣])\.\s+(.+?)\s*$") + + def flush(end_line: int) -> None: + nonlocal current + if not current: + return + current["line_end"] = end_line + current["text"] = _norm_space(current.get("text", "")) + text = current["text"] + current["tags"] = { + "current_state": _extract_keywords(text, CURRENT_STATE_PATTERNS), + "conflict_pattern": _extract_keywords(text, CONFLICT_PATTERNS), + "succession": _extract_keywords(text, SUCCESSION_PATTERNS), + "mixed_contract": _extract_keywords(text, MIXED_CONTRACT_PATTERNS), + } + current["text_excerpt"] = _short_excerpt(text) + clauses.append(current) + current = None + + lines = meeting_text.splitlines() + for idx, line in enumerate(lines, 1): + stripped = line.strip() + if not stripped or stripped.startswith("|") or stripped.startswith("---"): + continue + m_section = section_re.match(stripped) + m_major = major_re.match(stripped) + m_sub = sub_re.match(stripped) + if m_section: + flush(idx - 1) + section_seq += 1 + current_section_label = _norm_space(m_section.group(1)) + current_section_key = _section_key_from_heading(current_section_label, section_seq) + current_major = None + elif m_major: + flush(idx - 1) + current_major = m_major.group(1) + current = { + "clause_id": f"{current_section_key}:{current_major}", + "local_clause_id": current_major, + "section_key": current_section_key, + "section_label": current_section_label, + "major_no": current_major, + "sub_no": None, + "heading": _norm_space(m_major.group(2)), + "line_start": idx, + "text": _norm_space(m_major.group(2)), + } + elif m_sub and current_major: + flush(idx - 1) + sub_no = m_sub.group(1) + current = { + "clause_id": f"{current_section_key}:{current_major}-{sub_no}", + "local_clause_id": f"{current_major}-{sub_no}", + "section_key": current_section_key, + "section_label": current_section_label, + "major_no": current_major, + "sub_no": sub_no, + "heading": None, + "line_start": idx, + "text": _norm_space(m_sub.group(2)), + } + elif current: + current["text"] = _norm_space(str(current.get("text", "")) + " " + stripped) + flush(len(lines)) + + if not clauses: + raise RuntimeError("client_meeting.md parse produced no numbered meeting clauses") + + by_id: dict[str, Any] = {} + duplicate_clause_ids: list[str] = [] + for clause in clauses: + cid = clause["clause_id"] + if cid in by_id: + duplicate_clause_ids.append(cid) + by_id[cid] = clause + if duplicate_clause_ids: + raise RuntimeError(f"Duplicate meeting clause ids after section normalization: {duplicate_clause_ids}") + return { + "schema_version": "meeting_clause_map.v1", + "clause_order": [c["clause_id"] for c in clauses], + "by_clause_id": by_id, + "sections": _uniq([{"section_key": c["section_key"], "section_label": c["section_label"]} for c in clauses]), + "current_state_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["current_state"]], + "conflict_pattern_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["conflict_pattern"]], + "succession_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["succession"]], + "mixed_contract_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["mixed_contract"]], + } + + def _candidate_id(candidate: dict[str, Any], evidence_index: str, seq: int) -> str: + return str(candidate.get("candidate_id") or candidate.get("candidate_id_proposed") or f"internal_missing_candidate_id::{evidence_index}::{seq:02d}") + + def _evidence_index_from_item(item: dict[str, Any]) -> str | None: + return _text(item.get("evidence_index") or item.get("evidence_index_proposed") or item.get("source_evidence_index")) + + def _build_event_candidate_map(event_obj: Any) -> dict[str, Any]: + items = _items(event_obj) + if not items: + raise RuntimeError("evidence_event_candidates.json parse produced no document-level items") + by_candidate_id: dict[str, Any] = {} + by_evidence_index: dict[str, list[str]] = {} + by_meeting_clause_ref: dict[str, list[str]] = {} + state_candidate_ids: list[str] = [] + succession_candidate_ids: list[str] = [] + mixed_contract_candidate_ids: list[str] = [] + zero_event_evidence_indexes: list[str] = [] + asset_cluster_values: list[str] = [] + malformed_item_refs: list[str] = [] + + for item_seq, item in enumerate(items, 1): + if not isinstance(item, dict): + malformed_item_refs.append(f"item#{item_seq}: non-object") + continue + evidence_index = _evidence_index_from_item(item) + if not evidence_index: + malformed_item_refs.append(f"item#{item_seq}: missing evidence_index") + continue + item_ref = evidence_index + event_candidates_raw = item.get("event_candidates") + has_event_candidates_list = isinstance(event_candidates_raw, list) + has_event_candidates = has_event_candidates_list and len(event_candidates_raw) > 0 + has_zero_event_reason = bool(item.get("zero_event_reason")) + if event_candidates_raw is not None and not has_event_candidates_list: + malformed_item_refs.append(f"{item_ref}: event_candidates must be a list when present") + continue + if not has_event_candidates and not has_zero_event_reason: + malformed_item_refs.append(f"{item_ref}: requires non-empty event_candidates or zero_event_reason") + continue + if item.get("zero_event_reason"): + zero_event_evidence_indexes.append(evidence_index) + item_flags = item.get("doc_semantic_flags") if isinstance(item.get("doc_semantic_flags"), list) else [] + property_cluster_id = _text(item.get("property_cluster_id")) + if property_cluster_id: + asset_cluster_values.append(property_cluster_id) + for asset in item.get("affected_asset_cluster_ids") or []: + if _text(asset): + asset_cluster_values.append(str(asset)) + for seq, cand in enumerate(event_candidates_raw or [], 1): + if not isinstance(cand, dict): + malformed_item_refs.append(f"{item_ref}: event_candidates[{seq}] must be an object") + continue + cid = _candidate_id(cand, evidence_index, seq) + refs = cand.get("meeting_clause_refs") if isinstance(cand.get("meeting_clause_refs"), list) else [] + cand_flags = cand.get("doc_semantic_flags") if isinstance(cand.get("doc_semantic_flags"), list) else [] + candidate_flags = _uniq(item_flags + cand_flags) + record = { + "candidate_id": cid, + "source_evidence_index": _text(cand.get("source_evidence_index") or cand.get("source_evidence_index_proposed")) or evidence_index, + "title": item.get("title"), + "doc_type": item.get("doc_type"), + "event_or_state": cand.get("event_or_state"), + "event_kind": cand.get("event_kind"), + "event_subkind": cand.get("event_subkind"), + "action_type_candidate": cand.get("action_type_candidate"), + "action_summary": cand.get("action_summary"), + "participants": cand.get("participants") or {}, + "object_spec": cand.get("object_spec"), + "amount": cand.get("amount"), + "event_date": cand.get("event_date"), + "event_end_date": cand.get("event_end_date"), + "time_text": cand.get("time_text"), + "time_precision": cand.get("time_precision"), + "current_state_candidate": cand.get("current_state_candidate"), + "succession_candidate": cand.get("succession_candidate"), + "legal_effect_candidate": cand.get("legal_effect_candidate"), + "defense_candidate": cand.get("defense_candidate"), + "commercial_successor_candidates": cand.get("commercial_successor_candidates"), + "meeting_clause_refs": refs, + "actio_relevance_candidates": cand.get("actio_relevance_candidates"), + "identity_signature": cand.get("identity_signature"), + "property_cluster_id": property_cluster_id, + "affected_asset_cluster_ids": item.get("affected_asset_cluster_ids") or [], + "doc_semantic_flags": candidate_flags, + "fraction_ref": cand.get("fraction_ref"), + "confidence": cand.get("confidence"), + } + by_candidate_id[cid] = record + by_evidence_index.setdefault(evidence_index, []).append(cid) + for ref in refs: + by_meeting_clause_ref.setdefault(str(ref), []).append(cid) + if cand.get("event_or_state") == "state" or cand.get("current_state_candidate"): + state_candidate_ids.append(cid) + if cand.get("succession_candidate"): + succession_candidate_ids.append(cid) + if "mixed_contract_doc" in candidate_flags or cand.get("commercial_successor_candidates"): + mixed_contract_candidate_ids.append(cid) + if _text(cand.get("object_spec")): + asset_cluster_values.append(str(cand.get("object_spec"))) + + if malformed_item_refs: + raise RuntimeError(f"Malformed evidence_event_candidates document items: {malformed_item_refs[:20]}") + + return { + "schema_version": "event_candidate_map.v2", + "candidate_count": len(by_candidate_id), + "candidate_id_set": sorted(by_candidate_id.keys()), + "by_candidate_id": by_candidate_id, + "by_evidence_index": {k: _uniq(v) for k, v in by_evidence_index.items()}, + "by_meeting_clause_ref": {k: _uniq(v) for k, v in by_meeting_clause_ref.items()}, + "state_candidate_ids": _uniq(state_candidate_ids), + "succession_candidate_ids": _uniq(succession_candidate_ids), + "mixed_contract_candidate_ids": _uniq(mixed_contract_candidate_ids), + "zero_event_evidence_indexes": _uniq(zero_event_evidence_indexes), + "asset_cluster_values": _uniq([v for v in asset_cluster_values if _text(v)]), + } + + def _component_count(item: dict[str, Any], key: str) -> int: + val = item.get(key) + if isinstance(val, list): + return len(val) + if isinstance(val, dict): + return len(val) + return 1 if val not in (None, "", []) else 0 + + COMPONENT_DETAIL_PROJECTION = { + "loan_components": ["loan_component_id", "principal_amount", "loan_date", "due_date", + "agreed_interest_rate", "agreed_delay_damage_rate", "rate_text", + "rate_source_locator", "interest_accrual_start_hint"], + "secured_debt_components": ["agreed_interest_rate", "agreed_delay_damage_rate", "rate_text", + "rate_source_locator", "due_date"], + "notice_components": ["notice_id_proposed", "notice_type", "drafted_date", "dispatch_date", + "arrival_date", "received_date", "legal_effect_date_candidate", + "requires_receipt_for_effect", "receipt_evidence_refs", + "recipient_admission_text", "sender_candidates", "receiver_candidates"], + "registry_row_components": ["registry_ref", "fraction_ref", "fraction_display", + "fraction_numerator", "fraction_denominator", "receipt_no"], + "schedule_items": ["schedule_ref", "canonical_label_candidate", "usable_in_relief"], + } + + def _component_details(item: dict[str, Any]) -> dict[str, Any]: + details: dict[str, Any] = {} + for key, fields in COMPONENT_DETAIL_PROJECTION.items(): + rows = item.get(key) + if isinstance(rows, dict): + rows = [rows] + if not isinstance(rows, list): + continue + projected = [{field: row.get(field) for field in fields if field in row} for row in rows if isinstance(row, dict)] + projected = [p for p in projected if p] + if projected: + details[key] = projected + return details + + def _build_evidence_authority_map(evidence_obj: Any) -> dict[str, Any]: + items = _items(evidence_obj, "evidence_indexed") + if not items: + raise RuntimeError("evidence_indexed.json parse produced no evidence authority items") + by_evidence_index: dict[str, Any] = {} + by_doc_type: dict[str, list[str]] = {} + by_title_norm: dict[str, list[str]] = {} + semantic_flag_index: dict[str, list[str]] = {} + evidence_index_set: list[str] = [] + malformed_item_refs: list[str] = [] + duplicate_evidence_indexes: list[str] = [] + + component_keys = [ + "contract_components", "valuation_object", "legal_calculation_object", + "legal_calculation_objects", "claim_document_components", + "business_transfer_components", "registry_row_components", + "secured_debt_components", "valuation_rows", "mixed_contract_payment_semantics", + "loan_components", "valuation_matrix", "registry_row_role_map", + "defense_assertions", "admission_components", "defense_components", + "notice_components", "party_assertion_components", "schedule_items", + "registry_object_display", "succession_document_components", + ] + + for item_seq, item in enumerate(items, 1): + if not isinstance(item, dict): + malformed_item_refs.append(f"item#{item_seq}: non-object") + continue + evidence_index = _text(item.get("evidence_index") or item.get("evidence_index_proposed")) + if not evidence_index: + malformed_item_refs.append(f"item#{item_seq}: missing finalized evidence_index") + continue + if evidence_index in by_evidence_index: + duplicate_evidence_indexes.append(evidence_index) + continue + flags = item.get("doc_semantic_flags") if isinstance(item.get("doc_semantic_flags"), list) else [] + component_summary = {key: _component_count(item, key) for key in component_keys} + record = { + "evidence_index": evidence_index, + "doc_uid": item.get("doc_uid"), + "title": item.get("title"), + "title_normalized": item.get("title_normalized") or _norm_space(item.get("title")), + "doc_type": item.get("doc_type"), + "key_facts": item.get("key_facts") or [], + "key_dates": item.get("key_dates") or [], + "key_amounts": item.get("key_amounts") or [], + "key_parties": item.get("key_parties") or [], + "source_pointer": item.get("source_pointer") or {}, + "property_cluster_id": item.get("property_cluster_id"), + "doc_semantic_flags": flags, + "multi_event_registry": item.get("multi_event_registry"), + "component_summary": component_summary, + "component_details": _component_details(item), + } + by_evidence_index[evidence_index] = record + evidence_index_set.append(evidence_index) + by_doc_type.setdefault(str(item.get("doc_type") or "unknown"), []).append(evidence_index) + title_key = _norm_key(record["title_normalized"]) + if title_key: + by_title_norm.setdefault(title_key, []).append(evidence_index) + for flag in flags: + semantic_flag_index.setdefault(str(flag), []).append(evidence_index) + + if malformed_item_refs: + raise RuntimeError(f"Malformed evidence_indexed authority items: {malformed_item_refs[:20]}") + if duplicate_evidence_indexes: + raise RuntimeError(f"Duplicate evidence_index values in evidence_indexed.json: {_uniq(duplicate_evidence_indexes)}") + + return { + "schema_version": "evidence_authority_map.v2", + "evidence_index_set": _sort_evidence_indexes(_uniq(evidence_index_set)), + "by_evidence_index": by_evidence_index, + "by_doc_type": {k: _sort_evidence_indexes(_uniq(v)) for k, v in by_doc_type.items()}, + "by_title_norm": {k: _sort_evidence_indexes(_uniq(v)) for k, v in by_title_norm.items()}, + "semantic_flag_index": {k: _sort_evidence_indexes(_uniq(v)) for k, v in semantic_flag_index.items()}, + } + + def _sort_evidence_indexes(values: list[str]) -> list[str]: + def key(v: str) -> tuple[int, str]: + m = re.search(r"E-(\d+)", str(v)) + return (int(m.group(1)) if m else 10**9, str(v)) + return sorted(values, key=key) + + def _split_examples(value: str) -> list[str]: + parts = re.split(r"[,,、]| / |/|ㆍ|·", value or "") + return [_norm_space(p) for p in parts if _norm_space(p)] + + def _build_juristic_act_map(markdown: str) -> dict[str, Any]: + categories: dict[str, Any] = {} + example_to_gubun: dict[str, list[str]] = {} + for line in markdown.splitlines(): + if "|" not in line: + continue + cells = [_norm_space(c) for c in line.strip().strip("|").split("|")] + if len(cells) < 3: + continue + if cells[0] in ("구분", "---") or set(cells[0]) <= {"-"}: + continue + gubun, content, examples = cells[0], cells[1], cells[2] + if not gubun or gubun.startswith("-"): + continue + example_list = _split_examples(examples) + categories[gubun] = {"content": content, "examples": example_list} + for ex in example_list: + example_to_gubun.setdefault(_norm_key(ex), []).append(gubun) + + axis_groups = { + "structure": ["단독행위", "계약행위", "합동행위"], + "form": ["요식(要式)행위", "불요식행위"], + "time_effect": ["생전 행위", "사후(또는 사인) 행위"], + "legal_object": ["채권행위", "물권행위", "준물권행위"], + "contribution": ["출연행위", "비출연행위"], + "dependency": ["주(主)된 행위", "종(從)된 행위", "유인 행위", "무인 행위"], + "defect_or_condition": ["무효 행위", "취소가능한 법률 행위", "조건부 법률행위", "기한부 법률행위"], + "controversy": ["견해대립있는 행위"], + } + + return { + "schema_version": "juristic_act_taxonomy_map.v1", + "categories": categories, + "example_to_gubun": {k: _uniq(v) for k, v in example_to_gubun.items()}, + "axis_groups": axis_groups, + "source_digest_sha256": _stable_hash(markdown), + } + + def _validate_juristic_act_map(juristic_map: dict[str, Any]) -> None: + categories = juristic_map.get("categories") or {} + example_to_gubun = juristic_map.get("example_to_gubun") or {} + required = ["단독행위", "계약행위", "채권행위", "물권행위"] + missing = [name for name in required if name not in categories] + if not categories or not example_to_gubun: + raise RuntimeError("JuristicAct taxonomy parse produced empty categories or examples") + if missing: + raise RuntimeError(f"JuristicAct taxonomy missing required categories: {missing}") + + def _collect_asset_clusters(meeting_map: dict[str, Any], event_map: dict[str, Any], evidence_map: dict[str, Any]) -> list[str]: + values: list[str] = [] + asset_hint_re = re.compile(r"([가-힣A-Za-z0-9·ㆍ() \-]{1,30}(?:대지|건물|상가|빌라|임야|부동산|지분|영업|상호|채권|대금))") + for clause in meeting_map.get("by_clause_id", {}).values(): + text = str(clause.get("text") or "") + values.extend([_norm_space(m.group(1)) for m in asset_hint_re.finditer(text)]) + values.extend(event_map.get("asset_cluster_values") or []) + for ev in evidence_map.get("by_evidence_index", {}).values(): + if _text(ev.get("property_cluster_id")): + values.append(str(ev.get("property_cluster_id"))) + for fact in ev.get("key_facts") or []: + values.extend([_norm_space(m.group(1)) for m in asset_hint_re.finditer(str(fact))]) + return _uniq([v for v in values if _text(v)]) + + def _build_global_hazard_seed(meeting_map: dict[str, Any], event_map: dict[str, Any], evidence_map: dict[str, Any]) -> dict[str, Any]: + evidence_index_set = evidence_map.get("evidence_index_set") or [] + current_state_ids = event_map.get("state_candidate_ids") or [] + succession_candidate_ids = event_map.get("succession_candidate_ids") or [] + mixed_contract_candidate_ids = event_map.get("mixed_contract_candidate_ids") or [] + + def meeting_expression_records(clause_ids: list[str], tag_key: str) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + by_clause = meeting_map.get("by_clause_id", {}) + for cid in clause_ids: + clause = by_clause.get(cid) or {} + matched = (clause.get("tags") or {}).get(tag_key) or [] + out.append({ + "source": "client_meeting.md", + "clause_id": cid, + "local_clause_id": clause.get("local_clause_id"), + "section_key": clause.get("section_key"), + "matched_patterns": matched, + "text_excerpt": clause.get("text_excerpt") or _short_excerpt(clause.get("text")), + }) + return out + + def event_expression_records(candidate_ids: list[str]) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + by_candidate = event_map.get("by_candidate_id", {}) + for cid in candidate_ids: + cand = by_candidate.get(cid) or {} + text = " ".join(str(cand.get(k) or "") for k in ("action_summary", "event_kind", "event_subkind", "identity_signature")) + matched = _uniq( + _extract_keywords(text, CURRENT_STATE_PATTERNS) + + _extract_keywords(text, CONFLICT_PATTERNS) + + _extract_keywords(text, SUCCESSION_PATTERNS) + + _extract_keywords(text, MIXED_CONTRACT_PATTERNS) + ) + out.append({ + "source": "evidence_event_candidates.json", + "candidate_id": cid, + "source_evidence_index": cand.get("source_evidence_index"), + "event_or_state": cand.get("event_or_state"), + "event_kind": cand.get("event_kind"), + "matched_patterns": matched, + "text_excerpt": _short_excerpt(cand.get("action_summary") or cand.get("identity_signature")), + }) + return out + + conflict_candidate_ids: list[str] = [] + for cid, cand in event_map.get("by_candidate_id", {}).items(): + combined = " ".join( + str(cand.get(k) or "") + for k in ("action_summary", "event_kind", "event_subkind", "identity_signature") + ) + if _extract_keywords(combined, CONFLICT_PATTERNS): + conflict_candidate_ids.append(cid) + + succession_evidence_indexes = [] + mixed_contract_evidence_indexes = [] + for eidx, ev in evidence_map.get("by_evidence_index", {}).items(): + flags = ev.get("doc_semantic_flags") or [] + title_and_facts = " ".join([str(ev.get("title") or "")] + [str(x) for x in ev.get("key_facts") or []]) + if "succession_doc" in flags or _extract_keywords(title_and_facts, SUCCESSION_PATTERNS): + succession_evidence_indexes.append(eidx) + if "mixed_contract_doc" in flags or _extract_keywords(title_and_facts, MIXED_CONTRACT_PATTERNS): + mixed_contract_evidence_indexes.append(eidx) + + return { + "schema_version": "global_hazard_seed.v1", + "meeting_clause_ids": meeting_map.get("clause_order") or [], + "current_state_expressions": { + "meeting_clause_ids": meeting_map.get("current_state_clause_ids") or [], + "event_candidate_ids": current_state_ids, + "meeting_expressions": meeting_expression_records(meeting_map.get("current_state_clause_ids") or [], "current_state"), + "event_expressions": event_expression_records(current_state_ids), + }, + "conflict_pattern_expressions": { + "meeting_clause_ids": meeting_map.get("conflict_pattern_clause_ids") or [], + "event_candidate_ids": _uniq(conflict_candidate_ids), + "meeting_expressions": meeting_expression_records(meeting_map.get("conflict_pattern_clause_ids") or [], "conflict_pattern"), + "event_expressions": event_expression_records(_uniq(conflict_candidate_ids)), + }, + "asset_clusters": _collect_asset_clusters(meeting_map, event_map, evidence_map), + "succession_related_candidates": { + "meeting_clause_ids": meeting_map.get("succession_clause_ids") or [], + "event_candidate_ids": succession_candidate_ids, + "evidence_indexes": _sort_evidence_indexes(_uniq(succession_evidence_indexes)), + "meeting_expressions": meeting_expression_records(meeting_map.get("succession_clause_ids") or [], "succession"), + "event_expressions": event_expression_records(succession_candidate_ids), + }, + "mixed_contract_candidates": { + "meeting_clause_ids": meeting_map.get("mixed_contract_clause_ids") or [], + "event_candidate_ids": mixed_contract_candidate_ids, + "evidence_indexes": _sort_evidence_indexes(_uniq(mixed_contract_evidence_indexes)), + "meeting_expressions": meeting_expression_records(meeting_map.get("mixed_contract_clause_ids") or [], "mixed_contract"), + "event_expressions": event_expression_records(mixed_contract_candidate_ids), + }, + "evidence_index_set": evidence_index_set, + } + + def _read_optional_doc_once(doc_name: str) -> str | None: + try: + payload = _unwrap(_call("read_docs", {"doc_names": [doc_name]}), "read_docs_optional") + except Exception: + return None + if isinstance(payload, dict) and isinstance(payload.get("results"), list): + for item in payload["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + content = item.get("content") or item.get("text") or item.get("markdown") or item.get("raw") + if name and (str(name) == doc_name or str(name).endswith("/" + doc_name) or str(name).split("/")[-1] == doc_name.split("/")[-1]): + return content if isinstance(content, str) else json.dumps(content, ensure_ascii=False) + if isinstance(payload, dict) and doc_name in payload: + val = payload[doc_name] + return val if isinstance(val, str) else json.dumps(val, ensure_ascii=False) + if isinstance(payload, str) and payload.strip(): + return payload + return None + + def _compact_part1_soft_gate_handoff(existence_check: dict[str, Any], input_digests: dict[str, str], meeting_map: dict[str, Any], event_map: dict[str, Any], evidence_map: dict[str, Any]) -> dict[str, Any]: + found_names = existence_check.get("found_names") or [] + base = { + "schema_version": "stage1_part1_soft_gate_handoff.v1", + "status": "ABSENT", + "stage2_auto_progression_allowed": True, + "review_gate_count": 0, + "highest_review_severity": "PASS", + "review_items_compact": [], + "routing_index": {}, + "lawyer_workflow_queues": {}, + "dropped_review_items": [], + "usage_rules": { + "not_factual_authority": True, + "must_not_create_fact_without_stage_a_source": True, + "must_preserve_as_needs_review_if_used": True, + "must_not_override_stage_e_final_progression": True, + "must_not_override_stage_f_single_writer_gate": True, + }, + } + if not _doc_name_matches(found_names, PART1_SOFT_GATE_HANDOFF_FILE): + return base + raw = _read_optional_doc_once(PART1_SOFT_GATE_HANDOFF_FILE) + if not raw: + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": "sidecar listed but unreadable"}) + return base + try: + sidecar = json.loads(raw.strip()) + except Exception as exc: + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": f"sidecar JSON parse failed: {exc}"}) + return base + if not isinstance(sidecar, dict) or sidecar.get("schema_version") != "stage1_part1_soft_gate_handoff.v1": + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": "wrong or missing schema_version"}) + return base + if sidecar.get("handoff_status") == "BLOCKED" or sidecar.get("stage2_auto_progression_allowed") is False: + raise RuntimeError("Part 1 soft gate handoff is BLOCKED; Part 2-6 cannot proceed") + guard = sidecar.get("digest_guard") if isinstance(sidecar.get("digest_guard"), dict) else {} + invalid_guard_fields = [] + stale = [] + for guard_key, doc_name in [("evidence_indexed_sha256", "evidence_indexed.json"), ("evidence_event_candidates_sha256", "evidence_event_candidates.json")]: + val = guard.get(guard_key) + if not isinstance(val, str) or not re.fullmatch(r"[0-9a-f]{64}", val): + invalid_guard_fields.append(guard_key) + elif val != input_digests.get(doc_name): + stale.append(guard_key) + if invalid_guard_fields: + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": "digest_guard missing real sha256", "fields": invalid_guard_fields}) + return base + if stale: + base["status"] = "IGNORED_STALE" + base["dropped_review_items"].append({"reason": "digest_guard mismatch", "fields": stale}) + return base + evidence_set = set(evidence_map.get("evidence_index_set") or []) + event_set = set(event_map.get("candidate_id_set") or []) + meeting_set = set(meeting_map.get("clause_order") or []) + compact = [] + dropped = [] + for idx, item in enumerate(sidecar.get("review_items") or []): + if len(compact) >= 40: + dropped.append({"index": idx, "reason": "review_items_compact max 40 reached"}) + continue + if not isinstance(item, dict): + dropped.append({"index": idx, "reason": "non-object review item"}) + continue + refs = item.get("source_refs") if isinstance(item.get("source_refs"), dict) else {} + bad_e = [x for x in refs.get("evidence_indexes", []) if x not in evidence_set] + bad_ev = [x for x in refs.get("event_candidate_ids", []) if x not in event_set] + bad_m = [x for x in refs.get("meeting_clause_refs", []) if x not in meeting_set] + if bad_e or bad_ev or bad_m: + dropped.append({"review_id": item.get("review_id"), "reason": "source_refs membership failed", "bad_evidence_indexes": bad_e, "bad_event_candidate_ids": bad_ev, "bad_meeting_clause_refs": bad_m}) + continue + compact.append({ + "review_id": item.get("review_id"), + "severity": item.get("severity"), + "issue_type": item.get("issue_type"), + "lawyer_workflow_role": item.get("lawyer_workflow_role"), + "domain_hints": item.get("domain_hints") or [], + "target_stage_hints": item.get("target_stage_hints") or [], + "target_task_hints": item.get("target_task_hints") or [], + "source_refs": refs, + "candidate_use_policy": item.get("candidate_use_policy"), + "review_instruction": item.get("review_instruction"), + "negative_instruction": item.get("negative_instruction"), + }) + summary = sidecar.get("review_gate_summary") if isinstance(sidecar.get("review_gate_summary"), dict) else {} + base.update({ + "status": sidecar.get("handoff_status") if sidecar.get("handoff_status") in {"READY_WITH_REVIEW", "READY_NO_REVIEW"} else "READY_WITH_REVIEW", + "stage2_auto_progression_allowed": sidecar.get("stage2_auto_progression_allowed") is not False, + "review_gate_count": len(compact), + "highest_review_severity": summary.get("highest_review_severity") or ("HARD_WARNING" if any(i.get("severity") == "HARD_WARNING" for i in compact) else ("SOFT_WARNING" if compact else "PASS")), + "review_items_compact": compact, + "routing_index": sidecar.get("routing_index") if isinstance(sidecar.get("routing_index"), dict) else {}, + "lawyer_workflow_queues": sidecar.get("lawyer_workflow_queues") if isinstance(sidecar.get("lawyer_workflow_queues"), dict) else {}, + "dropped_review_items": dropped, + }) + return base + + + + # ==================== A0 domain slicing (B0 x5 통합; 3등급 라우팅 = 개선전략서 X-1) ==================== + A0_DOMAIN_CONFIGS = { + "B1_Money_Successor": { + "domain_id": "B1_Money_Successor", + "domain_label": "금전채권 및 상호속용 책임", + "allowed_legal_effect_bo_types": [ + "money_claim_chain_BO", + "commercial_successor_liability_chain_BO", + "liability_group_hint_BO", + ], + "terms": [ + "대여금", "차용", "매매대금", "물품대금", "보증금", "정산금", "공사대금", + "원금", "이자", "지연손해금", "변제기", "이행기", "일부변제", "미변제", + "채무승인", "지급각서", "확인서", "최고", "통지", "상호속용", "영업양도", + "양수인", "채무불승계", "business_transfer", "loan", "payment", "admission", + "notice", "claim_document", "mixed_contract_payment", + ], + "meeting_sources": ["mixed_contract_clause_ids", "conflict_pattern_clause_ids"], + "event_sources": ["mixed_contract_candidate_ids"], + "hazard_keys": ["mixed_contract_candidates", "conflict_pattern_expressions", "asset_clusters"], + "taxonomy_axes": ["legal_object", "structure", "dependency"], + }, + "B2_Secured_Registry": { + "domain_id": "B2_Secured_Registry", + "domain_label": "담보채무, 변제충당, 등기무효, 경매", + "allowed_legal_effect_bo_types": [ + "secured_debt_chain_BO", + "payment_allocation_and_mortgage_extinguishment_BO", + "auction_invalidity_BO", + "derivative_bank_mortgage_invalidity_BO", + ], + "terms": [ + "근저당", "저당", "담보", "피담보", "채무액", "변제", "변제충당", + "법정변제충당", "지분", "말소", "소유권이전등기", "등기", "등기원인", + "등기무효", "경매", "담보권실행경매", "경매개시", "매각", "배당", + "후행 담보권", "registry", "mortgage", "secured", "auction", "dividend", + "payment_allocation", "registry_row", "legal_calculation", + ], + "meeting_sources": ["conflict_pattern_clause_ids", "mixed_contract_clause_ids"], + "event_sources": [], + "hazard_keys": ["conflict_pattern_expressions", "asset_clusters", "mixed_contract_candidates"], + "taxonomy_axes": ["legal_object", "form", "dependency", "defect_or_condition"], + }, + "B3_Land_Valuation": { + "domain_id": "B3_Land_Valuation", + "domain_label": "토지 사용이익, 점유, 사용수익, 가치평가", + "allowed_legal_effect_bo_types": ["land_use_gain_timeline_BO", "liability_overlap_BO"], + "terms": [ + "토지", "건물", "부지", "인도", "점유", "사용", "사용수익", "현재 사용", + "현재 수익", "거주", "운영", "임대", "차임", "감정", "시가", "가액", + "미등기 건물", "사실상 처분권", "부진정연대", "valuation", "valuation_object", + "valuation_rows", "valuation_matrix", "registry_object_display", "possession", "land", "building", + ], + "meeting_sources": ["current_state_clause_ids", "conflict_pattern_clause_ids", "mixed_contract_clause_ids"], + "event_sources": ["state_candidate_ids"], + "hazard_keys": ["current_state_expressions", "asset_clusters", "mixed_contract_candidates", "conflict_pattern_expressions"], + "taxonomy_axes": ["legal_object", "contribution", "structure"], + }, + "B4_Actio": { + "domain_id": "B4_Actio", + "domain_label": "사해행위취소, 피보전채권, 책임재산, 가액배상", + "allowed_legal_effect_bo_types": [ + "preserved_claim_bundle_BO", + "fraudulent_transfer_BO", + "encumbrance_timeline_BO", + "valuation_matrix_BO", + "remedy_mode_decision_BO", + "value_compensation_cap_BO", + "defense_rebuttal_matrix_BO", + "actio_balance_sheet_BO", + ], + "terms": [ + "사해행위", "피보전채권", "채무초과", "책임재산", "공동담보", "매매", + "증여", "담보설정", "처분행위", "수익자", "전득자", "사해성", "악의", + "선의", "원상회복", "가액배상", "선순위담보", "말소", "가액 산정", + "항변", "actio", "fraudulent", "transfer", "preserved_claim", "encumbrance", + "valuation", "remedy", "compensation", "defense", "claim_document", + ], + "meeting_sources": ["conflict_pattern_clause_ids", "mixed_contract_clause_ids", "current_state_clause_ids", "succession_clause_ids"], + "event_sources": [], + "hazard_keys": ["conflict_pattern_expressions", "asset_clusters", "mixed_contract_candidates", "current_state_expressions", "succession_related_candidates"], + "taxonomy_axes": ["legal_object", "structure", "contribution", "defect_or_condition", "dependency", "form"], + }, + "B5_Succession_Notice_Lien_Asset_Defense": { + "domain_id": "B5_Succession_Notice_Lien_Asset_Defense", + "domain_label": "상속, 통지, 유치권, 목적물 특정, 항변", + "allowed_legal_effect_bo_types": [ + "succession_resolution_BO", + "notice_lifecycle_BO", + "lien_state_timeline_BO", + "asset_alias_BO", + "defense_rebuttal_BO", + "delivery_use_gain_claim_group_BO", + ], + "terms": [ + "사망", "상속", "상속포기", "배우자", "직계비속", "가족관계", "피상속인", + "상속인", "통지", "내용증명", "도달", "수령", "최고", "유치권", "점유", + "인도", "거절", "별지목록", "등기부 표시", "목적물", "동일성", "피고 답변", + "항변", "권리남용", "신의칙", "동시이행", "succession", "notice", "lien", + "possession", "asset", "defense", "party_assertion", "registry_object_display", + ], + "meeting_sources": ["succession_clause_ids", "current_state_clause_ids", "conflict_pattern_clause_ids", "mixed_contract_clause_ids"], + "event_sources": ["succession_candidate_ids", "state_candidate_ids"], + "hazard_keys": ["succession_related_candidates", "current_state_expressions", "conflict_pattern_expressions", "asset_clusters", "mixed_contract_candidates"], + "taxonomy_axes": ["structure", "time_effect", "legal_object", "defect_or_condition"], + }, + } + + # 3등급 라우팅 구조화 셀렉터 (개선전략서 §5.4 X-1): primary 승격은 구조화 키 + # (event_kind/event_subkind/component_summary)와 named-component strong term만 가능. + # broad term(등기·변제·항변·점유·목적물 등)의 단독 blob 매칭은 support 등급에 머문다. + A0_STRUCTURED_ROUTING = { + "B1_Money_Successor": { + "event_kinds": ["채권발생", "변제", "채권관리", "영업양도", "사업자등록"], + "event_subkinds": ["contract_formation", "대가수수", "payment_consideration", + "debt_acknowledgement", "new_due_date", "underlying_claim_arising", + "demand_notice_dispatch", "demand_notice_arrival", + "commercial_successor_liability_candidate", "defense_assertion_setoff", + "defense_assertion_benefit_applied_to_secured_claim"], + "evidence_component_keys": ["claim_document_components", "loan_components", + "business_transfer_components", "admission_components", + "mixed_contract_payment_semantics", "notice_components", "defense_assertions"], + "strong_terms": ["차용", "금전소비대차", "대여", "공사대금", "물품대금", "매매대금", + "지급각서", "이행각서", "채무승인", "상호속용", "영업양도", "사업자등록", "상계", + "답변서", "도급"], + }, + "B2_Secured_Registry": { + "event_kinds": ["담보설정", "담보말소", "경매개시", "경매매각", "소유권이전", "변제", + "가압류/압류", "증여"], + "event_subkinds": ["secured_debt_creation", "mortgage_extinguishment_by_fraction", + "auction_application_after_extinguishment", "auction_sale_and_registry_transfer", + "소유권보존", "매매", "defense_assertion_benefit_applied_to_secured_claim", + "defense_assertion_setoff", "defense_assertion_subsequent_owner_no_extinction_right"], + "evidence_component_keys": ["registry_row_components", "secured_debt_components", + "legal_calculation_object", "legal_calculation_objects", "registry_object_display", + "registry_row_role_map", "loan_components", "claim_document_components"], + "strong_terms": ["근저당", "저당권", "말소", "소유권이전등기", "등기부", "등기사항", + "경매", "배당", "변제충당", "법정변제충당", "채권최고액", "유치권", "상계", + "답변서", "차용", "도급"], + }, + "B3_Land_Valuation": { + "event_kinds": ["가치평가", "임대차", "인도", "점유", "사업자등록"], + "event_subkinds": ["ongoing_physical_possession", "해제/해지의사표시", "책임부인", + "defense_assertion_benefit_applied_to_secured_claim"], + "evidence_component_keys": ["valuation_object", "valuation_rows", "valuation_matrix", + "contract_components"], + "strong_terms": ["감정", "임료", "시세", "사용수익", "사용이익", "명도", "임대차", + "매매계약", "사업자등록", "답변서", "해제", "차임"], + }, + "B4_Actio": { + "event_kinds": ["처분", "증여", "소유권이전", "담보설정", "담보말소", "채권발생", + "가치평가", "가압류/압류"], + "event_subkinds": ["대가수수", "매매", "mortgage_extinguishment_by_fraction", + "underlying_claim_arising", "secured_debt_creation", "defense_assertion_setoff"], + "evidence_component_keys": ["claim_document_components", "loan_components", + "valuation_object", "valuation_matrix", "registry_row_components", + "defense_assertions"], + "strong_terms": ["사해", "피보전", "채무초과", "책임재산", "명의신탁", "수익자", + "전득자", "가액배상", "증여", "감정", "답변서"], + }, + "B5_Succession_Notice_Lien_Asset_Defense": { + "event_kinds": ["상속포기", "상속개시", "통지", "도달", "항변", "점유", "인도", + "사업자등록", "판결/결정", "영업양도"], + "event_subkinds": ["succession_renunciation_filing", "succession_renunciation_acceptance", + "demand_notice_dispatch", "demand_notice_arrival", "해제/해지의사표시", + "해제/해지 통지", "책임부인", "ongoing_physical_possession", + "defense_assertion_setoff", "defense_assertion_benefit_applied_to_secured_claim", + "defense_assertion_subsequent_owner_no_extinction_right"], + "evidence_component_keys": ["succession_document_components", "notice_components", + "defense_assertions", "schedule_items", "business_transfer_components", + "contract_components", "registry_row_components"], + "strong_terms": ["상속", "유치권", "소멸청구", "가족관계", "사업자등록", "별지", + "상호속용", "해제", "심판", "최고"], + }, + } + + A0_MEETING_FIELDS = ["clause_id", "local_clause_id", "section_key", "section_label", "major_no", + "sub_no", "heading", "line_start", "line_end", "text_excerpt", "tags"] + A0_EVENT_FIELDS = ["candidate_id", "source_evidence_index", "title", "doc_type", "event_or_state", "event_kind", "event_subkind", "action_type_candidate", "action_summary", "participants", "object_spec", "amount", "event_date", "event_end_date", "time_text", "time_precision", "current_state_candidate", "succession_candidate", "legal_effect_candidate", "defense_candidate", "commercial_successor_candidates", "meeting_clause_refs", "actio_relevance_candidates", "identity_signature", "property_cluster_id", "affected_asset_cluster_ids", "doc_semantic_flags", "confidence", "fraction_ref"] + A0_EVIDENCE_FIELDS = ["evidence_index", "doc_uid", "title", "title_normalized", "doc_type", "key_facts", "key_dates", "key_amounts", "key_parties", "source_pointer", "property_cluster_id", "doc_semantic_flags", "multi_event_registry", "component_summary", "component_details"] + A0_STAGE_A_PATH = "stage1_tmp/task_c_bo/stage_a_context.json" + A0_MANIFEST_PATH = "stage1_tmp/task_c_bo/source_universe_manifest.json" + A0_SLICE_PATHS = {dom: f"stage1_tmp/task_c_bo/domain_slices/{dom.split('_')[0]}.json" for dom in A0_DOMAIN_CONFIGS} + A0_BROADNESS_WARN_RATIO = 0.6 + + def _a0_write(path, content): + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def _a0_natural_key(value): + return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", str(value))] + + def _a0_sorted_ids(values): + return sorted([str(v) for v in values if str(v)], key=_a0_natural_key) + + def _a0_list(value): + return value if isinstance(value, list) else [] + + def _a0_blob(value): + return json.dumps(value, ensure_ascii=False, sort_keys=True).lower() + + def _a0_matches(value, terms): + haystack = _a0_blob(value) + return any(str(term).lower() in haystack for term in terms if str(term).strip()) + + def _a0_copy(source, fields): + return {f: source.get(f) for f in fields if f in source} + + def _a0_matched_domains(record): + return [d for d, cfg in A0_DOMAIN_CONFIGS.items() if _a0_matches(record, cfg["terms"])] + + def _a0_event_primary(record, routing): + if str(record.get("event_kind")) in routing["event_kinds"]: + return True + if str(record.get("event_subkind")) in routing["event_subkinds"]: + return True + return _a0_matches(record, routing["strong_terms"]) + + def _a0_evidence_primary(record, routing): + summary = record.get("component_summary") or {} + if any(summary.get(key) for key in routing["evidence_component_keys"]): + return True + return _a0_matches(record, routing["strong_terms"]) + + def _a0_clause_primary(record, routing): + return _a0_matches(record, routing["strong_terms"]) + + def _a0_select(stage_a, config): + """3등급 라우팅(X-1): 구조화 키/named-component 매칭 = primary, + broad term 단독 매칭·closure 유입 = support, 잔여 참조 = review_only(id만).""" + routing = A0_STRUCTURED_ROUTING[config["domain_id"]] + meeting_map = stage_a.get("meeting_clause_map") or {} + event_map = stage_a.get("event_candidate_map") or {} + evidence_map = stage_a.get("evidence_authority_map") or {} + by_clause = meeting_map.get("by_clause_id") or {} + by_event = event_map.get("by_candidate_id") or {} + by_evidence = evidence_map.get("by_evidence_index") or {} + + # 1) primary: 구조화 키(event_kind/event_subkind/component_summary) + named-component strong term + # + hazard 유래 구조화 id 목록(meeting_sources/event_sources). broad term 단독으로는 승격 불가. + primary = {"clause": set(), "event": set(), "evidence": set()} + for key in config["meeting_sources"]: + for value in _a0_list(meeting_map.get(key)): + if str(value) in by_clause: + primary["clause"].add(str(value)) + for key in config["event_sources"]: + for value in _a0_list(event_map.get(key)): + if str(value) in by_event: + primary["event"].add(str(value)) + for cid, record in by_clause.items(): + if _a0_clause_primary(record, routing): + primary["clause"].add(str(cid)) + for eid, record in by_event.items(): + if _a0_event_primary(record, routing): + primary["event"].add(str(eid)) + for vid, record in by_evidence.items(): + if _a0_evidence_primary(record, routing): + primary["evidence"].add(str(vid)) + + # 2) support: broad term 매칭(비-primary) + primary의 source-ref closure 유입분 + clause_ids = set(primary["clause"]) + event_ids = set(primary["event"]) + evidence_ids = set(primary["evidence"]) + for cid, record in by_clause.items(): + if _a0_matches(record, config["terms"]): + clause_ids.add(str(cid)) + for eid, record in by_event.items(): + if _a0_matches(record, config["terms"]): + event_ids.add(str(eid)) + for vid, record in by_evidence.items(): + if _a0_matches(record, config["terms"]): + evidence_ids.add(str(vid)) + for _ in range(2): + for evidence_id in list(evidence_ids): + event_ids.update(str(v) for v in _a0_list((event_map.get("by_evidence_index") or {}).get(evidence_id))) + for clause_id in list(clause_ids): + event_ids.update(str(v) for v in _a0_list((event_map.get("by_meeting_clause_ref") or {}).get(clause_id))) + for event_id in list(event_ids): + event = by_event.get(event_id) or {} + if event.get("source_evidence_index"): + evidence_ids.add(str(event.get("source_evidence_index"))) + for ref in _a0_list(event.get("meeting_clause_refs")): + clause_ids.add(str(ref)) + + clause_ids = {v for v in clause_ids if v in by_clause} + event_ids = {v for v in event_ids if v in by_event} + evidence_ids = {v for v in evidence_ids if v in by_evidence} + support = {"clause": clause_ids - primary["clause"], "event": event_ids - primary["event"], + "evidence": evidence_ids - primary["evidence"]} + # 3) review_only: universe 잔여 참조 (id만 — 존재 확인용) + review_only = {"clause": set(by_clause) - clause_ids, "event": set(by_event) - event_ids, + "evidence": set(by_evidence) - evidence_ids} + + def _label(source, item_id, keys): + rec = source.get(item_id) or {} + for k in keys: + v = rec.get(k) + if v: + return _short_excerpt(v, 80) + return item_id + + shared = [] + for item_type, ids_all, source in [("meeting_clause", clause_ids, by_clause), + ("event_candidate", event_ids, by_event), + ("evidence", evidence_ids, by_evidence)]: + for item_id in _a0_sorted_ids(ids_all): + domains = _a0_matched_domains(source.get(item_id) or {}) + if len(domains) > 1: + shared.append({"item_type": item_type, "id": item_id, "matched_domain_ids": domains}) + + support_refs = ( + [{"item_type": "meeting_clause", "id": i, "label": _label(by_clause, i, ["heading", "text_excerpt"])} for i in _a0_sorted_ids(support["clause"])] + + [{"item_type": "event_candidate", "id": i, "label": _label(by_event, i, ["action_summary", "title"])} for i in _a0_sorted_ids(support["event"])] + + [{"item_type": "evidence", "id": i, "label": _label(by_evidence, i, ["title"])} for i in _a0_sorted_ids(support["evidence"])] + ) + review_only_refs = ( + [{"item_type": "meeting_clause", "id": i} for i in _a0_sorted_ids(review_only["clause"])] + + [{"item_type": "event_candidate", "id": i} for i in _a0_sorted_ids(review_only["event"])] + + [{"item_type": "evidence", "id": i} for i in _a0_sorted_ids(review_only["evidence"])] + ) + return { + "meeting_clause_ids": _a0_sorted_ids(clause_ids), + "event_candidate_ids": _a0_sorted_ids(event_ids), + "evidence_indexes": _a0_sorted_ids(evidence_ids), + "primary": primary, + "selected_meetings": [_a0_copy(by_clause[v], A0_MEETING_FIELDS) for v in _a0_sorted_ids(primary["clause"])], + "selected_events": [_a0_copy(by_event[v], A0_EVENT_FIELDS) for v in _a0_sorted_ids(primary["event"])], + "selected_evidence": [_a0_copy(by_evidence[v], A0_EVIDENCE_FIELDS) for v in _a0_sorted_ids(primary["evidence"])], + "support_refs": support_refs, + "review_only_refs": review_only_refs, + "shared": shared, + } + + def _a0_taxonomy_subset(stage_a, config): + taxonomy = stage_a.get("juristic_act_taxonomy_map") or {} + axes = taxonomy.get("axis_groups") or {} + return { + "schema_version": taxonomy.get("schema_version"), + "categories": taxonomy.get("categories") or {}, + "example_to_gubun": taxonomy.get("example_to_gubun") or {}, + "axis_groups": {key: axes.get(key) for key in config["taxonomy_axes"] if key in axes}, + "source_digest_sha256": taxonomy.get("source_digest_sha256"), + } + + def _a0_hazard_subset(stage_a, config): + hazard = stage_a.get("global_hazard_seed") or {} + return {key: hazard.get(key) for key in config["hazard_keys"] if key in hazard} + + def _a0_part1_review(stage_a, selected, config): + handoff = stage_a.get("part1_soft_gate_handoff") or {} + guard = { + "schema_version": handoff.get("schema_version"), + "status": handoff.get("status"), + "stage2_auto_progression_allowed": handoff.get("stage2_auto_progression_allowed"), + "usage_rules": handoff.get("usage_rules") or {}, + } + se, sev, sm = set(selected["evidence_indexes"]), set(selected["event_candidate_ids"]), set(selected["meeting_clause_ids"]) + relevant = [] + for item in _a0_list(handoff.get("review_items_compact")): + refs = item.get("source_refs") if isinstance(item, dict) else {} + domain_text = _a0_blob({ + "domain_hints": item.get("domain_hints"), + "target_task_hints": item.get("target_task_hints"), + "issue_type": item.get("issue_type"), + "review_instruction": item.get("review_instruction"), + }) + ref_hit = bool( + se.intersection(str(v) for v in _a0_list((refs or {}).get("evidence_indexes"))) + or sev.intersection(str(v) for v in _a0_list((refs or {}).get("event_candidate_ids"))) + or sm.intersection(str(v) for v in _a0_list((refs or {}).get("meeting_clause_refs"))) + ) + if ref_hit or any(str(term).lower() in domain_text for term in config["terms"]): + relevant.append(item) + return guard, relevant + + def _a0_emit(stage_a_context): + import hashlib as _hl + _a0_write(A0_STAGE_A_PATH, json.dumps({"stage_a_context": stage_a_context}, ensure_ascii=False, indent=2)) + meeting_map = stage_a_context.get("meeting_clause_map") or {} + event_map = stage_a_context.get("event_candidate_map") or {} + evidence_map = stage_a_context.get("evidence_authority_map") or {} + manifest = { + "schema_version": "source_universe_manifest.v1", + "input_digests_sha256": stage_a_context.get("input_digests_sha256") or {}, + "evidence_index_set": evidence_map.get("evidence_index_set") or [], + "event_candidate_ids": event_map.get("candidate_id_set") or [], + "meeting_clause_ids": meeting_map.get("clause_order") or [], + "counts": { + "evidence": len(evidence_map.get("evidence_index_set") or []), + "event": len(event_map.get("candidate_id_set") or []), + "meeting": len(meeting_map.get("clause_order") or []), + }, + } + _a0_write(A0_MANIFEST_PATH, json.dumps(manifest, ensure_ascii=False, indent=2)) + digests_summary = _hl.sha256(json.dumps(stage_a_context.get("input_digests_sha256") or {}, + ensure_ascii=False, sort_keys=True).encode("utf-8")).hexdigest() + worker_contract = stage_a_context.get("stage_b_worker_contract") or {} + universe_events = len(event_map.get("candidate_id_set") or []) or 1 + universe_evidence = len(evidence_map.get("evidence_index_set") or []) or 1 + routing_summary = {} + for domain_id, config in A0_DOMAIN_CONFIGS.items(): + selected = _a0_select(stage_a_context, config) + part1_guard, part1_items = _a0_part1_review(stage_a_context, selected, config) + primary = selected["primary"] + broadness = (len(primary["event"]) + len(primary["evidence"])) / (universe_events + universe_evidence) + warnings = ["ROUTING_BROADNESS_WARNING"] if broadness > A0_BROADNESS_WARN_RATIO else [] + slice_obj = { + "stage_b_domain_slice": { + "schema_version": "task_c_bo_stage_b_domain_slice.v1", + "stage": "Task_C_BO.Stage_B0.DomainInputRouter", + "router_task": "Task_C_BO_A0_context_and_domain_slice_compiler", + "router_status": "READY", + "domain_id": domain_id, + "domain_label": config["domain_label"], + "allowed_legal_effect_bo_types": config["allowed_legal_effect_bo_types"], + "source_stage_a_schema_version": stage_a_context.get("schema_version"), + "source_stage_a_status": stage_a_context.get("status"), + "source_stage_a_created_at_utc": stage_a_context.get("created_at_utc"), + "input_digests_sha256": stage_a_context.get("input_digests_sha256") or {}, + "slice_guard_echo": { + "source_stage_a_created_at_utc": stage_a_context.get("created_at_utc"), + "input_digests_summary": digests_summary, + }, + "stage_b_worker_contract": { + "context_is_read_only": worker_contract.get("context_is_read_only"), + "bo_id_finalization_allowed": worker_contract.get("bo_id_finalization_allowed"), + "candidate_generation_allowed": worker_contract.get("candidate_generation_allowed"), + "must_use_evidence_index_set": worker_contract.get("must_use_evidence_index_set") or [], + }, + "source_universe": { + "meeting_clause_ids": meeting_map.get("clause_order") or [], + "event_candidate_ids": event_map.get("candidate_id_set") or [], + "evidence_indexes": evidence_map.get("evidence_index_set") or [], + }, + "selected_meeting_clauses": selected["selected_meetings"], + "selected_event_candidates": selected["selected_events"], + "selected_evidence_authority": selected["selected_evidence"], + "support_refs": selected["support_refs"], + "review_only_refs": selected["review_only_refs"], + "juristic_act_taxonomy_subset": _a0_taxonomy_subset(stage_a_context, config), + "hazard_subset": _a0_hazard_subset(stage_a_context, config), + "part1_review_guard": part1_guard, + "part1_review_items_relevant": part1_items, + "shared_or_ambiguous_items": selected["shared"], + "routing_audit": { + "primary_ids": { + "meeting_clause_ids": _a0_sorted_ids(primary["clause"]), + "event_candidate_ids": _a0_sorted_ids(primary["event"]), + "evidence_indexes": _a0_sorted_ids(primary["evidence"]), + }, + "support_ids": [r["id"] for r in selected["support_refs"]], + "review_only_ids": [r["id"] for r in selected["review_only_refs"]], + "selected_meeting_clause_ids": selected["meeting_clause_ids"], + "selected_event_candidate_ids": selected["event_candidate_ids"], + "selected_evidence_indexes": selected["evidence_indexes"], + "multi_domain_item_refs": selected["shared"], + "unrouted_but_preserved_refs": [], + "broadness_ratio": round(broadness, 4), + "warnings": warnings, + "selection_basis": "deterministic keyword/flag/doc_type routing; closure-joined items demoted to support tier", + "field_policy": "primary records are full compact authority projections; support items are id+label refs only.", + }, + } + } + _a0_write(A0_SLICE_PATHS[domain_id], json.dumps(slice_obj, ensure_ascii=False, indent=2)) + routing_summary[domain_id] = { + "primary": {"meeting": len(primary["clause"]), "event": len(primary["event"]), "evidence": len(primary["evidence"])}, + "support": len(selected["support_refs"]), + "broadness_ratio": round(broadness, 4), + "warnings": warnings, + } + print(json.dumps({ + "status": "READY", + "message": "A0 context + domain slice compile 완료", + "stage_a_path": A0_STAGE_A_PATH, + "source_manifest_path": A0_MANIFEST_PATH, + "domain_slice_paths": A0_SLICE_PATHS, + "input_digests_sha256": stage_a_context.get("input_digests_sha256") or {}, + "routing_summary": routing_summary, + }, ensure_ascii=False)) + + + def main() -> None: + _init() + + # Stage A.1-A.2: allowed input guard + existence check. + existence_check = _preflight_required_files() + + # Stage A.3-A.4: read exactly four inputs once, then parse/fail fast. + docs = _read_required_docs_once() + event_candidates = _parse_json_doc("evidence_event_candidates.json", docs["evidence_event_candidates.json"]) + evidence_indexed = _parse_json_doc("evidence_indexed.json", docs["evidence_indexed.json"]) + meeting_text = docs["client_meeting.md"] + juristic_text = docs["Default_Agent/Juristic_Act.md"] + + # Stage A.5-A.8: build normalized maps and the global hazard seed. + meeting_clause_map = _build_meeting_clause_map(meeting_text) + event_candidate_map = _build_event_candidate_map(event_candidates) + evidence_authority_map = _build_evidence_authority_map(evidence_indexed) + juristic_act_taxonomy_map = _build_juristic_act_map(juristic_text) + _validate_juristic_act_map(juristic_act_taxonomy_map) + global_hazard_seed = _build_global_hazard_seed(meeting_clause_map, event_candidate_map, evidence_authority_map) + + if not evidence_authority_map["evidence_index_set"]: + raise RuntimeError("No finalized evidence_index values available") + + input_digests_sha256 = { + name: _stable_hash(docs[name]) + for name in REQUIRED_FILES + } + part1_soft_gate_handoff = _compact_part1_soft_gate_handoff( + existence_check, + input_digests_sha256, + meeting_clause_map, + event_candidate_map, + evidence_authority_map, + ) + + stage_a_context = { + "schema_version": "task_c_bo_stage_a_context.v1", + "stage": "Task_C_BO.Stage_A", + "status": "READY", + "created_at_utc": datetime.now(timezone.utc).isoformat(), + "allowed_input_files": REQUIRED_FILES, + "forbidden_files_attested": ["evidence_all.json", "BO.json", "actio_case_signals.json", "case_liability_signals.json", "legal_effect_signals.json"], + "file_io_policy": { + "list_docs_called_for": REQUIRED_FILES, + "read_docs_called_once_for": REQUIRED_FILES, + "write_file_called": False, + "raw_evidence_all_read": False, + "output_file_written": False, + }, + "existence_check": existence_check, + "input_digests_sha256": input_digests_sha256, + "meeting_clause_map": meeting_clause_map, + "event_candidate_map": event_candidate_map, + "evidence_authority_map": evidence_authority_map, + "juristic_act_taxonomy_map": juristic_act_taxonomy_map, + "global_hazard_seed": global_hazard_seed, + "part1_soft_gate_handoff": part1_soft_gate_handoff, + "stage_b_worker_contract": { + "context_is_read_only": True, + "bo_id_finalization_allowed": False, + "candidate_generation_allowed": True, + "must_use_evidence_index_set": evidence_authority_map["evidence_index_set"], + }, + } + + _a0_emit(stage_a_context) + + if __name__ == "__main__": + try: + main() + except Exception as exc: + print(json.dumps({"stage": "Task_C_BO.Stage_A", "status": "FAILED", "error": str(exc)}, ensure_ascii=False)) + raise + - task_name: Task_C_BO_Stage_B_B1_Money_Successor + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: true + preflight_files: + - stage1_tmp/task_c_bo/domain_slices/B1.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + 본 task의 유일한 입력은 preflight로 제공된 도메인 slice 파일 `stage1_tmp/task_c_bo/domain_slices/B1.json`의 내용이다. 그 안의 `stage_b_domain_slice` 객체만 사용한다. 다른 파일 요청·재구성은 금지된다. + + + + + - You MUST use only the `stage_b_domain_slice` object provided via the preflight file `stage1_tmp/task_c_bo/domain_slices/B1.json`. + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B1_Money_Successor"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B1_Money_Successor + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for money claim, payment, notice, admission, and commercial successor liability. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B1:001`, `B1:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - money claim origin: loan, sale price, deposit, settlement, construction price, unpaid balance + - due date, acceleration, demand, notice, payment deadline, or delayed performance + - partial payment, debt admission, payment memorandum, balance confirmation, account statement, or settlement confirmation + - business transfer, trade-name continuation, transferee liability, debt non-assumption clause, or commercial successor liability + - uncertain creditor/debtor/successor linkage that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - `support_refs` (id+label 참조 전용 — 사실 인용 근거로 사용 금지) + - `review_only_refs` + - `slice_guard_echo` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use B0 routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the current B0 slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split money claim origin, maturity/demand, payment/admission, and successor liability when they are distinct legal-effect seeds. + 4. Preserve payment history, demand/notice hints, admission hints, successor liability hints, and successor-link uncertainty in `extensions.domain_payload`. Preserve agreed interest rate and agreed delay damage rate as literal strings from B1 `loan_components`/`secured_debt_components` (`component_details` of `selected_evidence_authority`) when present; never substitute statutory rates, and keep the supporting evidence indexes in `interest_rate_source_evidence_refs`. + 5. Use `domain_review_queue` for missing amount/date, uncertain creditor/debtor identity, conflicting payment status, or cross-domain successor merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Keep all unsupported or uncertain values as null or [] and route review needs to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + - 값이 null, [], {}, 빈 문자열인 키는 출력하지 않는다. 의미 있는 false와 0은 유지한다. 후속 결정적 reducer(R0)가 canonical schema로 확장하며 키 부재를 null로 해석한다. + - 다음 필드는 출력하지 않는다 (R0가 결정적으로 재구성/복원한다): `transport_candidate_refs`, `Reason`, `PriorAct`, `ReasonRefs`, `downstream_seed_refs`(단, 선행행위 후보가 실재할 때의 `prior_candidate_refs`/`reason_refs_candidate_refs`는 예외적으로 출력 가능), `slice_guard`의 `source_universe`/`input_digests_sha256`/`stage_b_worker_contract` 전체 에코. + - `transport_metadata.slice_guard`에는 slice의 `slice_guard_echo` 값 2개(`source_stage_a_created_at_utc`, `input_digests_summary`)만 문자 그대로 복사한다. + - `domain_review_queue` 항목은 `review_id`, `severity`, `issue_type`, `source_event_candidate_ids`, `source_evidence_indexes`, `source_meeting_clause_ids`, `recommended_downstream_owner`만 출력한다. 서술형 `description`은 출력하지 않는다(고정 문구는 R0가 부여한다). + - candidate의 실질 내용(BOType, ActionType, JuristicAct, Action, core_field_base, amount, source refs, domain_payload의 유의미 값, 도메인 named fields)은 생략 대상이 아니다 — 값이 있으면 반드시 출력한다. + + + + Return exactly one JSON object: + { + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B1_Money_Successor", + "status": "READY|READY_WITH_REVIEW|FAILED", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B1_Money_Successor", + "domain_label": "금전채권 및 상호속용 책임", + "slice_guard": { + "source_stage_a_created_at_utc": "slice_guard_echo에서 문자 그대로 복사", + "input_digests_summary": "slice_guard_echo에서 문자 그대로 복사" + } + }, + "bo_seed_candidates": [ + { + "candidate_ref": "B1:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "JuristicAct": {"label": "string|null"}, + "Action": "short neutral factual/legal-action draft", + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [], + "asset_refs": [], + "legal_effect_summary": null + }, + "amount": { + "value": null, + "currency": "KRW|null", + "basis": null + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": null, + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": {"claim_type_candidate": null, "principal_amount_hint": null, "interest_hint": null, "delay_damage_hint": null, "agreed_interest_rate": null, "agreed_delay_damage_rate": null, "interest_accrual_start_hint": null, "interest_rate_source_evidence_refs": [], "unpaid_balance_hint": null, "creditor_candidate": null, "debtor_candidate": null}, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": "string|null", + "conflict_flags": [] + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B1:review:001", + "severity": "SOFT_WARNING|HARD_WARNING", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "publisher|domain_join|C0|C1|C2|C3|C5|D|E" + } + ] + } + } + + Status rule: use `FAILED` when any input acceptance or `stage_b_worker_contract` guard fails; use `READY_WITH_REVIEW` when `domain_review_queue` is non-empty and no hard gate failed; otherwise use `READY`. + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B2_Secured_Registry + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: true + preflight_files: + - stage1_tmp/task_c_bo/domain_slices/B2.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + 본 task의 유일한 입력은 preflight로 제공된 도메인 slice 파일 `stage1_tmp/task_c_bo/domain_slices/B2.json`의 내용이다. 그 안의 `stage_b_domain_slice` 객체만 사용한다. 다른 파일 요청·재구성은 금지된다. + + + + + - You MUST use only the `stage_b_domain_slice` object provided via the preflight file `stage1_tmp/task_c_bo/domain_slices/B2.json`. + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B2_Secured_Registry"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B2_Secured_Registry + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for secured debt, registry, payment allocation, auction, distribution, and derivative invalidity issues. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B2:001`, `B2:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - secured debt creation, change, repayment, payment allocation, and secured obligation balance + - mortgage, pledge, collateral security, registration, cancellation, correction, or invalidity chain + - auction, distribution, dividend, satisfaction, or residual balance facts + - derivative invalidity, registry error, title defect, or secured property dispute + - uncertain registry sequence or security/right-holder linkage that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - `support_refs` (id+label 참조 전용 — 사실 인용 근거로 사용 금지) + - `review_only_refs` + - `slice_guard_echo` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use B0 routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the current B0 slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split secured debt chain, registry act, payment allocation, auction step, dividend distribution, and derivative invalidity when they are distinct legal-effect seeds. + 4. Preserve registry rows, secured debt chain, payment allocation, auction step, dividend distribution hints, derivative invalidity hints, and registry display values in `extensions.domain_payload`. Carry `fraction_ref` from source event candidates unchanged; do not merge or renumber. Populate `fractional_effect_events` as an event-reference list only (fraction-bearing setting/extinguishment/transfer events); never fold fraction states or normalize/reduce fractions (that is Stage 2 work). + 5. Use `domain_review_queue` for missing registry date, unclear secured obligation balance, conflicting registration sequence, or cross-domain property merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Keep all unsupported or uncertain values as null or [] and route review needs to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + - 값이 null, [], {}, 빈 문자열인 키는 출력하지 않는다. 의미 있는 false와 0은 유지한다. 후속 결정적 reducer(R0)가 canonical schema로 확장하며 키 부재를 null로 해석한다. + - 다음 필드는 출력하지 않는다 (R0가 결정적으로 재구성/복원한다): `transport_candidate_refs`, `Reason`, `PriorAct`, `ReasonRefs`, `downstream_seed_refs`(단, 선행행위 후보가 실재할 때의 `prior_candidate_refs`/`reason_refs_candidate_refs`는 예외적으로 출력 가능), `slice_guard`의 `source_universe`/`input_digests_sha256`/`stage_b_worker_contract` 전체 에코. + - `transport_metadata.slice_guard`에는 slice의 `slice_guard_echo` 값 2개(`source_stage_a_created_at_utc`, `input_digests_summary`)만 문자 그대로 복사한다. + - `domain_review_queue` 항목은 `review_id`, `severity`, `issue_type`, `source_event_candidate_ids`, `source_evidence_indexes`, `source_meeting_clause_ids`, `recommended_downstream_owner`만 출력한다. 서술형 `description`은 출력하지 않는다(고정 문구는 R0가 부여한다). + - candidate의 실질 내용(BOType, ActionType, JuristicAct, Action, core_field_base, amount, source refs, domain_payload의 유의미 값, 도메인 named fields)은 생략 대상이 아니다 — 값이 있으면 반드시 출력한다. + + + + Return exactly one JSON object: + { + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B2_Secured_Registry", + "status": "READY|READY_WITH_REVIEW|FAILED", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B2_Secured_Registry", + "domain_label": "담보채무, 변제충당, 등기무효, 경매", + "slice_guard": { + "source_stage_a_created_at_utc": "slice_guard_echo에서 문자 그대로 복사", + "input_digests_summary": "slice_guard_echo에서 문자 그대로 복사" + } + }, + "bo_seed_candidates": [ + { + "candidate_ref": "B2:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "JuristicAct": {"label": "string|null"}, + "Action": "short neutral factual/legal-action draft", + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [], + "asset_refs": [], + "legal_effect_summary": null + }, + "amount": { + "value": null, + "currency": "KRW|null", + "basis": null + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "title_hint": null, + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "registry_rows": [ + {"registry_ref": null, "fraction_ref": null, "fraction_display": null, "fraction_numerator": null, "fraction_denominator": null, "receipt_no": null} + ], + "fractional_effect_events": [ + {"fraction_ref": null, "fraction_display": null, "fraction_numerator": null, "fraction_denominator": null, "effect_kind_hint": "mortgage_setting|mortgage_extinguishment|transfer|other", "source_event_candidate_ids": [], "registry_refs": [], "evidence_indexes": []} + ], + "secured_debt_chain": [], + "payment_allocation": [], + "mortgage_extinguishment_hints": [], + "auction_steps": [], + "dividend_distribution_hints": [], + "derivative_invalidity_hints": [], + "registry_object_display": "string|null" + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B2:review:001", + "severity": "SOFT_WARNING|HARD_WARNING", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "publisher|domain_join|C0|C1|C2|C3|C5|D|E" + } + ] + } + } + + Status rule: use `FAILED` when any input acceptance or `stage_b_worker_contract` guard fails; use `READY_WITH_REVIEW` when `domain_review_queue` is non-empty and no hard gate failed; otherwise use `READY`. + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B3_Land_Valuation + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: true + preflight_files: + - stage1_tmp/task_c_bo/domain_slices/B3.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + 본 task의 유일한 입력은 preflight로 제공된 도메인 slice 파일 `stage1_tmp/task_c_bo/domain_slices/B3.json`의 내용이다. 그 안의 `stage_b_domain_slice` 객체만 사용한다. 다른 파일 요청·재구성은 금지된다. + + + + + - You MUST use only the `stage_b_domain_slice` object provided via the preflight file `stage1_tmp/task_c_bo/domain_slices/B3.json`. + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B3_Land_Valuation"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B3_Land_Valuation + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for land use gain, possession/use, valuation, and asset-value facts. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B3:001`, `B3:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - land or building possession, use, occupation, benefit, rent-equivalent gain, or loss of use + - valuation fact, appraisal, market value, rent value, area, period, and calculation base + - delivery, possession start/end, unauthorized use, co-occupancy, or liability overlap + - asset identity, address, parcel, unit, area, or other value-bearing object description + - uncertain use period, value basis, possession status, or asset binding that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - `support_refs` (id+label 참조 전용 — 사실 인용 근거로 사용 금지) + - `review_only_refs` + - `slice_guard_echo` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use B0 routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the current B0 slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split possession/use state, use-gain period, valuation fact, and liability overlap when they are distinct legal-effect seeds. + 4. Preserve valuation rows, valuation matrix, use-gain facts, possession state, asset binding hints, and liability overlap hints in `extensions.domain_payload`. Populate the `possession_state` two-track structure (possession facts vs. asserted legal causes) from selected event candidates only; never resolve which legal cause prevails (that is Stage 2 work); preserve conflicting assertions side by side. + 5. Use `domain_review_queue` for missing period, missing value base, unclear possession state, or cross-domain asset merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Keep all unsupported or uncertain values as null or [] and route review needs to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + - 값이 null, [], {}, 빈 문자열인 키는 출력하지 않는다. 의미 있는 false와 0은 유지한다. 후속 결정적 reducer(R0)가 canonical schema로 확장하며 키 부재를 null로 해석한다. + - 다음 필드는 출력하지 않는다 (R0가 결정적으로 재구성/복원한다): `transport_candidate_refs`, `Reason`, `PriorAct`, `ReasonRefs`, `downstream_seed_refs`(단, 선행행위 후보가 실재할 때의 `prior_candidate_refs`/`reason_refs_candidate_refs`는 예외적으로 출력 가능), `slice_guard`의 `source_universe`/`input_digests_sha256`/`stage_b_worker_contract` 전체 에코. + - `transport_metadata.slice_guard`에는 slice의 `slice_guard_echo` 값 2개(`source_stage_a_created_at_utc`, `input_digests_summary`)만 문자 그대로 복사한다. + - `domain_review_queue` 항목은 `review_id`, `severity`, `issue_type`, `source_event_candidate_ids`, `source_evidence_indexes`, `source_meeting_clause_ids`, `recommended_downstream_owner`만 출력한다. 서술형 `description`은 출력하지 않는다(고정 문구는 R0가 부여한다). + - candidate의 실질 내용(BOType, ActionType, JuristicAct, Action, core_field_base, amount, source refs, domain_payload의 유의미 값, 도메인 named fields)은 생략 대상이 아니다 — 값이 있으면 반드시 출력한다. + + + + Return exactly one JSON object: + { + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B3_Land_Valuation", + "status": "READY|READY_WITH_REVIEW|FAILED", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B3_Land_Valuation", + "domain_label": "토지 사용이익, 점유, 사용수익, 가치평가", + "slice_guard": { + "source_stage_a_created_at_utc": "slice_guard_echo에서 문자 그대로 복사", + "input_digests_summary": "slice_guard_echo에서 문자 그대로 복사" + } + }, + "bo_seed_candidates": [ + { + "candidate_ref": "B3:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "JuristicAct": {"label": "string|null"}, + "Action": "short neutral factual/legal-action draft", + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [], + "asset_refs": [], + "legal_effect_summary": null + }, + "amount": { + "value": null, + "currency": "KRW|null", + "basis": null + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "title_hint": null, + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "valuation_rows": [], + "valuation_matrix": [], + "valuation_object": "string|null", + "use_gain_facts": [], + "possession_state": { + "asset_ref": null, + "possession_events": [ + {"possessor_candidates": [], "start_date": null, "end_date": null, "source_event_candidate_ids": [], "evidence_indexes": []} + ], + "legal_cause_assertions": [ + {"cause_asserted": "유치권행사중|임대차|사용대차|물권|동시이행항변|명도후잔류|무권원주장|미상", "asserted_by": null, "assertion_date": null, "source_event_candidate_ids": [], "evidence_indexes": []} + ] + }, + "delivery_or_possession_start_hints": [], + "unregistered_building_hints": [], + "liability_overlap_hints": [] + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B3:review:001", + "severity": "SOFT_WARNING|HARD_WARNING", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "publisher|domain_join|C0|C1|C2|C3|C5|D|E" + } + ] + } + } + + Status rule: use `FAILED` when any input acceptance or `stage_b_worker_contract` guard fails; use `READY_WITH_REVIEW` when `domain_review_queue` is non-empty and no hard gate failed; otherwise use `READY`. + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B4_Actio + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: true + preflight_files: + - stage1_tmp/task_c_bo/domain_slices/B4.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + 본 task의 유일한 입력은 preflight로 제공된 도메인 slice 파일 `stage1_tmp/task_c_bo/domain_slices/B4.json`의 내용이다. 그 안의 `stage_b_domain_slice` 객체만 사용한다. 다른 파일 요청·재구성은 금지된다. + + + + + - You MUST use only the `stage_b_domain_slice` object provided via the preflight file `stage1_tmp/task_c_bo/domain_slices/B4.json`. + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B4_Actio"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B4_Actio + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for actio pauliana, preserved claim, debtor fraudulent act, beneficiary/transferee, remedy, cap, and defenses. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B4:001`, `B4:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - preserved claim existence, claim date, creditor/debtor identity, and claim amount basis + - debtor act, asset decrease, insolvency, common collateral impairment, and harmfulness + - beneficiary, transferee, bad faith/good faith, notice, value compensation, restitution, and remedy cap + - encumbrance, valuation, property transfer, mortgage, sale, gift, collateral provision, or registration act used as fraudulent act basis + - uncertain fraudulent act date, beneficiary chain, asset value, remedy mode, or defense rebuttal that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - `support_refs` (id+label 참조 전용 — 사실 인용 근거로 사용 금지) + - `review_only_refs` + - `slice_guard_echo` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use B0 routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the current B0 slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split preserved claim, fraudulent transfer, balance-sheet impairment, encumbrance/valuation, beneficiary/transferee state, remedy/cap, and defense rebuttal when they are distinct legal-effect seeds. + 4. Preserve preserved claim bundle, fraudulent transfer facts, balance sheet, encumbrance timeline, valuation matrix, remedy/cap hints, and defense rebuttal in `extensions.domain_payload`. + 5. Use `domain_review_queue` for missing claim date, uncertain fraudulent act date, conflicting value basis, unclear beneficiary chain, or cross-domain asset/claim merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Keep all unsupported or uncertain values as null or [] and route review needs to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + - 값이 null, [], {}, 빈 문자열인 키는 출력하지 않는다. 의미 있는 false와 0은 유지한다. 후속 결정적 reducer(R0)가 canonical schema로 확장하며 키 부재를 null로 해석한다. + - 다음 필드는 출력하지 않는다 (R0가 결정적으로 재구성/복원한다): `transport_candidate_refs`, `Reason`, `PriorAct`, `ReasonRefs`, `downstream_seed_refs`(단, 선행행위 후보가 실재할 때의 `prior_candidate_refs`/`reason_refs_candidate_refs`는 예외적으로 출력 가능), `slice_guard`의 `source_universe`/`input_digests_sha256`/`stage_b_worker_contract` 전체 에코. + - `transport_metadata.slice_guard`에는 slice의 `slice_guard_echo` 값 2개(`source_stage_a_created_at_utc`, `input_digests_summary`)만 문자 그대로 복사한다. + - `domain_review_queue` 항목은 `review_id`, `severity`, `issue_type`, `source_event_candidate_ids`, `source_evidence_indexes`, `source_meeting_clause_ids`, `recommended_downstream_owner`만 출력한다. 서술형 `description`은 출력하지 않는다(고정 문구는 R0가 부여한다). + - candidate의 실질 내용(BOType, ActionType, JuristicAct, Action, core_field_base, amount, source refs, domain_payload의 유의미 값, 도메인 named fields)은 생략 대상이 아니다 — 값이 있으면 반드시 출력한다. + + + + Return exactly one JSON object: + { + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B4_Actio", + "status": "READY|READY_WITH_REVIEW|FAILED", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B4_Actio", + "domain_label": "사해행위취소, 피보전채권, 책임재산, 가액배상", + "slice_guard": { + "source_stage_a_created_at_utc": "slice_guard_echo에서 문자 그대로 복사", + "input_digests_summary": "slice_guard_echo에서 문자 그대로 복사" + } + }, + "bo_seed_candidates": [ + { + "candidate_ref": "B4:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "JuristicAct": {"label": "string|null"}, + "Action": "short neutral factual/legal-action draft", + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [], + "asset_refs": [], + "legal_effect_summary": null + }, + "amount": { + "value": null, + "currency": "KRW|null", + "basis": null + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "actio_fraudulent_transfer", + "title_hint": null, + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "preserved_claim_bundle": {}, + "fraudulent_transfer_facts": [], + "debtor_act_hints": [], + "balance_sheet": {"asset_decrease_hints": [], "insolvency_hints": [], "common_collateral_impairment_hints": []}, + "encumbrance_timeline": [], + "valuation_matrix": [], + "remedy_mode_hints": [], + "value_compensation_cap_hints": [], + "beneficiary_transferee_hints": [], + "bad_faith_good_faith_hints": [], + "defense_rebuttal": [] + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B4:review:001", + "severity": "SOFT_WARNING|HARD_WARNING", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "publisher|domain_join|C0|C1|C2|C3|C5|D|E" + } + ] + } + } + + Status rule: use `FAILED` when any input acceptance or `stage_b_worker_contract` guard fails; use `READY_WITH_REVIEW` when `domain_review_queue` is non-empty and no hard gate failed; otherwise use `READY`. + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: true + preflight_files: + - stage1_tmp/task_c_bo/domain_slices/B5.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + 본 task의 유일한 입력은 preflight로 제공된 도메인 slice 파일 `stage1_tmp/task_c_bo/domain_slices/B5.json`의 내용이다. 그 안의 `stage_b_domain_slice` 객체만 사용한다. 다른 파일 요청·재구성은 금지된다. + + + + + - You MUST use only the `stage_b_domain_slice` object provided via the preflight file `stage1_tmp/task_c_bo/domain_slices/B5.json`. + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B5_Succession_Notice_Lien_Asset_Defense"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B5_Succession_Notice_Lien_Asset_Defense + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for succession, notice lifecycle, lien, asset identity, possession, and major defenses. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B5:001`, `B5:002`, ... in deterministic order of the candidate seed list. + - If multiple lien seeds exist, assign `lien_id` values `B5:LIEN:001`, `B5:LIEN:002`, ... ordered by the earliest related event date (ties broken by `candidate_ref` lexicographic order). This id is transport-scope only, not a final BO id. + - Do not produce `violation_candidates` or `status_after_notice_receipt`; route such needs to `domain_review_queue` with `issue_type="legal_theory_required"`. + - `legal_sender_after_resolution.party` must always remain null in Stage 1 (value finalization is Stage 2 legal work); fill only its `status` and `succession_resolution_ref` from directly-read sources. + + + + Create separate seeds when supported for: + - general succession, specific succession, inheritance, assignment, acquisition, transfer, or party-status succession + - notice dispatch, arrival, receipt, refusal, content, timing, and legal effect of notice + - lien, possession, retained object, claim-object connection, asset alias, and asset identity + - simultaneous performance, setoff, payment, prescription, delivery, possession, or other major defense facts + - uncertain notice arrival, lien possession continuity, asset identity, succession scope, or defense linkage that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - `support_refs` (id+label 참조 전용 — 사실 인용 근거로 사용 금지) + - `review_only_refs` + - `slice_guard_echo` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use B0 routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the current B0 slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split succession state, notice lifecycle, lien/possession state, asset identity, and defense rebuttal when they are distinct legal-effect seeds. + 4. Preserve succession resolution, notice lifecycle, lien state, possession state, asset alias, simultaneous-performance hints, setoff/payment defense hints, and defense rebuttal in `extensions.domain_payload`. Populate the `possession_state` two-track structure (possession facts vs. asserted legal causes) from selected event candidates only; never resolve which legal cause prevails (that is Stage 2 work); preserve conflicting assertions side by side. Assign `lien_id` deterministically and reference it from possession/notice payloads of the same seed group. Inherit `notice_id` from B1 `notice_components[*].notice_id_proposed` (`component_details` of `selected_evidence_authority`); when multiple notices exist, split seeds so each seed carries one notice lifecycle. Inherit `schedule_refs`/`registry_refs` (SCH-*/REG-* keys) and `canonical_label_candidate`/`usable_in_relief` into `asset_alias` unchanged. + 5. Use `domain_review_queue` for missing arrival evidence, uncertain asset identity, unclear possession continuity, conflicting succession scope, or cross-domain defense merge need. When `received_date` cannot be established from selected sources, leave it null and add a `domain_review_queue` item with `issue_type="missing_source"` (do not fabricate); the sealed drafting gate is applied downstream at FL. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Keep all unsupported or uncertain values as null or [] and route review needs to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + - 값이 null, [], {}, 빈 문자열인 키는 출력하지 않는다. 의미 있는 false와 0은 유지한다. 후속 결정적 reducer(R0)가 canonical schema로 확장하며 키 부재를 null로 해석한다. + - 다음 필드는 출력하지 않는다 (R0가 결정적으로 재구성/복원한다): `transport_candidate_refs`, `Reason`, `PriorAct`, `ReasonRefs`, `downstream_seed_refs`(단, 선행행위 후보가 실재할 때의 `prior_candidate_refs`/`reason_refs_candidate_refs`는 예외적으로 출력 가능), `slice_guard`의 `source_universe`/`input_digests_sha256`/`stage_b_worker_contract` 전체 에코. + - `transport_metadata.slice_guard`에는 slice의 `slice_guard_echo` 값 2개(`source_stage_a_created_at_utc`, `input_digests_summary`)만 문자 그대로 복사한다. + - `domain_review_queue` 항목은 `review_id`, `severity`, `issue_type`, `source_event_candidate_ids`, `source_evidence_indexes`, `source_meeting_clause_ids`, `recommended_downstream_owner`만 출력한다. 서술형 `description`은 출력하지 않는다(고정 문구는 R0가 부여한다). + - candidate의 실질 내용(BOType, ActionType, JuristicAct, Action, core_field_base, amount, source refs, domain_payload의 유의미 값, 도메인 named fields)은 생략 대상이 아니다 — 값이 있으면 반드시 출력한다. + + + + Return exactly one JSON object: + { + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B5_Succession_Notice_Lien_Asset_Defense", + "status": "READY|READY_WITH_REVIEW|FAILED", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B5_Succession_Notice_Lien_Asset_Defense", + "domain_label": "상속, 통지, 유치권, 목적물 특정, 항변", + "slice_guard": { + "source_stage_a_created_at_utc": "slice_guard_echo에서 문자 그대로 복사", + "input_digests_summary": "slice_guard_echo에서 문자 그대로 복사" + } + }, + "bo_seed_candidates": [ + { + "candidate_ref": "B5:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "JuristicAct": {"label": "string|null"}, + "Action": "short neutral factual/legal-action draft", + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [], + "asset_refs": [], + "legal_effect_summary": null + }, + "amount": { + "value": null, + "currency": "KRW|null", + "basis": null + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "title_hint": null, + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "succession_resolution": {}, + "general_succession_status": {}, + "asset_specific_succession_resolution": {}, + "notice_lifecycle": { + "notice_id": null, + "notice_type": null, + "drafted_date": null, + "dispatch_date": null, + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [], + "legal_sender_after_resolution": {"status": "resolved|unresolved|disputed|not_required", "party": null, "succession_resolution_ref": null}, + "receiver_candidates": [], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + }, + "lien_state": { + "lien_id": null, + "asset_ref": null, + "lien_holder_candidates": [], + "secured_claim_hint": {"debtor_candidate": null, "basis_text": null, "claimed_remaining_amount_hint": null, "evidence_indexes": []}, + "possession_event_refs": [], + "assertion_events": [], + "extinction_notice_refs": [], + "status_raw_hint": null + }, + "possession_state": { + "asset_ref": null, + "possession_events": [ + {"possessor_candidates": [], "start_date": null, "end_date": null, "source_event_candidate_ids": [], "evidence_indexes": []} + ], + "legal_cause_assertions": [ + {"cause_asserted": "유치권행사중|임대차|사용대차|물권|동시이행항변|명도후잔류|무권원주장|미상", "asserted_by": null, "assertion_date": null, "source_event_candidate_ids": [], "evidence_indexes": []} + ] + }, + "asset_alias": [ + {"asset_ref": null, "alias_strings": [], "canonical_label_candidate": null, "usable_in_relief": "yes|no|unknown", "schedule_refs": [], "registry_refs": [{"registry_ref": null, "receipt_no_refs": []}], "source_evidence_indexes": []} + ], + "defense_rebuttal": [], + "simultaneous_performance_hints": [], + "setoff_or_payment_defense_hints": [] + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B5:review:001", + "severity": "SOFT_WARNING|HARD_WARNING", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "publisher|domain_join|C0|C1|C2|C3|C5|D|E" + } + ] + } + } + + Status rule: use `FAILED` when any input acceptance or `stage_b_worker_contract` guard fails; use `READY_WITH_REVIEW` when `domain_review_queue` is non-empty and no hard gate failed; otherwise use `READY`. + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_R0_seed_reducer_and_exception_planner + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: |- + #!/usr/bin/env python3 + # Task_C_BO_R0_seed_reducer_and_exception_planner (v3) + # publisher + domain_join + PostB_1 통합 결정적 reducer. + # Spec: Part_2_Improvement_Strategy_Claude_v1.md §7 (defer policy = 개선전략서 X-2, pack 조건 = X-3) + from __future__ import annotations + import itertools + import json + import re + import sys + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + WORKER_RAW_FIELDS = { + "B1_Money_Successor": r"""{{prev.Task_C_BO_Stage_B_B1_Money_Successor.json_output}}""", + "B2_Secured_Registry": r"""{{prev.Task_C_BO_Stage_B_B2_Secured_Registry.json_output}}""", + "B3_Land_Valuation": r"""{{prev.Task_C_BO_Stage_B_B3_Land_Valuation.json_output}}""", + "B4_Actio": r"""{{prev.Task_C_BO_Stage_B_B4_Actio.json_output}}""", + "B5_Succession_Notice_Lien_Asset_Defense": r"""{{prev.Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense.json_output}}""", + } + SEED_DOCS = { + "B1_Money_Successor": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B1_Money_Successor.json", + "B2_Secured_Registry": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B2_Secured_Registry.json", + "B3_Land_Valuation": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B3_Land_Valuation.json", + "B4_Actio": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B4_Actio.json", + "B5_Succession_Notice_Lien_Asset_Defense": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B5_Succession_Notice_Lien_Asset_Defense.json", + } + SEED_TASK_NAMES = { + "B1_Money_Successor": "Task_C_BO_Stage_B_B1_Money_Successor", + "B2_Secured_Registry": "Task_C_BO_Stage_B_B2_Secured_Registry", + "B3_Land_Valuation": "Task_C_BO_Stage_B_B3_Land_Valuation", + "B4_Actio": "Task_C_BO_Stage_B_B4_Actio", + "B5_Succession_Notice_Lien_Asset_Defense": "Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense", + } + DOMAIN_ORDER = list(SEED_DOCS.keys()) + DOMAIN_PREFIX = { + "B1_Money_Successor": "B1", + "B2_Secured_Registry": "B2", + "B3_Land_Valuation": "B3", + "B4_Actio": "B4", + "B5_Succession_Notice_Lien_Asset_Defense": "B5", + } + DOMAIN_LABELS = { + "B1_Money_Successor": "금전채권 및 상호속용 책임", + "B2_Secured_Registry": "담보채무, 변제충당, 등기무효, 경매", + "B3_Land_Valuation": "토지 사용이익, 점유, 사용수익, 가치평가", + "B4_Actio": "사해행위취소, 피보전채권, 책임재산, 가액배상", + "B5_Succession_Notice_Lien_Asset_Defense": "상속, 통지, 유치권, 목적물 특정, 항변", + } + STAGE_A_PATH = "stage1_tmp/task_c_bo/stage_a_context.json" + MANIFEST_PATH = "stage1_tmp/task_c_bo/source_universe_manifest.json" + LEDGER_PATH = "stage1_tmp/task_c_bo/postb_seed_ledger.json" + PACK_PATH = "quality_gates/stage1_part2_exception_pack.json" + REVIEW_HANDOFF_PATH = "quality_gates/stage1_part2_review_handoff.json" + CANDIDATE_REF_RE = re.compile(r"^B[1-5]:[0-9]{3}$") + REVIEW_ISSUE_ENUM = { + "missing_source", "source_conflict", "cross_domain_merge_needed", + "amount_or_date_uncertain", "legal_effect_uncertain", "review_required", + "legal_theory_required", "near_duplicate_kept_separate", + "meeting_only_evidence_gap", "schema_field_fallback", "prior_link_ambiguous", + } + DOWNSTREAM_OWNER_ENUM = {"publisher", "domain_join", "C0", "C1", "C2", "C3", "C5", "D", "E", "Stage2"} + ALLOWED_SEED_KEYS = { + "candidate_ref", "source_domain", "BOType", "ActionType", "JuristicAct", "Action", + "core_field_base", "amount", "source_evidence_indexes", "provenance", + "downstream_seed_refs", "extensions", "Legal_Keywords", "transport_audit", + "Reason", "PriorAct", "ReasonRefs", + } + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-c-bo-r0-seed-reducer-and-exception-planner", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse_mcp(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "id": _mid(), "method": "tools/call", + "params": {"name": name, "arguments": args}}, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse_mcp(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def read_json_doc(path: str) -> Any: + result = _call("read_docs", {"doc_names": [path]}) + content = result["result"].get("content", []) + text = content[0].get("text", "") if content else "" + try: + parsed = json.loads(text) + except json.JSONDecodeError as exc: + raise RuntimeError(f"Invalid read_docs envelope for {path}") from exc + if isinstance(parsed, dict) and "results" in parsed: + rows = parsed.get("results") or [] + if not rows or rows[0].get("error"): + raise RuntimeError(f"read_docs failed for {path}: {rows[:1]}") + inner = rows[0].get("content") or "" + if isinstance(inner, str): + try: + return json.loads(inner) + except json.JSONDecodeError: + obj, end = JSON_DECODER.raw_decode(inner.strip()) + if inner.strip()[end:].strip(): + raise RuntimeError(f"{path}: non-json tail") + return obj + return inner + return parsed + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def clip(value: Any, limit: int = 120) -> str: + text = " ".join(str(value or "").split()) + return text if len(text) <= limit else text[:limit].rstrip() + "..." + + # ---------- 관용 파서 (SKILL.md §6.7 + v2 publisher unwrap 계승; Y-3) ---------- + def parse_llm_json(raw: str, label: str, salvage_notes: list[dict[str, Any]]) -> dict[str, Any]: + text = (raw or "").strip() + if not text or text.startswith("{{"): + raise ValueError(f"{label}: no injected JSON body") + if text.startswith("```"): + salvage_notes.append({"label": label, "salvage": "stripped_code_fence"}) + parts = text.split("```") + for part in parts: + part = part.strip() + if part.startswith("json"): + part = part[4:].strip() + if part.startswith("{"): + text = part + break + start = text.find("{") + if start < 0: + raise ValueError(f"{label}: JSON object start not found") + if start > 0: + salvage_notes.append({"label": label, "salvage": "stripped_leading_prose"}) + obj, end = JSON_DECODER.raw_decode(text[start:].strip()) + tail = text[start:].strip()[end:].strip() + if tail: + salvage_notes.append({"label": label, "salvage": "stripped_trailing_tail", "tail_preview": tail[:80]}) + if not isinstance(obj, dict): + raise ValueError(f"{label}: JSON root must be object") + if obj.get("type") == "text" and isinstance(obj.get("text"), str): + return parse_llm_json(obj["text"], f"{label}.text", salvage_notes) + if obj.get("success") is True and isinstance(obj.get("output"), str): + return parse_llm_json(obj["output"], f"{label}.output", salvage_notes) + return obj + + # ---------- sparse -> canonical schema expander (사실 창작 없이 default만 복원) ---------- + # v2 워커 OUTPUT_CONTRACT의 domain_payload 상위 키·순서·기본형 (B5는 후보 유형별 가변 스키마이므로 제외) + DOMAIN_PAYLOAD_CANON: dict[str, list[tuple[str, Any]]] = { + "B1_Money_Successor": [ + ("domain_legal_effect_tag", None), ("title_hint", None), ("summary_hint", None), + ("conflict_patterns", []), ("asset_bindings", []), ("parties_hint", []), + ("temporal_hint", None), ("hazard_flags", []), ("legal_effect_tags", []), + ("event_state_type", None), ("action_summary", None), ("time_interval_hint", None), + ("asset_binding_hint", None), ("mixed_contract_split_hint", None), ("conflict_pattern_hint", None), + ("candidate_details", {}), ("facts", {}), ("money_claim_details", {}), + ("payment_history", []), ("demand_or_notice_hints", []), ("admission_hints", []), + ("successor_liability_hints", []), ("successor_link_uncertainty", None), ("conflict_flags", []), + ], + "B2_Secured_Registry": [ + ("domain_legal_effect_tag", None), ("title_hint", None), ("summary_hint", None), + ("conflict_patterns", []), ("asset_bindings", []), ("parties_hint", []), + ("temporal_hint", None), ("hazard_flags", []), ("legal_effect_tags", []), + ("event_state_type", None), ("action_summary", None), ("time_interval_hint", None), + ("asset_binding_hint", None), ("mixed_contract_split_hint", None), ("conflict_pattern_hint", None), + ("candidate_details", {}), ("facts", {}), ("registry_rows", []), + ("fractional_effect_events", []), ("secured_debt_chain", []), ("payment_allocation", []), + ("mortgage_extinguishment_hints", []), ("auction_steps", []), ("dividend_distribution_hints", []), + ("derivative_invalidity_hints", []), ("registry_object_display", None), + ], + "B3_Land_Valuation": [ + ("domain_legal_effect_tag", None), ("title_hint", None), ("summary_hint", None), + ("conflict_patterns", []), ("asset_bindings", []), ("parties_hint", []), + ("temporal_hint", None), ("hazard_flags", []), ("legal_effect_tags", []), + ("event_state_type", None), ("action_summary", None), ("time_interval_hint", None), + ("asset_binding_hint", None), ("mixed_contract_split_hint", None), ("conflict_pattern_hint", None), + ("candidate_details", {}), ("facts", {}), ("valuation_rows", []), + ("valuation_matrix", []), ("valuation_object", None), ("use_gain_facts", []), + ("possession_state", {}), ("delivery_or_possession_start_hints", []), ("unregistered_building_hints", []), + ("liability_overlap_hints", []), + ], + "B4_Actio": [ + ("domain_legal_effect_tag", None), ("title_hint", None), ("summary_hint", None), + ("conflict_patterns", []), ("asset_bindings", []), ("parties_hint", []), + ("temporal_hint", None), ("hazard_flags", []), ("legal_effect_tags", []), + ("event_state_type", None), ("action_summary", None), ("time_interval_hint", None), + ("asset_binding_hint", None), ("mixed_contract_split_hint", None), ("conflict_pattern_hint", None), + ("candidate_details", {}), ("facts", {}), ("preserved_claim_bundle", {}), + ("fraudulent_transfer_facts", []), ("debtor_act_hints", []), ("balance_sheet", {}), + ("encumbrance_timeline", []), ("valuation_matrix", []), ("remedy_mode_hints", []), + ("value_compensation_cap_hints", []), ("beneficiary_transferee_hints", []), ("bad_faith_good_faith_hints", []), + ("defense_rebuttal", []), + ], + } + + def _canon_payload(payload: dict[str, Any], domain_id: str) -> dict[str, Any]: + canon = DOMAIN_PAYLOAD_CANON.get(domain_id) + if canon is None: + return payload # B5: 후보 유형별 가변 스키마 — 워커 발행 형태 유지 (v2 동작 동일) + out: dict[str, Any] = {} + for key, default in canon: + if key in payload: + out[key] = payload[key] + else: + out[key] = [] if isinstance(default, list) else {} if isinstance(default, dict) else None + for key, value in payload.items(): + if key not in out: + out[key] = value + return out + + def expand_candidate(cand: dict[str, Any], domain_id: str) -> dict[str, Any]: + out = dict(cand) + out.setdefault("Reason", None) + out.setdefault("PriorAct", None) + out.setdefault("ReasonRefs", []) + out.setdefault("Legal_Keywords", []) + out.setdefault("core_field_base", {}) + if out.get("JuristicAct") is None: + out["JuristicAct"] = {"label": None} + if out.get("amount") is None: + out["amount"] = {} + out.setdefault("source_evidence_indexes", []) + prov = _dict(out.get("provenance")) + prov.setdefault("source_event_candidate_ids", []) + prov.setdefault("source_meeting_clause_ids", []) + prov.setdefault("source_evidence_indexes", list(out.get("source_evidence_indexes") or [])) + out["provenance"] = prov + out.setdefault("downstream_seed_refs", {}) + ext = _dict(out.get("extensions")) + ext["domain_payload"] = _canon_payload(_dict(ext.get("domain_payload")), domain_id) + out["extensions"] = ext + return out + + def expand_review_item(item: dict[str, Any], domain_id: str, idx: int) -> dict[str, Any]: + out = dict(item) + out.setdefault("review_id", f"{DOMAIN_PREFIX[domain_id]}:review:{idx:03d}") + out.setdefault("severity", "SOFT_WARNING") + out.setdefault("issue_type", "review_required") + out.setdefault("source_event_candidate_ids", []) + out.setdefault("source_evidence_indexes", []) + out.setdefault("source_meeting_clause_ids", []) + out.setdefault("recommended_downstream_owner", "Stage2") + return out + + def digest_summary(input_digests: dict[str, Any]) -> str: + import hashlib + return hashlib.sha256(json.dumps(input_digests, ensure_ascii=False, sort_keys=True).encode("utf-8")).hexdigest() + + # ---------- 워커 출력 수용 검증 (publisher 이식, sparse 인지형: expander 후 강검증) ---------- + def validate_seed_object(seed_obj: dict[str, Any], domain_id: str, stage_a: dict[str, Any], warnings: list[dict[str, Any]]) -> None: + if seed_obj.get("schema_version") != "task_c_bo_stage_b_domain_bo_seed.v2": + raise ValueError(f"{domain_id}: seed schema_version mismatch") + if seed_obj.get("domain_id") != domain_id: + raise ValueError(f"{domain_id}: seed domain_id mismatch") + if seed_obj.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError(f"{domain_id}: seed status must be READY or READY_WITH_REVIEW") + guard = _dict(_dict(seed_obj.get("transport_metadata")).get("slice_guard")) + created = guard.get("source_stage_a_created_at_utc") + if created and created != stage_a.get("created_at_utc"): + raise ValueError(f"{domain_id}: stale seed output: Stage A created_at_utc mismatch") + summary = guard.get("input_digests_summary") + full = guard.get("input_digests_sha256") + expected = digest_summary(stage_a.get("input_digests_sha256") or {}) + if isinstance(full, dict) and full and full != stage_a.get("input_digests_sha256"): + raise ValueError(f"{domain_id}: stale seed output: input digest mismatch") + if isinstance(summary, str) and summary and summary != expected: + raise ValueError(f"{domain_id}: stale seed output: input digest summary mismatch") + if not created and not summary and not full: + warnings.append({"domain_id": domain_id, "warning": "seed output carries no freshness guard"}) + + def validate_candidate(cand: dict[str, Any], domain_id: str, idx: int) -> None: + prefix = DOMAIN_PREFIX[domain_id] + ref = cand.get("candidate_ref") + if not isinstance(ref, str) or not CANDIDATE_REF_RE.fullmatch(ref): + raise ValueError(f"{domain_id}.bo_seed_candidates[{idx}].candidate_ref invalid") + if not ref.startswith(prefix + ":"): + raise ValueError(f"{domain_id}.bo_seed_candidates[{idx}].candidate_ref prefix mismatch") + for forbidden in ("BO_ID", "id", "Evidence", "EvidenceTitles"): + if forbidden in cand: + raise ValueError(f"{domain_id}.{ref}: final field {forbidden} is prohibited") + if cand.get("Reason") is not None: + raise ValueError(f"{domain_id}.{ref}: Reason must be null/absent") + if cand.get("PriorAct") is not None: + raise ValueError(f"{domain_id}.{ref}: PriorAct must be null/absent") + if cand.get("ReasonRefs") not in ([], None): + raise ValueError(f"{domain_id}.{ref}: ReasonRefs must be []/absent") + + # ---------- PostB_1 이식: sort key / duplicate keys / schema risk ---------- + def _source_refs(seed: dict[str, Any]) -> dict[str, list[str]]: + provenance = _dict(seed.get("provenance")) + return { + "source_evidence_indexes": _strings(seed.get("source_evidence_indexes") or provenance.get("source_evidence_indexes")), + "source_event_candidate_ids": _strings(provenance.get("source_event_candidate_ids")), + "source_meeting_clause_ids": _strings(provenance.get("source_meeting_clause_ids")), + } + + def _sort_key(seed: dict[str, Any]) -> dict[str, Any]: + core = _dict(seed.get("core_field_base")) + domain = seed.get("source_domain") + juristic = _dict(seed.get("JuristicAct")) + return { + "BehaviorTime": core.get("BehaviorTime"), + "domain_order": DOMAIN_ORDER.index(domain) if domain in DOMAIN_ORDER else 99, + "BOType": seed.get("BOType"), + "ActionType": seed.get("ActionType"), + "JuristicActLabel": juristic.get("label"), + "Action": seed.get("Action"), + "candidate_ref": seed.get("candidate_ref"), + } + + def _duplicate_key(seed: dict[str, Any]) -> tuple[Any, ...]: + core = _dict(seed.get("core_field_base")) + juristic = _dict(seed.get("JuristicAct")) + refs = _source_refs(seed) + return ( + tuple(sorted(refs["source_evidence_indexes"])), + tuple(sorted(refs["source_event_candidate_ids"])), + seed.get("BOType"), + seed.get("ActionType"), + juristic.get("label"), + str(seed.get("Action") or "").strip(), + str(core.get("BehaviorTime") or "").strip(), + str(core.get("Object") or "").strip(), + ) + + def _normalize_juristic(value: Any) -> Any: + if value is None: + return None + if isinstance(value, dict): + label = value.get("label") + return {"label": str(label).strip()} if label not in (None, "") else {"label": None} + text = str(value).strip() + return {"label": text} if text else None + + def _seed_payload(cand: dict[str, Any], domain_id: str) -> dict[str, Any]: + payload: dict[str, Any] = {} + for key in ALLOWED_SEED_KEYS: + if key in cand: + payload[key] = cand[key] + payload["candidate_ref"] = cand.get("candidate_ref") + payload["source_domain"] = domain_id + payload["JuristicAct"] = _normalize_juristic(payload.get("JuristicAct")) + provenance = _dict(payload.get("provenance")) + payload["provenance"] = { + "source_event_candidate_ids": _strings(provenance.get("source_event_candidate_ids")), + "source_meeting_clause_ids": _strings(provenance.get("source_meeting_clause_ids")), + "source_evidence_indexes": _strings(cand.get("source_evidence_indexes") or provenance.get("source_evidence_indexes")), + "source_domain": domain_id, + } + payload["source_evidence_indexes"] = _strings(cand.get("source_evidence_indexes") or payload["provenance"]["source_evidence_indexes"]) + payload["Reason"] = None + payload["PriorAct"] = None + payload["ReasonRefs"] = [] + payload["core_field_base"] = _dict(payload.get("core_field_base")) + payload["downstream_seed_refs"] = _dict(payload.get("downstream_seed_refs")) + payload["extensions"] = _dict(payload.get("extensions")) + if "domain_payload" not in payload["extensions"]: + payload["extensions"]["domain_payload"] = {} + return payload + + def _compact_exception_payload(seeds: list[dict[str, Any]]) -> list[dict[str, Any]]: + compact = [] + for seed in seeds: + compact.append({ + "candidate_ref": seed.get("candidate_ref"), + "source_domain": seed.get("source_domain"), + "BOType": seed.get("BOType"), + "ActionType": seed.get("ActionType"), + "JuristicAct": seed.get("JuristicAct"), + "Action": seed.get("Action"), + "core_field_base": seed.get("core_field_base"), + "amount": seed.get("amount"), + "source_refs": _source_refs(seed), + }) + return compact + + def main() -> None: + _init() + salvage_notes: list[dict[str, Any]] = [] + guard_warnings: list[dict[str, Any]] = [] + stage_a_outer = read_json_doc(STAGE_A_PATH) + stage_a = _dict(_dict(stage_a_outer).get("stage_a_context") or stage_a_outer) + if stage_a.get("schema_version") != "task_c_bo_stage_a_context.v1" or stage_a.get("status") != "READY": + raise RuntimeError("Stage A context must be READY task_c_bo_stage_a_context.v1") + manifest = _dict(read_json_doc(MANIFEST_PATH)) + universe = { + "source_event_candidate_ids": set(_strings(manifest.get("event_candidate_ids"))), + "source_evidence_indexes": set(_strings(manifest.get("evidence_index_set"))), + "source_meeting_clause_ids": set(_strings(manifest.get("meeting_clause_ids"))), + } + if not universe["source_evidence_indexes"]: + raise RuntimeError("source universe manifest has no evidence indexes") + + # 1) 워커 출력 수용: 관용 파싱 -> sparse 검증 -> expander -> seed 파일 발행 + seed_objects: dict[str, dict[str, Any]] = {} + review_handoff_items: list[dict[str, Any]] = [] + for domain_id in DOMAIN_ORDER: + outer = parse_llm_json(WORKER_RAW_FIELDS[domain_id], domain_id, salvage_notes) + seed_obj = _dict(outer.get("stage_b_domain_bo_seed_output")) + if not seed_obj: + raise RuntimeError(f"{domain_id}: stage_b_domain_bo_seed_output missing") + validate_seed_object(seed_obj, domain_id, stage_a, guard_warnings) + cands = _list(seed_obj.get("bo_seed_candidates")) + expanded: list[dict[str, Any]] = [] + for idx, cand in enumerate(cands): + if not isinstance(cand, dict): + raise RuntimeError(f"{domain_id}.bo_seed_candidates[{idx}] must be object") + validate_candidate(cand, domain_id, idx) + full = expand_candidate(cand, domain_id) + # membership 검사 (PostB_1 이식 + 개정 defer 정책 §7): universe 밖 source ref는 hard BLOCK + prov = full["provenance"] + for key, allowed in ( + ("source_event_candidate_ids", universe["source_event_candidate_ids"]), + ("source_meeting_clause_ids", universe["source_meeting_clause_ids"]), + ("source_evidence_indexes", universe["source_evidence_indexes"]), + ): + if not allowed: + continue + bad = [v for v in _strings(prov.get(key)) if v not in allowed] + if bad: + raise RuntimeError(f"BLOCK: {domain_id}.{cand.get('candidate_ref')}: {key} outside Stage A universe: {bad}") + top_bad = [v for v in _strings(full.get("source_evidence_indexes")) if v not in universe["source_evidence_indexes"]] + if top_bad: + raise RuntimeError(f"BLOCK: {domain_id}.{cand.get('candidate_ref')}: source_evidence_indexes outside Stage A universe: {top_bad}") + expanded.append(full) + seed_obj = dict(seed_obj) + seed_obj["bo_seed_candidates"] = expanded + reviews = [expand_review_item(_dict(it), domain_id, idx + 1) for idx, it in enumerate(_list(seed_obj.get("domain_review_queue")))] + seed_obj["domain_review_queue"] = reviews + # transport 재구성 (워커 출력 금지 필드 — R0가 provenance에서 결정적으로 재구성) + seed_obj["transport_candidate_refs"] = [ + { + "candidate_ref": c.get("candidate_ref"), + "payload_index": i, + "source_event_candidate_ids": _strings(_dict(c.get("provenance")).get("source_event_candidate_ids")), + "source_evidence_indexes": _strings(c.get("source_evidence_indexes")), + "source_meeting_clause_ids": _strings(_dict(c.get("provenance")).get("source_meeting_clause_ids")), + } + for i, c in enumerate(expanded) + ] + metadata = _dict(seed_obj.get("transport_metadata")) + metadata.setdefault("stage", "Task_C_BO.Stage_B.DomainSeed") + metadata.setdefault("source_task", domain_id) + metadata.setdefault("domain_label", DOMAIN_LABELS[domain_id]) + metadata["slice_guard"] = { + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": stage_a.get("created_at_utc"), + "input_digests_sha256": stage_a.get("input_digests_sha256"), + "source_universe": { + "meeting_clause_ids": sorted(universe["source_meeting_clause_ids"]), + "event_candidate_ids": sorted(universe["source_event_candidate_ids"]), + "evidence_indexes": sorted(universe["source_evidence_indexes"]), + }, + "stage_b_worker_contract": { + "context_is_read_only": True, + "bo_id_finalization_allowed": False, + "candidate_generation_allowed": True, + }, + } + seed_obj["transport_metadata"] = metadata + seed_obj["handoff_guard"] = { + "schema_version": "task_c_bo_stage_b_seed_handoff_guard.v1", + "handoff_target": SEED_DOCS[domain_id], + "publisher_task": "Task_C_BO_R0_seed_reducer_and_exception_planner", + "source_worker_task": SEED_TASK_NAMES[domain_id], + "source_task": domain_id, + "source_contract": "direct_prev_task_json_output.stage_b_domain_bo_seed_output", + "source_prev_path": f"prev.{SEED_TASK_NAMES[domain_id]}.json_output", + "domain_id": domain_id, + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": stage_a.get("created_at_utc"), + "input_digests_sha256": stage_a.get("input_digests_sha256"), + } + write_doc(SEED_DOCS[domain_id], json.dumps({"stage_b_domain_bo_seed_output": seed_obj}, ensure_ascii=False, indent=2)) + seed_objects[domain_id] = seed_obj + for idx, item in enumerate(reviews, start=1): + issue = item.get("issue_type") if item.get("issue_type") in REVIEW_ISSUE_ENUM else "review_required" + owner = item.get("recommended_downstream_owner") + review_handoff_items.append({ + "review_id": item.get("review_id"), + "source_domain": domain_id, + "severity": item.get("severity") if item.get("severity") in ("SOFT_WARNING", "HARD_WARNING") else "SOFT_WARNING", + "issue_type": issue, + "source_event_candidate_ids": _strings(item.get("source_event_candidate_ids")), + "source_evidence_indexes": _strings(item.get("source_evidence_indexes")), + "source_meeting_clause_ids": _strings(item.get("source_meeting_clause_ids")), + "downstream_owner": owner if owner in DOWNSTREAM_OWNER_ENUM else "Stage2", + "template_note": "후속 단계에서 해당 review 항목의 증거와 법률상 의미를 재검토한다.", + }) + + # 2) ledger 구성 (PostB_1 이식) + input_candidate_total = 0 + seeds: list[dict[str, Any]] = [] + for domain_id in DOMAIN_ORDER: + cands = seed_objects[domain_id]["bo_seed_candidates"] + input_candidate_total += len(cands) + for cand in cands: + seeds.append(_seed_payload(cand, domain_id)) + if not seeds: + raise RuntimeError("no seed candidate from Stage B workers") + + seen_refs: set[str] = set() + ledger_candidates: list[dict[str, Any]] = [] + deterministic_decisions: list[dict[str, Any]] = [] + exceptions: list[dict[str, Any]] = [] + duplicate_buckets: dict[tuple[Any, ...], list[dict[str, Any]]] = {} + policy_review_counter = 0 + + def add_policy_review(domain_id: str, issue_type: str, refs: dict[str, list[str]], severity: str = "SOFT_WARNING") -> None: + nonlocal policy_review_counter + policy_review_counter += 1 + review_handoff_items.append({ + "review_id": f"R0:policy:{policy_review_counter:03d}", + "source_domain": domain_id, + "severity": severity, + "issue_type": issue_type, + "source_event_candidate_ids": refs.get("source_event_candidate_ids", []), + "source_evidence_indexes": refs.get("source_evidence_indexes", []), + "source_meeting_clause_ids": refs.get("source_meeting_clause_ids", []), + "downstream_owner": "Stage2", + "template_note": "결정적 defer 정책에 의해 보존된 검토 항목이다.", + }) + + for seed in seeds: + ref = seed.get("candidate_ref") + if not isinstance(ref, str) or not CANDIDATE_REF_RE.fullmatch(ref): + raise RuntimeError(f"invalid candidate_ref: {ref!r}") + if ref in seen_refs: + raise RuntimeError(f"duplicate candidate_ref: {ref}") + seen_refs.add(ref) + refs = _source_refs(seed) + # hard BLOCK: universe 밖 ref는 soft-strip 이후에도 남아있으면 안 된다 (방어적 재검사) + for key, values in refs.items(): + allowed = universe.get(key, set()) + outside = [v for v in values if allowed and v not in allowed] + if outside: + raise RuntimeError(f"{ref}: {key} outside Stage A universe: {outside}") + flags: list[str] = [] + # 결정적 defer 정책 (개선전략서 X-2): + if not refs["source_evidence_indexes"]: + flags.append("meeting_only_evidence_gap") + add_policy_review(seed.get("source_domain"), "meeting_only_evidence_gap", refs) + if seed.get("BOType") not in {"event", "state"} or not seed.get("ActionType") or not ( + seed.get("Action") or _dict(_dict(seed.get("extensions")).get("domain_payload")).get("action_summary") + ): + flags.append("schema_field_fallback") + add_policy_review(seed.get("source_domain"), "schema_field_fallback", refs) + link_candidates = _strings(_dict(seed.get("downstream_seed_refs")).get("prior_candidate_refs")) + if len(link_candidates) > 1: + flags.append("prior_link_ambiguous") + add_policy_review(seed.get("source_domain"), "prior_link_ambiguous", refs) + duplicate_buckets.setdefault(_duplicate_key(seed), []).append(seed) + ledger_candidates.append({ + "candidate_ref": ref, + "source_domain": seed.get("source_domain"), + "seed_payload": seed, + "source_refs": refs, + "deterministic_sort_key": _sort_key(seed), + "flags": flags, + }) + + # exact duplicate: provenance union 무손실이므로 canonical merge (v2 규칙 계승) + for bucket in duplicate_buckets.values(): + if len(bucket) <= 1: + continue + canonical = bucket[0].get("candidate_ref") + duplicates = [item.get("candidate_ref") for item in bucket[1:]] + deterministic_decisions.append({ + "decision_type": "EXACT_DUPLICATE_MERGE", + "canonical_candidate_ref": canonical, + "duplicate_candidate_refs": duplicates, + "basis": "exact duplicate deterministic rule (provenance-lossless union)", + }) + + # near duplicate: KEEP_SEPARATE + cluster id + review (LLM 금지 — defer 정책) + near_buckets: dict[tuple[Any, ...], list[dict[str, Any]]] = {} + for seed in seeds: + refs = _source_refs(seed) + key = ( + tuple(sorted(refs["source_evidence_indexes"])), + tuple(sorted(refs["source_event_candidate_ids"])), + seed.get("BOType"), + seed.get("ActionType"), + ) + near_buckets.setdefault(key, []).append(seed) + near_cluster_count = 0 + pack_field_conflicts: list[dict[str, Any]] = [] + for bucket in near_buckets.values(): + if len(bucket) <= 1 or len({_duplicate_key(s) for s in bucket}) <= 1: + continue + near_cluster_count += 1 + cluster_id = f"near-dup-{near_cluster_count:03d}" + cluster_refs = [str(s.get("candidate_ref")) for s in bucket] + for item in ledger_candidates: + if item["candidate_ref"] in cluster_refs: + item.setdefault("near_dup_cluster_id", cluster_id) + if "near_duplicate_kept_separate" not in item["flags"]: + item["flags"].append("near_duplicate_kept_separate") + add_policy_review(bucket[0].get("source_domain"), "near_duplicate_kept_separate", + {"source_evidence_indexes": _source_refs(bucket[0])["source_evidence_indexes"], + "source_event_candidate_ids": _source_refs(bucket[0])["source_event_candidate_ids"], + "source_meeting_clause_ids": []}) + # non-deferrable 판정(X-3 4중 조건): 같은 near cluster에서 BehaviorTime 또는 amount가 + # 서로 다른 non-null 값으로 충돌하면 writer가 단일 값을 고를 수 없으므로 pack에 수록 + times = {str(_dict(s.get("core_field_base")).get("BehaviorTime")) for s in bucket if _dict(s.get("core_field_base")).get("BehaviorTime")} + amounts = set() + for s in bucket: + av = s.get("amount") + if isinstance(av, dict) and av.get("value_text"): + amounts.add(str(av.get("value_text"))) + elif isinstance(av, str) and av.strip(): + amounts.add(av.strip()) + if len(times) > 1 or len(amounts) > 1: + pack_field_conflicts.append({ + "exception_id": f"EX-FIELD-{len(pack_field_conflicts) + 1:03d}", + "exception_type": "field_conflict", + "candidate_refs": cluster_refs, + "reason": "same-source candidates carry conflicting BehaviorTime/amount values", + "conflicting_values": {"BehaviorTime": sorted(times), "amount": sorted(amounts)}, + "compact_candidate_payload": _compact_exception_payload(bucket), + "allowed_decisions": ["KEEP_SEPARATE", "MERGE", "SPLIT", "DROP", "BLOCK_REVIEW"], + "escalation_flag": True, + }) + + exceptions.extend(pack_field_conflicts) + has_exceptions = bool(exceptions) + + # 3) conservation invariant (write 전) + merged_absorbed = sum(len(_strings(d.get("duplicate_candidate_refs"))) for d in deterministic_decisions) + if len(ledger_candidates) != input_candidate_total: + raise RuntimeError(f"ledger candidate count {len(ledger_candidates)} != input candidates {input_candidate_total}") + if len(seen_refs) != input_candidate_total: + raise RuntimeError("candidate_ref conservation failed") + + ledger = { + "postb_seed_ledger": { + "schema_version": "task_c_bo_postb_seed_ledger.v1", + "status": "READY", + "source_stage_a_created_at_utc": stage_a.get("created_at_utc"), + "input_digests_sha256": stage_a.get("input_digests_sha256"), + "source_universe": { + "source_event_candidate_ids": sorted(universe["source_event_candidate_ids"]), + "source_evidence_indexes": sorted(universe["source_evidence_indexes"]), + "source_meeting_clause_ids": sorted(universe["source_meeting_clause_ids"]), + }, + "stage_b_source_contract": { + "schema_version": "task_c_bo_stage_b_bo_seed_universe.compat_from_r0.v1", + "status": "READY", + "compatibility_source": "r0_seed_reducer.direct_worker_outputs", + }, + "ledger_candidates": sorted(ledger_candidates, key=lambda item: ( + item["deterministic_sort_key"].get("BehaviorTime") is None, + item["deterministic_sort_key"].get("BehaviorTime") or "", + item["deterministic_sort_key"].get("domain_order", 99), + item["deterministic_sort_key"].get("BOType") or "", + item["deterministic_sort_key"].get("ActionType") or "", + item["deterministic_sort_key"].get("JuristicActLabel") or "", + item["deterministic_sort_key"].get("Action") or "", + item["deterministic_sort_key"].get("candidate_ref") or "", + )), + "deterministic_decisions": deterministic_decisions, + "exception_pack": { + "has_exceptions": has_exceptions, + "clusters": [], + "field_conflicts": pack_field_conflicts, + "link_ambiguities": [], + "schema_risks": [], + }, + "audit_trace": { + "removed_or_sidecar_fields": [], + "source_membership_policy": "outside-universe source ref => hard BLOCK (defer 정책 §7)", + "normalization_notes": salvage_notes + guard_warnings, + }, + } + } + write_doc(LEDGER_PATH, json.dumps(ledger, ensure_ascii=False, indent=2)) + + pack = { + "schema_version": "stage1_part2_exception_pack.v1", + "has_exceptions": has_exceptions, + "exception_count": len(exceptions), + "exceptions": exceptions, + "budget": {"max_candidates_per_exception": 8, "max_payload_chars_per_candidate": 2000}, + } + write_doc(PACK_PATH, json.dumps(pack, ensure_ascii=False, indent=2)) + + handoff = { + "schema_version": "stage1_part2_review_handoff.v1", + "status": "PENDING_FINALIZE", + "review_items": review_handoff_items, + } + write_doc(REVIEW_HANDOFF_PATH, json.dumps(handoff, ensure_ascii=False, indent=2)) + + print(json.dumps({ + "status": "READY", + "message": "R0 seed reducer 완료: ledger/exception pack/review handoff 생성", + "ledger_path": LEDGER_PATH, + "exception_pack_path": PACK_PATH, + "review_handoff_path": REVIEW_HANDOFF_PATH, + "has_exceptions": has_exceptions, + "exception_count": len(exceptions), + "candidate_counts": { + "input": input_candidate_total, + "ledger": len(ledger_candidates), + "exact_duplicate_absorbed": merged_absorbed, + "near_dup_clusters": near_cluster_count, + }, + "review_item_count": len(review_handoff_items), + "salvage_count": len(salvage_notes), + }, ensure_ascii=False)) + + if __name__ == "__main__": + try: + main() + except Exception as exc: + print(json.dumps({"status": "FAILED", "message": "R0 seed reducer 실패: downstream 진행 금지", + "reason": str(exc)}, ensure_ascii=False)) + raise + - task_name: Task_C_BO_R1_exception_adjudicator + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: low + llm_verbosity: low + max_iterations: 1 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 20m + preflight: true + preflight_files: + - quality_gates/stage1_part2_exception_pack.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + + TASK_NAME: Task_C_BO_R1_exception_adjudicator + STAGE: PostB conditional exception adjudicator (Part 1 v3 GB 패턴) + MISSION: 결정적 reducer(R0)가 non-deferrable로 판정한 compact exception만 판정한다. 병합·최종 파일 작성·사실 창작은 하지 않는다. + + + + - 유일한 입력은 preflight로 제공된 `quality_gates/stage1_part2_exception_pack.json`이다. + - Stage A context, seed ledger 전문, raw evidence, meeting 원문을 읽거나 요청하지 않는다. + - pack에 없는 exception_id·candidate_ref·bh# id를 창작하지 않는다. + - BO.json, ledger, review handoff, signal 파일을 작성하지 않는다. + - 출력 파일은 `stage1_tmp/task_c_bo/postb_adjudication_decisions.json` 하나뿐이다. + + + + 1. preflight로 제공된 exception pack의 `has_exceptions`를 확인한다. + 2. `has_exceptions == false`이면: `write_file(overwrite=true)`로 아래 no-exception 객체를 `stage1_tmp/task_c_bo/postb_adjudication_decisions.json`에 저장하고 `"NO_EXCEPTIONS"`만 출력한 뒤 즉시 종료한다(terminate). 다른 어떤 파일도 읽지 않는다. + {"postb_exception_adjudication": {"schema_version": "task_c_bo_postb_exception_adjudication.v1", "status": "READY_NO_EXCEPTIONS", "exception_count": 0, "canonical_decisions": [], "field_decisions": [], "link_decisions": [], "semantic_gate_decisions": [], "blocked_review_items": []}} + 3. `has_exceptions == true`이면: 각 exception을 `compact_candidate_payload`만으로 판정한다. 추가 read는 금지된다. + 4. 판정 규칙: + - canonical decision: KEEP_SEPARATE, MERGE, SPLIT, DROP, BLOCK_REVIEW 중 하나. MERGE는 `input_candidate_refs`와 `merge_target_ref`를 명시한다. + - field conflict: 제공된 conflicting_values 중 하나를 `selected_value`로 선택하거나 BLOCK_REVIEW. 임의 값 창작 금지. + - link ambiguity: exception에 나열된 candidate ref 중 선택, NO_LINK, 또는 BLOCK_REVIEW. + - semantic risk: PASS, WARNING, BLOCK_REVIEW. + - compact payload로 확정할 수 없으면 반드시 `blocked_review_items`에 넣는다(확신 없는 확정 금지 — 인간 검토 라우팅). + 5. `write_file(overwrite=true)`로 결과를 저장한다. root는 `postb_exception_adjudication`이며 schema_version은 `task_c_bo_postb_exception_adjudication.v1`, status는 `READY`, `exception_count`는 판정한 exception 수다. 모든 decision은 pack의 `exception_id`를 인용한다. + 6. `"R1 예외 판정 완료 (decisions=<건수>)"`만 출력하고 작업을 끝낸다(terminate). + + + + - Stage 1은 법률효과·청구원인을 확정하지 않는다. 두 값을 모두 보존하거나 Stage 2로 defer할 수 있는 사안은 이미 R0가 결정적으로 처리했으므로, 여기 도달한 항목은 final writer가 단일 값을 선택해야만 진행되는 사안이다. + - 같은 source에 근거한 상충 값(BehaviorTime·amount)은: 원문 근거가 더 구체적인 쪽(payload의 core_field_base·amount 기재가 더 완전한 후보)을 선택하고, 우열을 가릴 수 없으면 BLOCK_REVIEW. + - KEEP_SEPARATE가 provenance를 보존하는 기본값이다. MERGE는 provenance 합집합이 무손실일 때만 선택한다. + - DROP은 어떤 경우에도 source 유일 후보에 적용하지 않는다. + + + + - exception pack 부재·파싱 불가: 즉시 중단하고 채팅으로만 보고한다. decisions 파일은 쓰지 않는다. + - tool 오류: 1회만 재시도. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + - task_name: Task_C_BO_F0_final_bo_compiler_gate_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: |- + #!/usr/bin/env python3 + # Task_C_BO_F0_final_bo_compiler_gate_writer (v3) + # PostB_3(final compiler) + PostB_4(final gate/writer) 통합. 입력은 파일 계약(ledger/decisions/stage_a). + # Spec: Part_2_Improvement_Strategy_Claude_v1.md §9 (bh# 규칙 N-6, Reason/PriorAct 정책 R-5) + from __future__ import annotations + import itertools + import json + import re + import sys + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + STAGE_A_PATH = "stage1_tmp/task_c_bo/stage_a_context.json" + LEDGER_PATH = "stage1_tmp/task_c_bo/postb_seed_ledger.json" + DECISIONS_PATH = "stage1_tmp/task_c_bo/postb_adjudication_decisions.json" + REVIEW_HANDOFF_PATH = "quality_gates/stage1_part2_review_handoff.json" + BUNDLE_COMPACT_PATH = "stage1_tmp/task_c_bo/postb_compiled_bundle_compact.json" + TARGET_NAME = "BO.json" + BH_ID_RE = re.compile(r"^bh[1-9][0-9]*$") + ACTION_TYPE_ENUM = { + "법률행위(legal acts)", + "준법률행위(quasi-legal acts)", + "사실행위(factual acts)", + "위법행위(unlawful acts)", + "소송행위(litigation acts)", + } + ALLOWED_TOP_LEVEL = { + "BO_ID", "id", "BOType", "ActionType", "JuristicAct", "Action", "Reason", + "PriorAct", "ReasonRefs", "Legal_Keywords", "core_field_base", "amount", + "EvidenceTitles", "Evidence", "source_evidence_indexes", "provenance", + "downstream_seed_refs", "extensions", + } + REQUIRED_TOP_LEVEL = { + "BO_ID", "id", "BOType", "ActionType", "JuristicAct", "Action", "Reason", + "PriorAct", "ReasonRefs", "Legal_Keywords", "core_field_base", + "EvidenceTitles", "Evidence", "source_evidence_indexes", "provenance", + "downstream_seed_refs", + } + CORE_KEYS = [ + "Performer", "PerformerType", "Action_proposal", "Subject", "Object", + "BehaviorTime", "TimeText", "TimePrecision", "StatementType", "Perspective", + ] + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-c-bo-f0-final-bo-compiler-gate-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse_mcp(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "id": _mid(), "method": "tools/call", + "params": {"name": name, "arguments": args}}, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse_mcp(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def read_json_doc(path: str) -> Any: + result = _call("read_docs", {"doc_names": [path]}) + content = result["result"].get("content", []) + text = content[0].get("text", "") if content else "" + try: + parsed = json.loads(text) + except json.JSONDecodeError as exc: + raise RuntimeError(f"Invalid read_docs envelope for {path}") from exc + if isinstance(parsed, dict) and "results" in parsed: + rows = parsed.get("results") or [] + if not rows or rows[0].get("error"): + raise RuntimeError(f"read_docs failed for {path}: {rows[:1]}") + inner = rows[0].get("content") or "" + if isinstance(inner, str): + try: + return json.loads(inner) + except json.JSONDecodeError: + obj, end = JSON_DECODER.raw_decode(inner.strip()) + if inner.strip()[end:].strip(): + raise RuntimeError(f"{path}: non-json tail") + return obj + return inner + return parsed + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + # ---------- PostB_3 이식 ---------- + def _field_decision_map(adj: dict[str, Any]) -> dict[tuple[str, str], Any]: + out: dict[tuple[str, str], Any] = {} + for item in _list(adj.get("field_decisions")): + if isinstance(item, dict) and item.get("candidate_ref") and item.get("field") and item.get("selected_value") != "BLOCK_REVIEW": + out[(str(item["candidate_ref"]), str(item["field"]))] = item.get("selected_value") + return out + + def _link_decision_map(adj: dict[str, Any]) -> dict[str, dict[str, Any]]: + out: dict[str, dict[str, Any]] = {} + for item in _list(adj.get("link_decisions")): + if isinstance(item, dict) and item.get("candidate_ref"): + out[str(item["candidate_ref"])] = item + return out + + def _decision_sets(ledger: dict[str, Any], adj: dict[str, Any], blockers: list[Any]) -> tuple[set[str], dict[str, str]]: + dropped: set[str] = set() + merge_into: dict[str, str] = {} + for decision in _list(ledger.get("deterministic_decisions")): + if not isinstance(decision, dict) or decision.get("decision_type") != "EXACT_DUPLICATE_MERGE": + continue + canonical = decision.get("canonical_candidate_ref") + for dup in _strings(decision.get("duplicate_candidate_refs")): + if canonical: + merge_into[dup] = str(canonical) + dropped.add(dup) + for decision in _list(adj.get("canonical_decisions")): + if not isinstance(decision, dict): + continue + kind = decision.get("decision") + refs = _strings(decision.get("input_candidate_refs")) + if kind == "DROP": + dropped.update(_strings(decision.get("drop_candidate_refs")) or refs) + elif kind == "MERGE": + target = decision.get("merge_target_ref") or (refs[0] if refs else None) + if target: + for ref in refs: + if ref != target: + merge_into[ref] = str(target) + dropped.add(ref) + elif kind == "BLOCK_REVIEW": + blockers.append(decision) + return dropped, merge_into + + def _sort_tuple(item: dict[str, Any]) -> tuple[Any, ...]: + key = _dict(item.get("deterministic_sort_key")) + return ( + key.get("BehaviorTime") is None, + key.get("BehaviorTime") or "", + key.get("domain_order", 99), + key.get("BOType") or "", + key.get("ActionType") or "", + key.get("JuristicActLabel") or "", + key.get("Action") or "", + key.get("candidate_ref") or "", + ) + + def _juristic(value: Any) -> dict[str, Any] | None: + if value is None: + return None + if isinstance(value, dict): + label = value.get("label") + return {"label": str(label).strip()} if label not in (None, "") else {"label": None} + text = str(value).strip() + return {"label": text} if text else None + + def _core(seed: dict[str, Any]) -> dict[str, Any]: + src = _dict(seed.get("core_field_base")) + out = {key: src.get(key) for key in CORE_KEYS} + if out.get("Action_proposal") is None and seed.get("Action"): + out["Action_proposal"] = seed.get("Action") + if out.get("StatementType") is None: + out["StatementType"] = seed.get("BOType") + if out.get("Perspective") is None: + out["Perspective"] = "plaintiff" + return out + + def _amount(value: Any) -> dict[str, Any] | None: + if value is None: + return None + if isinstance(value, dict): + return { + "value_text": value.get("value_text") or value.get("text"), + "numeric_value": value.get("numeric_value"), + "currency": value.get("currency"), + } + text = str(value).strip() + return {"value_text": text, "numeric_value": None, "currency": None} if text else None + + def _evidence_item(index: str, source: Any, gaps: list[Any], bo_id: str) -> dict[str, Any]: + obj = _dict(source) + title = obj.get("source_title") or obj.get("title") or obj.get("evidence_title") or obj.get("document_title") or index + relevant = obj.get("relevant_content") or obj.get("excerpt") or obj.get("summary") or obj.get("content") + if relevant in (None, ""): + gaps.append({"BO_ID": bo_id, "evidence_index": index, "gap": "missing_relevant_content"}) + relevant = None + return { + "evidence_index": index, + "source_title": str(title), + "priority_class": obj.get("priority_class") or obj.get("priority") or None, + "relevant_content": relevant, + "authentication_status": obj.get("authentication_status") or obj.get("auth_status") or None, + "corroboration": obj.get("corroboration") or None, + "selection_basis": "source_evidence_indexes membership", + } + + def _downstream_refs(seed: dict[str, Any]) -> dict[str, Any]: + src = _dict(seed.get("downstream_seed_refs")) + return { + "claim_group_seed_refs_proposed": _strings(src.get("claim_group_seed_refs_proposed") or src.get("claim_group_seed_refs")), + "canonical_theory_graph_seed_ref_proposed": src.get("canonical_theory_graph_seed_ref_proposed") or src.get("canonical_theory_graph_seed_ref"), + "legal_effect_structure_seed_ref_proposed": src.get("legal_effect_structure_seed_ref_proposed") or src.get("legal_effect_structure_seed_ref"), + } + + def _keywords(seed: dict[str, Any], juristic: dict[str, Any] | None) -> list[str]: + out = _strings(seed.get("Legal_Keywords")) + domain_payload = _dict(_dict(seed.get("extensions")).get("domain_payload")) + out.extend(_strings(domain_payload.get("legal_effect_tags"))) + if isinstance(juristic, dict) and juristic.get("label"): + out.append(str(juristic["label"])) + deduped: list[str] = [] + for item in out: + if item not in deduped: + deduped.append(item) + return deduped + + def _evidence_map_from_stage_a(stage_a: dict[str, Any]) -> dict[str, Any]: + evidence_map = _dict(stage_a.get("evidence_authority_map")) + by_index = _dict(evidence_map.get("by_evidence_index")) + if by_index: + return by_index + out: dict[str, Any] = {} + for item in _list(evidence_map.get("items")): + if isinstance(item, dict): + idx = item.get("evidence_index") or item.get("index") or item.get("id") + if idx is not None: + out[str(idx)] = item + return out + + def _stage_a_universe(stage_a: dict[str, Any]) -> dict[str, set[str]]: + event_map = _dict(stage_a.get("event_candidate_map")) + evidence_map = _dict(stage_a.get("evidence_authority_map")) + meeting_map = _dict(stage_a.get("meeting_clause_map")) + event_ids = set(_strings(event_map.get("candidate_id_set"))) + evidence_ids = set(_strings(evidence_map.get("evidence_index_set"))) + meeting_ids = set(_strings(meeting_map.get("clause_order"))) + event_ids.update(str(k) for k in _dict(event_map.get("by_event_candidate_id")).keys()) + evidence_ids.update(str(k) for k in _dict(evidence_map.get("by_evidence_index")).keys()) + meeting_ids.update(str(k) for k in _dict(meeting_map.get("by_clause_id")).keys()) + return { + "source_event_candidate_ids": event_ids, + "source_evidence_indexes": evidence_ids, + "source_meeting_clause_ids": meeting_ids, + } + + # ---------- PostB_4 이식: 게이트 ---------- + def _add_gate(gates: list[dict[str, Any]], key: str, passed: bool, detail: str) -> None: + gates.append({"gate_key": key, "status": "PASS" if passed else "FAILED", "detail": detail}) + + def _validate_item(item: Any, idx: int, ids: set[str], universe: dict[str, set[str]]) -> list[str]: + errors: list[str] = [] + if not isinstance(item, dict): + return [f"item {idx} must be object"] + extra = sorted(set(item.keys()) - ALLOWED_TOP_LEVEL) + missing = sorted(REQUIRED_TOP_LEVEL - set(item.keys())) + if extra: + errors.append(f"{item.get('BO_ID', idx)} additional fields: {extra}") + if missing: + errors.append(f"{item.get('BO_ID', idx)} missing fields: {missing}") + bo_id = item.get("BO_ID") + expected = f"bh{idx}" + if bo_id != expected or item.get("id") != bo_id or not isinstance(bo_id, str) or not BH_ID_RE.fullmatch(bo_id): + errors.append(f"BO_ID/id sequence mismatch: expected {expected}") + if item.get("BOType") not in {"event", "state"}: + errors.append(f"{bo_id}.BOType invalid") + if item.get("ActionType") not in ACTION_TYPE_ENUM: + errors.append(f"{bo_id}.ActionType invalid") + juristic = item.get("JuristicAct") + if juristic is not None and (not isinstance(juristic, dict) or set(juristic.keys()) != {"label"}): + errors.append(f"{bo_id}.JuristicAct invalid") + for key in ("Action", "Reason"): + if not isinstance(item.get(key), str) or not item.get(key).strip(): + errors.append(f"{bo_id}.{key} must be non-empty string") + prior = item.get("PriorAct") + if prior is not None and prior not in ids: + errors.append(f"{bo_id}.PriorAct references missing BO_ID") + for ref in _list(item.get("ReasonRefs")): + if ref not in ids: + errors.append(f"{bo_id}.ReasonRefs references missing BO_ID {ref}") + core = item.get("core_field_base") + if not isinstance(core, dict) or set(core.keys()) != set(CORE_KEYS): + errors.append(f"{bo_id}.core_field_base keys invalid") + amount = item.get("amount") + if amount is not None and (not isinstance(amount, dict) or set(amount.keys()) - {"value_text", "numeric_value", "currency"}): + errors.append(f"{bo_id}.amount invalid") + evidence = _list(item.get("Evidence")) + evidence_indexes = _strings(item.get("source_evidence_indexes")) + evidence_index_set: set[str] = set() + titles: list[str] = [] + for ev in evidence: + if not isinstance(ev, dict): + errors.append(f"{bo_id}.Evidence item must be object") + continue + required_ev = {"evidence_index", "source_title", "priority_class", "relevant_content", "authentication_status", "corroboration", "selection_basis"} + if set(ev.keys()) != required_ev: + errors.append(f"{bo_id}.Evidence item keys invalid") + if isinstance(ev.get("evidence_index"), str): + evidence_index_set.add(ev["evidence_index"]) + if isinstance(ev.get("source_title"), str) and ev.get("source_title") not in titles: + titles.append(ev["source_title"]) + if item.get("EvidenceTitles") != titles: + errors.append(f"{bo_id}.EvidenceTitles mismatch") + if set(evidence_indexes) != evidence_index_set: + errors.append(f"{bo_id}.source_evidence_indexes must equal Evidence[].evidence_index") + if universe["source_evidence_indexes"] and not set(evidence_indexes).issubset(universe["source_evidence_indexes"]): + errors.append(f"{bo_id}.source_evidence_indexes outside Stage A universe") + provenance = item.get("provenance") + if not isinstance(provenance, dict) or set(provenance.keys()) != {"source_event_candidate_ids", "source_meeting_clause_ids", "source_domain"}: + errors.append(f"{bo_id}.provenance invalid") + else: + if universe["source_event_candidate_ids"] and not set(_strings(provenance.get("source_event_candidate_ids"))).issubset(universe["source_event_candidate_ids"]): + errors.append(f"{bo_id}.provenance.source_event_candidate_ids outside Stage A universe") + if universe["source_meeting_clause_ids"] and not set(_strings(provenance.get("source_meeting_clause_ids"))).issubset(universe["source_meeting_clause_ids"]): + errors.append(f"{bo_id}.provenance.source_meeting_clause_ids outside Stage A universe") + downstream = item.get("downstream_seed_refs") + if not isinstance(downstream, dict) or set(downstream.keys()) != { + "claim_group_seed_refs_proposed", "canonical_theory_graph_seed_ref_proposed", "legal_effect_structure_seed_ref_proposed", + }: + errors.append(f"{bo_id}.downstream_seed_refs invalid") + extensions = item.get("extensions", {"domain_payload": {}}) + if extensions is not None and (not isinstance(extensions, dict) or set(extensions.keys()) - {"domain_payload"} or not isinstance(extensions.get("domain_payload", {}), dict)): + errors.append(f"{bo_id}.extensions invalid") + return errors + + def _fail(message: str, gates: list[dict[str, Any]], reasons: list[str]) -> None: + print(json.dumps({ + "status": "FAILED", + "message": message, + "write_target": TARGET_NAME, + "gate_results": gates, + "failure_reasons": reasons[:40], + }, ensure_ascii=False)) + sys.exit(1) + + def main() -> None: + _init() + gates: list[dict[str, Any]] = [] + stage_a = _dict(_dict(read_json_doc(STAGE_A_PATH)).get("stage_a_context") or read_json_doc(STAGE_A_PATH)) + if stage_a.get("schema_version") != "task_c_bo_stage_a_context.v1" or stage_a.get("status") != "READY": + _fail("Stage A freshness guard failed", gates, ["stage_a not READY"]) + universe = _stage_a_universe(stage_a) + evidence_map = _evidence_map_from_stage_a(stage_a) + ledger = _dict(_dict(read_json_doc(LEDGER_PATH)).get("postb_seed_ledger")) + if ledger.get("schema_version") != "task_c_bo_postb_seed_ledger.v1" or ledger.get("status") != "READY": + _fail("R0 seed ledger not READY", gates, [str(ledger.get("status"))]) + try: + adj_doc = read_json_doc(DECISIONS_PATH) + except Exception: + adj_doc = {"postb_exception_adjudication": {"schema_version": "task_c_bo_postb_exception_adjudication.v1", + "status": "READY_NO_EXCEPTIONS", "exception_count": 0, + "canonical_decisions": [], "field_decisions": [], + "link_decisions": [], "semantic_gate_decisions": [], + "blocked_review_items": []}} + adj = _dict(_dict(adj_doc).get("postb_exception_adjudication")) + if adj.get("schema_version") != "task_c_bo_postb_exception_adjudication.v1": + _fail("R1 adjudication schema mismatch", gates, [str(adj.get("schema_version"))]) + if adj.get("status") not in {"READY", "READY_NO_EXCEPTIONS"}: + _fail("R1 adjudication status invalid", gates, [str(adj.get("status"))]) + blocked = _list(adj.get("blocked_review_items")) + block_decisions: list[Any] = [] + field_decisions = _field_decision_map(adj) + link_decisions = _link_decision_map(adj) + dropped, merge_into = _decision_sets(ledger, adj, block_decisions) + if blocked or block_decisions: + _fail("R1 returned BLOCK_REVIEW items: 인간 검토 필요", gates, + [json.dumps(x, ensure_ascii=False)[:200] for x in (blocked + block_decisions)]) + + candidates = [item for item in _list(ledger.get("ledger_candidates")) if isinstance(item, dict)] + survivors = [item for item in candidates if item.get("candidate_ref") not in dropped] + survivors.sort(key=_sort_tuple) + if not survivors: + _fail("no surviving BO candidates after decisions", gates, []) + + candidate_ref_to_bo_id: dict[str, str] = {} + for idx, item in enumerate(survivors, start=1): + candidate_ref_to_bo_id[str(item["candidate_ref"])] = f"bh{idx}" + for source_ref, target_ref in merge_into.items(): + if target_ref in candidate_ref_to_bo_id: + candidate_ref_to_bo_id[source_ref] = candidate_ref_to_bo_id[target_ref] + + bo_items: list[dict[str, Any]] = [] + normalization_notes: list[dict[str, Any]] = [] + evidence_gaps: list[Any] = [] + prior_link_notes: list[dict[str, Any]] = [] + + for idx, ledger_item in enumerate(survivors, start=1): + seed = _dict(ledger_item.get("seed_payload")) + candidate_ref = str(ledger_item.get("candidate_ref")) + bo_id = f"bh{idx}" + bo_type = field_decisions.get((candidate_ref, "BOType"), seed.get("BOType")) + action_type = field_decisions.get((candidate_ref, "ActionType"), seed.get("ActionType")) + juristic = _juristic(field_decisions.get((candidate_ref, "JuristicAct.label"), seed.get("JuristicAct"))) + domain_payload = _dict(_dict(seed.get("extensions")).get("domain_payload")) + action = field_decisions.get((candidate_ref, "Action"), seed.get("Action") or domain_payload.get("action_summary") or _dict(seed.get("core_field_base")).get("Action_proposal")) + if bo_type not in {"event", "state"}: + normalization_notes.append({"candidate_ref": candidate_ref, "field": "BOType", "received": bo_type, "fallback": "event"}) + bo_type = "event" + if action_type not in ACTION_TYPE_ENUM: + normalization_notes.append({"candidate_ref": candidate_ref, "field": "ActionType", "received": action_type, "fallback": "사실행위(factual acts)"}) + action_type = "사실행위(factual acts)" + if not isinstance(action, str) or not action.strip(): + normalization_notes.append({"candidate_ref": candidate_ref, "field": "Action", "fallback": "source-backed BO"}) + action = f"{candidate_ref} source-backed BO" + + refs = _dict(ledger_item.get("source_refs")) + source_evidence_indexes = _strings(refs.get("source_evidence_indexes")) + evidence_items = [_evidence_item(eidx, evidence_map.get(eidx), evidence_gaps, bo_id) for eidx in source_evidence_indexes] + evidence_titles: list[str] = [] + for ev in evidence_items: + title = ev["source_title"] + if title not in evidence_titles: + evidence_titles.append(title) + + link = link_decisions.get(candidate_ref, {}) + reason_ref_candidates = _strings(link.get("reason_refs_candidate_refs")) + prior_candidate = link.get("prior_candidate_ref") + if prior_candidate == "NO_LINK": + prior_candidate = None + explicit_refs = _dict(seed.get("downstream_seed_refs")) + if not reason_ref_candidates: + reason_ref_candidates = _strings(explicit_refs.get("reason_refs_candidate_refs")) + if not prior_candidate: + prior_list = _strings(explicit_refs.get("prior_candidate_refs")) + if len(prior_list) == 1: + prior_candidate = prior_list[0] + elif len(prior_list) > 1: + # 결정적 defer 정책 (R-5): PriorAct 불명은 null 유지 + review note (blocker 아님) + prior_candidate = None + prior_link_notes.append({"candidate_ref": candidate_ref, "prior_candidates": prior_list, + "policy": "prior_link_ambiguous_kept_null"}) + reason_refs = [candidate_ref_to_bo_id[ref] for ref in reason_ref_candidates if ref in candidate_ref_to_bo_id and candidate_ref_to_bo_id[ref] != bo_id] + if prior_candidate and prior_candidate in candidate_ref_to_bo_id: + prior_act = candidate_ref_to_bo_id[prior_candidate] + elif reason_refs: + prior_act = reason_refs[0] + else: + prior_act = None + reason = "ReasonRefs에 기재된 선행 BO와 source evidence/event chain으로 연결됨" if reason_refs else "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO" + + bo_items.append({ + "BO_ID": bo_id, + "id": bo_id, + "BOType": bo_type, + "ActionType": action_type, + "JuristicAct": juristic, + "Action": str(action).strip(), + "Reason": reason, + "PriorAct": prior_act, + "ReasonRefs": reason_refs, + "Legal_Keywords": _keywords(seed, juristic), + "core_field_base": _core(seed), + "amount": _amount(seed.get("amount")), + "EvidenceTitles": evidence_titles, + "Evidence": evidence_items, + "source_evidence_indexes": source_evidence_indexes, + "provenance": { + "source_event_candidate_ids": _strings(refs.get("source_event_candidate_ids")), + "source_meeting_clause_ids": _strings(refs.get("source_meeting_clause_ids")), + "source_domain": seed.get("source_domain"), + }, + "downstream_seed_refs": _downstream_refs(seed), + "extensions": {"domain_payload": domain_payload}, + }) + + # ---------- conservation + 게이트 (PostB_4 이식) ---------- + total_ledger = len(candidates) + absorbed = len(dropped) + _add_gate(gates, "candidate_conservation", len(bo_items) + absorbed == total_ledger, + f"BO {len(bo_items)} + absorbed {absorbed} == ledger {total_ledger}") + _add_gate(gates, "bo_items_array_non_empty", len(bo_items) > 0, "bo_items must be non-empty array") + ids = {item["BO_ID"] for item in bo_items} + errors: list[str] = [] + for idx, item in enumerate(bo_items, start=1): + errors.extend(_validate_item(item, idx, ids, universe)) + _add_gate(gates, "bo_schema_and_reference_validation", not errors, "BO_JSON_Schema target validation") + if any(g["status"] != "PASS" for g in gates) or errors: + _fail("pre-write gate failed", gates, errors) + + payload = json.dumps(bo_items, ensure_ascii=False, indent=2) + "\n" + write_doc(TARGET_NAME, payload) + reread = read_json_doc(TARGET_NAME) + _add_gate(gates, "post_write_json_parse", isinstance(reread, list) and len(reread) == len(bo_items), "BO.json reread JSON parse") + if not isinstance(reread, list) or len(reread) != len(bo_items): + _fail("post-write verification failed", gates, ["reread mismatch"]) + + write_doc(BUNDLE_COMPACT_PATH, json.dumps({ + "schema_version": "task_c_bo_postb_compiled_bundle_compact.v1", + "status": "READY", + "candidate_ref_to_bo_id": candidate_ref_to_bo_id, + "bo_item_count": len(bo_items), + "normalization_notes": normalization_notes, + "evidence_gap_items": evidence_gaps, + "prior_link_notes": prior_link_notes, + }, ensure_ascii=False, indent=2)) + + # review handoff 최종 status 갱신 + try: + handoff = _dict(read_json_doc(REVIEW_HANDOFF_PATH)) + except Exception: + handoff = {"schema_version": "stage1_part2_review_handoff.v1", "review_items": []} + handoff["status"] = "FINALIZED" + handoff["bo_item_count"] = len(bo_items) + for note in prior_link_notes: + handoff.setdefault("review_items", []).append({ + "review_id": f"F0:prior:{note['candidate_ref']}", + "source_domain": None, + "severity": "SOFT_WARNING", + "issue_type": "prior_link_ambiguous", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Stage2", + "template_note": "선행행위 후보가 복수여서 PriorAct를 null로 보존하였다.", + }) + write_doc(REVIEW_HANDOFF_PATH, json.dumps(handoff, ensure_ascii=False, indent=2)) + + print(json.dumps({ + "status": "PASS", + "message": f"BO.json 작성 완료 (BO {len(bo_items)}건)", + "write_target": TARGET_NAME, + "bo_item_count": len(bo_items), + "absorbed_by_merge": absorbed, + "gate_results": gates, + "bundle_compact_path": BUNDLE_COMPACT_PATH, + }, ensure_ascii=False)) + + if __name__ == "__main__": + main() + - task_name: Task_C_BO_S0_signal_bundle_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 180 + code: |- + #!/usr/bin/env python3 + # Task_C_BO_S0_signal_bundle_writer (v3) + # Middle_Input 3개 task 통합: BO.json 1회 read로 signal 3파일 생성. + # 각 signal 빌더는 v2 원본 함수를 모듈 소스 그대로 exec하여 사용한다 (검증 로직 포함 무손실 이식). + # Spec: Part_2_Improvement_Strategy_Claude_v1.md §10 + from __future__ import annotations + import itertools + import json + import re + import sys + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + STAGE_A_PATH = "stage1_tmp/task_c_bo/stage_a_context.json" + LEDGER_PATH = "stage1_tmp/task_c_bo/postb_seed_ledger.json" + BUNDLE_COMPACT_PATH = "stage1_tmp/task_c_bo/postb_compiled_bundle_compact.json" + B4_SEED_PATH = "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B4_Actio.json" + BO_PATH = "BO.json" + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-c-bo-s0-signal-bundle-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse_mcp(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "id": _mid(), "method": "tools/call", + "params": {"name": name, "arguments": args}}, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse_mcp(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def read_json_doc(path: str) -> Any: + result = _call("read_docs", {"doc_names": [path]}) + content = result["result"].get("content", []) + text = content[0].get("text", "") if content else "" + try: + parsed = json.loads(text) + except json.JSONDecodeError as exc: + raise RuntimeError(f"Invalid read_docs envelope for {path}") from exc + if isinstance(parsed, dict) and "results" in parsed: + rows = parsed.get("results") or [] + if not rows or rows[0].get("error"): + raise RuntimeError(f"read_docs failed for {path}: {rows[:1]}") + inner = rows[0].get("content") or "" + if isinstance(inner, str): + try: + return json.loads(inner) + except json.JSONDecodeError: + obj, end = JSON_DECODER.raw_decode(inner.strip()) + if inner.strip()[end:].strip(): + raise RuntimeError(f"{path}: non-json tail") + return obj + return inner + return parsed + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + _MOD_ACTIO_SRC = r''' + ACTIO_SCOPE_ENUM = { + "preserved_claim", "fraudulent_act", "beneficiary_or_transferee", + "target_asset", "encumbrance", "remedy_or_cap", "defense", + } + + SUSPICION_LEVEL_ENUM = {"NONE", "LOW", "MEDIUM", "HIGH", "CRITICAL"} + + ASSET_KIND_ENUM = { + "real_property", "movable", "claim", "security_right", + "cash_or_deposit", "business_asset", "other", "unknown", + } + + ROLE_ENUM = { + "fraudulent_act_date", "debtor_act", "beneficiary", "transferee", + "remedy_or_cap", "defense", "encumbrance", + } + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def _first_string(*values: Any) -> str | None: + for value in values: + for item in _strings(value): + return item + return None + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + def _bo_by_id(bo_items: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: + return {str(item.get("BO_ID") or item.get("id")): item for item in bo_items if item.get("BO_ID") or item.get("id")} + + def _candidate_mapping(bundle: dict[str, Any]) -> dict[str, str]: + mapping = _dict(bundle.get("candidate_ref_to_bo_id")) + return {str(k): str(v) for k, v in mapping.items() if k and v} + + def _candidate_to_bo(ledger: dict[str, Any], mapping: dict[str, str]) -> dict[str, dict[str, Any]]: + out: dict[str, dict[str, Any]] = {} + for item in _list(ledger.get("ledger_candidates")): + if isinstance(item, dict) and item.get("candidate_ref") in mapping: + out[str(item["candidate_ref"])] = item + return out + + def _source_evidence(value: Any) -> list[str]: + if isinstance(value, dict): + refs = _dict(value.get("source_refs")) + if refs: + return _strings(refs.get("source_evidence_indexes")) + return _strings(value.get("source_evidence_indexes")) + return [] + + def _seed_payload(candidate: dict[str, Any]) -> dict[str, Any]: + return _dict(candidate.get("seed_payload")) + + def _domain_payload_from_seed(seed: dict[str, Any]) -> dict[str, Any]: + return _dict(_dict(seed.get("extensions")).get("domain_payload")) + + def _domain_payload_from_bo(bo: dict[str, Any]) -> dict[str, Any]: + return _dict(_dict(bo.get("extensions")).get("domain_payload")) + + def _flatten_strings(value: Any, limit: int = 10) -> list[str]: + out: list[str] = [] + + def visit(node: Any) -> None: + if len(out) >= limit: + return + if node is None: + return + if isinstance(node, (str, int, float, bool)): + text = str(node).strip() + if text and text not in out: + out.append(text) + return + if isinstance(node, list): + for child in node: + visit(child) + return + if isinstance(node, dict): + for key in ( + "asset_label", "asset", "property", "object", "Object", "description", + "value", "name", "party", "beneficiary", "transferee", "debtor_act", + "remedy", "defense", "encumbrance", "date", "claim_type", + ): + if key in node: + visit(node.get(key)) + visit(value) + return out[:limit] + + def _asset_kind_from_text(text: str) -> str: + lowered = text.lower() + if any(key in text for key in ["부동산", "토지", "건물", "아파트", "대지", "등기", "근저당"]): + return "real_property" + if any(key in text for key in ["채권", "대여금", "매매대금", "임대차보증금"]): + return "claim" + if any(key in text for key in ["근저당", "담보", "저당", "질권", "유치권"]): + return "security_right" + if any(key in text for key in ["예금", "현금", "계좌", "deposit", "cash"]) or "bank" in lowered: + return "cash_or_deposit" + if any(key in text for key in ["영업", "사업", "상호", "business"]) or "company" in lowered: + return "business_asset" + if any(key in text for key in ["동산", "차량", "기계", "movable"]): + return "movable" + return "unknown" + + def _target_property_candidates(payload: dict[str, Any], bo_ids: list[str], evidence: list[str]) -> list[dict[str, Any]]: + sources = [ + payload.get("asset_bindings"), + payload.get("asset_binding_hint"), + payload.get("valuation_matrix"), + payload.get("fraudulent_transfer_facts"), + payload.get("encumbrance_timeline"), + payload.get("target_property_candidates"), + ] + out: list[dict[str, Any]] = [] + for source in sources: + for item in _as_list(source): + labels = _flatten_strings(item, limit=4) + if not labels and isinstance(item, dict): + labels = _strings(item.get("asset_label") or item.get("property_label")) + for label in labels: + candidate = { + "asset_label": label, + "asset_kind": _asset_kind_from_text(label), + "related_bo_ids": bo_ids, + "related_evidence_indexes": evidence, + } + key = json.dumps(candidate, ensure_ascii=False, sort_keys=True) + if key not in {json.dumps(x, ensure_ascii=False, sort_keys=True) for x in out}: + out.append(candidate) + return out + + def _preserved_claim_candidates(payload: dict[str, Any], mapping: dict[str, str], bo_ids: list[str], evidence: list[str]) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for item in _as_list(payload.get("preserved_claim_bundle") or payload.get("preserved_claim_candidates")): + obj = _dict(item) + ref = _first_string(obj.get("candidate_ref"), obj.get("bo_candidate_ref")) + bo_id = mapping.get(ref) if ref else _first_string(obj.get("bo_id")) + if bo_id not in bo_ids and bo_ids: + bo_id = bo_id or None + out.append({ + "bo_id": bo_id, + "claim_type_candidate": _first_string(obj.get("claim_type_candidate"), obj.get("claim_type"), obj.get("type"), obj.get("label")), + "creditor_candidate": _first_string(obj.get("creditor_candidate"), obj.get("creditor"), obj.get("plaintiff")), + "debtor_candidate": _first_string(obj.get("debtor_candidate"), obj.get("debtor"), obj.get("defendant")), + "claim_date_candidate": _first_string(obj.get("claim_date_candidate"), obj.get("claim_date"), obj.get("date"), obj.get("BehaviorTime")), + "related_evidence_indexes": _dedupe(evidence + _strings(obj.get("related_evidence_indexes") or obj.get("source_evidence_indexes"))), + }) + if not out and bo_ids: + out.append({ + "bo_id": bo_ids[0], + "claim_type_candidate": None, + "creditor_candidate": None, + "debtor_candidate": None, + "claim_date_candidate": None, + "related_evidence_indexes": evidence, + }) + return out + + def _role_hint(role: str, value: Any, date: Any, bo_ids: list[str], evidence: list[str]) -> dict[str, Any] | None: + if role not in ROLE_ENUM: + return None + value_candidate = _first_string(value) + date_candidate = _first_string(date) + if value_candidate is None and date_candidate is None: + return None + return { + "role": role, + "value_candidate": value_candidate, + "date_candidate": date_candidate, + "related_bo_ids": bo_ids, + "related_evidence_indexes": evidence, + } + + def _actio_role_hints(payload: dict[str, Any], seed: dict[str, Any], bo_ids: list[str], evidence: list[str]) -> list[dict[str, Any]]: + hints: list[dict[str, Any]] = [] + for fact in _as_list(payload.get("fraudulent_transfer_facts")): + obj = _dict(fact) + for hint in ( + _role_hint("fraudulent_act_date", obj.get("date") or obj.get("act_date"), obj.get("date") or obj.get("act_date"), bo_ids, evidence), + _role_hint("debtor_act", obj.get("debtor_act") or obj.get("action") or obj.get("description"), obj.get("date"), bo_ids, evidence), + ): + if hint: + hints.append(hint) + for party in _as_list(payload.get("beneficiary_transferee_hints")): + obj = _dict(party) + role_text = "transferee" if "transferee" in str(obj).lower() or "양수" in str(obj) else "beneficiary" + hint = _role_hint(role_text, obj.get("party") or obj.get("name") or obj.get("value") or obj, obj.get("date"), bo_ids, evidence) + if hint: + hints.append(hint) + for role, key in [ + ("remedy_or_cap", "remedy_mode_hints"), + ("defense", "defense_rebuttal"), + ("encumbrance", "encumbrance_timeline"), + ]: + for item in _as_list(payload.get(key)): + obj = _dict(item) + hint = _role_hint(role, obj or item, obj.get("date") if obj else None, bo_ids, evidence) + if hint: + hints.append(hint) + if not hints: + action = _first_string(seed.get("Action"), _dict(seed.get("core_field_base")).get("Action_proposal")) + hint = _role_hint("debtor_act", action, _dict(seed.get("core_field_base")).get("BehaviorTime"), bo_ids, evidence) + if hint: + hints.append(hint) + deduped: list[dict[str, Any]] = [] + seen: set[str] = set() + for hint in hints: + key = json.dumps(hint, ensure_ascii=False, sort_keys=True) + if key not in seen: + seen.add(key) + deduped.append(hint) + return deduped + + def _scopes(payload: dict[str, Any]) -> list[str]: + scopes: list[str] = [] + if payload.get("preserved_claim_bundle") or payload.get("preserved_claim_candidates"): + scopes.append("preserved_claim") + if payload.get("fraudulent_transfer_facts") or "actio" in " ".join(_strings(payload.get("legal_effect_tags"))).lower(): + scopes.append("fraudulent_act") + if payload.get("beneficiary_transferee_hints"): + scopes.append("beneficiary_or_transferee") + if payload.get("asset_bindings") or payload.get("asset_binding_hint") or payload.get("valuation_matrix"): + scopes.append("target_asset") + if payload.get("encumbrance_timeline"): + scopes.append("encumbrance") + if payload.get("remedy_mode_hints"): + scopes.append("remedy_or_cap") + if payload.get("defense_rebuttal"): + scopes.append("defense") + return [scope for scope in _dedupe(scopes) if scope in ACTIO_SCOPE_ENUM] + + def _suspicion_level(scopes: list[str]) -> str: + core = {"preserved_claim", "fraudulent_act", "beneficiary_or_transferee"} + if core.issubset(set(scopes)): + return "HIGH" + if {"preserved_claim", "fraudulent_act"}.issubset(set(scopes)): + return "MEDIUM" + if scopes: + return "LOW" + return "NONE" + + def _reasons(scopes: list[str], payload: dict[str, Any], b4_review_queue: list[Any]) -> str: + fragments: list[str] = [] + if "preserved_claim" in scopes: + fragments.append("피보전채권 후보가 B4 seed/domain payload에서 확인됨") + if "fraudulent_act" in scopes: + fragments.append("채무자 처분행위 또는 사해행위 후보 사실이 확인됨") + if "beneficiary_or_transferee" in scopes: + fragments.append("수익자 또는 전득자 후보가 확인됨") + if "target_asset" in scopes: + fragments.append("취소·원상회복 대상 재산 후보가 확인됨") + if "encumbrance" in scopes: + fragments.append("담보권·가압류 등 부담 관련 단서가 확인됨") + if "remedy_or_cap" in scopes: + fragments.append("원상회복 방법 또는 가액배상 한도 단서가 확인됨") + if "defense" in scopes: + fragments.append("항변 또는 선의·악의 관련 반박 단서가 확인됨") + for item in b4_review_queue[:2]: + if isinstance(item, dict): + reason = _first_string(item.get("reason"), item.get("issue"), item.get("review_reason")) + if reason: + fragments.append(f"B4 review queue: {reason}") + return ", ".join(fragments) if fragments else "사해행위취소 관련 구조화 단서가 충분히 확인되지 않음" + + def _validate(payload: dict[str, Any]) -> None: + if payload.get("schema_version") != "actio_case_signals.v1": + raise ValueError("schema_version mismatch") + if payload.get("status") not in {"READY", "READY_WITH_REVIEW", "EMPTY", "FAILED"}: + raise ValueError("invalid status") + signals = payload.get("actio_case_signals") + if not isinstance(signals, list): + raise ValueError("actio_case_signals must be array") + for idx, item in enumerate(signals): + if not isinstance(item, dict): + raise ValueError(f"signal[{idx}] must be object") + for key in [ + "signal_id", "actio_scope", "suspicion_level", "suspicion_reasons", + "related_bo_ids", "related_evidence_indexes", "target_property_candidates", + "preserved_claim_candidates", "actio_role_hints", + ]: + if key not in item: + raise ValueError(f"signal[{idx}] missing {key}") + if not isinstance(item["signal_id"], str): + raise ValueError(f"signal[{idx}].signal_id must be string") + if item["suspicion_level"] not in SUSPICION_LEVEL_ENUM: + raise ValueError(f"signal[{idx}].suspicion_level invalid") + if not isinstance(item["suspicion_reasons"], str): + raise ValueError(f"signal[{idx}].suspicion_reasons must be string") + if any(scope not in ACTIO_SCOPE_ENUM for scope in _list(item["actio_scope"])): + raise ValueError(f"signal[{idx}].actio_scope invalid") + for field in ["related_bo_ids", "related_evidence_indexes"]: + if not all(isinstance(x, str) for x in _list(item[field])): + raise ValueError(f"signal[{idx}].{field} must be string array") + for prop in _list(item["target_property_candidates"]): + if not isinstance(prop, dict): + raise ValueError(f"signal[{idx}].target_property_candidates item must be object") + for key in ["asset_label", "asset_kind", "related_bo_ids", "related_evidence_indexes"]: + if key not in prop: + raise ValueError(f"target_property_candidate missing {key}") + if prop["asset_kind"] not in ASSET_KIND_ENUM: + raise ValueError("target_property_candidate.asset_kind invalid") + for claim in _list(item["preserved_claim_candidates"]): + if not isinstance(claim, dict): + raise ValueError(f"signal[{idx}].preserved_claim_candidates item must be object") + for key in [ + "bo_id", "claim_type_candidate", "creditor_candidate", + "debtor_candidate", "claim_date_candidate", "related_evidence_indexes", + ]: + if key not in claim: + raise ValueError(f"preserved_claim_candidate missing {key}") + for hint in _list(item["actio_role_hints"]): + if not isinstance(hint, dict): + raise ValueError(f"signal[{idx}].actio_role_hints item must be object") + for key in ["role", "value_candidate", "date_candidate", "related_bo_ids", "related_evidence_indexes"]: + if key not in hint: + raise ValueError(f"actio_role_hint missing {key}") + if hint["role"] not in ROLE_ENUM: + raise ValueError("actio_role_hint.role invalid") + + + def build_signals(ledger, compiled, b4, bo_items): + bo_by_id = _bo_by_id(bo_items) + mapping = _candidate_mapping(compiled) + candidates_by_ref = _candidate_to_bo(ledger, mapping) + signals = [] + review_needed = False + for seed_candidate in _list(b4.get("bo_seed_candidates")): + if not isinstance(seed_candidate, dict): + continue + candidate_ref = str(seed_candidate.get("candidate_ref") or "") + bo_id = mapping.get(candidate_ref) + bo_ids = [bo_id] if bo_id else [] + ledger_item = candidates_by_ref.get(candidate_ref, {}) + ledger_seed = _seed_payload(ledger_item) + seed = seed_candidate if seed_candidate else ledger_seed + payload = _domain_payload_from_seed(seed) + if not payload and ledger_seed: + payload = _domain_payload_from_seed(ledger_seed) + if bo_id and bo_id in bo_by_id: + payload = {**payload, **_domain_payload_from_bo(bo_by_id[bo_id])} + bo_evidence = _source_evidence(bo_by_id.get(bo_id, {})) if bo_id else [] + evidence = _dedupe(_source_evidence(seed_candidate) + _source_evidence(ledger_item) + bo_evidence) + scopes = _scopes(payload) + if not scopes and candidate_ref: + scopes = ["fraudulent_act"] + review_needed = True + related_bo_ids = _dedupe(bo_ids + [x for x in _strings(seed_candidate.get("related_bo_ids")) if x in bo_by_id]) + if not related_bo_ids: + review_needed = True + target_properties = _target_property_candidates(payload, related_bo_ids, evidence) + preserved_claims = _preserved_claim_candidates(payload, mapping, related_bo_ids, evidence) + role_hints = _actio_role_hints(payload, seed, related_bo_ids, evidence) + if not target_properties or not preserved_claims or not role_hints: + review_needed = True + if scopes or related_bo_ids or target_properties or preserved_claims or role_hints: + signals.append({ + "signal_id": f"actio-signal-{len(signals) + 1:03d}", + "actio_scope": scopes, + "suspicion_level": _suspicion_level(scopes), + "suspicion_reasons": _reasons(scopes, payload, _list(b4.get("domain_review_queue"))), + "related_bo_ids": related_bo_ids, + "related_evidence_indexes": evidence, + "target_property_candidates": target_properties, + "preserved_claim_candidates": preserved_claims, + "actio_role_hints": role_hints, + }) + status = "EMPTY" if not signals else ("READY_WITH_REVIEW" if review_needed or b4.get("status") == "READY_WITH_REVIEW" else "READY") + payload = {"schema_version": "actio_case_signals.v1", "status": status, "actio_case_signals": signals} + _validate(payload) + return payload + + ''' + + _MOD_LIABILITY_SRC = r''' + LIABILITY_ENUM = { + "money_claim", "commercial_successor", "secured_debt", "registry_invalidity", + "land_use_gain", "actio_related", "succession_related", "notice_or_lien_related", + "defense_or_setoff", "other", + } + + PARTY_ROLE_ENUM = {"plaintiff", "defendant", "creditor", "debtor", "liable_party"} + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def _first_string(*values: Any) -> str | None: + for value in values: + for item in _strings(value): + return item + return None + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + def _source_evidence(bo: dict[str, Any]) -> list[str]: + return _strings(bo.get("source_evidence_indexes")) + + def _domain_payload(bo: dict[str, Any]) -> dict[str, Any]: + return _dict(_dict(bo.get("extensions")).get("domain_payload")) + + def _candidate_mapping(bundle: dict[str, Any]) -> dict[str, str]: + mapping = _dict(bundle.get("candidate_ref_to_bo_id")) + return {str(k): str(v) for k, v in mapping.items() if k and v} + + def _candidate_ref_for_bo(bo_id: str, mapping: dict[str, str]) -> str | None: + for ref, mapped in mapping.items(): + if mapped == bo_id: + return ref + return None + + def _liability_type(bo: dict[str, Any]) -> str: + payload = _domain_payload(bo) + source_domain = str(_dict(bo.get("provenance")).get("source_domain") or "").lower() + text = " ".join([ + source_domain, + str(bo.get("Action") or ""), + str(bo.get("ActionType") or ""), + str(_dict(bo.get("JuristicAct")).get("label") or ""), + " ".join(_strings(bo.get("Legal_Keywords"))), + " ".join(_strings(payload.get("legal_effect_tags"))), + str(payload.get("domain_legal_effect_tag") or ""), + ]).lower() + if "b4" in source_domain or "actio" in text or "사해" in text: + return "actio_related" + if "successor" in text or "영업양수" in text or "상호속용" in text: + return "commercial_successor" + if "b1" in source_domain or "대여금" in text or "매매대금" in text or "채권" in text: + return "money_claim" + if "registry_invalid" in text or "등기무효" in text or "말소" in text: + return "registry_invalidity" + if "secured" in text or "근저당" in text or "담보" in text or "유치권" in text: + return "secured_debt" + if "land" in text or "valuation" in text or "사용이익" in text or "부당이득" in text: + return "land_use_gain" + if "succession" in text or "상속" in text or "승계" in text: + return "succession_related" + if "notice" in text or "lien" in text or "통지" in text or "유치권" in text: + return "notice_or_lien_related" + if "defense" in text or "항변" in text or "상계" in text: + return "defense_or_setoff" + return "other" + + def _normalize_role(value: Any) -> str | None: + text = str(value or "").strip().lower() + if not text: + return None + if text in PARTY_ROLE_ENUM: + return text + if "plaintiff" in text or "원고" in text: + return "plaintiff" + if "defendant" in text or "피고" in text: + return "defendant" + if "creditor" in text or "채권자" in text: + return "creditor" + if "debtor" in text or "채무자" in text: + return "debtor" + if "liable" in text or "책임" in text or "이행" in text: + return "liable_party" + return None + + def _party_roles(bo: dict[str, Any]) -> list[dict[str, Any]]: + bo_id = str(bo.get("BO_ID") or bo.get("id") or "") + payload = _domain_payload(bo) + roles: list[dict[str, Any]] = [] + for item in _as_list(payload.get("party_roles") or payload.get("parties_hint") or payload.get("liability_party_hints")): + obj = _dict(item) + party = _first_string(obj.get("party"), obj.get("name"), obj.get("value")) + role = _normalize_role(obj.get("role") or obj.get("party_role")) + if party and role: + roles.append({"party": party, "role": role, "bo_ids": [bo_id] if bo_id else []}) + core = _dict(bo.get("core_field_base")) + performer = _first_string(core.get("Performer"), bo.get("Performer")) + subject = _first_string(core.get("Subject"), bo.get("Subject")) + if performer and not any(item["party"] == performer for item in roles): + roles.append({"party": performer, "role": "liable_party", "bo_ids": [bo_id] if bo_id else []}) + if subject and not any(item["party"] == subject for item in roles): + role = "creditor" if _liability_type(bo) in {"money_claim", "actio_related"} else "liable_party" + roles.append({"party": subject, "role": role, "bo_ids": [bo_id] if bo_id else []}) + deduped: list[dict[str, Any]] = [] + seen: set[str] = set() + for item in roles: + key = json.dumps(item, ensure_ascii=False, sort_keys=True) + if key not in seen: + seen.add(key) + deduped.append(item) + return deduped + + def _claim_group_id(bo: dict[str, Any]) -> str | None: + refs = _dict(bo.get("downstream_seed_refs")) + return _first_string(refs.get("claim_group_seed_refs_proposed") or refs.get("claim_group_seed_refs")) + + def _liability_group_id(bo: dict[str, Any], candidate_ref: str | None) -> str | None: + payload = _domain_payload(bo) + return _first_string(payload.get("liability_group_id"), payload.get("liability_group_ref"), candidate_ref) + + def _linked_structure_ids(bo: dict[str, Any]) -> list[str]: + refs = _dict(bo.get("downstream_seed_refs")) + return _strings(refs.get("legal_effect_structure_seed_ref_proposed") or refs.get("linked_structure_ids")) + + def _validate(payload: dict[str, Any]) -> None: + if payload.get("schema_version") != "case_liability_signals.v1": + raise ValueError("schema_version mismatch") + if payload.get("status") not in {"READY", "READY_WITH_REVIEW", "EMPTY", "FAILED"}: + raise ValueError("invalid status") + signals = payload.get("case_liability_signals") + if not isinstance(signals, list): + raise ValueError("case_liability_signals must be array") + for idx, item in enumerate(signals): + if not isinstance(item, dict): + raise ValueError(f"signal[{idx}] must be object") + for key in [ + "liability_candidate_type", "related_bo_ids", "related_evidence_indexes", + "party_roles", "claim_group_id", "liability_group_id", "linked_structure_ids", + ]: + if key not in item: + raise ValueError(f"signal[{idx}] missing {key}") + if item["liability_candidate_type"] not in LIABILITY_ENUM: + raise ValueError(f"signal[{idx}].liability_candidate_type invalid") + for field in ["related_bo_ids", "related_evidence_indexes", "linked_structure_ids"]: + if not isinstance(item[field], list) or not all(isinstance(x, str) for x in item[field]): + raise ValueError(f"signal[{idx}].{field} must be string array") + for role in _list(item["party_roles"]): + if not isinstance(role, dict): + raise ValueError(f"signal[{idx}].party_roles item must be object") + for key in ["party", "role", "bo_ids"]: + if key not in role: + raise ValueError(f"party_role missing {key}") + if not isinstance(role["party"], str) or role["role"] not in PARTY_ROLE_ENUM: + raise ValueError("party_role type invalid") + if not isinstance(role["bo_ids"], list) or not all(isinstance(x, str) for x in role["bo_ids"]): + raise ValueError("party_role.bo_ids must be string array") + + + def build_signals(ledger, compiled, b4, bo_items): + mapping = _candidate_mapping(compiled) + signals = [] + review_needed = b4.get("status") == "READY_WITH_REVIEW" + for bo in bo_items: + bo_id = str(bo.get("BO_ID") or bo.get("id") or "") + if not bo_id: + review_needed = True + continue + candidate_ref = _candidate_ref_for_bo(bo_id, mapping) + party_roles = _party_roles(bo) + if not party_roles: + review_needed = True + liability_group_id = _liability_group_id(bo, candidate_ref) + if liability_group_id is None: + review_needed = True + signals.append({ + "liability_candidate_type": _liability_type(bo), + "related_bo_ids": [bo_id], + "related_evidence_indexes": _source_evidence(bo), + "party_roles": party_roles, + "claim_group_id": _claim_group_id(bo), + "liability_group_id": liability_group_id, + "linked_structure_ids": _linked_structure_ids(bo), + }) + status = "EMPTY" if not signals else ("READY_WITH_REVIEW" if review_needed else "READY") + payload = {"schema_version": "case_liability_signals.v1", "status": status, "case_liability_signals": signals} + _validate(payload) + return payload + + ''' + + _MOD_LEGAL_EFFECT_SRC = r''' + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + def _domain_payload(bo: dict[str, Any]) -> dict[str, Any]: + return _dict(_dict(bo.get("extensions")).get("domain_payload")) + + def _source_evidence(bo: dict[str, Any]) -> list[str]: + return _strings(bo.get("source_evidence_indexes")) + + def _candidate_structure_types(bo: dict[str, Any]) -> list[str]: + payload = _domain_payload(bo) + juristic = _dict(bo.get("JuristicAct")) + candidates: list[str] = [] + candidates.extend(_strings(payload.get("candidate_structure_types"))) + candidates.extend(_strings(payload.get("legal_effect_tags"))) + candidates.extend(_strings(payload.get("domain_legal_effect_tag"))) + candidates.extend(_strings(bo.get("Legal_Keywords"))) + candidates.extend(_strings(bo.get("BOType"))) + candidates.extend(_strings(bo.get("ActionType"))) + candidates.extend(_strings(juristic.get("label"))) + source_domain = _dict(bo.get("provenance")).get("source_domain") + if source_domain: + candidates.append(str(source_domain)) + return _dedupe(candidates) or ["generic_legal_effect"] + + def _linked_structure_ids(bo: dict[str, Any]) -> list[str]: + refs = _dict(bo.get("downstream_seed_refs")) + payload = _domain_payload(bo) + return _dedupe( + _strings(refs.get("legal_effect_structure_seed_ref_proposed")) + + _strings(refs.get("linked_structure_ids")) + + _strings(payload.get("linked_structure_ids")) + ) + + def _validate(payload: dict[str, Any]) -> None: + if payload.get("schema_version") != "legal_effect_signals.v1": + raise ValueError("schema_version mismatch") + if payload.get("status") not in {"READY", "READY_WITH_REVIEW", "EMPTY", "FAILED"}: + raise ValueError("invalid status") + routes = payload.get("bo_legal_effect_routes") + if not isinstance(routes, list): + raise ValueError("bo_legal_effect_routes must be array") + for idx, item in enumerate(routes): + if not isinstance(item, dict): + raise ValueError(f"route[{idx}] must be object") + for key in ["bo_id", "candidate_structure_types", "linked_structure_ids", "related_evidence_indexes"]: + if key not in item: + raise ValueError(f"route[{idx}] missing {key}") + if not isinstance(item["bo_id"], str): + raise ValueError(f"route[{idx}].bo_id must be string") + for field in ["candidate_structure_types", "linked_structure_ids", "related_evidence_indexes"]: + if not isinstance(item[field], list) or not all(isinstance(x, str) for x in item[field]): + raise ValueError(f"route[{idx}].{field} must be string array") + + + def build_signals(ledger, compiled, b4, bo_items): + routes = [] + review_needed = b4.get("status") == "READY_WITH_REVIEW" + for bo in bo_items: + bo_id = str(bo.get("BO_ID") or bo.get("id") or "") + if not bo_id: + review_needed = True + continue + linked = _linked_structure_ids(bo) + if not linked: + review_needed = True + routes.append({ + "bo_id": bo_id, + "candidate_structure_types": _candidate_structure_types(bo), + "linked_structure_ids": linked, + "related_evidence_indexes": _source_evidence(bo), + }) + status = "EMPTY" if not routes else ("READY_WITH_REVIEW" if review_needed else "READY") + payload = {"schema_version": "legal_effect_signals.v1", "status": status, "bo_legal_effect_routes": routes} + _validate(payload) + return payload + + ''' + + + def _load_module(src: str) -> dict[str, Any]: + ns: dict[str, Any] = {"json": json, "re": re, "Any": Any} + exec(compile(src, "", "exec"), ns) + return ns + + def main() -> None: + _init() + stage_a_doc = read_json_doc(STAGE_A_PATH) + stage_a = stage_a_doc.get("stage_a_context") if isinstance(stage_a_doc, dict) else None + if not isinstance(stage_a, dict) or stage_a.get("status") != "READY": + raise RuntimeError("Stage A context must be READY") + ledger = (read_json_doc(LEDGER_PATH) or {}).get("postb_seed_ledger") + if not isinstance(ledger, dict) or ledger.get("status") != "READY": + raise RuntimeError("R0 seed ledger must be READY") + compiled = read_json_doc(BUNDLE_COMPACT_PATH) + if not isinstance(compiled, dict) or compiled.get("status") != "READY": + raise RuntimeError("F0 compiled bundle compact must be READY") + b4_doc = read_json_doc(B4_SEED_PATH) + b4 = b4_doc.get("stage_b_domain_bo_seed_output") if isinstance(b4_doc, dict) else None + if not isinstance(b4, dict) or b4.get("domain_id") != "B4_Actio": + raise RuntimeError("B4 seed artifact invalid") + if b4.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise RuntimeError("B4 seed status invalid") + bo_items = read_json_doc(BO_PATH) + if not isinstance(bo_items, list) or not bo_items: + raise RuntimeError("BO.json root must be non-empty array") + bo_items = [item for item in bo_items if isinstance(item, dict)] + + results = [] + for target, src in ( + ("actio_case_signals.json", _MOD_ACTIO_SRC), + ("case_liability_signals.json", _MOD_LIABILITY_SRC), + ("legal_effect_signals.json", _MOD_LEGAL_EFFECT_SRC), + ): + ns = _load_module(src) + payload = ns["build_signals"](ledger, compiled, b4, bo_items) + write_doc(target, json.dumps(payload, ensure_ascii=False, indent=2) + "\n") + count_key = next(k for k in payload if isinstance(payload[k], list)) + results.append({"written_file": target, "payload_status": payload.get("status"), + "item_count": len(payload[count_key])}) + + print(json.dumps({ + "status": "READY", + "message": "signal 3종 생성 완료", + "schema_version": "stage1_middle_input_signal_writer.v2", + "results": results, + }, ensure_ascii=False)) + + if __name__ == "__main__": + try: + main() + except Exception as exc: + print(json.dumps({"status": "FAILED", "message": "S0 signal bundle writer 실패", + "reason": str(exc)}, ensure_ascii=False)) + raise + task_procedure: + IN: + nexts: + - Task_C_BO_A0_context_and_domain_slice_compiler + wait_until: [] + Task_C_BO_A0_context_and_domain_slice_compiler: + nexts: + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + wait_until: + - IN + Task_C_BO_Stage_B_B1_Money_Successor: + nexts: + - Task_C_BO_R0_seed_reducer_and_exception_planner + wait_until: + - Task_C_BO_A0_context_and_domain_slice_compiler + Task_C_BO_Stage_B_B2_Secured_Registry: + nexts: + - Task_C_BO_R0_seed_reducer_and_exception_planner + wait_until: + - Task_C_BO_A0_context_and_domain_slice_compiler + Task_C_BO_Stage_B_B3_Land_Valuation: + nexts: + - Task_C_BO_R0_seed_reducer_and_exception_planner + wait_until: + - Task_C_BO_A0_context_and_domain_slice_compiler + Task_C_BO_Stage_B_B4_Actio: + nexts: + - Task_C_BO_R0_seed_reducer_and_exception_planner + wait_until: + - Task_C_BO_A0_context_and_domain_slice_compiler + Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense: + nexts: + - Task_C_BO_R0_seed_reducer_and_exception_planner + wait_until: + - Task_C_BO_A0_context_and_domain_slice_compiler + Task_C_BO_R0_seed_reducer_and_exception_planner: + nexts: + - Task_C_BO_R1_exception_adjudicator + wait_until: + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + Task_C_BO_R1_exception_adjudicator: + nexts: + - Task_C_BO_F0_final_bo_compiler_gate_writer + wait_until: + - Task_C_BO_R0_seed_reducer_and_exception_planner + Task_C_BO_F0_final_bo_compiler_gate_writer: + nexts: + - Task_C_BO_S0_signal_bundle_writer + wait_until: + - Task_C_BO_R1_exception_adjudicator + Task_C_BO_S0_signal_bundle_writer: + nexts: + - OUT + wait_until: + - Task_C_BO_F0_final_bo_compiler_gate_writer + OUT: + nexts: [] + wait_until: + - Task_C_BO_S0_signal_bundle_writer diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Claude YAML/Stage_1_Part_3_Claude_v2.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Claude YAML/Stage_1_Part_3_Claude_v2.yml new file mode 100644 index 00000000..38cd3b8a --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Claude YAML/Stage_1_Part_3_Claude_v2.yml @@ -0,0 +1,2250 @@ +--- +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_3 + description: 민사소송 원고 송무 초지능 AI변호사 - Legal Effect 생성 + version: v2.0-claude + Stages: + - name: stage1_legal_effect_생성 + description: Legal Effect Structure 작성 + tools: + mcpServers: + localdocs: + type: streamable-http + url: http://mcp-localdocs:8012/mcp + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + task_procedure: + IN: + nexts: + - Task_L0_structure_seed_reducer + wait_until: [] + Task_L0_structure_seed_reducer: + nexts: + - Task_L1_exception_adjudicator + wait_until: + - IN + Task_L1_exception_adjudicator: + nexts: + - Task_L2_final_structure_index_writer + wait_until: + - Task_L0_structure_seed_reducer + Task_L2_final_structure_index_writer: + nexts: + - OUT + wait_until: + - Task_L1_exception_adjudicator + OUT: + nexts: [] + wait_until: + - Task_L2_final_structure_index_writer + + tasks: + - task_name: Task_L0_structure_seed_reducer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + import sys + from collections import defaultdict + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + INPUT_FILES = [ + "BO.json", + "evidence_indexed.json", + "actio_case_signals.json", + "case_liability_signals.json", + "legal_effect_signals.json", + ] + SEED_OUT_PATH = "stage1_tmp/legal_effect_structure_seed_bundle.json" + PACK_OUT_PATH = "stage1_tmp/legal_effect_structure_exception_pack.json" + AUDIT_OUT_PATH = "stage1_tmp/legal_effect_structure_audit.json" + MAX_EXCERPT = 700 + ISSUE_THRESHOLD = 3 + ISSUE_MARGIN = 2 + STRUCTURE_TYPE_PRIORITY = [ + "money_claim", + "commercial_successor", + "secured_debt", + "registry_invalidity", + "land_use_gain", + "valuation", + "fraudulent_transfer", + "preserved_claim", + "actio_remedy_or_cap", + "succession_notice_lien_asset_defense", + "general_legal_effect", + ] + # X-1: closed canonical routing enum — 이 값만 candidate_structure_types에 들어간다. + CANONICAL_TYPE_SET = set(STRUCTURE_TYPE_PRIORITY) + DOWNSTREAM_ROLE_FAMILY = { + "money_claim": "claim_chain_ref", + "commercial_successor": "claim_chain_ref", + "secured_debt": "linked_structures", + "registry_invalidity": "linked_structures", + "land_use_gain": "legal_calculation_object", + "valuation": "legal_calculation_object", + "fraudulent_transfer": "actio_roles", + "preserved_claim": "claim_chain_ref", + "actio_remedy_or_cap": "linked_actio_structures", + "succession_notice_lien_asset_defense": "linked_structures", + "general_legal_effect": "legal_effect_roles", + } + ISSUE_KEYWORDS = { + "money_claim": ["money", "loan", "대여금", "매매대금", "미지급", "채권", "변제"], + "commercial_successor": ["successor", "상호속용", "영업양수", "승계"], + "secured_debt": ["secured", "담보", "근저당", "피담보", "저당"], + "registry_invalidity": ["registry", "등기", "말소", "무효", "소유권이전"], + "land_use_gain": ["land", "사용이익", "임료", "점유", "인도"], + "valuation": ["valuation", "감정", "평가", "가액", "시가"], + "actio": ["fraudulent", "actio", "사해", "수익자", "전득자", "피보전", "가액배상"], + "asset_defense": ["notice", "lien", "통지", "유치권", "항변", "방어"], + } + HARD_ISSUE_KEY_PRIORITY = ["claim_group_ids", "liability_group_ids", + "canonical_theory_graph_seed_ref", "linked_structure_ids"] + # X-4 방어 상한 (자동 차단 금지 — 초과분은 deterministic_review로 라우팅) + PACK_MAX_ITEMS = 30 + PACK_MAX_BYTES = 40000 + PACK_ITEM_CHAR_LIMIT = 2000 + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-legal-effect-l0-structure-seed-reducer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + + def _loads_tolerant(raw: str, label: str) -> Any: + """SKILL §6.4: strict json.loads 실패 시 raw_decode fallback (read_docs Extra-data 대응).""" + try: + return json.loads(raw) + except json.JSONDecodeError: + try: + obj, _ = JSON_DECODER.raw_decode(raw.strip()) + return obj + except json.JSONDecodeError as exc: + raise RuntimeError(f"Content of {label} is not valid JSON: {raw[:300]}") from exc + + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result.get("result", {}).get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + text = content[0].get("text", "") + envelope = _loads_tolerant(text, doc_name) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + item = results[0] + if isinstance(item, dict) and item.get("error"): + raise RuntimeError(f"read_docs error for {doc_name}: {item.get('error')}") + raw = item.get("content", "") + else: + raw = envelope + if isinstance(raw, str): + return _loads_tolerant(raw, doc_name) + return raw + + + def _read_doc(doc_name: str) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + return _extract_json_from_read_docs_result(result, doc_name) + + + def _write_doc(doc_name: str, payload: dict[str, Any]) -> str: + content = json.dumps(payload, ensure_ascii=False, indent=2) + "\n" + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=_next_msg_id()) + return content + + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + if value is None: + continue + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + + def _first_string(*values: Any) -> str | None: + for value in values: + for item in _strings(value): + return item + return None + + + def _short(value: Any, limit: int = MAX_EXCERPT) -> str | None: + if value is None: + return None + text = re.sub(r"\s+", " ", str(value)).strip() + if not text: + return None + return text[:limit] + + + def _norm_text(text: Any) -> str: + value = re.sub(r"\s+", "", str(text or "").lower()) + value = re.sub(r"[()\[\]{}'\"·,.;:]", "", value) + value = re.sub(r"(에관하여|에대한|및|의|을|를|은|는|이|가)$", "", value) + return value + + + def _asset_norm(label: str) -> str: + text = _norm_text(label) + text = re.sub(r"(지분|전체|소유권|목적물)$", "", text) + return text + + + def _legal_unit_tokens(label: str) -> list[str]: + tokens = [] + for token in ["대지", "토지", "건물", "지분", "근저당", "담보", "점유", "사용이익", "상가", "임야", "채권"]: + if token in str(label): + tokens.append(token) + return tokens + + + def _make_id(prefix: str, ordinal: int) -> str: + return f"{prefix}-{ordinal:03d}" + + + # ---------------------------------------------------------------- 컴파일 (v1 LES0 이식) + def _evidence_index_from_item(item: dict[str, Any]) -> str | None: + return _first_string( + item.get("evidence_index"), + item.get("evidence_id"), + item.get("index"), + item.get("id"), + item.get("source_id"), + ) + + + def _evidence_authority_item(index: str, item: dict[str, Any], source: str) -> dict[str, Any]: + return { + "evidence_index": index, + "source_title": _first_string(item.get("source_title"), item.get("title"), item.get("evidence_title"), item.get("document_title"), index), + "priority_class": _first_string(item.get("priority_class"), item.get("priority"), item.get("evidence_priority")), + "authentication_status": _first_string(item.get("authentication_status"), item.get("auth_status"), item.get("authentication")), + "excerpt": _short(item.get("relevant_content") or item.get("excerpt") or item.get("summary") or item.get("content") or item.get("text")), + "source_file": source, + } + + + def _collect_evidence_authority(evidence_indexed: Any, bo_items: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: + """v1 LES0 이식 — evidence_all 입력 제거판 (포함/제외 산출 동일 실측: 개정 보고서 P3-3).""" + out: dict[str, dict[str, Any]] = {} + + def add(index: str | None, item: dict[str, Any], source: str) -> None: + if not index: + return + new = _evidence_authority_item(index, item, source) + old = out.get(index, {}) + merged = {**new, **{k: v for k, v in old.items() if v not in (None, "", [])}} + if old.get("excerpt") and len(str(old.get("excerpt"))) >= len(str(new.get("excerpt") or "")): + merged["excerpt"] = old.get("excerpt") + out[index] = merged + + def visit(node: Any, source: str) -> None: + if isinstance(node, dict): + idx = _evidence_index_from_item(node) + if idx: + add(idx, node, source) + for key in ("items", "evidence", "documents", "records", "results", "events"): + for child in _list(node.get(key)): + visit(child, source) + elif isinstance(node, list): + for child in node: + visit(child, source) + + visit(evidence_indexed, "evidence_indexed.json") + for bo in bo_items: + for ev in _list(bo.get("Evidence")): + if isinstance(ev, dict): + add(_evidence_index_from_item(ev), ev, "BO.json.Evidence") + return out + + + def _domain_payload(bo: dict[str, Any]) -> dict[str, Any]: + return _dict(_dict(bo.get("extensions")).get("domain_payload")) + + + def _compact_bo(bo: dict[str, Any]) -> dict[str, Any]: + payload = _domain_payload(bo) + core = _dict(bo.get("core_field_base")) + evidence_indexes = _dedupe(_strings(bo.get("source_evidence_indexes")) + [_first_string(ev.get("evidence_index")) for ev in _list(bo.get("Evidence")) if isinstance(ev, dict)]) + evidence_indexes = [idx for idx in evidence_indexes if idx] + return { + "BO_ID": str(bo.get("BO_ID") or bo.get("id") or ""), + "id": str(bo.get("id") or bo.get("BO_ID") or ""), + "BOType": bo.get("BOType"), + "ActionType": bo.get("ActionType"), + "JuristicAct": _dict(bo.get("JuristicAct")), + "Action": bo.get("Action"), + "Reason": bo.get("Reason"), + "PriorAct": bo.get("PriorAct"), + "ReasonRefs": _strings(bo.get("ReasonRefs")), + "Legal_Keywords": _strings(bo.get("Legal_Keywords")), + "core_field_base": core, + "amount": bo.get("amount"), + "source_evidence_indexes": evidence_indexes, + "provenance": _dict(bo.get("provenance")), + "downstream_seed_refs": _dict(bo.get("downstream_seed_refs")), + "domain_payload": { + "domain_legal_effect_tag": payload.get("domain_legal_effect_tag"), + "legal_effect_tags": _strings(payload.get("legal_effect_tags")), + "asset_bindings": _as_list(payload.get("asset_bindings")), + "asset_binding_hint": payload.get("asset_binding_hint"), + "candidate_details": payload.get("candidate_details"), + "facts": payload.get("facts"), + "parties_hint": _as_list(payload.get("parties_hint")), + "action_summary": payload.get("action_summary"), + "title_hint": payload.get("title_hint"), + "summary_hint": payload.get("summary_hint"), + "event_state_type": payload.get("event_state_type"), + "asset_alias": _as_list(payload.get("asset_alias")), + }, + } + + + def _actio_signals_by_bo(data: Any) -> dict[str, list[dict[str, Any]]]: + root = _dict(data) + out: dict[str, list[dict[str, Any]]] = {} + for sig in _list(root.get("actio_case_signals")): + if not isinstance(sig, dict): + continue + compact = { + "signal_id": _first_string(sig.get("signal_id")), + "actio_scope": _strings(sig.get("actio_scope")), + "suspicion_level": sig.get("suspicion_level"), + "related_evidence_indexes": _strings(sig.get("related_evidence_indexes")), + "target_property_candidates": _list(sig.get("target_property_candidates")), + "preserved_claim_candidates": _list(sig.get("preserved_claim_candidates")), + "actio_role_hints": _list(sig.get("actio_role_hints")), + } + for bo_id in _strings(sig.get("related_bo_ids")): + out.setdefault(bo_id, []).append(compact) + for item in _list(sig.get("target_property_candidates")): + if isinstance(item, dict): + for bo_id in _strings(item.get("related_bo_ids")): + out.setdefault(bo_id, []).append(compact) + return out + + + def _case_signals_by_bo(data: Any) -> dict[str, list[dict[str, Any]]]: + root = _dict(data) + out: dict[str, list[dict[str, Any]]] = {} + for idx, sig in enumerate(_list(root.get("case_liability_signals")), start=1): + if not isinstance(sig, dict): + continue + compact = { + "signal_ref": f"case-liability-signal-{idx:03d}", + "liability_candidate_type": sig.get("liability_candidate_type"), + "related_evidence_indexes": _strings(sig.get("related_evidence_indexes")), + "party_roles": _list(sig.get("party_roles")), + "claim_group_id": sig.get("claim_group_id"), + "liability_group_id": sig.get("liability_group_id"), + "linked_structure_ids": _strings(sig.get("linked_structure_ids")), + } + bo_ids = _strings(sig.get("related_bo_ids")) + for role in _list(sig.get("party_roles")): + if isinstance(role, dict): + bo_ids.extend(_strings(role.get("bo_ids"))) + for bo_id in _dedupe(bo_ids): + out.setdefault(bo_id, []).append(compact) + return out + + + def _legal_routes_by_bo(data: Any) -> dict[str, list[dict[str, Any]]]: + root = _dict(data) + out: dict[str, list[dict[str, Any]]] = {} + for route in _list(root.get("bo_legal_effect_routes")): + if not isinstance(route, dict): + continue + bo_id = _first_string(route.get("bo_id")) + if not bo_id: + continue + out.setdefault(bo_id, []).append({ + "candidate_structure_types": _strings(route.get("candidate_structure_types")), + "linked_structure_ids": _strings(route.get("linked_structure_ids")), + "related_evidence_indexes": _strings(route.get("related_evidence_indexes")), + }) + return out + + + def _normalize_structure_type(value: str) -> str: + text = str(value).strip().lower() + if any(x in text for x in ["money", "loan", "대여금", "매매대금", "미지급"]): + return "money_claim" + if any(x in text for x in ["successor", "상호속용", "영업양수"]): + return "commercial_successor" + if any(x in text for x in ["secured", "담보", "근저당", "피담보"]): + return "secured_debt" + if any(x in text for x in ["registry", "등기", "말소", "무효"]): + return "registry_invalidity" + if any(x in text for x in ["land", "사용이익", "임료", "점유"]): + return "land_use_gain" + if any(x in text for x in ["valuation", "감정", "평가", "가액", "cap"]): + return "valuation" + if any(x in text for x in ["fraudulent", "actio", "사해", "수익자", "전득자"]): + return "fraudulent_transfer" + if any(x in text for x in ["preserved", "피보전"]): + return "preserved_claim" + if any(x in text for x in ["remedy", "원상회복", "가액배상"]): + return "actio_remedy_or_cap" + if any(x in text for x in ["succession", "notice", "lien", "asset_defense", "승계", "통지", "유치권", "인도", "항변"]): + return "succession_notice_lien_asset_defense" + return text.replace(" ", "_") if text else "general_legal_effect" + + + def _candidate_structure_types(bo: dict[str, Any], signals: dict[str, list[dict[str, Any]]]) -> tuple[list[str], list[str]]: + """X-1: canonical routing enum만 candidate_structure_types로. 비-canonical은 nonrouting_labels로 보존.""" + types: list[str] = [] + for route in signals.get("legal_effect", []): + types.extend(_strings(route.get("candidate_structure_types"))) + for case_sig in signals.get("case_liability", []): + types.extend(_strings(case_sig.get("liability_candidate_type"))) + for actio_sig in signals.get("actio", []): + for scope in _strings(actio_sig.get("actio_scope")): + mapping = { + "fraudulent_act": "fraudulent_transfer", + "preserved_claim": "preserved_claim", + "remedy_or_cap": "actio_remedy_or_cap", + "target_asset": "actio_asset_target", + "beneficiary_or_transferee": "fraudulent_transfer", + "encumbrance": "secured_debt", + "defense": "succession_notice_lien_asset_defense", + } + if scope in mapping: + types.append(mapping[scope]) + payload = _dict(bo.get("domain_payload")) + types.extend(_strings(payload.get("domain_legal_effect_tag"))) + types.extend(_strings(payload.get("legal_effect_tags"))) + source_domain = _first_string(_dict(bo.get("provenance")).get("source_domain")) + if source_domain: + if "B1" in source_domain: + types.append("money_claim") + elif "B2" in source_domain: + types.append("secured_debt") + elif "B3" in source_domain: + types.append("land_use_gain") + elif "B4" in source_domain: + types.append("fraudulent_transfer") + elif "B5" in source_domain: + types.append("succession_notice_lien_asset_defense") + canonical: list[str] = [] + nonrouting: list[str] = [] + for value in types: + if not value: + continue + norm = _normalize_structure_type(value) + if norm in CANONICAL_TYPE_SET: + canonical.append(norm) + else: + nonrouting.append(norm) + return _dedupe(canonical), _dedupe(nonrouting) + + + def _asset_kind(label: str, fallback: Any = None) -> str: + if fallback: + text = str(fallback) + if text in {"real_property", "movable", "claim", "security_right", "cash_or_deposit", "business_asset", "other", "unknown"}: + return text + if any(x in label for x in ["토지", "대지", "건물", "부동산", "아파트", "상가", "임야", "등기"]): + return "real_property" + if any(x in label for x in ["근저당", "담보", "저당", "유치권", "질권"]): + return "security_right" + if any(x in label for x in ["채권", "대여금", "매매대금", "보증금"]): + return "claim" + if any(x in label for x in ["예금", "현금", "계좌"]): + return "cash_or_deposit" + if any(x in label for x in ["영업", "사업", "상호"]): + return "business_asset" + if any(x in label for x in ["동산", "차량", "기계"]): + return "movable" + return "unknown" + + + def _asset_strings(value: Any) -> list[str]: + out: list[str] = [] + + def visit(node: Any) -> None: + if node is None or len(out) >= 20: + return + if isinstance(node, (str, int, float)): + text = str(node).strip() + if text and len(text) <= 180: + out.append(text) + return + if isinstance(node, list): + for child in node: + visit(child) + return + if isinstance(node, dict): + for key in [ + "asset_label", "asset", "assets", "property", "properties", "object", "objects", + "collateral", "collaterals", "target", "official_designation", "alias", + ]: + if key in node: + visit(node.get(key)) + visit(value) + return _dedupe(out) + + + def _dedupe_assets(items: list[dict[str, Any]]) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + seen: set[str] = set() + for item in items: + key = json.dumps({ + "asset_label": item.get("asset_label"), + "asset_kind": item.get("asset_kind"), + "source": item.get("source"), + # X-3: ref는 자산 identity 정보 — 상이 ref 행을 중복으로 소거하지 않는다 + "asset_ref_keys": sorted(_strings(item.get("asset_ref_keys"))), + }, ensure_ascii=False, sort_keys=True) + if key not in seen: + seen.add(key) + out.append(item) + return out + + + def _extract_asset_candidates(bo: dict[str, Any], signals: dict[str, list[dict[str, Any]]]) -> list[dict[str, Any]]: + bo_id = bo.get("BO_ID") + evidence = _strings(bo.get("source_evidence_indexes")) + out: list[dict[str, Any]] = [] + for sig in signals.get("actio", []): + for item in _list(sig.get("target_property_candidates")): + if isinstance(item, dict): + label = _first_string(item.get("asset_label")) + if label: + out.append({ + "asset_label": label, + "asset_kind": _asset_kind(label, item.get("asset_kind")), + "source": "actio_case_signals.target_property_candidates", + "related_bo_ids": _dedupe([bo_id] + _strings(item.get("related_bo_ids"))), + "related_evidence_indexes": _dedupe(evidence + _strings(item.get("related_evidence_indexes"))), + }) + payload = _dict(bo.get("domain_payload")) + for source_key in ["asset_bindings", "asset_binding_hint", "candidate_details", "facts"]: + values = _asset_strings(payload.get(source_key)) + for label in values: + out.append({ + "asset_label": label, + "asset_kind": _asset_kind(label), + "source": f"BO.extensions.domain_payload.{source_key}", + "related_bo_ids": [bo_id] if bo_id else [], + "related_evidence_indexes": evidence, + }) + for alias in _as_list(payload.get("asset_alias")): + if not isinstance(alias, dict): + continue + ref_keys = _strings(alias.get("schedule_refs")) + for reg in _as_list(alias.get("registry_refs")): + if isinstance(reg, dict): + ref_keys.extend(_strings(reg.get("registry_ref"))) + else: + ref_keys.extend(_strings(reg)) + alias_labels = _strings(alias.get("alias_strings")) + _strings(alias.get("canonical_label_candidate")) + for label in alias_labels or ([ref_keys[0]] if ref_keys else []): + out.append({ + "asset_label": label, + "asset_kind": _asset_kind(label), + "source": "BO.extensions.domain_payload.asset_alias", + "asset_ref_keys": _dedupe(ref_keys), + "related_bo_ids": [bo_id] if bo_id else [], + "related_evidence_indexes": evidence, + }) + core_obj = _first_string(_dict(bo.get("core_field_base")).get("Object")) + if core_obj: + out.append({ + "asset_label": core_obj, + "asset_kind": _asset_kind(core_obj), + "source": "BO.core_field_base.Object", + "related_bo_ids": [bo_id] if bo_id else [], + "related_evidence_indexes": evidence, + }) + return _dedupe_assets(out) + + + def _require_signal_root(data: Any, doc_name: str, schema_version: str, array_key: str) -> None: + root = _dict(data) + if root.get("schema_version") != schema_version: + raise ValueError(f"{doc_name} schema_version must be {schema_version}") + if root.get("status") not in {"READY", "READY_WITH_REVIEW", "EMPTY"}: + raise ValueError(f"{doc_name} status must be READY, READY_WITH_REVIEW, or EMPTY") + if not isinstance(root.get(array_key), list): + raise ValueError(f"{doc_name}.{array_key} must be array") + + + # ---------------------------------------------------------------- 결정 로직 (v1 LES1 이식 + X-2/X-3/X-4/X-5) + def _score_issue(candidate: dict[str, Any]) -> list[dict[str, Any]]: + fields = _dict(candidate.get("keyword_fields")) + text = " ".join( + _strings(fields.get("Legal_Keywords")) + + _strings(fields.get("Action")) + + _strings(fields.get("Reason")) + + _strings(fields.get("JuristicAct_label")) + + _strings(fields.get("ActionType")) + + _strings(fields.get("domain_legal_effect_tag")) + + _strings(fields.get("legal_effect_tags")) + + _strings(candidate.get("candidate_structure_types")) + + _strings(candidate.get("source_domain")) + ).lower() + rows = [] + for family, keywords in ISSUE_KEYWORDS.items(): + score = sum(1 for keyword in keywords if keyword.lower() in text) + if score: + rows.append({"candidate_value": family, "score": score, "basis": [kw for kw in keywords if kw.lower() in text]}) + rows.sort(key=lambda row: (-row["score"], row["candidate_value"])) + return rows + + + def _hard_issue_keys(candidate: dict[str, Any]) -> list[dict[str, Any]]: + hard = _dict(candidate.get("hard_issue_keys")) + rows: list[dict[str, Any]] = [] + for field in ["claim_group_ids", "liability_group_ids", "linked_structure_ids", "canonical_theory_graph_seed_ref"]: + for value in _strings(hard.get(field)): + rows.append({"field": field, "value": value}) + return rows + + + def _issue_decision(candidate: dict[str, Any]) -> tuple[str, dict[str, Any], list[str]]: + """v1 이식 + 개정: llm_required 분기를 deterministic_review로 전환 (예외 생성 없음 — X-4 ④ 불충족 계열).""" + bo_id = candidate["bo_id"] + hard_rows = _hard_issue_keys(candidate) + score_rows = _score_issue(candidate) + conflict_flags: list[str] = [] + hard_by_field = {field: _strings(_dict(candidate.get("hard_issue_keys")).get(field)) + for field in HARD_ISSUE_KEY_PRIORITY} + intra_conflict_fields = [field for field, values in hard_by_field.items() if len(values) > 1] + if intra_conflict_fields: + conflict_flags.append("multiple_hard_issue_keys") + else: + for field in HARD_ISSUE_KEY_PRIORITY: + if hard_by_field[field]: + demoted = {f: hard_by_field[f] for f in HARD_ISSUE_KEY_PRIORITY + if f != field and hard_by_field[f]} + return f"hard:{hard_by_field[field][0]}", { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [field], + "demoted_hard_keys": demoted, + "conflict_flags": [], + }, [] + if score_rows: + top = score_rows[0] + second = score_rows[1]["score"] if len(score_rows) > 1 else 0 + if top["score"] >= ISSUE_THRESHOLD and top["score"] - second >= ISSUE_MARGIN and not conflict_flags: + return f"issue:{top['candidate_value']}", { + "decision": "score_confident", + "confidence": "MEDIUM", + "basis_fields": ["Legal_Keywords", "Action", "Reason", "JuristicAct.label", "domain tags"], + "conflict_flags": [], + }, [] + types = set(_strings(candidate.get("candidate_structure_types"))) + if {"money_claim", "fraudulent_transfer"}.issubset(types): + conflict_flags.append("money_claim_and_actio_same_bo") + if {"registry_invalidity", "secured_debt"}.issubset(types): + conflict_flags.append("registry_invalidity_and_secured_debt_same_bo") + candidate_pool = _dedupe([row["value"] for row in hard_rows] + [row["candidate_value"] for row in score_rows]) + if len(candidate_pool) == 1 and not conflict_flags: + return f"issue:{candidate_pool[0]}", { + "decision": "auto_selected_single_candidate", + "confidence": "MEDIUM", + "basis_fields": ["hard_issue_keys", "score_table"], + "conflict_flags": [], + }, [] + if not hard_rows and not score_rows: + return f"review_issue:{bo_id}", { + "decision": "deterministic_review", + "confidence": "LOW", + "basis_fields": [], + "conflict_flags": ["missing_candidate_values"], + }, ["missing_candidate_values"] + # 개정: compact 근거로 확정 불충분(스코어 저신뢰·다후보)한 issue 모호성은 LLM행이 아니라 + # 보존적 defer — 별도 issue cluster로 유지하고 review code만 남긴다 (X-4 ④ 불충족). + return f"review_issue:{bo_id}", { + "decision": "deterministic_review", + "confidence": "LOW", + "basis_fields": ["hard_issue_keys", "Legal_Keywords", "Action", "Reason", "JuristicAct.label", "candidate_structure_types"], + "conflict_flags": conflict_flags or ["insufficient_issue_confidence"], + }, ["ambiguous_issue_cluster_deferred"] + + + def _structure_type_decision(candidate: dict[str, Any]) -> tuple[str, list[str], dict[str, Any]]: + """X-2: canonical 복수 타입은 예외가 아니다 — 전부 보존 + priority 대표. 예외 생성 경로 없음. + (현행 최종 스키마에는 타입 간 상호 배타 필드가 존재하지 않아 X-4 ① 조건을 충족하는 + type 충돌이 성립하지 않는다 — 개정 전략서 §5-4.)""" + types = _dedupe(_strings(candidate.get("candidate_structure_types"))) or ["general_legal_effect"] + roles = {DOWNSTREAM_ROLE_FAMILY.get(t, "legal_effect_roles") for t in types} + representative = sorted(types, key=lambda t: STRUCTURE_TYPE_PRIORITY.index(t) if t in STRUCTURE_TYPE_PRIORITY else 999)[0] + diag = { + "decision": "deterministic_multi_routing" if len(types) > 1 else "deterministic", + "candidate_types": types, + "downstream_roles": sorted(roles), + "representative_candidate_structure_type": representative, + "basis_fields": ["legal_effect_signals", "case_liability_signals", "actio_case_signals", "BO.downstream_seed_refs", "BO.domain_payload"], + } + return representative, types, diag + + + def _build_asset_alias_map(candidates: dict[str, dict[str, Any]]) -> dict[str, list[list[str]]]: + """X-3(b): label(정규화) ↔ ref 계열(lineage) map. 같은 alias 항목에 함께 기재되었거나 + ref가 겹치는 집합은 하나의 lineage로 통합한다 (등기행 다수 + 별지목록 = 동일 자산의 정상 기재). + 한 라벨이 서로소인 lineage 2개 이상을 주장할 때만 진짜 충돌이다.""" + label_to_refsets: dict[str, list[set[str]]] = defaultdict(list) + for bo_id in sorted(candidates): + for asset in _list(candidates[bo_id].get("asset_candidates")): + if not isinstance(asset, dict): + continue + refs = set(_strings(asset.get("asset_ref_keys"))) + label = asset.get("asset_label") + if not refs or not label: + continue + sets = label_to_refsets[_asset_norm(str(label))] + merged = False + for existing in sets: + if existing & refs: + existing |= refs + merged = True + break + if not merged: + sets.append(set(refs)) + out: dict[str, list[list[str]]] = {} + for key, sets in label_to_refsets.items(): + changed = True + while changed: + changed = False + for i in range(len(sets)): + for j in range(i + 1, len(sets)): + if sets[i] & sets[j]: + sets[i] |= sets[j] + del sets[j] + changed = True + break + if changed: + break + out[key] = sorted((sorted(s, key=_ref_sort) for s in sets), key=lambda s: s[0]) + return out + + + def _ref_sort(ref_key: str) -> tuple[int, str]: + return (0 if ref_key.startswith("REG-") else 1 if ref_key.startswith("SCH-") else 2, ref_key) + + + def _asset_decisions(candidate: dict[str, Any], alias_map: dict[str, set[str]], + key_alias: dict[str, str]) -> tuple[list[str], list[dict[str, Any]], list[dict[str, Any]], list[str], list[dict[str, Any]]]: + """X-3 ref-first 규칙: + (a) 동일 REG-*/SCH-* ref 공유 → 동일 클러스터 (v1 keyed 그룹핑 유지) + (b) unkeyed 라벨이 alias map에서 단일 ref 계열로 연결 → 결정론 병합 (deterministic_resolution) + (c) 한 BO에 서로 다른 ref 복수 → 클러스터 분리 + multi_asset_bundle 보존 (예외 아님) + (d) alias 상충(한 라벨이 2+ 상이 ref 계열 주장) → X-4 4중 조건 충족 → pack 예외 + (e) ref/alias 근거 없는 자유문구 동일성 불명(kind/legal unit 상이) → SEPARATE 보존 + review code + 반환: (cluster_keys, bundle_items, pack_exceptions, review_codes, resolutions)""" + bo_id = candidate["bo_id"] + assets = [item for item in _list(candidate.get("asset_candidates")) if isinstance(item, dict) and item.get("asset_label")] + if not assets: + return [], [], [], [], [] + cluster_keys: list[str] = [] + bundle_items: list[dict[str, Any]] = [] + pack_exceptions: list[dict[str, Any]] = [] + review_codes: list[str] = [] + resolutions: list[dict[str, Any]] = [] + + def _root_for_refs(refs: list[str]) -> str: + existing = next((key_alias[r] for r in refs if r in key_alias), None) + root = existing or f"ref:{sorted(refs, key=_ref_sort)[0]}" + for r in refs: + key_alias[r] = root + return root + + groups: dict[str, list[dict[str, Any]]] = defaultdict(list) + packed_labels: set[str] = set() + for asset in assets: + label_norm = _asset_norm(str(asset.get("asset_label"))) + lineages_all = alias_map.get(label_norm) or [] + if len(lineages_all) > 1: + # (d) keyed/unkeyed 불문: 한 라벨이 서로소 ref lineage 2+를 주장 — non-deferrable pack + if label_norm not in packed_labels: + packed_labels.add(label_norm) + pack_exceptions.append({ + "exception_id": "", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [bo_id], + "candidate_values": _dedupe([str(asset.get("asset_label"))] + [lineage[0] for lineage in lineages_all]), + "basis_fields": ["asset_alias", str(asset.get("source"))], + "score_summary": [{"candidate_value": "|".join(lineage), "basis": "disjoint ref lineage"} for lineage in lineages_all[:3]], + "conflict_flags": ["label_claims_disjoint_ref_lineages"], + "allowed_decisions": ["SAME_CLUSTER", "SEPARATE_CLUSTER", "MULTI_ASSET_BUNDLE", "BLOCK_REVIEW"], + "compact_context": {"lineages": lineages_all, "asset_kind": asset.get("asset_kind")}, + }) + cluster_keys.append(f"review_asset:{bo_id}:{len(pack_exceptions)}") + continue + refs = _strings(asset.get("asset_ref_keys")) + if refs: + groups[_root_for_refs(refs)].append(asset) + continue + if lineages_all: + # (b) 단일 lineage — alias 근거 결정론 병합 + root = _root_for_refs(list(lineages_all[0])) + groups[root].append(asset) + resolutions.append({ + "kind": "alias_merged", + "bo_id": bo_id, + "asset_label": asset.get("asset_label"), + "ref_root": root, + "basis": "asset_alias ref-label linkage (X-3b)", + }) + continue + groups[f"{label_norm}::{asset.get('asset_kind') or 'unknown'}"].append(asset) + + for key, group in groups.items(): + if key.startswith("ref:"): + cluster_keys.append(f"asset:{key}") + continue + labels = _dedupe([str(item.get("asset_label")) for item in group]) + kinds = set(_strings([item.get("asset_kind") for item in group])) + legal_units = {tuple(_legal_unit_tokens(label)) for label in labels} + if len(kinds) == 1 and len(legal_units) <= 1: + cluster_keys.append(f"asset:{key}") + else: + # (e) 개정: v1의 LLM 예외 대신 라벨별 SEPARATE 보존 + review code (fuzzy 병합 금지) + for label in labels: + cluster_keys.append(f"asset:{_asset_norm(label)}::{_asset_kind(label)}") + review_codes.append("asset_identity_unresolved_separate_preserved") + resolutions.append({ + "kind": "separate_preserved", + "bo_id": bo_id, + "labels": labels, + "basis": "kind/legal-unit divergence without ref linkage (X-3e)", + }) + normalized_roots: set[str] = set() + for item in assets: + item_refs = _strings(item.get("asset_ref_keys")) + if item_refs and item_refs[0] in key_alias: + normalized_roots.add(key_alias[item_refs[0]]) + else: + label_norm = _asset_norm(str(item.get("asset_label"))) + lineages = alias_map.get(label_norm) or [] + if len(lineages) == 1 and lineages[0] and lineages[0][0] in key_alias: + normalized_roots.add(key_alias[lineages[0][0]]) + else: + normalized_roots.add(label_norm) + if len(normalized_roots) > 1: + bundle_items.append({ + "bundle_ref": f"bundle:{bo_id}", + "bundle_type": "multi_asset_bundle", + "source_bo_ids": [bo_id], + "member_asset_labels": _dedupe([str(item.get("asset_label")) for item in assets]), + }) + return _dedupe(cluster_keys), bundle_items, pack_exceptions, _dedupe(review_codes), resolutions + + + def _merge_exceptions(exceptions: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + """v1 서명 병합 이식 (단일 리스트 형태).""" + merged: dict[str, dict[str, Any]] = {} + for exc in exceptions: + signature = json.dumps({ + "type": exc.get("exception_type"), + "candidates": sorted(_strings(exc.get("candidate_values"))), + "flags": sorted(_strings(exc.get("conflict_flags"))), + "decisions": sorted(_strings(exc.get("allowed_decisions"))), + }, ensure_ascii=False, sort_keys=True) + if signature in merged: + root = merged[signature] + root["source_bo_ids"] = _dedupe(_strings(root.get("source_bo_ids")) + _strings(exc.get("source_bo_ids"))) + root["merged_from_count"] = root.get("merged_from_count", 1) + 1 + else: + merged[signature] = {**exc, "merged_from_count": 1} + ordered = sorted( + merged.values(), + key=lambda item: ( + min(_strings(item.get("source_bo_ids")) or [""]), + json.dumps(sorted(_strings(item.get("candidate_values"))), ensure_ascii=False), + ), + ) + merge_groups: list[dict[str, Any]] = [] + for ordinal, exc in enumerate(ordered, start=1): + exc["exception_id"] = _make_id("LEX", ordinal) + if exc.get("merged_from_count", 1) > 1: + merge_groups.append({ + "representative_exception_id": exc["exception_id"], + "source_bo_ids": _strings(exc.get("source_bo_ids")), + "merged_from_count": exc["merged_from_count"], + }) + return ordered, merge_groups + + + def _add_issue_seed(target: dict[str, dict[str, Any]], issue_id: str, issue_key: str, item: dict[str, Any], candidate: dict[str, Any], diag: dict[str, Any]) -> None: + row = target.setdefault(issue_id, { + "issue_cluster_id": issue_id, + "issue_key": issue_key, + "issue_type": issue_key.split(":", 1)[-1], + "source_bo_ids": [], + "candidate_structure_types": [], + "claim_group_ids": [], + "liability_group_ids": [], + "evidence_indexes": [], + "asset_cluster_ids": [], + "basis_fields": [], + "pending_exception_ids": [], + "deterministic_review_codes": [], + }) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + item["source_bo_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + item["candidate_structure_types"]) + hard = _dict(candidate.get("hard_issue_keys")) + row["claim_group_ids"] = _dedupe(row["claim_group_ids"] + _strings(hard.get("claim_group_ids"))) + row["liability_group_ids"] = _dedupe(row["liability_group_ids"] + _strings(hard.get("liability_group_ids"))) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + item["evidence_indexes"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + item["asset_cluster_ids"]) + row["basis_fields"] = _dedupe(row["basis_fields"] + _strings(diag.get("basis_fields"))) + row["pending_exception_ids"] = _dedupe(row["pending_exception_ids"] + item["pending_exception_ids"]) + row["deterministic_review_codes"] = _dedupe(row["deterministic_review_codes"] + item["deterministic_review_codes"]) + + + def _add_asset_seeds(target: dict[str, dict[str, Any]], asset_ids: list[str], item: dict[str, Any], candidate: dict[str, Any]) -> None: + assets = [asset for asset in _list(candidate.get("asset_candidates")) if isinstance(asset, dict)] + for asset_id in asset_ids: + row = target.setdefault(asset_id, { + "asset_cluster_id": asset_id, + "canonical_asset_label": None, + "asset_kind": None, + "member_asset_mentions": [], + "source_bo_ids": [], + "evidence_indexes": [], + "issue_cluster_ids": [], + "candidate_structure_types": [], + "basis_fields": [], + "pending_exception_ids": [], + "deterministic_review_codes": [], + }) + related_assets = assets or [] + labels = _dedupe([str(asset.get("asset_label")) for asset in related_assets if asset.get("asset_label")]) + if row["canonical_asset_label"] is None and labels: + row["canonical_asset_label"] = labels[0] + kinds = _dedupe([str(asset.get("asset_kind")) for asset in related_assets if asset.get("asset_kind")]) + if row["asset_kind"] is None and kinds: + row["asset_kind"] = kinds[0] + row["member_asset_mentions"] = _dedupe(row["member_asset_mentions"] + labels) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + item["source_bo_ids"]) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + item["evidence_indexes"]) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + item["issue_cluster_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + item["candidate_structure_types"]) + row["basis_fields"] = _dedupe(row["basis_fields"] + [str(asset.get("source")) for asset in related_assets if asset.get("source")]) + row["pending_exception_ids"] = _dedupe(row["pending_exception_ids"] + item["pending_exception_ids"]) + row["deterministic_review_codes"] = _dedupe(row["deterministic_review_codes"] + item["deterministic_review_codes"]) + + + def _add_evidence_index(target: dict[str, dict[str, Any]], evidence_indexes: list[str], item: dict[str, Any], authority: dict[str, Any]) -> None: + for idx in evidence_indexes: + auth = _dict(authority.get(idx)) + row = target.setdefault(idx, { + "evidence_index": idx, + "source_title": auth.get("source_title"), + "priority_class": auth.get("priority_class"), + "authentication_status": auth.get("authentication_status"), + "seed_ids": [], + "source_bo_ids": [], + "issue_cluster_ids": [], + "asset_cluster_ids": [], + "candidate_structure_types": [], + "gap_flags": [], + }) + row["seed_ids"] = _dedupe(row["seed_ids"] + [item["seed_id"]]) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + item["source_bo_ids"]) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + item["issue_cluster_ids"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + item["asset_cluster_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + item["candidate_structure_types"]) + if not auth: + row["gap_flags"] = _dedupe(row["gap_flags"] + ["missing_evidence_authority"]) + elif not auth.get("excerpt"): + row["gap_flags"] = _dedupe(row["gap_flags"] + ["missing_evidence_excerpt"]) + + + def main() -> None: + try: + _init_mcp_session() + docs = {name: _read_doc(name) for name in INPUT_FILES} + bo_items = docs["BO.json"] + if not isinstance(bo_items, list): + raise ValueError("BO.json root must be array") + _require_signal_root(docs["actio_case_signals.json"], "actio_case_signals.json", "actio_case_signals.v1", "actio_case_signals") + _require_signal_root(docs["case_liability_signals.json"], "case_liability_signals.json", "case_liability_signals.v1", "case_liability_signals") + _require_signal_root(docs["legal_effect_signals.json"], "legal_effect_signals.json", "legal_effect_signals.v1", "bo_legal_effect_routes") + evidence_authority = _collect_evidence_authority(docs["evidence_indexed.json"], bo_items) + compact_bo_items = [_compact_bo(bo) for bo in bo_items if isinstance(bo, dict)] + bo_by_id = {bo["BO_ID"]: bo for bo in compact_bo_items if bo.get("BO_ID")} + actio_by_bo = _actio_signals_by_bo(docs["actio_case_signals.json"]) + case_by_bo = _case_signals_by_bo(docs["case_liability_signals.json"]) + legal_by_bo = _legal_routes_by_bo(docs["legal_effect_signals.json"]) + + candidates: dict[str, dict[str, Any]] = {} + nonrouting_by_bo: dict[str, list[str]] = {} + input_warnings: list[dict[str, Any]] = [] + for bo_id, bo in bo_by_id.items(): + signals = { + "actio": actio_by_bo.get(bo_id, []), + "case_liability": case_by_bo.get(bo_id, []), + "legal_effect": legal_by_bo.get(bo_id, []), + } + evidence_indexes = _dedupe( + _strings(bo.get("source_evidence_indexes")) + + [idx for route in signals["legal_effect"] for idx in _strings(route.get("related_evidence_indexes"))] + + [idx for sig in signals["case_liability"] for idx in _strings(sig.get("related_evidence_indexes"))] + + [idx for sig in signals["actio"] for idx in _strings(sig.get("related_evidence_indexes"))] + ) + missing_evidence = [idx for idx in evidence_indexes if idx not in evidence_authority] + if missing_evidence: + input_warnings.append({"bo_id": bo_id, "warning": "missing_evidence_authority", "evidence_indexes": missing_evidence}) + canonical_types, nonrouting = _candidate_structure_types(bo, signals) + nonrouting_by_bo[bo_id] = nonrouting + candidates[bo_id] = { + "bo_id": bo_id, + "source_domain": _first_string(_dict(bo.get("provenance")).get("source_domain")), + "hard_issue_keys": { + "claim_group_ids": _dedupe([x for sig in signals["case_liability"] for x in _strings(sig.get("claim_group_id"))] + _strings(_dict(bo.get("downstream_seed_refs")).get("claim_group_seed_refs_proposed"))), + "liability_group_ids": _dedupe([x for sig in signals["case_liability"] for x in _strings(sig.get("liability_group_id"))]), + "linked_structure_ids": _dedupe( + [x for route in signals["legal_effect"] for x in _strings(route.get("linked_structure_ids"))] + + [x for sig in signals["case_liability"] for x in _strings(sig.get("linked_structure_ids"))] + + _strings(_dict(bo.get("downstream_seed_refs")).get("legal_effect_structure_seed_ref_proposed")) + ), + "canonical_theory_graph_seed_ref": _strings(_dict(bo.get("downstream_seed_refs")).get("canonical_theory_graph_seed_ref_proposed")), + }, + "candidate_structure_types": canonical_types, + "asset_candidates": _extract_asset_candidates(bo, signals), + "evidence_indexes": evidence_indexes, + "keyword_fields": { + "Legal_Keywords": _strings(bo.get("Legal_Keywords")), + "Action": bo.get("Action"), + "Reason": bo.get("Reason"), + "JuristicAct_label": _dict(bo.get("JuristicAct")).get("label"), + "ActionType": bo.get("ActionType"), + "BOType": bo.get("BOType"), + "domain_legal_effect_tag": _dict(bo.get("domain_payload")).get("domain_legal_effect_tag"), + "legal_effect_tags": _strings(_dict(bo.get("domain_payload")).get("legal_effect_tags")), + }, + "signals": signals, + } + + alias_map = _build_asset_alias_map(candidates) + issue_key_to_id: dict[str, str] = {} + asset_key_to_id: dict[str, str] = {} + key_alias: dict[str, str] = {} + issue_seed_map: dict[str, dict[str, Any]] = {} + asset_seed_map: dict[str, dict[str, Any]] = {} + evidence_index_seed: dict[str, dict[str, Any]] = {} + seed_items: list[dict[str, Any]] = [] + raw_exceptions: list[dict[str, Any]] = [] + deterministic_resolutions: list[dict[str, Any]] = [] + multi_asset_bundles: list[dict[str, Any]] = [] + + for bo_id in sorted(candidates): + candidate = _dict(candidates[bo_id]) + issue_key, issue_diag, issue_codes = _issue_decision(candidate) + if issue_key not in issue_key_to_id: + issue_key_to_id[issue_key] = _make_id("IC", len(issue_key_to_id) + 1) + issue_id = issue_key_to_id[issue_key] + asset_keys, bundles, asset_excs, asset_codes, resolutions = _asset_decisions(candidate, alias_map, key_alias) + asset_ids: list[str] = [] + for key in asset_keys: + if key not in asset_key_to_id: + asset_key_to_id[key] = _make_id("AC", len(asset_key_to_id) + 1) + asset_ids.append(asset_key_to_id[key]) + multi_asset_bundles.extend(bundles) + deterministic_resolutions.extend(resolutions) + raw_exceptions.extend(asset_excs) + representative_type, all_types, type_diag = _structure_type_decision(candidate) + if type_diag.get("decision") == "deterministic_multi_routing": + deterministic_resolutions.append({ + "kind": "multi_routing_preserved", + "bo_id": bo_id, + "candidate_structure_types": all_types, + "representative": representative_type, + "basis": "canonical multi-routing preserved deterministically (X-2)", + }) + + seed_id = _make_id("LESS", len(seed_items) + 1) + evidence_indexes = _strings(candidate.get("evidence_indexes")) + signals = _dict(candidate.get("signals")) + case_signals = _list(signals.get("case_liability")) + actio_signals = _list(signals.get("actio")) + item = { + "seed_id": seed_id, + "source_bo_ids": [bo_id], + "representative_candidate_structure_type": representative_type, + "candidate_structure_types": all_types, + "issue_cluster_ids": [issue_id], + "asset_cluster_ids": asset_ids, + "multi_asset_bundle_refs": _strings([bundle.get("bundle_ref") for bundle in bundles]), + "evidence_indexes": evidence_indexes, + "linked_claim_group_ids": _dedupe([x for sig in case_signals if isinstance(sig, dict) for x in _strings(sig.get("claim_group_id"))]), + "linked_liability_group_ids": _dedupe([x for sig in case_signals if isinstance(sig, dict) for x in _strings(sig.get("liability_group_id"))]), + "linked_actio_signal_ids": _dedupe([x for sig in actio_signals if isinstance(sig, dict) for x in _strings(sig.get("signal_id"))]), + "basis_fields": _dedupe(_strings(issue_diag.get("basis_fields")) + _strings(type_diag.get("basis_fields")) + ["BO.source_evidence_indexes"]), + "pending_exception_ids": [], + "deterministic_review_codes": _dedupe(issue_codes + asset_codes), + "diagnostics": { + "issue": {"decision": issue_diag.get("decision"), "confidence": issue_diag.get("confidence")}, + "candidate_structure_type": {"decision": type_diag.get("decision")}, + }, + } + seed_items.append(item) + _add_issue_seed(issue_seed_map, issue_id, issue_key, item, candidate, issue_diag) + _add_asset_seeds(asset_seed_map, asset_ids, item, candidate) + _add_evidence_index(evidence_index_seed, evidence_indexes, item, evidence_authority) + + merged_exceptions, merge_groups = _merge_exceptions(raw_exceptions) + # 병합 후 LEX id를 seed의 pending_exception_ids로 역결합 + exc_by_signature_bo: dict[str, list[str]] = defaultdict(list) + for exc in merged_exceptions: + for exc_bo in _strings(exc.get("source_bo_ids")): + exc_by_signature_bo[exc_bo].append(str(exc["exception_id"])) + # X-4 방어 상한: 자동 차단 금지 — 초과분은 deterministic_review로 전환 (id 유지, 추적 가능) + deterministic_review_overflow: list[dict[str, Any]] = [] + kept_exceptions: list[dict[str, Any]] = [] + total_bytes = 0 + for exc in merged_exceptions: + serialized = len(json.dumps(exc, ensure_ascii=False).encode("utf-8")) + item_chars = len(json.dumps(exc, ensure_ascii=False)) + if item_chars > PACK_ITEM_CHAR_LIMIT or len(kept_exceptions) >= PACK_MAX_ITEMS or total_bytes + serialized > PACK_MAX_BYTES: + deterministic_review_overflow.append({ + "exception_id": exc["exception_id"], + "exception_type": exc.get("exception_type"), + "source_bo_ids": _strings(exc.get("source_bo_ids")), + "reason": "pack_budget_deferred_to_review", + }) + continue + kept_exceptions.append(exc) + total_bytes += serialized + overflow_ids = {str(item["exception_id"]) for item in deterministic_review_overflow} + for item in seed_items: + bo_id = item["source_bo_ids"][0] + ids = exc_by_signature_bo.get(bo_id, []) + item["pending_exception_ids"] = _dedupe([i for i in ids if i not in overflow_ids]) + if any(i in overflow_ids for i in ids): + item["deterministic_review_codes"] = _dedupe(item["deterministic_review_codes"] + ["pack_budget_deferred_to_review"]) + for issue_row in issue_seed_map.values(): + issue_row["pending_exception_ids"] = _dedupe([i for bo in issue_row["source_bo_ids"] for i in exc_by_signature_bo.get(bo, []) if i not in overflow_ids]) + for asset_row in asset_seed_map.values(): + asset_row["pending_exception_ids"] = _dedupe([i for bo in asset_row["source_bo_ids"] for i in exc_by_signature_bo.get(bo, []) if i not in overflow_ids]) + + exception_pack = { + "schema_version": "stage1_legal_structure_exception_pack.v2", + "has_exceptions": bool(kept_exceptions), + "exception_count": len(kept_exceptions), + "exceptions": kept_exceptions, + "budget": {"max_items": PACK_MAX_ITEMS, "max_bytes": PACK_MAX_BYTES, + "max_chars_per_item": PACK_ITEM_CHAR_LIMIT, + "overflow_policy": "DEFER_TO_DETERMINISTIC_REVIEW"}, + } + pack_content = _write_doc(PACK_OUT_PATH, exception_pack) + pack_sha = hashlib.sha256(pack_content.encode("utf-8")).hexdigest() + + audit = { + "schema_version": "stage1_legal_structure_audit.v1", + "nonrouting_labels_by_bo_id": {k: v for k, v in sorted(nonrouting_by_bo.items()) if v}, + "deterministic_resolutions": deterministic_resolutions, + "deterministic_review_items": deterministic_review_overflow + [ + {"seed_id": item["seed_id"], "source_bo_ids": item["source_bo_ids"], + "codes": item["deterministic_review_codes"]} + for item in seed_items if item["deterministic_review_codes"] + ], + "merge_groups": merge_groups, + "input_warnings": input_warnings, + "review_reduction_trace": [], + } + _write_doc(AUDIT_OUT_PATH, audit) + + seed_bundle = { + "legal_structure_seed_bundle": { + "schema_version": "stage1_legal_structure_seed_bundle.v2", + "status": "READY", + "structure_seed_items": seed_items, + "issue_cluster_seed_map": issue_seed_map, + "asset_cluster_seed_map": asset_seed_map, + "evidence_index_seed": evidence_index_seed, + "multi_asset_bundles": multi_asset_bundles, + "exception_pack_ref": { + "path": PACK_OUT_PATH, + "sha256": pack_sha, + "expected_exception_ids": [str(exc["exception_id"]) for exc in kept_exceptions], + }, + } + } + _write_doc(SEED_OUT_PATH, seed_bundle) + + print(json.dumps({ + "status": "READY", + "message": "L0 structure seed reduce 완료: seed bundle/exception pack/audit 생성", + "seed_bundle_path": SEED_OUT_PATH, + "exception_pack_path": PACK_OUT_PATH, + "audit_path": AUDIT_OUT_PATH, + "seed_count": len(seed_items), + "has_exceptions": exception_pack["has_exceptions"], + "exception_count": exception_pack["exception_count"], + "deterministic_resolution_count": len(deterministic_resolutions), + "nonrouting_label_count": sum(len(v) for v in nonrouting_by_bo.values()), + }, ensure_ascii=False)) + except Exception as exc: + print(json.dumps({ + "status": "FAILED", + "message": "L0 structure seed reducer 실패", + "reason": str(exc), + }, ensure_ascii=False)) + raise + + + if __name__ == "__main__": + main() + - task_name: Task_L1_exception_adjudicator + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: low + llm_verbosity: low + max_iterations: 1 + use_tools: [localdocs] + cache_control: + mode: auto + ttl: 20m + preflight: true + preflight_files: + - stage1_tmp/legal_effect_structure_exception_pack.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + + TASK_NAME: Task_L1_exception_adjudicator + STAGE: Stage 1 legal effect structure/index builder — 조건부 예외 판정 (Part 1 v3 GB / Part 2 v3 R1 패턴) + MISSION: 결정론 reducer(L0)가 non-deferrable로 판정한 compact exception만 판정한다. 파일 병합·최종 파일 작성·사실 창작은 하지 않는다. + + + + - 유일한 입력은 preflight로 제공된 `stage1_tmp/legal_effect_structure_exception_pack.json`이다. + - seed bundle, BO.json, evidence, signal 파일, raw 원문을 읽거나 요청하지 않는다. + - pack에 없는 exception_id·BO ID·evidence index·structure/cluster ID·사실·당사자·일자·금액을 창작하지 않는다. + - `legal_effect_structures.json`·seed bundle·audit 파일을 작성하지 않는다. + - 출력 파일은 `stage1_tmp/legal_effect_structure_adjudication.json` 하나뿐이다. + - markdown fence·prose 없이 raw JSON만 파일에 쓴다. + + + + 1. preflight로 제공된 exception pack의 `has_exceptions`를 확인한다. + 2. `has_exceptions == false`이면: `write_file(overwrite=true)`로 아래 no-exception 객체를 `stage1_tmp/legal_effect_structure_adjudication.json`에 저장하고 `"NO_EXCEPTIONS"`만 출력한 뒤 즉시 종료한다(terminate). 다른 어떤 파일도 읽지 않는다. + {"legal_structure_exception_adjudication": {"schema_version": "stage1_legal_structure_exception_adjudication.v1", "status": "READY_NO_EXCEPTIONS", "source_exception_pack_schema_version": "stage1_legal_structure_exception_pack.v2", "decisions": [], "blocked_review_items": [], "adjudication_gate": {"input_exception_count": 0, "decision_count": 0, "blocked_count": 0, "coverage_pass": true, "json_only_contract_pass": true}}} + 3. `has_exceptions == true`이면: `exceptions[]`의 각 항목을 해당 항목의 `compact_context`·`candidate_values`·`basis_fields`·`score_summary`·`conflict_flags`만으로 판정한다. 추가 read는 금지된다. + 4. 판정 규칙: + - `ambiguous_issue_cluster`: `USE_EXISTING_HARD_KEY`, `SELECT_ISSUE_FAMILY`, `SPLIT_ISSUE_CLUSTER`, `BLOCK_REVIEW` 중 하나. + - `ambiguous_asset_cluster`: `SAME_CLUSTER`, `SEPARATE_CLUSTER`, `MULTI_ASSET_BUNDLE`, `BLOCK_REVIEW` 중 하나. 자산 동일성은 등기행·별지목록 ref가 자유문구보다 우선한다. 병합이 권리객체를 훼손할 수 있으면 SEPARATE_CLUSTER 또는 BLOCK_REVIEW. + - `ambiguous_candidate_structure_type`: `SELECT_REPRESENTATIVE_TYPE`, `PRESERVE_MULTI_ROUTING`, `SPLIT_STRUCTURE_SEED`, `BLOCK_REVIEW` 중 하나. 다중 라우팅 보존이 기본적으로 안전하다. + - 각 항목의 `allowed_decisions` 안에서만 선택한다. `selected_value`/`selected_values`/`preserve_candidate_values`/`split_plan`의 값은 반드시 해당 항목의 `candidate_values` 부분집합이어야 한다. + - compact 근거로 확정할 수 없으면 반드시 `BLOCK_REVIEW`로 판정하고 같은 항목을 `blocked_review_items`에 넣는다(확신 없는 확정 금지 — 인간 검토 라우팅). + 5. `write_file(overwrite=true)`로 결과를 저장한다. 루트는 `legal_structure_exception_adjudication`, `schema_version`은 `stage1_legal_structure_exception_adjudication.v1`, `status`는 전 항목 비차단이면 `READY`, 차단 존재 시 `READY_WITH_BLOCKS`다. decision 항목 필드: `exception_id`, `exception_type`, `source_bo_ids`, `decision`, `selected_value|null`, `selected_values[]`, `preserve_candidate_values[]`, `split_plan[]`(split일 때만), `review_required`, `reason_code`, `basis_fields_used[]`, `conflict_flags_acknowledged[]`. 모든 decision은 pack의 `exception_id`를 인용하며, 각 exception마다 정확히 하나의 decision을 만든다. + 6. `"L1 예외 판정 완료 (decisions=<건수>)"`만 출력하고 작업을 끝낸다(terminate). + + + + - Stage 1은 법률효과·청구원인을 확정하지 않는다. 여기 도달한 항목은 final writer가 단일 join 값을 선택해야만 진행되는 사안뿐이다. + - 하나의 라벨이 서로 다른 등기행/별지목록 ref 계열을 주장하는 경우: 원문 ref 근거가 더 구체적인 쪽을 선택하고, 우열을 가릴 수 없으면 BLOCK_REVIEW. + - SEPARATE_CLUSTER(보존)가 권리객체를 지키는 기본값이다. SAME_CLUSTER 병합은 ref 근거가 무손실로 합치될 때만 선택한다. + - 판정은 라우팅/조인 키 결정일 뿐 소유권·담보권·평가액의 실체 판단이 아니다. + + + + - exception pack 부재·파싱 불가: 즉시 중단하고 채팅으로만 보고한다. adjudication 파일은 쓰지 않는다. + - tool 오류: 1회만 재시도. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + - task_name: Task_L2_final_structure_index_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import sys + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + SEED_PATH = "stage1_tmp/legal_effect_structure_seed_bundle.json" + PACK_PATH = "stage1_tmp/legal_effect_structure_exception_pack.json" + ADJUDICATION_PATH = "stage1_tmp/legal_effect_structure_adjudication.json" + AUDIT_PATH = "stage1_tmp/legal_effect_structure_audit.json" + TARGET_NAME = "legal_effect_structures.json" + STRUCTURE_TYPE_ROLE = { + "money_claim": "claim_chain_ref", + "commercial_successor": "claim_chain_ref", + "secured_debt": "linked_structures", + "registry_invalidity": "linked_structures", + "land_use_gain": "legal_calculation_object", + "valuation": "legal_calculation_object", + "fraudulent_transfer": "actio_roles", + "preserved_claim": "claim_chain_ref", + "actio_remedy_or_cap": "linked_actio_structures", + "succession_notice_lien_asset_defense": "linked_structures", + "general_legal_effect": "legal_effect_roles", + } + ISSUE_DECISIONS = {"USE_EXISTING_HARD_KEY", "SELECT_ISSUE_FAMILY", "SPLIT_ISSUE_CLUSTER", "BLOCK_REVIEW"} + ASSET_DECISIONS = {"SAME_CLUSTER", "SEPARATE_CLUSTER", "MULTI_ASSET_BUNDLE", "BLOCK_REVIEW"} + TYPE_DECISIONS = {"SELECT_REPRESENTATIVE_TYPE", "PRESERVE_MULTI_ROUTING", "SPLIT_STRUCTURE_SEED", "BLOCK_REVIEW"} + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-legal-effect-l2-final-structure-index-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + + def _lenient_json(raw: str, label: str) -> Any: + """SKILL §6.4/§6.7: strict 실패 시 코드펜스 제거 + raw_decode salvage.""" + try: + return json.loads(raw) + except json.JSONDecodeError: + text = raw.strip() + if text.startswith("```"): + first_newline = text.find("\n") + if first_newline >= 0: + text = text[first_newline + 1:] + if text.rstrip().endswith("```"): + text = text.rstrip()[:-3] + start = text.find("{") + if start < 0: + raise RuntimeError(f"Content of {label} is not valid JSON: {raw[:300]}") + try: + obj, _ = JSON_DECODER.raw_decode(text[start:].strip()) + return obj + except json.JSONDecodeError as exc: + raise RuntimeError(f"Content of {label} is not valid JSON: {raw[:300]}") from exc + + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result.get("result", {}).get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + text = content[0].get("text", "") + envelope = _lenient_json(text, doc_name) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + item = results[0] + if isinstance(item, dict) and item.get("error"): + raise RuntimeError(f"read_docs error for {doc_name}: {item.get('error')}") + raw = item.get("content", "") + else: + raw = envelope + if isinstance(raw, str): + return _lenient_json(raw, doc_name) + return raw + + + def _read_doc(doc_name: str) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + return _extract_json_from_read_docs_result(result, doc_name) + + + def _write_doc(doc_name: str, payload: dict[str, Any]) -> None: + content = json.dumps(payload, ensure_ascii=False, indent=2) + "\n" + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=_next_msg_id()) + + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _strings(value: Any) -> list[str]: + if value is None: + return [] + items = value if isinstance(value, list) else [value] + out: list[str] = [] + for item in items: + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + if value is None: + continue + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + + def _make_id(prefix: str, ordinal: int) -> str: + return f"{prefix}-{ordinal:03d}" + + + # ---------------------------------------------------------------- 입력 적재 (파일 채널 일원화 — X-9) + def _load_seed_bundle() -> dict[str, Any]: + outer = _read_doc(SEED_PATH) + root = _dict(_dict(outer).get("legal_structure_seed_bundle")) + if root.get("schema_version") != "stage1_legal_structure_seed_bundle.v2": + raise ValueError("legal_structure_seed_bundle schema mismatch (v2 required)") + if root.get("status") != "READY": + raise ValueError("legal_structure_seed_bundle status invalid") + return root + + + def _load_exception_pack(seed_bundle: dict[str, Any]) -> dict[str, Any]: + ref = _dict(seed_bundle.get("exception_pack_ref")) + if ref.get("path") != PACK_PATH: + raise ValueError("exception_pack_ref path mismatch") + result = _call_tool("read_docs", {"doc_names": [PACK_PATH]}, msg_id=_next_msg_id()) + content = result.get("result", {}).get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {PACK_PATH}") + envelope = _lenient_json(content[0].get("text", ""), PACK_PATH) + raw_text = None + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if results and isinstance(results[0], dict): + raw_text = results[0].get("content") + pack = _lenient_json(raw_text, PACK_PATH) if isinstance(raw_text, str) else (raw_text or envelope) + pack = _dict(pack) + if pack.get("schema_version") != "stage1_legal_structure_exception_pack.v2": + raise ValueError("exception pack schema mismatch (v2 required)") + if isinstance(raw_text, str) and ref.get("sha256"): + digest = hashlib.sha256(raw_text.encode("utf-8")).hexdigest() + if digest != ref.get("sha256"): + raise ValueError("exception pack sha256 mismatch against seed bundle ref") + expected_ids = _strings(ref.get("expected_exception_ids")) + pack_ids = [str(_dict(e).get("exception_id")) for e in _list(pack.get("exceptions"))] + if sorted(expected_ids) != sorted(pack_ids): + raise ValueError("exception pack ids mismatch against seed bundle ref") + return pack + + + def _load_adjudication() -> dict[str, Any]: + outer = _read_doc(ADJUDICATION_PATH) + root = _dict(_dict(outer).get("legal_structure_exception_adjudication")) + if root.get("schema_version") != "stage1_legal_structure_exception_adjudication.v1": + raise ValueError("legal_structure_exception_adjudication schema mismatch") + if root.get("status") not in {"READY", "READY_NO_EXCEPTIONS", "READY_WITH_BLOCKS"}: + raise ValueError("legal_structure_exception_adjudication status is not accepted") + return root + + + # ---------------------------------------------------------------- 판정 검증 (v1 LES3 이식 + coverage 등식) + def _expected_exceptions(pack: dict[str, Any]) -> dict[str, dict[str, Any]]: + out: dict[str, dict[str, Any]] = {} + for item in _list(pack.get("exceptions")): + if not isinstance(item, dict) or not item.get("exception_id"): + raise ValueError("exception item without exception_id in pack") + out[str(item["exception_id"])] = item + return out + + + def _validate_decision_enum(decision: dict[str, Any]) -> None: + exception_type = decision.get("exception_type") + choice = decision.get("decision") + allowed = { + "ambiguous_issue_cluster": ISSUE_DECISIONS, + "ambiguous_asset_cluster": ASSET_DECISIONS, + "ambiguous_candidate_structure_type": TYPE_DECISIONS, + }.get(exception_type) + if allowed is None: + raise ValueError(f"invalid exception_type: {exception_type}") + if choice not in allowed: + raise ValueError(f"invalid decision {choice} for {exception_type}") + + + def _validate_decision_subsets(decision: dict[str, Any], expected: dict[str, Any]) -> None: + candidates = set(_strings(expected.get("candidate_values"))) + basis = set(_strings(expected.get("basis_fields"))) + flags = set(_strings(expected.get("conflict_flags"))) + for field in ["selected_values", "preserve_candidate_values"]: + extras = set(_strings(decision.get(field))) - candidates + if extras: + raise ValueError(f"{decision.get('exception_id')}: {field} contains values outside candidate_values: {sorted(extras)}") + selected = decision.get("selected_value") + if selected not in (None, "") and candidates and str(selected) not in candidates: + raise ValueError(f"{decision.get('exception_id')}: selected_value outside candidate_values") + for split in _list(decision.get("split_plan")): + if isinstance(split, dict): + extras = set(_strings(split.get("candidate_values"))) - candidates + if extras: + raise ValueError(f"{decision.get('exception_id')}: split_plan candidate outside candidate_values") + if basis: + extras = set(_strings(decision.get("basis_fields_used"))) - basis + if extras: + raise ValueError(f"{decision.get('exception_id')}: basis_fields_used outside basis_fields") + if flags: + extras = set(_strings(decision.get("conflict_flags_acknowledged"))) - flags + if extras: + raise ValueError(f"{decision.get('exception_id')}: conflict_flags_acknowledged outside conflict_flags") + + + def _validate_decisions(pack: dict[str, Any], adjudication: dict[str, Any]) -> tuple[dict[str, dict[str, Any]], list[dict[str, Any]], set[str], set[str]]: + """coverage 등식(X-5): expected == resolved ∪ blocked. 반환: (decisions, blocked, resolved_ids, blocked_ids).""" + expected = _expected_exceptions(pack) + decisions: dict[str, dict[str, Any]] = {} + blocked = _list(adjudication.get("blocked_review_items")) + if not expected: + if adjudication.get("status") != "READY_NO_EXCEPTIONS": + raise ValueError("no exceptions expected; adjudication status must be READY_NO_EXCEPTIONS") + if _list(adjudication.get("decisions")): + raise ValueError("no exceptions expected; decisions must be empty") + return decisions, [], set(), set() + for decision in _list(adjudication.get("decisions")): + if not isinstance(decision, dict) or not decision.get("exception_id"): + raise ValueError("decision item missing exception_id") + exception_id = str(decision["exception_id"]) + if exception_id not in expected: + raise ValueError(f"decision references unknown exception_id: {exception_id}") + if exception_id in decisions: + raise ValueError(f"duplicate decision for exception_id: {exception_id}") + _validate_decision_enum(decision) + _validate_decision_subsets(decision, expected[exception_id]) + decisions[exception_id] = decision + missing = sorted(set(expected) - set(decisions)) + if missing: + raise ValueError(f"adjudication missing decisions: {missing}") + blocked_ids = {str(item.get("exception_id")) for item in blocked if isinstance(item, dict)} + for exception_id, decision in decisions.items(): + if decision.get("decision") == "BLOCK_REVIEW" and exception_id not in blocked_ids: + blocked.append({ + "exception_id": exception_id, + "exception_type": decision.get("exception_type"), + "source_bo_ids": _strings(decision.get("source_bo_ids")), + "block_reason": "block_review_decision_without_block_item", + }) + blocked_ids.add(exception_id) + resolved_ids = {eid for eid, d in decisions.items() if d.get("decision") != "BLOCK_REVIEW"} + if resolved_ids | blocked_ids != set(expected): + raise ValueError("coverage equation failed: expected != resolved ∪ blocked") + return decisions, blocked, resolved_ids, blocked_ids + + + # ---------------------------------------------------------------- 구조 빌더 (v1 LES3 이식 + X-5/X-6) + def _decisions_for_bo(decisions: dict[str, dict[str, Any]], bo_id: str, exception_type: str) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for decision in decisions.values(): + if decision.get("exception_type") == exception_type and bo_id in _strings(decision.get("source_bo_ids")): + out.append(decision) + return out + + + def _issue_ids_for_item(item: dict[str, Any], decisions: dict[str, dict[str, Any]], issue_key_ids: dict[str, str]) -> list[str]: + bo_id = _strings(item.get("source_bo_ids"))[0] if _strings(item.get("source_bo_ids")) else "" + issue_decisions = _decisions_for_bo(decisions, bo_id, "ambiguous_issue_cluster") + keys: list[str] = [] + for decision in issue_decisions: + choice = decision.get("decision") + if choice == "BLOCK_REVIEW": + continue + if choice == "SPLIT_ISSUE_CLUSTER": + for split in _list(decision.get("split_plan")): + if isinstance(split, dict): + label = split.get("split_label") or "|".join(_strings(split.get("candidate_values"))) + if label: + keys.append(f"split_issue:{label}") + else: + value = decision.get("selected_value") or (_strings(decision.get("selected_values"))[0] if _strings(decision.get("selected_values")) else None) + if value: + keys.append(f"adjudicated_issue:{value}") + if not keys: + keys = _strings(item.get("issue_cluster_ids")) + out: list[str] = [] + for key in keys: + if key not in issue_key_ids: + issue_key_ids[key] = _make_id("IC", len(issue_key_ids) + 1) + out.append(issue_key_ids[key]) + return _dedupe(out) + + + def _asset_ids_for_item(item: dict[str, Any], decisions: dict[str, dict[str, Any]], asset_key_ids: dict[str, str]) -> tuple[list[str], list[str]]: + bo_id = _strings(item.get("source_bo_ids"))[0] if _strings(item.get("source_bo_ids")) else "" + asset_decisions = _decisions_for_bo(decisions, bo_id, "ambiguous_asset_cluster") + keys = _strings(item.get("asset_cluster_ids")) + bundle_refs = _strings(item.get("multi_asset_bundle_refs")) + for decision in asset_decisions: + choice = decision.get("decision") + if choice == "BLOCK_REVIEW": + continue + if choice == "SAME_CLUSTER": + value = decision.get("selected_value") or "|".join(_strings(decision.get("selected_values")) or _strings(decision.get("preserve_candidate_values"))) + if value: + keys = [f"same_asset:{value}"] + elif choice == "SEPARATE_CLUSTER": + vals = _strings(decision.get("selected_values")) or _strings(decision.get("preserve_candidate_values")) or _strings(decision.get("candidate_values")) + keys = [f"separate_asset:{value}" for value in vals] or keys + elif choice == "MULTI_ASSET_BUNDLE": + value = decision.get("selected_value") or f"bundle:{bo_id}" + if value not in bundle_refs: + bundle_refs.append(str(value)) + out: list[str] = [] + for key in keys: + if key not in asset_key_ids: + asset_key_ids[key] = _make_id("AC", len(asset_key_ids) + 1) + out.append(asset_key_ids[key]) + return _dedupe(out), _dedupe(bundle_refs) + + + def _type_plan_for_item(item: dict[str, Any], decisions: dict[str, dict[str, Any]]) -> list[dict[str, Any]]: + bo_id = _strings(item.get("source_bo_ids"))[0] if _strings(item.get("source_bo_ids")) else "" + original_types = _strings(item.get("candidate_structure_types")) or _strings(item.get("representative_candidate_structure_type")) or ["general_legal_effect"] + representative = item.get("representative_candidate_structure_type") or original_types[0] + type_decisions = _decisions_for_bo(decisions, bo_id, "ambiguous_candidate_structure_type") + if not type_decisions: + return [{"candidate_structure_type": representative, "candidate_structure_types": original_types, "split_label": None}] + plans: list[dict[str, Any]] = [] + for decision in type_decisions: + choice = decision.get("decision") + if choice == "BLOCK_REVIEW": + plans.append({"candidate_structure_type": representative, "candidate_structure_types": original_types, "split_label": None}) + elif choice == "SELECT_REPRESENTATIVE_TYPE": + selected = decision.get("selected_value") or (_strings(decision.get("selected_values"))[0] if _strings(decision.get("selected_values")) else representative) + plans.append({"candidate_structure_type": selected, "candidate_structure_types": _dedupe([selected] + original_types), "split_label": None}) + elif choice == "PRESERVE_MULTI_ROUTING": + vals = _strings(decision.get("selected_values")) or _strings(decision.get("preserve_candidate_values")) or original_types + selected = decision.get("selected_value") or vals[0] + plans.append({"candidate_structure_type": selected, "candidate_structure_types": vals, "split_label": None}) + elif choice == "SPLIT_STRUCTURE_SEED": + for split in _list(decision.get("split_plan")): + if isinstance(split, dict): + vals = _strings(split.get("candidate_values")) or original_types + plans.append({ + "candidate_structure_type": vals[0], + "candidate_structure_types": vals, + "split_label": split.get("split_label"), + }) + return plans or [{"candidate_structure_type": representative, "candidate_structure_types": original_types, "split_label": None}] + + + def _legal_roles(types: list[str]) -> list[str]: + return _dedupe([STRUCTURE_TYPE_ROLE.get(typ, "legal_effect_roles") for typ in types]) + + + def _basis(item: dict[str, Any], types: list[str]) -> dict[str, Any]: + signal_files: list[str] = [] + if _strings(item.get("linked_actio_signal_ids")): + signal_files.append("actio_case_signals.json") + if _strings(item.get("linked_claim_group_ids")) or _strings(item.get("linked_liability_group_ids")): + signal_files.append("case_liability_signals.json") + if types: + signal_files.append("legal_effect_signals.json") + return { + "primary_fields": _strings(item.get("basis_fields")), + "source_signal_files": _dedupe(signal_files), + "source_evidence_indexes": _strings(item.get("evidence_indexes")), + } + + + def _structure_label(candidate_type: str, source_bo_ids: list[str], split_label: Any) -> str: + label = candidate_type.replace("_", " ") + suffix = f" / {split_label}" if split_label else "" + return f"{label} structure for {', '.join(source_bo_ids)}{suffix}" + + + def _build_structures(seed_bundle: dict[str, Any], decisions: dict[str, dict[str, Any]], + blocked_ids: set[str], resolved_ids: set[str]) -> list[dict[str, Any]]: + issue_key_ids: dict[str, str] = {} + asset_key_ids: dict[str, str] = {} + structures: list[dict[str, Any]] = [] + for item in _list(seed_bundle.get("structure_seed_items")): + if not isinstance(item, dict): + continue + source_bo_ids = _strings(item.get("source_bo_ids")) + if not source_bo_ids: + continue + pending = _strings(item.get("pending_exception_ids")) + review_codes = _strings(item.get("deterministic_review_codes")) + item_blocked = sorted(set(pending) & blocked_ids) + item_resolved = sorted(set(pending) & resolved_ids) + issue_ids = _issue_ids_for_item(item, decisions, issue_key_ids) + asset_ids, bundle_refs = _asset_ids_for_item(item, decisions, asset_key_ids) + type_plans = _type_plan_for_item(item, decisions) + for plan in type_plans: + structure_id = _make_id("LES", len(structures) + 1) + types = _strings(plan.get("candidate_structure_types")) or ["general_legal_effect"] + # X-5: 해소된 판정은 review를 발생시키지 않는다 — blocked/deterministic review/split만 review + review_required = bool(item_blocked or review_codes or plan.get("split_label")) + structures.append({ + "structure_id": structure_id, + "candidate_structure_type": str(plan.get("candidate_structure_type") or types[0]), + "candidate_structure_types": types, + "structure_label": _structure_label(str(plan.get("candidate_structure_type") or types[0]), source_bo_ids, plan.get("split_label")), + "source_bo_ids": source_bo_ids, + "issue_cluster_ids": issue_ids, + "asset_cluster_ids": asset_ids, + "evidence_indexes": _strings(item.get("evidence_indexes")), + "linked_claim_group_ids": _strings(item.get("linked_claim_group_ids")), + "linked_liability_group_ids": _strings(item.get("linked_liability_group_ids")), + "linked_actio_signal_ids": _strings(item.get("linked_actio_signal_ids")), + "legal_effect_roles": _legal_roles(types), + "basis": _basis(item, types), + "review_required": review_required, + "pending_exception_state": { + "resolved": item_resolved, + "blocked": item_blocked, + "deterministic_review_codes": review_codes, + }, + "routing_audit": { + "source_seed_id": item.get("seed_id"), + "candidate_structure_types": types, + "multi_asset_bundle_refs": bundle_refs, + }, + }) + return structures + + + def _build_structure_index(structures: list[dict[str, Any]], seed_bundle: dict[str, Any]) -> dict[str, Any]: + by_issue: dict[str, Any] = {} + by_asset: dict[str, Any] = {} + by_evidence: dict[str, Any] = {} + by_type: dict[str, Any] = {} + by_bo: dict[str, Any] = {} + issue_seed_map = _dict(seed_bundle.get("issue_cluster_seed_map")) + asset_seed_map = _dict(seed_bundle.get("asset_cluster_seed_map")) + evidence_seed = _dict(seed_bundle.get("evidence_index_seed")) + + for structure in structures: + sid = structure["structure_id"] + basis = _dict(structure.get("basis")) + routing_audit = _dict(structure.get("routing_audit")) + all_types = _strings(structure.get("candidate_structure_types")) or [structure["candidate_structure_type"]] + for bo_id in _strings(structure.get("source_bo_ids")): + row = by_bo.setdefault(bo_id, { + "bo_id": bo_id, + "structure_ids": [], + "structure_labels": [], + "issue_cluster_ids": [], + "asset_cluster_ids": [], + "evidence_indexes": [], + "candidate_structure_types": [], + "claim_group_ids": [], + "liability_group_ids": [], + "related_actio_signal_ids": [], + "legal_effect_roles": [], + "basis_fields": [], + "multi_asset_bundle_refs": [], + "review_required": False, + }) + row["structure_ids"] = _dedupe(row["structure_ids"] + [sid]) + row["structure_labels"] = _dedupe(row["structure_labels"] + _strings(structure.get("structure_label"))) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + structure["issue_cluster_ids"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + structure["asset_cluster_ids"]) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + structure["evidence_indexes"]) + # X-6: 대표 타입만이 아니라 canonical 전 타입을 인덱스에 반영 (additive) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + all_types) + row["claim_group_ids"] = _dedupe(row["claim_group_ids"] + structure["linked_claim_group_ids"]) + row["liability_group_ids"] = _dedupe(row["liability_group_ids"] + structure["linked_liability_group_ids"]) + row["related_actio_signal_ids"] = _dedupe(row["related_actio_signal_ids"] + structure["linked_actio_signal_ids"]) + row["legal_effect_roles"] = _dedupe(row["legal_effect_roles"] + structure["legal_effect_roles"]) + row["basis_fields"] = _dedupe(row["basis_fields"] + _strings(basis.get("primary_fields"))) + row["multi_asset_bundle_refs"] = _dedupe(row["multi_asset_bundle_refs"] + _strings(routing_audit.get("multi_asset_bundle_refs"))) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + for issue_id in _strings(structure.get("issue_cluster_ids")): + seed = _dict(issue_seed_map.get(issue_id)) + row = by_issue.setdefault(issue_id, { + "issue_cluster_id": issue_id, + "issue_type": seed.get("issue_type"), + "issue_label": seed.get("issue_key") or issue_id, + "source_bo_ids": [], + "candidate_structure_types": [], + "claim_group_ids": [], + "liability_group_ids": [], + "evidence_indexes": [], + "asset_cluster_ids": [], + "structure_ids": [], + "basis_fields": [], + "review_required": False, + }) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + structure["source_bo_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + all_types) + row["claim_group_ids"] = _dedupe(row["claim_group_ids"] + structure["linked_claim_group_ids"]) + row["liability_group_ids"] = _dedupe(row["liability_group_ids"] + structure["linked_liability_group_ids"]) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + structure["evidence_indexes"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + structure["asset_cluster_ids"]) + row["structure_ids"] = _dedupe(row["structure_ids"] + [sid]) + row["basis_fields"] = _dedupe(row["basis_fields"] + _strings(structure.get("basis", {}).get("primary_fields"))) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + for asset_id in _strings(structure.get("asset_cluster_ids")): + seed = _dict(asset_seed_map.get(asset_id)) + row = by_asset.setdefault(asset_id, { + "asset_cluster_id": asset_id, + "canonical_asset_label": seed.get("canonical_asset_label"), + "asset_kind": seed.get("asset_kind"), + "member_asset_mentions": _strings(seed.get("member_asset_mentions")), + "source_bo_ids": [], + "evidence_indexes": [], + "issue_cluster_ids": [], + "candidate_structure_types": [], + "structure_ids": [], + "basis_fields": _strings(seed.get("basis_fields")), + "review_required": bool(_strings(seed.get("deterministic_review_codes"))), + }) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + structure["source_bo_ids"]) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + structure["evidence_indexes"]) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + structure["issue_cluster_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + all_types) + row["structure_ids"] = _dedupe(row["structure_ids"] + [sid]) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + for evidence_index in _strings(structure.get("evidence_indexes")): + seed = _dict(evidence_seed.get(evidence_index)) + row = by_evidence.setdefault(evidence_index, { + "evidence_index": evidence_index, + "source_title": seed.get("source_title"), + "priority_class": seed.get("priority_class"), + "authentication_status": seed.get("authentication_status"), + "source_bo_ids": [], + "structure_ids": [], + "issue_cluster_ids": [], + "asset_cluster_ids": [], + "candidate_structure_types": [], + "related_event_candidate_ids": [], + "related_signal_refs": [], + "support_role": "primary", + "gap_flags": _strings(seed.get("gap_flags")), + }) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + structure["source_bo_ids"]) + row["structure_ids"] = _dedupe(row["structure_ids"] + [sid]) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + structure["issue_cluster_ids"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + structure["asset_cluster_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + all_types) + row["related_signal_refs"] = _dedupe(row["related_signal_refs"] + structure["linked_actio_signal_ids"] + structure["linked_claim_group_ids"] + structure["linked_liability_group_ids"]) + # X-6: by_candidate_structure_type을 전체 canonical 타입에 대해 생성 + for typ in all_types: + row = by_type.setdefault(typ, { + "candidate_structure_type": typ, + "structure_ids": [], + "source_bo_ids": [], + "issue_cluster_ids": [], + "asset_cluster_ids": [], + "evidence_indexes": [], + "claim_group_ids": [], + "liability_group_ids": [], + "related_actio_signal_ids": [], + "downstream_fact_roles": [], + "basis_fields": [], + "review_required": False, + }) + row["structure_ids"] = _dedupe(row["structure_ids"] + [sid]) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + structure["source_bo_ids"]) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + structure["issue_cluster_ids"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + structure["asset_cluster_ids"]) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + structure["evidence_indexes"]) + row["claim_group_ids"] = _dedupe(row["claim_group_ids"] + structure["linked_claim_group_ids"]) + row["liability_group_ids"] = _dedupe(row["liability_group_ids"] + structure["linked_liability_group_ids"]) + row["related_actio_signal_ids"] = _dedupe(row["related_actio_signal_ids"] + structure["linked_actio_signal_ids"]) + row["downstream_fact_roles"] = _dedupe(row["downstream_fact_roles"] + structure["legal_effect_roles"]) + row["basis_fields"] = _dedupe(row["basis_fields"] + _strings(structure.get("basis", {}).get("primary_fields"))) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + + return { + "by_bo_id": by_bo, + "by_issue_cluster_id": by_issue, + "by_asset_cluster_id": by_asset, + "by_evidence_index": by_evidence, + "by_candidate_structure_type": by_type, + } + + + def _quality_gate(structures: list[dict[str, Any]], seed_bundle: dict[str, Any], + blocked: list[dict[str, Any]]) -> dict[str, Any]: + seed_items = [item for item in _list(seed_bundle.get("structure_seed_items")) if isinstance(item, dict)] + seed_bo_ids = sorted({bo for item in seed_items for bo in _strings(item.get("source_bo_ids"))}) + covered_bo_ids = sorted({bo for structure in structures for bo in _strings(structure.get("source_bo_ids"))}) + seed_evidence = sorted({ev for item in seed_items for ev in _strings(item.get("evidence_indexes"))}) + covered_evidence = sorted({ev for structure in structures for ev in _strings(structure.get("evidence_indexes"))}) + review_queue = [] + for structure in structures: + if structure.get("review_required"): + review_queue.append({ + "review_type": "structure_review_required", + "structure_id": structure["structure_id"], + "source_bo_ids": structure["source_bo_ids"], + }) + for item in blocked: + if isinstance(item, dict): + review_queue.append({ + "review_type": "blocked_exception", + "exception_id": item.get("exception_id"), + "exception_type": item.get("exception_type"), + "source_bo_ids": _strings(item.get("source_bo_ids")), + "block_reason": item.get("block_reason"), + }) + # X-5: 자동차단 추적성 복원 — deterministic review 항목을 식별 가능한 큐 항목으로 남긴다 + for item in seed_items: + codes = _strings(item.get("deterministic_review_codes")) + if codes: + review_queue.append({ + "review_type": "deterministic_review", + "seed_id": item.get("seed_id"), + "source_bo_ids": _strings(item.get("source_bo_ids")), + "codes": codes, + }) + return { + "bo_coverage": { + "source_bo_count": len(seed_bo_ids), + "covered_bo_count": len(covered_bo_ids), + "missing_bo_ids": sorted(set(seed_bo_ids) - set(covered_bo_ids)), + "coverage_pass": set(seed_bo_ids) == set(covered_bo_ids), + }, + "evidence_coverage": { + "source_evidence_count": len(seed_evidence), + "covered_evidence_count": len(covered_evidence), + "missing_evidence_indexes": sorted(set(seed_evidence) - set(covered_evidence)), + "coverage_pass": set(seed_evidence).issubset(set(covered_evidence)), + }, + "review_queue": review_queue, + } + + + def _validate_payload(payload: dict[str, Any]) -> None: + if payload.get("schema_version") != "stage1_legal_effect_structures.v1": + raise ValueError("final payload schema_version mismatch") + if payload.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("final payload status invalid") + structures = _list(payload.get("legal_effect_structures")) + index = _dict(payload.get("structure_index")) + for key in ["by_bo_id", "by_issue_cluster_id", "by_asset_cluster_id", "by_evidence_index", "by_candidate_structure_type"]: + if not isinstance(index.get(key), dict): + raise ValueError(f"structure_index.{key} must be object") + structure_ids = {str(item.get("structure_id")) for item in structures if isinstance(item, dict)} + if len(structure_ids) != len(structures): + raise ValueError("structure_id values must be unique") + for item in structures: + if not isinstance(item, dict): + raise ValueError("legal_effect_structures item must be object") + for field in [ + "structure_id", "candidate_structure_type", "candidate_structure_types", "structure_label", + "source_bo_ids", "issue_cluster_ids", "asset_cluster_ids", "evidence_indexes", + "linked_claim_group_ids", "linked_liability_group_ids", "linked_actio_signal_ids", + "legal_effect_roles", "basis", "review_required", "pending_exception_state", + ]: + if field not in item: + raise ValueError(f"structure item missing {field}") + for bo_id in _strings(item.get("source_bo_ids")): + bo_row = _dict(index["by_bo_id"].get(bo_id)) + if not bo_row: + raise ValueError(f"missing BO index for {bo_id}") + if item.get("structure_id") not in _strings(bo_row.get("structure_ids")): + raise ValueError(f"BO index for {bo_id} does not reference {item.get('structure_id')}") + for issue_id in _strings(item.get("issue_cluster_ids")): + if issue_id not in index["by_issue_cluster_id"]: + raise ValueError(f"missing issue index for {issue_id}") + for asset_id in _strings(item.get("asset_cluster_ids")): + if asset_id not in index["by_asset_cluster_id"]: + raise ValueError(f"missing asset index for {asset_id}") + for evidence_index in _strings(item.get("evidence_indexes")): + if evidence_index not in index["by_evidence_index"]: + raise ValueError(f"missing evidence index for {evidence_index}") + for typ in _strings(item.get("candidate_structure_types")) or [item.get("candidate_structure_type")]: + if typ not in index["by_candidate_structure_type"]: + raise ValueError(f"missing candidate type index for {typ}") + gate = _dict(payload.get("quality_gate")) + if not _dict(gate.get("bo_coverage")).get("coverage_pass"): + raise ValueError("BO coverage failed") + if not _dict(gate.get("evidence_coverage")).get("coverage_pass"): + raise ValueError("evidence coverage failed") + + + def _update_audit(resolved_ids: set[str], blocked_ids: set[str], decisions: dict[str, dict[str, Any]]) -> None: + try: + audit = _dict(_read_doc(AUDIT_PATH)) + except Exception: + audit = {"schema_version": "stage1_legal_structure_audit.v1"} + trace = _list(audit.get("review_reduction_trace")) + for eid in sorted(resolved_ids): + decision = _dict(decisions.get(eid)) + trace.append({ + "exception_id": eid, + "resolution": "llm_adjudicated", + "decision": decision.get("decision"), + "reason_code": decision.get("reason_code"), + }) + for eid in sorted(blocked_ids): + trace.append({"exception_id": eid, "resolution": "blocked_to_review"}) + audit["review_reduction_trace"] = trace + _write_doc(AUDIT_PATH, audit) + + + def main() -> None: + try: + _init_mcp_session() + seed_bundle = _load_seed_bundle() + pack = _load_exception_pack(seed_bundle) + adjudication = _load_adjudication() + decisions, blocked, resolved_ids, blocked_ids = _validate_decisions(pack, adjudication) + structures = _build_structures(seed_bundle, decisions, blocked_ids, resolved_ids) + if not structures: + raise ValueError("no legal effect structures generated") + structure_index = _build_structure_index(structures, seed_bundle) + quality_gate = _quality_gate(structures, seed_bundle, blocked) + status = "READY_WITH_REVIEW" if quality_gate["review_queue"] else "READY" + payload = { + "schema_version": "stage1_legal_effect_structures.v1", + "status": status, + "legal_effect_structures": structures, + "structure_index": structure_index, + "quality_gate": quality_gate, + } + _validate_payload(payload) + _write_doc(TARGET_NAME, payload) + reread = _read_doc(TARGET_NAME) + if not isinstance(reread, dict) or reread.get("schema_version") != "stage1_legal_effect_structures.v1": + raise ValueError("post-write reread validation failed") + _update_audit(resolved_ids, blocked_ids, decisions) + print(json.dumps({ + "status": "READY", + "message": "legal_effect_structures.json 작성 완료", + "written_file": TARGET_NAME, + "payload_status": status, + "structure_count": len(structures), + "review_queue_count": len(quality_gate["review_queue"]), + "resolved_exception_count": len(resolved_ids), + "blocked_exception_count": len(blocked_ids), + }, ensure_ascii=False)) + except Exception as exc: + print(json.dumps({ + "status": "FAILED", + "message": "L2 final structure index writer 실패", + "target_file": TARGET_NAME, + "reason": str(exc), + }, ensure_ascii=False)) + raise + + + if __name__ == "__main__": + main() diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Claude YAML/Stage_1_Part_4_Claude_v3.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Claude YAML/Stage_1_Part_4_Claude_v3.yml new file mode 100644 index 00000000..8e5d71a1 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Claude YAML/Stage_1_Part_4_Claude_v3.yml @@ -0,0 +1,1963 @@ +--- +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_4 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Fact Ledger Generation + version: v3.0-claude + Stages: + - name: stage1_fact_ledger_generation + description: BO, Legal Effect 사용해서 Fact_Ledger_base.json 생성 + tools: + mcpServers: + localdocs: + type: streamable-http + url: http://mcp-localdocs:8012/mcp + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + task_procedure: + IN: + nexts: + - Task_F0_fact_ledger_reducer + wait_until: [] + Task_F0_fact_ledger_reducer: + nexts: + - Task_F1_fact_exception_adjudicator + wait_until: + - IN + Task_F1_fact_exception_adjudicator: + nexts: + - Task_F2_final_fact_ledger_gate_and_writer + wait_until: + - Task_F0_fact_ledger_reducer + Task_F2_final_fact_ledger_gate_and_writer: + nexts: + - OUT + wait_until: + - Task_F1_fact_exception_adjudicator + OUT: + nexts: [] + wait_until: + - Task_F2_final_fact_ledger_gate_and_writer + + tasks: + - task_name: Task_F0_fact_ledger_reducer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + import sys + from copy import deepcopy + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + # X-4: evidence_all.json / evidence_event_candidates.json / client_goal.json은 기여 0 실측으로 제거. + INPUT_FILES = { + "bo": "BO.json", + "legal_effect_structures": "legal_effect_structures.json", + "actio_case_signals": "actio_case_signals.json", + "case_liability_signals": "case_liability_signals.json", + "legal_effect_signals": "legal_effect_signals.json", + "evidence_indexed": "evidence_indexed.json", + } + REMOVED_INPUTS = ["evidence_all.json", "evidence_event_candidates.json", "client_goal.json"] + CANDIDATE_OUT_PATH = "stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json" + PACK_OUT_PATH = "stage1_tmp/fact_ledger/fact_exception_pack.json" + MAX_TEXT = 900 + MAX_SIGNAL_ITEMS_PER_BO = 12 + # X-3 방어 상한 (자동 차단 금지 — 초과분은 exception_id 유지한 채 deterministic review로 라우팅) + PACK_MAX_ITEMS = 30 + PACK_MAX_BYTES = 40000 + PACK_ITEM_CHAR_LIMIT = 2000 + FINAL_FIELDS = [ + "fact_id", "source_bo_id", "type", "date", "parties", "object_spec", "amount", "action", + "evidence_refs", "credibility", "legal_centrality", "proof_strength", "legal_effect_roles", + "claim_chain_ref", "linked_structures", "must_not_drop_in_claim_types", "must_consider", + "state_context", "money_claim_effect", "commercial_successor_effect", "actio_roles", + "linked_actio_structures", "downstream_module_candidates", "skeleton_validation_codes", + "derived_fact", "derivation_basis", "legal_calculation_object", + ] + STRUCTURE_TYPE_TO_MODULE = { + "money_claim": "money_claim", + "commercial_successor": "commercial_successor", + "secured_debt": "secured_property", + "registry_invalidity": "registry_invalidity", + "land_use_gain": "land_use_gain", + "valuation": "valuation", + "fraudulent_transfer": "actio", + "preserved_claim": "actio", + "succession_notice_lien_asset_defense": "succession_notice_lien_asset_defense", + "general_legal_effect": "legal_effect", + } + STRUCTURE_TYPE_TO_ROLE = { + "money_claim": "claim_chain_ref", + "commercial_successor": "claim_chain_ref", + "secured_debt": "linked_structures", + "registry_invalidity": "linked_structures", + "land_use_gain": "legal_calculation_object", + "valuation": "legal_calculation_object", + "fraudulent_transfer": "actio_roles", + "preserved_claim": "claim_chain_ref", + "succession_notice_lien_asset_defense": "linked_structures", + "general_legal_effect": "legal_effect_roles", + } + PACK_EXCEPTION_TYPES = [ + "legal_calculation_required", + "unresolved_asset_cluster_projection", + "unresolved_structure_role_projection", + ] + REVIEW_CODE_BY_EXCEPTION_TYPE = { + "legal_calculation_required": "CALCULATION_BASIS_INCOMPLETE_REVIEW", + "unresolved_asset_cluster_projection": "ASSET_CLUSTER_PROJECTION_GAP_REVIEW", + "unresolved_structure_role_projection": "STRUCTURE_ROLE_PROJECTION_GAP_REVIEW", + } + DOMAIN_PAYLOAD_KEEP_KEYS = [ + "money_claim_details", "possession_state", "lien_state", + "notice_lifecycle", "asset_alias", "fractional_effect_events", + ] + POSSESSION_INCOMPATIBLE_CAUSE_PAIRS = [ + ("유치권행사중", "무권원주장"), + ] + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-fact-ledger-f0-reducer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + + def _lenient_json(raw: str, label: str) -> Any: + """SKILL §6.4/§6.7: strict 실패 시 raw_decode fallback (envelope 계층 포함 — X-9).""" + text = (raw or "").strip() + if not text: + raise RuntimeError(f"Empty content for {label}") + try: + return json.loads(text) + except json.JSONDecodeError: + start = text.find("{") + alt = text.find("[") + if alt >= 0 and (start < 0 or alt < start): + start = alt + if start < 0: + raise RuntimeError(f"Content of {label} is not valid JSON: {text[:300]}") + try: + obj, _ = JSON_DECODER.raw_decode(text[start:]) + return obj + except json.JSONDecodeError as exc: + raise RuntimeError(f"Content of {label} is not valid JSON: {text[:300]}") from exc + + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result["result"].get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + text = content[0].get("text", "") + envelope = _lenient_json(text, doc_name) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + first = results[0] + if isinstance(first, dict) and first.get("error"): + raise RuntimeError(f"{doc_name}: {first['error']}") + inner = first.get("content", "") if isinstance(first, dict) else "" + if inner in (None, ""): + raise RuntimeError(f"Empty content for {doc_name}") + if isinstance(inner, str): + return _lenient_json(inner, doc_name) + return inner + return envelope + + + def _read_doc(doc_name: str) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + return _extract_json_from_read_docs_result(result, doc_name) + + + def _write_text_doc(doc_name: str, content: str) -> None: + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=_next_msg_id()) + + + def _read_doc_raw_text(doc_name: str) -> str: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + content = result["result"].get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + envelope = _lenient_json(content[0].get("text", ""), doc_name) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if results and isinstance(results[0], dict): + raw = results[0].get("content") + if isinstance(raw, str): + return raw + raise RuntimeError(f"raw text unavailable for {doc_name}") + + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _text(value: Any, limit: int = MAX_TEXT) -> str: + if value is None: + return "" + if isinstance(value, str): + s = value + else: + s = json.dumps(value, ensure_ascii=False, sort_keys=True) + s = re.sub(r"\s+", " ", s).strip() + return s[:limit] + + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + if value is None: + return out + if isinstance(value, (str, int, float)): + s = str(value).strip() + return [s] if s else [] + if isinstance(value, list): + for item in value: + out.extend(_strings(item)) + return _dedupe(out) + + + def _dedupe(values: list[str]) -> list[str]: + seen: set[str] = set() + out: list[str] = [] + for value in values: + if value and value not in seen: + seen.add(value) + out.append(value) + return out + + + def _first_key(d: dict[str, Any], keys: list[str]) -> Any: + for key in keys: + if key in d and d.get(key) not in (None, "", [], {}): + return d.get(key) + return None + + + def _first_value(*values: Any) -> Any: + for value in values: + if value not in (None, "", [], {}): + return value + return None + + + def _items_from_doc(doc: Any, candidate_keys: list[str]) -> list[dict[str, Any]]: + if isinstance(doc, list): + return [x for x in doc if isinstance(x, dict)] + if isinstance(doc, dict): + for key in candidate_keys: + if isinstance(doc.get(key), list): + return [x for x in doc[key] if isinstance(x, dict)] + if any(k in doc for k in ("BO_ID", "bo_id", "id", "fact_id", "evidence_index", "structure_id")): + return [doc] + return [] + + + def _bo_id(bo: dict[str, Any]) -> str: + return str(_first_key(bo, ["BO_ID", "bo_id", "id", "canonical_bo_id", "bh_id"]) or "").strip() + + + def _bo_items(bo_doc: Any) -> list[dict[str, Any]]: + items = _items_from_doc(bo_doc, ["BO", "bo", "items", "bo_items", "canonical_bo_items", "body"]) + valid = [item for item in items if _bo_id(item)] + if not valid: + raise ValueError("BO.json must contain BO items with BO_ID/id") + return valid + + + def _evidence_refs_from_bo(bo: dict[str, Any]) -> list[str]: + refs: list[str] = [] + for key in ("source_evidence_indexes", "evidence_refs", "Evidence", "EvidenceIndexes", "related_evidence_indexes"): + refs.extend(_strings(bo.get(key))) + core = _as_dict(bo.get("core_field_base")) + refs.extend(_strings(core.get("source_evidence_indexes"))) + return _dedupe(refs) + + + def _compact_bo(bo: dict[str, Any]) -> dict[str, Any]: + core = _as_dict(bo.get("core_field_base")) + return { + "bo_id": _bo_id(bo), + "bo_type": _first_key(bo, ["BOType", "bo_type", "type"]), + "action": _first_key(bo, ["Action", "action"]), + "action_type": _first_key(bo, ["ActionType", "action_type"]), + "juristic_act": _first_key(bo, ["JuristicAct", "juristic_act"]), + "reason": _first_key(bo, ["Reason", "reason"]), + "prior_act": _first_key(bo, ["PriorAct", "prior_act"]), + "legal_keywords": _strings(bo.get("Legal_Keywords")), + "amount": _first_key(bo, ["amount", "proposed_amount", "Amount"]), + "date": _first_key(bo, ["date", "proposed_date", "Date"]), + "core_field_base": { + "BehaviorTime": _first_key(core, ["BehaviorTime", "behavior_time", "date", "Date"]), + "Performer": _first_key(core, ["Performer", "Actor", "actor", "performer"]), + "Subject": _first_key(core, ["Subject", "Counterparty", "subject", "counterparty"]), + "Object": _first_key(core, ["Object", "Property", "Asset", "object", "asset"]), + "Amount": _first_key(core, ["Amount", "amount", "Value", "value"]), + "Action": _first_key(core, ["Action", "action"]), + }, + "source_evidence_indexes": _evidence_refs_from_bo(bo), + "domain_payload_compact": { + k: v + for k, v in _as_dict(_as_dict(bo.get("extensions")).get("domain_payload")).items() + if k in DOMAIN_PAYLOAD_KEEP_KEYS and v not in (None, {}, []) + }, + } + + + def _validate_legal_effect_structures(doc: Any, bo_ids: set[str]) -> tuple[list[dict[str, Any]], dict[str, Any], list[str]]: + warnings: list[str] = [] + if not isinstance(doc, dict): + raise ValueError("legal_effect_structures.json root must be object") + if doc.get("schema_version") != "stage1_legal_effect_structures.v1": + raise ValueError("legal_effect_structures.json schema_version mismatch") + if doc.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("legal_effect_structures.json status must be READY or READY_WITH_REVIEW") + structures = _as_list(doc.get("legal_effect_structures")) + index = _as_dict(doc.get("structure_index")) + if not isinstance(index.get("by_bo_id"), dict): + raise ValueError("legal_effect_structures.structure_index.by_bo_id must be object") + for key in ["by_bo_id", "by_issue_cluster_id", "by_asset_cluster_id", "by_evidence_index", "by_candidate_structure_type"]: + if not isinstance(index.get(key), dict): + raise ValueError(f"legal_effect_structures.structure_index.{key} must be object") + structure_ids = {str(s.get("structure_id")) for s in structures if isinstance(s, dict) and s.get("structure_id")} + for structure in structures: + if not isinstance(structure, dict): + continue + sid = str(structure.get("structure_id") or "") + for bo_id in _strings(structure.get("source_bo_ids")): + if bo_id not in bo_ids: + warnings.append(f"STRUCTURE_SOURCE_BO_NOT_IN_BO_JSON:{sid}:{bo_id}") + bo_row = _as_dict(index["by_bo_id"].get(bo_id)) + if sid and sid not in _strings(bo_row.get("structure_ids")): + warnings.append(f"BY_BO_ID_MISSING_STRUCTURE_REF:{bo_id}:{sid}") + for bo_id, row in index["by_bo_id"].items(): + if bo_id not in bo_ids: + warnings.append(f"BY_BO_ID_UNKNOWN_BO:{bo_id}") + for sid in _strings(_as_dict(row).get("structure_ids")): + if sid not in structure_ids: + warnings.append(f"BY_BO_ID_UNKNOWN_STRUCTURE:{bo_id}:{sid}") + return structures, index, warnings + + + def _compact_structure(structure: dict[str, Any]) -> dict[str, Any]: + return { + "structure_id": structure.get("structure_id"), + "candidate_structure_type": structure.get("candidate_structure_type"), + "structure_label": structure.get("structure_label"), + "source_bo_ids": _strings(structure.get("source_bo_ids")), + "issue_cluster_ids": _strings(structure.get("issue_cluster_ids")), + "asset_cluster_ids": _strings(structure.get("asset_cluster_ids")), + "evidence_indexes": _strings(structure.get("evidence_indexes")), + "linked_claim_group_ids": _strings(structure.get("linked_claim_group_ids")), + "linked_liability_group_ids": _strings(structure.get("linked_liability_group_ids")), + "linked_actio_signal_ids": _strings(structure.get("linked_actio_signal_ids")), + "legal_effect_roles": _strings(structure.get("legal_effect_roles")), + "review_required": bool(structure.get("review_required")), + } + + + def _evidence_id(record: dict[str, Any]) -> str: + return str(_first_key(record, ["evidence_index", "evidence_id", "id", "index", "EvidenceIndex", "doc_id"]) or "").strip() + + + def _flatten_records(value: Any, depth: int = 0) -> list[dict[str, Any]]: + if depth > 3: + return [] + if isinstance(value, list): + out: list[dict[str, Any]] = [] + for item in value: + out.extend(_flatten_records(item, depth + 1)) + return out + if isinstance(value, dict): + if any(k in value for k in ("evidence_index", "evidence_id", "id", "related_bo_ids", "source_bo_ids", "signal_id", "route_id")): + return [value] + out: list[dict[str, Any]] = [] + for child in value.values(): + if isinstance(child, (list, dict)): + out.extend(_flatten_records(child, depth + 1)) + return out + return [] + + + def _excerpt_from_indexed(record: dict[str, Any]) -> str: + """X-8: evidence_indexed.json의 실제 필드(key_facts 우선, key_dates/key_amounts 보조)로 발췌 실질화. + 기존 키(excerpt/summary/content/text/body)는 fallback으로 유지.""" + key_facts = _strings(record.get("key_facts")) + if key_facts: + return _text(" / ".join(key_facts)) + parts: list[str] = [] + key_dates = _strings(record.get("key_dates")) + key_amounts = _strings(record.get("key_amounts")) + if key_dates: + parts.append("dates: " + " / ".join(key_dates)) + if key_amounts: + parts.append("amounts: " + " / ".join(key_amounts)) + if parts: + return _text("; ".join(parts)) + return _text(_first_key(record, ["excerpt", "summary", "content", "text", "body"])) + + + def _build_evidence_authority(evidence_indexed: Any) -> dict[str, Any]: + authority: dict[str, Any] = {} + for record in _flatten_records(evidence_indexed): + eid = _evidence_id(record) + if not eid: + continue + authority[eid] = { + "evidence_index": eid, + "title": _first_key(record, ["title", "Title", "name", "doc_title"]), + "excerpt": _excerpt_from_indexed(record), + "priority": _first_key(record, ["priority", "Priority", "weight"]), + "authentication": _first_key(record, ["authentication", "auth", "source_authentication"]), + "source": "evidence_indexed.json", + } + return authority + + + def _collect_bo_refs(value: Any, bo_ids: set[str]) -> list[str]: + refs: list[str] = [] + if isinstance(value, str): + if value in bo_ids: + refs.append(value) + elif isinstance(value, list): + for item in value: + refs.extend(_collect_bo_refs(item, bo_ids)) + elif isinstance(value, dict): + for key in ("related_bo_ids", "source_bo_ids", "bo_ids", "BO_ID", "bo_id", "source_bo_id"): + refs.extend([x for x in _strings(value.get(key)) if x in bo_ids]) + for child in value.values(): + if isinstance(child, (dict, list)): + refs.extend(_collect_bo_refs(child, bo_ids)) + return _dedupe(refs) + + + def _compact_signal(record: dict[str, Any], signal_type: str) -> dict[str, Any]: + return { + "signal_type": signal_type, + "signal_id": _first_key(record, ["signal_id", "route_id", "id", "structure_id"]), + "candidate_structure_types": _strings(record.get("candidate_structure_types")), + "linked_structure_ids": _strings(record.get("linked_structure_ids")), + "related_evidence_indexes": _strings(_first_key(record, ["related_evidence_indexes", "evidence_refs", "source_evidence_indexes"])), + "party_roles": record.get("party_roles") if isinstance(record.get("party_roles"), list) else [], + "actio_role_hints": record.get("actio_role_hints") if isinstance(record.get("actio_role_hints"), list) else [], + "claim_group_candidates": record.get("claim_group_candidates") if isinstance(record.get("claim_group_candidates"), list) else [], + "liability_group_candidates": record.get("liability_group_candidates") if isinstance(record.get("liability_group_candidates"), list) else [], + "summary": _text(record, 500), + } + + + def _build_signal_hints_by_bo_id(docs: dict[str, Any], bo_ids: set[str]) -> dict[str, dict[str, list[dict[str, Any]]]]: + out: dict[str, dict[str, list[dict[str, Any]]]] = { + bo_id: {"actio": [], "case_liability": [], "legal_effect": []} for bo_id in bo_ids + } + for signal_type, doc in docs.items(): + for record in _flatten_records(doc): + bo_refs = _collect_bo_refs(record, bo_ids) + for bo_id in bo_refs: + bucket = out.setdefault(bo_id, {"actio": [], "case_liability": [], "legal_effect": []}) + key = {"actio_case_signals": "actio", "case_liability_signals": "case_liability", "legal_effect_signals": "legal_effect"}[signal_type] + if len(bucket[key]) < MAX_SIGNAL_ITEMS_PER_BO: + bucket[key].append(_compact_signal(record, key)) + return out + + + def _linked_by_bo(bo_id: str, index: dict[str, Any]) -> dict[str, Any]: + row = _as_dict(index.get("by_bo_id", {}).get(bo_id)) + return { + "structure_ids": _strings(row.get("structure_ids")), + "issue_cluster_ids": _strings(row.get("issue_cluster_ids")), + "asset_cluster_ids": _strings(row.get("asset_cluster_ids")), + "evidence_index_ids": _strings(row.get("evidence_index_ids") or row.get("evidence_indexes")), + "candidate_structure_types": _strings(row.get("candidate_structure_types")), + "claim_group_ids": _strings(row.get("claim_group_ids") or row.get("linked_claim_group_ids")), + "liability_group_ids": _strings(row.get("liability_group_ids") or row.get("linked_liability_group_ids")), + "downstream_fact_roles": _strings(row.get("downstream_fact_roles") or row.get("legal_effect_roles")), + } + + + def _score_evidence(evidence_rows: list[dict[str, Any]]) -> tuple[str, str]: + if not evidence_rows: + return "low", "low" + text = " ".join(_text(row, 300).lower() for row in evidence_rows) + documentary_markers = ["등기", "계약", "판결", "공문", "금융", "송금", "계좌", "registry", "contract", "judgment"] + if any(marker.lower() in text for marker in documentary_markers): + return "high", "documentary" + if len(evidence_rows) >= 2: + return "high", "high" + if "meeting" in text or "회의" in text or "면담" in text: + return "medium", "meeting_only_core" + return "medium", "medium" + + + def _legal_roles(types: list[str], structures: list[dict[str, Any]], signals: dict[str, Any]) -> list[str]: + roles: list[str] = [] + for structure in structures: + roles.extend(_strings(structure.get("legal_effect_roles"))) + roles.extend([STRUCTURE_TYPE_TO_ROLE.get(t, "legal_effect_roles") for t in types]) + if signals.get("actio"): + roles.append("actio_roles") + if signals.get("case_liability"): + roles.append("claim_chain_ref") + if signals.get("legal_effect"): + roles.append("legal_effect_roles") + return _dedupe(roles) + + + def _modules(types: list[str], signals: dict[str, Any]) -> list[str]: + modules = [STRUCTURE_TYPE_TO_MODULE.get(t, "legal_effect") for t in types] + if signals.get("actio"): + modules.append("actio") + if signals.get("case_liability"): + modules.append("claim_liability") + if signals.get("legal_effect"): + modules.append("legal_effect") + return _dedupe(modules) + + + def _linked_structures(linked: dict[str, Any]) -> dict[str, Any]: + return { + "structure_ids": _strings(linked.get("structure_ids")), + "issue_cluster_ids": _strings(linked.get("issue_cluster_ids")), + "asset_cluster_ids": _strings(linked.get("asset_cluster_ids")), + "evidence_index_ids": _strings(linked.get("evidence_index_ids")), + "candidate_structure_types": _strings(linked.get("candidate_structure_types")), + "claim_group_ids": _strings(linked.get("claim_group_ids")), + "liability_group_ids": _strings(linked.get("liability_group_ids")), + } + + + def _parties(bo: dict[str, Any], signals: dict[str, Any]) -> list[dict[str, Any]]: + core = _as_dict(bo.get("core_field_base")) + out: list[dict[str, Any]] = [] + for role, key in [("actor", "Performer"), ("counterparty", "Subject")]: + party = core.get(key) + if party: + out.append({"party": _text(party, 160), "role": role, "source": "BO.core_field_base"}) + for signal in _as_list(signals.get("case_liability")): + for item in _as_list(_as_dict(signal).get("party_roles")): + if isinstance(item, dict) and item.get("party"): + out.append({"party": _text(item.get("party"), 160), "role": _text(item.get("role"), 80), "source": "case_liability_signals"}) + seen: set[tuple[str, str]] = set() + deduped: list[dict[str, Any]] = [] + for row in out: + key = (row.get("party", ""), row.get("role", "")) + if key not in seen: + seen.add(key) + deduped.append(row) + return deduped + + + def _derived_fact(bo: dict[str, Any], row: dict[str, Any]) -> str: + date = row.get("date") or "" + actor = "" + for party in row.get("parties", []): + if isinstance(party, dict) and party.get("role") == "actor": + actor = party.get("party") or "" + break + obj = row.get("object_spec") or "" + action = row.get("action") or bo.get("action") or "" + if date and actor and obj and action: + return f"[{date}] {actor}가 {obj}에 관하여 {action}하였다는 사실" + if action: + return str(action) + return "" + + + def _basis(bo_id: str, evidence_refs: list[str], structures: dict[str, Any]) -> str: + parts = [f"source_bo_id={bo_id}"] + if evidence_refs: + parts.append("evidence_refs=" + ",".join(evidence_refs[:8])) + sids = _strings(structures.get("structure_ids")) + if sids: + parts.append("structure_ids=" + ",".join(sids[:8])) + return "; ".join(parts) + + + def _claim_chain_ref(linked: dict[str, Any], signals: dict[str, Any]) -> dict[str, Any]: + return { + "claim_group_ids": _strings(linked.get("claim_group_ids")), + "liability_group_ids": _strings(linked.get("liability_group_ids")), + "signal_count": len(_as_list(signals.get("case_liability"))), + } + + + def _legal_calculation_object(row: dict[str, Any], types: list[str]) -> dict[str, Any]: + needs_calc = any(t in {"money_claim", "secured_debt", "land_use_gain", "valuation", "fraudulent_transfer"} for t in types) + return { + "calculation_required": needs_calc, + "amount_source": "BO.amount" if row.get("amount") else None, + "amount_text": row.get("amount"), + "object_basis": row.get("object_spec"), + "candidate_structure_types": types, + } + + + def _build_row(idx: int, item: dict[str, Any], structure_by_id: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]: + bo_id = item["source_bo_id"] + bo = _as_dict(item.get("bo_compact")) + linked = _linked_structures(_as_dict(item.get("linked_structures"))) + signals = _as_dict(item.get("signal_hints")) + related_structures = [_as_dict(structure_by_id.get(sid)) for sid in linked["structure_ids"] if isinstance(structure_by_id.get(sid), dict)] + types = _dedupe(linked["candidate_structure_types"] + [str(s.get("candidate_structure_type")) for s in related_structures if s.get("candidate_structure_type")]) + evidence_refs = _dedupe(_strings(item.get("evidence_refs")) + linked["evidence_index_ids"]) + evidence_rows = [_as_dict(_as_dict(item.get("evidence_authority")).get(eid)) for eid in evidence_refs] + evidence_rows = [row for row in evidence_rows if row] + credibility, proof_strength = _score_evidence(evidence_rows) + core = _as_dict(bo.get("core_field_base")) + row = { + "fact_id": f"F-{idx:03d}", + "source_bo_id": bo_id, + "type": _first_value(bo.get("bo_type"), types[0] if types else None), + "date": _first_value(core.get("BehaviorTime"), bo.get("date")), + "parties": _parties(bo, signals), + "object_spec": _first_value(core.get("Object"), _text(linked["asset_cluster_ids"], 160)), + "amount": _text(_first_value(bo.get("amount"), core.get("Amount")), 240) or None, + "action": _first_value(bo.get("action"), core.get("Action")), + "evidence_refs": evidence_refs, + "credibility": credibility, + "legal_centrality": "critical" if linked["structure_ids"] or signals.get("actio") else "medium", + "proof_strength": proof_strength, + "legal_effect_roles": _legal_roles(types, related_structures, signals), + "claim_chain_ref": _claim_chain_ref(linked, signals), + "linked_structures": linked, + "must_not_drop_in_claim_types": _modules(types, signals) if linked["structure_ids"] or signals.get("actio") else [], + "must_consider": bool(linked["structure_ids"] or signals.get("actio")), + "state_context": { + "bo_type": bo.get("bo_type"), + "issue_cluster_ids": linked["issue_cluster_ids"], + "asset_cluster_ids": linked["asset_cluster_ids"], + }, + "money_claim_effect": {"enabled": "money_claim" in types, "source_bo_id": bo_id if "money_claim" in types else None}, + "commercial_successor_effect": {"enabled": "commercial_successor" in types, "source_bo_id": bo_id if "commercial_successor" in types else None}, + "actio_roles": _as_list(signals.get("actio")), + "linked_actio_structures": { + "structure_ids": [s.get("structure_id") for s in related_structures if s.get("candidate_structure_type") in {"fraudulent_transfer", "preserved_claim"}], + "actio_signal_count": len(_as_list(signals.get("actio"))), + }, + "downstream_module_candidates": _modules(types, signals), + "skeleton_validation_codes": [], + "derived_fact": "", + "derivation_basis": "", + "legal_calculation_object": {}, + } + row["legal_calculation_object"] = _legal_calculation_object(row, types) + row["derived_fact"] = _derived_fact(bo, row) + row["derivation_basis"] = _basis(bo_id, evidence_refs, linked) + if not row["derived_fact"]: + row["skeleton_validation_codes"].append("EMPTY_DERIVED_FACT") + if not row["evidence_refs"]: + row["skeleton_validation_codes"].append("NO_EVIDENCE_REFS") + if not linked["structure_ids"]: + row["skeleton_validation_codes"].append("NO_LINKED_LEGAL_EFFECT_STRUCTURE") + return row, related_structures + + + def _mark_row_code(row: dict[str, Any], code: str) -> None: + codes = row.setdefault("skeleton_validation_codes", []) + if isinstance(codes, list) and code not in codes: + codes.append(code) + + + def _domain_payload_compact(bo_by_id: dict[str, Any], row: dict[str, Any]) -> dict[str, Any]: + bo = _as_dict(bo_by_id.get(str(row.get("source_bo_id")))) + return _as_dict(bo.get("domain_payload_compact")) + + + def _clean_str(value: Any) -> str: + return str(value).strip() if value not in (None, "") else "" + + + def _periods_overlap(start_a: str, end_a: str, start_b: str, end_b: str) -> bool: + a0, a1 = start_a or "0000-00-00", end_a or "9999-12-31" + b0, b1 = start_b or "0000-00-00", end_b or "9999-12-31" + return a0 <= b1 and b0 <= a1 + + + def _possession_conflict_scan(rows: list[dict[str, Any]], bo_by_id: dict[str, Any]) -> None: + incompatible = {frozenset(pair) for pair in POSSESSION_INCOMPATIBLE_CAUSE_PAIRS} + entries: list[dict[str, Any]] = [] + for row in rows: + possession = _as_dict(_domain_payload_compact(bo_by_id, row).get("possession_state")) + if not possession: + continue + asset_keys = _strings(possession.get("asset_ref")) or _strings(_as_dict(row.get("state_context")).get("asset_cluster_ids")) + events = [x for x in (_as_dict(e) for e in _as_list(possession.get("possession_events"))) if x] + causes = [x for x in (_as_dict(c) for c in _as_list(possession.get("legal_cause_assertions"))) if _clean_str(x.get("cause_asserted"))] + if events and not causes: + _mark_row_code(row, "LEGAL_CAUSE_STATE_UNKNOWN_REVIEW") + starts = sorted([s for s in (_clean_str(e.get("start_date")) for e in events) if s]) + ends = [_clean_str(e.get("end_date")) for e in events] + open_ended = (not events) or any(not e for e in ends) + period_start = starts[0] if starts else "" + period_end = "" if open_ended else max(e for e in ends if e) + for cause in causes: + entries.append({ + "row": row, + "asset_keys": set(asset_keys), + "cause": _clean_str(cause.get("cause_asserted")), + "start": _clean_str(cause.get("assertion_date")) or period_start, + "end": period_end, + }) + for i in range(len(entries)): + for j in range(i + 1, len(entries)): + first, second = entries[i], entries[j] + if first["row"] is second["row"]: + continue + if not (first["asset_keys"] & second["asset_keys"]): + continue + if frozenset((first["cause"], second["cause"])) not in incompatible: + continue + if not _periods_overlap(first["start"], first["end"], second["start"], second["end"]): + continue + for row in (first["row"], second["row"]): + _mark_row_code(row, "POSSESSION_CAUSE_CONFLICT") + row["must_consider"] = True + + + def _notice_receipt_gate_scan(rows: list[dict[str, Any]], bo_by_id: dict[str, Any]) -> None: + for row in rows: + notice = _as_dict(_domain_payload_compact(bo_by_id, row).get("notice_lifecycle")) + if not notice: + continue + if notice.get("requires_receipt_for_effect") is not True: + continue + received = _clean_str(notice.get("received_date")) + arrival = _clean_str(notice.get("arrival_date")) + if not received and not arrival: + _mark_row_code(row, "NOTICE_RECEIPT_UNVERIFIED") + row["must_consider"] = True + if received and not _strings(notice.get("receipt_evidence_refs")) and not _clean_str(notice.get("recipient_admission_text")): + _mark_row_code(row, "NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW") + + + def _lien_extinction_notice_scan(rows: list[dict[str, Any]], bo_by_id: dict[str, Any]) -> None: + receipt_by_notice_id: dict[str, str] = {} + possession_entries: list[dict[str, Any]] = [] + for row in rows: + payload = _domain_payload_compact(bo_by_id, row) + notice = _as_dict(payload.get("notice_lifecycle")) + notice_id = _clean_str(notice.get("notice_id")) + receipt_date = _clean_str(notice.get("received_date")) or _clean_str(notice.get("arrival_date")) + if notice_id and receipt_date: + receipt_by_notice_id[notice_id] = min(receipt_date, receipt_by_notice_id.get(notice_id, "9999-12-31")) + possession = _as_dict(payload.get("possession_state")) + asset_keys = set(_strings(possession.get("asset_ref"))) + for event in (_as_dict(x) for x in _as_list(possession.get("possession_events"))): + start = _clean_str(event.get("start_date")) + if start and asset_keys: + possession_entries.append({"row": row, "asset_keys": asset_keys, "start": start}) + for row in rows: + lien = _as_dict(_domain_payload_compact(bo_by_id, row).get("lien_state")) + if not lien: + continue + receipt_dates = [receipt_by_notice_id[ref] for ref in _strings(lien.get("extinction_notice_refs")) if ref in receipt_by_notice_id] + lien_assets = set(_strings(lien.get("asset_ref"))) + if not receipt_dates or not lien_assets: + continue + receipt_date = min(receipt_dates) + for entry in possession_entries: + if (lien_assets & entry["asset_keys"]) and entry["start"] > receipt_date: + _mark_row_code(row, "LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW") + _mark_row_code(entry["row"], "LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW") + + + # ---------------------------------------------------------------- X-1/X-2/X-3: 예외의 상류 소거 + def _distinct_values(values: list[Any]) -> list[str]: + out: list[str] = [] + for value in values: + s = _text(value, 240) + if s and s not in out: + out.append(s) + return out + + + def _projection_conflicts_for_row(row: dict[str, Any], bo: dict[str, Any]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + """X-3: v2 트리거 잔여 3유형에 4중 조건(단일 값 선택 필수 / source-backed 상충 후보 2+ / + 보존·defer 불가 / compact 판정 충분)을 적용한다. 후보 2개 미만의 단순 누락은 조건 ② 불충족으로 + deterministic review code로 전환한다. X-1(복수 role/module/그룹 = 정상 다중 라우팅)과 + X-2(파생 사실 문자열 길이 기반)의 trigger는 폐지되었다(배열 보존이 기본값). + 반환: (pack_candidates, deterministic_reviews).""" + core = _as_dict(bo.get("core_field_base")) + linked = _as_dict(row.get("linked_structures")) + roles = _strings(row.get("legal_effect_roles")) + modules = _strings(row.get("downstream_module_candidates")) + calc = _as_dict(row.get("legal_calculation_object")) + pack_candidates: list[dict[str, Any]] = [] + reviews: list[dict[str, Any]] = [] + + def _review(review_code: str, exception_type: str, reason: str) -> None: + reviews.append({ + "fact_id": row["fact_id"], + "source_bo_id": row["source_bo_id"], + "review_code": review_code, + "origin_exception_type": exception_type, + "reason": reason, + }) + + def _candidate(exception_type: str, trigger_reason: str, allowed_output_fields: list[str], + compact_context: dict[str, Any], conflict_flags: list[str]) -> None: + pack_candidates.append({ + "exception_type": exception_type, + "trigger_reason": trigger_reason, + "allowed_output_fields": allowed_output_fields, + "compact_context": compact_context, + "conflict_flags": conflict_flags, + "row": row, + }) + + if calc.get("calculation_required"): + conflict_fields: dict[str, list[str]] = {} + # X-3 ①②: 양 원천(BO.amount vs core.Amount)이 실재하며 상충하면 단일 값 선택이 필수인 진짜 예외. + amount_candidates = _distinct_values([bo.get("amount"), core.get("Amount")]) + if len(amount_candidates) >= 2: + conflict_fields["amount"] = amount_candidates + if not row.get("object_spec"): + object_candidates = _distinct_values([core.get("Object"), ", ".join(_strings(linked.get("asset_cluster_ids")))]) + if len(object_candidates) >= 2: + conflict_fields["object_spec"] = object_candidates + if conflict_fields: + _candidate( + "legal_calculation_required", "calculation_source_conflicting_candidates", + ["amount", "object_spec", "legal_calculation_object", "derivation_basis", "skeleton_validation_codes"], + { + "target_fields": sorted(conflict_fields), + "source_backed_candidates": conflict_fields, + "candidate_structure_types": _strings(calc.get("candidate_structure_types")), + "derivation_basis": row.get("derivation_basis"), + }, + ["CALCULATION_BASIS_CONFLICT"], + ) + elif not row.get("amount") or not row.get("object_spec"): + _review("CALCULATION_BASIS_INCOMPLETE_REVIEW", "legal_calculation_required", + "calculation_source_missing_without_competing_candidates") + asset_ids = _strings(linked.get("asset_cluster_ids")) + if asset_ids and not row.get("object_spec"): + if len(asset_ids) >= 2: + _candidate( + "unresolved_asset_cluster_projection", "asset_cluster_projection_requires_single_object_choice", + ["object_spec", "linked_structures", "legal_calculation_object", "derivation_basis"], + { + "target_fields": ["object_spec"], + "source_backed_candidates": {"object_spec": asset_ids}, + "structure_ids": _strings(linked.get("structure_ids")), + "derivation_basis": row.get("derivation_basis"), + }, + ["ASSET_CLUSTER_PROJECTION_GAP"], + ) + else: + _review("ASSET_CLUSTER_PROJECTION_GAP_REVIEW", "unresolved_asset_cluster_projection", + "single_asset_cluster_without_object_spec") + if _strings(linked.get("structure_ids")) and (not roles or not modules): + types = _strings(linked.get("candidate_structure_types")) + role_candidates = _distinct_values([STRUCTURE_TYPE_TO_ROLE.get(t, "legal_effect_roles") for t in types]) if not roles else [] + module_candidates = _distinct_values([STRUCTURE_TYPE_TO_MODULE.get(t, "legal_effect") for t in types]) if not modules else [] + if len(role_candidates) >= 2 or len(module_candidates) >= 2: + _candidate( + "unresolved_structure_role_projection", "structure_role_projection_requires_single_choice", + ["legal_effect_roles", "downstream_module_candidates", "claim_chain_ref", "actio_roles", "linked_structures"], + { + "target_fields": ["legal_effect_roles", "downstream_module_candidates"], + "source_backed_candidates": {"legal_effect_roles": role_candidates, "downstream_module_candidates": module_candidates}, + "structure_ids": _strings(linked.get("structure_ids")), + "candidate_structure_types": types, + }, + ["STRUCTURE_ROLE_PROJECTION_GAP"], + ) + else: + _review("STRUCTURE_ROLE_PROJECTION_GAP_REVIEW", "unresolved_structure_role_projection", + "structure_role_projection_without_competing_candidates") + return pack_candidates, reviews + + + def _exception_item(exception_id: str, candidate: dict[str, Any]) -> dict[str, Any]: + row = candidate["row"] + return { + "exception_id": exception_id, + "exception_type": candidate["exception_type"], + "fact_id": row["fact_id"], + "source_bo_id": row["source_bo_id"], + "severity": "blocking_projection", + "trigger_reason": candidate["trigger_reason"], + "candidate_fact_row_subset": {key: deepcopy(row.get(key)) for key in candidate["allowed_output_fields"] if key in row}, + "compact_context": candidate["compact_context"], + "allowed_output_fields": candidate["allowed_output_fields"], + "allowed_decisions": ["PATCH", "KEEP_AS_IS", "BLOCK_REVIEW"], + "conflict_flags": candidate["conflict_flags"], + } + + + def _assemble_exception_pack(pack_candidates: list[dict[str, Any]], deterministic_reviews: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], int]: + """FX-### 부여 후 방어 상한 적용. legal_theory 표지는 무조건 LLM 금지 → deterministic review. + 상한 초과분은 자동 차단이 아니라 exception_id를 유지한 채 review로 라우팅한다(§5-6).""" + exceptions: list[dict[str, Any]] = [] + overflow = 0 + total_bytes = 0 + counter = itertools.count(1) + for candidate in pack_candidates: + exception_id = f"FX-{next(counter):03d}" + row = candidate["row"] + review_code = REVIEW_CODE_BY_EXCEPTION_TYPE[candidate["exception_type"]] + if any("legal_theory" in flag for flag in candidate["conflict_flags"]): + deterministic_reviews.append({ + "fact_id": row["fact_id"], "source_bo_id": row["source_bo_id"], + "review_code": "LEGAL_THEORY_REQUIRED_REVIEW", + "origin_exception_type": candidate["exception_type"], + "reason": f"legal_theory_marker_routes_to_stage2:{exception_id}", + }) + _mark_row_code(row, "LEGAL_THEORY_REQUIRED_REVIEW") + row["must_consider"] = True + continue + item = _exception_item(exception_id, candidate) + item_text = json.dumps(item, ensure_ascii=False, sort_keys=True) + oversize = len(item_text) > PACK_ITEM_CHAR_LIMIT + over_budget = len(exceptions) >= PACK_MAX_ITEMS or total_bytes + len(item_text.encode("utf-8")) > PACK_MAX_BYTES + if oversize or over_budget: + overflow += 1 + deterministic_reviews.append({ + "fact_id": row["fact_id"], "source_bo_id": row["source_bo_id"], + "review_code": review_code, + "origin_exception_type": candidate["exception_type"], + "reason": ("pack_item_over_char_limit:" if oversize else "pack_budget_overflow:") + exception_id, + }) + _mark_row_code(row, review_code) + row["must_consider"] = True + continue + total_bytes += len(item_text.encode("utf-8")) + exceptions.append(item) + return exceptions, overflow + + + def _les_review_origins(les_doc: Any, structures: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]: + """X-7: LES 실질 review 상태 수집 — (a) review_required=true, (b) pending_exception_state.blocked + 비공백(Part 3 v2 additive 필드, 부재 시 무시), (c) quality_gate.review_queue 실질 항목.""" + origin_by_sid: dict[str, list[dict[str, Any]]] = {} + for structure in structures: + if not isinstance(structure, dict): + continue + sid = str(structure.get("structure_id") or "") + if not sid: + continue + if structure.get("review_required") is True: + origin_by_sid.setdefault(sid, []).append({"origin": "structure_review_required"}) + blocked = _strings(_as_dict(structure.get("pending_exception_state")).get("blocked")) + if blocked: + origin_by_sid.setdefault(sid, []).append({"origin": "pending_exception_blocked", "blocked_exception_ids": blocked}) + for entry in _as_list(_as_dict(_as_dict(les_doc).get("quality_gate")).get("review_queue")): + item = _as_dict(entry) + sid = str(item.get("structure_id") or "") + review_type = _clean_str(item.get("review_type")) + if not sid or not review_type: + continue + origins = origin_by_sid.setdefault(sid, []) + if review_type == "structure_review_required" and any(o.get("origin") == "structure_review_required" for o in origins): + continue + origins.append({"origin": "quality_gate_review_queue", "review_type": review_type}) + return origin_by_sid + + + def _apply_les_inheritance(rows: list[dict[str, Any]], origin_by_sid: dict[str, list[dict[str, Any]]]) -> dict[str, dict[str, Any]]: + by_fact_id: dict[str, dict[str, Any]] = {} + for row in rows: + sids = sorted(set(_strings(_as_dict(row.get("linked_structures")).get("structure_ids"))) & set(origin_by_sid)) + if not sids: + continue + _mark_row_code(row, "LES_REVIEW_REQUIRED") + row["must_consider"] = True + by_fact_id[row["fact_id"]] = { + "fact_id": row["fact_id"], + "source_bo_id": row["source_bo_id"], + "structure_ids": sids, + "origins": {sid: origin_by_sid[sid] for sid in sids}, + } + return by_fact_id + + + def _validate_candidate_bundle(bundle: dict[str, Any]) -> None: + if bundle.get("schema_version") != "stage1_fact_ledger_candidate_bundle.v2": + raise ValueError("candidate bundle schema_version mismatch") + items = _as_list(bundle.get("candidate_items")) + if not items: + raise ValueError("candidate_items is empty") + for item in items: + missing = [field for field in FINAL_FIELDS if field not in item] + if missing: + raise ValueError(f"candidate item missing fields: {missing}") + extra = [field for field in item if field not in FINAL_FIELDS] + if extra: + raise ValueError(f"candidate item has arbitrary fields: {extra}") + manifest = _as_dict(bundle.get("ref_manifest")) + for key in ("bo_ids", "structure_ids", "evidence_ids"): + if not isinstance(manifest.get(key), list): + raise ValueError(f"ref_manifest.{key} must be list") + ref = _as_dict(bundle.get("exception_pack_ref")) + if ref.get("path") != PACK_OUT_PATH or not ref.get("sha256"): + raise ValueError("exception_pack_ref invalid") + if "exception_pack" in bundle or "created_at_utc" in bundle: + raise ValueError("candidate bundle must not embed exception_pack or created_at_utc") + + + def _validate_exception_pack(pack: dict[str, Any]) -> None: + if pack.get("schema_version") != "stage1_fact_ledger_exception_pack.v2": + raise ValueError("exception pack schema_version mismatch") + for exc in _as_list(pack.get("exceptions")): + if exc.get("exception_type") not in PACK_EXCEPTION_TYPES: + raise ValueError(f"unsupported exception_type: {exc.get('exception_type')}") + + + def main() -> None: + try: + _init_mcp_session() + docs = {key: _read_doc(name) for key, name in INPUT_FILES.items()} + bo_items = _bo_items(docs["bo"]) + bo_by_id = {_bo_id(item): _compact_bo(item) for item in bo_items} + bo_ids = set(bo_by_id) + structures, structure_index, structure_warnings = _validate_legal_effect_structures(docs["legal_effect_structures"], bo_ids) + structure_by_id = { + str(item.get("structure_id")): _compact_structure(item) + for item in structures + if isinstance(item, dict) and item.get("structure_id") + } + evidence_authority = _build_evidence_authority(docs["evidence_indexed"]) + signal_hints = _build_signal_hints_by_bo_id({ + "actio_case_signals": docs["actio_case_signals"], + "case_liability_signals": docs["case_liability_signals"], + "legal_effect_signals": docs["legal_effect_signals"], + }, bo_ids) + warnings = list(structure_warnings) + # per-BO fact input 중간 표현은 메모리에서만 구성한다(§5-3 — 어떤 채널에도 물질화 금지). + rows: list[dict[str, Any]] = [] + pack_candidates: list[dict[str, Any]] = [] + deterministic_reviews: list[dict[str, Any]] = [] + for idx, bo_id in enumerate(sorted(bo_by_id), start=1): + bo = bo_by_id[bo_id] + linked = _linked_by_bo(bo_id, structure_index) + evidence_refs = _dedupe(bo["source_evidence_indexes"] + _strings(linked.get("evidence_index_ids"))) + for eid in evidence_refs: + if eid not in evidence_authority: + warnings.append(f"EVIDENCE_REF_NOT_IN_EVIDENCE_UNIVERSE:{bo_id}:{eid}") + item = { + "source_bo_id": bo_id, + "bo_compact": bo, + "linked_structures": linked, + "signal_hints": signal_hints.get(bo_id, {"actio": [], "case_liability": [], "legal_effect": []}), + "evidence_refs": evidence_refs, + "evidence_authority": {eid: evidence_authority.get(eid, {"evidence_index": eid, "missing": True}) for eid in evidence_refs}, + } + row, _related = _build_row(idx, item, structure_by_id) + rows.append(row) + candidates, reviews = _projection_conflicts_for_row(row, bo) + pack_candidates.extend(candidates) + deterministic_reviews.extend(reviews) + bo_compact_by_id = bo_by_id + _possession_conflict_scan(rows, bo_compact_by_id) + _notice_receipt_gate_scan(rows, bo_compact_by_id) + _lien_extinction_notice_scan(rows, bo_compact_by_id) + for review in deterministic_reviews: + for row in rows: + if row["fact_id"] == review["fact_id"]: + _mark_row_code(row, review["review_code"]) + row["must_consider"] = True + exceptions, overflow = _assemble_exception_pack(pack_candidates, deterministic_reviews) + origin_by_sid = _les_review_origins(docs["legal_effect_structures"], structures) + les_by_fact_id = _apply_les_inheritance(rows, origin_by_sid) + # F2 drafting gate 입력(소스팩 재읽기 폐지 대체): NOTICE_RECEIPT_UNVERIFIED 행의 notice_id. + notice_gate_fact_ids: list[str] = [] + notice_gate_ids: list[str] = [] + for row in rows: + if "NOTICE_RECEIPT_UNVERIFIED" in _as_list(row.get("skeleton_validation_codes")): + notice_gate_fact_ids.append(row["fact_id"]) + notice_id = _clean_str(_as_dict(_domain_payload_compact(bo_compact_by_id, row).get("notice_lifecycle")).get("notice_id")) + if notice_id: + notice_gate_ids.append(notice_id) + pack_payload = { + "schema_version": "stage1_fact_ledger_exception_pack.v2", + "status": "READY", + "has_exceptions": bool(exceptions), + "exception_count": len(exceptions), + "exceptions": exceptions, + "budget": { + "max_items": PACK_MAX_ITEMS, + "max_bytes": PACK_MAX_BYTES, + "item_char_limit": PACK_ITEM_CHAR_LIMIT, + "overflow_routed_to_review_count": overflow, + }, + } + _validate_exception_pack(pack_payload) + pack_text = json.dumps(pack_payload, ensure_ascii=False, indent=2) + "\n" + pack_sha = hashlib.sha256(pack_text.encode("utf-8")).hexdigest() + bundle = { + "schema_version": "stage1_fact_ledger_candidate_bundle.v2", + "status": "READY_WITH_EXCEPTIONS" if exceptions else "READY", + "candidate_items": rows, + "ref_manifest": { + "bo_ids": sorted(bo_ids), + "structure_ids": sorted(structure_by_id), + "evidence_ids": sorted(evidence_authority), + }, + "les_review_inheritance": {"by_fact_id": {fact_id: les_by_fact_id[fact_id] for fact_id in sorted(les_by_fact_id)}}, + "exception_pack_ref": { + "path": PACK_OUT_PATH, + "sha256": pack_sha, + "expected_exception_ids": [exc["exception_id"] for exc in exceptions], + }, + "compile_gate": { + "candidate_count": len(rows), + "exception_count": len(exceptions), + "deterministic_review_count": len(deterministic_reviews), + "les_inheritance_count": len(les_by_fact_id), + "deterministic_review_items": deterministic_reviews, + "notice_gate": {"affected_fact_ids": notice_gate_fact_ids, "notice_ids": _dedupe(notice_gate_ids)}, + "source_integrity_warnings": _dedupe(warnings), + "candidate_schema_closed": True, + "exception_schema_closed": True, + }, + } + _validate_candidate_bundle(bundle) + bundle_text = json.dumps({"fact_ledger_candidate_bundle": bundle}, ensure_ascii=False, indent=2) + "\n" + _write_text_doc(PACK_OUT_PATH, pack_text) + _write_text_doc(CANDIDATE_OUT_PATH, bundle_text) + pack_reread = _read_doc_raw_text(PACK_OUT_PATH) + if hashlib.sha256(pack_reread.encode("utf-8")).hexdigest() != pack_sha: + raise ValueError("post-write reread validation failed: exception pack sha mismatch") + bundle_reread = _read_doc(CANDIDATE_OUT_PATH) + if _as_dict(_as_dict(bundle_reread).get("fact_ledger_candidate_bundle")).get("schema_version") != "stage1_fact_ledger_candidate_bundle.v2": + raise ValueError("post-write reread validation failed: candidate bundle") + print(json.dumps({ + "status": bundle["status"], + "message": "F0 fact ledger reducer complete", + "candidate_path": CANDIDATE_OUT_PATH, + "exception_pack_path": PACK_OUT_PATH, + "fact_count": len(rows), + "has_exceptions": bool(exceptions), + "exception_count": len(exceptions), + "deterministic_review_count": len(deterministic_reviews), + "les_inheritance_count": len(les_by_fact_id), + "removed_inputs": REMOVED_INPUTS, + }, ensure_ascii=False)) + except Exception as exc: + print(json.dumps({ + "status": "FAILED", + "message": str(exc)[:500], + "candidate_path": CANDIDATE_OUT_PATH, + "exception_pack_path": PACK_OUT_PATH, + }, ensure_ascii=False)) + raise + + + if __name__ == "__main__": + main() + - task_name: Task_F1_fact_exception_adjudicator + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: low + llm_verbosity: low + max_iterations: 1 + use_tools: [localdocs] + cache_control: + mode: auto + ttl: 20m + preflight: true + preflight_files: + - stage1_tmp/fact_ledger/fact_exception_pack.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + + TASK_NAME: Task_F1_fact_exception_adjudicator + STAGE: Stage 1 Fact Ledger 직접 컴파일러 — 조건부 예외 판정 (Part 1 v3 GB / Part 2 v3 R1 / Part 3 v2 L1 패턴) + MISSION: 결정론 reducer(F0)가 non-deferrable로 판정한 compact 투영 예외만 판정한다. 파일 병합·최종 파일 작성·사실 창작·LES 모호성 재판정은 하지 않는다. + + + + - 20년 이상 경력을 지닌 대한민국 최고 수준 민사송무 변호사이자 세계적 수준의 AI 아키텍트의 관점에서 판정한다. + - 유일한 입력은 preflight로 제공된 `stage1_tmp/fact_ledger/fact_exception_pack.json`이다. + - `Fact_Ledger_base_candidate.json`·`BO.json`·`legal_effect_structures.json`·`client_goal.json`·evidence 파일·signal 파일·raw 원문을 읽거나 요청하지 않는다. + - pack에 없는 exception_id·fact_id·BO ID·evidence index·structure/cluster ID·사실·당사자·일자·금액·출처를 창작하지 않는다. + - LES2가 확정한 issue cluster·asset cluster·candidate structure type 결정을 재판정하지 않는다. BO들이 같은 법적 쟁점·자산군·구조 유형에 속하는지도 판단하지 않는다. + - `Fact_Ledger_base.json`·candidate bundle·writer report를 작성하지 않는다. 출력 파일은 `stage1_tmp/fact_ledger/fact_adjudication.json` 하나뿐이다. + - markdown fence·prose 없이 raw JSON만 파일에 쓴다. + + + + - `unresolved_asset_cluster_projection`은 자산들이 동일한 법적 자산군인지의 판단을 허용하지 않는다. 이미 확정된 asset cluster의 투영 값 선택만 허용한다. + - `unresolved_structure_role_projection`은 candidate structure type의 선택·변경을 허용하지 않는다. 이미 확정된 구조의 role/module 투영 값 선택만 허용한다. + - 판정이 LES2 법적 구조 모호성의 재개를 요구하면 그 항목은 BLOCK_REVIEW로 차단하고 `adjudication_gate.reopened_les_ambiguity`를 true로 표시한다. + - `trigger_reason`·`conflict_flags`에 `legal_theory` 표지가 있으면 PATCH를 금지한다 — BLOCK_REVIEW(하류 법리 모듈 소관). + + + + 1. preflight로 제공된 exception pack의 `has_exceptions`를 확인한다. + 2. `has_exceptions == false`이면: `write_file(overwrite=true)`로 아래 no-exception 판정 객체를 `stage1_tmp/fact_ledger/fact_adjudication.json`에 저장하고 `"NO_EXCEPTIONS"`만 출력한 뒤 즉시 종료한다(terminate). 다른 어떤 파일도 읽지 않는다. + {"fact_exception_adjudication": {"schema_version": "stage1_fact_exception_adjudication.v1", "status": "READY_NO_EXCEPTIONS", "source_exception_pack_schema_version": "stage1_fact_ledger_exception_pack.v2", "decisions": [], "blocked_review_items": [], "adjudication_gate": {"input_exception_count": 0, "decision_count": 0, "blocked_count": 0, "coverage_pass": true, "json_only_contract_pass": true, "reopened_les_ambiguity": false}}} + 3. `has_exceptions == true`이면: `exceptions[]`의 각 항목을 그 항목에 내장된 `candidate_fact_row_subset`·`compact_context`(target_fields·source_backed_candidates)·`allowed_output_fields`·`allowed_decisions`·`conflict_flags`만으로 판정한다. 추가 read는 금지된다. + 4. 판정 규칙: + - `legal_calculation_required`: `compact_context.source_backed_candidates`의 상충 후보 중 문서 근거가 더 구체적인 단일 값을 선택해 PATCH하거나, 현행 행이 적정하면 KEEP_AS_IS. 후보 밖 값 창작 금지. + - `unresolved_asset_cluster_projection`: `source_backed_candidates.object_spec` 후보 중 등기행·별지목록 등 원문 ref 근거가 더 구체적인 값을 선택한다. 우열을 가릴 수 없으면 BLOCK_REVIEW. + - `unresolved_structure_role_projection`: role/module 투영 값을 후보 안에서 선택한다. 배열 보존이 기본값이며 축소 선택을 강제하지 않는다. + - PATCH의 patch 객체 키는 그 항목의 `allowed_output_fields` 안에서만 쓴다(F2가 위반 patch를 hard reject한다). patch 값은 `source_backed_candidates`에 실재하는 값이어야 한다. + - compact 근거로 확정할 수 없으면 반드시 BLOCK_REVIEW로 차단한다(확신 없는 확정 금지 — 인간 검토 라우팅). + - PATCH·KEEP_AS_IS 판정은 `decisions[]`에, BLOCK_REVIEW 판정은 `blocked_review_items[]`에 넣는다. 각 exception_id는 정확히 두 배열 중 한 곳에만 나타난다(F2가 exact coverage 등식으로 검증한다). + 5. `write_file(overwrite=true)`로 결과를 `stage1_tmp/fact_ledger/fact_adjudication.json`에 저장한다. + - 루트는 `fact_exception_adjudication`, `schema_version`은 `stage1_fact_exception_adjudication.v1`, `source_exception_pack_schema_version`은 `stage1_fact_ledger_exception_pack.v2`. + - `status`: 전 항목 비차단이면 `READY`, 차단 존재 시 `READY_WITH_BLOCKS`. + - decision 항목 필드: `exception_id`, `exception_type`(`legal_calculation_required|unresolved_asset_cluster_projection|unresolved_structure_role_projection`), `fact_id`, `source_bo_id`, `decision`(`PATCH|KEEP_AS_IS`), `patch`(PATCH일 때만, allowed_output_fields 키만), `review_required`, `reason_code`(`calculation_basis_selected|asset_projection_selected|structure_role_projected|keep_existing`), `basis_fields_used[]`(항목에 내장된 필드명의 부분집합), `conflict_flags_acknowledged[]`(항목 `conflict_flags`의 부분집합). + - blocked 항목 필드: `exception_id`, `exception_type`, `fact_id`, `source_bo_id`, `block_reason`(`insufficient_compact_record|reopens_les_ambiguity|unsupported_patch|missing_candidate_basis|schema_violation`). + - `adjudication_gate`: `input_exception_count`(pack의 예외 수와 동일), `decision_count`(decisions 배열 길이), `blocked_count`(blocked 배열 길이), `coverage_pass`(모든 exception_id가 정확히 한 곳에 나타날 때만 true), `json_only_contract_pass`, `reopened_les_ambiguity`. + - `exception_id`·`fact_id`·`source_bo_id`·`exception_type`은 pack 항목의 값을 그대로 인용한다(F2가 정체성 불일치를 hard fail 처리한다). + 6. `"F1 예외 판정 완료 (decisions=<건수>)"`만 출력하고 작업을 끝낸다(terminate). + + + + - Stage 1은 법률효과·청구원인을 확정하지 않는다. 여기 도달한 항목은 F2가 진행하려면 단일 투영 값 선택이 필수인 사안뿐이다. + - 판정은 Fact Ledger 행의 투영/조인 값 결정일 뿐 소유권·담보권·평가액·법리의 실체 판단이 아니다. + - 등기행·계약서·판결 등 문서 근거가 자유 문구보다 우선한다. 우열을 가릴 수 없으면 BLOCK_REVIEW. + - 복수 role/module/claim group은 배열형 최종 스키마에 보존되는 정상 상태다. 축소 선택을 강제하지 않는다. + + + + - exception pack 부재·파싱 불가: 즉시 중단하고 `FAILED: <사유>`만 출력한다. 판정 파일은 쓰지 않는다. + - tool 오류: 1회만 재시도. 재실패 시 `FAILED: `만 출력하고 종료한다. + + + - task_name: Task_F2_final_fact_ledger_gate_and_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import sys + from copy import deepcopy + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + CANDIDATE_PATH = "stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json" + PACK_PATH = "stage1_tmp/fact_ledger/fact_exception_pack.json" + ADJUDICATION_PATH = "stage1_tmp/fact_ledger/fact_adjudication.json" + TARGET_PATH = "Fact_Ledger_base.json" + REPORT_PATH = "stage1_tmp/fact_ledger/fact_ledger_writer_report.json" + FINAL_FIELDS = [ + "fact_id", "source_bo_id", "type", "date", "parties", "object_spec", "amount", "action", + "evidence_refs", "credibility", "legal_centrality", "proof_strength", "legal_effect_roles", + "claim_chain_ref", "linked_structures", "must_not_drop_in_claim_types", "must_consider", + "state_context", "money_claim_effect", "commercial_successor_effect", "actio_roles", + "linked_actio_structures", "downstream_module_candidates", "skeleton_validation_codes", + "derived_fact", "derivation_basis", "legal_calculation_object", + ] + FINAL_FIELD_SET = set(FINAL_FIELDS) + FINAL_DEFAULTS = { + "type": None, + "date": None, + "parties": [], + "object_spec": None, + "amount": None, + "action": None, + "evidence_refs": [], + "credibility": "low", + "legal_centrality": "low", + "proof_strength": "low", + "legal_effect_roles": [], + "claim_chain_ref": {}, + "linked_structures": {}, + "must_not_drop_in_claim_types": [], + "must_consider": False, + "state_context": {}, + "money_claim_effect": {}, + "commercial_successor_effect": {}, + "actio_roles": [], + "linked_actio_structures": {}, + "downstream_module_candidates": [], + "skeleton_validation_codes": [], + "derived_fact": "", + "derivation_basis": "", + "legal_calculation_object": {}, + } + GATE_REVIEW_CODES = { + "POSSESSION_CAUSE_CONFLICT", "NOTICE_RECEIPT_UNVERIFIED", "NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW", + "LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW", "LEGAL_CAUSE_STATE_UNKNOWN_REVIEW", + } + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-fact-ledger-f2-final-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + + def _strip_code_fence(text: str) -> str: + stripped = text.strip() + if stripped.startswith("```"): + first_newline = stripped.find("\n") + if first_newline >= 0: + stripped = stripped[first_newline + 1:] + if stripped.rstrip().endswith("```"): + stripped = stripped.rstrip()[:-3] + return stripped.strip() + + + def _lenient_json(raw: str, label: str) -> Any: + """SKILL §6.4/§6.7 관용 파서: strict 실패 시 코드펜스 제거 + raw_decode salvage(envelope 계층 포함).""" + text = (raw or "").strip() + if not text: + raise RuntimeError(f"Empty content for {label}") + try: + return json.loads(text) + except json.JSONDecodeError: + pass + text = _strip_code_fence(text) + try: + return json.loads(text) + except json.JSONDecodeError: + start = text.find("{") + alt = text.find("[") + if alt >= 0 and (start < 0 or alt < start): + start = alt + if start < 0: + raise RuntimeError(f"Content of {label} is not valid JSON: {text[:300]}") + try: + obj, _ = JSON_DECODER.raw_decode(text[start:]) + return obj + except json.JSONDecodeError as exc: + raise RuntimeError(f"Content of {label} is not valid JSON: {text[:300]}") from exc + + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result["result"].get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + text = content[0].get("text", "") + envelope = _lenient_json(text, doc_name) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + first = results[0] + if isinstance(first, dict) and first.get("error"): + raise RuntimeError(f"{doc_name}: {first['error']}") + inner = first.get("content", "") if isinstance(first, dict) else "" + if inner in (None, ""): + raise RuntimeError(f"Empty content for {doc_name}") + if isinstance(inner, str): + return _lenient_json(inner, doc_name) + return inner + return envelope + + + def _read_doc(doc_name: str) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + return _extract_json_from_read_docs_result(result, doc_name) + + + def _read_doc_raw_text(doc_name: str) -> str | None: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + content = result["result"].get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + envelope = _lenient_json(content[0].get("text", ""), doc_name) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if results and isinstance(results[0], dict): + raw = results[0].get("content") + if isinstance(raw, str): + return raw + return None + + + def _write_doc(doc_name: str, content: str) -> None: + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=_next_msg_id()) + + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + if value is None: + return out + if isinstance(value, (str, int, float)): + s = str(value).strip() + return [s] if s else [] + if isinstance(value, list): + for item in value: + out.extend(_strings(item)) + return _dedupe(out) + + + def _dedupe(values: list[str]) -> list[str]: + seen: set[str] = set() + out: list[str] = [] + for value in values: + if value and value not in seen: + seen.add(value) + out.append(value) + return out + + + def _load_candidate_bundle() -> dict[str, Any]: + outer = _read_doc(CANDIDATE_PATH) + bundle = _as_dict(_as_dict(outer).get("fact_ledger_candidate_bundle")) + if bundle.get("schema_version") != "stage1_fact_ledger_candidate_bundle.v2": + raise ValueError("candidate bundle schema_version mismatch (v2 required)") + if bundle.get("status") not in {"READY", "READY_WITH_EXCEPTIONS"}: + raise ValueError("candidate bundle status invalid") + items = _as_list(bundle.get("candidate_items")) + if not items: + raise ValueError("candidate_items is empty") + for item in items: + missing = [field for field in FINAL_FIELDS if field not in item] + if missing: + raise ValueError(f"candidate item missing fields: {missing}") + extra = [field for field in item if field not in FINAL_FIELD_SET] + if extra: + raise ValueError(f"candidate item has arbitrary fields: {extra}") + manifest = _as_dict(bundle.get("ref_manifest")) + for key in ("bo_ids", "structure_ids", "evidence_ids"): + if not isinstance(manifest.get(key), list): + raise ValueError(f"ref_manifest.{key} must be list") + return bundle + + + def _load_exception_pack(bundle: dict[str, Any]) -> dict[str, Any]: + """X-6: pack은 bundle의 exception_pack_ref 경유로만 읽고 sha256·expected id를 대조한다.""" + ref = _as_dict(bundle.get("exception_pack_ref")) + if ref.get("path") != PACK_PATH: + raise ValueError("exception_pack_ref path mismatch") + raw_text = _read_doc_raw_text(PACK_PATH) + if isinstance(raw_text, str) and ref.get("sha256"): + digest = hashlib.sha256(raw_text.encode("utf-8")).hexdigest() + if digest != ref.get("sha256"): + raise ValueError("exception pack sha256 mismatch against candidate bundle ref") + pack = _lenient_json(raw_text, PACK_PATH) if isinstance(raw_text, str) else _read_doc(PACK_PATH) + pack = _as_dict(pack) + if pack.get("schema_version") != "stage1_fact_ledger_exception_pack.v2": + raise ValueError("exception pack schema_version mismatch (v2 required)") + expected_ids = _strings(ref.get("expected_exception_ids")) + pack_ids = [str(_as_dict(e).get("exception_id")) for e in _as_list(pack.get("exceptions"))] + if sorted(expected_ids) != sorted(pack_ids): + raise ValueError("exception pack ids mismatch against candidate bundle ref") + return pack + + + def _load_adjudication() -> dict[str, Any]: + outer = _read_doc(ADJUDICATION_PATH) + adjudication = _as_dict(_as_dict(outer).get("fact_exception_adjudication")) + if adjudication.get("schema_version") != "stage1_fact_exception_adjudication.v1": + raise ValueError("fact_exception_adjudication schema_version mismatch") + if adjudication.get("status") not in {"READY", "READY_NO_EXCEPTIONS", "READY_WITH_BLOCKS"}: + raise ValueError(f"fact_exception_adjudication status invalid: {adjudication.get('status')}") + return adjudication + + + def _gate_int(gate: dict[str, Any], key: str) -> int: + value = gate.get(key) + if isinstance(value, bool) or not isinstance(value, int): + return -1 + return value + + + def _check_decision_identity(entry: dict[str, Any], exception: dict[str, Any], label: str) -> None: + for key in ("exception_type", "fact_id", "source_bo_id"): + if str(entry.get(key) or "") != str(exception.get(key) or ""): + raise ValueError(f"{label} {entry.get('exception_id')}: {key} mismatch against exception pack") + + + def _validate_adjudication_coverage(adjudication: dict[str, Any], pack: dict[str, Any]) -> tuple[dict[str, dict[str, Any]], list[dict[str, Any]]]: + """X-6①: exact coverage 등식 — expected == decided ∪ blocked, decided ∩ blocked == ∅. + 중복 decision·미지 ID·예외 0건의 임의 decision·gate 수치 불일치·정체성 불일치는 전부 hard fail.""" + exceptions = { + str(exc.get("exception_id")): exc + for exc in _as_list(pack.get("exceptions")) + if isinstance(exc, dict) and exc.get("exception_id") + } + expected = set(exceptions) + raw_decisions = _as_list(adjudication.get("decisions")) + raw_blocked = _as_list(adjudication.get("blocked_review_items")) + gate = _as_dict(adjudication.get("adjudication_gate")) + if not expected: + if adjudication.get("status") != "READY_NO_EXCEPTIONS": + raise ValueError("no exceptions expected; adjudication status must be READY_NO_EXCEPTIONS") + if raw_decisions or raw_blocked: + raise ValueError("no exceptions expected; decisions and blocked_review_items must be empty") + if gate.get("input_exception_count") not in (0, None): + raise ValueError("adjudication_gate.input_exception_count must be 0 when no exceptions exist") + return {}, [] + blocked_items: list[dict[str, Any]] = [] + blocked_ids: set[str] = set() + for item in raw_blocked: + entry = _as_dict(item) + exception_id = str(entry.get("exception_id") or "") + if not exception_id: + raise ValueError("blocked_review_item missing exception_id") + if exception_id not in expected: + raise ValueError(f"blocked_review_item references unknown exception_id: {exception_id}") + if exception_id in blocked_ids: + raise ValueError(f"duplicate blocked_review_item for exception_id: {exception_id}") + _check_decision_identity(entry, exceptions[exception_id], "blocked_review_item") + blocked_ids.add(exception_id) + blocked_items.append(entry) + decisions_by_id: dict[str, dict[str, Any]] = {} + for item in raw_decisions: + decision = _as_dict(item) + exception_id = str(decision.get("exception_id") or "") + if not exception_id: + raise ValueError("decision missing exception_id") + if exception_id not in expected: + raise ValueError(f"decision references unknown exception_id: {exception_id}") + if exception_id in decisions_by_id: + raise ValueError(f"duplicate decision for exception_id: {exception_id}") + _check_decision_identity(decision, exceptions[exception_id], "decision") + decision_type = decision.get("decision") + if decision_type == "BLOCK_REVIEW": + if exception_id not in blocked_ids: + blocked_ids.add(exception_id) + blocked_items.append({ + "exception_id": exception_id, + "exception_type": decision.get("exception_type"), + "fact_id": decision.get("fact_id"), + "source_bo_id": decision.get("source_bo_id"), + "block_reason": str(decision.get("reason_code") or "insufficient_compact_record"), + }) + continue + if decision_type not in {"PATCH", "KEEP_AS_IS"}: + raise ValueError(f"unknown decision enum: {decision_type}") + decisions_by_id[exception_id] = decision + decided = set(decisions_by_id) + overlap = decided & blocked_ids + if overlap: + raise ValueError(f"coverage violation: decided and blocked overlap: {sorted(overlap)}") + if expected != (decided | blocked_ids): + missing = sorted(expected - (decided | blocked_ids)) + raise ValueError(f"coverage equation failed: missing={missing}") + if _gate_int(gate, "input_exception_count") != len(expected): + raise ValueError("adjudication_gate.input_exception_count mismatch") + if _gate_int(gate, "decision_count") != len(raw_decisions): + raise ValueError("adjudication_gate.decision_count mismatch") + if _gate_int(gate, "blocked_count") != len(raw_blocked): + raise ValueError("adjudication_gate.blocked_count mismatch") + if not gate.get("coverage_pass"): + raise ValueError("adjudication_gate.coverage_pass must be true when exceptions exist") + return decisions_by_id, blocked_items + + + def _exception_allowed_fields(pack: dict[str, Any]) -> dict[str, list[str]]: + out: dict[str, list[str]] = {} + for exc in _as_list(pack.get("exceptions")): + if isinstance(exc, dict) and exc.get("exception_id"): + out[str(exc["exception_id"])] = _strings(exc.get("allowed_output_fields")) + return out + + + def _add_code(row: dict[str, Any], code: str) -> None: + codes = row.setdefault("skeleton_validation_codes", []) + if isinstance(codes, list) and code not in codes: + codes.append(code) + + + def _append_basis(row: dict[str, Any], text: str) -> None: + basis = str(row.get("derivation_basis") or "") + row["derivation_basis"] = (basis + "; " + text).strip("; ") + + + def _apply_patch(row: dict[str, Any], decision: dict[str, Any], allowed_fields: list[str], report: dict[str, Any]) -> bool: + """X-6②: patch key가 allowed_output_fields ∩ FINAL_FIELD_SET 밖이면 decision 전체를 거부하고 + 해당 fact를 BLOCK 처리한다. '경고 후 적용'(v2 방식)은 금지.""" + patch = _as_dict(decision.get("patch")) + exception_id = str(decision.get("exception_id") or "") + allowed = set(allowed_fields) & FINAL_FIELD_SET + bad = sorted(key for key in patch if key not in allowed) + if bad: + report["rejected_patches"].append({ + "exception_id": exception_id, + "fields": bad, + "reason": "PATCH_FIELD_OUTSIDE_ALLOWED_OUTPUT_FIELDS", + }) + _add_code(row, "BLOCK_REVIEW_PRESENT") + row["must_consider"] = True + _append_basis(row, f"rejected_patch={exception_id}") + return False + for key, value in patch.items(): + row[key] = deepcopy(value) + return True + + + def _validate_refs(row: dict[str, Any], bo_ids: set[str], structure_ids: set[str], evidence_ids: set[str], report: dict[str, Any]) -> None: + if row.get("source_bo_id") not in bo_ids: + raise ValueError(f"unknown source_bo_id: {row.get('source_bo_id')}") + for sid in _strings(_as_dict(row.get("linked_structures")).get("structure_ids")): + if sid not in structure_ids: + report["row_warnings"].append({"fact_id": row.get("fact_id"), "warning": "UNKNOWN_LINKED_STRUCTURE_ID", "value": sid}) + _add_code(row, "UNKNOWN_LINKED_STRUCTURE_ID") + for eid in _strings(row.get("evidence_refs")): + if eid not in evidence_ids: + report["row_warnings"].append({"fact_id": row.get("fact_id"), "warning": "EVIDENCE_REF_GAP", "value": eid}) + _add_code(row, "EVIDENCE_REF_GAP") + + + def _normalize_final_row(row: dict[str, Any], ordinal: int) -> dict[str, Any]: + out = {field: deepcopy(row.get(field, FINAL_DEFAULTS.get(field))) for field in FINAL_FIELDS} + out["fact_id"] = f"F-{ordinal:03d}" + out["source_bo_id"] = str(out.get("source_bo_id") or "") + for key in ("parties", "evidence_refs", "legal_effect_roles", "must_not_drop_in_claim_types", "actio_roles", "downstream_module_candidates", "skeleton_validation_codes"): + out[key] = _as_list(out.get(key)) + for key in ("claim_chain_ref", "linked_structures", "state_context", "money_claim_effect", "commercial_successor_effect", "linked_actio_structures", "legal_calculation_object"): + out[key] = _as_dict(out.get(key)) + out["credibility"] = out.get("credibility") if out.get("credibility") in {"high", "medium", "low"} else "low" + out["legal_centrality"] = out.get("legal_centrality") if out.get("legal_centrality") in {"low", "medium", "critical"} else "low" + out["proof_strength"] = out.get("proof_strength") if out.get("proof_strength") in {"low", "medium", "high", "documentary", "meeting_only_core"} else "low" + if not out.get("derived_fact"): + raise ValueError(f"{out['source_bo_id']}: derived_fact is empty") + return out + + + def main() -> None: + try: + _init_mcp_session() + bundle = _load_candidate_bundle() + candidate_items = [deepcopy(x) for x in _as_list(bundle.get("candidate_items")) if isinstance(x, dict)] + exception_pack = _load_exception_pack(bundle) + adjudication = _load_adjudication() + decisions_by_id, blocked_items = _validate_adjudication_coverage(adjudication, exception_pack) + allowed_fields_by_exception = _exception_allowed_fields(exception_pack) + rows_by_fact_id = {str(row.get("fact_id")): row for row in candidate_items} + compile_gate = _as_dict(bundle.get("compile_gate")) + les_inheritance = _as_dict(_as_dict(bundle.get("les_review_inheritance")).get("by_fact_id")) + deterministic_review_items = _as_list(compile_gate.get("deterministic_review_items")) + report: dict[str, Any] = { + "schema_version": "stage1_fact_ledger_writer_report.v1", + "status": "READY", + "blocked_review_items": blocked_items, + "rejected_patches": [], + "patch_policy_warnings": [], + "unapplied_patch_fields": [], + "row_warnings": [], + "possession_conflict_gate": {"status": "PASS", "conflicts": []}, + "drafting_gates": [], + "gate_review_code_summary": {}, + "les_review_inheritance": [les_inheritance[fact_id] for fact_id in sorted(les_inheritance)], + "deterministic_review_items": deterministic_review_items, + } + for fact_id in sorted(les_inheritance): + if fact_id not in rows_by_fact_id: + raise ValueError(f"les_review_inheritance references unknown fact_id: {fact_id}") + for blocked in blocked_items: + fact_id = str(_as_dict(blocked).get("fact_id") or "") + row = rows_by_fact_id.get(fact_id) + if not row: + raise ValueError(f"blocked_review_item references unknown fact_id: {fact_id}") + _add_code(row, "BLOCK_REVIEW_PRESENT") + row["must_consider"] = True + _append_basis(row, "blocked_review=" + str(_as_dict(blocked).get("block_reason") or "unspecified")) + resolved_count = 0 + for exception_id in sorted(decisions_by_id): + decision = decisions_by_id[exception_id] + fact_id = str(decision.get("fact_id") or "") + row = rows_by_fact_id.get(fact_id) + if not row: + raise ValueError(f"decision references unknown fact_id: {fact_id}") + decision_type = decision.get("decision") + if decision_type == "KEEP_AS_IS": + resolved_count += 1 + continue + applied = _apply_patch(row, decision, allowed_fields_by_exception.get(exception_id, []), report) + if applied: + resolved_count += 1 + if decision.get("review_required"): + _add_code(row, "FL2_REVIEW_REQUIRED") + manifest = _as_dict(bundle.get("ref_manifest")) + bo_ids = set(_strings(manifest.get("bo_ids"))) + structure_ids = set(_strings(manifest.get("structure_ids"))) + evidence_ids = set(_strings(manifest.get("evidence_ids"))) + final_rows: list[dict[str, Any]] = [] + for ordinal, row in enumerate(candidate_items, start=1): + normalized = _normalize_final_row(row, ordinal) + _validate_refs(normalized, bo_ids, structure_ids, evidence_ids, report) + extra = [key for key in normalized if key not in FINAL_FIELD_SET] + if extra: + raise ValueError(f"final row has arbitrary fields: {extra}") + final_rows.append(normalized) + # conservation: 후보 BO 집합 == 최종 행 BO 집합, F-### 연속·유일. + candidate_bo_ids = sorted(str(row.get("source_bo_id") or "") for row in candidate_items) + final_bo_ids = sorted(str(row.get("source_bo_id") or "") for row in final_rows) + if candidate_bo_ids != final_bo_ids: + raise ValueError("BO conservation violated between candidate and final rows") + fact_ids = [row["fact_id"] for row in final_rows] + if fact_ids != [f"F-{i:03d}" for i in range(1, len(final_rows) + 1)]: + raise ValueError("fact_id sequence must be continuous and unique") + gate_code_rows: dict[str, list[dict[str, Any]]] = {} + for row in final_rows: + for code in _as_list(row.get("skeleton_validation_codes")): + if code in GATE_REVIEW_CODES: + gate_code_rows.setdefault(str(code), []).append(row) + conflict_rows = gate_code_rows.get("POSSESSION_CAUSE_CONFLICT", []) + report["possession_conflict_gate"] = { + "status": "HARD_WARNING" if conflict_rows else "PASS", + "conflicts": [{"fact_id": row.get("fact_id"), "source_bo_id": row.get("source_bo_id")} for row in conflict_rows], + } + notice_gate_rows = gate_code_rows.get("NOTICE_RECEIPT_UNVERIFIED", []) + if notice_gate_rows: + notice_gate = _as_dict(compile_gate.get("notice_gate")) + report["drafting_gates"].append({ + "gate": "BLOCK_FINAL_DRAFTING_NOTICE_RECEIPT_MISSING", + "row_code": "NOTICE_RECEIPT_UNVERIFIED", + "notice_ids": _strings(notice_gate.get("notice_ids")), + "affected_fact_ids": [row.get("fact_id") for row in notice_gate_rows], + }) + report["gate_review_code_summary"] = {code: [row.get("fact_id") for row in code_rows] for code, code_rows in sorted(gate_code_rows.items())} + # X-6③ status 3단 규칙: blocked/거부 patch/결정론 review/LES 승계/게이트 review 존재 → READY_WITH_REVIEW. + has_review = bool( + blocked_items + or report["rejected_patches"] + or deterministic_review_items + or les_inheritance + or report["gate_review_code_summary"] + ) + report["status"] = "READY_WITH_REVIEW" if has_review else "READY" + _write_doc(TARGET_PATH, json.dumps(final_rows, ensure_ascii=False, indent=2)) + _write_doc(REPORT_PATH, json.dumps(report, ensure_ascii=False, indent=2)) + reread = _read_doc(TARGET_PATH) + if not isinstance(reread, list) or len(reread) != len(final_rows): + raise ValueError("post-write reread validation failed: target is not the expected top-level array") + print(json.dumps({ + "status": report["status"], + "message": "F2 final fact ledger gate and writer complete", + "written_file": TARGET_PATH, + "report_file": REPORT_PATH, + "fact_count": len(final_rows), + "blocked_review_count": len(blocked_items), + "resolved_exception_count": resolved_count, + "deterministic_review_count": len(deterministic_review_items), + "les_inheritance_count": len(les_inheritance), + "drafting_gate_count": len(report["drafting_gates"]), + }, ensure_ascii=False)) + except Exception as exc: + print(json.dumps({ + "status": "FAILED", + "message": str(exc)[:500], + "written_file": TARGET_PATH, + "report_file": REPORT_PATH, + }, ensure_ascii=False)) + raise + + + if __name__ == "__main__": + main() diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Codex YAML/Stage_1_Part_1_Codex_v3.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Codex YAML/Stage_1_Part_1_Codex_v3.yml new file mode 100644 index 00000000..9de1a4f5 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Codex YAML/Stage_1_Part_1_Codex_v3.yml @@ -0,0 +1,3937 @@ +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_1 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Part 1 기초작업 + version: v3.0 + + Stages: + - name: stage1_사건개요파악_기초작업 + description: 사건개요 파악, 기초정보 추출 + llm_provider: openai + llm_model: gpt-4o-2024-08-06 + tools: + mcpServers: + localdocs: + type: streamable-http + url: "http://mcp-localdocs:8012/mcp" + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + + tasks: + - task_name: Task_A_client_goal + llm_provider: google + llm_model: 'gemini-3.1-flash-lite' + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + preflight: false + preflight_files: + - client_meeting.md + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + - Aggregate, compile, publish, commit, or write only the inputs and final/derived file authorized by the current task. + - Preserve stable order, source IDs, support IDs, BO_ID, fact_id, structure_id, claim_group_id, liability_group_id, and required enum semantics exactly. + - Create or finalize IDs only from the current task's specified pattern and deterministic ordering rule. + - Do not merge, deduplicate, supersede, normalize, repair, or drop candidates unless the current reducer/writer schema authorizes that action. + - If validation or source support is insufficient, use the current failure, rollback, warning, or blocked path instead of fabricating a passing final artifact. + - Treat downstream authority and handoff fields as machine contracts; do not add prose, alternative files, or extra writer attestations outside the schema. + + + + + Task_A_client_goal + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. + + + `client_meeting.md`만 읽고 `client_goal.json`을 생성한다. + 이 파일은 고객의 소송 목적, 명시적 제약, 핵심 사건군, 당사자 universe를 Stage 1의 가장 앞단에서 안정적으로 구조화한 요약본이어야 한다. + v.3에서도 기존 필수 schema는 유지하되, 복수 원고와 복수 분쟁군이 뒤섞여 later stage가 공동원고를 과잉 일반화하지 않도록 사건군 단위의 optional 구조, 사건군별 원고적격 구조, 현재 지배상태 구조를 함께 남긴다. + 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않는다. + 특히 상속·승계형 사건군에서는 meeting만으로 확정되지 않은 친족관계 또는 공동행동 정보를 direct standing anchor처럼 과잉 일반화하지 않는다. + 상속이 개입된 사건군에서는 `legal entitlement layer`와 `complaint drafting preference layer`를 분리하여 기록한다. + 사망한 종전 권리주체의 과거 소유사실, 매수사실, 등기명의사실을 living plaintiff의 현재 `소유자/final`로 치환하지 않는다. + 필요하면 사건군별 optional additive field `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`, `preferred_plaintiff_selection_basis`, `succession_resolution_needed`를 사용하여, later stage가 실체법상 가능한 권리자 후보층과 실제 소장에 세울 우선 원고층을 혼동하지 않도록 한다. + 또한 `current_control_map`은 보조 메모가 아니라 later stage의 부당이득·인도·철거 청구 식별에 직접 쓰이는 authority map이므로, 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속을 가능한 한 4축으로 분해 저장한다. + v.3 기준으로 Task_A_client_goal은 모든 issue cluster에 사건군별 법률효과 구조의 seed를 남겨야 한다. 단순 사건 요약이 아니라 금전채권 timeline, 상호속용 책임, 담보부 부동산·지분별 권리상태, 사해행위취소 가액배상, 유치권·상속·통지 lifecycle을 downstream이 구조적으로 읽을 수 있게 `domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints`, `legal_entitlement_candidates`, `current_control_map_4axis`를 additive하게 생성한다. + `client_requested_relief`는 의뢰인이 원하는 청구 기간·금액·상대방을 보존하는 층이고, `legal_sustainability_watchpoints`는 법률상 기산일·원고적격·청구범위 재검토 필요성을 보존하는 층이다. 두 층을 절대 하나로 합치지 않는다. + 상속·승계형 사건군에서 current right holder가 meeting만으로 확정되지 않으면 `current_control_map`의 해당 row는 `certainty="provisional"`로 두고 `standing_watchpoints`에 succession-resolution trigger를 반드시 남긴다. + 또한 현재 무자력/집행난과 실제 제소 유지 의사를 혼동하지 않으며, 특정 상대방을 현재 무자력이라는 이유만으로 소 제기 대상에서 소거하지 않는다. + 필요하면 optional additive field `defendant_target_matrix`를 사용하여 피고별 소제기 유지/제외 의사, 현재 회수위험, 장래 회수신호를 분리 보존한다. + 본 task는 v.3 architecture에서 final writer로서 `client_goal.json`을 직접 작성한다(Task_A는 island/mapper가 없으므로 propose 단계 없이 직접 finalize 가능). + + + + + - In this task, legal/domain fields are meeting-grounded routing seeds and watchpoints for downstream stages. + - They are not final legal conclusions, norm search results, merits judgments, or complaint-drafting advice. + - The EXTRACTION overlay's prohibition on norm search and final legal-effect classification does not prohibit schema-authorized `domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints`, or entitlement/watchpoint seed fields. + + + + + - `client_meeting.md` + + + - `client_goal.json` + + + - 다른 파일 확인/읽기 + - 중간 파일 생성 + - `client_goal.json` 재읽기 + - meeting에 없는 당사자, 사건군, 제약, 희망사항, 청구유형을 창작하기 + - 사건군별 당사자 범위를 무시하고 전 사건 공통 사실처럼 과잉 압축하기 + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하기 + + + + + 1. `list_docs`로 `client_meeting.md` 존재만 확인한다. + 2. `read_docs`로 `client_meeting.md`만 읽는다. + 3. 메모리에서 `client_goal.json` 객체를 생성한다. + 4. `write_file(overwrite=true)`로 `client_goal.json`을 저장한다. + 5. `list_docs`로 `client_goal.json` 존재만 확인한다. 절대 읽지 않는다. + 6. 정확히 `"고객의사 초안 생성 완료"`만 출력하고 종료한다. + + + + - 필수 입력 누락, 빈 파일, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. 파일은 쓰지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + + - `client_meeting.md`만 사실 원천이다. + - 상대방 주장, 의뢰인 희망사항, 상담서두의 인적 관계, 본문 1~5항의 사건군을 모두 읽되, meeting에 직접 나타난 범위만 구조화한다. + - meeting의 문장들 사이를 조합해 새로운 사실을 만들지 않는다. + + + + + - `primary_goal`은 사건 전체를 한 문장으로 요약하되, 개별 분쟁군을 완전히 뭉개지 않을 정도의 수준으로만 추상화한다. + - `희망사항`이 복수이면 공통 분모를 중심으로 요약하되, 특정 핵심 구제수단이 반복되면 반영할 수 있다. + - 희망사항이 피고별 비대칭 전략을 직접 드러내면, 그 비대칭을 보존한다. + - 현재 회수곤란 사실이 있더라도 장래 재산취득, 상속, 보험금 회수, 급여압류 가능성 등으로 특정 피고를 제소 대상으로 유지하려는 의사가 직접 서술되면, 이를 `primary_goal` 또는 `summary_key_incidents`에 반영한다. + - 직접 금전청구 대상과 사해행위취소 대상이 다르면 한쪽으로 뭉개지 않는다. + - 불명확하면 `null`. + + + + - `constraints`에는 의뢰인이 명시적으로 원하지 않는 소, 특정 대상 제외, 가능하면/다만/아울러와 같은 제한적 요청, 상속·경매 공신력 등 사건 진행상의 명시적 제약을 넣는다. + - `constraints`는 소송전략상 하지 말 것, 제한할 것, 절차상·법률상 한계만 담는다. + - 단순한 회수곤란 사실, 현재 무자력, 현재 재산 부족, 집행난은 `constraints`에 넣지 않는다. + - 현재 무자력과 장래 재산취득 가능성이 함께 서술되면, 이를 부정적 제약으로 압축하지 않는다. + - 특정 상대방을 제외하라는 명시적 no-sue 지시가 없는 한, current collectability만으로 대상 제외 제약을 만들지 않는다. + - 해석상 추정 제약은 넣지 않는다. + - 배열 순서는 meeting 서술 순서를 최대한 따른다. + + + + - `summary_key_incidents`는 최대 4문장, 각 100자 이하. + - 각 문장은 가능한 한 서로 다른 사건군을 대표하도록 구성한다. + - 사건군 라벨은 입력자료에 직접 나타난 분쟁 대상, 거래유형, 자산군, 청구 방향을 짧게 추상화하여 만든다. 특정 사건의 지명·상호·인명·자산명을 예시값처럼 재사용하지 않는다. + - 하나의 문장에 서로 다른 사건군을 과도하게 병합하지 않는다. + - 직접 서술된 날짜/당사자/핵심 행위만 사용한다. + - 원고가 특정 피고를 직접 금전청구 대상으로 유지하려는 의사가 직접 서술되면, 사건군 대표 문장 중 하나에 그 방향성을 반영할 수 있다. + - 현재 회수곤란 사실과 장래 회수가능성 신호가 같은 사람에 관하여 함께 직접 서술되면, 둘 중 하나만 선택적으로 삭제하지 않는다. + + + + - `key_facts`는 최대 5개, 각 60자 이하. + - 가능한 한 사건군별 핵심 fact를 1개씩 대표시킨다. + - later stage의 원고적격 판단에 중요할 수 있는 소유관계, 상속관계, 현재 권리귀속, 현재 점유/사용/수익귀속 상태, 특정 처분/경매/통지 사실을 우선한다. + - 직접 서술이 없는 금액·날짜·상태를 보충하지 않는다. + + + + - `claim_type_candidates`는 의뢰인의 희망사항과 사건군 구조를 바탕으로 선택한다. + - 허용 enum만 사용한다: `"금전"|"물건인도"|"행위"|"확인"|"형성"|"보전(가처분/가압류)"`. + - later stage 분류기를 돕기 위한 상위 유형 수준만 남기고, 세부 청구명을 쓰지 않는다. + + + + + + - 이름은 원문 우선, 명백히 동일인/동일법인인 경우만 canonicalize 한다. + - 별칭/변형은 `aliases`에 기록한다. + - 당사자 배열은 사건 전체 participant universe다. 모든 분쟁군의 실질 원고적격/피고적격을 의미하지 않는다. + + + + - `parties.plaintiffs`에는 실제 의뢰인 또는 meeting 서두와 희망사항에서 원고 측 client로 직접 읽히는 사람만 넣는다. + - `parties.plaintiffs`는 client universe일 뿐, 사건군별 실질 원고적격을 의미하지 않는다. + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않는다. + - 사건군별 원고 범위 차이는 `issue_clusters`, `plaintiff_capacity_matrix`, `standing_watchpoints`에서 보정한다. + + + + - `parties.defendants`에는 meeting 본문, 상대방 주장, 희망사항에서 분쟁 상대방 또는 소송상 직접 대상으로 명시된 자만 넣는다. + - `asset_status`는 meeting에 직접 나온 재산 상태, 현재 소유/점유/담보권 설정 상태 등 later stage에 직접 도움이 되는 짧은 상태만 넣는다. + - `asset_status`는 현재 재산 상태에 관한 사실기술일 뿐, 소 제기 대상 제외 신호가 아니다. + - meeting이 "현재 무자력이나 장래 상속/재산취득 가능성 있음"을 직접 서술하면, `asset_status`는 그 둘을 함께 보존하는 방향으로 적는다. + - 현재 무자력만을 이유로 defendants에서 제외하지 않는다. 제외는 명시적 no-sue 지시가 있을 때만 허용한다. + - meeting에 없는 재산상태 추정 금지. + + + + - `parties.third_parties`에는 사건군에 중요하지만 즉시 원고/피고로 확정되지 않은 자를 넣는다. + - `relationship`은 later stage가 읽을 수 있도록 짧고 직접적으로 적는다. + + + + + - optional additive field `defendant_target_matrix`를 허용한다. + - 이 필드는 피고별 소제기 유지/제외 의사와 현재 회수위험을 분리 기록하는 전략 매트릭스다. + - `target_status`는 제소 유지 여부를 나타내고, `execution_risk`는 current collectability를 나타낸다. + - 현재 무자력은 `execution_risk`에 반영하고, 제소 유지 여부는 `target_status`에 별도로 반영한다. + - 장래 상속, 장래 재산취득, 보험금 회수 가능성, 급여압류 가능성 등 미래 회수신호가 직접 서술되면 `future_recovery_signal`에 반영한다. + - 명시적 no-sue 지시가 없는 한, 단순한 current uncollectability만으로 `exclude_now`를 부여하지 않는다. + - 각 원소는 아래 구조를 따른다. + { + "name": "string", + "target_status": "target_now|exclude_now|secondary_target|review", + "basis_text": "meeting 직접 문장에 근거한 짧은 사유", + "execution_risk": "low|medium|high|unknown", + "future_recovery_signal": "none|possible|explicit" + } + - `basis_text`는 반드시 meeting 직접 문장에 근거한 짧은 근거여야 한다. + - 이 필드는 optional이다. 명시적 피고별 타기팅 차이가 없으면 생략할 수 있다. + + + + - optional additive field `issue_clusters`를 허용한다. + - 각 cluster는 meeting의 독립 분쟁군을 1개씩 반영한다. + - 이 필드는 later stage가 전 사건 공통 parties 배열만 보고 과잉 일반화하는 것을 줄이기 위한 보조 구조다. + - `candidate_plaintiffs`는 사건군별 잠정 plaintiff anchor일 뿐, 최종 원고적격 확정값이 아니다. + - `legal_entitlement_candidates`는 해당 사건군에서 meeting 직접 서술상 실체법상 권리자일 가능성이 있는 의뢰인 후보층이다. + - `preferred_plaintiffs_for_complaint`는 later stage가 실제 소장 원고로 우선 검토할 수 있는 narrower drafting layer다. + - 상속·승계형 사건군에서는 `candidate_plaintiffs`, `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`를 서로 자동 동일시하지 않는다. + - `candidate_plaintiffs`에는 meeting에서 해당 사건군의 직접 권리주체, 직접 채권자, 직접 임차인, 직접 점유권자 등으로 읽히는 의뢰인만 넣는다. + - 친족관계, 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 `candidate_plaintiffs`를 확장하지 않는다. + - 사망한 자가 해당 사건군의 종전 권리주체로 보이는데 meeting만으로 승계가 해소되지 않으면, 친족관계나 공동 통지 사실만으로 `candidate_plaintiffs`를 채우지 않는다. + - 위와 같은 unresolved succession cluster에서는 meeting이 직접 보여주는 친족관계와 자산 연결 범위 안에서만 `legal_entitlement_candidates`를 별도 기록할 수 있다. + - `preferred_plaintiffs_for_complaint`는 meeting이 특정 의뢰인을 직접 권리주체 또는 직접 소송유지 주체로 좁혀 보여주거나, 비상속형 사건군에서 direct right-holder가 명백할 때만 채운다. + - meeting만으로 drafting preference가 명백하지 않은 unresolved succession cluster에서는 `preferred_plaintiffs_for_complaint`를 빈 배열로 둘 수 있다. + - `preferred_plaintiff_selection_basis` 허용값은 `"direct_right_holder|resolved_succession|resolved_succession_needed|drafting_preference|review|불명"`이다. + - `succession_resolution_needed`는 사망한 종전 권리주체와 사건군의 연결은 보이지만 meeting만으로 현재 권리귀속 또는 complaint plaintiff narrowing이 해소되지 않을 때 `true`로 둔다. + - meeting이 직접 금전청구 대상 피고와 사해행위 상대방/수익자를 구별해 보여주면, 이를 별도 cluster로 분리할 수 있다. + - 직접 금전회수 대상이 특정 상대방으로 서술되면, property transfer cluster와 별도의 monetary recovery cluster를 허용한다. + - cluster 수는 1개 이상 6개 이하로 제한한다. + - 각 cluster는 아래 구조를 따른다. + { + "cluster_id": "IC-01", + "cluster_label": "짧은 사건군명", + "candidate_plaintiffs": ["이 사건군에서 직접 권리주체로 읽히는 의뢰인"], + "legal_entitlement_candidates": ["실체법상 권리자 후보로 읽히는 의뢰인"], + "preferred_plaintiffs_for_complaint": ["실제 소장 원고로 우선 검토할 의뢰인"], + "preferred_plaintiff_selection_basis": "direct_right_holder|resolved_succession|resolved_succession_needed|drafting_preference|review|불명", + "succession_resolution_needed": true, + "candidate_defendants_or_counterparties": ["직접 상대방"], + "desired_relief_keywords": ["금전", "명도", "퇴거", "확인", "보전" 등 짧은 키워드] + } + - 비상속형 사건군에서 direct right-holder가 명백하면 `candidate_plaintiffs`, `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`가 동일할 수 있다. + - `issue_clusters`는 optional이다. 명확하지 않으면 생략 가능하다. + - 추가 사실 창작 없이 meeting 본문과 희망사항에서 직접 읽히는 범위만 기록한다. + + + + - optional additive field `domain_profiles`를 각 `issue_clusters[*]` 내부에 허용한다. + - `domain_profiles`는 아래 네 하위 profile을 담는 object다. 해당하지 않는 profile은 `null`로 둔다. + - `monetary_claim_profile` + - `secured_property_dispute_profile` + - `actio_property_transfer_profile` + - `possession_lien_succession_profile` + - `client_requested_relief`와 `legal_sustainability_watchpoints`를 각 cluster 내부에 둘 수 있다. + - 의뢰인이 원하는 청구취지·기간·금액은 `client_requested_relief`에 보존하고, 법률상 기산일·원고적격·범위가 불명확한 점은 `legal_sustainability_watchpoints`에 분리한다. + - 당사자가 원하는 기간이나 금액을 법률상 청구취지에 바로 사용할 수 있다고 단정하지 않는다. + + + - 금전청구 사건군에서 원채무자 외 보증인, 채무인수자, 영업양수인, 상호속용자, 공동채무자 후보가 함께 등장하면 이를 별도 후보 피고 목록에만 두지 말고 `monetary_claim_profile`, `liability_group_hint`, `commercial_successor_liability_hint`로 묶어 보존한다. + - 동일 금액, 동일 원채권, 동일 원고를 기준으로 복수 피고가 책임질 가능성이 있으면 `liability_group_hint.same_underlying_debt=true`로 둔다. + - 지급기일, 이행청구, 최고, 채무승인, 이행각서, 소멸시효, 변제기 재설정 단서가 등장하면 `monetary_claim_profile.requires_money_claim_timeline=true`로 둔다. + - 영업양도, 상호 계속 사용, 사업 양수인, 채무승계 부인, 책임배제 등기 단서가 등장하면 `commercial_successor_liability_hint.requires_article_42_review=true`로 둔다. + + + + - 부동산 사건에서 여러 대여금, 여러 근저당권, 지분별 담보, 변제, 경매, 후행 등기가 함께 등장하면 이를 흩어진 issue로 분리하지 말고 `secured_property_dispute_profile`에 묶는다. + - 변제금이 복수 채무에 영향을 줄 수 있으면 `requires_payment_allocation=true`로 표시한다. + - 전체 부동산이 아니라 지분별 유효/무효가 문제될 수 있으면 `requires_fractional_validity_analysis=true`로 표시한다. + - 원고 주장 지분, 피고 측 유효 가능 지분, 전체 부동산 말소 금지 필요성을 `fractional_title_issue_hint`에 남긴다. + - 미등기 건물, 건물 신축, 건물 매매, 대지 임대차가 함께 등장하면 `land_use_gain_hint`를 생성한다. + - 임대차보증금, 월 차임, 건물 매매대금은 각각 법적 성격이 다르므로 `lease_payment_semantics_hint`에 별도 표시한다. + - 임대차보증금은 토지소유자의 부당이득 원금으로 자동 산정하지 말라는 watchpoint를 남긴다. + + + + - 채무자가 부동산을 수익자에게 이전하고, 원고가 사해행위취소를 희망하는 사건군을 발견하면 `actio_property_transfer_profile`을 생성한다. + - 부동산에 근저당권 등 부담이 있고, 그 부담이 사해행위 이후 변제·말소되었다는 단서가 있으면 `requires_value_compensation_review=true`로 표시한다. + - 같은 채권자와 채무자 사이에 복수 대여금이 등장하면 단순 후보채권 목록으로 흩뜨리지 말고 `preserved_claim_bundle_hint`로 묶는다. + - 피고가 명의신탁, 충분 담보, 상계, 선의 등을 주장하면 이를 `defense_hints`에 남기고, 청구원인 성립 또는 범위에 직접 영향을 주는 항변이면 `requires_stage2_rebuttal=true`로 표시한다. + + + + - 현재 점유자와 현재 권리자가 분리되는 자산군 또는 사건군이 입력자료에서 직접 확인되면 `possession_lien_succession_profile`을 생성한다. + - 상속이 개입되면 current right holder를 final로 확정하지 말고 `succession_resolution_needed=true`와 `standing_watchpoints`를 남긴다. + - 유치권, 제3자 임대, 직접 거주, 피담보채권 충당, 후소유자 소멸청구권 부정 주장이 보이면 `possession_basis_asserted`, `notice_lifecycle_hints`, `legal_sustainability_watchpoints`를 구조화한다. + - 통지서 발송일과 답변서상 수령 인정일이 함께 보이면 발송일과 도달일을 분리하여 `notice_lifecycle_hints`에 남긴다. + - 상속·승계가 미해결인 cluster에서는 입력자료상 공동 통지 발신자, 친족 후보, 직계존속·직계비속 후보를 그 지위의 후보로만 보존하고, 상속 해결 전 `preferred_plaintiffs_for_complaint` 확정자로 두지 않는다. + + + + + - optional additive field `current_control_map_4axis`를 허용한다. + - legacy `current_control_map` row 구조는 유지하되, 목적물별 4축 요약이 가능한 경우 `current_control_map_4axis`를 추가한다. + - 각 item은 아래 구조를 따른다. + { + "object": "string", + "cluster_id": "IC-01", + "current_right_holder_candidate": "string|null", + "current_possessor": "string|null", + "current_user": "string|null", + "current_benefit_holder": "string|null", + "possession_basis_asserted": "string|null", + "possession_basis_transition_needed": true, + "certainty": "final|provisional|불명" + } + - 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속을 하나의 generic holder로 뭉개지 않는다. + - 상속·승계형 사건군에서 현재 권리귀속이 decedent의 과거 권리사실과 친족관계로만 추론되는 경우 `certainty="provisional"`로 둔다. + + + + - optional additive field `plaintiff_capacity_matrix`를 허용한다. + - 이 필드는 사건군별 실질 원고적격을 later stage로 넘기기 위한 standing map이다. + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않는다. + - 각 원소는 아래 구조를 따른다. + { + "cluster_id": "IC-01", + "plaintiff_name": "string", + "capacity_basis": "소유자|공유자|상속인|임차인|채권자|점유권자|불명", + "source_basis_text": "meeting에서 직접 읽히는 짧은 근거", + "certainty": "final|provisional" + } + - 사건군별 직접 권리주체로 읽히는 사람만 넣는다. + - `capacity_basis="상속인"` row라도, 근거가 친족관계, 사망, 공동 통지, 공동 상담 같은 succession trigger 수준이면 `certainty`는 반드시 `"provisional"`로 둔다. + - 상속·승계형 사건군에서 decedent의 과거 소유사실, 매수사실, 등기명의사실만 보일 뿐 living plaintiff의 승계 완료 또는 현재 권리귀속이 meeting에서 직접 해소되지 않으면, living plaintiff를 `capacity_basis="소유자"` 또는 `capacity_basis="공유자"` + `certainty="final"`로 두지 않는다. + - 위 경우에는 원칙적으로 `capacity_basis`를 `"상속인"` 또는 `"불명"`으로 두고, `certainty`를 `"provisional"`로 둔다. + - meeting이 특정 사건군에 관하여 직접 계약당사자, 직접 채권자, 직접 임차인, 직접 소유·공유 관계 또는 승계 완료 상태를 직접 보여줄 때만 `certainty="final"`을 허용한다. + - 원고적격이 불명확하거나 later stage 재검토가 필요하면 `certainty`는 `"provisional"`로 둔다. + - `preferred_plaintiffs_for_complaint`가 별도로 존재하더라도, 이를 맞추기 위해 `plaintiff_capacity_matrix`를 `소유자/final`로 과잉 보정하지 않는다. + + + + - optional additive field `current_control_map`를 허용한다. + - 이 필드는 사건군별 현재 지배상태를 later stage로 넘기기 위한 authority map이다. + - 사건군에 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속 사실이 직접 읽히면 가능한 한 `current_control_map`을 생성한다. + - 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속은 가능한 한 분리한다. + - 각 원소는 아래 구조를 따른다. + { + "cluster_id": "IC-01", + "holder_name": "string", + "holder_role": "current_right_holder|current_possessor|current_user|current_benefit_holder", + "basis_text": "meeting에서 직접 읽히는 짧은 근거", + "certainty": "final|provisional" + } + - 같은 사람이 복수 role을 가지면 중복 기재할 수 있다. + - meeting에 직접 없는 role은 추정하지 않는다. + - 상속·승계형 사건군에서 current right holder가 decedent의 과거 권리사실 + 친족관계만으로 유도되는 경우, `holder_role="current_right_holder"` row는 원칙적으로 `certainty="provisional"`로 둔다. + - decedent의 과거 소유사실만으로 living plaintiff를 `current_right_holder/final`로 기재하지 않는다. + - 비상속형 사건군에서 meeting이 현재 소유자, 현재 점유자, 현재 사용자, 현재 수익귀속자를 직접 적시하면 해당 row는 `certainty="final"`을 우선 검토한다. + + + + - optional additive field `standing_watchpoints`를 허용한다. + - later stage가 반드시 재검토해야 하는 원고적격/권리귀속 리스크를 짧은 경고문으로 남긴다. + - 상속, 상속포기, 사망, 경매, 공동소유, 현재 권리주체 불명, 현재 점유자와 권리주체 불일치가 보이면 우선 검토한다. + - 경고문은 가능한 한 자산군 또는 사건군을 특정하는 asset-specific trigger 형태로 쓴다. + - 예시 형식: + - "상속·승계 자료 추가 확인 후 해당 자산군 원고적격 재확인 필요" + - "현재 권리귀속과 현재 점유자 분리 검토 필요" + - "사망한 종전 권리주체 관련 succession resolution 필요" + - meeting에 직접 읽히는 범위를 넘는 장문 법률판단은 쓰지 않는다. + + + + - `summary_key_incidents`, `key_facts`, `constraints`는 exact-match 중복 제거 후 최초 순서를 유지한다. + - `aliases`는 명백한 경우에만 기록한다. + - `claim_type_candidates`는 중복 제거 후 최초 순서를 유지한다. + - `defendant_target_matrix`를 사용하면 same `name` + `target_status` 조합 기준 exact-match 중복 제거 후 최초 순서를 유지한다. + - 비nullable string 외에는 `불명` 사용을 피하고 `null` 또는 빈 배열을 우선한다. + + + + 기존 필수 schema는 유지하고, 아래 optional additive field를 추가할 수 있다. + v.3 schema registry 기준 `IssueClusterDomainProfile`, `LegalEntitlementCandidate`, `CurrentControlMap4Axis`를 반영한다. + + { + "primary_goal": "string|null", + "constraints": [], + "summary_key_incidents": [], + "key_facts": [], + "claim_type_candidates": ["금전","물건인도","행위","확인","형성","보전(가처분/가압류)"], + "parties": { + "plaintiffs": [{"name":"string","type":"법인|자연인|기관"}], + "defendants": [{"name":"string","type":"법인|자연인|기관|미확정","asset_status":"string|null"}], + "third_parties": [{"name":"string","relationship":"string"}] + }, + "aliases": {}, + + "defendant_target_matrix": [ + { + "name": "string", + "target_status": "target_now|exclude_now|secondary_target|review", + "basis_text": "string", + "execution_risk": "low|medium|high|unknown", + "future_recovery_signal": "none|possible|explicit" + } + ], + + "issue_clusters": [ + { + "cluster_id": "IC-01", + "cluster_label": "string", + "candidate_plaintiffs": ["string"], + "legal_entitlement_candidates": ["string"], + "preferred_plaintiffs_for_complaint": ["string"], + "preferred_plaintiff_selection_basis": "direct_right_holder|resolved_succession|resolved_succession_needed|drafting_preference|review|불명", + "succession_resolution_needed": true, + "candidate_defendants_or_counterparties": ["string"], + "desired_relief_keywords": ["string"], + "domain_profiles": { + "monetary_claim_profile": { + "underlying_claim_type": "string|null", + "principal_amount_candidate": "string|null", + "principal_debtor_candidates": ["string"], + "successor_or_secondary_liability_candidates": [], + "default_or_limitation_issue_expected": true, + "known_demand_or_notice_signals": ["string"], + "requires_money_claim_timeline": true + }, + "secured_property_dispute_profile": { + "has_multiple_secured_debts": true, + "requires_payment_allocation": true, + "requires_fractional_validity_analysis": true, + "secured_object_candidates": ["string"], + "auction_or_registry_invalidity_expected": true + }, + "actio_property_transfer_profile": { + "debtor": "string|null", + "creditor": "string|null", + "beneficiary": "string|null", + "fraudulent_act_asset": "string|null", + "fraudulent_act_type": "string|null", + "fraudulent_act_date_hint": "string|null", + "is_encumbered_real_estate": true, + "requires_value_compensation_review": true + }, + "possession_lien_succession_profile": { + "possession_basis_asserted": "string|null", + "succession_resolution_needed": true, + "notice_lifecycle_hints": [], + "lien_violation_or_extinction_hints": [] + } + }, + "client_requested_relief": ["string"], + "legal_sustainability_watchpoints": ["string"], + "liability_group_hint": {}, + "commercial_successor_liability_hint": {}, + "fractional_title_issue_hint": {}, + "land_use_gain_hint": {}, + "lease_payment_semantics_hint": {}, + "preserved_claim_bundle_hint": {}, + "value_compensation_hint": {}, + "defense_hints": [] + } + ], + + "plaintiff_capacity_matrix": [ + { + "cluster_id": "IC-01", + "plaintiff_name": "string", + "capacity_basis": "소유자|공유자|상속인|임차인|채권자|점유권자|불명", + "source_basis_text": "string", + "certainty": "final|provisional" + } + ], + + "current_control_map": [ + { + "cluster_id": "IC-01", + "holder_name": "string", + "holder_role": "current_right_holder|current_possessor|current_user|current_benefit_holder", + "basis_text": "string", + "certainty": "final|provisional" + } + ], + + "current_control_map_4axis": [ + { + "object": "string", + "cluster_id": "IC-01", + "current_right_holder_candidate": "string|null", + "current_possessor": "string|null", + "current_user": "string|null", + "current_benefit_holder": "string|null", + "possession_basis_asserted": "string|null", + "possession_basis_transition_needed": true, + "certainty": "final|provisional|불명" + } + ], + + "standing_watchpoints": ["string"] + } + + 추가 제약: + - `defendant_target_matrix`, `issue_clusters`, `plaintiff_capacity_matrix`, `current_control_map`, `standing_watchpoints`는 optional이다. + - `domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints`, `liability_group_hint`, `commercial_successor_liability_hint`, `fractional_title_issue_hint`, `land_use_gain_hint`, `lease_payment_semantics_hint`, `preserved_claim_bundle_hint`, `value_compensation_hint`, `defense_hints`는 `issue_clusters` 내부 optional additive field다. + - `current_control_map_4axis`는 top-level optional additive field이며 legacy `current_control_map`을 대체하지 않는다. + - `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`, `preferred_plaintiff_selection_basis`, `succession_resolution_needed`는 `issue_clusters` 내부 optional additive field다. + - 기존 필수 필드는 모두 유지한다. + - 허용된 additive field 외 임의 추가 필드 금지. + + + + - `parties.plaintiffs`에는 실제 의뢰인만 남았는지 점검한다. + - `parties.plaintiffs`가 client universe로만 쓰였고, 사건군별 원고적격 확정값처럼 오염되지 않았는지 점검한다. + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않았는지 점검한다. + - `parties.defendants`와 `third_parties`가 뒤바뀌지 않았는지 점검한다. + - `summary_key_incidents`가 사건군별로 과잉 병합되지 않았는지 점검한다. + - `key_facts`가 later stage에 중요한 standing/ownership/current control fact를 누락하지 않았는지 점검한다. + - `issue_clusters`를 썼다면 각 cluster의 당사자와 desired relief가 meeting 직접 서술 범위를 넘지 않는지 점검한다. + - 상속·승계형 사건군에서 `candidate_plaintiffs`가 친족관계, 공동 통지, 공동 상담 같은 간접 신호만으로 확장되지 않았는지 점검한다. + - `issue_clusters`를 썼다면 unresolved succession cluster에서 `legal_entitlement_candidates`와 `preferred_plaintiffs_for_complaint`가 자동 동일시되지 않았는지 점검한다. + - `preferred_plaintiffs_for_complaint`를 썼다면 그 basis가 `preferred_plaintiff_selection_basis`에 남아 있고, meeting 직접 서술 또는 direct right-holder 구조를 넘지 않는지 점검한다. + - `succession_resolution_needed=true`인데도 해당 cluster가 direct owner/final cluster처럼 오염되지 않았는지 점검한다. + - `plaintiff_capacity_matrix`를 썼다면 각 row의 `capacity_basis`와 `source_basis_text`가 meeting 직접 서술 범위를 넘지 않는지 점검한다. + - `capacity_basis="상속인"` row가 친족관계나 공동행동 사실만으로 `certainty="final"`로 오염되지 않았는지 점검한다. + - 사망한 종전 권리주체의 과거 소유사실, 매수사실, 등기명의사실이 living plaintiff의 `소유자/final` row로 잘못 치환되지 않았는지 점검한다. + - `current_control_map`을 썼다면 `holder_role`이 `current_right_holder|current_possessor|current_user|current_benefit_holder` 중 하나인지와, basis가 meeting 직접 문장에 근거하는지 점검한다. + - 상속·승계형 사건군에서 `current_right_holder` row가 있으면, meeting만으로 승계가 해소되지 않은 경우 `certainty="provisional"`인지 점검한다. + - 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속이 서로 다른데 하나의 generic holder row로 뭉개지지 않았는지 점검한다. + - 사망한 종전 권리주체가 보이는 사건군이 있으면 `standing_watchpoints`에 succession-resolution trigger가 남았는지 점검한다. + - `standing_watchpoints`를 썼다면 장문 법률판단이 아니라 later stage 재검토용 짧은 경고인지 점검한다. + - meeting이 피고별로 현재 집행가능성과 장래 회수가능성을 구별하여 서술하면, 그 차이가 `constraints`, `primary_goal`, `summary_key_incidents`, `parties.defendants.asset_status`, `defendant_target_matrix` 중 어디엔가 비대칭적으로 보존되었는지 점검한다. + - "현재 무자력"과 "장래 재산취득 가능성"이 같은 사람에게 함께 서술되면, 그 사람을 자동 배제 대상으로 압축하지 않았는지 점검한다. + - `constraints`에 current collectability나 단순 회수곤란 사실이 잘못 들어가 있지 않은지 점검한다. + - `asset_status`가 제소 배제 신호처럼 쓰이지 않았는지 점검한다. + - `defendant_target_matrix`를 썼다면 각 `name`이 `parties.defendants`와 모순되지 않는지 점검한다. + - `defendant_target_matrix`에서 `target_status="exclude_now"`가 붙은 사람에게는 meeting상 명시적 no-sue 지시가 있는지 점검한다. + - `defendant_target_matrix`에서 `execution_risk="high"`와 `target_status="target_now"`가 함께 존재할 수 있음을 허용하되, 그 경우 `basis_text` 또는 `future_recovery_signal`이 비어 있지 않은지 점검한다. + - 장래 상속/재산취득 가능성이 직접 서술된 피고가 `exclude_now`로 잘못 내려가지 않았는지 점검한다. + - 금전채권 cluster에 최고ㆍ채무승인ㆍ이행각서ㆍ시효 단서가 있는데 `monetary_claim_profile.requires_money_claim_timeline`이 없거나 false이면 warning으로 본다. + - 동일 사건군에 원채무자와 승계ㆍ양수ㆍ보증ㆍ상호속용 책임 후보가 함께 있는데 `liability_group_hint`가 비어 있으면 warning으로 본다. + - 영업양도ㆍ상호속용 단서가 있는데 `commercial_successor_liability_hint.requires_article_42_review`가 없으면 warning으로 본다. + - 대여금ㆍ근저당권ㆍ경매ㆍ지분 단서가 같은 cluster에 있는데 `secured_property_dispute_profile.requires_payment_allocation`이 없으면 warning으로 본다. + - 상담문서에 지분별 권리상태가 보이는데 `fractional_title_issue_hint`가 없으면 warning으로 본다. + - 미등기 건물ㆍ임대차ㆍ건물 매매ㆍ토지 사용 단서가 있는데 `land_use_gain_hint`가 없으면 warning으로 본다. + - 사해행위취소ㆍ부담부 부동산ㆍ사후 담보권 말소 단서가 있는데 `actio_property_transfer_profile.requires_value_compensation_review`가 false이면 error로 본다. + - 같은 채권자와 채무자 사이 복수 대여금이 보이는데 `preserved_claim_bundle_hint`가 없으면 warning으로 본다. + - 명의신탁ㆍ충분담보ㆍ상계ㆍ선의 등 항변 단서가 있는데 `defense_hints`가 없으면 warning으로 본다. + - 유치권ㆍ상속ㆍ승계 사건군에서 `succession_resolution_needed=true`인데도 입력자료상 공동 통지 발신자, 친족 후보, 직계존속·직계비속 후보가 `preferred_plaintiffs_for_complaint` 확정자로 들어가면 error로 본다. + - 통지 발송일과 도달일 단서가 모두 있는데 `notice_lifecycle_hints`가 없으면 warning으로 본다. + - `current_control_map_4axis`를 썼다면 4축 중 직접 읽히는 축이 generic holder 하나로 합쳐지지 않았는지 점검한다. + - 출력 파일명과 성공 메시지가 정확한지 점검한다. + + + + + + [ ] 1) Preflight: `list_docs` 사용해서 `client_meeting.md` 확인, `read_docs` 사용해서 `client_meeting.md` 읽기 + [ ] 2) `write_file` 사용해서 `client_goal.json` 생성 + [ ] 3) `list_docs` 사용해서 `client_goal.json` 파일 존재만 확인(절대 읽기 금지) + [ ] 4) `"고객의사 초안 생성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `client_meeting.md` + + + - `client_goal.json` + + + + + - `client_goal.json`은 절대 재읽지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"고객의사 초안 생성 완료"` 한 줄만 사용한다. + + + + - task_name: Task_Evidence_shard_planner + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import hashlib + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + def _mid(): return next(MSG_ID) + def _init(): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"task-evidence-shard-planner","version":"1.0","user_id":"{{__user_hash__}}","workspace_id":"{{__workspace_hash__}}"}}}, headers=MCP_HEADERS) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post(LOCALDOCS_URL, json={"jsonrpc":"2.0","method":"notifications/initialized"}, headers=MCP_HEADERS).raise_for_status() + def _parse(text): + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: return json.loads(line[6:]) + except Exception: pass + try: return json.loads(text) + except Exception: return None + def _call(name, args): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc":"2.0","id":_mid(),"method":"tools/call","params":{"name":name,"arguments":args}}, headers=MCP_HEADERS) + r.raise_for_status() + p = _parse(r.text) + if not p or "result" not in p: raise RuntimeError(f"MCP {name} failed") + return p + def _unwrap(p, name): + text = (p["result"].get("content") or [{}])[0].get("text", "") + if not text: raise RuntimeError(f"Empty: {name}") + try: outer = json.loads(text) + except Exception: return text + if isinstance(outer, dict) and "results" in outer: + r0 = (outer.get("results") or [{}])[0] + inner = r0.get("content") or r0.get("text") or "" + if not inner: raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return json.dumps(outer, ensure_ascii=False) if not isinstance(outer, str) else outer + def read_json(name): + raw = _unwrap(_call("read_docs", {"doc_names": [name]}), name) + if not isinstance(raw, str): return raw + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + obj, _ = decoder.raw_decode(raw.strip()) + return obj + def write_doc(path, content): + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def _items(obj: Any) -> list[Any]: + if isinstance(obj, list): + return obj + if isinstance(obj, dict): + for key in ("items", "documents", "evidences", "evidence", "docs"): + val = obj.get(key) + if isinstance(val, list): + return val + raise RuntimeError("evidence_all.json must be a JSON array or an object containing a document array") + + def _text(v: Any) -> str | None: + if v is None: + return None + s = str(v).strip() + return s or None + + def _doc_title(doc: Any, ordinal: int) -> str | None: + if not isinstance(doc, dict): + return f"evidence_all[{ordinal}]" + for key in ("title", "name", "filename", "file_name", "document_title", "doc_title"): + val = _text(doc.get(key)) + if val: + return val + return f"evidence_all[{ordinal}]" + + def _doc_uid(doc: Any, ordinal: int) -> str: + if isinstance(doc, dict): + for key in ("doc_uid", "uid", "id", "document_id", "source_id"): + val = _text(doc.get(key)) + if val: + return val + return f"DOC-{ordinal:03d}" + + def _stable_hash(doc: Any) -> str: + raw = json.dumps(doc, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _list_doc_names(args) -> list[str]: + payload = _unwrap(_call("list_docs", args), "list_docs") + if isinstance(payload, str): + try: + payload = json.loads(payload) + except Exception: + return re.findall(r"[\w./-]+\.json", payload) + vals: list[str] = [] + if isinstance(payload, dict): + for value in payload.values(): + if isinstance(value, list): + for item in value: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + elif isinstance(value, str): + vals.append(value) + elif isinstance(payload, list): + for item in payload: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + return [v for v in vals if v] + + def _read_existing_doc_names() -> list[str]: + preferred = [ + {"path": "evidence_indexed_parts", "pattern": "E-*.json"}, + {"path": "evidence_event_candidate_parts", "pattern": "E-*.json"}, + ] + preferred_errors = [] + preferred_names: list[str] = [] + for args in preferred: + try: + folder = str(args["path"]).rstrip("/") + preferred_names.extend(f"{folder}/{name.split('/')[-1]}" for name in _list_doc_names(args)) + except Exception as exc: + preferred_errors.append(f"{args['path']}: {exc}") + if not preferred_errors: + return sorted(set(preferred_names)) + + fallback_errors = [] + for args in ({}, {"doc_names": []}): + try: + return sorted(set(preferred_names + _list_doc_names(args))) + except Exception as exc: + fallback_errors.append(f"{args}: {exc}") + raise RuntimeError("list_docs failed for preferred schemas and fallback schemas; stale part detection cannot safely continue: " + " | ".join(preferred_errors + fallback_errors)) + + def _stale_parts(existing_names: list[str], n: int, folder: str) -> list[str]: + stale: list[str] = [] + pattern = re.compile(rf"(?:^|/){re.escape(folder)}/E-(\d{{3}})\.json$") + for name in existing_names: + m = pattern.search(name) + if not m: + continue + ordinal = int(m.group(1)) + if ordinal < 1 or ordinal > n: + stale.append(name) + return sorted(set(stale)) + + def main(): + _init() + evidence_all = read_json("evidence_all.json") + docs = _items(evidence_all) + n = len(docs) + if n <= 0: + raise RuntimeError("evidence_all.json contains no evidence documents") + + existing_names = _read_existing_doc_names() + stale_b1 = _stale_parts(existing_names, n, "evidence_indexed_parts") + stale_b2 = _stale_parts(existing_names, n, "evidence_event_candidate_parts") + + shards = [] + streaming_edges = [] + b1_waits = [] + b2_waits = [] + b1_files = [] + b2_files = [] + for idx, doc in enumerate(docs, 1): + label = f"{idx:03d}" + evidence_id = f"E-{label}" + b1_task_id = f"Task_B1_map_doc_{label}" + b2_task_id = f"Task_B2_map_events_{label}" + b1_output = f"evidence_indexed_parts/{evidence_id}.json" + b2_output = f"evidence_event_candidate_parts/{evidence_id}.json" + # 문서별 shard 파일 — B1/B2 인스턴스가 evidence_all.json 풀 대신 자기 shard만 읽도록. + shard_doc_path = f"evidence_shards/{evidence_id}.json" + write_doc(shard_doc_path, json.dumps(doc, ensure_ascii=False, indent=2)) + b1_waits.append(b1_task_id) + b2_waits.append(b2_task_id) + b1_files.append(b1_output) + b2_files.append(b2_output) + shards.append({ + "assigned_ordinal": idx, + "ordinal_label": label, + "evidence_index_candidate": evidence_id, + "b1_task_id": b1_task_id, + "b2_task_id": b2_task_id, + "b1_output_path": b1_output, + "b2_output_path": b2_output, + "shard_doc_path": shard_doc_path, + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": idx, + "array_index": idx - 1, + "doc_uid_candidate": _doc_uid(doc, idx), + "title_candidate": _doc_title(doc, idx), + "content_sha256": _stable_hash(doc), + "shard_doc_path": shard_doc_path + }, + "runtime_bindings": { + "__assigned_ordinal__": idx, + "__ordinal_label__": label, + "__evidence_index_candidate__": evidence_id, + "__b1_part_path__": b1_output, + "__b2_part_path__": b2_output, + "__shard_doc_path__": shard_doc_path + } + }) + streaming_edges.append({ + "from": b1_task_id, + "to": b2_task_id, + "condition": {"file_exists": b1_output}, + "bind": {"__assigned_ordinal__": idx} + }) + + plan = { + "schema_version": "evidence_shard_plan.v1", + "planner_task": "Task_Evidence_shard_planner", + "created_at_utc": datetime.now(timezone.utc).isoformat(), + "source_file": "evidence_all.json", + "source_selector": "$.items || $", + "shard_count": n, + "expected_ordinals": [f"{i:03d}" for i in range(1, n + 1)], + "expected_evidence_ids": [f"E-{i:03d}" for i in range(1, n + 1)], + "ordinal_policy": { + "base": 1, + "assignment": "array_order", + "format": "%03d", + "assigned_ordinal_var": "__assigned_ordinal__", + "evidence_index_candidate": "E-{ordinal:03d}", + "no_legal_sorting": True, + "no_content_based_reordering": True + }, + "shards": shards, + # 백엔드 wildcard fan-out: dynamic_fanout 리스트의 각 항목이 한 인스턴스 컨텍스트({{item.*}}로 접근). + # shards를 그대로 사용 — 디스크립터 메타(id/count/task_template 등)는 fanout_meta로 분리 보존. + "dynamic_fanout": shards, + "fanout_meta": [ + { + "id": "B1_evidence_doc_fanout", + "count": n, + "task_template": "Task_B1_map_doc", + "instance_id_template": "Task_B1_map_doc_{ordinal:03d}", + "output_path_template": "evidence_indexed_parts/E-{ordinal:03d}.json" + }, + { + "id": "B2_event_candidate_fanout", + "count": n, + "task_template": "Task_B2_map_events", + "instance_id_template": "Task_B2_map_events_{ordinal:03d}", + "output_path_template": "evidence_event_candidate_parts/E-{ordinal:03d}.json", + "wait_until_template": "Task_B1_map_doc_{ordinal:03d}" + } + ], + "streaming_edges": streaming_edges, + "barriers": { + "B1_all_parts_barrier": { + "wait_for_tasks": b1_waits, + "require_files": b1_files, + "next": "Task_B1_gate_merge_deterministic" + }, + "B2_all_parts_barrier": { + "wait_for_tasks": b2_waits, + "require_files": b2_files, + "plus": ["Task_B1_gate_merge_deterministic"], + "next": "Task_B2_gate_merge_deterministic" + } + }, + "stale_part_policy": { + "mode": "block", + "allowed_ordinals": [f"{i:03d}" for i in range(1, n + 1)], + "checked_directories": ["evidence_indexed_parts", "evidence_event_candidate_parts"], + "detected_stale_parts": { + "evidence_indexed_parts": stale_b1, + "evidence_event_candidate_parts": stale_b2 + }, + "downstream_gate_instruction": "B1/B2 quality gates must BLOCK if detected_stale_parts is non-empty or if files outside expected_ordinals are present." + }, + "non_legal_assertions": { + "does_not_classify_document_type": True, + "does_not_extract_facts": True, + "does_not_evaluate_evidence": True, + "does_not_calculate_amounts": True, + "does_not_decide_legal_effect": True, + "does_not_finalize_evidence_index": True, + "does_not_finalize_candidate_id": True + } + } + + if stale_b1 or stale_b2: + plan["planner_status"] = "BLOCKED_BY_STALE_PARTS" + plan["mapper_execution_allowed"] = False + plan["blocked_reason"] = "stale mapper part detected; B1/B2 mapper fanout must not execute and existing parts must not be reused" + else: + plan["planner_status"] = "READY" + plan["mapper_execution_allowed"] = True + plan["blocked_reason"] = None + + write_doc("evidence_shard_plan.json", json.dumps(plan, ensure_ascii=False, indent=2)) + # 백엔드 wildcard fan-out 확장이 stdout JSON의 dynamic_fanout 키를 읽음. + # 전체 plan은 너무 커서 dynamic_fanout만 출력한다. 단 planner_status가 READY가 아니면 mapper fanout은 반드시 비운다. + fanout_payload = plan["dynamic_fanout"] if plan["planner_status"] == "READY" else [] + print(json.dumps({"dynamic_fanout": fanout_payload, "planner_status": plan["planner_status"], "mapper_execution_allowed": plan["mapper_execution_allowed"]}, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B1_map_doc_* + max_concurrency: 12 + preflight_files: + - "{{item.shard_doc_path}}" + - client_meeting.md + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + + Task_B1_map_doc + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. Your role in this task is a **per-document mapper** within the v.3 Step D streaming map-reduce pipeline. + + + 본 task는 v.3 Step D의 B1 mapper다. orchestrator 가 runtime parameter (`{{item.*}}`) 로 주입한 assigned_ordinal 단일 증거문서만 처리하여, 그 문서의 authority component·document component envelope·legal component envelope을 추출한 part 파일을 작성한다. + 본 task는 증거 문서 수 N을 계산하지 않고, ordinal을 배정하지 않고, B1/B2 mapper 인스턴스를 생성하지 않는다. 이러한 orchestration 책임은 전부 `Task_Evidence_shard_planner`와 orchestrator의 권한이다. + 최종 `evidence_indexed.json`은 본 task가 작성하지 않는다. 본 task는 `evidence_indexed_parts/E-{ordinal:03d}.json` part 파일만 작성하며, finalize는 `Task_B1_gate_merge_deterministic`가 수행한다. + 본 task는 자기 문서 안에서 직접 확인되는 component만 작성한다. 다른 문서의 사실을 자기 part에 주입하면 quality gate의 authority drift check에서 BLOCK 또는 HARD_WARNING으로 검출된다. + `evidence_index`는 planner가 부여한 `E-{ordinal:03d}` 형식의 deterministic candidate를 그대로 propose 하되, final 부여는 quality gate가 수행한다. 본 task는 candidate id를 자기 part의 `evidence_index_proposed` 필드에 기록한다. + 본 task의 추출 깊이는 v.2 원본 Task_B1과 동일하다. document component envelope의 모든 하위 구조(claim_document_components, business_transfer_components, registry_row_components, secured_debt_components, valuation_rows, mixed_contract_payment_semantics, loan_components, valuation_matrix, registry_row_role_map, defense_assertions, admission_components, defense_components, notice_components, party_assertion_components, schedule_items, registry_object_display, succession_document_components)를 자기 문서 범위 안에서 가능한 한 분리 추출한다. + 변호사 관점에서 본 단계의 핵심은 등기부 row, 차용증 component, 통지서 발송/도달일 분리, 가족관계·상속포기 component, 별지목록 등의 직접 보존이다. 이는 청구원인 timeline·청구취지 별지목록·금액 계산의 기초가 된다. + + + + + + - runtime parameters: `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.source_document_ref}}`, `{{item.b1_output_path}}` (planner 가 부여한 단일 source of truth — 별도 파일 read 불요) + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 문서 1건 — planner가 evidence_all.json에서 분리해 둔 단일 shard 파일) + - `client_meeting.md` (보조 사용만 허용) + + + - `evidence_indexed_parts/E-{ordinal:03d}.json` (본 mapper 인스턴스의 part 파일 1개만) + + + - 다른 ordinal의 문서를 처리하기 + - 다른 ordinal의 사실을 자기 part에 주입하기 (authority drift) + - 증거 문서 수 N을 재계산하여 planner 결과를 대체하기 + - assigned_ordinal을 새로 생성하거나 재정렬하기 + - B1/B2 mapper fan-out 또는 streaming edge를 설계하기 + - 최종 `evidence_indexed.json` 작성 (이는 quality gate의 단독 권한) + - 다른 mapper의 part 파일 읽기/덮어쓰기 + - root 전체 탐색 + - 이전 task 결과 또는 backend 자동 로드 데이터 사용 + - 자기 part 재읽기 + - cross-document legal conclusion 확정 + - 입력에 없는 사실 추가 + + + + + 1. `list_docs`로 `{{item.shard_doc_path}}`, `client_meeting.md` 존재만 확인한다. + 2. `read_docs`로 위 두 파일을 읽는다. `{{item.shard_doc_path}}`는 이 인스턴스의 단일 evidence 문서로 그대로 사용한다 (별도의 ordinal 추출 불필요). assigned_ordinal·evidence_index_candidate·b1_output_path 는 runtime parameter (`{{item.*}}`) 그대로 사용한다. + 3. 메모리에서 part item 객체를 생성한다(items 배열이 아닌 단일 item object 또는 1-element items 형태). + 4. `write_file(overwrite=true)`로 runtime parameter 가 지정한 `evidence_indexed_parts/E-{ordinal:03d}.json`을 저장한다. + 5. `list_docs`로 자기 part 파일 존재만 확인한다. 절대 읽지 않는다. + 6. 정확히 `"E-{ordinal:03d} mapper part 작성 완료"`만 출력하고 종료한다. + + + + - 입력 누락, 빈 파일, runtime parameter 누락, ordinal 매핑 실패, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. part 파일은 쓰지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + - tool로 읽은 내용만 사실로 취급한다. + + + + - 본 mapper 인스턴스는 정확히 runtime parameter (`{{item.assigned_ordinal}}`) 에 해당하는 1개 문서만 처리한다. + - runtime parameters (`{{item.*}}`) 는 ordinal/binding 의 단일 source of truth 다 (별도 plan 파일 read 불요). + - `evidence_all.json` 배열 순서를 절대 바꾸지 않고, 배열 순서를 근거로 새 plan을 만들지도 않는다. + - planner의 `source_document_ref.content_sha256` 또는 `doc_uid_candidate`가 현재 문서와 명백히 충돌하면 part 파일을 쓰지 않는다. + - 본 task는 planner가 확정한 `evidence_index_candidate`, `b1_output_path`, assigned_ordinal을 소비할 뿐 재작성하지 않는다. + - 다른 ordinal의 문서 내용을 현재 문서에 보충하지 않는다(authority drift 금지). + - unknown 처리: + - nullable scalar → `null` + - 배열 → `[]` + - 비nullable string만 `"불명"` 사용 + - 배열은 exact-match 중복 제거 후 최초 순서를 유지한다. + - part 파일은 단일 item object 형태이며, 기존 필수 필드(`evidence_index_proposed`, `doc_uid`, `title`, `title_normalized`, `doc_type`, `key_*`, `source_pointer`)를 모두 포함한다. + - optional additive field는 downstream(quality gate)이 무시해도 안전해야 한다. + - `evidence_index_proposed`는 deterministic 규칙 `E-{ordinal:03d}`로만 생성한다. 임의 변형 금지. + + + + 이 인스턴스의 `{{item.shard_doc_path}}`(planner가 evidence_all.json에서 분리한 단일 evidence 1건) + `client_meeting.md` (보조만 허용) + + + `client_meeting.md`는 아래 목적에만 사용할 수 있다. + - `asset_id` + - `property_label_for_schedule` + - `transaction_type` + - `transaction_date` + - `market_value_at_act` + - `market_value_at_close` + - `consideration_breakdown` + - `property_cluster_id` + - 현재 문서의 축약 명칭이 어떤 자산군을 가리키는지에 관한 shorthand 해석 + + + + - 현재 문서와 `client_meeting.md`가 충돌하면 현재 문서가 우선한다. + - 예외: 등기부상 형식적 원인이 `매매`여도, 현재 문서 특약 또는 `client_meeting.md`가 실질적으로 채무갈음/대물변제를 직접 드러내면 `transaction_type`은 `"대물변제"`로 할 수 있다. + - 단, 이 예외는 현재 문서 또는 meeting이 실질 원인을 직접적으로 드러내는 경우에만 적용한다. + + + + + - `ordinal` = `evidence_all.json`의 1-based 배열 위치 (assigned_ordinal과 동일) + - `evidence_index_proposed` = `E-{ordinal:03d}` (mapper propose; quality gate가 finalize) + - `title` = 현재 문서 제목 원문 그대로 + - `title_normalized` = 앞뒤 공백 제거 + 내부 연속 공백/줄바꿈을 1칸으로 축약 + - `doc_uid` = `DOC-{ordinal:03d}-{title_normalized의 공백을 "_"로 치환한 값}` + - `source_pointer` = `{"source":"evidence_all.json","ordinal":ordinal}` + + + + 다음 순서로만 핵심을 읽는다. + 1. `title` + 2. `heading` + 3. `key_value` / `form` + 4. 관련 `table` 행 + 5. `list` + 6. `paragraph` + + 보일러플레이트(바코드 안내, 발급확인 문구, 반복 footer/page text)는 무시한다. + 단, `관할등기소`, `접수번호`, `등기원인일`, `발행일` 등 필요한 사실이 있으면 사용한다. + + + + `doc_type` enum은 v.2 원본과 동일하다. nuance는 `doc_semantic_flags`로 보강한다. + 1. `판결|결정|배당표|가압류|가처분|경매|타경` → `"판결/결정"` + 2. `등기사항전부증명서|주민등록|가족관계증명서|사업자등록증` → `"공문서"` + 3. `대출|명세|거래|계산서` → `"거래기록"` + 4. `카톡|문자|이메일|통화` → `"통신기록"` + 5. `계약서|약정서|확인서|영수증|차용증|각서` → `"처분문서"` + 6. 그 외 → `"기타"` + + + + - `key_facts`: 최대 3개, 각 60자 이하 + - `key_dates`: 최대 3개 + - `key_amounts`: 최대 3개 + - `key_parties`: 최대 5개 + - 현재 문서에서 직접 읽히는 핵심만 남긴다. + - 장문 요약, 계산식, 추정 보정 금지. + - 가능하면 heading / key_value / 표의 핵심 행만 사용한다. + + + + + - `property_cluster_id`는 optional field다. + - 현재 문서만으로 특정 자산군이 명백하거나, 현재 문서의 표기와 `client_meeting.md`가 정확히 대응할 때만 채운다. + - 예: `"해당 대지/건물"`, `"해당 상가"`, `"해당 주거용 부동산"`, `"해당 임야"`, `"해당 영업양도"`, `"해당 물품대금"`. + - 명백하지 않으면 `null`. + + + + `doc_semantic_flags`는 optional field다. 아래 값만 사용한다. + - `multi_event_doc` + - `registry_doc` + - `mixed_contract_doc` + - `valuation_doc` + - `claim_doc` + - `status_doc` + - `notice_doc` + - `response_doc` + - `business_transfer_doc` + - `supporting_reference_doc` + - `admission_doc` + - `defense_doc` + - `notice_receipt_admission_doc` + - `lien_assertion_doc` + - `schedule_doc` + - `relief_object_reference_doc` + - `succession_doc` + + 부여 규칙: + - 등기부처럼 복수 row가 서로 다른 법적 효과를 가지면 `multi_event_doc` + - 갑구/을구 등기행위가 있으면 `registry_doc` + - 임대차와 매매 등 서로 다른 계약 구성요소가 같은 문서에 공존하면 `mixed_contract_doc` + - 시가, 차임시세, 사용이익 가치가 표로 제시되면 `valuation_doc` + - 차용증, 보증서, 대여약정 등 채권 발생 문서면 `claim_doc` + - 답변서, 사업자등록증, 현재 상태 확인서처럼 상태를 보여주면 `status_doc` + - 내용증명, 통지서, 최고서, 해제통지서면 `notice_doc` + - 상대방 답신, 답변서, 반박문이면 `response_doc` + - 영업양도 문서면 `business_transfer_doc` + - 별지 목록처럼 단독 계산객체는 없으나 다른 문서 식별을 돕는 문서면 `supporting_reference_doc` + - 상대방이 통지 수령, 임대, 차임, 점유, 채무, 변제 등을 인정하면 `admission_doc` + - 명의신탁, 충분담보, 상계, 선의, 후소유자 소멸청구권 부정, 피담보채권 충당 등 항변이 있으면 `defense_doc` + - 유치권 소멸청구 통지의 수령일을 인정하면 `notice_receipt_admission_doc` + - 유치권 주장, 공사잔금, 유치권 목적물 사용·임대가 있으면 `lien_assertion_doc` + - 별지목록, 목적물 목록, 청구취지에서 그대로 사용할 문구가 있으면 `schedule_doc` 및 `relief_object_reference_doc` + - 가족관계증명서, 상속포기 심판문, 사망·상속 관련 공문서이면 `succession_doc` + + + + - 등기부 문서에서 갑구/을구 여러 row가 서로 다른 법적 효과를 가지면 `multi_event_registry=true`를 optional field로 기록한다. + - `multi_event_registry=true`인 문서는 단일 snapshot 요약을 강요하지 않는다. + + + + + - 문서가 여러 법률효과를 담고 있으면 component 단위로 분해한다. + - 날짜는 작성일, 발송일, 도달일, 법률효과 발생일, 변제기, 기산일 후보로 의미별 분리한다. + - 금액은 원금, 이자, 보증금, 차임, 사용이익, 채권최고액, 실제 변제액, 가액배상 공제액으로 성격별 분리한다. + - 아래 component field들은 optional additive field이며, 현재 문서에 직접 읽히는 범위에서만 사용한다. + + + - 문서가 이행각서, 채무승인서, 최고서, 내용증명, 지급요구서, 변제약정서, 차용증, 납품대금 확인서이면 `claim_document_components`를 생성한다. + - 단순 `key_dates`에 문서작성일만 넣고 끝내지 말고, 문서 안에 지급기일, 새 변제기, 최고 도달일, 채무승인일, 청구금액, 채권자, 채무자가 직접 읽히면 component field로 분해한다. + - 이행각서에서 "언제까지 지급한다"는 문구가 있으면 반드시 `new_due_date` 후보로 분리한다. + - 최고서에서 발송일과 수령일/도달일이 모두 있으면 `dispatch_date`와 `arrival_date`를 분리한다. + - claim component의 `component_kind` 허용값은 `underlying_claim|debt_acknowledgement|demand_notice|notice_dispatch|notice_arrival|new_due_date|payment_promise|limitation_relevance|other`다. + + + + - 영업양도계약서, 상호 계속 사용, 사업 양수인, 영업자산 이전, 고객·직원 승계, 채무불승계 조항, 책임배제 등기 단서가 있으면 `business_transfer_components`를 생성한다. + - `business_name`, `transferor_candidates`, `transferee_candidates`, `business_assets_transferred`, `trade_name_continuation_clause`, `customer_or_employee_succession_clause`, `pre_transfer_debt_exclusion_clause`, `liability_exclusion_registration_clause`를 직접 읽히는 범위에서 채운다. + - 기존 `contract_components`가 있어도 영업양도 책임 검토용 구조는 `business_transfer_components`에 별도 보존한다. + + + + - 등기부, 등기사항전부증명서, 등기내역 문서는 반드시 row 단위로 쪼갠다. + - 각 row마다 갑구/을구, 순위번호, 등기목적, 접수일, 접수번호, 원인, 권리자, 채무자, 채권최고액, 객체 범위를 추출하여 `registry_row_components`에 둔다. + - 청구취지 특정에 필요한 접수번호는 `key_facts` 요약에만 두지 말고 `registry_row_components[*].receipt_no`로 보존한다. + - 차용증, 근저당권 설정계약, 등기부 row가 연결되면 `secured_debt_components`를 생성한다. + - 복수 채무의 원금, 이율, 변제기, 담보 지분이 다르면 법정변제충당 계산 대상이므로 모두 구조화한다. + - 사해행위취소 사건의 등기부 row는 필요하면 `registry_row_role_map`에 actio role을 추가로 남긴다. + - `registry_row_components`의 각 row에 `registry_ref`를 부여한다. 형식: `REG-{ordinal:03d}-{row_seq:02d}` (갑구/을구·순위번호 순 deterministic). + - row에 지분 표시(예: "2분의 1", "1/2 지분")가 직접 읽히면 `registry_row_components[*].fraction_display`(원문 문자열)와 `registry_row_components[*].fraction_ref`를 채운다. + - `fraction_ref`는 `FR-{ordinal:03d}-{row_seq:02d}` 형식의 deterministic propose 값이다. 같은 row에서 파생되는 모든 component는 같은 `fraction_ref`를 공유한다. + - 지분 표시의 분자·분모가 직접 읽히는 경우에만 optional 필드 `fraction_numerator`/`fraction_denominator`를 채운다(예: "2분의 1" → 1/2). 환산·약분·보간 금지. 직접 읽히지 않으면 null. + + + + - 감정표는 금액만 추출하지 말고 `valuation_rows`에 row별 전제조건, 금액, 단위, 법률상 사용 가능성을 함께 추출한다. + - 사해행위취소 valuation 문서가 복수 자산 또는 복수 시점을 담으면 `valuation_matrix`를 생성하고, 각 row의 `legal_uses`를 표시한다. + - 임대차보증금, 월 차임, 매매대금이 한 문서에 같이 있으면 `mixed_contract_payment_semantics`에서 각각 별도 payment semantic으로 분리한다. + - 보증금은 원칙적으로 반환 예정 담보성 금원인지 검토하고, 원고가 직접 수령하지 않았다면 원고의 부당이득 원금으로 자동 전환하지 않는다. + - 월 차임은 피고들 사이의 계약상 차임과 객관적 토지 사용이익을 구분한다. + + + + - 사해행위취소 사건의 증거문서는 일반 key_facts 요약만으로 충분하지 않다. + - 차용증에 복수 대여금이 있으면 `loan_components`로 분리하고, 각 원금·변제기·이자율·담보 목적물·청구 내 역할을 추출한다. + - 감정평가서는 시점별 `valuation_matrix`로 보존하고, 각 row의 legal use를 표시한다. + - 등기부는 갑구/을구 row별로 쪼개고, 각 row의 `actio_roles`를 부여한다. + - 내용증명 답신에 명의신탁, 충분담보, 상계, 선의 주장이 있으면 `defense_assertions`로 추출한다. + - 사후 말소된 담보권과 그 말소를 유발한 변제액은 가액배상 계산의 필수 입력으로 표시한다. + + + + - `loan_components`의 각 항목은 아래 named 필드를 가진다. 직접 읽히지 않는 필드는 null. + { + "loan_component_id": "E-{ordinal:03d}-LC-{seq:02d}", + "principal_amount": null, + "loan_date": null, + "due_date": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "rate_text": null, + "rate_source_locator": null, + "interest_accrual_start_hint": null, + "collateral_object_hint": null, + "role_in_claim_hint": null + } + - `agreed_interest_rate`는 원문 그대로의 문자열("연 5%", "월 1%" 등)만 허용한다. + - `agreed_delay_damage_rate`는 약정 지연손해금율이다. 없으면 null로 둔다(법정이율로 보충 금지). + - `rate_text`는 이율 관련 원문 문자열 그대로, `rate_source_locator`는 그 이율이 읽힌 조항 locator(예: "제2조", "특약")다. + - `agreed_interest_rate`/`agreed_delay_damage_rate`는 계산·환산·보간 금지. 원문 문자열만 허용. + - `secured_debt_components`의 각 항목에도 동일한 optional rate 필드(`agreed_interest_rate`, `agreed_delay_damage_rate`, `rate_text`, `rate_source_locator`, `due_date`)를 직접 읽히는 범위에서 둘 수 있다(근저당권 설정계약서·차용증 겸용 문서의 이율 보존용). + - 이율이 문서에 있는데 두 rate 필드가 모두 null이면 self-warning으로 본다. + + + + - 답변서에 통지 수령 인정, 제3자 임대 자인, 월 차임 자인, 유치권자 사용 허용 주장, 후소유자 소멸청구권 부정 주장, 피담보채권 충당 주장이 있으면 `admission_components`와 `defense_components`로 분리한다. + - 통지서에는 `notice_components`를 생성하고, 통지 유형, 발송일, 도달 필요 여부, 수령자, 목적물, 법률효과 후보를 분리한다. + - `notice_components`의 각 항목은 아래 named 필드를 가진다. 직접 읽히지 않는 필드는 null. + { + "notice_id_proposed": "E-{ordinal:03d}-NT-{seq:02d}", + "notice_type": "최고|해제통지|유치권소멸청구통지|기타", + "drafted_date": null, + "dispatch_date": null, + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [], + "receiver_candidates": [], + "target_object_hint": null, + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "legal_effect_candidate_hint": null + } + - 5개 lifecycle 날짜의 의미: `drafted_date`=작성일, `dispatch_date`=발송일, `arrival_date`=도달일(민법 제111조 도달주의 — 도달≠수령을 구분), `received_date`=수령일(수령 인정 포함; 도달일과 다르면 별도 보존), `legal_effect_date_candidate`=법률효과 발생 후보일(계산 금지, 문서 직접 기재만). + - `requires_receipt_for_effect`는 true|false|null — 문서 유형상 도달/수령이 효력요건인지의 후보 표시다. + - `receipt_evidence_refs`는 수령을 뒷받침하는 같은 문서 내 단서 locator다. + - `recipient_admission_text`는 통지 수령을 인정하는 원문 문자열이며, 답변서 문서의 component에서만 채운다. + - 수령일이 상대방 답변서에서 인정되는 경우 그 답변서의 `admission_components`에 남기고, 본 통지서 문서의 `received_date`에 타 문서 사실을 주입하지 않는다(authority drift 금지). cross-document 연결은 Part 2 B5가 수행한다. + - 당사자가 요구한 기간이나 금액이 법률상 기산일로 자동 사용되면 안 되는 경우 `party_assertion_components.should_not_auto_use_as_legal_start_date=true`를 남긴다. + - 별지목록에는 `schedule_items`를 생성하고, 청구취지에서 그대로 사용할 수 있는 문구와 asset alias, 전유부분, 대지권, 도로명주소를 보존한다. + - `schedule_items`의 각 항목에 `schedule_ref`를 부여한다. 형식: `SCH-{ordinal:03d}-{seq:02d}` (별지 기재 순서 deterministic). asset alias 문자열은 기존대로 함께 보존한다. + - `schedule_items`의 각 항목에 optional 필드 `canonical_label_candidate`(별지 표제 문구 원문)와 `usable_in_relief`("yes|no|unknown" — 청구취지에서 그대로 사용할 수 있는 문구인지)를 직접 읽히는 범위에서 둘 수 있다. + - 등기부가 목적물 특정 자료로 쓰이면 `registry_object_display`를 생성한다. + - 가족관계증명서, 상속포기 심판문, 사망·상속 자료에는 `succession_document_components`를 생성하고 피상속인, 사망일, 배우자 후보, 직계비속 후보, 직계존속 후보, 상속포기자, 신고일·수리일, 연결 asset을 보존한다. + + + + + + - 기존 optional field `legal_calculation_object`는 backward-compatible summary field로 유지한다. + - 그러나 이 필드는 안전할 때만 사용한다. + - 문서가 `multi_event_doc` 또는 `multi_event_registry=true`이면 원칙적으로 `legal_calculation_object`를 생성하지 않는다. + - 문서에 서로 다른 법적 효과의 slot이 2개 이상 공존하면 단일 summary `legal_calculation_object`로 압축하지 않는다. + - 단일 효과가 명백한 문서, 또는 서로 다른 값이 경쟁하지 않는 문서에서만 `legal_calculation_object`를 생성한다. + + + + - multi-event 또는 mixed-contract 문서를 위해 optional field `legal_calculation_objects`를 추가할 수 있다. + - `legal_calculation_objects`는 현재 문서 안의 개별 slot을 권위 있게 분해한 배열이다. + - 문서당 0개 이상 허용한다. + - 단일 slot 문서라면 `legal_calculation_objects`를 생략할 수 있다. + - 각 slot은 현재 문서의 직접 기재만 사용한다. 계산, 합산, 차감 금지. + + + + 개별 slot은 아래 경우에 생성한다. + - 소유권이전 / 매매 / 증여 / 대물변제 / 경매매각 / 경매개시결정 / 근저당권설정 / 근저당권말소 / 가압류 / 임대차 / 영업양도 / 대여 / 담보제공 + - valuation 문서는 시가표/차임표/사용이익표를 slot이 아니라 `valuation_object`로 처리한다. + - 등기부는 갑구/을구의 의미 있는 row마다 1 slot씩 생성한다. + - 혼합계약서는 계약 조항상 독립된 법적 효과마다 1 slot씩 생성한다. + + 각 slot은 아래 구조를 따른다. + { + "slot_id": "E-{ordinal:03d}-LCO-01", + "slot_kind": "ownership_transfer|gift|sale|dation|mortgage_setting|mortgage_cancellation|auction_commencement|auction_sale|lease|building_sale|loan|business_transfer|other", + "source_locator": { + "locator_type": "heading|key_value|table_row|paragraph|unknown", + "locator_hint": "갑구4|을구1|제1조|특약 등", + "excerpt": "짧은 단서" + }, + "asset_id": null, + "property_label_for_schedule": null, + "transaction_type": "매매|증여|대물변제|null", + "transaction_date": null, + "registration_date": null, + "registry_office": null, + "registry_receipt_no": null, + "registry_recorded_transfer_date": null, + "market_value_at_act": null, + "market_value_at_close": null, + "sale_price": null, + "monthly_rent": null, + "consideration_breakdown": [], + "parties": [] + } + - slot이 등기부 row 또는 별지 항목에서 직접 파생되면 optional 필드 `registry_ref_hint`/`schedule_ref_hint`를 남긴다. + + + + - 임대차와 매매가 함께 있는 혼합계약서, 영업양도와 채무불인수 조항이 함께 있는 문서 등은 optional field `contract_components`를 추가할 수 있다. + - `contract_components`는 현재 문서의 조항별 법적 효과 분해 메모다. + - 각 component는 아래 구조를 따른다. + { + "component_id": "E-{ordinal:03d}-CC-01", + "component_kind": "lease|sale|delivery|management_use_shift|permit_cooperation|business_transfer|debt_non_assumption|other", + "object_spec": "짧은 목적물", + "amount": null, + "date": null, + "payer_candidates": [], + "recipient_candidates": [], + "possession_recipient_candidates": [], + "benefit_shift_to_candidates": [], + "source_locator": { + "locator_type": "paragraph|table_row|key_value|unknown", + "locator_hint": "제1조|제2조|특약 등", + "excerpt": "짧은 단서" + } + } + - 혼합계약서에서 component가 2개 이상 명백하면 2개 이상 생성한다. + - mixed contract에 임대차, 매매, 인도, 관리·사용 귀속이 직접 적혀 있으면 이를 lease/sale 1개로 뭉개지지 말고 가능한 한 분리한다. + - 조항에서 누가 대가를 지급하고 누가 수령했는지가 직접 읽히면 `payer_candidates`, `recipient_candidates`를 채운다. + - 인도받는 자가 직접 읽히면 `possession_recipient_candidates`를 채운다. + - 관리·사용 또는 사용수익 귀속 주체가 직접 읽히면 `benefit_shift_to_candidates`를 채운다. + - 위 directionality field는 현재 문서에 직접 적힌 경우에만 채우고, 추정으로 보충하지 않는다. + + + + - 시가, 차임시세, 사용이익 평가 문서는 optional field `valuation_object`를 추가할 수 있다. + - `valuation_object`는 아래 구조를 따른다. + { + "valuation_kind": "market_value|rent_value|use_gain_value", + "target_object": "짧은 목적물", + "basis_date_or_period": "원문 기간 또는 기준일", + "value_rows": [ + { + "label": "2022.1.1.~2022.12.31.|보증금 3억 원인 경우 등", + "amount": "금액 문자열", + "unit": "총액|월액|기타" + } + ] + } + - valuation 문서는 기존 `doc_type`를 바꾸지 않고 `doc_semantic_flags`와 `valuation_object`로 구체화한다. + - 차임표/사용이익표가 있으면 `market_value_*`를 억지로 채우지 않는다. + + + + + - 날짜: 정확한 일자가 있으면 `YYYY-MM-DD`, 없으면 `null` + - 단, `key_dates`와 `valuation_object.basis_date_or_period`는 짧은 원문 텍스트 허용 + - 금액: 입력에 명시된 값만 `"300,000,000원"` 형식 문자열로 정규화 + - 이름/당사자: 원문 우선, 명백히 동일할 때만 canonicalize + - `consideration_breakdown`는 직접 읽히는 짧은 대가 항목만 저장, 최대 4개, 각 40자 이하 + + + + part 파일은 단일 item object 형태이며, top-level에 mapper가 propose하는 모든 component field를 둔다. + + { + "evidence_index_proposed": "E-{ordinal:03d}", + "doc_uid": "DOC-{ordinal:03d}-...", + "title": "...", + "title_normalized": "...", + "doc_type": "판결/결정|공문서|거래기록|통신기록|처분문서|기타", + "key_facts": [], + "key_dates": [], + "key_amounts": [], + "key_parties": [], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": ordinal + }, + "property_cluster_id": null, + "doc_semantic_flags": [], + "multi_event_registry": null, + "contract_components": [], + "valuation_object": null, + "legal_calculation_object": null, + "legal_calculation_objects": [], + "claim_document_components": [], + "business_transfer_components": [], + "registry_row_components": [], + "secured_debt_components": [], + "valuation_rows": [], + "mixed_contract_payment_semantics": {}, + "loan_components": [], + "valuation_matrix": {}, + "registry_row_role_map": {}, + "defense_assertions": [], + "admission_components": [], + "defense_components": [], + "notice_components": [], + "party_assertion_components": [], + "schedule_items": [], + "registry_object_display": {}, + "succession_document_components": null, + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } + } + + 추가 제약: + - top-level은 단일 object다. envelope이 아닌 단일 item. + - `evidence_index_proposed`는 mapper의 propose 식별자이며, finalize는 quality gate가 한다. + - `mapper_meta.self_isolation_attested=true`는 본 mapper가 자기 문서 외 사실을 주입하지 않았음을 self-attest 한다는 의미다. + - optional component field는 직접 읽히는 범위에서만 채우고, 빈 배열/객체/`null`로 두는 것을 우선한다. + + + + - part 파일이 단일 object이고 `evidence_index_proposed`, `doc_uid`, `title`, `title_normalized`, `doc_type`, `source_pointer`를 모두 포함하는지 점검한다. + - `planner_status=="READY"` 및 `mapper_execution_allowed=true`가 확인되지 않았으면 part 파일이 작성되지 않았는지 점검한다. + - `evidence_index_proposed`가 정확히 `E-{ordinal:03d}` 형식인지 점검한다. + - 자기 문서 외 ordinal의 사실이 part에 주입되지 않았는지 self-check 한다(authority drift 방지). + - `registry_doc`이면서 서로 다른 법적 효과 row가 2개 이상 읽히면 `doc_semantic_flags`에 `multi_event_doc`를 넣고, 가능한 경우 `legal_calculation_objects`를 생성한다. + - `multi_event_registry=true` 또는 `doc_semantic_flags`에 `multi_event_doc`가 있으면 단일 `legal_calculation_object`를 억지로 채우지 않는다. + - `mixed_contract_doc`인데 명백한 독립 component가 2개 이상이면 `contract_components`를 2개 이상 생성한다. + - `mixed_contract_doc`에 인도 또는 관리·사용 귀속이 직접 적혀 있으면 `delivery` 또는 `management_use_shift` component가 누락되지 않았는지 점검한다. + - 혼합계약 조항에서 지급자·수령자 또는 점유 이전 상대방이 직접 읽히면 `payer_candidates`, `recipient_candidates`, `possession_recipient_candidates`, `benefit_shift_to_candidates`가 비어 있지 않은지 점검한다. + - `valuation_doc`인데 가치표/차임표가 직접 보이면 `valuation_object`를 생성한다. + - 같은 문서에서 복수 행위가 보이는데도 단일 snapshot `legal_calculation_object`만으로 압축하지 않는다. + - `claim_doc`인데 이행각서ㆍ채무승인ㆍ최고ㆍ변제기ㆍ청구금액 단서가 직접 보이고 `claim_document_components`가 없으면 self-warning으로 본다. + - 이행각서에 새 변제기가 있는데 `new_due_date`가 없으면 self-warning으로 본다. + - 최고서 또는 통지서에 도달·수령 단서가 있는데 `arrival_date` 또는 notice receipt component가 없으면 self-warning으로 본다. + - `business_transfer_doc`에 상호ㆍ영업명ㆍ영업자산ㆍ채무불승계 단서가 있는데 `business_transfer_components`가 없으면 self-warning으로 본다. + - 등기부에 접수번호가 있는데 `registry_row_components[*].receipt_no`가 없으면 self-error로 본다. + - 차용금 문서에 이율 또는 변제기가 있는데 `secured_debt_components` 또는 `loan_components`가 없으면 self-warning으로 본다. + - `loan_components`가 있는데 이율 문구가 문서에 보이면서 `agreed_interest_rate`가 null이면 self-warning으로 본다. + - valuation 문서에 복수 시점 또는 복수 row가 있는데 `valuation_rows[*].legal_use_case` 또는 `valuation_matrix.rows[*].legal_uses`가 없으면 self-warning으로 본다. + - 임대차보증금이 추출되었는데 `mixed_contract_payment_semantics`에서 반환성 보증금과 사용이익 원금 배제 여부가 구분되지 않으면 self-warning으로 본다. + - 등기부 문서에 사후 말소 row가 있는데 `registry_row_role_map` 또는 `registry_row_components`에 release role이 없으면 self-error로 본다. + - 답변서에 수령 인정ㆍ제3자 임대ㆍ항변이 있는데 `admission_components` 또는 `defense_components`가 없으면 self-warning으로 본다. + - 별지목록에 목적물 항목이 있는데 `schedule_items`가 없으면 self-error로 본다. + - 상속자료가 있는데 `succession_document_components`가 없으면 self-warning으로 본다. + - self-warning/self-error는 mapper가 part 파일에 그대로 남겨도 되며, quality gate가 최종 severity를 판정한다. + + + + + {{item.assigned_ordinal}} + + + [ ] 1) Preflight: `list_docs` 사용해서 `{{item.shard_doc_path}}`, `client_meeting.md` 만 확인, `read_docs` 사용해서 두 파일만 읽기 + [ ] 2) runtime parameter `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.b1_output_path}}` 그대로 소비. 별도 검증 read 불요. + [ ] 3) `{{item.shard_doc_path}}`의 단일 evidence 문서를 메모리에서 사용하고, 주입된 shard 밖으로 나가지 않기 + [ ] 4) `write_file` 사용해서 `evidence_indexed_parts/E-{ordinal:03d}.json` 생성 (ordinal은 `{{item.assigned_ordinal}}`) + [ ] 5) `list_docs` 사용해서 자기 part 파일만 존재 확인(절대 읽기 금지) + [ ] 6) `"E-{ordinal:03d} mapper part 작성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 1건; planner가 evidence_all.json에서 분리) + - `client_meeting.md` + + + - `evidence_indexed_parts/E-{ordinal:03d}.json` + + + + + - `evidence_index_proposed`는 정확히 `E-{ordinal:03d}` 형식만 허용한다. + - `ordinal`과 output path는 runtime parameter (`{{item.*}}`) 를 그대로 따른다. + - 증거 문서 수 N, assigned_ordinal, mapper fan-out, streaming edge는 본 task에서 만들지 않는다. + - 자기 part 파일은 절대 재읽지 않는다. + - 다른 ordinal 문서를 처리하지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"E-{ordinal:03d} mapper part 작성 완료"` 한 줄만 사용한다. + + + + - task_name: Task_B2_map_events_* + max_concurrency: 12 + preflight_files: + - "{{item.shard_doc_path}}" + - "{{item.b1_output_path}}" + - client_meeting.md + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + + Task_B2_map_events + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. Your role in this task is a **per-document event/state candidate mapper** within the v.3 Step D streaming map-reduce pipeline. + + + 본 task는 v.3 Step D의 B2 mapper다. orchestrator 가 runtime parameter (`{{item.*}}`) 로 주입한 assigned_ordinal 단일 문서의 event/state candidate(raw event + legal-effect event 포함)를 추출하여 part 파일을 작성한다. + 본 mapper 인스턴스는 orchestrator가 같은 ordinal의 B1 mapper(Task_B1_map_doc) 완료와 `evidence_indexed_parts/E-{ordinal:03d}.json` 생성을 확인한 뒤 시작한다. 본 task는 streaming edge를 설계하지 않고, planner/orchestrator가 이미 결정한 same-ordinal edge를 소비한다. + 최종 `evidence_event_candidates.json`은 본 task가 작성하지 않는다. 본 task는 `evidence_event_candidate_parts/E-{ordinal:03d}.json` part 파일만 작성하며, finalize는 `Task_B2_gate_merge_deterministic`가 수행한다. + 본 task는 같은 ordinal의 B1 part 파일과 planner shard row를 authority metadata로 참조하고, planner가 분리해 둔 `{{item.shard_doc_path}}` 의 raw 문서 1건을 메모리에서 사용한다. + 같은 문서 내 복수 legal-effect event는 분리한다. 문서 밖 사실을 candidate payload에 넣지 않는다(authority drift 금지). + `candidate_id`는 `EVT-{ordinal:03d}-{seq:02d}` 형식의 deterministic candidate로 propose 하되, final 부여 및 collision 처리는 quality gate가 수행한다. + 변호사 관점에서 본 단계의 핵심은: (a) 등기부 갑구/을구 row별 분리, (b) 혼합계약서의 계약성립/대가수수/인도/관리·사용 귀속 4종 분리, (c) 통지 발송일과 도달일의 분리 candidate, (d) 답변서의 admission/defense 분리 candidate, (e) valuation 문서의 시점별/지표별 candidate, (f) 가족관계·상속포기 문서의 succession candidate 분리다. + legal-effect event는 raw event와 함께 추출하며, payment, notice, auction, lien, succession, valuation, defense assertion, commercial successor liability에 대해 v.2 원본의 REQUIRED_EVENT_SUBKINDS를 그대로 따른다. + + + + + + - runtime parameters: `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.b1_output_path}}`, `{{item.b2_output_path}}` (planner 가 부여한 단일 source of truth — 별도 plan 파일 read 불요) + - `{{item.b1_output_path}}` (같은 ordinal 의 B1 mapper part — runtime parameter 그대로 사용) + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 문서 1건; planner가 evidence_all.json에서 분리해 둔 단일 shard 파일) + - `client_meeting.md` (보조 사용만 허용) + + + - `evidence_event_candidate_parts/E-{ordinal:03d}.json` (본 mapper 인스턴스의 part 파일 1개만) + + + - 다른 ordinal의 B1 part 또는 raw 문서 처리 + - 다른 ordinal의 사실을 자기 part에 주입하기 (authority drift) + - 증거 문서 수 N을 재계산하여 planner 결과를 대체하기 + - assigned_ordinal을 새로 생성하거나 재정렬하기 + - B1/B2 mapper fan-out 또는 streaming edge를 설계하기 + - 최종 `evidence_event_candidates.json` 작성 (이는 quality gate의 단독 권한) + - 다른 mapper의 part 파일 읽기/덮어쓰기 + - 자기 part 재읽기 + - 문서 summary를 다시 쓰는 작업으로 변질시키기 + - meeting만으로 문서에 없는 사건행위를 새로 만들기 + - 입력에 없는 날짜, 당사자, 금액, 목적물을 창작하기 + + + + + 1. `list_docs`로 `{{item.b1_output_path}}`, `{{item.shard_doc_path}}`, `client_meeting.md` 존재만 확인한다. + 2. `read_docs`로 위 세 파일을 읽는다. `{{item.shard_doc_path}}`는 이 인스턴스의 단일 evidence 문서로 그대로 사용한다 (별도의 ordinal 추출 불필요). assigned_ordinal·evidence_index_candidate·b2_output_path 는 runtime parameter (`{{item.*}}`) 그대로 사용한다. + 3. 메모리에서 part 객체를 생성한다. 단일 item object 또는 1-element items 형태이며, event 후보가 0건이어도 파일을 생략하지 말고 `zero_event_reason`을 반드시 가진다. + 4. `write_file` 전에 아래 을 수행한다. 불량이면 먼저 내부 보정을 시도한다. + 5. 보정 후에도 schema invariant가 불량이면 part 파일을 쓰지 않고 `FAILED: E-{ordinal:03d} event candidate part schema invariant violation`만 보고하고 종료한다. + 6. 통과한 경우에만 `write_file(overwrite=true)`로 runtime parameter 가 지정한 `evidence_event_candidate_parts/E-{ordinal:03d}.json`을 저장한다. + 7. `list_docs`로 자기 part 파일 존재만 확인한다. 절대 읽지 않는다. + 8. 정확히 `"E-{ordinal:03d} event candidates part 작성 완료"`만 출력하고 종료한다. + + + + - 입력 누락, 빈 파일, runtime parameter 누락, B1 part 부재, ordinal 매핑 실패, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. part 파일은 쓰지 않는다. + - event 후보가 0건인 것은 실패가 아니다. 이 경우에도 runtime parameter 가 지정한 output path에 `zero_event_reason`을 포함한 schema-valid part 파일을 쓴다. + - `event_candidates: []`와 `zero_event_reason` 누락 또는 null이 함께 존재하는 것은 정상 zero-event가 아니라 schema invariant 위반이다. + - part schema invariant 위반이 내부 보정 후에도 남으면 part 파일은 쓰지 않는다. 불완전한 part를 생성하여 quality gate에 넘기지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + + - 본 mapper 인스턴스는 정확히 runtime parameter (`{{item.assigned_ordinal}}`) 에 해당하는 1개 문서만 처리한다. + - runtime parameters (`{{item.*}}`) 는 ordinal/binding 의 단일 source of truth 다 (별도 plan 파일 read 불요). + - B1 part의 `evidence_index_proposed`, `title`, `doc_type`, `source_pointer.ordinal`을 authority로 사용한다. + - `{{item.shard_doc_path}}`의 단일 evidence 문서(planner가 evidence_all.json에서 분리)를 직접 참조하여 candidate를 추출한다. + - 본 task는 planner가 확정한 `evidence_index_candidate`, `b2_output_path`, assigned_ordinal을 소비할 뿐 재작성하지 않는다. + - 다른 ordinal의 사실을 자기 part에 주입하지 않는다(authority drift 금지). + - candidate layer에 머문다. 최종 BO, 최종 JuristicAct, 최종 청구권, 최종 사해행위 판단을 확정하지 않는다. + - 문서 단위 1요약으로 뭉개지 않는다. 한 문서에 복수의 법적 중요 행위가 있으면 반드시 여러 candidate로 분리한다. + - support가 희박한 후보를 억지로 채우지 않는다. 불명확한 필드는 `null` 또는 빈 배열을 사용할 수 있다. + - 잘못된 과잉 특정화보다 보수적 분해가 낫다. 다만 자기 문서 커버리지를 포기해서는 안 된다. + - `candidate_id`는 deterministic 규칙 `EVT-{ordinal:03d}-{seq:02d}`로 생성한다(seq는 자기 part 내 1-based 순번). + + + + 이 인스턴스의 `{{item.shard_doc_path}}`(planner가 evidence_all.json에서 분리한 단일 evidence 1건) + `evidence_indexed_parts/E-{ordinal:03d}.json`의 authority metadata + `client_meeting.md` + + + `client_meeting.md`는 아래 목적에만 사용할 수 있다. + - alias 정규화 + - 동일인 식별 보조 + - property cluster shorthand 해석 + - 현재 문서가 어느 상담 조항을 뒷받침하는지에 대한 `meeting_clause_refs` 보조 + - 현재 문서에서 직접 읽히는 내용의 중요도 보조 + - 사망자와 특정 자산군의 연결이 meeting에서 직접 읽히는 경우 succession 관련 candidate의 `affected_asset_cluster_ids` 보조 + 금지: + - meeting만으로 문서에 없는 사건행위를 candidate로 생성 + - meeting만으로 현재 문서의 당사자/금액/날짜를 보충해 direct evidence처럼 쓰기 + + + + + - 자기 part는 정확히 1개 item을 가진다. + - 그 item은 아래 둘 중 하나를 반드시 가진다. + - `event_candidates`가 1개 이상 + - `zero_event_reason`가 non-null + - `zero_event_reason` 허용값: + - `"NO_LEGAL_EVENT_FOUND"` + - `"REFERENCE_ONLY_DOC"` + - `"PARSE_INSUFFICIENT"` + - `"ORDINAL_MATCH_FAILED"` + - 등기부, 계약서, 통지서, 답변서, 감정/시세 문서처럼 통상 event-bearing 문서에 대해서는 `zero_event_reason`를 매우 예외적으로만 사용한다. + + + + `write_file` 전에 반드시 메모리의 part object를 검증한다. 이 검증은 생성 지점 안전장치이며 자동 강제 재실행을 의미하지 않는다. + + 필수 invariant: + 1. 메모리상 top-level은 단일 item object 또는 1-element `items` envelope일 수 있으나, `write_file` 직전 serialization 대상은 단일 item object여야 하며 실질 item은 정확히 1개다. + 2. item의 `evidence_index_proposed`는 runtime parameter `{{item.evidence_index_candidate}}` 또는 그와 동일한 `E-{ordinal:03d}` 값이어야 한다. + 3. item은 아래 둘 중 하나를 반드시 만족해야 한다. + - `event_candidates`가 non-empty list + - `zero_event_reason`이 non-null이며 허용값 중 하나 + 4. `event_candidates`가 빈 배열이면 `zero_event_reason`은 반드시 non-null 허용값이어야 한다. + 5. candidate가 없어 `zero_event_reason`이 non-null이면 schema 안정성을 위해 `event_candidates`는 빈 배열로 둔다. + 6. candidate가 있으면 각 candidate는 object이며, `candidate_id_proposed`는 `EVT-{ordinal:03d}-{seq:02d}` 형식이고 `source_evidence_index_proposed`는 자기 `evidence_index_proposed`와 일치해야 한다. + + 내부 보정 원칙: + - 문서 검토 결과 법적 중요 event/state 후보가 없는 것이 맞으면 `zero_event_reason`을 허용값 중 하나로 명시한다. + - event/state candidate 생성 조건이 충족되는데 빈 배열이면 extraction routines로 되돌아가 candidate를 재작성한다. + - 통상 event-bearing 문서에 `zero_event_reason`을 사용하는 경우, 원문상 후보 부재 또는 파싱 불충분 이유가 허용값과 맞는지 재확인한다. + + 보정 실패: + - 위 invariant 중 하나라도 충족하지 못하면 `write_file`을 호출하지 않는다. + - 마지막 채팅 출력은 정확히 `FAILED: E-{ordinal:03d} event candidate part schema invariant violation`로 한다. + + + + 아래 범주의 법적 중요 행위를 후보로 추출한다. + - 소유권 이전, 매매, 증여, 대물변제, 처분 + - 근저당/저당/전세권/가압류/압류/담보 설정 및 말소 + - 대여, 보증, 신용보증, 대위변제, 구상권 발생, 변제, 배당 + - 임대차, 점유, 인도, 현재 점유, 현재 사용자, 현재 사용수익, 현재 수익귀속, 현재 권리귀속, 현재 관리 상태 + - 경매신청, 경매개시결정, 경매매각, 인도명령, 집행 관련 사실 + - 통지, 최고, 해제/해지의사표시, 도달, 채무승인, 감액요구, 책임부인, 현재상태 주장 + - 판결, 결정, 지급명령, 배당표 등 소송/집행 관련 법적 중요 사실 + - 신축, 영업양도, 사업자등록, 현재 영업상태 등 사건핵심 상태 형성 행위 + - 위 범주와 직접 연결되는 기타 법적 중요 행위 또는 현재상태 + + 제외: + - 순수 배경설명만 있는 문장 + - 정보 증가 없는 반복 진술 + - 법적 효과와 관련 없는 주변사정 + + + + + - 갑구/을구의 각 의미 있는 row를 개별 candidate로 본다. + - 최소 추출 대상: 소유권보존, 소유권이전, 증여, 매매, 담보권실행을 위한 경매개시결정, 담보권실행을 위한 경매로 인한 매각, 근저당권설정, 근저당권설정등기말소, 가압류. + - `support_locators.locator_hint`에는 가능하면 `갑구4`, `을구1`, `순위번호 3` 같은 row identifier를 남긴다. + - 말소표시가 있더라도 법적 의미가 있으면 candidate로 남길 수 있다. + - row에 지분 표시가 직접 읽히면 candidate에 같은 ordinal B1 part의 `registry_row_components[*].fraction_ref`와 동일한 `fraction_ref`를 남긴다. 지분 표시가 없으면 null. + + + + - 하나의 문서에 서로 다른 계약 요소가 공존하면 clause별로 분해한다. + - 예: 대지 임대차 + 건물 매매 + 인도 + 관리사용 귀속 + 명의변경 협력은 가능한 한 별개 candidate로 분리한다. + - B1 part의 `contract_components`가 있으면 그것을 우선 참고하되, raw 문서 확인을 생략하지 않는다. + - 혼합계약서가 계약성립, 대가수수, 인도, 관리·사용 귀속을 직접 보여주면 아래 후보군을 가능한 한 독립 candidate로 생성한다. + 1. 계약성립 candidate + 2. 대가수수 candidate + 3. 인도 candidate + 4. 관리·사용 또는 현재 수익귀속 state candidate + - 위 4종 중 raw 문서가 직접 보여주는 것은 formation candidate 1개로 뭉개지지 않도록 반드시 분리한다. + - 대가수수 candidate는 적절한 기존 enum이 없으면 `event_kind="기타"`, `event_subkind="대가수수"`로 둘 수 있다. + - 대가수수 candidate에는 가능한 한 지급자, 수령자, 금액, 목적물, 해당 조항 locator를 함께 남긴다. + - 인도 candidate에는 가능한 한 인도자, 인도수령자, 목적물, 개시일, locator를 함께 남긴다. + - 관리·사용 귀속 candidate는 `event_or_state="state"`를 우선 검토하고, `current_state_candidate` 하위 4축과 interval field를 함께 남긴다. + - 문서가 계약일로부터 현재 점유/사용 귀속을 직접 보여주면 `state_start_date`를 그 개시일로 남기고, 뒤에 다른 점유/사용 candidate가 있으면 선행 state를 open-ended로 두지 않는다(같은 문서 내). + + + + - 내용증명, 답신, 답변서, 최고서, 해제통지서, 소멸청구 통지서에서는 아래를 개별 candidate로 검토한다. + - 지급 요구 + - 해제/해지 통지 + - 도달 + - 책임 부인 + - 현재 점유/현재 거주/현재 운영 주장 + - 사용수익 귀속 주장 + - 현재 권리귀속 주장 + - 채무승인 또는 시효항변 주장 + - 감액요구, 명도요구 + - 문서에 현재 상태가 직접 적혀 있으면 `event_or_state="state"` candidate를 생성할 수 있다. + - 현재 상태가 후행 시점에서 선행 상태를 대체하는 구조가 직접 보이면 interval field를 함께 남긴다. + + + + - 감정평가서, 감정평가 의견서, 임료 시세 확인서 등은 `event_candidates`가 0개라고 판단하지 않는다. + - 시가표, 차임표, 사용이익표, 시장가치표가 있으면 최소 1개의 factual/state candidate를 생성한다. + - rent/use-gain valuation은 `action_type_candidate="사실행위(factual acts)"`로 둘 수 있다. + - 문서가 특정 현재 수익귀속자 또는 현재 권리귀속자를 직접 보여주면, 해당 정보는 `current_state_candidate`의 대응 배열에 분리해 둔다. + + + + - 사업자등록증, 가족관계증명서, 현재상태 확인 문서, 심판문 등은 상태형 후보 또는 법적 지위 관련 후보를 생성할 수 있다. + - 예: 사업자등록은 현재 영업상태/점포 운영 개시의 간접 또는 직접 후보가 된다. + - 가족관계증명서, 상속포기 심판문, 상속 관련 공문서가 사망자와 친족구성 또는 상속포기를 직접 보여주면 succession 관련 candidate를 생성한다. + - sparse status 문서라도 later stage linker가 사용할 수 있도록 가능한 범위에서 `participants`, `object_spec`, `event_date` 또는 `current_state_candidate.state_as_of`를 채운다. + - family/renunciation/status 문서를 generic status로만 남기지 않는다. + + + + - 사망, 가족관계, 상속포기, 재산상속회복, 승계 관련 문서는 가능한 한 succession 관련 candidate를 생성한다. + - succession 관련 candidate에는 optional field `affected_asset_cluster_ids`를 둘 수 있다. + - `affected_asset_cluster_ids`는 meeting이나 B1 part metadata가 사망자와 특정 자산군의 연결을 직접 또는 명백하게 보여줄 때만 채운다. + - succession 관련 candidate에는 optional field `succession_candidate`를 둘 수 있다. + { + "deceased_candidate": null, + "spouse_candidates": [], + "lineal_ascendant_candidates": [], + "lineal_descendant_candidates": [], + "renunciant_candidates": [] + } + - 가족관계증명서 같은 status doc은 direct 내용이 보이면 `deceased_candidate`, `spouse_candidates`, `lineal_ascendant_candidates`, `lineal_descendant_candidates`를 가능한 한 채우고, 자산 연결이 직접 또는 명백하면 `affected_asset_cluster_ids`도 함께 채운다. + - 상속포기 문서는 상속포기 그 자체를 direct candidate로 남기되, direct names가 보이면 `succession_candidate.renunciant_candidates`를 반드시 채운다. + - 상속포기 문서가 특정 사망자와 자산 cluster를 직접 또는 명백하게 연결하면 동일한 `affected_asset_cluster_ids`를 함께 남긴다. + - direct names가 있는 succession/status 문서에서는 가능한 범위에서 `participants`와 `object_spec`도 함께 남겨 later stage overlap scorer가 sparse document를 놓치지 않게 한다. + - 이 단계는 최종 상속분, 단독상속, complaint plaintiff narrowing을 확정하지 않는다. 다만 downstream이 succession bundle을 읽을 수 있을 정도의 구조는 반드시 남긴다. + + + + + - raw event는 문서가 직접 보여주는 사실행위·법률행위·준법률행위다. + - legal-effect event는 raw event에서 downstream 청구권 식별에 필요한 법률효과 후보를 도출한 candidate다. + - raw event와 legal-effect event는 같은 문서에서 함께 생성할 수 있으며, 서로 다른 `candidate_id`를 가진다. + - legal-effect event는 최종 법률판단이 아니라 Stage 2가 검토할 구조화된 후보이므로 `confidence`와 `review_flags`를 함께 남긴다. + + + 금전채권: + - `underlying_claim_arising` + - `first_demand` + - `demand_notice_dispatch` + - `demand_notice_arrival` + - `debt_acknowledgement` + - `new_due_date` + - `business_transfer` + - `trade_name_continuation_candidate` + - `commercial_successor_liability_candidate` + + 담보부 부동산: + - `secured_debt_creation` + - `mortgage_setting_by_fraction` + - `undesignated_payment` + - `payment_allocation_effect` + - `mortgage_extinguishment_by_fraction` + - `auction_application_after_extinguishment` + - `auction_sale_and_registry_transfer` + - `derivative_mortgage_setting_by_invalid_title_holder` + - `unregistered_building_original_acquisition` + - `de_facto_disposition_holder_acquisition` + - `land_use_gain_start` + - `liability_overlap_start` + + 사해행위취소: + - `preserved_claim_arising` + - `collateral_setting_for_preserved_claim` + - `fraudulent_transfer` + - `asset_value_at_fraudulent_act` + - `asset_value_at_close_proxy` + - `collateral_insufficiency_at_act` + - `encumbrance_release_after_act` + - `value_compensation_exception_trigger` + - `value_compensation_cap_calculation_candidate` + - `defense_assertion_title_trust` + - `defense_assertion_sufficient_collateral` + - `defense_assertion_setoff` + + 유치권ㆍ상속ㆍ통지: + - `lien_assertion` + - `third_party_lease_by_lien_holder` + - `lien_violation_candidate` + - `lien_extinction_notice_dispatch` + - `lien_extinction_notice_receipt` + - `lien_extinction_effect_candidate` + - `ongoing_physical_possession` + - `succession_renunciation_filing` + - `succession_renunciation_acceptance` + - `spouse_sole_succession_effect_candidate` + - `defense_assertion_subsequent_owner_no_extinction_right` + - `defense_assertion_benefit_applied_to_secured_claim` + + + + - claim_doc 문서가 채무승인, 최고, 변제기, 변제약정, 지급기일을 함께 포함하면 하나의 generic event로 압축하지 말고 legal effect별 candidate를 분리한다. + - 이행각서에는 최소한 `debt_acknowledgement`, `payment_promise`, `new_due_date` 후보가 있는지 검토한다. + - 최고서에는 `demand_notice_dispatch`, `demand_notice_arrival`, `limitation_preservation` 관련 후보가 있는지 검토한다. + - 발송일과 도달일이 다르면 각각 별도 `event_date`로 보존한다. + - 채무승인 event에는 `legal_effect_candidate.acknowledgement_of_debt=true`, `limitation_interruption_candidate=true`, `related_claim_type`을 직접 읽히는 범위에서 남긴다. + + + + - business_transfer_doc 문서는 단순 영업양도 formation event로만 끝내지 않는다. + - 문서 또는 상담내용에서 상호, 영업명, 영업자산, 고객관계, 고용관계, 채무불승계 조항, 책임배제 등기 필요성이 보이면 `commercial_successor_candidates`로 분해한다. + - 상법 제42조 책임을 최종 확정하지는 않되, downstream이 요건판단을 할 수 있도록 transferor, transferee, business_name, trade_name_continuation_candidate, debt_exclusion_clause_candidate를 남긴다. + + + + - 원자료 event가 곧바로 법률효과를 발생시키는 경우 raw event와 legal-effect event를 모두 생성한다. + - 복수 채무에 대한 지정 없는 변제가 등장하면 `undesignated_payment` event와 별도로 `payment_allocation_effect` 후보를 생성한다. + - 등기부상 경매 매각과 후행 근저당권이 등장하면, 그 경매가 어떤 담보권에 기초했는지, 그 담보권의 피담보채무가 이미 소멸했는지 검토하는 event chain을 만든다. + - 미등기 건물 신축일과 미등기 건물 매수일은 점유·사용이익 발생 시점을 정하므로 `land_use_gain_start` 및 `liability_overlap_start` event 후보를 생성한다. + - `mortgage_setting_by_fraction`/`mortgage_extinguishment_by_fraction` 등 `*_by_fraction` subkind candidate는 지분 표시가 문서에 직접 읽히는 한 `fraction_ref`를 반드시 가진다. + + + + - 사해행위취소 사건에서 raw event와 legal-effect event를 구분한다. + - 사해행위 당시 존재하던 담보권이 사후 말소되면 `value_compensation_exception_trigger` event를 생성한다. + - valuation row와 사후 말소 부담, 존속 부담이 모두 있으면 `value_compensation_cap_calculation_candidate`를 생성한다. + - 피고가 항변을 명시한 내용증명은 `defense_assertion_*` event로 만들고, Stage 2 claim description에 반영해야 하는지 표시한다. + + + + - 통지 문서와 답변서가 서로 대응하고, 답변서가 특정 통지의 수령일을 인정하면 별도의 receipt event candidate를 생성한다. + - receipt event candidate는 발송 event와 병합하지 않는다. + - 발송일과 도달일이 다르면 도달일을 법률효과 후보일로 별도 보존한다. + - 문서에 유치권, 공사잔금, 점유, 임대, 직접 거주, 피담보채권 충당, 후소유자 항변이 함께 나타나면 일반 current possession event가 아니라 lien_event 후보군으로 분해한다. + - 제3자 임대는 `lien_violation_candidate`로, 직접 거주는 ongoing use state로, 피담보채권 충당 주장은 `defense_candidate`로, 소멸청구 수령은 `lien_extinction_effect_candidate`로 각각 분리한다. + - 상속포기 수리 event가 있고 배우자와 자녀 전부 상속포기 구조가 직접 보이면 `spouse_sole_succession_effect_candidate`를 생성하되, 최종 소장 원고 확정은 Task_C 이후로 미룬다. + + + + + `action_type_candidate`는 아래 중 1개를 선택한다. + - `"법률행위(legal acts)"` + - `"준법률행위(quasi-legal acts)"` + - `"사실행위(factual acts)"` + - `"위법행위(unlawful acts)"` + - `"소송행위(litigation acts)"` + - `"불명"` + + + + `event_kind`는 아래 값 중 가장 가까운 것을 선택한다. + - `"처분"`, `"소유권이전"`, `"증여"`, `"대물변제"`, `"담보설정"`, `"담보말소"`, `"채권발생"`, `"보증"`, `"대위변제"`, `"변제"`, `"배당"`, `"임대차"`, `"점유"`, `"인도"`, `"가압류/압류"`, `"경매신청"`, `"경매개시"`, `"경매매각"`, `"통지"`, `"해제/해지"`, `"판결/결정"`, `"영업양도"`, `"사업자등록"`, `"신축"`, `"상속개시"`, `"상속포기"`, `"채권관리"`, `"도달"`, `"항변"`, `"가치평가"`, `"법률효과"`, `"기타"` + + 필요하면 optional field `event_subkind`에 더 구체한 라벨(REQUIRED_EVENT_SUBKINDS 참조)을 남길 수 있다. + + + + - `participants.actor_candidates`는 직접 행위를 한 사람/법인 + - `participants.counterparty_candidates`는 직접 상대방 + - `participants.beneficiary_candidates`는 행위로 이익을 직접 취득한 사람/법인 + - `participants.third_party_candidates`는 법원, 피상속인, 기존 권리자 등 제3자 + - `object_spec`는 짧은 목적물/채권/담보/통지대상/상태 대상 명사구 + - `amount`는 현재 candidate와 직접 연결되는 대표 금액 1개만 사용한다 + - state candidate의 현재 점유/사용/수익/권리 귀속은 `participants`에 뭉개지지 않고 `current_state_candidate` 하위 배열에 분리한다 + - succession/status 문서처럼 sparse한 문서라도 direct names와 direct relation이 보이면 `participants`와 `object_spec`를 불필요하게 비워두지 않는다. + - 잘못된 당사자 확정보다 `[]` 또는 `null`이 낫다 + + + + - `event_date`는 가능하면 `YYYY-MM-DD` + - 불명확하면 `null` + - `time_text`는 짧은 원문 표현을 유지할 수 있다 + - `time_precision` 허용값: `"exact"`, `"approximate"`, `"range"`, `"unknown"` + - 현재상태 candidate의 경우 `state_as_of` 개념은 optional `current_state_candidate.state_as_of`에 남기고, `event_date`는 상태 개시 시점이 직접 보일 때만 채운다. + - state interval이 직접 보이면 `current_state_candidate.state_start_date`, `current_state_candidate.state_end_date_candidate`를 함께 채운다. + - 후행 상태가 선행 상태를 대체하는 것이 직접 보이면 `current_state_candidate.superseded_by_candidate_id`를 사용한다. + + + + - optional field `event_or_state`를 추가할 수 있다. 허용값은 `"event"` 또는 `"state"`. + - 문서가 단발 사건이 아니라 `현재까지`, `현재도`, `계속`, `현재 운영`, `현재 점유`, `현재 거주`, `부지로만 사용`, `현재 명의`, `현재 소유`, `차임을 받고 있다` 같은 계속상태를 직접 보여주면 `event_or_state="state"` candidate를 생성할 수 있다. + - state candidate에는 optional field `current_state_candidate`를 둘 수 있다. + { + "state_summary": "짧은 상태 설명", + "state_as_of": "현재|2024-10-31 등", + "state_start_date": null, + "state_end_date_candidate": null, + "superseded_by_candidate_id": null, + "is_ongoing": true, + "current_possessor_candidates": [], + "current_user_candidates": [], + "current_benefit_holder_candidates": [], + "current_right_holder_candidates": [], + "possession_legal_cause_asserted": null, + "possession_legal_cause_asserted_by": [] + } + - `current_state_candidate`는 가능한 한 아래 4축으로 분해한다. + - 점유자 -> `current_possessor_candidates` + - 사용자/운영자 -> `current_user_candidates` + - 차임·사용이익·영업수익 등 현재 수익귀속자 -> `current_benefit_holder_candidates` + - 현재 소유자·등기명의인·권리귀속자 -> `current_right_holder_candidates` + - 같은 사람이 복수 role을 가지면 각 배열에 중복 기재할 수 있다. + - 문서에 직접 읽히지 않는 축은 빈 배열로 둔다. 추정으로 채우지 않는다. + - event candidate와 state candidate는 동일 문서에서 함께 존재할 수 있다. + - 동일 문서에 과거 행위와 현재 상태가 함께 있으면 분리한다. + - 동일 문서에 현재 점유자와 현재 권리귀속자가 다르게 읽히면 하나로 뭉개지지 않도록 분리 기재한다. + - 상태 개시 시점이 직접 보이면 `state_start_date`를 채운다. + - 후행 점유/사용/수익귀속 candidate가 선행 상태를 대체하는 것이 직접 보이면, 선행 state에 `state_end_date_candidate`와 `superseded_by_candidate_id`를 남기고 open-ended current state로 방치하지 않는다. + - `state_end_date_candidate`가 직접 보이면 `is_ongoing=false`를 우선 검토한다. + - 혼합계약서가 인도, 관리·사용 귀속, 사용수익 귀속을 직접 보여주면 그 상대방에 대한 state candidate를 high confidence로 생성할 수 있다. + - raw 문서가 현재 점유, 현재 사용, 현재 수익귀속, 현재 권리귀속을 직접 적시하면 candidate `confidence`는 원칙적으로 `high`를 우선 검토한다. + - meeting 보조나 간접 정황에 크게 의존한 상태 후보는 `medium` 또는 `low`로 둔다. + - `possession_legal_cause_asserted`는 optional 필드다. 허용값: "유치권행사중|임대차|사용대차|물권|동시이행항변|명도후잔류|무권원주장|미상". `possession_legal_cause_asserted_by`는 그 권원을 주장한 주체(원고측/피고측/제3자 이름)이며 문서에 직접 읽힐 때만 채운다. + - 점유 사실과 권원 주장은 같은 candidate 안에서 축을 분리해 남긴다(사실=4축 배열, 권원=`possession_legal_cause_asserted`). 권원 주장이 문서에 없으면 `미상`이 아니라 null로 둔다. + - 서로 다른 문서가 동일 자산에 대해 다른 권원을 주장하는 것은 본 mapper의 검출 대상이 아니다(authority drift 금지). 각 문서의 주장을 그대로 보존한다. + + + + - optional field `meeting_clause_refs`를 추가할 수 있다. + - 형식은 `1-가`, `2-라`, `3-다`, `4-가`, `5-나`처럼 `client_meeting.md`의 본문 번호체계를 사용한다. + - 현재 문서가 특정 상담조항을 직접 뒷받침할 때만 기록한다. + - meeting clause ref는 보조 recall 장치일 뿐, 문서에 없는 사실을 가져오는 수단이 아니다. + + + + 이 단계는 최종 사해행위 판단을 하지 않는다. 다만 후속 단계의 recall 강화를 위해 candidate tag를 남긴다. + `actio_relevance_candidates.candidate_role_tags` 허용값: + - `"사해행위목적물후보"`, `"원고채권후보"`, `"선순위담보후보"`, `"존속담보후보"`, `"임차권후보"`, `"가압류후보"`, `"수익자이익후보"`, `"현재점유후보"`, `"현재사용수익후보"` + + `fraudulent_act_date_candidate`는 다음 경우에만 채운다. + - 현재 candidate가 직접 목적물의 처분, 이전, 이전등기, 매매일, 증여일, 대물변제일, 경매매각일과 연결되는 경우 + - 그 외에는 쓰지 않는다 + + + + downstream Task_C BO와 evidence 기반 candidate가 중복 제거할 수 있도록, 각 candidate마다 아래 정규화 문자열을 생성한다. + + `(event_or_state + event_kind + 핵심 actor + 핵심 counterparty + 핵심 object_spec/amount + 핵심 date_or_state_as_of)` + + 규칙: + - 불명확한 요소는 생략하되, 최소한 `event_or_state` 또는 `event_kind`와 1개 이상의 핵심 요소는 남긴다. + - 중복 제거를 위해 whitespace, 중복 조사, 과도한 수식어는 제거한다. + - title이나 evidence_index는 넣지 않는다. + - event와 state는 같은 signature가 되지 않도록 구분한다. + - mapper가 propose하는 candidate_id는 `EVT-{ordinal:03d}-{seq:02d}`이며 quality gate가 collision을 처리한다. + + + + part 파일은 단일 item object 형태이며, top-level item은 non-empty `event_candidates` 또는 non-null `zero_event_reason` 중 하나를 반드시 가진다. `event_candidates: []` 단독은 금지된다. + + { + "evidence_index_proposed": "E-{ordinal:03d}", + "title": "문서 제목", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": ordinal + }, + "property_cluster_id": null, + "doc_semantic_flags": [], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-{ordinal:03d}-01", + "event_or_state": "event|state", + "event_kind": "소유권이전", + "event_subkind": null, + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "짧은 1문장 요약", + "source_evidence_index_proposed": "E-{ordinal:03d}", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [], + "counterparty_candidates": [], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": null, + "fraction_ref": null, + "amount": null, + "event_date": null, + "event_end_date": null, + "time_text": null, + "time_precision": "exact|approximate|range|unknown", + "location": null, + "legal_keywords": [], + "support_locators": [ + { + "locator_type": "heading|key_value|table_row|paragraph|caption|unknown", + "locator_hint": "갑구4|제2조 등", + "excerpt": "짧은 단서", + "directness": "직접|간접|불명" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "string|null", + "related_claim_type": "string|null", + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "state|점유|...", + "confidence": "high|medium|low|unknown" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } + } + + 추가 제약: + - top-level은 단일 object다. envelope이 아닌 단일 item. + - `event_candidates`가 빈 배열이면 `zero_event_reason`은 반드시 non-null 허용값이어야 한다. + - `event_candidates: []`와 `zero_event_reason` 누락/null이 결합된 part는 작성 금지되는 schema invariant violation이다. + - `candidate_id_proposed`는 mapper의 propose 식별자(`EVT-{ordinal:03d}-{seq:02d}`)이며, finalize는 quality gate가 한다. + - `source_evidence_index_proposed`는 mapper가 propose 한 자기 ordinal의 candidate evidence_index다. + - `mapper_meta.self_isolation_attested=true`는 본 mapper가 자기 문서 외 사실을 주입하지 않았음을 self-attest 한다는 의미다. + + + + - `write_file` 전 을 수행하고, 불량이면 내부 보정을 먼저 시도했는지 점검한다. + - 자기 part object가 단일 item이고 `evidence_index_proposed`, non-empty `event_candidates` 또는 non-null `zero_event_reason`을 가지는지 점검한다. + - `event_candidates: []`와 `zero_event_reason` 누락/null이 함께 있으면 내부 보정 대상이며, 보정 불가 시 part 파일을 쓰지 않고 FAILED로 종료한다. + - `planner_status=="READY"` 및 `mapper_execution_allowed=true`가 확인되지 않았으면 part 파일이 작성되지 않았는지 점검한다. + - 모든 candidate의 `candidate_id_proposed`가 `EVT-{ordinal:03d}-{seq:02d}` 형식인지 점검한다. + - 자기 문서 외 ordinal의 사실이 part에 주입되지 않았는지 self-check 한다(authority drift 방지). + - `registry_doc` 또는 `multi_event_doc`인데 `event_candidates`가 0개면 다시 점검한다. + - `mixed_contract_doc`인데 서로 다른 법적 효과가 2개 이상 보이면 candidate도 최소 2개 이상 검토한다. + - 혼합계약서가 계약성립, 대가지급·수령, 인도, 관리·사용 귀속을 직접 보여주면 formation candidate만 남기지 말고 consideration/delivery/state candidate가 함께 검토되었는지 점검한다. + - 문서가 위 네 요소를 모두 직접 보여주면 최소 3개 이상, 가능하면 4개까지 독립 candidate로 분리되었는지 점검한다. + - `valuation_doc`인데 가치표/차임표가 직접 보이면 candidate를 최소 1개 생성한다. + - 문서가 상담일지의 명시 조항을 직접 뒷받침하는데도 `meeting_clause_refs`가 전혀 없으면 다시 점검한다. + - event와 state는 구분해 생성한다. 현재상태를 과거 단발행위로 뭉개지 않는다. + - state candidate에서 현재 점유자/사용자/수익귀속자/권리귀속자가 직접 읽히는데도 하나의 participant 배열로 뭉개졌다면 다시 점검한다. + - 현재 점유자와 현재 권리귀속자가 다르게 읽히는 문서는 `current_state_candidate` 하위 배열에서 분리되었는지 다시 점검한다. + - 상태 개시와 종료가 직접 보이거나 후행 상태가 선행 상태를 대체하는 것이 직접 보이면 `state_start_date`, `state_end_date_candidate`, `superseded_by_candidate_id` 검토가 이루어졌는지 점검한다. + - 가족관계증명서가 사망자와 친족구성을 직접 보여주면 `succession_candidate.deceased_candidate`, `spouse_candidates`, `lineal_descendant_candidates` 또는 `lineal_ascendant_candidates`가 필요한 범위에서 채워졌는지 점검한다. + - 상속포기 심판문이 direct names를 보여주면 `succession_candidate.renunciant_candidates`가 비어 있지 않은지 점검한다. + - 가족관계증명서, 상속포기 심판문, 상속 관련 공문서가 사망자와 특정 자산군의 연결을 직접 또는 명백하게 보여주면 `affected_asset_cluster_ids` 검토가 이루어졌는지 점검한다. + - succession/status 문서처럼 sparse한 문서라도 direct names, direct relation, direct date가 보이는데 `participants`, `object_spec`, `event_date` 또는 `state_as_of`가 불필요하게 비어 있지 않은지 점검한다. + - raw 문서가 현재상태를 직접 적시한 경우 candidate `confidence`가 불필요하게 low로 떨어지지 않았는지 점검한다. + - claim doc에 채무승인ㆍ최고ㆍ변제기 재설정 단서가 있는데 `legal_effect_candidate`가 있는 event가 전혀 없으면 self-warning으로 본다. + - 이행각서에 채무승인일과 새 변제기가 함께 있는데 `debt_acknowledgement`와 `new_due_date`가 분리되지 않으면 self-warning으로 본다. + - business transfer 문서에 상호속용 후보 event 또는 `commercial_successor_candidates`가 없으면 self-warning으로 본다. + - 복수 피담보채무와 변제가 있는데 `payment_allocation_effect`가 없으면 self-warning으로 본다. + - 경매 event가 있는데 기초 담보권 및 피담보채무 chain과 연결되지 않으면 `AUCTION_BASIS_CHAIN_MISSING` self-warning으로 본다. + - 미등기 건물 매수 event가 있는데 원시취득 event가 없으면 `UNREGISTERED_BUILDING_ORIGINAL_ACQUISITION_MISSING` self-warning으로 본다. + - 사후 담보권 말소 row가 있는데 `value_compensation_exception_trigger` event가 없으면 self-error로 본다. + - valuation matrix와 released encumbrance가 있는데 `value_compensation_cap_calculation_candidate`가 없으면 self-warning으로 본다. + - defense assertion event가 있는데 `defense_candidate.requires_rebuttal_in_stage2` 검토가 없으면 self-warning으로 본다. + - 유치권 소멸 통지서와 수령 인정 답변서가 있는데 도달 event 또는 `lien_extinction_notice_receipt`가 없으면 self-error로 본다. + - 제3자 임대 event가 있는데 `lien_violation_candidate`가 없으면 self-warning으로 본다. + - 상속포기 수리 event가 있는데 `spouse_sole_succession_effect_candidate` 후보가 없으면 self-warning으로 본다. + - self-warning/self-error는 mapper가 part 파일에 그대로 남겨도 되며, quality gate가 최종 severity를 판정한다. + + + + + {{item.assigned_ordinal}} + + + [ ] 1) Preflight: `list_docs` 사용해서 `{{item.b1_output_path}}`, `{{item.shard_doc_path}}`, `client_meeting.md` 만 확인. `read_docs` 사용해서 동일 파일만 읽기 + [ ] 2) runtime parameter `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.b1_output_path}}`, `{{item.b2_output_path}}` 그대로 소비. 별도 검증 read 불요. + [ ] 3) `{{item.shard_doc_path}}`의 단일 evidence 문서를 메모리에서 사용하고, 같은 ordinal의 B1 part(`{{item.b1_output_path}}`)만 authority metadata로 사용 + [ ] 4) `write_file` 전 수행. 불량이면 내부 보정 시도 + [ ] 5) 보정 후에도 불량이면 part 파일을 쓰지 않고 `FAILED: E-{ordinal:03d} event candidate part schema invariant violation` 출력 후 종료 + [ ] 6) 통과한 경우에만 `write_file` 사용해서 `evidence_event_candidate_parts/E-{ordinal:03d}.json` 생성. event 후보가 0건이어도 non-null `zero_event_reason` part 파일을 작성 + [ ] 7) `list_docs` 사용해서 자기 part 파일만 존재 확인(절대 읽기 금지) + [ ] 8) `"E-{ordinal:03d} event candidates part 작성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `{{item.b1_output_path}}` (같은 ordinal 의 B1 part) + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 1건; planner가 evidence_all.json에서 분리) + - `client_meeting.md` + + + - `evidence_event_candidate_parts/E-{ordinal:03d}.json` + + + + + - `candidate_id_proposed`는 정확히 `EVT-{ordinal:03d}-{seq:02d}` 형식만 허용한다. + - `ordinal`과 output path는 runtime parameter (`{{item.*}}`) 를 그대로 따른다. + - 증거 문서 수 N, assigned_ordinal, mapper fan-out, streaming edge는 본 task에서 만들지 않는다. + - 자기 part 파일은 절대 재읽지 않는다. + - 다른 ordinal 문서를 처리하지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"E-{ordinal:03d} event candidates part 작성 완료"` 한 줄만 사용한다. + + + + - task_name: Task_B1_gate_merge_deterministic + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import copy + import hashlib + import itertools + import json + import re + import time + from collections import defaultdict + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + PLAN_PATH = "evidence_shard_plan.json" + PART_DIR = "evidence_indexed_parts" + FINAL_PATH = "evidence_indexed.json" + MANIFEST_PATH = "finalized_evidence_id_manifest.json" + PRECHECK_PATH = "quality_gates/B1_gate_precheck.json" + ID_RE = re.compile(r"^E-(\d{3})$") + + def _mid(): return next(MSG_ID) + + def _init(): + r = CLIENT.post(LOCALDOCS_URL, json={ + "jsonrpc": "2.0", "id": 1, "method": "initialize", + "params": {"protocolVersion": "2025-03-26", "capabilities": {}, + "clientInfo": {"name": "task-b1-gate-merge-deterministic", "version": "1.0", + "user_id": "{{__user_hash__}}", "workspace_id": "{{__workspace_hash__}}"}} + }, headers=MCP_HEADERS) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post(LOCALDOCS_URL, json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS).raise_for_status() + + def _parse_response(text: str): + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: return json.loads(line[6:]) + except Exception: pass + try: return json.loads(text) + except Exception: return None + + def _call(name: str, args: dict[str, Any]): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc": "2.0", "id": _mid(), "method": "tools/call", + "params": {"name": name, "arguments": args}}, + headers=MCP_HEADERS) + r.raise_for_status() + payload = _parse_response(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: dict[str, Any], name: str): + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: raise RuntimeError(f"Empty MCP result: {name}") + try: outer = json.loads(text) + except Exception: return text + if isinstance(outer, dict) and "results" in outer: + row = (outer.get("results") or [{}])[0] + inner = row.get("content") or row.get("text") or "" + if not inner: raise RuntimeError(f"Empty document content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer + + def read_json(path: str): + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str): return raw + try: return json.loads(raw) + except json.JSONDecodeError: + obj, _ = json.JSONDecoder().raw_decode(raw.strip()) + return obj + + def write_doc(path: str, obj: Any): + text = json.dumps(obj, ensure_ascii=False, indent=2) + _call("write_file", {"path": path, "content": text, "overwrite": True}) + return text + + def _walk_strings(value: Any): + if isinstance(value, str): + yield value + elif isinstance(value, list): + for item in value: yield from _walk_strings(item) + elif isinstance(value, dict): + for item in value.values(): yield from _walk_strings(item) + + def list_part_names(folder: str): + payload = None + errors = [] + for args in ({"path": folder, "pattern": "E-*.json"}, {}): + try: + payload = _unwrap(_call("list_docs", args), "list_docs") + break + except Exception as exc: + errors.append(str(exc)) + if payload is None: + raise RuntimeError("list_docs failed: " + " | ".join(errors)) + if isinstance(payload, str): + try: payload = json.loads(payload) + except Exception: pass + names = [] + for value in _walk_strings(payload): + match = re.search(r"(?:^|/)(E-\d{3}\.json)$", value) + if match: + names.append(f"{folder}/{match.group(1)}") + return sorted(set(names)) + + def single_item(obj: Any, path: str): + if isinstance(obj, dict) and isinstance(obj.get("items"), list): + if len(obj["items"]) != 1 or not isinstance(obj["items"][0], dict): + raise ValueError(f"{path}: envelope must contain exactly one object") + return obj["items"][0] + if isinstance(obj, dict): + return obj + raise ValueError(f"{path}: part must be an object") + + def refs(evidence_indexes=None, candidate_ids=None, meeting_refs=None, titles=None): + return { + "evidence_indexes": sorted(set(str(x) for x in (evidence_indexes or []) if x)), + "event_candidate_ids": sorted(set(str(x) for x in (candidate_ids or []) if x)), + "meeting_clause_refs": sorted(set(str(x) for x in (meeting_refs or []) if x)), + "doc_titles": sorted(set(str(x) for x in (titles or []) if x)), + } + + def finding(code: str, detail: str, evidence_indexes=None, rerun=None): + row = {"issue_type": code.lower(), "source_check": code.lower(), "severity": "BLOCK", + "detail": detail, "source_refs": refs(evidence_indexes)} + if rerun: row["rerun_target_task"] = rerun + return row + + def suspect(kind: str, basis: str, evidence_indexes=None, titles=None, context=None): + return {"origin_gate": "B1", "exception_type": kind, "severity_candidate": "HARD_WARNING", + "source_refs": refs(evidence_indexes, titles=titles), "concise_basis": basis, + "compact_context": context or {}} + + def warning_records(node: Any, path: str = "$"): + out = [] + if isinstance(node, dict): + for key, value in node.items(): + child = f"{path}.{key}" + if re.search(r"(self_?)?(warning|error)", str(key), re.I) and value not in (None, False, "", [], {}): + out.append({"path": child, "value": value}) + out.extend(warning_records(value, child)) + elif isinstance(node, list): + for i, value in enumerate(node): out.extend(warning_records(value, f"{path}[{i}]")) + return out + + def main(): + started = time.monotonic() + _init() + plan = read_json(PLAN_PATH) + if not isinstance(plan, dict): raise RuntimeError("planner must be an object") + expected_ordinals = [str(x).zfill(3) for x in plan.get("expected_ordinals") or []] + expected_ids = [str(x) for x in plan.get("expected_evidence_ids") or + [f"E-{x}" for x in expected_ordinals]] + shard_count = int(plan.get("shard_count") or len(expected_ordinals)) + hard = [] + reviews = [] + parse_failures = [] + if plan.get("planner_status") != "READY" or plan.get("mapper_execution_allowed") is not True: + hard.append(finding("PLANNER_NOT_READY", "planner did not authorize mapper output consumption")) + actual_names = list_part_names(PART_DIR) + actual_ordinals = sorted({re.search(r"E-(\d{3})\.json$", x).group(1) for x in actual_names + if re.search(r"E-(\d{3})\.json$", x)}) + missing = sorted(set(expected_ordinals) - set(actual_ordinals)) + unexpected = sorted(set(actual_ordinals) - set(expected_ordinals)) + stale = sorted(set((plan.get("stale_part_policy") or {}).get("detected_stale_parts", {}) + .get("evidence_indexed_parts") or []) | + {f"{PART_DIR}/E-{x}.json" for x in unexpected}) + for ordinal in missing: + hard.append(finding("MISSING_B1_PART", f"missing B1 mapper part E-{ordinal}", + [f"E-{ordinal}"], f"Task_B1_map_doc_{ordinal}")) + for ordinal in unexpected: + hard.append(finding("UNEXPECTED_B1_PART", f"unexpected B1 mapper part E-{ordinal}", + [f"E-{ordinal}"])) + for path in stale: + hard.append(finding("STALE_B1_PART", f"stale B1 mapper part: {path}")) + + shards = {str((row.get("ordinal_label") or row.get("assigned_ordinal") or "")).zfill(3): row + for row in (plan.get("shards") or []) if isinstance(row, dict)} + items = [] + input_bytes = 0 + uid_rows = defaultdict(list) + title_rows = defaultdict(list) + for ordinal in expected_ordinals: + path = f"{PART_DIR}/E-{ordinal}.json" + if ordinal in missing: continue + try: + raw = read_json(path) + input_bytes += len(json.dumps(raw, ensure_ascii=False).encode("utf-8")) + item = single_item(raw, path) + proposed = str(item.get("evidence_index_proposed") or "") + expected = f"E-{ordinal}" + if proposed != expected or not ID_RE.fullmatch(proposed): + raise ValueError(f"evidence_index_proposed {proposed!r} != {expected}") + pointer = item.get("source_pointer") + if not isinstance(pointer, dict) or int(pointer.get("ordinal") or -1) != int(ordinal): + raise ValueError("source_pointer.ordinal mismatch") + meta = item.get("mapper_meta") + if not isinstance(meta, dict) or meta.get("self_isolation_attested") is not True: + raise ValueError("mapper self-isolation attestation missing or false") + final_item = copy.deepcopy(item) + final_item["evidence_index"] = final_item.pop("evidence_index_proposed") + final_item.pop("mapper_meta", None) + items.append(final_item) + shard = shards.get(ordinal) or {} + source_ref = shard.get("source_document_ref") or {} + uid = str(item.get("doc_uid") or source_ref.get("doc_uid_candidate") or "") + content_hash = str(source_ref.get("content_sha256") or "") + title_norm = str(item.get("title_normalized") or "").strip().lower() + row = {"ordinal": ordinal, "evidence_index": expected, "title": item.get("title"), + "title_normalized": title_norm, "content_sha256": content_hash} + if uid: uid_rows[uid].append(row) + if title_norm: title_rows[title_norm].append({**row, "doc_uid": uid}) + for flag in warning_records(item): + reviews.append(suspect("mapper_self_warning", "B1 mapper emitted an explicit warning or error", + [expected], [item.get("title")], + {"warning": flag, "doc_type": item.get("doc_type")})) + semantic_flags = set(str(x) for x in (item.get("doc_semantic_flags") or [])) + title_text = str(item.get("title") or "") + coverage_gaps = [] + if "registry_doc" in semantic_flags and not (item.get("registry_row_components") or []): + coverage_gaps.append("registry_row_components") + if ("schedule_doc" in semantic_flags and re.search(r"별지|목록", title_text) + and not (item.get("schedule_items") or [])): + coverage_gaps.append("schedule_items") + if ("notice_doc" in semantic_flags and re.search(r"통지|내용증명|최고", title_text) + and not (item.get("notice_components") or [])): + coverage_gaps.append("notice_components") + if ("claim_doc" in semantic_flags and re.search(r"차용|금전소비대차|이행각서|각서", title_text) + and not (item.get("claim_document_components") or []) + and not (item.get("loan_components") or [])): + coverage_gaps.append("claim_or_loan_components") + if coverage_gaps: + reviews.append(suspect( + "component_coverage_concern", + "document type and title indicate an empty required component container", + [expected], [item.get("title")], + {"missing_components": coverage_gaps, "doc_type": item.get("doc_type"), + "doc_semantic_flags": sorted(semantic_flags)}, + )) + except Exception as exc: + parse_failures.append({"ordinal": ordinal, "part_path": path, "reason": str(exc), + "rerun_target_task": f"Task_B1_map_doc_{ordinal}"}) + hard.append(finding("B1_PART_SCHEMA_OR_PARSE_FAILURE", str(exc), [f"E-{ordinal}"], + f"Task_B1_map_doc_{ordinal}")) + + for uid, rows in sorted(uid_rows.items()): + if len(rows) < 2: continue + hashes = {row["content_sha256"] for row in rows if row["content_sha256"]} + ids = [row["evidence_index"] for row in rows] + if len(hashes) > 1: + hard.append(finding("EVIDENCE_IDENTITY_COLLISION", + f"doc_uid {uid} maps to different source hashes", ids)) + else: + reviews.append(suspect("suspected_duplicate_alias", + f"doc_uid {uid} appears in multiple evidence ordinals", ids, + [row["title"] for row in rows], {"doc_uid": uid})) + # A generic normalized title such as "내용증명" or "차용증" is not an alias signal by itself. + # Alias review is emitted only by the doc_uid/source-hash rule above. + + items.sort(key=lambda row: int(str(row.get("evidence_index")).split("-")[-1])) + final_ids = [str(row.get("evidence_index")) for row in items] + if set(final_ids) != set(expected_ids) or len(items) != shard_count: + hard.append(finding("EVIDENCE_CONSERVATION_FAILURE", + f"expected {shard_count} items/{len(expected_ids)} IDs, got {len(items)} items/{len(final_ids)} IDs", + sorted(set(expected_ids) ^ set(final_ids)))) + if len(final_ids) != len(set(final_ids)): + hard.append(finding("DUPLICATE_FINAL_EVIDENCE_ID", "final evidence IDs are not unique", final_ids)) + + final_obj = {"schema_contract_version": "evidence_indexed.v3", "items": items} + final_text = json.dumps(final_obj, ensure_ascii=False, indent=2) + parsed_final = json.loads(final_text) + if [x.get("evidence_index") for x in parsed_final["items"]] != final_ids: + raise RuntimeError("post-serialization B1 ID verification failed") + set_material = json.dumps(sorted(final_ids), ensure_ascii=False, separators=(",", ":")) + blocked = bool(hard) + manifest = { + "schema_version": "finalized_evidence_id_manifest.v1", + "status": "BLOCKED" if blocked else "READY", + "source_artifact": FINAL_PATH, + "shard_count": shard_count, + "expected_evidence_ids": expected_ids, + "finalized_evidence_ids": final_ids, + "missing_evidence_ids": sorted(set(expected_ids) - set(final_ids)), + "unexpected_evidence_ids": sorted(set(final_ids) - set(expected_ids)), + "evidence_id_set_sha256": hashlib.sha256(set_material.encode("utf-8")).hexdigest(), + } + checks = [ + {"check_id": "shard_completeness", "severity": "BLOCK" if missing or unexpected or stale else "PASS", + "detail": f"missing={len(missing)}, unexpected={len(unexpected)}, stale={len(stale)}"}, + {"check_id": "evidence_index_determinism", "severity": "BLOCK" if parse_failures else "PASS", + "detail": f"parse_or_schema_failures={len(parse_failures)}"}, + {"check_id": "duplicate_document_alias", "severity": "HARD_WARNING" if any(x["exception_type"] == "suspected_duplicate_alias" for x in reviews) else "PASS", + "detail": "duplicate aliases are preserved and routed for review"}, + {"check_id": "document_component_coverage", "severity": "HARD_WARNING" if any(x["exception_type"] in ("mapper_self_warning", "component_coverage_concern") for x in reviews) else "PASS", + "detail": "named component containers and explicit mapper warnings checked without raw-evidence re-read"}, + {"check_id": "authority_drift_detection", "severity": "BLOCK" if any(x["issue_type"] in ("b1_part_schema_or_parse_failure", "evidence_identity_collision") for x in hard) else "PASS", + "detail": "ordinal, source pointer and self-isolation attestation checked"}, + ] + status = "BLOCKED" if blocked else ("READY_WITH_SUSPECTS" if reviews else "READY") + precheck = { + "schema_version": "B1_gate_precheck.v1", "status": status, + "expected_ordinals": expected_ordinals, "received_ordinals": actual_ordinals, + "missing_ordinals": missing, "unexpected_ordinals": unexpected, "stale_parts": stale, + "parse_failures": parse_failures, "hard_gate_findings": hard, + "review_candidates": reviews, "checks": checks, + "metrics": {"input_parts_count": len(actual_ordinals), "final_items_count": len(items), + "input_bytes": input_bytes, "output_bytes": len(final_text.encode("utf-8")), + "elapsed_ms": int((time.monotonic() - started) * 1000)} + } + write_doc(FINAL_PATH, final_obj) + write_doc(MANIFEST_PATH, manifest) + write_doc(PRECHECK_PATH, precheck) + print(json.dumps({"status": status, "final_items": len(items), "review_candidates": len(reviews), + "hard_findings": len(hard)}, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B2_gate_merge_deterministic + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import copy + import hashlib + import itertools + import json + import re + import time + from collections import defaultdict + from datetime import date + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + PLAN_PATH = "evidence_shard_plan.json" + PART_DIR = "evidence_event_candidate_parts" + MANIFEST_PATH = "finalized_evidence_id_manifest.json" + B1_PRECHECK_PATH = "quality_gates/B1_gate_precheck.json" + FINAL_PATH = "evidence_event_candidates.json" + PRECHECK_PATH = "quality_gates/B2_gate_precheck.json" + EXCEPTION_PATH = "quality_gates/B12_exception_pack.json" + ZERO_REASONS = {"NO_LEGAL_EVENT_FOUND", "REFERENCE_ONLY_DOC", "PARSE_INSUFFICIENT", "ORDINAL_MATCH_FAILED"} + CANDIDATE_RE = re.compile(r"^EVT-(\d{3})-(\d{2})$") + CHAIN_ORDER = { + "underlying_claim_arising": ("claim", 0), "first_demand": ("claim", 1), + "demand_notice_dispatch": ("claim", 2), "demand_notice_arrival": ("claim", 3), + "debt_acknowledgement": ("claim", 4), + "lien_extinction_notice_dispatch": ("lien", 0), "lien_extinction_notice_receipt": ("lien", 1), + "lien_extinction_effect_candidate": ("lien", 2), + "secured_debt_creation": ("auction", 0), "mortgage_setting_by_fraction": ("auction", 1), + "auction_application_after_extinguishment": ("auction", 2), + "auction_sale_and_registry_transfer": ("auction", 3), + "preserved_claim_arising": ("actio", 0), "fraudulent_transfer": ("actio", 1), + "encumbrance_release_after_act": ("actio", 2), + } + + def _mid(): return next(MSG_ID) + + def _init(): + r = CLIENT.post(LOCALDOCS_URL, json={ + "jsonrpc": "2.0", "id": 1, "method": "initialize", + "params": {"protocolVersion": "2025-03-26", "capabilities": {}, + "clientInfo": {"name": "task-b2-gate-merge-deterministic", "version": "1.0", + "user_id": "{{__user_hash__}}", "workspace_id": "{{__workspace_hash__}}"}} + }, headers=MCP_HEADERS) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post(LOCALDOCS_URL, json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS).raise_for_status() + + def _parse_response(text: str): + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: return json.loads(line[6:]) + except Exception: pass + try: return json.loads(text) + except Exception: return None + + def _call(name: str, args: dict[str, Any]): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc": "2.0", "id": _mid(), "method": "tools/call", + "params": {"name": name, "arguments": args}}, + headers=MCP_HEADERS) + r.raise_for_status() + payload = _parse_response(r.text) + if not payload or "result" not in payload: raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: dict[str, Any], name: str): + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: raise RuntimeError(f"Empty MCP result: {name}") + try: outer = json.loads(text) + except Exception: return text + if isinstance(outer, dict) and "results" in outer: + row = (outer.get("results") or [{}])[0] + inner = row.get("content") or row.get("text") or "" + if not inner: raise RuntimeError(f"Empty document content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer + + def read_json(path: str): + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str): return raw + try: return json.loads(raw) + except json.JSONDecodeError: + obj, _ = json.JSONDecoder().raw_decode(raw.strip()) + return obj + + def write_doc(path: str, obj: Any): + text = json.dumps(obj, ensure_ascii=False, indent=2) + _call("write_file", {"path": path, "content": text, "overwrite": True}) + return text + + def _walk_strings(value: Any): + if isinstance(value, str): yield value + elif isinstance(value, list): + for item in value: yield from _walk_strings(item) + elif isinstance(value, dict): + for item in value.values(): yield from _walk_strings(item) + + def list_part_names(folder: str): + payload = None + errors = [] + for args in ({"path": folder, "pattern": "E-*.json"}, {}): + try: + payload = _unwrap(_call("list_docs", args), "list_docs") + break + except Exception as exc: errors.append(str(exc)) + if payload is None: raise RuntimeError("list_docs failed: " + " | ".join(errors)) + if isinstance(payload, str): + try: payload = json.loads(payload) + except Exception: pass + names = [] + for value in _walk_strings(payload): + match = re.search(r"(?:^|/)(E-\d{3}\.json)$", value) + if match: names.append(f"{folder}/{match.group(1)}") + return sorted(set(names)) + + def single_item(obj: Any, path: str): + if isinstance(obj, dict) and isinstance(obj.get("items"), list): + if len(obj["items"]) != 1 or not isinstance(obj["items"][0], dict): + raise ValueError(f"{path}: envelope must contain exactly one object") + return obj["items"][0] + if isinstance(obj, dict): return obj + raise ValueError(f"{path}: part must be an object") + + def refs(evidence_indexes=None, candidate_ids=None, meeting_refs=None, titles=None): + return { + "evidence_indexes": sorted(set(str(x) for x in (evidence_indexes or []) if x)), + "event_candidate_ids": sorted(set(str(x) for x in (candidate_ids or []) if x)), + "meeting_clause_refs": sorted(set(str(x) for x in (meeting_refs or []) if x)), + "doc_titles": sorted(set(str(x) for x in (titles or []) if x)), + } + + def hard_finding(code: str, detail: str, evidence_indexes=None, candidate_ids=None, rerun=None): + row = {"issue_type": code.lower(), "source_check": code.lower(), "severity": "BLOCK", + "detail": detail, "source_refs": refs(evidence_indexes, candidate_ids)} + if rerun: row["rerun_target_task"] = rerun + return row + + def review_candidate(kind: str, basis: str, evidence_indexes=None, candidate_ids=None, + titles=None, context=None): + return {"origin_gate": "B2", "exception_type": kind, "severity_candidate": "HARD_WARNING", + "source_refs": refs(evidence_indexes, candidate_ids, titles=titles), + "concise_basis": basis, "compact_context": context or {}} + + def warning_records(node: Any, path: str = "$"): + out = [] + if isinstance(node, dict): + for key, value in node.items(): + child = f"{path}.{key}" + if re.search(r"(self_?)?(warning|error)", str(key), re.I) and value not in (None, False, "", [], {}): + out.append({"path": child, "value": value}) + out.extend(warning_records(value, child)) + elif isinstance(node, list): + for i, value in enumerate(node): out.extend(warning_records(value, f"{path}[{i}]")) + return out + + def parse_date(value: Any): + if not value: return None + text = str(value) + m = re.search(r"(20\d{2}|19\d{2})[-./년]\s*(\d{1,2})[-./월]\s*(\d{1,2})", text) + if not m: return None + try: return date(int(m.group(1)), int(m.group(2)), int(m.group(3))) + except ValueError: return None + + def relation_key(candidate: dict[str, Any]): + legal = candidate.get("legal_effect_candidate") + if not isinstance(legal, dict): legal = {} + explicit = legal.get("related_claim_type") or candidate.get("claim_id") or candidate.get("lien_id") + if not explicit: return None + return json.dumps({"explicit": explicit, "object": candidate.get("object_spec"), + "fraction": candidate.get("fraction_ref")}, ensure_ascii=False, + sort_keys=True, separators=(",", ":")) + + def candidate_rank(row: dict[str, Any]): + candidate = row["candidate"] + subkind = str(candidate.get("event_subkind") or "") + action = str(candidate.get("action_type_candidate") or "").lower() + derived = subkind.endswith("_candidate") or "derived" in action + confidence = {"high": 0, "medium": 1, "low": 2, "unknown": 3}.get(str(candidate.get("confidence")), 4) + supports = candidate.get("support_locators") + support_count = len(supports) if isinstance(supports, list) else 0 + return (1 if derived else 0, confidence, -support_count, row["ordinal"], row["candidate_id"]) + + def build_packs(reviews: list[dict[str, Any]], blocked: bool): + def clipped_context(value: Any, remaining: list[int]): + if isinstance(value, str): + take = max(0, min(len(value), remaining[0])) + remaining[0] -= take + return value[:take] + if isinstance(value, list): + return [clipped_context(x, remaining) for x in value] + if isinstance(value, dict): + return {str(k): clipped_context(v, remaining) for k, v in value.items()} + return value + + prepared = [] + for raw in reviews: + item = copy.deepcopy(raw) + item["compact_context"] = clipped_context(item.get("compact_context") or {}, [1500]) + prepared.append(item) + ordered = sorted(prepared, key=lambda x: (x.get("origin_gate", ""), x.get("exception_type", ""), + json.dumps(x.get("source_refs") or {}, ensure_ascii=False, sort_keys=True), + x.get("concise_basis", ""))) + for i, item in enumerate(ordered, 1): item["exception_id"] = f"B12-EXC-{i:04d}" + if blocked: + return [], [{**item, "reason": "hard_gate_blocked_before_adjudication"} for item in ordered], False + groups = defaultdict(list) + for item in ordered: groups[item["exception_type"]].append(item) + packs = [] + overflow = [] + for exception_type in sorted(groups): + current = [] + for item in groups[exception_type]: + item_refs = item.get("source_refs") or {} + item_evidence = set(item_refs.get("evidence_indexes") or []) + item_candidates = set(item_refs.get("event_candidate_ids") or []) + item_chars = len(json.dumps([item], ensure_ascii=False)) + if len(item_evidence) > 2 or len(item_candidates) > 8 or item_chars > 6000: + overflow.append({**item, "reason": "BUDGET_OVERFLOW_BLOCK_REVIEW"}) + continue + trial = current + [item] + ev = {x for row in trial for x in (row.get("source_refs") or {}).get("evidence_indexes", [])} + cand = {x for row in trial for x in (row.get("source_refs") or {}).get("event_candidate_ids", [])} + chars = len(json.dumps(trial, ensure_ascii=False)) + if current and (len(ev) > 2 or len(cand) > 8 or chars > 6000): + packs.append({"exception_type": exception_type, "items": current}) + current = [item] + else: + current = trial + if current: packs.append({"exception_type": exception_type, "items": current}) + if len(packs) > 8: + for pack in packs[8:]: + overflow.extend({**item, "reason": "BUDGET_OVERFLOW_BLOCK_REVIEW"} for item in pack["items"]) + packs = packs[:8] + descriptors = [] + for i, pack in enumerate(packs, 1): + pack_id = f"PACK-{i:03d}" + descriptors.append({ + "pack_id": pack_id, + "output_path": f"quality_gates/B12_adjudication_parts/{pack_id}.json", + "exception_type": pack["exception_type"], + "exception_ids": [x["exception_id"] for x in pack["items"]], + "items": pack["items"], + }) + return descriptors, overflow, bool(overflow) + + def main(): + started = time.monotonic() + _init() + plan = read_json(PLAN_PATH) + manifest = read_json(MANIFEST_PATH) + b1_precheck = read_json(B1_PRECHECK_PATH) + expected_ordinals = [str(x).zfill(3) for x in plan.get("expected_ordinals") or []] + expected_ids = [str(x) for x in manifest.get("finalized_evidence_ids") or []] + shard_count = int(plan.get("shard_count") or len(expected_ordinals)) + hard = [] + reviews = list(b1_precheck.get("review_candidates") or []) + b2_reviews = [] + schema_findings = [] + dedup_findings = [] + field_failures = [] + if manifest.get("status") != "READY" or b1_precheck.get("status") == "BLOCKED": + hard.append(hard_finding("UPSTREAM_B1_BLOCKED", "B1 deterministic gate is blocked")) + actual_names = list_part_names(PART_DIR) + actual_ordinals = sorted({re.search(r"E-(\d{3})\.json$", x).group(1) for x in actual_names + if re.search(r"E-(\d{3})\.json$", x)}) + missing = sorted(set(expected_ordinals) - set(actual_ordinals)) + unexpected = sorted(set(actual_ordinals) - set(expected_ordinals)) + stale = sorted(set((plan.get("stale_part_policy") or {}).get("detected_stale_parts", {}) + .get("evidence_event_candidate_parts") or []) | + {f"{PART_DIR}/E-{x}.json" for x in unexpected}) + for ordinal in missing: + hard.append(hard_finding("MISSING_B2_PART", f"missing B2 mapper part E-{ordinal}", + [f"E-{ordinal}"], rerun=f"Task_B2_map_events_{ordinal}")) + for ordinal in unexpected: + hard.append(hard_finding("UNEXPECTED_B2_PART", f"unexpected B2 mapper part E-{ordinal}", + [f"E-{ordinal}"])) + for path in stale: hard.append(hard_finding("STALE_B2_PART", f"stale B2 mapper part: {path}")) + + final_items = [] + candidate_rows = [] + all_candidate_ids = [] + input_candidate_count = 0 + input_bytes = 0 + for ordinal in expected_ordinals: + path = f"{PART_DIR}/E-{ordinal}.json" + if ordinal in missing: continue + evidence_id = f"E-{ordinal}" + try: + raw = read_json(path) + input_bytes += len(json.dumps(raw, ensure_ascii=False).encode("utf-8")) + item = single_item(raw, path) + if str(item.get("evidence_index_proposed") or "") != evidence_id: + raise ValueError("evidence_index_proposed mismatch") + candidates = item.get("event_candidates") + zero_reason = item.get("zero_event_reason") + if not isinstance(candidates, list): raise ValueError("event_candidates must be an array") + if not candidates and zero_reason not in ZERO_REASONS: + raise ValueError("empty event_candidates requires an allowed non-null zero_event_reason") + final_item = copy.deepcopy(item) + final_item["evidence_index"] = final_item.pop("evidence_index_proposed") + final_item.pop("mapper_meta", None) + final_candidates = [] + input_candidate_count += len(candidates) + for seq, candidate in enumerate(candidates, 1): + if not isinstance(candidate, dict): raise ValueError(f"candidate {seq} is not an object") + proposed = str(candidate.get("candidate_id_proposed") or "") + source = str(candidate.get("source_evidence_index_proposed") or "") + match = CANDIDATE_RE.fullmatch(proposed) + if not match or match.group(1) != ordinal: + raise ValueError(f"invalid candidate_id_proposed: {proposed}") + if source != evidence_id or source not in expected_ids: + raise ValueError(f"invalid source evidence reference: {source}") + if not candidate.get("identity_signature"): + raise ValueError(f"{proposed}: identity_signature missing") + projected = copy.deepcopy(candidate) + projected["candidate_id"] = projected.pop("candidate_id_proposed") + projected["source_evidence_index"] = projected.pop("source_evidence_index_proposed") + expected_keys = (set(candidate) - {"candidate_id_proposed", "source_evidence_index_proposed"} | + {"candidate_id", "source_evidence_index"}) + if set(projected) != expected_keys: + field_failures.append({"candidate_id": proposed, "reason": "key set mismatch"}) + restored = copy.deepcopy(projected) + restored["candidate_id_proposed"] = restored.pop("candidate_id") + restored["source_evidence_index_proposed"] = restored.pop("source_evidence_index") + if restored != candidate: + field_failures.append({"candidate_id": proposed, "reason": "deep value mismatch"}) + final_candidates.append(projected) + all_candidate_ids.append(proposed) + candidate_rows.append({"ordinal": int(ordinal), "evidence_index": evidence_id, + "candidate_id": proposed, "candidate": projected, + "title": item.get("title")}) + for flag in warning_records(candidate): + b2_reviews.append(review_candidate("mapper_self_warning", + "B2 mapper emitted an explicit warning or error", + [evidence_id], [proposed], [item.get("title")], + {"warning": flag})) + final_item["event_candidates"] = final_candidates + final_items.append(final_item) + except Exception as exc: + row = {"ordinal": ordinal, "part_path": path, "reason": str(exc), "severity": "BLOCK", + "rerun_target_task": f"Task_B2_map_events_{ordinal}"} + schema_findings.append(row) + hard.append(hard_finding("B2_PART_SCHEMA_OR_PARSE_FAILURE", str(exc), [evidence_id], + rerun=f"Task_B2_map_events_{ordinal}")) + + if len(all_candidate_ids) != len(set(all_candidate_ids)): + hard.append(hard_finding("DUPLICATE_FINAL_CANDIDATE_ID", "candidate IDs are not globally unique", + candidate_ids=all_candidate_ids)) + if field_failures: + hard.append(hard_finding("CANDIDATE_FIELD_CONSERVATION_FAILURE", + f"{len(field_failures)} candidate projection failures", + candidate_ids=[x["candidate_id"] for x in field_failures])) + + by_signature = defaultdict(list) + for row in candidate_rows: by_signature[str(row["candidate"].get("identity_signature"))].append(row) + for signature, rows in sorted(by_signature.items()): + if not signature or len(rows) < 2: continue + canonical = sorted(rows, key=candidate_rank)[0] + ids = [row["candidate_id"] for row in rows] + evs = [row["evidence_index"] for row in rows] + dedup_findings.append({"identity_signature": signature, "candidate_ids": ids, + "canonical_candidate_id": canonical["candidate_id"], + "preservation_policy": "all_candidates_preserved"}) + b2_reviews.append(review_candidate("suspected_duplicate_alias", + "same identity signature appears in multiple evidence candidates", + evs, ids, [row["title"] for row in rows], + {"canonical_candidate_id": canonical["candidate_id"]})) + + chronology_groups = defaultdict(list) + for row in candidate_rows: + subkind = str(row["candidate"].get("event_subkind") or "") + if subkind not in CHAIN_ORDER: continue + key = relation_key(row["candidate"]) + event_date = parse_date(row["candidate"].get("event_date")) + if key and event_date: + chain, position = CHAIN_ORDER[subkind] + chronology_groups[(chain, key)].append((position, event_date, row)) + for (chain, _), rows in chronology_groups.items(): + rows.sort(key=lambda x: x[0]) + for left, right in zip(rows, rows[1:]): + if left[1] > right[1]: + involved = [left[2], right[2]] + b2_reviews.append(review_candidate("chronology_conflict", + f"deterministic date inversion in {chain} chain", + [x["evidence_index"] for x in involved], + [x["candidate_id"] for x in involved], + [x["title"] for x in involved], + {"left_date": str(left[1]), "right_date": str(right[1])})) + + # Defense assertions are explicit routing facts, not ambiguity exceptions. + # Relay them as one deterministic Stage 2 review item without an LLM call. + defense_rows = [] + for row in candidate_rows: + candidate = row["candidate"] + defense = candidate.get("defense_candidate") + legal = candidate.get("legal_effect_candidate") + subkind = str(candidate.get("event_subkind") or "") + needs_rebuttal = isinstance(defense, dict) and defense.get("requires_rebuttal_in_stage2") is True + explicit_defense = subkind.startswith("defense_assertion") + legal_defense = isinstance(legal, dict) and legal.get("effect_type") == "defense_assertion" + if needs_rebuttal or explicit_defense or legal_defense: + defense_rows.append(row) + auto_review_findings = [] + if defense_rows: + auto_review_findings.append({ + "origin_gate": "B2", + "exception_type": "defense_and_obstacle_review", + "severity": "SOFT_WARNING", + "source_check": "deterministic_defense_assertion_relay", + "source_refs": refs( + [row["evidence_index"] for row in defense_rows], + [row["candidate_id"] for row in defense_rows], + titles=[row["title"] for row in defense_rows], + ), + "candidate_use_policy": "preserve_as_needs_review", + "review_instruction": "Stage 2에서 상계, 책임부인 및 기타 방어 주장의 성립과 반박 필요성을 검토한다.", + "concise_basis": "구조화된 defense assertion 후보를 후속 방어·장애사유 검토로 전달", + }) + + final_items.sort(key=lambda row: int(str(row.get("evidence_index")).split("-")[-1])) + final_candidate_count = sum(len(row.get("event_candidates") or []) for row in final_items) + final_ids = [str(row.get("evidence_index")) for row in final_items] + if set(final_ids) != set(expected_ids) or len(final_items) != shard_count: + hard.append(hard_finding("EVENT_ITEM_CONSERVATION_FAILURE", + f"expected {shard_count} items, got {len(final_items)}", + sorted(set(expected_ids) ^ set(final_ids)))) + if input_candidate_count != final_candidate_count: + hard.append(hard_finding("CANDIDATE_CONSERVATION_FAILURE", + f"input={input_candidate_count}, final={final_candidate_count}")) + + final_obj = {"schema_version": "evidence_event_candidates.v1", "items": final_items} + final_text = json.dumps(final_obj, ensure_ascii=False, indent=2) + reparsed = json.loads(final_text) + if sum(len(x.get("event_candidates") or []) for x in reparsed["items"]) != input_candidate_count: + raise RuntimeError("post-serialization candidate conservation failed") + reviews.extend(b2_reviews) + blocked = bool(hard) + packs, auto_blocked, overflow = build_packs(reviews, blocked) + exception_status = "BLOCKED" if blocked else ("READY" if packs else "READY_NO_EXCEPTIONS") + exception_obj = { + "schema_version": "stage1_part1_gate_exception_pack.v1", + "status": exception_status, "exception_count": len(reviews), "pack_count": len(packs), + "budget_gate": {"max_evidence_items_per_pack": 2, "max_event_candidates_per_pack": 8, + "max_excerpt_chars_per_pack": 1500, "max_serialized_chars_per_pack": 6000, + "max_pack_count": 8, "overflow_detected": overflow}, + "packs": packs, "auto_blocked_review_items": auto_blocked, + } + checks = [ + {"check_id": "event_candidate_completeness", "severity": "BLOCK" if missing or unexpected or stale else "PASS", + "detail": f"missing={len(missing)}, unexpected={len(unexpected)}, stale={len(stale)}"}, + {"check_id": "evidence_reference_integrity", "severity": "BLOCK" if any(x["issue_type"] == "b2_part_schema_or_parse_failure" for x in hard) else "PASS", + "detail": "all candidate evidence references checked against compact manifest"}, + {"check_id": "candidate_id_collision", "severity": "BLOCK" if len(all_candidate_ids) != len(set(all_candidate_ids)) else ("HARD_WARNING" if dedup_findings else "PASS"), + "detail": f"duplicate_signature_groups={len(dedup_findings)}; candidates are not deleted"}, + {"check_id": "chronological_ordering", "severity": "HARD_WARNING" if any(x["exception_type"] == "chronology_conflict" for x in b2_reviews) else "PASS", + "detail": "only exact dates with an explicit relation key are compared"}, + {"check_id": "legal_effect_event_coverage", "severity": "HARD_WARNING" if any(x["exception_type"] == "mapper_self_warning" for x in b2_reviews) else "PASS", + "detail": "explicit mapper warnings are escalated without re-reading raw evidence"}, + ] + status = "BLOCKED" if blocked else ( + "READY_WITH_SUSPECTS" if reviews or auto_review_findings else "READY") + precheck = { + "schema_version": "B2_gate_precheck.v1", "status": status, + "upstream_b1_status": b1_precheck.get("status"), + "expected_ordinals": expected_ordinals, "received_ordinals": actual_ordinals, + "missing_ordinals": missing, "unexpected_ordinals": unexpected, "stale_parts": stale, + "schema_invariant_findings": schema_findings, "hard_gate_findings": hard, + "review_candidates": b2_reviews, "auto_review_findings": auto_review_findings, + "dedup_findings": dedup_findings, + "conservation": {"expected_items": shard_count, "final_items": len(final_items), + "input_candidates": input_candidate_count, "final_candidates": final_candidate_count, + "candidate_field_failures": field_failures}, + "checks": checks, + "metrics": {"input_bytes": input_bytes, "output_bytes": len(final_text.encode("utf-8")), + "elapsed_ms": int((time.monotonic() - started) * 1000)} + } + write_doc(FINAL_PATH, final_obj) + write_doc(PRECHECK_PATH, precheck) + write_doc(EXCEPTION_PATH, exception_obj) + print(json.dumps({"dynamic_fanout": packs, "exception_status": exception_status, + "exception_pack_count": len(packs)}, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B1B2_gate_llm_adjudicator_* + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: medium + llm_verbosity: low + max_iterations: 2 + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Execute only this compact Stage 1 exception-adjudication sub-task. + - The assigned pack, output schema, and writer boundary override broader instructions. + - Use only the assigned pack embedded below. + - Do not read or reconstruct any full planner, evidence, event, meeting, BO, or downstream file. + - Do not create facts, dates, amounts, parties, IDs, or source references. + - Write only the assigned adjudication output path. + - Do not modify any final evidence/event artifact, mapper part, precheck, or audit. + - Return only short basis fields; keep chain-of-thought private. + - Do not draft complaint prose or decide final merits. + + + + { + "pack_id": "{{item.pack_id}}", + "output_path": "{{item.output_path}}", + "exception_type": "{{item.exception_type}}", + "exception_ids": {{item.exception_ids}}, + "items": {{item.items}} + } + + + + You are a compact exception-only adjudicator assisting a top-tier Korean civil-litigation lawyer. + Validate pack_id, exception_type, exception_ids, items, and output_path. + Produce exactly one decision for every assigned exception_id and no others. + Allowed decisions are PASS_NO_ACTION, REVIEW_SOFT, REVIEW_HARD, BLOCK_SOURCE_INTEGRITY, BLOCK_REVIEW. + Use BLOCK_SOURCE_INTEGRITY only when source contamination or identity failure is explicit in the pack. + Use BLOCK_REVIEW when the compact record is insufficient. Legal ambiguity is not a Stage 1 integrity BLOCK. + Source refs in each decision must be a subset of that exception item's supplied source_refs. + Severity mapping is exact: + PASS_NO_ACTION -> PASS + REVIEW_SOFT -> SOFT_WARNING + REVIEW_HARD -> HARD_WARNING + BLOCK_REVIEW -> HARD_WARNING + BLOCK_SOURCE_INTEGRITY -> BLOCK + + + + { + "schema_version": "stage1_part1_gate_adjudication_part.v1", + "pack_id": "{{item.pack_id}}", + "status": "READY|READY_WITH_BLOCK_REVIEW|FAILED", + "decisions": [ + { + "exception_id": "B12-EXC-0001", + "exception_type": "assigned exception type", + "decision": "PASS_NO_ACTION|REVIEW_SOFT|REVIEW_HARD|BLOCK_SOURCE_INTEGRITY|BLOCK_REVIEW", + "severity": "PASS|SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "assigned exception type", + "source_refs": { + "evidence_indexes": [], + "event_candidate_ids": [], + "meeting_clause_refs": [], + "doc_titles": [] + }, + "candidate_use_policy": "prioritize_if_supported|preserve_as_needs_review|do_not_generate_without_support", + "review_instruction": "120 characters or fewer", + "concise_basis": "240 characters or fewer" + } + ] + } + + + + 1. Build the exact JSON object in memory. + 2. Call localdocs write_file with path={{item.output_path}}, overwrite=true, and the raw JSON object serialized as JSON. + 3. Do not write any other file. + 4. End with exactly: {{item.pack_id}} adjudication written + + + - task_name: Task_B12_gate_audit_finalizer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import itertools + import json + from datetime import datetime, timezone + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + B1_PRECHECK = "quality_gates/B1_gate_precheck.json" + B2_PRECHECK = "quality_gates/B2_gate_precheck.json" + EXCEPTION_PACK = "quality_gates/B12_exception_pack.json" + B1_AUDIT = "quality_gates/B1_evidence_indexed_gate.json" + B2_AUDIT = "quality_gates/B2_event_candidates_gate.json" + REF_KEYS = ("evidence_indexes", "event_candidate_ids", "meeting_clause_refs", "doc_titles") + ALLOWED_DECISIONS = {"PASS_NO_ACTION", "REVIEW_SOFT", "REVIEW_HARD", + "BLOCK_SOURCE_INTEGRITY", "BLOCK_REVIEW"} + DECISION_SEVERITY = {"PASS_NO_ACTION": "PASS", "REVIEW_SOFT": "SOFT_WARNING", + "REVIEW_HARD": "HARD_WARNING", "BLOCK_REVIEW": "HARD_WARNING", + "BLOCK_SOURCE_INTEGRITY": "BLOCK"} + + def _mid(): return next(MSG_ID) + + def _init(): + r = CLIENT.post(LOCALDOCS_URL, json={ + "jsonrpc": "2.0", "id": 1, "method": "initialize", + "params": {"protocolVersion": "2025-03-26", "capabilities": {}, + "clientInfo": {"name": "task-b12-gate-audit-finalizer", "version": "1.0", + "user_id": "{{__user_hash__}}", "workspace_id": "{{__workspace_hash__}}"}} + }, headers=MCP_HEADERS) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post(LOCALDOCS_URL, json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS).raise_for_status() + + def _parse_response(text: str): + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: return json.loads(line[6:]) + except Exception: pass + try: return json.loads(text) + except Exception: return None + + def _call(name: str, args: dict[str, Any]): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc": "2.0", "id": _mid(), "method": "tools/call", + "params": {"name": name, "arguments": args}}, + headers=MCP_HEADERS) + r.raise_for_status() + payload = _parse_response(r.text) + if not payload or "result" not in payload: raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: dict[str, Any], name: str): + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: raise RuntimeError(f"Empty MCP result: {name}") + try: outer = json.loads(text) + except Exception: return text + if isinstance(outer, dict) and "results" in outer: + row = (outer.get("results") or [{}])[0] + inner = row.get("content") or row.get("text") or "" + if not inner: raise RuntimeError(f"Empty document content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer + + def read_json(path: str): + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str): return raw + try: return json.loads(raw) + except json.JSONDecodeError: + obj, _ = json.JSONDecoder().raw_decode(raw.strip()) + return obj + + def write_doc(path: str, obj: Any): + text = json.dumps(obj, ensure_ascii=False, indent=2) + json.loads(text) + _call("write_file", {"path": path, "content": text, "overwrite": True}) + + def norm_refs(value: Any): + obj = value if isinstance(value, dict) else {} + return {key: sorted(set(str(x) for x in (obj.get(key) or []) if x)) for key in REF_KEYS} + + def source_subset(candidate: dict[str, list[str]], allowed: dict[str, list[str]]): + return all(set(candidate[key]).issubset(set(allowed[key])) for key in REF_KEYS) + + def normalized_hard(raw: Any, origin: str, seq: int): + row = raw if isinstance(raw, dict) else {"detail": str(raw)} + return { + "gate_finding_id": str(row.get("gate_finding_id") or f"{origin}-HARD-{seq:03d}"), + "source_check": str(row.get("source_check") or row.get("issue_type") or "integrity_failure"), + "severity": "BLOCK", + "issue_type": str(row.get("issue_type") or "integrity_failure"), + "source_refs": norm_refs(row.get("source_refs")), + "detail": str(row.get("detail") or row.get("reason") or "integrity failure"), + **({"rerun_target_task": row.get("rerun_target_task")} if row.get("rerun_target_task") else {}), + } + + def review_from_item(item: dict[str, Any], severity: str, basis: str, decision: str): + return { + "source_check": str(item.get("source_check") or item.get("exception_type") or "review_finding"), + "severity": severity, + "issue_type": str(item.get("exception_type") or "review_finding"), + "domain_hints": list(item.get("domain_hints") or []), + "target_stage_hints": list(item.get("target_stage_hints") or []), + "target_task_hints": list(item.get("target_task_hints") or []), + "source_refs": norm_refs(item.get("source_refs")), + "review_instruction": str(item.get("review_instruction") or "후속 검토 단계에서 증거와 법률상 의미를 재검토한다.")[:120], + "candidate_use_policy": str(item.get("candidate_use_policy") or "preserve_as_needs_review"), + "concise_basis": str(basis or item.get("concise_basis") or decision)[:240], + "decision": decision, + } + + def severity_of(hard: list[Any], reviews: list[dict[str, Any]]): + if hard: return "BLOCK" + if any(x.get("severity") == "HARD_WARNING" for x in reviews): return "HARD_WARNING" + if reviews: return "SOFT_WARNING" + return "PASS" + + def progression(severity: str): + return "BLOCKED" if severity == "BLOCK" else ("ALLOWED" if severity == "PASS" else "ALLOWED_WITH_WARNING") + + def clean_checks(value: Any): + out = [] + for row in value if isinstance(value, list) else []: + if not isinstance(row, dict): continue + out.append({k: v for k, v in row.items() if k not in ("hard_gate_findings", "review_findings")}) + return out + + def main(): + _init() + b1 = read_json(B1_PRECHECK) + b2 = read_json(B2_PRECHECK) + pack = read_json(EXCEPTION_PACK) + b1_hard = [normalized_hard(x, "B1", i) for i, x in enumerate(b1.get("hard_gate_findings") or [], 1)] + b2_hard = [normalized_hard(x, "B2", i) for i, x in enumerate(b2.get("hard_gate_findings") or [], 1)] + exception_map = {} + pack_by_id = {} + for descriptor in pack.get("packs") or []: + pack_id = str(descriptor.get("pack_id") or "") + pack_by_id[pack_id] = descriptor + for item in descriptor.get("items") or []: + if isinstance(item, dict): exception_map[str(item.get("exception_id"))] = item + + decisions = {} + for pack_id, descriptor in sorted(pack_by_id.items()): + output_path = str(descriptor.get("output_path") or "") + try: + adjudication = read_json(output_path) + if adjudication.get("schema_version") != "stage1_part1_gate_adjudication_part.v1": + raise ValueError("schema_version mismatch") + if adjudication.get("pack_id") != pack_id: raise ValueError("pack_id mismatch") + expected = set(str(x) for x in descriptor.get("exception_ids") or []) + seen = [] + for decision in adjudication.get("decisions") or []: + if not isinstance(decision, dict): raise ValueError("decision must be an object") + exception_id = str(decision.get("exception_id") or "") + if exception_id not in expected or exception_id in decisions: + raise ValueError(f"unexpected or duplicate exception_id {exception_id}") + code = str(decision.get("decision") or "") + if code not in ALLOWED_DECISIONS: raise ValueError(f"invalid decision {code}") + if decision.get("severity") != DECISION_SEVERITY[code]: + raise ValueError(f"severity mismatch for {exception_id}") + item = exception_map[exception_id] + decision_refs = norm_refs(decision.get("source_refs")) + if not source_subset(decision_refs, norm_refs(item.get("source_refs"))): + raise ValueError(f"ADJUDICATION_SOURCE_REF_OUT_OF_SCOPE: {exception_id}") + decision = {**decision, "source_refs": decision_refs} + decisions[exception_id] = decision + seen.append(exception_id) + if set(seen) != expected: raise ValueError("decision set does not equal expected exception set") + except Exception as exc: + origins = {str(exception_map[x].get("origin_gate") or "B2") for x in descriptor.get("exception_ids") or [] + if x in exception_map} + target = b1_hard if origins == {"B1"} else b2_hard + target.append(normalized_hard({"issue_type": "adjudication_part_missing_or_invalid", + "detail": f"{output_path}: {exc}"}, "B1" if target is b1_hard else "B2", + len(target) + 1)) + + b1_reviews = [] + b2_reviews = [] + for item in b2.get("auto_review_findings") or []: + if not isinstance(item, dict): continue + b2_reviews.append(review_from_item( + item, + str(item.get("severity") or "SOFT_WARNING"), + str(item.get("concise_basis") or "deterministic review relay"), + "AUTO_REVIEW", + )) + for exception_id, item in sorted(exception_map.items()): + decision = decisions.get(exception_id) + if not decision: continue + code = decision["decision"] + origin = str(item.get("origin_gate") or "B2") + if code == "PASS_NO_ACTION": continue + if code == "BLOCK_SOURCE_INTEGRITY": + target = b1_hard if origin == "B1" else b2_hard + target.append(normalized_hard({"issue_type": item.get("exception_type"), + "source_refs": decision.get("source_refs"), + "detail": decision.get("concise_basis")}, origin, + len(target) + 1)) + continue + row = review_from_item({**item, **decision}, DECISION_SEVERITY[code], + str(decision.get("concise_basis") or ""), code) + (b1_reviews if origin == "B1" else b2_reviews).append(row) + + for item in pack.get("auto_blocked_review_items") or []: + if not isinstance(item, dict): continue + origin = str(item.get("origin_gate") or "B2") + row = review_from_item(item, "HARD_WARNING", str(item.get("reason") or "BLOCK_REVIEW"), + "BLOCK_REVIEW") + (b1_reviews if origin == "B1" else b2_reviews).append(row) + + all_reviews = b1_reviews + b2_reviews + if len(all_reviews) > 40: + overflow = all_reviews[40:] + b1_count = min(len(b1_reviews), 40) + b1_reviews = all_reviews[:b1_count] + b2_reviews = all_reviews[b1_count:40] + b2_hard.append(normalized_hard({ + "issue_type": "review_queue_overflow", + "detail": f"review findings exceed SHA writer limit: total={len(all_reviews)}, overflow={len(overflow)}", + "source_refs": {"evidence_indexes": sorted({x for row in overflow for x in row["source_refs"]["evidence_indexes"]})} + }, "B2", len(b2_hard) + 1)) + + for origin, rows in (("B1", b1_reviews), ("B2", b2_reviews)): + for i, row in enumerate(rows, 1): row["gate_finding_id"] = f"{origin}-REVIEW-{i:03d}" + b1_severity = severity_of(b1_hard, b1_reviews) + b2_severity = severity_of(b2_hard, b2_reviews) + highest_global = "HARD_WARNING" if any(x.get("severity") == "HARD_WARNING" for x in b1_reviews + b2_reviews) else ( + "SOFT_WARNING" if b1_reviews or b2_reviews else "PASS") + now = datetime.now(timezone.utc).isoformat() + + def risk_notes(rows): + return [str(x.get("concise_basis") or x.get("issue_type"))[:120] for x in rows] + + b1_audit = { + "gate_id": "B1_evidence_indexed", "executed_at": now, + "checks": clean_checks(b1.get("checks")), "overall_severity": b1_severity, + "block_reasons": [x["detail"] for x in b1_hard], + "warning_reasons": risk_notes(b1_reviews), + "hard_gate_findings": b1_hard, "review_findings": b1_reviews, + "rerun_targets": sorted({x.get("rerun_target_task") for x in b1_hard if x.get("rerun_target_task")}), + "stage2_risk_notes": risk_notes(b1_reviews), + "review_gate_summary": {"review_gate_count": len(b1_reviews), + "highest_review_severity": severity_of([], b1_reviews)}, + "handoff_export_summary": {"is_soft_handoff_writer": False, + "handoff_writer_task": "Task_B2_SHA256_soft_gate_handoff_writer", + "exportable_review_count": len(b1_reviews)}, + "downstream_progression": progression(b1_severity), + "stage2_auto_progression_allowed": b1_severity != "BLOCK", + "schema_contract_version": "B1_quality_gate.v4", + } + b2_audit = { + "gate_id": "B2_event_candidates", "executed_at": now, + "schema_invariant_findings": b2.get("schema_invariant_findings") or [], + "checks": clean_checks(b2.get("checks")), "overall_severity": b2_severity, + "block_reasons": [x["detail"] for x in b2_hard], + "warning_reasons": risk_notes(b2_reviews), + "hard_gate_findings": b2_hard, "review_findings": b2_reviews, + "rerun_targets": sorted({x.get("rerun_target_task") for x in b2_hard if x.get("rerun_target_task")}), + "stage2_risk_notes": risk_notes(b2_reviews), + "review_gate_summary": {"review_gate_count": len(b2_reviews), + "highest_review_severity": severity_of([], b2_reviews), + "b1_review_count": len(b1_reviews), "b2_review_count": len(b2_reviews)}, + "handoff_export_summary": {"is_soft_handoff_writer": False, + "handoff_writer_task": "Task_B2_SHA256_soft_gate_handoff_writer", + "handoff_path": "quality_gates/stage1_part1_soft_gate_handoff.json", + "exported_review_count": len(b2_reviews), + "handoff_status_candidate": "BLOCKED" if b1_hard or b2_hard else ( + "READY_WITH_REVIEW" if b1_reviews or b2_reviews else "READY_NO_REVIEW")}, + "downstream_progression": progression(b2_severity), + "stage2_auto_progression_allowed": b2_severity != "BLOCK", + "schema_contract_version": "B2_quality_gate.v4", + } + if b1_audit["review_gate_summary"]["review_gate_count"] != len(b1_audit["review_findings"]): + raise RuntimeError("B1 review count mismatch") + if b2_audit["review_gate_summary"]["review_gate_count"] != len(b2_audit["review_findings"]): + raise RuntimeError("B2 review count mismatch") + for audit in (b1_audit, b2_audit): + for row in audit["hard_gate_findings"] + audit["review_findings"]: + if set((row.get("source_refs") or {}).keys()) != set(REF_KEYS): + raise RuntimeError("source_refs named-object schema violation") + write_doc(B1_AUDIT, b1_audit) + write_doc(B2_AUDIT, b2_audit) + print(json.dumps({"status": "AUDITS_FINALIZED", "b1_severity": b1_severity, + "b2_severity": b2_severity, "review_count": len(b1_reviews) + len(b2_reviews), + "highest_review_severity": highest_global}, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B2_SHA256_soft_gate_handoff_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import hashlib + import itertools + import json + import re + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + SIDE_CAR_PATH = "quality_gates/stage1_part1_soft_gate_handoff.json" + INPUT_PATHS = { + "evidence_indexed_sha256": "evidence_indexed.json", + "evidence_event_candidates_sha256": "evidence_event_candidates.json", + "b1_gate_sha256": "quality_gates/B1_evidence_indexed_gate.json", + "b2_gate_sha256": "quality_gates/B2_event_candidates_gate.json", + } + WORKFLOW_ROLES = ( + "claim_right_identification", + "cause_of_action_element_build", + "evidence_weight_and_gap_check", + "chronology_and_state_split", + "asset_registry_succession_check", + "defense_and_obstacle_check", + "relief_scope_and_quantification", + "actio_value_compensation_check", + ) + SOURCE_REF_KEYS = ( + "evidence_indexes", + "event_candidate_ids", + "meeting_clause_refs", + "doc_titles", + ) + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-b2-sha256-soft-gate-handoff-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _mid(), + "method": "tools/call", + "params": {"name": name, "arguments": args}, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: Any, name: str) -> str: + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError(f"Empty: {name}") + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, dict) and "results" in outer: + result0 = (outer.get("results") or [{}])[0] + inner = result0.get("content") or result0.get("text") or "" + if not inner: + raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer if isinstance(outer, str) else json.dumps(outer, ensure_ascii=False) + + def read_raw(path: str) -> str: + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str) or not raw.strip(): + raise RuntimeError(f"Missing or empty required input: {path}") + return raw + + def parse_json(raw: str, path: str) -> Any: + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + try: + obj, _ = decoder.raw_decode(raw.strip()) + return obj + except Exception as exc: + raise RuntimeError(f"Invalid JSON input: {path}") from exc + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def digest(raw: str) -> str: + value = hashlib.sha256(raw.encode("utf-8")).hexdigest() + if not re.fullmatch(r"[0-9a-f]{64}", value): + raise RuntimeError("digest_guard produced a non-SHA256 value") + return value + + def as_list(value: Any) -> list[Any]: + if value is None: + return [] + if isinstance(value, list): + return value + return [value] + + def as_str_list(value: Any) -> list[str]: + out: list[str] = [] + for item in as_list(value): + if item is None: + continue + if isinstance(item, dict): + for key in ("id", "value", "name", "title", "ref"): + if item.get(key): + out.append(str(item[key]).strip()) + break + else: + out.append(str(item).strip()) + return [x for x in out if x] + + def clean_enum(value: Any, fallback: str) -> str: + text = str(value or fallback).strip().lower() + text = re.sub(r"[^a-z0-9_]+", "_", text) + text = re.sub(r"_+", "_", text).strip("_") + return text or fallback + + def clip(value: Any, limit: int = 120) -> str: + text = " ".join(str(value or "").split()) + if len(text) <= limit: + return text + return text[:limit].rstrip() + "..." + + def normalize_severity(value: Any) -> str: + text = str(value or "").upper() + if "HARD" in text: + return "HARD_WARNING" + return "SOFT_WARNING" + + def role_for(issue_type: str, finding: dict[str, Any]) -> str: + explicit = finding.get("lawyer_workflow_role") + if explicit in WORKFLOW_ROLES: + return str(explicit) + text = " ".join([ + issue_type, + str(finding.get("source_check") or ""), + str(finding.get("check_id") or ""), + str(finding.get("detail") or finding.get("reason") or ""), + ]).lower() + if any(k in text for k in ("chrono", "date", "timeline", "state", "order")): + return "chronology_and_state_split" + if any(k in text for k in ("evidence", "proof", "source", "locator")): + return "evidence_weight_and_gap_check" + if any(k in text for k in ("registry", "secured", "lien", "succession", "asset", "auction", "mortgage")): + return "asset_registry_succession_check" + if any(k in text for k in ("defense", "obstacle", "setoff", "limitation")): + return "defense_and_obstacle_check" + if any(k in text for k in ("relief", "amount", "scope", "valuation", "damage")): + return "relief_scope_and_quantification" + if "actio" in text or "fraudulent" in text or "value_compensation" in text: + return "actio_value_compensation_check" + if any(k in text for k in ("claim", "right", "element", "legal_effect")): + return "claim_right_identification" + return "cause_of_action_element_build" + + def source_refs(finding: dict[str, Any]) -> dict[str, list[str]]: + refs = {key: [] for key in SOURCE_REF_KEYS} + nested = finding.get("source_refs") + if isinstance(nested, dict): + for key in SOURCE_REF_KEYS: + refs[key].extend(as_str_list(nested.get(key))) + refs["evidence_indexes"].extend(as_str_list(finding.get("evidence_indexes"))) + refs["evidence_indexes"].extend(as_str_list(finding.get("evidence_index"))) + refs["evidence_indexes"].extend(as_str_list(finding.get("source_evidence_index"))) + refs["event_candidate_ids"].extend(as_str_list(finding.get("event_candidate_ids"))) + refs["event_candidate_ids"].extend(as_str_list(finding.get("candidate_id"))) + refs["event_candidate_ids"].extend(as_str_list(finding.get("candidate_ids"))) + refs["meeting_clause_refs"].extend(as_str_list(finding.get("meeting_clause_refs"))) + refs["meeting_clause_refs"].extend(as_str_list(finding.get("meeting_clause_ref"))) + refs["doc_titles"].extend(as_str_list(finding.get("doc_titles"))) + refs["doc_titles"].extend(as_str_list(finding.get("doc_title"))) + return {key: sorted(set(value)) for key, value in refs.items()} + + def extract_findings(gate: Any, key: str) -> list[Any]: + findings: list[Any] = [] + if isinstance(gate, dict): + findings.extend(as_list(gate.get(key))) + for check in as_list(gate.get("checks")): + if isinstance(check, dict): + findings.extend(as_list(check.get(key))) + return findings + + def hard_findings(gate: Any, source_gate: str) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for item in extract_findings(gate, "hard_gate_findings"): + out.append(item if isinstance(item, dict) else {"detail": str(item)}) + if isinstance(gate, dict): + blocked = gate.get("overall_severity") == "BLOCK" or gate.get("downstream_progression") == "BLOCKED" or gate.get("stage2_auto_progression_allowed") is False + if blocked and not out: + reasons = as_list(gate.get("block_reasons")) + if not reasons: + reasons = [f"{source_gate} gate blocked without explicit hard_gate_findings"] + for reason in reasons: + out.append({"issue_type": "block_reason", "detail": str(reason)}) + for index, item in enumerate(out, 1): + item.setdefault("gate_finding_id", f"{source_gate}-HARD-{index:03d}") + return out + + def compact_review_items(b1_gate: Any, b2_gate: Any) -> list[dict[str, Any]]: + compact: list[dict[str, Any]] = [] + ordered = [("B1", b1_gate), ("B2", b2_gate)] + seq = 1 + for source_gate, gate in ordered: + for raw in extract_findings(gate, "review_findings"): + finding = raw if isinstance(raw, dict) else {"detail": str(raw)} + issue_type = clean_enum( + finding.get("issue_type") or finding.get("check_id") or finding.get("source_check") or "review_finding", + "review_finding", + ) + role = role_for(issue_type, finding) + refs = source_refs(finding) + policy = finding.get("candidate_use_policy") + if policy not in ("prioritize_if_supported", "preserve_as_needs_review", "do_not_generate_without_support"): + policy = "preserve_as_needs_review" + item = { + "review_id": f"P1RG-{seq:04d}", + "source_gate": source_gate, + "source_check": clean_enum(finding.get("source_check") or finding.get("check_id") or issue_type, issue_type), + "severity": normalize_severity(finding.get("severity")), + "issue_type": issue_type, + "lawyer_workflow_role": role, + "domain_hints": as_str_list(finding.get("domain_hints") or finding.get("domain_hint") or finding.get("domains")), + "target_stage_hints": as_str_list(finding.get("target_stage_hints") or finding.get("target_stage_hint")), + "target_task_hints": as_str_list(finding.get("target_task_hints") or finding.get("target_task_hint")), + "source_refs": refs, + "review_instruction": clip(finding.get("review_instruction") or finding.get("instruction") or "후속 검토 단계에서 해당 review finding의 증거와 법률상 의미를 재검토한다.", 120), + "negative_instruction": "사실 확정 또는 자동 차단으로 취급하지 말 것", + "candidate_use_policy": policy, + "concise_basis": clip(finding.get("concise_basis") or finding.get("detail") or finding.get("reason") or issue_type, 120), + "created_from": { + "gate_file": "quality_gates/B1_evidence_indexed_gate.json" if source_gate == "B1" else "quality_gates/B2_event_candidates_gate.json", + "gate_finding_id": finding.get("gate_finding_id") or finding.get("finding_id") or finding.get("id"), + }, + } + compact.append(item) + seq += 1 + if len(compact) >= 40: + return compact + return compact + + def add_index(index: dict[str, dict[str, list[str]]], bucket: str, key: str, review_id: str) -> None: + if not key: + return + index[bucket].setdefault(key, []) + if review_id not in index[bucket][key]: + index[bucket][key].append(review_id) + + def build_routing(review_items: list[dict[str, Any]]) -> tuple[dict[str, dict[str, list[str]]], dict[str, list[str]]]: + routing = { + "by_evidence_index": {}, + "by_event_candidate_id": {}, + "by_meeting_clause_ref": {}, + "by_issue_type": {}, + "by_domain_hint": {}, + "by_target_stage": {}, + "by_lawyer_workflow_role": {}, + } + queues = {role: [] for role in WORKFLOW_ROLES} + for item in review_items: + rid = item["review_id"] + refs = item.get("source_refs") or {} + for value in refs.get("evidence_indexes", []): + add_index(routing, "by_evidence_index", value, rid) + for value in refs.get("event_candidate_ids", []): + add_index(routing, "by_event_candidate_id", value, rid) + for value in refs.get("meeting_clause_refs", []): + add_index(routing, "by_meeting_clause_ref", value, rid) + add_index(routing, "by_issue_type", item.get("issue_type", ""), rid) + for value in item.get("domain_hints", []): + add_index(routing, "by_domain_hint", value, rid) + for value in item.get("target_stage_hints", []): + add_index(routing, "by_target_stage", value, rid) + role = item.get("lawyer_workflow_role", "cause_of_action_element_build") + add_index(routing, "by_lawyer_workflow_role", role, rid) + queues.setdefault(role, []).append(rid) + return routing, {role: queues.get(role, []) for role in WORKFLOW_ROLES} + + def gate_blocked(gate: Any) -> bool: + return isinstance(gate, dict) and ( + gate.get("overall_severity") == "BLOCK" + or gate.get("downstream_progression") == "BLOCKED" + or gate.get("stage2_auto_progression_allowed") is False + ) + + def main() -> None: + _init() + raw_by_digest_key = {key: read_raw(path) for key, path in INPUT_PATHS.items()} + parsed = {key: parse_json(raw, INPUT_PATHS[key]) for key, raw in raw_by_digest_key.items()} + + digest_guard = {key: digest(raw) for key, raw in raw_by_digest_key.items()} + b1_gate = parsed["b1_gate_sha256"] + b2_gate = parsed["b2_gate_sha256"] + review_items = compact_review_items(b1_gate, b2_gate) + routing_index, lawyer_workflow_queues = build_routing(review_items) + + hard_items = hard_findings(b1_gate, "B1") + hard_findings(b2_gate, "B2") + hard_ids = [ + str(item.get("gate_finding_id") or item.get("finding_id") or item.get("id") or f"HARD-{idx:03d}") + for idx, item in enumerate(hard_items, 1) + ] + blocked = bool(hard_items) or gate_blocked(b1_gate) or gate_blocked(b2_gate) + highest_review = "PASS" + if any(item["severity"] == "HARD_WARNING" for item in review_items): + highest_review = "HARD_WARNING" + elif review_items: + highest_review = "SOFT_WARNING" + b1_count = sum(1 for item in review_items if item["source_gate"] == "B1") + b2_count = sum(1 for item in review_items if item["source_gate"] == "B2") + + if blocked: + handoff_status = "BLOCKED" + elif review_items: + handoff_status = "READY_WITH_REVIEW" + else: + handoff_status = "READY_NO_REVIEW" + + sidecar = { + "schema_version": "stage1_part1_soft_gate_handoff.v1", + "source_stage": "Stage_1.Part_1", + "source_gate_files": [ + "quality_gates/B1_evidence_indexed_gate.json", + "quality_gates/B2_event_candidates_gate.json", + ], + "handoff_status": handoff_status, + "stage2_auto_progression_allowed": not blocked, + "hard_gate_summary": { + "hard_gate_count": len(hard_items), + "hard_gate_ids": hard_ids, + }, + "review_gate_summary": { + "review_gate_count": len(review_items), + "highest_review_severity": highest_review, + "b1_review_count": b1_count, + "b2_review_count": b2_count, + }, + "digest_guard": digest_guard, + "review_items": review_items, + "routing_index": routing_index, + "lawyer_workflow_queues": lawyer_workflow_queues, + } + + content = json.dumps(sidecar, ensure_ascii=False, indent=2) + write_doc(SIDE_CAR_PATH, content) + print(json.dumps({ + "status": "SOFT_GATE_HANDOFF_WRITTEN", + "path": SIDE_CAR_PATH, + "handoff_status": handoff_status, + "review_items": len(review_items), + "digest_guard_valid": all(re.fullmatch(r"[0-9a-f]{64}", v) for v in digest_guard.values()), + }, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + task_procedure: + IN: + nexts: ["Task_A_client_goal", "Task_Evidence_shard_planner"] + wait_until: [] + + Task_A_client_goal: + nexts: [] + wait_until: ["IN"] + + Task_Evidence_shard_planner: + nexts: ["Task_B1_map_doc_*"] + wait_until: ["IN"] + + Task_B1_map_doc_*: + nexts: + - "Task_B2_map_events_{same_ordinal}" + - "Task_B1_gate_merge_deterministic" + wait_until: ["Task_Evidence_shard_planner"] + + Task_B2_map_events_*: + nexts: ["Task_B2_gate_merge_deterministic"] + wait_until: ["Task_B1_map_doc_{same_ordinal}"] + + Task_B1_gate_merge_deterministic: + nexts: ["Task_B2_gate_merge_deterministic"] + wait_until: ["all Task_B1_map_doc_*"] + + Task_B2_gate_merge_deterministic: + nexts: + - "Task_B1B2_gate_llm_adjudicator_*" + - "Task_B12_gate_audit_finalizer" + wait_until: + - "all Task_B2_map_events_*" + - "Task_B1_gate_merge_deterministic" + + Task_B1B2_gate_llm_adjudicator_*: + nexts: ["Task_B12_gate_audit_finalizer"] + wait_until: ["Task_B2_gate_merge_deterministic"] + + Task_B12_gate_audit_finalizer: + nexts: ["Task_B2_SHA256_soft_gate_handoff_writer"] + wait_until: + - "Task_B2_gate_merge_deterministic" + - "all Task_B1B2_gate_llm_adjudicator_*" + + Task_B2_SHA256_soft_gate_handoff_writer: + nexts: ["OUT"] + wait_until: ["Task_B12_gate_audit_finalizer"] + + OUT: + nexts: [] + wait_until: ["Task_B2_SHA256_soft_gate_handoff_writer"] + + prevs: [] + nexts: [] diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Codex YAML/Stage_1_Part_2_Codex_v3.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Codex YAML/Stage_1_Part_2_Codex_v3.yml new file mode 100644 index 00000000..ea89beb1 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Codex YAML/Stage_1_Part_2_Codex_v3.yml @@ -0,0 +1,4013 @@ +--- +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_2 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Part 2 + version: v.3 + Stages: + - name: stage1_BO_시그널_생성 + description: BO 생성, 시그널 생성 + tools: + mcpServers: + localdocs: + type: streamable-http + url: http://mcp-localdocs:8012/mcp + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + tasks: + - task_name: Task_C_BO_A0_context_domain_slice_compiler + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_IDS = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + def _next_id() -> int: + return next(MSG_IDS) + + def _parse_json_first(raw: str, label: str, allow_trailing: bool = False) -> Any: + text = (raw or "").lstrip("\ufeff").strip() + if not text: + raise RuntimeError(f"{label}: empty JSON text") + try: + return json.loads(text) + except json.JSONDecodeError as first_error: + try: + value, end = JSON_DECODER.raw_decode(text) + except json.JSONDecodeError as second_error: + raise RuntimeError(f"{label}: invalid JSON") from second_error + if not allow_trailing and text[end:].strip(): + raise RuntimeError(f"{label}: trailing non-JSON data") from first_error + return value + + def _parse_rpc(raw: str) -> dict[str, Any]: + events: list[dict[str, Any]] = [] + for line in (raw or "").splitlines(): + if not line.startswith("data:"): + continue + candidate = line[5:].strip() + if not candidate or candidate == "[DONE]": + continue + try: + parsed = json.loads(candidate) + except json.JSONDecodeError: + continue + if isinstance(parsed, dict): + events.append(parsed) + if events: + return events[-1] + parsed = _parse_json_first(raw, "MCP response", allow_trailing=True) + if not isinstance(parsed, dict): + raise RuntimeError("MCP response root must be object") + return parsed + + def _init(task_name: str) -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": task_name, + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _call(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_rpc(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP {name} failed: {payload['error']}") + result = payload.get("result") + if not isinstance(result, dict): + raise RuntimeError(f"MCP {name} returned no result") + if result.get("isError") is True: + raise RuntimeError(f"MCP {name} returned isError") + return payload + + def _unwrap(payload: dict[str, Any], label: str) -> Any: + result = payload.get("result") or {} + content = result.get("content") or [] + texts = [block.get("text") for block in content if isinstance(block, dict) and block.get("text")] + if not texts: + raise RuntimeError(f"{label}: empty MCP content") + raw = texts[0] + try: + return _parse_json_first(raw, label, allow_trailing=True) + except RuntimeError: + return raw + + def _extract_doc(payload: dict[str, Any], doc_name: str) -> str: + outer = _unwrap(payload, f"read_docs:{doc_name}") + if isinstance(outer, dict) and isinstance(outer.get("results"), list): + for item in outer["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + if name and not (str(name) == doc_name or str(name).endswith("/" + doc_name)): + continue + value = item.get("content") + if value is None: + value = item.get("text") + if value is None: + value = item.get("raw") + if value is not None: + return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False) + if isinstance(outer, dict) and doc_name in outer: + value = outer[doc_name] + return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False) + if isinstance(outer, str): + return outer + return json.dumps(outer, ensure_ascii=False) + + def _read_text(doc_name: str) -> str: + return _extract_doc(_call("read_docs", {"doc_names": [doc_name]}), doc_name) + + def _read_json(doc_name: str) -> Any: + raw = _read_text(doc_name) + return _parse_json_first(raw, doc_name, allow_trailing=True) + + def _canonical(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + def _sha(value: Any) -> str: + raw = value if isinstance(value, str) else _canonical(value) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _write_json_verified(doc_name: str, value: Any) -> str: + content = json.dumps(value, ensure_ascii=False, indent=2) + _call("write_file", {"path": doc_name, "content": content, "overwrite": True}) + reread = _read_json(doc_name) + if _canonical(reread) != _canonical(value): + raise RuntimeError(f"{doc_name}: post-write verification mismatch") + return _sha(content) + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _uniq(values: list[Any]) -> list[Any]: + seen: set[str] = set() + output: list[Any] = [] + for value in values: + marker = _canonical(value) if isinstance(value, (dict, list)) else str(value) + if marker in seen: + continue + seen.add(marker) + output.append(value) + return output + + def _natural_key(value: Any) -> list[Any]: + return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", str(value))] + + REQUIRED_FILES = [ + "client_meeting.md", + "evidence_event_candidates.json", + "evidence_indexed.json", + "Default_Agent/Juristic_Act.md", + ] + PART1_SOFT_GATE_HANDOFF_FILE = "quality_gates/stage1_part1_soft_gate_handoff.json" + + CURRENT_STATE_PATTERNS = [ + "현재", "현재까지", "현재도", "계속", "거주", "점유", "사용", "운영", + "부지로만 사용", "인도받아", "사업자등록", "유치권", "임대", "수익", + ] + CONFLICT_PATTERNS = [ + "하였음에도", "그럼에도", "불구하고", "거절", "응하지", "주장", + "항변", "다툼", "무효", "취소", "해제", "해지", "말소", "신뢰", + ] + SUCCESSION_PATTERNS = [ + "사망", "상속", "상속포기", "배우자", "직계비속", "직계존속", + "피상속인", "상속인", "수리", "가족관계", + ] + MIXED_CONTRACT_PATTERNS = [ + "임대차", "매매", "보증금", "매매대금", "인도", "관리", "사용", + "사용수익", "영업양도", "채무불승계", "대물변제", + ] + + + DOMAIN_CONFIGS = [ + { + "domain_id": "B1_Money_Successor", + "domain_label": "금전채권 및 상호속용 책임", + "allowed_legal_effect_bo_types": [ + "money_claim_chain_BO", + "commercial_successor_liability_chain_BO", + "liability_group_hint_BO", + ], + "terms": [ + "대여금", "차용", "매매대금", "물품대금", "보증금", "정산금", "공사대금", + "원금", "이자", "지연손해금", "변제기", "이행기", "일부변제", "미변제", + "채무승인", "지급각서", "확인서", "최고", "통지", "상호속용", "영업양도", + "양수인", "채무불승계", "business_transfer", "loan", "payment", "admission", + "notice", "claim_document", "mixed_contract_payment", + ], + "meeting_sources": ["mixed_contract_clause_ids", "conflict_pattern_clause_ids"], + "event_sources": ["mixed_contract_candidate_ids"], + "hazard_keys": ["mixed_contract_candidates", "conflict_pattern_expressions", "asset_clusters"], + "taxonomy_axes": ["legal_object", "structure", "dependency"], + }, + { + "domain_id": "B2_Secured_Registry", + "domain_label": "담보채무, 변제충당, 등기무효, 경매", + "allowed_legal_effect_bo_types": [ + "secured_debt_chain_BO", + "payment_allocation_and_mortgage_extinguishment_BO", + "auction_invalidity_BO", + "derivative_bank_mortgage_invalidity_BO", + ], + "terms": [ + "근저당", "저당", "담보", "피담보", "채무액", "변제", "변제충당", + "법정변제충당", "지분", "말소", "소유권이전등기", "등기", "등기원인", + "등기무효", "경매", "담보권실행경매", "경매개시", "매각", "배당", + "후행 담보권", "registry", "mortgage", "secured", "auction", "dividend", + "payment_allocation", "registry_row", "legal_calculation", + ], + "meeting_sources": ["conflict_pattern_clause_ids", "mixed_contract_clause_ids"], + "event_sources": [], + "hazard_keys": ["conflict_pattern_expressions", "asset_clusters", "mixed_contract_candidates"], + "taxonomy_axes": ["legal_object", "form", "dependency", "defect_or_condition"], + }, + { + "domain_id": "B3_Land_Valuation", + "domain_label": "토지 사용이익, 점유, 사용수익, 가치평가", + "allowed_legal_effect_bo_types": ["land_use_gain_timeline_BO", "liability_overlap_BO"], + "terms": [ + "토지", "건물", "부지", "인도", "점유", "사용", "사용수익", "현재 사용", + "현재 수익", "거주", "운영", "임대", "차임", "감정", "시가", "가액", + "미등기 건물", "사실상 처분권", "부진정연대", "valuation", "valuation_object", + "valuation_rows", "valuation_matrix", "registry_object_display", "possession", "land", "building", + ], + "meeting_sources": ["current_state_clause_ids", "conflict_pattern_clause_ids", "mixed_contract_clause_ids"], + "event_sources": ["state_candidate_ids"], + "hazard_keys": ["current_state_expressions", "asset_clusters", "mixed_contract_candidates", "conflict_pattern_expressions"], + "taxonomy_axes": ["legal_object", "contribution", "structure"], + }, + { + "domain_id": "B4_Actio", + "domain_label": "사해행위취소, 피보전채권, 책임재산, 가액배상", + "allowed_legal_effect_bo_types": [ + "preserved_claim_bundle_BO", + "fraudulent_transfer_BO", + "encumbrance_timeline_BO", + "valuation_matrix_BO", + "remedy_mode_decision_BO", + "value_compensation_cap_BO", + "defense_rebuttal_matrix_BO", + "actio_balance_sheet_BO", + ], + "terms": [ + "사해행위", "피보전채권", "채무초과", "책임재산", "공동담보", "매매", + "증여", "담보설정", "처분행위", "수익자", "전득자", "사해성", "악의", + "선의", "원상회복", "가액배상", "선순위담보", "말소", "가액 산정", + "항변", "actio", "fraudulent", "transfer", "preserved_claim", "encumbrance", + "valuation", "remedy", "compensation", "defense", "claim_document", + ], + "meeting_sources": ["conflict_pattern_clause_ids", "mixed_contract_clause_ids", "current_state_clause_ids", "succession_clause_ids"], + "event_sources": [], + "hazard_keys": ["conflict_pattern_expressions", "asset_clusters", "mixed_contract_candidates", "current_state_expressions", "succession_related_candidates"], + "taxonomy_axes": ["legal_object", "structure", "contribution", "defect_or_condition", "dependency", "form"], + }, + { + "domain_id": "B5_Succession_Notice_Lien_Asset_Defense", + "domain_label": "상속, 통지, 유치권, 목적물 특정, 항변", + "allowed_legal_effect_bo_types": [ + "succession_resolution_BO", + "notice_lifecycle_BO", + "lien_state_timeline_BO", + "asset_alias_BO", + "defense_rebuttal_BO", + "delivery_use_gain_claim_group_BO", + ], + "terms": [ + "사망", "상속", "상속포기", "배우자", "직계비속", "가족관계", "피상속인", + "상속인", "통지", "내용증명", "도달", "수령", "최고", "유치권", "점유", + "인도", "거절", "별지목록", "등기부 표시", "목적물", "동일성", "피고 답변", + "항변", "권리남용", "신의칙", "동시이행", "succession", "notice", "lien", + "possession", "asset", "defense", "party_assertion", "registry_object_display", + ], + "meeting_sources": ["succession_clause_ids", "current_state_clause_ids", "conflict_pattern_clause_ids", "mixed_contract_clause_ids"], + "event_sources": ["succession_candidate_ids", "state_candidate_ids"], + "hazard_keys": ["succession_related_candidates", "current_state_expressions", "conflict_pattern_expressions", "asset_clusters", "mixed_contract_candidates"], + "taxonomy_axes": ["structure", "time_effect", "legal_object", "defect_or_condition"], + } + ] + + def _stable_hash(value: Any) -> str: + if isinstance(value, str): + raw = value + else: + raw = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _text(value: Any) -> str | None: + if value is None: + return None + s = str(value).strip() + return s or None + + def _norm_space(value: Any) -> str: + return re.sub(r"\s+", " ", str(value or "")).strip() + + def _norm_key(value: Any) -> str: + text = str(value or "").strip().lower() + return "".join(ch for ch in text if ch.isalnum()) + + def _short_excerpt(value: Any, limit: int = 120) -> str: + text = _norm_space(value) + return text if len(text) <= limit else text[: limit - 1] + "…" + + def _section_key_from_heading(heading: str, seq: int) -> str: + normalized = _norm_key(heading) + if "상담내용" in normalized: + suffix = "consultation" + elif "사건관계인의주장" in normalized or ("사건관계인" in normalized and "주장" in normalized): + suffix = "party_assertions" + elif "희망사항" in normalized or "의뢰인희망" in normalized: + suffix = "client_requests" + elif normalized: + suffix = normalized[:24] + else: + suffix = "unnamed" + return f"s{seq:02d}_{suffix}" + + def _uniq(values: list[Any]) -> list[Any]: + seen: set[str] = set() + out: list[Any] = [] + for value in values: + marker = json.dumps(value, ensure_ascii=False, sort_keys=True) if isinstance(value, (dict, list)) else str(value) + if marker not in seen: + seen.add(marker) + out.append(value) + return out + + def _extract_doc_names(payload: Any) -> list[str]: + names: list[str] = [] + def walk(v: Any) -> None: + if isinstance(v, str): + if "." in v or "/" in v: + names.append(v) + names.extend(re.findall(r"[\w./()가-힣ᄀ-ᇿ -]+\.[A-Za-z0-9]+", v)) + elif isinstance(v, list): + for item in v: + walk(item) + elif isinstance(v, dict): + for key in ("name", "path", "doc_name", "document_name", "file", "filename"): + if v.get(key): + names.append(str(v[key])) + for val in v.values(): + if isinstance(val, (list, dict)): + walk(val) + walk(payload) + return sorted(set(names)) + + def _doc_name_matches(found_names: list[str], required: str) -> bool: + required_norm = required.strip("/") + required_base = required_norm.split("/")[-1] + for name in found_names: + n = name.strip("/") + if n == required_norm or n.endswith("/" + required_norm) or n.split("/")[-1] == required_base: + return True + if required_norm in n or required_base in n: + return True + return False + + def _preflight_required_files() -> dict[str, Any]: + found_names_set: set[str] = set() + list_docs_args = ( + {"path": "/", "pattern": "*", "recursive": True}, + {"path": ".", "pattern": "*", "recursive": True}, + {"path": "/", "pattern": "*"}, + {"path": ".", "pattern": "*"}, + {"path": "Default_Agent", "pattern": "*"}, + {"path": "quality_gates", "pattern": "*.json"}, + ) + for args in list_docs_args: + try: + payload = _unwrap(_call("list_docs", args), "list_docs") + found_names_set.update(_extract_doc_names(payload)) + except Exception: + continue + found_names_now = sorted(found_names_set) + required_ready = all(_doc_name_matches(found_names_now, path) for path in REQUIRED_FILES) + sidecar_checked = _doc_name_matches(found_names_now, PART1_SOFT_GATE_HANDOFF_FILE) or args.get("path") == "quality_gates" + if required_ready and sidecar_checked: + break + found_names = sorted(found_names_set) + if not found_names: + raise RuntimeError("list_docs returned no recognizable document names for required inputs") + missing = [path for path in REQUIRED_FILES if not _doc_name_matches(found_names, path)] + if missing: + raise RuntimeError(f"Missing required input files by list_docs: {missing}") + return {"found_names": found_names, "required_files": REQUIRED_FILES} + + def _read_required_docs_once() -> dict[str, str]: + payload = _unwrap(_call("read_docs", {"doc_names": REQUIRED_FILES}), "read_docs") + docs: dict[str, str] = {} + + def assign(name: str | None, content: Any) -> None: + if not name: + return + text = content if isinstance(content, str) else json.dumps(content, ensure_ascii=False) + for required in REQUIRED_FILES: + if name == required or name.endswith("/" + required) or name.split("/")[-1] == required.split("/")[-1]: + docs[required] = text + + if isinstance(payload, dict) and isinstance(payload.get("results"), list): + for item in payload["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + content = item.get("content") or item.get("text") or item.get("markdown") or item.get("raw") + assign(str(name) if name else None, content) + elif isinstance(payload, dict): + for required in REQUIRED_FILES: + if required in payload: + assign(required, payload[required]) + elif isinstance(payload, str): + if len(REQUIRED_FILES) == 1: + assign(REQUIRED_FILES[0], payload) + + missing = [name for name in REQUIRED_FILES if name not in docs] + if missing: + # Some Localdocs runtimes return one JSON wrapper inside the text field. + raw = payload if isinstance(payload, str) else json.dumps(payload, ensure_ascii=False) + try: + outer = json.loads(raw) + if isinstance(outer, dict) and isinstance(outer.get("results"), list): + for item in outer["results"]: + if isinstance(item, dict): + name = item.get("name") or item.get("path") or item.get("doc_name") + content = item.get("content") or item.get("text") + assign(str(name) if name else None, content) + except Exception: + raw = "" + missing = [name for name in REQUIRED_FILES if name not in docs] + if missing: + raise RuntimeError(f"read_docs did not return required inputs: {missing}") + for name, content in docs.items(): + if not str(content).strip(): + raise RuntimeError(f"Required input is empty: {name}") + return docs + + def _parse_json_doc(name: str, raw: str) -> Any: + try: + return json.loads(raw.strip()) + except json.JSONDecodeError as exc: + raise RuntimeError(f"Strict JSON parse failed: {name}: {exc.msg} at line {exc.lineno} column {exc.colno}") from exc + + def _items(obj: Any, *keys: str) -> list[Any]: + if isinstance(obj, list): + return obj + if isinstance(obj, dict): + for key in keys + ("items", "evidence_indexed", "documents", "results"): + val = obj.get(key) + if isinstance(val, list): + return val + return [] + + def _extract_keywords(text: str, patterns: list[str]) -> list[str]: + return [p for p in patterns if p in text] + + def _build_meeting_clause_map(meeting_text: str) -> dict[str, Any]: + clauses: list[dict[str, Any]] = [] + current_major: str | None = None + current: dict[str, Any] | None = None + section_seq = 0 + current_section_key = "preamble" + current_section_label = "preamble" + + section_re = re.compile(r"^\s*#?\s*【\s*(.+?)\s*】\s*$") + major_re = re.compile(r"^\s*(\d+)\.\s+(.+?)\s*$") + sub_re = re.compile(r"^\s*([가-힣])\.\s+(.+?)\s*$") + + def flush(end_line: int) -> None: + nonlocal current + if not current: + return + current["line_end"] = end_line + current["text"] = _norm_space(current.get("text", "")) + text = current["text"] + current["tags"] = { + "current_state": _extract_keywords(text, CURRENT_STATE_PATTERNS), + "conflict_pattern": _extract_keywords(text, CONFLICT_PATTERNS), + "succession": _extract_keywords(text, SUCCESSION_PATTERNS), + "mixed_contract": _extract_keywords(text, MIXED_CONTRACT_PATTERNS), + } + current["text_excerpt"] = _short_excerpt(text) + clauses.append(current) + current = None + + lines = meeting_text.splitlines() + for idx, line in enumerate(lines, 1): + stripped = line.strip() + if not stripped or stripped.startswith("|") or stripped.startswith("---"): + continue + m_section = section_re.match(stripped) + m_major = major_re.match(stripped) + m_sub = sub_re.match(stripped) + if m_section: + flush(idx - 1) + section_seq += 1 + current_section_label = _norm_space(m_section.group(1)) + current_section_key = _section_key_from_heading(current_section_label, section_seq) + current_major = None + elif m_major: + flush(idx - 1) + current_major = m_major.group(1) + current = { + "clause_id": f"{current_section_key}:{current_major}", + "local_clause_id": current_major, + "section_key": current_section_key, + "section_label": current_section_label, + "major_no": current_major, + "sub_no": None, + "heading": _norm_space(m_major.group(2)), + "line_start": idx, + "text": _norm_space(m_major.group(2)), + } + elif m_sub and current_major: + flush(idx - 1) + sub_no = m_sub.group(1) + current = { + "clause_id": f"{current_section_key}:{current_major}-{sub_no}", + "local_clause_id": f"{current_major}-{sub_no}", + "section_key": current_section_key, + "section_label": current_section_label, + "major_no": current_major, + "sub_no": sub_no, + "heading": None, + "line_start": idx, + "text": _norm_space(m_sub.group(2)), + } + elif current: + current["text"] = _norm_space(str(current.get("text", "")) + " " + stripped) + flush(len(lines)) + + if not clauses: + raise RuntimeError("client_meeting.md parse produced no numbered meeting clauses") + + by_id: dict[str, Any] = {} + duplicate_clause_ids: list[str] = [] + for clause in clauses: + cid = clause["clause_id"] + if cid in by_id: + duplicate_clause_ids.append(cid) + by_id[cid] = clause + if duplicate_clause_ids: + raise RuntimeError(f"Duplicate meeting clause ids after section normalization: {duplicate_clause_ids}") + return { + "schema_version": "meeting_clause_map.v1", + "clause_order": [c["clause_id"] for c in clauses], + "by_clause_id": by_id, + "sections": _uniq([{"section_key": c["section_key"], "section_label": c["section_label"]} for c in clauses]), + "current_state_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["current_state"]], + "conflict_pattern_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["conflict_pattern"]], + "succession_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["succession"]], + "mixed_contract_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["mixed_contract"]], + } + + def _candidate_id(candidate: dict[str, Any], evidence_index: str, seq: int) -> str: + return str(candidate.get("candidate_id") or candidate.get("candidate_id_proposed") or f"internal_missing_candidate_id::{evidence_index}::{seq:02d}") + + def _evidence_index_from_item(item: dict[str, Any]) -> str | None: + return _text(item.get("evidence_index") or item.get("evidence_index_proposed") or item.get("source_evidence_index")) + + def _build_event_candidate_map(event_obj: Any) -> dict[str, Any]: + items = _items(event_obj) + if not items: + raise RuntimeError("evidence_event_candidates.json parse produced no document-level items") + by_candidate_id: dict[str, Any] = {} + by_evidence_index: dict[str, list[str]] = {} + by_meeting_clause_ref: dict[str, list[str]] = {} + state_candidate_ids: list[str] = [] + succession_candidate_ids: list[str] = [] + mixed_contract_candidate_ids: list[str] = [] + zero_event_evidence_indexes: list[str] = [] + asset_cluster_values: list[str] = [] + malformed_item_refs: list[str] = [] + + for item_seq, item in enumerate(items, 1): + if not isinstance(item, dict): + malformed_item_refs.append(f"item#{item_seq}: non-object") + continue + evidence_index = _evidence_index_from_item(item) + if not evidence_index: + malformed_item_refs.append(f"item#{item_seq}: missing evidence_index") + continue + item_ref = evidence_index + event_candidates_raw = item.get("event_candidates") + has_event_candidates_list = isinstance(event_candidates_raw, list) + has_event_candidates = has_event_candidates_list and len(event_candidates_raw) > 0 + has_zero_event_reason = bool(item.get("zero_event_reason")) + if event_candidates_raw is not None and not has_event_candidates_list: + malformed_item_refs.append(f"{item_ref}: event_candidates must be a list when present") + continue + if not has_event_candidates and not has_zero_event_reason: + malformed_item_refs.append(f"{item_ref}: requires non-empty event_candidates or zero_event_reason") + continue + if item.get("zero_event_reason"): + zero_event_evidence_indexes.append(evidence_index) + item_flags = item.get("doc_semantic_flags") if isinstance(item.get("doc_semantic_flags"), list) else [] + property_cluster_id = _text(item.get("property_cluster_id")) + if property_cluster_id: + asset_cluster_values.append(property_cluster_id) + for asset in item.get("affected_asset_cluster_ids") or []: + if _text(asset): + asset_cluster_values.append(str(asset)) + for seq, cand in enumerate(event_candidates_raw or [], 1): + if not isinstance(cand, dict): + malformed_item_refs.append(f"{item_ref}: event_candidates[{seq}] must be an object") + continue + cid = _candidate_id(cand, evidence_index, seq) + refs = cand.get("meeting_clause_refs") if isinstance(cand.get("meeting_clause_refs"), list) else [] + cand_flags = cand.get("doc_semantic_flags") if isinstance(cand.get("doc_semantic_flags"), list) else [] + candidate_flags = _uniq(item_flags + cand_flags) + record = { + "candidate_id": cid, + "source_evidence_index": _text(cand.get("source_evidence_index") or cand.get("source_evidence_index_proposed")) or evidence_index, + "title": item.get("title"), + "doc_type": item.get("doc_type"), + "event_or_state": cand.get("event_or_state"), + "event_kind": cand.get("event_kind"), + "event_subkind": cand.get("event_subkind"), + "action_type_candidate": cand.get("action_type_candidate"), + "action_summary": cand.get("action_summary"), + "participants": cand.get("participants") or {}, + "object_spec": cand.get("object_spec"), + "amount": cand.get("amount"), + "event_date": cand.get("event_date"), + "event_end_date": cand.get("event_end_date"), + "time_text": cand.get("time_text"), + "time_precision": cand.get("time_precision"), + "current_state_candidate": cand.get("current_state_candidate"), + "succession_candidate": cand.get("succession_candidate"), + "legal_effect_candidate": cand.get("legal_effect_candidate"), + "defense_candidate": cand.get("defense_candidate"), + "commercial_successor_candidates": cand.get("commercial_successor_candidates"), + "meeting_clause_refs": refs, + "actio_relevance_candidates": cand.get("actio_relevance_candidates"), + "identity_signature": cand.get("identity_signature"), + "property_cluster_id": property_cluster_id, + "affected_asset_cluster_ids": item.get("affected_asset_cluster_ids") or [], + "doc_semantic_flags": candidate_flags, + "fraction_ref": cand.get("fraction_ref"), + "confidence": cand.get("confidence"), + } + by_candidate_id[cid] = record + by_evidence_index.setdefault(evidence_index, []).append(cid) + for ref in refs: + by_meeting_clause_ref.setdefault(str(ref), []).append(cid) + if cand.get("event_or_state") == "state" or cand.get("current_state_candidate"): + state_candidate_ids.append(cid) + if cand.get("succession_candidate"): + succession_candidate_ids.append(cid) + if "mixed_contract_doc" in candidate_flags or cand.get("commercial_successor_candidates"): + mixed_contract_candidate_ids.append(cid) + if _text(cand.get("object_spec")): + asset_cluster_values.append(str(cand.get("object_spec"))) + + if malformed_item_refs: + raise RuntimeError(f"Malformed evidence_event_candidates document items: {malformed_item_refs[:20]}") + + return { + "schema_version": "event_candidate_map.v2", + "candidate_count": len(by_candidate_id), + "candidate_id_set": sorted(by_candidate_id.keys()), + "by_candidate_id": by_candidate_id, + "by_evidence_index": {k: _uniq(v) for k, v in by_evidence_index.items()}, + "by_meeting_clause_ref": {k: _uniq(v) for k, v in by_meeting_clause_ref.items()}, + "state_candidate_ids": _uniq(state_candidate_ids), + "succession_candidate_ids": _uniq(succession_candidate_ids), + "mixed_contract_candidate_ids": _uniq(mixed_contract_candidate_ids), + "zero_event_evidence_indexes": _uniq(zero_event_evidence_indexes), + "asset_cluster_values": _uniq([v for v in asset_cluster_values if _text(v)]), + } + + def _component_count(item: dict[str, Any], key: str) -> int: + val = item.get(key) + if isinstance(val, list): + return len(val) + if isinstance(val, dict): + return len(val) + return 1 if val not in (None, "", []) else 0 + + COMPONENT_DETAIL_PROJECTION = { + "loan_components": ["loan_component_id", "principal_amount", "loan_date", "due_date", + "agreed_interest_rate", "agreed_delay_damage_rate", "rate_text", + "rate_source_locator", "interest_accrual_start_hint"], + "secured_debt_components": ["agreed_interest_rate", "agreed_delay_damage_rate", "rate_text", + "rate_source_locator", "due_date"], + "notice_components": ["notice_id_proposed", "notice_type", "drafted_date", "dispatch_date", + "arrival_date", "received_date", "legal_effect_date_candidate", + "requires_receipt_for_effect", "receipt_evidence_refs", + "recipient_admission_text", "sender_candidates", "receiver_candidates"], + "registry_row_components": ["registry_ref", "fraction_ref", "fraction_display", + "fraction_numerator", "fraction_denominator", "receipt_no"], + "schedule_items": ["schedule_ref", "canonical_label_candidate", "usable_in_relief"], + } + + def _component_details(item: dict[str, Any]) -> dict[str, Any]: + details: dict[str, Any] = {} + for key, fields in COMPONENT_DETAIL_PROJECTION.items(): + rows = item.get(key) + if isinstance(rows, dict): + rows = [rows] + if not isinstance(rows, list): + continue + projected = [{field: row.get(field) for field in fields if field in row} for row in rows if isinstance(row, dict)] + projected = [p for p in projected if p] + if projected: + details[key] = projected + return details + + def _build_evidence_authority_map(evidence_obj: Any) -> dict[str, Any]: + items = _items(evidence_obj, "evidence_indexed") + if not items: + raise RuntimeError("evidence_indexed.json parse produced no evidence authority items") + by_evidence_index: dict[str, Any] = {} + by_doc_type: dict[str, list[str]] = {} + by_title_norm: dict[str, list[str]] = {} + semantic_flag_index: dict[str, list[str]] = {} + evidence_index_set: list[str] = [] + malformed_item_refs: list[str] = [] + duplicate_evidence_indexes: list[str] = [] + + component_keys = [ + "contract_components", "valuation_object", "legal_calculation_object", + "legal_calculation_objects", "claim_document_components", + "business_transfer_components", "registry_row_components", + "secured_debt_components", "valuation_rows", "mixed_contract_payment_semantics", + "loan_components", "valuation_matrix", "registry_row_role_map", + "defense_assertions", "admission_components", "defense_components", + "notice_components", "party_assertion_components", "schedule_items", + "registry_object_display", "succession_document_components", + ] + + for item_seq, item in enumerate(items, 1): + if not isinstance(item, dict): + malformed_item_refs.append(f"item#{item_seq}: non-object") + continue + evidence_index = _text(item.get("evidence_index") or item.get("evidence_index_proposed")) + if not evidence_index: + malformed_item_refs.append(f"item#{item_seq}: missing finalized evidence_index") + continue + if evidence_index in by_evidence_index: + duplicate_evidence_indexes.append(evidence_index) + continue + flags = item.get("doc_semantic_flags") if isinstance(item.get("doc_semantic_flags"), list) else [] + component_summary = {key: _component_count(item, key) for key in component_keys} + record = { + "evidence_index": evidence_index, + "doc_uid": item.get("doc_uid"), + "title": item.get("title"), + "title_normalized": item.get("title_normalized") or _norm_space(item.get("title")), + "doc_type": item.get("doc_type"), + "key_facts": item.get("key_facts") or [], + "key_dates": item.get("key_dates") or [], + "key_amounts": item.get("key_amounts") or [], + "key_parties": item.get("key_parties") or [], + "source_pointer": item.get("source_pointer") or {}, + "property_cluster_id": item.get("property_cluster_id"), + "doc_semantic_flags": flags, + "multi_event_registry": item.get("multi_event_registry"), + "component_summary": component_summary, + "component_details": _component_details(item), + } + by_evidence_index[evidence_index] = record + evidence_index_set.append(evidence_index) + by_doc_type.setdefault(str(item.get("doc_type") or "unknown"), []).append(evidence_index) + title_key = _norm_key(record["title_normalized"]) + if title_key: + by_title_norm.setdefault(title_key, []).append(evidence_index) + for flag in flags: + semantic_flag_index.setdefault(str(flag), []).append(evidence_index) + + if malformed_item_refs: + raise RuntimeError(f"Malformed evidence_indexed authority items: {malformed_item_refs[:20]}") + if duplicate_evidence_indexes: + raise RuntimeError(f"Duplicate evidence_index values in evidence_indexed.json: {_uniq(duplicate_evidence_indexes)}") + + return { + "schema_version": "evidence_authority_map.v2", + "evidence_index_set": _sort_evidence_indexes(_uniq(evidence_index_set)), + "by_evidence_index": by_evidence_index, + "by_doc_type": {k: _sort_evidence_indexes(_uniq(v)) for k, v in by_doc_type.items()}, + "by_title_norm": {k: _sort_evidence_indexes(_uniq(v)) for k, v in by_title_norm.items()}, + "semantic_flag_index": {k: _sort_evidence_indexes(_uniq(v)) for k, v in semantic_flag_index.items()}, + } + + def _sort_evidence_indexes(values: list[str]) -> list[str]: + def key(v: str) -> tuple[int, str]: + m = re.search(r"E-(\d+)", str(v)) + return (int(m.group(1)) if m else 10**9, str(v)) + return sorted(values, key=key) + + def _split_examples(value: str) -> list[str]: + parts = re.split(r"[,,、]| / |/|ㆍ|·", value or "") + return [_norm_space(p) for p in parts if _norm_space(p)] + + def _build_juristic_act_map(markdown: str) -> dict[str, Any]: + categories: dict[str, Any] = {} + example_to_gubun: dict[str, list[str]] = {} + for line in markdown.splitlines(): + if "|" not in line: + continue + cells = [_norm_space(c) for c in line.strip().strip("|").split("|")] + if len(cells) < 3: + continue + if cells[0] in ("구분", "---") or set(cells[0]) <= {"-"}: + continue + gubun, content, examples = cells[0], cells[1], cells[2] + if not gubun or gubun.startswith("-"): + continue + example_list = _split_examples(examples) + categories[gubun] = {"content": content, "examples": example_list} + for ex in example_list: + example_to_gubun.setdefault(_norm_key(ex), []).append(gubun) + + axis_groups = { + "structure": ["단독행위", "계약행위", "합동행위"], + "form": ["요식(要式)행위", "불요식행위"], + "time_effect": ["생전 행위", "사후(또는 사인) 행위"], + "legal_object": ["채권행위", "물권행위", "준물권행위"], + "contribution": ["출연행위", "비출연행위"], + "dependency": ["주(主)된 행위", "종(從)된 행위", "유인 행위", "무인 행위"], + "defect_or_condition": ["무효 행위", "취소가능한 법률 행위", "조건부 법률행위", "기한부 법률행위"], + "controversy": ["견해대립있는 행위"], + } + + return { + "schema_version": "juristic_act_taxonomy_map.v1", + "categories": categories, + "example_to_gubun": {k: _uniq(v) for k, v in example_to_gubun.items()}, + "axis_groups": axis_groups, + "source_digest_sha256": _stable_hash(markdown), + } + + def _validate_juristic_act_map(juristic_map: dict[str, Any]) -> None: + categories = juristic_map.get("categories") or {} + example_to_gubun = juristic_map.get("example_to_gubun") or {} + required = ["단독행위", "계약행위", "채권행위", "물권행위"] + missing = [name for name in required if name not in categories] + if not categories or not example_to_gubun: + raise RuntimeError("JuristicAct taxonomy parse produced empty categories or examples") + if missing: + raise RuntimeError(f"JuristicAct taxonomy missing required categories: {missing}") + + def _collect_asset_clusters(meeting_map: dict[str, Any], event_map: dict[str, Any], evidence_map: dict[str, Any]) -> list[str]: + values: list[str] = [] + asset_hint_re = re.compile(r"([가-힣A-Za-z0-9·ㆍ() \-]{1,30}(?:대지|건물|상가|빌라|임야|부동산|지분|영업|상호|채권|대금))") + for clause in meeting_map.get("by_clause_id", {}).values(): + text = str(clause.get("text") or "") + values.extend([_norm_space(m.group(1)) for m in asset_hint_re.finditer(text)]) + values.extend(event_map.get("asset_cluster_values") or []) + for ev in evidence_map.get("by_evidence_index", {}).values(): + if _text(ev.get("property_cluster_id")): + values.append(str(ev.get("property_cluster_id"))) + for fact in ev.get("key_facts") or []: + values.extend([_norm_space(m.group(1)) for m in asset_hint_re.finditer(str(fact))]) + return _uniq([v for v in values if _text(v)]) + + def _build_global_hazard_seed(meeting_map: dict[str, Any], event_map: dict[str, Any], evidence_map: dict[str, Any]) -> dict[str, Any]: + evidence_index_set = evidence_map.get("evidence_index_set") or [] + current_state_ids = event_map.get("state_candidate_ids") or [] + succession_candidate_ids = event_map.get("succession_candidate_ids") or [] + mixed_contract_candidate_ids = event_map.get("mixed_contract_candidate_ids") or [] + + def meeting_expression_records(clause_ids: list[str], tag_key: str) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + by_clause = meeting_map.get("by_clause_id", {}) + for cid in clause_ids: + clause = by_clause.get(cid) or {} + matched = (clause.get("tags") or {}).get(tag_key) or [] + out.append({ + "source": "client_meeting.md", + "clause_id": cid, + "local_clause_id": clause.get("local_clause_id"), + "section_key": clause.get("section_key"), + "matched_patterns": matched, + "text_excerpt": clause.get("text_excerpt") or _short_excerpt(clause.get("text")), + }) + return out + + def event_expression_records(candidate_ids: list[str]) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + by_candidate = event_map.get("by_candidate_id", {}) + for cid in candidate_ids: + cand = by_candidate.get(cid) or {} + text = " ".join(str(cand.get(k) or "") for k in ("action_summary", "event_kind", "event_subkind", "identity_signature")) + matched = _uniq( + _extract_keywords(text, CURRENT_STATE_PATTERNS) + + _extract_keywords(text, CONFLICT_PATTERNS) + + _extract_keywords(text, SUCCESSION_PATTERNS) + + _extract_keywords(text, MIXED_CONTRACT_PATTERNS) + ) + out.append({ + "source": "evidence_event_candidates.json", + "candidate_id": cid, + "source_evidence_index": cand.get("source_evidence_index"), + "event_or_state": cand.get("event_or_state"), + "event_kind": cand.get("event_kind"), + "matched_patterns": matched, + "text_excerpt": _short_excerpt(cand.get("action_summary") or cand.get("identity_signature")), + }) + return out + + conflict_candidate_ids: list[str] = [] + for cid, cand in event_map.get("by_candidate_id", {}).items(): + combined = " ".join( + str(cand.get(k) or "") + for k in ("action_summary", "event_kind", "event_subkind", "identity_signature") + ) + if _extract_keywords(combined, CONFLICT_PATTERNS): + conflict_candidate_ids.append(cid) + + succession_evidence_indexes = [] + mixed_contract_evidence_indexes = [] + for eidx, ev in evidence_map.get("by_evidence_index", {}).items(): + flags = ev.get("doc_semantic_flags") or [] + title_and_facts = " ".join([str(ev.get("title") or "")] + [str(x) for x in ev.get("key_facts") or []]) + if "succession_doc" in flags or _extract_keywords(title_and_facts, SUCCESSION_PATTERNS): + succession_evidence_indexes.append(eidx) + if "mixed_contract_doc" in flags or _extract_keywords(title_and_facts, MIXED_CONTRACT_PATTERNS): + mixed_contract_evidence_indexes.append(eidx) + + return { + "schema_version": "global_hazard_seed.v1", + "meeting_clause_ids": meeting_map.get("clause_order") or [], + "current_state_expressions": { + "meeting_clause_ids": meeting_map.get("current_state_clause_ids") or [], + "event_candidate_ids": current_state_ids, + "meeting_expressions": meeting_expression_records(meeting_map.get("current_state_clause_ids") or [], "current_state"), + "event_expressions": event_expression_records(current_state_ids), + }, + "conflict_pattern_expressions": { + "meeting_clause_ids": meeting_map.get("conflict_pattern_clause_ids") or [], + "event_candidate_ids": _uniq(conflict_candidate_ids), + "meeting_expressions": meeting_expression_records(meeting_map.get("conflict_pattern_clause_ids") or [], "conflict_pattern"), + "event_expressions": event_expression_records(_uniq(conflict_candidate_ids)), + }, + "asset_clusters": _collect_asset_clusters(meeting_map, event_map, evidence_map), + "succession_related_candidates": { + "meeting_clause_ids": meeting_map.get("succession_clause_ids") or [], + "event_candidate_ids": succession_candidate_ids, + "evidence_indexes": _sort_evidence_indexes(_uniq(succession_evidence_indexes)), + "meeting_expressions": meeting_expression_records(meeting_map.get("succession_clause_ids") or [], "succession"), + "event_expressions": event_expression_records(succession_candidate_ids), + }, + "mixed_contract_candidates": { + "meeting_clause_ids": meeting_map.get("mixed_contract_clause_ids") or [], + "event_candidate_ids": mixed_contract_candidate_ids, + "evidence_indexes": _sort_evidence_indexes(_uniq(mixed_contract_evidence_indexes)), + "meeting_expressions": meeting_expression_records(meeting_map.get("mixed_contract_clause_ids") or [], "mixed_contract"), + "event_expressions": event_expression_records(mixed_contract_candidate_ids), + }, + "evidence_index_set": evidence_index_set, + } + + def _read_optional_doc_once(doc_name: str) -> str | None: + try: + payload = _unwrap(_call("read_docs", {"doc_names": [doc_name]}), "read_docs_optional") + except Exception: + return None + if isinstance(payload, dict) and isinstance(payload.get("results"), list): + for item in payload["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + content = item.get("content") or item.get("text") or item.get("markdown") or item.get("raw") + if name and (str(name) == doc_name or str(name).endswith("/" + doc_name) or str(name).split("/")[-1] == doc_name.split("/")[-1]): + return content if isinstance(content, str) else json.dumps(content, ensure_ascii=False) + if isinstance(payload, dict) and doc_name in payload: + val = payload[doc_name] + return val if isinstance(val, str) else json.dumps(val, ensure_ascii=False) + if isinstance(payload, str) and payload.strip(): + return payload + return None + + def _compact_part1_soft_gate_handoff(existence_check: dict[str, Any], input_digests: dict[str, str], meeting_map: dict[str, Any], event_map: dict[str, Any], evidence_map: dict[str, Any]) -> dict[str, Any]: + found_names = existence_check.get("found_names") or [] + base = { + "schema_version": "stage1_part1_soft_gate_handoff.v1", + "status": "ABSENT", + "stage2_auto_progression_allowed": True, + "review_gate_count": 0, + "highest_review_severity": "PASS", + "review_items_compact": [], + "routing_index": {}, + "lawyer_workflow_queues": {}, + "dropped_review_items": [], + "usage_rules": { + "not_factual_authority": True, + "must_not_create_fact_without_stage_a_source": True, + "must_preserve_as_needs_review_if_used": True, + "must_not_override_stage_e_final_progression": True, + "must_not_override_stage_f_single_writer_gate": True, + }, + } + if not _doc_name_matches(found_names, PART1_SOFT_GATE_HANDOFF_FILE): + return base + raw = _read_optional_doc_once(PART1_SOFT_GATE_HANDOFF_FILE) + if not raw: + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": "sidecar listed but unreadable"}) + return base + try: + sidecar = json.loads(raw.strip()) + except Exception as exc: + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": f"sidecar JSON parse failed: {exc}"}) + return base + if not isinstance(sidecar, dict) or sidecar.get("schema_version") != "stage1_part1_soft_gate_handoff.v1": + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": "wrong or missing schema_version"}) + return base + if sidecar.get("handoff_status") == "BLOCKED" or sidecar.get("stage2_auto_progression_allowed") is False: + raise RuntimeError("Part 1 soft gate handoff is BLOCKED; Part 2-6 cannot proceed") + guard = sidecar.get("digest_guard") if isinstance(sidecar.get("digest_guard"), dict) else {} + invalid_guard_fields = [] + stale = [] + for guard_key, doc_name in [("evidence_indexed_sha256", "evidence_indexed.json"), ("evidence_event_candidates_sha256", "evidence_event_candidates.json")]: + val = guard.get(guard_key) + if not isinstance(val, str) or not re.fullmatch(r"[0-9a-f]{64}", val): + invalid_guard_fields.append(guard_key) + elif val != input_digests.get(doc_name): + stale.append(guard_key) + if invalid_guard_fields: + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": "digest_guard missing real sha256", "fields": invalid_guard_fields}) + return base + if stale: + base["status"] = "IGNORED_STALE" + base["dropped_review_items"].append({"reason": "digest_guard mismatch", "fields": stale}) + return base + evidence_set = set(evidence_map.get("evidence_index_set") or []) + event_set = set(event_map.get("candidate_id_set") or []) + meeting_set = set(meeting_map.get("clause_order") or []) + compact = [] + dropped = [] + for idx, item in enumerate(sidecar.get("review_items") or []): + if len(compact) >= 40: + dropped.append({"index": idx, "reason": "review_items_compact max 40 reached"}) + continue + if not isinstance(item, dict): + dropped.append({"index": idx, "reason": "non-object review item"}) + continue + refs = item.get("source_refs") if isinstance(item.get("source_refs"), dict) else {} + bad_e = [x for x in refs.get("evidence_indexes", []) if x not in evidence_set] + bad_ev = [x for x in refs.get("event_candidate_ids", []) if x not in event_set] + bad_m = [x for x in refs.get("meeting_clause_refs", []) if x not in meeting_set] + if bad_e or bad_ev or bad_m: + dropped.append({"review_id": item.get("review_id"), "reason": "source_refs membership failed", "bad_evidence_indexes": bad_e, "bad_event_candidate_ids": bad_ev, "bad_meeting_clause_refs": bad_m}) + continue + compact.append({ + "review_id": item.get("review_id"), + "severity": item.get("severity"), + "issue_type": item.get("issue_type"), + "lawyer_workflow_role": item.get("lawyer_workflow_role"), + "domain_hints": item.get("domain_hints") or [], + "target_stage_hints": item.get("target_stage_hints") or [], + "target_task_hints": item.get("target_task_hints") or [], + "source_refs": refs, + "candidate_use_policy": item.get("candidate_use_policy"), + "review_instruction": item.get("review_instruction"), + "negative_instruction": item.get("negative_instruction"), + }) + summary = sidecar.get("review_gate_summary") if isinstance(sidecar.get("review_gate_summary"), dict) else {} + base.update({ + "status": sidecar.get("handoff_status") if sidecar.get("handoff_status") in {"READY_WITH_REVIEW", "READY_NO_REVIEW"} else "READY_WITH_REVIEW", + "stage2_auto_progression_allowed": sidecar.get("stage2_auto_progression_allowed") is not False, + "review_gate_count": len(compact), + "highest_review_severity": summary.get("highest_review_severity") or ("HARD_WARNING" if any(i.get("severity") == "HARD_WARNING" for i in compact) else ("SOFT_WARNING" if compact else "PASS")), + "review_items_compact": compact, + "routing_index": sidecar.get("routing_index") if isinstance(sidecar.get("routing_index"), dict) else {}, + "lawyer_workflow_queues": sidecar.get("lawyer_workflow_queues") if isinstance(sidecar.get("lawyer_workflow_queues"), dict) else {}, + "dropped_review_items": dropped, + }) + return base + + + ALL_DOMAIN_TERMS = {config["domain_id"]: config["terms"] for config in DOMAIN_CONFIGS} + PRIMARY_TERMS = { + "B1_Money_Successor": ["대여금", "차용", "물품대금", "공사대금", "원금", "이자", "지연손해금", "변제기", "일부변제", "미변제", "채무승인", "지급각서", "상호속용", "영업양도", "양수인", "채무불승계", "business_transfer", "loan", "admission"], + "B2_Secured_Registry": ["근저당", "저당", "담보", "피담보", "변제충당", "말소", "경매", "배당", "registry", "mortgage", "secured", "auction", "dividend"], + "B3_Land_Valuation": ["토지", "건물", "점유", "수익", "인도", "차임", "감정", "시가", "valuation", "possession", "land", "building"], + "B4_Actio": ["사해행위", "피보전채권", "채무초과", "책임재산", "공동담보", "처분행위", "수익자", "전득자", "가액배상", "actio", "fraudulent"], + "B5_Succession_Notice_Lien_Asset_Defense": ["사망", "상속", "상속포기", "내용증명", "유치권", "별지목록", "동시이행", "상계", "succession", "notice", "lien", "defense"], + } + + MEETING_FIELDS = [ + "clause_id", "local_clause_id", "section_key", "section_label", "major_no", + "sub_no", "heading", "line_start", "line_end", "text_excerpt", "tags", + ] + EVENT_FIELDS = ["candidate_id", "source_evidence_index", "title", "doc_type", "event_or_state", "event_kind", "event_subkind", "action_type_candidate", "action_summary", "participants", "object_spec", "amount", "event_date", "event_end_date", "time_text", "time_precision", "current_state_candidate", "succession_candidate", "legal_effect_candidate", "defense_candidate", "commercial_successor_candidates", "meeting_clause_refs", "actio_relevance_candidates", "identity_signature", "property_cluster_id", "affected_asset_cluster_ids", "doc_semantic_flags", "confidence", "fraction_ref"] + EVIDENCE_FIELDS = ["evidence_index", "doc_uid", "title", "title_normalized", "doc_type", "key_facts", "key_dates", "key_amounts", "key_parties", "source_pointer", "property_cluster_id", "doc_semantic_flags", "multi_event_registry", "component_summary", "component_details"] + + def _parse_json(raw: str, label: str) -> dict[str, Any]: + text = (raw or "").strip() + if not text or text == "null": + raise RuntimeError(f"{label}: empty injected JSON") + decoder = json.JSONDecoder() + obj, idx = decoder.raw_decode(text) + if text[idx:].strip(): + raise RuntimeError(f"{label}: trailing data after JSON object") + if not isinstance(obj, dict): + raise RuntimeError(f"{label}: root must be object") + return obj + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _uniq(values: list[Any]) -> list[Any]: + seen: set[str] = set() + out: list[Any] = [] + for value in values: + marker = json.dumps(value, ensure_ascii=False, sort_keys=True) if isinstance(value, (dict, list)) else str(value) + if marker not in seen: + seen.add(marker) + out.append(value) + return out + + def _natural_key(value: Any) -> list[Any]: + return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", str(value))] + + def _sorted_ids(values: set[str] | list[str]) -> list[str]: + return sorted([str(v) for v in values if str(v)], key=_natural_key) + + def _blob(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True).lower() + + def _matches(value: Any, terms: list[str]) -> bool: + haystack = _blob(value) + return any(str(term).lower() in haystack for term in terms if str(term).strip()) + + def _copy_fields(source: dict[str, Any], fields: list[str]) -> dict[str, Any]: + out: dict[str, Any] = {} + for field in fields: + if field in source: + out[field] = source.get(field) + return out + + def _project_meeting(record: dict[str, Any]) -> dict[str, Any]: + out = _copy_fields(record, MEETING_FIELDS) + if not out.get("text_excerpt") and record.get("text"): + out["text_excerpt"] = str(record.get("text"))[:240] + return out + + def _project_event(record: dict[str, Any]) -> dict[str, Any]: + return _copy_fields(record, EVENT_FIELDS) + + def _project_evidence(record: dict[str, Any]) -> dict[str, Any]: + return _copy_fields(record, EVIDENCE_FIELDS) + + def _accept_stage_a(stage_a: dict[str, Any]) -> None: + checks = [ + ("stage_a_context.schema_version", stage_a.get("schema_version") == "task_c_bo_stage_a_context.v1"), + ("stage_a_context.status", stage_a.get("status") == "READY"), + ("meeting_clause_map.schema_version", (stage_a.get("meeting_clause_map") or {}).get("schema_version") == "meeting_clause_map.v1"), + ("event_candidate_map.schema_version", (stage_a.get("event_candidate_map") or {}).get("schema_version") == "event_candidate_map.v2"), + ("evidence_authority_map.schema_version", (stage_a.get("evidence_authority_map") or {}).get("schema_version") == "evidence_authority_map.v2"), + ("juristic_act_taxonomy_map.schema_version", (stage_a.get("juristic_act_taxonomy_map") or {}).get("schema_version") == "juristic_act_taxonomy_map.v1"), + ("global_hazard_seed.schema_version", (stage_a.get("global_hazard_seed") or {}).get("schema_version") == "global_hazard_seed.v1"), + ("stage_b_worker_contract.context_is_read_only", (stage_a.get("stage_b_worker_contract") or {}).get("context_is_read_only") is True), + ("stage_b_worker_contract.bo_id_finalization_allowed", (stage_a.get("stage_b_worker_contract") or {}).get("bo_id_finalization_allowed") is False), + ("stage_b_worker_contract.candidate_generation_allowed", (stage_a.get("stage_b_worker_contract") or {}).get("candidate_generation_allowed") is True), + ] + failed = [name for name, ok in checks if not ok] + if failed: + raise RuntimeError(f"Stage A acceptance failed: {failed}") + + def _matched_domains(record: dict[str, Any]) -> list[str]: + return [domain_id for domain_id, terms in ALL_DOMAIN_TERMS.items() if _matches(record, terms)] + + def _select(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + meeting_map = stage_a.get("meeting_clause_map") or {} + event_map = stage_a.get("event_candidate_map") or {} + evidence_map = stage_a.get("evidence_authority_map") or {} + by_clause = meeting_map.get("by_clause_id") or {} + by_event = event_map.get("by_candidate_id") or {} + by_evidence = evidence_map.get("by_evidence_index") or {} + + clause_ids: set[str] = set() + event_ids: set[str] = set() + evidence_ids: set[str] = set() + + for key in config["meeting_sources"]: + for value in _as_list(meeting_map.get(key)): + value_id = str(value) + if _matches(by_clause.get(value_id) or {}, config["terms"]): + clause_ids.add(value_id) + for key in config["event_sources"]: + for value in _as_list(event_map.get(key)): + value_id = str(value) + if _matches(by_event.get(value_id) or {}, config["terms"]): + event_ids.add(value_id) + + for clause_id, record in by_clause.items(): + if _matches(record, config["terms"]): + clause_ids.add(str(clause_id)) + for event_id, record in by_event.items(): + if _matches(record, config["terms"]): + event_ids.add(str(event_id)) + for evidence_id, record in by_evidence.items(): + if _matches(record, config["terms"]): + evidence_ids.add(str(evidence_id)) + + for _ in range(2): + for evidence_id in list(evidence_ids): + event_ids.update(str(v) for v in _as_list((event_map.get("by_evidence_index") or {}).get(evidence_id))) + for clause_id in list(clause_ids): + event_ids.update(str(v) for v in _as_list((event_map.get("by_meeting_clause_ref") or {}).get(clause_id))) + for event_id in list(event_ids): + event = by_event.get(event_id) or {} + source_evidence = event.get("source_evidence_index") + if source_evidence: + evidence_ids.add(str(source_evidence)) + for ref in _as_list(event.get("meeting_clause_refs")): + clause_ids.add(str(ref)) + + clause_ids = {v for v in clause_ids if v in by_clause} + event_ids = {v for v in event_ids if v in by_event} + evidence_ids = {v for v in evidence_ids if v in by_evidence} + + selected_meetings = [_project_meeting(by_clause[v]) for v in _sorted_ids(clause_ids)] + selected_events = [_project_event(by_event[v]) for v in _sorted_ids(event_ids)] + selected_evidence = [_project_evidence(by_evidence[v]) for v in _sorted_ids(evidence_ids)] + + shared: list[dict[str, Any]] = [] + for item_type, id_values, source in [ + ("meeting_clause", clause_ids, by_clause), + ("event_candidate", event_ids, by_event), + ("evidence", evidence_ids, by_evidence), + ]: + for item_id in _sorted_ids(id_values): + domains = _matched_domains(source.get(item_id) or {}) + if len(domains) > 1: + shared.append({"item_type": item_type, "id": item_id, "matched_domain_ids": domains}) + + return { + "meeting_clause_ids": _sorted_ids(clause_ids), + "event_candidate_ids": _sorted_ids(event_ids), + "evidence_indexes": _sorted_ids(evidence_ids), + "selected_meetings": selected_meetings, + "selected_events": selected_events, + "selected_evidence": selected_evidence, + "shared": shared, + } + + def _taxonomy_subset(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + taxonomy = stage_a.get("juristic_act_taxonomy_map") or {} + axes = taxonomy.get("axis_groups") or {} + return { + "schema_version": taxonomy.get("schema_version"), + "categories": taxonomy.get("categories") or {}, + "example_to_gubun": taxonomy.get("example_to_gubun") or {}, + "axis_groups": {key: axes.get(key) for key in config["taxonomy_axes"] if key in axes}, + "source_digest_sha256": taxonomy.get("source_digest_sha256"), + } + + def _hazard_subset(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + hazard = stage_a.get("global_hazard_seed") or {} + return {key: hazard.get(key) for key in config["hazard_keys"] if key in hazard} + + def _part1_review(stage_a: dict[str, Any], selected: dict[str, Any], config: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]: + handoff = stage_a.get("part1_soft_gate_handoff") or {} + guard = { + "schema_version": handoff.get("schema_version"), + "status": handoff.get("status"), + "stage2_auto_progression_allowed": handoff.get("stage2_auto_progression_allowed"), + "usage_rules": handoff.get("usage_rules") or {}, + } + selected_e = set(selected["evidence_indexes"]) + selected_ev = set(selected["event_candidate_ids"]) + selected_m = set(selected["meeting_clause_ids"]) + relevant: list[dict[str, Any]] = [] + for item in _as_list(handoff.get("review_items_compact")): + refs = item.get("source_refs") if isinstance(item, dict) else {} + domain_text = _blob({ + "domain_hints": item.get("domain_hints"), + "target_task_hints": item.get("target_task_hints"), + "issue_type": item.get("issue_type"), + "review_instruction": item.get("review_instruction"), + }) + ref_hit = bool( + selected_e.intersection(str(v) for v in _as_list(refs.get("evidence_indexes"))) + or selected_ev.intersection(str(v) for v in _as_list(refs.get("event_candidate_ids"))) + or selected_m.intersection(str(v) for v in _as_list(refs.get("meeting_clause_refs"))) + ) + if ref_hit or any(str(term).lower() in domain_text for term in config["terms"]): + relevant.append(item) + return guard, relevant + + + def _build_stage_a_context() -> dict[str, Any]: + existence_check = _preflight_required_files() + docs = _read_required_docs_once() + event_candidates = _parse_json_doc("evidence_event_candidates.json", docs["evidence_event_candidates.json"]) + evidence_indexed = _parse_json_doc("evidence_indexed.json", docs["evidence_indexed.json"]) + meeting_clause_map = _build_meeting_clause_map(docs["client_meeting.md"]) + event_candidate_map = _build_event_candidate_map(event_candidates) + evidence_authority_map = _build_evidence_authority_map(evidence_indexed) + juristic_act_taxonomy_map = _build_juristic_act_map(docs["Default_Agent/Juristic_Act.md"]) + _validate_juristic_act_map(juristic_act_taxonomy_map) + if not evidence_authority_map["evidence_index_set"]: + raise RuntimeError("No finalized evidence_index values available") + input_digests = {name: _sha(docs[name]) for name in REQUIRED_FILES} + part1_handoff = _compact_part1_soft_gate_handoff( + existence_check, input_digests, meeting_clause_map, event_candidate_map, evidence_authority_map + ) + return { + "schema_version": "task_c_bo_stage_a_context.v1", + "stage": "Task_C_BO.Stage_A", + "status": "READY", + "created_at_utc": datetime.now(timezone.utc).isoformat(), + "allowed_input_files": REQUIRED_FILES, + "forbidden_files_attested": ["evidence_all.json", "BO.json", "actio_case_signals.json", "case_liability_signals.json", "legal_effect_signals.json"], + "file_io_policy": { + "list_docs_called_for": REQUIRED_FILES, + "read_docs_called_once_for": REQUIRED_FILES, + "raw_evidence_all_read": False, + }, + "existence_check": existence_check, + "input_digests_sha256": input_digests, + "meeting_clause_map": meeting_clause_map, + "event_candidate_map": event_candidate_map, + "evidence_authority_map": evidence_authority_map, + "juristic_act_taxonomy_map": juristic_act_taxonomy_map, + "global_hazard_seed": _build_global_hazard_seed(meeting_clause_map, event_candidate_map, evidence_authority_map), + "part1_soft_gate_handoff": part1_handoff, + "stage_b_worker_contract": { + "context_is_read_only": True, + "bo_id_finalization_allowed": False, + "candidate_generation_allowed": True, + "must_use_evidence_index_set": evidence_authority_map["evidence_index_set"], + }, + } + + def _tier_ids(selected: dict[str, Any], config: dict[str, Any]) -> dict[str, list[str]]: + primary: list[str] = [] + support: list[str] = [] + review_only: list[str] = [] + records = [ + ("meeting", "clause_id", selected["selected_meetings"]), + ("event", "candidate_id", selected["selected_events"]), + ("evidence", "evidence_index", selected["selected_evidence"]), + ] + primary_terms = PRIMARY_TERMS[config["domain_id"]] + for kind, id_key, items in records: + for item in items: + item_id = str(item.get(id_key) or "") + if kind == "meeting": + direct_value = {key: item.get(key) for key in ("heading", "text_excerpt", "tags")} + elif kind == "event": + direct_value = {key: item.get(key) for key in ("event_kind", "event_subkind", "action_summary", "object_spec", "participants", "amount", "fraction_ref", "doc_semantic_flags")} + else: + direct_value = {key: item.get(key) for key in ("title", "doc_type", "key_facts", "key_dates", "key_amounts", "key_parties", "doc_semantic_flags", "component_summary", "component_details")} + direct_blob = _blob(direct_value) + strong_hits = [term for term in primary_terms if str(term).lower() in direct_blob] + broad_hits = [term for term in config["terms"] if str(term).lower() in direct_blob] + if strong_hits: + primary.append(item_id) + elif broad_hits: + review_only.append(item_id) + else: + support.append(item_id) + return {"primary_ids": _uniq(primary), "support_ids": _uniq(support), "review_only_ids": _uniq(review_only)} + + def _tier_project(items: list[dict[str, Any]], id_key: str, kind: str, tiers: dict[str, list[str]], config: dict[str, Any]) -> list[dict[str, Any]]: + primary = set(tiers["primary_ids"]) + review = set(tiers["review_only_ids"]) + projected: list[dict[str, Any]] = [] + support_fields = { + "meeting": ["clause_id", "section_key", "heading", "text_excerpt", "tags"], + "event": ["candidate_id", "source_evidence_index", "event_or_state", "event_kind", "event_subkind", "action_summary", "participants", "object_spec", "amount", "event_date", "time_text", "meeting_clause_refs", "fraction_ref"], + "evidence": ["evidence_index", "title", "doc_type", "key_dates", "key_amounts", "key_parties", "source_pointer", "doc_semantic_flags", "component_summary"], + } + for item in items: + item_id = str(item.get(id_key) or "") + if item_id in primary: + projected.append({**item, "routing_tier": "primary"}) + continue + if item_id in review: + projected.append({ + id_key: item_id, + "routing_tier": "review_only", + "source_type": kind, + "match_reason": [term for term in config["terms"] if str(term).lower() in _blob(item)][:4], + **({"source_evidence_index": item.get("source_evidence_index")} if kind == "event" else {}), + }) + continue + projected.append({ + **{key: item.get(key) for key in support_fields[kind] if key in item}, + "routing_tier": "support", + }) + return projected + + def _build_slice(stage_a: dict[str, Any], config: dict[str, Any], run_fingerprint: str) -> dict[str, Any]: + _accept_stage_a(stage_a) + selected = _select(stage_a, config) + all_clauses = _as_dict(_as_dict(stage_a.get("meeting_clause_map")).get("by_clause_id")) + selected_clause_ids = {str(item.get("clause_id")) for item in selected["selected_meetings"]} + for clause_id in _as_list(_as_dict(stage_a.get("meeting_clause_map")).get("clause_order")): + if str(clause_id) in selected_clause_ids: + continue + record = _as_dict(all_clauses.get(str(clause_id))) + selected["selected_meetings"].append(_project_meeting(record)) + selected["meeting_clause_ids"].append(str(clause_id)) + selected["meeting_clause_ids"] = _sorted_ids(set(selected["meeting_clause_ids"])) + part1_guard, part1_items = _part1_review(stage_a, selected, config) + tiers = _tier_ids(selected, config) + selected_meetings = _tier_project(selected["selected_meetings"], "clause_id", "meeting", tiers, config) + selected_events = _tier_project(selected["selected_events"], "candidate_id", "event", tiers, config) + selected_evidence = _tier_project(selected["selected_evidence"], "evidence_index", "evidence", tiers, config) + meeting_map = stage_a["meeting_clause_map"] + event_map = stage_a["event_candidate_map"] + evidence_map = stage_a["evidence_authority_map"] + slice_body = { + "schema_version": "task_c_bo_stage_b_domain_slice.v1", + "router_status": "READY", + "domain_id": config["domain_id"], + "domain_label": config["domain_label"], + "allowed_legal_effect_bo_types": config["allowed_legal_effect_bo_types"], + "source_stage_a_schema_version": stage_a["schema_version"], + "source_stage_a_status": stage_a["status"], + "source_stage_a_created_at_utc": stage_a["created_at_utc"], + "run_fingerprint": run_fingerprint, + "input_digests_sha256": stage_a["input_digests_sha256"], + "stage_b_worker_contract": stage_a["stage_b_worker_contract"], + "source_universe": { + "meeting_clause_ids": meeting_map.get("clause_order") or [], + "event_candidate_ids": event_map.get("candidate_id_set") or [], + "evidence_indexes": evidence_map.get("evidence_index_set") or [], + }, + "selected_meeting_clauses": selected_meetings, + "selected_event_candidates": selected_events, + "selected_evidence_authority": selected_evidence, + "juristic_act_taxonomy_subset": _taxonomy_subset(stage_a, config), + "hazard_subset": _hazard_subset(stage_a, config), + "part1_review_guard": part1_guard, + "part1_review_items_relevant": part1_items, + "shared_or_ambiguous_items": selected["shared"], + "routing_audit": { + **tiers, + "activation_reasons": ["deterministic domain terms", "source-reference closure"], + "source_count": { + "meeting": len(selected_meetings), + "event": len(selected_events), + "evidence": len(selected_evidence), + }, + "broadness_ratio": round(len(tiers["review_only_ids"]) / max(1, sum(len(v) for v in tiers.values())), 4), + }, + } + slice_body["routing_audit"]["slice_bytes"] = len(_canonical(slice_body).encode("utf-8")) + slice_body["slice_digest_sha256"] = _sha(slice_body) + return {"stage_b_domain_slice": slice_body} + + def main() -> None: + _init("task-c-bo-a0-context-domain-slice-compiler") + stage_a = _build_stage_a_context() + run_fingerprint = _sha(stage_a["input_digests_sha256"]) + stage_a["run_fingerprint"] = run_fingerprint + slices = {config["domain_id"]: _build_slice(stage_a, config, run_fingerprint) for config in DOMAIN_CONFIGS} + domain_paths = {domain_id: f"stage1_tmp/task_c_bo/domain_slices/{domain_id}.json" for domain_id in slices} + domain_digests = {domain_id: root["stage_b_domain_slice"]["slice_digest_sha256"] for domain_id, root in slices.items()} + domain_sources = { + domain_id: { + "meeting_clause_ids": root["stage_b_domain_slice"]["routing_audit"]["primary_ids"] + root["stage_b_domain_slice"]["routing_audit"]["support_ids"] + root["stage_b_domain_slice"]["routing_audit"]["review_only_ids"], + "event_candidate_ids": [str(item.get("candidate_id")) for item in root["stage_b_domain_slice"]["selected_event_candidates"] if item.get("candidate_id")], + "evidence_indexes": [str(item.get("evidence_index")) for item in root["stage_b_domain_slice"]["selected_evidence_authority"] if item.get("evidence_index")], + "meeting_clause_refs": [str(item.get("clause_id")) for item in root["stage_b_domain_slice"]["selected_meeting_clauses"] if item.get("clause_id")], + } + for domain_id, root in slices.items() + } + source_manifest = { + "schema_version": "task_c_bo_source_universe_manifest.v1", + "status": "READY", + "run_fingerprint": run_fingerprint, + "input_digests_sha256": stage_a["input_digests_sha256"], + "source_universe": { + "meeting_clause_ids": stage_a["meeting_clause_map"].get("clause_order") or [], + "event_candidate_ids": stage_a["event_candidate_map"].get("candidate_id_set") or [], + "evidence_indexes": stage_a["evidence_authority_map"].get("evidence_index_set") or [], + }, + "domain_source_universes": domain_sources, + "counts": { + "meeting_clauses": len(stage_a["meeting_clause_map"].get("clause_order") or []), + "event_candidates": len(stage_a["event_candidate_map"].get("candidate_id_set") or []), + "evidence_indexes": len(stage_a["evidence_authority_map"].get("evidence_index_set") or []), + }, + "domain_slice_digests_sha256": domain_digests, + } + _write_json_verified("stage1_tmp/task_c_bo/stage_a_context.json", {"stage_a_context": stage_a}) + _write_json_verified("stage1_tmp/task_c_bo/source_universe_manifest.json", source_manifest) + for domain_id, root in slices.items(): + _write_json_verified(domain_paths[domain_id], root) + print(json.dumps({"a0_manifest": { + "schema_version": "task_c_bo_a0_manifest.v1", + "status": "READY", + "run_fingerprint": run_fingerprint, + "stage_a_context_path": "stage1_tmp/task_c_bo/stage_a_context.json", + "source_manifest_path": "stage1_tmp/task_c_bo/source_universe_manifest.json", + "domain_slice_paths": domain_paths, + "input_digests_sha256": stage_a["input_digests_sha256"], + "counts": source_manifest["counts"], + }}, ensure_ascii=False, separators=(",", ":"))) + + if __name__ == "__main__": + try: + main() + except Exception as exc: + print(json.dumps({"a0_manifest": {"schema_version": "task_c_bo_a0_manifest.v1", "status": "FAILED", "error": str(exc)}}, ensure_ascii=False)) + raise + - task_name: Task_C_BO_Stage_B_B1_Money_Successor + llm_provider: google + llm_model: gemini-3.5-flash + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + Use localdocs exactly once to call read_docs for only this path: + stage1_tmp/task_c_bo/domain_slices/B1_Money_Successor.json + Do not call list_docs, do not read another path, and do not write any file. + The read result is the only factual input for this task. + + + + + - You MUST use only the JSON object read from . + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B1_Money_Successor"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B1_Money_Successor + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for money claim, payment, notice, admission, and commercial successor liability. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B1:001`, `B1:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - money claim origin: loan, sale price, deposit, settlement, construction price, unpaid balance + - due date, acceleration, demand, notice, payment deadline, or delayed performance + - partial payment, debt admission, payment memorandum, balance confirmation, account statement, or settlement confirmation + - business transfer, trade-name continuation, transferee liability, debt non-assumption clause, or commercial successor liability + - uncertain creditor/debtor/successor linkage that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use domain routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the assigned domain slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split money claim origin, maturity/demand, payment/admission, and successor liability when they are distinct legal-effect seeds. + 4. Preserve payment history, demand/notice hints, admission hints, successor liability hints, and successor-link uncertainty in `extensions.domain_payload`. Preserve agreed interest rate and agreed delay damage rate as literal strings from B1 `loan_components`/`secured_debt_components` (`component_details` of `selected_evidence_authority`) when present; never substitute statutory rates, and keep the supporting evidence indexes in `interest_rate_source_evidence_refs`. + 5. Use `domain_review_queue` for missing amount/date, uncertain creditor/debtor identity, conflicting payment status, or cross-domain successor merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Omit unsupported optional values and route unresolved extractable facts to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + Return exactly one JSON object with this sparse schema. Omit optional keys that are unsupported; preserve meaningful false and zero values. + { + "stage_b_domain_bo_seed_delta": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed_delta.v1", + "domain_id": "B1_Money_Successor", + "status": "READY|READY_WITH_REVIEW|FAILED", + "run_fingerprint": "sha256", + "slice_digest_sha256": "sha256", + "bo_seed_candidates": [ + { + "candidate_ref": "B1:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "Action": "short neutral source-backed draft", + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability" + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B1:R001", + "severity": "SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "candidate_refs": [], + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Part3|Part4|Stage2|human_review" + } + ] + } + } + Optional candidate keys allowed only when supported: JuristicAct, Legal_Keywords, core_field_base, amount, downstream_seed_refs. + Protected domain_payload keys whose source-backed content must be preserved when present: money_claim_details, payment_history, demand_or_notice_hints, admission_hints, successor_liability_hints, successor_link_uncertainty. + Do not emit transport_metadata, transport_candidate_refs, Reason, PriorAct, ReasonRefs, Evidence, EvidenceTitles, BO_ID, id, null-only scaffolding, or empty optional containers. + candidate_ref values must be contiguous in output order with the domain prefix and three-digit ordinal. + Status is FAILED on input guard failure, READY_WITH_REVIEW when the review queue is non-empty, otherwise READY. + + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B2_Secured_Registry + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + Use localdocs exactly once to call read_docs for only this path: + stage1_tmp/task_c_bo/domain_slices/B2_Secured_Registry.json + Do not call list_docs, do not read another path, and do not write any file. + The read result is the only factual input for this task. + + + + + - You MUST use only the JSON object read from . + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B2_Secured_Registry"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B2_Secured_Registry + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for secured debt, registry, payment allocation, auction, distribution, and derivative invalidity issues. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B2:001`, `B2:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - secured debt creation, change, repayment, payment allocation, and secured obligation balance + - mortgage, pledge, collateral security, registration, cancellation, correction, or invalidity chain + - auction, distribution, dividend, satisfaction, or residual balance facts + - derivative invalidity, registry error, title defect, or secured property dispute + - uncertain registry sequence or security/right-holder linkage that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use domain routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the assigned domain slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split secured debt chain, registry act, payment allocation, auction step, dividend distribution, and derivative invalidity when they are distinct legal-effect seeds. + 4. Preserve registry rows, secured debt chain, payment allocation, auction step, dividend distribution hints, derivative invalidity hints, and registry display values in `extensions.domain_payload`. Carry `fraction_ref` from source event candidates unchanged; do not merge or renumber. Populate `fractional_effect_events` as an event-reference list only (fraction-bearing setting/extinguishment/transfer events); never fold fraction states or normalize/reduce fractions (that is Stage 2 work). + 5. Use `domain_review_queue` for missing registry date, unclear secured obligation balance, conflicting registration sequence, or cross-domain property merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Omit unsupported optional values and route unresolved extractable facts to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + Return exactly one JSON object with this sparse schema. Omit optional keys that are unsupported; preserve meaningful false and zero values. + { + "stage_b_domain_bo_seed_delta": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed_delta.v1", + "domain_id": "B2_Secured_Registry", + "status": "READY|READY_WITH_REVIEW|FAILED", + "run_fingerprint": "sha256", + "slice_digest_sha256": "sha256", + "bo_seed_candidates": [ + { + "candidate_ref": "B2:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "Action": "short neutral source-backed draft", + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction" + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B2:R001", + "severity": "SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "candidate_refs": [], + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Part3|Part4|Stage2|human_review" + } + ] + } + } + Optional candidate keys allowed only when supported: JuristicAct, Legal_Keywords, core_field_base, amount, downstream_seed_refs. + Protected domain_payload keys whose source-backed content must be preserved when present: secured_debt_chain, payment_allocation, registry_rows, fractional_effect_events, mortgage_extinguishment_hints, auction_steps, dividend_distribution_hints, derivative_invalidity_hints. + Do not emit transport_metadata, transport_candidate_refs, Reason, PriorAct, ReasonRefs, Evidence, EvidenceTitles, BO_ID, id, null-only scaffolding, or empty optional containers. + candidate_ref values must be contiguous in output order with the domain prefix and three-digit ordinal. + Status is FAILED on input guard failure, READY_WITH_REVIEW when the review queue is non-empty, otherwise READY. + + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B3_Land_Valuation + llm_provider: google + llm_model: gemini-3.5-flash + llm_reasoning: medium + llm_verbosity: low + max_iterations: 2 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + Use localdocs exactly once to call read_docs for only this path: + stage1_tmp/task_c_bo/domain_slices/B3_Land_Valuation.json + Do not call list_docs, do not read another path, and do not write any file. + The read result is the only factual input for this task. + + + + + - You MUST use only the JSON object read from . + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B3_Land_Valuation"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B3_Land_Valuation + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for land use gain, possession/use, valuation, and asset-value facts. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B3:001`, `B3:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - land or building possession, use, occupation, benefit, rent-equivalent gain, or loss of use + - valuation fact, appraisal, market value, rent value, area, period, and calculation base + - delivery, possession start/end, unauthorized use, co-occupancy, or liability overlap + - asset identity, address, parcel, unit, area, or other value-bearing object description + - uncertain use period, value basis, possession status, or asset binding that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use domain routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the assigned domain slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split possession/use state, use-gain period, valuation fact, and liability overlap when they are distinct legal-effect seeds. + 4. Preserve valuation rows, valuation matrix, use-gain facts, possession state, asset binding hints, and liability overlap hints in `extensions.domain_payload`. Populate the `possession_state` two-track structure (possession facts vs. asserted legal causes) from selected event candidates only; never resolve which legal cause prevails (that is Stage 2 work); preserve conflicting assertions side by side. + 5. Use `domain_review_queue` for missing period, missing value base, unclear possession state, or cross-domain asset merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Omit unsupported optional values and route unresolved extractable facts to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + Return exactly one JSON object with this sparse schema. Omit optional keys that are unsupported; preserve meaningful false and zero values. + { + "stage_b_domain_bo_seed_delta": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed_delta.v1", + "domain_id": "B3_Land_Valuation", + "status": "READY|READY_WITH_REVIEW|FAILED", + "run_fingerprint": "sha256", + "slice_digest_sha256": "sha256", + "bo_seed_candidates": [ + { + "candidate_ref": "B3:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "Action": "short neutral source-backed draft", + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation" + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B3:R001", + "severity": "SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "candidate_refs": [], + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Part3|Part4|Stage2|human_review" + } + ] + } + } + Optional candidate keys allowed only when supported: JuristicAct, Legal_Keywords, core_field_base, amount, downstream_seed_refs. + Protected domain_payload keys whose source-backed content must be preserved when present: use_gain_facts, valuation_object, valuation_rows, valuation_matrix, possession_state, delivery_or_possession_start_hints, unregistered_building_hints, liability_overlap_hints. + Do not emit transport_metadata, transport_candidate_refs, Reason, PriorAct, ReasonRefs, Evidence, EvidenceTitles, BO_ID, id, null-only scaffolding, or empty optional containers. + candidate_ref values must be contiguous in output order with the domain prefix and three-digit ordinal. + Status is FAILED on input guard failure, READY_WITH_REVIEW when the review queue is non-empty, otherwise READY. + + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B4_Actio + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + Use localdocs exactly once to call read_docs for only this path: + stage1_tmp/task_c_bo/domain_slices/B4_Actio.json + Do not call list_docs, do not read another path, and do not write any file. + The read result is the only factual input for this task. + + + + + - You MUST use only the JSON object read from . + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B4_Actio"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B4_Actio + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for actio pauliana, preserved claim, debtor fraudulent act, beneficiary/transferee, remedy, cap, and defenses. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B4:001`, `B4:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - preserved claim existence, claim date, creditor/debtor identity, and claim amount basis + - debtor act, asset decrease, insolvency, common collateral impairment, and harmfulness + - beneficiary, transferee, bad faith/good faith, notice, value compensation, restitution, and remedy cap + - encumbrance, valuation, property transfer, mortgage, sale, gift, collateral provision, or registration act used as fraudulent act basis + - uncertain fraudulent act date, beneficiary chain, asset value, remedy mode, or defense rebuttal that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use domain routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the assigned domain slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split preserved claim, fraudulent transfer, balance-sheet impairment, encumbrance/valuation, beneficiary/transferee state, remedy/cap, and defense rebuttal when they are distinct legal-effect seeds. + 4. Preserve preserved claim bundle, fraudulent transfer facts, balance sheet, encumbrance timeline, valuation matrix, remedy/cap hints, and defense rebuttal in `extensions.domain_payload`. + 5. Use `domain_review_queue` for missing claim date, uncertain fraudulent act date, conflicting value basis, unclear beneficiary chain, or cross-domain asset/claim merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Omit unsupported optional values and route unresolved extractable facts to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + Return exactly one JSON object with this sparse schema. Omit optional keys that are unsupported; preserve meaningful false and zero values. + { + "stage_b_domain_bo_seed_delta": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed_delta.v1", + "domain_id": "B4_Actio", + "status": "READY|READY_WITH_REVIEW|FAILED", + "run_fingerprint": "sha256", + "slice_digest_sha256": "sha256", + "bo_seed_candidates": [ + { + "candidate_ref": "B4:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "Action": "short neutral source-backed draft", + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "actio_fraudulent_transfer" + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B4:R001", + "severity": "SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "candidate_refs": [], + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Part3|Part4|Stage2|human_review" + } + ] + } + } + Optional candidate keys allowed only when supported: JuristicAct, Legal_Keywords, core_field_base, amount, downstream_seed_refs. + Protected domain_payload keys whose source-backed content must be preserved when present: preserved_claim_bundle, fraudulent_transfer_facts, encumbrance_timeline, valuation_matrix, balance_sheet, remedy_mode_hints, value_compensation_cap_hints, defense_rebuttal. + Do not emit transport_metadata, transport_candidate_refs, Reason, PriorAct, ReasonRefs, Evidence, EvidenceTitles, BO_ID, id, null-only scaffolding, or empty optional containers. + candidate_ref values must be contiguous in output order with the domain prefix and three-digit ordinal. + Status is FAILED on input guard failure, READY_WITH_REVIEW when the review queue is non-empty, otherwise READY. + + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + Use localdocs exactly once to call read_docs for only this path: + stage1_tmp/task_c_bo/domain_slices/B5_Succession_Notice_Lien_Asset_Defense.json + Do not call list_docs, do not read another path, and do not write any file. + The read result is the only factual input for this task. + + + + + - You MUST use only the JSON object read from . + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B5_Succession_Notice_Lien_Asset_Defense"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B5_Succession_Notice_Lien_Asset_Defense + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for succession, notice lifecycle, lien, asset identity, possession, and major defenses. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B5:001`, `B5:002`, ... in deterministic order of the candidate seed list. + - If multiple lien seeds exist, assign `lien_id` values `B5:LIEN:001`, `B5:LIEN:002`, ... ordered by the earliest related event date (ties broken by `candidate_ref` lexicographic order). This id is transport-scope only, not a final BO id. + - Do not produce `violation_candidates` or `status_after_notice_receipt`; route such needs to `domain_review_queue` with `issue_type="legal_theory_required"`. + - `legal_sender_after_resolution.party` must always remain null in Stage 1 (value finalization is Stage 2 legal work); fill only its `status` and `succession_resolution_ref` from directly-read sources. + + + + Create separate seeds when supported for: + - general succession, specific succession, inheritance, assignment, acquisition, transfer, or party-status succession + - notice dispatch, arrival, receipt, refusal, content, timing, and legal effect of notice + - lien, possession, retained object, claim-object connection, asset alias, and asset identity + - simultaneous performance, setoff, payment, prescription, delivery, possession, or other major defense facts + - uncertain notice arrival, lien possession continuity, asset identity, succession scope, or defense linkage that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use domain routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the assigned domain slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split succession state, notice lifecycle, lien/possession state, asset identity, and defense rebuttal when they are distinct legal-effect seeds. + 4. Preserve succession resolution, notice lifecycle, lien state, possession state, asset alias, simultaneous-performance hints, setoff/payment defense hints, and defense rebuttal in `extensions.domain_payload`. Populate the `possession_state` two-track structure (possession facts vs. asserted legal causes) from selected event candidates only; never resolve which legal cause prevails (that is Stage 2 work); preserve conflicting assertions side by side. Assign `lien_id` deterministically and reference it from possession/notice payloads of the same seed group. Inherit `notice_id` from B1 `notice_components[*].notice_id_proposed` (`component_details` of `selected_evidence_authority`); when multiple notices exist, split seeds so each seed carries one notice lifecycle. Inherit `schedule_refs`/`registry_refs` (SCH-*/REG-* keys) and `canonical_label_candidate`/`usable_in_relief` into `asset_alias` unchanged. + 5. Use `domain_review_queue` for missing arrival evidence, uncertain asset identity, unclear possession continuity, conflicting succession scope, or cross-domain defense merge need. When `received_date` cannot be established from selected sources, leave it null and add a `domain_review_queue` item with `issue_type="missing_source"` (do not fabricate); the sealed drafting gate is applied downstream at FL. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Omit unsupported optional values and route unresolved extractable facts to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + Return exactly one JSON object with this sparse schema. Omit optional keys that are unsupported; preserve meaningful false and zero values. + { + "stage_b_domain_bo_seed_delta": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed_delta.v1", + "domain_id": "B5_Succession_Notice_Lien_Asset_Defense", + "status": "READY|READY_WITH_REVIEW|FAILED", + "run_fingerprint": "sha256", + "slice_digest_sha256": "sha256", + "bo_seed_candidates": [ + { + "candidate_ref": "B5:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "Action": "short neutral source-backed draft", + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense" + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B5:R001", + "severity": "SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "candidate_refs": [], + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Part3|Part4|Stage2|human_review" + } + ] + } + } + Optional candidate keys allowed only when supported: JuristicAct, Legal_Keywords, core_field_base, amount, downstream_seed_refs. + Protected domain_payload keys whose source-backed content must be preserved when present: succession_resolution, general_succession_status, asset_specific_succession_resolution, notice_lifecycle, lien_state, possession_state, asset_alias, defense_rebuttal, simultaneous_performance_hints, setoff_or_payment_defense_hints. + Do not emit transport_metadata, transport_candidate_refs, Reason, PriorAct, ReasonRefs, Evidence, EvidenceTitles, BO_ID, id, null-only scaffolding, or empty optional containers. + candidate_ref values must be contiguous in output order with the domain prefix and three-digit ordinal. + Status is FAILED on input guard failure, READY_WITH_REVIEW when the review queue is non-empty, otherwise READY. + + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_R0_seed_reducer_exception_planner + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: "#!/usr/bin/env python3\nfrom __future__ import annotations\n\nimport hashlib\nimport + itertools\nimport json\nimport re\nfrom datetime import datetime, timezone\nfrom + typing import Any\n\nimport httpx\n\nLOCALDOCS_URL = \"http://mcp-localdocs:8012/mcp\"\nMCP_HEADERS + = {\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/json, + text/event-stream\",\n}\nCLIENT = httpx.Client(timeout=60)\nMSG_IDS = itertools.count(10)\nJSON_DECODER + = json.JSONDecoder()\n\ndef _next_id() -> int:\n return next(MSG_IDS)\n\ndef + _parse_json_first(raw: str, label: str, allow_trailing: bool = False) -> Any:\n + \ text = (raw or \"\").lstrip(\"\\ufeff\").strip()\n if not text:\n raise + RuntimeError(f\"{label}: empty JSON text\")\n try:\n return json.loads(text)\n + \ except json.JSONDecodeError as first_error:\n try:\n value, + end = JSON_DECODER.raw_decode(text)\n except json.JSONDecodeError as + second_error:\n raise RuntimeError(f\"{label}: invalid JSON\") from + second_error\n if not allow_trailing and text[end:].strip():\n raise + RuntimeError(f\"{label}: trailing non-JSON data\") from first_error\n return + value\n\ndef _parse_rpc(raw: str) -> dict[str, Any]:\n events: list[dict[str, + Any]] = []\n for line in (raw or \"\").splitlines():\n if not line.startswith(\"data:\"):\n + \ continue\n candidate = line[5:].strip()\n if not candidate + or candidate == \"[DONE]\":\n continue\n try:\n parsed + = json.loads(candidate)\n except json.JSONDecodeError:\n continue\n + \ if isinstance(parsed, dict):\n events.append(parsed)\n if + events:\n return events[-1]\n parsed = _parse_json_first(raw, \"MCP + response\", allow_trailing=True)\n if not isinstance(parsed, dict):\n raise + RuntimeError(\"MCP response root must be object\")\n return parsed\n\ndef + _init(task_name: str) -> None:\n response = CLIENT.post(\n LOCALDOCS_URL,\n + \ json={\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n + \ \"method\": \"initialize\",\n \"params\": {\n \"protocolVersion\": + \"2025-03-26\",\n \"capabilities\": {},\n \"clientInfo\": + {\n \"name\": task_name,\n \"version\": + \"1.0\",\n \"user_id\": \"{{__user_hash__}}\",\n \"workspace_id\": + \"{{__workspace_hash__}}\",\n },\n },\n },\n + \ headers=MCP_HEADERS,\n )\n response.raise_for_status()\n session_id + = response.headers.get(\"mcp-session-id\")\n if session_id:\n MCP_HEADERS[\"mcp-session-id\"] + = session_id\n CLIENT.post(\n LOCALDOCS_URL,\n json={\"jsonrpc\": + \"2.0\", \"method\": \"notifications/initialized\"},\n headers=MCP_HEADERS,\n + \ ).raise_for_status()\n\ndef _call(name: str, arguments: dict[str, Any]) + -> dict[str, Any]:\n response = CLIENT.post(\n LOCALDOCS_URL,\n json={\n + \ \"jsonrpc\": \"2.0\",\n \"id\": _next_id(),\n \"method\": + \"tools/call\",\n \"params\": {\"name\": name, \"arguments\": arguments},\n + \ },\n headers=MCP_HEADERS,\n )\n response.raise_for_status()\n + \ payload = _parse_rpc(response.text)\n if payload.get(\"error\"):\n raise + RuntimeError(f\"MCP {name} failed: {payload['error']}\")\n result = payload.get(\"result\")\n + \ if not isinstance(result, dict):\n raise RuntimeError(f\"MCP {name} + returned no result\")\n if result.get(\"isError\") is True:\n raise + RuntimeError(f\"MCP {name} returned isError\")\n return payload\n\ndef _unwrap(payload: + dict[str, Any], label: str) -> Any:\n result = payload.get(\"result\") or + {}\n content = result.get(\"content\") or []\n texts = [block.get(\"text\") + for block in content if isinstance(block, dict) and block.get(\"text\")]\n if + not texts:\n raise RuntimeError(f\"{label}: empty MCP content\")\n raw + = texts[0]\n try:\n return _parse_json_first(raw, label, allow_trailing=True)\n + \ except RuntimeError:\n return raw\n\ndef _extract_doc(payload: dict[str, + Any], doc_name: str) -> str:\n outer = _unwrap(payload, f\"read_docs:{doc_name}\")\n + \ if isinstance(outer, dict) and isinstance(outer.get(\"results\"), list):\n + \ for item in outer[\"results\"]:\n if not isinstance(item, + dict):\n continue\n name = item.get(\"name\") or item.get(\"path\") + or item.get(\"doc_name\") or item.get(\"document_name\")\n if name + and not (str(name) == doc_name or str(name).endswith(\"/\" + doc_name)):\n continue\n + \ value = item.get(\"content\")\n if value is None:\n value + = item.get(\"text\")\n if value is None:\n value = + item.get(\"raw\")\n if value is not None:\n return + value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)\n + \ if isinstance(outer, dict) and doc_name in outer:\n value = outer[doc_name]\n + \ return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)\n + \ if isinstance(outer, str):\n return outer\n return json.dumps(outer, + ensure_ascii=False)\n\ndef _read_text(doc_name: str) -> str:\n return _extract_doc(_call(\"read_docs\", + {\"doc_names\": [doc_name]}), doc_name)\n\ndef _read_json(doc_name: str) -> + Any:\n raw = _read_text(doc_name)\n return _parse_json_first(raw, doc_name, + allow_trailing=True)\n\ndef _canonical(value: Any) -> str:\n return json.dumps(value, + ensure_ascii=False, sort_keys=True, separators=(\",\", \":\"))\n\ndef _sha(value: + Any) -> str:\n raw = value if isinstance(value, str) else _canonical(value)\n + \ return hashlib.sha256(raw.encode(\"utf-8\")).hexdigest()\n\ndef _write_json_verified(doc_name: + str, value: Any) -> str:\n content = json.dumps(value, ensure_ascii=False, + indent=2)\n _call(\"write_file\", {\"path\": doc_name, \"content\": content, + \"overwrite\": True})\n reread = _read_json(doc_name)\n if _canonical(reread) + != _canonical(value):\n raise RuntimeError(f\"{doc_name}: post-write + verification mismatch\")\n return _sha(content)\n\ndef _as_list(value: Any) + -> list[Any]:\n return value if isinstance(value, list) else []\n\ndef _as_dict(value: + Any) -> dict[str, Any]:\n return value if isinstance(value, dict) else {}\n\ndef + _uniq(values: list[Any]) -> list[Any]:\n seen: set[str] = set()\n output: + list[Any] = []\n for value in values:\n marker = _canonical(value) + if isinstance(value, (dict, list)) else str(value)\n if marker in seen:\n + \ continue\n seen.add(marker)\n output.append(value)\n + \ return output\n\ndef _natural_key(value: Any) -> list[Any]:\n return + [int(part) if part.isdigit() else part for part in re.split(r\"(\\d+)\", str(value))]\n\nTASK_NAME + = \"Task_C_BO_R0_seed_reducer_exception_planner\"\nWORKER_RAWS = {\n \"B1_Money_Successor\": + r\"\"\"{{prev.Task_C_BO_Stage_B_B1_Money_Successor.json_output}}\"\"\",\n \"B2_Secured_Registry\": + r\"\"\"{{prev.Task_C_BO_Stage_B_B2_Secured_Registry.json_output}}\"\"\",\n \"B3_Land_Valuation\": + r\"\"\"{{prev.Task_C_BO_Stage_B_B3_Land_Valuation.json_output}}\"\"\",\n \"B4_Actio\": + r\"\"\"{{prev.Task_C_BO_Stage_B_B4_Actio.json_output}}\"\"\",\n \"B5_Succession_Notice_Lien_Asset_Defense\": + r\"\"\"{{prev.Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense.json_output}}\"\"\",\n}\nSEED_PATHS + = {domain: f\"stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/{domain}.json\" + for domain in WORKER_RAWS}\nACTION_TYPES = {\n \"법률행위(legal acts)\", \"준법률행위(quasi-legal + acts)\", \"사실행위(factual acts)\",\n \"위법행위(unlawful acts)\", \"소송행위(litigation + acts)\",\n}\nDOMAIN_PAYLOAD_DEFAULTS = json.loads(r'''{\"B1_Money_Successor\":{\"money_claim_details\":{\"claim_type_candidate\":null,\"principal_amount_hint\":null,\"interest_hint\":null,\"delay_damage_hint\":null,\"agreed_interest_rate\":null,\"agreed_delay_damage_rate\":null,\"interest_accrual_start_hint\":null,\"interest_rate_source_evidence_refs\":[],\"unpaid_balance_hint\":null,\"creditor_candidate\":null,\"debtor_candidate\":null},\"payment_history\":[],\"demand_or_notice_hints\":[],\"admission_hints\":[],\"successor_liability_hints\":[],\"successor_link_uncertainty\":null},\"B2_Secured_Registry\":{\"secured_debt_chain\":[],\"payment_allocation\":[],\"registry_rows\":[],\"fractional_effect_events\":[],\"mortgage_extinguishment_hints\":[],\"auction_steps\":[],\"dividend_distribution_hints\":[],\"derivative_invalidity_hints\":[]},\"B3_Land_Valuation\":{\"use_gain_facts\":[],\"valuation_object\":{},\"valuation_rows\":[],\"valuation_matrix\":{},\"possession_state\":{},\"delivery_or_possession_start_hints\":[],\"unregistered_building_hints\":[],\"liability_overlap_hints\":[]},\"B4_Actio\":{\"preserved_claim_bundle\":{},\"fraudulent_transfer_facts\":{},\"encumbrance_timeline\":[],\"valuation_matrix\":{},\"balance_sheet\":{},\"remedy_mode_hints\":[],\"value_compensation_cap_hints\":[],\"defense_rebuttal\":{}},\"B5_Succession_Notice_Lien_Asset_Defense\":{\"succession_resolution\":{},\"general_succession_status\":{},\"asset_specific_succession_resolution\":{},\"notice_lifecycle\":{},\"lien_state\":{},\"possession_state\":{},\"asset_alias\":{},\"defense_rebuttal\":{},\"simultaneous_performance_hints\":[],\"setoff_or_payment_defense_hints\":[]}}''')\nCOMMON_DOMAIN_DEFAULTS + = {\n \"title_hint\": None, \"summary_hint\": None, \"conflict_patterns\": + [], \"asset_bindings\": [],\n \"parties_hint\": [], \"temporal_hint\": None, + \"hazard_flags\": [], \"legal_effect_tags\": [],\n \"event_state_type\": + None, \"action_summary\": None, \"time_interval_hint\": None,\n \"asset_binding_hint\": + None, \"mixed_contract_split_hint\": None, \"conflict_pattern_hint\": None,\n + \ \"candidate_details\": {}, \"facts\": {}, \"conflict_flags\": [],\n}\n\ndef + _strict_worker_json(raw: str, label: str) -> dict[str, Any]:\n text = (raw + or \"\").lstrip(\"\uFEFF\").strip()\n fence = re.fullmatch(r\"```(?:json)? + *(.*?) *```\", text, flags=re.DOTALL | re.IGNORECASE)\n if fence:\n text + = fence.group(1).strip()\n value = _parse_json_first(text, label, allow_trailing=False)\n + \ for _ in range(3):\n if isinstance(value, dict) and set(value).issubset({\"success\", + \"output\", \"json_output\"}) and any(key in value for key in (\"output\", \"json_output\")):\n + \ value = value.get(\"json_output\", value.get(\"output\"))\n if + isinstance(value, str):\n value = _parse_json_first(value, label, + allow_trailing=False)\n continue\n if isinstance(value, dict) + and set(value).issubset({\"type\", \"text\"}) and isinstance(value.get(\"text\"), + str):\n value = _parse_json_first(value[\"text\"], label, allow_trailing=False)\n + \ continue\n break\n if not isinstance(value, dict):\n raise + RuntimeError(f\"{label}: root must be object\")\n return value\n\ndef _deep_defaults(default: + Any, supplied: Any) -> Any:\n if isinstance(default, dict):\n incoming + = supplied if isinstance(supplied, dict) else {}\n result = {key: _deep_defaults(value, + incoming[key]) if key in incoming else value for key, value in default.items()}\n + \ for key, value in incoming.items():\n if key not in result:\n + \ result[key] = value\n return result\n return supplied\n\ndef + _source_refs(candidate: dict[str, Any]) -> dict[str, list[str]]:\n provenance + = _as_dict(candidate.get(\"provenance\"))\n return {\n \"source_event_candidate_ids\": + _uniq([str(v) for v in _as_list(provenance.get(\"source_event_candidate_ids\")) + if v]),\n \"source_meeting_clause_ids\": _uniq([str(v) for v in _as_list(provenance.get(\"source_meeting_clause_ids\")) + if v]),\n \"source_evidence_indexes\": _uniq([str(v) for v in _as_list(candidate.get(\"source_evidence_indexes\")) + + _as_list(provenance.get(\"source_evidence_indexes\")) if v]),\n }\n\ndef + _expand_candidate(candidate: dict[str, Any], domain: str, manifest: dict[str, + Any]) -> dict[str, Any]:\n refs = _source_refs(candidate)\n domain_payload + = _as_dict(_as_dict(candidate.get(\"extensions\")).get(\"domain_payload\"))\n + \ defaults = {**COMMON_DOMAIN_DEFAULTS, **DOMAIN_PAYLOAD_DEFAULTS[domain]}\n + \ expanded_payload = _deep_defaults(defaults, domain_payload)\n core = + _as_dict(candidate.get(\"core_field_base\"))\n amount = candidate.get(\"amount\") + if isinstance(candidate.get(\"amount\"), dict) else None\n downstream = _as_dict(candidate.get(\"downstream_seed_refs\"))\n + \ return {\n \"candidate_ref\": candidate[\"candidate_ref\"],\n \"BOType\": + candidate[\"BOType\"],\n \"ActionType\": candidate[\"ActionType\"],\n + \ \"JuristicAct\": candidate.get(\"JuristicAct\") if isinstance(candidate.get(\"JuristicAct\"), + dict) else {\"label\": None},\n \"Action\": candidate[\"Action\"],\n + \ \"Reason\": None,\n \"PriorAct\": None,\n \"ReasonRefs\": + [],\n \"Legal_Keywords\": _as_list(candidate.get(\"Legal_Keywords\")),\n + \ \"core_field_base\": {\n \"date\": core.get(\"date\"), \"amount\": + core.get(\"amount\"), \"parties\": _as_list(core.get(\"parties\")),\n \"asset_refs\": + _as_list(core.get(\"asset_refs\")), \"legal_effect_summary\": core.get(\"legal_effect_summary\"),\n + \ },\n \"amount\": amount,\n \"source_evidence_indexes\": + refs[\"source_evidence_indexes\"],\n \"provenance\": refs,\n \"downstream_seed_refs\": + {\n \"legal_effect_structure_seed\": downstream.get(\"legal_effect_structure_seed\"),\n + \ \"claim_group_seed\": downstream.get(\"claim_group_seed\"),\n \"asset_cluster_seed\": + downstream.get(\"asset_cluster_seed\"),\n \"issue_cluster_seed\": + downstream.get(\"issue_cluster_seed\"),\n },\n \"extensions\": + {\"domain_payload\": expanded_payload},\n \"transport_metadata\": {\n + \ \"stage\": \"Task_C_BO.Stage_B.DomainSeed\", \"source_task\": domain,\n + \ \"run_fingerprint\": manifest[\"run_fingerprint\"],\n \"slice_digest_sha256\": + manifest[\"domain_slice_digests_sha256\"][domain],\n },\n \"transport_candidate_refs\": + refs,\n }\n\ndef _validate_refs(candidate: dict[str, Any], domain: str, manifest: + dict[str, Any]) -> list[str]:\n refs = _source_refs(candidate)\n global_u + = _as_dict(manifest.get(\"source_universe\"))\n domain_u = _as_dict(_as_dict(manifest.get(\"domain_source_universes\")).get(domain))\n + \ failures: list[str] = []\n checks = [\n (\"source_event_candidate_ids\", + \"event_candidate_ids\"),\n (\"source_meeting_clause_ids\", \"meeting_clause_refs\"),\n + \ (\"source_evidence_indexes\", \"evidence_indexes\"),\n ]\n for + ref_key, universe_key in checks:\n global_key = \"meeting_clause_ids\" + if ref_key == \"source_meeting_clause_ids\" else universe_key\n for value + in refs[ref_key]:\n if value not in set(str(v) for v in _as_list(global_u.get(global_key))):\n + \ failures.append(f\"{candidate.get('candidate_ref')}:{ref_key}:outside_global:{value}\")\n + \ if value not in set(str(v) for v in _as_list(domain_u.get(universe_key))):\n + \ failures.append(f\"{candidate.get('candidate_ref')}:{ref_key}:outside_slice:{value}\")\n + \ return failures\n\ndef _substantive_key(candidate: dict[str, Any]) -> str:\n + \ return _sha({key: candidate.get(key) for key in (\"BOType\", \"ActionType\", + \"JuristicAct\", \"Action\", \"core_field_base\", \"amount\", \"downstream_seed_refs\", + \"extensions\")})\n\ndef main() -> None:\n _init(\"task-c-bo-r0-seed-reducer-exception-planner\")\n + \ stage_root = _read_json(\"stage1_tmp/task_c_bo/stage_a_context.json\")\n + \ stage_a = _as_dict(_as_dict(stage_root).get(\"stage_a_context\"))\n manifest + = _read_json(\"stage1_tmp/task_c_bo/source_universe_manifest.json\")\n if + stage_a.get(\"status\") != \"READY\" or manifest.get(\"status\") != \"READY\":\n + \ raise RuntimeError(\"A0 artifacts are not READY\")\n if stage_a.get(\"run_fingerprint\") + != manifest.get(\"run_fingerprint\"):\n raise RuntimeError(\"A0 run fingerprint + mismatch\")\n ledger_candidates: list[dict[str, Any]] = []\n reviews: + list[dict[str, Any]] = []\n failures: list[str] = []\n seed_counts: dict[str, + int] = {}\n for domain, raw in WORKER_RAWS.items():\n parsed = _strict_worker_json(raw, + domain)\n delta = _as_dict(parsed.get(\"stage_b_domain_bo_seed_delta\"))\n + \ if delta.get(\"schema_version\") != \"task_c_bo_stage_b_domain_bo_seed_delta.v1\" + or delta.get(\"domain_id\") != domain:\n failures.append(f\"{domain}:delta + schema/domain mismatch\")\n continue\n if delta.get(\"status\") + == \"FAILED\":\n failures.append(f\"{domain}:worker FAILED\")\n if + delta.get(\"run_fingerprint\") != manifest.get(\"run_fingerprint\"):\n failures.append(f\"{domain}:run + fingerprint mismatch\")\n if delta.get(\"slice_digest_sha256\") != manifest.get(\"domain_slice_digests_sha256\", + {}).get(domain):\n failures.append(f\"{domain}:slice digest mismatch\")\n + \ candidates = _as_list(delta.get(\"bo_seed_candidates\"))\n prefix + = domain.split(\"_\", 1)[0]\n expected_refs = [f\"{prefix}:{index:03d}\" + for index in range(1, len(candidates) + 1)]\n actual_refs = [str(_as_dict(candidate).get(\"candidate_ref\") + or \"\") for candidate in candidates]\n if actual_refs != expected_refs:\n + \ failures.append(f\"{domain}:candidate_ref sequence mismatch\")\n + \ expanded: list[dict[str, Any]] = []\n for candidate in candidates:\n + \ if not isinstance(candidate, dict):\n failures.append(f\"{domain}:non-object + candidate\")\n continue\n forbidden_keys = {\"transport_metadata\", + \"transport_candidate_refs\", \"Reason\", \"PriorAct\", \"ReasonRefs\", \"Evidence\", + \"EvidenceTitles\", \"BO_ID\", \"id\"}\n present_forbidden = sorted(forbidden_keys.intersection(candidate))\n + \ if present_forbidden:\n failures.append(f\"{candidate.get('candidate_ref')}:forbidden + sparse keys:{present_forbidden}\")\n continue\n if + candidate.get(\"BOType\") not in {\"event\", \"state\"} or candidate.get(\"ActionType\") + not in ACTION_TYPES or not str(candidate.get(\"Action\") or \"\").strip():\n + \ failures.append(f\"{candidate.get('candidate_ref')}:required + enum/action invalid\")\n continue\n failures.extend(_validate_refs(candidate, + domain, manifest))\n seed = _expand_candidate(candidate, domain, + manifest)\n expanded.append(seed)\n ledger_candidates.append({\"candidate_ref\": + seed[\"candidate_ref\"], \"source_domain\": domain, \"seed\": seed})\n reviews.extend([dict(item, + source_domain=domain) for item in _as_list(delta.get(\"domain_review_queue\")) + if isinstance(item, dict)])\n artifact = {\"stage_b_domain_bo_seed_output\": + {\n \"schema_version\": \"task_c_bo_stage_b_domain_bo_seed.v2\", + \"domain_id\": domain,\n \"status\": \"READY_WITH_REVIEW\" if delta.get(\"domain_review_queue\") + else \"READY\",\n \"run_fingerprint\": manifest[\"run_fingerprint\"], + \"bo_seed_candidates\": expanded,\n \"domain_review_queue\": _as_list(delta.get(\"domain_review_queue\")),\n + \ }}\n _write_json_verified(SEED_PATHS[domain], artifact)\n seed_counts[domain] + = len(expanded)\n\n deterministic: list[dict[str, Any]] = []\n merged_refs: + set[str] = set()\n by_key: dict[str, list[dict[str, Any]]] = {}\n for + item in ledger_candidates:\n by_key.setdefault(_substantive_key(item[\"seed\"]), + []).append(item)\n for group in by_key.values():\n if len(group) < + 2:\n continue\n survivor = group[0][\"candidate_ref\"]\n absorbed + = [item[\"candidate_ref\"] for item in group[1:]]\n merged_refs.update(absorbed)\n + \ deterministic.append({\"decision\": \"MERGE_PROVENANCE_UNION\", \"survivor_ref\": + survivor, \"absorbed_refs\": absorbed})\n for item in ledger_candidates:\n + \ seed = item[\"seed\"]\n refs = _source_refs(seed)\n if + refs[\"source_meeting_clause_ids\"] and not refs[\"source_evidence_indexes\"] + and not refs[\"source_event_candidate_ids\"]:\n reviews.append({\n + \ \"review_id\": f\"R0:{item['candidate_ref']}:MEETING_ONLY\", + \"severity\": \"SOFT_WARNING\",\n \"issue_type\": \"MEETING_ONLY_EVIDENCE_GAP\", + \"candidate_refs\": [item[\"candidate_ref\"]],\n **refs, \"downstream_owner\": + \"human_review\", \"source_domain\": item[\"source_domain\"],\n })\n + \ for left_index, left in enumerate(ledger_candidates):\n for right + in ledger_candidates[left_index + 1:]:\n if left[\"candidate_ref\"] + in merged_refs or right[\"candidate_ref\"] in merged_refs:\n continue\n + \ left_refs = set(_source_refs(left[\"seed\"])[\"source_evidence_indexes\"])\n + \ right_refs = set(_source_refs(right[\"seed\"])[\"source_evidence_indexes\"])\n + \ if left_refs.intersection(right_refs) and left[\"seed\"][\"ActionType\"] + == right[\"seed\"][\"ActionType\"]:\n deterministic.append({\"decision\": + \"KEEP_SEPARATE\", \"cluster_id\": f\"NEAR-{len(deterministic)+1:03d}\", \"candidate_refs\": + [left[\"candidate_ref\"], right[\"candidate_ref\"]]})\n\n packs: list[dict[str, + Any]] = []\n exception_number = 1\n for review in reviews:\n candidate_refs + = [str(value) for value in _as_list(review.get(\"candidate_refs\"))]\n if + review.get(\"issue_type\") != \"cross_domain_merge_needed\" or len(candidate_refs) + < 2:\n continue\n candidates = [item for item in ledger_candidates + if item[\"candidate_ref\"] in candidate_refs][:8]\n exception_id = f\"P2-EXC-{exception_number:03d}\"\n + \ pack_id = f\"P2-PACK-{len(packs)+1:03d}\"\n pack = {\n \"pack_id\": + pack_id, \"exception_type\": \"canonicalization_conflict\", \"exception_ids\": + [exception_id],\n \"items\": [{\n \"exception_id\": + exception_id, \"exception_type\": \"canonicalization_conflict\",\n \"candidate_refs\": + candidate_refs[:8], \"conflicting_field\": \"cross_domain_identity\",\n \"allowed_values\": + [],\n \"compact_candidates\": [{\n \"candidate_ref\": + item[\"candidate_ref\"], \"source_domain\": item[\"source_domain\"],\n \"relevant_fields\": + {key: item[\"seed\"].get(key) for key in (\"BOType\", \"ActionType\", \"Action\")},\n + \ \"source_refs\": _source_refs(item[\"seed\"]),\n } + for item in candidates],\n \"allowed_decisions\": [\"KEEP_SEPARATE\", + \"MERGE_PROVENANCE_UNION\", \"SPLIT\", \"BLOCK_REVIEW\"],\n }],\n + \ \"output_path\": f\"quality_gates/stage1_part2_adjudication_parts/PACK-{len(packs)+1:03d}.json\",\n + \ }\n if len(_canonical(pack).encode(\"utf-8\")) > 20000:\n reviews.append({\"review_id\": + f\"R0:BUDGET:{exception_number:03d}\", \"severity\": \"BLOCK\", \"issue_type\": + \"BUDGET_OVERFLOW_BLOCK_REVIEW\", \"candidate_refs\": candidate_refs, \"downstream_owner\": + \"human_review\"})\n elif len(packs) < 8:\n packs.append(pack)\n + \ exception_number += 1\n\n status = \"BLOCKED\" if failures or any(item.get(\"severity\") + == \"BLOCK\" for item in reviews) else (\"READY_WITH_REVIEW\" if reviews else + \"READY\")\n all_refs = [item[\"candidate_ref\"] for item in ledger_candidates]\n + \ ledger = {\"postb_seed_ledger\": {\n \"schema_version\": \"task_c_bo_postb_seed_ledger.v2\", + \"status\": status,\n \"run_fingerprint\": manifest[\"run_fingerprint\"], + \"source_universe\": manifest[\"source_universe\"],\n \"ledger_candidates\": + ledger_candidates,\n \"candidate_index\": {item[\"candidate_ref\"]: {\"source_domain\": + item[\"source_domain\"], \"ordinal\": index} for index, item in enumerate(ledger_candidates)},\n + \ \"domain_review_queue\": reviews, \"deterministic_decisions\": deterministic,\n + \ \"exception_summary\": {\"pack_count\": len(packs), \"exception_count\": + sum(len(pack[\"items\"]) for pack in packs)},\n \"audit_trace\": {\"failures\": + failures, \"seed_counts\": seed_counts},\n \"conservation\": {\"input_candidate_refs\": + all_refs, \"input_count\": len(all_refs), \"unique_count\": len(set(all_refs))},\n + \ }}\n handoff = {\n \"schema_version\": \"stage1_part2_review_handoff.v1\", + \"status\": status,\n \"run_fingerprint\": manifest[\"run_fingerprint\"], + \"review_count\": len(reviews), \"items\": reviews,\n }\n exception_pack + = {\n \"schema_version\": \"stage1_part2_exception_pack.v1\", \"status\": + status,\n \"run_fingerprint\": manifest[\"run_fingerprint\"], \"pack_count\": + len(packs), \"packs\": packs,\n }\n _write_json_verified(\"stage1_tmp/task_c_bo/postb_seed_ledger.json\", + ledger)\n _write_json_verified(\"quality_gates/stage1_part2_review_handoff.json\", + handoff)\n _write_json_verified(\"quality_gates/stage1_part2_exception_pack.json\", + exception_pack)\n output = {\"r0_manifest\": {\n \"schema_version\": + \"task_c_bo_r0_manifest.v1\", \"status\": status,\n \"run_fingerprint\": + manifest[\"run_fingerprint\"], \"seed_ledger_path\": \"stage1_tmp/task_c_bo/postb_seed_ledger.json\",\n + \ \"review_handoff_path\": \"quality_gates/stage1_part2_review_handoff.json\",\n + \ \"exception_pack_path\": \"quality_gates/stage1_part2_exception_pack.json\",\n + \ \"candidate_count\": len(ledger_candidates), \"review_count\": len(reviews), + \"pack_count\": len(packs),\n }, \"dynamic_fanout\": packs if status != \"BLOCKED\" + else []}\n print(json.dumps(output, ensure_ascii=False, separators=(\",\", + \":\")))\n if status == \"BLOCKED\":\n raise RuntimeError(\"R0 blocked + by schema, source, or budget findings\")\n\nif __name__ == \"__main__\":\n try:\n + \ main()\n except Exception as exc:\n print(json.dumps({\"r0_manifest\": + {\"schema_version\": \"task_c_bo_r0_manifest.v1\", \"status\": \"FAILED\", \"error\": + str(exc)}, \"dynamic_fanout\": []}, ensure_ascii=False))\n raise\n" + - task_name: Task_C_BO_R1_exception_adjudicator_* + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: medium + llm_verbosity: low + max_iterations: 2 + max_concurrency: 2 + cache_control: + mode: auto + ttl: 1h + use_tools: + - localdocs + preflight: false + prompts: + - role: user + content: | + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + You are the conditional, compact exception adjudicator for Stage 1 Part 2. + Use only the item fields below. Do not read any Stage A file, ledger, BO, domain seed, meeting, event, or evidence file. + + pack_id: {{item.pack_id}} + exception_type: {{item.exception_type}} + exception_ids: {{item.exception_ids}} + items: {{item.items}} + output_path: {{item.output_path}} + + For every exception return exactly one decision drawn both from the item's allowed_decisions and this closed enum: + KEEP_SEPARATE, MERGE_PROVENANCE_UNION, SPLIT, DROP_UNSUPPORTED, SELECT_VALUE, SELECT_LINK, NO_LINK, BLOCK_REVIEW. + Never invent a candidate_ref, source ref, exception id, value, or fact. SELECT_VALUE must use allowed_values. When compact data is insufficient, use BLOCK_REVIEW. + Write exactly one JSON file to {{item.output_path}} with localdocs write_file and overwrite=true. Do not write another file. + The file schema is: + { + "schema_version": "stage1_part2_adjudication_part.v1", + "status": "READY|BLOCKED", + "pack_id": "{{item.pack_id}}", + "exception_type": "{{item.exception_type}}", + "decisions": [ + { + "exception_id": "from item", + "decision": "closed enum", + "candidate_refs": [], + "selected_value": null, + "selected_link": null, + "reason_code": "SOURCE_SUPPORT|IDENTITY_MATCH|NO_SAFE_RESOLUTION|INSUFFICIENT_COMPACT_CONTEXT" + } + ] + } + Return only a compact raw JSON object containing pack_id, status, output_path, and decision_count after the write succeeds. + + - task_name: Task_C_BO_F0_final_bo_gate_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_IDS = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + def _next_id() -> int: + return next(MSG_IDS) + + def _parse_json_first(raw: str, label: str, allow_trailing: bool = False) -> Any: + text = (raw or "").lstrip("\ufeff").strip() + if not text: + raise RuntimeError(f"{label}: empty JSON text") + try: + return json.loads(text) + except json.JSONDecodeError as first_error: + try: + value, end = JSON_DECODER.raw_decode(text) + except json.JSONDecodeError as second_error: + raise RuntimeError(f"{label}: invalid JSON") from second_error + if not allow_trailing and text[end:].strip(): + raise RuntimeError(f"{label}: trailing non-JSON data") from first_error + return value + + def _parse_rpc(raw: str) -> dict[str, Any]: + events: list[dict[str, Any]] = [] + for line in (raw or "").splitlines(): + if not line.startswith("data:"): + continue + candidate = line[5:].strip() + if not candidate or candidate == "[DONE]": + continue + try: + parsed = json.loads(candidate) + except json.JSONDecodeError: + continue + if isinstance(parsed, dict): + events.append(parsed) + if events: + return events[-1] + parsed = _parse_json_first(raw, "MCP response", allow_trailing=True) + if not isinstance(parsed, dict): + raise RuntimeError("MCP response root must be object") + return parsed + + def _init(task_name: str) -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": task_name, + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _call(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_rpc(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP {name} failed: {payload['error']}") + result = payload.get("result") + if not isinstance(result, dict): + raise RuntimeError(f"MCP {name} returned no result") + if result.get("isError") is True: + raise RuntimeError(f"MCP {name} returned isError") + return payload + + def _unwrap(payload: dict[str, Any], label: str) -> Any: + result = payload.get("result") or {} + content = result.get("content") or [] + texts = [block.get("text") for block in content if isinstance(block, dict) and block.get("text")] + if not texts: + raise RuntimeError(f"{label}: empty MCP content") + raw = texts[0] + try: + return _parse_json_first(raw, label, allow_trailing=True) + except RuntimeError: + return raw + + def _extract_doc(payload: dict[str, Any], doc_name: str) -> str: + outer = _unwrap(payload, f"read_docs:{doc_name}") + if isinstance(outer, dict) and isinstance(outer.get("results"), list): + for item in outer["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + if name and not (str(name) == doc_name or str(name).endswith("/" + doc_name)): + continue + value = item.get("content") + if value is None: + value = item.get("text") + if value is None: + value = item.get("raw") + if value is not None: + return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False) + if isinstance(outer, dict) and doc_name in outer: + value = outer[doc_name] + return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False) + if isinstance(outer, str): + return outer + return json.dumps(outer, ensure_ascii=False) + + def _read_text(doc_name: str) -> str: + return _extract_doc(_call("read_docs", {"doc_names": [doc_name]}), doc_name) + + def _read_json(doc_name: str) -> Any: + raw = _read_text(doc_name) + return _parse_json_first(raw, doc_name, allow_trailing=True) + + def _canonical(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + def _sha(value: Any) -> str: + raw = value if isinstance(value, str) else _canonical(value) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _write_json_verified(doc_name: str, value: Any) -> str: + content = json.dumps(value, ensure_ascii=False, indent=2) + _call("write_file", {"path": doc_name, "content": content, "overwrite": True}) + reread = _read_json(doc_name) + if _canonical(reread) != _canonical(value): + raise RuntimeError(f"{doc_name}: post-write verification mismatch") + return _sha(content) + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _uniq(values: list[Any]) -> list[Any]: + seen: set[str] = set() + output: list[Any] = [] + for value in values: + marker = _canonical(value) if isinstance(value, (dict, list)) else str(value) + if marker in seen: + continue + seen.add(marker) + output.append(value) + return output + + def _natural_key(value: Any) -> list[Any]: + return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", str(value))] + + TASK_NAME = "Task_C_BO_F0_final_bo_gate_writer" + R0_RAW = r"""{{prev.Task_C_BO_R0_seed_reducer_exception_planner}}""" + BO_KEYS = ["BO_ID", "id", "BOType", "ActionType", "JuristicAct", "Action", "Reason", "PriorAct", "ReasonRefs", "Legal_Keywords", "core_field_base", "amount", "EvidenceTitles", "Evidence", "source_evidence_indexes", "provenance", "downstream_seed_refs", "extensions"] + CORE_KEYS = ["Performer", "PerformerType", "Action_proposal", "Subject", "Object", "BehaviorTime", "TimeText", "TimePrecision", "StatementType", "Perspective"] + DOWNSTREAM_KEYS = ["claim_group_seed_refs_proposed", "canonical_theory_graph_seed_ref_proposed", "legal_effect_structure_seed_ref_proposed"] + EVIDENCE_KEYS = ["evidence_index", "source_title", "priority_class", "relevant_content", "authentication_status", "corroboration", "selection_basis"] + + def _r0_manifest() -> dict[str, Any]: + parsed = _parse_json_first(R0_RAW, "R0_RAW", allow_trailing=False) + if not isinstance(parsed, dict) or not isinstance(parsed.get("r0_manifest"), dict): + raise RuntimeError("R0 stdout manifest missing") + return parsed["r0_manifest"] + + def _decision_parts(exception_pack: dict[str, Any]) -> dict[str, dict[str, Any]]: + output: dict[str, dict[str, Any]] = {} + for pack in _as_list(exception_pack.get("packs")): + path = str(_as_dict(pack).get("output_path") or "") + part = _read_json(path) + if not isinstance(part, dict) or part.get("schema_version") != "stage1_part2_adjudication_part.v1": + raise RuntimeError(f"invalid adjudication part: {path}") + pack_id = str(pack.get("pack_id")) + if part.get("pack_id") != pack_id or pack_id in output: + raise RuntimeError(f"adjudication pack coverage mismatch: {pack_id}") + expected = set(str(v) for v in _as_list(pack.get("exception_ids"))) + actual = set(str(_as_dict(v).get("exception_id")) for v in _as_list(part.get("decisions"))) + if actual != expected: + raise RuntimeError(f"adjudication exception coverage mismatch: {pack_id}") + output[pack_id] = part + return output + + def _merge_refs(target: dict[str, Any], source: dict[str, Any]) -> None: + for key in ("source_event_candidate_ids", "source_meeting_clause_ids", "source_evidence_indexes"): + target["provenance"][key] = _uniq(_as_list(target["provenance"].get(key)) + _as_list(source["provenance"].get(key))) + target["source_evidence_indexes"] = _uniq(_as_list(target.get("source_evidence_indexes")) + _as_list(source.get("source_evidence_indexes"))) + + def _apply_decisions(ledger: dict[str, Any], parts: dict[str, dict[str, Any]]) -> tuple[list[dict[str, Any]], dict[str, Any]]: + candidates = {item["candidate_ref"]: {**item, "seed": json.loads(json.dumps(item["seed"], ensure_ascii=False))} for item in _as_list(ledger.get("ledger_candidates"))} + removed: set[str] = set() + blocked: set[str] = set() + merged: set[str] = set() + for decision in _as_list(ledger.get("deterministic_decisions")): + if decision.get("decision") != "MERGE_PROVENANCE_UNION": + continue + survivor = str(decision.get("survivor_ref")) + if survivor not in candidates: + raise RuntimeError("deterministic merge survivor missing") + for absorbed in _as_list(decision.get("absorbed_refs")): + absorbed = str(absorbed) + if absorbed not in candidates: + raise RuntimeError("deterministic merge absorbed ref missing") + _merge_refs(candidates[survivor]["seed"], candidates[absorbed]["seed"]) + removed.add(absorbed) + merged.add(absorbed) + for part in parts.values(): + for decision in _as_list(part.get("decisions")): + action = decision.get("decision") + refs = [str(v) for v in _as_list(decision.get("candidate_refs"))] + if any(ref not in candidates for ref in refs): + raise RuntimeError("foreign candidate ref in adjudication") + if action == "MERGE_PROVENANCE_UNION" and len(refs) >= 2: + for absorbed in refs[1:]: + _merge_refs(candidates[refs[0]]["seed"], candidates[absorbed]["seed"]) + removed.add(absorbed) + merged.add(absorbed) + elif action == "DROP_UNSUPPORTED": + for ref in refs: + if _as_list(candidates[ref]["seed"].get("source_evidence_indexes")): + raise RuntimeError("source-backed candidate cannot be dropped") + removed.add(ref) + elif action == "SELECT_VALUE": + for ref in refs: + candidates[ref]["seed"]["ActionType"] = decision.get("selected_value") + elif action == "BLOCK_REVIEW": + blocked.update(refs) + elif action not in {"KEEP_SEPARATE", "SPLIT", "SELECT_LINK", "NO_LINK"}: + raise RuntimeError(f"unsupported adjudication decision: {action}") + final_items = [item for ref, item in candidates.items() if ref not in removed and ref not in blocked] + audit = {"input_count": len(candidates), "final_candidate_count": len(final_items), "merged_refs": sorted(merged), "blocked_refs": sorted(blocked), "removed_refs": sorted(removed)} + if audit["input_count"] != audit["final_candidate_count"] + len(set(audit["merged_refs"] + audit["blocked_refs"] + audit["removed_refs"])): + raise RuntimeError("candidate conservation failed") + return final_items, audit + + def _evidence_item(index: str, evidence_map: dict[str, Any]) -> dict[str, Any]: + record = _as_dict(_as_dict(evidence_map.get("by_evidence_index")).get(index)) + return { + "evidence_index": index, "source_title": record.get("title"), "priority_class": record.get("priority_class"), + "relevant_content": record.get("relevant_content"), "authentication_status": record.get("authentication_status"), + "corroboration": record.get("corroboration"), "selection_basis": "source_evidence_indexes membership", + } + + def _final_bo(item: dict[str, Any], ordinal: int, evidence_map: dict[str, Any]) -> dict[str, Any]: + seed = item["seed"] + seed_core = _as_dict(seed.get("core_field_base")) + parties = _as_list(seed_core.get("parties")) + assets = _as_list(seed_core.get("asset_refs")) + source_indexes = sorted(_uniq([str(v) for v in _as_list(seed.get("source_evidence_indexes"))]), key=_natural_key) + evidence = [_evidence_item(index, evidence_map) for index in source_indexes] + amount = _as_dict(seed.get("amount")) + value = amount.get("value") if "value" in amount else amount.get("numeric_value") + value_text = amount.get("basis") if amount.get("basis") is not None else amount.get("value_text") + downstream = _as_dict(seed.get("downstream_seed_refs")) + bo_id = f"bh{ordinal}" + provenance = _as_dict(seed.get("provenance")) + return { + "BO_ID": bo_id, "id": bo_id, "BOType": seed.get("BOType"), "ActionType": seed.get("ActionType"), + "JuristicAct": seed.get("JuristicAct") if isinstance(seed.get("JuristicAct"), dict) else {"label": None}, + "Action": seed.get("Action"), "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": None, "ReasonRefs": [], "Legal_Keywords": _as_list(seed.get("Legal_Keywords")), + "core_field_base": { + "Performer": parties[0] if parties else None, "PerformerType": None, "Action_proposal": seed.get("Action"), + "Subject": parties[1] if len(parties) > 1 else None, "Object": assets[0] if assets else None, + "BehaviorTime": seed_core.get("date"), "TimeText": seed_core.get("time_text"), + "TimePrecision": seed_core.get("time_precision"), "StatementType": seed.get("BOType"), "Perspective": "plaintiff", + }, + "amount": {"value_text": value_text, "numeric_value": value, "currency": amount.get("currency") or "KRW"}, + "EvidenceTitles": _uniq([entry.get("source_title") for entry in evidence if entry.get("source_title")]), + "Evidence": evidence, "source_evidence_indexes": source_indexes, + "provenance": { + "source_event_candidate_ids": _uniq(_as_list(provenance.get("source_event_candidate_ids"))), + "source_meeting_clause_ids": _uniq(_as_list(provenance.get("source_meeting_clause_ids"))), + "source_domain": item.get("source_domain"), + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": _as_list(downstream.get("claim_group_seed")), + "canonical_theory_graph_seed_ref_proposed": downstream.get("issue_cluster_seed"), + "legal_effect_structure_seed_ref_proposed": downstream.get("legal_effect_structure_seed"), + }, + "extensions": seed.get("extensions") if isinstance(seed.get("extensions"), dict) else {"domain_payload": {}}, + } + + def _validate_bo(bo_list: list[dict[str, Any]], source_universe: set[str]) -> None: + for index, bo in enumerate(bo_list, 1): + if list(bo) != BO_KEYS or bo.get("BO_ID") != f"bh{index}" or bo.get("id") != f"bh{index}": + raise RuntimeError(f"BO schema/id mismatch at ordinal {index}") + if list(_as_dict(bo.get("core_field_base"))) != CORE_KEYS: + raise RuntimeError(f"BO core schema mismatch at ordinal {index}") + if list(_as_dict(bo.get("downstream_seed_refs"))) != DOWNSTREAM_KEYS: + raise RuntimeError(f"BO downstream schema mismatch at ordinal {index}") + if any(list(_as_dict(item)) != EVIDENCE_KEYS for item in _as_list(bo.get("Evidence"))): + raise RuntimeError(f"BO Evidence schema mismatch at ordinal {index}") + if any(value not in source_universe for value in _as_list(bo.get("source_evidence_indexes"))): + raise RuntimeError(f"BO source membership mismatch at ordinal {index}") + + def main() -> None: + _init("task-c-bo-f0-final-bo-gate-writer") + r0 = _r0_manifest() + stage_a = _as_dict(_as_dict(_read_json("stage1_tmp/task_c_bo/stage_a_context.json")).get("stage_a_context")) + manifest = _read_json("stage1_tmp/task_c_bo/source_universe_manifest.json") + ledger = _as_dict(_as_dict(_read_json("stage1_tmp/task_c_bo/postb_seed_ledger.json")).get("postb_seed_ledger")) + exception_pack = _read_json("quality_gates/stage1_part2_exception_pack.json") + fingerprints = {stage_a.get("run_fingerprint"), manifest.get("run_fingerprint"), ledger.get("run_fingerprint"), exception_pack.get("run_fingerprint"), r0.get("run_fingerprint")} + if len(fingerprints) != 1 or None in fingerprints: + raise RuntimeError("F0 run fingerprint mismatch") + if ledger.get("status") not in {"READY", "READY_WITH_REVIEW"} or r0.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise RuntimeError("R0 ledger is not finalizable") + if r0.get("pack_count") != exception_pack.get("pack_count"): + raise RuntimeError("R0 pack count mismatch") + parts = _decision_parts(exception_pack) + items, conservation = _apply_decisions(ledger, parts) + items.sort(key=lambda item: (_natural_key(item.get("source_domain")), _natural_key(item.get("candidate_ref")))) + evidence_map = _as_dict(stage_a.get("evidence_authority_map")) + bo_list = [_final_bo(item, ordinal, evidence_map) for ordinal, item in enumerate(items, 1)] + source_universe = set(str(v) for v in _as_list(_as_dict(manifest.get("source_universe")).get("evidence_indexes"))) + _validate_bo(bo_list, source_universe) + bo_text = json.dumps(bo_list, ensure_ascii=False, indent=2) + bo_digest = _sha(bo_text) + compiled = {"postb_compiled_bo_bundle": { + "schema_version": "task_c_bo_postb_compiled_bo_bundle.v1", "status": "READY", + "run_fingerprint": manifest["run_fingerprint"], "bo_count": len(bo_list), "behavior_objects": bo_list, + "candidate_conservation": conservation, + }} + _write_json_verified("stage1_tmp/task_c_bo/postb_compiled_bo_bundle.json", compiled) + _write_json_verified("BO.json", bo_list) + reread_bo = _read_json("BO.json") + _validate_bo(reread_bo, source_universe) + if len(reread_bo) != len(bo_list): + raise RuntimeError("BO post-write count mismatch") + gate = { + "schema_version": "stage1_part2_bo_gate.v1", "status": "PASS", + "run_fingerprint": manifest["run_fingerprint"], "bo_count": len(bo_list), + "candidate_conservation": conservation, + "source_membership": {"status": "PASS", "universe_count": len(source_universe)}, + "adjudication_coverage": {"expected_pack_count": exception_pack.get("pack_count"), "actual_pack_count": len(parts)}, + "schema_gate": {"status": "PASS", "bo_keys": BO_KEYS}, + "post_write": {"file_exists": True, "json_parse_pass": True, "sha256": bo_digest}, + "findings": [], + } + _write_json_verified("quality_gates/stage1_part2_bo_gate.json", gate) + print(json.dumps({"bo_write_report": {"schema_version": "stage1_part2_bo_write_report.v1", "status": "PASS", "run_fingerprint": manifest["run_fingerprint"], "bo_count": len(bo_list), "bo_sha256": bo_digest}}, ensure_ascii=False, separators=(",", ":"))) + + if __name__ == "__main__": + try: + main() + except Exception as exc: + print(json.dumps({"bo_write_report": {"schema_version": "stage1_part2_bo_write_report.v1", "status": "FAILED", "error": str(exc)}}, ensure_ascii=False)) + raise + - task_name: Task_C_BO_S0_signal_bundle_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_IDS = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + def _next_id() -> int: + return next(MSG_IDS) + + def _parse_json_first(raw: str, label: str, allow_trailing: bool = False) -> Any: + text = (raw or "").lstrip("\ufeff").strip() + if not text: + raise RuntimeError(f"{label}: empty JSON text") + try: + return json.loads(text) + except json.JSONDecodeError as first_error: + try: + value, end = JSON_DECODER.raw_decode(text) + except json.JSONDecodeError as second_error: + raise RuntimeError(f"{label}: invalid JSON") from second_error + if not allow_trailing and text[end:].strip(): + raise RuntimeError(f"{label}: trailing non-JSON data") from first_error + return value + + def _parse_rpc(raw: str) -> dict[str, Any]: + events: list[dict[str, Any]] = [] + for line in (raw or "").splitlines(): + if not line.startswith("data:"): + continue + candidate = line[5:].strip() + if not candidate or candidate == "[DONE]": + continue + try: + parsed = json.loads(candidate) + except json.JSONDecodeError: + continue + if isinstance(parsed, dict): + events.append(parsed) + if events: + return events[-1] + parsed = _parse_json_first(raw, "MCP response", allow_trailing=True) + if not isinstance(parsed, dict): + raise RuntimeError("MCP response root must be object") + return parsed + + def _init(task_name: str) -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": task_name, + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _call(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_rpc(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP {name} failed: {payload['error']}") + result = payload.get("result") + if not isinstance(result, dict): + raise RuntimeError(f"MCP {name} returned no result") + if result.get("isError") is True: + raise RuntimeError(f"MCP {name} returned isError") + return payload + + def _unwrap(payload: dict[str, Any], label: str) -> Any: + result = payload.get("result") or {} + content = result.get("content") or [] + texts = [block.get("text") for block in content if isinstance(block, dict) and block.get("text")] + if not texts: + raise RuntimeError(f"{label}: empty MCP content") + raw = texts[0] + try: + return _parse_json_first(raw, label, allow_trailing=True) + except RuntimeError: + return raw + + def _extract_doc(payload: dict[str, Any], doc_name: str) -> str: + outer = _unwrap(payload, f"read_docs:{doc_name}") + if isinstance(outer, dict) and isinstance(outer.get("results"), list): + for item in outer["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + if name and not (str(name) == doc_name or str(name).endswith("/" + doc_name)): + continue + value = item.get("content") + if value is None: + value = item.get("text") + if value is None: + value = item.get("raw") + if value is not None: + return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False) + if isinstance(outer, dict) and doc_name in outer: + value = outer[doc_name] + return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False) + if isinstance(outer, str): + return outer + return json.dumps(outer, ensure_ascii=False) + + def _read_text(doc_name: str) -> str: + return _extract_doc(_call("read_docs", {"doc_names": [doc_name]}), doc_name) + + def _read_json(doc_name: str) -> Any: + raw = _read_text(doc_name) + return _parse_json_first(raw, doc_name, allow_trailing=True) + + def _canonical(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + def _sha(value: Any) -> str: + raw = value if isinstance(value, str) else _canonical(value) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _write_json_verified(doc_name: str, value: Any) -> str: + content = json.dumps(value, ensure_ascii=False, indent=2) + _call("write_file", {"path": doc_name, "content": content, "overwrite": True}) + reread = _read_json(doc_name) + if _canonical(reread) != _canonical(value): + raise RuntimeError(f"{doc_name}: post-write verification mismatch") + return _sha(content) + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _uniq(values: list[Any]) -> list[Any]: + seen: set[str] = set() + output: list[Any] = [] + for value in values: + marker = _canonical(value) if isinstance(value, (dict, list)) else str(value) + if marker in seen: + continue + seen.add(marker) + output.append(value) + return output + + def _natural_key(value: Any) -> list[Any]: + return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", str(value))] + + TASK_NAME = "Task_C_BO_S0_signal_bundle_writer" + LIABILITY_TYPES = { + "B1_Money_Successor": "money_claim", "B2_Secured_Registry": "secured_debt", + "B3_Land_Valuation": "land_use_gain", "B4_Actio": "actio_related", + "B5_Succession_Notice_Lien_Asset_Defense": "notice_or_lien_related", + } + LIABILITY_ENUM = {"money_claim", "commercial_successor", "secured_debt", "registry_invalidity", "land_use_gain", "actio_related", "succession_related", "notice_or_lien_related", "defense_or_setoff", "other"} + ACTIO_SCOPES = {"preserved_claim", "fraudulent_act", "beneficiary_or_transferee", "target_asset", "encumbrance", "remedy_or_cap", "defense"} + SUSPICION_LEVELS = {"NONE", "LOW", "MEDIUM", "HIGH", "CRITICAL"} + ACTIO_SCOPE_ENUM = ACTIO_SCOPES + ROLE_ENUM = {"fraudulent_act_date", "debtor_act", "beneficiary", "transferee", "remedy_or_cap", "defense", "encumbrance"} + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def _first_string(*values: Any) -> str | None: + for value in values: + for item in _strings(value): + return item + return None + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + + def _flatten_strings(value: Any, limit: int = 10) -> list[str]: + out: list[str] = [] + + def visit(node: Any) -> None: + if len(out) >= limit: + return + if node is None: + return + if isinstance(node, (str, int, float, bool)): + text = str(node).strip() + if text and text not in out: + out.append(text) + return + if isinstance(node, list): + for child in node: + visit(child) + return + if isinstance(node, dict): + for key in ( + "asset_label", "asset", "property", "object", "Object", "description", + "value", "name", "party", "beneficiary", "transferee", "debtor_act", + "remedy", "defense", "encumbrance", "date", "claim_type", + ): + if key in node: + visit(node.get(key)) + visit(value) + return out[:limit] + + def _asset_kind_from_text(text: str) -> str: + lowered = text.lower() + if any(key in text for key in ["부동산", "토지", "건물", "아파트", "대지", "등기", "근저당"]): + return "real_property" + if any(key in text for key in ["채권", "대여금", "매매대금", "임대차보증금"]): + return "claim" + if any(key in text for key in ["근저당", "담보", "저당", "질권", "유치권"]): + return "security_right" + if any(key in text for key in ["예금", "현금", "계좌", "deposit", "cash"]) or "bank" in lowered: + return "cash_or_deposit" + if any(key in text for key in ["영업", "사업", "상호", "business"]) or "company" in lowered: + return "business_asset" + if any(key in text for key in ["동산", "차량", "기계", "movable"]): + return "movable" + return "unknown" + + def _target_property_candidates(payload: dict[str, Any], bo_ids: list[str], evidence: list[str]) -> list[dict[str, Any]]: + sources = [ + payload.get("asset_bindings"), + payload.get("asset_binding_hint"), + payload.get("valuation_matrix"), + payload.get("fraudulent_transfer_facts"), + payload.get("encumbrance_timeline"), + payload.get("target_property_candidates"), + ] + out: list[dict[str, Any]] = [] + for source in sources: + for item in _as_list(source): + labels = _flatten_strings(item, limit=4) + if not labels and isinstance(item, dict): + labels = _strings(item.get("asset_label") or item.get("property_label")) + for label in labels: + candidate = { + "asset_label": label, + "asset_kind": _asset_kind_from_text(label), + "related_bo_ids": bo_ids, + "related_evidence_indexes": evidence, + } + key = json.dumps(candidate, ensure_ascii=False, sort_keys=True) + if key not in {json.dumps(x, ensure_ascii=False, sort_keys=True) for x in out}: + out.append(candidate) + return out + + def _preserved_claim_candidates(payload: dict[str, Any], mapping: dict[str, str], bo_ids: list[str], evidence: list[str]) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for item in _as_list(payload.get("preserved_claim_bundle") or payload.get("preserved_claim_candidates")): + obj = _dict(item) + ref = _first_string(obj.get("candidate_ref"), obj.get("bo_candidate_ref")) + bo_id = mapping.get(ref) if ref else _first_string(obj.get("bo_id")) + if bo_id not in bo_ids and bo_ids: + bo_id = bo_id or None + out.append({ + "bo_id": bo_id, + "claim_type_candidate": _first_string(obj.get("claim_type_candidate"), obj.get("claim_type"), obj.get("type"), obj.get("label")), + "creditor_candidate": _first_string(obj.get("creditor_candidate"), obj.get("creditor"), obj.get("plaintiff")), + "debtor_candidate": _first_string(obj.get("debtor_candidate"), obj.get("debtor"), obj.get("defendant")), + "claim_date_candidate": _first_string(obj.get("claim_date_candidate"), obj.get("claim_date"), obj.get("date"), obj.get("BehaviorTime")), + "related_evidence_indexes": _dedupe(evidence + _strings(obj.get("related_evidence_indexes") or obj.get("source_evidence_indexes"))), + }) + if not out and bo_ids: + out.append({ + "bo_id": bo_ids[0], + "claim_type_candidate": None, + "creditor_candidate": None, + "debtor_candidate": None, + "claim_date_candidate": None, + "related_evidence_indexes": evidence, + }) + return out + + def _role_hint(role: str, value: Any, date: Any, bo_ids: list[str], evidence: list[str]) -> dict[str, Any] | None: + if role not in ROLE_ENUM: + return None + value_candidate = _first_string(value) + date_candidate = _first_string(date) + if value_candidate is None and date_candidate is None: + return None + return { + "role": role, + "value_candidate": value_candidate, + "date_candidate": date_candidate, + "related_bo_ids": bo_ids, + "related_evidence_indexes": evidence, + } + + def _actio_role_hints(payload: dict[str, Any], seed: dict[str, Any], bo_ids: list[str], evidence: list[str]) -> list[dict[str, Any]]: + hints: list[dict[str, Any]] = [] + for fact in _as_list(payload.get("fraudulent_transfer_facts")): + obj = _dict(fact) + for hint in ( + _role_hint("fraudulent_act_date", obj.get("date") or obj.get("act_date"), obj.get("date") or obj.get("act_date"), bo_ids, evidence), + _role_hint("debtor_act", obj.get("debtor_act") or obj.get("action") or obj.get("description"), obj.get("date"), bo_ids, evidence), + ): + if hint: + hints.append(hint) + for party in _as_list(payload.get("beneficiary_transferee_hints")): + obj = _dict(party) + role_text = "transferee" if "transferee" in str(obj).lower() or "양수" in str(obj) else "beneficiary" + hint = _role_hint(role_text, obj.get("party") or obj.get("name") or obj.get("value") or obj, obj.get("date"), bo_ids, evidence) + if hint: + hints.append(hint) + for role, key in [ + ("remedy_or_cap", "remedy_mode_hints"), + ("defense", "defense_rebuttal"), + ("encumbrance", "encumbrance_timeline"), + ]: + for item in _as_list(payload.get(key)): + obj = _dict(item) + hint = _role_hint(role, obj or item, obj.get("date") if obj else None, bo_ids, evidence) + if hint: + hints.append(hint) + if not hints: + action = _first_string(seed.get("Action"), _dict(seed.get("core_field_base")).get("Action_proposal")) + hint = _role_hint("debtor_act", action, _dict(seed.get("core_field_base")).get("BehaviorTime"), bo_ids, evidence) + if hint: + hints.append(hint) + deduped: list[dict[str, Any]] = [] + seen: set[str] = set() + for hint in hints: + key = json.dumps(hint, ensure_ascii=False, sort_keys=True) + if key not in seen: + seen.add(key) + deduped.append(hint) + return deduped + + def _scopes(payload: dict[str, Any]) -> list[str]: + scopes: list[str] = [] + if payload.get("preserved_claim_bundle") or payload.get("preserved_claim_candidates"): + scopes.append("preserved_claim") + if payload.get("fraudulent_transfer_facts") or "actio" in " ".join(_strings(payload.get("legal_effect_tags"))).lower(): + scopes.append("fraudulent_act") + if payload.get("beneficiary_transferee_hints"): + scopes.append("beneficiary_or_transferee") + if payload.get("asset_bindings") or payload.get("asset_binding_hint") or payload.get("valuation_matrix"): + scopes.append("target_asset") + if payload.get("encumbrance_timeline"): + scopes.append("encumbrance") + if payload.get("remedy_mode_hints"): + scopes.append("remedy_or_cap") + if payload.get("defense_rebuttal"): + scopes.append("defense") + return [scope for scope in _dedupe(scopes) if scope in ACTIO_SCOPE_ENUM] + + def _suspicion_level(scopes: list[str]) -> str: + core = {"preserved_claim", "fraudulent_act", "beneficiary_or_transferee"} + if core.issubset(set(scopes)): + return "HIGH" + if {"preserved_claim", "fraudulent_act"}.issubset(set(scopes)): + return "MEDIUM" + if scopes: + return "LOW" + return "NONE" + + def _reasons(scopes: list[str], payload: dict[str, Any], b4_review_queue: list[Any]) -> str: + fragments: list[str] = [] + if "preserved_claim" in scopes: + fragments.append("피보전채권 후보가 B4 seed/domain payload에서 확인됨") + if "fraudulent_act" in scopes: + fragments.append("채무자 처분행위 또는 사해행위 후보 사실이 확인됨") + if "beneficiary_or_transferee" in scopes: + fragments.append("수익자 또는 전득자 후보가 확인됨") + if "target_asset" in scopes: + fragments.append("취소·원상회복 대상 재산 후보가 확인됨") + if "encumbrance" in scopes: + fragments.append("담보권·가압류 등 부담 관련 단서가 확인됨") + if "remedy_or_cap" in scopes: + fragments.append("원상회복 방법 또는 가액배상 한도 단서가 확인됨") + if "defense" in scopes: + fragments.append("항변 또는 선의·악의 관련 반박 단서가 확인됨") + for item in b4_review_queue[:2]: + if isinstance(item, dict): + reason = _first_string(item.get("reason"), item.get("issue"), item.get("review_reason")) + if reason: + fragments.append(f"B4 review queue: {reason}") + return ", ".join(fragments) if fragments else "사해행위취소 관련 구조화 단서가 충분히 확인되지 않음" + + + def _payload(bo: dict[str, Any]) -> dict[str, Any]: + return _as_dict(_as_dict(bo.get("extensions")).get("domain_payload")) + + def _actio_scope(payload: dict[str, Any]) -> list[str]: + scopes: list[str] = [] + if payload.get("preserved_claim_bundle"): + scopes.append("preserved_claim") + if payload.get("fraudulent_transfer_facts"): + scopes.append("fraudulent_act") + if payload.get("encumbrance_timeline"): + scopes.append("encumbrance") + if payload.get("remedy_mode_hints") or payload.get("value_compensation_cap_hints"): + scopes.append("remedy_or_cap") + if payload.get("defense_rebuttal"): + scopes.append("defense") + if payload.get("asset_bindings") or payload.get("valuation_matrix"): + scopes.append("target_asset") + return [value for value in _uniq(scopes) if value in ACTIO_SCOPES] + + def _candidate_types(bo: dict[str, Any]) -> list[str]: + payload = _payload(bo) + values = [payload.get("domain_legal_effect_tag"), bo.get("BOType"), bo.get("ActionType"), _as_dict(bo.get("provenance")).get("source_domain")] + values.extend(_as_list(payload.get("legal_effect_tags"))) + label = _as_dict(bo.get("JuristicAct")).get("label") + if label: + values.append(label) + return _uniq([str(value) for value in values if value not in (None, "")]) + + def _liability_type(domain: str, payload: dict[str, Any]) -> str: + if domain == "B1_Money_Successor" and payload.get("successor_liability_hints"): + return "commercial_successor" + if domain == "B2_Secured_Registry" and payload.get("derivative_invalidity_hints"): + return "registry_invalidity" + if domain == "B5_Succession_Notice_Lien_Asset_Defense": + if payload.get("succession_resolution") or payload.get("general_succession_status") or payload.get("asset_specific_succession_resolution"): + return "succession_related" + if payload.get("setoff_or_payment_defense_hints") or payload.get("simultaneous_performance_hints") or payload.get("defense_rebuttal"): + return "defense_or_setoff" + return LIABILITY_TYPES.get(domain, "other") + + def _validate_signal_payloads(actio: dict[str, Any], liability: dict[str, Any], legal: dict[str, Any], bo_ids: set[str]) -> None: + if actio.get("schema_version") != "actio_case_signals.v1" or liability.get("schema_version") != "case_liability_signals.v1" or legal.get("schema_version") != "legal_effect_signals.v1": + raise RuntimeError("signal schema version mismatch") + checks = [ + (actio.get("actio_case_signals"), {"signal_id", "actio_scope", "suspicion_level", "suspicion_reasons", "related_bo_ids", "related_evidence_indexes", "target_property_candidates", "preserved_claim_candidates", "actio_role_hints"}), + (liability.get("case_liability_signals"), {"liability_candidate_type", "related_bo_ids", "related_evidence_indexes", "party_roles", "claim_group_id", "liability_group_id", "linked_structure_ids"}), + (legal.get("bo_legal_effect_routes"), {"bo_id", "candidate_structure_types", "linked_structure_ids", "related_evidence_indexes"}), + ] + for items, keys in checks: + if not isinstance(items, list): + raise RuntimeError("signal item array missing") + for item in items: + if set(_as_dict(item)) != keys: + raise RuntimeError("signal item key mismatch") + related = _as_list(_as_dict(item).get("related_bo_ids")) + if any(value not in bo_ids for value in related): + raise RuntimeError("signal related BO outside BO.json") + if any(item.get("suspicion_level") not in SUSPICION_LEVELS or any(scope not in ACTIO_SCOPES for scope in _as_list(item.get("actio_scope"))) for item in actio["actio_case_signals"]): + raise RuntimeError("actio signal enum mismatch") + if any(item.get("liability_candidate_type") not in LIABILITY_ENUM for item in liability["case_liability_signals"]): + raise RuntimeError("liability signal enum mismatch") + if any(item.get("bo_id") not in bo_ids for item in legal["bo_legal_effect_routes"]): + raise RuntimeError("legal effect route BO outside BO.json") + + def main() -> None: + _init("task-c-bo-s0-signal-bundle-writer") + gate = _read_json("quality_gates/stage1_part2_bo_gate.json") + manifest = _read_json("stage1_tmp/task_c_bo/source_universe_manifest.json") + ledger = _as_dict(_as_dict(_read_json("stage1_tmp/task_c_bo/postb_seed_ledger.json")).get("postb_seed_ledger")) + _read_json("stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B4_Actio.json") + bo_list = _read_json("BO.json") + if gate.get("status") != "PASS" or gate.get("run_fingerprint") != manifest.get("run_fingerprint") or ledger.get("run_fingerprint") != manifest.get("run_fingerprint"): + raise RuntimeError("F0 gate or run fingerprint invalid") + bo_digest = _sha(json.dumps(bo_list, ensure_ascii=False, indent=2)) + if gate.get("post_write", {}).get("sha256") != bo_digest: + raise RuntimeError("BO digest mismatch") + bo_ids = {str(bo.get("BO_ID")) for bo in bo_list} + actio_items: list[dict[str, Any]] = [] + liability_items: list[dict[str, Any]] = [] + legal_items: list[dict[str, Any]] = [] + for bo in bo_list: + bo_id = str(bo["BO_ID"]) + evidence = _as_list(bo.get("source_evidence_indexes")) + domain = str(_as_dict(bo.get("provenance")).get("source_domain") or "") + payload = _payload(bo) + scopes = _actio_scope(payload) + if domain == "B4_Actio": + scopes = _scopes(payload) + actio_items.append({ + "signal_id": f"actio-signal-{len(actio_items)+1:03d}", "actio_scope": scopes or ["fraudulent_act"], + "suspicion_level": _suspicion_level(scopes), "suspicion_reasons": _reasons(scopes, payload, []), + "related_bo_ids": [bo_id], "related_evidence_indexes": evidence, + "target_property_candidates": _target_property_candidates(payload, [bo_id], evidence), + "preserved_claim_candidates": _preserved_claim_candidates(payload, {}, [bo_id], evidence), + "actio_role_hints": _actio_role_hints(payload, bo, [bo_id], evidence), + }) + downstream = _as_dict(bo.get("downstream_seed_refs")) + liability_items.append({ + "liability_candidate_type": _liability_type(domain, payload), "related_bo_ids": [bo_id], + "related_evidence_indexes": evidence, "party_roles": [], + "claim_group_id": (_as_list(downstream.get("claim_group_seed_refs_proposed")) or [None])[0], + "liability_group_id": None, "linked_structure_ids": [value for value in [downstream.get("legal_effect_structure_seed_ref_proposed")] if value], + }) + legal_items.append({ + "bo_id": bo_id, "candidate_structure_types": _candidate_types(bo), + "linked_structure_ids": [value for value in [downstream.get("legal_effect_structure_seed_ref_proposed")] if value], + "related_evidence_indexes": evidence, + }) + actio = {"schema_version": "actio_case_signals.v1", "status": "READY_WITH_REVIEW" if actio_items else "READY", "actio_case_signals": actio_items} + liability = {"schema_version": "case_liability_signals.v1", "status": "READY_WITH_REVIEW", "case_liability_signals": liability_items} + legal = {"schema_version": "legal_effect_signals.v1", "status": "READY_WITH_REVIEW", "bo_legal_effect_routes": legal_items} + _validate_signal_payloads(actio, liability, legal, bo_ids) + payloads = {"actio_case_signals.json": actio, "case_liability_signals.json": liability, "legal_effect_signals.json": legal} + reports: dict[str, Any] = {} + for path, payload in payloads.items(): + digest = _write_json_verified(path, payload) + reread = _read_json(path) + item_key = "actio_case_signals" if path.startswith("actio") else ("case_liability_signals" if path.startswith("case") else "bo_legal_effect_routes") + reports[path] = {"count": len(reread[item_key]), "status": reread["status"], "sha256": digest} + print(json.dumps({"signal_bundle_write_report": { + "schema_version": "stage1_part2_signal_bundle_write_report.v1", "status": "PASS", + "run_fingerprint": manifest["run_fingerprint"], "files": reports, + }}, ensure_ascii=False, separators=(",", ":"))) + + if __name__ == "__main__": + try: + main() + except Exception as exc: + print(json.dumps({"signal_bundle_write_report": {"schema_version": "stage1_part2_signal_bundle_write_report.v1", "status": "FAILED", "error": str(exc)}}, ensure_ascii=False)) + raise + task_procedure: + IN: + nexts: + - Task_C_BO_A0_context_domain_slice_compiler + wait_until: [] + Task_C_BO_A0_context_domain_slice_compiler: + nexts: + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + wait_until: + - IN + Task_C_BO_Stage_B_B1_Money_Successor: + nexts: + - Task_C_BO_R0_seed_reducer_exception_planner + wait_until: + - Task_C_BO_A0_context_domain_slice_compiler + Task_C_BO_Stage_B_B2_Secured_Registry: + nexts: + - Task_C_BO_R0_seed_reducer_exception_planner + wait_until: + - Task_C_BO_A0_context_domain_slice_compiler + Task_C_BO_Stage_B_B3_Land_Valuation: + nexts: + - Task_C_BO_R0_seed_reducer_exception_planner + wait_until: + - Task_C_BO_A0_context_domain_slice_compiler + Task_C_BO_Stage_B_B4_Actio: + nexts: + - Task_C_BO_R0_seed_reducer_exception_planner + wait_until: + - Task_C_BO_A0_context_domain_slice_compiler + Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense: + nexts: + - Task_C_BO_R0_seed_reducer_exception_planner + wait_until: + - Task_C_BO_A0_context_domain_slice_compiler + Task_C_BO_R0_seed_reducer_exception_planner: + nexts: + - Task_C_BO_R1_exception_adjudicator_* + - Task_C_BO_F0_final_bo_gate_writer + wait_until: + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + Task_C_BO_R1_exception_adjudicator_*: + nexts: + - Task_C_BO_F0_final_bo_gate_writer + wait_until: + - Task_C_BO_R0_seed_reducer_exception_planner + Task_C_BO_F0_final_bo_gate_writer: + nexts: + - Task_C_BO_S0_signal_bundle_writer + wait_until: + - Task_C_BO_R0_seed_reducer_exception_planner + - all Task_C_BO_R1_exception_adjudicator_* + Task_C_BO_S0_signal_bundle_writer: + nexts: + - OUT + wait_until: + - Task_C_BO_F0_final_bo_gate_writer + OUT: + nexts: [] + wait_until: + - Task_C_BO_S0_signal_bundle_writer diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Codex YAML/Stage_1_Part_3_Codex_v2.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Codex YAML/Stage_1_Part_3_Codex_v2.yml new file mode 100644 index 00000000..29aa24be --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Codex YAML/Stage_1_Part_3_Codex_v2.yml @@ -0,0 +1,2165 @@ +--- +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_3 + description: 민사소송 원고 송무 초지능 AI변호사 - Legal Effect 생성 + version: v2.0 + Stages: + - name: stage1_legal_effect_생성 + description: Legal Effect Structure 작성 + tools: + mcpServers: + localdocs: + type: streamable-http + url: http://mcp-localdocs:8012/mcp + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + task_procedure: + IN: + nexts: + - Task_LES0_input_pack_compiler + wait_until: [] + Task_LES0_input_pack_compiler: + nexts: + - Task_LES1_deterministic_structure_seed_builder + wait_until: + - IN + Task_LES1_deterministic_structure_seed_builder: + nexts: + - Task_LES2_exception_adjudicator_* + - Task_LES3_final_structure_index_writer + wait_until: + - Task_LES0_input_pack_compiler + Task_LES2_exception_adjudicator_*: + nexts: + - Task_LES3_final_structure_index_writer + wait_until: + - Task_LES1_deterministic_structure_seed_builder + Task_LES3_final_structure_index_writer: + nexts: + - OUT + wait_until: + - Task_LES1_deterministic_structure_seed_builder + - all Task_LES2_exception_adjudicator_* + OUT: + nexts: [] + wait_until: + - Task_LES3_final_structure_index_writer + + tasks: + - task_name: Task_LES0_input_pack_compiler + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: |- + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MESSAGE_IDS = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + INPUT_FILES = [ + "BO.json", + "evidence_indexed.json", + "actio_case_signals.json", + "case_liability_signals.json", + "legal_effect_signals.json", + ] + OUTPUT_PATH = "stage1_tmp/legal_effect_structure_input_pack.json" + CANONICAL_TYPES = [ + "money_claim", + "commercial_successor", + "secured_debt", + "registry_invalidity", + "land_use_gain", + "valuation", + "fraudulent_transfer", + "preserved_claim", + "succession_notice_lien_asset_defense", + "general_legal_effect", + ] + SOURCE_DOMAIN_FALLBACK = { + "B1_Money_Successor": "money_claim", + "B2_Secured_Registry": "secured_debt", + "B3_Land_Valuation": "land_use_gain", + "B4_Actio": "fraudulent_transfer", + "B5_Succession_Notice_Lien_Asset_Defense": "succession_notice_lien_asset_defense", + } + EXACT_TYPE_ALIASES = { + "auction_invalidity": ["registry_invalidity"], + "derivative_bank_mortgage_invalidity": ["secured_debt", "registry_invalidity"], + "mortgage_extinguishment": ["secured_debt", "registry_invalidity"], + "actio_remedy_or_cap": ["fraudulent_transfer"], + "remedy_or_cap": ["fraudulent_transfer"], + "value_compensation": ["fraudulent_transfer", "valuation"], + "insolvency_check": ["fraudulent_transfer"], + "actio_asset_target": ["fraudulent_transfer"], + "target_asset": ["fraudulent_transfer"], + "preserved_claim": ["preserved_claim"], + } + SIGNAL_CONTRACTS = { + "actio_case_signals.json": ("actio_case_signals.v1", "actio_case_signals"), + "case_liability_signals.json": ("case_liability_signals.v1", "case_liability_signals"), + "legal_effect_signals.json": ("legal_effect_signals.v1", "bo_legal_effect_routes"), + } + + def _next_id() -> int: + return next(MESSAGE_IDS) + + def _decode_json(value: Any, label: str) -> Any: + if not isinstance(value, str): + return value + text = value.strip() + if not text: + raise ValueError(f"{label}: empty JSON text") + try: + return json.loads(text) + except json.JSONDecodeError: + try: + obj, _ = JSON_DECODER.raw_decode(text) + return obj + except json.JSONDecodeError as exc: + raise ValueError(f"{label}: invalid JSON: {text[:240]}") from exc + + def _parse_rpc(text: str) -> dict[str, Any]: + candidates = [] + for line in text.splitlines(): + if line.startswith("data:"): + candidates.append(line[5:].strip()) + candidates.append(text.strip()) + for candidate in candidates: + if not candidate: + continue + try: + value = _decode_json(candidate, "MCP response") + except ValueError: + continue + if isinstance(value, dict) and ("result" in value or "error" in value): + return value + raise RuntimeError(f"MCP response could not be parsed: {text[:300]}") + + def _initialize() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage1-part3-les0-v2", + "version": "2.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + initialized = CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ) + initialized.raise_for_status() + + def _call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_rpc(response.text) + if parsed.get("error"): + raise RuntimeError(f"MCP {name} failed: {parsed['error']}") + return parsed + + def _read_doc(path: str) -> Any: + rpc = _call_tool("read_docs", {"doc_names": [path]}) + content = rpc.get("result", {}).get("content", []) + if not content or not isinstance(content[0], dict): + raise RuntimeError(f"{path}: read_docs returned no content") + envelope = _decode_json(content[0].get("text", ""), f"{path} envelope") + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results or not isinstance(results[0], dict): + raise RuntimeError(f"{path}: read_docs results are empty") + if results[0].get("error"): + raise RuntimeError(f"{path}: {results[0]['error']}") + inner = results[0].get("content", "") + else: + inner = envelope + return _decode_json(inner, path) + + def _artifact_text(payload: dict[str, Any]) -> str: + return json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n" + + def _write_doc(path: str, payload: dict[str, Any]) -> None: + _call_tool("write_file", {"path": path, "content": _artifact_text(payload), "overwrite": True}) + + def _digest(value: Any) -> str: + text = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return "sha256:" + hashlib.sha256(text.encode("utf-8")).hexdigest() + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + output = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in output: + output.append(text) + return output + + def _sorted_strings(value: Any) -> list[str]: + return sorted(set(_strings(value))) + + def _first(*values: Any) -> str | None: + for value in values: + items = _strings(value) + if items: + return items[0] + return None + + def _normalized(text: Any) -> str: + return re.sub(r"[^0-9a-zA-Z가-힣]", "", str(text or "").lower()) + + def _evidence_rows(doc: Any) -> list[dict[str, Any]]: + root = _as_dict(doc) + rows = root.get("evidence_indexed") + if not isinstance(rows, list): + rows = root.get("items") + if not isinstance(rows, list): + raise ValueError("evidence_indexed.json must contain evidence_indexed or items array") + return [row for row in rows if isinstance(row, dict)] + + def _canonicalize_label(label: str, source_domain: str | None) -> list[str]: + raw = label.strip() + lower = raw.lower().replace("-", "_").replace(" ", "_") + if lower in CANONICAL_TYPES: + return [lower] + if lower in EXACT_TYPE_ALIASES: + return EXACT_TYPE_ALIASES[lower] + types = [] + text = lower + " " + raw + families = [ + (["money", "loan", "debt_payment", "claim", "대여", "공사대금", "물품대금", "미지급", "채권", "변제"], "money_claim"), + (["successor", "business_transfer", "continued_trade", "상호속용", "영업양수", "승계책임"], "commercial_successor"), + (["secured", "mortgage", "collateral", "담보", "근저당", "피담보", "저당"], "secured_debt"), + (["registry", "auction_invalid", "cancellation", "derivative_invalid", "등기", "말소", "경매무효"], "registry_invalidity"), + (["land_use", "use_gain", "rent", "delivery", "사용이익", "임료", "차임", "인도"], "land_use_gain"), + (["valuation", "appraisal", "value_compensation", "감정", "평가", "가액", "시가"], "valuation"), + (["fraudulent", "beneficiary", "transferee", "insolvency", "remedy_or_cap", "target_asset", "사해", "수익자", "전득자", "무자력", "가액배상"], "fraudulent_transfer"), + (["preserved_claim", "피보전채권"], "preserved_claim"), + (["succession", "notice", "lien", "asset_defense", "상속", "통지", "유치권", "항변"], "succession_notice_lien_asset_defense"), + ] + if "possession" in text or "점유" in text: + if source_domain == "B3_Land_Valuation": + types.append("land_use_gain") + elif source_domain == "B5_Succession_Notice_Lien_Asset_Defense": + types.append("succession_notice_lien_asset_defense") + for needles, canonical in families: + if any(needle in text for needle in needles): + types.append(canonical) + return [item for item in CANONICAL_TYPES if item in types] + + def _signal_rows(doc: Any, name: str) -> list[dict[str, Any]]: + schema, key = SIGNAL_CONTRACTS[name] + root = _as_dict(doc) + if root.get("schema_version") != schema: + raise ValueError(f"{name}: schema_version must be {schema}") + if root.get("status") not in {"READY", "READY_WITH_REVIEW", "EMPTY"}: + raise ValueError(f"{name}: invalid status") + rows = root.get(key) + if not isinstance(rows, list): + raise ValueError(f"{name}.{key} must be array") + return [row for row in rows if isinstance(row, dict)] + + def _signal_bo_ids(row: Any) -> list[str]: + output = [] + if isinstance(row, dict): + for key, value in row.items(): + if key in {"bo_id", "BO_ID"}: + output.extend(_strings(value)) + elif key in {"related_bo_ids", "source_bo_ids"}: + output.extend(_strings(value)) + elif isinstance(value, (dict, list)): + output.extend(_signal_bo_ids(value)) + elif isinstance(row, list): + for item in row: + output.extend(_signal_bo_ids(item)) + return _sorted_strings(output) + + def _group_signals(rows: list[dict[str, Any]], bo_ids: set[str]) -> dict[str, list[dict[str, Any]]]: + output = {bo_id: [] for bo_id in bo_ids} + for row in rows: + refs = _signal_bo_ids(row) + unknown = sorted(set(refs) - bo_ids) + if unknown: + raise ValueError(f"signal references unknown BO IDs: {unknown}") + for bo_id in refs: + output[bo_id].append(row) + return output + + def _add_alias(alias_map: dict[str, dict[str, Any]], ref: str, row: dict[str, Any], review: list[dict[str, Any]]) -> None: + old = alias_map.get(ref) + label = row.get("canonical_label_candidate") + if old and old.get("canonical_label_candidate") and label and old["canonical_label_candidate"] != label: + review.append({ + "review_code": "ASSET_ALIAS_LABEL_CONFLICT", + "source_bo_ids": _sorted_strings(row.get("source_bo_ids")), + "evidence_indexes": _sorted_strings([old.get("evidence_index"), row.get("evidence_index")]), + "related_ref_keys": [ref], + "reason_code": "CONFLICTING_NON_NULL_CANONICAL_LABELS", + }) + return + merged = dict(old or {}) + for key, value in row.items(): + if value not in (None, "", []): + merged[key] = value + merged["source_bo_ids"] = _sorted_strings(_strings((old or {}).get("source_bo_ids")) + _strings(row.get("source_bo_ids"))) + alias_map[ref] = merged + + def _build_evidence_and_aliases(rows: list[dict[str, Any]], bo_items: list[dict[str, Any]]) -> tuple[dict[str, Any], dict[str, Any], list[dict[str, Any]]]: + evidence = {} + aliases = {} + review = [] + for row in rows: + evidence_index = _first(row.get("evidence_index"), row.get("id")) + if not evidence_index: + raise ValueError("evidence row missing evidence_index") + if evidence_index in evidence: + raise ValueError(f"duplicate evidence_index: {evidence_index}") + evidence[evidence_index] = { + "evidence_index": evidence_index, + "source_title": _first(row.get("source_title"), row.get("title")), + "doc_type": _first(row.get("doc_type")), + "source_pointer": _as_dict(row.get("source_pointer")), + "priority_class": _first(row.get("priority_class")), + "authentication_status": _first(row.get("authentication_status")), + } + for schedule in _as_list(row.get("schedule_items")): + if not isinstance(schedule, dict): + continue + ref = _first(schedule.get("schedule_ref")) + if ref: + _add_alias(aliases, ref, { + "ref_type": "schedule", + "canonical_label_candidate": _first(schedule.get("canonical_label_candidate")), + "evidence_index": evidence_index, + "usable_in_relief": _first(schedule.get("usable_in_relief")), + "source_bo_ids": [], + }, review) + for registry in _as_list(row.get("registry_row_components")): + if not isinstance(registry, dict): + continue + ref = _first(registry.get("registry_ref")) + if ref: + _add_alias(aliases, ref, { + "ref_type": "registry_row", + "canonical_label_candidate": None, + "evidence_index": evidence_index, + "section": _first(registry.get("section"), registry.get("kind")), + "row_seq": registry.get("row_seq"), + "source_bo_ids": [], + }, review) + for bo in bo_items: + bo_id = _first(bo.get("BO_ID"), bo.get("id")) + payload = _as_dict(_as_dict(bo.get("extensions")).get("domain_payload")) + for alias in _as_list(payload.get("asset_alias")): + if not isinstance(alias, dict): + continue + labels = _strings(alias.get("canonical_label_candidate")) + _strings(alias.get("alias_strings")) + label = labels[0] if labels else None + for ref in _strings(alias.get("schedule_refs")) + _strings(alias.get("registry_refs")): + _add_alias(aliases, ref, { + "ref_type": "schedule" if ref.startswith("SCH-") else "registry_row", + "canonical_label_candidate": label, + "evidence_index": None, + "source_bo_ids": [bo_id] if bo_id else [], + }, review) + return evidence, aliases, review + + def _asset_kind(label: str, refs: list[str]) -> str: + text = label.lower() + if any(ref.startswith(("REG-", "SCH-")) for ref in refs) or any(word in text for word in ["토지", "대지", "건물", "상가", "빌라", "임야", "부동산"]): + return "real_property" + if any(word in text for word in ["근저당", "저당", "담보권"]): + return "security_right" + if any(word in text for word in ["채권", "대여금", "공사대금", "물품대금"]): + return "claim" + if any(word in text for word in ["예금", "현금", "계좌"]): + return "cash_or_deposit" + return "unknown" + + def _extract_ref_keys(value: Any) -> list[str]: + refs = [] + for text in _strings(value): + refs.extend(re.findall(r"(?:REG|SCH)-\d{3}-\d{2}", text)) + return _sorted_strings(refs) + + def _asset_candidates(bo: dict[str, Any], actio_rows: list[dict[str, Any]], alias_map: dict[str, Any]) -> list[dict[str, Any]]: + bo_id = _first(bo.get("BO_ID"), bo.get("id")) or "" + evidence_indexes = _sorted_strings(bo.get("source_evidence_indexes")) + payload = _as_dict(_as_dict(bo.get("extensions")).get("domain_payload")) + candidates = [] + + def add(label_value: Any, source_code: str, refs_value: Any = None, evidence_value: Any = None) -> None: + for label in _strings(label_value): + if re.fullmatch(r"\d{4}-\d{2}-\d{2}", label): + continue + refs = _sorted_strings(_strings(refs_value) + _extract_ref_keys(label)) + if not refs: + normalized_label = _normalized(label) + matches = [] + if len(normalized_label) >= 4: + for ref, alias in alias_map.items(): + if not ref.startswith("SCH-"): + continue + alias_label = _normalized(_as_dict(alias).get("canonical_label_candidate")) + same_scope = bo_id in _strings(_as_dict(alias).get("source_bo_ids")) or _as_dict(alias).get("evidence_index") in evidence_indexes + if same_scope and normalized_label in alias_label: + matches.append(ref) + if len(matches) == 1: + refs = matches + candidates.append({ + "asset_label": label, + "asset_kind": _asset_kind(label, refs), + "asset_ref_keys": refs, + "source_code": source_code, + "evidence_indexes": _sorted_strings(evidence_value or evidence_indexes), + }) + + add(payload.get("asset_bindings"), "BO_ASSET_BINDINGS") + add(payload.get("asset_binding_hint"), "BO_ASSET_BINDING_HINT") + add(payload.get("registry_object_display"), "BO_REGISTRY_OBJECT_DISPLAY") + add(payload.get("valuation_object"), "BO_VALUATION_OBJECT") + possession_ref = _as_dict(payload.get("possession_state")).get("asset_ref") + if possession_ref: + add(possession_ref, "BO_POSSESSION_ASSET_REF", possession_ref) + for registry in _as_list(payload.get("registry_rows")): + if isinstance(registry, dict) and registry.get("registry_ref"): + add(registry.get("registry_ref"), "BO_REGISTRY_ROW", registry.get("registry_ref")) + for alias in _as_list(payload.get("asset_alias")): + if isinstance(alias, dict): + refs = _strings(alias.get("schedule_refs")) + _strings(alias.get("registry_refs")) + labels = _strings(alias.get("canonical_label_candidate")) + _strings(alias.get("alias_strings")) + add(labels or refs, "BO_EXPLICIT_ASSET_ALIAS", refs) + for signal in actio_rows: + for candidate in _as_list(signal.get("target_property_candidates")): + if isinstance(candidate, dict): + add(candidate.get("asset_label"), "ACTIO_TARGET_PROPERTY", None, candidate.get("related_evidence_indexes")) + output = [] + seen = set() + for item in candidates: + key = (item["asset_label"], tuple(item["asset_ref_keys"]), item["source_code"]) + if key not in seen: + seen.add(key) + output.append(item) + return sorted(output, key=lambda row: (row["asset_ref_keys"], row["asset_label"], row["source_code"])) + + def _candidate_record(bo: dict[str, Any], signals: dict[str, list[dict[str, Any]]], alias_map: dict[str, Any]) -> dict[str, Any]: + bo_id = _first(bo.get("BO_ID"), bo.get("id")) + if not bo_id: + raise ValueError("BO row missing BO_ID and id") + payload = _as_dict(_as_dict(bo.get("extensions")).get("domain_payload")) + source_domain = _first(_as_dict(bo.get("provenance")).get("source_domain")) + raw_labels = [] + for route in signals["legal_effect"]: + raw_labels.extend(_strings(route.get("candidate_structure_types"))) + for row in signals["case_liability"]: + raw_labels.extend(_strings(row.get("liability_candidate_type"))) + for row in signals["actio"]: + raw_labels.extend(_strings(row.get("actio_scope"))) + raw_labels.extend(_strings(payload.get("domain_legal_effect_tag"))) + raw_labels.extend(_strings(payload.get("legal_effect_tags"))) + raw_labels.extend(_strings(bo.get("Legal_Keywords"))) + canonical = [] + nonrouting = [] + for label in _strings(raw_labels): + mapped = _canonicalize_label(label, source_domain) + if mapped: + canonical.extend(mapped) + else: + nonrouting.append(label) + if not canonical: + canonical = [SOURCE_DOMAIN_FALLBACK.get(source_domain, "general_legal_effect")] + canonical = [item for item in CANONICAL_TYPES if item in canonical] + claim_ids = [] + liability_ids = [] + linked_structure_ids = [] + actio_signal_ids = [] + evidence_indexes = _strings(bo.get("source_evidence_indexes")) + for row in signals["case_liability"]: + claim_ids.extend(_strings(row.get("claim_group_id"))) + liability_ids.extend(_strings(row.get("liability_group_id"))) + linked_structure_ids.extend(_strings(row.get("linked_structure_ids"))) + evidence_indexes.extend(_strings(row.get("related_evidence_indexes"))) + for row in signals["legal_effect"]: + linked_structure_ids.extend(_strings(row.get("linked_structure_ids"))) + evidence_indexes.extend(_strings(row.get("related_evidence_indexes"))) + for row in signals["actio"]: + actio_signal_ids.extend(_strings(row.get("signal_id"))) + evidence_indexes.extend(_strings(row.get("related_evidence_indexes"))) + downstream = _as_dict(bo.get("downstream_seed_refs")) + claim_ids.extend(_strings(downstream.get("claim_group_seed_refs_proposed"))) + linked_structure_ids.extend(_strings(downstream.get("legal_effect_structure_seed_ref_proposed"))) + theory_refs = _strings(downstream.get("canonical_theory_graph_seed_ref_proposed")) + basis_codes = ["BO_SOURCE", "EVIDENCE_INDEX", "SOURCE_DOMAIN_FALLBACK"] + if signals["legal_effect"]: + basis_codes.append("LEGAL_EFFECT_SIGNAL") + if signals["case_liability"]: + basis_codes.append("CASE_LIABILITY_SIGNAL") + if signals["actio"]: + basis_codes.append("ACTIO_SIGNAL") + return { + "bo_id": bo_id, + "source_domain": source_domain, + "hard_issue_keys": { + "claim_group_ids": _sorted_strings(claim_ids), + "liability_group_ids": _sorted_strings(liability_ids), + "canonical_theory_graph_seed_refs": _sorted_strings(theory_refs), + "linked_structure_ids": _sorted_strings(linked_structure_ids), + }, + "canonical_structure_types": canonical, + "nonrouting_labels": _sorted_strings(nonrouting), + "asset_candidates": _asset_candidates(bo, signals["actio"], alias_map), + "evidence_indexes": _sorted_strings(evidence_indexes), + "linked_claim_group_ids": _sorted_strings(claim_ids), + "linked_liability_group_ids": _sorted_strings(liability_ids), + "linked_actio_signal_ids": _sorted_strings(actio_signal_ids), + "issue_basis": { + "legal_keywords": _sorted_strings(bo.get("Legal_Keywords")), + "action": _first(bo.get("Action")), + "reason": _first(bo.get("Reason")), + "juristic_act_label": _first(_as_dict(bo.get("JuristicAct")).get("label")), + "action_type": _first(bo.get("ActionType")), + "bo_type": _first(bo.get("BOType")), + }, + "basis_codes": _sorted_strings(basis_codes), + } + + def main() -> None: + fingerprint = None + try: + _initialize() + docs = {name: _read_doc(name) for name in INPUT_FILES} + bo_items = docs["BO.json"] + if not isinstance(bo_items, list): + raise ValueError("BO.json root must be array") + bo_rows = [row for row in bo_items if isinstance(row, dict)] + bo_ids = [_first(row.get("BO_ID"), row.get("id")) for row in bo_rows] + if any(not value for value in bo_ids): + raise ValueError("every BO row must have BO_ID or id") + if len(set(bo_ids)) != len(bo_ids): + raise ValueError("BO IDs must be unique") + bo_universe = set(bo_ids) + evidence_rows = _evidence_rows(docs["evidence_indexed.json"]) + signal_rows = {name: _signal_rows(docs[name], name) for name in SIGNAL_CONTRACTS} + grouped = {name: _group_signals(rows, bo_universe) for name, rows in signal_rows.items()} + evidence, aliases, input_review = _build_evidence_and_aliases(evidence_rows, bo_rows) + records = {} + for bo in sorted(bo_rows, key=lambda row: _first(row.get("BO_ID"), row.get("id")) or ""): + bo_id = _first(bo.get("BO_ID"), bo.get("id")) or "" + signals = { + "actio": grouped["actio_case_signals.json"].get(bo_id, []), + "case_liability": grouped["case_liability_signals.json"].get(bo_id, []), + "legal_effect": grouped["legal_effect_signals.json"].get(bo_id, []), + } + record = _candidate_record(bo, signals, aliases) + missing = sorted(set(record["evidence_indexes"]) - set(evidence)) + if missing: + input_review.append({ + "review_code": "MISSING_EVIDENCE_AUTHORITY", + "source_bo_ids": [bo_id], + "evidence_indexes": missing, + "related_ref_keys": [], + "reason_code": "REFERENCED_EVIDENCE_NOT_INDEXED", + }) + records[bo_id] = record + if set(records) != bo_universe: + raise ValueError("BO-to-candidate conservation failed") + input_digests = {name: _digest(docs[name]) for name in INPUT_FILES} + fingerprint = _digest(input_digests) + status = "READY_WITH_REVIEW" if input_review else "READY" + payload = { + "legal_structure_input_pack": { + "schema_version": "stage1_legal_structure_input_pack.v2", + "status": status, + "run_fingerprint": fingerprint, + "input_digests": input_digests, + "source_counts": { + "bo": len(bo_rows), + "evidence": len(evidence_rows), + "actio_signals": len(signal_rows["actio_case_signals.json"]), + "case_liability_signals": len(signal_rows["case_liability_signals.json"]), + "legal_effect_signals": len(signal_rows["legal_effect_signals.json"]), + }, + "evidence_authority_by_index": evidence, + "asset_alias_by_ref": aliases, + "candidate_records_by_bo_id": records, + "input_review_items": input_review, + } + } + _write_doc(OUTPUT_PATH, payload) + reread = _read_doc(OUTPUT_PATH) + if _digest(reread) != _digest(payload): + raise ValueError("LES0 post-write reread mismatch") + print(json.dumps({ + "les0_manifest": { + "schema_version": "stage1_legal_structure_input_manifest.v2", + "status": status, + "artifact_path": OUTPUT_PATH, + "run_fingerprint": fingerprint, + "bo_count": len(bo_rows), + "candidate_count": len(records), + "review_count": len(input_review), + } + }, ensure_ascii=False, separators=(",", ":"))) + except Exception as exc: + print(json.dumps({ + "les0_manifest": { + "schema_version": "stage1_legal_structure_input_manifest.v2", + "status": "FAILED", + "artifact_path": OUTPUT_PATH, + "run_fingerprint": fingerprint, + "failure_reason": str(exc), + } + }, ensure_ascii=False, separators=(",", ":"))) + raise + + if __name__ == "__main__": + main() + + - task_name: Task_LES1_deterministic_structure_seed_builder + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: |- + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + from collections import defaultdict + from typing import Any + + import httpx + + LES0_MANIFEST_RAW = r'''{{prev.Task_LES0_input_pack_compiler}}''' + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MESSAGE_IDS = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + INPUT_PATH = "stage1_tmp/legal_effect_structure_input_pack.json" + SEED_PATH = "stage1_tmp/legal_effect_structure_seed_bundle.json" + EXCEPTION_PATH = "stage1_tmp/legal_effect_structure_exception_pack.json" + MAX_EXCEPTION_ITEMS_PER_PACK = 4 + TARGET_EXCEPTION_ITEMS_PER_PACK = 3 + MAX_EXCEPTION_PACK_BYTES = 12000 + MAX_EXCEPTION_CANDIDATE_VALUES = 8 + MAX_EXCEPTION_PACKS = 8 + MAX_CONCURRENCY = 2 + CANONICAL_TYPES = [ + "money_claim", + "commercial_successor", + "secured_debt", + "registry_invalidity", + "land_use_gain", + "valuation", + "fraudulent_transfer", + "preserved_claim", + "succession_notice_lien_asset_defense", + "general_legal_effect", + ] + SOURCE_DOMAIN_FALLBACK = { + "B1_Money_Successor": "money_claim", + "B2_Secured_Registry": "secured_debt", + "B3_Land_Valuation": "land_use_gain", + "B4_Actio": "fraudulent_transfer", + "B5_Succession_Notice_Lien_Asset_Defense": "succession_notice_lien_asset_defense", + } + TYPE_ROLES = { + "money_claim": "claim_chain_ref", + "commercial_successor": "claim_chain_ref", + "secured_debt": "linked_structures", + "registry_invalidity": "linked_structures", + "land_use_gain": "legal_calculation_object", + "valuation": "legal_calculation_object", + "fraudulent_transfer": "actio_roles", + "preserved_claim": "claim_chain_ref", + "succession_notice_lien_asset_defense": "linked_structures", + "general_legal_effect": "legal_effect_roles", + } + HARD_ISSUE_KEY_PRIORITY = [ + "claim_group_ids", + "liability_group_ids", + "canonical_theory_graph_seed_refs", + "linked_structure_ids", + ] + ALLOWED_DECISIONS = { + "ambiguous_issue_cluster": ["USE_EXISTING_HARD_KEY", "PRESERVE_MULTI_ISSUE", "SPLIT_ISSUE_CLUSTER", "BLOCK_REVIEW"], + "ambiguous_asset_cluster": ["SAME_CLUSTER", "SEPARATE_CLUSTER", "MULTI_ASSET_BUNDLE", "BLOCK_REVIEW"], + "ambiguous_candidate_structure_type": ["SELECT_REPRESENTATIVE_TYPE", "PRESERVE_MULTI_ROUTING", "SPLIT_STRUCTURE_SEED", "BLOCK_REVIEW"], + } + REVIEW_CODES = { + "ASSET_ALIAS_LABEL_CONFLICT", + "ASSET_IDENTITY_DEFERRED_SEPARATE", + "ISSUE_FAMILY_LOW_MARGIN", + "MISSING_EVIDENCE_AUTHORITY", + "PACK_CONTEXT_TOO_LARGE", + } + + def _next_id() -> int: + return next(MESSAGE_IDS) + + def _decode_json(value: Any, label: str) -> Any: + if not isinstance(value, str): + return value + text = value.strip() + if not text: + raise ValueError(f"{label}: empty JSON text") + try: + return json.loads(text) + except json.JSONDecodeError: + try: + obj, _ = JSON_DECODER.raw_decode(text) + return obj + except json.JSONDecodeError as exc: + raise ValueError(f"{label}: invalid JSON: {text[:240]}") from exc + + def _parse_rpc(text: str) -> dict[str, Any]: + candidates = [line[5:].strip() for line in text.splitlines() if line.startswith("data:")] + candidates.append(text.strip()) + for candidate in candidates: + if not candidate: + continue + try: + value = _decode_json(candidate, "MCP response") + except ValueError: + continue + if isinstance(value, dict) and ("result" in value or "error" in value): + return value + raise RuntimeError(f"MCP response could not be parsed: {text[:300]}") + + def _initialize() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage1-part3-les1-v2", + "version": "2.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_rpc(response.text) + if parsed.get("error"): + raise RuntimeError(f"MCP {name} failed: {parsed['error']}") + return parsed + + def _read_doc(path: str) -> Any: + rpc = _call_tool("read_docs", {"doc_names": [path]}) + content = rpc.get("result", {}).get("content", []) + if not content or not isinstance(content[0], dict): + raise RuntimeError(f"{path}: read_docs returned no content") + envelope = _decode_json(content[0].get("text", ""), f"{path} envelope") + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results or not isinstance(results[0], dict): + raise RuntimeError(f"{path}: read_docs results are empty") + if results[0].get("error"): + raise RuntimeError(f"{path}: {results[0]['error']}") + inner = results[0].get("content", "") + else: + inner = envelope + return _decode_json(inner, path) + + def _artifact_text(payload: dict[str, Any]) -> str: + return json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n" + + def _artifact_hash(payload: dict[str, Any]) -> str: + return "sha256:" + hashlib.sha256(_artifact_text(payload).encode("utf-8")).hexdigest() + + def _write_doc(path: str, payload: dict[str, Any]) -> None: + _call_tool("write_file", {"path": path, "content": _artifact_text(payload), "overwrite": True}) + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + output = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in output: + output.append(text) + return output + + def _sorted_strings(value: Any) -> list[str]: + return sorted(set(_strings(value))) + + def _stable_id(prefix: str, ordinal: int) -> str: + return f"{prefix}-{ordinal:03d}" + + def _normalized(value: Any) -> str: + return re.sub(r"[^0-9a-zA-Z가-힣]", "", str(value or "").lower()) or "unknown" + + def _priority_types(value: Any, source_domain: str | None) -> list[str]: + supplied = _strings(value) + unknown = sorted(set(supplied) - set(CANONICAL_TYPES)) + if unknown: + raise ValueError(f"candidate record contains non-canonical types: {unknown}") + if not supplied: + supplied = [SOURCE_DOMAIN_FALLBACK.get(source_domain, "general_legal_effect")] + return [item for item in CANONICAL_TYPES if item in supplied] + + def _issue_keys(record: dict[str, Any], representative_type: str) -> tuple[list[str], bool]: + hard = _as_dict(record.get("hard_issue_keys")) + keys = [] + for family in HARD_ISSUE_KEY_PRIORITY: + for value in _sorted_strings(hard.get(family)): + keys.append(f"hard:{family}:{value}") + if keys: + return keys, False + return [f"family:{representative_type}"], representative_type == "general_legal_effect" + + def _add_review( + reviews: list[dict[str, Any]], + review_index: dict[tuple[Any, ...], str], + review_by_bo: dict[str, list[str]], + code: str, + bo_ids: Any, + evidence_indexes: Any, + ref_keys: Any, + reason_code: str, + severity: str = "WARNING", + owner: str = "STAGE_2", + ) -> str: + if code not in REVIEW_CODES: + raise ValueError(f"unsupported review code: {code}") + bos = _sorted_strings(bo_ids) + evidence = _sorted_strings(evidence_indexes) + refs = _sorted_strings(ref_keys) + key = (code, tuple(bos), tuple(evidence), tuple(refs), reason_code, severity, owner) + if key in review_index: + return review_index[key] + review_id = _stable_id("LRV", len(reviews) + 1) + item = { + "review_id": review_id, + "review_code": code, + "severity": severity, + "source_bo_ids": bos, + "evidence_indexes": evidence, + "related_ref_keys": refs, + "reason_code": reason_code, + "downstream_owner": owner, + } + reviews.append(item) + review_index[key] = review_id + for bo_id in bos: + review_by_bo[bo_id].append(review_id) + return review_id + + def _explicit_exception_draft(record: dict[str, Any], issue_ids: list[str], asset_ids: list[str], types: list[str]) -> dict[str, Any] | None: + codes = set(_strings(record.get("basis_codes"))) + common = {"FINAL_SINGLE_SELECTION_REQUIRED", "PRESERVE_OR_DEFER_PROHIBITED", "COMPACT_CONTEXT_SUFFICIENT"} + if not common.issubset(codes): + return None + if "AMBIGUOUS_ISSUE_FINAL_REQUIRED" in codes: + exception_type = "ambiguous_issue_cluster" + candidates = issue_ids + elif "AMBIGUOUS_ASSET_FINAL_REQUIRED" in codes: + exception_type = "ambiguous_asset_cluster" + candidates = asset_ids + elif "AMBIGUOUS_TYPE_FINAL_REQUIRED" in codes: + exception_type = "ambiguous_candidate_structure_type" + candidates = types + else: + return None + candidates = _sorted_strings(candidates) + if len(candidates) < 2: + return None + return { + "exception_type": exception_type, + "source_bo_ids": [record["bo_id"]], + "candidate_values": candidates, + "basis_codes": sorted(common | {code for code in codes if code.startswith("AMBIGUOUS_")}), + "score_summary": [], + "conflict_codes": ["FINALIZATION_REQUIRES_IRREVERSIBLE_CHOICE"], + "allowed_decisions": ALLOWED_DECISIONS[exception_type], + "compact_context": { + "bo_id": record["bo_id"], + "source_domain": record.get("source_domain"), + "candidate_values": candidates, + "basis_codes": sorted(common), + }, + } + + def main() -> None: + fingerprint = None + try: + _initialize() + manifest_outer = _decode_json(LES0_MANIFEST_RAW, "LES0 manifest") + manifest = _as_dict(_as_dict(manifest_outer).get("les0_manifest")) + if manifest.get("schema_version") != "stage1_legal_structure_input_manifest.v2": + raise ValueError("LES0 manifest schema mismatch") + if manifest.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("LES0 manifest is not consumable") + if manifest.get("artifact_path") != INPUT_PATH: + raise ValueError("LES0 artifact path mismatch") + input_outer = _read_doc(INPUT_PATH) + input_pack = _as_dict(_as_dict(input_outer).get("legal_structure_input_pack")) + if input_pack.get("schema_version") != "stage1_legal_structure_input_pack.v2": + raise ValueError("input pack schema mismatch") + if input_pack.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("input pack status is not consumable") + fingerprint = input_pack.get("run_fingerprint") + if not fingerprint or fingerprint != manifest.get("run_fingerprint"): + raise ValueError("LES0 fingerprint mismatch") + records = _as_dict(input_pack.get("candidate_records_by_bo_id")) + if len(records) != int(manifest.get("candidate_count", -1)): + raise ValueError("LES0 candidate count mismatch") + evidence_authority = _as_dict(input_pack.get("evidence_authority_by_index")) + alias_map = _as_dict(input_pack.get("asset_alias_by_ref")) + + reviews = [] + review_index = {} + review_by_bo = defaultdict(list) + for source_review in _as_list(input_pack.get("input_review_items")): + if not isinstance(source_review, dict): + continue + code = source_review.get("review_code") + if code not in REVIEW_CODES: + continue + _add_review( + reviews, + review_index, + review_by_bo, + code, + source_review.get("source_bo_ids"), + source_review.get("evidence_indexes"), + source_review.get("related_ref_keys"), + str(source_review.get("reason_code") or "UPSTREAM_REVIEW"), + "HARD_WARNING" if code == "ASSET_ALIAS_LABEL_CONFLICT" else "WARNING", + "HUMAN_REVIEW" if code == "ASSET_ALIAS_LABEL_CONFLICT" else "STAGE_2", + ) + + issue_key_to_id = {} + asset_key_to_id = {} + issue_map = {} + asset_map = {} + seeds = [] + exception_drafts = [] + + for bo_id in sorted(records): + record = _as_dict(records[bo_id]) + if record.get("bo_id") != bo_id: + raise ValueError(f"candidate record key mismatch: {bo_id}") + types = _priority_types(record.get("canonical_structure_types"), record.get("source_domain")) + representative = types[0] + issue_keys, low_margin = _issue_keys(record, representative) + issue_ids = [] + for key in issue_keys: + if key not in issue_key_to_id: + issue_id = _stable_id("IC", len(issue_key_to_id) + 1) + issue_key_to_id[key] = issue_id + issue_map[issue_id] = { + "issue_cluster_id": issue_id, + "cluster_key": key, + "source_bo_ids": [], + "evidence_indexes": [], + "candidate_structure_types": [], + "basis_codes": [], + } + issue_id = issue_key_to_id[key] + issue_ids.append(issue_id) + row = issue_map[issue_id] + row["source_bo_ids"] = _sorted_strings(row["source_bo_ids"] + [bo_id]) + row["evidence_indexes"] = _sorted_strings(row["evidence_indexes"] + _strings(record.get("evidence_indexes"))) + row["candidate_structure_types"] = [item for item in CANONICAL_TYPES if item in row["candidate_structure_types"] + types] + row["basis_codes"] = _sorted_strings(row["basis_codes"] + _strings(record.get("basis_codes"))) + if low_margin: + _add_review( + reviews, review_index, review_by_bo, + "ISSUE_FAMILY_LOW_MARGIN", [bo_id], record.get("evidence_indexes"), [], + "GENERAL_LEGAL_EFFECT_FALLBACK", "WARNING", "STAGE_2", + ) + + asset_ids = [] + free_asset_count = 0 + for candidate in _as_list(record.get("asset_candidates")): + if not isinstance(candidate, dict): + continue + refs = _sorted_strings(candidate.get("asset_ref_keys")) + if refs: + keys = [f"ref:{ref}" for ref in refs] + unknown_refs = sorted(set(refs) - set(alias_map)) + if unknown_refs: + _add_review( + reviews, review_index, review_by_bo, + "ASSET_IDENTITY_DEFERRED_SEPARATE", [bo_id], candidate.get("evidence_indexes"), unknown_refs, + "UNKNOWN_SOURCE_REF", "HARD_WARNING", "HUMAN_REVIEW", + ) + else: + free_asset_count += 1 + keys = [f"free:{bo_id}:{candidate.get('asset_kind') or 'unknown'}:{_normalized(candidate.get('asset_label'))}"] + for key in keys: + if key not in asset_key_to_id: + asset_id = _stable_id("AC", len(asset_key_to_id) + 1) + asset_key_to_id[key] = asset_id + asset_map[asset_id] = { + "asset_cluster_id": asset_id, + "cluster_key": key, + "asset_kind": candidate.get("asset_kind") or "unknown", + "canonical_label_candidate": candidate.get("asset_label"), + "asset_ref_keys": refs, + "source_bo_ids": [], + "evidence_indexes": [], + } + asset_id = asset_key_to_id[key] + asset_ids.append(asset_id) + row = asset_map[asset_id] + row["source_bo_ids"] = _sorted_strings(row["source_bo_ids"] + [bo_id]) + row["evidence_indexes"] = _sorted_strings(row["evidence_indexes"] + _strings(candidate.get("evidence_indexes"))) + asset_ids = _sorted_strings(asset_ids) + if free_asset_count >= 2: + _add_review( + reviews, review_index, review_by_bo, + "ASSET_IDENTITY_DEFERRED_SEPARATE", [bo_id], record.get("evidence_indexes"), [], + "MULTIPLE_REF_FREE_ASSET_MENTIONS", "WARNING", "STAGE_2", + ) + bundle_refs = [_stable_id("BUNDLE", len(seeds) + 1)] if len(asset_ids) > 1 else [] + seed_id = _stable_id("LESS", len(seeds) + 1) + seed = { + "seed_id": seed_id, + "source_bo_ids": [bo_id], + "candidate_structure_type": representative, + "candidate_structure_types": types, + "issue_cluster_ids": _sorted_strings(issue_ids), + "asset_cluster_ids": asset_ids, + "multi_asset_bundle_refs": bundle_refs, + "evidence_indexes": _sorted_strings(record.get("evidence_indexes")), + "linked_claim_group_ids": _sorted_strings(record.get("linked_claim_group_ids")), + "linked_liability_group_ids": _sorted_strings(record.get("linked_liability_group_ids")), + "linked_actio_signal_ids": _sorted_strings(record.get("linked_actio_signal_ids")), + "legal_effect_roles": _sorted_strings([TYPE_ROLES[item] for item in types]), + "basis_codes": _sorted_strings(record.get("basis_codes")), + "confidence": "HIGH", + "pending_exception_ids": [], + "deterministic_review_ids": [], + } + draft = _explicit_exception_draft(record, issue_ids, asset_ids, types) + if draft: + exception_drafts.append((seed, draft)) + seeds.append(seed) + + exceptions = [] + for seed, draft in exception_drafts: + candidates = _strings(draft.get("candidate_values")) + preview = dict(draft) + if len(candidates) > MAX_EXCEPTION_CANDIDATE_VALUES or len(_artifact_text(preview).encode("utf-8")) > MAX_EXCEPTION_PACK_BYTES: + review_id = _add_review( + reviews, review_index, review_by_bo, + "PACK_CONTEXT_TOO_LARGE", draft.get("source_bo_ids"), [], [], + "EXCEPTION_ITEM_EXCEEDS_MICRO_PACK_BUDGET", "HARD_WARNING", "HUMAN_REVIEW", + ) + seed["deterministic_review_ids"].append(review_id) + continue + exception_id = _stable_id("LEX", len(exceptions) + 1) + item = {"exception_id": exception_id, **draft} + exceptions.append(item) + seed["pending_exception_ids"].append(exception_id) + + for seed in seeds: + bo_ids = seed["source_bo_ids"] + seed["deterministic_review_ids"] = _sorted_strings( + seed["deterministic_review_ids"] + [review_id for bo_id in bo_ids for review_id in review_by_bo.get(bo_id, [])] + ) + if seed["deterministic_review_ids"]: + seed["confidence"] = "MEDIUM" + + packs = [] + by_type = defaultdict(list) + for item in exceptions: + by_type[item["exception_type"]].append(item) + for exception_type in sorted(by_type): + rows = by_type[exception_type] + for offset in range(0, len(rows), TARGET_EXCEPTION_ITEMS_PER_PACK): + items = rows[offset:offset + TARGET_EXCEPTION_ITEMS_PER_PACK] + pack_id = _stable_id("LES-PACK", len(packs) + 1) + descriptor = { + "pack_id": pack_id, + "exception_type": exception_type, + "exception_ids": [item["exception_id"] for item in items], + "items": items, + "output_path": f"stage1_tmp/legal_effect_structure_adjudication_parts/PACK-{len(packs) + 1:03d}.json", + "run_fingerprint": fingerprint, + } + if len(_artifact_text(descriptor).encode("utf-8")) > MAX_EXCEPTION_PACK_BYTES: + raise ValueError(f"{pack_id}: serialized micro-pack exceeds {MAX_EXCEPTION_PACK_BYTES} bytes") + packs.append(descriptor) + if len(packs) > MAX_EXCEPTION_PACKS: + blocked_payload = { + "legal_structure_exception_pack": { + "schema_version": "stage1_legal_structure_exception_pack.v2", + "status": "BLOCKED", + "run_fingerprint": fingerprint, + "expected_exception_ids": [item["exception_id"] for item in exceptions], + "exceptions": exceptions, + "deterministic_review_items": reviews, + "pack_count": len(packs), + "packs": packs, + } + } + _write_doc(EXCEPTION_PATH, blocked_payload) + raise ValueError(f"exception pack count {len(packs)} exceeds {MAX_EXCEPTION_PACKS}") + + expected_ids = [item["exception_id"] for item in exceptions] + packed_ids = [exception_id for pack in packs for exception_id in pack["exception_ids"]] + if sorted(expected_ids) != sorted(packed_ids) or len(packed_ids) != len(set(packed_ids)): + raise ValueError("exception-to-pack exact coverage failed") + exception_status = "READY" if exceptions else ("READY_WITH_REVIEW" if reviews else "READY_NO_EXCEPTIONS") + exception_payload = { + "legal_structure_exception_pack": { + "schema_version": "stage1_legal_structure_exception_pack.v2", + "status": exception_status, + "run_fingerprint": fingerprint, + "expected_exception_ids": expected_ids, + "exceptions": exceptions, + "deterministic_review_items": reviews, + "pack_count": len(packs), + "packs": packs, + } + } + exception_hash = _artifact_hash(exception_payload) + referenced_evidence = sorted({evidence_id for seed in seeds for evidence_id in seed["evidence_indexes"]}) + seed_status = "READY_WITH_REVIEW" if reviews else "READY" + seed_payload = { + "legal_structure_seed_bundle": { + "schema_version": "stage1_legal_structure_seed_bundle.v2", + "status": seed_status, + "run_fingerprint": fingerprint, + "source_input_pack": { + "path": INPUT_PATH, + "schema_version": "stage1_legal_structure_input_pack.v2", + "sha256": _artifact_hash(input_outer), + }, + "structure_seed_items": seeds, + "issue_cluster_seed_map": issue_map, + "asset_cluster_seed_map": asset_map, + "evidence_index_seed": {key: evidence_authority[key] for key in referenced_evidence if key in evidence_authority}, + "deterministic_review_items": reviews, + "exception_pack_ref": { + "path": EXCEPTION_PATH, + "schema_version": "stage1_legal_structure_exception_pack.v2", + "sha256": exception_hash, + "expected_exception_ids": expected_ids, + }, + } + } + if {bo_id for seed in seeds for bo_id in seed["source_bo_ids"]} != set(records): + raise ValueError("candidate-to-seed BO conservation failed") + _write_doc(EXCEPTION_PATH, exception_payload) + _write_doc(SEED_PATH, seed_payload) + if _artifact_hash(_read_doc(EXCEPTION_PATH)) != exception_hash: + raise ValueError("exception pack post-write hash mismatch") + if _artifact_hash(_read_doc(SEED_PATH)) != _artifact_hash(seed_payload): + raise ValueError("seed bundle post-write hash mismatch") + manifest_status = "READY" if exceptions and not reviews else ("READY_WITH_REVIEW" if reviews else "READY_NO_EXCEPTIONS") + print(json.dumps({ + "les1_manifest": { + "schema_version": "stage1_legal_structure_les1_manifest.v2", + "status": manifest_status, + "seed_path": SEED_PATH, + "exception_pack_path": EXCEPTION_PATH, + "run_fingerprint": fingerprint, + "seed_count": len(seeds), + "exception_count": len(exceptions), + "review_count": len(reviews), + "pack_count": len(packs), + }, + "dynamic_fanout": packs, + }, ensure_ascii=False, separators=(",", ":"))) + except Exception as exc: + print(json.dumps({ + "les1_manifest": { + "schema_version": "stage1_legal_structure_les1_manifest.v2", + "status": "FAILED", + "seed_path": SEED_PATH, + "exception_pack_path": EXCEPTION_PATH, + "run_fingerprint": fingerprint, + "failure_reason": str(exc), + }, + "dynamic_fanout": [], + }, ensure_ascii=False, separators=(",", ":"))) + raise + + if __name__ == "__main__": + main() + + - task_name: Task_LES2_exception_adjudicator_* + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: medium + llm_verbosity: low + max_iterations: 2 + max_concurrency: 2 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + + TASK_NAME: Task_LES2_exception_adjudicator_* + STAGE: Stage 1 legal effect structure and index builder + ROLE: compact exception-only routing adjudicator + + + + pack_id: {{item.pack_id}} + exception_type: {{item.exception_type}} + exception_ids: {{item.exception_ids}} + items: {{item.items}} + output_path: {{item.output_path}} + run_fingerprint: {{item.run_fingerprint}} + + + + - Act as a senior Korean civil-litigation lawyer and AI systems architect, but make only the narrow routing decision authorized below. + - Use only ASSIGNED_PACK. Do not read, request, or reconstruct BO.json, evidence files, signal files, the LES0 input pack, the LES1 seed bundle, or any other pack. + - Do not create a fact, party, date, amount, BO ID, evidence index, exception ID, candidate value, cluster ID, or structure ID. + - Process every assigned item exactly once. Do not omit, merge, duplicate, or reorder exception IDs. + - Choose only a decision listed in that item's allowed_decisions. + - selected_value, selected_values, preserve_candidate_values, and split_plan candidate_values must be subsets of that item's candidate_values. + - If compact context is insufficient, use BLOCK_REVIEW. Never guess. + - Do not write legal_effect_structures.json, the seed bundle, or the exception pack. + - Write exactly one adjudication part to ASSIGNED_PACK.output_path with localdocs write_file and overwrite=true. + - After the write succeeds, return only the compact manifest JSON specified below. No markdown or prose. + + + + For ambiguous_issue_cluster, use only: + - USE_EXISTING_HARD_KEY when one listed hard-key candidate controls. + - PRESERVE_MULTI_ISSUE when all listed issue routes must remain. + - SPLIT_ISSUE_CLUSTER only when the listed candidates require separate structures and the split is supported by compact context. + - BLOCK_REVIEW otherwise. + + For ambiguous_asset_cluster, use only: + - SAME_CLUSTER when a listed exact ref or explicit alias proves identity. + - SEPARATE_CLUSTER when listed source refs differ or conservative separation is required. + - MULTI_ASSET_BUNDLE when all listed assets remain jointly relevant without identity merger. + - BLOCK_REVIEW otherwise. + + For ambiguous_candidate_structure_type, use only: + - SELECT_REPRESENTATIVE_TYPE when one listed canonical type must control finalization. + - PRESERVE_MULTI_ROUTING when all listed canonical routes remain valid. + - SPLIT_STRUCTURE_SEED only when separate structures are required and supported. + - BLOCK_REVIEW otherwise. + + + + Each decisions[] item must contain exactly: + { + "exception_id": "LEX-001", + "exception_type": "ambiguous_issue_cluster|ambiguous_asset_cluster|ambiguous_candidate_structure_type", + "source_bo_ids": [], + "decision": "one allowed enum", + "selected_value": null, + "selected_values": [], + "preserve_candidate_values": [], + "split_plan": [], + "review_required": false, + "reason_code": "HARD_KEY_CONTROLS|SOURCE_REF_IDENTITY|CONSERVATIVE_SEPARATION|PRESERVE_ALL_ROUTING|SPLIT_REQUIRED|INSUFFICIENT_COMPACT_CONTEXT", + "basis_codes_used": [], + "conflict_codes_acknowledged": [] + } + BLOCK_REVIEW and every split decision must set review_required=true. Other completed decisions set it to false. + + + + Write this raw JSON object to ASSIGNED_PACK.output_path: + { + "schema_version": "stage1_legal_structure_adjudication_part.v2", + "status": "READY|READY_WITH_BLOCKS|FAILED", + "pack_id": "the assigned pack_id", + "run_fingerprint": "the assigned run_fingerprint", + "exception_type": "the assigned exception_type", + "input_exception_ids": ["all assigned exception IDs exactly once"], + "decisions": ["one decision per assigned exception"], + "coverage": { + "expected": 0, + "actual": 0, + "missing_exception_ids": [], + "unknown_exception_ids": [], + "coverage_pass": true + } + } + Use READY_WITH_BLOCKS when any decision is BLOCK_REVIEW; otherwise use READY. + + + + After localdocs confirms the write, return exactly: + { + "pack_id": "the assigned pack_id", + "status": "READY|READY_WITH_BLOCKS|FAILED", + "output_path": "the assigned output_path", + "decision_count": 0 + } + + + + - task_name: Task_LES3_final_structure_index_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: |- + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + from collections import defaultdict + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MESSAGE_IDS = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + SEED_PATH = "stage1_tmp/legal_effect_structure_seed_bundle.json" + EXCEPTION_PATH = "stage1_tmp/legal_effect_structure_exception_pack.json" + TARGET_PATH = "legal_effect_structures.json" + CANONICAL_TYPES = [ + "money_claim", + "commercial_successor", + "secured_debt", + "registry_invalidity", + "land_use_gain", + "valuation", + "fraudulent_transfer", + "preserved_claim", + "succession_notice_lien_asset_defense", + "general_legal_effect", + ] + TYPE_ROLES = { + "money_claim": "claim_chain_ref", + "commercial_successor": "claim_chain_ref", + "secured_debt": "linked_structures", + "registry_invalidity": "linked_structures", + "land_use_gain": "legal_calculation_object", + "valuation": "legal_calculation_object", + "fraudulent_transfer": "actio_roles", + "preserved_claim": "claim_chain_ref", + "succession_notice_lien_asset_defense": "linked_structures", + "general_legal_effect": "legal_effect_roles", + } + ALLOWED_DECISIONS = { + "ambiguous_issue_cluster": {"USE_EXISTING_HARD_KEY", "PRESERVE_MULTI_ISSUE", "SPLIT_ISSUE_CLUSTER", "BLOCK_REVIEW"}, + "ambiguous_asset_cluster": {"SAME_CLUSTER", "SEPARATE_CLUSTER", "MULTI_ASSET_BUNDLE", "BLOCK_REVIEW"}, + "ambiguous_candidate_structure_type": {"SELECT_REPRESENTATIVE_TYPE", "PRESERVE_MULTI_ROUTING", "SPLIT_STRUCTURE_SEED", "BLOCK_REVIEW"}, + } + + def _next_id() -> int: + return next(MESSAGE_IDS) + + def _decode_json(value: Any, label: str) -> Any: + if not isinstance(value, str): + return value + text = value.strip() + if not text: + raise ValueError(f"{label}: empty JSON text") + if text.startswith("```"): + text = re.sub(r"^```(?:json)?\s*", "", text) + text = re.sub(r"\s*```$", "", text) + try: + return json.loads(text) + except json.JSONDecodeError: + try: + obj, _ = JSON_DECODER.raw_decode(text) + return obj + except json.JSONDecodeError as exc: + raise ValueError(f"{label}: invalid JSON: {text[:240]}") from exc + + def _parse_rpc(text: str) -> dict[str, Any]: + candidates = [line[5:].strip() for line in text.splitlines() if line.startswith("data:")] + candidates.append(text.strip()) + for candidate in candidates: + if not candidate: + continue + try: + value = _decode_json(candidate, "MCP response") + except ValueError: + continue + if isinstance(value, dict) and ("result" in value or "error" in value): + return value + raise RuntimeError(f"MCP response could not be parsed: {text[:300]}") + + def _initialize() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage1-part3-les3-v2", + "version": "2.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_rpc(response.text) + if parsed.get("error"): + raise RuntimeError(f"MCP {name} failed: {parsed['error']}") + return parsed + + def _read_doc(path: str) -> Any: + rpc = _call_tool("read_docs", {"doc_names": [path]}) + content = rpc.get("result", {}).get("content", []) + if not content or not isinstance(content[0], dict): + raise RuntimeError(f"{path}: read_docs returned no content") + envelope = _decode_json(content[0].get("text", ""), f"{path} envelope") + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results or not isinstance(results[0], dict): + raise RuntimeError(f"{path}: read_docs results are empty") + if results[0].get("error"): + raise RuntimeError(f"{path}: {results[0]['error']}") + inner = results[0].get("content", "") + else: + inner = envelope + return _decode_json(inner, path) + + def _artifact_text(payload: dict[str, Any]) -> str: + return json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n" + + def _artifact_hash(payload: dict[str, Any]) -> str: + return "sha256:" + hashlib.sha256(_artifact_text(payload).encode("utf-8")).hexdigest() + + def _write_doc(path: str, payload: dict[str, Any]) -> None: + _call_tool("write_file", {"path": path, "content": _artifact_text(payload), "overwrite": True}) + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + output = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in output: + output.append(text) + return output + + def _sorted_strings(value: Any) -> list[str]: + return sorted(set(_strings(value))) + + def _priority_types(value: Any) -> list[str]: + supplied = _strings(value) + unknown = sorted(set(supplied) - set(CANONICAL_TYPES)) + if unknown: + raise ValueError(f"non-canonical structure types: {unknown}") + if not supplied: + supplied = ["general_legal_effect"] + return [item for item in CANONICAL_TYPES if item in supplied] + + def _next_numeric_id(prefix: str, rows: dict[str, Any]) -> str: + numbers = [] + for key in rows: + match = re.fullmatch(re.escape(prefix) + r"-(\d+)", str(key)) + if match: + numbers.append(int(match.group(1))) + return f"{prefix}-{max(numbers, default=0) + 1:03d}" + + def _validate_decision(decision: dict[str, Any], expected: dict[str, Any]) -> None: + if decision.get("exception_id") != expected.get("exception_id"): + raise ValueError("decision exception_id mismatch") + exception_type = expected.get("exception_type") + if decision.get("exception_type") != exception_type: + raise ValueError(f"{expected.get('exception_id')}: exception_type mismatch") + choice = decision.get("decision") + if choice not in ALLOWED_DECISIONS.get(exception_type, set()): + raise ValueError(f"{expected.get('exception_id')}: invalid decision {choice}") + candidates = set(_strings(expected.get("candidate_values"))) + for field in ["selected_values", "preserve_candidate_values"]: + extras = set(_strings(decision.get(field))) - candidates + if extras: + raise ValueError(f"{expected.get('exception_id')}: {field} outside candidates: {sorted(extras)}") + selected = decision.get("selected_value") + if selected is not None and str(selected) not in candidates: + raise ValueError(f"{expected.get('exception_id')}: selected_value outside candidates") + for split in _as_list(decision.get("split_plan")): + values = split.get("candidate_values") if isinstance(split, dict) else split + extras = set(_strings(values)) - candidates + if extras: + raise ValueError(f"{expected.get('exception_id')}: split_plan outside candidates: {sorted(extras)}") + if choice == "BLOCK_REVIEW" and decision.get("review_required") is not True: + raise ValueError(f"{expected.get('exception_id')}: BLOCK_REVIEW must require review") + + def _load_inputs() -> tuple[dict[str, Any], dict[str, Any], dict[str, dict[str, Any]]]: + seed_outer = _read_doc(SEED_PATH) + exception_outer = _read_doc(EXCEPTION_PATH) + seed = _as_dict(_as_dict(seed_outer).get("legal_structure_seed_bundle")) + pack = _as_dict(_as_dict(exception_outer).get("legal_structure_exception_pack")) + if seed.get("schema_version") != "stage1_legal_structure_seed_bundle.v2": + raise ValueError("seed bundle schema mismatch") + if seed.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("seed bundle status is not consumable") + if pack.get("schema_version") != "stage1_legal_structure_exception_pack.v2": + raise ValueError("exception pack schema mismatch") + if pack.get("status") not in {"READY", "READY_NO_EXCEPTIONS", "READY_WITH_REVIEW"}: + raise ValueError("exception pack status is not consumable") + fingerprint = seed.get("run_fingerprint") + if not fingerprint or fingerprint != pack.get("run_fingerprint"): + raise ValueError("seed/exception fingerprint mismatch") + pack_ref = _as_dict(seed.get("exception_pack_ref")) + if pack_ref.get("path") != EXCEPTION_PATH or pack_ref.get("schema_version") != "stage1_legal_structure_exception_pack.v2": + raise ValueError("seed exception_pack_ref path/schema mismatch") + if pack_ref.get("sha256") != _artifact_hash(exception_outer): + raise ValueError("seed exception_pack_ref hash mismatch") + expected_rows = {} + for item in _as_list(pack.get("exceptions")): + if not isinstance(item, dict) or not item.get("exception_id"): + raise ValueError("exception item missing exception_id") + exception_id = str(item["exception_id"]) + if exception_id in expected_rows: + raise ValueError(f"duplicate exception_id: {exception_id}") + expected_rows[exception_id] = item + expected_ids = _strings(pack.get("expected_exception_ids")) + if set(expected_ids) != set(expected_rows) or len(expected_ids) != len(expected_rows): + raise ValueError("expected exception ID set mismatch") + packs = [row for row in _as_list(pack.get("packs")) if isinstance(row, dict)] + if int(pack.get("pack_count", -1)) != len(packs): + raise ValueError("exception pack count mismatch") + packed_ids = [exception_id for row in packs for exception_id in _strings(row.get("exception_ids"))] + if set(packed_ids) != set(expected_ids) or len(packed_ids) != len(set(packed_ids)): + raise ValueError("pack-to-exception exact coverage failed") + decisions = {} + for descriptor in packs: + output_path = descriptor.get("output_path") + if not isinstance(output_path, str) or not output_path: + raise ValueError("pack descriptor missing output_path") + part = _as_dict(_read_doc(output_path)) + if part.get("schema_version") != "stage1_legal_structure_adjudication_part.v2": + raise ValueError(f"{output_path}: adjudication schema mismatch") + if part.get("status") not in {"READY", "READY_WITH_BLOCKS"}: + raise ValueError(f"{output_path}: adjudication status is not consumable") + if part.get("pack_id") != descriptor.get("pack_id") or part.get("run_fingerprint") != fingerprint: + raise ValueError(f"{output_path}: pack ID/fingerprint mismatch") + part_expected = _strings(part.get("input_exception_ids")) + descriptor_expected = _strings(descriptor.get("exception_ids")) + if set(part_expected) != set(descriptor_expected) or len(part_expected) != len(descriptor_expected): + raise ValueError(f"{output_path}: input exception coverage mismatch") + coverage = _as_dict(part.get("coverage")) + if coverage.get("coverage_pass") is not True: + raise ValueError(f"{output_path}: reported coverage failure") + for decision in _as_list(part.get("decisions")): + if not isinstance(decision, dict) or not decision.get("exception_id"): + raise ValueError(f"{output_path}: decision missing exception_id") + exception_id = str(decision["exception_id"]) + if exception_id not in expected_rows: + raise ValueError(f"{output_path}: unknown exception ID {exception_id}") + if exception_id in decisions: + raise ValueError(f"duplicate decision for {exception_id}") + _validate_decision(decision, expected_rows[exception_id]) + decisions[exception_id] = decision + if set(decisions) != set(expected_ids): + missing = sorted(set(expected_ids) - set(decisions)) + unknown = sorted(set(decisions) - set(expected_ids)) + raise ValueError(f"decision exact coverage failed; missing={missing}; unknown={unknown}") + return seed, pack, decisions + + def _decision_groups(seed: dict[str, Any], decisions: dict[str, dict[str, Any]]) -> dict[str, list[dict[str, Any]]]: + output = defaultdict(list) + for exception_id in _strings(seed.get("pending_exception_ids")): + if exception_id not in decisions: + raise ValueError(f"seed references missing decision: {exception_id}") + output[decisions[exception_id]["exception_type"]].append(decisions[exception_id]) + return output + + def _split_values(decision: dict[str, Any]) -> list[list[str]]: + groups = [] + for row in _as_list(decision.get("split_plan")): + values = row.get("candidate_values") if isinstance(row, dict) else row + values = _strings(values) + if values: + groups.append(values) + if not groups: + values = _strings(decision.get("selected_values")) + groups = [[value] for value in values] + return groups + + def _apply_issue_decisions(ids: list[str], rows: list[dict[str, Any]], issue_map: dict[str, Any], bo_ids: list[str]) -> tuple[list[str], bool]: + output = list(ids) + split_review = False + for decision in rows: + choice = decision.get("decision") + if choice == "USE_EXISTING_HARD_KEY": + selected = _strings(decision.get("selected_value")) + _strings(decision.get("selected_values")) + selected = [value for value in selected if value in issue_map] + if selected: + output = selected + elif choice == "SPLIT_ISSUE_CLUSTER": + split_review = True + output = [] + for group in _split_values(decision): + existing = [value for value in group if value in issue_map] + if existing: + output.extend(existing) + else: + issue_id = _next_numeric_id("IC", issue_map) + issue_map[issue_id] = { + "issue_cluster_id": issue_id, + "cluster_key": "adjudicated:" + "|".join(group), + "source_bo_ids": bo_ids, + "evidence_indexes": [], + "candidate_structure_types": [], + "basis_codes": ["LES2_SPLIT_ISSUE_CLUSTER"], + } + output.append(issue_id) + return _sorted_strings(output), split_review + + def _apply_asset_decisions(ids: list[str], rows: list[dict[str, Any]], asset_map: dict[str, Any], bo_ids: list[str]) -> tuple[list[str], list[str], bool]: + output = list(ids) + bundle_refs = [] + split_review = False + for decision in rows: + choice = decision.get("decision") + values = _strings(decision.get("selected_value")) + _strings(decision.get("selected_values")) + existing = [value for value in values if value in asset_map] + if choice == "SAME_CLUSTER" and existing: + output = [existing[0]] + elif choice == "SEPARATE_CLUSTER" and existing: + output = existing + elif choice == "MULTI_ASSET_BUNDLE": + if existing: + output = existing + bundle_refs.append("BUNDLE-ADJ-" + str(decision.get("exception_id"))) + if choice in {"SEPARATE_CLUSTER", "MULTI_ASSET_BUNDLE"}: + split_review = bool(decision.get("review_required")) + return _sorted_strings(output), _sorted_strings(bundle_refs), split_review + + def _type_plans(types: list[str], representative: str, rows: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], bool]: + plans = [{"types": types, "representative": representative, "split_label": None}] + split_review = False + for decision in rows: + choice = decision.get("decision") + if choice == "SELECT_REPRESENTATIVE_TYPE": + selected = str(decision.get("selected_value") or representative) + if selected not in types: + raise ValueError(f"{decision.get('exception_id')}: selected type not in seed types") + plans = [{"types": types, "representative": selected, "split_label": None}] + elif choice == "PRESERVE_MULTI_ROUTING": + plans = [{"types": types, "representative": representative, "split_label": None}] + elif choice == "SPLIT_STRUCTURE_SEED": + split_review = True + groups = _split_values(decision) + plans = [] + for ordinal, group in enumerate(groups, 1): + group_types = _priority_types(group) + plans.append({"types": group_types, "representative": group_types[0], "split_label": f"split-{ordinal}"}) + return plans, split_review + + def _build_structures(seed: dict[str, Any], decisions: dict[str, dict[str, Any]]) -> tuple[list[dict[str, Any]], dict[str, Any], dict[str, Any]]: + issue_map = {key: dict(value) for key, value in _as_dict(seed.get("issue_cluster_seed_map")).items() if isinstance(value, dict)} + asset_map = {key: dict(value) for key, value in _as_dict(seed.get("asset_cluster_seed_map")).items() if isinstance(value, dict)} + structures = [] + for seed_item in _as_list(seed.get("structure_seed_items")): + if not isinstance(seed_item, dict): + raise ValueError("structure seed item must be object") + bo_ids = _sorted_strings(seed_item.get("source_bo_ids")) + if not bo_ids: + raise ValueError("structure seed item missing source BO") + groups = _decision_groups(seed_item, decisions) + issue_ids, issue_split = _apply_issue_decisions( + _strings(seed_item.get("issue_cluster_ids")), groups.get("ambiguous_issue_cluster", []), issue_map, bo_ids + ) + asset_ids, adjudicated_bundles, asset_split = _apply_asset_decisions( + _strings(seed_item.get("asset_cluster_ids")), groups.get("ambiguous_asset_cluster", []), asset_map, bo_ids + ) + types = _priority_types(seed_item.get("candidate_structure_types")) + representative = str(seed_item.get("candidate_structure_type") or types[0]) + if representative not in types: + representative = types[0] + plans, type_split = _type_plans(types, representative, groups.get("ambiguous_candidate_structure_type", [])) + pending_ids = _strings(seed_item.get("pending_exception_ids")) + blocked_ids = [exception_id for exception_id in pending_ids if decisions[exception_id].get("decision") == "BLOCK_REVIEW"] + resolved_ids = [exception_id for exception_id in pending_ids if exception_id not in blocked_ids] + deterministic_review_ids = _sorted_strings(seed_item.get("deterministic_review_ids")) + review_required = bool(deterministic_review_ids or blocked_ids or issue_split or asset_split or type_split) + bundle_refs = _sorted_strings(_strings(seed_item.get("multi_asset_bundle_refs")) + adjudicated_bundles) + for plan in plans: + structure_id = f"LES-{len(structures) + 1:03d}" + plan_types = _priority_types(plan["types"]) + plan_representative = plan["representative"] if plan["representative"] in plan_types else plan_types[0] + suffix = f" ({plan['split_label']})" if plan.get("split_label") else "" + sources = ["BO.json", "legal_effect_signals.json"] + if seed_item.get("linked_claim_group_ids") or seed_item.get("linked_liability_group_ids"): + sources.append("case_liability_signals.json") + if seed_item.get("linked_actio_signal_ids"): + sources.append("actio_case_signals.json") + structures.append({ + "structure_id": structure_id, + "candidate_structure_type": plan_representative, + "candidate_structure_types": plan_types, + "structure_label": f"{plan_representative} structure for {', '.join(bo_ids)}{suffix}", + "source_bo_ids": bo_ids, + "issue_cluster_ids": issue_ids, + "asset_cluster_ids": asset_ids, + "evidence_indexes": _sorted_strings(seed_item.get("evidence_indexes")), + "linked_claim_group_ids": _sorted_strings(seed_item.get("linked_claim_group_ids")), + "linked_liability_group_ids": _sorted_strings(seed_item.get("linked_liability_group_ids")), + "linked_actio_signal_ids": _sorted_strings(seed_item.get("linked_actio_signal_ids")), + "legal_effect_roles": _sorted_strings([TYPE_ROLES[item] for item in plan_types]), + "basis": { + "primary_fields": _sorted_strings(seed_item.get("basis_codes")), + "source_signal_files": sorted(set(sources)), + "source_evidence_indexes": _sorted_strings(seed_item.get("evidence_indexes")), + }, + "review_required": review_required, + "routing_audit": { + "source_seed_id": seed_item.get("seed_id"), + "candidate_structure_types": plan_types, + "multi_asset_bundle_refs": bundle_refs, + "resolved_exception_ids": _sorted_strings(resolved_ids), + "blocked_exception_ids": _sorted_strings(blocked_ids), + "deterministic_review_ids": deterministic_review_ids, + }, + }) + return structures, issue_map, asset_map + + def _merge(row: dict[str, Any], field: str, values: Any) -> None: + row[field] = _sorted_strings(_strings(row.get(field)) + _strings(values)) + + def _build_index(structures: list[dict[str, Any]], issue_map: dict[str, Any], asset_map: dict[str, Any], evidence_seed: dict[str, Any]) -> dict[str, Any]: + by_bo = {} + by_issue = {} + by_asset = {} + by_evidence = {} + by_type = {} + for structure in structures: + sid = structure["structure_id"] + types = structure["candidate_structure_types"] + routing = _as_dict(structure.get("routing_audit")) + basis = _as_dict(structure.get("basis")) + for bo_id in structure["source_bo_ids"]: + row = by_bo.setdefault(bo_id, { + "bo_id": bo_id, "structure_ids": [], "structure_labels": [], "issue_cluster_ids": [], + "asset_cluster_ids": [], "evidence_indexes": [], "candidate_structure_types": [], + "claim_group_ids": [], "liability_group_ids": [], "related_actio_signal_ids": [], + "legal_effect_roles": [], "basis_fields": [], "multi_asset_bundle_refs": [], "review_required": False, + }) + _merge(row, "structure_ids", [sid]) + _merge(row, "structure_labels", [structure["structure_label"]]) + _merge(row, "issue_cluster_ids", structure["issue_cluster_ids"]) + _merge(row, "asset_cluster_ids", structure["asset_cluster_ids"]) + _merge(row, "evidence_indexes", structure["evidence_indexes"]) + row["candidate_structure_types"] = [item for item in CANONICAL_TYPES if item in row["candidate_structure_types"] + types] + _merge(row, "claim_group_ids", structure["linked_claim_group_ids"]) + _merge(row, "liability_group_ids", structure["linked_liability_group_ids"]) + _merge(row, "related_actio_signal_ids", structure["linked_actio_signal_ids"]) + _merge(row, "legal_effect_roles", structure["legal_effect_roles"]) + _merge(row, "basis_fields", basis.get("primary_fields")) + _merge(row, "multi_asset_bundle_refs", routing.get("multi_asset_bundle_refs")) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + for issue_id in structure["issue_cluster_ids"]: + source = _as_dict(issue_map.get(issue_id)) + row = by_issue.setdefault(issue_id, { + "issue_cluster_id": issue_id, "cluster_key": source.get("cluster_key"), "source_bo_ids": [], + "evidence_indexes": [], "candidate_structure_types": [], "structure_ids": [], "review_required": False, + }) + _merge(row, "source_bo_ids", structure["source_bo_ids"]) + _merge(row, "evidence_indexes", structure["evidence_indexes"]) + row["candidate_structure_types"] = [item for item in CANONICAL_TYPES if item in row["candidate_structure_types"] + types] + _merge(row, "structure_ids", [sid]) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + for asset_id in structure["asset_cluster_ids"]: + source = _as_dict(asset_map.get(asset_id)) + row = by_asset.setdefault(asset_id, { + "asset_cluster_id": asset_id, "cluster_key": source.get("cluster_key"), + "asset_kind": source.get("asset_kind"), "canonical_label_candidate": source.get("canonical_label_candidate"), + "asset_ref_keys": _sorted_strings(source.get("asset_ref_keys")), "source_bo_ids": [], + "evidence_indexes": [], "issue_cluster_ids": [], "candidate_structure_types": [], + "structure_ids": [], "review_required": False, + }) + _merge(row, "source_bo_ids", structure["source_bo_ids"]) + _merge(row, "evidence_indexes", structure["evidence_indexes"]) + _merge(row, "issue_cluster_ids", structure["issue_cluster_ids"]) + row["candidate_structure_types"] = [item for item in CANONICAL_TYPES if item in row["candidate_structure_types"] + types] + _merge(row, "structure_ids", [sid]) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + for evidence_index in structure["evidence_indexes"]: + source = _as_dict(evidence_seed.get(evidence_index)) + row = by_evidence.setdefault(evidence_index, { + "evidence_index": evidence_index, "source_title": source.get("source_title"), + "priority_class": source.get("priority_class"), "authentication_status": source.get("authentication_status"), + "source_bo_ids": [], "structure_ids": [], "issue_cluster_ids": [], "asset_cluster_ids": [], + "candidate_structure_types": [], "related_event_candidate_ids": [], "related_signal_refs": [], + "support_role": "primary", "gap_flags": [], + }) + _merge(row, "source_bo_ids", structure["source_bo_ids"]) + _merge(row, "structure_ids", [sid]) + _merge(row, "issue_cluster_ids", structure["issue_cluster_ids"]) + _merge(row, "asset_cluster_ids", structure["asset_cluster_ids"]) + row["candidate_structure_types"] = [item for item in CANONICAL_TYPES if item in row["candidate_structure_types"] + types] + _merge(row, "related_signal_refs", structure["linked_claim_group_ids"] + structure["linked_liability_group_ids"] + structure["linked_actio_signal_ids"]) + for candidate_type in types: + row = by_type.setdefault(candidate_type, { + "candidate_structure_type": candidate_type, "structure_ids": [], "source_bo_ids": [], + "issue_cluster_ids": [], "asset_cluster_ids": [], "evidence_indexes": [], + "claim_group_ids": [], "liability_group_ids": [], "related_actio_signal_ids": [], + "downstream_fact_roles": [], "basis_fields": [], "review_required": False, + }) + _merge(row, "structure_ids", [sid]) + _merge(row, "source_bo_ids", structure["source_bo_ids"]) + _merge(row, "issue_cluster_ids", structure["issue_cluster_ids"]) + _merge(row, "asset_cluster_ids", structure["asset_cluster_ids"]) + _merge(row, "evidence_indexes", structure["evidence_indexes"]) + _merge(row, "claim_group_ids", structure["linked_claim_group_ids"]) + _merge(row, "liability_group_ids", structure["linked_liability_group_ids"]) + _merge(row, "related_actio_signal_ids", structure["linked_actio_signal_ids"]) + _merge(row, "downstream_fact_roles", structure["legal_effect_roles"]) + _merge(row, "basis_fields", basis.get("primary_fields")) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + return { + "by_bo_id": by_bo, + "by_issue_cluster_id": by_issue, + "by_asset_cluster_id": by_asset, + "by_evidence_index": by_evidence, + "by_candidate_structure_type": by_type, + } + + def _quality_gate(structures: list[dict[str, Any]], index: dict[str, Any], seed: dict[str, Any], pack: dict[str, Any], decisions: dict[str, dict[str, Any]]) -> dict[str, Any]: + seed_items = [item for item in _as_list(seed.get("structure_seed_items")) if isinstance(item, dict)] + expected_bo = sorted({bo for item in seed_items for bo in _strings(item.get("source_bo_ids"))}) + covered_bo = sorted(index["by_bo_id"]) + expected_evidence = sorted({evidence for item in seed_items for evidence in _strings(item.get("evidence_indexes"))}) + covered_evidence = sorted(index["by_evidence_index"]) + expected_exception_ids = _strings(pack.get("expected_exception_ids")) + blocked_ids = sorted(exception_id for exception_id, decision in decisions.items() if decision.get("decision") == "BLOCK_REVIEW") + resolved_ids = sorted(set(expected_exception_ids) - set(blocked_ids)) + reviews = [] + for item in _as_list(seed.get("deterministic_review_items")): + if isinstance(item, dict): + reviews.append({ + "review_type": "deterministic_review", + "review_id": item.get("review_id"), + "review_code": item.get("review_code"), + "source_bo_ids": _sorted_strings(item.get("source_bo_ids")), + "evidence_indexes": _sorted_strings(item.get("evidence_indexes")), + "downstream_owner": item.get("downstream_owner"), + }) + for exception_id in blocked_ids: + decision = decisions[exception_id] + reviews.append({ + "review_type": "blocked_exception", + "exception_id": exception_id, + "exception_type": decision.get("exception_type"), + "source_bo_ids": _sorted_strings(decision.get("source_bo_ids")), + "reason_code": decision.get("reason_code"), + }) + split_structure_ids = [ + structure["structure_id"] for structure in structures + if structure.get("review_required") and ( + _as_dict(structure.get("routing_audit")).get("resolved_exception_ids") + and "split" in structure.get("structure_label", "") + ) + ] + for structure_id in split_structure_ids: + reviews.append({"review_type": "review_sensitive_split", "structure_id": structure_id}) + return { + "bo_coverage": { + "source_bo_count": len(expected_bo), "covered_bo_count": len(covered_bo), + "missing_bo_ids": sorted(set(expected_bo) - set(covered_bo)), + "unknown_bo_ids": sorted(set(covered_bo) - set(expected_bo)), + "coverage_pass": expected_bo == covered_bo, + }, + "evidence_coverage": { + "source_evidence_count": len(expected_evidence), "covered_evidence_count": len(covered_evidence), + "missing_evidence_indexes": sorted(set(expected_evidence) - set(covered_evidence)), + "coverage_pass": set(expected_evidence).issubset(set(covered_evidence)), + }, + "exception_coverage": { + "expected_exception_count": len(expected_exception_ids), + "resolved_exception_count": len(resolved_ids), + "blocked_exception_count": len(blocked_ids), + "missing_exception_ids": sorted(set(expected_exception_ids) - set(decisions)), + "coverage_pass": set(expected_exception_ids) == set(decisions), + }, + "run_fingerprint_gate": {"match": seed.get("run_fingerprint") == pack.get("run_fingerprint")}, + "review_summary": { + "structure_review_count": sum(1 for structure in structures if structure.get("review_required")), + "deterministic_review_count": len(_as_list(seed.get("deterministic_review_items"))), + "blocked_exception_count": len(blocked_ids), + }, + "review_queue": reviews, + } + + def _validate_final(payload: dict[str, Any]) -> None: + if payload.get("schema_version") != "stage1_legal_effect_structures.v1": + raise ValueError("final schema mismatch") + if payload.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("final status invalid") + structures = _as_list(payload.get("legal_effect_structures")) + if not structures: + raise ValueError("final structures must be non-empty") + index = _as_dict(payload.get("structure_index")) + required_indexes = ["by_bo_id", "by_issue_cluster_id", "by_asset_cluster_id", "by_evidence_index", "by_candidate_structure_type"] + for key in required_indexes: + if not isinstance(index.get(key), dict): + raise ValueError(f"structure_index.{key} must be object") + structure_ids = [str(_as_dict(row).get("structure_id")) for row in structures] + if len(structure_ids) != len(set(structure_ids)): + raise ValueError("structure IDs must be unique") + for structure in structures: + if not isinstance(structure, dict): + raise ValueError("structure must be object") + types = _priority_types(structure.get("candidate_structure_types")) + if structure.get("candidate_structure_type") not in types: + raise ValueError(f"{structure.get('structure_id')}: representative type missing from type array") + audit_types = _priority_types(_as_dict(structure.get("routing_audit")).get("candidate_structure_types")) + if types != audit_types: + raise ValueError(f"{structure.get('structure_id')}: top-level/audit type mismatch") + sid = structure.get("structure_id") + for bo_id in _strings(structure.get("source_bo_ids")): + if sid not in _strings(_as_dict(index["by_bo_id"].get(bo_id)).get("structure_ids")): + raise ValueError(f"{sid}: missing BO index link {bo_id}") + for issue_id in _strings(structure.get("issue_cluster_ids")): + if issue_id not in index["by_issue_cluster_id"]: + raise ValueError(f"{sid}: missing issue index {issue_id}") + for asset_id in _strings(structure.get("asset_cluster_ids")): + if asset_id not in index["by_asset_cluster_id"]: + raise ValueError(f"{sid}: missing asset index {asset_id}") + for evidence_index in _strings(structure.get("evidence_indexes")): + if evidence_index not in index["by_evidence_index"]: + raise ValueError(f"{sid}: missing evidence index {evidence_index}") + for candidate_type in types: + if sid not in _strings(_as_dict(index["by_candidate_structure_type"].get(candidate_type)).get("structure_ids")): + raise ValueError(f"{sid}: missing type index {candidate_type}") + gate = _as_dict(payload.get("quality_gate")) + for key in ["bo_coverage", "evidence_coverage", "exception_coverage"]: + if _as_dict(gate.get(key)).get("coverage_pass") is not True: + raise ValueError(f"quality gate failed: {key}") + if _as_dict(gate.get("run_fingerprint_gate")).get("match") is not True: + raise ValueError("quality gate failed: run_fingerprint") + + def main() -> None: + try: + _initialize() + seed, pack, decisions = _load_inputs() + structures, issue_map, asset_map = _build_structures(seed, decisions) + index = _build_index(structures, issue_map, asset_map, _as_dict(seed.get("evidence_index_seed"))) + gate = _quality_gate(structures, index, seed, pack, decisions) + status = "READY_WITH_REVIEW" if gate["review_queue"] else "READY" + payload = { + "schema_version": "stage1_legal_effect_structures.v1", + "status": status, + "legal_effect_structures": structures, + "structure_index": index, + "quality_gate": gate, + } + _validate_final(payload) + _write_doc(TARGET_PATH, payload) + reread = _read_doc(TARGET_PATH) + if _artifact_hash(reread) != _artifact_hash(payload): + raise ValueError("final post-write reread mismatch") + _validate_final(reread) + print(json.dumps({ + "legal_effect_structures_writer": { + "schema_version": "stage1_legal_effect_structures_writer.v2", + "status": "READY", + "written_file": TARGET_PATH, + "payload_status": status, + "structure_count": len(structures), + "exception_count": len(_strings(pack.get("expected_exception_ids"))), + "review_queue_count": len(gate["review_queue"]), + "run_fingerprint": seed.get("run_fingerprint"), + } + }, ensure_ascii=False, separators=(",", ":"))) + except Exception as exc: + print(json.dumps({ + "legal_effect_structures_writer": { + "schema_version": "stage1_legal_effect_structures_writer.v2", + "status": "FAILED", + "target_file": TARGET_PATH, + "failure_reason": str(exc), + } + }, ensure_ascii=False, separators=(",", ":"))) + raise + + if __name__ == "__main__": + main() diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Codex YAML/Stage_1_Part_4_Codex_v3.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Codex YAML/Stage_1_Part_4_Codex_v3.yml new file mode 100644 index 00000000..a0586631 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Codex YAML/Stage_1_Part_4_Codex_v3.yml @@ -0,0 +1,2197 @@ +--- +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_4 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Fact Ledger Generation + version: v2.0 + Stages: + - name: stage1_fact_ledger_generation + description: BO, Legal Effect 사용해서 Fact_Ledger_base.json 생성 + tools: + mcpServers: + localdocs: + type: streamable-http + url: http://mcp-localdocs:8012/mcp + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + + task_procedure: + IN: + nexts: + - Task_FL0_fact_source_pack_compiler + wait_until: [] + Task_FL0_fact_source_pack_compiler: + nexts: + - Task_FL1_deterministic_fact_ledger_candidate_builder + wait_until: + - IN + Task_FL1_deterministic_fact_ledger_candidate_builder: + nexts: + - Task_FL2_fact_exception_adjudicator_* + - Task_FL3_final_fact_ledger_gate_and_writer + wait_until: + - Task_FL0_fact_source_pack_compiler + Task_FL2_fact_exception_adjudicator_*: + nexts: + - Task_FL3_final_fact_ledger_gate_and_writer + wait_until: + - Task_FL1_deterministic_fact_ledger_candidate_builder + Task_FL3_final_fact_ledger_gate_and_writer: + nexts: + - OUT + wait_until: + - Task_FL1_deterministic_fact_ledger_candidate_builder + - all Task_FL2_fact_exception_adjudicator_* + OUT: + nexts: [] + wait_until: + - Task_FL3_final_fact_ledger_gate_and_writer + + tasks: + - task_name: Task_FL0_fact_source_pack_compiler + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import copy + import hashlib + import itertools + import json + import re + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _safe_error(exc: BaseException) -> str: + return re.sub(r"\s+", " ", str(exc)).strip()[:300] + + + def _parse_json_value(raw: Any, label: str, allow_trailing: bool = False) -> Any: + if isinstance(raw, (dict, list)): + return raw + if not isinstance(raw, str): + raise ValueError(f"{label} is not JSON text") + text = raw.lstrip("\ufeff").strip() + if not text: + raise ValueError(f"{label} is empty") + try: + return json.loads(text) + except json.JSONDecodeError: + try: + value, end = JSON_DECODER.raw_decode(text) + except json.JSONDecodeError as exc: + raise ValueError(f"{label} invalid JSON: {text[:300]}") from exc + if not allow_trailing and text[end:].strip(): + raise ValueError(f"{label} has trailing content: {text[end:end + 300]}") + return value + + + def _parse_mcp_response(text: str) -> dict[str, Any]: + parsed: dict[str, Any] | None = None + for line in text.strip().splitlines(): + if not line.startswith("data: "): + continue + try: + item = _parse_json_value(line[6:], "MCP SSE data", True) + except ValueError: + continue + if isinstance(item, dict): + parsed = item + if parsed is None: + item = _parse_json_value(text, "MCP response", True) + if not isinstance(item, dict): + raise RuntimeError("MCP response root is not an object") + parsed = item + return parsed + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-fact-ledger-fl0-source-compiler", + "version": "2.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_mcp_response(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP initialize failed: {payload['error']}") + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + initialized = CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ) + initialized.raise_for_status() + + + def _call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_msg_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_mcp_response(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP {name} failed: {payload['error']}") + result = payload.get("result") + if not isinstance(result, dict) or result.get("isError") is True: + raise RuntimeError(f"MCP {name} returned an invalid result") + return result + + + def _extract_doc(result: dict[str, Any], doc_name: str) -> Any: + blocks = result.get("content") or [] + if not blocks or not isinstance(blocks[0], dict): + raise RuntimeError(f"Empty response: {doc_name}") + text = blocks[0].get("text") + if not isinstance(text, str) or not text.strip(): + raise RuntimeError(f"Empty text response: {doc_name}") + outer = _parse_json_value(text, f"{doc_name} outer envelope", True) + inner: Any = outer + if isinstance(outer, dict) and "results" in outer: + results = outer.get("results") or [] + if not results or not isinstance(results[0], dict): + raise RuntimeError(f"Empty results envelope: {doc_name}") + inner = results[0].get("content") + if inner in (None, ""): + inner = results[0].get("text") + if isinstance(inner, (dict, list)): + return inner + return _parse_json_value(inner, f"{doc_name} inner document", False) + + + def _read_json(path: str) -> Any: + return _extract_doc(_call_tool("read_docs", {"doc_names": [path]}), path) + + + def _write_text(path: str, content: str) -> None: + _call_tool("write_file", {"path": path, "content": content, "overwrite": True}) + + + def _canonical(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + + def _digest(value: Any) -> str: + return "sha256:" + hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest() + + + def _write_json(path: str, value: Any) -> None: + _write_text(path, json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2)) + + + def _verify_reread(path: str, expected: Any) -> None: + actual = _read_json(path) + if _digest(actual) != _digest(expected): + raise RuntimeError(f"Post-write verification failed: {path}") + + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _strings(value: Any) -> list[str]: + values = value if isinstance(value, list) else [value] + out: list[str] = [] + for item in values: + if item is None: + continue + if isinstance(item, str): + text = item.strip() + elif isinstance(item, (int, float)) and not isinstance(item, bool): + text = str(item) + else: + continue + if text and text not in out: + out.append(text) + return out + + + def _sorted_strings(value: Any) -> list[str]: + return sorted(set(_strings(value))) + + + def _text(value: Any, limit: int = 500) -> str: + if value in (None, "", [], {}): + return "" + raw = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, sort_keys=True) + return re.sub(r"\s+", " ", raw).strip()[:limit] + + INPUT_FILES = { + "bo": "BO.json", + "structures": "legal_effect_structures.json", + "actio": "actio_case_signals.json", + "liability": "case_liability_signals.json", + "legal_effect": "legal_effect_signals.json", + "evidence": "evidence_indexed.json", + } + OUT_PATH = "stage1_tmp/fact_ledger/fact_source_pack.json" + STRUCTURE_INDEX_KEYS = ( + "by_bo_id", + "by_issue_cluster_id", + "by_asset_cluster_id", + "by_evidence_index", + "by_candidate_structure_type", + ) + DOMAIN_KEYS = ( + "money_claim_details", + "possession_state", + "lien_state", + "notice_lifecycle", + "asset_alias", + "fractional_effect_events", + ) + + + def _bo_id(bo: dict[str, Any]) -> str: + return _text(bo.get("BO_ID") or bo.get("id"), 120) + + + def _evidence_id(row: dict[str, Any]) -> str: + return _text(row.get("evidence_index") or row.get("evidence_id") or row.get("id"), 120) + + + def _sanitize(value: Any, depth: int = 0) -> Any: + if depth > 8: + return None + if isinstance(value, dict): + return {str(k): _sanitize(v, depth + 1) for k, v in sorted(value.items())} + if isinstance(value, list): + out: list[Any] = [] + seen: set[str] = set() + for item in value: + compact = _sanitize(item, depth + 1) + key = _canonical(compact) + if key not in seen: + seen.add(key) + out.append(compact) + return out + if isinstance(value, str): + return _text(value, 500) + if value is None or isinstance(value, (bool, int, float)): + return value + return _text(value, 500) + + + def _bo_evidence_refs(bo: dict[str, Any]) -> list[str]: + refs = _strings(bo.get("source_evidence_indexes")) + for row in _as_list(bo.get("Evidence")): + if isinstance(row, dict): + refs.extend(_strings(row.get("evidence_index"))) + return sorted(set(refs)) + + + def _compact_bo(bo: dict[str, Any]) -> dict[str, Any]: + core = _as_dict(bo.get("core_field_base")) + payload = _as_dict(_as_dict(bo.get("extensions")).get("domain_payload")) + domain = {key: _sanitize(payload.get(key)) for key in DOMAIN_KEYS if payload.get(key) not in (None, {}, [])} + juristic = bo.get("JuristicAct") + return { + "bo_id": _bo_id(bo), + "bo_type": bo.get("BOType") or bo.get("bo_type"), + "action": bo.get("Action") or bo.get("action"), + "action_type": bo.get("ActionType") or bo.get("action_type"), + "juristic_act": _sanitize(juristic), + "reason": _text(bo.get("Reason") or bo.get("reason"), 500) or None, + "prior_act": _sanitize(bo.get("PriorAct") or bo.get("prior_act")), + "legal_keywords": _sorted_strings(bo.get("Legal_Keywords") or bo.get("legal_keywords")), + "amount": _sanitize(bo.get("amount") if bo.get("amount") is not None else core.get("Amount")), + "date": core.get("BehaviorTime") or bo.get("date"), + "core_field_base": { + key: _sanitize(core.get(key)) + for key in ("BehaviorTime", "Performer", "Subject", "Object", "Amount", "Action") + }, + "source_evidence_indexes": _bo_evidence_refs(bo), + "domain_payload_compact": domain, + } + + + def _compact_structure(row: dict[str, Any]) -> dict[str, Any]: + audit = _as_dict(row.get("routing_audit")) + return { + "structure_id": _text(row.get("structure_id"), 120), + "candidate_structure_type": _text(row.get("candidate_structure_type"), 120) or None, + "candidate_structure_types": _sorted_strings(audit.get("candidate_structure_types")), + "structure_label": _text(row.get("structure_label"), 300), + "source_bo_ids": _sorted_strings(row.get("source_bo_ids")), + "issue_cluster_ids": _sorted_strings(row.get("issue_cluster_ids")), + "asset_cluster_ids": _sorted_strings(row.get("asset_cluster_ids")), + "evidence_indexes": _sorted_strings(row.get("evidence_indexes")), + "linked_claim_group_ids": _sorted_strings(row.get("linked_claim_group_ids")), + "linked_liability_group_ids": _sorted_strings(row.get("linked_liability_group_ids")), + "linked_actio_signal_ids": _sorted_strings(row.get("linked_actio_signal_ids")), + "legal_effect_roles": _sorted_strings(row.get("legal_effect_roles")), + "review_required": row.get("review_required") is True, + "routing_audit": { + "blocked_exception_ids": _sorted_strings(audit.get("blocked_exception_ids")), + "deterministic_review_ids": _sorted_strings(audit.get("deterministic_review_ids")), + }, + } + + + def _compact_evidence(row: dict[str, Any]) -> dict[str, Any]: + def compact_values(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + text = _text(item, 500) + if text and text not in out: + out.append(text) + return out + + return { + "evidence_index": _evidence_id(row), + "title": _text(row.get("title") or row.get("source_title"), 500), + "doc_type": _text(row.get("doc_type"), 160) or None, + "key_facts": compact_values(row.get("key_facts")), + "key_dates": compact_values(row.get("key_dates")), + "key_amounts": compact_values(row.get("key_amounts")), + "source_pointer": _sanitize(row.get("source_pointer")) if isinstance(row.get("source_pointer"), dict) else {}, + "authentication": _sanitize(row.get("authentication") or row.get("authentication_status")), + } + + + def _signal_rows(doc: dict[str, Any], root: str, schema: str) -> list[dict[str, Any]]: + if doc.get("schema_version") != schema: + raise ValueError(f"{root} schema_version mismatch") + if doc.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError(f"{root} status invalid") + rows = doc.get(root) + if not isinstance(rows, list): + raise ValueError(f"{root} must be an array") + return [row for row in rows if isinstance(row, dict)] + + + def _compact_signal(row: dict[str, Any], signal_type: str) -> dict[str, Any]: + return { + "signal_type": signal_type, + "signal_id": _text(row.get("signal_id") or row.get("id"), 120) or None, + "candidate_structure_types": _sorted_strings(row.get("candidate_structure_types") or row.get("candidate_structure_type")), + "linked_structure_ids": _sorted_strings(row.get("linked_structure_ids")), + "related_evidence_indexes": _sorted_strings(row.get("related_evidence_indexes") or row.get("evidence_indexes")), + "party_roles": _sanitize(_as_list(row.get("party_roles"))), + "actio_role_hints": _sanitize(_as_list(row.get("actio_role_hints"))), + "claim_group_candidates": _sanitize(_as_list(row.get("claim_group_candidates")) + _strings(row.get("claim_group_id"))), + "liability_group_candidates": _sanitize(_as_list(row.get("liability_group_candidates")) + _strings(row.get("liability_group_id"))), + } + + + def _signal_bo_ids(row: dict[str, Any], signal_type: str) -> list[str]: + if signal_type == "legal_effect": + return _strings(row.get("bo_id") or row.get("related_bo_ids")) + return _strings(row.get("related_bo_ids") or row.get("source_bo_ids") or row.get("bo_id")) + + + def _review_for_bo(queue_item: dict[str, Any], bo_id: str) -> dict[str, Any]: + raw_type = _text(queue_item.get("review_type"), 120) + if raw_type == "blocked_exception": + review_type = "blocked_exception" + elif raw_type in {"deterministic_review", "legal_structure_deterministic_review"}: + review_type = "deterministic_review" + elif raw_type in {"review_sensitive_split", "split_review"}: + review_type = "review_sensitive_split" + else: + review_type = "structure_review" + structure_ids = _strings(queue_item.get("structure_ids") or queue_item.get("structure_id")) + return { + "review_type": review_type, + "review_id": _text(queue_item.get("review_id"), 120) or None, + "exception_id": _text(queue_item.get("exception_id"), 120) or None, + "review_code": _text(queue_item.get("review_code"), 160) or None, + "reason_code": _text(queue_item.get("reason_code") or queue_item.get("block_reason"), 160) or None, + "downstream_owner": _text(queue_item.get("downstream_owner"), 80) or ("HUMAN_REVIEW" if review_type == "blocked_exception" else "STAGE_2"), + "structure_ids": sorted(set(structure_ids)), + "blocking": review_type == "blocked_exception" or queue_item.get("blocking") is True, + "source_bo_id": bo_id, + } + + + def main() -> None: + _init_mcp_session() + docs = {key: _read_json(path) for key, path in INPUT_FILES.items()} + input_digests = {path: _digest(docs[key]) for key, path in INPUT_FILES.items()} + run_fingerprint = _digest({"contract": "stage1_fact_ledger_run.v3", "input_digests": input_digests}) + + bo_rows = docs["bo"] + if not isinstance(bo_rows, list) or not bo_rows: + raise ValueError("BO.json must be a non-empty array") + bo_by_id: dict[str, Any] = {} + for row in bo_rows: + if not isinstance(row, dict): + raise ValueError("BO.json contains a non-object item") + bo_id = _bo_id(row) + if not bo_id or bo_id in bo_by_id: + raise ValueError(f"BO ID missing or duplicated: {bo_id}") + bo_by_id[bo_id] = _compact_bo(row) + bo_ids = set(bo_by_id) + + structures_doc = _as_dict(docs["structures"]) + if structures_doc.get("schema_version") != "stage1_legal_effect_structures.v1": + raise ValueError("legal_effect_structures schema_version mismatch") + if structures_doc.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("legal_effect_structures status invalid") + structures_raw = structures_doc.get("legal_effect_structures") + indexes = structures_doc.get("structure_index") + quality_gate = structures_doc.get("quality_gate") + if not isinstance(structures_raw, list) or not isinstance(indexes, dict) or not isinstance(quality_gate, dict): + raise ValueError("legal_effect_structures required roots missing") + for key in STRUCTURE_INDEX_KEYS: + if not isinstance(indexes.get(key), dict): + raise ValueError(f"structure_index.{key} missing") + for gate_name in ("bo_coverage", "evidence_coverage"): + if _as_dict(quality_gate.get(gate_name)).get("coverage_pass") is not True: + raise ValueError(f"quality_gate.{gate_name} failed") + + structure_by_id: dict[str, Any] = {} + expected_by_bo: dict[str, list[str]] = {bo_id: [] for bo_id in bo_ids} + for raw in structures_raw: + if not isinstance(raw, dict): + raise ValueError("legal_effect_structures contains a non-object item") + compact = _compact_structure(raw) + sid = compact["structure_id"] + if not sid or sid in structure_by_id: + raise ValueError(f"structure ID missing or duplicated: {sid}") + unknown_bos = sorted(set(compact["source_bo_ids"]) - bo_ids) + if unknown_bos: + raise ValueError(f"structure {sid} has unknown BO refs: {unknown_bos}") + structure_by_id[sid] = compact + for bo_id in compact["source_bo_ids"]: + expected_by_bo[bo_id].append(sid) + + by_bo = _as_dict(indexes.get("by_bo_id")) + for bo_id in sorted(bo_ids): + indexed = _sorted_strings(_as_dict(by_bo.get(bo_id)).get("structure_ids")) + expected = sorted(set(expected_by_bo.get(bo_id, []))) + if indexed != expected: + raise ValueError(f"structure_index.by_bo_id mismatch: {bo_id}") + + evidence_doc = _as_dict(docs["evidence"]) + evidence_rows = evidence_doc.get("items") or evidence_doc.get("evidence_indexed") + if not isinstance(evidence_rows, list) or not evidence_rows: + raise ValueError("evidence_indexed conservation array missing") + evidence_by_id: dict[str, Any] = {} + for raw in evidence_rows: + if not isinstance(raw, dict): + raise ValueError("evidence_indexed contains a non-object item") + compact = _compact_evidence(raw) + eid = compact["evidence_index"] + if not eid or eid in evidence_by_id: + raise ValueError(f"evidence ID missing or duplicated: {eid}") + evidence_by_id[eid] = compact + evidence_ids = set(evidence_by_id) + for sid, structure in structure_by_id.items(): + unknown = sorted(set(structure["evidence_indexes"]) - evidence_ids) + if unknown: + raise ValueError(f"structure {sid} has unknown evidence refs: {unknown}") + + signal_specs = ( + ("actio", _as_dict(docs["actio"]), "actio_case_signals", "actio_case_signals.v1"), + ("case_liability", _as_dict(docs["liability"]), "case_liability_signals", "case_liability_signals.v1"), + ("legal_effect", _as_dict(docs["legal_effect"]), "bo_legal_effect_routes", "legal_effect_signals.v1"), + ) + signal_hints: dict[str, dict[str, list[dict[str, Any]]]] = { + bo_id: {"actio": [], "case_liability": [], "legal_effect": []} for bo_id in bo_ids + } + source_schema_versions = { + "BO.json": "array", + "legal_effect_structures.json": structures_doc.get("schema_version"), + "evidence_indexed.json": evidence_doc.get("schema_contract_version") or evidence_doc.get("schema_version") or "conservation-array", + } + for signal_type, doc, root, schema in signal_specs: + rows = _signal_rows(doc, root, schema) + source_schema_versions[INPUT_FILES[{"actio": "actio", "case_liability": "liability", "legal_effect": "legal_effect"}[signal_type]]] = schema + for raw in rows: + compact = _compact_signal(raw, signal_type) + for bo_id in _signal_bo_ids(raw, signal_type): + if bo_id in signal_hints: + signal_hints[bo_id][signal_type].append(compact) + for bo_id in signal_hints: + for signal_type in signal_hints[bo_id]: + signal_hints[bo_id][signal_type].sort(key=_canonical) + + structure_refs_by_bo: dict[str, Any] = {} + for bo_id in sorted(bo_ids): + row = _as_dict(by_bo.get(bo_id)) + structure_refs_by_bo[bo_id] = { + "structure_ids": _sorted_strings(row.get("structure_ids")), + "issue_cluster_ids": _sorted_strings(row.get("issue_cluster_ids")), + "asset_cluster_ids": _sorted_strings(row.get("asset_cluster_ids")), + "evidence_index_ids": _sorted_strings(row.get("evidence_indexes")), + "candidate_structure_types": _sorted_strings(row.get("candidate_structure_types")), + "claim_group_ids": _sorted_strings(row.get("claim_group_ids")), + "liability_group_ids": _sorted_strings(row.get("liability_group_ids")), + "downstream_fact_roles": _sorted_strings(row.get("legal_effect_roles") or row.get("downstream_fact_roles")), + } + + upstream_reviews: dict[str, list[dict[str, Any]]] = {bo_id: [] for bo_id in bo_ids} + queue = _as_list(quality_gate.get("review_queue")) + for raw in queue: + if not isinstance(raw, dict): + continue + for bo_id in _strings(raw.get("source_bo_ids") or raw.get("source_bo_id")): + if bo_id in upstream_reviews: + upstream_reviews[bo_id].append(_review_for_bo(raw, bo_id)) + for sid, structure in structure_by_id.items(): + if structure.get("review_required") is not True: + continue + for bo_id in structure["source_bo_ids"]: + upstream_reviews[bo_id].append(_review_for_bo({"review_type": "structure_review", "structure_id": sid}, bo_id)) + for bo_id, rows in upstream_reviews.items(): + unique = {_canonical(row): row for row in rows} + upstream_reviews[bo_id] = sorted(unique.values(), key=lambda row: (_text(row.get("review_type")), _text(row.get("review_id") or row.get("exception_id")), _canonical(row))) + + warnings: list[dict[str, Any]] = [] + for bo_id, bo in bo_by_id.items(): + for eid in bo["source_evidence_indexes"]: + if eid not in evidence_ids: + warnings.append({"code": "UNKNOWN_BO_EVIDENCE_REF", "source_bo_id": bo_id, "evidence_index": eid}) + review_count = sum(len(rows) for rows in upstream_reviews.values()) + blocking_count = sum(1 for rows in upstream_reviews.values() for row in rows if row.get("blocking") is True) + status = "READY_WITH_REVIEW" if review_count or warnings else "READY" + source_pack = { + "schema_version": "stage1_fact_source_pack.v3", + "status": status, + "run_fingerprint": run_fingerprint, + "input_manifest": {"files": input_digests, "source_schema_versions": source_schema_versions}, + "bo_by_id": {bo_id: bo_by_id[bo_id] for bo_id in sorted(bo_by_id)}, + "structure_by_id": {sid: structure_by_id[sid] for sid in sorted(structure_by_id)}, + "structure_refs_by_bo_id": structure_refs_by_bo, + "signal_hints_by_bo_id": {bo_id: signal_hints[bo_id] for bo_id in sorted(signal_hints)}, + "evidence_authority_by_index": {eid: evidence_by_id[eid] for eid in sorted(evidence_by_id)}, + "upstream_review_by_bo_id": {bo_id: upstream_reviews[bo_id] for bo_id in sorted(upstream_reviews)}, + "reference_universe": { + "bo_ids": sorted(bo_ids), + "structure_ids": sorted(structure_by_id), + "evidence_indexes": sorted(evidence_ids), + }, + "source_integrity": { + "warnings": sorted(warnings, key=_canonical), + "warning_count": len(warnings), + "reference_validation_pass": True, + }, + "source_counts": { + "bo_count": len(bo_by_id), + "structure_count": len(structure_by_id), + "evidence_count": len(evidence_by_id), + "review_count": review_count, + "blocking_review_count": blocking_count, + }, + } + payload = {"fact_source_pack": source_pack} + _write_json(OUT_PATH, payload) + _verify_reread(OUT_PATH, payload) + print(_canonical({"fact_source_pack_manifest": { + "schema_version": "stage1_fact_source_pack_manifest.v2", + "status": status, + "path": OUT_PATH, + "sha256": _digest(payload), + "run_fingerprint": run_fingerprint, + "bo_count": len(bo_by_id), + "structure_count": len(structure_by_id), + "evidence_count": len(evidence_by_id), + "review_count": review_count, + }})) + + + try: + main() + except Exception as exc: + print(_canonical({"fact_source_pack_manifest": { + "schema_version": "stage1_fact_source_pack_manifest.v2", + "status": "FAILED", + "error": _safe_error(exc), + }})) + raise + + - task_name: Task_FL1_deterministic_fact_ledger_candidate_builder + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import copy + import hashlib + import itertools + import json + import re + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _safe_error(exc: BaseException) -> str: + return re.sub(r"\s+", " ", str(exc)).strip()[:300] + + + def _parse_json_value(raw: Any, label: str, allow_trailing: bool = False) -> Any: + if isinstance(raw, (dict, list)): + return raw + if not isinstance(raw, str): + raise ValueError(f"{label} is not JSON text") + text = raw.lstrip("\ufeff").strip() + if not text: + raise ValueError(f"{label} is empty") + try: + return json.loads(text) + except json.JSONDecodeError: + try: + value, end = JSON_DECODER.raw_decode(text) + except json.JSONDecodeError as exc: + raise ValueError(f"{label} invalid JSON: {text[:300]}") from exc + if not allow_trailing and text[end:].strip(): + raise ValueError(f"{label} has trailing content: {text[end:end + 300]}") + return value + + + def _parse_mcp_response(text: str) -> dict[str, Any]: + parsed: dict[str, Any] | None = None + for line in text.strip().splitlines(): + if not line.startswith("data: "): + continue + try: + item = _parse_json_value(line[6:], "MCP SSE data", True) + except ValueError: + continue + if isinstance(item, dict): + parsed = item + if parsed is None: + item = _parse_json_value(text, "MCP response", True) + if not isinstance(item, dict): + raise RuntimeError("MCP response root is not an object") + parsed = item + return parsed + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-fact-ledger-fl1-candidate-builder", + "version": "2.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_mcp_response(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP initialize failed: {payload['error']}") + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + initialized = CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ) + initialized.raise_for_status() + + + def _call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_msg_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_mcp_response(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP {name} failed: {payload['error']}") + result = payload.get("result") + if not isinstance(result, dict) or result.get("isError") is True: + raise RuntimeError(f"MCP {name} returned an invalid result") + return result + + + def _extract_doc(result: dict[str, Any], doc_name: str) -> Any: + blocks = result.get("content") or [] + if not blocks or not isinstance(blocks[0], dict): + raise RuntimeError(f"Empty response: {doc_name}") + text = blocks[0].get("text") + if not isinstance(text, str) or not text.strip(): + raise RuntimeError(f"Empty text response: {doc_name}") + outer = _parse_json_value(text, f"{doc_name} outer envelope", True) + inner: Any = outer + if isinstance(outer, dict) and "results" in outer: + results = outer.get("results") or [] + if not results or not isinstance(results[0], dict): + raise RuntimeError(f"Empty results envelope: {doc_name}") + inner = results[0].get("content") + if inner in (None, ""): + inner = results[0].get("text") + if isinstance(inner, (dict, list)): + return inner + return _parse_json_value(inner, f"{doc_name} inner document", False) + + + def _read_json(path: str) -> Any: + return _extract_doc(_call_tool("read_docs", {"doc_names": [path]}), path) + + + def _write_text(path: str, content: str) -> None: + _call_tool("write_file", {"path": path, "content": content, "overwrite": True}) + + + def _canonical(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + + def _digest(value: Any) -> str: + return "sha256:" + hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest() + + + def _write_json(path: str, value: Any) -> None: + _write_text(path, json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2)) + + + def _verify_reread(path: str, expected: Any) -> None: + actual = _read_json(path) + if _digest(actual) != _digest(expected): + raise RuntimeError(f"Post-write verification failed: {path}") + + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _strings(value: Any) -> list[str]: + values = value if isinstance(value, list) else [value] + out: list[str] = [] + for item in values: + if item is None: + continue + if isinstance(item, str): + text = item.strip() + elif isinstance(item, (int, float)) and not isinstance(item, bool): + text = str(item) + else: + continue + if text and text not in out: + out.append(text) + return out + + + def _sorted_strings(value: Any) -> list[str]: + return sorted(set(_strings(value))) + + + def _text(value: Any, limit: int = 500) -> str: + if value in (None, "", [], {}): + return "" + raw = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, sort_keys=True) + return re.sub(r"\s+", " ", raw).strip()[:limit] + + SOURCE_PACK_PATH = "stage1_tmp/fact_ledger/fact_source_pack.json" + CANDIDATE_PATH = "stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json" + EXCEPTION_MANIFEST_PATH = "stage1_tmp/fact_ledger/fact_exception_manifest.json" + INPUT_PART_DIR = "stage1_tmp/fact_ledger/fact_exception_input_parts" + OUTPUT_PART_DIR = "stage1_tmp/fact_ledger/fact_exception_adjudication_parts" + MAX_EXCEPTION_ITEMS_PER_PACK = 4 + TARGET_EXCEPTION_ITEMS_PER_PACK = 3 + MAX_EXCEPTION_ITEM_BYTES = 12000 + MAX_EXCEPTION_PACK_BYTES = 25000 + MAX_EXCEPTION_PACKS = 8 + MAX_CONCURRENCY = 2 + FINAL_FIELDS = [ + "fact_id", "source_bo_id", "type", "date", "parties", "object_spec", "amount", "action", + "evidence_refs", "credibility", "legal_centrality", "proof_strength", "legal_effect_roles", + "claim_chain_ref", "linked_structures", "must_not_drop_in_claim_types", "must_consider", + "state_context", "money_claim_effect", "commercial_successor_effect", "actio_roles", + "linked_actio_structures", "downstream_module_candidates", "skeleton_validation_codes", + "derived_fact", "derivation_basis", "legal_calculation_object", + ] + FINAL_FIELD_SET = set(FINAL_FIELDS) + STRUCTURE_TYPE_TO_MODULE = { + "money_claim": "money_claim", + "commercial_successor": "commercial_successor", + "secured_debt": "secured_registry", + "registry_invalidity": "secured_registry", + "land_use_gain": "land_valuation", + "valuation": "land_valuation", + "fraudulent_transfer": "actio", + "preserved_claim": "actio", + "succession_notice_lien_asset_defense": "succession_notice_lien_asset_defense", + "general_legal_effect": "legal_effect", + } + STRUCTURE_TYPE_TO_ROLE = { + "money_claim": "claim_chain_ref", + "commercial_successor": "claim_chain_ref", + "secured_debt": "linked_structures", + "registry_invalidity": "linked_structures", + "land_use_gain": "legal_calculation_object", + "valuation": "legal_calculation_object", + "fraudulent_transfer": "actio_roles", + "preserved_claim": "actio_roles", + "succession_notice_lien_asset_defense": "linked_structures", + "general_legal_effect": "legal_effect_roles", + } + CLOSED_EXCEPTION_TYPES = { + "conflicting_fact_projection", + "conflicting_calculation_projection", + "conflicting_asset_projection", + "conflicting_structure_role_projection", + } + CLOSED_TARGET_FIELDS = { + "date", "amount", "action", "object_spec", "derived_fact", "claim_chain_ref", + "linked_structures", "state_context", "legal_calculation_object", + } + + + def _first(*values: Any) -> Any: + for value in values: + if value not in (None, "", [], {}): + return value + return None + + + def _add_code(row: dict[str, Any], code: str) -> None: + codes = _strings(row.get("skeleton_validation_codes")) + if code not in codes: + codes.append(code) + row["skeleton_validation_codes"] = sorted(codes) + + + def _score_evidence(rows: list[dict[str, Any]]) -> tuple[str, str, str]: + if not rows: + return "low", "low", "NO_EVIDENCE" + texts = " ".join( + " ".join([_text(row.get("title"), 200), _text(row.get("doc_type"), 100)] + _strings(row.get("key_facts"))) + for row in rows + ).lower() + meeting = any(marker in texts for marker in ("meeting", "회의", "면담")) + documentary = any(marker in texts for marker in ("등기", "계약", "판결", "결정", "공문", "금융", "송금", "계좌", "registry", "contract", "judgment")) + has_structured_fact = any(_as_list(row.get("key_facts")) for row in rows) + has_pointer = any(isinstance(row.get("source_pointer"), dict) and row.get("source_pointer") for row in rows) + if meeting and not documentary and all("meeting" in _text(row.get("doc_type"), 100).lower() or "회의" in _text(row.get("doc_type"), 100) or "면담" in _text(row.get("doc_type"), 100) for row in rows): + return "medium", "meeting_only_core", "MEETING_ONLY" + if documentary and has_structured_fact and has_pointer: + return "high", "documentary", "DOCUMENTARY_STRUCTURED" + if len(rows) >= 2 and has_structured_fact: + return "high", "high", "MULTI_SOURCE_STRUCTURED" + return "medium", "medium", "SINGLE_STRUCTURED_SOURCE" + + + def _linked_view(linked: dict[str, Any]) -> dict[str, Any]: + return { + "structure_ids": _sorted_strings(linked.get("structure_ids")), + "issue_cluster_ids": _sorted_strings(linked.get("issue_cluster_ids")), + "asset_cluster_ids": _sorted_strings(linked.get("asset_cluster_ids")), + "evidence_index_ids": _sorted_strings(linked.get("evidence_index_ids")), + "candidate_structure_types": _sorted_strings(linked.get("candidate_structure_types")), + "claim_group_ids": _sorted_strings(linked.get("claim_group_ids")), + "liability_group_ids": _sorted_strings(linked.get("liability_group_ids")), + } + + + def _legal_roles(types: list[str], structures: list[dict[str, Any]], signals: dict[str, Any]) -> list[str]: + roles: list[str] = [] + for structure in structures: + roles.extend(_strings(structure.get("legal_effect_roles"))) + roles.extend(STRUCTURE_TYPE_TO_ROLE.get(item, "legal_effect_roles") for item in types) + if _as_list(signals.get("actio")): + roles.append("actio_roles") + if _as_list(signals.get("case_liability")): + roles.append("claim_chain_ref") + if _as_list(signals.get("legal_effect")): + roles.append("legal_effect_roles") + return sorted(set(roles)) + + + def _modules(types: list[str], signals: dict[str, Any]) -> list[str]: + modules = [STRUCTURE_TYPE_TO_MODULE.get(item, "legal_effect") for item in types] + if _as_list(signals.get("actio")): + modules.append("actio") + if _as_list(signals.get("case_liability")): + modules.append("claim_liability") + return sorted(set(modules)) + + + def _parties(bo: dict[str, Any], signals: dict[str, Any]) -> list[dict[str, Any]]: + core = _as_dict(bo.get("core_field_base")) + out: list[dict[str, Any]] = [] + for role, field in (("actor", "Performer"), ("counterparty", "Subject")): + value = _text(core.get(field), 160) + if value: + out.append({"party": value, "role": role, "source": "BO.core_field_base"}) + for signal in _as_list(signals.get("case_liability")): + for item in _as_list(_as_dict(signal).get("party_roles")): + if not isinstance(item, dict): + continue + party = _text(item.get("party") or item.get("name"), 160) + if party: + out.append({"party": party, "role": _text(item.get("role"), 80), "source": "case_liability_signals"}) + unique = {(row["party"], row["role"]): row for row in out} + return [unique[key] for key in sorted(unique)] + + + def _claim_groups(linked: dict[str, Any], signals: dict[str, Any]) -> dict[str, Any]: + claims = _strings(linked.get("claim_group_ids")) + liabilities = _strings(linked.get("liability_group_ids")) + for signal in _as_list(signals.get("case_liability")): + claims.extend(_strings(_as_dict(signal).get("claim_group_candidates"))) + liabilities.extend(_strings(_as_dict(signal).get("liability_group_candidates"))) + return { + "claim_group_ids": sorted(set(claims)), + "liability_group_ids": sorted(set(liabilities)), + "signal_count": len(_as_list(signals.get("case_liability"))), + } + + + def _fallback_derived(bo: dict[str, Any], row: dict[str, Any]) -> str: + if _text(row.get("action"), 900): + return _text(row.get("action"), 900) + core = _as_dict(bo.get("core_field_base")) + fields = [core.get(key) for key in ("BehaviorTime", "Performer", "Subject", "Object", "Action", "Amount")] + parts = [_text(value, 240) for value in fields] + return " ".join(value for value in parts if value).strip() + + + def _review_template(code: str, row: dict[str, Any], reason: str, severity: str = "WARNING", owner: str = "STAGE_2") -> dict[str, Any]: + return { + "review_id": None, + "review_code": code, + "severity": severity, + "fact_id": row["fact_id"], + "source_bo_id": row["source_bo_id"], + "structure_ids": _strings(_as_dict(row.get("linked_structures")).get("structure_ids")), + "evidence_refs": _strings(row.get("evidence_refs")), + "reason_code": reason, + "downstream_owner": owner, + } + + + def _build_row(index: int, bo_id: str, source: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]], list[dict[str, Any]], str]: + bo = _as_dict(_as_dict(source.get("bo_by_id")).get(bo_id)) + linked = _linked_view(_as_dict(_as_dict(source.get("structure_refs_by_bo_id")).get(bo_id))) + signals = _as_dict(_as_dict(source.get("signal_hints_by_bo_id")).get(bo_id)) + structure_map = _as_dict(source.get("structure_by_id")) + structures = [_as_dict(structure_map.get(sid)) for sid in linked["structure_ids"] if isinstance(structure_map.get(sid), dict)] + types = sorted(set(linked["candidate_structure_types"] + [str(row.get("candidate_structure_type")) for row in structures if row.get("candidate_structure_type")])) + evidence_refs = sorted(set(_strings(bo.get("source_evidence_indexes")) + linked["evidence_index_ids"])) + evidence_map = _as_dict(source.get("evidence_authority_by_index")) + evidence_rows = [_as_dict(evidence_map.get(eid)) for eid in evidence_refs if isinstance(evidence_map.get(eid), dict)] + credibility, proof_strength, score_basis = _score_evidence(evidence_rows) + core = _as_dict(bo.get("core_field_base")) + modules = _modules(types, signals) + row = { + "fact_id": f"F-{index:03d}", + "source_bo_id": bo_id, + "type": _first(bo.get("bo_type"), types[0] if types else None), + "date": _first(core.get("BehaviorTime"), bo.get("date")), + "parties": _parties(bo, signals), + "object_spec": _first(core.get("Object"), json.dumps(linked["asset_cluster_ids"], ensure_ascii=False, sort_keys=True)), + "amount": _text(_first(bo.get("amount"), core.get("Amount")), 500) or None, + "action": _first(bo.get("action"), core.get("Action")), + "evidence_refs": evidence_refs, + "credibility": credibility, + "legal_centrality": "critical" if linked["structure_ids"] or _as_list(signals.get("actio")) else "medium", + "proof_strength": proof_strength, + "legal_effect_roles": _legal_roles(types, structures, signals), + "claim_chain_ref": _claim_groups(linked, signals), + "linked_structures": linked, + "must_not_drop_in_claim_types": modules if linked["structure_ids"] or _as_list(signals.get("actio")) else [], + "must_consider": bool(linked["structure_ids"] or _as_list(signals.get("actio"))), + "state_context": {"bo_type": bo.get("bo_type"), "issue_cluster_ids": linked["issue_cluster_ids"], "asset_cluster_ids": linked["asset_cluster_ids"]}, + "money_claim_effect": {"enabled": "money_claim" in types, "source_bo_id": bo_id if "money_claim" in types else None}, + "commercial_successor_effect": {"enabled": "commercial_successor" in types, "source_bo_id": bo_id if "commercial_successor" in types else None}, + "actio_roles": copy.deepcopy(_as_list(signals.get("actio"))), + "linked_actio_structures": { + "structure_ids": sorted(s["structure_id"] for s in structures if s.get("candidate_structure_type") in {"fraudulent_transfer", "preserved_claim"}), + "actio_signal_count": len(_as_list(signals.get("actio"))), + }, + "downstream_module_candidates": modules, + "skeleton_validation_codes": [], + "derived_fact": "", + "derivation_basis": "", + "legal_calculation_object": {}, + } + needs_calc = any(item in {"money_claim", "secured_debt", "land_use_gain", "valuation", "fraudulent_transfer"} for item in types) + row["legal_calculation_object"] = { + "calculation_required": needs_calc, + "amount_source": "BO.amount" if row.get("amount") else None, + "amount_text": row.get("amount"), + "object_basis": row.get("object_spec"), + "candidate_structure_types": types, + } + row["derived_fact"] = _fallback_derived(bo, row) + basis = [f"source_bo_id={bo_id}"] + if evidence_refs: + basis.append("evidence_refs=" + ",".join(evidence_refs)) + if linked["structure_ids"]: + basis.append("structure_ids=" + ",".join(linked["structure_ids"])) + row["derivation_basis"] = "; ".join(basis) + reviews: list[dict[str, Any]] = [] + if not row["derived_fact"]: + _add_code(row, "EMPTY_DERIVED_FACT") + _add_code(row, "EMPTY_DERIVED_FACT_AFTER_FALLBACK") + row["must_consider"] = True + reviews.append(_review_template("EMPTY_DERIVED_FACT_AFTER_FALLBACK", row, "NO_SOURCE_BACKED_FALLBACK")) + if not evidence_refs: + _add_code(row, "NO_EVIDENCE_REFS") + if not linked["structure_ids"]: + _add_code(row, "NO_LINKED_LEGAL_EFFECT_STRUCTURE") + if needs_calc and not row.get("amount") and not row.get("object_spec"): + _add_code(row, "CALCULATION_BASIS_MISSING_STAGE2") + reviews.append(_review_template("CALCULATION_BASIS_MISSING_STAGE2", row, "CALCULATION_SOURCE_DEFERRED")) + + upstream = _as_list(_as_dict(source.get("upstream_review_by_bo_id")).get(bo_id)) + for item in upstream: + review = _as_dict(item) + review_type = review.get("review_type") + if review_type == "blocked_exception": + code, severity, owner = "LES_BLOCKED_EXCEPTION_REVIEW", "BLOCK_FINAL_DRAFTING", "HUMAN_REVIEW" + elif review_type == "deterministic_review": + code, severity, owner = "LES_DETERMINISTIC_REVIEW", "WARNING", "STAGE_2" + elif review_type == "review_sensitive_split": + code, severity, owner = "LES_REVIEW_SENSITIVE_SPLIT", "WARNING", "STAGE_2" + else: + code, severity, owner = "LES_REVIEW_REQUIRED", "WARNING", "STAGE_2" + _add_code(row, code) + row["must_consider"] = True + reviews.append(_review_template(code, row, _text(review.get("reason_code") or review_type, 160), severity, owner)) + return row, reviews, evidence_rows, score_basis + + + def _clean_date(value: Any) -> str: + return _text(value, 40) + + + def _periods_overlap(a_start: str, a_end: str, b_start: str, b_end: str) -> bool: + a1, a2 = a_start or "0000-00-00", a_end or "9999-99-99" + b1, b2 = b_start or "0000-00-00", b_end or "9999-99-99" + return max(a1, b1) <= min(a2, b2) + + + def _domain_scans(rows: list[dict[str, Any]], source: dict[str, Any]) -> list[dict[str, Any]]: + reviews: list[dict[str, Any]] = [] + bo_map = _as_dict(source.get("bo_by_id")) + possession_entries: list[tuple[dict[str, Any], set[str], str, str, list[dict[str, Any]]]] = [] + receipt_by_notice: dict[str, str] = {} + for row in rows: + payload = _as_dict(_as_dict(bo_map.get(row["source_bo_id"])).get("domain_payload_compact")) + possession = _as_dict(payload.get("possession_state")) + events = [item for item in _as_list(possession.get("possession_events")) if isinstance(item, dict)] + causes = [item for item in _as_list(possession.get("legal_cause_assertions")) if isinstance(item, dict)] + assets = set(_strings(possession.get("asset_ref")) + _strings(_as_dict(row.get("state_context")).get("asset_cluster_ids"))) + if events and not causes: + _add_code(row, "LEGAL_CAUSE_STATE_UNKNOWN_REVIEW") + reviews.append(_review_template("LEGAL_CAUSE_STATE_UNKNOWN_REVIEW", row, "POSSESSION_WITHOUT_LEGAL_CAUSE")) + cause_names = {_text(item.get("cause_asserted"), 120) for item in causes if _text(item.get("cause_asserted"), 120)} + conflict = any("무권원" in left and any(term in right for term in ("유치권", "임대차", "사용대차")) for left in cause_names for right in cause_names if left != right) + if conflict: + _add_code(row, "POSSESSION_CAUSE_CONFLICT") + reviews.append(_review_template("POSSESSION_CAUSE_CONFLICT", row, "INCOMPATIBLE_LEGAL_CAUSE_ASSERTIONS", "HARD_WARNING", "HUMAN_REVIEW")) + for event in events: + possession_entries.append((row, assets, _clean_date(event.get("start_date")), _clean_date(event.get("end_date")), causes)) + + notice = _as_dict(payload.get("notice_lifecycle")) + notice_id = _text(notice.get("notice_id"), 120) + received = _clean_date(notice.get("received_date")) or _clean_date(notice.get("arrival_date")) + if notice_id and received: + receipt_by_notice[notice_id] = min(received, receipt_by_notice.get(notice_id, "9999-99-99")) + if notice.get("requires_receipt_for_effect") is True and not received: + _add_code(row, "NOTICE_RECEIPT_UNVERIFIED") + reviews.append(_review_template("NOTICE_RECEIPT_UNVERIFIED", row, "RECEIPT_DATE_REQUIRED", "BLOCK_FINAL_DRAFTING", "HUMAN_REVIEW")) + if notice.get("requires_receipt_for_effect") is True and received and not _strings(notice.get("receipt_evidence_refs")) and not _text(notice.get("recipient_admission_text"), 300): + _add_code(row, "NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW") + reviews.append(_review_template("NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW", row, "RECEIPT_SUPPORT_WEAK")) + + for row in rows: + payload = _as_dict(_as_dict(bo_map.get(row["source_bo_id"])).get("domain_payload_compact")) + lien = _as_dict(payload.get("lien_state")) + if not lien: + continue + dates = [receipt_by_notice[ref] for ref in _strings(lien.get("extinction_notice_refs")) if ref in receipt_by_notice] + lien_assets = set(_strings(lien.get("asset_ref"))) + if not dates or not lien_assets: + continue + receipt = min(dates) + for possession_row, assets, start, _, _ in possession_entries: + if assets & lien_assets and start and start > receipt: + _add_code(possession_row, "LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW") + reviews.append(_review_template("LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW", possession_row, "POSSESSION_AFTER_NOTICE_RECEIPT", "HARD_WARNING", "HUMAN_REVIEW")) + return reviews + + + def _walk_explicit_candidates(value: Any, path: str = "") -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + if isinstance(value, dict): + for key, child in value.items(): + child_path = f"{path}.{key}" if path else str(key) + if key in {"candidate_values", "projection_candidates", "conflict_candidates"} and isinstance(child, list): + for item in child: + if isinstance(item, dict): + candidate = copy.deepcopy(item) + candidate["_path"] = child_path + out.append(candidate) + else: + out.extend(_walk_explicit_candidates(child, child_path)) + elif isinstance(value, list): + for index, child in enumerate(value): + out.extend(_walk_explicit_candidates(child, f"{path}[{index}]")) + return out + + + def _true_exceptions(row: dict[str, Any], source: dict[str, Any]) -> list[dict[str, Any]]: + bo = _as_dict(_as_dict(source.get("bo_by_id")).get(row["source_bo_id"])) + payload = _as_dict(bo.get("domain_payload_compact")) + explicit = _walk_explicit_candidates(payload) + grouped: dict[str, list[dict[str, Any]]] = {} + for item in explicit: + field = _text(item.get("field") or item.get("target_field"), 120) + refs = sorted(set(_strings(item.get("source_refs")) + _strings(item.get("evidence_refs")) + _strings(item.get("structure_refs")))) + if field not in CLOSED_TARGET_FIELDS or not refs or "value" not in item: + continue + grouped.setdefault(field, []).append({ + "field": field, + "value": copy.deepcopy(item.get("value")), + "basis_codes": _sorted_strings(item.get("basis_codes")) or ["EXPLICIT_SOURCE_BACKED_CANDIDATE"], + "source_refs": refs, + }) + out: list[dict[str, Any]] = [] + for field, candidates in sorted(grouped.items()): + unique_values = {_canonical(item["value"]) for item in candidates} + source_sets = {tuple(item["source_refs"]) for item in candidates} + if len(unique_values) < 2 or len(source_sets) < 2: + continue + if field in {"amount", "legal_calculation_object"}: + exception_type = "conflicting_calculation_projection" + elif field in {"object_spec", "state_context"}: + exception_type = "conflicting_asset_projection" + elif field in {"claim_chain_ref", "linked_structures"}: + exception_type = "conflicting_structure_role_projection" + else: + exception_type = "conflicting_fact_projection" + out.append({ + "exception_id": None, + "exception_type": exception_type, + "fact_id": row["fact_id"], + "source_bo_id": row["source_bo_id"], + "target_fields": [field], + "candidate_values": sorted(candidates, key=lambda item: (_canonical(item["value"]), _canonical(item["source_refs"]))), + "candidate_fact_subset": {field: copy.deepcopy(row.get(field))}, + "structure_refs": _strings(_as_dict(row.get("linked_structures")).get("structure_ids")), + "evidence_facts": [], + "allowed_output_fields": [field], + "allowed_decisions": ["KEEP_AS_IS", "PATCH", "BLOCK_REVIEW"], + "conflict_codes": ["EXPLICIT_SOURCE_BACKED_CONFLICT"], + }) + return out + + + def _assign_reviews(items: list[dict[str, Any]]) -> list[dict[str, Any]]: + unique: dict[str, dict[str, Any]] = {} + for item in items: + key = _canonical({key: value for key, value in item.items() if key != "review_id"}) + unique[key] = item + ordered = sorted(unique.values(), key=lambda item: (item["source_bo_id"], item["review_code"], item["reason_code"], item["fact_id"])) + for index, item in enumerate(ordered, start=1): + item["review_id"] = f"FRV-{index:03d}" + return ordered + + + def _pack_exceptions(exceptions: list[dict[str, Any]], reviews: list[dict[str, Any]], rows_by_fact: dict[str, dict[str, Any]], run_fingerprint: str) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]: + eligible: list[dict[str, Any]] = [] + for item in sorted(exceptions, key=lambda value: (value["source_bo_id"], value["exception_type"], value["target_fields"][0])): + if len(_canonical(item).encode("utf-8")) > MAX_EXCEPTION_ITEM_BYTES: + row = rows_by_fact[item["fact_id"]] + _add_code(row, "FL2_REVIEW_REQUIRED") + reviews.append(_review_template("PACK_CONTEXT_TOO_LARGE", row, "EXCEPTION_ITEM_OVER_12000_BYTES", "BLOCK_FINAL_DRAFTING", "HUMAN_REVIEW")) + else: + eligible.append(item) + for index, item in enumerate(eligible, start=1): + item["exception_id"] = f"FX-{index:03d}" + + groups: list[list[dict[str, Any]]] = [] + for exception_type in sorted(CLOSED_EXCEPTION_TYPES): + typed = [item for item in eligible if item["exception_type"] == exception_type] + current: list[dict[str, Any]] = [] + for item in typed: + trial = current + [item] + trial_bytes = len(_canonical({"items": trial}).encode("utf-8")) + if current and (len(trial) > MAX_EXCEPTION_ITEMS_PER_PACK or trial_bytes > MAX_EXCEPTION_PACK_BYTES or len(current) >= TARGET_EXCEPTION_ITEMS_PER_PACK): + groups.append(current) + current = [item] + else: + current = trial + if current: + groups.append(current) + if len(groups) > MAX_EXCEPTION_PACKS: + raise ValueError(f"exception pack count exceeds {MAX_EXCEPTION_PACKS}") + + pack_rows: list[dict[str, Any]] = [] + fanout: list[dict[str, Any]] = [] + for ordinal, items in enumerate(groups, start=1): + file_id = f"PACK-{ordinal:03d}" + pack_id = f"FL-PACK-{ordinal:03d}" + input_path = f"{INPUT_PART_DIR}/{file_id}.json" + output_path = f"{OUTPUT_PART_DIR}/{file_id}.json" + exception_ids = [item["exception_id"] for item in items] + exception_type = items[0]["exception_type"] + part = { + "schema_version": "stage1_fact_exception_input_part.v2", + "status": "READY", + "pack_id": pack_id, + "run_fingerprint": run_fingerprint, + "exception_type": exception_type, + "input_exception_ids": exception_ids, + "items": items, + "output_path": output_path, + } + serialized_bytes = len(_canonical(part).encode("utf-8")) + if serialized_bytes > MAX_EXCEPTION_PACK_BYTES: + raise ValueError(f"serialized exception pack exceeds budget: {pack_id}") + _write_json(input_path, part) + _verify_reread(input_path, part) + pack_rows.append({ + "pack_id": pack_id, + "exception_type": exception_type, + "exception_ids": exception_ids, + "input_path": input_path, + "output_path": output_path, + "serialized_bytes": serialized_bytes, + }) + fanout.append({ + "pack_id": pack_id, + "exception_type": exception_type, + "exception_ids": exception_ids, + "items": items, + "output_path": output_path, + "run_fingerprint": run_fingerprint, + }) + return eligible, pack_rows, fanout + + + def main() -> None: + _init_mcp_session() + source_outer = _read_json(SOURCE_PACK_PATH) + source = _as_dict(_as_dict(source_outer).get("fact_source_pack")) + if source.get("schema_version") != "stage1_fact_source_pack.v3": + raise ValueError("fact_source_pack schema_version mismatch") + if source.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("fact_source_pack status invalid") + run_fingerprint = _text(source.get("run_fingerprint"), 100) + if not run_fingerprint.startswith("sha256:"): + raise ValueError("fact_source_pack run_fingerprint invalid") + bo_ids = _sorted_strings(_as_dict(source.get("reference_universe")).get("bo_ids")) + if not bo_ids or bo_ids != sorted(_as_dict(source.get("bo_by_id")).keys()): + raise ValueError("source BO universe mismatch") + + rows: list[dict[str, Any]] = [] + reviews: list[dict[str, Any]] = [] + evidence_score_basis: dict[str, str] = {} + exceptions: list[dict[str, Any]] = [] + for index, bo_id in enumerate(bo_ids, start=1): + row, row_reviews, _, score_basis = _build_row(index, bo_id, source) + if set(row) != FINAL_FIELD_SET: + raise ValueError(f"candidate row schema mismatch: {bo_id}") + rows.append(row) + reviews.extend(row_reviews) + evidence_score_basis[row["fact_id"]] = score_basis + exceptions.extend(_true_exceptions(row, source)) + reviews.extend(_domain_scans(rows, source)) + rows_by_fact = {row["fact_id"]: row for row in rows} + exceptions, pack_rows, fanout = _pack_exceptions(exceptions, reviews, rows_by_fact, run_fingerprint) + reviews = _assign_reviews(reviews) + + exception_ids = [item["exception_id"] for item in exceptions] + if sorted(item for pack in pack_rows for item in pack["exception_ids"]) != sorted(exception_ids): + raise ValueError("exception pack conservation failed") + manifest_status = "READY" if exceptions else ("READY_WITH_REVIEW" if reviews else "READY_NO_EXCEPTIONS") + manifest = { + "fact_exception_manifest": { + "schema_version": "stage1_fact_exception_manifest.v2", + "status": manifest_status, + "run_fingerprint": run_fingerprint, + "expected_exception_ids": exception_ids, + "exception_count": len(exceptions), + "deterministic_review_items": reviews, + "pack_count": len(pack_rows), + "packs": pack_rows, + } + } + _write_json(EXCEPTION_MANIFEST_PATH, manifest) + _verify_reread(EXCEPTION_MANIFEST_PATH, manifest) + + candidate_bo_ids = [row["source_bo_id"] for row in rows] + missing = sorted(set(bo_ids) - set(candidate_bo_ids)) + unknown = sorted(set(candidate_bo_ids) - set(bo_ids)) + duplicates = sorted({item for item in candidate_bo_ids if candidate_bo_ids.count(item) > 1}) + if missing or unknown or duplicates or len(rows) != len(bo_ids): + raise ValueError("candidate BO conservation failed") + candidate_status = "READY_WITH_EXCEPTIONS" if exceptions else ("READY_WITH_REVIEW" if reviews else "READY") + bundle = { + "fact_ledger_candidate_bundle": { + "schema_version": "stage1_fact_ledger_candidate_bundle.v2", + "status": candidate_status, + "run_fingerprint": run_fingerprint, + "source_pack_ref": { + "path": SOURCE_PACK_PATH, + "schema_version": "stage1_fact_source_pack.v3", + "sha256": _digest(source_outer), + }, + "candidate_items": rows, + "exception_manifest_ref": { + "path": EXCEPTION_MANIFEST_PATH, + "schema_version": "stage1_fact_exception_manifest.v2", + "sha256": _digest(manifest), + "expected_exception_ids": exception_ids, + }, + "compile_gate": { + "source_bo_count": len(bo_ids), + "candidate_count": len(rows), + "source_bo_ids": bo_ids, + "candidate_bo_ids": candidate_bo_ids, + "missing_bo_ids": missing, + "unknown_bo_ids": unknown, + "duplicate_bo_ids": duplicates, + "bo_conservation_pass": True, + "candidate_schema_closed": True, + "reference_validation_pass": True, + "evidence_score_basis": evidence_score_basis, + }, + } + } + _write_json(CANDIDATE_PATH, bundle) + _verify_reread(CANDIDATE_PATH, bundle) + stdout_status = "READY" if exceptions else ("READY_WITH_REVIEW" if reviews else "READY_NO_EXCEPTIONS") + print(_canonical({ + "fact_ledger_fl1_manifest": { + "schema_version": "stage1_fact_ledger_fl1_manifest.v2", + "status": stdout_status, + "candidate_path": CANDIDATE_PATH, + "exception_manifest_path": EXCEPTION_MANIFEST_PATH, + "run_fingerprint": run_fingerprint, + "candidate_count": len(rows), + "exception_count": len(exceptions), + "review_count": len(reviews), + "pack_count": len(pack_rows), + }, + "dynamic_fanout": fanout, + })) + + + try: + main() + except Exception as exc: + print(_canonical({"fact_ledger_fl1_manifest": { + "schema_version": "stage1_fact_ledger_fl1_manifest.v2", + "status": "FAILED", + "error": _safe_error(exc), + }, "dynamic_fanout": []})) + raise + + - task_name: Task_FL2_fact_exception_adjudicator_* + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: medium + llm_verbosity: low + max_iterations: 2 + max_concurrency: 2 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: | + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + TASK_NAME: Task_FL2_fact_exception_adjudicator_* + STAGE: Stage 1 Fact Ledger generation + ROLE: compact source-backed Fact projection exception adjudicator + + + + pack_id: {{item.pack_id}} + exception_type: {{item.exception_type}} + exception_ids: {{item.exception_ids}} + items: {{item.items}} + output_path: {{item.output_path}} + run_fingerprint: {{item.run_fingerprint}} + + + + - Act as a senior Korean civil-litigation lawyer and AI systems architect, but decide only the assigned compact projection conflicts. + - Use only ASSIGNED_PACK. Do not read or request any file, any other pack, BO, evidence, signal, structure, source pack, candidate bundle, or final ledger. + - Process every assigned exception ID exactly once, without omission, duplication, merge, split, or reorder. + - Select only KEEP_AS_IS, PATCH, or BLOCK_REVIEW when that decision appears in the item's allowed_decisions. + - PATCH keys must be a subset of allowed_output_fields. Every PATCH value must exactly equal one candidate_values[].value for the same field. + - source_refs_used must be a subset of source_refs embedded in that exception item. + - Never create a fact, party, date, amount, asset, BO ID, Fact ID, exception ID, structure ID, evidence ID, candidate value, source ref, or legal theory. + - If compact context is insufficient, use BLOCK_REVIEW. Never guess. + - Do not write Fact_Ledger_base.json or any input, source, candidate, manifest, or other adjudication file. + - The only permitted tool action is localdocs write_file with overwrite=true to ASSIGNED_PACK.output_path. Do not call read_docs, list_docs, or any other tool. + - Write exactly one raw JSON adjudication part, then return only the compact manifest JSON. Do not return markdown, code fences, explanation, or the file body. + + + + - KEEP_AS_IS: preserve the deterministic candidate when its source priority remains supportable; patch must be {}, review_required=false. + - PATCH: choose exactly one source-backed candidate for each patched field; review_required=false. + - BLOCK_REVIEW: use when sources remain materially balanced, compact context is insufficient, or a safe selection requires legal theory; patch must be {}, review_required=true. + - No split decision is allowed. + + + + Each decisions[] item must contain exactly: + { + "exception_id": "FX-001", + "exception_type": "conflicting_fact_projection|conflicting_calculation_projection|conflicting_asset_projection|conflicting_structure_role_projection", + "fact_id": "F-001", + "source_bo_id": "bh1", + "decision": "KEEP_AS_IS|PATCH|BLOCK_REVIEW", + "patch": {}, + "review_required": false, + "reason_code": "KEEP_EXISTING_SOURCE_PRIORITY|SELECT_SOURCE_BACKED_CANDIDATE|INSUFFICIENT_COMPACT_CONTEXT|UNSUPPORTED_PATCH", + "basis_codes_used": [], + "source_refs_used": [], + "conflict_codes_acknowledged": [] + } + + + + Write this raw JSON object to ASSIGNED_PACK.output_path: + { + "schema_version": "stage1_fact_exception_adjudication_part.v2", + "status": "READY|READY_WITH_BLOCKS|FAILED", + "pack_id": "the assigned pack_id", + "run_fingerprint": "the assigned run_fingerprint", + "exception_type": "the assigned exception_type", + "input_exception_ids": ["all assigned exception IDs exactly once"], + "decisions": ["one decision per assigned exception"], + "coverage": { + "expected": 0, + "actual": 0, + "missing_exception_ids": [], + "unknown_exception_ids": [], + "duplicate_exception_ids": [], + "coverage_pass": true + } + } + Use READY_WITH_BLOCKS when any decision is BLOCK_REVIEW; otherwise use READY. expected and actual must equal the assigned exception count. + + + + After localdocs confirms the write, return exactly: + { + "pack_id": "the assigned pack_id", + "status": "READY|READY_WITH_BLOCKS|FAILED", + "output_path": "the assigned output_path", + "decision_count": 0 + } + + + + - task_name: Task_FL3_final_fact_ledger_gate_and_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import copy + import hashlib + import itertools + import json + import re + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _safe_error(exc: BaseException) -> str: + return re.sub(r"\s+", " ", str(exc)).strip()[:300] + + + def _parse_json_value(raw: Any, label: str, allow_trailing: bool = False) -> Any: + if isinstance(raw, (dict, list)): + return raw + if not isinstance(raw, str): + raise ValueError(f"{label} is not JSON text") + text = raw.lstrip("\ufeff").strip() + if not text: + raise ValueError(f"{label} is empty") + try: + return json.loads(text) + except json.JSONDecodeError: + try: + value, end = JSON_DECODER.raw_decode(text) + except json.JSONDecodeError as exc: + raise ValueError(f"{label} invalid JSON: {text[:300]}") from exc + if not allow_trailing and text[end:].strip(): + raise ValueError(f"{label} has trailing content: {text[end:end + 300]}") + return value + + + def _parse_mcp_response(text: str) -> dict[str, Any]: + parsed: dict[str, Any] | None = None + for line in text.strip().splitlines(): + if not line.startswith("data: "): + continue + try: + item = _parse_json_value(line[6:], "MCP SSE data", True) + except ValueError: + continue + if isinstance(item, dict): + parsed = item + if parsed is None: + item = _parse_json_value(text, "MCP response", True) + if not isinstance(item, dict): + raise RuntimeError("MCP response root is not an object") + parsed = item + return parsed + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-fact-ledger-fl3-final-writer", + "version": "2.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_mcp_response(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP initialize failed: {payload['error']}") + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + initialized = CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ) + initialized.raise_for_status() + + + def _call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_msg_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_mcp_response(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP {name} failed: {payload['error']}") + result = payload.get("result") + if not isinstance(result, dict) or result.get("isError") is True: + raise RuntimeError(f"MCP {name} returned an invalid result") + return result + + + def _extract_doc(result: dict[str, Any], doc_name: str) -> Any: + blocks = result.get("content") or [] + if not blocks or not isinstance(blocks[0], dict): + raise RuntimeError(f"Empty response: {doc_name}") + text = blocks[0].get("text") + if not isinstance(text, str) or not text.strip(): + raise RuntimeError(f"Empty text response: {doc_name}") + outer = _parse_json_value(text, f"{doc_name} outer envelope", True) + inner: Any = outer + if isinstance(outer, dict) and "results" in outer: + results = outer.get("results") or [] + if not results or not isinstance(results[0], dict): + raise RuntimeError(f"Empty results envelope: {doc_name}") + inner = results[0].get("content") + if inner in (None, ""): + inner = results[0].get("text") + if isinstance(inner, (dict, list)): + return inner + return _parse_json_value(inner, f"{doc_name} inner document", False) + + + def _read_json(path: str) -> Any: + return _extract_doc(_call_tool("read_docs", {"doc_names": [path]}), path) + + + def _write_text(path: str, content: str) -> None: + _call_tool("write_file", {"path": path, "content": content, "overwrite": True}) + + + def _canonical(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + + def _digest(value: Any) -> str: + return "sha256:" + hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest() + + + def _write_json(path: str, value: Any) -> None: + _write_text(path, json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2)) + + + def _verify_reread(path: str, expected: Any) -> None: + actual = _read_json(path) + if _digest(actual) != _digest(expected): + raise RuntimeError(f"Post-write verification failed: {path}") + + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _strings(value: Any) -> list[str]: + values = value if isinstance(value, list) else [value] + out: list[str] = [] + for item in values: + if item is None: + continue + if isinstance(item, str): + text = item.strip() + elif isinstance(item, (int, float)) and not isinstance(item, bool): + text = str(item) + else: + continue + if text and text not in out: + out.append(text) + return out + + + def _sorted_strings(value: Any) -> list[str]: + return sorted(set(_strings(value))) + + + def _text(value: Any, limit: int = 500) -> str: + if value in (None, "", [], {}): + return "" + raw = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, sort_keys=True) + return re.sub(r"\s+", " ", raw).strip()[:limit] + + SOURCE_PACK_PATH = "stage1_tmp/fact_ledger/fact_source_pack.json" + CANDIDATE_PATH = "stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json" + EXCEPTION_MANIFEST_PATH = "stage1_tmp/fact_ledger/fact_exception_manifest.json" + TARGET_PATH = "Fact_Ledger_base.json" + REPORT_PATH = "stage1_tmp/fact_ledger/fact_ledger_writer_report.json" + FINAL_FIELDS = [ + "fact_id", "source_bo_id", "type", "date", "parties", "object_spec", "amount", "action", + "evidence_refs", "credibility", "legal_centrality", "proof_strength", "legal_effect_roles", + "claim_chain_ref", "linked_structures", "must_not_drop_in_claim_types", "must_consider", + "state_context", "money_claim_effect", "commercial_successor_effect", "actio_roles", + "linked_actio_structures", "downstream_module_candidates", "skeleton_validation_codes", + "derived_fact", "derivation_basis", "legal_calculation_object", + ] + FINAL_FIELD_SET = set(FINAL_FIELDS) + CLOSED_EXCEPTION_TYPES = { + "conflicting_fact_projection", + "conflicting_calculation_projection", + "conflicting_asset_projection", + "conflicting_structure_role_projection", + } + CLOSED_DECISIONS = {"KEEP_AS_IS", "PATCH", "BLOCK_REVIEW"} + + + def _add_code(row: dict[str, Any], code: str) -> None: + row["skeleton_validation_codes"] = sorted(set(_strings(row.get("skeleton_validation_codes")) + [code])) + + + def _validate_rows(bundle: dict[str, Any]) -> list[dict[str, Any]]: + if bundle.get("schema_version") != "stage1_fact_ledger_candidate_bundle.v2": + raise ValueError("candidate bundle schema_version mismatch") + if bundle.get("status") not in {"READY", "READY_WITH_REVIEW", "READY_WITH_EXCEPTIONS"}: + raise ValueError("candidate bundle status invalid") + rows = bundle.get("candidate_items") + if not isinstance(rows, list) or not rows: + raise ValueError("candidate_items missing") + for row in rows: + if not isinstance(row, dict) or set(row) != FINAL_FIELD_SET: + raise ValueError("candidate row schema is not closed") + return copy.deepcopy(rows) + + + def _load_exception_inputs(manifest: dict[str, Any], run_fingerprint: str) -> tuple[dict[str, dict[str, Any]], list[str]]: + items: dict[str, dict[str, Any]] = {} + input_ids: list[str] = [] + packs = _as_list(manifest.get("packs")) + if len(packs) != manifest.get("pack_count"): + raise ValueError("exception manifest pack_count mismatch") + for pack in packs: + row = _as_dict(pack) + part = _as_dict(_read_json(_text(row.get("input_path"), 300))) + if part.get("schema_version") != "stage1_fact_exception_input_part.v2" or part.get("status") != "READY": + raise ValueError("exception input part schema/status invalid") + if part.get("pack_id") != row.get("pack_id") or part.get("run_fingerprint") != run_fingerprint: + raise ValueError("exception input part identity mismatch") + if part.get("output_path") != row.get("output_path") or part.get("exception_type") != row.get("exception_type"): + raise ValueError("exception input part routing mismatch") + part_ids = _strings(part.get("input_exception_ids")) + if part_ids != _strings(row.get("exception_ids")): + raise ValueError("exception input ID mismatch") + for item in _as_list(part.get("items")): + if not isinstance(item, dict): + raise ValueError("exception input item invalid") + exception_id = _text(item.get("exception_id"), 120) + if not exception_id or exception_id in items: + raise ValueError(f"duplicate exception input: {exception_id}") + if item.get("exception_type") not in CLOSED_EXCEPTION_TYPES: + raise ValueError("exception input type invalid") + items[exception_id] = item + input_ids.append(exception_id) + return items, input_ids + + + def _load_decisions(manifest: dict[str, Any], run_fingerprint: str) -> tuple[dict[str, dict[str, Any]], list[str]]: + decisions: dict[str, dict[str, Any]] = {} + decision_ids: list[str] = [] + for pack in _as_list(manifest.get("packs")): + expected_pack = _as_dict(pack) + part = _as_dict(_read_json(_text(expected_pack.get("output_path"), 300))) + if part.get("schema_version") != "stage1_fact_exception_adjudication_part.v2": + raise ValueError("adjudication part schema_version mismatch") + if part.get("status") not in {"READY", "READY_WITH_BLOCKS"}: + raise ValueError("adjudication part failed or has invalid status") + if part.get("pack_id") != expected_pack.get("pack_id") or part.get("run_fingerprint") != run_fingerprint: + raise ValueError("adjudication part identity mismatch") + if part.get("exception_type") != expected_pack.get("exception_type"): + raise ValueError("adjudication part type mismatch") + if _strings(part.get("input_exception_ids")) != _strings(expected_pack.get("exception_ids")): + raise ValueError("adjudication input IDs mismatch") + coverage = _as_dict(part.get("coverage")) + if coverage.get("coverage_pass") is not True or _as_list(coverage.get("missing_exception_ids")) or _as_list(coverage.get("unknown_exception_ids")) or _as_list(coverage.get("duplicate_exception_ids")): + raise ValueError("adjudication part coverage failed") + for decision in _as_list(part.get("decisions")): + if not isinstance(decision, dict): + raise ValueError("adjudication decision invalid") + exception_id = _text(decision.get("exception_id"), 120) + if not exception_id or exception_id in decisions: + raise ValueError(f"duplicate adjudication decision: {exception_id}") + decisions[exception_id] = decision + decision_ids.append(exception_id) + return decisions, decision_ids + + + def _allowed_values(item: dict[str, Any], field: str) -> list[Any]: + return [candidate.get("value") for candidate in _as_list(item.get("candidate_values")) if isinstance(candidate, dict) and candidate.get("field") == field] + + + def _all_source_refs(item: dict[str, Any]) -> set[str]: + refs: set[str] = set() + for candidate in _as_list(item.get("candidate_values")): + if isinstance(candidate, dict): + refs.update(_strings(candidate.get("source_refs"))) + refs.update(_strings(item.get("structure_refs"))) + return refs + + + def _apply_decision(row: dict[str, Any], item: dict[str, Any], decision: dict[str, Any]) -> bool: + for key in ("exception_id", "exception_type", "fact_id", "source_bo_id"): + if decision.get(key) != item.get(key): + raise ValueError(f"decision identity mismatch: {key}") + decision_type = decision.get("decision") + if decision_type not in CLOSED_DECISIONS or decision_type not in _as_list(item.get("allowed_decisions")): + raise ValueError("decision enum invalid") + patch = decision.get("patch") + if not isinstance(patch, dict): + raise ValueError("decision patch must be an object") + used_refs = set(_strings(decision.get("source_refs_used"))) + if not used_refs.issubset(_all_source_refs(item)): + raise ValueError("decision used unknown source refs") + if decision_type == "KEEP_AS_IS": + if patch or decision.get("review_required") is True: + raise ValueError("KEEP_AS_IS contract violated") + return False + if decision_type == "BLOCK_REVIEW": + if patch or decision.get("review_required") is not True: + raise ValueError("BLOCK_REVIEW contract violated") + _add_code(row, "BLOCK_REVIEW_PRESENT") + _add_code(row, "FL2_REVIEW_REQUIRED") + row["must_consider"] = True + row["derivation_basis"] = (str(row.get("derivation_basis") or "") + "; blocked_exception=" + str(item.get("exception_id"))).strip("; ") + return True + if decision.get("review_required") is True or not patch: + raise ValueError("PATCH contract violated") + allowed_fields = set(_strings(item.get("allowed_output_fields"))) + if not set(patch).issubset(allowed_fields) or not set(patch).issubset(FINAL_FIELD_SET): + raise ValueError("PATCH field outside hard whitelist") + for field, value in patch.items(): + if not any(_canonical(value) == _canonical(candidate) for candidate in _allowed_values(item, field)): + raise ValueError("PATCH value is not source-backed") + row[field] = copy.deepcopy(value) + return False + + + def _reference_universes(source: dict[str, Any]) -> dict[str, set[str]]: + structures = _as_dict(source.get("structure_by_id")) + claims: set[str] = set() + liabilities: set[str] = set() + actio_signals: set[str] = set() + for structure in structures.values(): + row = _as_dict(structure) + claims.update(_strings(row.get("linked_claim_group_ids"))) + liabilities.update(_strings(row.get("linked_liability_group_ids"))) + actio_signals.update(_strings(row.get("linked_actio_signal_ids"))) + for by_type in _as_dict(source.get("signal_hints_by_bo_id")).values(): + for signal_type, rows in _as_dict(by_type).items(): + for signal in _as_list(rows): + item = _as_dict(signal) + claims.update(_strings(item.get("claim_group_candidates"))) + liabilities.update(_strings(item.get("liability_group_candidates"))) + if signal_type == "actio": + actio_signals.update(_strings(item.get("signal_id"))) + return {"claim": claims, "liability": liabilities, "actio": actio_signals} + + + def _validate_row_refs(row: dict[str, Any], source: dict[str, Any], group_universes: dict[str, set[str]], report: dict[str, Any]) -> None: + universe = _as_dict(source.get("reference_universe")) + bo_ids = set(_strings(universe.get("bo_ids"))) + structure_ids = set(_strings(universe.get("structure_ids"))) + evidence_ids = set(_strings(universe.get("evidence_indexes"))) + bo_id = _text(row.get("source_bo_id"), 120) + unknown_bo = [] if bo_id in bo_ids else [bo_id] + unknown_structures = sorted(set(_strings(_as_dict(row.get("linked_structures")).get("structure_ids"))) - structure_ids) + unknown_evidence = sorted(set(_strings(row.get("evidence_refs"))) - evidence_ids) + report["reference_gate"]["unknown_bo_refs"].extend(unknown_bo) + report["reference_gate"]["unknown_structure_refs"].extend(unknown_structures) + report["reference_gate"]["unknown_evidence_refs"].extend(unknown_evidence) + claim = _as_dict(row.get("claim_chain_ref")) + if not set(_strings(claim.get("claim_group_ids"))).issubset(group_universes["claim"]): + raise ValueError(f"unknown claim group ref: {bo_id}") + if not set(_strings(claim.get("liability_group_ids"))).issubset(group_universes["liability"]): + raise ValueError(f"unknown liability group ref: {bo_id}") + + + def main() -> None: + _init_mcp_session() + source_outer = _read_json(SOURCE_PACK_PATH) + candidate_outer = _read_json(CANDIDATE_PATH) + manifest_outer = _read_json(EXCEPTION_MANIFEST_PATH) + source = _as_dict(_as_dict(source_outer).get("fact_source_pack")) + bundle = _as_dict(_as_dict(candidate_outer).get("fact_ledger_candidate_bundle")) + manifest = _as_dict(_as_dict(manifest_outer).get("fact_exception_manifest")) + if source.get("schema_version") != "stage1_fact_source_pack.v3" or source.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("fact_source_pack invalid") + if manifest.get("schema_version") != "stage1_fact_exception_manifest.v2" or manifest.get("status") not in {"READY", "READY_NO_EXCEPTIONS", "READY_WITH_REVIEW"}: + raise ValueError("fact_exception_manifest invalid") + rows = _validate_rows(bundle) + run_fingerprint = _text(source.get("run_fingerprint"), 100) + if not run_fingerprint or bundle.get("run_fingerprint") != run_fingerprint or manifest.get("run_fingerprint") != run_fingerprint: + raise ValueError("run_fingerprint mismatch") + source_ref = _as_dict(bundle.get("source_pack_ref")) + manifest_ref = _as_dict(bundle.get("exception_manifest_ref")) + if source_ref.get("path") != SOURCE_PACK_PATH or source_ref.get("schema_version") != "stage1_fact_source_pack.v3" or source_ref.get("sha256") != _digest(source_outer): + raise ValueError("source pack hash/ref mismatch") + if manifest_ref.get("path") != EXCEPTION_MANIFEST_PATH or manifest_ref.get("schema_version") != "stage1_fact_exception_manifest.v2" or manifest_ref.get("sha256") != _digest(manifest_outer): + raise ValueError("exception manifest hash/ref mismatch") + + expected_ids = _strings(manifest.get("expected_exception_ids")) + if expected_ids != _strings(manifest_ref.get("expected_exception_ids")) or len(expected_ids) != manifest.get("exception_count"): + raise ValueError("expected exception IDs mismatch") + if not expected_ids and (_as_list(manifest.get("packs")) or manifest.get("pack_count") != 0): + raise ValueError("no-exception manifest contains packs") + items_by_id, input_ids = _load_exception_inputs(manifest, run_fingerprint) + decisions_by_id, decision_ids = _load_decisions(manifest, run_fingerprint) + missing_ids = sorted(set(expected_ids) - set(decision_ids)) + unknown_ids = sorted(set(decision_ids) - set(expected_ids)) + duplicate_ids = sorted({item for item in decision_ids if decision_ids.count(item) > 1}) + if sorted(expected_ids) != sorted(input_ids) or missing_ids or unknown_ids or duplicate_ids or len(decision_ids) != len(expected_ids): + raise ValueError("exact exception coverage failed") + + rows_by_fact = {row["fact_id"]: row for row in rows} + blocked_items: list[dict[str, Any]] = [] + for exception_id in expected_ids: + item = items_by_id.get(exception_id) + decision = decisions_by_id.get(exception_id) + if not item or not decision: + raise ValueError(f"exception coverage missing: {exception_id}") + row = rows_by_fact.get(_text(item.get("fact_id"), 120)) + if not row or row.get("source_bo_id") != item.get("source_bo_id"): + raise ValueError("exception Fact/BO target mismatch") + blocked = _apply_decision(row, item, decision) + if blocked: + blocked_items.append({ + "exception_id": exception_id, + "exception_type": item.get("exception_type"), + "fact_id": item.get("fact_id"), + "source_bo_id": item.get("source_bo_id"), + "reason_code": decision.get("reason_code"), + }) + + source_bo_ids = _strings(_as_dict(source.get("reference_universe")).get("bo_ids")) + compile_gate = _as_dict(bundle.get("compile_gate")) + candidate_bo_ids = [str(row.get("source_bo_id")) for row in rows] + duplicate_bos = sorted({item for item in candidate_bo_ids if candidate_bo_ids.count(item) > 1}) + missing_bos = sorted(set(source_bo_ids) - set(candidate_bo_ids)) + unknown_bos = sorted(set(candidate_bo_ids) - set(source_bo_ids)) + if source_bo_ids != _strings(compile_gate.get("source_bo_ids")) or candidate_bo_ids != _strings(compile_gate.get("candidate_bo_ids")) or missing_bos or unknown_bos or duplicate_bos or compile_gate.get("bo_conservation_pass") is not True: + raise ValueError("candidate/BO conservation failed") + + deterministic_reviews = _as_list(manifest.get("deterministic_review_items")) + report = { + "schema_version": "stage1_fact_ledger_writer_report.v2", + "status": "READY", + "run_fingerprint": run_fingerprint, + "conservation_gate": { + "source_bo_count": len(source_bo_ids), + "candidate_count": len(candidate_bo_ids), + "final_count": len(rows), + "missing_bo_ids": missing_bos, + "unknown_bo_ids": unknown_bos, + "duplicate_bo_ids": duplicate_bos, + "pass": True, + }, + "exception_gate": { + "expected_count": len(expected_ids), + "decision_count": len(decision_ids), + "blocked_count": len(blocked_items), + "missing_exception_ids": missing_ids, + "unknown_exception_ids": unknown_ids, + "duplicate_exception_ids": duplicate_ids, + "coverage_pass": True, + }, + "reference_gate": {"unknown_bo_refs": [], "unknown_structure_refs": [], "unknown_evidence_refs": [], "pass": True}, + "upstream_les_review_gate": {}, + "blocked_review_items": blocked_items, + "row_warnings": [], + "possession_conflict_gate": {"status": "PASS", "conflicts": []}, + "drafting_gates": [], + "gate_review_code_summary": {}, + "evidence_score_change_summary": [], + } + group_universes = _reference_universes(source) + for row in rows: + _validate_row_refs(row, source, group_universes, report) + for key in ("unknown_bo_refs", "unknown_structure_refs", "unknown_evidence_refs"): + report["reference_gate"][key] = sorted(set(report["reference_gate"][key])) + report["reference_gate"]["pass"] = not any(report["reference_gate"][key] for key in ("unknown_bo_refs", "unknown_structure_refs", "unknown_evidence_refs")) + if not report["reference_gate"]["pass"]: + raise ValueError("reference gate failed") + + code_rows: dict[str, list[str]] = {} + for ordinal, row in enumerate(sorted(rows, key=lambda item: item["source_bo_id"]), start=1): + row["fact_id"] = f"F-{ordinal:03d}" + row["skeleton_validation_codes"] = sorted(set(_strings(row.get("skeleton_validation_codes")))) + for code in row["skeleton_validation_codes"]: + code_rows.setdefault(code, []).append(row["fact_id"]) + rows.sort(key=lambda item: item["fact_id"]) + report["gate_review_code_summary"] = {code: ids for code, ids in sorted(code_rows.items())} + conflicts = code_rows.get("POSSESSION_CAUSE_CONFLICT", []) + report["possession_conflict_gate"] = {"status": "HARD_WARNING" if conflicts else "PASS", "conflicts": conflicts} + + blocking_reviews = [item for item in deterministic_reviews if _as_dict(item).get("severity") == "BLOCK_FINAL_DRAFTING" or _as_dict(item).get("review_code") == "LES_BLOCKED_EXCEPTION_REVIEW"] + affected_facts = sorted(set(_text(_as_dict(item).get("fact_id"), 120) for item in deterministic_reviews if _text(_as_dict(item).get("fact_id"), 120))) + affected_bos = sorted(set(_text(_as_dict(item).get("source_bo_id"), 120) for item in deterministic_reviews if _text(_as_dict(item).get("source_bo_id"), 120))) + upstream_status = "BLOCK_FINAL_DRAFTING" if blocking_reviews else ("REVIEW" if deterministic_reviews else "PASS") + report["upstream_les_review_gate"] = { + "review_count": len(deterministic_reviews), + "blocking_count": len(blocking_reviews), + "affected_fact_ids": affected_facts, + "affected_bo_ids": affected_bos, + "status": upstream_status, + } + if blocking_reviews: + report["drafting_gates"].append({ + "gate": "BLOCK_FINAL_DRAFTING_UPSTREAM_LES_REVIEW", + "affected_fact_ids": sorted(set(_text(_as_dict(item).get("fact_id"), 120) for item in blocking_reviews)), + "affected_bo_ids": sorted(set(_text(_as_dict(item).get("source_bo_id"), 120) for item in blocking_reviews)), + }) + notice_rows = code_rows.get("NOTICE_RECEIPT_UNVERIFIED", []) + if notice_rows: + report["drafting_gates"].append({"gate": "BLOCK_FINAL_DRAFTING_NOTICE_RECEIPT", "affected_fact_ids": notice_rows}) + report["evidence_score_change_summary"] = [ + {"fact_id": fact_id, "basis_code": code} + for fact_id, code in sorted(_as_dict(compile_gate.get("evidence_score_basis")).items()) + ] + has_review = bool(deterministic_reviews or blocked_items or report["drafting_gates"] or conflicts) + report["status"] = "READY_WITH_REVIEW" if has_review else "READY" + + _write_json(TARGET_PATH, rows) + _write_json(REPORT_PATH, report) + _verify_reread(TARGET_PATH, rows) + _verify_reread(REPORT_PATH, report) + print(_canonical({"fact_ledger_final_writer": { + "schema_version": "stage1_fact_ledger_final_writer.v2", + "status": report["status"], + "written_file": TARGET_PATH, + "writer_report_file": REPORT_PATH, + "run_fingerprint": run_fingerprint, + "fact_count": len(rows), + "review_count": len(deterministic_reviews), + "blocked_count": len(blocked_items), + "drafting_gate_count": len(report["drafting_gates"]), + }})) + + + try: + main() + except Exception as exc: + print(_canonical({"fact_ledger_final_writer": { + "schema_version": "stage1_fact_ledger_final_writer.v2", + "status": "FAILED", + "written_file": None, + "error": _safe_error(exc), + }})) + raise + diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Prompt_Analysis_Stage_1.txt b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Prompt_Analysis_Stage_1.txt new file mode 100644 index 00000000..75e7b7c2 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Prompt_Analysis_Stage_1.txt @@ -0,0 +1,53 @@ + + +Create a markdown document that describes the whole tasks of Stage 1's 4 yaml files: Part 1 to Part 4. + + + + +You are a world leading AI architect engineer as well as top-tier Lawyer in South Korea + + + +Stage 1 Task YAML files in order: +- +- +- +- + + + +Write the analysis document for 4 yaml files of stage 1 in the following order: +- Title: Analysis of whole Tasks in Stage 1 +- Section 1: Analysis of Stage 1 -- Part 1 +- Section 2: Analysis of Stage 1 -- Part 2 +- Section 3: Analysis of Stage 1 -- Part 3 +- Section 4: Analysis of Stage 1 -- Part 4 + +In each section, following sub-section must be discussed in depth: +- Ultimate Goal of the Part X +- Detailed workflows of all sub-tasks of the Part X with DAG expression in ASCII art +- IO structure of Part X + - IN files + - OUT files +- Description of each sub-task: + - goal of the sub-task + - how the task is supposed to be done + - how IOs are aligned in order +- JSON format of outputs of sub-tasks in Part X (if exits) +, where X = 1, 2, 3, 4 + + + +Use **Natural Korean** in writing the document with "~하다", "~이다" 문체 (NOT ~입니다, 혹은 ~습니다). + + + +Write the document by performing method in and follow the . And then save it as . + + + + + + + diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_1_Claude_v3.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_1_Claude_v3.yml new file mode 100644 index 00000000..50f2e87c --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_1_Claude_v3.yml @@ -0,0 +1,4504 @@ +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_1 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Part 1 기초작업 + version: v.2 + + Stages: + - name: stage1_사건개요파악_기초작업 + description: 사건개요 파악, 기초정보 추출 + llm_provider: openai + llm_model: gpt-4o-2024-08-06 + tools: + mcpServers: + localdocs: + type: streamable-http + url: "http://mcp-localdocs:8012/mcp" + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + + tasks: + - task_name: Task_A_client_goal + llm_provider: google + llm_model: 'gemini-3.1-flash-lite' + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + preflight: false + preflight_files: + - client_meeting.md + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + - Aggregate, compile, publish, commit, or write only the inputs and final/derived file authorized by the current task. + - Preserve stable order, source IDs, support IDs, BO_ID, fact_id, structure_id, claim_group_id, liability_group_id, and required enum semantics exactly. + - Create or finalize IDs only from the current task's specified pattern and deterministic ordering rule. + - Do not merge, deduplicate, supersede, normalize, repair, or drop candidates unless the current reducer/writer schema authorizes that action. + - If validation or source support is insufficient, use the current failure, rollback, warning, or blocked path instead of fabricating a passing final artifact. + - Treat downstream authority and handoff fields as machine contracts; do not add prose, alternative files, or extra writer attestations outside the schema. + + + + + Task_A_client_goal + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. + + + `client_meeting.md`만 읽고 `client_goal.json`을 생성한다. + 이 파일은 고객의 소송 목적, 명시적 제약, 핵심 사건군, 당사자 universe를 Stage 1의 가장 앞단에서 안정적으로 구조화한 요약본이어야 한다. + v.3에서도 기존 필수 schema는 유지하되, 복수 원고와 복수 분쟁군이 뒤섞여 later stage가 공동원고를 과잉 일반화하지 않도록 사건군 단위의 optional 구조, 사건군별 원고적격 구조, 현재 지배상태 구조를 함께 남긴다. + 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않는다. + 특히 상속·승계형 사건군에서는 meeting만으로 확정되지 않은 친족관계 또는 공동행동 정보를 direct standing anchor처럼 과잉 일반화하지 않는다. + 상속이 개입된 사건군에서는 `legal entitlement layer`와 `complaint drafting preference layer`를 분리하여 기록한다. + 사망한 종전 권리주체의 과거 소유사실, 매수사실, 등기명의사실을 living plaintiff의 현재 `소유자/final`로 치환하지 않는다. + 필요하면 사건군별 optional additive field `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`, `preferred_plaintiff_selection_basis`, `succession_resolution_needed`를 사용하여, later stage가 실체법상 가능한 권리자 후보층과 실제 소장에 세울 우선 원고층을 혼동하지 않도록 한다. + 또한 `current_control_map`은 보조 메모가 아니라 later stage의 부당이득·인도·철거 청구 식별에 직접 쓰이는 authority map이므로, 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속을 가능한 한 4축으로 분해 저장한다. + v.3 기준으로 Task_A_client_goal은 모든 issue cluster에 사건군별 법률효과 구조의 seed를 남겨야 한다. 단순 사건 요약이 아니라 금전채권 timeline, 상호속용 책임, 담보부 부동산·지분별 권리상태, 사해행위취소 가액배상, 유치권·상속·통지 lifecycle을 downstream이 구조적으로 읽을 수 있게 `domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints`, `legal_entitlement_candidates`, `current_control_map_4axis`를 additive하게 생성한다. + `client_requested_relief`는 의뢰인이 원하는 청구 기간·금액·상대방을 보존하는 층이고, `legal_sustainability_watchpoints`는 법률상 기산일·원고적격·청구범위 재검토 필요성을 보존하는 층이다. 두 층을 절대 하나로 합치지 않는다. + 상속·승계형 사건군에서 current right holder가 meeting만으로 확정되지 않으면 `current_control_map`의 해당 row는 `certainty="provisional"`로 두고 `standing_watchpoints`에 succession-resolution trigger를 반드시 남긴다. + 또한 현재 무자력/집행난과 실제 제소 유지 의사를 혼동하지 않으며, 특정 상대방을 현재 무자력이라는 이유만으로 소 제기 대상에서 소거하지 않는다. + 필요하면 optional additive field `defendant_target_matrix`를 사용하여 피고별 소제기 유지/제외 의사, 현재 회수위험, 장래 회수신호를 분리 보존한다. + 본 task는 v.3 architecture에서 final writer로서 `client_goal.json`을 직접 작성한다(Task_A는 island/mapper가 없으므로 propose 단계 없이 직접 finalize 가능). + + + + + - In this task, legal/domain fields are meeting-grounded routing seeds and watchpoints for downstream stages. + - They are not final legal conclusions, norm search results, merits judgments, or complaint-drafting advice. + - The EXTRACTION overlay's prohibition on norm search and final legal-effect classification does not prohibit schema-authorized `domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints`, or entitlement/watchpoint seed fields. + + + + + - `client_meeting.md` + + + - `client_goal.json` + + + - 다른 파일 확인/읽기 + - 중간 파일 생성 + - `client_goal.json` 재읽기 + - meeting에 없는 당사자, 사건군, 제약, 희망사항, 청구유형을 창작하기 + - 사건군별 당사자 범위를 무시하고 전 사건 공통 사실처럼 과잉 압축하기 + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하기 + + + + + 1. `list_docs`로 `client_meeting.md` 존재만 확인한다. + 2. `read_docs`로 `client_meeting.md`만 읽는다. + 3. 메모리에서 `client_goal.json` 객체를 생성한다. + 4. `write_file(overwrite=true)`로 `client_goal.json`을 저장한다. + 5. `list_docs`로 `client_goal.json` 존재만 확인한다. 절대 읽지 않는다. + 6. 정확히 `"고객의사 초안 생성 완료"`만 출력하고 종료한다. + + + + - 필수 입력 누락, 빈 파일, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. 파일은 쓰지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + + - `client_meeting.md`만 사실 원천이다. + - 상대방 주장, 의뢰인 희망사항, 상담서두의 인적 관계, 본문 1~5항의 사건군을 모두 읽되, meeting에 직접 나타난 범위만 구조화한다. + - meeting의 문장들 사이를 조합해 새로운 사실을 만들지 않는다. + + + + + - `primary_goal`은 사건 전체를 한 문장으로 요약하되, 개별 분쟁군을 완전히 뭉개지 않을 정도의 수준으로만 추상화한다. + - `희망사항`이 복수이면 공통 분모를 중심으로 요약하되, 특정 핵심 구제수단이 반복되면 반영할 수 있다. + - 희망사항이 피고별 비대칭 전략을 직접 드러내면, 그 비대칭을 보존한다. + - 현재 회수곤란 사실이 있더라도 장래 재산취득, 상속, 보험금 회수, 급여압류 가능성 등으로 특정 피고를 제소 대상으로 유지하려는 의사가 직접 서술되면, 이를 `primary_goal` 또는 `summary_key_incidents`에 반영한다. + - 직접 금전청구 대상과 사해행위취소 대상이 다르면 한쪽으로 뭉개지 않는다. + - 불명확하면 `null`. + + + + - `constraints`에는 의뢰인이 명시적으로 원하지 않는 소, 특정 대상 제외, 가능하면/다만/아울러와 같은 제한적 요청, 상속·경매 공신력 등 사건 진행상의 명시적 제약을 넣는다. + - `constraints`는 소송전략상 하지 말 것, 제한할 것, 절차상·법률상 한계만 담는다. + - 단순한 회수곤란 사실, 현재 무자력, 현재 재산 부족, 집행난은 `constraints`에 넣지 않는다. + - 현재 무자력과 장래 재산취득 가능성이 함께 서술되면, 이를 부정적 제약으로 압축하지 않는다. + - 특정 상대방을 제외하라는 명시적 no-sue 지시가 없는 한, current collectability만으로 대상 제외 제약을 만들지 않는다. + - 해석상 추정 제약은 넣지 않는다. + - 배열 순서는 meeting 서술 순서를 최대한 따른다. + + + + - `summary_key_incidents`는 최대 4문장, 각 100자 이하. + - 각 문장은 가능한 한 서로 다른 사건군을 대표하도록 구성한다. + - 사건군 라벨은 입력자료에 직접 나타난 분쟁 대상, 거래유형, 자산군, 청구 방향을 짧게 추상화하여 만든다. 특정 사건의 지명·상호·인명·자산명을 예시값처럼 재사용하지 않는다. + - 하나의 문장에 서로 다른 사건군을 과도하게 병합하지 않는다. + - 직접 서술된 날짜/당사자/핵심 행위만 사용한다. + - 원고가 특정 피고를 직접 금전청구 대상으로 유지하려는 의사가 직접 서술되면, 사건군 대표 문장 중 하나에 그 방향성을 반영할 수 있다. + - 현재 회수곤란 사실과 장래 회수가능성 신호가 같은 사람에 관하여 함께 직접 서술되면, 둘 중 하나만 선택적으로 삭제하지 않는다. + + + + - `key_facts`는 최대 5개, 각 60자 이하. + - 가능한 한 사건군별 핵심 fact를 1개씩 대표시킨다. + - later stage의 원고적격 판단에 중요할 수 있는 소유관계, 상속관계, 현재 권리귀속, 현재 점유/사용/수익귀속 상태, 특정 처분/경매/통지 사실을 우선한다. + - 직접 서술이 없는 금액·날짜·상태를 보충하지 않는다. + + + + - `claim_type_candidates`는 의뢰인의 희망사항과 사건군 구조를 바탕으로 선택한다. + - 허용 enum만 사용한다: `"금전"|"물건인도"|"행위"|"확인"|"형성"|"보전(가처분/가압류)"`. + - later stage 분류기를 돕기 위한 상위 유형 수준만 남기고, 세부 청구명을 쓰지 않는다. + + + + + + - 이름은 원문 우선, 명백히 동일인/동일법인인 경우만 canonicalize 한다. + - 별칭/변형은 `aliases`에 기록한다. + - 당사자 배열은 사건 전체 participant universe다. 모든 분쟁군의 실질 원고적격/피고적격을 의미하지 않는다. + + + + - `parties.plaintiffs`에는 실제 의뢰인 또는 meeting 서두와 희망사항에서 원고 측 client로 직접 읽히는 사람만 넣는다. + - `parties.plaintiffs`는 client universe일 뿐, 사건군별 실질 원고적격을 의미하지 않는다. + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않는다. + - 사건군별 원고 범위 차이는 `issue_clusters`, `plaintiff_capacity_matrix`, `standing_watchpoints`에서 보정한다. + + + + - `parties.defendants`에는 meeting 본문, 상대방 주장, 희망사항에서 분쟁 상대방 또는 소송상 직접 대상으로 명시된 자만 넣는다. + - `asset_status`는 meeting에 직접 나온 재산 상태, 현재 소유/점유/담보권 설정 상태 등 later stage에 직접 도움이 되는 짧은 상태만 넣는다. + - `asset_status`는 현재 재산 상태에 관한 사실기술일 뿐, 소 제기 대상 제외 신호가 아니다. + - meeting이 "현재 무자력이나 장래 상속/재산취득 가능성 있음"을 직접 서술하면, `asset_status`는 그 둘을 함께 보존하는 방향으로 적는다. + - 현재 무자력만을 이유로 defendants에서 제외하지 않는다. 제외는 명시적 no-sue 지시가 있을 때만 허용한다. + - meeting에 없는 재산상태 추정 금지. + + + + - `parties.third_parties`에는 사건군에 중요하지만 즉시 원고/피고로 확정되지 않은 자를 넣는다. + - `relationship`은 later stage가 읽을 수 있도록 짧고 직접적으로 적는다. + + + + + - optional additive field `defendant_target_matrix`를 허용한다. + - 이 필드는 피고별 소제기 유지/제외 의사와 현재 회수위험을 분리 기록하는 전략 매트릭스다. + - `target_status`는 제소 유지 여부를 나타내고, `execution_risk`는 current collectability를 나타낸다. + - 현재 무자력은 `execution_risk`에 반영하고, 제소 유지 여부는 `target_status`에 별도로 반영한다. + - 장래 상속, 장래 재산취득, 보험금 회수 가능성, 급여압류 가능성 등 미래 회수신호가 직접 서술되면 `future_recovery_signal`에 반영한다. + - 명시적 no-sue 지시가 없는 한, 단순한 current uncollectability만으로 `exclude_now`를 부여하지 않는다. + - 각 원소는 아래 구조를 따른다. + { + "name": "string", + "target_status": "target_now|exclude_now|secondary_target|review", + "basis_text": "meeting 직접 문장에 근거한 짧은 사유", + "execution_risk": "low|medium|high|unknown", + "future_recovery_signal": "none|possible|explicit" + } + - `basis_text`는 반드시 meeting 직접 문장에 근거한 짧은 근거여야 한다. + - 이 필드는 optional이다. 명시적 피고별 타기팅 차이가 없으면 생략할 수 있다. + + + + - optional additive field `issue_clusters`를 허용한다. + - 각 cluster는 meeting의 독립 분쟁군을 1개씩 반영한다. + - 이 필드는 later stage가 전 사건 공통 parties 배열만 보고 과잉 일반화하는 것을 줄이기 위한 보조 구조다. + - `candidate_plaintiffs`는 사건군별 잠정 plaintiff anchor일 뿐, 최종 원고적격 확정값이 아니다. + - `legal_entitlement_candidates`는 해당 사건군에서 meeting 직접 서술상 실체법상 권리자일 가능성이 있는 의뢰인 후보층이다. + - `preferred_plaintiffs_for_complaint`는 later stage가 실제 소장 원고로 우선 검토할 수 있는 narrower drafting layer다. + - 상속·승계형 사건군에서는 `candidate_plaintiffs`, `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`를 서로 자동 동일시하지 않는다. + - `candidate_plaintiffs`에는 meeting에서 해당 사건군의 직접 권리주체, 직접 채권자, 직접 임차인, 직접 점유권자 등으로 읽히는 의뢰인만 넣는다. + - 친족관계, 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 `candidate_plaintiffs`를 확장하지 않는다. + - 사망한 자가 해당 사건군의 종전 권리주체로 보이는데 meeting만으로 승계가 해소되지 않으면, 친족관계나 공동 통지 사실만으로 `candidate_plaintiffs`를 채우지 않는다. + - 위와 같은 unresolved succession cluster에서는 meeting이 직접 보여주는 친족관계와 자산 연결 범위 안에서만 `legal_entitlement_candidates`를 별도 기록할 수 있다. + - `preferred_plaintiffs_for_complaint`는 meeting이 특정 의뢰인을 직접 권리주체 또는 직접 소송유지 주체로 좁혀 보여주거나, 비상속형 사건군에서 direct right-holder가 명백할 때만 채운다. + - meeting만으로 drafting preference가 명백하지 않은 unresolved succession cluster에서는 `preferred_plaintiffs_for_complaint`를 빈 배열로 둘 수 있다. + - `preferred_plaintiff_selection_basis` 허용값은 `"direct_right_holder|resolved_succession|resolved_succession_needed|drafting_preference|review|불명"`이다. + - `succession_resolution_needed`는 사망한 종전 권리주체와 사건군의 연결은 보이지만 meeting만으로 현재 권리귀속 또는 complaint plaintiff narrowing이 해소되지 않을 때 `true`로 둔다. + - meeting이 직접 금전청구 대상 피고와 사해행위 상대방/수익자를 구별해 보여주면, 이를 별도 cluster로 분리할 수 있다. + - 직접 금전회수 대상이 특정 상대방으로 서술되면, property transfer cluster와 별도의 monetary recovery cluster를 허용한다. + - cluster 수는 1개 이상 6개 이하로 제한한다. + - 각 cluster는 아래 구조를 따른다. + { + "cluster_id": "IC-01", + "cluster_label": "짧은 사건군명", + "candidate_plaintiffs": ["이 사건군에서 직접 권리주체로 읽히는 의뢰인"], + "legal_entitlement_candidates": ["실체법상 권리자 후보로 읽히는 의뢰인"], + "preferred_plaintiffs_for_complaint": ["실제 소장 원고로 우선 검토할 의뢰인"], + "preferred_plaintiff_selection_basis": "direct_right_holder|resolved_succession|resolved_succession_needed|drafting_preference|review|불명", + "succession_resolution_needed": true, + "candidate_defendants_or_counterparties": ["직접 상대방"], + "desired_relief_keywords": ["금전", "명도", "퇴거", "확인", "보전" 등 짧은 키워드] + } + - 비상속형 사건군에서 direct right-holder가 명백하면 `candidate_plaintiffs`, `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`가 동일할 수 있다. + - `issue_clusters`는 optional이다. 명확하지 않으면 생략 가능하다. + - 추가 사실 창작 없이 meeting 본문과 희망사항에서 직접 읽히는 범위만 기록한다. + + + + - optional additive field `domain_profiles`를 각 `issue_clusters[*]` 내부에 허용한다. + - `domain_profiles`는 아래 네 하위 profile을 담는 object다. 해당하지 않는 profile은 `null`로 둔다. + - `monetary_claim_profile` + - `secured_property_dispute_profile` + - `actio_property_transfer_profile` + - `possession_lien_succession_profile` + - `client_requested_relief`와 `legal_sustainability_watchpoints`를 각 cluster 내부에 둘 수 있다. + - 의뢰인이 원하는 청구취지·기간·금액은 `client_requested_relief`에 보존하고, 법률상 기산일·원고적격·범위가 불명확한 점은 `legal_sustainability_watchpoints`에 분리한다. + - 당사자가 원하는 기간이나 금액을 법률상 청구취지에 바로 사용할 수 있다고 단정하지 않는다. + + + - 금전청구 사건군에서 원채무자 외 보증인, 채무인수자, 영업양수인, 상호속용자, 공동채무자 후보가 함께 등장하면 이를 별도 후보 피고 목록에만 두지 말고 `monetary_claim_profile`, `liability_group_hint`, `commercial_successor_liability_hint`로 묶어 보존한다. + - 동일 금액, 동일 원채권, 동일 원고를 기준으로 복수 피고가 책임질 가능성이 있으면 `liability_group_hint.same_underlying_debt=true`로 둔다. + - 지급기일, 이행청구, 최고, 채무승인, 이행각서, 소멸시효, 변제기 재설정 단서가 등장하면 `monetary_claim_profile.requires_money_claim_timeline=true`로 둔다. + - 영업양도, 상호 계속 사용, 사업 양수인, 채무승계 부인, 책임배제 등기 단서가 등장하면 `commercial_successor_liability_hint.requires_article_42_review=true`로 둔다. + + + + - 부동산 사건에서 여러 대여금, 여러 근저당권, 지분별 담보, 변제, 경매, 후행 등기가 함께 등장하면 이를 흩어진 issue로 분리하지 말고 `secured_property_dispute_profile`에 묶는다. + - 변제금이 복수 채무에 영향을 줄 수 있으면 `requires_payment_allocation=true`로 표시한다. + - 전체 부동산이 아니라 지분별 유효/무효가 문제될 수 있으면 `requires_fractional_validity_analysis=true`로 표시한다. + - 원고 주장 지분, 피고 측 유효 가능 지분, 전체 부동산 말소 금지 필요성을 `fractional_title_issue_hint`에 남긴다. + - 미등기 건물, 건물 신축, 건물 매매, 대지 임대차가 함께 등장하면 `land_use_gain_hint`를 생성한다. + - 임대차보증금, 월 차임, 건물 매매대금은 각각 법적 성격이 다르므로 `lease_payment_semantics_hint`에 별도 표시한다. + - 임대차보증금은 토지소유자의 부당이득 원금으로 자동 산정하지 말라는 watchpoint를 남긴다. + + + + - 채무자가 부동산을 수익자에게 이전하고, 원고가 사해행위취소를 희망하는 사건군을 발견하면 `actio_property_transfer_profile`을 생성한다. + - 부동산에 근저당권 등 부담이 있고, 그 부담이 사해행위 이후 변제·말소되었다는 단서가 있으면 `requires_value_compensation_review=true`로 표시한다. + - 같은 채권자와 채무자 사이에 복수 대여금이 등장하면 단순 후보채권 목록으로 흩뜨리지 말고 `preserved_claim_bundle_hint`로 묶는다. + - 피고가 명의신탁, 충분 담보, 상계, 선의 등을 주장하면 이를 `defense_hints`에 남기고, 청구원인 성립 또는 범위에 직접 영향을 주는 항변이면 `requires_stage2_rebuttal=true`로 표시한다. + + + + - 현재 점유자와 현재 권리자가 분리되는 자산군 또는 사건군이 입력자료에서 직접 확인되면 `possession_lien_succession_profile`을 생성한다. + - 상속이 개입되면 current right holder를 final로 확정하지 말고 `succession_resolution_needed=true`와 `standing_watchpoints`를 남긴다. + - 유치권, 제3자 임대, 직접 거주, 피담보채권 충당, 후소유자 소멸청구권 부정 주장이 보이면 `possession_basis_asserted`, `notice_lifecycle_hints`, `legal_sustainability_watchpoints`를 구조화한다. + - 통지서 발송일과 답변서상 수령 인정일이 함께 보이면 발송일과 도달일을 분리하여 `notice_lifecycle_hints`에 남긴다. + - 상속·승계가 미해결인 cluster에서는 입력자료상 공동 통지 발신자, 친족 후보, 직계존속·직계비속 후보를 그 지위의 후보로만 보존하고, 상속 해결 전 `preferred_plaintiffs_for_complaint` 확정자로 두지 않는다. + + + + + - optional additive field `current_control_map_4axis`를 허용한다. + - legacy `current_control_map` row 구조는 유지하되, 목적물별 4축 요약이 가능한 경우 `current_control_map_4axis`를 추가한다. + - 각 item은 아래 구조를 따른다. + { + "object": "string", + "cluster_id": "IC-01", + "current_right_holder_candidate": "string|null", + "current_possessor": "string|null", + "current_user": "string|null", + "current_benefit_holder": "string|null", + "possession_basis_asserted": "string|null", + "possession_basis_transition_needed": true, + "certainty": "final|provisional|불명" + } + - 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속을 하나의 generic holder로 뭉개지 않는다. + - 상속·승계형 사건군에서 현재 권리귀속이 decedent의 과거 권리사실과 친족관계로만 추론되는 경우 `certainty="provisional"`로 둔다. + + + + - optional additive field `plaintiff_capacity_matrix`를 허용한다. + - 이 필드는 사건군별 실질 원고적격을 later stage로 넘기기 위한 standing map이다. + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않는다. + - 각 원소는 아래 구조를 따른다. + { + "cluster_id": "IC-01", + "plaintiff_name": "string", + "capacity_basis": "소유자|공유자|상속인|임차인|채권자|점유권자|불명", + "source_basis_text": "meeting에서 직접 읽히는 짧은 근거", + "certainty": "final|provisional" + } + - 사건군별 직접 권리주체로 읽히는 사람만 넣는다. + - `capacity_basis="상속인"` row라도, 근거가 친족관계, 사망, 공동 통지, 공동 상담 같은 succession trigger 수준이면 `certainty`는 반드시 `"provisional"`로 둔다. + - 상속·승계형 사건군에서 decedent의 과거 소유사실, 매수사실, 등기명의사실만 보일 뿐 living plaintiff의 승계 완료 또는 현재 권리귀속이 meeting에서 직접 해소되지 않으면, living plaintiff를 `capacity_basis="소유자"` 또는 `capacity_basis="공유자"` + `certainty="final"`로 두지 않는다. + - 위 경우에는 원칙적으로 `capacity_basis`를 `"상속인"` 또는 `"불명"`으로 두고, `certainty`를 `"provisional"`로 둔다. + - meeting이 특정 사건군에 관하여 직접 계약당사자, 직접 채권자, 직접 임차인, 직접 소유·공유 관계 또는 승계 완료 상태를 직접 보여줄 때만 `certainty="final"`을 허용한다. + - 원고적격이 불명확하거나 later stage 재검토가 필요하면 `certainty`는 `"provisional"`로 둔다. + - `preferred_plaintiffs_for_complaint`가 별도로 존재하더라도, 이를 맞추기 위해 `plaintiff_capacity_matrix`를 `소유자/final`로 과잉 보정하지 않는다. + + + + - optional additive field `current_control_map`를 허용한다. + - 이 필드는 사건군별 현재 지배상태를 later stage로 넘기기 위한 authority map이다. + - 사건군에 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속 사실이 직접 읽히면 가능한 한 `current_control_map`을 생성한다. + - 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속은 가능한 한 분리한다. + - 각 원소는 아래 구조를 따른다. + { + "cluster_id": "IC-01", + "holder_name": "string", + "holder_role": "current_right_holder|current_possessor|current_user|current_benefit_holder", + "basis_text": "meeting에서 직접 읽히는 짧은 근거", + "certainty": "final|provisional" + } + - 같은 사람이 복수 role을 가지면 중복 기재할 수 있다. + - meeting에 직접 없는 role은 추정하지 않는다. + - 상속·승계형 사건군에서 current right holder가 decedent의 과거 권리사실 + 친족관계만으로 유도되는 경우, `holder_role="current_right_holder"` row는 원칙적으로 `certainty="provisional"`로 둔다. + - decedent의 과거 소유사실만으로 living plaintiff를 `current_right_holder/final`로 기재하지 않는다. + - 비상속형 사건군에서 meeting이 현재 소유자, 현재 점유자, 현재 사용자, 현재 수익귀속자를 직접 적시하면 해당 row는 `certainty="final"`을 우선 검토한다. + + + + - optional additive field `standing_watchpoints`를 허용한다. + - later stage가 반드시 재검토해야 하는 원고적격/권리귀속 리스크를 짧은 경고문으로 남긴다. + - 상속, 상속포기, 사망, 경매, 공동소유, 현재 권리주체 불명, 현재 점유자와 권리주체 불일치가 보이면 우선 검토한다. + - 경고문은 가능한 한 자산군 또는 사건군을 특정하는 asset-specific trigger 형태로 쓴다. + - 예시 형식: + - "상속·승계 자료 추가 확인 후 해당 자산군 원고적격 재확인 필요" + - "현재 권리귀속과 현재 점유자 분리 검토 필요" + - "사망한 종전 권리주체 관련 succession resolution 필요" + - meeting에 직접 읽히는 범위를 넘는 장문 법률판단은 쓰지 않는다. + + + + - `summary_key_incidents`, `key_facts`, `constraints`는 exact-match 중복 제거 후 최초 순서를 유지한다. + - `aliases`는 명백한 경우에만 기록한다. + - `claim_type_candidates`는 중복 제거 후 최초 순서를 유지한다. + - `defendant_target_matrix`를 사용하면 same `name` + `target_status` 조합 기준 exact-match 중복 제거 후 최초 순서를 유지한다. + - 비nullable string 외에는 `불명` 사용을 피하고 `null` 또는 빈 배열을 우선한다. + + + + 기존 필수 schema는 유지하고, 아래 optional additive field를 추가할 수 있다. + v.3 schema registry 기준 `IssueClusterDomainProfile`, `LegalEntitlementCandidate`, `CurrentControlMap4Axis`를 반영한다. + + { + "primary_goal": "string|null", + "constraints": [], + "summary_key_incidents": [], + "key_facts": [], + "claim_type_candidates": ["금전","물건인도","행위","확인","형성","보전(가처분/가압류)"], + "parties": { + "plaintiffs": [{"name":"string","type":"법인|자연인|기관"}], + "defendants": [{"name":"string","type":"법인|자연인|기관|미확정","asset_status":"string|null"}], + "third_parties": [{"name":"string","relationship":"string"}] + }, + "aliases": {}, + + "defendant_target_matrix": [ + { + "name": "string", + "target_status": "target_now|exclude_now|secondary_target|review", + "basis_text": "string", + "execution_risk": "low|medium|high|unknown", + "future_recovery_signal": "none|possible|explicit" + } + ], + + "issue_clusters": [ + { + "cluster_id": "IC-01", + "cluster_label": "string", + "candidate_plaintiffs": ["string"], + "legal_entitlement_candidates": ["string"], + "preferred_plaintiffs_for_complaint": ["string"], + "preferred_plaintiff_selection_basis": "direct_right_holder|resolved_succession|resolved_succession_needed|drafting_preference|review|불명", + "succession_resolution_needed": true, + "candidate_defendants_or_counterparties": ["string"], + "desired_relief_keywords": ["string"], + "domain_profiles": { + "monetary_claim_profile": { + "underlying_claim_type": "string|null", + "principal_amount_candidate": "string|null", + "principal_debtor_candidates": ["string"], + "successor_or_secondary_liability_candidates": [], + "default_or_limitation_issue_expected": true, + "known_demand_or_notice_signals": ["string"], + "requires_money_claim_timeline": true + }, + "secured_property_dispute_profile": { + "has_multiple_secured_debts": true, + "requires_payment_allocation": true, + "requires_fractional_validity_analysis": true, + "secured_object_candidates": ["string"], + "auction_or_registry_invalidity_expected": true + }, + "actio_property_transfer_profile": { + "debtor": "string|null", + "creditor": "string|null", + "beneficiary": "string|null", + "fraudulent_act_asset": "string|null", + "fraudulent_act_type": "string|null", + "fraudulent_act_date_hint": "string|null", + "is_encumbered_real_estate": true, + "requires_value_compensation_review": true + }, + "possession_lien_succession_profile": { + "possession_basis_asserted": "string|null", + "succession_resolution_needed": true, + "notice_lifecycle_hints": [], + "lien_violation_or_extinction_hints": [] + } + }, + "client_requested_relief": ["string"], + "legal_sustainability_watchpoints": ["string"], + "liability_group_hint": {}, + "commercial_successor_liability_hint": {}, + "fractional_title_issue_hint": {}, + "land_use_gain_hint": {}, + "lease_payment_semantics_hint": {}, + "preserved_claim_bundle_hint": {}, + "value_compensation_hint": {}, + "defense_hints": [] + } + ], + + "plaintiff_capacity_matrix": [ + { + "cluster_id": "IC-01", + "plaintiff_name": "string", + "capacity_basis": "소유자|공유자|상속인|임차인|채권자|점유권자|불명", + "source_basis_text": "string", + "certainty": "final|provisional" + } + ], + + "current_control_map": [ + { + "cluster_id": "IC-01", + "holder_name": "string", + "holder_role": "current_right_holder|current_possessor|current_user|current_benefit_holder", + "basis_text": "string", + "certainty": "final|provisional" + } + ], + + "current_control_map_4axis": [ + { + "object": "string", + "cluster_id": "IC-01", + "current_right_holder_candidate": "string|null", + "current_possessor": "string|null", + "current_user": "string|null", + "current_benefit_holder": "string|null", + "possession_basis_asserted": "string|null", + "possession_basis_transition_needed": true, + "certainty": "final|provisional|불명" + } + ], + + "standing_watchpoints": ["string"] + } + + 추가 제약: + - `defendant_target_matrix`, `issue_clusters`, `plaintiff_capacity_matrix`, `current_control_map`, `standing_watchpoints`는 optional이다. + - `domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints`, `liability_group_hint`, `commercial_successor_liability_hint`, `fractional_title_issue_hint`, `land_use_gain_hint`, `lease_payment_semantics_hint`, `preserved_claim_bundle_hint`, `value_compensation_hint`, `defense_hints`는 `issue_clusters` 내부 optional additive field다. + - `current_control_map_4axis`는 top-level optional additive field이며 legacy `current_control_map`을 대체하지 않는다. + - `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`, `preferred_plaintiff_selection_basis`, `succession_resolution_needed`는 `issue_clusters` 내부 optional additive field다. + - 기존 필수 필드는 모두 유지한다. + - 허용된 additive field 외 임의 추가 필드 금지. + + + + - `parties.plaintiffs`에는 실제 의뢰인만 남았는지 점검한다. + - `parties.plaintiffs`가 client universe로만 쓰였고, 사건군별 원고적격 확정값처럼 오염되지 않았는지 점검한다. + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않았는지 점검한다. + - `parties.defendants`와 `third_parties`가 뒤바뀌지 않았는지 점검한다. + - `summary_key_incidents`가 사건군별로 과잉 병합되지 않았는지 점검한다. + - `key_facts`가 later stage에 중요한 standing/ownership/current control fact를 누락하지 않았는지 점검한다. + - `issue_clusters`를 썼다면 각 cluster의 당사자와 desired relief가 meeting 직접 서술 범위를 넘지 않는지 점검한다. + - 상속·승계형 사건군에서 `candidate_plaintiffs`가 친족관계, 공동 통지, 공동 상담 같은 간접 신호만으로 확장되지 않았는지 점검한다. + - `issue_clusters`를 썼다면 unresolved succession cluster에서 `legal_entitlement_candidates`와 `preferred_plaintiffs_for_complaint`가 자동 동일시되지 않았는지 점검한다. + - `preferred_plaintiffs_for_complaint`를 썼다면 그 basis가 `preferred_plaintiff_selection_basis`에 남아 있고, meeting 직접 서술 또는 direct right-holder 구조를 넘지 않는지 점검한다. + - `succession_resolution_needed=true`인데도 해당 cluster가 direct owner/final cluster처럼 오염되지 않았는지 점검한다. + - `plaintiff_capacity_matrix`를 썼다면 각 row의 `capacity_basis`와 `source_basis_text`가 meeting 직접 서술 범위를 넘지 않는지 점검한다. + - `capacity_basis="상속인"` row가 친족관계나 공동행동 사실만으로 `certainty="final"`로 오염되지 않았는지 점검한다. + - 사망한 종전 권리주체의 과거 소유사실, 매수사실, 등기명의사실이 living plaintiff의 `소유자/final` row로 잘못 치환되지 않았는지 점검한다. + - `current_control_map`을 썼다면 `holder_role`이 `current_right_holder|current_possessor|current_user|current_benefit_holder` 중 하나인지와, basis가 meeting 직접 문장에 근거하는지 점검한다. + - 상속·승계형 사건군에서 `current_right_holder` row가 있으면, meeting만으로 승계가 해소되지 않은 경우 `certainty="provisional"`인지 점검한다. + - 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속이 서로 다른데 하나의 generic holder row로 뭉개지지 않았는지 점검한다. + - 사망한 종전 권리주체가 보이는 사건군이 있으면 `standing_watchpoints`에 succession-resolution trigger가 남았는지 점검한다. + - `standing_watchpoints`를 썼다면 장문 법률판단이 아니라 later stage 재검토용 짧은 경고인지 점검한다. + - meeting이 피고별로 현재 집행가능성과 장래 회수가능성을 구별하여 서술하면, 그 차이가 `constraints`, `primary_goal`, `summary_key_incidents`, `parties.defendants.asset_status`, `defendant_target_matrix` 중 어디엔가 비대칭적으로 보존되었는지 점검한다. + - "현재 무자력"과 "장래 재산취득 가능성"이 같은 사람에게 함께 서술되면, 그 사람을 자동 배제 대상으로 압축하지 않았는지 점검한다. + - `constraints`에 current collectability나 단순 회수곤란 사실이 잘못 들어가 있지 않은지 점검한다. + - `asset_status`가 제소 배제 신호처럼 쓰이지 않았는지 점검한다. + - `defendant_target_matrix`를 썼다면 각 `name`이 `parties.defendants`와 모순되지 않는지 점검한다. + - `defendant_target_matrix`에서 `target_status="exclude_now"`가 붙은 사람에게는 meeting상 명시적 no-sue 지시가 있는지 점검한다. + - `defendant_target_matrix`에서 `execution_risk="high"`와 `target_status="target_now"`가 함께 존재할 수 있음을 허용하되, 그 경우 `basis_text` 또는 `future_recovery_signal`이 비어 있지 않은지 점검한다. + - 장래 상속/재산취득 가능성이 직접 서술된 피고가 `exclude_now`로 잘못 내려가지 않았는지 점검한다. + - 금전채권 cluster에 최고ㆍ채무승인ㆍ이행각서ㆍ시효 단서가 있는데 `monetary_claim_profile.requires_money_claim_timeline`이 없거나 false이면 warning으로 본다. + - 동일 사건군에 원채무자와 승계ㆍ양수ㆍ보증ㆍ상호속용 책임 후보가 함께 있는데 `liability_group_hint`가 비어 있으면 warning으로 본다. + - 영업양도ㆍ상호속용 단서가 있는데 `commercial_successor_liability_hint.requires_article_42_review`가 없으면 warning으로 본다. + - 대여금ㆍ근저당권ㆍ경매ㆍ지분 단서가 같은 cluster에 있는데 `secured_property_dispute_profile.requires_payment_allocation`이 없으면 warning으로 본다. + - 상담문서에 지분별 권리상태가 보이는데 `fractional_title_issue_hint`가 없으면 warning으로 본다. + - 미등기 건물ㆍ임대차ㆍ건물 매매ㆍ토지 사용 단서가 있는데 `land_use_gain_hint`가 없으면 warning으로 본다. + - 사해행위취소ㆍ부담부 부동산ㆍ사후 담보권 말소 단서가 있는데 `actio_property_transfer_profile.requires_value_compensation_review`가 false이면 error로 본다. + - 같은 채권자와 채무자 사이 복수 대여금이 보이는데 `preserved_claim_bundle_hint`가 없으면 warning으로 본다. + - 명의신탁ㆍ충분담보ㆍ상계ㆍ선의 등 항변 단서가 있는데 `defense_hints`가 없으면 warning으로 본다. + - 유치권ㆍ상속ㆍ승계 사건군에서 `succession_resolution_needed=true`인데도 입력자료상 공동 통지 발신자, 친족 후보, 직계존속·직계비속 후보가 `preferred_plaintiffs_for_complaint` 확정자로 들어가면 error로 본다. + - 통지 발송일과 도달일 단서가 모두 있는데 `notice_lifecycle_hints`가 없으면 warning으로 본다. + - `current_control_map_4axis`를 썼다면 4축 중 직접 읽히는 축이 generic holder 하나로 합쳐지지 않았는지 점검한다. + - 출력 파일명과 성공 메시지가 정확한지 점검한다. + + + + + + [ ] 1) Preflight: `list_docs` 사용해서 `client_meeting.md` 확인, `read_docs` 사용해서 `client_meeting.md` 읽기 + [ ] 2) `write_file` 사용해서 `client_goal.json` 생성 + [ ] 3) `list_docs` 사용해서 `client_goal.json` 파일 존재만 확인(절대 읽기 금지) + [ ] 4) `"고객의사 초안 생성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `client_meeting.md` + + + - `client_goal.json` + + + + + - `client_goal.json`은 절대 재읽지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"고객의사 초안 생성 완료"` 한 줄만 사용한다. + + + + - task_name: Task_Evidence_shard_planner + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import hashlib + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + def _mid(): return next(MSG_ID) + def _init(): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"task-evidence-shard-planner","version":"1.0","user_id":"{{__user_hash__}}","workspace_id":"{{__workspace_hash__}}"}}}, headers=MCP_HEADERS) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post(LOCALDOCS_URL, json={"jsonrpc":"2.0","method":"notifications/initialized"}, headers=MCP_HEADERS).raise_for_status() + def _parse(text): + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: return json.loads(line[6:]) + except Exception: pass + try: return json.loads(text) + except Exception: return None + def _call(name, args): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc":"2.0","id":_mid(),"method":"tools/call","params":{"name":name,"arguments":args}}, headers=MCP_HEADERS) + r.raise_for_status() + p = _parse(r.text) + if not p or "result" not in p: raise RuntimeError(f"MCP {name} failed") + return p + def _unwrap(p, name): + text = (p["result"].get("content") or [{}])[0].get("text", "") + if not text: raise RuntimeError(f"Empty: {name}") + try: outer = json.loads(text) + except Exception: return text + if isinstance(outer, dict) and "results" in outer: + r0 = (outer.get("results") or [{}])[0] + inner = r0.get("content") or r0.get("text") or "" + if not inner: raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return json.dumps(outer, ensure_ascii=False) if not isinstance(outer, str) else outer + def read_json(name): + raw = _unwrap(_call("read_docs", {"doc_names": [name]}), name) + if not isinstance(raw, str): return raw + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + obj, _ = decoder.raw_decode(raw.strip()) + return obj + def write_doc(path, content): + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def _items(obj: Any) -> list[Any]: + if isinstance(obj, list): + return obj + if isinstance(obj, dict): + for key in ("items", "documents", "evidences", "evidence", "docs"): + val = obj.get(key) + if isinstance(val, list): + return val + raise RuntimeError("evidence_all.json must be a JSON array or an object containing a document array") + + def _text(v: Any) -> str | None: + if v is None: + return None + s = str(v).strip() + return s or None + + def _doc_title(doc: Any, ordinal: int) -> str | None: + if not isinstance(doc, dict): + return f"evidence_all[{ordinal}]" + for key in ("title", "name", "filename", "file_name", "document_title", "doc_title"): + val = _text(doc.get(key)) + if val: + return val + return f"evidence_all[{ordinal}]" + + def _doc_uid(doc: Any, ordinal: int) -> str: + if isinstance(doc, dict): + for key in ("doc_uid", "uid", "id", "document_id", "source_id"): + val = _text(doc.get(key)) + if val: + return val + return f"DOC-{ordinal:03d}" + + def _stable_hash(doc: Any) -> str: + raw = json.dumps(doc, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _list_doc_names(args) -> list[str]: + payload = _unwrap(_call("list_docs", args), "list_docs") + if isinstance(payload, str): + try: + payload = json.loads(payload) + except Exception: + return re.findall(r"[\w./-]+\.json", payload) + vals: list[str] = [] + if isinstance(payload, dict): + for value in payload.values(): + if isinstance(value, list): + for item in value: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + elif isinstance(value, str): + vals.append(value) + elif isinstance(payload, list): + for item in payload: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + return [v for v in vals if v] + + def _read_existing_doc_names() -> list[str]: + preferred = [ + {"path": "evidence_indexed_parts", "pattern": "E-*.json"}, + {"path": "evidence_event_candidate_parts", "pattern": "E-*.json"}, + ] + preferred_errors = [] + preferred_names: list[str] = [] + for args in preferred: + try: + folder = str(args["path"]).rstrip("/") + preferred_names.extend(f"{folder}/{name.split('/')[-1]}" for name in _list_doc_names(args)) + except Exception as exc: + preferred_errors.append(f"{args['path']}: {exc}") + if not preferred_errors: + return sorted(set(preferred_names)) + + fallback_errors = [] + for args in ({}, {"doc_names": []}): + try: + return sorted(set(preferred_names + _list_doc_names(args))) + except Exception as exc: + fallback_errors.append(f"{args}: {exc}") + raise RuntimeError("list_docs failed for preferred schemas and fallback schemas; stale part detection cannot safely continue: " + " | ".join(preferred_errors + fallback_errors)) + + def _stale_parts(existing_names: list[str], n: int, folder: str) -> list[str]: + stale: list[str] = [] + pattern = re.compile(rf"(?:^|/){re.escape(folder)}/E-(\d{{3}})\.json$") + for name in existing_names: + m = pattern.search(name) + if not m: + continue + ordinal = int(m.group(1)) + if ordinal < 1 or ordinal > n: + stale.append(name) + return sorted(set(stale)) + + def main(): + _init() + evidence_all = read_json("evidence_all.json") + docs = _items(evidence_all) + n = len(docs) + if n <= 0: + raise RuntimeError("evidence_all.json contains no evidence documents") + + existing_names = _read_existing_doc_names() + stale_b1 = _stale_parts(existing_names, n, "evidence_indexed_parts") + stale_b2 = _stale_parts(existing_names, n, "evidence_event_candidate_parts") + + shards = [] + streaming_edges = [] + b1_waits = [] + b2_waits = [] + b1_files = [] + b2_files = [] + for idx, doc in enumerate(docs, 1): + label = f"{idx:03d}" + evidence_id = f"E-{label}" + b1_task_id = f"Task_B1_map_doc_{label}" + b2_task_id = f"Task_B2_map_events_{label}" + b1_output = f"evidence_indexed_parts/{evidence_id}.json" + b2_output = f"evidence_event_candidate_parts/{evidence_id}.json" + # 문서별 shard 파일 — B1/B2 인스턴스가 evidence_all.json 풀 대신 자기 shard만 읽도록. + shard_doc_path = f"evidence_shards/{evidence_id}.json" + write_doc(shard_doc_path, json.dumps(doc, ensure_ascii=False, indent=2)) + b1_waits.append(b1_task_id) + b2_waits.append(b2_task_id) + b1_files.append(b1_output) + b2_files.append(b2_output) + shards.append({ + "assigned_ordinal": idx, + "ordinal_label": label, + "evidence_index_candidate": evidence_id, + "b1_task_id": b1_task_id, + "b2_task_id": b2_task_id, + "b1_output_path": b1_output, + "b2_output_path": b2_output, + "shard_doc_path": shard_doc_path, + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": idx, + "array_index": idx - 1, + "doc_uid_candidate": _doc_uid(doc, idx), + "title_candidate": _doc_title(doc, idx), + "content_sha256": _stable_hash(doc), + "shard_doc_path": shard_doc_path + }, + "runtime_bindings": { + "__assigned_ordinal__": idx, + "__ordinal_label__": label, + "__evidence_index_candidate__": evidence_id, + "__b1_part_path__": b1_output, + "__b2_part_path__": b2_output, + "__shard_doc_path__": shard_doc_path + } + }) + streaming_edges.append({ + "from": b1_task_id, + "to": b2_task_id, + "condition": {"file_exists": b1_output}, + "bind": {"__assigned_ordinal__": idx} + }) + + plan = { + "schema_version": "evidence_shard_plan.v1", + "planner_task": "Task_Evidence_shard_planner", + "created_at_utc": datetime.now(timezone.utc).isoformat(), + "source_file": "evidence_all.json", + "source_selector": "$.items || $", + "shard_count": n, + "expected_ordinals": [f"{i:03d}" for i in range(1, n + 1)], + "expected_evidence_ids": [f"E-{i:03d}" for i in range(1, n + 1)], + "ordinal_policy": { + "base": 1, + "assignment": "array_order", + "format": "%03d", + "assigned_ordinal_var": "__assigned_ordinal__", + "evidence_index_candidate": "E-{ordinal:03d}", + "no_legal_sorting": True, + "no_content_based_reordering": True + }, + "shards": shards, + # 백엔드 wildcard fan-out: dynamic_fanout 리스트의 각 항목이 한 인스턴스 컨텍스트({{item.*}}로 접근). + # shards를 그대로 사용 — 디스크립터 메타(id/count/task_template 등)는 fanout_meta로 분리 보존. + "dynamic_fanout": shards, + "fanout_meta": [ + { + "id": "B1_evidence_doc_fanout", + "count": n, + "task_template": "Task_B1_map_doc", + "instance_id_template": "Task_B1_map_doc_{ordinal:03d}", + "output_path_template": "evidence_indexed_parts/E-{ordinal:03d}.json" + }, + { + "id": "B2_event_candidate_fanout", + "count": n, + "task_template": "Task_B2_map_events", + "instance_id_template": "Task_B2_map_events_{ordinal:03d}", + "output_path_template": "evidence_event_candidate_parts/E-{ordinal:03d}.json", + "wait_until_template": "Task_B1_map_doc_{ordinal:03d}" + } + ], + "streaming_edges": streaming_edges, + "barriers": { + "B1_all_parts_barrier": { + "wait_for_tasks": b1_waits, + "require_files": b1_files, + "next": "Task_B1_quality_gate_evidence_indexed" + }, + "B2_all_parts_barrier": { + "wait_for_tasks": b2_waits, + "require_files": b2_files, + "plus": ["Task_B1_quality_gate_evidence_indexed"], + "next": "Task_B2_quality_gate_event_candidates" + } + }, + "stale_part_policy": { + "mode": "block", + "allowed_ordinals": [f"{i:03d}" for i in range(1, n + 1)], + "checked_directories": ["evidence_indexed_parts", "evidence_event_candidate_parts"], + "detected_stale_parts": { + "evidence_indexed_parts": stale_b1, + "evidence_event_candidate_parts": stale_b2 + }, + "downstream_gate_instruction": "B1/B2 quality gates must BLOCK if detected_stale_parts is non-empty or if files outside expected_ordinals are present." + }, + "non_legal_assertions": { + "does_not_classify_document_type": True, + "does_not_extract_facts": True, + "does_not_evaluate_evidence": True, + "does_not_calculate_amounts": True, + "does_not_decide_legal_effect": True, + "does_not_finalize_evidence_index": True, + "does_not_finalize_candidate_id": True + } + } + + if stale_b1 or stale_b2: + plan["planner_status"] = "BLOCKED_BY_STALE_PARTS" + plan["mapper_execution_allowed"] = False + plan["blocked_reason"] = "stale mapper part detected; B1/B2 mapper fanout must not execute and existing parts must not be reused" + else: + plan["planner_status"] = "READY" + plan["mapper_execution_allowed"] = True + plan["blocked_reason"] = None + + write_doc("evidence_shard_plan.json", json.dumps(plan, ensure_ascii=False, indent=2)) + # 백엔드 wildcard fan-out 확장이 stdout JSON의 dynamic_fanout 키를 읽음. + # 전체 plan은 너무 커서 dynamic_fanout만 출력한다. 단 planner_status가 READY가 아니면 mapper fanout은 반드시 비운다. + fanout_payload = plan["dynamic_fanout"] if plan["planner_status"] == "READY" else [] + print(json.dumps({"dynamic_fanout": fanout_payload, "planner_status": plan["planner_status"], "mapper_execution_allowed": plan["mapper_execution_allowed"]}, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B1_map_doc_* + max_concurrency: 12 + preflight_files: + - "{{item.shard_doc_path}}" + - client_meeting.md + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + + Task_B1_map_doc + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. Your role in this task is a **per-document mapper** within the v.3 Step D streaming map-reduce pipeline. + + + 본 task는 v.3 Step D의 B1 mapper다. orchestrator 가 runtime parameter (`{{item.*}}`) 로 주입한 assigned_ordinal 단일 증거문서만 처리하여, 그 문서의 authority component·document component envelope·legal component envelope을 추출한 part 파일을 작성한다. + 본 task는 증거 문서 수 N을 계산하지 않고, ordinal을 배정하지 않고, B1/B2 mapper 인스턴스를 생성하지 않는다. 이러한 orchestration 책임은 전부 `Task_Evidence_shard_planner`와 orchestrator의 권한이다. + 최종 `evidence_indexed.json`은 본 task가 작성하지 않는다. 본 task는 `evidence_indexed_parts/E-{ordinal:03d}.json` part 파일만 작성하며, finalize는 `Task_B1_quality_gate_evidence_indexed`가 수행한다. + 본 task는 자기 문서 안에서 직접 확인되는 component만 작성한다. 다른 문서의 사실을 자기 part에 주입하면 quality gate의 authority drift check에서 BLOCK 또는 HARD_WARNING으로 검출된다. + `evidence_index`는 planner가 부여한 `E-{ordinal:03d}` 형식의 deterministic candidate를 그대로 propose 하되, final 부여는 quality gate가 수행한다. 본 task는 candidate id를 자기 part의 `evidence_index_proposed` 필드에 기록한다. + 본 task의 추출 깊이는 v.2 원본 Task_B1과 동일하다. document component envelope의 모든 하위 구조(claim_document_components, business_transfer_components, registry_row_components, secured_debt_components, valuation_rows, mixed_contract_payment_semantics, loan_components, valuation_matrix, registry_row_role_map, defense_assertions, admission_components, defense_components, notice_components, party_assertion_components, schedule_items, registry_object_display, succession_document_components)를 자기 문서 범위 안에서 가능한 한 분리 추출한다. + 변호사 관점에서 본 단계의 핵심은 등기부 row, 차용증 component, 통지서 발송/도달일 분리, 가족관계·상속포기 component, 별지목록 등의 직접 보존이다. 이는 청구원인 timeline·청구취지 별지목록·금액 계산의 기초가 된다. + + + + + + - runtime parameters: `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.source_document_ref}}`, `{{item.b1_output_path}}` (planner 가 부여한 단일 source of truth — 별도 파일 read 불요) + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 문서 1건 — planner가 evidence_all.json에서 분리해 둔 단일 shard 파일) + - `client_meeting.md` (보조 사용만 허용) + + + - `evidence_indexed_parts/E-{ordinal:03d}.json` (본 mapper 인스턴스의 part 파일 1개만) + + + - 다른 ordinal의 문서를 처리하기 + - 다른 ordinal의 사실을 자기 part에 주입하기 (authority drift) + - 증거 문서 수 N을 재계산하여 planner 결과를 대체하기 + - assigned_ordinal을 새로 생성하거나 재정렬하기 + - B1/B2 mapper fan-out 또는 streaming edge를 설계하기 + - 최종 `evidence_indexed.json` 작성 (이는 quality gate의 단독 권한) + - 다른 mapper의 part 파일 읽기/덮어쓰기 + - root 전체 탐색 + - 이전 task 결과 또는 backend 자동 로드 데이터 사용 + - 자기 part 재읽기 + - cross-document legal conclusion 확정 + - 입력에 없는 사실 추가 + + + + + 1. `list_docs`로 `{{item.shard_doc_path}}`, `client_meeting.md` 존재만 확인한다. + 2. `read_docs`로 위 두 파일을 읽는다. `{{item.shard_doc_path}}`는 이 인스턴스의 단일 evidence 문서로 그대로 사용한다 (별도의 ordinal 추출 불필요). assigned_ordinal·evidence_index_candidate·b1_output_path 는 runtime parameter (`{{item.*}}`) 그대로 사용한다. + 3. 메모리에서 part item 객체를 생성한다(items 배열이 아닌 단일 item object 또는 1-element items 형태). + 4. `write_file(overwrite=true)`로 runtime parameter 가 지정한 `evidence_indexed_parts/E-{ordinal:03d}.json`을 저장한다. + 5. `list_docs`로 자기 part 파일 존재만 확인한다. 절대 읽지 않는다. + 6. 정확히 `"E-{ordinal:03d} mapper part 작성 완료"`만 출력하고 종료한다. + + + + - 입력 누락, 빈 파일, runtime parameter 누락, ordinal 매핑 실패, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. part 파일은 쓰지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + - tool로 읽은 내용만 사실로 취급한다. + + + + - 본 mapper 인스턴스는 정확히 runtime parameter (`{{item.assigned_ordinal}}`) 에 해당하는 1개 문서만 처리한다. + - runtime parameters (`{{item.*}}`) 는 ordinal/binding 의 단일 source of truth 다 (별도 plan 파일 read 불요). + - `evidence_all.json` 배열 순서를 절대 바꾸지 않고, 배열 순서를 근거로 새 plan을 만들지도 않는다. + - planner의 `source_document_ref.content_sha256` 또는 `doc_uid_candidate`가 현재 문서와 명백히 충돌하면 part 파일을 쓰지 않는다. + - 본 task는 planner가 확정한 `evidence_index_candidate`, `b1_output_path`, assigned_ordinal을 소비할 뿐 재작성하지 않는다. + - 다른 ordinal의 문서 내용을 현재 문서에 보충하지 않는다(authority drift 금지). + - unknown 처리: + - nullable scalar → `null` + - 배열 → `[]` + - 비nullable string만 `"불명"` 사용 + - 배열은 exact-match 중복 제거 후 최초 순서를 유지한다. + - part 파일은 단일 item object 형태이며, 기존 필수 필드(`evidence_index_proposed`, `doc_uid`, `title`, `title_normalized`, `doc_type`, `key_*`, `source_pointer`)를 모두 포함한다. + - optional additive field는 downstream(quality gate)이 무시해도 안전해야 한다. + - `evidence_index_proposed`는 deterministic 규칙 `E-{ordinal:03d}`로만 생성한다. 임의 변형 금지. + + + + 이 인스턴스의 `{{item.shard_doc_path}}`(planner가 evidence_all.json에서 분리한 단일 evidence 1건) + `client_meeting.md` (보조만 허용) + + + `client_meeting.md`는 아래 목적에만 사용할 수 있다. + - `asset_id` + - `property_label_for_schedule` + - `transaction_type` + - `transaction_date` + - `market_value_at_act` + - `market_value_at_close` + - `consideration_breakdown` + - `property_cluster_id` + - 현재 문서의 축약 명칭이 어떤 자산군을 가리키는지에 관한 shorthand 해석 + + + + - 현재 문서와 `client_meeting.md`가 충돌하면 현재 문서가 우선한다. + - 예외: 등기부상 형식적 원인이 `매매`여도, 현재 문서 특약 또는 `client_meeting.md`가 실질적으로 채무갈음/대물변제를 직접 드러내면 `transaction_type`은 `"대물변제"`로 할 수 있다. + - 단, 이 예외는 현재 문서 또는 meeting이 실질 원인을 직접적으로 드러내는 경우에만 적용한다. + + + + + - `ordinal` = `evidence_all.json`의 1-based 배열 위치 (assigned_ordinal과 동일) + - `evidence_index_proposed` = `E-{ordinal:03d}` (mapper propose; quality gate가 finalize) + - `title` = 현재 문서 제목 원문 그대로 + - `title_normalized` = 앞뒤 공백 제거 + 내부 연속 공백/줄바꿈을 1칸으로 축약 + - `doc_uid` = `DOC-{ordinal:03d}-{title_normalized의 공백을 "_"로 치환한 값}` + - `source_pointer` = `{"source":"evidence_all.json","ordinal":ordinal}` + + + + 다음 순서로만 핵심을 읽는다. + 1. `title` + 2. `heading` + 3. `key_value` / `form` + 4. 관련 `table` 행 + 5. `list` + 6. `paragraph` + + 보일러플레이트(바코드 안내, 발급확인 문구, 반복 footer/page text)는 무시한다. + 단, `관할등기소`, `접수번호`, `등기원인일`, `발행일` 등 필요한 사실이 있으면 사용한다. + + + + `doc_type` enum은 v.2 원본과 동일하다. nuance는 `doc_semantic_flags`로 보강한다. + 1. `판결|결정|배당표|가압류|가처분|경매|타경` → `"판결/결정"` + 2. `등기사항전부증명서|주민등록|가족관계증명서|사업자등록증` → `"공문서"` + 3. `대출|명세|거래|계산서` → `"거래기록"` + 4. `카톡|문자|이메일|통화` → `"통신기록"` + 5. `계약서|약정서|확인서|영수증|차용증|각서` → `"처분문서"` + 6. 그 외 → `"기타"` + + + + - `key_facts`: 최대 3개, 각 60자 이하 + - `key_dates`: 최대 3개 + - `key_amounts`: 최대 3개 + - `key_parties`: 최대 5개 + - 현재 문서에서 직접 읽히는 핵심만 남긴다. + - 장문 요약, 계산식, 추정 보정 금지. + - 가능하면 heading / key_value / 표의 핵심 행만 사용한다. + + + + + - `property_cluster_id`는 optional field다. + - 현재 문서만으로 특정 자산군이 명백하거나, 현재 문서의 표기와 `client_meeting.md`가 정확히 대응할 때만 채운다. + - 예: `"해당 대지/건물"`, `"해당 상가"`, `"해당 주거용 부동산"`, `"해당 임야"`, `"해당 영업양도"`, `"해당 물품대금"`. + - 명백하지 않으면 `null`. + + + + `doc_semantic_flags`는 optional field다. 아래 값만 사용한다. + - `multi_event_doc` + - `registry_doc` + - `mixed_contract_doc` + - `valuation_doc` + - `claim_doc` + - `status_doc` + - `notice_doc` + - `response_doc` + - `business_transfer_doc` + - `supporting_reference_doc` + - `admission_doc` + - `defense_doc` + - `notice_receipt_admission_doc` + - `lien_assertion_doc` + - `schedule_doc` + - `relief_object_reference_doc` + - `succession_doc` + + 부여 규칙: + - 등기부처럼 복수 row가 서로 다른 법적 효과를 가지면 `multi_event_doc` + - 갑구/을구 등기행위가 있으면 `registry_doc` + - 임대차와 매매 등 서로 다른 계약 구성요소가 같은 문서에 공존하면 `mixed_contract_doc` + - 시가, 차임시세, 사용이익 가치가 표로 제시되면 `valuation_doc` + - 차용증, 보증서, 대여약정 등 채권 발생 문서면 `claim_doc` + - 답변서, 사업자등록증, 현재 상태 확인서처럼 상태를 보여주면 `status_doc` + - 내용증명, 통지서, 최고서, 해제통지서면 `notice_doc` + - 상대방 답신, 답변서, 반박문이면 `response_doc` + - 영업양도 문서면 `business_transfer_doc` + - 별지 목록처럼 단독 계산객체는 없으나 다른 문서 식별을 돕는 문서면 `supporting_reference_doc` + - 상대방이 통지 수령, 임대, 차임, 점유, 채무, 변제 등을 인정하면 `admission_doc` + - 명의신탁, 충분담보, 상계, 선의, 후소유자 소멸청구권 부정, 피담보채권 충당 등 항변이 있으면 `defense_doc` + - 유치권 소멸청구 통지의 수령일을 인정하면 `notice_receipt_admission_doc` + - 유치권 주장, 공사잔금, 유치권 목적물 사용·임대가 있으면 `lien_assertion_doc` + - 별지목록, 목적물 목록, 청구취지에서 그대로 사용할 문구가 있으면 `schedule_doc` 및 `relief_object_reference_doc` + - 가족관계증명서, 상속포기 심판문, 사망·상속 관련 공문서이면 `succession_doc` + + + + - 등기부 문서에서 갑구/을구 여러 row가 서로 다른 법적 효과를 가지면 `multi_event_registry=true`를 optional field로 기록한다. + - `multi_event_registry=true`인 문서는 단일 snapshot 요약을 강요하지 않는다. + + + + + - 문서가 여러 법률효과를 담고 있으면 component 단위로 분해한다. + - 날짜는 작성일, 발송일, 도달일, 법률효과 발생일, 변제기, 기산일 후보로 의미별 분리한다. + - 금액은 원금, 이자, 보증금, 차임, 사용이익, 채권최고액, 실제 변제액, 가액배상 공제액으로 성격별 분리한다. + - 아래 component field들은 optional additive field이며, 현재 문서에 직접 읽히는 범위에서만 사용한다. + + + - 문서가 이행각서, 채무승인서, 최고서, 내용증명, 지급요구서, 변제약정서, 차용증, 납품대금 확인서이면 `claim_document_components`를 생성한다. + - 단순 `key_dates`에 문서작성일만 넣고 끝내지 말고, 문서 안에 지급기일, 새 변제기, 최고 도달일, 채무승인일, 청구금액, 채권자, 채무자가 직접 읽히면 component field로 분해한다. + - 이행각서에서 "언제까지 지급한다"는 문구가 있으면 반드시 `new_due_date` 후보로 분리한다. + - 최고서에서 발송일과 수령일/도달일이 모두 있으면 `dispatch_date`와 `arrival_date`를 분리한다. + - claim component의 `component_kind` 허용값은 `underlying_claim|debt_acknowledgement|demand_notice|notice_dispatch|notice_arrival|new_due_date|payment_promise|limitation_relevance|other`다. + + + + - 영업양도계약서, 상호 계속 사용, 사업 양수인, 영업자산 이전, 고객·직원 승계, 채무불승계 조항, 책임배제 등기 단서가 있으면 `business_transfer_components`를 생성한다. + - `business_name`, `transferor_candidates`, `transferee_candidates`, `business_assets_transferred`, `trade_name_continuation_clause`, `customer_or_employee_succession_clause`, `pre_transfer_debt_exclusion_clause`, `liability_exclusion_registration_clause`를 직접 읽히는 범위에서 채운다. + - 기존 `contract_components`가 있어도 영업양도 책임 검토용 구조는 `business_transfer_components`에 별도 보존한다. + + + + - 등기부, 등기사항전부증명서, 등기내역 문서는 반드시 row 단위로 쪼갠다. + - 각 row마다 갑구/을구, 순위번호, 등기목적, 접수일, 접수번호, 원인, 권리자, 채무자, 채권최고액, 객체 범위를 추출하여 `registry_row_components`에 둔다. + - 청구취지 특정에 필요한 접수번호는 `key_facts` 요약에만 두지 말고 `registry_row_components[*].receipt_no`로 보존한다. + - 차용증, 근저당권 설정계약, 등기부 row가 연결되면 `secured_debt_components`를 생성한다. + - 복수 채무의 원금, 이율, 변제기, 담보 지분이 다르면 법정변제충당 계산 대상이므로 모두 구조화한다. + - 사해행위취소 사건의 등기부 row는 필요하면 `registry_row_role_map`에 actio role을 추가로 남긴다. + - `registry_row_components`의 각 row에 `registry_ref`를 부여한다. 형식: `REG-{ordinal:03d}-{row_seq:02d}` (갑구/을구·순위번호 순 deterministic). + - row에 지분 표시(예: "2분의 1", "1/2 지분")가 직접 읽히면 `registry_row_components[*].fraction_display`(원문 문자열)와 `registry_row_components[*].fraction_ref`를 채운다. + - `fraction_ref`는 `FR-{ordinal:03d}-{row_seq:02d}` 형식의 deterministic propose 값이다. 같은 row에서 파생되는 모든 component는 같은 `fraction_ref`를 공유한다. + - 지분 표시의 분자·분모가 직접 읽히는 경우에만 optional 필드 `fraction_numerator`/`fraction_denominator`를 채운다(예: "2분의 1" → 1/2). 환산·약분·보간 금지. 직접 읽히지 않으면 null. + + + + - 감정표는 금액만 추출하지 말고 `valuation_rows`에 row별 전제조건, 금액, 단위, 법률상 사용 가능성을 함께 추출한다. + - 사해행위취소 valuation 문서가 복수 자산 또는 복수 시점을 담으면 `valuation_matrix`를 생성하고, 각 row의 `legal_uses`를 표시한다. + - 임대차보증금, 월 차임, 매매대금이 한 문서에 같이 있으면 `mixed_contract_payment_semantics`에서 각각 별도 payment semantic으로 분리한다. + - 보증금은 원칙적으로 반환 예정 담보성 금원인지 검토하고, 원고가 직접 수령하지 않았다면 원고의 부당이득 원금으로 자동 전환하지 않는다. + - 월 차임은 피고들 사이의 계약상 차임과 객관적 토지 사용이익을 구분한다. + + + + - 사해행위취소 사건의 증거문서는 일반 key_facts 요약만으로 충분하지 않다. + - 차용증에 복수 대여금이 있으면 `loan_components`로 분리하고, 각 원금·변제기·이자율·담보 목적물·청구 내 역할을 추출한다. + - 감정평가서는 시점별 `valuation_matrix`로 보존하고, 각 row의 legal use를 표시한다. + - 등기부는 갑구/을구 row별로 쪼개고, 각 row의 `actio_roles`를 부여한다. + - 내용증명 답신에 명의신탁, 충분담보, 상계, 선의 주장이 있으면 `defense_assertions`로 추출한다. + - 사후 말소된 담보권과 그 말소를 유발한 변제액은 가액배상 계산의 필수 입력으로 표시한다. + + + + - `loan_components`의 각 항목은 아래 named 필드를 가진다. 직접 읽히지 않는 필드는 null. + { + "loan_component_id": "E-{ordinal:03d}-LC-{seq:02d}", + "principal_amount": null, + "loan_date": null, + "due_date": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "rate_text": null, + "rate_source_locator": null, + "interest_accrual_start_hint": null, + "collateral_object_hint": null, + "role_in_claim_hint": null + } + - `agreed_interest_rate`는 원문 그대로의 문자열("연 5%", "월 1%" 등)만 허용한다. + - `agreed_delay_damage_rate`는 약정 지연손해금율이다. 없으면 null로 둔다(법정이율로 보충 금지). + - `rate_text`는 이율 관련 원문 문자열 그대로, `rate_source_locator`는 그 이율이 읽힌 조항 locator(예: "제2조", "특약")다. + - `agreed_interest_rate`/`agreed_delay_damage_rate`는 계산·환산·보간 금지. 원문 문자열만 허용. + - `secured_debt_components`의 각 항목에도 동일한 optional rate 필드(`agreed_interest_rate`, `agreed_delay_damage_rate`, `rate_text`, `rate_source_locator`, `due_date`)를 직접 읽히는 범위에서 둘 수 있다(근저당권 설정계약서·차용증 겸용 문서의 이율 보존용). + - 이율이 문서에 있는데 두 rate 필드가 모두 null이면 self-warning으로 본다. + + + + - 답변서에 통지 수령 인정, 제3자 임대 자인, 월 차임 자인, 유치권자 사용 허용 주장, 후소유자 소멸청구권 부정 주장, 피담보채권 충당 주장이 있으면 `admission_components`와 `defense_components`로 분리한다. + - 통지서에는 `notice_components`를 생성하고, 통지 유형, 발송일, 도달 필요 여부, 수령자, 목적물, 법률효과 후보를 분리한다. + - `notice_components`의 각 항목은 아래 named 필드를 가진다. 직접 읽히지 않는 필드는 null. + { + "notice_id_proposed": "E-{ordinal:03d}-NT-{seq:02d}", + "notice_type": "최고|해제통지|유치권소멸청구통지|기타", + "drafted_date": null, + "dispatch_date": null, + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [], + "receiver_candidates": [], + "target_object_hint": null, + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "legal_effect_candidate_hint": null + } + - 5개 lifecycle 날짜의 의미: `drafted_date`=작성일, `dispatch_date`=발송일, `arrival_date`=도달일(민법 제111조 도달주의 — 도달≠수령을 구분), `received_date`=수령일(수령 인정 포함; 도달일과 다르면 별도 보존), `legal_effect_date_candidate`=법률효과 발생 후보일(계산 금지, 문서 직접 기재만). + - `requires_receipt_for_effect`는 true|false|null — 문서 유형상 도달/수령이 효력요건인지의 후보 표시다. + - `receipt_evidence_refs`는 수령을 뒷받침하는 같은 문서 내 단서 locator다. + - `recipient_admission_text`는 통지 수령을 인정하는 원문 문자열이며, 답변서 문서의 component에서만 채운다. + - 수령일이 상대방 답변서에서 인정되는 경우 그 답변서의 `admission_components`에 남기고, 본 통지서 문서의 `received_date`에 타 문서 사실을 주입하지 않는다(authority drift 금지). cross-document 연결은 Part 2 B5가 수행한다. + - 당사자가 요구한 기간이나 금액이 법률상 기산일로 자동 사용되면 안 되는 경우 `party_assertion_components.should_not_auto_use_as_legal_start_date=true`를 남긴다. + - 별지목록에는 `schedule_items`를 생성하고, 청구취지에서 그대로 사용할 수 있는 문구와 asset alias, 전유부분, 대지권, 도로명주소를 보존한다. + - `schedule_items`의 각 항목에 `schedule_ref`를 부여한다. 형식: `SCH-{ordinal:03d}-{seq:02d}` (별지 기재 순서 deterministic). asset alias 문자열은 기존대로 함께 보존한다. + - `schedule_items`의 각 항목에 optional 필드 `canonical_label_candidate`(별지 표제 문구 원문)와 `usable_in_relief`("yes|no|unknown" — 청구취지에서 그대로 사용할 수 있는 문구인지)를 직접 읽히는 범위에서 둘 수 있다. + - 등기부가 목적물 특정 자료로 쓰이면 `registry_object_display`를 생성한다. + - 가족관계증명서, 상속포기 심판문, 사망·상속 자료에는 `succession_document_components`를 생성하고 피상속인, 사망일, 배우자 후보, 직계비속 후보, 직계존속 후보, 상속포기자, 신고일·수리일, 연결 asset을 보존한다. + + + + + + - 기존 optional field `legal_calculation_object`는 backward-compatible summary field로 유지한다. + - 그러나 이 필드는 안전할 때만 사용한다. + - 문서가 `multi_event_doc` 또는 `multi_event_registry=true`이면 원칙적으로 `legal_calculation_object`를 생성하지 않는다. + - 문서에 서로 다른 법적 효과의 slot이 2개 이상 공존하면 단일 summary `legal_calculation_object`로 압축하지 않는다. + - 단일 효과가 명백한 문서, 또는 서로 다른 값이 경쟁하지 않는 문서에서만 `legal_calculation_object`를 생성한다. + + + + - multi-event 또는 mixed-contract 문서를 위해 optional field `legal_calculation_objects`를 추가할 수 있다. + - `legal_calculation_objects`는 현재 문서 안의 개별 slot을 권위 있게 분해한 배열이다. + - 문서당 0개 이상 허용한다. + - 단일 slot 문서라면 `legal_calculation_objects`를 생략할 수 있다. + - 각 slot은 현재 문서의 직접 기재만 사용한다. 계산, 합산, 차감 금지. + + + + 개별 slot은 아래 경우에 생성한다. + - 소유권이전 / 매매 / 증여 / 대물변제 / 경매매각 / 경매개시결정 / 근저당권설정 / 근저당권말소 / 가압류 / 임대차 / 영업양도 / 대여 / 담보제공 + - valuation 문서는 시가표/차임표/사용이익표를 slot이 아니라 `valuation_object`로 처리한다. + - 등기부는 갑구/을구의 의미 있는 row마다 1 slot씩 생성한다. + - 혼합계약서는 계약 조항상 독립된 법적 효과마다 1 slot씩 생성한다. + + 각 slot은 아래 구조를 따른다. + { + "slot_id": "E-{ordinal:03d}-LCO-01", + "slot_kind": "ownership_transfer|gift|sale|dation|mortgage_setting|mortgage_cancellation|auction_commencement|auction_sale|lease|building_sale|loan|business_transfer|other", + "source_locator": { + "locator_type": "heading|key_value|table_row|paragraph|unknown", + "locator_hint": "갑구4|을구1|제1조|특약 등", + "excerpt": "짧은 단서" + }, + "asset_id": null, + "property_label_for_schedule": null, + "transaction_type": "매매|증여|대물변제|null", + "transaction_date": null, + "registration_date": null, + "registry_office": null, + "registry_receipt_no": null, + "registry_recorded_transfer_date": null, + "market_value_at_act": null, + "market_value_at_close": null, + "sale_price": null, + "monthly_rent": null, + "consideration_breakdown": [], + "parties": [] + } + - slot이 등기부 row 또는 별지 항목에서 직접 파생되면 optional 필드 `registry_ref_hint`/`schedule_ref_hint`를 남긴다. + + + + - 임대차와 매매가 함께 있는 혼합계약서, 영업양도와 채무불인수 조항이 함께 있는 문서 등은 optional field `contract_components`를 추가할 수 있다. + - `contract_components`는 현재 문서의 조항별 법적 효과 분해 메모다. + - 각 component는 아래 구조를 따른다. + { + "component_id": "E-{ordinal:03d}-CC-01", + "component_kind": "lease|sale|delivery|management_use_shift|permit_cooperation|business_transfer|debt_non_assumption|other", + "object_spec": "짧은 목적물", + "amount": null, + "date": null, + "payer_candidates": [], + "recipient_candidates": [], + "possession_recipient_candidates": [], + "benefit_shift_to_candidates": [], + "source_locator": { + "locator_type": "paragraph|table_row|key_value|unknown", + "locator_hint": "제1조|제2조|특약 등", + "excerpt": "짧은 단서" + } + } + - 혼합계약서에서 component가 2개 이상 명백하면 2개 이상 생성한다. + - mixed contract에 임대차, 매매, 인도, 관리·사용 귀속이 직접 적혀 있으면 이를 lease/sale 1개로 뭉개지지 말고 가능한 한 분리한다. + - 조항에서 누가 대가를 지급하고 누가 수령했는지가 직접 읽히면 `payer_candidates`, `recipient_candidates`를 채운다. + - 인도받는 자가 직접 읽히면 `possession_recipient_candidates`를 채운다. + - 관리·사용 또는 사용수익 귀속 주체가 직접 읽히면 `benefit_shift_to_candidates`를 채운다. + - 위 directionality field는 현재 문서에 직접 적힌 경우에만 채우고, 추정으로 보충하지 않는다. + + + + - 시가, 차임시세, 사용이익 평가 문서는 optional field `valuation_object`를 추가할 수 있다. + - `valuation_object`는 아래 구조를 따른다. + { + "valuation_kind": "market_value|rent_value|use_gain_value", + "target_object": "짧은 목적물", + "basis_date_or_period": "원문 기간 또는 기준일", + "value_rows": [ + { + "label": "2022.1.1.~2022.12.31.|보증금 3억 원인 경우 등", + "amount": "금액 문자열", + "unit": "총액|월액|기타" + } + ] + } + - valuation 문서는 기존 `doc_type`를 바꾸지 않고 `doc_semantic_flags`와 `valuation_object`로 구체화한다. + - 차임표/사용이익표가 있으면 `market_value_*`를 억지로 채우지 않는다. + + + + + - 날짜: 정확한 일자가 있으면 `YYYY-MM-DD`, 없으면 `null` + - 단, `key_dates`와 `valuation_object.basis_date_or_period`는 짧은 원문 텍스트 허용 + - 금액: 입력에 명시된 값만 `"300,000,000원"` 형식 문자열로 정규화 + - 이름/당사자: 원문 우선, 명백히 동일할 때만 canonicalize + - `consideration_breakdown`는 직접 읽히는 짧은 대가 항목만 저장, 최대 4개, 각 40자 이하 + + + + part 파일은 단일 item object 형태이며, top-level에 mapper가 propose하는 모든 component field를 둔다. + + { + "evidence_index_proposed": "E-{ordinal:03d}", + "doc_uid": "DOC-{ordinal:03d}-...", + "title": "...", + "title_normalized": "...", + "doc_type": "판결/결정|공문서|거래기록|통신기록|처분문서|기타", + "key_facts": [], + "key_dates": [], + "key_amounts": [], + "key_parties": [], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": ordinal + }, + "property_cluster_id": null, + "doc_semantic_flags": [], + "multi_event_registry": null, + "contract_components": [], + "valuation_object": null, + "legal_calculation_object": null, + "legal_calculation_objects": [], + "claim_document_components": [], + "business_transfer_components": [], + "registry_row_components": [], + "secured_debt_components": [], + "valuation_rows": [], + "mixed_contract_payment_semantics": {}, + "loan_components": [], + "valuation_matrix": {}, + "registry_row_role_map": {}, + "defense_assertions": [], + "admission_components": [], + "defense_components": [], + "notice_components": [], + "party_assertion_components": [], + "schedule_items": [], + "registry_object_display": {}, + "succession_document_components": null, + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } + } + + 추가 제약: + - top-level은 단일 object다. envelope이 아닌 단일 item. + - `evidence_index_proposed`는 mapper의 propose 식별자이며, finalize는 quality gate가 한다. + - `mapper_meta.self_isolation_attested=true`는 본 mapper가 자기 문서 외 사실을 주입하지 않았음을 self-attest 한다는 의미다. + - optional component field는 직접 읽히는 범위에서만 채우고, 빈 배열/객체/`null`로 두는 것을 우선한다. + + + + - part 파일이 단일 object이고 `evidence_index_proposed`, `doc_uid`, `title`, `title_normalized`, `doc_type`, `source_pointer`를 모두 포함하는지 점검한다. + - `planner_status=="READY"` 및 `mapper_execution_allowed=true`가 확인되지 않았으면 part 파일이 작성되지 않았는지 점검한다. + - `evidence_index_proposed`가 정확히 `E-{ordinal:03d}` 형식인지 점검한다. + - 자기 문서 외 ordinal의 사실이 part에 주입되지 않았는지 self-check 한다(authority drift 방지). + - `registry_doc`이면서 서로 다른 법적 효과 row가 2개 이상 읽히면 `doc_semantic_flags`에 `multi_event_doc`를 넣고, 가능한 경우 `legal_calculation_objects`를 생성한다. + - `multi_event_registry=true` 또는 `doc_semantic_flags`에 `multi_event_doc`가 있으면 단일 `legal_calculation_object`를 억지로 채우지 않는다. + - `mixed_contract_doc`인데 명백한 독립 component가 2개 이상이면 `contract_components`를 2개 이상 생성한다. + - `mixed_contract_doc`에 인도 또는 관리·사용 귀속이 직접 적혀 있으면 `delivery` 또는 `management_use_shift` component가 누락되지 않았는지 점검한다. + - 혼합계약 조항에서 지급자·수령자 또는 점유 이전 상대방이 직접 읽히면 `payer_candidates`, `recipient_candidates`, `possession_recipient_candidates`, `benefit_shift_to_candidates`가 비어 있지 않은지 점검한다. + - `valuation_doc`인데 가치표/차임표가 직접 보이면 `valuation_object`를 생성한다. + - 같은 문서에서 복수 행위가 보이는데도 단일 snapshot `legal_calculation_object`만으로 압축하지 않는다. + - `claim_doc`인데 이행각서ㆍ채무승인ㆍ최고ㆍ변제기ㆍ청구금액 단서가 직접 보이고 `claim_document_components`가 없으면 self-warning으로 본다. + - 이행각서에 새 변제기가 있는데 `new_due_date`가 없으면 self-warning으로 본다. + - 최고서 또는 통지서에 도달·수령 단서가 있는데 `arrival_date` 또는 notice receipt component가 없으면 self-warning으로 본다. + - `business_transfer_doc`에 상호ㆍ영업명ㆍ영업자산ㆍ채무불승계 단서가 있는데 `business_transfer_components`가 없으면 self-warning으로 본다. + - 등기부에 접수번호가 있는데 `registry_row_components[*].receipt_no`가 없으면 self-error로 본다. + - 차용금 문서에 이율 또는 변제기가 있는데 `secured_debt_components` 또는 `loan_components`가 없으면 self-warning으로 본다. + - `loan_components`가 있는데 이율 문구가 문서에 보이면서 `agreed_interest_rate`가 null이면 self-warning으로 본다. + - valuation 문서에 복수 시점 또는 복수 row가 있는데 `valuation_rows[*].legal_use_case` 또는 `valuation_matrix.rows[*].legal_uses`가 없으면 self-warning으로 본다. + - 임대차보증금이 추출되었는데 `mixed_contract_payment_semantics`에서 반환성 보증금과 사용이익 원금 배제 여부가 구분되지 않으면 self-warning으로 본다. + - 등기부 문서에 사후 말소 row가 있는데 `registry_row_role_map` 또는 `registry_row_components`에 release role이 없으면 self-error로 본다. + - 답변서에 수령 인정ㆍ제3자 임대ㆍ항변이 있는데 `admission_components` 또는 `defense_components`가 없으면 self-warning으로 본다. + - 별지목록에 목적물 항목이 있는데 `schedule_items`가 없으면 self-error로 본다. + - 상속자료가 있는데 `succession_document_components`가 없으면 self-warning으로 본다. + - self-warning/self-error는 mapper가 part 파일에 그대로 남겨도 되며, quality gate가 최종 severity를 판정한다. + + + + + {{item.assigned_ordinal}} + + + [ ] 1) Preflight: `list_docs` 사용해서 `{{item.shard_doc_path}}`, `client_meeting.md` 만 확인, `read_docs` 사용해서 두 파일만 읽기 + [ ] 2) runtime parameter `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.b1_output_path}}` 그대로 소비. 별도 검증 read 불요. + [ ] 3) `{{item.shard_doc_path}}`의 단일 evidence 문서를 메모리에서 사용하고, 주입된 shard 밖으로 나가지 않기 + [ ] 4) `write_file` 사용해서 `evidence_indexed_parts/E-{ordinal:03d}.json` 생성 (ordinal은 `{{item.assigned_ordinal}}`) + [ ] 5) `list_docs` 사용해서 자기 part 파일만 존재 확인(절대 읽기 금지) + [ ] 6) `"E-{ordinal:03d} mapper part 작성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 1건; planner가 evidence_all.json에서 분리) + - `client_meeting.md` + + + - `evidence_indexed_parts/E-{ordinal:03d}.json` + + + + + - `evidence_index_proposed`는 정확히 `E-{ordinal:03d}` 형식만 허용한다. + - `ordinal`과 output path는 runtime parameter (`{{item.*}}`) 를 그대로 따른다. + - 증거 문서 수 N, assigned_ordinal, mapper fan-out, streaming edge는 본 task에서 만들지 않는다. + - 자기 part 파일은 절대 재읽지 않는다. + - 다른 ordinal 문서를 처리하지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"E-{ordinal:03d} mapper part 작성 완료"` 한 줄만 사용한다. + + + + - task_name: Task_B2_map_events_* + max_concurrency: 12 + preflight_files: + - "{{item.shard_doc_path}}" + - "{{item.b1_output_path}}" + - client_meeting.md + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + + Task_B2_map_events + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. Your role in this task is a **per-document event/state candidate mapper** within the v.3 Step D streaming map-reduce pipeline. + + + 본 task는 v.3 Step D의 B2 mapper다. orchestrator 가 runtime parameter (`{{item.*}}`) 로 주입한 assigned_ordinal 단일 문서의 event/state candidate(raw event + legal-effect event 포함)를 추출하여 part 파일을 작성한다. + 본 mapper 인스턴스는 orchestrator가 같은 ordinal의 B1 mapper(Task_B1_map_doc) 완료와 `evidence_indexed_parts/E-{ordinal:03d}.json` 생성을 확인한 뒤 시작한다. 본 task는 streaming edge를 설계하지 않고, planner/orchestrator가 이미 결정한 same-ordinal edge를 소비한다. + 최종 `evidence_event_candidates.json`은 본 task가 작성하지 않는다. 본 task는 `evidence_event_candidate_parts/E-{ordinal:03d}.json` part 파일만 작성하며, finalize는 `Task_B2_quality_gate_event_candidates`가 수행한다. + 본 task는 같은 ordinal의 B1 part 파일과 planner shard row를 authority metadata로 참조하고, planner가 분리해 둔 `{{item.shard_doc_path}}` 의 raw 문서 1건을 메모리에서 사용한다. + 같은 문서 내 복수 legal-effect event는 분리한다. 문서 밖 사실을 candidate payload에 넣지 않는다(authority drift 금지). + `candidate_id`는 `EVT-{ordinal:03d}-{seq:02d}` 형식의 deterministic candidate로 propose 하되, final 부여 및 collision 처리는 quality gate가 수행한다. + 변호사 관점에서 본 단계의 핵심은: (a) 등기부 갑구/을구 row별 분리, (b) 혼합계약서의 계약성립/대가수수/인도/관리·사용 귀속 4종 분리, (c) 통지 발송일과 도달일의 분리 candidate, (d) 답변서의 admission/defense 분리 candidate, (e) valuation 문서의 시점별/지표별 candidate, (f) 가족관계·상속포기 문서의 succession candidate 분리다. + legal-effect event는 raw event와 함께 추출하며, payment, notice, auction, lien, succession, valuation, defense assertion, commercial successor liability에 대해 v.2 원본의 REQUIRED_EVENT_SUBKINDS를 그대로 따른다. + + + + + + - runtime parameters: `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.b1_output_path}}`, `{{item.b2_output_path}}` (planner 가 부여한 단일 source of truth — 별도 plan 파일 read 불요) + - `{{item.b1_output_path}}` (같은 ordinal 의 B1 mapper part — runtime parameter 그대로 사용) + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 문서 1건; planner가 evidence_all.json에서 분리해 둔 단일 shard 파일) + - `client_meeting.md` (보조 사용만 허용) + + + - `evidence_event_candidate_parts/E-{ordinal:03d}.json` (본 mapper 인스턴스의 part 파일 1개만) + + + - 다른 ordinal의 B1 part 또는 raw 문서 처리 + - 다른 ordinal의 사실을 자기 part에 주입하기 (authority drift) + - 증거 문서 수 N을 재계산하여 planner 결과를 대체하기 + - assigned_ordinal을 새로 생성하거나 재정렬하기 + - B1/B2 mapper fan-out 또는 streaming edge를 설계하기 + - 최종 `evidence_event_candidates.json` 작성 (이는 quality gate의 단독 권한) + - 다른 mapper의 part 파일 읽기/덮어쓰기 + - 자기 part 재읽기 + - 문서 summary를 다시 쓰는 작업으로 변질시키기 + - meeting만으로 문서에 없는 사건행위를 새로 만들기 + - 입력에 없는 날짜, 당사자, 금액, 목적물을 창작하기 + + + + + 1. `list_docs`로 `{{item.b1_output_path}}`, `{{item.shard_doc_path}}`, `client_meeting.md` 존재만 확인한다. + 2. `read_docs`로 위 세 파일을 읽는다. `{{item.shard_doc_path}}`는 이 인스턴스의 단일 evidence 문서로 그대로 사용한다 (별도의 ordinal 추출 불필요). assigned_ordinal·evidence_index_candidate·b2_output_path 는 runtime parameter (`{{item.*}}`) 그대로 사용한다. + 3. 메모리에서 part 객체를 생성한다. 단일 item object 또는 1-element items 형태이며, event 후보가 0건이어도 파일을 생략하지 말고 `zero_event_reason`을 반드시 가진다. + 4. `write_file` 전에 아래 을 수행한다. 불량이면 먼저 내부 보정을 시도한다. + 5. 보정 후에도 schema invariant가 불량이면 part 파일을 쓰지 않고 `FAILED: E-{ordinal:03d} event candidate part schema invariant violation`만 보고하고 종료한다. + 6. 통과한 경우에만 `write_file(overwrite=true)`로 runtime parameter 가 지정한 `evidence_event_candidate_parts/E-{ordinal:03d}.json`을 저장한다. + 7. `list_docs`로 자기 part 파일 존재만 확인한다. 절대 읽지 않는다. + 8. 정확히 `"E-{ordinal:03d} event candidates part 작성 완료"`만 출력하고 종료한다. + + + + - 입력 누락, 빈 파일, runtime parameter 누락, B1 part 부재, ordinal 매핑 실패, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. part 파일은 쓰지 않는다. + - event 후보가 0건인 것은 실패가 아니다. 이 경우에도 runtime parameter 가 지정한 output path에 `zero_event_reason`을 포함한 schema-valid part 파일을 쓴다. + - `event_candidates: []`와 `zero_event_reason` 누락 또는 null이 함께 존재하는 것은 정상 zero-event가 아니라 schema invariant 위반이다. + - part schema invariant 위반이 내부 보정 후에도 남으면 part 파일은 쓰지 않는다. 불완전한 part를 생성하여 quality gate에 넘기지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + + - 본 mapper 인스턴스는 정확히 runtime parameter (`{{item.assigned_ordinal}}`) 에 해당하는 1개 문서만 처리한다. + - runtime parameters (`{{item.*}}`) 는 ordinal/binding 의 단일 source of truth 다 (별도 plan 파일 read 불요). + - B1 part의 `evidence_index_proposed`, `title`, `doc_type`, `source_pointer.ordinal`을 authority로 사용한다. + - `{{item.shard_doc_path}}`의 단일 evidence 문서(planner가 evidence_all.json에서 분리)를 직접 참조하여 candidate를 추출한다. + - 본 task는 planner가 확정한 `evidence_index_candidate`, `b2_output_path`, assigned_ordinal을 소비할 뿐 재작성하지 않는다. + - 다른 ordinal의 사실을 자기 part에 주입하지 않는다(authority drift 금지). + - candidate layer에 머문다. 최종 BO, 최종 JuristicAct, 최종 청구권, 최종 사해행위 판단을 확정하지 않는다. + - 문서 단위 1요약으로 뭉개지 않는다. 한 문서에 복수의 법적 중요 행위가 있으면 반드시 여러 candidate로 분리한다. + - support가 희박한 후보를 억지로 채우지 않는다. 불명확한 필드는 `null` 또는 빈 배열을 사용할 수 있다. + - 잘못된 과잉 특정화보다 보수적 분해가 낫다. 다만 자기 문서 커버리지를 포기해서는 안 된다. + - `candidate_id`는 deterministic 규칙 `EVT-{ordinal:03d}-{seq:02d}`로 생성한다(seq는 자기 part 내 1-based 순번). + + + + 이 인스턴스의 `{{item.shard_doc_path}}`(planner가 evidence_all.json에서 분리한 단일 evidence 1건) + `evidence_indexed_parts/E-{ordinal:03d}.json`의 authority metadata + `client_meeting.md` + + + `client_meeting.md`는 아래 목적에만 사용할 수 있다. + - alias 정규화 + - 동일인 식별 보조 + - property cluster shorthand 해석 + - 현재 문서가 어느 상담 조항을 뒷받침하는지에 대한 `meeting_clause_refs` 보조 + - 현재 문서에서 직접 읽히는 내용의 중요도 보조 + - 사망자와 특정 자산군의 연결이 meeting에서 직접 읽히는 경우 succession 관련 candidate의 `affected_asset_cluster_ids` 보조 + 금지: + - meeting만으로 문서에 없는 사건행위를 candidate로 생성 + - meeting만으로 현재 문서의 당사자/금액/날짜를 보충해 direct evidence처럼 쓰기 + + + + + - 자기 part는 정확히 1개 item을 가진다. + - 그 item은 아래 둘 중 하나를 반드시 가진다. + - `event_candidates`가 1개 이상 + - `zero_event_reason`가 non-null + - `zero_event_reason` 허용값: + - `"NO_LEGAL_EVENT_FOUND"` + - `"REFERENCE_ONLY_DOC"` + - `"PARSE_INSUFFICIENT"` + - `"ORDINAL_MATCH_FAILED"` + - 등기부, 계약서, 통지서, 답변서, 감정/시세 문서처럼 통상 event-bearing 문서에 대해서는 `zero_event_reason`를 매우 예외적으로만 사용한다. + + + + `write_file` 전에 반드시 메모리의 part object를 검증한다. 이 검증은 생성 지점 안전장치이며 자동 강제 재실행을 의미하지 않는다. + + 필수 invariant: + 1. 메모리상 top-level은 단일 item object 또는 1-element `items` envelope일 수 있으나, `write_file` 직전 serialization 대상은 단일 item object여야 하며 실질 item은 정확히 1개다. + 2. item의 `evidence_index_proposed`는 runtime parameter `{{item.evidence_index_candidate}}` 또는 그와 동일한 `E-{ordinal:03d}` 값이어야 한다. + 3. item은 아래 둘 중 하나를 반드시 만족해야 한다. + - `event_candidates`가 non-empty list + - `zero_event_reason`이 non-null이며 허용값 중 하나 + 4. `event_candidates`가 빈 배열이면 `zero_event_reason`은 반드시 non-null 허용값이어야 한다. + 5. candidate가 없어 `zero_event_reason`이 non-null이면 schema 안정성을 위해 `event_candidates`는 빈 배열로 둔다. + 6. candidate가 있으면 각 candidate는 object이며, `candidate_id_proposed`는 `EVT-{ordinal:03d}-{seq:02d}` 형식이고 `source_evidence_index_proposed`는 자기 `evidence_index_proposed`와 일치해야 한다. + + 내부 보정 원칙: + - 문서 검토 결과 법적 중요 event/state 후보가 없는 것이 맞으면 `zero_event_reason`을 허용값 중 하나로 명시한다. + - event/state candidate 생성 조건이 충족되는데 빈 배열이면 extraction routines로 되돌아가 candidate를 재작성한다. + - 통상 event-bearing 문서에 `zero_event_reason`을 사용하는 경우, 원문상 후보 부재 또는 파싱 불충분 이유가 허용값과 맞는지 재확인한다. + + 보정 실패: + - 위 invariant 중 하나라도 충족하지 못하면 `write_file`을 호출하지 않는다. + - 마지막 채팅 출력은 정확히 `FAILED: E-{ordinal:03d} event candidate part schema invariant violation`로 한다. + + + + 아래 범주의 법적 중요 행위를 후보로 추출한다. + - 소유권 이전, 매매, 증여, 대물변제, 처분 + - 근저당/저당/전세권/가압류/압류/담보 설정 및 말소 + - 대여, 보증, 신용보증, 대위변제, 구상권 발생, 변제, 배당 + - 임대차, 점유, 인도, 현재 점유, 현재 사용자, 현재 사용수익, 현재 수익귀속, 현재 권리귀속, 현재 관리 상태 + - 경매신청, 경매개시결정, 경매매각, 인도명령, 집행 관련 사실 + - 통지, 최고, 해제/해지의사표시, 도달, 채무승인, 감액요구, 책임부인, 현재상태 주장 + - 판결, 결정, 지급명령, 배당표 등 소송/집행 관련 법적 중요 사실 + - 신축, 영업양도, 사업자등록, 현재 영업상태 등 사건핵심 상태 형성 행위 + - 위 범주와 직접 연결되는 기타 법적 중요 행위 또는 현재상태 + + 제외: + - 순수 배경설명만 있는 문장 + - 정보 증가 없는 반복 진술 + - 법적 효과와 관련 없는 주변사정 + + + + + - 갑구/을구의 각 의미 있는 row를 개별 candidate로 본다. + - 최소 추출 대상: 소유권보존, 소유권이전, 증여, 매매, 담보권실행을 위한 경매개시결정, 담보권실행을 위한 경매로 인한 매각, 근저당권설정, 근저당권설정등기말소, 가압류. + - `support_locators.locator_hint`에는 가능하면 `갑구4`, `을구1`, `순위번호 3` 같은 row identifier를 남긴다. + - 말소표시가 있더라도 법적 의미가 있으면 candidate로 남길 수 있다. + - row에 지분 표시가 직접 읽히면 candidate에 같은 ordinal B1 part의 `registry_row_components[*].fraction_ref`와 동일한 `fraction_ref`를 남긴다. 지분 표시가 없으면 null. + + + + - 하나의 문서에 서로 다른 계약 요소가 공존하면 clause별로 분해한다. + - 예: 대지 임대차 + 건물 매매 + 인도 + 관리사용 귀속 + 명의변경 협력은 가능한 한 별개 candidate로 분리한다. + - B1 part의 `contract_components`가 있으면 그것을 우선 참고하되, raw 문서 확인을 생략하지 않는다. + - 혼합계약서가 계약성립, 대가수수, 인도, 관리·사용 귀속을 직접 보여주면 아래 후보군을 가능한 한 독립 candidate로 생성한다. + 1. 계약성립 candidate + 2. 대가수수 candidate + 3. 인도 candidate + 4. 관리·사용 또는 현재 수익귀속 state candidate + - 위 4종 중 raw 문서가 직접 보여주는 것은 formation candidate 1개로 뭉개지지 않도록 반드시 분리한다. + - 대가수수 candidate는 적절한 기존 enum이 없으면 `event_kind="기타"`, `event_subkind="대가수수"`로 둘 수 있다. + - 대가수수 candidate에는 가능한 한 지급자, 수령자, 금액, 목적물, 해당 조항 locator를 함께 남긴다. + - 인도 candidate에는 가능한 한 인도자, 인도수령자, 목적물, 개시일, locator를 함께 남긴다. + - 관리·사용 귀속 candidate는 `event_or_state="state"`를 우선 검토하고, `current_state_candidate` 하위 4축과 interval field를 함께 남긴다. + - 문서가 계약일로부터 현재 점유/사용 귀속을 직접 보여주면 `state_start_date`를 그 개시일로 남기고, 뒤에 다른 점유/사용 candidate가 있으면 선행 state를 open-ended로 두지 않는다(같은 문서 내). + + + + - 내용증명, 답신, 답변서, 최고서, 해제통지서, 소멸청구 통지서에서는 아래를 개별 candidate로 검토한다. + - 지급 요구 + - 해제/해지 통지 + - 도달 + - 책임 부인 + - 현재 점유/현재 거주/현재 운영 주장 + - 사용수익 귀속 주장 + - 현재 권리귀속 주장 + - 채무승인 또는 시효항변 주장 + - 감액요구, 명도요구 + - 문서에 현재 상태가 직접 적혀 있으면 `event_or_state="state"` candidate를 생성할 수 있다. + - 현재 상태가 후행 시점에서 선행 상태를 대체하는 구조가 직접 보이면 interval field를 함께 남긴다. + + + + - 감정평가서, 감정평가 의견서, 임료 시세 확인서 등은 `event_candidates`가 0개라고 판단하지 않는다. + - 시가표, 차임표, 사용이익표, 시장가치표가 있으면 최소 1개의 factual/state candidate를 생성한다. + - rent/use-gain valuation은 `action_type_candidate="사실행위(factual acts)"`로 둘 수 있다. + - 문서가 특정 현재 수익귀속자 또는 현재 권리귀속자를 직접 보여주면, 해당 정보는 `current_state_candidate`의 대응 배열에 분리해 둔다. + + + + - 사업자등록증, 가족관계증명서, 현재상태 확인 문서, 심판문 등은 상태형 후보 또는 법적 지위 관련 후보를 생성할 수 있다. + - 예: 사업자등록은 현재 영업상태/점포 운영 개시의 간접 또는 직접 후보가 된다. + - 가족관계증명서, 상속포기 심판문, 상속 관련 공문서가 사망자와 친족구성 또는 상속포기를 직접 보여주면 succession 관련 candidate를 생성한다. + - sparse status 문서라도 later stage linker가 사용할 수 있도록 가능한 범위에서 `participants`, `object_spec`, `event_date` 또는 `current_state_candidate.state_as_of`를 채운다. + - family/renunciation/status 문서를 generic status로만 남기지 않는다. + + + + - 사망, 가족관계, 상속포기, 재산상속회복, 승계 관련 문서는 가능한 한 succession 관련 candidate를 생성한다. + - succession 관련 candidate에는 optional field `affected_asset_cluster_ids`를 둘 수 있다. + - `affected_asset_cluster_ids`는 meeting이나 B1 part metadata가 사망자와 특정 자산군의 연결을 직접 또는 명백하게 보여줄 때만 채운다. + - succession 관련 candidate에는 optional field `succession_candidate`를 둘 수 있다. + { + "deceased_candidate": null, + "spouse_candidates": [], + "lineal_ascendant_candidates": [], + "lineal_descendant_candidates": [], + "renunciant_candidates": [] + } + - 가족관계증명서 같은 status doc은 direct 내용이 보이면 `deceased_candidate`, `spouse_candidates`, `lineal_ascendant_candidates`, `lineal_descendant_candidates`를 가능한 한 채우고, 자산 연결이 직접 또는 명백하면 `affected_asset_cluster_ids`도 함께 채운다. + - 상속포기 문서는 상속포기 그 자체를 direct candidate로 남기되, direct names가 보이면 `succession_candidate.renunciant_candidates`를 반드시 채운다. + - 상속포기 문서가 특정 사망자와 자산 cluster를 직접 또는 명백하게 연결하면 동일한 `affected_asset_cluster_ids`를 함께 남긴다. + - direct names가 있는 succession/status 문서에서는 가능한 범위에서 `participants`와 `object_spec`도 함께 남겨 later stage overlap scorer가 sparse document를 놓치지 않게 한다. + - 이 단계는 최종 상속분, 단독상속, complaint plaintiff narrowing을 확정하지 않는다. 다만 downstream이 succession bundle을 읽을 수 있을 정도의 구조는 반드시 남긴다. + + + + + - raw event는 문서가 직접 보여주는 사실행위·법률행위·준법률행위다. + - legal-effect event는 raw event에서 downstream 청구권 식별에 필요한 법률효과 후보를 도출한 candidate다. + - raw event와 legal-effect event는 같은 문서에서 함께 생성할 수 있으며, 서로 다른 `candidate_id`를 가진다. + - legal-effect event는 최종 법률판단이 아니라 Stage 2가 검토할 구조화된 후보이므로 `confidence`와 `review_flags`를 함께 남긴다. + + + 금전채권: + - `underlying_claim_arising` + - `first_demand` + - `demand_notice_dispatch` + - `demand_notice_arrival` + - `debt_acknowledgement` + - `new_due_date` + - `business_transfer` + - `trade_name_continuation_candidate` + - `commercial_successor_liability_candidate` + + 담보부 부동산: + - `secured_debt_creation` + - `mortgage_setting_by_fraction` + - `undesignated_payment` + - `payment_allocation_effect` + - `mortgage_extinguishment_by_fraction` + - `auction_application_after_extinguishment` + - `auction_sale_and_registry_transfer` + - `derivative_mortgage_setting_by_invalid_title_holder` + - `unregistered_building_original_acquisition` + - `de_facto_disposition_holder_acquisition` + - `land_use_gain_start` + - `liability_overlap_start` + + 사해행위취소: + - `preserved_claim_arising` + - `collateral_setting_for_preserved_claim` + - `fraudulent_transfer` + - `asset_value_at_fraudulent_act` + - `asset_value_at_close_proxy` + - `collateral_insufficiency_at_act` + - `encumbrance_release_after_act` + - `value_compensation_exception_trigger` + - `value_compensation_cap_calculation_candidate` + - `defense_assertion_title_trust` + - `defense_assertion_sufficient_collateral` + - `defense_assertion_setoff` + + 유치권ㆍ상속ㆍ통지: + - `lien_assertion` + - `third_party_lease_by_lien_holder` + - `lien_violation_candidate` + - `lien_extinction_notice_dispatch` + - `lien_extinction_notice_receipt` + - `lien_extinction_effect_candidate` + - `ongoing_physical_possession` + - `succession_renunciation_filing` + - `succession_renunciation_acceptance` + - `spouse_sole_succession_effect_candidate` + - `defense_assertion_subsequent_owner_no_extinction_right` + - `defense_assertion_benefit_applied_to_secured_claim` + + + + - claim_doc 문서가 채무승인, 최고, 변제기, 변제약정, 지급기일을 함께 포함하면 하나의 generic event로 압축하지 말고 legal effect별 candidate를 분리한다. + - 이행각서에는 최소한 `debt_acknowledgement`, `payment_promise`, `new_due_date` 후보가 있는지 검토한다. + - 최고서에는 `demand_notice_dispatch`, `demand_notice_arrival`, `limitation_preservation` 관련 후보가 있는지 검토한다. + - 발송일과 도달일이 다르면 각각 별도 `event_date`로 보존한다. + - 채무승인 event에는 `legal_effect_candidate.acknowledgement_of_debt=true`, `limitation_interruption_candidate=true`, `related_claim_type`을 직접 읽히는 범위에서 남긴다. + + + + - business_transfer_doc 문서는 단순 영업양도 formation event로만 끝내지 않는다. + - 문서 또는 상담내용에서 상호, 영업명, 영업자산, 고객관계, 고용관계, 채무불승계 조항, 책임배제 등기 필요성이 보이면 `commercial_successor_candidates`로 분해한다. + - 상법 제42조 책임을 최종 확정하지는 않되, downstream이 요건판단을 할 수 있도록 transferor, transferee, business_name, trade_name_continuation_candidate, debt_exclusion_clause_candidate를 남긴다. + + + + - 원자료 event가 곧바로 법률효과를 발생시키는 경우 raw event와 legal-effect event를 모두 생성한다. + - 복수 채무에 대한 지정 없는 변제가 등장하면 `undesignated_payment` event와 별도로 `payment_allocation_effect` 후보를 생성한다. + - 등기부상 경매 매각과 후행 근저당권이 등장하면, 그 경매가 어떤 담보권에 기초했는지, 그 담보권의 피담보채무가 이미 소멸했는지 검토하는 event chain을 만든다. + - 미등기 건물 신축일과 미등기 건물 매수일은 점유·사용이익 발생 시점을 정하므로 `land_use_gain_start` 및 `liability_overlap_start` event 후보를 생성한다. + - `mortgage_setting_by_fraction`/`mortgage_extinguishment_by_fraction` 등 `*_by_fraction` subkind candidate는 지분 표시가 문서에 직접 읽히는 한 `fraction_ref`를 반드시 가진다. + + + + - 사해행위취소 사건에서 raw event와 legal-effect event를 구분한다. + - 사해행위 당시 존재하던 담보권이 사후 말소되면 `value_compensation_exception_trigger` event를 생성한다. + - valuation row와 사후 말소 부담, 존속 부담이 모두 있으면 `value_compensation_cap_calculation_candidate`를 생성한다. + - 피고가 항변을 명시한 내용증명은 `defense_assertion_*` event로 만들고, Stage 2 claim description에 반영해야 하는지 표시한다. + + + + - 통지 문서와 답변서가 서로 대응하고, 답변서가 특정 통지의 수령일을 인정하면 별도의 receipt event candidate를 생성한다. + - receipt event candidate는 발송 event와 병합하지 않는다. + - 발송일과 도달일이 다르면 도달일을 법률효과 후보일로 별도 보존한다. + - 문서에 유치권, 공사잔금, 점유, 임대, 직접 거주, 피담보채권 충당, 후소유자 항변이 함께 나타나면 일반 current possession event가 아니라 lien_event 후보군으로 분해한다. + - 제3자 임대는 `lien_violation_candidate`로, 직접 거주는 ongoing use state로, 피담보채권 충당 주장은 `defense_candidate`로, 소멸청구 수령은 `lien_extinction_effect_candidate`로 각각 분리한다. + - 상속포기 수리 event가 있고 배우자와 자녀 전부 상속포기 구조가 직접 보이면 `spouse_sole_succession_effect_candidate`를 생성하되, 최종 소장 원고 확정은 Task_C 이후로 미룬다. + + + + + `action_type_candidate`는 아래 중 1개를 선택한다. + - `"법률행위(legal acts)"` + - `"준법률행위(quasi-legal acts)"` + - `"사실행위(factual acts)"` + - `"위법행위(unlawful acts)"` + - `"소송행위(litigation acts)"` + - `"불명"` + + + + `event_kind`는 아래 값 중 가장 가까운 것을 선택한다. + - `"처분"`, `"소유권이전"`, `"증여"`, `"대물변제"`, `"담보설정"`, `"담보말소"`, `"채권발생"`, `"보증"`, `"대위변제"`, `"변제"`, `"배당"`, `"임대차"`, `"점유"`, `"인도"`, `"가압류/압류"`, `"경매신청"`, `"경매개시"`, `"경매매각"`, `"통지"`, `"해제/해지"`, `"판결/결정"`, `"영업양도"`, `"사업자등록"`, `"신축"`, `"상속개시"`, `"상속포기"`, `"채권관리"`, `"도달"`, `"항변"`, `"가치평가"`, `"법률효과"`, `"기타"` + + 필요하면 optional field `event_subkind`에 더 구체한 라벨(REQUIRED_EVENT_SUBKINDS 참조)을 남길 수 있다. + + + + - `participants.actor_candidates`는 직접 행위를 한 사람/법인 + - `participants.counterparty_candidates`는 직접 상대방 + - `participants.beneficiary_candidates`는 행위로 이익을 직접 취득한 사람/법인 + - `participants.third_party_candidates`는 법원, 피상속인, 기존 권리자 등 제3자 + - `object_spec`는 짧은 목적물/채권/담보/통지대상/상태 대상 명사구 + - `amount`는 현재 candidate와 직접 연결되는 대표 금액 1개만 사용한다 + - state candidate의 현재 점유/사용/수익/권리 귀속은 `participants`에 뭉개지지 않고 `current_state_candidate` 하위 배열에 분리한다 + - succession/status 문서처럼 sparse한 문서라도 direct names와 direct relation이 보이면 `participants`와 `object_spec`를 불필요하게 비워두지 않는다. + - 잘못된 당사자 확정보다 `[]` 또는 `null`이 낫다 + + + + - `event_date`는 가능하면 `YYYY-MM-DD` + - 불명확하면 `null` + - `time_text`는 짧은 원문 표현을 유지할 수 있다 + - `time_precision` 허용값: `"exact"`, `"approximate"`, `"range"`, `"unknown"` + - 현재상태 candidate의 경우 `state_as_of` 개념은 optional `current_state_candidate.state_as_of`에 남기고, `event_date`는 상태 개시 시점이 직접 보일 때만 채운다. + - state interval이 직접 보이면 `current_state_candidate.state_start_date`, `current_state_candidate.state_end_date_candidate`를 함께 채운다. + - 후행 상태가 선행 상태를 대체하는 것이 직접 보이면 `current_state_candidate.superseded_by_candidate_id`를 사용한다. + + + + - optional field `event_or_state`를 추가할 수 있다. 허용값은 `"event"` 또는 `"state"`. + - 문서가 단발 사건이 아니라 `현재까지`, `현재도`, `계속`, `현재 운영`, `현재 점유`, `현재 거주`, `부지로만 사용`, `현재 명의`, `현재 소유`, `차임을 받고 있다` 같은 계속상태를 직접 보여주면 `event_or_state="state"` candidate를 생성할 수 있다. + - state candidate에는 optional field `current_state_candidate`를 둘 수 있다. + { + "state_summary": "짧은 상태 설명", + "state_as_of": "현재|2024-10-31 등", + "state_start_date": null, + "state_end_date_candidate": null, + "superseded_by_candidate_id": null, + "is_ongoing": true, + "current_possessor_candidates": [], + "current_user_candidates": [], + "current_benefit_holder_candidates": [], + "current_right_holder_candidates": [], + "possession_legal_cause_asserted": null, + "possession_legal_cause_asserted_by": [] + } + - `current_state_candidate`는 가능한 한 아래 4축으로 분해한다. + - 점유자 -> `current_possessor_candidates` + - 사용자/운영자 -> `current_user_candidates` + - 차임·사용이익·영업수익 등 현재 수익귀속자 -> `current_benefit_holder_candidates` + - 현재 소유자·등기명의인·권리귀속자 -> `current_right_holder_candidates` + - 같은 사람이 복수 role을 가지면 각 배열에 중복 기재할 수 있다. + - 문서에 직접 읽히지 않는 축은 빈 배열로 둔다. 추정으로 채우지 않는다. + - event candidate와 state candidate는 동일 문서에서 함께 존재할 수 있다. + - 동일 문서에 과거 행위와 현재 상태가 함께 있으면 분리한다. + - 동일 문서에 현재 점유자와 현재 권리귀속자가 다르게 읽히면 하나로 뭉개지지 않도록 분리 기재한다. + - 상태 개시 시점이 직접 보이면 `state_start_date`를 채운다. + - 후행 점유/사용/수익귀속 candidate가 선행 상태를 대체하는 것이 직접 보이면, 선행 state에 `state_end_date_candidate`와 `superseded_by_candidate_id`를 남기고 open-ended current state로 방치하지 않는다. + - `state_end_date_candidate`가 직접 보이면 `is_ongoing=false`를 우선 검토한다. + - 혼합계약서가 인도, 관리·사용 귀속, 사용수익 귀속을 직접 보여주면 그 상대방에 대한 state candidate를 high confidence로 생성할 수 있다. + - raw 문서가 현재 점유, 현재 사용, 현재 수익귀속, 현재 권리귀속을 직접 적시하면 candidate `confidence`는 원칙적으로 `high`를 우선 검토한다. + - meeting 보조나 간접 정황에 크게 의존한 상태 후보는 `medium` 또는 `low`로 둔다. + - `possession_legal_cause_asserted`는 optional 필드다. 허용값: "유치권행사중|임대차|사용대차|물권|동시이행항변|명도후잔류|무권원주장|미상". `possession_legal_cause_asserted_by`는 그 권원을 주장한 주체(원고측/피고측/제3자 이름)이며 문서에 직접 읽힐 때만 채운다. + - 점유 사실과 권원 주장은 같은 candidate 안에서 축을 분리해 남긴다(사실=4축 배열, 권원=`possession_legal_cause_asserted`). 권원 주장이 문서에 없으면 `미상`이 아니라 null로 둔다. + - 서로 다른 문서가 동일 자산에 대해 다른 권원을 주장하는 것은 본 mapper의 검출 대상이 아니다(authority drift 금지). 각 문서의 주장을 그대로 보존한다. + + + + - optional field `meeting_clause_refs`를 추가할 수 있다. + - 형식은 `1-가`, `2-라`, `3-다`, `4-가`, `5-나`처럼 `client_meeting.md`의 본문 번호체계를 사용한다. + - 현재 문서가 특정 상담조항을 직접 뒷받침할 때만 기록한다. + - meeting clause ref는 보조 recall 장치일 뿐, 문서에 없는 사실을 가져오는 수단이 아니다. + + + + 이 단계는 최종 사해행위 판단을 하지 않는다. 다만 후속 단계의 recall 강화를 위해 candidate tag를 남긴다. + `actio_relevance_candidates.candidate_role_tags` 허용값: + - `"사해행위목적물후보"`, `"원고채권후보"`, `"선순위담보후보"`, `"존속담보후보"`, `"임차권후보"`, `"가압류후보"`, `"수익자이익후보"`, `"현재점유후보"`, `"현재사용수익후보"` + + `fraudulent_act_date_candidate`는 다음 경우에만 채운다. + - 현재 candidate가 직접 목적물의 처분, 이전, 이전등기, 매매일, 증여일, 대물변제일, 경매매각일과 연결되는 경우 + - 그 외에는 쓰지 않는다 + + + + downstream Task_C BO와 evidence 기반 candidate가 중복 제거할 수 있도록, 각 candidate마다 아래 정규화 문자열을 생성한다. + + `(event_or_state + event_kind + 핵심 actor + 핵심 counterparty + 핵심 object_spec/amount + 핵심 date_or_state_as_of)` + + 규칙: + - 불명확한 요소는 생략하되, 최소한 `event_or_state` 또는 `event_kind`와 1개 이상의 핵심 요소는 남긴다. + - 중복 제거를 위해 whitespace, 중복 조사, 과도한 수식어는 제거한다. + - title이나 evidence_index는 넣지 않는다. + - event와 state는 같은 signature가 되지 않도록 구분한다. + - mapper가 propose하는 candidate_id는 `EVT-{ordinal:03d}-{seq:02d}`이며 quality gate가 collision을 처리한다. + + + + part 파일은 단일 item object 형태이며, top-level item은 non-empty `event_candidates` 또는 non-null `zero_event_reason` 중 하나를 반드시 가진다. `event_candidates: []` 단독은 금지된다. + + { + "evidence_index_proposed": "E-{ordinal:03d}", + "title": "문서 제목", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": ordinal + }, + "property_cluster_id": null, + "doc_semantic_flags": [], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-{ordinal:03d}-01", + "event_or_state": "event|state", + "event_kind": "소유권이전", + "event_subkind": null, + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "짧은 1문장 요약", + "source_evidence_index_proposed": "E-{ordinal:03d}", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [], + "counterparty_candidates": [], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": null, + "fraction_ref": null, + "amount": null, + "event_date": null, + "event_end_date": null, + "time_text": null, + "time_precision": "exact|approximate|range|unknown", + "location": null, + "legal_keywords": [], + "support_locators": [ + { + "locator_type": "heading|key_value|table_row|paragraph|caption|unknown", + "locator_hint": "갑구4|제2조 등", + "excerpt": "짧은 단서", + "directness": "직접|간접|불명" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "string|null", + "related_claim_type": "string|null", + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "state|점유|...", + "confidence": "high|medium|low|unknown" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } + } + + 추가 제약: + - top-level은 단일 object다. envelope이 아닌 단일 item. + - `event_candidates`가 빈 배열이면 `zero_event_reason`은 반드시 non-null 허용값이어야 한다. + - `event_candidates: []`와 `zero_event_reason` 누락/null이 결합된 part는 작성 금지되는 schema invariant violation이다. + - `candidate_id_proposed`는 mapper의 propose 식별자(`EVT-{ordinal:03d}-{seq:02d}`)이며, finalize는 quality gate가 한다. + - `source_evidence_index_proposed`는 mapper가 propose 한 자기 ordinal의 candidate evidence_index다. + - `mapper_meta.self_isolation_attested=true`는 본 mapper가 자기 문서 외 사실을 주입하지 않았음을 self-attest 한다는 의미다. + + + + - `write_file` 전 을 수행하고, 불량이면 내부 보정을 먼저 시도했는지 점검한다. + - 자기 part object가 단일 item이고 `evidence_index_proposed`, non-empty `event_candidates` 또는 non-null `zero_event_reason`을 가지는지 점검한다. + - `event_candidates: []`와 `zero_event_reason` 누락/null이 함께 있으면 내부 보정 대상이며, 보정 불가 시 part 파일을 쓰지 않고 FAILED로 종료한다. + - `planner_status=="READY"` 및 `mapper_execution_allowed=true`가 확인되지 않았으면 part 파일이 작성되지 않았는지 점검한다. + - 모든 candidate의 `candidate_id_proposed`가 `EVT-{ordinal:03d}-{seq:02d}` 형식인지 점검한다. + - 자기 문서 외 ordinal의 사실이 part에 주입되지 않았는지 self-check 한다(authority drift 방지). + - `registry_doc` 또는 `multi_event_doc`인데 `event_candidates`가 0개면 다시 점검한다. + - `mixed_contract_doc`인데 서로 다른 법적 효과가 2개 이상 보이면 candidate도 최소 2개 이상 검토한다. + - 혼합계약서가 계약성립, 대가지급·수령, 인도, 관리·사용 귀속을 직접 보여주면 formation candidate만 남기지 말고 consideration/delivery/state candidate가 함께 검토되었는지 점검한다. + - 문서가 위 네 요소를 모두 직접 보여주면 최소 3개 이상, 가능하면 4개까지 독립 candidate로 분리되었는지 점검한다. + - `valuation_doc`인데 가치표/차임표가 직접 보이면 candidate를 최소 1개 생성한다. + - 문서가 상담일지의 명시 조항을 직접 뒷받침하는데도 `meeting_clause_refs`가 전혀 없으면 다시 점검한다. + - event와 state는 구분해 생성한다. 현재상태를 과거 단발행위로 뭉개지 않는다. + - state candidate에서 현재 점유자/사용자/수익귀속자/권리귀속자가 직접 읽히는데도 하나의 participant 배열로 뭉개졌다면 다시 점검한다. + - 현재 점유자와 현재 권리귀속자가 다르게 읽히는 문서는 `current_state_candidate` 하위 배열에서 분리되었는지 다시 점검한다. + - 상태 개시와 종료가 직접 보이거나 후행 상태가 선행 상태를 대체하는 것이 직접 보이면 `state_start_date`, `state_end_date_candidate`, `superseded_by_candidate_id` 검토가 이루어졌는지 점검한다. + - 가족관계증명서가 사망자와 친족구성을 직접 보여주면 `succession_candidate.deceased_candidate`, `spouse_candidates`, `lineal_descendant_candidates` 또는 `lineal_ascendant_candidates`가 필요한 범위에서 채워졌는지 점검한다. + - 상속포기 심판문이 direct names를 보여주면 `succession_candidate.renunciant_candidates`가 비어 있지 않은지 점검한다. + - 가족관계증명서, 상속포기 심판문, 상속 관련 공문서가 사망자와 특정 자산군의 연결을 직접 또는 명백하게 보여주면 `affected_asset_cluster_ids` 검토가 이루어졌는지 점검한다. + - succession/status 문서처럼 sparse한 문서라도 direct names, direct relation, direct date가 보이는데 `participants`, `object_spec`, `event_date` 또는 `state_as_of`가 불필요하게 비어 있지 않은지 점검한다. + - raw 문서가 현재상태를 직접 적시한 경우 candidate `confidence`가 불필요하게 low로 떨어지지 않았는지 점검한다. + - claim doc에 채무승인ㆍ최고ㆍ변제기 재설정 단서가 있는데 `legal_effect_candidate`가 있는 event가 전혀 없으면 self-warning으로 본다. + - 이행각서에 채무승인일과 새 변제기가 함께 있는데 `debt_acknowledgement`와 `new_due_date`가 분리되지 않으면 self-warning으로 본다. + - business transfer 문서에 상호속용 후보 event 또는 `commercial_successor_candidates`가 없으면 self-warning으로 본다. + - 복수 피담보채무와 변제가 있는데 `payment_allocation_effect`가 없으면 self-warning으로 본다. + - 경매 event가 있는데 기초 담보권 및 피담보채무 chain과 연결되지 않으면 `AUCTION_BASIS_CHAIN_MISSING` self-warning으로 본다. + - 미등기 건물 매수 event가 있는데 원시취득 event가 없으면 `UNREGISTERED_BUILDING_ORIGINAL_ACQUISITION_MISSING` self-warning으로 본다. + - 사후 담보권 말소 row가 있는데 `value_compensation_exception_trigger` event가 없으면 self-error로 본다. + - valuation matrix와 released encumbrance가 있는데 `value_compensation_cap_calculation_candidate`가 없으면 self-warning으로 본다. + - defense assertion event가 있는데 `defense_candidate.requires_rebuttal_in_stage2` 검토가 없으면 self-warning으로 본다. + - 유치권 소멸 통지서와 수령 인정 답변서가 있는데 도달 event 또는 `lien_extinction_notice_receipt`가 없으면 self-error로 본다. + - 제3자 임대 event가 있는데 `lien_violation_candidate`가 없으면 self-warning으로 본다. + - 상속포기 수리 event가 있는데 `spouse_sole_succession_effect_candidate` 후보가 없으면 self-warning으로 본다. + - self-warning/self-error는 mapper가 part 파일에 그대로 남겨도 되며, quality gate가 최종 severity를 판정한다. + + + + + {{item.assigned_ordinal}} + + + [ ] 1) Preflight: `list_docs` 사용해서 `{{item.b1_output_path}}`, `{{item.shard_doc_path}}`, `client_meeting.md` 만 확인. `read_docs` 사용해서 동일 파일만 읽기 + [ ] 2) runtime parameter `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.b1_output_path}}`, `{{item.b2_output_path}}` 그대로 소비. 별도 검증 read 불요. + [ ] 3) `{{item.shard_doc_path}}`의 단일 evidence 문서를 메모리에서 사용하고, 같은 ordinal의 B1 part(`{{item.b1_output_path}}`)만 authority metadata로 사용 + [ ] 4) `write_file` 전 수행. 불량이면 내부 보정 시도 + [ ] 5) 보정 후에도 불량이면 part 파일을 쓰지 않고 `FAILED: E-{ordinal:03d} event candidate part schema invariant violation` 출력 후 종료 + [ ] 6) 통과한 경우에만 `write_file` 사용해서 `evidence_event_candidate_parts/E-{ordinal:03d}.json` 생성. event 후보가 0건이어도 non-null `zero_event_reason` part 파일을 작성 + [ ] 7) `list_docs` 사용해서 자기 part 파일만 존재 확인(절대 읽기 금지) + [ ] 8) `"E-{ordinal:03d} event candidates part 작성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `{{item.b1_output_path}}` (같은 ordinal 의 B1 part) + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 1건; planner가 evidence_all.json에서 분리) + - `client_meeting.md` + + + - `evidence_event_candidate_parts/E-{ordinal:03d}.json` + + + + + - `candidate_id_proposed`는 정확히 `EVT-{ordinal:03d}-{seq:02d}` 형식만 허용한다. + - `ordinal`과 output path는 runtime parameter (`{{item.*}}`) 를 그대로 따른다. + - 증거 문서 수 N, assigned_ordinal, mapper fan-out, streaming edge는 본 task에서 만들지 않는다. + - 자기 part 파일은 절대 재읽지 않는다. + - 다른 ordinal 문서를 처리하지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"E-{ordinal:03d} event candidates part 작성 완료"` 한 줄만 사용한다. + + + + - task_name: Task_B1_quality_gate_evidence_indexed + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 240 + code: | + #!/usr/bin/env python3 + # Task_B1_quality_gate_evidence_indexed (v3) - deterministic B1 gate reducer (GA-1) + # Spec: Part_1_Improvement_Strategy_Claude_v1.md §4 + from __future__ import annotations + import itertools + import json + import re + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + + PLAN_PATH = "evidence_shard_plan.json" + PART_DIR = "evidence_indexed_parts" + FINAL_PATH = "evidence_indexed.json" + MANIFEST_PATH = "stage1_tmp/quality_gate/finalized_evidence_id_manifest.json" + PRECHECK_PATH = "stage1_tmp/quality_gate/B1_precheck.json" + EXCERPT_LIMIT = 1500 + EVIDENCE_ID_RE = re.compile(r"^E-\d{3}$") + WARNING_KEY_RE = re.compile(r"warn|review|risk|note", re.IGNORECASE) + DRIFT_TEXT_RE = re.compile(r"drift|출처|다른\s*문서|타\s*문서", re.IGNORECASE) + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-b1-quality-gate-evidence-indexed", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _mid(), + "method": "tools/call", + "params": {"name": name, "arguments": args}, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: Any, name: str) -> str: + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError(f"Empty: {name}") + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, dict) and "results" in outer: + result0 = (outer.get("results") or [{}])[0] + inner = result0.get("content") or result0.get("text") or "" + if not inner: + raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer if isinstance(outer, str) else json.dumps(outer, ensure_ascii=False) + + def read_raw(path: str) -> str: + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str) or not raw.strip(): + raise RuntimeError(f"Missing or empty required input: {path}") + return raw + + def parse_json(raw: str, path: str) -> Any: + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + try: + obj, _ = decoder.raw_decode(raw.strip()) + return obj + except Exception as exc: + raise RuntimeError(f"Invalid JSON input: {path}") from exc + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def clip(value: Any, limit: int = 200) -> str: + text = " ".join(str(value or "").split()) + if len(text) <= limit: + return text + return text[:limit].rstrip() + "..." + + def list_part_names(folder: str) -> list[str] | None: + # planner-compatible list_docs enumeration; None => enumeration unavailable (recorded, non-fatal) + for args in ({"path": folder, "pattern": "E-*.json"}, {}, {"doc_names": []}): + try: + payload = _unwrap(_call("list_docs", args), "list_docs") + except Exception: + continue + if isinstance(payload, str): + try: + payload = json.loads(payload) + except Exception: + return re.findall(r"[\w./-]+\.json", payload) + vals: list[str] = [] + if isinstance(payload, dict): + for value in payload.values(): + if isinstance(value, list): + for item in value: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + elif isinstance(value, str): + vals.append(value) + elif isinstance(payload, list): + for item in payload: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + return [v for v in vals if v] + return None + + def get_single_item(part: Any, path: str) -> dict[str, Any]: + if isinstance(part, dict) and isinstance(part.get("items"), list): + items = part["items"] + if len(items) != 1 or not isinstance(items[0], dict): + raise RuntimeError(f"part must contain exactly one item object: {path}") + return items[0] + if isinstance(part, dict): + return part + raise RuntimeError(f"part top-level is not an object: {path}") + + def nonempty(value: Any) -> bool: + return value not in (None, "", [], {}) + + def flags_of(item: dict[str, Any]) -> list[str]: + return [str(f) for f in (item.get("doc_semantic_flags") or []) if f] + + def norm_title(value: Any) -> str: + return re.sub(r"\s+", "", str(value or "")).lower() + + # v.2 CHECK_4 rule table: (rule_id, flag token, required-nonempty fields (any-of)) + COVERAGE_RULES = ( + ("registry_rows", "registry", ("registry_row_components",)), + ("claim_components", "claim", ("claim_document_components",)), + ("schedule_items", "schedule", ("schedule_items",)), + ("business_transfer", "business_transfer", ("business_transfer_components",)), + ("valuation", "valuation", ("valuation_object", "valuation_rows")), + ("succession", "succession", ("succession_document_components",)), + ("defense", "defense", ("defense_assertions", "defense_components", "admission_components")), + ) + + def main() -> None: + _init() + plan = parse_json(read_raw(PLAN_PATH), PLAN_PATH) + if not isinstance(plan, dict): + raise RuntimeError("evidence_shard_plan.json is not an object") + planner_status = plan.get("planner_status") + if planner_status not in (None, "READY"): + print(json.dumps({"status": "FAILED", "gate": "B1", + "message": "B1 quality gate BLOCK 발생: downstream 진행 금지", + "reason": f"planner_status is not READY: {planner_status}"}, ensure_ascii=False)) + raise RuntimeError(f"planner_status is not READY: {planner_status}") + + expected_ordinals = [str(o) for o in (plan.get("expected_ordinals") or [])] + if not expected_ordinals: + raise RuntimeError("plan.expected_ordinals is empty") + expected_ids = [str(x) for x in (plan.get("expected_evidence_ids") or [f"E-{o}" for o in expected_ordinals])] + shard_count = int(plan.get("shard_count") or len(expected_ordinals)) + stale_policy = (plan.get("stale_part_policy") or {}).get("detected_stale_parts") or {} + if isinstance(stale_policy, dict): + stale_parts = [str(x) for x in (stale_policy.get(PART_DIR) or [])] + else: + stale_parts = [str(x) for x in (stale_policy or [])] + shard_paths: dict[str, str] = {} + for sh in plan.get("shards") or []: + if isinstance(sh, dict) and sh.get("assigned_ordinal") is not None: + try: + shard_paths[f"{int(sh['assigned_ordinal']):03d}"] = str(sh.get("shard_doc_path") or "") + except Exception: + continue + + hard_findings: list[dict[str, Any]] = [] + + def hard(issue_type: str, detail: str, **extra: Any) -> None: + hard_findings.append({"issue_type": issue_type, "detail": clip(detail), **extra}) + + # -------- CHECK 1: shard completeness (deterministic) -------- + if stale_parts: + hard("stale_part", f"stale parts detected by planner policy: {stale_parts}") + missing_ordinals: list[str] = [] + parse_failures: list[dict[str, str]] = [] + parts: dict[str, dict[str, Any]] = {} + for o in expected_ordinals: + path = f"{PART_DIR}/E-{o}.json" + try: + raw = read_raw(path) + except Exception: + missing_ordinals.append(o) + continue + try: + parts[o] = get_single_item(parse_json(raw, path), path) + except Exception as exc: + parse_failures.append({"ordinal": o, "reason": clip(exc)}) + if missing_ordinals or parse_failures: + # FAILURE_POLICY: input-level failure -> report only, write no final files + # SKILL.md §3.3: stdout = task output — 단일 JSON 라인만 출력한다 + print(json.dumps({ + "status": "FAILED", + "gate": "B1", + "message": "B1 quality gate BLOCK 발생: downstream 진행 금지", + "missing_ordinals": missing_ordinals, + "parse_failures": parse_failures, + }, ensure_ascii=False)) + raise RuntimeError("B1 input parts missing or unparsable; final files not written") + extra_parts: list[str] = [] + listed = list_part_names(PART_DIR) + extra_detection = "done" if listed is not None else "unavailable" + if listed is not None: + pat = re.compile(r"(?:^|/)E-(\d{3})\.json$") + expected_set = set(expected_ordinals) + for name in listed: + m = pat.search(str(name)) + if m and m.group(1) not in expected_set: + extra_parts.append(str(name)) + if extra_parts: + hard("unexpected_ordinal", f"unexpected part files outside planner plan: {sorted(set(extra_parts))}") + + # -------- CHECK 2: evidence index determinism (deterministic) -------- + determinism_violations: list[dict[str, str]] = [] + for o in expected_ordinals: + proposed = str(parts[o].get("evidence_index_proposed") or "") + if not EVIDENCE_ID_RE.match(proposed) or proposed != f"E-{o}": + determinism_violations.append({"ordinal": o, "proposed": proposed}) + if determinism_violations: + hard("evidence_index_format_mismatch", f"evidence_index_proposed violations: {determinism_violations}") + + # -------- CHECK 3: duplicate document alias (deterministic exact + suspect pairs) -------- + suspects: list[dict[str, Any]] = [] + dropped_aliases: list[dict[str, Any]] = [] + duplicate_groups: list[dict[str, Any]] = [] + canonical_choices: list[dict[str, Any]] = [] + # 자동 dedup은 강한 동시 일치(v.2 CHECK_3의 "동시 일치성")에서만 수행한다. + # 제목만 같은 서로 다른 문서(예: 서로 다른 채무의 차용증 2건)는 절대 자동 삭제하지 않는다 — R-ALIAS suspect로만 등록. + exact_groups: dict[tuple[str, str], list[str]] = {} + for o in expected_ordinals: + item = parts[o] + uid = str(item.get("doc_uid") or "") + tnorm = str(item.get("title_normalized") or "") + if uid: + exact_groups.setdefault(("doc_uid", uid), []).append(o) + if tnorm: + joint = json.dumps([tnorm, item.get("key_dates"), item.get("key_amounts"), item.get("key_parties")], + ensure_ascii=False, sort_keys=True) + exact_groups.setdefault(("joint_identity", joint), []).append(o) + dropped_set: set[str] = set() + for (kind, value), ords in sorted(exact_groups.items()): + ords = sorted(set(ords)) + if len(ords) < 2: + continue + canonical = ords[0] # v.2 canonical rule (i): smaller ordinal + duplicate_groups.append({"match_kind": kind, "match_value": value, "ordinals": ords}) + canonical_choices.append({"match_value": value, "canonical_ordinal": canonical}) + for dropped in ords[1:]: + if dropped in dropped_set: + continue + dropped_set.add(dropped) + dropped_aliases.append({ + "dropped_ordinal": dropped, + "dropped_evidence_index": f"E-{dropped}", + "canonical_ordinal": canonical, + "canonical_evidence_index": f"E-{canonical}", + "reason": f"exact {kind} match", + }) + suspects.append({ + "exception_id": f"B1-SUS-{len(suspects) + 1:03d}", + "source_gate": "B1", + "rule_id": "R-ALIAS", + "check_id": "duplicate_document_alias", + "ordinals": ords, + "evidence_indexes": [f"E-{o}" for o in ords], + "candidate_ids": [], + "payload": {"confirmed_exact": True, "match_kind": kind, "canonical_ordinal": canonical}, + "excerpt": "", + "escalation_flag": False, + }) + # near-duplicate suspect pairs (not dropped) + seen_pairs: set[tuple[str, str]] = set() + ords_list = [o for o in expected_ordinals if o not in dropped_set] + for i, oa in enumerate(ords_list): + for ob in ords_list[i + 1:]: + a, b = parts[oa], parts[ob] + ta, tb = str(a.get("title_normalized") or ""), str(b.get("title_normalized") or "") + joint_identical = ( + ta == tb + and a.get("key_dates") == b.get("key_dates") + and a.get("key_amounts") == b.get("key_amounts") + and a.get("key_parties") == b.get("key_parties") + ) + near_title = bool(ta and tb and norm_title(ta) == norm_title(tb) and not joint_identical) + triple = ( + nonempty(a.get("key_dates")) and a.get("key_dates") == b.get("key_dates") + and nonempty(a.get("key_amounts")) and a.get("key_amounts") == b.get("key_amounts") + and nonempty(a.get("key_parties")) and a.get("key_parties") == b.get("key_parties") + and ta != tb + ) + if (near_title or triple) and (oa, ob) not in seen_pairs: + seen_pairs.add((oa, ob)) + suspects.append({ + "exception_id": f"B1-SUS-{len(suspects) + 1:03d}", + "source_gate": "B1", + "rule_id": "R-ALIAS", + "check_id": "duplicate_document_alias", + "ordinals": [oa, ob], + "evidence_indexes": [f"E-{oa}", f"E-{ob}"], + "candidate_ids": [], + "payload": { + "confirmed_exact": False, + "titles": [ta, tb], + "key_dates": [a.get("key_dates"), b.get("key_dates")], + "key_amounts": [a.get("key_amounts"), b.get("key_amounts")], + "key_parties": [a.get("key_parties"), b.get("key_parties")], + }, + "excerpt": "", + "escalation_flag": False, + }) + + # -------- CHECK 4: document component coverage (deterministic rule table -> suspects) -------- + missing_components: list[dict[str, Any]] = [] + for o in ords_list: + item = parts[o] + fl = flags_of(item) + for rule_id, token, fields in COVERAGE_RULES: + if any(token in f for f in fl) and not any(nonempty(item.get(f)) for f in fields): + missing_components.append({"ordinal": o, "rule": rule_id, "fields": list(fields)}) + suspects.append({ + "exception_id": f"B1-SUS-{len(suspects) + 1:03d}", + "source_gate": "B1", + "rule_id": "R-COVERAGE", + "check_id": "document_component_coverage", + "ordinals": [o], + "evidence_indexes": [f"E-{o}"], + "candidate_ids": [], + "payload": { + "coverage_rule": rule_id, + "doc_type": item.get("doc_type"), + "doc_semantic_flags": fl, + "empty_fields": list(fields), + "key_facts": item.get("key_facts"), + }, + "excerpt": "", + "escalation_flag": False, + }) + notice_flag = any("notice" in f for f in fl) + ncomps = item.get("notice_components") or [] + if notice_flag and (not ncomps or all( + not nonempty(n.get("dispatch_date")) and not nonempty(n.get("arrival_date")) + for n in ncomps if isinstance(n, dict) + )): + missing_components.append({"ordinal": o, "rule": "notice_dates", "fields": ["notice_components"]}) + suspects.append({ + "exception_id": f"B1-SUS-{len(suspects) + 1:03d}", + "source_gate": "B1", + "rule_id": "R-COVERAGE", + "check_id": "document_component_coverage", + "ordinals": [o], + "evidence_indexes": [f"E-{o}"], + "candidate_ids": [], + "payload": {"coverage_rule": "notice_dates", "notice_components": ncomps, "key_dates": item.get("key_dates")}, + "excerpt": "", + "escalation_flag": False, + }) + loans = [ln for ln in (item.get("loan_components") or []) if isinstance(ln, dict)] + if loans and all( + not nonempty(ln.get("agreed_interest_rate")) and not nonempty(ln.get("agreed_delay_damage_rate")) + for ln in loans + ): + missing_components.append({"ordinal": o, "rule": "loan_rates", "fields": ["loan_components"]}) + suspects.append({ + "exception_id": f"B1-SUS-{len(suspects) + 1:03d}", + "source_gate": "B1", + "rule_id": "R-COVERAGE", + "check_id": "document_component_coverage", + "ordinals": [o], + "evidence_indexes": [f"E-{o}"], + "candidate_ids": [], + "payload": {"coverage_rule": "loan_rates", "loan_components": loans}, + "excerpt": "", + "escalation_flag": False, + }) + + # -------- CHECK 5: authority drift (demoted to suspicion-signal spot-check) -------- + review_candidates: list[dict[str, Any]] = [] + drift_signal_ordinals: list[dict[str, Any]] = [] + for o in ords_list: + item = parts[o] + fl = flags_of(item) + signals: list[str] = [] + if not nonempty(item.get("key_dates")) and not nonempty(item.get("key_amounts")) and any( + t in f for f in fl for t in ("registry", "claim", "notice") + ): + signals.append("empty_key_dates_and_amounts_for_core_doc_type") + sp = item.get("source_pointer") + if isinstance(sp, dict) and sp.get("ordinal") is not None: + try: + if int(sp["ordinal"]) != int(o): + signals.append("source_pointer_ordinal_mismatch") + except Exception: + signals.append("source_pointer_ordinal_unparsable") + mm = item.get("mapper_meta") or {} + for k, v in (mm.items() if isinstance(mm, dict) else []): + if WARNING_KEY_RE.search(str(k)) and nonempty(v): + review_candidates.append({"ordinal": o, "evidence_index": f"E-{o}", "field": str(k), "value": v}) + if DRIFT_TEXT_RE.search(json.dumps(v, ensure_ascii=False)): + signals.append(f"mapper_self_warning:{k}") + if signals: + excerpt = "" + shard_path = shard_paths.get(o) or "" + if shard_path: + try: + excerpt = read_raw(shard_path)[:EXCERPT_LIMIT] + except Exception: + excerpt = "" + drift_signal_ordinals.append({"ordinal": o, "signals": signals}) + suspects.append({ + "exception_id": f"B1-SUS-{len(suspects) + 1:03d}", + "source_gate": "B1", + "rule_id": "R-DRIFT", + "check_id": "authority_drift_detection", + "ordinals": [o], + "evidence_indexes": [f"E-{o}"], + "candidate_ids": [], + "payload": { + "signals": signals, + "source_pointer": sp, + "key_dates": item.get("key_dates"), + "key_amounts": item.get("key_amounts"), + "doc_semantic_flags": fl, + }, + "excerpt": excerpt, + "escalation_flag": "source_pointer_ordinal_mismatch" in signals, + }) + + # -------- finalize & merge (conservative pass-through; v.2 CONSOLIDATION_RULES) -------- + final_items: list[dict[str, Any]] = [] + for o in expected_ordinals: + if o in dropped_set: + continue + item = parts[o] + out: dict[str, Any] = {} + placed = False + for k, v in item.items(): + if k == "evidence_index_proposed": + out["evidence_index"] = f"E-{o}" + placed = True + elif k == "mapper_meta": + continue + else: + out[k] = v + if not placed: + out = {"evidence_index": f"E-{o}", **out} + final_items.append(out) + + envelope = {"schema_contract_version": "evidence_indexed.v3", "items": final_items} + # STEP 0-3 확정: 하류(Part 2/3/4)는 items 키를 우선 소비 — 중복 alias 배열은 두지 않는다 (Q-3 해소). + + # -------- conservation invariants (pre-write) -------- + conservation_errors: list[str] = [] + if len(final_items) != shard_count - len(dropped_set): + conservation_errors.append(f"item count {len(final_items)} != shard_count {shard_count} - dropped {len(dropped_set)}") + final_ids = [it["evidence_index"] for it in final_items] + want_ids = [i for i in expected_ids if i not in {f"E-{o}" for o in dropped_set}] + if set(final_ids) != set(want_ids) or len(final_ids) != len(set(final_ids)): + conservation_errors.append("final evidence_index set mismatch vs planner expected ids minus dropped aliases") + if sorted(final_ids) != final_ids: + conservation_errors.append("final items not sorted by ordinal") + if conservation_errors: + print(json.dumps({"status": "FAILED", "gate": "B1", + "message": "B1 quality gate BLOCK 발생: downstream 진행 금지", + "conservation_errors": conservation_errors}, ensure_ascii=False)) + raise RuntimeError("B1 conservation invariant violated; final file not written") + + content = json.dumps(envelope, ensure_ascii=False, indent=2) + # post-serialization verification (write 전·후 동일 검증 - 자기 파일 재읽기 없이 문자열 기준) + verify = json.loads(content) + if len(verify["items"]) != len(final_items) or [i["evidence_index"] for i in verify["items"]] != final_ids: + raise RuntimeError("post-serialization verification failed for evidence_indexed.json") + write_doc(FINAL_PATH, content) + + manifest = { + "schema_version": "finalized_evidence_id_manifest.v1", + "source_task": "Task_B1_quality_gate_evidence_indexed", + "shard_count": shard_count, + "finalized_evidence_indexes": final_ids, + "dropped_aliases": dropped_aliases, + } + write_doc(MANIFEST_PATH, json.dumps(manifest, ensure_ascii=False, indent=2)) + + block = bool(hard_findings) + completeness_sev = "BLOCK" if (stale_parts or extra_parts) else "PASS" + determinism_sev = "BLOCK" if determinism_violations else "PASS" + precheck = { + "schema_version": "B1_precheck.v1", + "gate_id": "B1_evidence_indexed", + "planner_source": PLAN_PATH, + "planner_shard_count": shard_count, + "expected_ordinals": expected_ordinals, + "received_ordinals": expected_ordinals, + "input_parts_count": len(expected_ordinals), + "extra_part_detection": extra_detection, + "deterministic_checks": [ + {"check_id": "shard_completeness", "severity": completeness_sev, + "detail": "expected/received/stale set 비교 (결정적)", "missing_ordinals": missing_ordinals}, + {"check_id": "evidence_index_determinism", "severity": determinism_sev, + "detail": "E-{ordinal:03d} 형식·ordinal 일치 검사 (결정적)", "violations": determinism_violations}, + {"check_id": "duplicate_document_alias", "severity": "PASS", + "detail": "exact 중복은 canonical 정리, 유사쌍은 GB 판정 대기", "duplicate_groups": duplicate_groups, + "canonical_choices": canonical_choices}, + {"check_id": "document_component_coverage", "severity": "PASS", + "detail": "rule table 위반은 GB 판정 대기", "missing_components": missing_components}, + {"check_id": "authority_drift_detection", "severity": "PASS", + "detail": "상시 전역 검사 삭제, 의심신호 spot-check", "drift_findings": drift_signal_ordinals}, + ], + "hard_gate_findings": hard_findings, + "review_candidates": review_candidates, + "suspects": suspects, + "dropped_aliases": dropped_aliases, + "conservation": {"input_parts": len(expected_ordinals), "final_items": len(final_items), "dropped": len(dropped_set)}, + "block": block, + } + write_doc(PRECHECK_PATH, json.dumps(precheck, ensure_ascii=False, indent=2)) + + overall = "BLOCK" if block else "PASS" + # SKILL.md §3.3: stdout = task output — 단일 JSON 라인. 상태 메시지는 message 필드에 보존. + message = ("B1 quality gate BLOCK 발생: downstream 진행 금지" if block + else f"증거문서 authority catalog finalize 완료 (severity={overall})") + print(json.dumps({ + "status": "B1_DETERMINISTIC_GATE_DONE", + "message": message, + "overall_severity_provisional": overall, + "pending_adjudication": "PENDING_ADJUDICATION" if suspects else "NO_SUSPECTS", + "final_items": len(final_items), + "dropped_aliases": len(dropped_aliases), + "suspect_count": len(suspects), + "review_candidate_count": len(review_candidates), + }, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B2_quality_gate_event_candidates + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 240 + code: | + #!/usr/bin/env python3 + # Task_B2_quality_gate_event_candidates (v3) - deterministic B2 gate reducer (GA-2) + # Spec: Part_1_Improvement_Strategy_Claude_v1.md §5 + from __future__ import annotations + import itertools + import json + import re + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + + PLAN_PATH = "evidence_shard_plan.json" + PART_DIR = "evidence_event_candidate_parts" + FINAL_PATH = "evidence_event_candidates.json" + MANIFEST_PATH = "stage1_tmp/quality_gate/finalized_evidence_id_manifest.json" + B1_PRECHECK_PATH = "stage1_tmp/quality_gate/B1_precheck.json" + PRECHECK_PATH = "stage1_tmp/quality_gate/B2_precheck.json" + PACK_PATH = "stage1_tmp/quality_gate/B12_exception_pack.json" + EXCERPT_LIMIT = 1500 + MAX_ITEMS_PER_EXCEPTION = 2 + MAX_CANDIDATES_PER_EXCEPTION = 8 + CANDIDATE_ID_RE = re.compile(r"^EVT-(\d{3})-(\d{2})$") + DATE_RE = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-b2-quality-gate-event-candidates", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _mid(), + "method": "tools/call", + "params": {"name": name, "arguments": args}, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: Any, name: str) -> str: + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError(f"Empty: {name}") + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, dict) and "results" in outer: + result0 = (outer.get("results") or [{}])[0] + inner = result0.get("content") or result0.get("text") or "" + if not inner: + raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer if isinstance(outer, str) else json.dumps(outer, ensure_ascii=False) + + def read_raw(path: str) -> str: + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str) or not raw.strip(): + raise RuntimeError(f"Missing or empty required input: {path}") + return raw + + def parse_json(raw: str, path: str) -> Any: + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + try: + obj, _ = decoder.raw_decode(raw.strip()) + return obj + except Exception as exc: + raise RuntimeError(f"Invalid JSON input: {path}") from exc + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def clip(value: Any, limit: int = 200) -> str: + text = " ".join(str(value or "").split()) + if len(text) <= limit: + return text + return text[:limit].rstrip() + "..." + + def list_part_names(folder: str) -> list[str] | None: + for args in ({"path": folder, "pattern": "E-*.json"}, {}, {"doc_names": []}): + try: + payload = _unwrap(_call("list_docs", args), "list_docs") + except Exception: + continue + if isinstance(payload, str): + try: + payload = json.loads(payload) + except Exception: + return re.findall(r"[\w./-]+\.json", payload) + vals: list[str] = [] + if isinstance(payload, dict): + for value in payload.values(): + if isinstance(value, list): + for item in value: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + elif isinstance(value, str): + vals.append(value) + elif isinstance(payload, list): + for item in payload: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + return [v for v in vals if v] + return None + + def get_single_item(part: Any, path: str) -> dict[str, Any]: + if isinstance(part, dict) and isinstance(part.get("items"), list): + items = part["items"] + if len(items) != 1 or not isinstance(items[0], dict): + raise RuntimeError(f"part must contain exactly one item object: {path}") + return items[0] + if isinstance(part, dict): + return part + raise RuntimeError(f"part top-level is not an object: {path}") + + def nonempty(value: Any) -> bool: + return value not in (None, "", [], {}) + + def flags_of(item: dict[str, Any]) -> list[str]: + return [str(f) for f in (item.get("doc_semantic_flags") or []) if f] + + def parse_date(value: Any) -> str | None: + m = DATE_RE.match(str(value or "").strip()) + return f"{m.group(1)}-{m.group(2)}-{m.group(3)}" if m else None + + def confidence_rank(value: Any) -> int: + order = {"high": 0, "medium": 1, "mid": 1, "low": 2} + return order.get(str(value or "").lower(), 3) + + def canonical_rank(cand: dict[str, Any], ordinal: str) -> tuple[int, int, int, int]: + # v.2 CHECK_3 canonical priority: (i) raw event > derived legal-effect event, + # (ii) higher confidence, (iii) more support_locators, (iv) earlier ordinal + kind_text = " ".join(str(cand.get(k) or "") for k in ("event_kind", "event_or_state", "action_type_candidate")).lower() + derived = 1 if "derived" in kind_text or "legal_effect" in kind_text else 0 + return (derived, confidence_rank(cand.get("confidence")), -len(cand.get("support_locators") or []), int(ordinal)) + + # v.2 CHECK_4 chronological chain rule table: (earlier subkind, later subkind, allow_equal) + CHRONO_RULES = ( + ("underlying_claim_arising", "first_demand", False), + ("first_demand", "demand_notice_dispatch", False), + ("demand_notice_dispatch", "demand_notice_arrival", True), + ("demand_notice_arrival", "debt_acknowledgement", False), + ("lien_extinction_notice_dispatch", "lien_extinction_notice_receipt", True), + ("lien_extinction_notice_receipt", "lien_extinction_effect_candidate", False), + ("secured_debt_creation", "mortgage_setting_by_fraction", False), + ("mortgage_setting_by_fraction", "auction_application_after_extinguishment", False), + ("auction_application_after_extinguishment", "auction_sale_and_registry_transfer", False), + ("preserved_claim_arising", "fraudulent_transfer", False), + ("fraudulent_transfer", "encumbrance_release_after_act", False), + ) + + def is_defense_relay(cand: dict[str, Any]) -> bool: + # 7/10 실측에서 확인된 mapper 항변 신호의 결정적 재현 규칙 (R-SELFWARN) + if nonempty(cand.get("defense_candidate")): + return True + if "defense_assertion" in str(cand.get("event_subkind") or ""): + return True + lec = cand.get("legal_effect_candidate") + if isinstance(lec, dict) and str(lec.get("effect_type") or "") == "defense_assertion": + return True + return False + + def main() -> None: + _init() + plan = parse_json(read_raw(PLAN_PATH), PLAN_PATH) + if not isinstance(plan, dict): + raise RuntimeError("evidence_shard_plan.json is not an object") + if plan.get("planner_status") not in (None, "READY"): + print(json.dumps({"status": "FAILED", "gate": "B2", + "message": "B2 quality gate BLOCK 발생: downstream 진행 금지", + "reason": f"planner_status is not READY: {plan.get('planner_status')}"}, ensure_ascii=False)) + raise RuntimeError(f"planner_status is not READY: {plan.get('planner_status')}") + + expected_ordinals = [str(o) for o in (plan.get("expected_ordinals") or [])] + if not expected_ordinals: + raise RuntimeError("plan.expected_ordinals is empty") + expected_ids = [str(x) for x in (plan.get("expected_evidence_ids") or [f"E-{o}" for o in expected_ordinals])] + shard_count = int(plan.get("shard_count") or len(expected_ordinals)) + stale_policy = (plan.get("stale_part_policy") or {}).get("detected_stale_parts") or {} + if isinstance(stale_policy, dict): + stale_parts = [str(x) for x in (stale_policy.get(PART_DIR) or [])] + else: + stale_parts = [str(x) for x in (stale_policy or [])] + + manifest = parse_json(read_raw(MANIFEST_PATH), MANIFEST_PATH) + finalized_ids = {str(x) for x in (manifest.get("finalized_evidence_indexes") or [])} + if not finalized_ids: + raise RuntimeError("finalized_evidence_id_manifest.json has no finalized ids (B1 final absent)") + b1_dropped_ordinals = {str(d.get("dropped_ordinal")) for d in (manifest.get("dropped_aliases") or []) if isinstance(d, dict)} + + b1_precheck = parse_json(read_raw(B1_PRECHECK_PATH), B1_PRECHECK_PATH) + b1_suspects = list(b1_precheck.get("suspects") or []) + b1_review_candidates = list(b1_precheck.get("review_candidates") or []) + + hard_findings: list[dict[str, Any]] = [] + + def hard(issue_type: str, detail: str, **extra: Any) -> None: + hard_findings.append({"issue_type": issue_type, "detail": clip(detail), **extra}) + + # -------- schema invariant gate (v.2 PART_ITEM_SCHEMA_INVARIANT_GATE, deterministic) -------- + schema_invariant_findings: list[dict[str, Any]] = [] + + def invariant_block(ordinal: str, reason: str) -> None: + schema_invariant_findings.append({ + "ordinal": ordinal, + "part_path": f"{PART_DIR}/E-{ordinal}.json", + "reason": clip(reason), + "severity": "BLOCK", + "rerun_target_task": f"Task_B2_map_events_{ordinal}", + }) + + if stale_parts: + hard("stale_part", f"stale parts detected by planner policy: {stale_parts}") + missing_ordinals: list[str] = [] + parse_failures: list[dict[str, str]] = [] + parts: dict[str, dict[str, Any]] = {} + for o in expected_ordinals: + path = f"{PART_DIR}/E-{o}.json" + try: + raw = read_raw(path) + except Exception: + missing_ordinals.append(o) + continue + try: + parts[o] = get_single_item(parse_json(raw, path), path) + except Exception as exc: + parse_failures.append({"ordinal": o, "reason": clip(exc)}) + if missing_ordinals or parse_failures: + # SKILL.md §3.3: stdout = task output — 단일 JSON 라인만 출력한다 + print(json.dumps({ + "status": "FAILED", + "gate": "B2", + "message": "B2 quality gate BLOCK 발생: downstream 진행 금지", + "missing_ordinals": missing_ordinals, + "parse_failures": parse_failures, + }, ensure_ascii=False)) + raise RuntimeError("B2 input parts missing or unparsable; final files not written") + extra_parts: list[str] = [] + listed = list_part_names(PART_DIR) + extra_detection = "done" if listed is not None else "unavailable" + if listed is not None: + pat = re.compile(r"(?:^|/)E-(\d{3})\.json$") + expected_set = set(expected_ordinals) + for name in listed: + m = pat.search(str(name)) + if m and m.group(1) not in expected_set: + extra_parts.append(str(name)) + if extra_parts: + hard("unexpected_ordinal", f"unexpected part files outside planner plan: {sorted(set(extra_parts))}") + + for o in expected_ordinals: + item = parts[o] + proposed = str(item.get("evidence_index_proposed") or item.get("evidence_index") or "") + if proposed != f"E-{o}" or proposed not in set(expected_ids): + invariant_block(o, f"evidence_index_proposed mismatch: {proposed}") + candidates = item.get("event_candidates") + zero_reason = item.get("zero_event_reason") + has_candidates = isinstance(candidates, list) and len(candidates) > 0 + if not has_candidates and not nonempty(zero_reason): + invariant_block(o, "event_candidates empty without non-null zero_event_reason") + if has_candidates: + for c in candidates: + if not isinstance(c, dict): + invariant_block(o, "event candidate is not an object") + continue + for req in ("candidate_id_proposed", "source_evidence_index_proposed", "identity_signature"): + if not nonempty(c.get(req)): + invariant_block(o, f"candidate missing required field: {req}") + + # -------- CHECK 2: evidence reference integrity (manifest set membership) -------- + reference_violations: list[dict[str, str]] = [] + for o in expected_ordinals: + for c in parts[o].get("event_candidates") or []: + if not isinstance(c, dict): + continue + ref = str(c.get("source_evidence_index_proposed") or "") + if ref not in finalized_ids: + reference_violations.append({"ordinal": o, "candidate_id": str(c.get("candidate_id_proposed")), "ref": ref}) + if reference_violations: + hard("invalid_evidence_reference", f"candidate references outside finalized evidence id set: {reference_violations}") + + # -------- CHECK 3: candidate id format/uniqueness + exact identity_signature dedup -------- + format_violations: list[dict[str, str]] = [] + for o in expected_ordinals: + seen_seq: set[str] = set() + for c in parts[o].get("event_candidates") or []: + if not isinstance(c, dict): + continue + cid = str(c.get("candidate_id_proposed") or "") + m = CANDIDATE_ID_RE.match(cid) + if not m or m.group(1) != o: + format_violations.append({"ordinal": o, "candidate_id": cid, "reason": "format or ordinal mismatch"}) + elif cid in seen_seq: + format_violations.append({"ordinal": o, "candidate_id": cid, "reason": "duplicate seq in ordinal"}) + else: + seen_seq.add(cid) + if format_violations: + hard("candidate_id_format_violation", f"candidate_id_proposed violations: {format_violations}") + + by_signature: dict[str, list[tuple[str, dict[str, Any]]]] = {} + for o in expected_ordinals: + for c in parts[o].get("event_candidates") or []: + if isinstance(c, dict) and nonempty(c.get("identity_signature")): + by_signature.setdefault(str(c["identity_signature"]), []).append((o, c)) + dedup_findings: list[dict[str, Any]] = [] + dropped_candidate_keys: set[tuple[str, str]] = set() + for sig, entries in sorted(by_signature.items()): + cross = {o for o, _ in entries} + if len(entries) < 2 or len(cross) < 2: + continue # cross-document duplicates only (v.2 CHECK_3) + ranked = sorted(entries, key=lambda oc: canonical_rank(oc[1], oc[0])) + keep_o, keep_c = ranked[0] + dropped = [] + for o, c in ranked[1:]: + dropped_candidate_keys.add((o, str(c.get("candidate_id_proposed")))) + dropped.append({ + "ordinal": o, + "candidate_id": str(c.get("candidate_id_proposed")), + "source_evidence_index": str(c.get("source_evidence_index_proposed")), + "dropped_candidate": c, + }) + dedup_findings.append({ + "identity_signature": sig, + "kept_ordinal": keep_o, + "kept_candidate_id": str(keep_c.get("candidate_id_proposed")), + "dropped": dropped, + }) + + # -------- suspects (semantic exceptions only; GB adjudicates) -------- + suspects: list[dict[str, Any]] = [] + + def add_suspect(rule_id: str, check_id: str, ordinals: list[str], candidate_ids: list[str], + payload: dict[str, Any], escalation: bool = False, excerpt: str = "") -> None: + suspects.append({ + "exception_id": f"B2-SUS-{len(suspects) + 1:03d}", + "source_gate": "B2", + "rule_id": rule_id, + "check_id": check_id, + "ordinals": sorted(set(ordinals)), + "evidence_indexes": [f"E-{o}" for o in sorted(set(ordinals))], + "candidate_ids": candidate_ids[:MAX_CANDIDATES_PER_EXCEPTION], + "payload": payload, + "excerpt": excerpt[:EXCERPT_LIMIT], + "escalation_flag": escalation, + }) + + # CHECK 4: chronological ordering — deterministic date-reversal scan on explicit chain rules + subkind_dates: dict[str, list[tuple[str, str, str | None]]] = {} + for o in expected_ordinals: + for c in parts[o].get("event_candidates") or []: + if not isinstance(c, dict): + continue + sk = str(c.get("event_subkind") or "") + if sk: + subkind_dates.setdefault(sk, []).append((o, str(c.get("candidate_id_proposed")), parse_date(c.get("event_date")))) + ordering_suspect_pairs: list[dict[str, Any]] = [] + null_date_chain_refs: list[dict[str, Any]] = [] + for earlier, later, allow_equal in CHRONO_RULES: + for oe, ce, de in subkind_dates.get(earlier, []): + for ol, cl, dl in subkind_dates.get(later, []): + if de is None or dl is None: + continue + if (de > dl) or (de == dl and not allow_equal and earlier != later and False): + if de > dl: + ordering_suspect_pairs.append({ + "rule": f"{earlier} < {later}" + ("=" if allow_equal else ""), + "earlier": {"ordinal": oe, "candidate_id": ce, "event_date": de}, + "later": {"ordinal": ol, "candidate_id": cl, "event_date": dl}, + }) + for sk in (earlier, later): + for o, cid, d in subkind_dates.get(sk, []): + if d is None: + null_date_chain_refs.append({"ordinal": o, "candidate_id": cid, "event_subkind": sk}) + if ordering_suspect_pairs: + pairs = ordering_suspect_pairs[:MAX_CANDIDATES_PER_EXCEPTION] + add_suspect("R-CHRONO", "chronological_ordering", + [p["earlier"]["ordinal"] for p in pairs] + [p["later"]["ordinal"] for p in pairs], + [p["earlier"]["candidate_id"] for p in pairs] + [p["later"]["candidate_id"] for p in pairs], + {"reversal_pairs": pairs, "total_pairs": len(ordering_suspect_pairs)}, escalation=True) + dedup_null_refs = [] + seen_null = set() + for r in null_date_chain_refs: + key = (r["ordinal"], r["candidate_id"]) + if key not in seen_null: + seen_null.add(key) + dedup_null_refs.append(r) + if dedup_null_refs: + add_suspect("R-CHRONO", "chronological_ordering", + [r["ordinal"] for r in dedup_null_refs][:MAX_CANDIDATES_PER_EXCEPTION], + [r["candidate_id"] for r in dedup_null_refs][:MAX_CANDIDATES_PER_EXCEPTION], + {"null_or_unparsable_dates_in_chain": dedup_null_refs[:MAX_CANDIDATES_PER_EXCEPTION], + "total": len(dedup_null_refs)}, escalation=False) + + # CHECK 5: legal-effect event coverage — REQUIRED_EVENT_SUBKINDS rule table + all_subkinds: set[str] = set(subkind_dates.keys()) + global_text = json.dumps({o: parts[o] for o in expected_ordinals}, ensure_ascii=False) + + def part_subkinds(o: str) -> set[str]: + return {str(c.get("event_subkind") or "") for c in parts[o].get("event_candidates") or [] if isinstance(c, dict)} + + for o in expected_ordinals: + fl = flags_of(parts[o]) + sks = part_subkinds(o) + if any("business_transfer" in f for f in fl) and "commercial_successor_liability_candidate" not in sks: + add_suspect("R-EVTCOV", "legal_effect_event_coverage", [o], + [str(c.get("candidate_id_proposed")) for c in (parts[o].get("event_candidates") or [])[:MAX_CANDIDATES_PER_EXCEPTION] if isinstance(c, dict)], + {"coverage_rule": "business_transfer_without_successor_liability", "doc_semantic_flags": fl}) + if any("claim" in f for f in fl) and not sks & {"debt_acknowledgement", "new_due_date", "demand_notice_dispatch", "demand_notice_arrival"}: + kw = json.dumps(parts[o], ensure_ascii=False) + if any(t in kw for t in ("변제기", "최고", "채무승인", "이행각서")): + add_suspect("R-EVTCOV", "legal_effect_event_coverage", [o], + [str(c.get("candidate_id_proposed")) for c in (parts[o].get("event_candidates") or [])[:MAX_CANDIDATES_PER_EXCEPTION] if isinstance(c, dict)], + {"coverage_rule": "claim_doc_without_demand_or_acknowledgement_subkinds", "doc_semantic_flags": fl}) + if ({"encumbrance_release_after_act", "mortgage_extinguishment_by_fraction"} & all_subkinds) and "value_compensation_exception_trigger" not in all_subkinds: + add_suspect("R-EVTCOV", "legal_effect_event_coverage", + sorted({o for sk in ("encumbrance_release_after_act", "mortgage_extinguishment_by_fraction") for o, _, _ in subkind_dates.get(sk, [])}), + [], {"coverage_rule": "post_act_release_without_value_compensation_exception_trigger"}) + if "auction_sale_and_registry_transfer" in all_subkinds and not {"auction_application_after_extinguishment", "mortgage_extinguishment_by_fraction"} <= all_subkinds: + add_suspect("R-EVTCOV", "legal_effect_event_coverage", + sorted({o for o, _, _ in subkind_dates.get("auction_sale_and_registry_transfer", [])}), + [], {"coverage_rule": "auction_chain_incomplete", + "missing": sorted({"auction_application_after_extinguishment", "mortgage_extinguishment_by_fraction"} - all_subkinds)}) + if "미등기" in global_text and not {"unregistered_building_original_acquisition", "de_facto_disposition_holder_acquisition"} & all_subkinds: + add_suspect("R-EVTCOV", "legal_effect_event_coverage", [], [], + {"coverage_rule": "unregistered_building_cue_without_acquisition_subkinds"}) + if "lien_extinction_notice_dispatch" in all_subkinds and "lien_extinction_notice_receipt" not in all_subkinds: + add_suspect("R-EVTCOV", "legal_effect_event_coverage", + sorted({o for o, _, _ in subkind_dates.get("lien_extinction_notice_dispatch", [])}), + [], {"coverage_rule": "lien_notice_dispatch_without_receipt"}) + succession_flag_ords = [o for o in expected_ordinals if any("succession" in f for f in flags_of(parts[o]))] + if succession_flag_ords and not {"succession_renunciation_acceptance", "spouse_sole_succession_effect_candidate"} & all_subkinds: + add_suspect("R-EVTCOV", "legal_effect_event_coverage", succession_flag_ords[:MAX_ITEMS_PER_EXCEPTION], [], + {"coverage_rule": "succession_doc_without_succession_subkinds"}) + if ({"fraudulent_transfer"} <= all_subkinds and "encumbrance_release_after_act" in all_subkinds + and "value_compensation_cap_calculation_candidate" not in all_subkinds): + add_suspect("R-EVTCOV", "legal_effect_event_coverage", [], [], + {"coverage_rule": "fraudulent_transfer_with_release_but_no_cap_calculation"}) + fraction_refs = [] + for o in expected_ordinals: + for c in parts[o].get("event_candidates") or []: + if isinstance(c, dict) and str(c.get("event_subkind") or "") in ("mortgage_setting_by_fraction", "mortgage_extinguishment_by_fraction") and not nonempty(c.get("fraction_ref")): + fraction_refs.append({"ordinal": o, "candidate_id": str(c.get("candidate_id_proposed"))}) + if fraction_refs: + add_suspect("R-EVTCOV", "legal_effect_event_coverage", + [r["ordinal"] for r in fraction_refs][:MAX_CANDIDATES_PER_EXCEPTION], + [r["candidate_id"] for r in fraction_refs][:MAX_CANDIDATES_PER_EXCEPTION], + {"coverage_rule": "fraction_subkind_with_null_fraction_ref", "refs": fraction_refs[:MAX_CANDIDATES_PER_EXCEPTION]}) + + # R-SELFWARN: mapper 항변/재반박 신호의 승격 판정 대상 (7/10 review finding 재현 경로) + defense_refs: list[dict[str, str]] = [] + for o in expected_ordinals: + for c in parts[o].get("event_candidates") or []: + if isinstance(c, dict) and is_defense_relay(c): + defense_refs.append({"ordinal": o, "candidate_id": str(c.get("candidate_id_proposed")), + "event_subkind": str(c.get("event_subkind") or ""), + "summary": clip(c.get("action_summary"), 120)}) + if defense_refs: + add_suspect("R-SELFWARN", "review_findings_in_mapper", + [r["ordinal"] for r in defense_refs], + [r["candidate_id"] for r in defense_refs], + {"relay_kind": "defense_assertion_requires_stage2_review", "refs": defense_refs}) + + # -------- finalize & merge (dict-copy pass-through; Q-1/Q-2 구조적 해소) -------- + block_ordinals = {f["ordinal"] for f in schema_invariant_findings} + final_items: list[dict[str, Any]] = [] + dropped_by_b1_alias: list[str] = [] + input_candidate_total = 0 + final_candidate_total = 0 + field_count_errors: list[str] = [] + for o in expected_ordinals: + item = parts[o] + input_candidate_total += len(item.get("event_candidates") or []) + if o in b1_dropped_ordinals: + dropped_by_b1_alias.append(o) + continue + if o in block_ordinals: + # schema invariant BLOCK part는 정상 item으로 승격하지 않는다 (명시적 BLOCK finding 대응) + continue + out: dict[str, Any] = {} + placed = False + for k, v in item.items(): + if k == "evidence_index_proposed": + out["evidence_index"] = f"E-{o}" + placed = True + elif k == "mapper_meta": + continue + elif k == "event_candidates": + new_cands = [] + for c in v or []: + if not isinstance(c, dict): + continue + if (o, str(c.get("candidate_id_proposed"))) in dropped_candidate_keys: + continue + nc: dict[str, Any] = {} + for ck, cv in c.items(): + if ck == "candidate_id_proposed": + nc["candidate_id"] = str(cv) + elif ck == "source_evidence_index_proposed": + nc["source_evidence_index"] = str(cv) + else: + nc[ck] = cv + if len(nc) != len(c): + field_count_errors.append(f"{o}:{c.get('candidate_id_proposed')} field count {len(nc)} != {len(c)}") + new_cands.append(nc) + final_candidate_total += 1 + out["event_candidates"] = new_cands + else: + out[k] = v + if not placed: + out = {"evidence_index": f"E-{o}", **out} + final_items.append(out) + + envelope = {"schema_version": "evidence_event_candidates.v1", "items": final_items} + + # -------- conservation invariants (write 전) -------- + dropped_cand_count = sum(len(d["dropped"]) for d in dedup_findings) + conservation_errors: list[str] = list(field_count_errors) + expected_item_count = shard_count - len(b1_dropped_ordinals) - len(block_ordinals) + if len(final_items) != expected_item_count: + conservation_errors.append(f"item count {len(final_items)} != shard_count {shard_count} - b1_dropped {len(b1_dropped_ordinals)} - schema_block {len(block_ordinals)}") + want_ids = {i for i in expected_ids if i not in {f"E-{o}" for o in b1_dropped_ordinals} and i not in {f"E-{o}" for o in block_ordinals}} + got_ids = [it["evidence_index"] for it in final_items] + if set(got_ids) != want_ids or len(got_ids) != len(set(got_ids)): + conservation_errors.append("final evidence_index set mismatch vs planner expected ids minus explicit drops") + blocked_cand_count = sum(len(parts[o].get("event_candidates") or []) for o in block_ordinals) + b1_dropped_cand_count = sum(len(parts[o].get("event_candidates") or []) for o in b1_dropped_ordinals) + if input_candidate_total != final_candidate_total + dropped_cand_count + blocked_cand_count + b1_dropped_cand_count: + conservation_errors.append( + f"candidate conservation failed: input {input_candidate_total} != final {final_candidate_total} + dedup {dropped_cand_count} + blocked {blocked_cand_count} + b1_dropped {b1_dropped_cand_count}") + if not dedup_findings and not block_ordinals and not b1_dropped_ordinals and final_candidate_total < input_candidate_total: + conservation_errors.append("candidate count decreased without any recorded dedup group (silent loss)") + if sorted(got_ids) != got_ids: + conservation_errors.append("final items not sorted by ordinal") + if conservation_errors: + print(json.dumps({"status": "FAILED", "gate": "B2", + "message": "B2 quality gate BLOCK 발생: downstream 진행 금지", + "conservation_errors": conservation_errors}, ensure_ascii=False)) + raise RuntimeError("B2 conservation invariant violated; final file not written") + + content = json.dumps(envelope, ensure_ascii=False, indent=2) + verify = json.loads(content) + if (len(verify["items"]) != len(final_items) + or [i["evidence_index"] for i in verify["items"]] != got_ids + or sum(len(i.get("event_candidates") or []) for i in verify["items"]) != final_candidate_total): + raise RuntimeError("post-serialization verification failed for evidence_event_candidates.json") + write_doc(FINAL_PATH, content) + + block = bool(hard_findings) or bool(schema_invariant_findings) + precheck = { + "schema_version": "B2_precheck.v1", + "gate_id": "B2_event_candidates", + "planner_source": PLAN_PATH, + "planner_shard_count": shard_count, + "expected_ordinals": expected_ordinals, + "received_ordinals": expected_ordinals, + "input_parts_count": len(expected_ordinals), + "extra_part_detection": extra_detection, + "schema_invariant_findings": schema_invariant_findings, + "deterministic_checks": [ + {"check_id": "event_candidate_completeness", + "severity": "BLOCK" if (stale_parts or extra_parts) else "PASS", + "detail": "expected/received/stale set 비교 (결정적)", "missing_ordinals": missing_ordinals}, + {"check_id": "evidence_reference_integrity", + "severity": "BLOCK" if reference_violations else "PASS", + "detail": "finalized evidence id manifest 집합 멤버십 검사 (결정적)", "violations": reference_violations}, + {"check_id": "candidate_id_collision", + "severity": "BLOCK" if format_violations else "PASS", + "detail": "EVT-{ordinal:03d}-{seq:02d} 형식·유일성 + exact identity_signature dedup (결정적)", + "format_violations": format_violations, "dedup_findings": dedup_findings}, + {"check_id": "chronological_ordering", "severity": "PASS", + "detail": "명시 날짜 chain rule 역전 스캔, 의심건은 GB 판정 대기", "ordering_violations": ordering_suspect_pairs}, + {"check_id": "legal_effect_event_coverage", "severity": "PASS", + "detail": "REQUIRED_EVENT_SUBKINDS rule table 검사, 의심건은 GB 판정 대기", + "missing_subkinds_by_ordinal": [s["payload"].get("coverage_rule") for s in suspects if s["rule_id"] == "R-EVTCOV"]}, + ], + "hard_gate_findings": hard_findings, + "suspects": suspects, + "dedup_findings": dedup_findings, + "dropped_by_b1_alias": dropped_by_b1_alias, + "conservation": { + "input_parts": len(expected_ordinals), + "final_items": len(final_items), + "input_candidates": input_candidate_total, + "final_candidates": final_candidate_total, + "dedup_dropped_candidates": dropped_cand_count, + }, + "block": block, + } + write_doc(PRECHECK_PATH, json.dumps(precheck, ensure_ascii=False, indent=2)) + + # -------- B12 exception pack (B1 suspects + B1 review candidates + B2 suspects) -------- + pack_exceptions: list[dict[str, Any]] = [] + overflow_items: list[dict[str, Any]] = [] + for src in itertools.chain(b1_suspects, ( + { + "exception_id": f"B1-RC-{idx:03d}", + "source_gate": "B1", + "rule_id": "R-SELFWARN", + "check_id": "review_findings_in_mapper", + "ordinals": [rc.get("ordinal")], + "evidence_indexes": [rc.get("evidence_index")], + "candidate_ids": [], + "payload": rc, + "excerpt": "", + "escalation_flag": False, + } for idx, rc in enumerate(b1_review_candidates, 1) + ), suspects): + entry = dict(src) + ev = list(entry.get("evidence_indexes") or []) + cands = list(entry.get("candidate_ids") or []) + if len(ev) > MAX_ITEMS_PER_EXCEPTION and entry.get("rule_id") != "R-SELFWARN": + overflow_items.append({"exception_id": entry.get("exception_id"), "overflow_evidence_indexes": ev[MAX_ITEMS_PER_EXCEPTION:]}) + entry["evidence_indexes"] = ev[:MAX_ITEMS_PER_EXCEPTION] + if len(cands) > MAX_CANDIDATES_PER_EXCEPTION: + overflow_items.append({"exception_id": entry.get("exception_id"), "overflow_candidate_ids": cands[MAX_CANDIDATES_PER_EXCEPTION:]}) + entry["candidate_ids"] = cands[:MAX_CANDIDATES_PER_EXCEPTION] + entry["excerpt"] = str(entry.get("excerpt") or "")[:EXCERPT_LIMIT] + pack_exceptions.append(entry) + pack = { + "schema_version": "B12_exception_pack.v1", + "exception_count": len(pack_exceptions), + "exceptions": pack_exceptions, + "overflow_items": overflow_items, + "budget": { + "max_items_per_exception": MAX_ITEMS_PER_EXCEPTION, + "max_candidates": MAX_CANDIDATES_PER_EXCEPTION, + "max_excerpt_chars": EXCERPT_LIMIT, + }, + } + write_doc(PACK_PATH, json.dumps(pack, ensure_ascii=False, indent=2)) + + overall = "BLOCK" if block else "PASS" + # SKILL.md §3.3: stdout = task output — 단일 JSON 라인. 상태 메시지는 message 필드에 보존. + message = ("B2 quality gate BLOCK 발생: downstream 진행 금지" if block + else f"증거문서 사건행위 후보 finalize 완료 (severity={overall})") + print(json.dumps({ + "status": "B2_DETERMINISTIC_GATE_DONE", + "message": message, + "overall_severity_provisional": overall, + "pending_adjudication": "PENDING_ADJUDICATION" if pack_exceptions else "NO_SUSPECTS", + "final_items": len(final_items), + "input_candidates": input_candidate_total, + "final_candidates": final_candidate_total, + "dedup_findings": len(dedup_findings), + "exception_count": len(pack_exceptions), + }, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B12_gate_llm_adjudicator + preflight_files: + - stage1_tmp/quality_gate/B12_exception_pack.json + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: low + llm_verbosity: low + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Validate only the artifact, input bundle, IDs, coverage, severity, and handoff condition assigned to the current task. + - Identify contradictions only within assigned inputs: date, amount, party, object, registration, payment, possession, notice, valuation, legal role, causal link, source identity, or required support chain. + - Distinguish contradiction, incompleteness, ambiguity, multiplicity, schema failure, writer-boundary breach, and review-only legal uncertainty. + - Preserve all material conflicting source IDs. Do not average, normalize, or repair unless the current task expressly authorizes repair. + - Treat PASS, SOFT_WARNING, HARD_WARNING, BLOCK, READY, READY_WITH_REVIEW, FAILED, rollback, and signoff statuses exactly as the current schema defines them. + - Do not downgrade severity for convenience, and do not block progression unless the current gate criteria require it. + - Validators report issues in the authorized validation, audit, gate, review, or rollback schema. They do not write final artifacts unless explicitly assigned. + + + + + Task_B12_gate_llm_adjudicator + + You are an MCP-enabled LLM-backed **exception adjudicator** assisting plaintiff-side Korean civil/commercial litigation counsel. You judge only the compact suspect items that the deterministic B1/B2 gate reducers extracted. You are not a merger, not a final writer, and not a fact creator. + + + 본 task는 v3 아키텍처의 조건부 국소 판정기(GB)다. 결정적 reducer(`Task_B1_quality_gate_evidence_indexed`, `Task_B2_quality_gate_event_candidates`)가 기계적 검사로 좁혀낸 의심 항목만 `stage1_tmp/quality_gate/B12_exception_pack.json`에서 읽어, 항목별 판정을 `stage1_tmp/quality_gate/B12_adjudication_decisions.json`에 기록한다. + 최종 파일(`evidence_indexed.json`, `evidence_event_candidates.json`)과 audit(`quality_gates/*.json`)은 본 task가 쓰지 않는다. audit 확정은 후속 결정적 task `Task_B12_gate_audit_finalizer`가, sidecar는 `Task_B2_SHA256_soft_gate_handoff_writer`가 전담한다. + 변호사 관점의 판정 원칙: 법률적 불확실성(suspected alias, component coverage 의문, chronology 모호성, 항변 사항의 Stage 2 검토 필요성)은 review 판정으로 남기고, provenance 오염·source trace 상실·final evidence identity collision처럼 downstream traceability를 실제로 깨는 경우에만 `CONFIRM_BLOCK`을 사용한다. review 판정은 사실 확정이 아니라 검토 큐다. + + + + + + - `stage1_tmp/quality_gate/B12_exception_pack.json` (유일한 입력 파일. 각 exception의 payload·excerpt가 이미 내장되어 있다) + + + - `stage1_tmp/quality_gate/B12_adjudication_decisions.json` + + + - `evidence_indexed.json`, `evidence_event_candidates.json`, `quality_gates/` 아래 어떤 파일도 작성·재작성 + - exception pack 외의 어떤 파일도 read (full planner/final/meeting/evidence 재독해 금지 — 필요한 발췌는 pack에 이미 포함됨) + - mapper part 수정 또는 새 candidate·새 사실 창작 + - pack에 없는 exception_id 창작, 또는 pack의 exception_id 중 판정 누락 + - `source_refs`를 배열로 출력 (반드시 named object: `evidence_indexes`/`event_candidate_ids`/`meeting_clause_refs`/`doc_titles`) + - count·통계·요약 필드 창작 (집계는 후속 결정적 task 소관) + + + + + 1. `read_docs`로 `stage1_tmp/quality_gate/B12_exception_pack.json` 1개 파일만 읽는다. `list_docs` 사전 확인은 이 파일 1건에 한한다. + 2. `exception_count == 0`이면: `write_file(overwrite=true)`로 `{"schema_version": "B12_adjudication_decisions.v1", "decisions": [], "no_suspects": true}`를 `stage1_tmp/quality_gate/B12_adjudication_decisions.json`에 저장하고 `"NO_SUSPECTS"`만 출력한 뒤 즉시 종료한다(terminate). 다른 어떤 파일도 읽지 않는다. + 3. `exception_count > 0`이면: 각 exception을 pack에 포함된 `payload`·`excerpt`만으로 판정한다. 추가 `read_docs` 호출은 금지된다. + 4. 각 exception에 대해 decision을 하나 결정한다: + - `DISMISS`: 기계적 신호였을 뿐 실질 문제가 없음 (예: 정상적인 rule-table 공백, 문서 성격상 자연스러운 component 부재) + - `SOFT_WARNING_REVIEW`: Stage 2 검토 큐에 남길 경미한 법률적 불확실성 + - `HARD_WARNING_REVIEW`: 청구원인·소권 판단에 영향 가능성이 있는 검토 필요 사항 + - `CONFIRM_BLOCK`: provenance 오염, source trace 상실, final evidence identity collision이 실제로 확인되는 경우만 + - `ESCALATE_TO_REVIEW`: `escalation_flag`가 true이고 pack 정보만으로 확정할 수 없는 경우 — 서로 다른 source가 같은 법률행위의 날짜·당사자·목적물을 실질적으로 충돌 표시하거나, authority drift 여부가 source trace 유효성을 좌우하거나, chronology 판단이 hard block 여부를 좌우하는 경우. 이 판정은 인간 변호사 검토 큐로 라우팅된다. + 5. `write_file(overwrite=true)`로 decisions 파일을 저장한다. 스키마: + {"schema_version": "B12_adjudication_decisions.v1", "no_suspects": false, "decisions": [ {"exception_id": "...", "decision": "DISMISS|SOFT_WARNING_REVIEW|HARD_WARNING_REVIEW|CONFIRM_BLOCK|ESCALATE_TO_REVIEW", "severity": "PASS|SOFT_WARNING|HARD_WARNING|BLOCK", "issue_type": "snake_case", "source_refs": {"evidence_indexes": [], "event_candidate_ids": [], "meeting_clause_refs": [], "doc_titles": []}, "review_instruction": "≤120자 한국어", "candidate_use_policy": "prioritize_if_supported|preserve_as_needs_review|do_not_generate_without_support", "concise_basis": "≤120자 한국어"} ] } + 6. `list_docs`로 decisions 파일 존재만 확인한다. 절대 읽지 않는다. + 7. `"B12 예외 판정 완료 (decisions=<건수>)"`만 출력하고 작업을 끝낸다(terminate). + + + + - exception pack 부재·빈 파일·파싱 불가: 즉시 중단하고 채팅으로만 보고한다. decisions 파일은 쓰지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + + - `hard`(BLOCK 상당)는 downstream 오염·출처·무결성 실패만: stale/provenance 오염, source trace 상실, final evidence identity collision. 그 외는 전부 review다. + - review 판정은 사실 확정이 아니며 자동 차단 사유가 아니다. `rule_id`별 기준: + - `R-ALIAS`: 두 문서가 실제 동일 문서의 alias인지. 동일 확정 + source trace 위험이면 CONFIRM_BLOCK, 동일 의심이면 HARD_WARNING_REVIEW, 별개 문서면 DISMISS. + - `R-COVERAGE`: 문서 유형상 필수 component가 실제로 원문에 존재할 개연성이 payload/excerpt에서 보이는데 비어 있으면 HARD_WARNING_REVIEW, 문서 성격상 자연스러운 부재면 DISMISS. + - `R-DRIFT`: excerpt(해당 shard 원문 발췌)와 part 필드를 대조하여 다른 문서 고유 사실의 주입이 보이면 CONFIRM_BLOCK, 판단 불가면 ESCALATE_TO_REVIEW, 근거 없으면 DISMISS. + - `R-CHRONO`: 날짜 역전이 같은 법률행위 chain의 실질 모순이면(이행기·시효·도달일·경매 chain) HARD_WARNING_REVIEW 또는 ESCALATE_TO_REVIEW, 서로 다른 채무·물건의 우연한 교차면 DISMISS. + - `R-EVTCOV`: payment/notice/auction/lien/succession/valuation/defense/상호속용 등 legal-effect event 누락 의심이 실질적이면 SOFT/HARD_WARNING_REVIEW, rule-table의 기계적 공백이면 DISMISS. + - `R-SELFWARN`: mapper가 남긴 항변·재반박 신호(상계 주장 등)는 원칙적으로 SOFT_WARNING_REVIEW로 승격하여 Stage 2 검토 큐에 보존한다. `candidate_use_policy="preserve_as_needs_review"`. + - `source_refs`는 pack의 `evidence_indexes`/`candidate_ids`를 named object로 재배치한다. 임의 ref 창작 금지. + + + + + + [ ] 1) `read_docs` 사용해서 `stage1_tmp/quality_gate/B12_exception_pack.json` 1개 파일만 읽기 + [ ] 2) `exception_count == 0`이면 빈 decisions 파일(`no_suspects: true`) 저장 후 `"NO_SUSPECTS"` 출력하고 즉시 종료 + [ ] 3) 각 exception을 pack 내장 payload/excerpt만으로 판정 (추가 read 금지) + [ ] 4) 모든 exception_id에 정확히 1개 decision, `source_refs`는 named object + [ ] 5) `write_file` 사용해서 `stage1_tmp/quality_gate/B12_adjudication_decisions.json` 저장 + [ ] 6) `list_docs`로 파일 존재만 확인(절대 읽기 금지) + [ ] 7) `"B12 예외 판정 완료 (decisions=<건수>)"` 출력하고 작업을 끝낸다(terminate) + + + + + - `stage1_tmp/quality_gate/B12_exception_pack.json` + + + - `stage1_tmp/quality_gate/B12_adjudication_decisions.json` + + + + + - 본 task는 판정만 한다. 병합·직렬화·집계·최종 파일 작성은 전부 결정적 task의 소관이다. + - pack 외 파일을 읽는 순간 read-diet 위반이다. 필요한 발췌는 이미 pack 안에 있다. + - review 판정은 검토 큐이지 사실 확정이 아니다. 확신 없는 BLOCK보다 ESCALATE_TO_REVIEW가 항상 안전하다. + + + + - task_name: Task_B12_gate_audit_finalizer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 120 + code: | + #!/usr/bin/env python3 + # Task_B12_gate_audit_finalizer (v3) - deterministic audit finalizer (GC) + # Spec: Part_1_Improvement_Strategy_Claude_v1.md §7 + from __future__ import annotations + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + + B1_PRECHECK_PATH = "stage1_tmp/quality_gate/B1_precheck.json" + B2_PRECHECK_PATH = "stage1_tmp/quality_gate/B2_precheck.json" + DECISIONS_PATH = "stage1_tmp/quality_gate/B12_adjudication_decisions.json" + PACK_PATH = "stage1_tmp/quality_gate/B12_exception_pack.json" + B1_AUDIT_PATH = "quality_gates/B1_evidence_indexed_gate.json" + B2_AUDIT_PATH = "quality_gates/B2_event_candidates_gate.json" + MAX_REVIEW_ITEMS = 40 + SOURCE_REF_KEYS = ("evidence_indexes", "event_candidate_ids", "meeting_clause_refs", "doc_titles") + SEVERITY_ORDER = {"PASS": 0, "SOFT_WARNING": 1, "HARD_WARNING": 2, "BLOCK": 3} + ALLOWED_POLICIES = ("prioritize_if_supported", "preserve_as_needs_review", "do_not_generate_without_support") + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-b12-gate-audit-finalizer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _mid(), + "method": "tools/call", + "params": {"name": name, "arguments": args}, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: Any, name: str) -> str: + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError(f"Empty: {name}") + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, dict) and "results" in outer: + result0 = (outer.get("results") or [{}])[0] + inner = result0.get("content") or result0.get("text") or "" + if not inner: + raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer if isinstance(outer, str) else json.dumps(outer, ensure_ascii=False) + + def read_raw(path: str) -> str: + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str) or not raw.strip(): + raise RuntimeError(f"Missing or empty required input: {path}") + return raw + + def parse_json(raw: str, path: str) -> Any: + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + try: + obj, _ = decoder.raw_decode(raw.strip()) + return obj + except Exception as exc: + raise RuntimeError(f"Invalid JSON input: {path}") from exc + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def clip(value: Any, limit: int = 120) -> str: + text = " ".join(str(value or "").split()) + if len(text) <= limit: + return text + return text[:limit].rstrip() + "..." + + def clean_enum(value: Any, fallback: str) -> str: + text = str(value or fallback).strip().lower() + text = re.sub(r"[^a-z0-9_]+", "_", text) + text = re.sub(r"_+", "_", text).strip("_") + return text or fallback + + def named_source_refs(value: Any, exception: dict[str, Any] | None) -> dict[str, list[str]]: + # source_refs는 반드시 named object — 배열형은 schema failure로 강제 정규화하지 않고 pack 기반으로 재구성 + refs = {key: [] for key in SOURCE_REF_KEYS} + if isinstance(value, dict): + for key in SOURCE_REF_KEYS: + vals = value.get(key) + if isinstance(vals, list): + refs[key].extend(str(v).strip() for v in vals if v) + if exception: + refs["evidence_indexes"].extend(str(v) for v in (exception.get("evidence_indexes") or []) if v) + refs["event_candidate_ids"].extend(str(v) for v in (exception.get("candidate_ids") or []) if v) + return {key: sorted(set(v for v in vals if v)) for key, vals in refs.items()} + + def worst(*severities: str) -> str: + top = "PASS" + for s in severities: + if SEVERITY_ORDER.get(s, 0) > SEVERITY_ORDER.get(top, 0): + top = s + return top + + DECISION_SEVERITY = { + "DISMISS": "PASS", + "SOFT_WARNING_REVIEW": "SOFT_WARNING", + "HARD_WARNING_REVIEW": "HARD_WARNING", + "ESCALATE_TO_REVIEW": "HARD_WARNING", + "CONFIRM_BLOCK": "BLOCK", + } + + def build_audit(gate: str, precheck: dict[str, Any], decisions_by_exc: dict[str, dict[str, Any]], + exceptions_by_id: dict[str, dict[str, Any]], executed_at: str) -> dict[str, Any]: + gate_id = "B1_evidence_indexed" if gate == "B1" else "B2_event_candidates" + checks: list[dict[str, Any]] = [] + check_sev: dict[str, str] = {} + for chk in precheck.get("deterministic_checks") or []: + entry = dict(chk) + check_sev[str(entry.get("check_id"))] = str(entry.get("severity") or "PASS") + checks.append(entry) + + hard_findings: list[dict[str, Any]] = [] + for idx, hf in enumerate(precheck.get("hard_gate_findings") or [], 1): + item = dict(hf) if isinstance(hf, dict) else {"detail": str(hf)} + item.setdefault("gate_finding_id", f"{gate}-HARD-{idx:03d}") + hard_findings.append(item) + + review_findings: list[dict[str, Any]] = [] + drift_confirmed: list[dict[str, Any]] = [] + for exc_id, exc in exceptions_by_id.items(): + if str(exc.get("source_gate")) != gate: + continue + dec = decisions_by_exc.get(exc_id) + if dec is None: + # 판정 누락 exception은 보수적으로 review로 승격 (silent pass 금지) + dec = {"exception_id": exc_id, "decision": "ESCALATE_TO_REVIEW", "severity": "HARD_WARNING", + "issue_type": clean_enum(exc.get("rule_id"), "review_finding"), + "source_refs": {}, "concise_basis": "adjudication decision missing for extracted suspect", + "candidate_use_policy": "preserve_as_needs_review"} + decision = str(dec.get("decision") or "ESCALATE_TO_REVIEW") + mapped = DECISION_SEVERITY.get(decision, "HARD_WARNING") + check_id = str(exc.get("check_id") or "review_finding") + if decision == "DISMISS": + continue + if decision == "CONFIRM_BLOCK": + item = { + "gate_finding_id": f"{gate}-HARD-{len(hard_findings) + 1:03d}", + "issue_type": clean_enum(dec.get("issue_type") or exc.get("rule_id"), "confirmed_block"), + "source_check": clean_enum(check_id, "gate_check"), + "detail": clip(dec.get("concise_basis") or "adjudicator confirmed hard gate violation"), + "source_refs": named_source_refs(dec.get("source_refs"), exc), + } + hard_findings.append(item) + check_sev[check_id] = worst(check_sev.get(check_id, "PASS"), "BLOCK") + if check_id == "authority_drift_detection": + drift_confirmed.append({"ordinals": exc.get("ordinals"), "basis": item["detail"]}) + continue + severity = str(dec.get("severity") or mapped) + if severity not in ("SOFT_WARNING", "HARD_WARNING"): + severity = mapped + policy = dec.get("candidate_use_policy") + if policy not in ALLOWED_POLICIES: + policy = "preserve_as_needs_review" + review_findings.append({ + "gate_finding_id": f"{gate}-REV-{len(review_findings) + 1:03d}", + "source_check": clean_enum(check_id, "review_finding"), + "severity": severity, + "issue_type": clean_enum(dec.get("issue_type") or exc.get("rule_id"), "review_finding"), + "domain_hints": [], + "target_stage_hints": ["Stage_2"], + "target_task_hints": [], + "source_refs": named_source_refs(dec.get("source_refs"), exc), + "review_instruction": clip(dec.get("review_instruction") + or "후속 검토 단계에서 해당 review finding의 증거와 법률상 의미를 재검토한다."), + "candidate_use_policy": policy, + "concise_basis": clip(dec.get("concise_basis") or exc.get("rule_id")), + }) + check_sev[check_id] = worst(check_sev.get(check_id, "PASS"), severity) + review_findings = review_findings[:MAX_REVIEW_ITEMS] + + for chk in checks: + cid = str(chk.get("check_id")) + chk["severity"] = check_sev.get(cid, str(chk.get("severity") or "PASS")) + if cid == "authority_drift_detection" and drift_confirmed: + chk["drift_findings"] = (chk.get("drift_findings") or []) + drift_confirmed + + schema_invariant = list(precheck.get("schema_invariant_findings") or []) if gate == "B2" else None + block = bool(hard_findings) or bool(schema_invariant) + overall = "BLOCK" if block else worst(*[str(c.get("severity") or "PASS") for c in checks], + *[str(r.get("severity")) for r in review_findings]) + if overall == "BLOCK" and not block: + overall = "HARD_WARNING" + progression = "BLOCKED" if block else ("ALLOWED" if overall == "PASS" else "ALLOWED_WITH_WARNING") + auto = not block + highest_review = "PASS" + for r in review_findings: + highest_review = worst(highest_review, str(r.get("severity"))) + handoff_status = "BLOCKED" if block else ("READY_WITH_REVIEW" if review_findings else "READY_NO_REVIEW") + + audit: dict[str, Any] = { + "gate_id": gate_id, + "executed_at": executed_at, + "input_parts_count": precheck.get("input_parts_count"), + "expected_ordinals": precheck.get("expected_ordinals") or [], + "planner_source": precheck.get("planner_source") or "evidence_shard_plan.json", + "planner_shard_count": precheck.get("planner_shard_count"), + "received_ordinals": precheck.get("received_ordinals") or [], + } + if gate == "B2": + audit["schema_invariant_findings"] = schema_invariant or [] + audit.update({ + "checks": checks, + "overall_severity": overall, + "block_reasons": [clip(h.get("detail") or h.get("issue_type")) for h in hard_findings] + + ([clip(f"{s.get('reason')} (rerun: {s.get('rerun_target_task')})") for s in (schema_invariant or [])]), + "warning_reasons": [clip(r.get("concise_basis")) for r in review_findings], + "hard_gate_findings": hard_findings, + "review_findings": review_findings, + "rerun_targets": sorted({str(s.get("rerun_target_task")) for s in (schema_invariant or []) if s.get("rerun_target_task")}), + "stage2_risk_notes": [clip(r.get("concise_basis")) for r in review_findings], + "review_gate_summary": { + "review_gate_count": len(review_findings), + "highest_review_severity": highest_review, + }, + "handoff_export_summary": { + "is_soft_handoff_writer": False, + "handoff_writer_task": "Task_B2_SHA256_soft_gate_handoff_writer", + "handoff_path": "quality_gates/stage1_part1_soft_gate_handoff.json", + "exported_review_count": len(review_findings), + "handoff_status_candidate": handoff_status, + }, + "downstream_progression": progression, + "stage2_auto_progression_allowed": auto, + }) + if gate == "B1": + audit["dropped_aliases"] = precheck.get("dropped_aliases") or [] + audit["schema_contract_version"] = "B1_quality_gate.v3" + else: + audit["dedup_findings"] = precheck.get("dedup_findings") or [] + audit["dropped_by_b1_alias"] = precheck.get("dropped_by_b1_alias") or [] + audit["conservation"] = precheck.get("conservation") or {} + audit["schema_contract_version"] = "B2_quality_gate.v3" + return audit + + def main() -> None: + _init() + b1_precheck = parse_json(read_raw(B1_PRECHECK_PATH), B1_PRECHECK_PATH) + b2_precheck = parse_json(read_raw(B2_PRECHECK_PATH), B2_PRECHECK_PATH) + try: + pack = parse_json(read_raw(PACK_PATH), PACK_PATH) + except Exception: + pack = {"exceptions": []} + try: + decisions_doc = parse_json(read_raw(DECISIONS_PATH), DECISIONS_PATH) + except Exception: + decisions_doc = {"decisions": [], "no_suspects": True} + + exceptions_by_id: dict[str, dict[str, Any]] = {} + for exc in pack.get("exceptions") or []: + if isinstance(exc, dict) and exc.get("exception_id"): + exceptions_by_id[str(exc["exception_id"])] = exc + decisions_by_exc: dict[str, dict[str, Any]] = {} + for dec in decisions_doc.get("decisions") or []: + if isinstance(dec, dict) and dec.get("exception_id"): + decisions_by_exc[str(dec["exception_id"])] = dec + + b2_review_summary_split = {"b1": 0, "b2": 0} + executed_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") + b1_audit = build_audit("B1", b1_precheck, decisions_by_exc, exceptions_by_id, executed_at) + b2_audit = build_audit("B2", b2_precheck, decisions_by_exc, exceptions_by_id, executed_at) + b2_review_summary_split["b1"] = b1_audit["review_gate_summary"]["review_gate_count"] + b2_review_summary_split["b2"] = b2_audit["review_gate_summary"]["review_gate_count"] + # v.2 B2 audit review_gate_summary는 B1/B2 review count를 함께 요약한다 + b2_audit["review_gate_summary"]["b1_review_count"] = b2_review_summary_split["b1"] + b2_audit["review_gate_summary"]["b2_review_count"] = b2_review_summary_split["b2"] + + write_doc(B1_AUDIT_PATH, json.dumps(b1_audit, ensure_ascii=False, indent=2)) + write_doc(B2_AUDIT_PATH, json.dumps(b2_audit, ensure_ascii=False, indent=2)) + + # SKILL.md §3.3: stdout = task output — 단일 JSON 라인. 상태 메시지는 message 필드에 보존. + messages = [] + if b1_audit["overall_severity"] == "BLOCK": + messages.append("B1 quality gate BLOCK 발생: downstream 진행 금지") + if b2_audit["overall_severity"] == "BLOCK": + messages.append("B2 quality gate BLOCK 발생: downstream 진행 금지") + print(json.dumps({ + "status": "GATE_AUDITS_FINALIZED", + "message": " | ".join(messages) if messages else "gate audit finalize 완료", + "b1_overall": b1_audit["overall_severity"], + "b2_overall": b2_audit["overall_severity"], + "review_count": b2_review_summary_split["b1"] + b2_review_summary_split["b2"], + "hard_count": len(b1_audit["hard_gate_findings"]) + len(b2_audit["hard_gate_findings"]) + + len(b2_audit.get("schema_invariant_findings") or []), + "no_suspects": bool(decisions_doc.get("no_suspects")), + }, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B2_SHA256_soft_gate_handoff_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import hashlib + import itertools + import json + import re + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + SIDE_CAR_PATH = "quality_gates/stage1_part1_soft_gate_handoff.json" + INPUT_PATHS = { + "evidence_indexed_sha256": "evidence_indexed.json", + "evidence_event_candidates_sha256": "evidence_event_candidates.json", + "b1_gate_sha256": "quality_gates/B1_evidence_indexed_gate.json", + "b2_gate_sha256": "quality_gates/B2_event_candidates_gate.json", + } + WORKFLOW_ROLES = ( + "claim_right_identification", + "cause_of_action_element_build", + "evidence_weight_and_gap_check", + "chronology_and_state_split", + "asset_registry_succession_check", + "defense_and_obstacle_check", + "relief_scope_and_quantification", + "actio_value_compensation_check", + ) + SOURCE_REF_KEYS = ( + "evidence_indexes", + "event_candidate_ids", + "meeting_clause_refs", + "doc_titles", + ) + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-b2-sha256-soft-gate-handoff-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _mid(), + "method": "tools/call", + "params": {"name": name, "arguments": args}, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: Any, name: str) -> str: + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError(f"Empty: {name}") + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, dict) and "results" in outer: + result0 = (outer.get("results") or [{}])[0] + inner = result0.get("content") or result0.get("text") or "" + if not inner: + raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer if isinstance(outer, str) else json.dumps(outer, ensure_ascii=False) + + def read_raw(path: str) -> str: + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str) or not raw.strip(): + raise RuntimeError(f"Missing or empty required input: {path}") + return raw + + def parse_json(raw: str, path: str) -> Any: + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + try: + obj, _ = decoder.raw_decode(raw.strip()) + return obj + except Exception as exc: + raise RuntimeError(f"Invalid JSON input: {path}") from exc + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def digest(raw: str) -> str: + value = hashlib.sha256(raw.encode("utf-8")).hexdigest() + if not re.fullmatch(r"[0-9a-f]{64}", value): + raise RuntimeError("digest_guard produced a non-SHA256 value") + return value + + def as_list(value: Any) -> list[Any]: + if value is None: + return [] + if isinstance(value, list): + return value + return [value] + + def as_str_list(value: Any) -> list[str]: + out: list[str] = [] + for item in as_list(value): + if item is None: + continue + if isinstance(item, dict): + for key in ("id", "value", "name", "title", "ref"): + if item.get(key): + out.append(str(item[key]).strip()) + break + else: + out.append(str(item).strip()) + return [x for x in out if x] + + def clean_enum(value: Any, fallback: str) -> str: + text = str(value or fallback).strip().lower() + text = re.sub(r"[^a-z0-9_]+", "_", text) + text = re.sub(r"_+", "_", text).strip("_") + return text or fallback + + def clip(value: Any, limit: int = 120) -> str: + text = " ".join(str(value or "").split()) + if len(text) <= limit: + return text + return text[:limit].rstrip() + "..." + + def normalize_severity(value: Any) -> str: + text = str(value or "").upper() + if "HARD" in text: + return "HARD_WARNING" + return "SOFT_WARNING" + + def role_for(issue_type: str, finding: dict[str, Any]) -> str: + explicit = finding.get("lawyer_workflow_role") + if explicit in WORKFLOW_ROLES: + return str(explicit) + text = " ".join([ + issue_type, + str(finding.get("source_check") or ""), + str(finding.get("check_id") or ""), + str(finding.get("detail") or finding.get("reason") or ""), + ]).lower() + if any(k in text for k in ("chrono", "date", "timeline", "state", "order")): + return "chronology_and_state_split" + if any(k in text for k in ("evidence", "proof", "source", "locator")): + return "evidence_weight_and_gap_check" + if any(k in text for k in ("registry", "secured", "lien", "succession", "asset", "auction", "mortgage")): + return "asset_registry_succession_check" + if any(k in text for k in ("defense", "obstacle", "setoff", "limitation")): + return "defense_and_obstacle_check" + if any(k in text for k in ("relief", "amount", "scope", "valuation", "damage")): + return "relief_scope_and_quantification" + if "actio" in text or "fraudulent" in text or "value_compensation" in text: + return "actio_value_compensation_check" + if any(k in text for k in ("claim", "right", "element", "legal_effect")): + return "claim_right_identification" + return "cause_of_action_element_build" + + def source_refs(finding: dict[str, Any]) -> dict[str, list[str]]: + refs = {key: [] for key in SOURCE_REF_KEYS} + nested = finding.get("source_refs") + if isinstance(nested, dict): + for key in SOURCE_REF_KEYS: + refs[key].extend(as_str_list(nested.get(key))) + refs["evidence_indexes"].extend(as_str_list(finding.get("evidence_indexes"))) + refs["evidence_indexes"].extend(as_str_list(finding.get("evidence_index"))) + refs["evidence_indexes"].extend(as_str_list(finding.get("source_evidence_index"))) + refs["event_candidate_ids"].extend(as_str_list(finding.get("event_candidate_ids"))) + refs["event_candidate_ids"].extend(as_str_list(finding.get("candidate_id"))) + refs["event_candidate_ids"].extend(as_str_list(finding.get("candidate_ids"))) + refs["meeting_clause_refs"].extend(as_str_list(finding.get("meeting_clause_refs"))) + refs["meeting_clause_refs"].extend(as_str_list(finding.get("meeting_clause_ref"))) + refs["doc_titles"].extend(as_str_list(finding.get("doc_titles"))) + refs["doc_titles"].extend(as_str_list(finding.get("doc_title"))) + return {key: sorted(set(value)) for key, value in refs.items()} + + def extract_findings(gate: Any, key: str) -> list[Any]: + findings: list[Any] = [] + if isinstance(gate, dict): + findings.extend(as_list(gate.get(key))) + for check in as_list(gate.get("checks")): + if isinstance(check, dict): + findings.extend(as_list(check.get(key))) + return findings + + def hard_findings(gate: Any, source_gate: str) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for item in extract_findings(gate, "hard_gate_findings"): + out.append(item if isinstance(item, dict) else {"detail": str(item)}) + if isinstance(gate, dict): + blocked = gate.get("overall_severity") == "BLOCK" or gate.get("downstream_progression") == "BLOCKED" or gate.get("stage2_auto_progression_allowed") is False + if blocked and not out: + reasons = as_list(gate.get("block_reasons")) + if not reasons: + reasons = [f"{source_gate} gate blocked without explicit hard_gate_findings"] + for reason in reasons: + out.append({"issue_type": "block_reason", "detail": str(reason)}) + for index, item in enumerate(out, 1): + item.setdefault("gate_finding_id", f"{source_gate}-HARD-{index:03d}") + return out + + def compact_review_items(b1_gate: Any, b2_gate: Any) -> list[dict[str, Any]]: + compact: list[dict[str, Any]] = [] + ordered = [("B1", b1_gate), ("B2", b2_gate)] + seq = 1 + for source_gate, gate in ordered: + for raw in extract_findings(gate, "review_findings"): + finding = raw if isinstance(raw, dict) else {"detail": str(raw)} + issue_type = clean_enum( + finding.get("issue_type") or finding.get("check_id") or finding.get("source_check") or "review_finding", + "review_finding", + ) + role = role_for(issue_type, finding) + refs = source_refs(finding) + policy = finding.get("candidate_use_policy") + if policy not in ("prioritize_if_supported", "preserve_as_needs_review", "do_not_generate_without_support"): + policy = "preserve_as_needs_review" + item = { + "review_id": f"P1RG-{seq:04d}", + "source_gate": source_gate, + "source_check": clean_enum(finding.get("source_check") or finding.get("check_id") or issue_type, issue_type), + "severity": normalize_severity(finding.get("severity")), + "issue_type": issue_type, + "lawyer_workflow_role": role, + "domain_hints": as_str_list(finding.get("domain_hints") or finding.get("domain_hint") or finding.get("domains")), + "target_stage_hints": as_str_list(finding.get("target_stage_hints") or finding.get("target_stage_hint")), + "target_task_hints": as_str_list(finding.get("target_task_hints") or finding.get("target_task_hint")), + "source_refs": refs, + "review_instruction": clip(finding.get("review_instruction") or finding.get("instruction") or "후속 검토 단계에서 해당 review finding의 증거와 법률상 의미를 재검토한다.", 120), + "negative_instruction": "사실 확정 또는 자동 차단으로 취급하지 말 것", + "candidate_use_policy": policy, + "concise_basis": clip(finding.get("concise_basis") or finding.get("detail") or finding.get("reason") or issue_type, 120), + "created_from": { + "gate_file": "quality_gates/B1_evidence_indexed_gate.json" if source_gate == "B1" else "quality_gates/B2_event_candidates_gate.json", + "gate_finding_id": finding.get("gate_finding_id") or finding.get("finding_id") or finding.get("id"), + }, + } + compact.append(item) + seq += 1 + if len(compact) >= 40: + return compact + return compact + + def add_index(index: dict[str, dict[str, list[str]]], bucket: str, key: str, review_id: str) -> None: + if not key: + return + index[bucket].setdefault(key, []) + if review_id not in index[bucket][key]: + index[bucket][key].append(review_id) + + def build_routing(review_items: list[dict[str, Any]]) -> tuple[dict[str, dict[str, list[str]]], dict[str, list[str]]]: + routing = { + "by_evidence_index": {}, + "by_event_candidate_id": {}, + "by_meeting_clause_ref": {}, + "by_issue_type": {}, + "by_domain_hint": {}, + "by_target_stage": {}, + "by_lawyer_workflow_role": {}, + } + queues = {role: [] for role in WORKFLOW_ROLES} + for item in review_items: + rid = item["review_id"] + refs = item.get("source_refs") or {} + for value in refs.get("evidence_indexes", []): + add_index(routing, "by_evidence_index", value, rid) + for value in refs.get("event_candidate_ids", []): + add_index(routing, "by_event_candidate_id", value, rid) + for value in refs.get("meeting_clause_refs", []): + add_index(routing, "by_meeting_clause_ref", value, rid) + add_index(routing, "by_issue_type", item.get("issue_type", ""), rid) + for value in item.get("domain_hints", []): + add_index(routing, "by_domain_hint", value, rid) + for value in item.get("target_stage_hints", []): + add_index(routing, "by_target_stage", value, rid) + role = item.get("lawyer_workflow_role", "cause_of_action_element_build") + add_index(routing, "by_lawyer_workflow_role", role, rid) + queues.setdefault(role, []).append(rid) + return routing, {role: queues.get(role, []) for role in WORKFLOW_ROLES} + + def gate_blocked(gate: Any) -> bool: + return isinstance(gate, dict) and ( + gate.get("overall_severity") == "BLOCK" + or gate.get("downstream_progression") == "BLOCKED" + or gate.get("stage2_auto_progression_allowed") is False + ) + + def main() -> None: + _init() + raw_by_digest_key = {key: read_raw(path) for key, path in INPUT_PATHS.items()} + parsed = {key: parse_json(raw, INPUT_PATHS[key]) for key, raw in raw_by_digest_key.items()} + + digest_guard = {key: digest(raw) for key, raw in raw_by_digest_key.items()} + b1_gate = parsed["b1_gate_sha256"] + b2_gate = parsed["b2_gate_sha256"] + review_items = compact_review_items(b1_gate, b2_gate) + routing_index, lawyer_workflow_queues = build_routing(review_items) + + hard_items = hard_findings(b1_gate, "B1") + hard_findings(b2_gate, "B2") + hard_ids = [ + str(item.get("gate_finding_id") or item.get("finding_id") or item.get("id") or f"HARD-{idx:03d}") + for idx, item in enumerate(hard_items, 1) + ] + blocked = bool(hard_items) or gate_blocked(b1_gate) or gate_blocked(b2_gate) + highest_review = "PASS" + if any(item["severity"] == "HARD_WARNING" for item in review_items): + highest_review = "HARD_WARNING" + elif review_items: + highest_review = "SOFT_WARNING" + b1_count = sum(1 for item in review_items if item["source_gate"] == "B1") + b2_count = sum(1 for item in review_items if item["source_gate"] == "B2") + + if blocked: + handoff_status = "BLOCKED" + elif review_items: + handoff_status = "READY_WITH_REVIEW" + else: + handoff_status = "READY_NO_REVIEW" + + sidecar = { + "schema_version": "stage1_part1_soft_gate_handoff.v1", + "source_stage": "Stage_1.Part_1", + "source_gate_files": [ + "quality_gates/B1_evidence_indexed_gate.json", + "quality_gates/B2_event_candidates_gate.json", + ], + "handoff_status": handoff_status, + "stage2_auto_progression_allowed": not blocked, + "hard_gate_summary": { + "hard_gate_count": len(hard_items), + "hard_gate_ids": hard_ids, + }, + "review_gate_summary": { + "review_gate_count": len(review_items), + "highest_review_severity": highest_review, + "b1_review_count": b1_count, + "b2_review_count": b2_count, + }, + "digest_guard": digest_guard, + "review_items": review_items, + "routing_index": routing_index, + "lawyer_workflow_queues": lawyer_workflow_queues, + } + + content = json.dumps(sidecar, ensure_ascii=False, indent=2) + write_doc(SIDE_CAR_PATH, content) + print(json.dumps({ + "status": "SOFT_GATE_HANDOFF_WRITTEN", + "path": SIDE_CAR_PATH, + "handoff_status": handoff_status, + "review_items": len(review_items), + "digest_guard_valid": all(re.fullmatch(r"[0-9a-f]{64}", v) for v in digest_guard.values()), + }, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + task_procedure: + IN: + nexts: ["Task_A_client_goal", "Task_Evidence_shard_planner"] + wait_until: [] + + Task_A_client_goal: + nexts: [] + wait_until: ["IN"] + + Task_Evidence_shard_planner: + nexts: ["Task_B1_map_doc_*"] + wait_until: ["IN"] + + Task_B1_map_doc_*: + nexts: + - "Task_B2_map_events_{same_ordinal}" + - "Task_B1_quality_gate_evidence_indexed" + wait_until: ["Task_Evidence_shard_planner"] + + Task_B2_map_events_*: + nexts: ["Task_B2_quality_gate_event_candidates"] + wait_until: ["Task_B1_map_doc_{same_ordinal}"] + + Task_B1_quality_gate_evidence_indexed: + nexts: ["Task_B2_quality_gate_event_candidates"] + wait_until: ["all Task_B1_map_doc_*"] + + Task_B2_quality_gate_event_candidates: + nexts: ["Task_B12_gate_llm_adjudicator"] + wait_until: ["all Task_B2_map_events_*", "Task_B1_quality_gate_evidence_indexed"] + + Task_B12_gate_llm_adjudicator: + nexts: ["Task_B12_gate_audit_finalizer"] + wait_until: ["Task_B2_quality_gate_event_candidates"] + + Task_B12_gate_audit_finalizer: + nexts: ["Task_B2_SHA256_soft_gate_handoff_writer"] + wait_until: ["Task_B12_gate_llm_adjudicator"] + + Task_B2_SHA256_soft_gate_handoff_writer: + nexts: ["OUT"] + wait_until: ["Task_B12_gate_audit_finalizer"] + + OUT: + nexts: [] + wait_until: ["Task_B2_SHA256_soft_gate_handoff_writer"] + + prevs: [] + nexts: [] diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_1_Codex_v3.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_1_Codex_v3.yml new file mode 100644 index 00000000..9de1a4f5 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_1_Codex_v3.yml @@ -0,0 +1,3937 @@ +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_1 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Part 1 기초작업 + version: v3.0 + + Stages: + - name: stage1_사건개요파악_기초작업 + description: 사건개요 파악, 기초정보 추출 + llm_provider: openai + llm_model: gpt-4o-2024-08-06 + tools: + mcpServers: + localdocs: + type: streamable-http + url: "http://mcp-localdocs:8012/mcp" + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + + tasks: + - task_name: Task_A_client_goal + llm_provider: google + llm_model: 'gemini-3.1-flash-lite' + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + preflight: false + preflight_files: + - client_meeting.md + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + - Aggregate, compile, publish, commit, or write only the inputs and final/derived file authorized by the current task. + - Preserve stable order, source IDs, support IDs, BO_ID, fact_id, structure_id, claim_group_id, liability_group_id, and required enum semantics exactly. + - Create or finalize IDs only from the current task's specified pattern and deterministic ordering rule. + - Do not merge, deduplicate, supersede, normalize, repair, or drop candidates unless the current reducer/writer schema authorizes that action. + - If validation or source support is insufficient, use the current failure, rollback, warning, or blocked path instead of fabricating a passing final artifact. + - Treat downstream authority and handoff fields as machine contracts; do not add prose, alternative files, or extra writer attestations outside the schema. + + + + + Task_A_client_goal + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. + + + `client_meeting.md`만 읽고 `client_goal.json`을 생성한다. + 이 파일은 고객의 소송 목적, 명시적 제약, 핵심 사건군, 당사자 universe를 Stage 1의 가장 앞단에서 안정적으로 구조화한 요약본이어야 한다. + v.3에서도 기존 필수 schema는 유지하되, 복수 원고와 복수 분쟁군이 뒤섞여 later stage가 공동원고를 과잉 일반화하지 않도록 사건군 단위의 optional 구조, 사건군별 원고적격 구조, 현재 지배상태 구조를 함께 남긴다. + 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않는다. + 특히 상속·승계형 사건군에서는 meeting만으로 확정되지 않은 친족관계 또는 공동행동 정보를 direct standing anchor처럼 과잉 일반화하지 않는다. + 상속이 개입된 사건군에서는 `legal entitlement layer`와 `complaint drafting preference layer`를 분리하여 기록한다. + 사망한 종전 권리주체의 과거 소유사실, 매수사실, 등기명의사실을 living plaintiff의 현재 `소유자/final`로 치환하지 않는다. + 필요하면 사건군별 optional additive field `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`, `preferred_plaintiff_selection_basis`, `succession_resolution_needed`를 사용하여, later stage가 실체법상 가능한 권리자 후보층과 실제 소장에 세울 우선 원고층을 혼동하지 않도록 한다. + 또한 `current_control_map`은 보조 메모가 아니라 later stage의 부당이득·인도·철거 청구 식별에 직접 쓰이는 authority map이므로, 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속을 가능한 한 4축으로 분해 저장한다. + v.3 기준으로 Task_A_client_goal은 모든 issue cluster에 사건군별 법률효과 구조의 seed를 남겨야 한다. 단순 사건 요약이 아니라 금전채권 timeline, 상호속용 책임, 담보부 부동산·지분별 권리상태, 사해행위취소 가액배상, 유치권·상속·통지 lifecycle을 downstream이 구조적으로 읽을 수 있게 `domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints`, `legal_entitlement_candidates`, `current_control_map_4axis`를 additive하게 생성한다. + `client_requested_relief`는 의뢰인이 원하는 청구 기간·금액·상대방을 보존하는 층이고, `legal_sustainability_watchpoints`는 법률상 기산일·원고적격·청구범위 재검토 필요성을 보존하는 층이다. 두 층을 절대 하나로 합치지 않는다. + 상속·승계형 사건군에서 current right holder가 meeting만으로 확정되지 않으면 `current_control_map`의 해당 row는 `certainty="provisional"`로 두고 `standing_watchpoints`에 succession-resolution trigger를 반드시 남긴다. + 또한 현재 무자력/집행난과 실제 제소 유지 의사를 혼동하지 않으며, 특정 상대방을 현재 무자력이라는 이유만으로 소 제기 대상에서 소거하지 않는다. + 필요하면 optional additive field `defendant_target_matrix`를 사용하여 피고별 소제기 유지/제외 의사, 현재 회수위험, 장래 회수신호를 분리 보존한다. + 본 task는 v.3 architecture에서 final writer로서 `client_goal.json`을 직접 작성한다(Task_A는 island/mapper가 없으므로 propose 단계 없이 직접 finalize 가능). + + + + + - In this task, legal/domain fields are meeting-grounded routing seeds and watchpoints for downstream stages. + - They are not final legal conclusions, norm search results, merits judgments, or complaint-drafting advice. + - The EXTRACTION overlay's prohibition on norm search and final legal-effect classification does not prohibit schema-authorized `domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints`, or entitlement/watchpoint seed fields. + + + + + - `client_meeting.md` + + + - `client_goal.json` + + + - 다른 파일 확인/읽기 + - 중간 파일 생성 + - `client_goal.json` 재읽기 + - meeting에 없는 당사자, 사건군, 제약, 희망사항, 청구유형을 창작하기 + - 사건군별 당사자 범위를 무시하고 전 사건 공통 사실처럼 과잉 압축하기 + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하기 + + + + + 1. `list_docs`로 `client_meeting.md` 존재만 확인한다. + 2. `read_docs`로 `client_meeting.md`만 읽는다. + 3. 메모리에서 `client_goal.json` 객체를 생성한다. + 4. `write_file(overwrite=true)`로 `client_goal.json`을 저장한다. + 5. `list_docs`로 `client_goal.json` 존재만 확인한다. 절대 읽지 않는다. + 6. 정확히 `"고객의사 초안 생성 완료"`만 출력하고 종료한다. + + + + - 필수 입력 누락, 빈 파일, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. 파일은 쓰지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + + - `client_meeting.md`만 사실 원천이다. + - 상대방 주장, 의뢰인 희망사항, 상담서두의 인적 관계, 본문 1~5항의 사건군을 모두 읽되, meeting에 직접 나타난 범위만 구조화한다. + - meeting의 문장들 사이를 조합해 새로운 사실을 만들지 않는다. + + + + + - `primary_goal`은 사건 전체를 한 문장으로 요약하되, 개별 분쟁군을 완전히 뭉개지 않을 정도의 수준으로만 추상화한다. + - `희망사항`이 복수이면 공통 분모를 중심으로 요약하되, 특정 핵심 구제수단이 반복되면 반영할 수 있다. + - 희망사항이 피고별 비대칭 전략을 직접 드러내면, 그 비대칭을 보존한다. + - 현재 회수곤란 사실이 있더라도 장래 재산취득, 상속, 보험금 회수, 급여압류 가능성 등으로 특정 피고를 제소 대상으로 유지하려는 의사가 직접 서술되면, 이를 `primary_goal` 또는 `summary_key_incidents`에 반영한다. + - 직접 금전청구 대상과 사해행위취소 대상이 다르면 한쪽으로 뭉개지 않는다. + - 불명확하면 `null`. + + + + - `constraints`에는 의뢰인이 명시적으로 원하지 않는 소, 특정 대상 제외, 가능하면/다만/아울러와 같은 제한적 요청, 상속·경매 공신력 등 사건 진행상의 명시적 제약을 넣는다. + - `constraints`는 소송전략상 하지 말 것, 제한할 것, 절차상·법률상 한계만 담는다. + - 단순한 회수곤란 사실, 현재 무자력, 현재 재산 부족, 집행난은 `constraints`에 넣지 않는다. + - 현재 무자력과 장래 재산취득 가능성이 함께 서술되면, 이를 부정적 제약으로 압축하지 않는다. + - 특정 상대방을 제외하라는 명시적 no-sue 지시가 없는 한, current collectability만으로 대상 제외 제약을 만들지 않는다. + - 해석상 추정 제약은 넣지 않는다. + - 배열 순서는 meeting 서술 순서를 최대한 따른다. + + + + - `summary_key_incidents`는 최대 4문장, 각 100자 이하. + - 각 문장은 가능한 한 서로 다른 사건군을 대표하도록 구성한다. + - 사건군 라벨은 입력자료에 직접 나타난 분쟁 대상, 거래유형, 자산군, 청구 방향을 짧게 추상화하여 만든다. 특정 사건의 지명·상호·인명·자산명을 예시값처럼 재사용하지 않는다. + - 하나의 문장에 서로 다른 사건군을 과도하게 병합하지 않는다. + - 직접 서술된 날짜/당사자/핵심 행위만 사용한다. + - 원고가 특정 피고를 직접 금전청구 대상으로 유지하려는 의사가 직접 서술되면, 사건군 대표 문장 중 하나에 그 방향성을 반영할 수 있다. + - 현재 회수곤란 사실과 장래 회수가능성 신호가 같은 사람에 관하여 함께 직접 서술되면, 둘 중 하나만 선택적으로 삭제하지 않는다. + + + + - `key_facts`는 최대 5개, 각 60자 이하. + - 가능한 한 사건군별 핵심 fact를 1개씩 대표시킨다. + - later stage의 원고적격 판단에 중요할 수 있는 소유관계, 상속관계, 현재 권리귀속, 현재 점유/사용/수익귀속 상태, 특정 처분/경매/통지 사실을 우선한다. + - 직접 서술이 없는 금액·날짜·상태를 보충하지 않는다. + + + + - `claim_type_candidates`는 의뢰인의 희망사항과 사건군 구조를 바탕으로 선택한다. + - 허용 enum만 사용한다: `"금전"|"물건인도"|"행위"|"확인"|"형성"|"보전(가처분/가압류)"`. + - later stage 분류기를 돕기 위한 상위 유형 수준만 남기고, 세부 청구명을 쓰지 않는다. + + + + + + - 이름은 원문 우선, 명백히 동일인/동일법인인 경우만 canonicalize 한다. + - 별칭/변형은 `aliases`에 기록한다. + - 당사자 배열은 사건 전체 participant universe다. 모든 분쟁군의 실질 원고적격/피고적격을 의미하지 않는다. + + + + - `parties.plaintiffs`에는 실제 의뢰인 또는 meeting 서두와 희망사항에서 원고 측 client로 직접 읽히는 사람만 넣는다. + - `parties.plaintiffs`는 client universe일 뿐, 사건군별 실질 원고적격을 의미하지 않는다. + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않는다. + - 사건군별 원고 범위 차이는 `issue_clusters`, `plaintiff_capacity_matrix`, `standing_watchpoints`에서 보정한다. + + + + - `parties.defendants`에는 meeting 본문, 상대방 주장, 희망사항에서 분쟁 상대방 또는 소송상 직접 대상으로 명시된 자만 넣는다. + - `asset_status`는 meeting에 직접 나온 재산 상태, 현재 소유/점유/담보권 설정 상태 등 later stage에 직접 도움이 되는 짧은 상태만 넣는다. + - `asset_status`는 현재 재산 상태에 관한 사실기술일 뿐, 소 제기 대상 제외 신호가 아니다. + - meeting이 "현재 무자력이나 장래 상속/재산취득 가능성 있음"을 직접 서술하면, `asset_status`는 그 둘을 함께 보존하는 방향으로 적는다. + - 현재 무자력만을 이유로 defendants에서 제외하지 않는다. 제외는 명시적 no-sue 지시가 있을 때만 허용한다. + - meeting에 없는 재산상태 추정 금지. + + + + - `parties.third_parties`에는 사건군에 중요하지만 즉시 원고/피고로 확정되지 않은 자를 넣는다. + - `relationship`은 later stage가 읽을 수 있도록 짧고 직접적으로 적는다. + + + + + - optional additive field `defendant_target_matrix`를 허용한다. + - 이 필드는 피고별 소제기 유지/제외 의사와 현재 회수위험을 분리 기록하는 전략 매트릭스다. + - `target_status`는 제소 유지 여부를 나타내고, `execution_risk`는 current collectability를 나타낸다. + - 현재 무자력은 `execution_risk`에 반영하고, 제소 유지 여부는 `target_status`에 별도로 반영한다. + - 장래 상속, 장래 재산취득, 보험금 회수 가능성, 급여압류 가능성 등 미래 회수신호가 직접 서술되면 `future_recovery_signal`에 반영한다. + - 명시적 no-sue 지시가 없는 한, 단순한 current uncollectability만으로 `exclude_now`를 부여하지 않는다. + - 각 원소는 아래 구조를 따른다. + { + "name": "string", + "target_status": "target_now|exclude_now|secondary_target|review", + "basis_text": "meeting 직접 문장에 근거한 짧은 사유", + "execution_risk": "low|medium|high|unknown", + "future_recovery_signal": "none|possible|explicit" + } + - `basis_text`는 반드시 meeting 직접 문장에 근거한 짧은 근거여야 한다. + - 이 필드는 optional이다. 명시적 피고별 타기팅 차이가 없으면 생략할 수 있다. + + + + - optional additive field `issue_clusters`를 허용한다. + - 각 cluster는 meeting의 독립 분쟁군을 1개씩 반영한다. + - 이 필드는 later stage가 전 사건 공통 parties 배열만 보고 과잉 일반화하는 것을 줄이기 위한 보조 구조다. + - `candidate_plaintiffs`는 사건군별 잠정 plaintiff anchor일 뿐, 최종 원고적격 확정값이 아니다. + - `legal_entitlement_candidates`는 해당 사건군에서 meeting 직접 서술상 실체법상 권리자일 가능성이 있는 의뢰인 후보층이다. + - `preferred_plaintiffs_for_complaint`는 later stage가 실제 소장 원고로 우선 검토할 수 있는 narrower drafting layer다. + - 상속·승계형 사건군에서는 `candidate_plaintiffs`, `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`를 서로 자동 동일시하지 않는다. + - `candidate_plaintiffs`에는 meeting에서 해당 사건군의 직접 권리주체, 직접 채권자, 직접 임차인, 직접 점유권자 등으로 읽히는 의뢰인만 넣는다. + - 친족관계, 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 `candidate_plaintiffs`를 확장하지 않는다. + - 사망한 자가 해당 사건군의 종전 권리주체로 보이는데 meeting만으로 승계가 해소되지 않으면, 친족관계나 공동 통지 사실만으로 `candidate_plaintiffs`를 채우지 않는다. + - 위와 같은 unresolved succession cluster에서는 meeting이 직접 보여주는 친족관계와 자산 연결 범위 안에서만 `legal_entitlement_candidates`를 별도 기록할 수 있다. + - `preferred_plaintiffs_for_complaint`는 meeting이 특정 의뢰인을 직접 권리주체 또는 직접 소송유지 주체로 좁혀 보여주거나, 비상속형 사건군에서 direct right-holder가 명백할 때만 채운다. + - meeting만으로 drafting preference가 명백하지 않은 unresolved succession cluster에서는 `preferred_plaintiffs_for_complaint`를 빈 배열로 둘 수 있다. + - `preferred_plaintiff_selection_basis` 허용값은 `"direct_right_holder|resolved_succession|resolved_succession_needed|drafting_preference|review|불명"`이다. + - `succession_resolution_needed`는 사망한 종전 권리주체와 사건군의 연결은 보이지만 meeting만으로 현재 권리귀속 또는 complaint plaintiff narrowing이 해소되지 않을 때 `true`로 둔다. + - meeting이 직접 금전청구 대상 피고와 사해행위 상대방/수익자를 구별해 보여주면, 이를 별도 cluster로 분리할 수 있다. + - 직접 금전회수 대상이 특정 상대방으로 서술되면, property transfer cluster와 별도의 monetary recovery cluster를 허용한다. + - cluster 수는 1개 이상 6개 이하로 제한한다. + - 각 cluster는 아래 구조를 따른다. + { + "cluster_id": "IC-01", + "cluster_label": "짧은 사건군명", + "candidate_plaintiffs": ["이 사건군에서 직접 권리주체로 읽히는 의뢰인"], + "legal_entitlement_candidates": ["실체법상 권리자 후보로 읽히는 의뢰인"], + "preferred_plaintiffs_for_complaint": ["실제 소장 원고로 우선 검토할 의뢰인"], + "preferred_plaintiff_selection_basis": "direct_right_holder|resolved_succession|resolved_succession_needed|drafting_preference|review|불명", + "succession_resolution_needed": true, + "candidate_defendants_or_counterparties": ["직접 상대방"], + "desired_relief_keywords": ["금전", "명도", "퇴거", "확인", "보전" 등 짧은 키워드] + } + - 비상속형 사건군에서 direct right-holder가 명백하면 `candidate_plaintiffs`, `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`가 동일할 수 있다. + - `issue_clusters`는 optional이다. 명확하지 않으면 생략 가능하다. + - 추가 사실 창작 없이 meeting 본문과 희망사항에서 직접 읽히는 범위만 기록한다. + + + + - optional additive field `domain_profiles`를 각 `issue_clusters[*]` 내부에 허용한다. + - `domain_profiles`는 아래 네 하위 profile을 담는 object다. 해당하지 않는 profile은 `null`로 둔다. + - `monetary_claim_profile` + - `secured_property_dispute_profile` + - `actio_property_transfer_profile` + - `possession_lien_succession_profile` + - `client_requested_relief`와 `legal_sustainability_watchpoints`를 각 cluster 내부에 둘 수 있다. + - 의뢰인이 원하는 청구취지·기간·금액은 `client_requested_relief`에 보존하고, 법률상 기산일·원고적격·범위가 불명확한 점은 `legal_sustainability_watchpoints`에 분리한다. + - 당사자가 원하는 기간이나 금액을 법률상 청구취지에 바로 사용할 수 있다고 단정하지 않는다. + + + - 금전청구 사건군에서 원채무자 외 보증인, 채무인수자, 영업양수인, 상호속용자, 공동채무자 후보가 함께 등장하면 이를 별도 후보 피고 목록에만 두지 말고 `monetary_claim_profile`, `liability_group_hint`, `commercial_successor_liability_hint`로 묶어 보존한다. + - 동일 금액, 동일 원채권, 동일 원고를 기준으로 복수 피고가 책임질 가능성이 있으면 `liability_group_hint.same_underlying_debt=true`로 둔다. + - 지급기일, 이행청구, 최고, 채무승인, 이행각서, 소멸시효, 변제기 재설정 단서가 등장하면 `monetary_claim_profile.requires_money_claim_timeline=true`로 둔다. + - 영업양도, 상호 계속 사용, 사업 양수인, 채무승계 부인, 책임배제 등기 단서가 등장하면 `commercial_successor_liability_hint.requires_article_42_review=true`로 둔다. + + + + - 부동산 사건에서 여러 대여금, 여러 근저당권, 지분별 담보, 변제, 경매, 후행 등기가 함께 등장하면 이를 흩어진 issue로 분리하지 말고 `secured_property_dispute_profile`에 묶는다. + - 변제금이 복수 채무에 영향을 줄 수 있으면 `requires_payment_allocation=true`로 표시한다. + - 전체 부동산이 아니라 지분별 유효/무효가 문제될 수 있으면 `requires_fractional_validity_analysis=true`로 표시한다. + - 원고 주장 지분, 피고 측 유효 가능 지분, 전체 부동산 말소 금지 필요성을 `fractional_title_issue_hint`에 남긴다. + - 미등기 건물, 건물 신축, 건물 매매, 대지 임대차가 함께 등장하면 `land_use_gain_hint`를 생성한다. + - 임대차보증금, 월 차임, 건물 매매대금은 각각 법적 성격이 다르므로 `lease_payment_semantics_hint`에 별도 표시한다. + - 임대차보증금은 토지소유자의 부당이득 원금으로 자동 산정하지 말라는 watchpoint를 남긴다. + + + + - 채무자가 부동산을 수익자에게 이전하고, 원고가 사해행위취소를 희망하는 사건군을 발견하면 `actio_property_transfer_profile`을 생성한다. + - 부동산에 근저당권 등 부담이 있고, 그 부담이 사해행위 이후 변제·말소되었다는 단서가 있으면 `requires_value_compensation_review=true`로 표시한다. + - 같은 채권자와 채무자 사이에 복수 대여금이 등장하면 단순 후보채권 목록으로 흩뜨리지 말고 `preserved_claim_bundle_hint`로 묶는다. + - 피고가 명의신탁, 충분 담보, 상계, 선의 등을 주장하면 이를 `defense_hints`에 남기고, 청구원인 성립 또는 범위에 직접 영향을 주는 항변이면 `requires_stage2_rebuttal=true`로 표시한다. + + + + - 현재 점유자와 현재 권리자가 분리되는 자산군 또는 사건군이 입력자료에서 직접 확인되면 `possession_lien_succession_profile`을 생성한다. + - 상속이 개입되면 current right holder를 final로 확정하지 말고 `succession_resolution_needed=true`와 `standing_watchpoints`를 남긴다. + - 유치권, 제3자 임대, 직접 거주, 피담보채권 충당, 후소유자 소멸청구권 부정 주장이 보이면 `possession_basis_asserted`, `notice_lifecycle_hints`, `legal_sustainability_watchpoints`를 구조화한다. + - 통지서 발송일과 답변서상 수령 인정일이 함께 보이면 발송일과 도달일을 분리하여 `notice_lifecycle_hints`에 남긴다. + - 상속·승계가 미해결인 cluster에서는 입력자료상 공동 통지 발신자, 친족 후보, 직계존속·직계비속 후보를 그 지위의 후보로만 보존하고, 상속 해결 전 `preferred_plaintiffs_for_complaint` 확정자로 두지 않는다. + + + + + - optional additive field `current_control_map_4axis`를 허용한다. + - legacy `current_control_map` row 구조는 유지하되, 목적물별 4축 요약이 가능한 경우 `current_control_map_4axis`를 추가한다. + - 각 item은 아래 구조를 따른다. + { + "object": "string", + "cluster_id": "IC-01", + "current_right_holder_candidate": "string|null", + "current_possessor": "string|null", + "current_user": "string|null", + "current_benefit_holder": "string|null", + "possession_basis_asserted": "string|null", + "possession_basis_transition_needed": true, + "certainty": "final|provisional|불명" + } + - 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속을 하나의 generic holder로 뭉개지 않는다. + - 상속·승계형 사건군에서 현재 권리귀속이 decedent의 과거 권리사실과 친족관계로만 추론되는 경우 `certainty="provisional"`로 둔다. + + + + - optional additive field `plaintiff_capacity_matrix`를 허용한다. + - 이 필드는 사건군별 실질 원고적격을 later stage로 넘기기 위한 standing map이다. + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않는다. + - 각 원소는 아래 구조를 따른다. + { + "cluster_id": "IC-01", + "plaintiff_name": "string", + "capacity_basis": "소유자|공유자|상속인|임차인|채권자|점유권자|불명", + "source_basis_text": "meeting에서 직접 읽히는 짧은 근거", + "certainty": "final|provisional" + } + - 사건군별 직접 권리주체로 읽히는 사람만 넣는다. + - `capacity_basis="상속인"` row라도, 근거가 친족관계, 사망, 공동 통지, 공동 상담 같은 succession trigger 수준이면 `certainty`는 반드시 `"provisional"`로 둔다. + - 상속·승계형 사건군에서 decedent의 과거 소유사실, 매수사실, 등기명의사실만 보일 뿐 living plaintiff의 승계 완료 또는 현재 권리귀속이 meeting에서 직접 해소되지 않으면, living plaintiff를 `capacity_basis="소유자"` 또는 `capacity_basis="공유자"` + `certainty="final"`로 두지 않는다. + - 위 경우에는 원칙적으로 `capacity_basis`를 `"상속인"` 또는 `"불명"`으로 두고, `certainty`를 `"provisional"`로 둔다. + - meeting이 특정 사건군에 관하여 직접 계약당사자, 직접 채권자, 직접 임차인, 직접 소유·공유 관계 또는 승계 완료 상태를 직접 보여줄 때만 `certainty="final"`을 허용한다. + - 원고적격이 불명확하거나 later stage 재검토가 필요하면 `certainty`는 `"provisional"`로 둔다. + - `preferred_plaintiffs_for_complaint`가 별도로 존재하더라도, 이를 맞추기 위해 `plaintiff_capacity_matrix`를 `소유자/final`로 과잉 보정하지 않는다. + + + + - optional additive field `current_control_map`를 허용한다. + - 이 필드는 사건군별 현재 지배상태를 later stage로 넘기기 위한 authority map이다. + - 사건군에 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속 사실이 직접 읽히면 가능한 한 `current_control_map`을 생성한다. + - 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속은 가능한 한 분리한다. + - 각 원소는 아래 구조를 따른다. + { + "cluster_id": "IC-01", + "holder_name": "string", + "holder_role": "current_right_holder|current_possessor|current_user|current_benefit_holder", + "basis_text": "meeting에서 직접 읽히는 짧은 근거", + "certainty": "final|provisional" + } + - 같은 사람이 복수 role을 가지면 중복 기재할 수 있다. + - meeting에 직접 없는 role은 추정하지 않는다. + - 상속·승계형 사건군에서 current right holder가 decedent의 과거 권리사실 + 친족관계만으로 유도되는 경우, `holder_role="current_right_holder"` row는 원칙적으로 `certainty="provisional"`로 둔다. + - decedent의 과거 소유사실만으로 living plaintiff를 `current_right_holder/final`로 기재하지 않는다. + - 비상속형 사건군에서 meeting이 현재 소유자, 현재 점유자, 현재 사용자, 현재 수익귀속자를 직접 적시하면 해당 row는 `certainty="final"`을 우선 검토한다. + + + + - optional additive field `standing_watchpoints`를 허용한다. + - later stage가 반드시 재검토해야 하는 원고적격/권리귀속 리스크를 짧은 경고문으로 남긴다. + - 상속, 상속포기, 사망, 경매, 공동소유, 현재 권리주체 불명, 현재 점유자와 권리주체 불일치가 보이면 우선 검토한다. + - 경고문은 가능한 한 자산군 또는 사건군을 특정하는 asset-specific trigger 형태로 쓴다. + - 예시 형식: + - "상속·승계 자료 추가 확인 후 해당 자산군 원고적격 재확인 필요" + - "현재 권리귀속과 현재 점유자 분리 검토 필요" + - "사망한 종전 권리주체 관련 succession resolution 필요" + - meeting에 직접 읽히는 범위를 넘는 장문 법률판단은 쓰지 않는다. + + + + - `summary_key_incidents`, `key_facts`, `constraints`는 exact-match 중복 제거 후 최초 순서를 유지한다. + - `aliases`는 명백한 경우에만 기록한다. + - `claim_type_candidates`는 중복 제거 후 최초 순서를 유지한다. + - `defendant_target_matrix`를 사용하면 same `name` + `target_status` 조합 기준 exact-match 중복 제거 후 최초 순서를 유지한다. + - 비nullable string 외에는 `불명` 사용을 피하고 `null` 또는 빈 배열을 우선한다. + + + + 기존 필수 schema는 유지하고, 아래 optional additive field를 추가할 수 있다. + v.3 schema registry 기준 `IssueClusterDomainProfile`, `LegalEntitlementCandidate`, `CurrentControlMap4Axis`를 반영한다. + + { + "primary_goal": "string|null", + "constraints": [], + "summary_key_incidents": [], + "key_facts": [], + "claim_type_candidates": ["금전","물건인도","행위","확인","형성","보전(가처분/가압류)"], + "parties": { + "plaintiffs": [{"name":"string","type":"법인|자연인|기관"}], + "defendants": [{"name":"string","type":"법인|자연인|기관|미확정","asset_status":"string|null"}], + "third_parties": [{"name":"string","relationship":"string"}] + }, + "aliases": {}, + + "defendant_target_matrix": [ + { + "name": "string", + "target_status": "target_now|exclude_now|secondary_target|review", + "basis_text": "string", + "execution_risk": "low|medium|high|unknown", + "future_recovery_signal": "none|possible|explicit" + } + ], + + "issue_clusters": [ + { + "cluster_id": "IC-01", + "cluster_label": "string", + "candidate_plaintiffs": ["string"], + "legal_entitlement_candidates": ["string"], + "preferred_plaintiffs_for_complaint": ["string"], + "preferred_plaintiff_selection_basis": "direct_right_holder|resolved_succession|resolved_succession_needed|drafting_preference|review|불명", + "succession_resolution_needed": true, + "candidate_defendants_or_counterparties": ["string"], + "desired_relief_keywords": ["string"], + "domain_profiles": { + "monetary_claim_profile": { + "underlying_claim_type": "string|null", + "principal_amount_candidate": "string|null", + "principal_debtor_candidates": ["string"], + "successor_or_secondary_liability_candidates": [], + "default_or_limitation_issue_expected": true, + "known_demand_or_notice_signals": ["string"], + "requires_money_claim_timeline": true + }, + "secured_property_dispute_profile": { + "has_multiple_secured_debts": true, + "requires_payment_allocation": true, + "requires_fractional_validity_analysis": true, + "secured_object_candidates": ["string"], + "auction_or_registry_invalidity_expected": true + }, + "actio_property_transfer_profile": { + "debtor": "string|null", + "creditor": "string|null", + "beneficiary": "string|null", + "fraudulent_act_asset": "string|null", + "fraudulent_act_type": "string|null", + "fraudulent_act_date_hint": "string|null", + "is_encumbered_real_estate": true, + "requires_value_compensation_review": true + }, + "possession_lien_succession_profile": { + "possession_basis_asserted": "string|null", + "succession_resolution_needed": true, + "notice_lifecycle_hints": [], + "lien_violation_or_extinction_hints": [] + } + }, + "client_requested_relief": ["string"], + "legal_sustainability_watchpoints": ["string"], + "liability_group_hint": {}, + "commercial_successor_liability_hint": {}, + "fractional_title_issue_hint": {}, + "land_use_gain_hint": {}, + "lease_payment_semantics_hint": {}, + "preserved_claim_bundle_hint": {}, + "value_compensation_hint": {}, + "defense_hints": [] + } + ], + + "plaintiff_capacity_matrix": [ + { + "cluster_id": "IC-01", + "plaintiff_name": "string", + "capacity_basis": "소유자|공유자|상속인|임차인|채권자|점유권자|불명", + "source_basis_text": "string", + "certainty": "final|provisional" + } + ], + + "current_control_map": [ + { + "cluster_id": "IC-01", + "holder_name": "string", + "holder_role": "current_right_holder|current_possessor|current_user|current_benefit_holder", + "basis_text": "string", + "certainty": "final|provisional" + } + ], + + "current_control_map_4axis": [ + { + "object": "string", + "cluster_id": "IC-01", + "current_right_holder_candidate": "string|null", + "current_possessor": "string|null", + "current_user": "string|null", + "current_benefit_holder": "string|null", + "possession_basis_asserted": "string|null", + "possession_basis_transition_needed": true, + "certainty": "final|provisional|불명" + } + ], + + "standing_watchpoints": ["string"] + } + + 추가 제약: + - `defendant_target_matrix`, `issue_clusters`, `plaintiff_capacity_matrix`, `current_control_map`, `standing_watchpoints`는 optional이다. + - `domain_profiles`, `client_requested_relief`, `legal_sustainability_watchpoints`, `liability_group_hint`, `commercial_successor_liability_hint`, `fractional_title_issue_hint`, `land_use_gain_hint`, `lease_payment_semantics_hint`, `preserved_claim_bundle_hint`, `value_compensation_hint`, `defense_hints`는 `issue_clusters` 내부 optional additive field다. + - `current_control_map_4axis`는 top-level optional additive field이며 legacy `current_control_map`을 대체하지 않는다. + - `legal_entitlement_candidates`, `preferred_plaintiffs_for_complaint`, `preferred_plaintiff_selection_basis`, `succession_resolution_needed`는 `issue_clusters` 내부 optional additive field다. + - 기존 필수 필드는 모두 유지한다. + - 허용된 additive field 외 임의 추가 필드 금지. + + + + - `parties.plaintiffs`에는 실제 의뢰인만 남았는지 점검한다. + - `parties.plaintiffs`가 client universe로만 쓰였고, 사건군별 원고적격 확정값처럼 오염되지 않았는지 점검한다. + - 공동 내방, 공동 최고, 공동 통지, 공동 희망만으로 사건군별 공동원고를 확정하지 않았는지 점검한다. + - `parties.defendants`와 `third_parties`가 뒤바뀌지 않았는지 점검한다. + - `summary_key_incidents`가 사건군별로 과잉 병합되지 않았는지 점검한다. + - `key_facts`가 later stage에 중요한 standing/ownership/current control fact를 누락하지 않았는지 점검한다. + - `issue_clusters`를 썼다면 각 cluster의 당사자와 desired relief가 meeting 직접 서술 범위를 넘지 않는지 점검한다. + - 상속·승계형 사건군에서 `candidate_plaintiffs`가 친족관계, 공동 통지, 공동 상담 같은 간접 신호만으로 확장되지 않았는지 점검한다. + - `issue_clusters`를 썼다면 unresolved succession cluster에서 `legal_entitlement_candidates`와 `preferred_plaintiffs_for_complaint`가 자동 동일시되지 않았는지 점검한다. + - `preferred_plaintiffs_for_complaint`를 썼다면 그 basis가 `preferred_plaintiff_selection_basis`에 남아 있고, meeting 직접 서술 또는 direct right-holder 구조를 넘지 않는지 점검한다. + - `succession_resolution_needed=true`인데도 해당 cluster가 direct owner/final cluster처럼 오염되지 않았는지 점검한다. + - `plaintiff_capacity_matrix`를 썼다면 각 row의 `capacity_basis`와 `source_basis_text`가 meeting 직접 서술 범위를 넘지 않는지 점검한다. + - `capacity_basis="상속인"` row가 친족관계나 공동행동 사실만으로 `certainty="final"`로 오염되지 않았는지 점검한다. + - 사망한 종전 권리주체의 과거 소유사실, 매수사실, 등기명의사실이 living plaintiff의 `소유자/final` row로 잘못 치환되지 않았는지 점검한다. + - `current_control_map`을 썼다면 `holder_role`이 `current_right_holder|current_possessor|current_user|current_benefit_holder` 중 하나인지와, basis가 meeting 직접 문장에 근거하는지 점검한다. + - 상속·승계형 사건군에서 `current_right_holder` row가 있으면, meeting만으로 승계가 해소되지 않은 경우 `certainty="provisional"`인지 점검한다. + - 현재 권리귀속, 현재 점유, 현재 사용, 현재 수익귀속이 서로 다른데 하나의 generic holder row로 뭉개지지 않았는지 점검한다. + - 사망한 종전 권리주체가 보이는 사건군이 있으면 `standing_watchpoints`에 succession-resolution trigger가 남았는지 점검한다. + - `standing_watchpoints`를 썼다면 장문 법률판단이 아니라 later stage 재검토용 짧은 경고인지 점검한다. + - meeting이 피고별로 현재 집행가능성과 장래 회수가능성을 구별하여 서술하면, 그 차이가 `constraints`, `primary_goal`, `summary_key_incidents`, `parties.defendants.asset_status`, `defendant_target_matrix` 중 어디엔가 비대칭적으로 보존되었는지 점검한다. + - "현재 무자력"과 "장래 재산취득 가능성"이 같은 사람에게 함께 서술되면, 그 사람을 자동 배제 대상으로 압축하지 않았는지 점검한다. + - `constraints`에 current collectability나 단순 회수곤란 사실이 잘못 들어가 있지 않은지 점검한다. + - `asset_status`가 제소 배제 신호처럼 쓰이지 않았는지 점검한다. + - `defendant_target_matrix`를 썼다면 각 `name`이 `parties.defendants`와 모순되지 않는지 점검한다. + - `defendant_target_matrix`에서 `target_status="exclude_now"`가 붙은 사람에게는 meeting상 명시적 no-sue 지시가 있는지 점검한다. + - `defendant_target_matrix`에서 `execution_risk="high"`와 `target_status="target_now"`가 함께 존재할 수 있음을 허용하되, 그 경우 `basis_text` 또는 `future_recovery_signal`이 비어 있지 않은지 점검한다. + - 장래 상속/재산취득 가능성이 직접 서술된 피고가 `exclude_now`로 잘못 내려가지 않았는지 점검한다. + - 금전채권 cluster에 최고ㆍ채무승인ㆍ이행각서ㆍ시효 단서가 있는데 `monetary_claim_profile.requires_money_claim_timeline`이 없거나 false이면 warning으로 본다. + - 동일 사건군에 원채무자와 승계ㆍ양수ㆍ보증ㆍ상호속용 책임 후보가 함께 있는데 `liability_group_hint`가 비어 있으면 warning으로 본다. + - 영업양도ㆍ상호속용 단서가 있는데 `commercial_successor_liability_hint.requires_article_42_review`가 없으면 warning으로 본다. + - 대여금ㆍ근저당권ㆍ경매ㆍ지분 단서가 같은 cluster에 있는데 `secured_property_dispute_profile.requires_payment_allocation`이 없으면 warning으로 본다. + - 상담문서에 지분별 권리상태가 보이는데 `fractional_title_issue_hint`가 없으면 warning으로 본다. + - 미등기 건물ㆍ임대차ㆍ건물 매매ㆍ토지 사용 단서가 있는데 `land_use_gain_hint`가 없으면 warning으로 본다. + - 사해행위취소ㆍ부담부 부동산ㆍ사후 담보권 말소 단서가 있는데 `actio_property_transfer_profile.requires_value_compensation_review`가 false이면 error로 본다. + - 같은 채권자와 채무자 사이 복수 대여금이 보이는데 `preserved_claim_bundle_hint`가 없으면 warning으로 본다. + - 명의신탁ㆍ충분담보ㆍ상계ㆍ선의 등 항변 단서가 있는데 `defense_hints`가 없으면 warning으로 본다. + - 유치권ㆍ상속ㆍ승계 사건군에서 `succession_resolution_needed=true`인데도 입력자료상 공동 통지 발신자, 친족 후보, 직계존속·직계비속 후보가 `preferred_plaintiffs_for_complaint` 확정자로 들어가면 error로 본다. + - 통지 발송일과 도달일 단서가 모두 있는데 `notice_lifecycle_hints`가 없으면 warning으로 본다. + - `current_control_map_4axis`를 썼다면 4축 중 직접 읽히는 축이 generic holder 하나로 합쳐지지 않았는지 점검한다. + - 출력 파일명과 성공 메시지가 정확한지 점검한다. + + + + + + [ ] 1) Preflight: `list_docs` 사용해서 `client_meeting.md` 확인, `read_docs` 사용해서 `client_meeting.md` 읽기 + [ ] 2) `write_file` 사용해서 `client_goal.json` 생성 + [ ] 3) `list_docs` 사용해서 `client_goal.json` 파일 존재만 확인(절대 읽기 금지) + [ ] 4) `"고객의사 초안 생성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `client_meeting.md` + + + - `client_goal.json` + + + + + - `client_goal.json`은 절대 재읽지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"고객의사 초안 생성 완료"` 한 줄만 사용한다. + + + + - task_name: Task_Evidence_shard_planner + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import hashlib + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + def _mid(): return next(MSG_ID) + def _init(): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"task-evidence-shard-planner","version":"1.0","user_id":"{{__user_hash__}}","workspace_id":"{{__workspace_hash__}}"}}}, headers=MCP_HEADERS) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post(LOCALDOCS_URL, json={"jsonrpc":"2.0","method":"notifications/initialized"}, headers=MCP_HEADERS).raise_for_status() + def _parse(text): + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: return json.loads(line[6:]) + except Exception: pass + try: return json.loads(text) + except Exception: return None + def _call(name, args): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc":"2.0","id":_mid(),"method":"tools/call","params":{"name":name,"arguments":args}}, headers=MCP_HEADERS) + r.raise_for_status() + p = _parse(r.text) + if not p or "result" not in p: raise RuntimeError(f"MCP {name} failed") + return p + def _unwrap(p, name): + text = (p["result"].get("content") or [{}])[0].get("text", "") + if not text: raise RuntimeError(f"Empty: {name}") + try: outer = json.loads(text) + except Exception: return text + if isinstance(outer, dict) and "results" in outer: + r0 = (outer.get("results") or [{}])[0] + inner = r0.get("content") or r0.get("text") or "" + if not inner: raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return json.dumps(outer, ensure_ascii=False) if not isinstance(outer, str) else outer + def read_json(name): + raw = _unwrap(_call("read_docs", {"doc_names": [name]}), name) + if not isinstance(raw, str): return raw + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + obj, _ = decoder.raw_decode(raw.strip()) + return obj + def write_doc(path, content): + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def _items(obj: Any) -> list[Any]: + if isinstance(obj, list): + return obj + if isinstance(obj, dict): + for key in ("items", "documents", "evidences", "evidence", "docs"): + val = obj.get(key) + if isinstance(val, list): + return val + raise RuntimeError("evidence_all.json must be a JSON array or an object containing a document array") + + def _text(v: Any) -> str | None: + if v is None: + return None + s = str(v).strip() + return s or None + + def _doc_title(doc: Any, ordinal: int) -> str | None: + if not isinstance(doc, dict): + return f"evidence_all[{ordinal}]" + for key in ("title", "name", "filename", "file_name", "document_title", "doc_title"): + val = _text(doc.get(key)) + if val: + return val + return f"evidence_all[{ordinal}]" + + def _doc_uid(doc: Any, ordinal: int) -> str: + if isinstance(doc, dict): + for key in ("doc_uid", "uid", "id", "document_id", "source_id"): + val = _text(doc.get(key)) + if val: + return val + return f"DOC-{ordinal:03d}" + + def _stable_hash(doc: Any) -> str: + raw = json.dumps(doc, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _list_doc_names(args) -> list[str]: + payload = _unwrap(_call("list_docs", args), "list_docs") + if isinstance(payload, str): + try: + payload = json.loads(payload) + except Exception: + return re.findall(r"[\w./-]+\.json", payload) + vals: list[str] = [] + if isinstance(payload, dict): + for value in payload.values(): + if isinstance(value, list): + for item in value: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + elif isinstance(value, str): + vals.append(value) + elif isinstance(payload, list): + for item in payload: + if isinstance(item, str): + vals.append(item) + elif isinstance(item, dict): + vals.extend(str(item.get(k)) for k in ("name", "path", "doc_name") if item.get(k)) + return [v for v in vals if v] + + def _read_existing_doc_names() -> list[str]: + preferred = [ + {"path": "evidence_indexed_parts", "pattern": "E-*.json"}, + {"path": "evidence_event_candidate_parts", "pattern": "E-*.json"}, + ] + preferred_errors = [] + preferred_names: list[str] = [] + for args in preferred: + try: + folder = str(args["path"]).rstrip("/") + preferred_names.extend(f"{folder}/{name.split('/')[-1]}" for name in _list_doc_names(args)) + except Exception as exc: + preferred_errors.append(f"{args['path']}: {exc}") + if not preferred_errors: + return sorted(set(preferred_names)) + + fallback_errors = [] + for args in ({}, {"doc_names": []}): + try: + return sorted(set(preferred_names + _list_doc_names(args))) + except Exception as exc: + fallback_errors.append(f"{args}: {exc}") + raise RuntimeError("list_docs failed for preferred schemas and fallback schemas; stale part detection cannot safely continue: " + " | ".join(preferred_errors + fallback_errors)) + + def _stale_parts(existing_names: list[str], n: int, folder: str) -> list[str]: + stale: list[str] = [] + pattern = re.compile(rf"(?:^|/){re.escape(folder)}/E-(\d{{3}})\.json$") + for name in existing_names: + m = pattern.search(name) + if not m: + continue + ordinal = int(m.group(1)) + if ordinal < 1 or ordinal > n: + stale.append(name) + return sorted(set(stale)) + + def main(): + _init() + evidence_all = read_json("evidence_all.json") + docs = _items(evidence_all) + n = len(docs) + if n <= 0: + raise RuntimeError("evidence_all.json contains no evidence documents") + + existing_names = _read_existing_doc_names() + stale_b1 = _stale_parts(existing_names, n, "evidence_indexed_parts") + stale_b2 = _stale_parts(existing_names, n, "evidence_event_candidate_parts") + + shards = [] + streaming_edges = [] + b1_waits = [] + b2_waits = [] + b1_files = [] + b2_files = [] + for idx, doc in enumerate(docs, 1): + label = f"{idx:03d}" + evidence_id = f"E-{label}" + b1_task_id = f"Task_B1_map_doc_{label}" + b2_task_id = f"Task_B2_map_events_{label}" + b1_output = f"evidence_indexed_parts/{evidence_id}.json" + b2_output = f"evidence_event_candidate_parts/{evidence_id}.json" + # 문서별 shard 파일 — B1/B2 인스턴스가 evidence_all.json 풀 대신 자기 shard만 읽도록. + shard_doc_path = f"evidence_shards/{evidence_id}.json" + write_doc(shard_doc_path, json.dumps(doc, ensure_ascii=False, indent=2)) + b1_waits.append(b1_task_id) + b2_waits.append(b2_task_id) + b1_files.append(b1_output) + b2_files.append(b2_output) + shards.append({ + "assigned_ordinal": idx, + "ordinal_label": label, + "evidence_index_candidate": evidence_id, + "b1_task_id": b1_task_id, + "b2_task_id": b2_task_id, + "b1_output_path": b1_output, + "b2_output_path": b2_output, + "shard_doc_path": shard_doc_path, + "source_document_ref": { + "source": "evidence_all.json", + "source_selector": "$.items || $", + "ordinal": idx, + "array_index": idx - 1, + "doc_uid_candidate": _doc_uid(doc, idx), + "title_candidate": _doc_title(doc, idx), + "content_sha256": _stable_hash(doc), + "shard_doc_path": shard_doc_path + }, + "runtime_bindings": { + "__assigned_ordinal__": idx, + "__ordinal_label__": label, + "__evidence_index_candidate__": evidence_id, + "__b1_part_path__": b1_output, + "__b2_part_path__": b2_output, + "__shard_doc_path__": shard_doc_path + } + }) + streaming_edges.append({ + "from": b1_task_id, + "to": b2_task_id, + "condition": {"file_exists": b1_output}, + "bind": {"__assigned_ordinal__": idx} + }) + + plan = { + "schema_version": "evidence_shard_plan.v1", + "planner_task": "Task_Evidence_shard_planner", + "created_at_utc": datetime.now(timezone.utc).isoformat(), + "source_file": "evidence_all.json", + "source_selector": "$.items || $", + "shard_count": n, + "expected_ordinals": [f"{i:03d}" for i in range(1, n + 1)], + "expected_evidence_ids": [f"E-{i:03d}" for i in range(1, n + 1)], + "ordinal_policy": { + "base": 1, + "assignment": "array_order", + "format": "%03d", + "assigned_ordinal_var": "__assigned_ordinal__", + "evidence_index_candidate": "E-{ordinal:03d}", + "no_legal_sorting": True, + "no_content_based_reordering": True + }, + "shards": shards, + # 백엔드 wildcard fan-out: dynamic_fanout 리스트의 각 항목이 한 인스턴스 컨텍스트({{item.*}}로 접근). + # shards를 그대로 사용 — 디스크립터 메타(id/count/task_template 등)는 fanout_meta로 분리 보존. + "dynamic_fanout": shards, + "fanout_meta": [ + { + "id": "B1_evidence_doc_fanout", + "count": n, + "task_template": "Task_B1_map_doc", + "instance_id_template": "Task_B1_map_doc_{ordinal:03d}", + "output_path_template": "evidence_indexed_parts/E-{ordinal:03d}.json" + }, + { + "id": "B2_event_candidate_fanout", + "count": n, + "task_template": "Task_B2_map_events", + "instance_id_template": "Task_B2_map_events_{ordinal:03d}", + "output_path_template": "evidence_event_candidate_parts/E-{ordinal:03d}.json", + "wait_until_template": "Task_B1_map_doc_{ordinal:03d}" + } + ], + "streaming_edges": streaming_edges, + "barriers": { + "B1_all_parts_barrier": { + "wait_for_tasks": b1_waits, + "require_files": b1_files, + "next": "Task_B1_gate_merge_deterministic" + }, + "B2_all_parts_barrier": { + "wait_for_tasks": b2_waits, + "require_files": b2_files, + "plus": ["Task_B1_gate_merge_deterministic"], + "next": "Task_B2_gate_merge_deterministic" + } + }, + "stale_part_policy": { + "mode": "block", + "allowed_ordinals": [f"{i:03d}" for i in range(1, n + 1)], + "checked_directories": ["evidence_indexed_parts", "evidence_event_candidate_parts"], + "detected_stale_parts": { + "evidence_indexed_parts": stale_b1, + "evidence_event_candidate_parts": stale_b2 + }, + "downstream_gate_instruction": "B1/B2 quality gates must BLOCK if detected_stale_parts is non-empty or if files outside expected_ordinals are present." + }, + "non_legal_assertions": { + "does_not_classify_document_type": True, + "does_not_extract_facts": True, + "does_not_evaluate_evidence": True, + "does_not_calculate_amounts": True, + "does_not_decide_legal_effect": True, + "does_not_finalize_evidence_index": True, + "does_not_finalize_candidate_id": True + } + } + + if stale_b1 or stale_b2: + plan["planner_status"] = "BLOCKED_BY_STALE_PARTS" + plan["mapper_execution_allowed"] = False + plan["blocked_reason"] = "stale mapper part detected; B1/B2 mapper fanout must not execute and existing parts must not be reused" + else: + plan["planner_status"] = "READY" + plan["mapper_execution_allowed"] = True + plan["blocked_reason"] = None + + write_doc("evidence_shard_plan.json", json.dumps(plan, ensure_ascii=False, indent=2)) + # 백엔드 wildcard fan-out 확장이 stdout JSON의 dynamic_fanout 키를 읽음. + # 전체 plan은 너무 커서 dynamic_fanout만 출력한다. 단 planner_status가 READY가 아니면 mapper fanout은 반드시 비운다. + fanout_payload = plan["dynamic_fanout"] if plan["planner_status"] == "READY" else [] + print(json.dumps({"dynamic_fanout": fanout_payload, "planner_status": plan["planner_status"], "mapper_execution_allowed": plan["mapper_execution_allowed"]}, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B1_map_doc_* + max_concurrency: 12 + preflight_files: + - "{{item.shard_doc_path}}" + - client_meeting.md + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + + Task_B1_map_doc + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. Your role in this task is a **per-document mapper** within the v.3 Step D streaming map-reduce pipeline. + + + 본 task는 v.3 Step D의 B1 mapper다. orchestrator 가 runtime parameter (`{{item.*}}`) 로 주입한 assigned_ordinal 단일 증거문서만 처리하여, 그 문서의 authority component·document component envelope·legal component envelope을 추출한 part 파일을 작성한다. + 본 task는 증거 문서 수 N을 계산하지 않고, ordinal을 배정하지 않고, B1/B2 mapper 인스턴스를 생성하지 않는다. 이러한 orchestration 책임은 전부 `Task_Evidence_shard_planner`와 orchestrator의 권한이다. + 최종 `evidence_indexed.json`은 본 task가 작성하지 않는다. 본 task는 `evidence_indexed_parts/E-{ordinal:03d}.json` part 파일만 작성하며, finalize는 `Task_B1_gate_merge_deterministic`가 수행한다. + 본 task는 자기 문서 안에서 직접 확인되는 component만 작성한다. 다른 문서의 사실을 자기 part에 주입하면 quality gate의 authority drift check에서 BLOCK 또는 HARD_WARNING으로 검출된다. + `evidence_index`는 planner가 부여한 `E-{ordinal:03d}` 형식의 deterministic candidate를 그대로 propose 하되, final 부여는 quality gate가 수행한다. 본 task는 candidate id를 자기 part의 `evidence_index_proposed` 필드에 기록한다. + 본 task의 추출 깊이는 v.2 원본 Task_B1과 동일하다. document component envelope의 모든 하위 구조(claim_document_components, business_transfer_components, registry_row_components, secured_debt_components, valuation_rows, mixed_contract_payment_semantics, loan_components, valuation_matrix, registry_row_role_map, defense_assertions, admission_components, defense_components, notice_components, party_assertion_components, schedule_items, registry_object_display, succession_document_components)를 자기 문서 범위 안에서 가능한 한 분리 추출한다. + 변호사 관점에서 본 단계의 핵심은 등기부 row, 차용증 component, 통지서 발송/도달일 분리, 가족관계·상속포기 component, 별지목록 등의 직접 보존이다. 이는 청구원인 timeline·청구취지 별지목록·금액 계산의 기초가 된다. + + + + + + - runtime parameters: `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.source_document_ref}}`, `{{item.b1_output_path}}` (planner 가 부여한 단일 source of truth — 별도 파일 read 불요) + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 문서 1건 — planner가 evidence_all.json에서 분리해 둔 단일 shard 파일) + - `client_meeting.md` (보조 사용만 허용) + + + - `evidence_indexed_parts/E-{ordinal:03d}.json` (본 mapper 인스턴스의 part 파일 1개만) + + + - 다른 ordinal의 문서를 처리하기 + - 다른 ordinal의 사실을 자기 part에 주입하기 (authority drift) + - 증거 문서 수 N을 재계산하여 planner 결과를 대체하기 + - assigned_ordinal을 새로 생성하거나 재정렬하기 + - B1/B2 mapper fan-out 또는 streaming edge를 설계하기 + - 최종 `evidence_indexed.json` 작성 (이는 quality gate의 단독 권한) + - 다른 mapper의 part 파일 읽기/덮어쓰기 + - root 전체 탐색 + - 이전 task 결과 또는 backend 자동 로드 데이터 사용 + - 자기 part 재읽기 + - cross-document legal conclusion 확정 + - 입력에 없는 사실 추가 + + + + + 1. `list_docs`로 `{{item.shard_doc_path}}`, `client_meeting.md` 존재만 확인한다. + 2. `read_docs`로 위 두 파일을 읽는다. `{{item.shard_doc_path}}`는 이 인스턴스의 단일 evidence 문서로 그대로 사용한다 (별도의 ordinal 추출 불필요). assigned_ordinal·evidence_index_candidate·b1_output_path 는 runtime parameter (`{{item.*}}`) 그대로 사용한다. + 3. 메모리에서 part item 객체를 생성한다(items 배열이 아닌 단일 item object 또는 1-element items 형태). + 4. `write_file(overwrite=true)`로 runtime parameter 가 지정한 `evidence_indexed_parts/E-{ordinal:03d}.json`을 저장한다. + 5. `list_docs`로 자기 part 파일 존재만 확인한다. 절대 읽지 않는다. + 6. 정확히 `"E-{ordinal:03d} mapper part 작성 완료"`만 출력하고 종료한다. + + + + - 입력 누락, 빈 파일, runtime parameter 누락, ordinal 매핑 실패, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. part 파일은 쓰지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + - tool로 읽은 내용만 사실로 취급한다. + + + + - 본 mapper 인스턴스는 정확히 runtime parameter (`{{item.assigned_ordinal}}`) 에 해당하는 1개 문서만 처리한다. + - runtime parameters (`{{item.*}}`) 는 ordinal/binding 의 단일 source of truth 다 (별도 plan 파일 read 불요). + - `evidence_all.json` 배열 순서를 절대 바꾸지 않고, 배열 순서를 근거로 새 plan을 만들지도 않는다. + - planner의 `source_document_ref.content_sha256` 또는 `doc_uid_candidate`가 현재 문서와 명백히 충돌하면 part 파일을 쓰지 않는다. + - 본 task는 planner가 확정한 `evidence_index_candidate`, `b1_output_path`, assigned_ordinal을 소비할 뿐 재작성하지 않는다. + - 다른 ordinal의 문서 내용을 현재 문서에 보충하지 않는다(authority drift 금지). + - unknown 처리: + - nullable scalar → `null` + - 배열 → `[]` + - 비nullable string만 `"불명"` 사용 + - 배열은 exact-match 중복 제거 후 최초 순서를 유지한다. + - part 파일은 단일 item object 형태이며, 기존 필수 필드(`evidence_index_proposed`, `doc_uid`, `title`, `title_normalized`, `doc_type`, `key_*`, `source_pointer`)를 모두 포함한다. + - optional additive field는 downstream(quality gate)이 무시해도 안전해야 한다. + - `evidence_index_proposed`는 deterministic 규칙 `E-{ordinal:03d}`로만 생성한다. 임의 변형 금지. + + + + 이 인스턴스의 `{{item.shard_doc_path}}`(planner가 evidence_all.json에서 분리한 단일 evidence 1건) + `client_meeting.md` (보조만 허용) + + + `client_meeting.md`는 아래 목적에만 사용할 수 있다. + - `asset_id` + - `property_label_for_schedule` + - `transaction_type` + - `transaction_date` + - `market_value_at_act` + - `market_value_at_close` + - `consideration_breakdown` + - `property_cluster_id` + - 현재 문서의 축약 명칭이 어떤 자산군을 가리키는지에 관한 shorthand 해석 + + + + - 현재 문서와 `client_meeting.md`가 충돌하면 현재 문서가 우선한다. + - 예외: 등기부상 형식적 원인이 `매매`여도, 현재 문서 특약 또는 `client_meeting.md`가 실질적으로 채무갈음/대물변제를 직접 드러내면 `transaction_type`은 `"대물변제"`로 할 수 있다. + - 단, 이 예외는 현재 문서 또는 meeting이 실질 원인을 직접적으로 드러내는 경우에만 적용한다. + + + + + - `ordinal` = `evidence_all.json`의 1-based 배열 위치 (assigned_ordinal과 동일) + - `evidence_index_proposed` = `E-{ordinal:03d}` (mapper propose; quality gate가 finalize) + - `title` = 현재 문서 제목 원문 그대로 + - `title_normalized` = 앞뒤 공백 제거 + 내부 연속 공백/줄바꿈을 1칸으로 축약 + - `doc_uid` = `DOC-{ordinal:03d}-{title_normalized의 공백을 "_"로 치환한 값}` + - `source_pointer` = `{"source":"evidence_all.json","ordinal":ordinal}` + + + + 다음 순서로만 핵심을 읽는다. + 1. `title` + 2. `heading` + 3. `key_value` / `form` + 4. 관련 `table` 행 + 5. `list` + 6. `paragraph` + + 보일러플레이트(바코드 안내, 발급확인 문구, 반복 footer/page text)는 무시한다. + 단, `관할등기소`, `접수번호`, `등기원인일`, `발행일` 등 필요한 사실이 있으면 사용한다. + + + + `doc_type` enum은 v.2 원본과 동일하다. nuance는 `doc_semantic_flags`로 보강한다. + 1. `판결|결정|배당표|가압류|가처분|경매|타경` → `"판결/결정"` + 2. `등기사항전부증명서|주민등록|가족관계증명서|사업자등록증` → `"공문서"` + 3. `대출|명세|거래|계산서` → `"거래기록"` + 4. `카톡|문자|이메일|통화` → `"통신기록"` + 5. `계약서|약정서|확인서|영수증|차용증|각서` → `"처분문서"` + 6. 그 외 → `"기타"` + + + + - `key_facts`: 최대 3개, 각 60자 이하 + - `key_dates`: 최대 3개 + - `key_amounts`: 최대 3개 + - `key_parties`: 최대 5개 + - 현재 문서에서 직접 읽히는 핵심만 남긴다. + - 장문 요약, 계산식, 추정 보정 금지. + - 가능하면 heading / key_value / 표의 핵심 행만 사용한다. + + + + + - `property_cluster_id`는 optional field다. + - 현재 문서만으로 특정 자산군이 명백하거나, 현재 문서의 표기와 `client_meeting.md`가 정확히 대응할 때만 채운다. + - 예: `"해당 대지/건물"`, `"해당 상가"`, `"해당 주거용 부동산"`, `"해당 임야"`, `"해당 영업양도"`, `"해당 물품대금"`. + - 명백하지 않으면 `null`. + + + + `doc_semantic_flags`는 optional field다. 아래 값만 사용한다. + - `multi_event_doc` + - `registry_doc` + - `mixed_contract_doc` + - `valuation_doc` + - `claim_doc` + - `status_doc` + - `notice_doc` + - `response_doc` + - `business_transfer_doc` + - `supporting_reference_doc` + - `admission_doc` + - `defense_doc` + - `notice_receipt_admission_doc` + - `lien_assertion_doc` + - `schedule_doc` + - `relief_object_reference_doc` + - `succession_doc` + + 부여 규칙: + - 등기부처럼 복수 row가 서로 다른 법적 효과를 가지면 `multi_event_doc` + - 갑구/을구 등기행위가 있으면 `registry_doc` + - 임대차와 매매 등 서로 다른 계약 구성요소가 같은 문서에 공존하면 `mixed_contract_doc` + - 시가, 차임시세, 사용이익 가치가 표로 제시되면 `valuation_doc` + - 차용증, 보증서, 대여약정 등 채권 발생 문서면 `claim_doc` + - 답변서, 사업자등록증, 현재 상태 확인서처럼 상태를 보여주면 `status_doc` + - 내용증명, 통지서, 최고서, 해제통지서면 `notice_doc` + - 상대방 답신, 답변서, 반박문이면 `response_doc` + - 영업양도 문서면 `business_transfer_doc` + - 별지 목록처럼 단독 계산객체는 없으나 다른 문서 식별을 돕는 문서면 `supporting_reference_doc` + - 상대방이 통지 수령, 임대, 차임, 점유, 채무, 변제 등을 인정하면 `admission_doc` + - 명의신탁, 충분담보, 상계, 선의, 후소유자 소멸청구권 부정, 피담보채권 충당 등 항변이 있으면 `defense_doc` + - 유치권 소멸청구 통지의 수령일을 인정하면 `notice_receipt_admission_doc` + - 유치권 주장, 공사잔금, 유치권 목적물 사용·임대가 있으면 `lien_assertion_doc` + - 별지목록, 목적물 목록, 청구취지에서 그대로 사용할 문구가 있으면 `schedule_doc` 및 `relief_object_reference_doc` + - 가족관계증명서, 상속포기 심판문, 사망·상속 관련 공문서이면 `succession_doc` + + + + - 등기부 문서에서 갑구/을구 여러 row가 서로 다른 법적 효과를 가지면 `multi_event_registry=true`를 optional field로 기록한다. + - `multi_event_registry=true`인 문서는 단일 snapshot 요약을 강요하지 않는다. + + + + + - 문서가 여러 법률효과를 담고 있으면 component 단위로 분해한다. + - 날짜는 작성일, 발송일, 도달일, 법률효과 발생일, 변제기, 기산일 후보로 의미별 분리한다. + - 금액은 원금, 이자, 보증금, 차임, 사용이익, 채권최고액, 실제 변제액, 가액배상 공제액으로 성격별 분리한다. + - 아래 component field들은 optional additive field이며, 현재 문서에 직접 읽히는 범위에서만 사용한다. + + + - 문서가 이행각서, 채무승인서, 최고서, 내용증명, 지급요구서, 변제약정서, 차용증, 납품대금 확인서이면 `claim_document_components`를 생성한다. + - 단순 `key_dates`에 문서작성일만 넣고 끝내지 말고, 문서 안에 지급기일, 새 변제기, 최고 도달일, 채무승인일, 청구금액, 채권자, 채무자가 직접 읽히면 component field로 분해한다. + - 이행각서에서 "언제까지 지급한다"는 문구가 있으면 반드시 `new_due_date` 후보로 분리한다. + - 최고서에서 발송일과 수령일/도달일이 모두 있으면 `dispatch_date`와 `arrival_date`를 분리한다. + - claim component의 `component_kind` 허용값은 `underlying_claim|debt_acknowledgement|demand_notice|notice_dispatch|notice_arrival|new_due_date|payment_promise|limitation_relevance|other`다. + + + + - 영업양도계약서, 상호 계속 사용, 사업 양수인, 영업자산 이전, 고객·직원 승계, 채무불승계 조항, 책임배제 등기 단서가 있으면 `business_transfer_components`를 생성한다. + - `business_name`, `transferor_candidates`, `transferee_candidates`, `business_assets_transferred`, `trade_name_continuation_clause`, `customer_or_employee_succession_clause`, `pre_transfer_debt_exclusion_clause`, `liability_exclusion_registration_clause`를 직접 읽히는 범위에서 채운다. + - 기존 `contract_components`가 있어도 영업양도 책임 검토용 구조는 `business_transfer_components`에 별도 보존한다. + + + + - 등기부, 등기사항전부증명서, 등기내역 문서는 반드시 row 단위로 쪼갠다. + - 각 row마다 갑구/을구, 순위번호, 등기목적, 접수일, 접수번호, 원인, 권리자, 채무자, 채권최고액, 객체 범위를 추출하여 `registry_row_components`에 둔다. + - 청구취지 특정에 필요한 접수번호는 `key_facts` 요약에만 두지 말고 `registry_row_components[*].receipt_no`로 보존한다. + - 차용증, 근저당권 설정계약, 등기부 row가 연결되면 `secured_debt_components`를 생성한다. + - 복수 채무의 원금, 이율, 변제기, 담보 지분이 다르면 법정변제충당 계산 대상이므로 모두 구조화한다. + - 사해행위취소 사건의 등기부 row는 필요하면 `registry_row_role_map`에 actio role을 추가로 남긴다. + - `registry_row_components`의 각 row에 `registry_ref`를 부여한다. 형식: `REG-{ordinal:03d}-{row_seq:02d}` (갑구/을구·순위번호 순 deterministic). + - row에 지분 표시(예: "2분의 1", "1/2 지분")가 직접 읽히면 `registry_row_components[*].fraction_display`(원문 문자열)와 `registry_row_components[*].fraction_ref`를 채운다. + - `fraction_ref`는 `FR-{ordinal:03d}-{row_seq:02d}` 형식의 deterministic propose 값이다. 같은 row에서 파생되는 모든 component는 같은 `fraction_ref`를 공유한다. + - 지분 표시의 분자·분모가 직접 읽히는 경우에만 optional 필드 `fraction_numerator`/`fraction_denominator`를 채운다(예: "2분의 1" → 1/2). 환산·약분·보간 금지. 직접 읽히지 않으면 null. + + + + - 감정표는 금액만 추출하지 말고 `valuation_rows`에 row별 전제조건, 금액, 단위, 법률상 사용 가능성을 함께 추출한다. + - 사해행위취소 valuation 문서가 복수 자산 또는 복수 시점을 담으면 `valuation_matrix`를 생성하고, 각 row의 `legal_uses`를 표시한다. + - 임대차보증금, 월 차임, 매매대금이 한 문서에 같이 있으면 `mixed_contract_payment_semantics`에서 각각 별도 payment semantic으로 분리한다. + - 보증금은 원칙적으로 반환 예정 담보성 금원인지 검토하고, 원고가 직접 수령하지 않았다면 원고의 부당이득 원금으로 자동 전환하지 않는다. + - 월 차임은 피고들 사이의 계약상 차임과 객관적 토지 사용이익을 구분한다. + + + + - 사해행위취소 사건의 증거문서는 일반 key_facts 요약만으로 충분하지 않다. + - 차용증에 복수 대여금이 있으면 `loan_components`로 분리하고, 각 원금·변제기·이자율·담보 목적물·청구 내 역할을 추출한다. + - 감정평가서는 시점별 `valuation_matrix`로 보존하고, 각 row의 legal use를 표시한다. + - 등기부는 갑구/을구 row별로 쪼개고, 각 row의 `actio_roles`를 부여한다. + - 내용증명 답신에 명의신탁, 충분담보, 상계, 선의 주장이 있으면 `defense_assertions`로 추출한다. + - 사후 말소된 담보권과 그 말소를 유발한 변제액은 가액배상 계산의 필수 입력으로 표시한다. + + + + - `loan_components`의 각 항목은 아래 named 필드를 가진다. 직접 읽히지 않는 필드는 null. + { + "loan_component_id": "E-{ordinal:03d}-LC-{seq:02d}", + "principal_amount": null, + "loan_date": null, + "due_date": null, + "agreed_interest_rate": null, + "agreed_delay_damage_rate": null, + "rate_text": null, + "rate_source_locator": null, + "interest_accrual_start_hint": null, + "collateral_object_hint": null, + "role_in_claim_hint": null + } + - `agreed_interest_rate`는 원문 그대로의 문자열("연 5%", "월 1%" 등)만 허용한다. + - `agreed_delay_damage_rate`는 약정 지연손해금율이다. 없으면 null로 둔다(법정이율로 보충 금지). + - `rate_text`는 이율 관련 원문 문자열 그대로, `rate_source_locator`는 그 이율이 읽힌 조항 locator(예: "제2조", "특약")다. + - `agreed_interest_rate`/`agreed_delay_damage_rate`는 계산·환산·보간 금지. 원문 문자열만 허용. + - `secured_debt_components`의 각 항목에도 동일한 optional rate 필드(`agreed_interest_rate`, `agreed_delay_damage_rate`, `rate_text`, `rate_source_locator`, `due_date`)를 직접 읽히는 범위에서 둘 수 있다(근저당권 설정계약서·차용증 겸용 문서의 이율 보존용). + - 이율이 문서에 있는데 두 rate 필드가 모두 null이면 self-warning으로 본다. + + + + - 답변서에 통지 수령 인정, 제3자 임대 자인, 월 차임 자인, 유치권자 사용 허용 주장, 후소유자 소멸청구권 부정 주장, 피담보채권 충당 주장이 있으면 `admission_components`와 `defense_components`로 분리한다. + - 통지서에는 `notice_components`를 생성하고, 통지 유형, 발송일, 도달 필요 여부, 수령자, 목적물, 법률효과 후보를 분리한다. + - `notice_components`의 각 항목은 아래 named 필드를 가진다. 직접 읽히지 않는 필드는 null. + { + "notice_id_proposed": "E-{ordinal:03d}-NT-{seq:02d}", + "notice_type": "최고|해제통지|유치권소멸청구통지|기타", + "drafted_date": null, + "dispatch_date": null, + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [], + "receiver_candidates": [], + "target_object_hint": null, + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "legal_effect_candidate_hint": null + } + - 5개 lifecycle 날짜의 의미: `drafted_date`=작성일, `dispatch_date`=발송일, `arrival_date`=도달일(민법 제111조 도달주의 — 도달≠수령을 구분), `received_date`=수령일(수령 인정 포함; 도달일과 다르면 별도 보존), `legal_effect_date_candidate`=법률효과 발생 후보일(계산 금지, 문서 직접 기재만). + - `requires_receipt_for_effect`는 true|false|null — 문서 유형상 도달/수령이 효력요건인지의 후보 표시다. + - `receipt_evidence_refs`는 수령을 뒷받침하는 같은 문서 내 단서 locator다. + - `recipient_admission_text`는 통지 수령을 인정하는 원문 문자열이며, 답변서 문서의 component에서만 채운다. + - 수령일이 상대방 답변서에서 인정되는 경우 그 답변서의 `admission_components`에 남기고, 본 통지서 문서의 `received_date`에 타 문서 사실을 주입하지 않는다(authority drift 금지). cross-document 연결은 Part 2 B5가 수행한다. + - 당사자가 요구한 기간이나 금액이 법률상 기산일로 자동 사용되면 안 되는 경우 `party_assertion_components.should_not_auto_use_as_legal_start_date=true`를 남긴다. + - 별지목록에는 `schedule_items`를 생성하고, 청구취지에서 그대로 사용할 수 있는 문구와 asset alias, 전유부분, 대지권, 도로명주소를 보존한다. + - `schedule_items`의 각 항목에 `schedule_ref`를 부여한다. 형식: `SCH-{ordinal:03d}-{seq:02d}` (별지 기재 순서 deterministic). asset alias 문자열은 기존대로 함께 보존한다. + - `schedule_items`의 각 항목에 optional 필드 `canonical_label_candidate`(별지 표제 문구 원문)와 `usable_in_relief`("yes|no|unknown" — 청구취지에서 그대로 사용할 수 있는 문구인지)를 직접 읽히는 범위에서 둘 수 있다. + - 등기부가 목적물 특정 자료로 쓰이면 `registry_object_display`를 생성한다. + - 가족관계증명서, 상속포기 심판문, 사망·상속 자료에는 `succession_document_components`를 생성하고 피상속인, 사망일, 배우자 후보, 직계비속 후보, 직계존속 후보, 상속포기자, 신고일·수리일, 연결 asset을 보존한다. + + + + + + - 기존 optional field `legal_calculation_object`는 backward-compatible summary field로 유지한다. + - 그러나 이 필드는 안전할 때만 사용한다. + - 문서가 `multi_event_doc` 또는 `multi_event_registry=true`이면 원칙적으로 `legal_calculation_object`를 생성하지 않는다. + - 문서에 서로 다른 법적 효과의 slot이 2개 이상 공존하면 단일 summary `legal_calculation_object`로 압축하지 않는다. + - 단일 효과가 명백한 문서, 또는 서로 다른 값이 경쟁하지 않는 문서에서만 `legal_calculation_object`를 생성한다. + + + + - multi-event 또는 mixed-contract 문서를 위해 optional field `legal_calculation_objects`를 추가할 수 있다. + - `legal_calculation_objects`는 현재 문서 안의 개별 slot을 권위 있게 분해한 배열이다. + - 문서당 0개 이상 허용한다. + - 단일 slot 문서라면 `legal_calculation_objects`를 생략할 수 있다. + - 각 slot은 현재 문서의 직접 기재만 사용한다. 계산, 합산, 차감 금지. + + + + 개별 slot은 아래 경우에 생성한다. + - 소유권이전 / 매매 / 증여 / 대물변제 / 경매매각 / 경매개시결정 / 근저당권설정 / 근저당권말소 / 가압류 / 임대차 / 영업양도 / 대여 / 담보제공 + - valuation 문서는 시가표/차임표/사용이익표를 slot이 아니라 `valuation_object`로 처리한다. + - 등기부는 갑구/을구의 의미 있는 row마다 1 slot씩 생성한다. + - 혼합계약서는 계약 조항상 독립된 법적 효과마다 1 slot씩 생성한다. + + 각 slot은 아래 구조를 따른다. + { + "slot_id": "E-{ordinal:03d}-LCO-01", + "slot_kind": "ownership_transfer|gift|sale|dation|mortgage_setting|mortgage_cancellation|auction_commencement|auction_sale|lease|building_sale|loan|business_transfer|other", + "source_locator": { + "locator_type": "heading|key_value|table_row|paragraph|unknown", + "locator_hint": "갑구4|을구1|제1조|특약 등", + "excerpt": "짧은 단서" + }, + "asset_id": null, + "property_label_for_schedule": null, + "transaction_type": "매매|증여|대물변제|null", + "transaction_date": null, + "registration_date": null, + "registry_office": null, + "registry_receipt_no": null, + "registry_recorded_transfer_date": null, + "market_value_at_act": null, + "market_value_at_close": null, + "sale_price": null, + "monthly_rent": null, + "consideration_breakdown": [], + "parties": [] + } + - slot이 등기부 row 또는 별지 항목에서 직접 파생되면 optional 필드 `registry_ref_hint`/`schedule_ref_hint`를 남긴다. + + + + - 임대차와 매매가 함께 있는 혼합계약서, 영업양도와 채무불인수 조항이 함께 있는 문서 등은 optional field `contract_components`를 추가할 수 있다. + - `contract_components`는 현재 문서의 조항별 법적 효과 분해 메모다. + - 각 component는 아래 구조를 따른다. + { + "component_id": "E-{ordinal:03d}-CC-01", + "component_kind": "lease|sale|delivery|management_use_shift|permit_cooperation|business_transfer|debt_non_assumption|other", + "object_spec": "짧은 목적물", + "amount": null, + "date": null, + "payer_candidates": [], + "recipient_candidates": [], + "possession_recipient_candidates": [], + "benefit_shift_to_candidates": [], + "source_locator": { + "locator_type": "paragraph|table_row|key_value|unknown", + "locator_hint": "제1조|제2조|특약 등", + "excerpt": "짧은 단서" + } + } + - 혼합계약서에서 component가 2개 이상 명백하면 2개 이상 생성한다. + - mixed contract에 임대차, 매매, 인도, 관리·사용 귀속이 직접 적혀 있으면 이를 lease/sale 1개로 뭉개지지 말고 가능한 한 분리한다. + - 조항에서 누가 대가를 지급하고 누가 수령했는지가 직접 읽히면 `payer_candidates`, `recipient_candidates`를 채운다. + - 인도받는 자가 직접 읽히면 `possession_recipient_candidates`를 채운다. + - 관리·사용 또는 사용수익 귀속 주체가 직접 읽히면 `benefit_shift_to_candidates`를 채운다. + - 위 directionality field는 현재 문서에 직접 적힌 경우에만 채우고, 추정으로 보충하지 않는다. + + + + - 시가, 차임시세, 사용이익 평가 문서는 optional field `valuation_object`를 추가할 수 있다. + - `valuation_object`는 아래 구조를 따른다. + { + "valuation_kind": "market_value|rent_value|use_gain_value", + "target_object": "짧은 목적물", + "basis_date_or_period": "원문 기간 또는 기준일", + "value_rows": [ + { + "label": "2022.1.1.~2022.12.31.|보증금 3억 원인 경우 등", + "amount": "금액 문자열", + "unit": "총액|월액|기타" + } + ] + } + - valuation 문서는 기존 `doc_type`를 바꾸지 않고 `doc_semantic_flags`와 `valuation_object`로 구체화한다. + - 차임표/사용이익표가 있으면 `market_value_*`를 억지로 채우지 않는다. + + + + + - 날짜: 정확한 일자가 있으면 `YYYY-MM-DD`, 없으면 `null` + - 단, `key_dates`와 `valuation_object.basis_date_or_period`는 짧은 원문 텍스트 허용 + - 금액: 입력에 명시된 값만 `"300,000,000원"` 형식 문자열로 정규화 + - 이름/당사자: 원문 우선, 명백히 동일할 때만 canonicalize + - `consideration_breakdown`는 직접 읽히는 짧은 대가 항목만 저장, 최대 4개, 각 40자 이하 + + + + part 파일은 단일 item object 형태이며, top-level에 mapper가 propose하는 모든 component field를 둔다. + + { + "evidence_index_proposed": "E-{ordinal:03d}", + "doc_uid": "DOC-{ordinal:03d}-...", + "title": "...", + "title_normalized": "...", + "doc_type": "판결/결정|공문서|거래기록|통신기록|처분문서|기타", + "key_facts": [], + "key_dates": [], + "key_amounts": [], + "key_parties": [], + "source_pointer": { + "source": "evidence_all.json", + "ordinal": ordinal + }, + "property_cluster_id": null, + "doc_semantic_flags": [], + "multi_event_registry": null, + "contract_components": [], + "valuation_object": null, + "legal_calculation_object": null, + "legal_calculation_objects": [], + "claim_document_components": [], + "business_transfer_components": [], + "registry_row_components": [], + "secured_debt_components": [], + "valuation_rows": [], + "mixed_contract_payment_semantics": {}, + "loan_components": [], + "valuation_matrix": {}, + "registry_row_role_map": {}, + "defense_assertions": [], + "admission_components": [], + "defense_components": [], + "notice_components": [], + "party_assertion_components": [], + "schedule_items": [], + "registry_object_display": {}, + "succession_document_components": null, + "mapper_meta": { + "mapper_task": "Task_B1_map_doc", + "schema_contract_version": "evidence_indexed_part.v4", + "self_isolation_attested": true + } + } + + 추가 제약: + - top-level은 단일 object다. envelope이 아닌 단일 item. + - `evidence_index_proposed`는 mapper의 propose 식별자이며, finalize는 quality gate가 한다. + - `mapper_meta.self_isolation_attested=true`는 본 mapper가 자기 문서 외 사실을 주입하지 않았음을 self-attest 한다는 의미다. + - optional component field는 직접 읽히는 범위에서만 채우고, 빈 배열/객체/`null`로 두는 것을 우선한다. + + + + - part 파일이 단일 object이고 `evidence_index_proposed`, `doc_uid`, `title`, `title_normalized`, `doc_type`, `source_pointer`를 모두 포함하는지 점검한다. + - `planner_status=="READY"` 및 `mapper_execution_allowed=true`가 확인되지 않았으면 part 파일이 작성되지 않았는지 점검한다. + - `evidence_index_proposed`가 정확히 `E-{ordinal:03d}` 형식인지 점검한다. + - 자기 문서 외 ordinal의 사실이 part에 주입되지 않았는지 self-check 한다(authority drift 방지). + - `registry_doc`이면서 서로 다른 법적 효과 row가 2개 이상 읽히면 `doc_semantic_flags`에 `multi_event_doc`를 넣고, 가능한 경우 `legal_calculation_objects`를 생성한다. + - `multi_event_registry=true` 또는 `doc_semantic_flags`에 `multi_event_doc`가 있으면 단일 `legal_calculation_object`를 억지로 채우지 않는다. + - `mixed_contract_doc`인데 명백한 독립 component가 2개 이상이면 `contract_components`를 2개 이상 생성한다. + - `mixed_contract_doc`에 인도 또는 관리·사용 귀속이 직접 적혀 있으면 `delivery` 또는 `management_use_shift` component가 누락되지 않았는지 점검한다. + - 혼합계약 조항에서 지급자·수령자 또는 점유 이전 상대방이 직접 읽히면 `payer_candidates`, `recipient_candidates`, `possession_recipient_candidates`, `benefit_shift_to_candidates`가 비어 있지 않은지 점검한다. + - `valuation_doc`인데 가치표/차임표가 직접 보이면 `valuation_object`를 생성한다. + - 같은 문서에서 복수 행위가 보이는데도 단일 snapshot `legal_calculation_object`만으로 압축하지 않는다. + - `claim_doc`인데 이행각서ㆍ채무승인ㆍ최고ㆍ변제기ㆍ청구금액 단서가 직접 보이고 `claim_document_components`가 없으면 self-warning으로 본다. + - 이행각서에 새 변제기가 있는데 `new_due_date`가 없으면 self-warning으로 본다. + - 최고서 또는 통지서에 도달·수령 단서가 있는데 `arrival_date` 또는 notice receipt component가 없으면 self-warning으로 본다. + - `business_transfer_doc`에 상호ㆍ영업명ㆍ영업자산ㆍ채무불승계 단서가 있는데 `business_transfer_components`가 없으면 self-warning으로 본다. + - 등기부에 접수번호가 있는데 `registry_row_components[*].receipt_no`가 없으면 self-error로 본다. + - 차용금 문서에 이율 또는 변제기가 있는데 `secured_debt_components` 또는 `loan_components`가 없으면 self-warning으로 본다. + - `loan_components`가 있는데 이율 문구가 문서에 보이면서 `agreed_interest_rate`가 null이면 self-warning으로 본다. + - valuation 문서에 복수 시점 또는 복수 row가 있는데 `valuation_rows[*].legal_use_case` 또는 `valuation_matrix.rows[*].legal_uses`가 없으면 self-warning으로 본다. + - 임대차보증금이 추출되었는데 `mixed_contract_payment_semantics`에서 반환성 보증금과 사용이익 원금 배제 여부가 구분되지 않으면 self-warning으로 본다. + - 등기부 문서에 사후 말소 row가 있는데 `registry_row_role_map` 또는 `registry_row_components`에 release role이 없으면 self-error로 본다. + - 답변서에 수령 인정ㆍ제3자 임대ㆍ항변이 있는데 `admission_components` 또는 `defense_components`가 없으면 self-warning으로 본다. + - 별지목록에 목적물 항목이 있는데 `schedule_items`가 없으면 self-error로 본다. + - 상속자료가 있는데 `succession_document_components`가 없으면 self-warning으로 본다. + - self-warning/self-error는 mapper가 part 파일에 그대로 남겨도 되며, quality gate가 최종 severity를 판정한다. + + + + + {{item.assigned_ordinal}} + + + [ ] 1) Preflight: `list_docs` 사용해서 `{{item.shard_doc_path}}`, `client_meeting.md` 만 확인, `read_docs` 사용해서 두 파일만 읽기 + [ ] 2) runtime parameter `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.b1_output_path}}` 그대로 소비. 별도 검증 read 불요. + [ ] 3) `{{item.shard_doc_path}}`의 단일 evidence 문서를 메모리에서 사용하고, 주입된 shard 밖으로 나가지 않기 + [ ] 4) `write_file` 사용해서 `evidence_indexed_parts/E-{ordinal:03d}.json` 생성 (ordinal은 `{{item.assigned_ordinal}}`) + [ ] 5) `list_docs` 사용해서 자기 part 파일만 존재 확인(절대 읽기 금지) + [ ] 6) `"E-{ordinal:03d} mapper part 작성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 1건; planner가 evidence_all.json에서 분리) + - `client_meeting.md` + + + - `evidence_indexed_parts/E-{ordinal:03d}.json` + + + + + - `evidence_index_proposed`는 정확히 `E-{ordinal:03d}` 형식만 허용한다. + - `ordinal`과 output path는 runtime parameter (`{{item.*}}`) 를 그대로 따른다. + - 증거 문서 수 N, assigned_ordinal, mapper fan-out, streaming edge는 본 task에서 만들지 않는다. + - 자기 part 파일은 절대 재읽지 않는다. + - 다른 ordinal 문서를 처리하지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"E-{ordinal:03d} mapper part 작성 완료"` 한 줄만 사용한다. + + + + - task_name: Task_B2_map_events_* + max_concurrency: 12 + preflight_files: + - "{{item.shard_doc_path}}" + - "{{item.b1_output_path}}" + - client_meeting.md + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + + Task_B2_map_events + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. Your role in this task is a **per-document event/state candidate mapper** within the v.3 Step D streaming map-reduce pipeline. + + + 본 task는 v.3 Step D의 B2 mapper다. orchestrator 가 runtime parameter (`{{item.*}}`) 로 주입한 assigned_ordinal 단일 문서의 event/state candidate(raw event + legal-effect event 포함)를 추출하여 part 파일을 작성한다. + 본 mapper 인스턴스는 orchestrator가 같은 ordinal의 B1 mapper(Task_B1_map_doc) 완료와 `evidence_indexed_parts/E-{ordinal:03d}.json` 생성을 확인한 뒤 시작한다. 본 task는 streaming edge를 설계하지 않고, planner/orchestrator가 이미 결정한 same-ordinal edge를 소비한다. + 최종 `evidence_event_candidates.json`은 본 task가 작성하지 않는다. 본 task는 `evidence_event_candidate_parts/E-{ordinal:03d}.json` part 파일만 작성하며, finalize는 `Task_B2_gate_merge_deterministic`가 수행한다. + 본 task는 같은 ordinal의 B1 part 파일과 planner shard row를 authority metadata로 참조하고, planner가 분리해 둔 `{{item.shard_doc_path}}` 의 raw 문서 1건을 메모리에서 사용한다. + 같은 문서 내 복수 legal-effect event는 분리한다. 문서 밖 사실을 candidate payload에 넣지 않는다(authority drift 금지). + `candidate_id`는 `EVT-{ordinal:03d}-{seq:02d}` 형식의 deterministic candidate로 propose 하되, final 부여 및 collision 처리는 quality gate가 수행한다. + 변호사 관점에서 본 단계의 핵심은: (a) 등기부 갑구/을구 row별 분리, (b) 혼합계약서의 계약성립/대가수수/인도/관리·사용 귀속 4종 분리, (c) 통지 발송일과 도달일의 분리 candidate, (d) 답변서의 admission/defense 분리 candidate, (e) valuation 문서의 시점별/지표별 candidate, (f) 가족관계·상속포기 문서의 succession candidate 분리다. + legal-effect event는 raw event와 함께 추출하며, payment, notice, auction, lien, succession, valuation, defense assertion, commercial successor liability에 대해 v.2 원본의 REQUIRED_EVENT_SUBKINDS를 그대로 따른다. + + + + + + - runtime parameters: `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.b1_output_path}}`, `{{item.b2_output_path}}` (planner 가 부여한 단일 source of truth — 별도 plan 파일 read 불요) + - `{{item.b1_output_path}}` (같은 ordinal 의 B1 mapper part — runtime parameter 그대로 사용) + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 문서 1건; planner가 evidence_all.json에서 분리해 둔 단일 shard 파일) + - `client_meeting.md` (보조 사용만 허용) + + + - `evidence_event_candidate_parts/E-{ordinal:03d}.json` (본 mapper 인스턴스의 part 파일 1개만) + + + - 다른 ordinal의 B1 part 또는 raw 문서 처리 + - 다른 ordinal의 사실을 자기 part에 주입하기 (authority drift) + - 증거 문서 수 N을 재계산하여 planner 결과를 대체하기 + - assigned_ordinal을 새로 생성하거나 재정렬하기 + - B1/B2 mapper fan-out 또는 streaming edge를 설계하기 + - 최종 `evidence_event_candidates.json` 작성 (이는 quality gate의 단독 권한) + - 다른 mapper의 part 파일 읽기/덮어쓰기 + - 자기 part 재읽기 + - 문서 summary를 다시 쓰는 작업으로 변질시키기 + - meeting만으로 문서에 없는 사건행위를 새로 만들기 + - 입력에 없는 날짜, 당사자, 금액, 목적물을 창작하기 + + + + + 1. `list_docs`로 `{{item.b1_output_path}}`, `{{item.shard_doc_path}}`, `client_meeting.md` 존재만 확인한다. + 2. `read_docs`로 위 세 파일을 읽는다. `{{item.shard_doc_path}}`는 이 인스턴스의 단일 evidence 문서로 그대로 사용한다 (별도의 ordinal 추출 불필요). assigned_ordinal·evidence_index_candidate·b2_output_path 는 runtime parameter (`{{item.*}}`) 그대로 사용한다. + 3. 메모리에서 part 객체를 생성한다. 단일 item object 또는 1-element items 형태이며, event 후보가 0건이어도 파일을 생략하지 말고 `zero_event_reason`을 반드시 가진다. + 4. `write_file` 전에 아래 을 수행한다. 불량이면 먼저 내부 보정을 시도한다. + 5. 보정 후에도 schema invariant가 불량이면 part 파일을 쓰지 않고 `FAILED: E-{ordinal:03d} event candidate part schema invariant violation`만 보고하고 종료한다. + 6. 통과한 경우에만 `write_file(overwrite=true)`로 runtime parameter 가 지정한 `evidence_event_candidate_parts/E-{ordinal:03d}.json`을 저장한다. + 7. `list_docs`로 자기 part 파일 존재만 확인한다. 절대 읽지 않는다. + 8. 정확히 `"E-{ordinal:03d} event candidates part 작성 완료"`만 출력하고 종료한다. + + + + - 입력 누락, 빈 파일, runtime parameter 누락, B1 part 부재, ordinal 매핑 실패, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. part 파일은 쓰지 않는다. + - event 후보가 0건인 것은 실패가 아니다. 이 경우에도 runtime parameter 가 지정한 output path에 `zero_event_reason`을 포함한 schema-valid part 파일을 쓴다. + - `event_candidates: []`와 `zero_event_reason` 누락 또는 null이 함께 존재하는 것은 정상 zero-event가 아니라 schema invariant 위반이다. + - part schema invariant 위반이 내부 보정 후에도 남으면 part 파일은 쓰지 않는다. 불완전한 part를 생성하여 quality gate에 넘기지 않는다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + + - 본 mapper 인스턴스는 정확히 runtime parameter (`{{item.assigned_ordinal}}`) 에 해당하는 1개 문서만 처리한다. + - runtime parameters (`{{item.*}}`) 는 ordinal/binding 의 단일 source of truth 다 (별도 plan 파일 read 불요). + - B1 part의 `evidence_index_proposed`, `title`, `doc_type`, `source_pointer.ordinal`을 authority로 사용한다. + - `{{item.shard_doc_path}}`의 단일 evidence 문서(planner가 evidence_all.json에서 분리)를 직접 참조하여 candidate를 추출한다. + - 본 task는 planner가 확정한 `evidence_index_candidate`, `b2_output_path`, assigned_ordinal을 소비할 뿐 재작성하지 않는다. + - 다른 ordinal의 사실을 자기 part에 주입하지 않는다(authority drift 금지). + - candidate layer에 머문다. 최종 BO, 최종 JuristicAct, 최종 청구권, 최종 사해행위 판단을 확정하지 않는다. + - 문서 단위 1요약으로 뭉개지 않는다. 한 문서에 복수의 법적 중요 행위가 있으면 반드시 여러 candidate로 분리한다. + - support가 희박한 후보를 억지로 채우지 않는다. 불명확한 필드는 `null` 또는 빈 배열을 사용할 수 있다. + - 잘못된 과잉 특정화보다 보수적 분해가 낫다. 다만 자기 문서 커버리지를 포기해서는 안 된다. + - `candidate_id`는 deterministic 규칙 `EVT-{ordinal:03d}-{seq:02d}`로 생성한다(seq는 자기 part 내 1-based 순번). + + + + 이 인스턴스의 `{{item.shard_doc_path}}`(planner가 evidence_all.json에서 분리한 단일 evidence 1건) + `evidence_indexed_parts/E-{ordinal:03d}.json`의 authority metadata + `client_meeting.md` + + + `client_meeting.md`는 아래 목적에만 사용할 수 있다. + - alias 정규화 + - 동일인 식별 보조 + - property cluster shorthand 해석 + - 현재 문서가 어느 상담 조항을 뒷받침하는지에 대한 `meeting_clause_refs` 보조 + - 현재 문서에서 직접 읽히는 내용의 중요도 보조 + - 사망자와 특정 자산군의 연결이 meeting에서 직접 읽히는 경우 succession 관련 candidate의 `affected_asset_cluster_ids` 보조 + 금지: + - meeting만으로 문서에 없는 사건행위를 candidate로 생성 + - meeting만으로 현재 문서의 당사자/금액/날짜를 보충해 direct evidence처럼 쓰기 + + + + + - 자기 part는 정확히 1개 item을 가진다. + - 그 item은 아래 둘 중 하나를 반드시 가진다. + - `event_candidates`가 1개 이상 + - `zero_event_reason`가 non-null + - `zero_event_reason` 허용값: + - `"NO_LEGAL_EVENT_FOUND"` + - `"REFERENCE_ONLY_DOC"` + - `"PARSE_INSUFFICIENT"` + - `"ORDINAL_MATCH_FAILED"` + - 등기부, 계약서, 통지서, 답변서, 감정/시세 문서처럼 통상 event-bearing 문서에 대해서는 `zero_event_reason`를 매우 예외적으로만 사용한다. + + + + `write_file` 전에 반드시 메모리의 part object를 검증한다. 이 검증은 생성 지점 안전장치이며 자동 강제 재실행을 의미하지 않는다. + + 필수 invariant: + 1. 메모리상 top-level은 단일 item object 또는 1-element `items` envelope일 수 있으나, `write_file` 직전 serialization 대상은 단일 item object여야 하며 실질 item은 정확히 1개다. + 2. item의 `evidence_index_proposed`는 runtime parameter `{{item.evidence_index_candidate}}` 또는 그와 동일한 `E-{ordinal:03d}` 값이어야 한다. + 3. item은 아래 둘 중 하나를 반드시 만족해야 한다. + - `event_candidates`가 non-empty list + - `zero_event_reason`이 non-null이며 허용값 중 하나 + 4. `event_candidates`가 빈 배열이면 `zero_event_reason`은 반드시 non-null 허용값이어야 한다. + 5. candidate가 없어 `zero_event_reason`이 non-null이면 schema 안정성을 위해 `event_candidates`는 빈 배열로 둔다. + 6. candidate가 있으면 각 candidate는 object이며, `candidate_id_proposed`는 `EVT-{ordinal:03d}-{seq:02d}` 형식이고 `source_evidence_index_proposed`는 자기 `evidence_index_proposed`와 일치해야 한다. + + 내부 보정 원칙: + - 문서 검토 결과 법적 중요 event/state 후보가 없는 것이 맞으면 `zero_event_reason`을 허용값 중 하나로 명시한다. + - event/state candidate 생성 조건이 충족되는데 빈 배열이면 extraction routines로 되돌아가 candidate를 재작성한다. + - 통상 event-bearing 문서에 `zero_event_reason`을 사용하는 경우, 원문상 후보 부재 또는 파싱 불충분 이유가 허용값과 맞는지 재확인한다. + + 보정 실패: + - 위 invariant 중 하나라도 충족하지 못하면 `write_file`을 호출하지 않는다. + - 마지막 채팅 출력은 정확히 `FAILED: E-{ordinal:03d} event candidate part schema invariant violation`로 한다. + + + + 아래 범주의 법적 중요 행위를 후보로 추출한다. + - 소유권 이전, 매매, 증여, 대물변제, 처분 + - 근저당/저당/전세권/가압류/압류/담보 설정 및 말소 + - 대여, 보증, 신용보증, 대위변제, 구상권 발생, 변제, 배당 + - 임대차, 점유, 인도, 현재 점유, 현재 사용자, 현재 사용수익, 현재 수익귀속, 현재 권리귀속, 현재 관리 상태 + - 경매신청, 경매개시결정, 경매매각, 인도명령, 집행 관련 사실 + - 통지, 최고, 해제/해지의사표시, 도달, 채무승인, 감액요구, 책임부인, 현재상태 주장 + - 판결, 결정, 지급명령, 배당표 등 소송/집행 관련 법적 중요 사실 + - 신축, 영업양도, 사업자등록, 현재 영업상태 등 사건핵심 상태 형성 행위 + - 위 범주와 직접 연결되는 기타 법적 중요 행위 또는 현재상태 + + 제외: + - 순수 배경설명만 있는 문장 + - 정보 증가 없는 반복 진술 + - 법적 효과와 관련 없는 주변사정 + + + + + - 갑구/을구의 각 의미 있는 row를 개별 candidate로 본다. + - 최소 추출 대상: 소유권보존, 소유권이전, 증여, 매매, 담보권실행을 위한 경매개시결정, 담보권실행을 위한 경매로 인한 매각, 근저당권설정, 근저당권설정등기말소, 가압류. + - `support_locators.locator_hint`에는 가능하면 `갑구4`, `을구1`, `순위번호 3` 같은 row identifier를 남긴다. + - 말소표시가 있더라도 법적 의미가 있으면 candidate로 남길 수 있다. + - row에 지분 표시가 직접 읽히면 candidate에 같은 ordinal B1 part의 `registry_row_components[*].fraction_ref`와 동일한 `fraction_ref`를 남긴다. 지분 표시가 없으면 null. + + + + - 하나의 문서에 서로 다른 계약 요소가 공존하면 clause별로 분해한다. + - 예: 대지 임대차 + 건물 매매 + 인도 + 관리사용 귀속 + 명의변경 협력은 가능한 한 별개 candidate로 분리한다. + - B1 part의 `contract_components`가 있으면 그것을 우선 참고하되, raw 문서 확인을 생략하지 않는다. + - 혼합계약서가 계약성립, 대가수수, 인도, 관리·사용 귀속을 직접 보여주면 아래 후보군을 가능한 한 독립 candidate로 생성한다. + 1. 계약성립 candidate + 2. 대가수수 candidate + 3. 인도 candidate + 4. 관리·사용 또는 현재 수익귀속 state candidate + - 위 4종 중 raw 문서가 직접 보여주는 것은 formation candidate 1개로 뭉개지지 않도록 반드시 분리한다. + - 대가수수 candidate는 적절한 기존 enum이 없으면 `event_kind="기타"`, `event_subkind="대가수수"`로 둘 수 있다. + - 대가수수 candidate에는 가능한 한 지급자, 수령자, 금액, 목적물, 해당 조항 locator를 함께 남긴다. + - 인도 candidate에는 가능한 한 인도자, 인도수령자, 목적물, 개시일, locator를 함께 남긴다. + - 관리·사용 귀속 candidate는 `event_or_state="state"`를 우선 검토하고, `current_state_candidate` 하위 4축과 interval field를 함께 남긴다. + - 문서가 계약일로부터 현재 점유/사용 귀속을 직접 보여주면 `state_start_date`를 그 개시일로 남기고, 뒤에 다른 점유/사용 candidate가 있으면 선행 state를 open-ended로 두지 않는다(같은 문서 내). + + + + - 내용증명, 답신, 답변서, 최고서, 해제통지서, 소멸청구 통지서에서는 아래를 개별 candidate로 검토한다. + - 지급 요구 + - 해제/해지 통지 + - 도달 + - 책임 부인 + - 현재 점유/현재 거주/현재 운영 주장 + - 사용수익 귀속 주장 + - 현재 권리귀속 주장 + - 채무승인 또는 시효항변 주장 + - 감액요구, 명도요구 + - 문서에 현재 상태가 직접 적혀 있으면 `event_or_state="state"` candidate를 생성할 수 있다. + - 현재 상태가 후행 시점에서 선행 상태를 대체하는 구조가 직접 보이면 interval field를 함께 남긴다. + + + + - 감정평가서, 감정평가 의견서, 임료 시세 확인서 등은 `event_candidates`가 0개라고 판단하지 않는다. + - 시가표, 차임표, 사용이익표, 시장가치표가 있으면 최소 1개의 factual/state candidate를 생성한다. + - rent/use-gain valuation은 `action_type_candidate="사실행위(factual acts)"`로 둘 수 있다. + - 문서가 특정 현재 수익귀속자 또는 현재 권리귀속자를 직접 보여주면, 해당 정보는 `current_state_candidate`의 대응 배열에 분리해 둔다. + + + + - 사업자등록증, 가족관계증명서, 현재상태 확인 문서, 심판문 등은 상태형 후보 또는 법적 지위 관련 후보를 생성할 수 있다. + - 예: 사업자등록은 현재 영업상태/점포 운영 개시의 간접 또는 직접 후보가 된다. + - 가족관계증명서, 상속포기 심판문, 상속 관련 공문서가 사망자와 친족구성 또는 상속포기를 직접 보여주면 succession 관련 candidate를 생성한다. + - sparse status 문서라도 later stage linker가 사용할 수 있도록 가능한 범위에서 `participants`, `object_spec`, `event_date` 또는 `current_state_candidate.state_as_of`를 채운다. + - family/renunciation/status 문서를 generic status로만 남기지 않는다. + + + + - 사망, 가족관계, 상속포기, 재산상속회복, 승계 관련 문서는 가능한 한 succession 관련 candidate를 생성한다. + - succession 관련 candidate에는 optional field `affected_asset_cluster_ids`를 둘 수 있다. + - `affected_asset_cluster_ids`는 meeting이나 B1 part metadata가 사망자와 특정 자산군의 연결을 직접 또는 명백하게 보여줄 때만 채운다. + - succession 관련 candidate에는 optional field `succession_candidate`를 둘 수 있다. + { + "deceased_candidate": null, + "spouse_candidates": [], + "lineal_ascendant_candidates": [], + "lineal_descendant_candidates": [], + "renunciant_candidates": [] + } + - 가족관계증명서 같은 status doc은 direct 내용이 보이면 `deceased_candidate`, `spouse_candidates`, `lineal_ascendant_candidates`, `lineal_descendant_candidates`를 가능한 한 채우고, 자산 연결이 직접 또는 명백하면 `affected_asset_cluster_ids`도 함께 채운다. + - 상속포기 문서는 상속포기 그 자체를 direct candidate로 남기되, direct names가 보이면 `succession_candidate.renunciant_candidates`를 반드시 채운다. + - 상속포기 문서가 특정 사망자와 자산 cluster를 직접 또는 명백하게 연결하면 동일한 `affected_asset_cluster_ids`를 함께 남긴다. + - direct names가 있는 succession/status 문서에서는 가능한 범위에서 `participants`와 `object_spec`도 함께 남겨 later stage overlap scorer가 sparse document를 놓치지 않게 한다. + - 이 단계는 최종 상속분, 단독상속, complaint plaintiff narrowing을 확정하지 않는다. 다만 downstream이 succession bundle을 읽을 수 있을 정도의 구조는 반드시 남긴다. + + + + + - raw event는 문서가 직접 보여주는 사실행위·법률행위·준법률행위다. + - legal-effect event는 raw event에서 downstream 청구권 식별에 필요한 법률효과 후보를 도출한 candidate다. + - raw event와 legal-effect event는 같은 문서에서 함께 생성할 수 있으며, 서로 다른 `candidate_id`를 가진다. + - legal-effect event는 최종 법률판단이 아니라 Stage 2가 검토할 구조화된 후보이므로 `confidence`와 `review_flags`를 함께 남긴다. + + + 금전채권: + - `underlying_claim_arising` + - `first_demand` + - `demand_notice_dispatch` + - `demand_notice_arrival` + - `debt_acknowledgement` + - `new_due_date` + - `business_transfer` + - `trade_name_continuation_candidate` + - `commercial_successor_liability_candidate` + + 담보부 부동산: + - `secured_debt_creation` + - `mortgage_setting_by_fraction` + - `undesignated_payment` + - `payment_allocation_effect` + - `mortgage_extinguishment_by_fraction` + - `auction_application_after_extinguishment` + - `auction_sale_and_registry_transfer` + - `derivative_mortgage_setting_by_invalid_title_holder` + - `unregistered_building_original_acquisition` + - `de_facto_disposition_holder_acquisition` + - `land_use_gain_start` + - `liability_overlap_start` + + 사해행위취소: + - `preserved_claim_arising` + - `collateral_setting_for_preserved_claim` + - `fraudulent_transfer` + - `asset_value_at_fraudulent_act` + - `asset_value_at_close_proxy` + - `collateral_insufficiency_at_act` + - `encumbrance_release_after_act` + - `value_compensation_exception_trigger` + - `value_compensation_cap_calculation_candidate` + - `defense_assertion_title_trust` + - `defense_assertion_sufficient_collateral` + - `defense_assertion_setoff` + + 유치권ㆍ상속ㆍ통지: + - `lien_assertion` + - `third_party_lease_by_lien_holder` + - `lien_violation_candidate` + - `lien_extinction_notice_dispatch` + - `lien_extinction_notice_receipt` + - `lien_extinction_effect_candidate` + - `ongoing_physical_possession` + - `succession_renunciation_filing` + - `succession_renunciation_acceptance` + - `spouse_sole_succession_effect_candidate` + - `defense_assertion_subsequent_owner_no_extinction_right` + - `defense_assertion_benefit_applied_to_secured_claim` + + + + - claim_doc 문서가 채무승인, 최고, 변제기, 변제약정, 지급기일을 함께 포함하면 하나의 generic event로 압축하지 말고 legal effect별 candidate를 분리한다. + - 이행각서에는 최소한 `debt_acknowledgement`, `payment_promise`, `new_due_date` 후보가 있는지 검토한다. + - 최고서에는 `demand_notice_dispatch`, `demand_notice_arrival`, `limitation_preservation` 관련 후보가 있는지 검토한다. + - 발송일과 도달일이 다르면 각각 별도 `event_date`로 보존한다. + - 채무승인 event에는 `legal_effect_candidate.acknowledgement_of_debt=true`, `limitation_interruption_candidate=true`, `related_claim_type`을 직접 읽히는 범위에서 남긴다. + + + + - business_transfer_doc 문서는 단순 영업양도 formation event로만 끝내지 않는다. + - 문서 또는 상담내용에서 상호, 영업명, 영업자산, 고객관계, 고용관계, 채무불승계 조항, 책임배제 등기 필요성이 보이면 `commercial_successor_candidates`로 분해한다. + - 상법 제42조 책임을 최종 확정하지는 않되, downstream이 요건판단을 할 수 있도록 transferor, transferee, business_name, trade_name_continuation_candidate, debt_exclusion_clause_candidate를 남긴다. + + + + - 원자료 event가 곧바로 법률효과를 발생시키는 경우 raw event와 legal-effect event를 모두 생성한다. + - 복수 채무에 대한 지정 없는 변제가 등장하면 `undesignated_payment` event와 별도로 `payment_allocation_effect` 후보를 생성한다. + - 등기부상 경매 매각과 후행 근저당권이 등장하면, 그 경매가 어떤 담보권에 기초했는지, 그 담보권의 피담보채무가 이미 소멸했는지 검토하는 event chain을 만든다. + - 미등기 건물 신축일과 미등기 건물 매수일은 점유·사용이익 발생 시점을 정하므로 `land_use_gain_start` 및 `liability_overlap_start` event 후보를 생성한다. + - `mortgage_setting_by_fraction`/`mortgage_extinguishment_by_fraction` 등 `*_by_fraction` subkind candidate는 지분 표시가 문서에 직접 읽히는 한 `fraction_ref`를 반드시 가진다. + + + + - 사해행위취소 사건에서 raw event와 legal-effect event를 구분한다. + - 사해행위 당시 존재하던 담보권이 사후 말소되면 `value_compensation_exception_trigger` event를 생성한다. + - valuation row와 사후 말소 부담, 존속 부담이 모두 있으면 `value_compensation_cap_calculation_candidate`를 생성한다. + - 피고가 항변을 명시한 내용증명은 `defense_assertion_*` event로 만들고, Stage 2 claim description에 반영해야 하는지 표시한다. + + + + - 통지 문서와 답변서가 서로 대응하고, 답변서가 특정 통지의 수령일을 인정하면 별도의 receipt event candidate를 생성한다. + - receipt event candidate는 발송 event와 병합하지 않는다. + - 발송일과 도달일이 다르면 도달일을 법률효과 후보일로 별도 보존한다. + - 문서에 유치권, 공사잔금, 점유, 임대, 직접 거주, 피담보채권 충당, 후소유자 항변이 함께 나타나면 일반 current possession event가 아니라 lien_event 후보군으로 분해한다. + - 제3자 임대는 `lien_violation_candidate`로, 직접 거주는 ongoing use state로, 피담보채권 충당 주장은 `defense_candidate`로, 소멸청구 수령은 `lien_extinction_effect_candidate`로 각각 분리한다. + - 상속포기 수리 event가 있고 배우자와 자녀 전부 상속포기 구조가 직접 보이면 `spouse_sole_succession_effect_candidate`를 생성하되, 최종 소장 원고 확정은 Task_C 이후로 미룬다. + + + + + `action_type_candidate`는 아래 중 1개를 선택한다. + - `"법률행위(legal acts)"` + - `"준법률행위(quasi-legal acts)"` + - `"사실행위(factual acts)"` + - `"위법행위(unlawful acts)"` + - `"소송행위(litigation acts)"` + - `"불명"` + + + + `event_kind`는 아래 값 중 가장 가까운 것을 선택한다. + - `"처분"`, `"소유권이전"`, `"증여"`, `"대물변제"`, `"담보설정"`, `"담보말소"`, `"채권발생"`, `"보증"`, `"대위변제"`, `"변제"`, `"배당"`, `"임대차"`, `"점유"`, `"인도"`, `"가압류/압류"`, `"경매신청"`, `"경매개시"`, `"경매매각"`, `"통지"`, `"해제/해지"`, `"판결/결정"`, `"영업양도"`, `"사업자등록"`, `"신축"`, `"상속개시"`, `"상속포기"`, `"채권관리"`, `"도달"`, `"항변"`, `"가치평가"`, `"법률효과"`, `"기타"` + + 필요하면 optional field `event_subkind`에 더 구체한 라벨(REQUIRED_EVENT_SUBKINDS 참조)을 남길 수 있다. + + + + - `participants.actor_candidates`는 직접 행위를 한 사람/법인 + - `participants.counterparty_candidates`는 직접 상대방 + - `participants.beneficiary_candidates`는 행위로 이익을 직접 취득한 사람/법인 + - `participants.third_party_candidates`는 법원, 피상속인, 기존 권리자 등 제3자 + - `object_spec`는 짧은 목적물/채권/담보/통지대상/상태 대상 명사구 + - `amount`는 현재 candidate와 직접 연결되는 대표 금액 1개만 사용한다 + - state candidate의 현재 점유/사용/수익/권리 귀속은 `participants`에 뭉개지지 않고 `current_state_candidate` 하위 배열에 분리한다 + - succession/status 문서처럼 sparse한 문서라도 direct names와 direct relation이 보이면 `participants`와 `object_spec`를 불필요하게 비워두지 않는다. + - 잘못된 당사자 확정보다 `[]` 또는 `null`이 낫다 + + + + - `event_date`는 가능하면 `YYYY-MM-DD` + - 불명확하면 `null` + - `time_text`는 짧은 원문 표현을 유지할 수 있다 + - `time_precision` 허용값: `"exact"`, `"approximate"`, `"range"`, `"unknown"` + - 현재상태 candidate의 경우 `state_as_of` 개념은 optional `current_state_candidate.state_as_of`에 남기고, `event_date`는 상태 개시 시점이 직접 보일 때만 채운다. + - state interval이 직접 보이면 `current_state_candidate.state_start_date`, `current_state_candidate.state_end_date_candidate`를 함께 채운다. + - 후행 상태가 선행 상태를 대체하는 것이 직접 보이면 `current_state_candidate.superseded_by_candidate_id`를 사용한다. + + + + - optional field `event_or_state`를 추가할 수 있다. 허용값은 `"event"` 또는 `"state"`. + - 문서가 단발 사건이 아니라 `현재까지`, `현재도`, `계속`, `현재 운영`, `현재 점유`, `현재 거주`, `부지로만 사용`, `현재 명의`, `현재 소유`, `차임을 받고 있다` 같은 계속상태를 직접 보여주면 `event_or_state="state"` candidate를 생성할 수 있다. + - state candidate에는 optional field `current_state_candidate`를 둘 수 있다. + { + "state_summary": "짧은 상태 설명", + "state_as_of": "현재|2024-10-31 등", + "state_start_date": null, + "state_end_date_candidate": null, + "superseded_by_candidate_id": null, + "is_ongoing": true, + "current_possessor_candidates": [], + "current_user_candidates": [], + "current_benefit_holder_candidates": [], + "current_right_holder_candidates": [], + "possession_legal_cause_asserted": null, + "possession_legal_cause_asserted_by": [] + } + - `current_state_candidate`는 가능한 한 아래 4축으로 분해한다. + - 점유자 -> `current_possessor_candidates` + - 사용자/운영자 -> `current_user_candidates` + - 차임·사용이익·영업수익 등 현재 수익귀속자 -> `current_benefit_holder_candidates` + - 현재 소유자·등기명의인·권리귀속자 -> `current_right_holder_candidates` + - 같은 사람이 복수 role을 가지면 각 배열에 중복 기재할 수 있다. + - 문서에 직접 읽히지 않는 축은 빈 배열로 둔다. 추정으로 채우지 않는다. + - event candidate와 state candidate는 동일 문서에서 함께 존재할 수 있다. + - 동일 문서에 과거 행위와 현재 상태가 함께 있으면 분리한다. + - 동일 문서에 현재 점유자와 현재 권리귀속자가 다르게 읽히면 하나로 뭉개지지 않도록 분리 기재한다. + - 상태 개시 시점이 직접 보이면 `state_start_date`를 채운다. + - 후행 점유/사용/수익귀속 candidate가 선행 상태를 대체하는 것이 직접 보이면, 선행 state에 `state_end_date_candidate`와 `superseded_by_candidate_id`를 남기고 open-ended current state로 방치하지 않는다. + - `state_end_date_candidate`가 직접 보이면 `is_ongoing=false`를 우선 검토한다. + - 혼합계약서가 인도, 관리·사용 귀속, 사용수익 귀속을 직접 보여주면 그 상대방에 대한 state candidate를 high confidence로 생성할 수 있다. + - raw 문서가 현재 점유, 현재 사용, 현재 수익귀속, 현재 권리귀속을 직접 적시하면 candidate `confidence`는 원칙적으로 `high`를 우선 검토한다. + - meeting 보조나 간접 정황에 크게 의존한 상태 후보는 `medium` 또는 `low`로 둔다. + - `possession_legal_cause_asserted`는 optional 필드다. 허용값: "유치권행사중|임대차|사용대차|물권|동시이행항변|명도후잔류|무권원주장|미상". `possession_legal_cause_asserted_by`는 그 권원을 주장한 주체(원고측/피고측/제3자 이름)이며 문서에 직접 읽힐 때만 채운다. + - 점유 사실과 권원 주장은 같은 candidate 안에서 축을 분리해 남긴다(사실=4축 배열, 권원=`possession_legal_cause_asserted`). 권원 주장이 문서에 없으면 `미상`이 아니라 null로 둔다. + - 서로 다른 문서가 동일 자산에 대해 다른 권원을 주장하는 것은 본 mapper의 검출 대상이 아니다(authority drift 금지). 각 문서의 주장을 그대로 보존한다. + + + + - optional field `meeting_clause_refs`를 추가할 수 있다. + - 형식은 `1-가`, `2-라`, `3-다`, `4-가`, `5-나`처럼 `client_meeting.md`의 본문 번호체계를 사용한다. + - 현재 문서가 특정 상담조항을 직접 뒷받침할 때만 기록한다. + - meeting clause ref는 보조 recall 장치일 뿐, 문서에 없는 사실을 가져오는 수단이 아니다. + + + + 이 단계는 최종 사해행위 판단을 하지 않는다. 다만 후속 단계의 recall 강화를 위해 candidate tag를 남긴다. + `actio_relevance_candidates.candidate_role_tags` 허용값: + - `"사해행위목적물후보"`, `"원고채권후보"`, `"선순위담보후보"`, `"존속담보후보"`, `"임차권후보"`, `"가압류후보"`, `"수익자이익후보"`, `"현재점유후보"`, `"현재사용수익후보"` + + `fraudulent_act_date_candidate`는 다음 경우에만 채운다. + - 현재 candidate가 직접 목적물의 처분, 이전, 이전등기, 매매일, 증여일, 대물변제일, 경매매각일과 연결되는 경우 + - 그 외에는 쓰지 않는다 + + + + downstream Task_C BO와 evidence 기반 candidate가 중복 제거할 수 있도록, 각 candidate마다 아래 정규화 문자열을 생성한다. + + `(event_or_state + event_kind + 핵심 actor + 핵심 counterparty + 핵심 object_spec/amount + 핵심 date_or_state_as_of)` + + 규칙: + - 불명확한 요소는 생략하되, 최소한 `event_or_state` 또는 `event_kind`와 1개 이상의 핵심 요소는 남긴다. + - 중복 제거를 위해 whitespace, 중복 조사, 과도한 수식어는 제거한다. + - title이나 evidence_index는 넣지 않는다. + - event와 state는 같은 signature가 되지 않도록 구분한다. + - mapper가 propose하는 candidate_id는 `EVT-{ordinal:03d}-{seq:02d}`이며 quality gate가 collision을 처리한다. + + + + part 파일은 단일 item object 형태이며, top-level item은 non-empty `event_candidates` 또는 non-null `zero_event_reason` 중 하나를 반드시 가진다. `event_candidates: []` 단독은 금지된다. + + { + "evidence_index_proposed": "E-{ordinal:03d}", + "title": "문서 제목", + "doc_type": "공문서", + "source_pointer": { + "source": "evidence_all.json", + "ordinal": ordinal + }, + "property_cluster_id": null, + "doc_semantic_flags": [], + "zero_event_reason": null, + "affected_asset_cluster_ids": [], + "event_candidates": [ + { + "candidate_id_proposed": "EVT-{ordinal:03d}-01", + "event_or_state": "event|state", + "event_kind": "소유권이전", + "event_subkind": null, + "action_type_candidate": "법률행위(legal acts)", + "action_summary": "짧은 1문장 요약", + "source_evidence_index_proposed": "E-{ordinal:03d}", + "source_fact_candidate": null, + "participants": { + "actor_candidates": [], + "counterparty_candidates": [], + "beneficiary_candidates": [], + "third_party_candidates": [] + }, + "object_spec": null, + "fraction_ref": null, + "amount": null, + "event_date": null, + "event_end_date": null, + "time_text": null, + "time_precision": "exact|approximate|range|unknown", + "location": null, + "legal_keywords": [], + "support_locators": [ + { + "locator_type": "heading|key_value|table_row|paragraph|caption|unknown", + "locator_hint": "갑구4|제2조 등", + "excerpt": "짧은 단서", + "directness": "직접|간접|불명" + } + ], + "current_state_candidate": null, + "succession_candidate": null, + "legal_effect_candidate": { + "effect_type": "string|null", + "related_claim_type": "string|null", + "acknowledgement_of_debt": null, + "limitation_interruption_candidate": null, + "new_due_date_candidate": null, + "default_start_candidate": null, + "payment_allocation_candidate": null, + "value_compensation_exception_trigger": null, + "downstream_date_roles": [], + "required_downstream_roles": [], + "review_flags": [] + }, + "defense_candidate": null, + "commercial_successor_candidates": null, + "meeting_clause_refs": [], + "actio_relevance_candidates": null, + "identity_signature": "state|점유|...", + "confidence": "high|medium|low|unknown" + } + ], + "mapper_meta": { + "mapper_task": "Task_B2_map_events", + "schema_contract_version": "evidence_event_candidate_part.v4", + "self_isolation_attested": true + } + } + + 추가 제약: + - top-level은 단일 object다. envelope이 아닌 단일 item. + - `event_candidates`가 빈 배열이면 `zero_event_reason`은 반드시 non-null 허용값이어야 한다. + - `event_candidates: []`와 `zero_event_reason` 누락/null이 결합된 part는 작성 금지되는 schema invariant violation이다. + - `candidate_id_proposed`는 mapper의 propose 식별자(`EVT-{ordinal:03d}-{seq:02d}`)이며, finalize는 quality gate가 한다. + - `source_evidence_index_proposed`는 mapper가 propose 한 자기 ordinal의 candidate evidence_index다. + - `mapper_meta.self_isolation_attested=true`는 본 mapper가 자기 문서 외 사실을 주입하지 않았음을 self-attest 한다는 의미다. + + + + - `write_file` 전 을 수행하고, 불량이면 내부 보정을 먼저 시도했는지 점검한다. + - 자기 part object가 단일 item이고 `evidence_index_proposed`, non-empty `event_candidates` 또는 non-null `zero_event_reason`을 가지는지 점검한다. + - `event_candidates: []`와 `zero_event_reason` 누락/null이 함께 있으면 내부 보정 대상이며, 보정 불가 시 part 파일을 쓰지 않고 FAILED로 종료한다. + - `planner_status=="READY"` 및 `mapper_execution_allowed=true`가 확인되지 않았으면 part 파일이 작성되지 않았는지 점검한다. + - 모든 candidate의 `candidate_id_proposed`가 `EVT-{ordinal:03d}-{seq:02d}` 형식인지 점검한다. + - 자기 문서 외 ordinal의 사실이 part에 주입되지 않았는지 self-check 한다(authority drift 방지). + - `registry_doc` 또는 `multi_event_doc`인데 `event_candidates`가 0개면 다시 점검한다. + - `mixed_contract_doc`인데 서로 다른 법적 효과가 2개 이상 보이면 candidate도 최소 2개 이상 검토한다. + - 혼합계약서가 계약성립, 대가지급·수령, 인도, 관리·사용 귀속을 직접 보여주면 formation candidate만 남기지 말고 consideration/delivery/state candidate가 함께 검토되었는지 점검한다. + - 문서가 위 네 요소를 모두 직접 보여주면 최소 3개 이상, 가능하면 4개까지 독립 candidate로 분리되었는지 점검한다. + - `valuation_doc`인데 가치표/차임표가 직접 보이면 candidate를 최소 1개 생성한다. + - 문서가 상담일지의 명시 조항을 직접 뒷받침하는데도 `meeting_clause_refs`가 전혀 없으면 다시 점검한다. + - event와 state는 구분해 생성한다. 현재상태를 과거 단발행위로 뭉개지 않는다. + - state candidate에서 현재 점유자/사용자/수익귀속자/권리귀속자가 직접 읽히는데도 하나의 participant 배열로 뭉개졌다면 다시 점검한다. + - 현재 점유자와 현재 권리귀속자가 다르게 읽히는 문서는 `current_state_candidate` 하위 배열에서 분리되었는지 다시 점검한다. + - 상태 개시와 종료가 직접 보이거나 후행 상태가 선행 상태를 대체하는 것이 직접 보이면 `state_start_date`, `state_end_date_candidate`, `superseded_by_candidate_id` 검토가 이루어졌는지 점검한다. + - 가족관계증명서가 사망자와 친족구성을 직접 보여주면 `succession_candidate.deceased_candidate`, `spouse_candidates`, `lineal_descendant_candidates` 또는 `lineal_ascendant_candidates`가 필요한 범위에서 채워졌는지 점검한다. + - 상속포기 심판문이 direct names를 보여주면 `succession_candidate.renunciant_candidates`가 비어 있지 않은지 점검한다. + - 가족관계증명서, 상속포기 심판문, 상속 관련 공문서가 사망자와 특정 자산군의 연결을 직접 또는 명백하게 보여주면 `affected_asset_cluster_ids` 검토가 이루어졌는지 점검한다. + - succession/status 문서처럼 sparse한 문서라도 direct names, direct relation, direct date가 보이는데 `participants`, `object_spec`, `event_date` 또는 `state_as_of`가 불필요하게 비어 있지 않은지 점검한다. + - raw 문서가 현재상태를 직접 적시한 경우 candidate `confidence`가 불필요하게 low로 떨어지지 않았는지 점검한다. + - claim doc에 채무승인ㆍ최고ㆍ변제기 재설정 단서가 있는데 `legal_effect_candidate`가 있는 event가 전혀 없으면 self-warning으로 본다. + - 이행각서에 채무승인일과 새 변제기가 함께 있는데 `debt_acknowledgement`와 `new_due_date`가 분리되지 않으면 self-warning으로 본다. + - business transfer 문서에 상호속용 후보 event 또는 `commercial_successor_candidates`가 없으면 self-warning으로 본다. + - 복수 피담보채무와 변제가 있는데 `payment_allocation_effect`가 없으면 self-warning으로 본다. + - 경매 event가 있는데 기초 담보권 및 피담보채무 chain과 연결되지 않으면 `AUCTION_BASIS_CHAIN_MISSING` self-warning으로 본다. + - 미등기 건물 매수 event가 있는데 원시취득 event가 없으면 `UNREGISTERED_BUILDING_ORIGINAL_ACQUISITION_MISSING` self-warning으로 본다. + - 사후 담보권 말소 row가 있는데 `value_compensation_exception_trigger` event가 없으면 self-error로 본다. + - valuation matrix와 released encumbrance가 있는데 `value_compensation_cap_calculation_candidate`가 없으면 self-warning으로 본다. + - defense assertion event가 있는데 `defense_candidate.requires_rebuttal_in_stage2` 검토가 없으면 self-warning으로 본다. + - 유치권 소멸 통지서와 수령 인정 답변서가 있는데 도달 event 또는 `lien_extinction_notice_receipt`가 없으면 self-error로 본다. + - 제3자 임대 event가 있는데 `lien_violation_candidate`가 없으면 self-warning으로 본다. + - 상속포기 수리 event가 있는데 `spouse_sole_succession_effect_candidate` 후보가 없으면 self-warning으로 본다. + - self-warning/self-error는 mapper가 part 파일에 그대로 남겨도 되며, quality gate가 최종 severity를 판정한다. + + + + + {{item.assigned_ordinal}} + + + [ ] 1) Preflight: `list_docs` 사용해서 `{{item.b1_output_path}}`, `{{item.shard_doc_path}}`, `client_meeting.md` 만 확인. `read_docs` 사용해서 동일 파일만 읽기 + [ ] 2) runtime parameter `{{item.assigned_ordinal}}`, `{{item.evidence_index_candidate}}`, `{{item.b1_output_path}}`, `{{item.b2_output_path}}` 그대로 소비. 별도 검증 read 불요. + [ ] 3) `{{item.shard_doc_path}}`의 단일 evidence 문서를 메모리에서 사용하고, 같은 ordinal의 B1 part(`{{item.b1_output_path}}`)만 authority metadata로 사용 + [ ] 4) `write_file` 전 수행. 불량이면 내부 보정 시도 + [ ] 5) 보정 후에도 불량이면 part 파일을 쓰지 않고 `FAILED: E-{ordinal:03d} event candidate part schema invariant violation` 출력 후 종료 + [ ] 6) 통과한 경우에만 `write_file` 사용해서 `evidence_event_candidate_parts/E-{ordinal:03d}.json` 생성. event 후보가 0건이어도 non-null `zero_event_reason` part 파일을 작성 + [ ] 7) `list_docs` 사용해서 자기 part 파일만 존재 확인(절대 읽기 금지) + [ ] 8) `"E-{ordinal:03d} event candidates part 작성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `{{item.b1_output_path}}` (같은 ordinal 의 B1 part) + - `{{item.shard_doc_path}}` (이 인스턴스의 evidence 1건; planner가 evidence_all.json에서 분리) + - `client_meeting.md` + + + - `evidence_event_candidate_parts/E-{ordinal:03d}.json` + + + + + - `candidate_id_proposed`는 정확히 `EVT-{ordinal:03d}-{seq:02d}` 형식만 허용한다. + - `ordinal`과 output path는 runtime parameter (`{{item.*}}`) 를 그대로 따른다. + - 증거 문서 수 N, assigned_ordinal, mapper fan-out, streaming edge는 본 task에서 만들지 않는다. + - 자기 part 파일은 절대 재읽지 않는다. + - 다른 ordinal 문서를 처리하지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"E-{ordinal:03d} event candidates part 작성 완료"` 한 줄만 사용한다. + + + + - task_name: Task_B1_gate_merge_deterministic + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import copy + import hashlib + import itertools + import json + import re + import time + from collections import defaultdict + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + PLAN_PATH = "evidence_shard_plan.json" + PART_DIR = "evidence_indexed_parts" + FINAL_PATH = "evidence_indexed.json" + MANIFEST_PATH = "finalized_evidence_id_manifest.json" + PRECHECK_PATH = "quality_gates/B1_gate_precheck.json" + ID_RE = re.compile(r"^E-(\d{3})$") + + def _mid(): return next(MSG_ID) + + def _init(): + r = CLIENT.post(LOCALDOCS_URL, json={ + "jsonrpc": "2.0", "id": 1, "method": "initialize", + "params": {"protocolVersion": "2025-03-26", "capabilities": {}, + "clientInfo": {"name": "task-b1-gate-merge-deterministic", "version": "1.0", + "user_id": "{{__user_hash__}}", "workspace_id": "{{__workspace_hash__}}"}} + }, headers=MCP_HEADERS) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post(LOCALDOCS_URL, json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS).raise_for_status() + + def _parse_response(text: str): + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: return json.loads(line[6:]) + except Exception: pass + try: return json.loads(text) + except Exception: return None + + def _call(name: str, args: dict[str, Any]): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc": "2.0", "id": _mid(), "method": "tools/call", + "params": {"name": name, "arguments": args}}, + headers=MCP_HEADERS) + r.raise_for_status() + payload = _parse_response(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: dict[str, Any], name: str): + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: raise RuntimeError(f"Empty MCP result: {name}") + try: outer = json.loads(text) + except Exception: return text + if isinstance(outer, dict) and "results" in outer: + row = (outer.get("results") or [{}])[0] + inner = row.get("content") or row.get("text") or "" + if not inner: raise RuntimeError(f"Empty document content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer + + def read_json(path: str): + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str): return raw + try: return json.loads(raw) + except json.JSONDecodeError: + obj, _ = json.JSONDecoder().raw_decode(raw.strip()) + return obj + + def write_doc(path: str, obj: Any): + text = json.dumps(obj, ensure_ascii=False, indent=2) + _call("write_file", {"path": path, "content": text, "overwrite": True}) + return text + + def _walk_strings(value: Any): + if isinstance(value, str): + yield value + elif isinstance(value, list): + for item in value: yield from _walk_strings(item) + elif isinstance(value, dict): + for item in value.values(): yield from _walk_strings(item) + + def list_part_names(folder: str): + payload = None + errors = [] + for args in ({"path": folder, "pattern": "E-*.json"}, {}): + try: + payload = _unwrap(_call("list_docs", args), "list_docs") + break + except Exception as exc: + errors.append(str(exc)) + if payload is None: + raise RuntimeError("list_docs failed: " + " | ".join(errors)) + if isinstance(payload, str): + try: payload = json.loads(payload) + except Exception: pass + names = [] + for value in _walk_strings(payload): + match = re.search(r"(?:^|/)(E-\d{3}\.json)$", value) + if match: + names.append(f"{folder}/{match.group(1)}") + return sorted(set(names)) + + def single_item(obj: Any, path: str): + if isinstance(obj, dict) and isinstance(obj.get("items"), list): + if len(obj["items"]) != 1 or not isinstance(obj["items"][0], dict): + raise ValueError(f"{path}: envelope must contain exactly one object") + return obj["items"][0] + if isinstance(obj, dict): + return obj + raise ValueError(f"{path}: part must be an object") + + def refs(evidence_indexes=None, candidate_ids=None, meeting_refs=None, titles=None): + return { + "evidence_indexes": sorted(set(str(x) for x in (evidence_indexes or []) if x)), + "event_candidate_ids": sorted(set(str(x) for x in (candidate_ids or []) if x)), + "meeting_clause_refs": sorted(set(str(x) for x in (meeting_refs or []) if x)), + "doc_titles": sorted(set(str(x) for x in (titles or []) if x)), + } + + def finding(code: str, detail: str, evidence_indexes=None, rerun=None): + row = {"issue_type": code.lower(), "source_check": code.lower(), "severity": "BLOCK", + "detail": detail, "source_refs": refs(evidence_indexes)} + if rerun: row["rerun_target_task"] = rerun + return row + + def suspect(kind: str, basis: str, evidence_indexes=None, titles=None, context=None): + return {"origin_gate": "B1", "exception_type": kind, "severity_candidate": "HARD_WARNING", + "source_refs": refs(evidence_indexes, titles=titles), "concise_basis": basis, + "compact_context": context or {}} + + def warning_records(node: Any, path: str = "$"): + out = [] + if isinstance(node, dict): + for key, value in node.items(): + child = f"{path}.{key}" + if re.search(r"(self_?)?(warning|error)", str(key), re.I) and value not in (None, False, "", [], {}): + out.append({"path": child, "value": value}) + out.extend(warning_records(value, child)) + elif isinstance(node, list): + for i, value in enumerate(node): out.extend(warning_records(value, f"{path}[{i}]")) + return out + + def main(): + started = time.monotonic() + _init() + plan = read_json(PLAN_PATH) + if not isinstance(plan, dict): raise RuntimeError("planner must be an object") + expected_ordinals = [str(x).zfill(3) for x in plan.get("expected_ordinals") or []] + expected_ids = [str(x) for x in plan.get("expected_evidence_ids") or + [f"E-{x}" for x in expected_ordinals]] + shard_count = int(plan.get("shard_count") or len(expected_ordinals)) + hard = [] + reviews = [] + parse_failures = [] + if plan.get("planner_status") != "READY" or plan.get("mapper_execution_allowed") is not True: + hard.append(finding("PLANNER_NOT_READY", "planner did not authorize mapper output consumption")) + actual_names = list_part_names(PART_DIR) + actual_ordinals = sorted({re.search(r"E-(\d{3})\.json$", x).group(1) for x in actual_names + if re.search(r"E-(\d{3})\.json$", x)}) + missing = sorted(set(expected_ordinals) - set(actual_ordinals)) + unexpected = sorted(set(actual_ordinals) - set(expected_ordinals)) + stale = sorted(set((plan.get("stale_part_policy") or {}).get("detected_stale_parts", {}) + .get("evidence_indexed_parts") or []) | + {f"{PART_DIR}/E-{x}.json" for x in unexpected}) + for ordinal in missing: + hard.append(finding("MISSING_B1_PART", f"missing B1 mapper part E-{ordinal}", + [f"E-{ordinal}"], f"Task_B1_map_doc_{ordinal}")) + for ordinal in unexpected: + hard.append(finding("UNEXPECTED_B1_PART", f"unexpected B1 mapper part E-{ordinal}", + [f"E-{ordinal}"])) + for path in stale: + hard.append(finding("STALE_B1_PART", f"stale B1 mapper part: {path}")) + + shards = {str((row.get("ordinal_label") or row.get("assigned_ordinal") or "")).zfill(3): row + for row in (plan.get("shards") or []) if isinstance(row, dict)} + items = [] + input_bytes = 0 + uid_rows = defaultdict(list) + title_rows = defaultdict(list) + for ordinal in expected_ordinals: + path = f"{PART_DIR}/E-{ordinal}.json" + if ordinal in missing: continue + try: + raw = read_json(path) + input_bytes += len(json.dumps(raw, ensure_ascii=False).encode("utf-8")) + item = single_item(raw, path) + proposed = str(item.get("evidence_index_proposed") or "") + expected = f"E-{ordinal}" + if proposed != expected or not ID_RE.fullmatch(proposed): + raise ValueError(f"evidence_index_proposed {proposed!r} != {expected}") + pointer = item.get("source_pointer") + if not isinstance(pointer, dict) or int(pointer.get("ordinal") or -1) != int(ordinal): + raise ValueError("source_pointer.ordinal mismatch") + meta = item.get("mapper_meta") + if not isinstance(meta, dict) or meta.get("self_isolation_attested") is not True: + raise ValueError("mapper self-isolation attestation missing or false") + final_item = copy.deepcopy(item) + final_item["evidence_index"] = final_item.pop("evidence_index_proposed") + final_item.pop("mapper_meta", None) + items.append(final_item) + shard = shards.get(ordinal) or {} + source_ref = shard.get("source_document_ref") or {} + uid = str(item.get("doc_uid") or source_ref.get("doc_uid_candidate") or "") + content_hash = str(source_ref.get("content_sha256") or "") + title_norm = str(item.get("title_normalized") or "").strip().lower() + row = {"ordinal": ordinal, "evidence_index": expected, "title": item.get("title"), + "title_normalized": title_norm, "content_sha256": content_hash} + if uid: uid_rows[uid].append(row) + if title_norm: title_rows[title_norm].append({**row, "doc_uid": uid}) + for flag in warning_records(item): + reviews.append(suspect("mapper_self_warning", "B1 mapper emitted an explicit warning or error", + [expected], [item.get("title")], + {"warning": flag, "doc_type": item.get("doc_type")})) + semantic_flags = set(str(x) for x in (item.get("doc_semantic_flags") or [])) + title_text = str(item.get("title") or "") + coverage_gaps = [] + if "registry_doc" in semantic_flags and not (item.get("registry_row_components") or []): + coverage_gaps.append("registry_row_components") + if ("schedule_doc" in semantic_flags and re.search(r"별지|목록", title_text) + and not (item.get("schedule_items") or [])): + coverage_gaps.append("schedule_items") + if ("notice_doc" in semantic_flags and re.search(r"통지|내용증명|최고", title_text) + and not (item.get("notice_components") or [])): + coverage_gaps.append("notice_components") + if ("claim_doc" in semantic_flags and re.search(r"차용|금전소비대차|이행각서|각서", title_text) + and not (item.get("claim_document_components") or []) + and not (item.get("loan_components") or [])): + coverage_gaps.append("claim_or_loan_components") + if coverage_gaps: + reviews.append(suspect( + "component_coverage_concern", + "document type and title indicate an empty required component container", + [expected], [item.get("title")], + {"missing_components": coverage_gaps, "doc_type": item.get("doc_type"), + "doc_semantic_flags": sorted(semantic_flags)}, + )) + except Exception as exc: + parse_failures.append({"ordinal": ordinal, "part_path": path, "reason": str(exc), + "rerun_target_task": f"Task_B1_map_doc_{ordinal}"}) + hard.append(finding("B1_PART_SCHEMA_OR_PARSE_FAILURE", str(exc), [f"E-{ordinal}"], + f"Task_B1_map_doc_{ordinal}")) + + for uid, rows in sorted(uid_rows.items()): + if len(rows) < 2: continue + hashes = {row["content_sha256"] for row in rows if row["content_sha256"]} + ids = [row["evidence_index"] for row in rows] + if len(hashes) > 1: + hard.append(finding("EVIDENCE_IDENTITY_COLLISION", + f"doc_uid {uid} maps to different source hashes", ids)) + else: + reviews.append(suspect("suspected_duplicate_alias", + f"doc_uid {uid} appears in multiple evidence ordinals", ids, + [row["title"] for row in rows], {"doc_uid": uid})) + # A generic normalized title such as "내용증명" or "차용증" is not an alias signal by itself. + # Alias review is emitted only by the doc_uid/source-hash rule above. + + items.sort(key=lambda row: int(str(row.get("evidence_index")).split("-")[-1])) + final_ids = [str(row.get("evidence_index")) for row in items] + if set(final_ids) != set(expected_ids) or len(items) != shard_count: + hard.append(finding("EVIDENCE_CONSERVATION_FAILURE", + f"expected {shard_count} items/{len(expected_ids)} IDs, got {len(items)} items/{len(final_ids)} IDs", + sorted(set(expected_ids) ^ set(final_ids)))) + if len(final_ids) != len(set(final_ids)): + hard.append(finding("DUPLICATE_FINAL_EVIDENCE_ID", "final evidence IDs are not unique", final_ids)) + + final_obj = {"schema_contract_version": "evidence_indexed.v3", "items": items} + final_text = json.dumps(final_obj, ensure_ascii=False, indent=2) + parsed_final = json.loads(final_text) + if [x.get("evidence_index") for x in parsed_final["items"]] != final_ids: + raise RuntimeError("post-serialization B1 ID verification failed") + set_material = json.dumps(sorted(final_ids), ensure_ascii=False, separators=(",", ":")) + blocked = bool(hard) + manifest = { + "schema_version": "finalized_evidence_id_manifest.v1", + "status": "BLOCKED" if blocked else "READY", + "source_artifact": FINAL_PATH, + "shard_count": shard_count, + "expected_evidence_ids": expected_ids, + "finalized_evidence_ids": final_ids, + "missing_evidence_ids": sorted(set(expected_ids) - set(final_ids)), + "unexpected_evidence_ids": sorted(set(final_ids) - set(expected_ids)), + "evidence_id_set_sha256": hashlib.sha256(set_material.encode("utf-8")).hexdigest(), + } + checks = [ + {"check_id": "shard_completeness", "severity": "BLOCK" if missing or unexpected or stale else "PASS", + "detail": f"missing={len(missing)}, unexpected={len(unexpected)}, stale={len(stale)}"}, + {"check_id": "evidence_index_determinism", "severity": "BLOCK" if parse_failures else "PASS", + "detail": f"parse_or_schema_failures={len(parse_failures)}"}, + {"check_id": "duplicate_document_alias", "severity": "HARD_WARNING" if any(x["exception_type"] == "suspected_duplicate_alias" for x in reviews) else "PASS", + "detail": "duplicate aliases are preserved and routed for review"}, + {"check_id": "document_component_coverage", "severity": "HARD_WARNING" if any(x["exception_type"] in ("mapper_self_warning", "component_coverage_concern") for x in reviews) else "PASS", + "detail": "named component containers and explicit mapper warnings checked without raw-evidence re-read"}, + {"check_id": "authority_drift_detection", "severity": "BLOCK" if any(x["issue_type"] in ("b1_part_schema_or_parse_failure", "evidence_identity_collision") for x in hard) else "PASS", + "detail": "ordinal, source pointer and self-isolation attestation checked"}, + ] + status = "BLOCKED" if blocked else ("READY_WITH_SUSPECTS" if reviews else "READY") + precheck = { + "schema_version": "B1_gate_precheck.v1", "status": status, + "expected_ordinals": expected_ordinals, "received_ordinals": actual_ordinals, + "missing_ordinals": missing, "unexpected_ordinals": unexpected, "stale_parts": stale, + "parse_failures": parse_failures, "hard_gate_findings": hard, + "review_candidates": reviews, "checks": checks, + "metrics": {"input_parts_count": len(actual_ordinals), "final_items_count": len(items), + "input_bytes": input_bytes, "output_bytes": len(final_text.encode("utf-8")), + "elapsed_ms": int((time.monotonic() - started) * 1000)} + } + write_doc(FINAL_PATH, final_obj) + write_doc(MANIFEST_PATH, manifest) + write_doc(PRECHECK_PATH, precheck) + print(json.dumps({"status": status, "final_items": len(items), "review_candidates": len(reviews), + "hard_findings": len(hard)}, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B2_gate_merge_deterministic + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import copy + import hashlib + import itertools + import json + import re + import time + from collections import defaultdict + from datetime import date + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + PLAN_PATH = "evidence_shard_plan.json" + PART_DIR = "evidence_event_candidate_parts" + MANIFEST_PATH = "finalized_evidence_id_manifest.json" + B1_PRECHECK_PATH = "quality_gates/B1_gate_precheck.json" + FINAL_PATH = "evidence_event_candidates.json" + PRECHECK_PATH = "quality_gates/B2_gate_precheck.json" + EXCEPTION_PATH = "quality_gates/B12_exception_pack.json" + ZERO_REASONS = {"NO_LEGAL_EVENT_FOUND", "REFERENCE_ONLY_DOC", "PARSE_INSUFFICIENT", "ORDINAL_MATCH_FAILED"} + CANDIDATE_RE = re.compile(r"^EVT-(\d{3})-(\d{2})$") + CHAIN_ORDER = { + "underlying_claim_arising": ("claim", 0), "first_demand": ("claim", 1), + "demand_notice_dispatch": ("claim", 2), "demand_notice_arrival": ("claim", 3), + "debt_acknowledgement": ("claim", 4), + "lien_extinction_notice_dispatch": ("lien", 0), "lien_extinction_notice_receipt": ("lien", 1), + "lien_extinction_effect_candidate": ("lien", 2), + "secured_debt_creation": ("auction", 0), "mortgage_setting_by_fraction": ("auction", 1), + "auction_application_after_extinguishment": ("auction", 2), + "auction_sale_and_registry_transfer": ("auction", 3), + "preserved_claim_arising": ("actio", 0), "fraudulent_transfer": ("actio", 1), + "encumbrance_release_after_act": ("actio", 2), + } + + def _mid(): return next(MSG_ID) + + def _init(): + r = CLIENT.post(LOCALDOCS_URL, json={ + "jsonrpc": "2.0", "id": 1, "method": "initialize", + "params": {"protocolVersion": "2025-03-26", "capabilities": {}, + "clientInfo": {"name": "task-b2-gate-merge-deterministic", "version": "1.0", + "user_id": "{{__user_hash__}}", "workspace_id": "{{__workspace_hash__}}"}} + }, headers=MCP_HEADERS) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post(LOCALDOCS_URL, json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS).raise_for_status() + + def _parse_response(text: str): + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: return json.loads(line[6:]) + except Exception: pass + try: return json.loads(text) + except Exception: return None + + def _call(name: str, args: dict[str, Any]): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc": "2.0", "id": _mid(), "method": "tools/call", + "params": {"name": name, "arguments": args}}, + headers=MCP_HEADERS) + r.raise_for_status() + payload = _parse_response(r.text) + if not payload or "result" not in payload: raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: dict[str, Any], name: str): + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: raise RuntimeError(f"Empty MCP result: {name}") + try: outer = json.loads(text) + except Exception: return text + if isinstance(outer, dict) and "results" in outer: + row = (outer.get("results") or [{}])[0] + inner = row.get("content") or row.get("text") or "" + if not inner: raise RuntimeError(f"Empty document content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer + + def read_json(path: str): + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str): return raw + try: return json.loads(raw) + except json.JSONDecodeError: + obj, _ = json.JSONDecoder().raw_decode(raw.strip()) + return obj + + def write_doc(path: str, obj: Any): + text = json.dumps(obj, ensure_ascii=False, indent=2) + _call("write_file", {"path": path, "content": text, "overwrite": True}) + return text + + def _walk_strings(value: Any): + if isinstance(value, str): yield value + elif isinstance(value, list): + for item in value: yield from _walk_strings(item) + elif isinstance(value, dict): + for item in value.values(): yield from _walk_strings(item) + + def list_part_names(folder: str): + payload = None + errors = [] + for args in ({"path": folder, "pattern": "E-*.json"}, {}): + try: + payload = _unwrap(_call("list_docs", args), "list_docs") + break + except Exception as exc: errors.append(str(exc)) + if payload is None: raise RuntimeError("list_docs failed: " + " | ".join(errors)) + if isinstance(payload, str): + try: payload = json.loads(payload) + except Exception: pass + names = [] + for value in _walk_strings(payload): + match = re.search(r"(?:^|/)(E-\d{3}\.json)$", value) + if match: names.append(f"{folder}/{match.group(1)}") + return sorted(set(names)) + + def single_item(obj: Any, path: str): + if isinstance(obj, dict) and isinstance(obj.get("items"), list): + if len(obj["items"]) != 1 or not isinstance(obj["items"][0], dict): + raise ValueError(f"{path}: envelope must contain exactly one object") + return obj["items"][0] + if isinstance(obj, dict): return obj + raise ValueError(f"{path}: part must be an object") + + def refs(evidence_indexes=None, candidate_ids=None, meeting_refs=None, titles=None): + return { + "evidence_indexes": sorted(set(str(x) for x in (evidence_indexes or []) if x)), + "event_candidate_ids": sorted(set(str(x) for x in (candidate_ids or []) if x)), + "meeting_clause_refs": sorted(set(str(x) for x in (meeting_refs or []) if x)), + "doc_titles": sorted(set(str(x) for x in (titles or []) if x)), + } + + def hard_finding(code: str, detail: str, evidence_indexes=None, candidate_ids=None, rerun=None): + row = {"issue_type": code.lower(), "source_check": code.lower(), "severity": "BLOCK", + "detail": detail, "source_refs": refs(evidence_indexes, candidate_ids)} + if rerun: row["rerun_target_task"] = rerun + return row + + def review_candidate(kind: str, basis: str, evidence_indexes=None, candidate_ids=None, + titles=None, context=None): + return {"origin_gate": "B2", "exception_type": kind, "severity_candidate": "HARD_WARNING", + "source_refs": refs(evidence_indexes, candidate_ids, titles=titles), + "concise_basis": basis, "compact_context": context or {}} + + def warning_records(node: Any, path: str = "$"): + out = [] + if isinstance(node, dict): + for key, value in node.items(): + child = f"{path}.{key}" + if re.search(r"(self_?)?(warning|error)", str(key), re.I) and value not in (None, False, "", [], {}): + out.append({"path": child, "value": value}) + out.extend(warning_records(value, child)) + elif isinstance(node, list): + for i, value in enumerate(node): out.extend(warning_records(value, f"{path}[{i}]")) + return out + + def parse_date(value: Any): + if not value: return None + text = str(value) + m = re.search(r"(20\d{2}|19\d{2})[-./년]\s*(\d{1,2})[-./월]\s*(\d{1,2})", text) + if not m: return None + try: return date(int(m.group(1)), int(m.group(2)), int(m.group(3))) + except ValueError: return None + + def relation_key(candidate: dict[str, Any]): + legal = candidate.get("legal_effect_candidate") + if not isinstance(legal, dict): legal = {} + explicit = legal.get("related_claim_type") or candidate.get("claim_id") or candidate.get("lien_id") + if not explicit: return None + return json.dumps({"explicit": explicit, "object": candidate.get("object_spec"), + "fraction": candidate.get("fraction_ref")}, ensure_ascii=False, + sort_keys=True, separators=(",", ":")) + + def candidate_rank(row: dict[str, Any]): + candidate = row["candidate"] + subkind = str(candidate.get("event_subkind") or "") + action = str(candidate.get("action_type_candidate") or "").lower() + derived = subkind.endswith("_candidate") or "derived" in action + confidence = {"high": 0, "medium": 1, "low": 2, "unknown": 3}.get(str(candidate.get("confidence")), 4) + supports = candidate.get("support_locators") + support_count = len(supports) if isinstance(supports, list) else 0 + return (1 if derived else 0, confidence, -support_count, row["ordinal"], row["candidate_id"]) + + def build_packs(reviews: list[dict[str, Any]], blocked: bool): + def clipped_context(value: Any, remaining: list[int]): + if isinstance(value, str): + take = max(0, min(len(value), remaining[0])) + remaining[0] -= take + return value[:take] + if isinstance(value, list): + return [clipped_context(x, remaining) for x in value] + if isinstance(value, dict): + return {str(k): clipped_context(v, remaining) for k, v in value.items()} + return value + + prepared = [] + for raw in reviews: + item = copy.deepcopy(raw) + item["compact_context"] = clipped_context(item.get("compact_context") or {}, [1500]) + prepared.append(item) + ordered = sorted(prepared, key=lambda x: (x.get("origin_gate", ""), x.get("exception_type", ""), + json.dumps(x.get("source_refs") or {}, ensure_ascii=False, sort_keys=True), + x.get("concise_basis", ""))) + for i, item in enumerate(ordered, 1): item["exception_id"] = f"B12-EXC-{i:04d}" + if blocked: + return [], [{**item, "reason": "hard_gate_blocked_before_adjudication"} for item in ordered], False + groups = defaultdict(list) + for item in ordered: groups[item["exception_type"]].append(item) + packs = [] + overflow = [] + for exception_type in sorted(groups): + current = [] + for item in groups[exception_type]: + item_refs = item.get("source_refs") or {} + item_evidence = set(item_refs.get("evidence_indexes") or []) + item_candidates = set(item_refs.get("event_candidate_ids") or []) + item_chars = len(json.dumps([item], ensure_ascii=False)) + if len(item_evidence) > 2 or len(item_candidates) > 8 or item_chars > 6000: + overflow.append({**item, "reason": "BUDGET_OVERFLOW_BLOCK_REVIEW"}) + continue + trial = current + [item] + ev = {x for row in trial for x in (row.get("source_refs") or {}).get("evidence_indexes", [])} + cand = {x for row in trial for x in (row.get("source_refs") or {}).get("event_candidate_ids", [])} + chars = len(json.dumps(trial, ensure_ascii=False)) + if current and (len(ev) > 2 or len(cand) > 8 or chars > 6000): + packs.append({"exception_type": exception_type, "items": current}) + current = [item] + else: + current = trial + if current: packs.append({"exception_type": exception_type, "items": current}) + if len(packs) > 8: + for pack in packs[8:]: + overflow.extend({**item, "reason": "BUDGET_OVERFLOW_BLOCK_REVIEW"} for item in pack["items"]) + packs = packs[:8] + descriptors = [] + for i, pack in enumerate(packs, 1): + pack_id = f"PACK-{i:03d}" + descriptors.append({ + "pack_id": pack_id, + "output_path": f"quality_gates/B12_adjudication_parts/{pack_id}.json", + "exception_type": pack["exception_type"], + "exception_ids": [x["exception_id"] for x in pack["items"]], + "items": pack["items"], + }) + return descriptors, overflow, bool(overflow) + + def main(): + started = time.monotonic() + _init() + plan = read_json(PLAN_PATH) + manifest = read_json(MANIFEST_PATH) + b1_precheck = read_json(B1_PRECHECK_PATH) + expected_ordinals = [str(x).zfill(3) for x in plan.get("expected_ordinals") or []] + expected_ids = [str(x) for x in manifest.get("finalized_evidence_ids") or []] + shard_count = int(plan.get("shard_count") or len(expected_ordinals)) + hard = [] + reviews = list(b1_precheck.get("review_candidates") or []) + b2_reviews = [] + schema_findings = [] + dedup_findings = [] + field_failures = [] + if manifest.get("status") != "READY" or b1_precheck.get("status") == "BLOCKED": + hard.append(hard_finding("UPSTREAM_B1_BLOCKED", "B1 deterministic gate is blocked")) + actual_names = list_part_names(PART_DIR) + actual_ordinals = sorted({re.search(r"E-(\d{3})\.json$", x).group(1) for x in actual_names + if re.search(r"E-(\d{3})\.json$", x)}) + missing = sorted(set(expected_ordinals) - set(actual_ordinals)) + unexpected = sorted(set(actual_ordinals) - set(expected_ordinals)) + stale = sorted(set((plan.get("stale_part_policy") or {}).get("detected_stale_parts", {}) + .get("evidence_event_candidate_parts") or []) | + {f"{PART_DIR}/E-{x}.json" for x in unexpected}) + for ordinal in missing: + hard.append(hard_finding("MISSING_B2_PART", f"missing B2 mapper part E-{ordinal}", + [f"E-{ordinal}"], rerun=f"Task_B2_map_events_{ordinal}")) + for ordinal in unexpected: + hard.append(hard_finding("UNEXPECTED_B2_PART", f"unexpected B2 mapper part E-{ordinal}", + [f"E-{ordinal}"])) + for path in stale: hard.append(hard_finding("STALE_B2_PART", f"stale B2 mapper part: {path}")) + + final_items = [] + candidate_rows = [] + all_candidate_ids = [] + input_candidate_count = 0 + input_bytes = 0 + for ordinal in expected_ordinals: + path = f"{PART_DIR}/E-{ordinal}.json" + if ordinal in missing: continue + evidence_id = f"E-{ordinal}" + try: + raw = read_json(path) + input_bytes += len(json.dumps(raw, ensure_ascii=False).encode("utf-8")) + item = single_item(raw, path) + if str(item.get("evidence_index_proposed") or "") != evidence_id: + raise ValueError("evidence_index_proposed mismatch") + candidates = item.get("event_candidates") + zero_reason = item.get("zero_event_reason") + if not isinstance(candidates, list): raise ValueError("event_candidates must be an array") + if not candidates and zero_reason not in ZERO_REASONS: + raise ValueError("empty event_candidates requires an allowed non-null zero_event_reason") + final_item = copy.deepcopy(item) + final_item["evidence_index"] = final_item.pop("evidence_index_proposed") + final_item.pop("mapper_meta", None) + final_candidates = [] + input_candidate_count += len(candidates) + for seq, candidate in enumerate(candidates, 1): + if not isinstance(candidate, dict): raise ValueError(f"candidate {seq} is not an object") + proposed = str(candidate.get("candidate_id_proposed") or "") + source = str(candidate.get("source_evidence_index_proposed") or "") + match = CANDIDATE_RE.fullmatch(proposed) + if not match or match.group(1) != ordinal: + raise ValueError(f"invalid candidate_id_proposed: {proposed}") + if source != evidence_id or source not in expected_ids: + raise ValueError(f"invalid source evidence reference: {source}") + if not candidate.get("identity_signature"): + raise ValueError(f"{proposed}: identity_signature missing") + projected = copy.deepcopy(candidate) + projected["candidate_id"] = projected.pop("candidate_id_proposed") + projected["source_evidence_index"] = projected.pop("source_evidence_index_proposed") + expected_keys = (set(candidate) - {"candidate_id_proposed", "source_evidence_index_proposed"} | + {"candidate_id", "source_evidence_index"}) + if set(projected) != expected_keys: + field_failures.append({"candidate_id": proposed, "reason": "key set mismatch"}) + restored = copy.deepcopy(projected) + restored["candidate_id_proposed"] = restored.pop("candidate_id") + restored["source_evidence_index_proposed"] = restored.pop("source_evidence_index") + if restored != candidate: + field_failures.append({"candidate_id": proposed, "reason": "deep value mismatch"}) + final_candidates.append(projected) + all_candidate_ids.append(proposed) + candidate_rows.append({"ordinal": int(ordinal), "evidence_index": evidence_id, + "candidate_id": proposed, "candidate": projected, + "title": item.get("title")}) + for flag in warning_records(candidate): + b2_reviews.append(review_candidate("mapper_self_warning", + "B2 mapper emitted an explicit warning or error", + [evidence_id], [proposed], [item.get("title")], + {"warning": flag})) + final_item["event_candidates"] = final_candidates + final_items.append(final_item) + except Exception as exc: + row = {"ordinal": ordinal, "part_path": path, "reason": str(exc), "severity": "BLOCK", + "rerun_target_task": f"Task_B2_map_events_{ordinal}"} + schema_findings.append(row) + hard.append(hard_finding("B2_PART_SCHEMA_OR_PARSE_FAILURE", str(exc), [evidence_id], + rerun=f"Task_B2_map_events_{ordinal}")) + + if len(all_candidate_ids) != len(set(all_candidate_ids)): + hard.append(hard_finding("DUPLICATE_FINAL_CANDIDATE_ID", "candidate IDs are not globally unique", + candidate_ids=all_candidate_ids)) + if field_failures: + hard.append(hard_finding("CANDIDATE_FIELD_CONSERVATION_FAILURE", + f"{len(field_failures)} candidate projection failures", + candidate_ids=[x["candidate_id"] for x in field_failures])) + + by_signature = defaultdict(list) + for row in candidate_rows: by_signature[str(row["candidate"].get("identity_signature"))].append(row) + for signature, rows in sorted(by_signature.items()): + if not signature or len(rows) < 2: continue + canonical = sorted(rows, key=candidate_rank)[0] + ids = [row["candidate_id"] for row in rows] + evs = [row["evidence_index"] for row in rows] + dedup_findings.append({"identity_signature": signature, "candidate_ids": ids, + "canonical_candidate_id": canonical["candidate_id"], + "preservation_policy": "all_candidates_preserved"}) + b2_reviews.append(review_candidate("suspected_duplicate_alias", + "same identity signature appears in multiple evidence candidates", + evs, ids, [row["title"] for row in rows], + {"canonical_candidate_id": canonical["candidate_id"]})) + + chronology_groups = defaultdict(list) + for row in candidate_rows: + subkind = str(row["candidate"].get("event_subkind") or "") + if subkind not in CHAIN_ORDER: continue + key = relation_key(row["candidate"]) + event_date = parse_date(row["candidate"].get("event_date")) + if key and event_date: + chain, position = CHAIN_ORDER[subkind] + chronology_groups[(chain, key)].append((position, event_date, row)) + for (chain, _), rows in chronology_groups.items(): + rows.sort(key=lambda x: x[0]) + for left, right in zip(rows, rows[1:]): + if left[1] > right[1]: + involved = [left[2], right[2]] + b2_reviews.append(review_candidate("chronology_conflict", + f"deterministic date inversion in {chain} chain", + [x["evidence_index"] for x in involved], + [x["candidate_id"] for x in involved], + [x["title"] for x in involved], + {"left_date": str(left[1]), "right_date": str(right[1])})) + + # Defense assertions are explicit routing facts, not ambiguity exceptions. + # Relay them as one deterministic Stage 2 review item without an LLM call. + defense_rows = [] + for row in candidate_rows: + candidate = row["candidate"] + defense = candidate.get("defense_candidate") + legal = candidate.get("legal_effect_candidate") + subkind = str(candidate.get("event_subkind") or "") + needs_rebuttal = isinstance(defense, dict) and defense.get("requires_rebuttal_in_stage2") is True + explicit_defense = subkind.startswith("defense_assertion") + legal_defense = isinstance(legal, dict) and legal.get("effect_type") == "defense_assertion" + if needs_rebuttal or explicit_defense or legal_defense: + defense_rows.append(row) + auto_review_findings = [] + if defense_rows: + auto_review_findings.append({ + "origin_gate": "B2", + "exception_type": "defense_and_obstacle_review", + "severity": "SOFT_WARNING", + "source_check": "deterministic_defense_assertion_relay", + "source_refs": refs( + [row["evidence_index"] for row in defense_rows], + [row["candidate_id"] for row in defense_rows], + titles=[row["title"] for row in defense_rows], + ), + "candidate_use_policy": "preserve_as_needs_review", + "review_instruction": "Stage 2에서 상계, 책임부인 및 기타 방어 주장의 성립과 반박 필요성을 검토한다.", + "concise_basis": "구조화된 defense assertion 후보를 후속 방어·장애사유 검토로 전달", + }) + + final_items.sort(key=lambda row: int(str(row.get("evidence_index")).split("-")[-1])) + final_candidate_count = sum(len(row.get("event_candidates") or []) for row in final_items) + final_ids = [str(row.get("evidence_index")) for row in final_items] + if set(final_ids) != set(expected_ids) or len(final_items) != shard_count: + hard.append(hard_finding("EVENT_ITEM_CONSERVATION_FAILURE", + f"expected {shard_count} items, got {len(final_items)}", + sorted(set(expected_ids) ^ set(final_ids)))) + if input_candidate_count != final_candidate_count: + hard.append(hard_finding("CANDIDATE_CONSERVATION_FAILURE", + f"input={input_candidate_count}, final={final_candidate_count}")) + + final_obj = {"schema_version": "evidence_event_candidates.v1", "items": final_items} + final_text = json.dumps(final_obj, ensure_ascii=False, indent=2) + reparsed = json.loads(final_text) + if sum(len(x.get("event_candidates") or []) for x in reparsed["items"]) != input_candidate_count: + raise RuntimeError("post-serialization candidate conservation failed") + reviews.extend(b2_reviews) + blocked = bool(hard) + packs, auto_blocked, overflow = build_packs(reviews, blocked) + exception_status = "BLOCKED" if blocked else ("READY" if packs else "READY_NO_EXCEPTIONS") + exception_obj = { + "schema_version": "stage1_part1_gate_exception_pack.v1", + "status": exception_status, "exception_count": len(reviews), "pack_count": len(packs), + "budget_gate": {"max_evidence_items_per_pack": 2, "max_event_candidates_per_pack": 8, + "max_excerpt_chars_per_pack": 1500, "max_serialized_chars_per_pack": 6000, + "max_pack_count": 8, "overflow_detected": overflow}, + "packs": packs, "auto_blocked_review_items": auto_blocked, + } + checks = [ + {"check_id": "event_candidate_completeness", "severity": "BLOCK" if missing or unexpected or stale else "PASS", + "detail": f"missing={len(missing)}, unexpected={len(unexpected)}, stale={len(stale)}"}, + {"check_id": "evidence_reference_integrity", "severity": "BLOCK" if any(x["issue_type"] == "b2_part_schema_or_parse_failure" for x in hard) else "PASS", + "detail": "all candidate evidence references checked against compact manifest"}, + {"check_id": "candidate_id_collision", "severity": "BLOCK" if len(all_candidate_ids) != len(set(all_candidate_ids)) else ("HARD_WARNING" if dedup_findings else "PASS"), + "detail": f"duplicate_signature_groups={len(dedup_findings)}; candidates are not deleted"}, + {"check_id": "chronological_ordering", "severity": "HARD_WARNING" if any(x["exception_type"] == "chronology_conflict" for x in b2_reviews) else "PASS", + "detail": "only exact dates with an explicit relation key are compared"}, + {"check_id": "legal_effect_event_coverage", "severity": "HARD_WARNING" if any(x["exception_type"] == "mapper_self_warning" for x in b2_reviews) else "PASS", + "detail": "explicit mapper warnings are escalated without re-reading raw evidence"}, + ] + status = "BLOCKED" if blocked else ( + "READY_WITH_SUSPECTS" if reviews or auto_review_findings else "READY") + precheck = { + "schema_version": "B2_gate_precheck.v1", "status": status, + "upstream_b1_status": b1_precheck.get("status"), + "expected_ordinals": expected_ordinals, "received_ordinals": actual_ordinals, + "missing_ordinals": missing, "unexpected_ordinals": unexpected, "stale_parts": stale, + "schema_invariant_findings": schema_findings, "hard_gate_findings": hard, + "review_candidates": b2_reviews, "auto_review_findings": auto_review_findings, + "dedup_findings": dedup_findings, + "conservation": {"expected_items": shard_count, "final_items": len(final_items), + "input_candidates": input_candidate_count, "final_candidates": final_candidate_count, + "candidate_field_failures": field_failures}, + "checks": checks, + "metrics": {"input_bytes": input_bytes, "output_bytes": len(final_text.encode("utf-8")), + "elapsed_ms": int((time.monotonic() - started) * 1000)} + } + write_doc(FINAL_PATH, final_obj) + write_doc(PRECHECK_PATH, precheck) + write_doc(EXCEPTION_PATH, exception_obj) + print(json.dumps({"dynamic_fanout": packs, "exception_status": exception_status, + "exception_pack_count": len(packs)}, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B1B2_gate_llm_adjudicator_* + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: medium + llm_verbosity: low + max_iterations: 2 + cache_control: + mode: auto + ttl: 20m + use_tools: + - localdocs + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Execute only this compact Stage 1 exception-adjudication sub-task. + - The assigned pack, output schema, and writer boundary override broader instructions. + - Use only the assigned pack embedded below. + - Do not read or reconstruct any full planner, evidence, event, meeting, BO, or downstream file. + - Do not create facts, dates, amounts, parties, IDs, or source references. + - Write only the assigned adjudication output path. + - Do not modify any final evidence/event artifact, mapper part, precheck, or audit. + - Return only short basis fields; keep chain-of-thought private. + - Do not draft complaint prose or decide final merits. + + + + { + "pack_id": "{{item.pack_id}}", + "output_path": "{{item.output_path}}", + "exception_type": "{{item.exception_type}}", + "exception_ids": {{item.exception_ids}}, + "items": {{item.items}} + } + + + + You are a compact exception-only adjudicator assisting a top-tier Korean civil-litigation lawyer. + Validate pack_id, exception_type, exception_ids, items, and output_path. + Produce exactly one decision for every assigned exception_id and no others. + Allowed decisions are PASS_NO_ACTION, REVIEW_SOFT, REVIEW_HARD, BLOCK_SOURCE_INTEGRITY, BLOCK_REVIEW. + Use BLOCK_SOURCE_INTEGRITY only when source contamination or identity failure is explicit in the pack. + Use BLOCK_REVIEW when the compact record is insufficient. Legal ambiguity is not a Stage 1 integrity BLOCK. + Source refs in each decision must be a subset of that exception item's supplied source_refs. + Severity mapping is exact: + PASS_NO_ACTION -> PASS + REVIEW_SOFT -> SOFT_WARNING + REVIEW_HARD -> HARD_WARNING + BLOCK_REVIEW -> HARD_WARNING + BLOCK_SOURCE_INTEGRITY -> BLOCK + + + + { + "schema_version": "stage1_part1_gate_adjudication_part.v1", + "pack_id": "{{item.pack_id}}", + "status": "READY|READY_WITH_BLOCK_REVIEW|FAILED", + "decisions": [ + { + "exception_id": "B12-EXC-0001", + "exception_type": "assigned exception type", + "decision": "PASS_NO_ACTION|REVIEW_SOFT|REVIEW_HARD|BLOCK_SOURCE_INTEGRITY|BLOCK_REVIEW", + "severity": "PASS|SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "assigned exception type", + "source_refs": { + "evidence_indexes": [], + "event_candidate_ids": [], + "meeting_clause_refs": [], + "doc_titles": [] + }, + "candidate_use_policy": "prioritize_if_supported|preserve_as_needs_review|do_not_generate_without_support", + "review_instruction": "120 characters or fewer", + "concise_basis": "240 characters or fewer" + } + ] + } + + + + 1. Build the exact JSON object in memory. + 2. Call localdocs write_file with path={{item.output_path}}, overwrite=true, and the raw JSON object serialized as JSON. + 3. Do not write any other file. + 4. End with exactly: {{item.pack_id}} adjudication written + + + - task_name: Task_B12_gate_audit_finalizer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import itertools + import json + from datetime import datetime, timezone + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + B1_PRECHECK = "quality_gates/B1_gate_precheck.json" + B2_PRECHECK = "quality_gates/B2_gate_precheck.json" + EXCEPTION_PACK = "quality_gates/B12_exception_pack.json" + B1_AUDIT = "quality_gates/B1_evidence_indexed_gate.json" + B2_AUDIT = "quality_gates/B2_event_candidates_gate.json" + REF_KEYS = ("evidence_indexes", "event_candidate_ids", "meeting_clause_refs", "doc_titles") + ALLOWED_DECISIONS = {"PASS_NO_ACTION", "REVIEW_SOFT", "REVIEW_HARD", + "BLOCK_SOURCE_INTEGRITY", "BLOCK_REVIEW"} + DECISION_SEVERITY = {"PASS_NO_ACTION": "PASS", "REVIEW_SOFT": "SOFT_WARNING", + "REVIEW_HARD": "HARD_WARNING", "BLOCK_REVIEW": "HARD_WARNING", + "BLOCK_SOURCE_INTEGRITY": "BLOCK"} + + def _mid(): return next(MSG_ID) + + def _init(): + r = CLIENT.post(LOCALDOCS_URL, json={ + "jsonrpc": "2.0", "id": 1, "method": "initialize", + "params": {"protocolVersion": "2025-03-26", "capabilities": {}, + "clientInfo": {"name": "task-b12-gate-audit-finalizer", "version": "1.0", + "user_id": "{{__user_hash__}}", "workspace_id": "{{__workspace_hash__}}"}} + }, headers=MCP_HEADERS) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post(LOCALDOCS_URL, json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS).raise_for_status() + + def _parse_response(text: str): + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: return json.loads(line[6:]) + except Exception: pass + try: return json.loads(text) + except Exception: return None + + def _call(name: str, args: dict[str, Any]): + r = CLIENT.post(LOCALDOCS_URL, json={"jsonrpc": "2.0", "id": _mid(), "method": "tools/call", + "params": {"name": name, "arguments": args}}, + headers=MCP_HEADERS) + r.raise_for_status() + payload = _parse_response(r.text) + if not payload or "result" not in payload: raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: dict[str, Any], name: str): + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: raise RuntimeError(f"Empty MCP result: {name}") + try: outer = json.loads(text) + except Exception: return text + if isinstance(outer, dict) and "results" in outer: + row = (outer.get("results") or [{}])[0] + inner = row.get("content") or row.get("text") or "" + if not inner: raise RuntimeError(f"Empty document content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer + + def read_json(path: str): + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str): return raw + try: return json.loads(raw) + except json.JSONDecodeError: + obj, _ = json.JSONDecoder().raw_decode(raw.strip()) + return obj + + def write_doc(path: str, obj: Any): + text = json.dumps(obj, ensure_ascii=False, indent=2) + json.loads(text) + _call("write_file", {"path": path, "content": text, "overwrite": True}) + + def norm_refs(value: Any): + obj = value if isinstance(value, dict) else {} + return {key: sorted(set(str(x) for x in (obj.get(key) or []) if x)) for key in REF_KEYS} + + def source_subset(candidate: dict[str, list[str]], allowed: dict[str, list[str]]): + return all(set(candidate[key]).issubset(set(allowed[key])) for key in REF_KEYS) + + def normalized_hard(raw: Any, origin: str, seq: int): + row = raw if isinstance(raw, dict) else {"detail": str(raw)} + return { + "gate_finding_id": str(row.get("gate_finding_id") or f"{origin}-HARD-{seq:03d}"), + "source_check": str(row.get("source_check") or row.get("issue_type") or "integrity_failure"), + "severity": "BLOCK", + "issue_type": str(row.get("issue_type") or "integrity_failure"), + "source_refs": norm_refs(row.get("source_refs")), + "detail": str(row.get("detail") or row.get("reason") or "integrity failure"), + **({"rerun_target_task": row.get("rerun_target_task")} if row.get("rerun_target_task") else {}), + } + + def review_from_item(item: dict[str, Any], severity: str, basis: str, decision: str): + return { + "source_check": str(item.get("source_check") or item.get("exception_type") or "review_finding"), + "severity": severity, + "issue_type": str(item.get("exception_type") or "review_finding"), + "domain_hints": list(item.get("domain_hints") or []), + "target_stage_hints": list(item.get("target_stage_hints") or []), + "target_task_hints": list(item.get("target_task_hints") or []), + "source_refs": norm_refs(item.get("source_refs")), + "review_instruction": str(item.get("review_instruction") or "후속 검토 단계에서 증거와 법률상 의미를 재검토한다.")[:120], + "candidate_use_policy": str(item.get("candidate_use_policy") or "preserve_as_needs_review"), + "concise_basis": str(basis or item.get("concise_basis") or decision)[:240], + "decision": decision, + } + + def severity_of(hard: list[Any], reviews: list[dict[str, Any]]): + if hard: return "BLOCK" + if any(x.get("severity") == "HARD_WARNING" for x in reviews): return "HARD_WARNING" + if reviews: return "SOFT_WARNING" + return "PASS" + + def progression(severity: str): + return "BLOCKED" if severity == "BLOCK" else ("ALLOWED" if severity == "PASS" else "ALLOWED_WITH_WARNING") + + def clean_checks(value: Any): + out = [] + for row in value if isinstance(value, list) else []: + if not isinstance(row, dict): continue + out.append({k: v for k, v in row.items() if k not in ("hard_gate_findings", "review_findings")}) + return out + + def main(): + _init() + b1 = read_json(B1_PRECHECK) + b2 = read_json(B2_PRECHECK) + pack = read_json(EXCEPTION_PACK) + b1_hard = [normalized_hard(x, "B1", i) for i, x in enumerate(b1.get("hard_gate_findings") or [], 1)] + b2_hard = [normalized_hard(x, "B2", i) for i, x in enumerate(b2.get("hard_gate_findings") or [], 1)] + exception_map = {} + pack_by_id = {} + for descriptor in pack.get("packs") or []: + pack_id = str(descriptor.get("pack_id") or "") + pack_by_id[pack_id] = descriptor + for item in descriptor.get("items") or []: + if isinstance(item, dict): exception_map[str(item.get("exception_id"))] = item + + decisions = {} + for pack_id, descriptor in sorted(pack_by_id.items()): + output_path = str(descriptor.get("output_path") or "") + try: + adjudication = read_json(output_path) + if adjudication.get("schema_version") != "stage1_part1_gate_adjudication_part.v1": + raise ValueError("schema_version mismatch") + if adjudication.get("pack_id") != pack_id: raise ValueError("pack_id mismatch") + expected = set(str(x) for x in descriptor.get("exception_ids") or []) + seen = [] + for decision in adjudication.get("decisions") or []: + if not isinstance(decision, dict): raise ValueError("decision must be an object") + exception_id = str(decision.get("exception_id") or "") + if exception_id not in expected or exception_id in decisions: + raise ValueError(f"unexpected or duplicate exception_id {exception_id}") + code = str(decision.get("decision") or "") + if code not in ALLOWED_DECISIONS: raise ValueError(f"invalid decision {code}") + if decision.get("severity") != DECISION_SEVERITY[code]: + raise ValueError(f"severity mismatch for {exception_id}") + item = exception_map[exception_id] + decision_refs = norm_refs(decision.get("source_refs")) + if not source_subset(decision_refs, norm_refs(item.get("source_refs"))): + raise ValueError(f"ADJUDICATION_SOURCE_REF_OUT_OF_SCOPE: {exception_id}") + decision = {**decision, "source_refs": decision_refs} + decisions[exception_id] = decision + seen.append(exception_id) + if set(seen) != expected: raise ValueError("decision set does not equal expected exception set") + except Exception as exc: + origins = {str(exception_map[x].get("origin_gate") or "B2") for x in descriptor.get("exception_ids") or [] + if x in exception_map} + target = b1_hard if origins == {"B1"} else b2_hard + target.append(normalized_hard({"issue_type": "adjudication_part_missing_or_invalid", + "detail": f"{output_path}: {exc}"}, "B1" if target is b1_hard else "B2", + len(target) + 1)) + + b1_reviews = [] + b2_reviews = [] + for item in b2.get("auto_review_findings") or []: + if not isinstance(item, dict): continue + b2_reviews.append(review_from_item( + item, + str(item.get("severity") or "SOFT_WARNING"), + str(item.get("concise_basis") or "deterministic review relay"), + "AUTO_REVIEW", + )) + for exception_id, item in sorted(exception_map.items()): + decision = decisions.get(exception_id) + if not decision: continue + code = decision["decision"] + origin = str(item.get("origin_gate") or "B2") + if code == "PASS_NO_ACTION": continue + if code == "BLOCK_SOURCE_INTEGRITY": + target = b1_hard if origin == "B1" else b2_hard + target.append(normalized_hard({"issue_type": item.get("exception_type"), + "source_refs": decision.get("source_refs"), + "detail": decision.get("concise_basis")}, origin, + len(target) + 1)) + continue + row = review_from_item({**item, **decision}, DECISION_SEVERITY[code], + str(decision.get("concise_basis") or ""), code) + (b1_reviews if origin == "B1" else b2_reviews).append(row) + + for item in pack.get("auto_blocked_review_items") or []: + if not isinstance(item, dict): continue + origin = str(item.get("origin_gate") or "B2") + row = review_from_item(item, "HARD_WARNING", str(item.get("reason") or "BLOCK_REVIEW"), + "BLOCK_REVIEW") + (b1_reviews if origin == "B1" else b2_reviews).append(row) + + all_reviews = b1_reviews + b2_reviews + if len(all_reviews) > 40: + overflow = all_reviews[40:] + b1_count = min(len(b1_reviews), 40) + b1_reviews = all_reviews[:b1_count] + b2_reviews = all_reviews[b1_count:40] + b2_hard.append(normalized_hard({ + "issue_type": "review_queue_overflow", + "detail": f"review findings exceed SHA writer limit: total={len(all_reviews)}, overflow={len(overflow)}", + "source_refs": {"evidence_indexes": sorted({x for row in overflow for x in row["source_refs"]["evidence_indexes"]})} + }, "B2", len(b2_hard) + 1)) + + for origin, rows in (("B1", b1_reviews), ("B2", b2_reviews)): + for i, row in enumerate(rows, 1): row["gate_finding_id"] = f"{origin}-REVIEW-{i:03d}" + b1_severity = severity_of(b1_hard, b1_reviews) + b2_severity = severity_of(b2_hard, b2_reviews) + highest_global = "HARD_WARNING" if any(x.get("severity") == "HARD_WARNING" for x in b1_reviews + b2_reviews) else ( + "SOFT_WARNING" if b1_reviews or b2_reviews else "PASS") + now = datetime.now(timezone.utc).isoformat() + + def risk_notes(rows): + return [str(x.get("concise_basis") or x.get("issue_type"))[:120] for x in rows] + + b1_audit = { + "gate_id": "B1_evidence_indexed", "executed_at": now, + "checks": clean_checks(b1.get("checks")), "overall_severity": b1_severity, + "block_reasons": [x["detail"] for x in b1_hard], + "warning_reasons": risk_notes(b1_reviews), + "hard_gate_findings": b1_hard, "review_findings": b1_reviews, + "rerun_targets": sorted({x.get("rerun_target_task") for x in b1_hard if x.get("rerun_target_task")}), + "stage2_risk_notes": risk_notes(b1_reviews), + "review_gate_summary": {"review_gate_count": len(b1_reviews), + "highest_review_severity": severity_of([], b1_reviews)}, + "handoff_export_summary": {"is_soft_handoff_writer": False, + "handoff_writer_task": "Task_B2_SHA256_soft_gate_handoff_writer", + "exportable_review_count": len(b1_reviews)}, + "downstream_progression": progression(b1_severity), + "stage2_auto_progression_allowed": b1_severity != "BLOCK", + "schema_contract_version": "B1_quality_gate.v4", + } + b2_audit = { + "gate_id": "B2_event_candidates", "executed_at": now, + "schema_invariant_findings": b2.get("schema_invariant_findings") or [], + "checks": clean_checks(b2.get("checks")), "overall_severity": b2_severity, + "block_reasons": [x["detail"] for x in b2_hard], + "warning_reasons": risk_notes(b2_reviews), + "hard_gate_findings": b2_hard, "review_findings": b2_reviews, + "rerun_targets": sorted({x.get("rerun_target_task") for x in b2_hard if x.get("rerun_target_task")}), + "stage2_risk_notes": risk_notes(b2_reviews), + "review_gate_summary": {"review_gate_count": len(b2_reviews), + "highest_review_severity": severity_of([], b2_reviews), + "b1_review_count": len(b1_reviews), "b2_review_count": len(b2_reviews)}, + "handoff_export_summary": {"is_soft_handoff_writer": False, + "handoff_writer_task": "Task_B2_SHA256_soft_gate_handoff_writer", + "handoff_path": "quality_gates/stage1_part1_soft_gate_handoff.json", + "exported_review_count": len(b2_reviews), + "handoff_status_candidate": "BLOCKED" if b1_hard or b2_hard else ( + "READY_WITH_REVIEW" if b1_reviews or b2_reviews else "READY_NO_REVIEW")}, + "downstream_progression": progression(b2_severity), + "stage2_auto_progression_allowed": b2_severity != "BLOCK", + "schema_contract_version": "B2_quality_gate.v4", + } + if b1_audit["review_gate_summary"]["review_gate_count"] != len(b1_audit["review_findings"]): + raise RuntimeError("B1 review count mismatch") + if b2_audit["review_gate_summary"]["review_gate_count"] != len(b2_audit["review_findings"]): + raise RuntimeError("B2 review count mismatch") + for audit in (b1_audit, b2_audit): + for row in audit["hard_gate_findings"] + audit["review_findings"]: + if set((row.get("source_refs") or {}).keys()) != set(REF_KEYS): + raise RuntimeError("source_refs named-object schema violation") + write_doc(B1_AUDIT, b1_audit) + write_doc(B2_AUDIT, b2_audit) + print(json.dumps({"status": "AUDITS_FINALIZED", "b1_severity": b1_severity, + "b2_severity": b2_severity, "review_count": len(b1_reviews) + len(b2_reviews), + "highest_review_severity": highest_global}, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + - task_name: Task_B2_SHA256_soft_gate_handoff_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + import hashlib + import itertools + import json + import re + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + SIDE_CAR_PATH = "quality_gates/stage1_part1_soft_gate_handoff.json" + INPUT_PATHS = { + "evidence_indexed_sha256": "evidence_indexed.json", + "evidence_event_candidates_sha256": "evidence_event_candidates.json", + "b1_gate_sha256": "quality_gates/B1_evidence_indexed_gate.json", + "b2_gate_sha256": "quality_gates/B2_event_candidates_gate.json", + } + WORKFLOW_ROLES = ( + "claim_right_identification", + "cause_of_action_element_build", + "evidence_weight_and_gap_check", + "chronology_and_state_split", + "asset_registry_succession_check", + "defense_and_obstacle_check", + "relief_scope_and_quantification", + "actio_value_compensation_check", + ) + SOURCE_REF_KEYS = ( + "evidence_indexes", + "event_candidate_ids", + "meeting_clause_refs", + "doc_titles", + ) + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-b2-sha256-soft-gate-handoff-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _mid(), + "method": "tools/call", + "params": {"name": name, "arguments": args}, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def _unwrap(payload: Any, name: str) -> str: + text = (payload["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError(f"Empty: {name}") + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, dict) and "results" in outer: + result0 = (outer.get("results") or [{}])[0] + inner = result0.get("content") or result0.get("text") or "" + if not inner: + raise RuntimeError(f"Empty content: {name}") + return inner if isinstance(inner, str) else json.dumps(inner, ensure_ascii=False) + return outer if isinstance(outer, str) else json.dumps(outer, ensure_ascii=False) + + def read_raw(path: str) -> str: + raw = _unwrap(_call("read_docs", {"doc_names": [path]}), path) + if not isinstance(raw, str) or not raw.strip(): + raise RuntimeError(f"Missing or empty required input: {path}") + return raw + + def parse_json(raw: str, path: str) -> Any: + try: + return json.loads(raw) + except json.JSONDecodeError: + decoder = json.JSONDecoder() + try: + obj, _ = decoder.raw_decode(raw.strip()) + return obj + except Exception as exc: + raise RuntimeError(f"Invalid JSON input: {path}") from exc + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def digest(raw: str) -> str: + value = hashlib.sha256(raw.encode("utf-8")).hexdigest() + if not re.fullmatch(r"[0-9a-f]{64}", value): + raise RuntimeError("digest_guard produced a non-SHA256 value") + return value + + def as_list(value: Any) -> list[Any]: + if value is None: + return [] + if isinstance(value, list): + return value + return [value] + + def as_str_list(value: Any) -> list[str]: + out: list[str] = [] + for item in as_list(value): + if item is None: + continue + if isinstance(item, dict): + for key in ("id", "value", "name", "title", "ref"): + if item.get(key): + out.append(str(item[key]).strip()) + break + else: + out.append(str(item).strip()) + return [x for x in out if x] + + def clean_enum(value: Any, fallback: str) -> str: + text = str(value or fallback).strip().lower() + text = re.sub(r"[^a-z0-9_]+", "_", text) + text = re.sub(r"_+", "_", text).strip("_") + return text or fallback + + def clip(value: Any, limit: int = 120) -> str: + text = " ".join(str(value or "").split()) + if len(text) <= limit: + return text + return text[:limit].rstrip() + "..." + + def normalize_severity(value: Any) -> str: + text = str(value or "").upper() + if "HARD" in text: + return "HARD_WARNING" + return "SOFT_WARNING" + + def role_for(issue_type: str, finding: dict[str, Any]) -> str: + explicit = finding.get("lawyer_workflow_role") + if explicit in WORKFLOW_ROLES: + return str(explicit) + text = " ".join([ + issue_type, + str(finding.get("source_check") or ""), + str(finding.get("check_id") or ""), + str(finding.get("detail") or finding.get("reason") or ""), + ]).lower() + if any(k in text for k in ("chrono", "date", "timeline", "state", "order")): + return "chronology_and_state_split" + if any(k in text for k in ("evidence", "proof", "source", "locator")): + return "evidence_weight_and_gap_check" + if any(k in text for k in ("registry", "secured", "lien", "succession", "asset", "auction", "mortgage")): + return "asset_registry_succession_check" + if any(k in text for k in ("defense", "obstacle", "setoff", "limitation")): + return "defense_and_obstacle_check" + if any(k in text for k in ("relief", "amount", "scope", "valuation", "damage")): + return "relief_scope_and_quantification" + if "actio" in text or "fraudulent" in text or "value_compensation" in text: + return "actio_value_compensation_check" + if any(k in text for k in ("claim", "right", "element", "legal_effect")): + return "claim_right_identification" + return "cause_of_action_element_build" + + def source_refs(finding: dict[str, Any]) -> dict[str, list[str]]: + refs = {key: [] for key in SOURCE_REF_KEYS} + nested = finding.get("source_refs") + if isinstance(nested, dict): + for key in SOURCE_REF_KEYS: + refs[key].extend(as_str_list(nested.get(key))) + refs["evidence_indexes"].extend(as_str_list(finding.get("evidence_indexes"))) + refs["evidence_indexes"].extend(as_str_list(finding.get("evidence_index"))) + refs["evidence_indexes"].extend(as_str_list(finding.get("source_evidence_index"))) + refs["event_candidate_ids"].extend(as_str_list(finding.get("event_candidate_ids"))) + refs["event_candidate_ids"].extend(as_str_list(finding.get("candidate_id"))) + refs["event_candidate_ids"].extend(as_str_list(finding.get("candidate_ids"))) + refs["meeting_clause_refs"].extend(as_str_list(finding.get("meeting_clause_refs"))) + refs["meeting_clause_refs"].extend(as_str_list(finding.get("meeting_clause_ref"))) + refs["doc_titles"].extend(as_str_list(finding.get("doc_titles"))) + refs["doc_titles"].extend(as_str_list(finding.get("doc_title"))) + return {key: sorted(set(value)) for key, value in refs.items()} + + def extract_findings(gate: Any, key: str) -> list[Any]: + findings: list[Any] = [] + if isinstance(gate, dict): + findings.extend(as_list(gate.get(key))) + for check in as_list(gate.get("checks")): + if isinstance(check, dict): + findings.extend(as_list(check.get(key))) + return findings + + def hard_findings(gate: Any, source_gate: str) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for item in extract_findings(gate, "hard_gate_findings"): + out.append(item if isinstance(item, dict) else {"detail": str(item)}) + if isinstance(gate, dict): + blocked = gate.get("overall_severity") == "BLOCK" or gate.get("downstream_progression") == "BLOCKED" or gate.get("stage2_auto_progression_allowed") is False + if blocked and not out: + reasons = as_list(gate.get("block_reasons")) + if not reasons: + reasons = [f"{source_gate} gate blocked without explicit hard_gate_findings"] + for reason in reasons: + out.append({"issue_type": "block_reason", "detail": str(reason)}) + for index, item in enumerate(out, 1): + item.setdefault("gate_finding_id", f"{source_gate}-HARD-{index:03d}") + return out + + def compact_review_items(b1_gate: Any, b2_gate: Any) -> list[dict[str, Any]]: + compact: list[dict[str, Any]] = [] + ordered = [("B1", b1_gate), ("B2", b2_gate)] + seq = 1 + for source_gate, gate in ordered: + for raw in extract_findings(gate, "review_findings"): + finding = raw if isinstance(raw, dict) else {"detail": str(raw)} + issue_type = clean_enum( + finding.get("issue_type") or finding.get("check_id") or finding.get("source_check") or "review_finding", + "review_finding", + ) + role = role_for(issue_type, finding) + refs = source_refs(finding) + policy = finding.get("candidate_use_policy") + if policy not in ("prioritize_if_supported", "preserve_as_needs_review", "do_not_generate_without_support"): + policy = "preserve_as_needs_review" + item = { + "review_id": f"P1RG-{seq:04d}", + "source_gate": source_gate, + "source_check": clean_enum(finding.get("source_check") or finding.get("check_id") or issue_type, issue_type), + "severity": normalize_severity(finding.get("severity")), + "issue_type": issue_type, + "lawyer_workflow_role": role, + "domain_hints": as_str_list(finding.get("domain_hints") or finding.get("domain_hint") or finding.get("domains")), + "target_stage_hints": as_str_list(finding.get("target_stage_hints") or finding.get("target_stage_hint")), + "target_task_hints": as_str_list(finding.get("target_task_hints") or finding.get("target_task_hint")), + "source_refs": refs, + "review_instruction": clip(finding.get("review_instruction") or finding.get("instruction") or "후속 검토 단계에서 해당 review finding의 증거와 법률상 의미를 재검토한다.", 120), + "negative_instruction": "사실 확정 또는 자동 차단으로 취급하지 말 것", + "candidate_use_policy": policy, + "concise_basis": clip(finding.get("concise_basis") or finding.get("detail") or finding.get("reason") or issue_type, 120), + "created_from": { + "gate_file": "quality_gates/B1_evidence_indexed_gate.json" if source_gate == "B1" else "quality_gates/B2_event_candidates_gate.json", + "gate_finding_id": finding.get("gate_finding_id") or finding.get("finding_id") or finding.get("id"), + }, + } + compact.append(item) + seq += 1 + if len(compact) >= 40: + return compact + return compact + + def add_index(index: dict[str, dict[str, list[str]]], bucket: str, key: str, review_id: str) -> None: + if not key: + return + index[bucket].setdefault(key, []) + if review_id not in index[bucket][key]: + index[bucket][key].append(review_id) + + def build_routing(review_items: list[dict[str, Any]]) -> tuple[dict[str, dict[str, list[str]]], dict[str, list[str]]]: + routing = { + "by_evidence_index": {}, + "by_event_candidate_id": {}, + "by_meeting_clause_ref": {}, + "by_issue_type": {}, + "by_domain_hint": {}, + "by_target_stage": {}, + "by_lawyer_workflow_role": {}, + } + queues = {role: [] for role in WORKFLOW_ROLES} + for item in review_items: + rid = item["review_id"] + refs = item.get("source_refs") or {} + for value in refs.get("evidence_indexes", []): + add_index(routing, "by_evidence_index", value, rid) + for value in refs.get("event_candidate_ids", []): + add_index(routing, "by_event_candidate_id", value, rid) + for value in refs.get("meeting_clause_refs", []): + add_index(routing, "by_meeting_clause_ref", value, rid) + add_index(routing, "by_issue_type", item.get("issue_type", ""), rid) + for value in item.get("domain_hints", []): + add_index(routing, "by_domain_hint", value, rid) + for value in item.get("target_stage_hints", []): + add_index(routing, "by_target_stage", value, rid) + role = item.get("lawyer_workflow_role", "cause_of_action_element_build") + add_index(routing, "by_lawyer_workflow_role", role, rid) + queues.setdefault(role, []).append(rid) + return routing, {role: queues.get(role, []) for role in WORKFLOW_ROLES} + + def gate_blocked(gate: Any) -> bool: + return isinstance(gate, dict) and ( + gate.get("overall_severity") == "BLOCK" + or gate.get("downstream_progression") == "BLOCKED" + or gate.get("stage2_auto_progression_allowed") is False + ) + + def main() -> None: + _init() + raw_by_digest_key = {key: read_raw(path) for key, path in INPUT_PATHS.items()} + parsed = {key: parse_json(raw, INPUT_PATHS[key]) for key, raw in raw_by_digest_key.items()} + + digest_guard = {key: digest(raw) for key, raw in raw_by_digest_key.items()} + b1_gate = parsed["b1_gate_sha256"] + b2_gate = parsed["b2_gate_sha256"] + review_items = compact_review_items(b1_gate, b2_gate) + routing_index, lawyer_workflow_queues = build_routing(review_items) + + hard_items = hard_findings(b1_gate, "B1") + hard_findings(b2_gate, "B2") + hard_ids = [ + str(item.get("gate_finding_id") or item.get("finding_id") or item.get("id") or f"HARD-{idx:03d}") + for idx, item in enumerate(hard_items, 1) + ] + blocked = bool(hard_items) or gate_blocked(b1_gate) or gate_blocked(b2_gate) + highest_review = "PASS" + if any(item["severity"] == "HARD_WARNING" for item in review_items): + highest_review = "HARD_WARNING" + elif review_items: + highest_review = "SOFT_WARNING" + b1_count = sum(1 for item in review_items if item["source_gate"] == "B1") + b2_count = sum(1 for item in review_items if item["source_gate"] == "B2") + + if blocked: + handoff_status = "BLOCKED" + elif review_items: + handoff_status = "READY_WITH_REVIEW" + else: + handoff_status = "READY_NO_REVIEW" + + sidecar = { + "schema_version": "stage1_part1_soft_gate_handoff.v1", + "source_stage": "Stage_1.Part_1", + "source_gate_files": [ + "quality_gates/B1_evidence_indexed_gate.json", + "quality_gates/B2_event_candidates_gate.json", + ], + "handoff_status": handoff_status, + "stage2_auto_progression_allowed": not blocked, + "hard_gate_summary": { + "hard_gate_count": len(hard_items), + "hard_gate_ids": hard_ids, + }, + "review_gate_summary": { + "review_gate_count": len(review_items), + "highest_review_severity": highest_review, + "b1_review_count": b1_count, + "b2_review_count": b2_count, + }, + "digest_guard": digest_guard, + "review_items": review_items, + "routing_index": routing_index, + "lawyer_workflow_queues": lawyer_workflow_queues, + } + + content = json.dumps(sidecar, ensure_ascii=False, indent=2) + write_doc(SIDE_CAR_PATH, content) + print(json.dumps({ + "status": "SOFT_GATE_HANDOFF_WRITTEN", + "path": SIDE_CAR_PATH, + "handoff_status": handoff_status, + "review_items": len(review_items), + "digest_guard_valid": all(re.fullmatch(r"[0-9a-f]{64}", v) for v in digest_guard.values()), + }, ensure_ascii=False)) + + if __name__ == "__main__": + main() + + task_procedure: + IN: + nexts: ["Task_A_client_goal", "Task_Evidence_shard_planner"] + wait_until: [] + + Task_A_client_goal: + nexts: [] + wait_until: ["IN"] + + Task_Evidence_shard_planner: + nexts: ["Task_B1_map_doc_*"] + wait_until: ["IN"] + + Task_B1_map_doc_*: + nexts: + - "Task_B2_map_events_{same_ordinal}" + - "Task_B1_gate_merge_deterministic" + wait_until: ["Task_Evidence_shard_planner"] + + Task_B2_map_events_*: + nexts: ["Task_B2_gate_merge_deterministic"] + wait_until: ["Task_B1_map_doc_{same_ordinal}"] + + Task_B1_gate_merge_deterministic: + nexts: ["Task_B2_gate_merge_deterministic"] + wait_until: ["all Task_B1_map_doc_*"] + + Task_B2_gate_merge_deterministic: + nexts: + - "Task_B1B2_gate_llm_adjudicator_*" + - "Task_B12_gate_audit_finalizer" + wait_until: + - "all Task_B2_map_events_*" + - "Task_B1_gate_merge_deterministic" + + Task_B1B2_gate_llm_adjudicator_*: + nexts: ["Task_B12_gate_audit_finalizer"] + wait_until: ["Task_B2_gate_merge_deterministic"] + + Task_B12_gate_audit_finalizer: + nexts: ["Task_B2_SHA256_soft_gate_handoff_writer"] + wait_until: + - "Task_B2_gate_merge_deterministic" + - "all Task_B1B2_gate_llm_adjudicator_*" + + Task_B2_SHA256_soft_gate_handoff_writer: + nexts: ["OUT"] + wait_until: ["Task_B12_gate_audit_finalizer"] + + OUT: + nexts: [] + wait_until: ["Task_B2_SHA256_soft_gate_handoff_writer"] + + prevs: [] + nexts: [] diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_2_Claude_v3.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_2_Claude_v3.yml new file mode 100644 index 00000000..a514060a --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_2_Claude_v3.yml @@ -0,0 +1,5280 @@ +--- +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_2 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Part 2 + version: v.2 + Stages: + - name: stage1_BO_시그널_생성 + description: BO 생성, 시그널 생성 + llm_provider: openai + llm_model: gpt-4o-2024-08-06 + tools: + mcpServers: + localdocs: + type: streamable-http + url: http://mcp-localdocs:8012/mcp + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + tasks: + - task_name: Task_C_BO_A0_context_and_domain_slice_compiler + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: |- + #!/usr/bin/env python3 + from __future__ import annotations + import hashlib + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + + REQUIRED_FILES = [ + "client_meeting.md", + "evidence_event_candidates.json", + "evidence_indexed.json", + "Default_Agent/Juristic_Act.md", + ] + PART1_SOFT_GATE_HANDOFF_FILE = "quality_gates/stage1_part1_soft_gate_handoff.json" + + CURRENT_STATE_PATTERNS = [ + "현재", "현재까지", "현재도", "계속", "거주", "점유", "사용", "운영", + "부지로만 사용", "인도받아", "사업자등록", "유치권", "임대", "수익", + ] + CONFLICT_PATTERNS = [ + "하였음에도", "그럼에도", "불구하고", "거절", "응하지", "주장", + "항변", "다툼", "무효", "취소", "해제", "해지", "말소", "신뢰", + ] + SUCCESSION_PATTERNS = [ + "사망", "상속", "상속포기", "배우자", "직계비속", "직계존속", + "피상속인", "상속인", "수리", "가족관계", + ] + MIXED_CONTRACT_PATTERNS = [ + "임대차", "매매", "보증금", "매매대금", "인도", "관리", "사용", + "사용수익", "영업양도", "채무불승계", "대물변제", + ] + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-c-bo-a0-context-and-domain-slice-compiler", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _mid(), + "method": "tools/call", + "params": {"name": name, "arguments": args}, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + p = _parse(r.text) + if not p or "result" not in p: + raise RuntimeError(f"MCP {name} failed") + return p + + def _unwrap(p: Any, name: str) -> Any: + text = (p["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError(f"Empty: {name}") + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, dict) and "results" in outer: + return outer + return outer + + def _stable_hash(value: Any) -> str: + if isinstance(value, str): + raw = value + else: + raw = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _text(value: Any) -> str | None: + if value is None: + return None + s = str(value).strip() + return s or None + + def _norm_space(value: Any) -> str: + return re.sub(r"\s+", " ", str(value or "")).strip() + + def _norm_key(value: Any) -> str: + text = str(value or "").strip().lower() + return "".join(ch for ch in text if ch.isalnum()) + + def _short_excerpt(value: Any, limit: int = 120) -> str: + text = _norm_space(value) + return text if len(text) <= limit else text[: limit - 1] + "…" + + def _section_key_from_heading(heading: str, seq: int) -> str: + normalized = _norm_key(heading) + if "상담내용" in normalized: + suffix = "consultation" + elif "사건관계인의주장" in normalized or ("사건관계인" in normalized and "주장" in normalized): + suffix = "party_assertions" + elif "희망사항" in normalized or "의뢰인희망" in normalized: + suffix = "client_requests" + elif normalized: + suffix = normalized[:24] + else: + suffix = "unnamed" + return f"s{seq:02d}_{suffix}" + + def _uniq(values: list[Any]) -> list[Any]: + seen: set[str] = set() + out: list[Any] = [] + for value in values: + marker = json.dumps(value, ensure_ascii=False, sort_keys=True) if isinstance(value, (dict, list)) else str(value) + if marker not in seen: + seen.add(marker) + out.append(value) + return out + + def _extract_doc_names(payload: Any) -> list[str]: + names: list[str] = [] + def walk(v: Any) -> None: + if isinstance(v, str): + if "." in v or "/" in v: + names.append(v) + names.extend(re.findall(r"[\w./()가-힣ᄀ-ᇿ -]+\.[A-Za-z0-9]+", v)) + elif isinstance(v, list): + for item in v: + walk(item) + elif isinstance(v, dict): + for key in ("name", "path", "doc_name", "document_name", "file", "filename"): + if v.get(key): + names.append(str(v[key])) + for val in v.values(): + if isinstance(val, (list, dict)): + walk(val) + walk(payload) + return sorted(set(names)) + + def _doc_name_matches(found_names: list[str], required: str) -> bool: + required_norm = required.strip("/") + required_base = required_norm.split("/")[-1] + for name in found_names: + n = name.strip("/") + if n == required_norm or n.endswith("/" + required_norm) or n.split("/")[-1] == required_base: + return True + if required_norm in n or required_base in n: + return True + return False + + def _preflight_required_files() -> dict[str, Any]: + found_names_set: set[str] = set() + list_docs_args = ( + {"path": "/", "pattern": "*", "recursive": True}, + {"path": ".", "pattern": "*", "recursive": True}, + {"path": "/", "pattern": "*"}, + {"path": ".", "pattern": "*"}, + {"path": "Default_Agent", "pattern": "*"}, + {"path": "quality_gates", "pattern": "*.json"}, + ) + for args in list_docs_args: + try: + payload = _unwrap(_call("list_docs", args), "list_docs") + found_names_set.update(_extract_doc_names(payload)) + except Exception: + continue + found_names_now = sorted(found_names_set) + required_ready = all(_doc_name_matches(found_names_now, path) for path in REQUIRED_FILES) + sidecar_checked = _doc_name_matches(found_names_now, PART1_SOFT_GATE_HANDOFF_FILE) or args.get("path") == "quality_gates" + if required_ready and sidecar_checked: + break + found_names = sorted(found_names_set) + if not found_names: + raise RuntimeError("list_docs returned no recognizable document names for required inputs") + missing = [path for path in REQUIRED_FILES if not _doc_name_matches(found_names, path)] + if missing: + raise RuntimeError(f"Missing required input files by list_docs: {missing}") + return {"found_names": found_names, "required_files": REQUIRED_FILES} + + def _read_required_docs_once() -> dict[str, str]: + payload = _unwrap(_call("read_docs", {"doc_names": REQUIRED_FILES}), "read_docs") + docs: dict[str, str] = {} + + def assign(name: str | None, content: Any) -> None: + if not name: + return + text = content if isinstance(content, str) else json.dumps(content, ensure_ascii=False) + for required in REQUIRED_FILES: + if name == required or name.endswith("/" + required) or name.split("/")[-1] == required.split("/")[-1]: + docs[required] = text + + if isinstance(payload, dict) and isinstance(payload.get("results"), list): + for item in payload["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + content = item.get("content") or item.get("text") or item.get("markdown") or item.get("raw") + assign(str(name) if name else None, content) + elif isinstance(payload, dict): + for required in REQUIRED_FILES: + if required in payload: + assign(required, payload[required]) + elif isinstance(payload, str): + if len(REQUIRED_FILES) == 1: + assign(REQUIRED_FILES[0], payload) + + missing = [name for name in REQUIRED_FILES if name not in docs] + if missing: + # Some Localdocs runtimes return one JSON wrapper inside the text field. + raw = payload if isinstance(payload, str) else json.dumps(payload, ensure_ascii=False) + try: + outer = json.loads(raw) + if isinstance(outer, dict) and isinstance(outer.get("results"), list): + for item in outer["results"]: + if isinstance(item, dict): + name = item.get("name") or item.get("path") or item.get("doc_name") + content = item.get("content") or item.get("text") + assign(str(name) if name else None, content) + except Exception: + pass + missing = [name for name in REQUIRED_FILES if name not in docs] + if missing: + raise RuntimeError(f"read_docs did not return required inputs: {missing}") + for name, content in docs.items(): + if not str(content).strip(): + raise RuntimeError(f"Required input is empty: {name}") + return docs + + def _parse_json_doc(name: str, raw: str) -> Any: + try: + return json.loads(raw.strip()) + except json.JSONDecodeError as exc: + try: + obj, _ = json.JSONDecoder().raw_decode(raw.strip()) + return obj + except json.JSONDecodeError: + raise RuntimeError(f"Strict JSON parse failed: {name}: {exc.msg} at line {exc.lineno} column {exc.colno}") from exc + + def _items(obj: Any, *keys: str) -> list[Any]: + if isinstance(obj, list): + return obj + if isinstance(obj, dict): + for key in keys + ("items", "evidence_indexed", "documents", "results"): + val = obj.get(key) + if isinstance(val, list): + return val + return [] + + def _extract_keywords(text: str, patterns: list[str]) -> list[str]: + return [p for p in patterns if p in text] + + def _build_meeting_clause_map(meeting_text: str) -> dict[str, Any]: + clauses: list[dict[str, Any]] = [] + current_major: str | None = None + current: dict[str, Any] | None = None + section_seq = 0 + current_section_key = "preamble" + current_section_label = "preamble" + + section_re = re.compile(r"^\s*#?\s*【\s*(.+?)\s*】\s*$") + major_re = re.compile(r"^\s*(\d+)\.\s+(.+?)\s*$") + sub_re = re.compile(r"^\s*([가-힣])\.\s+(.+?)\s*$") + + def flush(end_line: int) -> None: + nonlocal current + if not current: + return + current["line_end"] = end_line + current["text"] = _norm_space(current.get("text", "")) + text = current["text"] + current["tags"] = { + "current_state": _extract_keywords(text, CURRENT_STATE_PATTERNS), + "conflict_pattern": _extract_keywords(text, CONFLICT_PATTERNS), + "succession": _extract_keywords(text, SUCCESSION_PATTERNS), + "mixed_contract": _extract_keywords(text, MIXED_CONTRACT_PATTERNS), + } + current["text_excerpt"] = _short_excerpt(text) + clauses.append(current) + current = None + + lines = meeting_text.splitlines() + for idx, line in enumerate(lines, 1): + stripped = line.strip() + if not stripped or stripped.startswith("|") or stripped.startswith("---"): + continue + m_section = section_re.match(stripped) + m_major = major_re.match(stripped) + m_sub = sub_re.match(stripped) + if m_section: + flush(idx - 1) + section_seq += 1 + current_section_label = _norm_space(m_section.group(1)) + current_section_key = _section_key_from_heading(current_section_label, section_seq) + current_major = None + elif m_major: + flush(idx - 1) + current_major = m_major.group(1) + current = { + "clause_id": f"{current_section_key}:{current_major}", + "local_clause_id": current_major, + "section_key": current_section_key, + "section_label": current_section_label, + "major_no": current_major, + "sub_no": None, + "heading": _norm_space(m_major.group(2)), + "line_start": idx, + "text": _norm_space(m_major.group(2)), + } + elif m_sub and current_major: + flush(idx - 1) + sub_no = m_sub.group(1) + current = { + "clause_id": f"{current_section_key}:{current_major}-{sub_no}", + "local_clause_id": f"{current_major}-{sub_no}", + "section_key": current_section_key, + "section_label": current_section_label, + "major_no": current_major, + "sub_no": sub_no, + "heading": None, + "line_start": idx, + "text": _norm_space(m_sub.group(2)), + } + elif current: + current["text"] = _norm_space(str(current.get("text", "")) + " " + stripped) + flush(len(lines)) + + if not clauses: + raise RuntimeError("client_meeting.md parse produced no numbered meeting clauses") + + by_id: dict[str, Any] = {} + duplicate_clause_ids: list[str] = [] + for clause in clauses: + cid = clause["clause_id"] + if cid in by_id: + duplicate_clause_ids.append(cid) + by_id[cid] = clause + if duplicate_clause_ids: + raise RuntimeError(f"Duplicate meeting clause ids after section normalization: {duplicate_clause_ids}") + return { + "schema_version": "meeting_clause_map.v1", + "clause_order": [c["clause_id"] for c in clauses], + "by_clause_id": by_id, + "sections": _uniq([{"section_key": c["section_key"], "section_label": c["section_label"]} for c in clauses]), + "current_state_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["current_state"]], + "conflict_pattern_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["conflict_pattern"]], + "succession_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["succession"]], + "mixed_contract_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["mixed_contract"]], + } + + def _candidate_id(candidate: dict[str, Any], evidence_index: str, seq: int) -> str: + return str(candidate.get("candidate_id") or candidate.get("candidate_id_proposed") or f"internal_missing_candidate_id::{evidence_index}::{seq:02d}") + + def _evidence_index_from_item(item: dict[str, Any]) -> str | None: + return _text(item.get("evidence_index") or item.get("evidence_index_proposed") or item.get("source_evidence_index")) + + def _build_event_candidate_map(event_obj: Any) -> dict[str, Any]: + items = _items(event_obj) + if not items: + raise RuntimeError("evidence_event_candidates.json parse produced no document-level items") + by_candidate_id: dict[str, Any] = {} + by_evidence_index: dict[str, list[str]] = {} + by_meeting_clause_ref: dict[str, list[str]] = {} + state_candidate_ids: list[str] = [] + succession_candidate_ids: list[str] = [] + mixed_contract_candidate_ids: list[str] = [] + zero_event_evidence_indexes: list[str] = [] + asset_cluster_values: list[str] = [] + malformed_item_refs: list[str] = [] + + for item_seq, item in enumerate(items, 1): + if not isinstance(item, dict): + malformed_item_refs.append(f"item#{item_seq}: non-object") + continue + evidence_index = _evidence_index_from_item(item) + if not evidence_index: + malformed_item_refs.append(f"item#{item_seq}: missing evidence_index") + continue + item_ref = evidence_index + event_candidates_raw = item.get("event_candidates") + has_event_candidates_list = isinstance(event_candidates_raw, list) + has_event_candidates = has_event_candidates_list and len(event_candidates_raw) > 0 + has_zero_event_reason = bool(item.get("zero_event_reason")) + if event_candidates_raw is not None and not has_event_candidates_list: + malformed_item_refs.append(f"{item_ref}: event_candidates must be a list when present") + continue + if not has_event_candidates and not has_zero_event_reason: + malformed_item_refs.append(f"{item_ref}: requires non-empty event_candidates or zero_event_reason") + continue + if item.get("zero_event_reason"): + zero_event_evidence_indexes.append(evidence_index) + item_flags = item.get("doc_semantic_flags") if isinstance(item.get("doc_semantic_flags"), list) else [] + property_cluster_id = _text(item.get("property_cluster_id")) + if property_cluster_id: + asset_cluster_values.append(property_cluster_id) + for asset in item.get("affected_asset_cluster_ids") or []: + if _text(asset): + asset_cluster_values.append(str(asset)) + for seq, cand in enumerate(event_candidates_raw or [], 1): + if not isinstance(cand, dict): + malformed_item_refs.append(f"{item_ref}: event_candidates[{seq}] must be an object") + continue + cid = _candidate_id(cand, evidence_index, seq) + refs = cand.get("meeting_clause_refs") if isinstance(cand.get("meeting_clause_refs"), list) else [] + cand_flags = cand.get("doc_semantic_flags") if isinstance(cand.get("doc_semantic_flags"), list) else [] + candidate_flags = _uniq(item_flags + cand_flags) + record = { + "candidate_id": cid, + "source_evidence_index": _text(cand.get("source_evidence_index") or cand.get("source_evidence_index_proposed")) or evidence_index, + "title": item.get("title"), + "doc_type": item.get("doc_type"), + "event_or_state": cand.get("event_or_state"), + "event_kind": cand.get("event_kind"), + "event_subkind": cand.get("event_subkind"), + "action_type_candidate": cand.get("action_type_candidate"), + "action_summary": cand.get("action_summary"), + "participants": cand.get("participants") or {}, + "object_spec": cand.get("object_spec"), + "amount": cand.get("amount"), + "event_date": cand.get("event_date"), + "event_end_date": cand.get("event_end_date"), + "time_text": cand.get("time_text"), + "time_precision": cand.get("time_precision"), + "current_state_candidate": cand.get("current_state_candidate"), + "succession_candidate": cand.get("succession_candidate"), + "legal_effect_candidate": cand.get("legal_effect_candidate"), + "defense_candidate": cand.get("defense_candidate"), + "commercial_successor_candidates": cand.get("commercial_successor_candidates"), + "meeting_clause_refs": refs, + "actio_relevance_candidates": cand.get("actio_relevance_candidates"), + "identity_signature": cand.get("identity_signature"), + "property_cluster_id": property_cluster_id, + "affected_asset_cluster_ids": item.get("affected_asset_cluster_ids") or [], + "doc_semantic_flags": candidate_flags, + "fraction_ref": cand.get("fraction_ref"), + "confidence": cand.get("confidence"), + } + by_candidate_id[cid] = record + by_evidence_index.setdefault(evidence_index, []).append(cid) + for ref in refs: + by_meeting_clause_ref.setdefault(str(ref), []).append(cid) + if cand.get("event_or_state") == "state" or cand.get("current_state_candidate"): + state_candidate_ids.append(cid) + if cand.get("succession_candidate"): + succession_candidate_ids.append(cid) + if "mixed_contract_doc" in candidate_flags or cand.get("commercial_successor_candidates"): + mixed_contract_candidate_ids.append(cid) + if _text(cand.get("object_spec")): + asset_cluster_values.append(str(cand.get("object_spec"))) + + if malformed_item_refs: + raise RuntimeError(f"Malformed evidence_event_candidates document items: {malformed_item_refs[:20]}") + + return { + "schema_version": "event_candidate_map.v2", + "candidate_count": len(by_candidate_id), + "candidate_id_set": sorted(by_candidate_id.keys()), + "by_candidate_id": by_candidate_id, + "by_evidence_index": {k: _uniq(v) for k, v in by_evidence_index.items()}, + "by_meeting_clause_ref": {k: _uniq(v) for k, v in by_meeting_clause_ref.items()}, + "state_candidate_ids": _uniq(state_candidate_ids), + "succession_candidate_ids": _uniq(succession_candidate_ids), + "mixed_contract_candidate_ids": _uniq(mixed_contract_candidate_ids), + "zero_event_evidence_indexes": _uniq(zero_event_evidence_indexes), + "asset_cluster_values": _uniq([v for v in asset_cluster_values if _text(v)]), + } + + def _component_count(item: dict[str, Any], key: str) -> int: + val = item.get(key) + if isinstance(val, list): + return len(val) + if isinstance(val, dict): + return len(val) + return 1 if val not in (None, "", []) else 0 + + COMPONENT_DETAIL_PROJECTION = { + "loan_components": ["loan_component_id", "principal_amount", "loan_date", "due_date", + "agreed_interest_rate", "agreed_delay_damage_rate", "rate_text", + "rate_source_locator", "interest_accrual_start_hint"], + "secured_debt_components": ["agreed_interest_rate", "agreed_delay_damage_rate", "rate_text", + "rate_source_locator", "due_date"], + "notice_components": ["notice_id_proposed", "notice_type", "drafted_date", "dispatch_date", + "arrival_date", "received_date", "legal_effect_date_candidate", + "requires_receipt_for_effect", "receipt_evidence_refs", + "recipient_admission_text", "sender_candidates", "receiver_candidates"], + "registry_row_components": ["registry_ref", "fraction_ref", "fraction_display", + "fraction_numerator", "fraction_denominator", "receipt_no"], + "schedule_items": ["schedule_ref", "canonical_label_candidate", "usable_in_relief"], + } + + def _component_details(item: dict[str, Any]) -> dict[str, Any]: + details: dict[str, Any] = {} + for key, fields in COMPONENT_DETAIL_PROJECTION.items(): + rows = item.get(key) + if isinstance(rows, dict): + rows = [rows] + if not isinstance(rows, list): + continue + projected = [{field: row.get(field) for field in fields if field in row} for row in rows if isinstance(row, dict)] + projected = [p for p in projected if p] + if projected: + details[key] = projected + return details + + def _build_evidence_authority_map(evidence_obj: Any) -> dict[str, Any]: + items = _items(evidence_obj, "evidence_indexed") + if not items: + raise RuntimeError("evidence_indexed.json parse produced no evidence authority items") + by_evidence_index: dict[str, Any] = {} + by_doc_type: dict[str, list[str]] = {} + by_title_norm: dict[str, list[str]] = {} + semantic_flag_index: dict[str, list[str]] = {} + evidence_index_set: list[str] = [] + malformed_item_refs: list[str] = [] + duplicate_evidence_indexes: list[str] = [] + + component_keys = [ + "contract_components", "valuation_object", "legal_calculation_object", + "legal_calculation_objects", "claim_document_components", + "business_transfer_components", "registry_row_components", + "secured_debt_components", "valuation_rows", "mixed_contract_payment_semantics", + "loan_components", "valuation_matrix", "registry_row_role_map", + "defense_assertions", "admission_components", "defense_components", + "notice_components", "party_assertion_components", "schedule_items", + "registry_object_display", "succession_document_components", + ] + + for item_seq, item in enumerate(items, 1): + if not isinstance(item, dict): + malformed_item_refs.append(f"item#{item_seq}: non-object") + continue + evidence_index = _text(item.get("evidence_index") or item.get("evidence_index_proposed")) + if not evidence_index: + malformed_item_refs.append(f"item#{item_seq}: missing finalized evidence_index") + continue + if evidence_index in by_evidence_index: + duplicate_evidence_indexes.append(evidence_index) + continue + flags = item.get("doc_semantic_flags") if isinstance(item.get("doc_semantic_flags"), list) else [] + component_summary = {key: _component_count(item, key) for key in component_keys} + record = { + "evidence_index": evidence_index, + "doc_uid": item.get("doc_uid"), + "title": item.get("title"), + "title_normalized": item.get("title_normalized") or _norm_space(item.get("title")), + "doc_type": item.get("doc_type"), + "key_facts": item.get("key_facts") or [], + "key_dates": item.get("key_dates") or [], + "key_amounts": item.get("key_amounts") or [], + "key_parties": item.get("key_parties") or [], + "source_pointer": item.get("source_pointer") or {}, + "property_cluster_id": item.get("property_cluster_id"), + "doc_semantic_flags": flags, + "multi_event_registry": item.get("multi_event_registry"), + "component_summary": component_summary, + "component_details": _component_details(item), + } + by_evidence_index[evidence_index] = record + evidence_index_set.append(evidence_index) + by_doc_type.setdefault(str(item.get("doc_type") or "unknown"), []).append(evidence_index) + title_key = _norm_key(record["title_normalized"]) + if title_key: + by_title_norm.setdefault(title_key, []).append(evidence_index) + for flag in flags: + semantic_flag_index.setdefault(str(flag), []).append(evidence_index) + + if malformed_item_refs: + raise RuntimeError(f"Malformed evidence_indexed authority items: {malformed_item_refs[:20]}") + if duplicate_evidence_indexes: + raise RuntimeError(f"Duplicate evidence_index values in evidence_indexed.json: {_uniq(duplicate_evidence_indexes)}") + + return { + "schema_version": "evidence_authority_map.v2", + "evidence_index_set": _sort_evidence_indexes(_uniq(evidence_index_set)), + "by_evidence_index": by_evidence_index, + "by_doc_type": {k: _sort_evidence_indexes(_uniq(v)) for k, v in by_doc_type.items()}, + "by_title_norm": {k: _sort_evidence_indexes(_uniq(v)) for k, v in by_title_norm.items()}, + "semantic_flag_index": {k: _sort_evidence_indexes(_uniq(v)) for k, v in semantic_flag_index.items()}, + } + + def _sort_evidence_indexes(values: list[str]) -> list[str]: + def key(v: str) -> tuple[int, str]: + m = re.search(r"E-(\d+)", str(v)) + return (int(m.group(1)) if m else 10**9, str(v)) + return sorted(values, key=key) + + def _split_examples(value: str) -> list[str]: + parts = re.split(r"[,,、]| / |/|ㆍ|·", value or "") + return [_norm_space(p) for p in parts if _norm_space(p)] + + def _build_juristic_act_map(markdown: str) -> dict[str, Any]: + categories: dict[str, Any] = {} + example_to_gubun: dict[str, list[str]] = {} + for line in markdown.splitlines(): + if "|" not in line: + continue + cells = [_norm_space(c) for c in line.strip().strip("|").split("|")] + if len(cells) < 3: + continue + if cells[0] in ("구분", "---") or set(cells[0]) <= {"-"}: + continue + gubun, content, examples = cells[0], cells[1], cells[2] + if not gubun or gubun.startswith("-"): + continue + example_list = _split_examples(examples) + categories[gubun] = {"content": content, "examples": example_list} + for ex in example_list: + example_to_gubun.setdefault(_norm_key(ex), []).append(gubun) + + axis_groups = { + "structure": ["단독행위", "계약행위", "합동행위"], + "form": ["요식(要式)행위", "불요식행위"], + "time_effect": ["생전 행위", "사후(또는 사인) 행위"], + "legal_object": ["채권행위", "물권행위", "준물권행위"], + "contribution": ["출연행위", "비출연행위"], + "dependency": ["주(主)된 행위", "종(從)된 행위", "유인 행위", "무인 행위"], + "defect_or_condition": ["무효 행위", "취소가능한 법률 행위", "조건부 법률행위", "기한부 법률행위"], + "controversy": ["견해대립있는 행위"], + } + + return { + "schema_version": "juristic_act_taxonomy_map.v1", + "categories": categories, + "example_to_gubun": {k: _uniq(v) for k, v in example_to_gubun.items()}, + "axis_groups": axis_groups, + "source_digest_sha256": _stable_hash(markdown), + } + + def _validate_juristic_act_map(juristic_map: dict[str, Any]) -> None: + categories = juristic_map.get("categories") or {} + example_to_gubun = juristic_map.get("example_to_gubun") or {} + required = ["단독행위", "계약행위", "채권행위", "물권행위"] + missing = [name for name in required if name not in categories] + if not categories or not example_to_gubun: + raise RuntimeError("JuristicAct taxonomy parse produced empty categories or examples") + if missing: + raise RuntimeError(f"JuristicAct taxonomy missing required categories: {missing}") + + def _collect_asset_clusters(meeting_map: dict[str, Any], event_map: dict[str, Any], evidence_map: dict[str, Any]) -> list[str]: + values: list[str] = [] + asset_hint_re = re.compile(r"([가-힣A-Za-z0-9·ㆍ() \-]{1,30}(?:대지|건물|상가|빌라|임야|부동산|지분|영업|상호|채권|대금))") + for clause in meeting_map.get("by_clause_id", {}).values(): + text = str(clause.get("text") or "") + values.extend([_norm_space(m.group(1)) for m in asset_hint_re.finditer(text)]) + values.extend(event_map.get("asset_cluster_values") or []) + for ev in evidence_map.get("by_evidence_index", {}).values(): + if _text(ev.get("property_cluster_id")): + values.append(str(ev.get("property_cluster_id"))) + for fact in ev.get("key_facts") or []: + values.extend([_norm_space(m.group(1)) for m in asset_hint_re.finditer(str(fact))]) + return _uniq([v for v in values if _text(v)]) + + def _build_global_hazard_seed(meeting_map: dict[str, Any], event_map: dict[str, Any], evidence_map: dict[str, Any]) -> dict[str, Any]: + evidence_index_set = evidence_map.get("evidence_index_set") or [] + current_state_ids = event_map.get("state_candidate_ids") or [] + succession_candidate_ids = event_map.get("succession_candidate_ids") or [] + mixed_contract_candidate_ids = event_map.get("mixed_contract_candidate_ids") or [] + + def meeting_expression_records(clause_ids: list[str], tag_key: str) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + by_clause = meeting_map.get("by_clause_id", {}) + for cid in clause_ids: + clause = by_clause.get(cid) or {} + matched = (clause.get("tags") or {}).get(tag_key) or [] + out.append({ + "source": "client_meeting.md", + "clause_id": cid, + "local_clause_id": clause.get("local_clause_id"), + "section_key": clause.get("section_key"), + "matched_patterns": matched, + "text_excerpt": clause.get("text_excerpt") or _short_excerpt(clause.get("text")), + }) + return out + + def event_expression_records(candidate_ids: list[str]) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + by_candidate = event_map.get("by_candidate_id", {}) + for cid in candidate_ids: + cand = by_candidate.get(cid) or {} + text = " ".join(str(cand.get(k) or "") for k in ("action_summary", "event_kind", "event_subkind", "identity_signature")) + matched = _uniq( + _extract_keywords(text, CURRENT_STATE_PATTERNS) + + _extract_keywords(text, CONFLICT_PATTERNS) + + _extract_keywords(text, SUCCESSION_PATTERNS) + + _extract_keywords(text, MIXED_CONTRACT_PATTERNS) + ) + out.append({ + "source": "evidence_event_candidates.json", + "candidate_id": cid, + "source_evidence_index": cand.get("source_evidence_index"), + "event_or_state": cand.get("event_or_state"), + "event_kind": cand.get("event_kind"), + "matched_patterns": matched, + "text_excerpt": _short_excerpt(cand.get("action_summary") or cand.get("identity_signature")), + }) + return out + + conflict_candidate_ids: list[str] = [] + for cid, cand in event_map.get("by_candidate_id", {}).items(): + combined = " ".join( + str(cand.get(k) or "") + for k in ("action_summary", "event_kind", "event_subkind", "identity_signature") + ) + if _extract_keywords(combined, CONFLICT_PATTERNS): + conflict_candidate_ids.append(cid) + + succession_evidence_indexes = [] + mixed_contract_evidence_indexes = [] + for eidx, ev in evidence_map.get("by_evidence_index", {}).items(): + flags = ev.get("doc_semantic_flags") or [] + title_and_facts = " ".join([str(ev.get("title") or "")] + [str(x) for x in ev.get("key_facts") or []]) + if "succession_doc" in flags or _extract_keywords(title_and_facts, SUCCESSION_PATTERNS): + succession_evidence_indexes.append(eidx) + if "mixed_contract_doc" in flags or _extract_keywords(title_and_facts, MIXED_CONTRACT_PATTERNS): + mixed_contract_evidence_indexes.append(eidx) + + return { + "schema_version": "global_hazard_seed.v1", + "meeting_clause_ids": meeting_map.get("clause_order") or [], + "current_state_expressions": { + "meeting_clause_ids": meeting_map.get("current_state_clause_ids") or [], + "event_candidate_ids": current_state_ids, + "meeting_expressions": meeting_expression_records(meeting_map.get("current_state_clause_ids") or [], "current_state"), + "event_expressions": event_expression_records(current_state_ids), + }, + "conflict_pattern_expressions": { + "meeting_clause_ids": meeting_map.get("conflict_pattern_clause_ids") or [], + "event_candidate_ids": _uniq(conflict_candidate_ids), + "meeting_expressions": meeting_expression_records(meeting_map.get("conflict_pattern_clause_ids") or [], "conflict_pattern"), + "event_expressions": event_expression_records(_uniq(conflict_candidate_ids)), + }, + "asset_clusters": _collect_asset_clusters(meeting_map, event_map, evidence_map), + "succession_related_candidates": { + "meeting_clause_ids": meeting_map.get("succession_clause_ids") or [], + "event_candidate_ids": succession_candidate_ids, + "evidence_indexes": _sort_evidence_indexes(_uniq(succession_evidence_indexes)), + "meeting_expressions": meeting_expression_records(meeting_map.get("succession_clause_ids") or [], "succession"), + "event_expressions": event_expression_records(succession_candidate_ids), + }, + "mixed_contract_candidates": { + "meeting_clause_ids": meeting_map.get("mixed_contract_clause_ids") or [], + "event_candidate_ids": mixed_contract_candidate_ids, + "evidence_indexes": _sort_evidence_indexes(_uniq(mixed_contract_evidence_indexes)), + "meeting_expressions": meeting_expression_records(meeting_map.get("mixed_contract_clause_ids") or [], "mixed_contract"), + "event_expressions": event_expression_records(mixed_contract_candidate_ids), + }, + "evidence_index_set": evidence_index_set, + } + + def _read_optional_doc_once(doc_name: str) -> str | None: + try: + payload = _unwrap(_call("read_docs", {"doc_names": [doc_name]}), "read_docs_optional") + except Exception: + return None + if isinstance(payload, dict) and isinstance(payload.get("results"), list): + for item in payload["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + content = item.get("content") or item.get("text") or item.get("markdown") or item.get("raw") + if name and (str(name) == doc_name or str(name).endswith("/" + doc_name) or str(name).split("/")[-1] == doc_name.split("/")[-1]): + return content if isinstance(content, str) else json.dumps(content, ensure_ascii=False) + if isinstance(payload, dict) and doc_name in payload: + val = payload[doc_name] + return val if isinstance(val, str) else json.dumps(val, ensure_ascii=False) + if isinstance(payload, str) and payload.strip(): + return payload + return None + + def _compact_part1_soft_gate_handoff(existence_check: dict[str, Any], input_digests: dict[str, str], meeting_map: dict[str, Any], event_map: dict[str, Any], evidence_map: dict[str, Any]) -> dict[str, Any]: + found_names = existence_check.get("found_names") or [] + base = { + "schema_version": "stage1_part1_soft_gate_handoff.v1", + "status": "ABSENT", + "stage2_auto_progression_allowed": True, + "review_gate_count": 0, + "highest_review_severity": "PASS", + "review_items_compact": [], + "routing_index": {}, + "lawyer_workflow_queues": {}, + "dropped_review_items": [], + "usage_rules": { + "not_factual_authority": True, + "must_not_create_fact_without_stage_a_source": True, + "must_preserve_as_needs_review_if_used": True, + "must_not_override_stage_e_final_progression": True, + "must_not_override_stage_f_single_writer_gate": True, + }, + } + if not _doc_name_matches(found_names, PART1_SOFT_GATE_HANDOFF_FILE): + return base + raw = _read_optional_doc_once(PART1_SOFT_GATE_HANDOFF_FILE) + if not raw: + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": "sidecar listed but unreadable"}) + return base + try: + sidecar = json.loads(raw.strip()) + except Exception as exc: + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": f"sidecar JSON parse failed: {exc}"}) + return base + if not isinstance(sidecar, dict) or sidecar.get("schema_version") != "stage1_part1_soft_gate_handoff.v1": + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": "wrong or missing schema_version"}) + return base + if sidecar.get("handoff_status") == "BLOCKED" or sidecar.get("stage2_auto_progression_allowed") is False: + raise RuntimeError("Part 1 soft gate handoff is BLOCKED; Part 2-6 cannot proceed") + guard = sidecar.get("digest_guard") if isinstance(sidecar.get("digest_guard"), dict) else {} + invalid_guard_fields = [] + stale = [] + for guard_key, doc_name in [("evidence_indexed_sha256", "evidence_indexed.json"), ("evidence_event_candidates_sha256", "evidence_event_candidates.json")]: + val = guard.get(guard_key) + if not isinstance(val, str) or not re.fullmatch(r"[0-9a-f]{64}", val): + invalid_guard_fields.append(guard_key) + elif val != input_digests.get(doc_name): + stale.append(guard_key) + if invalid_guard_fields: + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": "digest_guard missing real sha256", "fields": invalid_guard_fields}) + return base + if stale: + base["status"] = "IGNORED_STALE" + base["dropped_review_items"].append({"reason": "digest_guard mismatch", "fields": stale}) + return base + evidence_set = set(evidence_map.get("evidence_index_set") or []) + event_set = set(event_map.get("candidate_id_set") or []) + meeting_set = set(meeting_map.get("clause_order") or []) + compact = [] + dropped = [] + for idx, item in enumerate(sidecar.get("review_items") or []): + if len(compact) >= 40: + dropped.append({"index": idx, "reason": "review_items_compact max 40 reached"}) + continue + if not isinstance(item, dict): + dropped.append({"index": idx, "reason": "non-object review item"}) + continue + refs = item.get("source_refs") if isinstance(item.get("source_refs"), dict) else {} + bad_e = [x for x in refs.get("evidence_indexes", []) if x not in evidence_set] + bad_ev = [x for x in refs.get("event_candidate_ids", []) if x not in event_set] + bad_m = [x for x in refs.get("meeting_clause_refs", []) if x not in meeting_set] + if bad_e or bad_ev or bad_m: + dropped.append({"review_id": item.get("review_id"), "reason": "source_refs membership failed", "bad_evidence_indexes": bad_e, "bad_event_candidate_ids": bad_ev, "bad_meeting_clause_refs": bad_m}) + continue + compact.append({ + "review_id": item.get("review_id"), + "severity": item.get("severity"), + "issue_type": item.get("issue_type"), + "lawyer_workflow_role": item.get("lawyer_workflow_role"), + "domain_hints": item.get("domain_hints") or [], + "target_stage_hints": item.get("target_stage_hints") or [], + "target_task_hints": item.get("target_task_hints") or [], + "source_refs": refs, + "candidate_use_policy": item.get("candidate_use_policy"), + "review_instruction": item.get("review_instruction"), + "negative_instruction": item.get("negative_instruction"), + }) + summary = sidecar.get("review_gate_summary") if isinstance(sidecar.get("review_gate_summary"), dict) else {} + base.update({ + "status": sidecar.get("handoff_status") if sidecar.get("handoff_status") in {"READY_WITH_REVIEW", "READY_NO_REVIEW"} else "READY_WITH_REVIEW", + "stage2_auto_progression_allowed": sidecar.get("stage2_auto_progression_allowed") is not False, + "review_gate_count": len(compact), + "highest_review_severity": summary.get("highest_review_severity") or ("HARD_WARNING" if any(i.get("severity") == "HARD_WARNING" for i in compact) else ("SOFT_WARNING" if compact else "PASS")), + "review_items_compact": compact, + "routing_index": sidecar.get("routing_index") if isinstance(sidecar.get("routing_index"), dict) else {}, + "lawyer_workflow_queues": sidecar.get("lawyer_workflow_queues") if isinstance(sidecar.get("lawyer_workflow_queues"), dict) else {}, + "dropped_review_items": dropped, + }) + return base + + + + # ==================== A0 domain slicing (B0 x5 통합; 3등급 라우팅 = 개선전략서 X-1) ==================== + A0_DOMAIN_CONFIGS = { + "B1_Money_Successor": { + "domain_id": "B1_Money_Successor", + "domain_label": "금전채권 및 상호속용 책임", + "allowed_legal_effect_bo_types": [ + "money_claim_chain_BO", + "commercial_successor_liability_chain_BO", + "liability_group_hint_BO", + ], + "terms": [ + "대여금", "차용", "매매대금", "물품대금", "보증금", "정산금", "공사대금", + "원금", "이자", "지연손해금", "변제기", "이행기", "일부변제", "미변제", + "채무승인", "지급각서", "확인서", "최고", "통지", "상호속용", "영업양도", + "양수인", "채무불승계", "business_transfer", "loan", "payment", "admission", + "notice", "claim_document", "mixed_contract_payment", + ], + "meeting_sources": ["mixed_contract_clause_ids", "conflict_pattern_clause_ids"], + "event_sources": ["mixed_contract_candidate_ids"], + "hazard_keys": ["mixed_contract_candidates", "conflict_pattern_expressions", "asset_clusters"], + "taxonomy_axes": ["legal_object", "structure", "dependency"], + }, + "B2_Secured_Registry": { + "domain_id": "B2_Secured_Registry", + "domain_label": "담보채무, 변제충당, 등기무효, 경매", + "allowed_legal_effect_bo_types": [ + "secured_debt_chain_BO", + "payment_allocation_and_mortgage_extinguishment_BO", + "auction_invalidity_BO", + "derivative_bank_mortgage_invalidity_BO", + ], + "terms": [ + "근저당", "저당", "담보", "피담보", "채무액", "변제", "변제충당", + "법정변제충당", "지분", "말소", "소유권이전등기", "등기", "등기원인", + "등기무효", "경매", "담보권실행경매", "경매개시", "매각", "배당", + "후행 담보권", "registry", "mortgage", "secured", "auction", "dividend", + "payment_allocation", "registry_row", "legal_calculation", + ], + "meeting_sources": ["conflict_pattern_clause_ids", "mixed_contract_clause_ids"], + "event_sources": [], + "hazard_keys": ["conflict_pattern_expressions", "asset_clusters", "mixed_contract_candidates"], + "taxonomy_axes": ["legal_object", "form", "dependency", "defect_or_condition"], + }, + "B3_Land_Valuation": { + "domain_id": "B3_Land_Valuation", + "domain_label": "토지 사용이익, 점유, 사용수익, 가치평가", + "allowed_legal_effect_bo_types": ["land_use_gain_timeline_BO", "liability_overlap_BO"], + "terms": [ + "토지", "건물", "부지", "인도", "점유", "사용", "사용수익", "현재 사용", + "현재 수익", "거주", "운영", "임대", "차임", "감정", "시가", "가액", + "미등기 건물", "사실상 처분권", "부진정연대", "valuation", "valuation_object", + "valuation_rows", "valuation_matrix", "registry_object_display", "possession", "land", "building", + ], + "meeting_sources": ["current_state_clause_ids", "conflict_pattern_clause_ids", "mixed_contract_clause_ids"], + "event_sources": ["state_candidate_ids"], + "hazard_keys": ["current_state_expressions", "asset_clusters", "mixed_contract_candidates", "conflict_pattern_expressions"], + "taxonomy_axes": ["legal_object", "contribution", "structure"], + }, + "B4_Actio": { + "domain_id": "B4_Actio", + "domain_label": "사해행위취소, 피보전채권, 책임재산, 가액배상", + "allowed_legal_effect_bo_types": [ + "preserved_claim_bundle_BO", + "fraudulent_transfer_BO", + "encumbrance_timeline_BO", + "valuation_matrix_BO", + "remedy_mode_decision_BO", + "value_compensation_cap_BO", + "defense_rebuttal_matrix_BO", + "actio_balance_sheet_BO", + ], + "terms": [ + "사해행위", "피보전채권", "채무초과", "책임재산", "공동담보", "매매", + "증여", "담보설정", "처분행위", "수익자", "전득자", "사해성", "악의", + "선의", "원상회복", "가액배상", "선순위담보", "말소", "가액 산정", + "항변", "actio", "fraudulent", "transfer", "preserved_claim", "encumbrance", + "valuation", "remedy", "compensation", "defense", "claim_document", + ], + "meeting_sources": ["conflict_pattern_clause_ids", "mixed_contract_clause_ids", "current_state_clause_ids", "succession_clause_ids"], + "event_sources": [], + "hazard_keys": ["conflict_pattern_expressions", "asset_clusters", "mixed_contract_candidates", "current_state_expressions", "succession_related_candidates"], + "taxonomy_axes": ["legal_object", "structure", "contribution", "defect_or_condition", "dependency", "form"], + }, + "B5_Succession_Notice_Lien_Asset_Defense": { + "domain_id": "B5_Succession_Notice_Lien_Asset_Defense", + "domain_label": "상속, 통지, 유치권, 목적물 특정, 항변", + "allowed_legal_effect_bo_types": [ + "succession_resolution_BO", + "notice_lifecycle_BO", + "lien_state_timeline_BO", + "asset_alias_BO", + "defense_rebuttal_BO", + "delivery_use_gain_claim_group_BO", + ], + "terms": [ + "사망", "상속", "상속포기", "배우자", "직계비속", "가족관계", "피상속인", + "상속인", "통지", "내용증명", "도달", "수령", "최고", "유치권", "점유", + "인도", "거절", "별지목록", "등기부 표시", "목적물", "동일성", "피고 답변", + "항변", "권리남용", "신의칙", "동시이행", "succession", "notice", "lien", + "possession", "asset", "defense", "party_assertion", "registry_object_display", + ], + "meeting_sources": ["succession_clause_ids", "current_state_clause_ids", "conflict_pattern_clause_ids", "mixed_contract_clause_ids"], + "event_sources": ["succession_candidate_ids", "state_candidate_ids"], + "hazard_keys": ["succession_related_candidates", "current_state_expressions", "conflict_pattern_expressions", "asset_clusters", "mixed_contract_candidates"], + "taxonomy_axes": ["structure", "time_effect", "legal_object", "defect_or_condition"], + }, + } + + # 3등급 라우팅 구조화 셀렉터 (개선전략서 §5.4 X-1): primary 승격은 구조화 키 + # (event_kind/event_subkind/component_summary)와 named-component strong term만 가능. + # broad term(등기·변제·항변·점유·목적물 등)의 단독 blob 매칭은 support 등급에 머문다. + A0_STRUCTURED_ROUTING = { + "B1_Money_Successor": { + "event_kinds": ["채권발생", "변제", "채권관리", "영업양도", "사업자등록"], + "event_subkinds": ["contract_formation", "대가수수", "payment_consideration", + "debt_acknowledgement", "new_due_date", "underlying_claim_arising", + "demand_notice_dispatch", "demand_notice_arrival", + "commercial_successor_liability_candidate", "defense_assertion_setoff", + "defense_assertion_benefit_applied_to_secured_claim"], + "evidence_component_keys": ["claim_document_components", "loan_components", + "business_transfer_components", "admission_components", + "mixed_contract_payment_semantics", "notice_components", "defense_assertions"], + "strong_terms": ["차용", "금전소비대차", "대여", "공사대금", "물품대금", "매매대금", + "지급각서", "이행각서", "채무승인", "상호속용", "영업양도", "사업자등록", "상계", + "답변서", "도급"], + }, + "B2_Secured_Registry": { + "event_kinds": ["담보설정", "담보말소", "경매개시", "경매매각", "소유권이전", "변제", + "가압류/압류", "증여"], + "event_subkinds": ["secured_debt_creation", "mortgage_extinguishment_by_fraction", + "auction_application_after_extinguishment", "auction_sale_and_registry_transfer", + "소유권보존", "매매", "defense_assertion_benefit_applied_to_secured_claim", + "defense_assertion_setoff", "defense_assertion_subsequent_owner_no_extinction_right"], + "evidence_component_keys": ["registry_row_components", "secured_debt_components", + "legal_calculation_object", "legal_calculation_objects", "registry_object_display", + "registry_row_role_map", "loan_components", "claim_document_components"], + "strong_terms": ["근저당", "저당권", "말소", "소유권이전등기", "등기부", "등기사항", + "경매", "배당", "변제충당", "법정변제충당", "채권최고액", "유치권", "상계", + "답변서", "차용", "도급"], + }, + "B3_Land_Valuation": { + "event_kinds": ["가치평가", "임대차", "인도", "점유", "사업자등록"], + "event_subkinds": ["ongoing_physical_possession", "해제/해지의사표시", "책임부인", + "defense_assertion_benefit_applied_to_secured_claim"], + "evidence_component_keys": ["valuation_object", "valuation_rows", "valuation_matrix", + "contract_components"], + "strong_terms": ["감정", "임료", "시세", "사용수익", "사용이익", "명도", "임대차", + "매매계약", "사업자등록", "답변서", "해제", "차임"], + }, + "B4_Actio": { + "event_kinds": ["처분", "증여", "소유권이전", "담보설정", "담보말소", "채권발생", + "가치평가", "가압류/압류"], + "event_subkinds": ["대가수수", "매매", "mortgage_extinguishment_by_fraction", + "underlying_claim_arising", "secured_debt_creation", "defense_assertion_setoff"], + "evidence_component_keys": ["claim_document_components", "loan_components", + "valuation_object", "valuation_matrix", "registry_row_components", + "defense_assertions"], + "strong_terms": ["사해", "피보전", "채무초과", "책임재산", "명의신탁", "수익자", + "전득자", "가액배상", "증여", "감정", "답변서"], + }, + "B5_Succession_Notice_Lien_Asset_Defense": { + "event_kinds": ["상속포기", "상속개시", "통지", "도달", "항변", "점유", "인도", + "사업자등록", "판결/결정", "영업양도"], + "event_subkinds": ["succession_renunciation_filing", "succession_renunciation_acceptance", + "demand_notice_dispatch", "demand_notice_arrival", "해제/해지의사표시", + "해제/해지 통지", "책임부인", "ongoing_physical_possession", + "defense_assertion_setoff", "defense_assertion_benefit_applied_to_secured_claim", + "defense_assertion_subsequent_owner_no_extinction_right"], + "evidence_component_keys": ["succession_document_components", "notice_components", + "defense_assertions", "schedule_items", "business_transfer_components", + "contract_components", "registry_row_components"], + "strong_terms": ["상속", "유치권", "소멸청구", "가족관계", "사업자등록", "별지", + "상호속용", "해제", "심판", "최고"], + }, + } + + A0_MEETING_FIELDS = ["clause_id", "local_clause_id", "section_key", "section_label", "major_no", + "sub_no", "heading", "line_start", "line_end", "text_excerpt", "tags"] + A0_EVENT_FIELDS = ["candidate_id", "source_evidence_index", "title", "doc_type", "event_or_state", "event_kind", "event_subkind", "action_type_candidate", "action_summary", "participants", "object_spec", "amount", "event_date", "event_end_date", "time_text", "time_precision", "current_state_candidate", "succession_candidate", "legal_effect_candidate", "defense_candidate", "commercial_successor_candidates", "meeting_clause_refs", "actio_relevance_candidates", "identity_signature", "property_cluster_id", "affected_asset_cluster_ids", "doc_semantic_flags", "confidence", "fraction_ref"] + A0_EVIDENCE_FIELDS = ["evidence_index", "doc_uid", "title", "title_normalized", "doc_type", "key_facts", "key_dates", "key_amounts", "key_parties", "source_pointer", "property_cluster_id", "doc_semantic_flags", "multi_event_registry", "component_summary", "component_details"] + A0_STAGE_A_PATH = "stage1_tmp/task_c_bo/stage_a_context.json" + A0_MANIFEST_PATH = "stage1_tmp/task_c_bo/source_universe_manifest.json" + A0_SLICE_PATHS = {dom: f"stage1_tmp/task_c_bo/domain_slices/{dom.split('_')[0]}.json" for dom in A0_DOMAIN_CONFIGS} + A0_BROADNESS_WARN_RATIO = 0.6 + + def _a0_write(path, content): + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def _a0_natural_key(value): + return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", str(value))] + + def _a0_sorted_ids(values): + return sorted([str(v) for v in values if str(v)], key=_a0_natural_key) + + def _a0_list(value): + return value if isinstance(value, list) else [] + + def _a0_blob(value): + return json.dumps(value, ensure_ascii=False, sort_keys=True).lower() + + def _a0_matches(value, terms): + haystack = _a0_blob(value) + return any(str(term).lower() in haystack for term in terms if str(term).strip()) + + def _a0_copy(source, fields): + return {f: source.get(f) for f in fields if f in source} + + def _a0_matched_domains(record): + return [d for d, cfg in A0_DOMAIN_CONFIGS.items() if _a0_matches(record, cfg["terms"])] + + def _a0_event_primary(record, routing): + if str(record.get("event_kind")) in routing["event_kinds"]: + return True + if str(record.get("event_subkind")) in routing["event_subkinds"]: + return True + return _a0_matches(record, routing["strong_terms"]) + + def _a0_evidence_primary(record, routing): + summary = record.get("component_summary") or {} + if any(summary.get(key) for key in routing["evidence_component_keys"]): + return True + return _a0_matches(record, routing["strong_terms"]) + + def _a0_clause_primary(record, routing): + return _a0_matches(record, routing["strong_terms"]) + + def _a0_select(stage_a, config): + """3등급 라우팅(X-1): 구조화 키/named-component 매칭 = primary, + broad term 단독 매칭·closure 유입 = support, 잔여 참조 = review_only(id만).""" + routing = A0_STRUCTURED_ROUTING[config["domain_id"]] + meeting_map = stage_a.get("meeting_clause_map") or {} + event_map = stage_a.get("event_candidate_map") or {} + evidence_map = stage_a.get("evidence_authority_map") or {} + by_clause = meeting_map.get("by_clause_id") or {} + by_event = event_map.get("by_candidate_id") or {} + by_evidence = evidence_map.get("by_evidence_index") or {} + + # 1) primary: 구조화 키(event_kind/event_subkind/component_summary) + named-component strong term + # + hazard 유래 구조화 id 목록(meeting_sources/event_sources). broad term 단독으로는 승격 불가. + primary = {"clause": set(), "event": set(), "evidence": set()} + for key in config["meeting_sources"]: + for value in _a0_list(meeting_map.get(key)): + if str(value) in by_clause: + primary["clause"].add(str(value)) + for key in config["event_sources"]: + for value in _a0_list(event_map.get(key)): + if str(value) in by_event: + primary["event"].add(str(value)) + for cid, record in by_clause.items(): + if _a0_clause_primary(record, routing): + primary["clause"].add(str(cid)) + for eid, record in by_event.items(): + if _a0_event_primary(record, routing): + primary["event"].add(str(eid)) + for vid, record in by_evidence.items(): + if _a0_evidence_primary(record, routing): + primary["evidence"].add(str(vid)) + + # 2) support: broad term 매칭(비-primary) + primary의 source-ref closure 유입분 + clause_ids = set(primary["clause"]) + event_ids = set(primary["event"]) + evidence_ids = set(primary["evidence"]) + for cid, record in by_clause.items(): + if _a0_matches(record, config["terms"]): + clause_ids.add(str(cid)) + for eid, record in by_event.items(): + if _a0_matches(record, config["terms"]): + event_ids.add(str(eid)) + for vid, record in by_evidence.items(): + if _a0_matches(record, config["terms"]): + evidence_ids.add(str(vid)) + for _ in range(2): + for evidence_id in list(evidence_ids): + event_ids.update(str(v) for v in _a0_list((event_map.get("by_evidence_index") or {}).get(evidence_id))) + for clause_id in list(clause_ids): + event_ids.update(str(v) for v in _a0_list((event_map.get("by_meeting_clause_ref") or {}).get(clause_id))) + for event_id in list(event_ids): + event = by_event.get(event_id) or {} + if event.get("source_evidence_index"): + evidence_ids.add(str(event.get("source_evidence_index"))) + for ref in _a0_list(event.get("meeting_clause_refs")): + clause_ids.add(str(ref)) + + clause_ids = {v for v in clause_ids if v in by_clause} + event_ids = {v for v in event_ids if v in by_event} + evidence_ids = {v for v in evidence_ids if v in by_evidence} + support = {"clause": clause_ids - primary["clause"], "event": event_ids - primary["event"], + "evidence": evidence_ids - primary["evidence"]} + # 3) review_only: universe 잔여 참조 (id만 — 존재 확인용) + review_only = {"clause": set(by_clause) - clause_ids, "event": set(by_event) - event_ids, + "evidence": set(by_evidence) - evidence_ids} + + def _label(source, item_id, keys): + rec = source.get(item_id) or {} + for k in keys: + v = rec.get(k) + if v: + return _short_excerpt(v, 80) + return item_id + + shared = [] + for item_type, ids_all, source in [("meeting_clause", clause_ids, by_clause), + ("event_candidate", event_ids, by_event), + ("evidence", evidence_ids, by_evidence)]: + for item_id in _a0_sorted_ids(ids_all): + domains = _a0_matched_domains(source.get(item_id) or {}) + if len(domains) > 1: + shared.append({"item_type": item_type, "id": item_id, "matched_domain_ids": domains}) + + support_refs = ( + [{"item_type": "meeting_clause", "id": i, "label": _label(by_clause, i, ["heading", "text_excerpt"])} for i in _a0_sorted_ids(support["clause"])] + + [{"item_type": "event_candidate", "id": i, "label": _label(by_event, i, ["action_summary", "title"])} for i in _a0_sorted_ids(support["event"])] + + [{"item_type": "evidence", "id": i, "label": _label(by_evidence, i, ["title"])} for i in _a0_sorted_ids(support["evidence"])] + ) + review_only_refs = ( + [{"item_type": "meeting_clause", "id": i} for i in _a0_sorted_ids(review_only["clause"])] + + [{"item_type": "event_candidate", "id": i} for i in _a0_sorted_ids(review_only["event"])] + + [{"item_type": "evidence", "id": i} for i in _a0_sorted_ids(review_only["evidence"])] + ) + return { + "meeting_clause_ids": _a0_sorted_ids(clause_ids), + "event_candidate_ids": _a0_sorted_ids(event_ids), + "evidence_indexes": _a0_sorted_ids(evidence_ids), + "primary": primary, + "selected_meetings": [_a0_copy(by_clause[v], A0_MEETING_FIELDS) for v in _a0_sorted_ids(primary["clause"])], + "selected_events": [_a0_copy(by_event[v], A0_EVENT_FIELDS) for v in _a0_sorted_ids(primary["event"])], + "selected_evidence": [_a0_copy(by_evidence[v], A0_EVIDENCE_FIELDS) for v in _a0_sorted_ids(primary["evidence"])], + "support_refs": support_refs, + "review_only_refs": review_only_refs, + "shared": shared, + } + + def _a0_taxonomy_subset(stage_a, config): + taxonomy = stage_a.get("juristic_act_taxonomy_map") or {} + axes = taxonomy.get("axis_groups") or {} + return { + "schema_version": taxonomy.get("schema_version"), + "categories": taxonomy.get("categories") or {}, + "example_to_gubun": taxonomy.get("example_to_gubun") or {}, + "axis_groups": {key: axes.get(key) for key in config["taxonomy_axes"] if key in axes}, + "source_digest_sha256": taxonomy.get("source_digest_sha256"), + } + + def _a0_hazard_subset(stage_a, config): + hazard = stage_a.get("global_hazard_seed") or {} + return {key: hazard.get(key) for key in config["hazard_keys"] if key in hazard} + + def _a0_part1_review(stage_a, selected, config): + handoff = stage_a.get("part1_soft_gate_handoff") or {} + guard = { + "schema_version": handoff.get("schema_version"), + "status": handoff.get("status"), + "stage2_auto_progression_allowed": handoff.get("stage2_auto_progression_allowed"), + "usage_rules": handoff.get("usage_rules") or {}, + } + se, sev, sm = set(selected["evidence_indexes"]), set(selected["event_candidate_ids"]), set(selected["meeting_clause_ids"]) + relevant = [] + for item in _a0_list(handoff.get("review_items_compact")): + refs = item.get("source_refs") if isinstance(item, dict) else {} + domain_text = _a0_blob({ + "domain_hints": item.get("domain_hints"), + "target_task_hints": item.get("target_task_hints"), + "issue_type": item.get("issue_type"), + "review_instruction": item.get("review_instruction"), + }) + ref_hit = bool( + se.intersection(str(v) for v in _a0_list((refs or {}).get("evidence_indexes"))) + or sev.intersection(str(v) for v in _a0_list((refs or {}).get("event_candidate_ids"))) + or sm.intersection(str(v) for v in _a0_list((refs or {}).get("meeting_clause_refs"))) + ) + if ref_hit or any(str(term).lower() in domain_text for term in config["terms"]): + relevant.append(item) + return guard, relevant + + def _a0_emit(stage_a_context): + import hashlib as _hl + _a0_write(A0_STAGE_A_PATH, json.dumps({"stage_a_context": stage_a_context}, ensure_ascii=False, indent=2)) + meeting_map = stage_a_context.get("meeting_clause_map") or {} + event_map = stage_a_context.get("event_candidate_map") or {} + evidence_map = stage_a_context.get("evidence_authority_map") or {} + manifest = { + "schema_version": "source_universe_manifest.v1", + "input_digests_sha256": stage_a_context.get("input_digests_sha256") or {}, + "evidence_index_set": evidence_map.get("evidence_index_set") or [], + "event_candidate_ids": event_map.get("candidate_id_set") or [], + "meeting_clause_ids": meeting_map.get("clause_order") or [], + "counts": { + "evidence": len(evidence_map.get("evidence_index_set") or []), + "event": len(event_map.get("candidate_id_set") or []), + "meeting": len(meeting_map.get("clause_order") or []), + }, + } + _a0_write(A0_MANIFEST_PATH, json.dumps(manifest, ensure_ascii=False, indent=2)) + digests_summary = _hl.sha256(json.dumps(stage_a_context.get("input_digests_sha256") or {}, + ensure_ascii=False, sort_keys=True).encode("utf-8")).hexdigest() + worker_contract = stage_a_context.get("stage_b_worker_contract") or {} + universe_events = len(event_map.get("candidate_id_set") or []) or 1 + universe_evidence = len(evidence_map.get("evidence_index_set") or []) or 1 + routing_summary = {} + for domain_id, config in A0_DOMAIN_CONFIGS.items(): + selected = _a0_select(stage_a_context, config) + part1_guard, part1_items = _a0_part1_review(stage_a_context, selected, config) + primary = selected["primary"] + broadness = (len(primary["event"]) + len(primary["evidence"])) / (universe_events + universe_evidence) + warnings = ["ROUTING_BROADNESS_WARNING"] if broadness > A0_BROADNESS_WARN_RATIO else [] + slice_obj = { + "stage_b_domain_slice": { + "schema_version": "task_c_bo_stage_b_domain_slice.v1", + "stage": "Task_C_BO.Stage_B0.DomainInputRouter", + "router_task": "Task_C_BO_A0_context_and_domain_slice_compiler", + "router_status": "READY", + "domain_id": domain_id, + "domain_label": config["domain_label"], + "allowed_legal_effect_bo_types": config["allowed_legal_effect_bo_types"], + "source_stage_a_schema_version": stage_a_context.get("schema_version"), + "source_stage_a_status": stage_a_context.get("status"), + "source_stage_a_created_at_utc": stage_a_context.get("created_at_utc"), + "input_digests_sha256": stage_a_context.get("input_digests_sha256") or {}, + "slice_guard_echo": { + "source_stage_a_created_at_utc": stage_a_context.get("created_at_utc"), + "input_digests_summary": digests_summary, + }, + "stage_b_worker_contract": { + "context_is_read_only": worker_contract.get("context_is_read_only"), + "bo_id_finalization_allowed": worker_contract.get("bo_id_finalization_allowed"), + "candidate_generation_allowed": worker_contract.get("candidate_generation_allowed"), + "must_use_evidence_index_set": worker_contract.get("must_use_evidence_index_set") or [], + }, + "source_universe": { + "meeting_clause_ids": meeting_map.get("clause_order") or [], + "event_candidate_ids": event_map.get("candidate_id_set") or [], + "evidence_indexes": evidence_map.get("evidence_index_set") or [], + }, + "selected_meeting_clauses": selected["selected_meetings"], + "selected_event_candidates": selected["selected_events"], + "selected_evidence_authority": selected["selected_evidence"], + "support_refs": selected["support_refs"], + "review_only_refs": selected["review_only_refs"], + "juristic_act_taxonomy_subset": _a0_taxonomy_subset(stage_a_context, config), + "hazard_subset": _a0_hazard_subset(stage_a_context, config), + "part1_review_guard": part1_guard, + "part1_review_items_relevant": part1_items, + "shared_or_ambiguous_items": selected["shared"], + "routing_audit": { + "primary_ids": { + "meeting_clause_ids": _a0_sorted_ids(primary["clause"]), + "event_candidate_ids": _a0_sorted_ids(primary["event"]), + "evidence_indexes": _a0_sorted_ids(primary["evidence"]), + }, + "support_ids": [r["id"] for r in selected["support_refs"]], + "review_only_ids": [r["id"] for r in selected["review_only_refs"]], + "selected_meeting_clause_ids": selected["meeting_clause_ids"], + "selected_event_candidate_ids": selected["event_candidate_ids"], + "selected_evidence_indexes": selected["evidence_indexes"], + "multi_domain_item_refs": selected["shared"], + "unrouted_but_preserved_refs": [], + "broadness_ratio": round(broadness, 4), + "warnings": warnings, + "selection_basis": "deterministic keyword/flag/doc_type routing; closure-joined items demoted to support tier", + "field_policy": "primary records are full compact authority projections; support items are id+label refs only.", + }, + } + } + _a0_write(A0_SLICE_PATHS[domain_id], json.dumps(slice_obj, ensure_ascii=False, indent=2)) + routing_summary[domain_id] = { + "primary": {"meeting": len(primary["clause"]), "event": len(primary["event"]), "evidence": len(primary["evidence"])}, + "support": len(selected["support_refs"]), + "broadness_ratio": round(broadness, 4), + "warnings": warnings, + } + print(json.dumps({ + "status": "READY", + "message": "A0 context + domain slice compile 완료", + "stage_a_path": A0_STAGE_A_PATH, + "source_manifest_path": A0_MANIFEST_PATH, + "domain_slice_paths": A0_SLICE_PATHS, + "input_digests_sha256": stage_a_context.get("input_digests_sha256") or {}, + "routing_summary": routing_summary, + }, ensure_ascii=False)) + + + def main() -> None: + _init() + + # Stage A.1-A.2: allowed input guard + existence check. + existence_check = _preflight_required_files() + + # Stage A.3-A.4: read exactly four inputs once, then parse/fail fast. + docs = _read_required_docs_once() + event_candidates = _parse_json_doc("evidence_event_candidates.json", docs["evidence_event_candidates.json"]) + evidence_indexed = _parse_json_doc("evidence_indexed.json", docs["evidence_indexed.json"]) + meeting_text = docs["client_meeting.md"] + juristic_text = docs["Default_Agent/Juristic_Act.md"] + + # Stage A.5-A.8: build normalized maps and the global hazard seed. + meeting_clause_map = _build_meeting_clause_map(meeting_text) + event_candidate_map = _build_event_candidate_map(event_candidates) + evidence_authority_map = _build_evidence_authority_map(evidence_indexed) + juristic_act_taxonomy_map = _build_juristic_act_map(juristic_text) + _validate_juristic_act_map(juristic_act_taxonomy_map) + global_hazard_seed = _build_global_hazard_seed(meeting_clause_map, event_candidate_map, evidence_authority_map) + + if not evidence_authority_map["evidence_index_set"]: + raise RuntimeError("No finalized evidence_index values available") + + input_digests_sha256 = { + name: _stable_hash(docs[name]) + for name in REQUIRED_FILES + } + part1_soft_gate_handoff = _compact_part1_soft_gate_handoff( + existence_check, + input_digests_sha256, + meeting_clause_map, + event_candidate_map, + evidence_authority_map, + ) + + stage_a_context = { + "schema_version": "task_c_bo_stage_a_context.v1", + "stage": "Task_C_BO.Stage_A", + "status": "READY", + "created_at_utc": datetime.now(timezone.utc).isoformat(), + "allowed_input_files": REQUIRED_FILES, + "forbidden_files_attested": ["evidence_all.json", "BO.json", "actio_case_signals.json", "case_liability_signals.json", "legal_effect_signals.json"], + "file_io_policy": { + "list_docs_called_for": REQUIRED_FILES, + "read_docs_called_once_for": REQUIRED_FILES, + "write_file_called": False, + "raw_evidence_all_read": False, + "output_file_written": False, + }, + "existence_check": existence_check, + "input_digests_sha256": input_digests_sha256, + "meeting_clause_map": meeting_clause_map, + "event_candidate_map": event_candidate_map, + "evidence_authority_map": evidence_authority_map, + "juristic_act_taxonomy_map": juristic_act_taxonomy_map, + "global_hazard_seed": global_hazard_seed, + "part1_soft_gate_handoff": part1_soft_gate_handoff, + "stage_b_worker_contract": { + "context_is_read_only": True, + "bo_id_finalization_allowed": False, + "candidate_generation_allowed": True, + "must_use_evidence_index_set": evidence_authority_map["evidence_index_set"], + }, + } + + _a0_emit(stage_a_context) + + if __name__ == "__main__": + try: + main() + except Exception as exc: + print(json.dumps({"stage": "Task_C_BO.Stage_A", "status": "FAILED", "error": str(exc)}, ensure_ascii=False)) + raise + - task_name: Task_C_BO_Stage_B_B1_Money_Successor + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: true + preflight_files: + - stage1_tmp/task_c_bo/domain_slices/B1.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + 본 task의 유일한 입력은 preflight로 제공된 도메인 slice 파일 `stage1_tmp/task_c_bo/domain_slices/B1.json`의 내용이다. 그 안의 `stage_b_domain_slice` 객체만 사용한다. 다른 파일 요청·재구성은 금지된다. + + + + + - You MUST use only the `stage_b_domain_slice` object provided via the preflight file `stage1_tmp/task_c_bo/domain_slices/B1.json`. + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B1_Money_Successor"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B1_Money_Successor + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for money claim, payment, notice, admission, and commercial successor liability. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B1:001`, `B1:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - money claim origin: loan, sale price, deposit, settlement, construction price, unpaid balance + - due date, acceleration, demand, notice, payment deadline, or delayed performance + - partial payment, debt admission, payment memorandum, balance confirmation, account statement, or settlement confirmation + - business transfer, trade-name continuation, transferee liability, debt non-assumption clause, or commercial successor liability + - uncertain creditor/debtor/successor linkage that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - `support_refs` (id+label 참조 전용 — 사실 인용 근거로 사용 금지) + - `review_only_refs` + - `slice_guard_echo` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use B0 routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the current B0 slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split money claim origin, maturity/demand, payment/admission, and successor liability when they are distinct legal-effect seeds. + 4. Preserve payment history, demand/notice hints, admission hints, successor liability hints, and successor-link uncertainty in `extensions.domain_payload`. Preserve agreed interest rate and agreed delay damage rate as literal strings from B1 `loan_components`/`secured_debt_components` (`component_details` of `selected_evidence_authority`) when present; never substitute statutory rates, and keep the supporting evidence indexes in `interest_rate_source_evidence_refs`. + 5. Use `domain_review_queue` for missing amount/date, uncertain creditor/debtor identity, conflicting payment status, or cross-domain successor merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Keep all unsupported or uncertain values as null or [] and route review needs to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + - 값이 null, [], {}, 빈 문자열인 키는 출력하지 않는다. 의미 있는 false와 0은 유지한다. 후속 결정적 reducer(R0)가 canonical schema로 확장하며 키 부재를 null로 해석한다. + - 다음 필드는 출력하지 않는다 (R0가 결정적으로 재구성/복원한다): `transport_candidate_refs`, `Reason`, `PriorAct`, `ReasonRefs`, `downstream_seed_refs`(단, 선행행위 후보가 실재할 때의 `prior_candidate_refs`/`reason_refs_candidate_refs`는 예외적으로 출력 가능), `slice_guard`의 `source_universe`/`input_digests_sha256`/`stage_b_worker_contract` 전체 에코. + - `transport_metadata.slice_guard`에는 slice의 `slice_guard_echo` 값 2개(`source_stage_a_created_at_utc`, `input_digests_summary`)만 문자 그대로 복사한다. + - `domain_review_queue` 항목은 `review_id`, `severity`, `issue_type`, `source_event_candidate_ids`, `source_evidence_indexes`, `source_meeting_clause_ids`, `recommended_downstream_owner`만 출력한다. 서술형 `description`은 출력하지 않는다(고정 문구는 R0가 부여한다). + - candidate의 실질 내용(BOType, ActionType, JuristicAct, Action, core_field_base, amount, source refs, domain_payload의 유의미 값, 도메인 named fields)은 생략 대상이 아니다 — 값이 있으면 반드시 출력한다. + + + + Return exactly one JSON object: + { + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B1_Money_Successor", + "status": "READY|READY_WITH_REVIEW|FAILED", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B1_Money_Successor", + "domain_label": "금전채권 및 상호속용 책임", + "slice_guard": { + "source_stage_a_created_at_utc": "slice_guard_echo에서 문자 그대로 복사", + "input_digests_summary": "slice_guard_echo에서 문자 그대로 복사" + } + }, + "bo_seed_candidates": [ + { + "candidate_ref": "B1:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "JuristicAct": {"label": "string|null"}, + "Action": "short neutral factual/legal-action draft", + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [], + "asset_refs": [], + "legal_effect_summary": null + }, + "amount": { + "value": null, + "currency": "KRW|null", + "basis": null + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability", + "title_hint": null, + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "money_claim_details": {"claim_type_candidate": null, "principal_amount_hint": null, "interest_hint": null, "delay_damage_hint": null, "agreed_interest_rate": null, "agreed_delay_damage_rate": null, "interest_accrual_start_hint": null, "interest_rate_source_evidence_refs": [], "unpaid_balance_hint": null, "creditor_candidate": null, "debtor_candidate": null}, + "payment_history": [], + "demand_or_notice_hints": [], + "admission_hints": [], + "successor_liability_hints": [], + "successor_link_uncertainty": "string|null", + "conflict_flags": [] + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B1:review:001", + "severity": "SOFT_WARNING|HARD_WARNING", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "publisher|domain_join|C0|C1|C2|C3|C5|D|E" + } + ] + } + } + + Status rule: use `FAILED` when any input acceptance or `stage_b_worker_contract` guard fails; use `READY_WITH_REVIEW` when `domain_review_queue` is non-empty and no hard gate failed; otherwise use `READY`. + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B2_Secured_Registry + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: true + preflight_files: + - stage1_tmp/task_c_bo/domain_slices/B2.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + 본 task의 유일한 입력은 preflight로 제공된 도메인 slice 파일 `stage1_tmp/task_c_bo/domain_slices/B2.json`의 내용이다. 그 안의 `stage_b_domain_slice` 객체만 사용한다. 다른 파일 요청·재구성은 금지된다. + + + + + - You MUST use only the `stage_b_domain_slice` object provided via the preflight file `stage1_tmp/task_c_bo/domain_slices/B2.json`. + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B2_Secured_Registry"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B2_Secured_Registry + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for secured debt, registry, payment allocation, auction, distribution, and derivative invalidity issues. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B2:001`, `B2:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - secured debt creation, change, repayment, payment allocation, and secured obligation balance + - mortgage, pledge, collateral security, registration, cancellation, correction, or invalidity chain + - auction, distribution, dividend, satisfaction, or residual balance facts + - derivative invalidity, registry error, title defect, or secured property dispute + - uncertain registry sequence or security/right-holder linkage that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - `support_refs` (id+label 참조 전용 — 사실 인용 근거로 사용 금지) + - `review_only_refs` + - `slice_guard_echo` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use B0 routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the current B0 slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split secured debt chain, registry act, payment allocation, auction step, dividend distribution, and derivative invalidity when they are distinct legal-effect seeds. + 4. Preserve registry rows, secured debt chain, payment allocation, auction step, dividend distribution hints, derivative invalidity hints, and registry display values in `extensions.domain_payload`. Carry `fraction_ref` from source event candidates unchanged; do not merge or renumber. Populate `fractional_effect_events` as an event-reference list only (fraction-bearing setting/extinguishment/transfer events); never fold fraction states or normalize/reduce fractions (that is Stage 2 work). + 5. Use `domain_review_queue` for missing registry date, unclear secured obligation balance, conflicting registration sequence, or cross-domain property merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Keep all unsupported or uncertain values as null or [] and route review needs to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + - 값이 null, [], {}, 빈 문자열인 키는 출력하지 않는다. 의미 있는 false와 0은 유지한다. 후속 결정적 reducer(R0)가 canonical schema로 확장하며 키 부재를 null로 해석한다. + - 다음 필드는 출력하지 않는다 (R0가 결정적으로 재구성/복원한다): `transport_candidate_refs`, `Reason`, `PriorAct`, `ReasonRefs`, `downstream_seed_refs`(단, 선행행위 후보가 실재할 때의 `prior_candidate_refs`/`reason_refs_candidate_refs`는 예외적으로 출력 가능), `slice_guard`의 `source_universe`/`input_digests_sha256`/`stage_b_worker_contract` 전체 에코. + - `transport_metadata.slice_guard`에는 slice의 `slice_guard_echo` 값 2개(`source_stage_a_created_at_utc`, `input_digests_summary`)만 문자 그대로 복사한다. + - `domain_review_queue` 항목은 `review_id`, `severity`, `issue_type`, `source_event_candidate_ids`, `source_evidence_indexes`, `source_meeting_clause_ids`, `recommended_downstream_owner`만 출력한다. 서술형 `description`은 출력하지 않는다(고정 문구는 R0가 부여한다). + - candidate의 실질 내용(BOType, ActionType, JuristicAct, Action, core_field_base, amount, source refs, domain_payload의 유의미 값, 도메인 named fields)은 생략 대상이 아니다 — 값이 있으면 반드시 출력한다. + + + + Return exactly one JSON object: + { + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B2_Secured_Registry", + "status": "READY|READY_WITH_REVIEW|FAILED", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B2_Secured_Registry", + "domain_label": "담보채무, 변제충당, 등기무효, 경매", + "slice_guard": { + "source_stage_a_created_at_utc": "slice_guard_echo에서 문자 그대로 복사", + "input_digests_summary": "slice_guard_echo에서 문자 그대로 복사" + } + }, + "bo_seed_candidates": [ + { + "candidate_ref": "B2:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "JuristicAct": {"label": "string|null"}, + "Action": "short neutral factual/legal-action draft", + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [], + "asset_refs": [], + "legal_effect_summary": null + }, + "amount": { + "value": null, + "currency": "KRW|null", + "basis": null + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction", + "title_hint": null, + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "registry_rows": [ + {"registry_ref": null, "fraction_ref": null, "fraction_display": null, "fraction_numerator": null, "fraction_denominator": null, "receipt_no": null} + ], + "fractional_effect_events": [ + {"fraction_ref": null, "fraction_display": null, "fraction_numerator": null, "fraction_denominator": null, "effect_kind_hint": "mortgage_setting|mortgage_extinguishment|transfer|other", "source_event_candidate_ids": [], "registry_refs": [], "evidence_indexes": []} + ], + "secured_debt_chain": [], + "payment_allocation": [], + "mortgage_extinguishment_hints": [], + "auction_steps": [], + "dividend_distribution_hints": [], + "derivative_invalidity_hints": [], + "registry_object_display": "string|null" + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B2:review:001", + "severity": "SOFT_WARNING|HARD_WARNING", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "publisher|domain_join|C0|C1|C2|C3|C5|D|E" + } + ] + } + } + + Status rule: use `FAILED` when any input acceptance or `stage_b_worker_contract` guard fails; use `READY_WITH_REVIEW` when `domain_review_queue` is non-empty and no hard gate failed; otherwise use `READY`. + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B3_Land_Valuation + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: true + preflight_files: + - stage1_tmp/task_c_bo/domain_slices/B3.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + 본 task의 유일한 입력은 preflight로 제공된 도메인 slice 파일 `stage1_tmp/task_c_bo/domain_slices/B3.json`의 내용이다. 그 안의 `stage_b_domain_slice` 객체만 사용한다. 다른 파일 요청·재구성은 금지된다. + + + + + - You MUST use only the `stage_b_domain_slice` object provided via the preflight file `stage1_tmp/task_c_bo/domain_slices/B3.json`. + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B3_Land_Valuation"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B3_Land_Valuation + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for land use gain, possession/use, valuation, and asset-value facts. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B3:001`, `B3:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - land or building possession, use, occupation, benefit, rent-equivalent gain, or loss of use + - valuation fact, appraisal, market value, rent value, area, period, and calculation base + - delivery, possession start/end, unauthorized use, co-occupancy, or liability overlap + - asset identity, address, parcel, unit, area, or other value-bearing object description + - uncertain use period, value basis, possession status, or asset binding that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - `support_refs` (id+label 참조 전용 — 사실 인용 근거로 사용 금지) + - `review_only_refs` + - `slice_guard_echo` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use B0 routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the current B0 slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split possession/use state, use-gain period, valuation fact, and liability overlap when they are distinct legal-effect seeds. + 4. Preserve valuation rows, valuation matrix, use-gain facts, possession state, asset binding hints, and liability overlap hints in `extensions.domain_payload`. Populate the `possession_state` two-track structure (possession facts vs. asserted legal causes) from selected event candidates only; never resolve which legal cause prevails (that is Stage 2 work); preserve conflicting assertions side by side. + 5. Use `domain_review_queue` for missing period, missing value base, unclear possession state, or cross-domain asset merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Keep all unsupported or uncertain values as null or [] and route review needs to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + - 값이 null, [], {}, 빈 문자열인 키는 출력하지 않는다. 의미 있는 false와 0은 유지한다. 후속 결정적 reducer(R0)가 canonical schema로 확장하며 키 부재를 null로 해석한다. + - 다음 필드는 출력하지 않는다 (R0가 결정적으로 재구성/복원한다): `transport_candidate_refs`, `Reason`, `PriorAct`, `ReasonRefs`, `downstream_seed_refs`(단, 선행행위 후보가 실재할 때의 `prior_candidate_refs`/`reason_refs_candidate_refs`는 예외적으로 출력 가능), `slice_guard`의 `source_universe`/`input_digests_sha256`/`stage_b_worker_contract` 전체 에코. + - `transport_metadata.slice_guard`에는 slice의 `slice_guard_echo` 값 2개(`source_stage_a_created_at_utc`, `input_digests_summary`)만 문자 그대로 복사한다. + - `domain_review_queue` 항목은 `review_id`, `severity`, `issue_type`, `source_event_candidate_ids`, `source_evidence_indexes`, `source_meeting_clause_ids`, `recommended_downstream_owner`만 출력한다. 서술형 `description`은 출력하지 않는다(고정 문구는 R0가 부여한다). + - candidate의 실질 내용(BOType, ActionType, JuristicAct, Action, core_field_base, amount, source refs, domain_payload의 유의미 값, 도메인 named fields)은 생략 대상이 아니다 — 값이 있으면 반드시 출력한다. + + + + Return exactly one JSON object: + { + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B3_Land_Valuation", + "status": "READY|READY_WITH_REVIEW|FAILED", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B3_Land_Valuation", + "domain_label": "토지 사용이익, 점유, 사용수익, 가치평가", + "slice_guard": { + "source_stage_a_created_at_utc": "slice_guard_echo에서 문자 그대로 복사", + "input_digests_summary": "slice_guard_echo에서 문자 그대로 복사" + } + }, + "bo_seed_candidates": [ + { + "candidate_ref": "B3:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "JuristicAct": {"label": "string|null"}, + "Action": "short neutral factual/legal-action draft", + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [], + "asset_refs": [], + "legal_effect_summary": null + }, + "amount": { + "value": null, + "currency": "KRW|null", + "basis": null + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation", + "title_hint": null, + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "valuation_rows": [], + "valuation_matrix": [], + "valuation_object": "string|null", + "use_gain_facts": [], + "possession_state": { + "asset_ref": null, + "possession_events": [ + {"possessor_candidates": [], "start_date": null, "end_date": null, "source_event_candidate_ids": [], "evidence_indexes": []} + ], + "legal_cause_assertions": [ + {"cause_asserted": "유치권행사중|임대차|사용대차|물권|동시이행항변|명도후잔류|무권원주장|미상", "asserted_by": null, "assertion_date": null, "source_event_candidate_ids": [], "evidence_indexes": []} + ] + }, + "delivery_or_possession_start_hints": [], + "unregistered_building_hints": [], + "liability_overlap_hints": [] + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B3:review:001", + "severity": "SOFT_WARNING|HARD_WARNING", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "publisher|domain_join|C0|C1|C2|C3|C5|D|E" + } + ] + } + } + + Status rule: use `FAILED` when any input acceptance or `stage_b_worker_contract` guard fails; use `READY_WITH_REVIEW` when `domain_review_queue` is non-empty and no hard gate failed; otherwise use `READY`. + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B4_Actio + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: true + preflight_files: + - stage1_tmp/task_c_bo/domain_slices/B4.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + 본 task의 유일한 입력은 preflight로 제공된 도메인 slice 파일 `stage1_tmp/task_c_bo/domain_slices/B4.json`의 내용이다. 그 안의 `stage_b_domain_slice` 객체만 사용한다. 다른 파일 요청·재구성은 금지된다. + + + + + - You MUST use only the `stage_b_domain_slice` object provided via the preflight file `stage1_tmp/task_c_bo/domain_slices/B4.json`. + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B4_Actio"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B4_Actio + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for actio pauliana, preserved claim, debtor fraudulent act, beneficiary/transferee, remedy, cap, and defenses. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B4:001`, `B4:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - preserved claim existence, claim date, creditor/debtor identity, and claim amount basis + - debtor act, asset decrease, insolvency, common collateral impairment, and harmfulness + - beneficiary, transferee, bad faith/good faith, notice, value compensation, restitution, and remedy cap + - encumbrance, valuation, property transfer, mortgage, sale, gift, collateral provision, or registration act used as fraudulent act basis + - uncertain fraudulent act date, beneficiary chain, asset value, remedy mode, or defense rebuttal that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - `support_refs` (id+label 참조 전용 — 사실 인용 근거로 사용 금지) + - `review_only_refs` + - `slice_guard_echo` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use B0 routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the current B0 slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split preserved claim, fraudulent transfer, balance-sheet impairment, encumbrance/valuation, beneficiary/transferee state, remedy/cap, and defense rebuttal when they are distinct legal-effect seeds. + 4. Preserve preserved claim bundle, fraudulent transfer facts, balance sheet, encumbrance timeline, valuation matrix, remedy/cap hints, and defense rebuttal in `extensions.domain_payload`. + 5. Use `domain_review_queue` for missing claim date, uncertain fraudulent act date, conflicting value basis, unclear beneficiary chain, or cross-domain asset/claim merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Keep all unsupported or uncertain values as null or [] and route review needs to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + - 값이 null, [], {}, 빈 문자열인 키는 출력하지 않는다. 의미 있는 false와 0은 유지한다. 후속 결정적 reducer(R0)가 canonical schema로 확장하며 키 부재를 null로 해석한다. + - 다음 필드는 출력하지 않는다 (R0가 결정적으로 재구성/복원한다): `transport_candidate_refs`, `Reason`, `PriorAct`, `ReasonRefs`, `downstream_seed_refs`(단, 선행행위 후보가 실재할 때의 `prior_candidate_refs`/`reason_refs_candidate_refs`는 예외적으로 출력 가능), `slice_guard`의 `source_universe`/`input_digests_sha256`/`stage_b_worker_contract` 전체 에코. + - `transport_metadata.slice_guard`에는 slice의 `slice_guard_echo` 값 2개(`source_stage_a_created_at_utc`, `input_digests_summary`)만 문자 그대로 복사한다. + - `domain_review_queue` 항목은 `review_id`, `severity`, `issue_type`, `source_event_candidate_ids`, `source_evidence_indexes`, `source_meeting_clause_ids`, `recommended_downstream_owner`만 출력한다. 서술형 `description`은 출력하지 않는다(고정 문구는 R0가 부여한다). + - candidate의 실질 내용(BOType, ActionType, JuristicAct, Action, core_field_base, amount, source refs, domain_payload의 유의미 값, 도메인 named fields)은 생략 대상이 아니다 — 값이 있으면 반드시 출력한다. + + + + Return exactly one JSON object: + { + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B4_Actio", + "status": "READY|READY_WITH_REVIEW|FAILED", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B4_Actio", + "domain_label": "사해행위취소, 피보전채권, 책임재산, 가액배상", + "slice_guard": { + "source_stage_a_created_at_utc": "slice_guard_echo에서 문자 그대로 복사", + "input_digests_summary": "slice_guard_echo에서 문자 그대로 복사" + } + }, + "bo_seed_candidates": [ + { + "candidate_ref": "B4:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "JuristicAct": {"label": "string|null"}, + "Action": "short neutral factual/legal-action draft", + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [], + "asset_refs": [], + "legal_effect_summary": null + }, + "amount": { + "value": null, + "currency": "KRW|null", + "basis": null + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "actio_fraudulent_transfer", + "title_hint": null, + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "preserved_claim_bundle": {}, + "fraudulent_transfer_facts": [], + "debtor_act_hints": [], + "balance_sheet": {"asset_decrease_hints": [], "insolvency_hints": [], "common_collateral_impairment_hints": []}, + "encumbrance_timeline": [], + "valuation_matrix": [], + "remedy_mode_hints": [], + "value_compensation_cap_hints": [], + "beneficiary_transferee_hints": [], + "bad_faith_good_faith_hints": [], + "defense_rebuttal": [] + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B4:review:001", + "severity": "SOFT_WARNING|HARD_WARNING", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "publisher|domain_join|C0|C1|C2|C3|C5|D|E" + } + ] + } + } + + Status rule: use `FAILED` when any input acceptance or `stage_b_worker_contract` guard fails; use `READY_WITH_REVIEW` when `domain_review_queue` is non-empty and no hard gate failed; otherwise use `READY`. + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: [] + cache_control: + mode: auto + ttl: 1h + preflight: true + preflight_files: + - stage1_tmp/task_c_bo/domain_slices/B5.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + 본 task의 유일한 입력은 preflight로 제공된 도메인 slice 파일 `stage1_tmp/task_c_bo/domain_slices/B5.json`의 내용이다. 그 안의 `stage_b_domain_slice` 객체만 사용한다. 다른 파일 요청·재구성은 금지된다. + + + + + - You MUST use only the `stage_b_domain_slice` object provided via the preflight file `stage1_tmp/task_c_bo/domain_slices/B5.json`. + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B5_Succession_Notice_Lien_Asset_Defense"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B5_Succession_Notice_Lien_Asset_Defense + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for succession, notice lifecycle, lien, asset identity, possession, and major defenses. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B5:001`, `B5:002`, ... in deterministic order of the candidate seed list. + - If multiple lien seeds exist, assign `lien_id` values `B5:LIEN:001`, `B5:LIEN:002`, ... ordered by the earliest related event date (ties broken by `candidate_ref` lexicographic order). This id is transport-scope only, not a final BO id. + - Do not produce `violation_candidates` or `status_after_notice_receipt`; route such needs to `domain_review_queue` with `issue_type="legal_theory_required"`. + - `legal_sender_after_resolution.party` must always remain null in Stage 1 (value finalization is Stage 2 legal work); fill only its `status` and `succession_resolution_ref` from directly-read sources. + + + + Create separate seeds when supported for: + - general succession, specific succession, inheritance, assignment, acquisition, transfer, or party-status succession + - notice dispatch, arrival, receipt, refusal, content, timing, and legal effect of notice + - lien, possession, retained object, claim-object connection, asset alias, and asset identity + - simultaneous performance, setoff, payment, prescription, delivery, possession, or other major defense facts + - uncertain notice arrival, lien possession continuity, asset identity, succession scope, or defense linkage that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - `support_refs` (id+label 참조 전용 — 사실 인용 근거로 사용 금지) + - `review_only_refs` + - `slice_guard_echo` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use B0 routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the current B0 slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split succession state, notice lifecycle, lien/possession state, asset identity, and defense rebuttal when they are distinct legal-effect seeds. + 4. Preserve succession resolution, notice lifecycle, lien state, possession state, asset alias, simultaneous-performance hints, setoff/payment defense hints, and defense rebuttal in `extensions.domain_payload`. Populate the `possession_state` two-track structure (possession facts vs. asserted legal causes) from selected event candidates only; never resolve which legal cause prevails (that is Stage 2 work); preserve conflicting assertions side by side. Assign `lien_id` deterministically and reference it from possession/notice payloads of the same seed group. Inherit `notice_id` from B1 `notice_components[*].notice_id_proposed` (`component_details` of `selected_evidence_authority`); when multiple notices exist, split seeds so each seed carries one notice lifecycle. Inherit `schedule_refs`/`registry_refs` (SCH-*/REG-* keys) and `canonical_label_candidate`/`usable_in_relief` into `asset_alias` unchanged. + 5. Use `domain_review_queue` for missing arrival evidence, uncertain asset identity, unclear possession continuity, conflicting succession scope, or cross-domain defense merge need. When `received_date` cannot be established from selected sources, leave it null and add a `domain_review_queue` item with `issue_type="missing_source"` (do not fabricate); the sealed drafting gate is applied downstream at FL. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Keep all unsupported or uncertain values as null or [] and route review needs to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + - 값이 null, [], {}, 빈 문자열인 키는 출력하지 않는다. 의미 있는 false와 0은 유지한다. 후속 결정적 reducer(R0)가 canonical schema로 확장하며 키 부재를 null로 해석한다. + - 다음 필드는 출력하지 않는다 (R0가 결정적으로 재구성/복원한다): `transport_candidate_refs`, `Reason`, `PriorAct`, `ReasonRefs`, `downstream_seed_refs`(단, 선행행위 후보가 실재할 때의 `prior_candidate_refs`/`reason_refs_candidate_refs`는 예외적으로 출력 가능), `slice_guard`의 `source_universe`/`input_digests_sha256`/`stage_b_worker_contract` 전체 에코. + - `transport_metadata.slice_guard`에는 slice의 `slice_guard_echo` 값 2개(`source_stage_a_created_at_utc`, `input_digests_summary`)만 문자 그대로 복사한다. + - `domain_review_queue` 항목은 `review_id`, `severity`, `issue_type`, `source_event_candidate_ids`, `source_evidence_indexes`, `source_meeting_clause_ids`, `recommended_downstream_owner`만 출력한다. 서술형 `description`은 출력하지 않는다(고정 문구는 R0가 부여한다). + - candidate의 실질 내용(BOType, ActionType, JuristicAct, Action, core_field_base, amount, source refs, domain_payload의 유의미 값, 도메인 named fields)은 생략 대상이 아니다 — 값이 있으면 반드시 출력한다. + + + + Return exactly one JSON object: + { + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v2", + "domain_id": "B5_Succession_Notice_Lien_Asset_Defense", + "status": "READY|READY_WITH_REVIEW|FAILED", + "transport_metadata": { + "stage": "Task_C_BO.Stage_B.DomainSeed", + "source_task": "B5_Succession_Notice_Lien_Asset_Defense", + "domain_label": "상속, 통지, 유치권, 목적물 특정, 항변", + "slice_guard": { + "source_stage_a_created_at_utc": "slice_guard_echo에서 문자 그대로 복사", + "input_digests_summary": "slice_guard_echo에서 문자 그대로 복사" + } + }, + "bo_seed_candidates": [ + { + "candidate_ref": "B5:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "JuristicAct": {"label": "string|null"}, + "Action": "short neutral factual/legal-action draft", + "Legal_Keywords": [], + "core_field_base": { + "date": null, + "amount": null, + "parties": [], + "asset_refs": [], + "legal_effect_summary": null + }, + "amount": { + "value": null, + "currency": "KRW|null", + "basis": null + }, + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "legal_effect_structure_seed": null, + "claim_group_seed": null, + "asset_cluster_seed": null, + "issue_cluster_seed": null + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense", + "title_hint": null, + "summary_hint": null, + "conflict_patterns": [], + "asset_bindings": [], + "parties_hint": [], + "temporal_hint": null, + "hazard_flags": [], + "legal_effect_tags": [], + "event_state_type": null, + "action_summary": null, + "time_interval_hint": null, + "asset_binding_hint": null, + "mixed_contract_split_hint": null, + "conflict_pattern_hint": null, + "candidate_details": {}, + "facts": {}, + "succession_resolution": {}, + "general_succession_status": {}, + "asset_specific_succession_resolution": {}, + "notice_lifecycle": { + "notice_id": null, + "notice_type": null, + "drafted_date": null, + "dispatch_date": null, + "arrival_date": null, + "received_date": null, + "legal_effect_date_candidate": null, + "requires_receipt_for_effect": null, + "sender_candidates": [], + "legal_sender_after_resolution": {"status": "resolved|unresolved|disputed|not_required", "party": null, "succession_resolution_ref": null}, + "receiver_candidates": [], + "receipt_evidence_refs": [], + "recipient_admission_text": null, + "dispatch_hints": [], + "arrival_hints": [] + }, + "lien_state": { + "lien_id": null, + "asset_ref": null, + "lien_holder_candidates": [], + "secured_claim_hint": {"debtor_candidate": null, "basis_text": null, "claimed_remaining_amount_hint": null, "evidence_indexes": []}, + "possession_event_refs": [], + "assertion_events": [], + "extinction_notice_refs": [], + "status_raw_hint": null + }, + "possession_state": { + "asset_ref": null, + "possession_events": [ + {"possessor_candidates": [], "start_date": null, "end_date": null, "source_event_candidate_ids": [], "evidence_indexes": []} + ], + "legal_cause_assertions": [ + {"cause_asserted": "유치권행사중|임대차|사용대차|물권|동시이행항변|명도후잔류|무권원주장|미상", "asserted_by": null, "assertion_date": null, "source_event_candidate_ids": [], "evidence_indexes": []} + ] + }, + "asset_alias": [ + {"asset_ref": null, "alias_strings": [], "canonical_label_candidate": null, "usable_in_relief": "yes|no|unknown", "schedule_refs": [], "registry_refs": [{"registry_ref": null, "receipt_no_refs": []}], "source_evidence_indexes": []} + ], + "defense_rebuttal": [], + "simultaneous_performance_hints": [], + "setoff_or_payment_defense_hints": [] + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B5:review:001", + "severity": "SOFT_WARNING|HARD_WARNING", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "recommended_downstream_owner": "publisher|domain_join|C0|C1|C2|C3|C5|D|E" + } + ] + } + } + + Status rule: use `FAILED` when any input acceptance or `stage_b_worker_contract` guard fails; use `READY_WITH_REVIEW` when `domain_review_queue` is non-empty and no hard gate failed; otherwise use `READY`. + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_R0_seed_reducer_and_exception_planner + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: |- + #!/usr/bin/env python3 + # Task_C_BO_R0_seed_reducer_and_exception_planner (v3) + # publisher + domain_join + PostB_1 통합 결정적 reducer. + # Spec: Part_2_Improvement_Strategy_Claude_v1.md §7 (defer policy = 개선전략서 X-2, pack 조건 = X-3) + from __future__ import annotations + import itertools + import json + import re + import sys + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + WORKER_RAW_FIELDS = { + "B1_Money_Successor": r"""{{prev.Task_C_BO_Stage_B_B1_Money_Successor.json_output}}""", + "B2_Secured_Registry": r"""{{prev.Task_C_BO_Stage_B_B2_Secured_Registry.json_output}}""", + "B3_Land_Valuation": r"""{{prev.Task_C_BO_Stage_B_B3_Land_Valuation.json_output}}""", + "B4_Actio": r"""{{prev.Task_C_BO_Stage_B_B4_Actio.json_output}}""", + "B5_Succession_Notice_Lien_Asset_Defense": r"""{{prev.Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense.json_output}}""", + } + SEED_DOCS = { + "B1_Money_Successor": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B1_Money_Successor.json", + "B2_Secured_Registry": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B2_Secured_Registry.json", + "B3_Land_Valuation": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B3_Land_Valuation.json", + "B4_Actio": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B4_Actio.json", + "B5_Succession_Notice_Lien_Asset_Defense": "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B5_Succession_Notice_Lien_Asset_Defense.json", + } + SEED_TASK_NAMES = { + "B1_Money_Successor": "Task_C_BO_Stage_B_B1_Money_Successor", + "B2_Secured_Registry": "Task_C_BO_Stage_B_B2_Secured_Registry", + "B3_Land_Valuation": "Task_C_BO_Stage_B_B3_Land_Valuation", + "B4_Actio": "Task_C_BO_Stage_B_B4_Actio", + "B5_Succession_Notice_Lien_Asset_Defense": "Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense", + } + DOMAIN_ORDER = list(SEED_DOCS.keys()) + DOMAIN_PREFIX = { + "B1_Money_Successor": "B1", + "B2_Secured_Registry": "B2", + "B3_Land_Valuation": "B3", + "B4_Actio": "B4", + "B5_Succession_Notice_Lien_Asset_Defense": "B5", + } + DOMAIN_LABELS = { + "B1_Money_Successor": "금전채권 및 상호속용 책임", + "B2_Secured_Registry": "담보채무, 변제충당, 등기무효, 경매", + "B3_Land_Valuation": "토지 사용이익, 점유, 사용수익, 가치평가", + "B4_Actio": "사해행위취소, 피보전채권, 책임재산, 가액배상", + "B5_Succession_Notice_Lien_Asset_Defense": "상속, 통지, 유치권, 목적물 특정, 항변", + } + STAGE_A_PATH = "stage1_tmp/task_c_bo/stage_a_context.json" + MANIFEST_PATH = "stage1_tmp/task_c_bo/source_universe_manifest.json" + LEDGER_PATH = "stage1_tmp/task_c_bo/postb_seed_ledger.json" + PACK_PATH = "quality_gates/stage1_part2_exception_pack.json" + REVIEW_HANDOFF_PATH = "quality_gates/stage1_part2_review_handoff.json" + CANDIDATE_REF_RE = re.compile(r"^B[1-5]:[0-9]{3}$") + REVIEW_ISSUE_ENUM = { + "missing_source", "source_conflict", "cross_domain_merge_needed", + "amount_or_date_uncertain", "legal_effect_uncertain", "review_required", + "legal_theory_required", "near_duplicate_kept_separate", + "meeting_only_evidence_gap", "schema_field_fallback", "prior_link_ambiguous", + } + DOWNSTREAM_OWNER_ENUM = {"publisher", "domain_join", "C0", "C1", "C2", "C3", "C5", "D", "E", "Stage2"} + ALLOWED_SEED_KEYS = { + "candidate_ref", "source_domain", "BOType", "ActionType", "JuristicAct", "Action", + "core_field_base", "amount", "source_evidence_indexes", "provenance", + "downstream_seed_refs", "extensions", "Legal_Keywords", "transport_audit", + "Reason", "PriorAct", "ReasonRefs", + } + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-c-bo-r0-seed-reducer-and-exception-planner", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse_mcp(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "id": _mid(), "method": "tools/call", + "params": {"name": name, "arguments": args}}, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse_mcp(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def read_json_doc(path: str) -> Any: + result = _call("read_docs", {"doc_names": [path]}) + content = result["result"].get("content", []) + text = content[0].get("text", "") if content else "" + try: + parsed = json.loads(text) + except json.JSONDecodeError as exc: + raise RuntimeError(f"Invalid read_docs envelope for {path}") from exc + if isinstance(parsed, dict) and "results" in parsed: + rows = parsed.get("results") or [] + if not rows or rows[0].get("error"): + raise RuntimeError(f"read_docs failed for {path}: {rows[:1]}") + inner = rows[0].get("content") or "" + if isinstance(inner, str): + try: + return json.loads(inner) + except json.JSONDecodeError: + obj, end = JSON_DECODER.raw_decode(inner.strip()) + if inner.strip()[end:].strip(): + raise RuntimeError(f"{path}: non-json tail") + return obj + return inner + return parsed + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def clip(value: Any, limit: int = 120) -> str: + text = " ".join(str(value or "").split()) + return text if len(text) <= limit else text[:limit].rstrip() + "..." + + # ---------- 관용 파서 (SKILL.md §6.7 + v2 publisher unwrap 계승; Y-3) ---------- + def parse_llm_json(raw: str, label: str, salvage_notes: list[dict[str, Any]]) -> dict[str, Any]: + text = (raw or "").strip() + if not text or text.startswith("{{"): + raise ValueError(f"{label}: no injected JSON body") + if text.startswith("```"): + salvage_notes.append({"label": label, "salvage": "stripped_code_fence"}) + parts = text.split("```") + for part in parts: + part = part.strip() + if part.startswith("json"): + part = part[4:].strip() + if part.startswith("{"): + text = part + break + start = text.find("{") + if start < 0: + raise ValueError(f"{label}: JSON object start not found") + if start > 0: + salvage_notes.append({"label": label, "salvage": "stripped_leading_prose"}) + obj, end = JSON_DECODER.raw_decode(text[start:].strip()) + tail = text[start:].strip()[end:].strip() + if tail: + salvage_notes.append({"label": label, "salvage": "stripped_trailing_tail", "tail_preview": tail[:80]}) + if not isinstance(obj, dict): + raise ValueError(f"{label}: JSON root must be object") + if obj.get("type") == "text" and isinstance(obj.get("text"), str): + return parse_llm_json(obj["text"], f"{label}.text", salvage_notes) + if obj.get("success") is True and isinstance(obj.get("output"), str): + return parse_llm_json(obj["output"], f"{label}.output", salvage_notes) + return obj + + # ---------- sparse -> canonical schema expander (사실 창작 없이 default만 복원) ---------- + # v2 워커 OUTPUT_CONTRACT의 domain_payload 상위 키·순서·기본형 (B5는 후보 유형별 가변 스키마이므로 제외) + DOMAIN_PAYLOAD_CANON: dict[str, list[tuple[str, Any]]] = { + "B1_Money_Successor": [ + ("domain_legal_effect_tag", None), ("title_hint", None), ("summary_hint", None), + ("conflict_patterns", []), ("asset_bindings", []), ("parties_hint", []), + ("temporal_hint", None), ("hazard_flags", []), ("legal_effect_tags", []), + ("event_state_type", None), ("action_summary", None), ("time_interval_hint", None), + ("asset_binding_hint", None), ("mixed_contract_split_hint", None), ("conflict_pattern_hint", None), + ("candidate_details", {}), ("facts", {}), ("money_claim_details", {}), + ("payment_history", []), ("demand_or_notice_hints", []), ("admission_hints", []), + ("successor_liability_hints", []), ("successor_link_uncertainty", None), ("conflict_flags", []), + ], + "B2_Secured_Registry": [ + ("domain_legal_effect_tag", None), ("title_hint", None), ("summary_hint", None), + ("conflict_patterns", []), ("asset_bindings", []), ("parties_hint", []), + ("temporal_hint", None), ("hazard_flags", []), ("legal_effect_tags", []), + ("event_state_type", None), ("action_summary", None), ("time_interval_hint", None), + ("asset_binding_hint", None), ("mixed_contract_split_hint", None), ("conflict_pattern_hint", None), + ("candidate_details", {}), ("facts", {}), ("registry_rows", []), + ("fractional_effect_events", []), ("secured_debt_chain", []), ("payment_allocation", []), + ("mortgage_extinguishment_hints", []), ("auction_steps", []), ("dividend_distribution_hints", []), + ("derivative_invalidity_hints", []), ("registry_object_display", None), + ], + "B3_Land_Valuation": [ + ("domain_legal_effect_tag", None), ("title_hint", None), ("summary_hint", None), + ("conflict_patterns", []), ("asset_bindings", []), ("parties_hint", []), + ("temporal_hint", None), ("hazard_flags", []), ("legal_effect_tags", []), + ("event_state_type", None), ("action_summary", None), ("time_interval_hint", None), + ("asset_binding_hint", None), ("mixed_contract_split_hint", None), ("conflict_pattern_hint", None), + ("candidate_details", {}), ("facts", {}), ("valuation_rows", []), + ("valuation_matrix", []), ("valuation_object", None), ("use_gain_facts", []), + ("possession_state", {}), ("delivery_or_possession_start_hints", []), ("unregistered_building_hints", []), + ("liability_overlap_hints", []), + ], + "B4_Actio": [ + ("domain_legal_effect_tag", None), ("title_hint", None), ("summary_hint", None), + ("conflict_patterns", []), ("asset_bindings", []), ("parties_hint", []), + ("temporal_hint", None), ("hazard_flags", []), ("legal_effect_tags", []), + ("event_state_type", None), ("action_summary", None), ("time_interval_hint", None), + ("asset_binding_hint", None), ("mixed_contract_split_hint", None), ("conflict_pattern_hint", None), + ("candidate_details", {}), ("facts", {}), ("preserved_claim_bundle", {}), + ("fraudulent_transfer_facts", []), ("debtor_act_hints", []), ("balance_sheet", {}), + ("encumbrance_timeline", []), ("valuation_matrix", []), ("remedy_mode_hints", []), + ("value_compensation_cap_hints", []), ("beneficiary_transferee_hints", []), ("bad_faith_good_faith_hints", []), + ("defense_rebuttal", []), + ], + } + + def _canon_payload(payload: dict[str, Any], domain_id: str) -> dict[str, Any]: + canon = DOMAIN_PAYLOAD_CANON.get(domain_id) + if canon is None: + return payload # B5: 후보 유형별 가변 스키마 — 워커 발행 형태 유지 (v2 동작 동일) + out: dict[str, Any] = {} + for key, default in canon: + if key in payload: + out[key] = payload[key] + else: + out[key] = [] if isinstance(default, list) else {} if isinstance(default, dict) else None + for key, value in payload.items(): + if key not in out: + out[key] = value + return out + + def expand_candidate(cand: dict[str, Any], domain_id: str) -> dict[str, Any]: + out = dict(cand) + out.setdefault("Reason", None) + out.setdefault("PriorAct", None) + out.setdefault("ReasonRefs", []) + out.setdefault("Legal_Keywords", []) + out.setdefault("core_field_base", {}) + if out.get("JuristicAct") is None: + out["JuristicAct"] = {"label": None} + if out.get("amount") is None: + out["amount"] = {} + out.setdefault("source_evidence_indexes", []) + prov = _dict(out.get("provenance")) + prov.setdefault("source_event_candidate_ids", []) + prov.setdefault("source_meeting_clause_ids", []) + prov.setdefault("source_evidence_indexes", list(out.get("source_evidence_indexes") or [])) + out["provenance"] = prov + out.setdefault("downstream_seed_refs", {}) + ext = _dict(out.get("extensions")) + ext["domain_payload"] = _canon_payload(_dict(ext.get("domain_payload")), domain_id) + out["extensions"] = ext + return out + + def expand_review_item(item: dict[str, Any], domain_id: str, idx: int) -> dict[str, Any]: + out = dict(item) + out.setdefault("review_id", f"{DOMAIN_PREFIX[domain_id]}:review:{idx:03d}") + out.setdefault("severity", "SOFT_WARNING") + out.setdefault("issue_type", "review_required") + out.setdefault("source_event_candidate_ids", []) + out.setdefault("source_evidence_indexes", []) + out.setdefault("source_meeting_clause_ids", []) + out.setdefault("recommended_downstream_owner", "Stage2") + return out + + def digest_summary(input_digests: dict[str, Any]) -> str: + import hashlib + return hashlib.sha256(json.dumps(input_digests, ensure_ascii=False, sort_keys=True).encode("utf-8")).hexdigest() + + # ---------- 워커 출력 수용 검증 (publisher 이식, sparse 인지형: expander 후 강검증) ---------- + def validate_seed_object(seed_obj: dict[str, Any], domain_id: str, stage_a: dict[str, Any], warnings: list[dict[str, Any]]) -> None: + if seed_obj.get("schema_version") != "task_c_bo_stage_b_domain_bo_seed.v2": + raise ValueError(f"{domain_id}: seed schema_version mismatch") + if seed_obj.get("domain_id") != domain_id: + raise ValueError(f"{domain_id}: seed domain_id mismatch") + if seed_obj.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError(f"{domain_id}: seed status must be READY or READY_WITH_REVIEW") + guard = _dict(_dict(seed_obj.get("transport_metadata")).get("slice_guard")) + created = guard.get("source_stage_a_created_at_utc") + if created and created != stage_a.get("created_at_utc"): + raise ValueError(f"{domain_id}: stale seed output: Stage A created_at_utc mismatch") + summary = guard.get("input_digests_summary") + full = guard.get("input_digests_sha256") + expected = digest_summary(stage_a.get("input_digests_sha256") or {}) + if isinstance(full, dict) and full and full != stage_a.get("input_digests_sha256"): + raise ValueError(f"{domain_id}: stale seed output: input digest mismatch") + if isinstance(summary, str) and summary and summary != expected: + raise ValueError(f"{domain_id}: stale seed output: input digest summary mismatch") + if not created and not summary and not full: + warnings.append({"domain_id": domain_id, "warning": "seed output carries no freshness guard"}) + + def validate_candidate(cand: dict[str, Any], domain_id: str, idx: int) -> None: + prefix = DOMAIN_PREFIX[domain_id] + ref = cand.get("candidate_ref") + if not isinstance(ref, str) or not CANDIDATE_REF_RE.fullmatch(ref): + raise ValueError(f"{domain_id}.bo_seed_candidates[{idx}].candidate_ref invalid") + if not ref.startswith(prefix + ":"): + raise ValueError(f"{domain_id}.bo_seed_candidates[{idx}].candidate_ref prefix mismatch") + for forbidden in ("BO_ID", "id", "Evidence", "EvidenceTitles"): + if forbidden in cand: + raise ValueError(f"{domain_id}.{ref}: final field {forbidden} is prohibited") + if cand.get("Reason") is not None: + raise ValueError(f"{domain_id}.{ref}: Reason must be null/absent") + if cand.get("PriorAct") is not None: + raise ValueError(f"{domain_id}.{ref}: PriorAct must be null/absent") + if cand.get("ReasonRefs") not in ([], None): + raise ValueError(f"{domain_id}.{ref}: ReasonRefs must be []/absent") + + # ---------- PostB_1 이식: sort key / duplicate keys / schema risk ---------- + def _source_refs(seed: dict[str, Any]) -> dict[str, list[str]]: + provenance = _dict(seed.get("provenance")) + return { + "source_evidence_indexes": _strings(seed.get("source_evidence_indexes") or provenance.get("source_evidence_indexes")), + "source_event_candidate_ids": _strings(provenance.get("source_event_candidate_ids")), + "source_meeting_clause_ids": _strings(provenance.get("source_meeting_clause_ids")), + } + + def _sort_key(seed: dict[str, Any]) -> dict[str, Any]: + core = _dict(seed.get("core_field_base")) + domain = seed.get("source_domain") + juristic = _dict(seed.get("JuristicAct")) + return { + "BehaviorTime": core.get("BehaviorTime"), + "domain_order": DOMAIN_ORDER.index(domain) if domain in DOMAIN_ORDER else 99, + "BOType": seed.get("BOType"), + "ActionType": seed.get("ActionType"), + "JuristicActLabel": juristic.get("label"), + "Action": seed.get("Action"), + "candidate_ref": seed.get("candidate_ref"), + } + + def _duplicate_key(seed: dict[str, Any]) -> tuple[Any, ...]: + core = _dict(seed.get("core_field_base")) + juristic = _dict(seed.get("JuristicAct")) + refs = _source_refs(seed) + return ( + tuple(sorted(refs["source_evidence_indexes"])), + tuple(sorted(refs["source_event_candidate_ids"])), + seed.get("BOType"), + seed.get("ActionType"), + juristic.get("label"), + str(seed.get("Action") or "").strip(), + str(core.get("BehaviorTime") or "").strip(), + str(core.get("Object") or "").strip(), + ) + + def _normalize_juristic(value: Any) -> Any: + if value is None: + return None + if isinstance(value, dict): + label = value.get("label") + return {"label": str(label).strip()} if label not in (None, "") else {"label": None} + text = str(value).strip() + return {"label": text} if text else None + + def _seed_payload(cand: dict[str, Any], domain_id: str) -> dict[str, Any]: + payload: dict[str, Any] = {} + for key in ALLOWED_SEED_KEYS: + if key in cand: + payload[key] = cand[key] + payload["candidate_ref"] = cand.get("candidate_ref") + payload["source_domain"] = domain_id + payload["JuristicAct"] = _normalize_juristic(payload.get("JuristicAct")) + provenance = _dict(payload.get("provenance")) + payload["provenance"] = { + "source_event_candidate_ids": _strings(provenance.get("source_event_candidate_ids")), + "source_meeting_clause_ids": _strings(provenance.get("source_meeting_clause_ids")), + "source_evidence_indexes": _strings(cand.get("source_evidence_indexes") or provenance.get("source_evidence_indexes")), + "source_domain": domain_id, + } + payload["source_evidence_indexes"] = _strings(cand.get("source_evidence_indexes") or payload["provenance"]["source_evidence_indexes"]) + payload["Reason"] = None + payload["PriorAct"] = None + payload["ReasonRefs"] = [] + payload["core_field_base"] = _dict(payload.get("core_field_base")) + payload["downstream_seed_refs"] = _dict(payload.get("downstream_seed_refs")) + payload["extensions"] = _dict(payload.get("extensions")) + if "domain_payload" not in payload["extensions"]: + payload["extensions"]["domain_payload"] = {} + return payload + + def _compact_exception_payload(seeds: list[dict[str, Any]]) -> list[dict[str, Any]]: + compact = [] + for seed in seeds: + compact.append({ + "candidate_ref": seed.get("candidate_ref"), + "source_domain": seed.get("source_domain"), + "BOType": seed.get("BOType"), + "ActionType": seed.get("ActionType"), + "JuristicAct": seed.get("JuristicAct"), + "Action": seed.get("Action"), + "core_field_base": seed.get("core_field_base"), + "amount": seed.get("amount"), + "source_refs": _source_refs(seed), + }) + return compact + + def main() -> None: + _init() + salvage_notes: list[dict[str, Any]] = [] + guard_warnings: list[dict[str, Any]] = [] + stage_a_outer = read_json_doc(STAGE_A_PATH) + stage_a = _dict(_dict(stage_a_outer).get("stage_a_context") or stage_a_outer) + if stage_a.get("schema_version") != "task_c_bo_stage_a_context.v1" or stage_a.get("status") != "READY": + raise RuntimeError("Stage A context must be READY task_c_bo_stage_a_context.v1") + manifest = _dict(read_json_doc(MANIFEST_PATH)) + universe = { + "source_event_candidate_ids": set(_strings(manifest.get("event_candidate_ids"))), + "source_evidence_indexes": set(_strings(manifest.get("evidence_index_set"))), + "source_meeting_clause_ids": set(_strings(manifest.get("meeting_clause_ids"))), + } + if not universe["source_evidence_indexes"]: + raise RuntimeError("source universe manifest has no evidence indexes") + + # 1) 워커 출력 수용: 관용 파싱 -> sparse 검증 -> expander -> seed 파일 발행 + seed_objects: dict[str, dict[str, Any]] = {} + review_handoff_items: list[dict[str, Any]] = [] + for domain_id in DOMAIN_ORDER: + outer = parse_llm_json(WORKER_RAW_FIELDS[domain_id], domain_id, salvage_notes) + seed_obj = _dict(outer.get("stage_b_domain_bo_seed_output")) + if not seed_obj: + raise RuntimeError(f"{domain_id}: stage_b_domain_bo_seed_output missing") + validate_seed_object(seed_obj, domain_id, stage_a, guard_warnings) + cands = _list(seed_obj.get("bo_seed_candidates")) + expanded: list[dict[str, Any]] = [] + for idx, cand in enumerate(cands): + if not isinstance(cand, dict): + raise RuntimeError(f"{domain_id}.bo_seed_candidates[{idx}] must be object") + validate_candidate(cand, domain_id, idx) + full = expand_candidate(cand, domain_id) + # membership 검사 (PostB_1 이식 + 개정 defer 정책 §7): universe 밖 source ref는 hard BLOCK + prov = full["provenance"] + for key, allowed in ( + ("source_event_candidate_ids", universe["source_event_candidate_ids"]), + ("source_meeting_clause_ids", universe["source_meeting_clause_ids"]), + ("source_evidence_indexes", universe["source_evidence_indexes"]), + ): + if not allowed: + continue + bad = [v for v in _strings(prov.get(key)) if v not in allowed] + if bad: + raise RuntimeError(f"BLOCK: {domain_id}.{cand.get('candidate_ref')}: {key} outside Stage A universe: {bad}") + top_bad = [v for v in _strings(full.get("source_evidence_indexes")) if v not in universe["source_evidence_indexes"]] + if top_bad: + raise RuntimeError(f"BLOCK: {domain_id}.{cand.get('candidate_ref')}: source_evidence_indexes outside Stage A universe: {top_bad}") + expanded.append(full) + seed_obj = dict(seed_obj) + seed_obj["bo_seed_candidates"] = expanded + reviews = [expand_review_item(_dict(it), domain_id, idx + 1) for idx, it in enumerate(_list(seed_obj.get("domain_review_queue")))] + seed_obj["domain_review_queue"] = reviews + # transport 재구성 (워커 출력 금지 필드 — R0가 provenance에서 결정적으로 재구성) + seed_obj["transport_candidate_refs"] = [ + { + "candidate_ref": c.get("candidate_ref"), + "payload_index": i, + "source_event_candidate_ids": _strings(_dict(c.get("provenance")).get("source_event_candidate_ids")), + "source_evidence_indexes": _strings(c.get("source_evidence_indexes")), + "source_meeting_clause_ids": _strings(_dict(c.get("provenance")).get("source_meeting_clause_ids")), + } + for i, c in enumerate(expanded) + ] + metadata = _dict(seed_obj.get("transport_metadata")) + metadata.setdefault("stage", "Task_C_BO.Stage_B.DomainSeed") + metadata.setdefault("source_task", domain_id) + metadata.setdefault("domain_label", DOMAIN_LABELS[domain_id]) + metadata["slice_guard"] = { + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": stage_a.get("created_at_utc"), + "input_digests_sha256": stage_a.get("input_digests_sha256"), + "source_universe": { + "meeting_clause_ids": sorted(universe["source_meeting_clause_ids"]), + "event_candidate_ids": sorted(universe["source_event_candidate_ids"]), + "evidence_indexes": sorted(universe["source_evidence_indexes"]), + }, + "stage_b_worker_contract": { + "context_is_read_only": True, + "bo_id_finalization_allowed": False, + "candidate_generation_allowed": True, + }, + } + seed_obj["transport_metadata"] = metadata + seed_obj["handoff_guard"] = { + "schema_version": "task_c_bo_stage_b_seed_handoff_guard.v1", + "handoff_target": SEED_DOCS[domain_id], + "publisher_task": "Task_C_BO_R0_seed_reducer_and_exception_planner", + "source_worker_task": SEED_TASK_NAMES[domain_id], + "source_task": domain_id, + "source_contract": "direct_prev_task_json_output.stage_b_domain_bo_seed_output", + "source_prev_path": f"prev.{SEED_TASK_NAMES[domain_id]}.json_output", + "domain_id": domain_id, + "source_stage_a_schema_version": "task_c_bo_stage_a_context.v1", + "source_stage_a_status": "READY", + "source_stage_a_created_at_utc": stage_a.get("created_at_utc"), + "input_digests_sha256": stage_a.get("input_digests_sha256"), + } + write_doc(SEED_DOCS[domain_id], json.dumps({"stage_b_domain_bo_seed_output": seed_obj}, ensure_ascii=False, indent=2)) + seed_objects[domain_id] = seed_obj + for idx, item in enumerate(reviews, start=1): + issue = item.get("issue_type") if item.get("issue_type") in REVIEW_ISSUE_ENUM else "review_required" + owner = item.get("recommended_downstream_owner") + review_handoff_items.append({ + "review_id": item.get("review_id"), + "source_domain": domain_id, + "severity": item.get("severity") if item.get("severity") in ("SOFT_WARNING", "HARD_WARNING") else "SOFT_WARNING", + "issue_type": issue, + "source_event_candidate_ids": _strings(item.get("source_event_candidate_ids")), + "source_evidence_indexes": _strings(item.get("source_evidence_indexes")), + "source_meeting_clause_ids": _strings(item.get("source_meeting_clause_ids")), + "downstream_owner": owner if owner in DOWNSTREAM_OWNER_ENUM else "Stage2", + "template_note": "후속 단계에서 해당 review 항목의 증거와 법률상 의미를 재검토한다.", + }) + + # 2) ledger 구성 (PostB_1 이식) + input_candidate_total = 0 + seeds: list[dict[str, Any]] = [] + for domain_id in DOMAIN_ORDER: + cands = seed_objects[domain_id]["bo_seed_candidates"] + input_candidate_total += len(cands) + for cand in cands: + seeds.append(_seed_payload(cand, domain_id)) + if not seeds: + raise RuntimeError("no seed candidate from Stage B workers") + + seen_refs: set[str] = set() + ledger_candidates: list[dict[str, Any]] = [] + deterministic_decisions: list[dict[str, Any]] = [] + exceptions: list[dict[str, Any]] = [] + duplicate_buckets: dict[tuple[Any, ...], list[dict[str, Any]]] = {} + policy_review_counter = 0 + + def add_policy_review(domain_id: str, issue_type: str, refs: dict[str, list[str]], severity: str = "SOFT_WARNING") -> None: + nonlocal policy_review_counter + policy_review_counter += 1 + review_handoff_items.append({ + "review_id": f"R0:policy:{policy_review_counter:03d}", + "source_domain": domain_id, + "severity": severity, + "issue_type": issue_type, + "source_event_candidate_ids": refs.get("source_event_candidate_ids", []), + "source_evidence_indexes": refs.get("source_evidence_indexes", []), + "source_meeting_clause_ids": refs.get("source_meeting_clause_ids", []), + "downstream_owner": "Stage2", + "template_note": "결정적 defer 정책에 의해 보존된 검토 항목이다.", + }) + + for seed in seeds: + ref = seed.get("candidate_ref") + if not isinstance(ref, str) or not CANDIDATE_REF_RE.fullmatch(ref): + raise RuntimeError(f"invalid candidate_ref: {ref!r}") + if ref in seen_refs: + raise RuntimeError(f"duplicate candidate_ref: {ref}") + seen_refs.add(ref) + refs = _source_refs(seed) + # hard BLOCK: universe 밖 ref는 soft-strip 이후에도 남아있으면 안 된다 (방어적 재검사) + for key, values in refs.items(): + allowed = universe.get(key, set()) + outside = [v for v in values if allowed and v not in allowed] + if outside: + raise RuntimeError(f"{ref}: {key} outside Stage A universe: {outside}") + flags: list[str] = [] + # 결정적 defer 정책 (개선전략서 X-2): + if not refs["source_evidence_indexes"]: + flags.append("meeting_only_evidence_gap") + add_policy_review(seed.get("source_domain"), "meeting_only_evidence_gap", refs) + if seed.get("BOType") not in {"event", "state"} or not seed.get("ActionType") or not ( + seed.get("Action") or _dict(_dict(seed.get("extensions")).get("domain_payload")).get("action_summary") + ): + flags.append("schema_field_fallback") + add_policy_review(seed.get("source_domain"), "schema_field_fallback", refs) + link_candidates = _strings(_dict(seed.get("downstream_seed_refs")).get("prior_candidate_refs")) + if len(link_candidates) > 1: + flags.append("prior_link_ambiguous") + add_policy_review(seed.get("source_domain"), "prior_link_ambiguous", refs) + duplicate_buckets.setdefault(_duplicate_key(seed), []).append(seed) + ledger_candidates.append({ + "candidate_ref": ref, + "source_domain": seed.get("source_domain"), + "seed_payload": seed, + "source_refs": refs, + "deterministic_sort_key": _sort_key(seed), + "flags": flags, + }) + + # exact duplicate: provenance union 무손실이므로 canonical merge (v2 규칙 계승) + for bucket in duplicate_buckets.values(): + if len(bucket) <= 1: + continue + canonical = bucket[0].get("candidate_ref") + duplicates = [item.get("candidate_ref") for item in bucket[1:]] + deterministic_decisions.append({ + "decision_type": "EXACT_DUPLICATE_MERGE", + "canonical_candidate_ref": canonical, + "duplicate_candidate_refs": duplicates, + "basis": "exact duplicate deterministic rule (provenance-lossless union)", + }) + + # near duplicate: KEEP_SEPARATE + cluster id + review (LLM 금지 — defer 정책) + near_buckets: dict[tuple[Any, ...], list[dict[str, Any]]] = {} + for seed in seeds: + refs = _source_refs(seed) + key = ( + tuple(sorted(refs["source_evidence_indexes"])), + tuple(sorted(refs["source_event_candidate_ids"])), + seed.get("BOType"), + seed.get("ActionType"), + ) + near_buckets.setdefault(key, []).append(seed) + near_cluster_count = 0 + pack_field_conflicts: list[dict[str, Any]] = [] + for bucket in near_buckets.values(): + if len(bucket) <= 1 or len({_duplicate_key(s) for s in bucket}) <= 1: + continue + near_cluster_count += 1 + cluster_id = f"near-dup-{near_cluster_count:03d}" + cluster_refs = [str(s.get("candidate_ref")) for s in bucket] + for item in ledger_candidates: + if item["candidate_ref"] in cluster_refs: + item.setdefault("near_dup_cluster_id", cluster_id) + if "near_duplicate_kept_separate" not in item["flags"]: + item["flags"].append("near_duplicate_kept_separate") + add_policy_review(bucket[0].get("source_domain"), "near_duplicate_kept_separate", + {"source_evidence_indexes": _source_refs(bucket[0])["source_evidence_indexes"], + "source_event_candidate_ids": _source_refs(bucket[0])["source_event_candidate_ids"], + "source_meeting_clause_ids": []}) + # non-deferrable 판정(X-3 4중 조건): 같은 near cluster에서 BehaviorTime 또는 amount가 + # 서로 다른 non-null 값으로 충돌하면 writer가 단일 값을 고를 수 없으므로 pack에 수록 + times = {str(_dict(s.get("core_field_base")).get("BehaviorTime")) for s in bucket if _dict(s.get("core_field_base")).get("BehaviorTime")} + amounts = set() + for s in bucket: + av = s.get("amount") + if isinstance(av, dict) and av.get("value_text"): + amounts.add(str(av.get("value_text"))) + elif isinstance(av, str) and av.strip(): + amounts.add(av.strip()) + if len(times) > 1 or len(amounts) > 1: + pack_field_conflicts.append({ + "exception_id": f"EX-FIELD-{len(pack_field_conflicts) + 1:03d}", + "exception_type": "field_conflict", + "candidate_refs": cluster_refs, + "reason": "same-source candidates carry conflicting BehaviorTime/amount values", + "conflicting_values": {"BehaviorTime": sorted(times), "amount": sorted(amounts)}, + "compact_candidate_payload": _compact_exception_payload(bucket), + "allowed_decisions": ["KEEP_SEPARATE", "MERGE", "SPLIT", "DROP", "BLOCK_REVIEW"], + "escalation_flag": True, + }) + + exceptions.extend(pack_field_conflicts) + has_exceptions = bool(exceptions) + + # 3) conservation invariant (write 전) + merged_absorbed = sum(len(_strings(d.get("duplicate_candidate_refs"))) for d in deterministic_decisions) + if len(ledger_candidates) != input_candidate_total: + raise RuntimeError(f"ledger candidate count {len(ledger_candidates)} != input candidates {input_candidate_total}") + if len(seen_refs) != input_candidate_total: + raise RuntimeError("candidate_ref conservation failed") + + ledger = { + "postb_seed_ledger": { + "schema_version": "task_c_bo_postb_seed_ledger.v1", + "status": "READY", + "source_stage_a_created_at_utc": stage_a.get("created_at_utc"), + "input_digests_sha256": stage_a.get("input_digests_sha256"), + "source_universe": { + "source_event_candidate_ids": sorted(universe["source_event_candidate_ids"]), + "source_evidence_indexes": sorted(universe["source_evidence_indexes"]), + "source_meeting_clause_ids": sorted(universe["source_meeting_clause_ids"]), + }, + "stage_b_source_contract": { + "schema_version": "task_c_bo_stage_b_bo_seed_universe.compat_from_r0.v1", + "status": "READY", + "compatibility_source": "r0_seed_reducer.direct_worker_outputs", + }, + "ledger_candidates": sorted(ledger_candidates, key=lambda item: ( + item["deterministic_sort_key"].get("BehaviorTime") is None, + item["deterministic_sort_key"].get("BehaviorTime") or "", + item["deterministic_sort_key"].get("domain_order", 99), + item["deterministic_sort_key"].get("BOType") or "", + item["deterministic_sort_key"].get("ActionType") or "", + item["deterministic_sort_key"].get("JuristicActLabel") or "", + item["deterministic_sort_key"].get("Action") or "", + item["deterministic_sort_key"].get("candidate_ref") or "", + )), + "deterministic_decisions": deterministic_decisions, + "exception_pack": { + "has_exceptions": has_exceptions, + "clusters": [], + "field_conflicts": pack_field_conflicts, + "link_ambiguities": [], + "schema_risks": [], + }, + "audit_trace": { + "removed_or_sidecar_fields": [], + "source_membership_policy": "outside-universe source ref => hard BLOCK (defer 정책 §7)", + "normalization_notes": salvage_notes + guard_warnings, + }, + } + } + write_doc(LEDGER_PATH, json.dumps(ledger, ensure_ascii=False, indent=2)) + + pack = { + "schema_version": "stage1_part2_exception_pack.v1", + "has_exceptions": has_exceptions, + "exception_count": len(exceptions), + "exceptions": exceptions, + "budget": {"max_candidates_per_exception": 8, "max_payload_chars_per_candidate": 2000}, + } + write_doc(PACK_PATH, json.dumps(pack, ensure_ascii=False, indent=2)) + + handoff = { + "schema_version": "stage1_part2_review_handoff.v1", + "status": "PENDING_FINALIZE", + "review_items": review_handoff_items, + } + write_doc(REVIEW_HANDOFF_PATH, json.dumps(handoff, ensure_ascii=False, indent=2)) + + print(json.dumps({ + "status": "READY", + "message": "R0 seed reducer 완료: ledger/exception pack/review handoff 생성", + "ledger_path": LEDGER_PATH, + "exception_pack_path": PACK_PATH, + "review_handoff_path": REVIEW_HANDOFF_PATH, + "has_exceptions": has_exceptions, + "exception_count": len(exceptions), + "candidate_counts": { + "input": input_candidate_total, + "ledger": len(ledger_candidates), + "exact_duplicate_absorbed": merged_absorbed, + "near_dup_clusters": near_cluster_count, + }, + "review_item_count": len(review_handoff_items), + "salvage_count": len(salvage_notes), + }, ensure_ascii=False)) + + if __name__ == "__main__": + try: + main() + except Exception as exc: + print(json.dumps({"status": "FAILED", "message": "R0 seed reducer 실패: downstream 진행 금지", + "reason": str(exc)}, ensure_ascii=False)) + raise + - task_name: Task_C_BO_R1_exception_adjudicator + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: low + llm_verbosity: low + max_iterations: 1 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 20m + preflight: true + preflight_files: + - quality_gates/stage1_part2_exception_pack.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + + TASK_NAME: Task_C_BO_R1_exception_adjudicator + STAGE: PostB conditional exception adjudicator (Part 1 v3 GB 패턴) + MISSION: 결정적 reducer(R0)가 non-deferrable로 판정한 compact exception만 판정한다. 병합·최종 파일 작성·사실 창작은 하지 않는다. + + + + - 유일한 입력은 preflight로 제공된 `quality_gates/stage1_part2_exception_pack.json`이다. + - Stage A context, seed ledger 전문, raw evidence, meeting 원문을 읽거나 요청하지 않는다. + - pack에 없는 exception_id·candidate_ref·bh# id를 창작하지 않는다. + - BO.json, ledger, review handoff, signal 파일을 작성하지 않는다. + - 출력 파일은 `stage1_tmp/task_c_bo/postb_adjudication_decisions.json` 하나뿐이다. + + + + 1. preflight로 제공된 exception pack의 `has_exceptions`를 확인한다. + 2. `has_exceptions == false`이면: `write_file(overwrite=true)`로 아래 no-exception 객체를 `stage1_tmp/task_c_bo/postb_adjudication_decisions.json`에 저장하고 `"NO_EXCEPTIONS"`만 출력한 뒤 즉시 종료한다(terminate). 다른 어떤 파일도 읽지 않는다. + {"postb_exception_adjudication": {"schema_version": "task_c_bo_postb_exception_adjudication.v1", "status": "READY_NO_EXCEPTIONS", "exception_count": 0, "canonical_decisions": [], "field_decisions": [], "link_decisions": [], "semantic_gate_decisions": [], "blocked_review_items": []}} + 3. `has_exceptions == true`이면: 각 exception을 `compact_candidate_payload`만으로 판정한다. 추가 read는 금지된다. + 4. 판정 규칙: + - canonical decision: KEEP_SEPARATE, MERGE, SPLIT, DROP, BLOCK_REVIEW 중 하나. MERGE는 `input_candidate_refs`와 `merge_target_ref`를 명시한다. + - field conflict: 제공된 conflicting_values 중 하나를 `selected_value`로 선택하거나 BLOCK_REVIEW. 임의 값 창작 금지. + - link ambiguity: exception에 나열된 candidate ref 중 선택, NO_LINK, 또는 BLOCK_REVIEW. + - semantic risk: PASS, WARNING, BLOCK_REVIEW. + - compact payload로 확정할 수 없으면 반드시 `blocked_review_items`에 넣는다(확신 없는 확정 금지 — 인간 검토 라우팅). + 5. `write_file(overwrite=true)`로 결과를 저장한다. root는 `postb_exception_adjudication`이며 schema_version은 `task_c_bo_postb_exception_adjudication.v1`, status는 `READY`, `exception_count`는 판정한 exception 수다. 모든 decision은 pack의 `exception_id`를 인용한다. + 6. `"R1 예외 판정 완료 (decisions=<건수>)"`만 출력하고 작업을 끝낸다(terminate). + + + + - Stage 1은 법률효과·청구원인을 확정하지 않는다. 두 값을 모두 보존하거나 Stage 2로 defer할 수 있는 사안은 이미 R0가 결정적으로 처리했으므로, 여기 도달한 항목은 final writer가 단일 값을 선택해야만 진행되는 사안이다. + - 같은 source에 근거한 상충 값(BehaviorTime·amount)은: 원문 근거가 더 구체적인 쪽(payload의 core_field_base·amount 기재가 더 완전한 후보)을 선택하고, 우열을 가릴 수 없으면 BLOCK_REVIEW. + - KEEP_SEPARATE가 provenance를 보존하는 기본값이다. MERGE는 provenance 합집합이 무손실일 때만 선택한다. + - DROP은 어떤 경우에도 source 유일 후보에 적용하지 않는다. + + + + - exception pack 부재·파싱 불가: 즉시 중단하고 채팅으로만 보고한다. decisions 파일은 쓰지 않는다. + - tool 오류: 1회만 재시도. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + - task_name: Task_C_BO_F0_final_bo_compiler_gate_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: |- + #!/usr/bin/env python3 + # Task_C_BO_F0_final_bo_compiler_gate_writer (v3) + # PostB_3(final compiler) + PostB_4(final gate/writer) 통합. 입력은 파일 계약(ledger/decisions/stage_a). + # Spec: Part_2_Improvement_Strategy_Claude_v1.md §9 (bh# 규칙 N-6, Reason/PriorAct 정책 R-5) + from __future__ import annotations + import itertools + import json + import re + import sys + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + STAGE_A_PATH = "stage1_tmp/task_c_bo/stage_a_context.json" + LEDGER_PATH = "stage1_tmp/task_c_bo/postb_seed_ledger.json" + DECISIONS_PATH = "stage1_tmp/task_c_bo/postb_adjudication_decisions.json" + REVIEW_HANDOFF_PATH = "quality_gates/stage1_part2_review_handoff.json" + BUNDLE_COMPACT_PATH = "stage1_tmp/task_c_bo/postb_compiled_bundle_compact.json" + TARGET_NAME = "BO.json" + BH_ID_RE = re.compile(r"^bh[1-9][0-9]*$") + ACTION_TYPE_ENUM = { + "법률행위(legal acts)", + "준법률행위(quasi-legal acts)", + "사실행위(factual acts)", + "위법행위(unlawful acts)", + "소송행위(litigation acts)", + } + ALLOWED_TOP_LEVEL = { + "BO_ID", "id", "BOType", "ActionType", "JuristicAct", "Action", "Reason", + "PriorAct", "ReasonRefs", "Legal_Keywords", "core_field_base", "amount", + "EvidenceTitles", "Evidence", "source_evidence_indexes", "provenance", + "downstream_seed_refs", "extensions", + } + REQUIRED_TOP_LEVEL = { + "BO_ID", "id", "BOType", "ActionType", "JuristicAct", "Action", "Reason", + "PriorAct", "ReasonRefs", "Legal_Keywords", "core_field_base", + "EvidenceTitles", "Evidence", "source_evidence_indexes", "provenance", + "downstream_seed_refs", + } + CORE_KEYS = [ + "Performer", "PerformerType", "Action_proposal", "Subject", "Object", + "BehaviorTime", "TimeText", "TimePrecision", "StatementType", "Perspective", + ] + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-c-bo-f0-final-bo-compiler-gate-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse_mcp(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "id": _mid(), "method": "tools/call", + "params": {"name": name, "arguments": args}}, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse_mcp(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def read_json_doc(path: str) -> Any: + result = _call("read_docs", {"doc_names": [path]}) + content = result["result"].get("content", []) + text = content[0].get("text", "") if content else "" + try: + parsed = json.loads(text) + except json.JSONDecodeError as exc: + raise RuntimeError(f"Invalid read_docs envelope for {path}") from exc + if isinstance(parsed, dict) and "results" in parsed: + rows = parsed.get("results") or [] + if not rows or rows[0].get("error"): + raise RuntimeError(f"read_docs failed for {path}: {rows[:1]}") + inner = rows[0].get("content") or "" + if isinstance(inner, str): + try: + return json.loads(inner) + except json.JSONDecodeError: + obj, end = JSON_DECODER.raw_decode(inner.strip()) + if inner.strip()[end:].strip(): + raise RuntimeError(f"{path}: non-json tail") + return obj + return inner + return parsed + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + # ---------- PostB_3 이식 ---------- + def _field_decision_map(adj: dict[str, Any]) -> dict[tuple[str, str], Any]: + out: dict[tuple[str, str], Any] = {} + for item in _list(adj.get("field_decisions")): + if isinstance(item, dict) and item.get("candidate_ref") and item.get("field") and item.get("selected_value") != "BLOCK_REVIEW": + out[(str(item["candidate_ref"]), str(item["field"]))] = item.get("selected_value") + return out + + def _link_decision_map(adj: dict[str, Any]) -> dict[str, dict[str, Any]]: + out: dict[str, dict[str, Any]] = {} + for item in _list(adj.get("link_decisions")): + if isinstance(item, dict) and item.get("candidate_ref"): + out[str(item["candidate_ref"])] = item + return out + + def _decision_sets(ledger: dict[str, Any], adj: dict[str, Any], blockers: list[Any]) -> tuple[set[str], dict[str, str]]: + dropped: set[str] = set() + merge_into: dict[str, str] = {} + for decision in _list(ledger.get("deterministic_decisions")): + if not isinstance(decision, dict) or decision.get("decision_type") != "EXACT_DUPLICATE_MERGE": + continue + canonical = decision.get("canonical_candidate_ref") + for dup in _strings(decision.get("duplicate_candidate_refs")): + if canonical: + merge_into[dup] = str(canonical) + dropped.add(dup) + for decision in _list(adj.get("canonical_decisions")): + if not isinstance(decision, dict): + continue + kind = decision.get("decision") + refs = _strings(decision.get("input_candidate_refs")) + if kind == "DROP": + dropped.update(_strings(decision.get("drop_candidate_refs")) or refs) + elif kind == "MERGE": + target = decision.get("merge_target_ref") or (refs[0] if refs else None) + if target: + for ref in refs: + if ref != target: + merge_into[ref] = str(target) + dropped.add(ref) + elif kind == "BLOCK_REVIEW": + blockers.append(decision) + return dropped, merge_into + + def _sort_tuple(item: dict[str, Any]) -> tuple[Any, ...]: + key = _dict(item.get("deterministic_sort_key")) + return ( + key.get("BehaviorTime") is None, + key.get("BehaviorTime") or "", + key.get("domain_order", 99), + key.get("BOType") or "", + key.get("ActionType") or "", + key.get("JuristicActLabel") or "", + key.get("Action") or "", + key.get("candidate_ref") or "", + ) + + def _juristic(value: Any) -> dict[str, Any] | None: + if value is None: + return None + if isinstance(value, dict): + label = value.get("label") + return {"label": str(label).strip()} if label not in (None, "") else {"label": None} + text = str(value).strip() + return {"label": text} if text else None + + def _core(seed: dict[str, Any]) -> dict[str, Any]: + src = _dict(seed.get("core_field_base")) + out = {key: src.get(key) for key in CORE_KEYS} + if out.get("Action_proposal") is None and seed.get("Action"): + out["Action_proposal"] = seed.get("Action") + if out.get("StatementType") is None: + out["StatementType"] = seed.get("BOType") + if out.get("Perspective") is None: + out["Perspective"] = "plaintiff" + return out + + def _amount(value: Any) -> dict[str, Any] | None: + if value is None: + return None + if isinstance(value, dict): + return { + "value_text": value.get("value_text") or value.get("text"), + "numeric_value": value.get("numeric_value"), + "currency": value.get("currency"), + } + text = str(value).strip() + return {"value_text": text, "numeric_value": None, "currency": None} if text else None + + def _evidence_item(index: str, source: Any, gaps: list[Any], bo_id: str) -> dict[str, Any]: + obj = _dict(source) + title = obj.get("source_title") or obj.get("title") or obj.get("evidence_title") or obj.get("document_title") or index + relevant = obj.get("relevant_content") or obj.get("excerpt") or obj.get("summary") or obj.get("content") + if relevant in (None, ""): + gaps.append({"BO_ID": bo_id, "evidence_index": index, "gap": "missing_relevant_content"}) + relevant = None + return { + "evidence_index": index, + "source_title": str(title), + "priority_class": obj.get("priority_class") or obj.get("priority") or None, + "relevant_content": relevant, + "authentication_status": obj.get("authentication_status") or obj.get("auth_status") or None, + "corroboration": obj.get("corroboration") or None, + "selection_basis": "source_evidence_indexes membership", + } + + def _downstream_refs(seed: dict[str, Any]) -> dict[str, Any]: + src = _dict(seed.get("downstream_seed_refs")) + return { + "claim_group_seed_refs_proposed": _strings(src.get("claim_group_seed_refs_proposed") or src.get("claim_group_seed_refs")), + "canonical_theory_graph_seed_ref_proposed": src.get("canonical_theory_graph_seed_ref_proposed") or src.get("canonical_theory_graph_seed_ref"), + "legal_effect_structure_seed_ref_proposed": src.get("legal_effect_structure_seed_ref_proposed") or src.get("legal_effect_structure_seed_ref"), + } + + def _keywords(seed: dict[str, Any], juristic: dict[str, Any] | None) -> list[str]: + out = _strings(seed.get("Legal_Keywords")) + domain_payload = _dict(_dict(seed.get("extensions")).get("domain_payload")) + out.extend(_strings(domain_payload.get("legal_effect_tags"))) + if isinstance(juristic, dict) and juristic.get("label"): + out.append(str(juristic["label"])) + deduped: list[str] = [] + for item in out: + if item not in deduped: + deduped.append(item) + return deduped + + def _evidence_map_from_stage_a(stage_a: dict[str, Any]) -> dict[str, Any]: + evidence_map = _dict(stage_a.get("evidence_authority_map")) + by_index = _dict(evidence_map.get("by_evidence_index")) + if by_index: + return by_index + out: dict[str, Any] = {} + for item in _list(evidence_map.get("items")): + if isinstance(item, dict): + idx = item.get("evidence_index") or item.get("index") or item.get("id") + if idx is not None: + out[str(idx)] = item + return out + + def _stage_a_universe(stage_a: dict[str, Any]) -> dict[str, set[str]]: + event_map = _dict(stage_a.get("event_candidate_map")) + evidence_map = _dict(stage_a.get("evidence_authority_map")) + meeting_map = _dict(stage_a.get("meeting_clause_map")) + event_ids = set(_strings(event_map.get("candidate_id_set"))) + evidence_ids = set(_strings(evidence_map.get("evidence_index_set"))) + meeting_ids = set(_strings(meeting_map.get("clause_order"))) + event_ids.update(str(k) for k in _dict(event_map.get("by_event_candidate_id")).keys()) + evidence_ids.update(str(k) for k in _dict(evidence_map.get("by_evidence_index")).keys()) + meeting_ids.update(str(k) for k in _dict(meeting_map.get("by_clause_id")).keys()) + return { + "source_event_candidate_ids": event_ids, + "source_evidence_indexes": evidence_ids, + "source_meeting_clause_ids": meeting_ids, + } + + # ---------- PostB_4 이식: 게이트 ---------- + def _add_gate(gates: list[dict[str, Any]], key: str, passed: bool, detail: str) -> None: + gates.append({"gate_key": key, "status": "PASS" if passed else "FAILED", "detail": detail}) + + def _validate_item(item: Any, idx: int, ids: set[str], universe: dict[str, set[str]]) -> list[str]: + errors: list[str] = [] + if not isinstance(item, dict): + return [f"item {idx} must be object"] + extra = sorted(set(item.keys()) - ALLOWED_TOP_LEVEL) + missing = sorted(REQUIRED_TOP_LEVEL - set(item.keys())) + if extra: + errors.append(f"{item.get('BO_ID', idx)} additional fields: {extra}") + if missing: + errors.append(f"{item.get('BO_ID', idx)} missing fields: {missing}") + bo_id = item.get("BO_ID") + expected = f"bh{idx}" + if bo_id != expected or item.get("id") != bo_id or not isinstance(bo_id, str) or not BH_ID_RE.fullmatch(bo_id): + errors.append(f"BO_ID/id sequence mismatch: expected {expected}") + if item.get("BOType") not in {"event", "state"}: + errors.append(f"{bo_id}.BOType invalid") + if item.get("ActionType") not in ACTION_TYPE_ENUM: + errors.append(f"{bo_id}.ActionType invalid") + juristic = item.get("JuristicAct") + if juristic is not None and (not isinstance(juristic, dict) or set(juristic.keys()) != {"label"}): + errors.append(f"{bo_id}.JuristicAct invalid") + for key in ("Action", "Reason"): + if not isinstance(item.get(key), str) or not item.get(key).strip(): + errors.append(f"{bo_id}.{key} must be non-empty string") + prior = item.get("PriorAct") + if prior is not None and prior not in ids: + errors.append(f"{bo_id}.PriorAct references missing BO_ID") + for ref in _list(item.get("ReasonRefs")): + if ref not in ids: + errors.append(f"{bo_id}.ReasonRefs references missing BO_ID {ref}") + core = item.get("core_field_base") + if not isinstance(core, dict) or set(core.keys()) != set(CORE_KEYS): + errors.append(f"{bo_id}.core_field_base keys invalid") + amount = item.get("amount") + if amount is not None and (not isinstance(amount, dict) or set(amount.keys()) - {"value_text", "numeric_value", "currency"}): + errors.append(f"{bo_id}.amount invalid") + evidence = _list(item.get("Evidence")) + evidence_indexes = _strings(item.get("source_evidence_indexes")) + evidence_index_set: set[str] = set() + titles: list[str] = [] + for ev in evidence: + if not isinstance(ev, dict): + errors.append(f"{bo_id}.Evidence item must be object") + continue + required_ev = {"evidence_index", "source_title", "priority_class", "relevant_content", "authentication_status", "corroboration", "selection_basis"} + if set(ev.keys()) != required_ev: + errors.append(f"{bo_id}.Evidence item keys invalid") + if isinstance(ev.get("evidence_index"), str): + evidence_index_set.add(ev["evidence_index"]) + if isinstance(ev.get("source_title"), str) and ev.get("source_title") not in titles: + titles.append(ev["source_title"]) + if item.get("EvidenceTitles") != titles: + errors.append(f"{bo_id}.EvidenceTitles mismatch") + if set(evidence_indexes) != evidence_index_set: + errors.append(f"{bo_id}.source_evidence_indexes must equal Evidence[].evidence_index") + if universe["source_evidence_indexes"] and not set(evidence_indexes).issubset(universe["source_evidence_indexes"]): + errors.append(f"{bo_id}.source_evidence_indexes outside Stage A universe") + provenance = item.get("provenance") + if not isinstance(provenance, dict) or set(provenance.keys()) != {"source_event_candidate_ids", "source_meeting_clause_ids", "source_domain"}: + errors.append(f"{bo_id}.provenance invalid") + else: + if universe["source_event_candidate_ids"] and not set(_strings(provenance.get("source_event_candidate_ids"))).issubset(universe["source_event_candidate_ids"]): + errors.append(f"{bo_id}.provenance.source_event_candidate_ids outside Stage A universe") + if universe["source_meeting_clause_ids"] and not set(_strings(provenance.get("source_meeting_clause_ids"))).issubset(universe["source_meeting_clause_ids"]): + errors.append(f"{bo_id}.provenance.source_meeting_clause_ids outside Stage A universe") + downstream = item.get("downstream_seed_refs") + if not isinstance(downstream, dict) or set(downstream.keys()) != { + "claim_group_seed_refs_proposed", "canonical_theory_graph_seed_ref_proposed", "legal_effect_structure_seed_ref_proposed", + }: + errors.append(f"{bo_id}.downstream_seed_refs invalid") + extensions = item.get("extensions", {"domain_payload": {}}) + if extensions is not None and (not isinstance(extensions, dict) or set(extensions.keys()) - {"domain_payload"} or not isinstance(extensions.get("domain_payload", {}), dict)): + errors.append(f"{bo_id}.extensions invalid") + return errors + + def _fail(message: str, gates: list[dict[str, Any]], reasons: list[str]) -> None: + print(json.dumps({ + "status": "FAILED", + "message": message, + "write_target": TARGET_NAME, + "gate_results": gates, + "failure_reasons": reasons[:40], + }, ensure_ascii=False)) + sys.exit(1) + + def main() -> None: + _init() + gates: list[dict[str, Any]] = [] + stage_a = _dict(_dict(read_json_doc(STAGE_A_PATH)).get("stage_a_context") or read_json_doc(STAGE_A_PATH)) + if stage_a.get("schema_version") != "task_c_bo_stage_a_context.v1" or stage_a.get("status") != "READY": + _fail("Stage A freshness guard failed", gates, ["stage_a not READY"]) + universe = _stage_a_universe(stage_a) + evidence_map = _evidence_map_from_stage_a(stage_a) + ledger = _dict(_dict(read_json_doc(LEDGER_PATH)).get("postb_seed_ledger")) + if ledger.get("schema_version") != "task_c_bo_postb_seed_ledger.v1" or ledger.get("status") != "READY": + _fail("R0 seed ledger not READY", gates, [str(ledger.get("status"))]) + try: + adj_doc = read_json_doc(DECISIONS_PATH) + except Exception: + adj_doc = {"postb_exception_adjudication": {"schema_version": "task_c_bo_postb_exception_adjudication.v1", + "status": "READY_NO_EXCEPTIONS", "exception_count": 0, + "canonical_decisions": [], "field_decisions": [], + "link_decisions": [], "semantic_gate_decisions": [], + "blocked_review_items": []}} + adj = _dict(_dict(adj_doc).get("postb_exception_adjudication")) + if adj.get("schema_version") != "task_c_bo_postb_exception_adjudication.v1": + _fail("R1 adjudication schema mismatch", gates, [str(adj.get("schema_version"))]) + if adj.get("status") not in {"READY", "READY_NO_EXCEPTIONS"}: + _fail("R1 adjudication status invalid", gates, [str(adj.get("status"))]) + blocked = _list(adj.get("blocked_review_items")) + block_decisions: list[Any] = [] + field_decisions = _field_decision_map(adj) + link_decisions = _link_decision_map(adj) + dropped, merge_into = _decision_sets(ledger, adj, block_decisions) + if blocked or block_decisions: + _fail("R1 returned BLOCK_REVIEW items: 인간 검토 필요", gates, + [json.dumps(x, ensure_ascii=False)[:200] for x in (blocked + block_decisions)]) + + candidates = [item for item in _list(ledger.get("ledger_candidates")) if isinstance(item, dict)] + survivors = [item for item in candidates if item.get("candidate_ref") not in dropped] + survivors.sort(key=_sort_tuple) + if not survivors: + _fail("no surviving BO candidates after decisions", gates, []) + + candidate_ref_to_bo_id: dict[str, str] = {} + for idx, item in enumerate(survivors, start=1): + candidate_ref_to_bo_id[str(item["candidate_ref"])] = f"bh{idx}" + for source_ref, target_ref in merge_into.items(): + if target_ref in candidate_ref_to_bo_id: + candidate_ref_to_bo_id[source_ref] = candidate_ref_to_bo_id[target_ref] + + bo_items: list[dict[str, Any]] = [] + normalization_notes: list[dict[str, Any]] = [] + evidence_gaps: list[Any] = [] + prior_link_notes: list[dict[str, Any]] = [] + + for idx, ledger_item in enumerate(survivors, start=1): + seed = _dict(ledger_item.get("seed_payload")) + candidate_ref = str(ledger_item.get("candidate_ref")) + bo_id = f"bh{idx}" + bo_type = field_decisions.get((candidate_ref, "BOType"), seed.get("BOType")) + action_type = field_decisions.get((candidate_ref, "ActionType"), seed.get("ActionType")) + juristic = _juristic(field_decisions.get((candidate_ref, "JuristicAct.label"), seed.get("JuristicAct"))) + domain_payload = _dict(_dict(seed.get("extensions")).get("domain_payload")) + action = field_decisions.get((candidate_ref, "Action"), seed.get("Action") or domain_payload.get("action_summary") or _dict(seed.get("core_field_base")).get("Action_proposal")) + if bo_type not in {"event", "state"}: + normalization_notes.append({"candidate_ref": candidate_ref, "field": "BOType", "received": bo_type, "fallback": "event"}) + bo_type = "event" + if action_type not in ACTION_TYPE_ENUM: + normalization_notes.append({"candidate_ref": candidate_ref, "field": "ActionType", "received": action_type, "fallback": "사실행위(factual acts)"}) + action_type = "사실행위(factual acts)" + if not isinstance(action, str) or not action.strip(): + normalization_notes.append({"candidate_ref": candidate_ref, "field": "Action", "fallback": "source-backed BO"}) + action = f"{candidate_ref} source-backed BO" + + refs = _dict(ledger_item.get("source_refs")) + source_evidence_indexes = _strings(refs.get("source_evidence_indexes")) + evidence_items = [_evidence_item(eidx, evidence_map.get(eidx), evidence_gaps, bo_id) for eidx in source_evidence_indexes] + evidence_titles: list[str] = [] + for ev in evidence_items: + title = ev["source_title"] + if title not in evidence_titles: + evidence_titles.append(title) + + link = link_decisions.get(candidate_ref, {}) + reason_ref_candidates = _strings(link.get("reason_refs_candidate_refs")) + prior_candidate = link.get("prior_candidate_ref") + if prior_candidate == "NO_LINK": + prior_candidate = None + explicit_refs = _dict(seed.get("downstream_seed_refs")) + if not reason_ref_candidates: + reason_ref_candidates = _strings(explicit_refs.get("reason_refs_candidate_refs")) + if not prior_candidate: + prior_list = _strings(explicit_refs.get("prior_candidate_refs")) + if len(prior_list) == 1: + prior_candidate = prior_list[0] + elif len(prior_list) > 1: + # 결정적 defer 정책 (R-5): PriorAct 불명은 null 유지 + review note (blocker 아님) + prior_candidate = None + prior_link_notes.append({"candidate_ref": candidate_ref, "prior_candidates": prior_list, + "policy": "prior_link_ambiguous_kept_null"}) + reason_refs = [candidate_ref_to_bo_id[ref] for ref in reason_ref_candidates if ref in candidate_ref_to_bo_id and candidate_ref_to_bo_id[ref] != bo_id] + if prior_candidate and prior_candidate in candidate_ref_to_bo_id: + prior_act = candidate_ref_to_bo_id[prior_candidate] + elif reason_refs: + prior_act = reason_refs[0] + else: + prior_act = None + reason = "ReasonRefs에 기재된 선행 BO와 source evidence/event chain으로 연결됨" if reason_refs else "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO" + + bo_items.append({ + "BO_ID": bo_id, + "id": bo_id, + "BOType": bo_type, + "ActionType": action_type, + "JuristicAct": juristic, + "Action": str(action).strip(), + "Reason": reason, + "PriorAct": prior_act, + "ReasonRefs": reason_refs, + "Legal_Keywords": _keywords(seed, juristic), + "core_field_base": _core(seed), + "amount": _amount(seed.get("amount")), + "EvidenceTitles": evidence_titles, + "Evidence": evidence_items, + "source_evidence_indexes": source_evidence_indexes, + "provenance": { + "source_event_candidate_ids": _strings(refs.get("source_event_candidate_ids")), + "source_meeting_clause_ids": _strings(refs.get("source_meeting_clause_ids")), + "source_domain": seed.get("source_domain"), + }, + "downstream_seed_refs": _downstream_refs(seed), + "extensions": {"domain_payload": domain_payload}, + }) + + # ---------- conservation + 게이트 (PostB_4 이식) ---------- + total_ledger = len(candidates) + absorbed = len(dropped) + _add_gate(gates, "candidate_conservation", len(bo_items) + absorbed == total_ledger, + f"BO {len(bo_items)} + absorbed {absorbed} == ledger {total_ledger}") + _add_gate(gates, "bo_items_array_non_empty", len(bo_items) > 0, "bo_items must be non-empty array") + ids = {item["BO_ID"] for item in bo_items} + errors: list[str] = [] + for idx, item in enumerate(bo_items, start=1): + errors.extend(_validate_item(item, idx, ids, universe)) + _add_gate(gates, "bo_schema_and_reference_validation", not errors, "BO_JSON_Schema target validation") + if any(g["status"] != "PASS" for g in gates) or errors: + _fail("pre-write gate failed", gates, errors) + + payload = json.dumps(bo_items, ensure_ascii=False, indent=2) + "\n" + write_doc(TARGET_NAME, payload) + reread = read_json_doc(TARGET_NAME) + _add_gate(gates, "post_write_json_parse", isinstance(reread, list) and len(reread) == len(bo_items), "BO.json reread JSON parse") + if not isinstance(reread, list) or len(reread) != len(bo_items): + _fail("post-write verification failed", gates, ["reread mismatch"]) + + write_doc(BUNDLE_COMPACT_PATH, json.dumps({ + "schema_version": "task_c_bo_postb_compiled_bundle_compact.v1", + "status": "READY", + "candidate_ref_to_bo_id": candidate_ref_to_bo_id, + "bo_item_count": len(bo_items), + "normalization_notes": normalization_notes, + "evidence_gap_items": evidence_gaps, + "prior_link_notes": prior_link_notes, + }, ensure_ascii=False, indent=2)) + + # review handoff 최종 status 갱신 + try: + handoff = _dict(read_json_doc(REVIEW_HANDOFF_PATH)) + except Exception: + handoff = {"schema_version": "stage1_part2_review_handoff.v1", "review_items": []} + handoff["status"] = "FINALIZED" + handoff["bo_item_count"] = len(bo_items) + for note in prior_link_notes: + handoff.setdefault("review_items", []).append({ + "review_id": f"F0:prior:{note['candidate_ref']}", + "source_domain": None, + "severity": "SOFT_WARNING", + "issue_type": "prior_link_ambiguous", + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Stage2", + "template_note": "선행행위 후보가 복수여서 PriorAct를 null로 보존하였다.", + }) + write_doc(REVIEW_HANDOFF_PATH, json.dumps(handoff, ensure_ascii=False, indent=2)) + + print(json.dumps({ + "status": "PASS", + "message": f"BO.json 작성 완료 (BO {len(bo_items)}건)", + "write_target": TARGET_NAME, + "bo_item_count": len(bo_items), + "absorbed_by_merge": absorbed, + "gate_results": gates, + "bundle_compact_path": BUNDLE_COMPACT_PATH, + }, ensure_ascii=False)) + + if __name__ == "__main__": + main() + - task_name: Task_C_BO_S0_signal_bundle_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 180 + code: |- + #!/usr/bin/env python3 + # Task_C_BO_S0_signal_bundle_writer (v3) + # Middle_Input 3개 task 통합: BO.json 1회 read로 signal 3파일 생성. + # 각 signal 빌더는 v2 원본 함수를 모듈 소스 그대로 exec하여 사용한다 (검증 로직 포함 무손실 이식). + # Spec: Part_2_Improvement_Strategy_Claude_v1.md §10 + from __future__ import annotations + import itertools + import json + import re + import sys + from typing import Any + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + STAGE_A_PATH = "stage1_tmp/task_c_bo/stage_a_context.json" + LEDGER_PATH = "stage1_tmp/task_c_bo/postb_seed_ledger.json" + BUNDLE_COMPACT_PATH = "stage1_tmp/task_c_bo/postb_compiled_bundle_compact.json" + B4_SEED_PATH = "stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B4_Actio.json" + BO_PATH = "BO.json" + + def _mid() -> int: + return next(MSG_ID) + + def _init() -> None: + r = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "task-c-bo-s0-signal-bundle-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + r.raise_for_status() + sid = r.headers.get("mcp-session-id") + if sid: + MCP_HEADERS["mcp-session-id"] = sid + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _parse_mcp(text: str) -> Any: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + pass + try: + return json.loads(text) + except Exception: + return None + + def _call(name: str, args: dict[str, Any]) -> Any: + r = CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "id": _mid(), "method": "tools/call", + "params": {"name": name, "arguments": args}}, + headers=MCP_HEADERS, + ) + r.raise_for_status() + payload = _parse_mcp(r.text) + if not payload or "result" not in payload: + raise RuntimeError(f"MCP {name} failed") + return payload + + def read_json_doc(path: str) -> Any: + result = _call("read_docs", {"doc_names": [path]}) + content = result["result"].get("content", []) + text = content[0].get("text", "") if content else "" + try: + parsed = json.loads(text) + except json.JSONDecodeError as exc: + raise RuntimeError(f"Invalid read_docs envelope for {path}") from exc + if isinstance(parsed, dict) and "results" in parsed: + rows = parsed.get("results") or [] + if not rows or rows[0].get("error"): + raise RuntimeError(f"read_docs failed for {path}: {rows[:1]}") + inner = rows[0].get("content") or "" + if isinstance(inner, str): + try: + return json.loads(inner) + except json.JSONDecodeError: + obj, end = JSON_DECODER.raw_decode(inner.strip()) + if inner.strip()[end:].strip(): + raise RuntimeError(f"{path}: non-json tail") + return obj + return inner + return parsed + + def write_doc(path: str, content: str) -> None: + _call("write_file", {"path": path, "content": content, "overwrite": True}) + + _MOD_ACTIO_SRC = r''' + ACTIO_SCOPE_ENUM = { + "preserved_claim", "fraudulent_act", "beneficiary_or_transferee", + "target_asset", "encumbrance", "remedy_or_cap", "defense", + } + + SUSPICION_LEVEL_ENUM = {"NONE", "LOW", "MEDIUM", "HIGH", "CRITICAL"} + + ASSET_KIND_ENUM = { + "real_property", "movable", "claim", "security_right", + "cash_or_deposit", "business_asset", "other", "unknown", + } + + ROLE_ENUM = { + "fraudulent_act_date", "debtor_act", "beneficiary", "transferee", + "remedy_or_cap", "defense", "encumbrance", + } + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def _first_string(*values: Any) -> str | None: + for value in values: + for item in _strings(value): + return item + return None + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + def _bo_by_id(bo_items: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: + return {str(item.get("BO_ID") or item.get("id")): item for item in bo_items if item.get("BO_ID") or item.get("id")} + + def _candidate_mapping(bundle: dict[str, Any]) -> dict[str, str]: + mapping = _dict(bundle.get("candidate_ref_to_bo_id")) + return {str(k): str(v) for k, v in mapping.items() if k and v} + + def _candidate_to_bo(ledger: dict[str, Any], mapping: dict[str, str]) -> dict[str, dict[str, Any]]: + out: dict[str, dict[str, Any]] = {} + for item in _list(ledger.get("ledger_candidates")): + if isinstance(item, dict) and item.get("candidate_ref") in mapping: + out[str(item["candidate_ref"])] = item + return out + + def _source_evidence(value: Any) -> list[str]: + if isinstance(value, dict): + refs = _dict(value.get("source_refs")) + if refs: + return _strings(refs.get("source_evidence_indexes")) + return _strings(value.get("source_evidence_indexes")) + return [] + + def _seed_payload(candidate: dict[str, Any]) -> dict[str, Any]: + return _dict(candidate.get("seed_payload")) + + def _domain_payload_from_seed(seed: dict[str, Any]) -> dict[str, Any]: + return _dict(_dict(seed.get("extensions")).get("domain_payload")) + + def _domain_payload_from_bo(bo: dict[str, Any]) -> dict[str, Any]: + return _dict(_dict(bo.get("extensions")).get("domain_payload")) + + def _flatten_strings(value: Any, limit: int = 10) -> list[str]: + out: list[str] = [] + + def visit(node: Any) -> None: + if len(out) >= limit: + return + if node is None: + return + if isinstance(node, (str, int, float, bool)): + text = str(node).strip() + if text and text not in out: + out.append(text) + return + if isinstance(node, list): + for child in node: + visit(child) + return + if isinstance(node, dict): + for key in ( + "asset_label", "asset", "property", "object", "Object", "description", + "value", "name", "party", "beneficiary", "transferee", "debtor_act", + "remedy", "defense", "encumbrance", "date", "claim_type", + ): + if key in node: + visit(node.get(key)) + visit(value) + return out[:limit] + + def _asset_kind_from_text(text: str) -> str: + lowered = text.lower() + if any(key in text for key in ["부동산", "토지", "건물", "아파트", "대지", "등기", "근저당"]): + return "real_property" + if any(key in text for key in ["채권", "대여금", "매매대금", "임대차보증금"]): + return "claim" + if any(key in text for key in ["근저당", "담보", "저당", "질권", "유치권"]): + return "security_right" + if any(key in text for key in ["예금", "현금", "계좌", "deposit", "cash"]) or "bank" in lowered: + return "cash_or_deposit" + if any(key in text for key in ["영업", "사업", "상호", "business"]) or "company" in lowered: + return "business_asset" + if any(key in text for key in ["동산", "차량", "기계", "movable"]): + return "movable" + return "unknown" + + def _target_property_candidates(payload: dict[str, Any], bo_ids: list[str], evidence: list[str]) -> list[dict[str, Any]]: + sources = [ + payload.get("asset_bindings"), + payload.get("asset_binding_hint"), + payload.get("valuation_matrix"), + payload.get("fraudulent_transfer_facts"), + payload.get("encumbrance_timeline"), + payload.get("target_property_candidates"), + ] + out: list[dict[str, Any]] = [] + for source in sources: + for item in _as_list(source): + labels = _flatten_strings(item, limit=4) + if not labels and isinstance(item, dict): + labels = _strings(item.get("asset_label") or item.get("property_label")) + for label in labels: + candidate = { + "asset_label": label, + "asset_kind": _asset_kind_from_text(label), + "related_bo_ids": bo_ids, + "related_evidence_indexes": evidence, + } + key = json.dumps(candidate, ensure_ascii=False, sort_keys=True) + if key not in {json.dumps(x, ensure_ascii=False, sort_keys=True) for x in out}: + out.append(candidate) + return out + + def _preserved_claim_candidates(payload: dict[str, Any], mapping: dict[str, str], bo_ids: list[str], evidence: list[str]) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for item in _as_list(payload.get("preserved_claim_bundle") or payload.get("preserved_claim_candidates")): + obj = _dict(item) + ref = _first_string(obj.get("candidate_ref"), obj.get("bo_candidate_ref")) + bo_id = mapping.get(ref) if ref else _first_string(obj.get("bo_id")) + if bo_id not in bo_ids and bo_ids: + bo_id = bo_id or None + out.append({ + "bo_id": bo_id, + "claim_type_candidate": _first_string(obj.get("claim_type_candidate"), obj.get("claim_type"), obj.get("type"), obj.get("label")), + "creditor_candidate": _first_string(obj.get("creditor_candidate"), obj.get("creditor"), obj.get("plaintiff")), + "debtor_candidate": _first_string(obj.get("debtor_candidate"), obj.get("debtor"), obj.get("defendant")), + "claim_date_candidate": _first_string(obj.get("claim_date_candidate"), obj.get("claim_date"), obj.get("date"), obj.get("BehaviorTime")), + "related_evidence_indexes": _dedupe(evidence + _strings(obj.get("related_evidence_indexes") or obj.get("source_evidence_indexes"))), + }) + if not out and bo_ids: + out.append({ + "bo_id": bo_ids[0], + "claim_type_candidate": None, + "creditor_candidate": None, + "debtor_candidate": None, + "claim_date_candidate": None, + "related_evidence_indexes": evidence, + }) + return out + + def _role_hint(role: str, value: Any, date: Any, bo_ids: list[str], evidence: list[str]) -> dict[str, Any] | None: + if role not in ROLE_ENUM: + return None + value_candidate = _first_string(value) + date_candidate = _first_string(date) + if value_candidate is None and date_candidate is None: + return None + return { + "role": role, + "value_candidate": value_candidate, + "date_candidate": date_candidate, + "related_bo_ids": bo_ids, + "related_evidence_indexes": evidence, + } + + def _actio_role_hints(payload: dict[str, Any], seed: dict[str, Any], bo_ids: list[str], evidence: list[str]) -> list[dict[str, Any]]: + hints: list[dict[str, Any]] = [] + for fact in _as_list(payload.get("fraudulent_transfer_facts")): + obj = _dict(fact) + for hint in ( + _role_hint("fraudulent_act_date", obj.get("date") or obj.get("act_date"), obj.get("date") or obj.get("act_date"), bo_ids, evidence), + _role_hint("debtor_act", obj.get("debtor_act") or obj.get("action") or obj.get("description"), obj.get("date"), bo_ids, evidence), + ): + if hint: + hints.append(hint) + for party in _as_list(payload.get("beneficiary_transferee_hints")): + obj = _dict(party) + role_text = "transferee" if "transferee" in str(obj).lower() or "양수" in str(obj) else "beneficiary" + hint = _role_hint(role_text, obj.get("party") or obj.get("name") or obj.get("value") or obj, obj.get("date"), bo_ids, evidence) + if hint: + hints.append(hint) + for role, key in [ + ("remedy_or_cap", "remedy_mode_hints"), + ("defense", "defense_rebuttal"), + ("encumbrance", "encumbrance_timeline"), + ]: + for item in _as_list(payload.get(key)): + obj = _dict(item) + hint = _role_hint(role, obj or item, obj.get("date") if obj else None, bo_ids, evidence) + if hint: + hints.append(hint) + if not hints: + action = _first_string(seed.get("Action"), _dict(seed.get("core_field_base")).get("Action_proposal")) + hint = _role_hint("debtor_act", action, _dict(seed.get("core_field_base")).get("BehaviorTime"), bo_ids, evidence) + if hint: + hints.append(hint) + deduped: list[dict[str, Any]] = [] + seen: set[str] = set() + for hint in hints: + key = json.dumps(hint, ensure_ascii=False, sort_keys=True) + if key not in seen: + seen.add(key) + deduped.append(hint) + return deduped + + def _scopes(payload: dict[str, Any]) -> list[str]: + scopes: list[str] = [] + if payload.get("preserved_claim_bundle") or payload.get("preserved_claim_candidates"): + scopes.append("preserved_claim") + if payload.get("fraudulent_transfer_facts") or "actio" in " ".join(_strings(payload.get("legal_effect_tags"))).lower(): + scopes.append("fraudulent_act") + if payload.get("beneficiary_transferee_hints"): + scopes.append("beneficiary_or_transferee") + if payload.get("asset_bindings") or payload.get("asset_binding_hint") or payload.get("valuation_matrix"): + scopes.append("target_asset") + if payload.get("encumbrance_timeline"): + scopes.append("encumbrance") + if payload.get("remedy_mode_hints"): + scopes.append("remedy_or_cap") + if payload.get("defense_rebuttal"): + scopes.append("defense") + return [scope for scope in _dedupe(scopes) if scope in ACTIO_SCOPE_ENUM] + + def _suspicion_level(scopes: list[str]) -> str: + core = {"preserved_claim", "fraudulent_act", "beneficiary_or_transferee"} + if core.issubset(set(scopes)): + return "HIGH" + if {"preserved_claim", "fraudulent_act"}.issubset(set(scopes)): + return "MEDIUM" + if scopes: + return "LOW" + return "NONE" + + def _reasons(scopes: list[str], payload: dict[str, Any], b4_review_queue: list[Any]) -> str: + fragments: list[str] = [] + if "preserved_claim" in scopes: + fragments.append("피보전채권 후보가 B4 seed/domain payload에서 확인됨") + if "fraudulent_act" in scopes: + fragments.append("채무자 처분행위 또는 사해행위 후보 사실이 확인됨") + if "beneficiary_or_transferee" in scopes: + fragments.append("수익자 또는 전득자 후보가 확인됨") + if "target_asset" in scopes: + fragments.append("취소·원상회복 대상 재산 후보가 확인됨") + if "encumbrance" in scopes: + fragments.append("담보권·가압류 등 부담 관련 단서가 확인됨") + if "remedy_or_cap" in scopes: + fragments.append("원상회복 방법 또는 가액배상 한도 단서가 확인됨") + if "defense" in scopes: + fragments.append("항변 또는 선의·악의 관련 반박 단서가 확인됨") + for item in b4_review_queue[:2]: + if isinstance(item, dict): + reason = _first_string(item.get("reason"), item.get("issue"), item.get("review_reason")) + if reason: + fragments.append(f"B4 review queue: {reason}") + return ", ".join(fragments) if fragments else "사해행위취소 관련 구조화 단서가 충분히 확인되지 않음" + + def _validate(payload: dict[str, Any]) -> None: + if payload.get("schema_version") != "actio_case_signals.v1": + raise ValueError("schema_version mismatch") + if payload.get("status") not in {"READY", "READY_WITH_REVIEW", "EMPTY", "FAILED"}: + raise ValueError("invalid status") + signals = payload.get("actio_case_signals") + if not isinstance(signals, list): + raise ValueError("actio_case_signals must be array") + for idx, item in enumerate(signals): + if not isinstance(item, dict): + raise ValueError(f"signal[{idx}] must be object") + for key in [ + "signal_id", "actio_scope", "suspicion_level", "suspicion_reasons", + "related_bo_ids", "related_evidence_indexes", "target_property_candidates", + "preserved_claim_candidates", "actio_role_hints", + ]: + if key not in item: + raise ValueError(f"signal[{idx}] missing {key}") + if not isinstance(item["signal_id"], str): + raise ValueError(f"signal[{idx}].signal_id must be string") + if item["suspicion_level"] not in SUSPICION_LEVEL_ENUM: + raise ValueError(f"signal[{idx}].suspicion_level invalid") + if not isinstance(item["suspicion_reasons"], str): + raise ValueError(f"signal[{idx}].suspicion_reasons must be string") + if any(scope not in ACTIO_SCOPE_ENUM for scope in _list(item["actio_scope"])): + raise ValueError(f"signal[{idx}].actio_scope invalid") + for field in ["related_bo_ids", "related_evidence_indexes"]: + if not all(isinstance(x, str) for x in _list(item[field])): + raise ValueError(f"signal[{idx}].{field} must be string array") + for prop in _list(item["target_property_candidates"]): + if not isinstance(prop, dict): + raise ValueError(f"signal[{idx}].target_property_candidates item must be object") + for key in ["asset_label", "asset_kind", "related_bo_ids", "related_evidence_indexes"]: + if key not in prop: + raise ValueError(f"target_property_candidate missing {key}") + if prop["asset_kind"] not in ASSET_KIND_ENUM: + raise ValueError("target_property_candidate.asset_kind invalid") + for claim in _list(item["preserved_claim_candidates"]): + if not isinstance(claim, dict): + raise ValueError(f"signal[{idx}].preserved_claim_candidates item must be object") + for key in [ + "bo_id", "claim_type_candidate", "creditor_candidate", + "debtor_candidate", "claim_date_candidate", "related_evidence_indexes", + ]: + if key not in claim: + raise ValueError(f"preserved_claim_candidate missing {key}") + for hint in _list(item["actio_role_hints"]): + if not isinstance(hint, dict): + raise ValueError(f"signal[{idx}].actio_role_hints item must be object") + for key in ["role", "value_candidate", "date_candidate", "related_bo_ids", "related_evidence_indexes"]: + if key not in hint: + raise ValueError(f"actio_role_hint missing {key}") + if hint["role"] not in ROLE_ENUM: + raise ValueError("actio_role_hint.role invalid") + + + def build_signals(ledger, compiled, b4, bo_items): + bo_by_id = _bo_by_id(bo_items) + mapping = _candidate_mapping(compiled) + candidates_by_ref = _candidate_to_bo(ledger, mapping) + signals = [] + review_needed = False + for seed_candidate in _list(b4.get("bo_seed_candidates")): + if not isinstance(seed_candidate, dict): + continue + candidate_ref = str(seed_candidate.get("candidate_ref") or "") + bo_id = mapping.get(candidate_ref) + bo_ids = [bo_id] if bo_id else [] + ledger_item = candidates_by_ref.get(candidate_ref, {}) + ledger_seed = _seed_payload(ledger_item) + seed = seed_candidate if seed_candidate else ledger_seed + payload = _domain_payload_from_seed(seed) + if not payload and ledger_seed: + payload = _domain_payload_from_seed(ledger_seed) + if bo_id and bo_id in bo_by_id: + payload = {**payload, **_domain_payload_from_bo(bo_by_id[bo_id])} + bo_evidence = _source_evidence(bo_by_id.get(bo_id, {})) if bo_id else [] + evidence = _dedupe(_source_evidence(seed_candidate) + _source_evidence(ledger_item) + bo_evidence) + scopes = _scopes(payload) + if not scopes and candidate_ref: + scopes = ["fraudulent_act"] + review_needed = True + related_bo_ids = _dedupe(bo_ids + [x for x in _strings(seed_candidate.get("related_bo_ids")) if x in bo_by_id]) + if not related_bo_ids: + review_needed = True + target_properties = _target_property_candidates(payload, related_bo_ids, evidence) + preserved_claims = _preserved_claim_candidates(payload, mapping, related_bo_ids, evidence) + role_hints = _actio_role_hints(payload, seed, related_bo_ids, evidence) + if not target_properties or not preserved_claims or not role_hints: + review_needed = True + if scopes or related_bo_ids or target_properties or preserved_claims or role_hints: + signals.append({ + "signal_id": f"actio-signal-{len(signals) + 1:03d}", + "actio_scope": scopes, + "suspicion_level": _suspicion_level(scopes), + "suspicion_reasons": _reasons(scopes, payload, _list(b4.get("domain_review_queue"))), + "related_bo_ids": related_bo_ids, + "related_evidence_indexes": evidence, + "target_property_candidates": target_properties, + "preserved_claim_candidates": preserved_claims, + "actio_role_hints": role_hints, + }) + status = "EMPTY" if not signals else ("READY_WITH_REVIEW" if review_needed or b4.get("status") == "READY_WITH_REVIEW" else "READY") + payload = {"schema_version": "actio_case_signals.v1", "status": status, "actio_case_signals": signals} + _validate(payload) + return payload + + ''' + + _MOD_LIABILITY_SRC = r''' + LIABILITY_ENUM = { + "money_claim", "commercial_successor", "secured_debt", "registry_invalidity", + "land_use_gain", "actio_related", "succession_related", "notice_or_lien_related", + "defense_or_setoff", "other", + } + + PARTY_ROLE_ENUM = {"plaintiff", "defendant", "creditor", "debtor", "liable_party"} + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def _first_string(*values: Any) -> str | None: + for value in values: + for item in _strings(value): + return item + return None + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + def _source_evidence(bo: dict[str, Any]) -> list[str]: + return _strings(bo.get("source_evidence_indexes")) + + def _domain_payload(bo: dict[str, Any]) -> dict[str, Any]: + return _dict(_dict(bo.get("extensions")).get("domain_payload")) + + def _candidate_mapping(bundle: dict[str, Any]) -> dict[str, str]: + mapping = _dict(bundle.get("candidate_ref_to_bo_id")) + return {str(k): str(v) for k, v in mapping.items() if k and v} + + def _candidate_ref_for_bo(bo_id: str, mapping: dict[str, str]) -> str | None: + for ref, mapped in mapping.items(): + if mapped == bo_id: + return ref + return None + + def _liability_type(bo: dict[str, Any]) -> str: + payload = _domain_payload(bo) + source_domain = str(_dict(bo.get("provenance")).get("source_domain") or "").lower() + text = " ".join([ + source_domain, + str(bo.get("Action") or ""), + str(bo.get("ActionType") or ""), + str(_dict(bo.get("JuristicAct")).get("label") or ""), + " ".join(_strings(bo.get("Legal_Keywords"))), + " ".join(_strings(payload.get("legal_effect_tags"))), + str(payload.get("domain_legal_effect_tag") or ""), + ]).lower() + if "b4" in source_domain or "actio" in text or "사해" in text: + return "actio_related" + if "successor" in text or "영업양수" in text or "상호속용" in text: + return "commercial_successor" + if "b1" in source_domain or "대여금" in text or "매매대금" in text or "채권" in text: + return "money_claim" + if "registry_invalid" in text or "등기무효" in text or "말소" in text: + return "registry_invalidity" + if "secured" in text or "근저당" in text or "담보" in text or "유치권" in text: + return "secured_debt" + if "land" in text or "valuation" in text or "사용이익" in text or "부당이득" in text: + return "land_use_gain" + if "succession" in text or "상속" in text or "승계" in text: + return "succession_related" + if "notice" in text or "lien" in text or "통지" in text or "유치권" in text: + return "notice_or_lien_related" + if "defense" in text or "항변" in text or "상계" in text: + return "defense_or_setoff" + return "other" + + def _normalize_role(value: Any) -> str | None: + text = str(value or "").strip().lower() + if not text: + return None + if text in PARTY_ROLE_ENUM: + return text + if "plaintiff" in text or "원고" in text: + return "plaintiff" + if "defendant" in text or "피고" in text: + return "defendant" + if "creditor" in text or "채권자" in text: + return "creditor" + if "debtor" in text or "채무자" in text: + return "debtor" + if "liable" in text or "책임" in text or "이행" in text: + return "liable_party" + return None + + def _party_roles(bo: dict[str, Any]) -> list[dict[str, Any]]: + bo_id = str(bo.get("BO_ID") or bo.get("id") or "") + payload = _domain_payload(bo) + roles: list[dict[str, Any]] = [] + for item in _as_list(payload.get("party_roles") or payload.get("parties_hint") or payload.get("liability_party_hints")): + obj = _dict(item) + party = _first_string(obj.get("party"), obj.get("name"), obj.get("value")) + role = _normalize_role(obj.get("role") or obj.get("party_role")) + if party and role: + roles.append({"party": party, "role": role, "bo_ids": [bo_id] if bo_id else []}) + core = _dict(bo.get("core_field_base")) + performer = _first_string(core.get("Performer"), bo.get("Performer")) + subject = _first_string(core.get("Subject"), bo.get("Subject")) + if performer and not any(item["party"] == performer for item in roles): + roles.append({"party": performer, "role": "liable_party", "bo_ids": [bo_id] if bo_id else []}) + if subject and not any(item["party"] == subject for item in roles): + role = "creditor" if _liability_type(bo) in {"money_claim", "actio_related"} else "liable_party" + roles.append({"party": subject, "role": role, "bo_ids": [bo_id] if bo_id else []}) + deduped: list[dict[str, Any]] = [] + seen: set[str] = set() + for item in roles: + key = json.dumps(item, ensure_ascii=False, sort_keys=True) + if key not in seen: + seen.add(key) + deduped.append(item) + return deduped + + def _claim_group_id(bo: dict[str, Any]) -> str | None: + refs = _dict(bo.get("downstream_seed_refs")) + return _first_string(refs.get("claim_group_seed_refs_proposed") or refs.get("claim_group_seed_refs")) + + def _liability_group_id(bo: dict[str, Any], candidate_ref: str | None) -> str | None: + payload = _domain_payload(bo) + return _first_string(payload.get("liability_group_id"), payload.get("liability_group_ref"), candidate_ref) + + def _linked_structure_ids(bo: dict[str, Any]) -> list[str]: + refs = _dict(bo.get("downstream_seed_refs")) + return _strings(refs.get("legal_effect_structure_seed_ref_proposed") or refs.get("linked_structure_ids")) + + def _validate(payload: dict[str, Any]) -> None: + if payload.get("schema_version") != "case_liability_signals.v1": + raise ValueError("schema_version mismatch") + if payload.get("status") not in {"READY", "READY_WITH_REVIEW", "EMPTY", "FAILED"}: + raise ValueError("invalid status") + signals = payload.get("case_liability_signals") + if not isinstance(signals, list): + raise ValueError("case_liability_signals must be array") + for idx, item in enumerate(signals): + if not isinstance(item, dict): + raise ValueError(f"signal[{idx}] must be object") + for key in [ + "liability_candidate_type", "related_bo_ids", "related_evidence_indexes", + "party_roles", "claim_group_id", "liability_group_id", "linked_structure_ids", + ]: + if key not in item: + raise ValueError(f"signal[{idx}] missing {key}") + if item["liability_candidate_type"] not in LIABILITY_ENUM: + raise ValueError(f"signal[{idx}].liability_candidate_type invalid") + for field in ["related_bo_ids", "related_evidence_indexes", "linked_structure_ids"]: + if not isinstance(item[field], list) or not all(isinstance(x, str) for x in item[field]): + raise ValueError(f"signal[{idx}].{field} must be string array") + for role in _list(item["party_roles"]): + if not isinstance(role, dict): + raise ValueError(f"signal[{idx}].party_roles item must be object") + for key in ["party", "role", "bo_ids"]: + if key not in role: + raise ValueError(f"party_role missing {key}") + if not isinstance(role["party"], str) or role["role"] not in PARTY_ROLE_ENUM: + raise ValueError("party_role type invalid") + if not isinstance(role["bo_ids"], list) or not all(isinstance(x, str) for x in role["bo_ids"]): + raise ValueError("party_role.bo_ids must be string array") + + + def build_signals(ledger, compiled, b4, bo_items): + mapping = _candidate_mapping(compiled) + signals = [] + review_needed = b4.get("status") == "READY_WITH_REVIEW" + for bo in bo_items: + bo_id = str(bo.get("BO_ID") or bo.get("id") or "") + if not bo_id: + review_needed = True + continue + candidate_ref = _candidate_ref_for_bo(bo_id, mapping) + party_roles = _party_roles(bo) + if not party_roles: + review_needed = True + liability_group_id = _liability_group_id(bo, candidate_ref) + if liability_group_id is None: + review_needed = True + signals.append({ + "liability_candidate_type": _liability_type(bo), + "related_bo_ids": [bo_id], + "related_evidence_indexes": _source_evidence(bo), + "party_roles": party_roles, + "claim_group_id": _claim_group_id(bo), + "liability_group_id": liability_group_id, + "linked_structure_ids": _linked_structure_ids(bo), + }) + status = "EMPTY" if not signals else ("READY_WITH_REVIEW" if review_needed else "READY") + payload = {"schema_version": "case_liability_signals.v1", "status": status, "case_liability_signals": signals} + _validate(payload) + return payload + + ''' + + _MOD_LEGAL_EFFECT_SRC = r''' + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + def _domain_payload(bo: dict[str, Any]) -> dict[str, Any]: + return _dict(_dict(bo.get("extensions")).get("domain_payload")) + + def _source_evidence(bo: dict[str, Any]) -> list[str]: + return _strings(bo.get("source_evidence_indexes")) + + def _candidate_structure_types(bo: dict[str, Any]) -> list[str]: + payload = _domain_payload(bo) + juristic = _dict(bo.get("JuristicAct")) + candidates: list[str] = [] + candidates.extend(_strings(payload.get("candidate_structure_types"))) + candidates.extend(_strings(payload.get("legal_effect_tags"))) + candidates.extend(_strings(payload.get("domain_legal_effect_tag"))) + candidates.extend(_strings(bo.get("Legal_Keywords"))) + candidates.extend(_strings(bo.get("BOType"))) + candidates.extend(_strings(bo.get("ActionType"))) + candidates.extend(_strings(juristic.get("label"))) + source_domain = _dict(bo.get("provenance")).get("source_domain") + if source_domain: + candidates.append(str(source_domain)) + return _dedupe(candidates) or ["generic_legal_effect"] + + def _linked_structure_ids(bo: dict[str, Any]) -> list[str]: + refs = _dict(bo.get("downstream_seed_refs")) + payload = _domain_payload(bo) + return _dedupe( + _strings(refs.get("legal_effect_structure_seed_ref_proposed")) + + _strings(refs.get("linked_structure_ids")) + + _strings(payload.get("linked_structure_ids")) + ) + + def _validate(payload: dict[str, Any]) -> None: + if payload.get("schema_version") != "legal_effect_signals.v1": + raise ValueError("schema_version mismatch") + if payload.get("status") not in {"READY", "READY_WITH_REVIEW", "EMPTY", "FAILED"}: + raise ValueError("invalid status") + routes = payload.get("bo_legal_effect_routes") + if not isinstance(routes, list): + raise ValueError("bo_legal_effect_routes must be array") + for idx, item in enumerate(routes): + if not isinstance(item, dict): + raise ValueError(f"route[{idx}] must be object") + for key in ["bo_id", "candidate_structure_types", "linked_structure_ids", "related_evidence_indexes"]: + if key not in item: + raise ValueError(f"route[{idx}] missing {key}") + if not isinstance(item["bo_id"], str): + raise ValueError(f"route[{idx}].bo_id must be string") + for field in ["candidate_structure_types", "linked_structure_ids", "related_evidence_indexes"]: + if not isinstance(item[field], list) or not all(isinstance(x, str) for x in item[field]): + raise ValueError(f"route[{idx}].{field} must be string array") + + + def build_signals(ledger, compiled, b4, bo_items): + routes = [] + review_needed = b4.get("status") == "READY_WITH_REVIEW" + for bo in bo_items: + bo_id = str(bo.get("BO_ID") or bo.get("id") or "") + if not bo_id: + review_needed = True + continue + linked = _linked_structure_ids(bo) + if not linked: + review_needed = True + routes.append({ + "bo_id": bo_id, + "candidate_structure_types": _candidate_structure_types(bo), + "linked_structure_ids": linked, + "related_evidence_indexes": _source_evidence(bo), + }) + status = "EMPTY" if not routes else ("READY_WITH_REVIEW" if review_needed else "READY") + payload = {"schema_version": "legal_effect_signals.v1", "status": status, "bo_legal_effect_routes": routes} + _validate(payload) + return payload + + ''' + + + def _load_module(src: str) -> dict[str, Any]: + ns: dict[str, Any] = {"json": json, "re": re, "Any": Any} + exec(compile(src, "", "exec"), ns) + return ns + + def main() -> None: + _init() + stage_a_doc = read_json_doc(STAGE_A_PATH) + stage_a = stage_a_doc.get("stage_a_context") if isinstance(stage_a_doc, dict) else None + if not isinstance(stage_a, dict) or stage_a.get("status") != "READY": + raise RuntimeError("Stage A context must be READY") + ledger = (read_json_doc(LEDGER_PATH) or {}).get("postb_seed_ledger") + if not isinstance(ledger, dict) or ledger.get("status") != "READY": + raise RuntimeError("R0 seed ledger must be READY") + compiled = read_json_doc(BUNDLE_COMPACT_PATH) + if not isinstance(compiled, dict) or compiled.get("status") != "READY": + raise RuntimeError("F0 compiled bundle compact must be READY") + b4_doc = read_json_doc(B4_SEED_PATH) + b4 = b4_doc.get("stage_b_domain_bo_seed_output") if isinstance(b4_doc, dict) else None + if not isinstance(b4, dict) or b4.get("domain_id") != "B4_Actio": + raise RuntimeError("B4 seed artifact invalid") + if b4.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise RuntimeError("B4 seed status invalid") + bo_items = read_json_doc(BO_PATH) + if not isinstance(bo_items, list) or not bo_items: + raise RuntimeError("BO.json root must be non-empty array") + bo_items = [item for item in bo_items if isinstance(item, dict)] + + results = [] + for target, src in ( + ("actio_case_signals.json", _MOD_ACTIO_SRC), + ("case_liability_signals.json", _MOD_LIABILITY_SRC), + ("legal_effect_signals.json", _MOD_LEGAL_EFFECT_SRC), + ): + ns = _load_module(src) + payload = ns["build_signals"](ledger, compiled, b4, bo_items) + write_doc(target, json.dumps(payload, ensure_ascii=False, indent=2) + "\n") + count_key = next(k for k in payload if isinstance(payload[k], list)) + results.append({"written_file": target, "payload_status": payload.get("status"), + "item_count": len(payload[count_key])}) + + print(json.dumps({ + "status": "READY", + "message": "signal 3종 생성 완료", + "schema_version": "stage1_middle_input_signal_writer.v2", + "results": results, + }, ensure_ascii=False)) + + if __name__ == "__main__": + try: + main() + except Exception as exc: + print(json.dumps({"status": "FAILED", "message": "S0 signal bundle writer 실패", + "reason": str(exc)}, ensure_ascii=False)) + raise + task_procedure: + IN: + nexts: + - Task_C_BO_A0_context_and_domain_slice_compiler + wait_until: [] + Task_C_BO_A0_context_and_domain_slice_compiler: + nexts: + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + wait_until: + - IN + Task_C_BO_Stage_B_B1_Money_Successor: + nexts: + - Task_C_BO_R0_seed_reducer_and_exception_planner + wait_until: + - Task_C_BO_A0_context_and_domain_slice_compiler + Task_C_BO_Stage_B_B2_Secured_Registry: + nexts: + - Task_C_BO_R0_seed_reducer_and_exception_planner + wait_until: + - Task_C_BO_A0_context_and_domain_slice_compiler + Task_C_BO_Stage_B_B3_Land_Valuation: + nexts: + - Task_C_BO_R0_seed_reducer_and_exception_planner + wait_until: + - Task_C_BO_A0_context_and_domain_slice_compiler + Task_C_BO_Stage_B_B4_Actio: + nexts: + - Task_C_BO_R0_seed_reducer_and_exception_planner + wait_until: + - Task_C_BO_A0_context_and_domain_slice_compiler + Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense: + nexts: + - Task_C_BO_R0_seed_reducer_and_exception_planner + wait_until: + - Task_C_BO_A0_context_and_domain_slice_compiler + Task_C_BO_R0_seed_reducer_and_exception_planner: + nexts: + - Task_C_BO_R1_exception_adjudicator + wait_until: + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + Task_C_BO_R1_exception_adjudicator: + nexts: + - Task_C_BO_F0_final_bo_compiler_gate_writer + wait_until: + - Task_C_BO_R0_seed_reducer_and_exception_planner + Task_C_BO_F0_final_bo_compiler_gate_writer: + nexts: + - Task_C_BO_S0_signal_bundle_writer + wait_until: + - Task_C_BO_R1_exception_adjudicator + Task_C_BO_S0_signal_bundle_writer: + nexts: + - OUT + wait_until: + - Task_C_BO_F0_final_bo_compiler_gate_writer + OUT: + nexts: [] + wait_until: + - Task_C_BO_S0_signal_bundle_writer diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_2_Codex_v3.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_2_Codex_v3.yml new file mode 100644 index 00000000..ea89beb1 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_2_Codex_v3.yml @@ -0,0 +1,4013 @@ +--- +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_2 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Part 2 + version: v.3 + Stages: + - name: stage1_BO_시그널_생성 + description: BO 생성, 시그널 생성 + tools: + mcpServers: + localdocs: + type: streamable-http + url: http://mcp-localdocs:8012/mcp + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + tasks: + - task_name: Task_C_BO_A0_context_domain_slice_compiler + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_IDS = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + def _next_id() -> int: + return next(MSG_IDS) + + def _parse_json_first(raw: str, label: str, allow_trailing: bool = False) -> Any: + text = (raw or "").lstrip("\ufeff").strip() + if not text: + raise RuntimeError(f"{label}: empty JSON text") + try: + return json.loads(text) + except json.JSONDecodeError as first_error: + try: + value, end = JSON_DECODER.raw_decode(text) + except json.JSONDecodeError as second_error: + raise RuntimeError(f"{label}: invalid JSON") from second_error + if not allow_trailing and text[end:].strip(): + raise RuntimeError(f"{label}: trailing non-JSON data") from first_error + return value + + def _parse_rpc(raw: str) -> dict[str, Any]: + events: list[dict[str, Any]] = [] + for line in (raw or "").splitlines(): + if not line.startswith("data:"): + continue + candidate = line[5:].strip() + if not candidate or candidate == "[DONE]": + continue + try: + parsed = json.loads(candidate) + except json.JSONDecodeError: + continue + if isinstance(parsed, dict): + events.append(parsed) + if events: + return events[-1] + parsed = _parse_json_first(raw, "MCP response", allow_trailing=True) + if not isinstance(parsed, dict): + raise RuntimeError("MCP response root must be object") + return parsed + + def _init(task_name: str) -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": task_name, + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _call(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_rpc(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP {name} failed: {payload['error']}") + result = payload.get("result") + if not isinstance(result, dict): + raise RuntimeError(f"MCP {name} returned no result") + if result.get("isError") is True: + raise RuntimeError(f"MCP {name} returned isError") + return payload + + def _unwrap(payload: dict[str, Any], label: str) -> Any: + result = payload.get("result") or {} + content = result.get("content") or [] + texts = [block.get("text") for block in content if isinstance(block, dict) and block.get("text")] + if not texts: + raise RuntimeError(f"{label}: empty MCP content") + raw = texts[0] + try: + return _parse_json_first(raw, label, allow_trailing=True) + except RuntimeError: + return raw + + def _extract_doc(payload: dict[str, Any], doc_name: str) -> str: + outer = _unwrap(payload, f"read_docs:{doc_name}") + if isinstance(outer, dict) and isinstance(outer.get("results"), list): + for item in outer["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + if name and not (str(name) == doc_name or str(name).endswith("/" + doc_name)): + continue + value = item.get("content") + if value is None: + value = item.get("text") + if value is None: + value = item.get("raw") + if value is not None: + return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False) + if isinstance(outer, dict) and doc_name in outer: + value = outer[doc_name] + return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False) + if isinstance(outer, str): + return outer + return json.dumps(outer, ensure_ascii=False) + + def _read_text(doc_name: str) -> str: + return _extract_doc(_call("read_docs", {"doc_names": [doc_name]}), doc_name) + + def _read_json(doc_name: str) -> Any: + raw = _read_text(doc_name) + return _parse_json_first(raw, doc_name, allow_trailing=True) + + def _canonical(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + def _sha(value: Any) -> str: + raw = value if isinstance(value, str) else _canonical(value) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _write_json_verified(doc_name: str, value: Any) -> str: + content = json.dumps(value, ensure_ascii=False, indent=2) + _call("write_file", {"path": doc_name, "content": content, "overwrite": True}) + reread = _read_json(doc_name) + if _canonical(reread) != _canonical(value): + raise RuntimeError(f"{doc_name}: post-write verification mismatch") + return _sha(content) + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _uniq(values: list[Any]) -> list[Any]: + seen: set[str] = set() + output: list[Any] = [] + for value in values: + marker = _canonical(value) if isinstance(value, (dict, list)) else str(value) + if marker in seen: + continue + seen.add(marker) + output.append(value) + return output + + def _natural_key(value: Any) -> list[Any]: + return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", str(value))] + + REQUIRED_FILES = [ + "client_meeting.md", + "evidence_event_candidates.json", + "evidence_indexed.json", + "Default_Agent/Juristic_Act.md", + ] + PART1_SOFT_GATE_HANDOFF_FILE = "quality_gates/stage1_part1_soft_gate_handoff.json" + + CURRENT_STATE_PATTERNS = [ + "현재", "현재까지", "현재도", "계속", "거주", "점유", "사용", "운영", + "부지로만 사용", "인도받아", "사업자등록", "유치권", "임대", "수익", + ] + CONFLICT_PATTERNS = [ + "하였음에도", "그럼에도", "불구하고", "거절", "응하지", "주장", + "항변", "다툼", "무효", "취소", "해제", "해지", "말소", "신뢰", + ] + SUCCESSION_PATTERNS = [ + "사망", "상속", "상속포기", "배우자", "직계비속", "직계존속", + "피상속인", "상속인", "수리", "가족관계", + ] + MIXED_CONTRACT_PATTERNS = [ + "임대차", "매매", "보증금", "매매대금", "인도", "관리", "사용", + "사용수익", "영업양도", "채무불승계", "대물변제", + ] + + + DOMAIN_CONFIGS = [ + { + "domain_id": "B1_Money_Successor", + "domain_label": "금전채권 및 상호속용 책임", + "allowed_legal_effect_bo_types": [ + "money_claim_chain_BO", + "commercial_successor_liability_chain_BO", + "liability_group_hint_BO", + ], + "terms": [ + "대여금", "차용", "매매대금", "물품대금", "보증금", "정산금", "공사대금", + "원금", "이자", "지연손해금", "변제기", "이행기", "일부변제", "미변제", + "채무승인", "지급각서", "확인서", "최고", "통지", "상호속용", "영업양도", + "양수인", "채무불승계", "business_transfer", "loan", "payment", "admission", + "notice", "claim_document", "mixed_contract_payment", + ], + "meeting_sources": ["mixed_contract_clause_ids", "conflict_pattern_clause_ids"], + "event_sources": ["mixed_contract_candidate_ids"], + "hazard_keys": ["mixed_contract_candidates", "conflict_pattern_expressions", "asset_clusters"], + "taxonomy_axes": ["legal_object", "structure", "dependency"], + }, + { + "domain_id": "B2_Secured_Registry", + "domain_label": "담보채무, 변제충당, 등기무효, 경매", + "allowed_legal_effect_bo_types": [ + "secured_debt_chain_BO", + "payment_allocation_and_mortgage_extinguishment_BO", + "auction_invalidity_BO", + "derivative_bank_mortgage_invalidity_BO", + ], + "terms": [ + "근저당", "저당", "담보", "피담보", "채무액", "변제", "변제충당", + "법정변제충당", "지분", "말소", "소유권이전등기", "등기", "등기원인", + "등기무효", "경매", "담보권실행경매", "경매개시", "매각", "배당", + "후행 담보권", "registry", "mortgage", "secured", "auction", "dividend", + "payment_allocation", "registry_row", "legal_calculation", + ], + "meeting_sources": ["conflict_pattern_clause_ids", "mixed_contract_clause_ids"], + "event_sources": [], + "hazard_keys": ["conflict_pattern_expressions", "asset_clusters", "mixed_contract_candidates"], + "taxonomy_axes": ["legal_object", "form", "dependency", "defect_or_condition"], + }, + { + "domain_id": "B3_Land_Valuation", + "domain_label": "토지 사용이익, 점유, 사용수익, 가치평가", + "allowed_legal_effect_bo_types": ["land_use_gain_timeline_BO", "liability_overlap_BO"], + "terms": [ + "토지", "건물", "부지", "인도", "점유", "사용", "사용수익", "현재 사용", + "현재 수익", "거주", "운영", "임대", "차임", "감정", "시가", "가액", + "미등기 건물", "사실상 처분권", "부진정연대", "valuation", "valuation_object", + "valuation_rows", "valuation_matrix", "registry_object_display", "possession", "land", "building", + ], + "meeting_sources": ["current_state_clause_ids", "conflict_pattern_clause_ids", "mixed_contract_clause_ids"], + "event_sources": ["state_candidate_ids"], + "hazard_keys": ["current_state_expressions", "asset_clusters", "mixed_contract_candidates", "conflict_pattern_expressions"], + "taxonomy_axes": ["legal_object", "contribution", "structure"], + }, + { + "domain_id": "B4_Actio", + "domain_label": "사해행위취소, 피보전채권, 책임재산, 가액배상", + "allowed_legal_effect_bo_types": [ + "preserved_claim_bundle_BO", + "fraudulent_transfer_BO", + "encumbrance_timeline_BO", + "valuation_matrix_BO", + "remedy_mode_decision_BO", + "value_compensation_cap_BO", + "defense_rebuttal_matrix_BO", + "actio_balance_sheet_BO", + ], + "terms": [ + "사해행위", "피보전채권", "채무초과", "책임재산", "공동담보", "매매", + "증여", "담보설정", "처분행위", "수익자", "전득자", "사해성", "악의", + "선의", "원상회복", "가액배상", "선순위담보", "말소", "가액 산정", + "항변", "actio", "fraudulent", "transfer", "preserved_claim", "encumbrance", + "valuation", "remedy", "compensation", "defense", "claim_document", + ], + "meeting_sources": ["conflict_pattern_clause_ids", "mixed_contract_clause_ids", "current_state_clause_ids", "succession_clause_ids"], + "event_sources": [], + "hazard_keys": ["conflict_pattern_expressions", "asset_clusters", "mixed_contract_candidates", "current_state_expressions", "succession_related_candidates"], + "taxonomy_axes": ["legal_object", "structure", "contribution", "defect_or_condition", "dependency", "form"], + }, + { + "domain_id": "B5_Succession_Notice_Lien_Asset_Defense", + "domain_label": "상속, 통지, 유치권, 목적물 특정, 항변", + "allowed_legal_effect_bo_types": [ + "succession_resolution_BO", + "notice_lifecycle_BO", + "lien_state_timeline_BO", + "asset_alias_BO", + "defense_rebuttal_BO", + "delivery_use_gain_claim_group_BO", + ], + "terms": [ + "사망", "상속", "상속포기", "배우자", "직계비속", "가족관계", "피상속인", + "상속인", "통지", "내용증명", "도달", "수령", "최고", "유치권", "점유", + "인도", "거절", "별지목록", "등기부 표시", "목적물", "동일성", "피고 답변", + "항변", "권리남용", "신의칙", "동시이행", "succession", "notice", "lien", + "possession", "asset", "defense", "party_assertion", "registry_object_display", + ], + "meeting_sources": ["succession_clause_ids", "current_state_clause_ids", "conflict_pattern_clause_ids", "mixed_contract_clause_ids"], + "event_sources": ["succession_candidate_ids", "state_candidate_ids"], + "hazard_keys": ["succession_related_candidates", "current_state_expressions", "conflict_pattern_expressions", "asset_clusters", "mixed_contract_candidates"], + "taxonomy_axes": ["structure", "time_effect", "legal_object", "defect_or_condition"], + } + ] + + def _stable_hash(value: Any) -> str: + if isinstance(value, str): + raw = value + else: + raw = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _text(value: Any) -> str | None: + if value is None: + return None + s = str(value).strip() + return s or None + + def _norm_space(value: Any) -> str: + return re.sub(r"\s+", " ", str(value or "")).strip() + + def _norm_key(value: Any) -> str: + text = str(value or "").strip().lower() + return "".join(ch for ch in text if ch.isalnum()) + + def _short_excerpt(value: Any, limit: int = 120) -> str: + text = _norm_space(value) + return text if len(text) <= limit else text[: limit - 1] + "…" + + def _section_key_from_heading(heading: str, seq: int) -> str: + normalized = _norm_key(heading) + if "상담내용" in normalized: + suffix = "consultation" + elif "사건관계인의주장" in normalized or ("사건관계인" in normalized and "주장" in normalized): + suffix = "party_assertions" + elif "희망사항" in normalized or "의뢰인희망" in normalized: + suffix = "client_requests" + elif normalized: + suffix = normalized[:24] + else: + suffix = "unnamed" + return f"s{seq:02d}_{suffix}" + + def _uniq(values: list[Any]) -> list[Any]: + seen: set[str] = set() + out: list[Any] = [] + for value in values: + marker = json.dumps(value, ensure_ascii=False, sort_keys=True) if isinstance(value, (dict, list)) else str(value) + if marker not in seen: + seen.add(marker) + out.append(value) + return out + + def _extract_doc_names(payload: Any) -> list[str]: + names: list[str] = [] + def walk(v: Any) -> None: + if isinstance(v, str): + if "." in v or "/" in v: + names.append(v) + names.extend(re.findall(r"[\w./()가-힣ᄀ-ᇿ -]+\.[A-Za-z0-9]+", v)) + elif isinstance(v, list): + for item in v: + walk(item) + elif isinstance(v, dict): + for key in ("name", "path", "doc_name", "document_name", "file", "filename"): + if v.get(key): + names.append(str(v[key])) + for val in v.values(): + if isinstance(val, (list, dict)): + walk(val) + walk(payload) + return sorted(set(names)) + + def _doc_name_matches(found_names: list[str], required: str) -> bool: + required_norm = required.strip("/") + required_base = required_norm.split("/")[-1] + for name in found_names: + n = name.strip("/") + if n == required_norm or n.endswith("/" + required_norm) or n.split("/")[-1] == required_base: + return True + if required_norm in n or required_base in n: + return True + return False + + def _preflight_required_files() -> dict[str, Any]: + found_names_set: set[str] = set() + list_docs_args = ( + {"path": "/", "pattern": "*", "recursive": True}, + {"path": ".", "pattern": "*", "recursive": True}, + {"path": "/", "pattern": "*"}, + {"path": ".", "pattern": "*"}, + {"path": "Default_Agent", "pattern": "*"}, + {"path": "quality_gates", "pattern": "*.json"}, + ) + for args in list_docs_args: + try: + payload = _unwrap(_call("list_docs", args), "list_docs") + found_names_set.update(_extract_doc_names(payload)) + except Exception: + continue + found_names_now = sorted(found_names_set) + required_ready = all(_doc_name_matches(found_names_now, path) for path in REQUIRED_FILES) + sidecar_checked = _doc_name_matches(found_names_now, PART1_SOFT_GATE_HANDOFF_FILE) or args.get("path") == "quality_gates" + if required_ready and sidecar_checked: + break + found_names = sorted(found_names_set) + if not found_names: + raise RuntimeError("list_docs returned no recognizable document names for required inputs") + missing = [path for path in REQUIRED_FILES if not _doc_name_matches(found_names, path)] + if missing: + raise RuntimeError(f"Missing required input files by list_docs: {missing}") + return {"found_names": found_names, "required_files": REQUIRED_FILES} + + def _read_required_docs_once() -> dict[str, str]: + payload = _unwrap(_call("read_docs", {"doc_names": REQUIRED_FILES}), "read_docs") + docs: dict[str, str] = {} + + def assign(name: str | None, content: Any) -> None: + if not name: + return + text = content if isinstance(content, str) else json.dumps(content, ensure_ascii=False) + for required in REQUIRED_FILES: + if name == required or name.endswith("/" + required) or name.split("/")[-1] == required.split("/")[-1]: + docs[required] = text + + if isinstance(payload, dict) and isinstance(payload.get("results"), list): + for item in payload["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + content = item.get("content") or item.get("text") or item.get("markdown") or item.get("raw") + assign(str(name) if name else None, content) + elif isinstance(payload, dict): + for required in REQUIRED_FILES: + if required in payload: + assign(required, payload[required]) + elif isinstance(payload, str): + if len(REQUIRED_FILES) == 1: + assign(REQUIRED_FILES[0], payload) + + missing = [name for name in REQUIRED_FILES if name not in docs] + if missing: + # Some Localdocs runtimes return one JSON wrapper inside the text field. + raw = payload if isinstance(payload, str) else json.dumps(payload, ensure_ascii=False) + try: + outer = json.loads(raw) + if isinstance(outer, dict) and isinstance(outer.get("results"), list): + for item in outer["results"]: + if isinstance(item, dict): + name = item.get("name") or item.get("path") or item.get("doc_name") + content = item.get("content") or item.get("text") + assign(str(name) if name else None, content) + except Exception: + raw = "" + missing = [name for name in REQUIRED_FILES if name not in docs] + if missing: + raise RuntimeError(f"read_docs did not return required inputs: {missing}") + for name, content in docs.items(): + if not str(content).strip(): + raise RuntimeError(f"Required input is empty: {name}") + return docs + + def _parse_json_doc(name: str, raw: str) -> Any: + try: + return json.loads(raw.strip()) + except json.JSONDecodeError as exc: + raise RuntimeError(f"Strict JSON parse failed: {name}: {exc.msg} at line {exc.lineno} column {exc.colno}") from exc + + def _items(obj: Any, *keys: str) -> list[Any]: + if isinstance(obj, list): + return obj + if isinstance(obj, dict): + for key in keys + ("items", "evidence_indexed", "documents", "results"): + val = obj.get(key) + if isinstance(val, list): + return val + return [] + + def _extract_keywords(text: str, patterns: list[str]) -> list[str]: + return [p for p in patterns if p in text] + + def _build_meeting_clause_map(meeting_text: str) -> dict[str, Any]: + clauses: list[dict[str, Any]] = [] + current_major: str | None = None + current: dict[str, Any] | None = None + section_seq = 0 + current_section_key = "preamble" + current_section_label = "preamble" + + section_re = re.compile(r"^\s*#?\s*【\s*(.+?)\s*】\s*$") + major_re = re.compile(r"^\s*(\d+)\.\s+(.+?)\s*$") + sub_re = re.compile(r"^\s*([가-힣])\.\s+(.+?)\s*$") + + def flush(end_line: int) -> None: + nonlocal current + if not current: + return + current["line_end"] = end_line + current["text"] = _norm_space(current.get("text", "")) + text = current["text"] + current["tags"] = { + "current_state": _extract_keywords(text, CURRENT_STATE_PATTERNS), + "conflict_pattern": _extract_keywords(text, CONFLICT_PATTERNS), + "succession": _extract_keywords(text, SUCCESSION_PATTERNS), + "mixed_contract": _extract_keywords(text, MIXED_CONTRACT_PATTERNS), + } + current["text_excerpt"] = _short_excerpt(text) + clauses.append(current) + current = None + + lines = meeting_text.splitlines() + for idx, line in enumerate(lines, 1): + stripped = line.strip() + if not stripped or stripped.startswith("|") or stripped.startswith("---"): + continue + m_section = section_re.match(stripped) + m_major = major_re.match(stripped) + m_sub = sub_re.match(stripped) + if m_section: + flush(idx - 1) + section_seq += 1 + current_section_label = _norm_space(m_section.group(1)) + current_section_key = _section_key_from_heading(current_section_label, section_seq) + current_major = None + elif m_major: + flush(idx - 1) + current_major = m_major.group(1) + current = { + "clause_id": f"{current_section_key}:{current_major}", + "local_clause_id": current_major, + "section_key": current_section_key, + "section_label": current_section_label, + "major_no": current_major, + "sub_no": None, + "heading": _norm_space(m_major.group(2)), + "line_start": idx, + "text": _norm_space(m_major.group(2)), + } + elif m_sub and current_major: + flush(idx - 1) + sub_no = m_sub.group(1) + current = { + "clause_id": f"{current_section_key}:{current_major}-{sub_no}", + "local_clause_id": f"{current_major}-{sub_no}", + "section_key": current_section_key, + "section_label": current_section_label, + "major_no": current_major, + "sub_no": sub_no, + "heading": None, + "line_start": idx, + "text": _norm_space(m_sub.group(2)), + } + elif current: + current["text"] = _norm_space(str(current.get("text", "")) + " " + stripped) + flush(len(lines)) + + if not clauses: + raise RuntimeError("client_meeting.md parse produced no numbered meeting clauses") + + by_id: dict[str, Any] = {} + duplicate_clause_ids: list[str] = [] + for clause in clauses: + cid = clause["clause_id"] + if cid in by_id: + duplicate_clause_ids.append(cid) + by_id[cid] = clause + if duplicate_clause_ids: + raise RuntimeError(f"Duplicate meeting clause ids after section normalization: {duplicate_clause_ids}") + return { + "schema_version": "meeting_clause_map.v1", + "clause_order": [c["clause_id"] for c in clauses], + "by_clause_id": by_id, + "sections": _uniq([{"section_key": c["section_key"], "section_label": c["section_label"]} for c in clauses]), + "current_state_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["current_state"]], + "conflict_pattern_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["conflict_pattern"]], + "succession_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["succession"]], + "mixed_contract_clause_ids": [c["clause_id"] for c in clauses if c["tags"]["mixed_contract"]], + } + + def _candidate_id(candidate: dict[str, Any], evidence_index: str, seq: int) -> str: + return str(candidate.get("candidate_id") or candidate.get("candidate_id_proposed") or f"internal_missing_candidate_id::{evidence_index}::{seq:02d}") + + def _evidence_index_from_item(item: dict[str, Any]) -> str | None: + return _text(item.get("evidence_index") or item.get("evidence_index_proposed") or item.get("source_evidence_index")) + + def _build_event_candidate_map(event_obj: Any) -> dict[str, Any]: + items = _items(event_obj) + if not items: + raise RuntimeError("evidence_event_candidates.json parse produced no document-level items") + by_candidate_id: dict[str, Any] = {} + by_evidence_index: dict[str, list[str]] = {} + by_meeting_clause_ref: dict[str, list[str]] = {} + state_candidate_ids: list[str] = [] + succession_candidate_ids: list[str] = [] + mixed_contract_candidate_ids: list[str] = [] + zero_event_evidence_indexes: list[str] = [] + asset_cluster_values: list[str] = [] + malformed_item_refs: list[str] = [] + + for item_seq, item in enumerate(items, 1): + if not isinstance(item, dict): + malformed_item_refs.append(f"item#{item_seq}: non-object") + continue + evidence_index = _evidence_index_from_item(item) + if not evidence_index: + malformed_item_refs.append(f"item#{item_seq}: missing evidence_index") + continue + item_ref = evidence_index + event_candidates_raw = item.get("event_candidates") + has_event_candidates_list = isinstance(event_candidates_raw, list) + has_event_candidates = has_event_candidates_list and len(event_candidates_raw) > 0 + has_zero_event_reason = bool(item.get("zero_event_reason")) + if event_candidates_raw is not None and not has_event_candidates_list: + malformed_item_refs.append(f"{item_ref}: event_candidates must be a list when present") + continue + if not has_event_candidates and not has_zero_event_reason: + malformed_item_refs.append(f"{item_ref}: requires non-empty event_candidates or zero_event_reason") + continue + if item.get("zero_event_reason"): + zero_event_evidence_indexes.append(evidence_index) + item_flags = item.get("doc_semantic_flags") if isinstance(item.get("doc_semantic_flags"), list) else [] + property_cluster_id = _text(item.get("property_cluster_id")) + if property_cluster_id: + asset_cluster_values.append(property_cluster_id) + for asset in item.get("affected_asset_cluster_ids") or []: + if _text(asset): + asset_cluster_values.append(str(asset)) + for seq, cand in enumerate(event_candidates_raw or [], 1): + if not isinstance(cand, dict): + malformed_item_refs.append(f"{item_ref}: event_candidates[{seq}] must be an object") + continue + cid = _candidate_id(cand, evidence_index, seq) + refs = cand.get("meeting_clause_refs") if isinstance(cand.get("meeting_clause_refs"), list) else [] + cand_flags = cand.get("doc_semantic_flags") if isinstance(cand.get("doc_semantic_flags"), list) else [] + candidate_flags = _uniq(item_flags + cand_flags) + record = { + "candidate_id": cid, + "source_evidence_index": _text(cand.get("source_evidence_index") or cand.get("source_evidence_index_proposed")) or evidence_index, + "title": item.get("title"), + "doc_type": item.get("doc_type"), + "event_or_state": cand.get("event_or_state"), + "event_kind": cand.get("event_kind"), + "event_subkind": cand.get("event_subkind"), + "action_type_candidate": cand.get("action_type_candidate"), + "action_summary": cand.get("action_summary"), + "participants": cand.get("participants") or {}, + "object_spec": cand.get("object_spec"), + "amount": cand.get("amount"), + "event_date": cand.get("event_date"), + "event_end_date": cand.get("event_end_date"), + "time_text": cand.get("time_text"), + "time_precision": cand.get("time_precision"), + "current_state_candidate": cand.get("current_state_candidate"), + "succession_candidate": cand.get("succession_candidate"), + "legal_effect_candidate": cand.get("legal_effect_candidate"), + "defense_candidate": cand.get("defense_candidate"), + "commercial_successor_candidates": cand.get("commercial_successor_candidates"), + "meeting_clause_refs": refs, + "actio_relevance_candidates": cand.get("actio_relevance_candidates"), + "identity_signature": cand.get("identity_signature"), + "property_cluster_id": property_cluster_id, + "affected_asset_cluster_ids": item.get("affected_asset_cluster_ids") or [], + "doc_semantic_flags": candidate_flags, + "fraction_ref": cand.get("fraction_ref"), + "confidence": cand.get("confidence"), + } + by_candidate_id[cid] = record + by_evidence_index.setdefault(evidence_index, []).append(cid) + for ref in refs: + by_meeting_clause_ref.setdefault(str(ref), []).append(cid) + if cand.get("event_or_state") == "state" or cand.get("current_state_candidate"): + state_candidate_ids.append(cid) + if cand.get("succession_candidate"): + succession_candidate_ids.append(cid) + if "mixed_contract_doc" in candidate_flags or cand.get("commercial_successor_candidates"): + mixed_contract_candidate_ids.append(cid) + if _text(cand.get("object_spec")): + asset_cluster_values.append(str(cand.get("object_spec"))) + + if malformed_item_refs: + raise RuntimeError(f"Malformed evidence_event_candidates document items: {malformed_item_refs[:20]}") + + return { + "schema_version": "event_candidate_map.v2", + "candidate_count": len(by_candidate_id), + "candidate_id_set": sorted(by_candidate_id.keys()), + "by_candidate_id": by_candidate_id, + "by_evidence_index": {k: _uniq(v) for k, v in by_evidence_index.items()}, + "by_meeting_clause_ref": {k: _uniq(v) for k, v in by_meeting_clause_ref.items()}, + "state_candidate_ids": _uniq(state_candidate_ids), + "succession_candidate_ids": _uniq(succession_candidate_ids), + "mixed_contract_candidate_ids": _uniq(mixed_contract_candidate_ids), + "zero_event_evidence_indexes": _uniq(zero_event_evidence_indexes), + "asset_cluster_values": _uniq([v for v in asset_cluster_values if _text(v)]), + } + + def _component_count(item: dict[str, Any], key: str) -> int: + val = item.get(key) + if isinstance(val, list): + return len(val) + if isinstance(val, dict): + return len(val) + return 1 if val not in (None, "", []) else 0 + + COMPONENT_DETAIL_PROJECTION = { + "loan_components": ["loan_component_id", "principal_amount", "loan_date", "due_date", + "agreed_interest_rate", "agreed_delay_damage_rate", "rate_text", + "rate_source_locator", "interest_accrual_start_hint"], + "secured_debt_components": ["agreed_interest_rate", "agreed_delay_damage_rate", "rate_text", + "rate_source_locator", "due_date"], + "notice_components": ["notice_id_proposed", "notice_type", "drafted_date", "dispatch_date", + "arrival_date", "received_date", "legal_effect_date_candidate", + "requires_receipt_for_effect", "receipt_evidence_refs", + "recipient_admission_text", "sender_candidates", "receiver_candidates"], + "registry_row_components": ["registry_ref", "fraction_ref", "fraction_display", + "fraction_numerator", "fraction_denominator", "receipt_no"], + "schedule_items": ["schedule_ref", "canonical_label_candidate", "usable_in_relief"], + } + + def _component_details(item: dict[str, Any]) -> dict[str, Any]: + details: dict[str, Any] = {} + for key, fields in COMPONENT_DETAIL_PROJECTION.items(): + rows = item.get(key) + if isinstance(rows, dict): + rows = [rows] + if not isinstance(rows, list): + continue + projected = [{field: row.get(field) for field in fields if field in row} for row in rows if isinstance(row, dict)] + projected = [p for p in projected if p] + if projected: + details[key] = projected + return details + + def _build_evidence_authority_map(evidence_obj: Any) -> dict[str, Any]: + items = _items(evidence_obj, "evidence_indexed") + if not items: + raise RuntimeError("evidence_indexed.json parse produced no evidence authority items") + by_evidence_index: dict[str, Any] = {} + by_doc_type: dict[str, list[str]] = {} + by_title_norm: dict[str, list[str]] = {} + semantic_flag_index: dict[str, list[str]] = {} + evidence_index_set: list[str] = [] + malformed_item_refs: list[str] = [] + duplicate_evidence_indexes: list[str] = [] + + component_keys = [ + "contract_components", "valuation_object", "legal_calculation_object", + "legal_calculation_objects", "claim_document_components", + "business_transfer_components", "registry_row_components", + "secured_debt_components", "valuation_rows", "mixed_contract_payment_semantics", + "loan_components", "valuation_matrix", "registry_row_role_map", + "defense_assertions", "admission_components", "defense_components", + "notice_components", "party_assertion_components", "schedule_items", + "registry_object_display", "succession_document_components", + ] + + for item_seq, item in enumerate(items, 1): + if not isinstance(item, dict): + malformed_item_refs.append(f"item#{item_seq}: non-object") + continue + evidence_index = _text(item.get("evidence_index") or item.get("evidence_index_proposed")) + if not evidence_index: + malformed_item_refs.append(f"item#{item_seq}: missing finalized evidence_index") + continue + if evidence_index in by_evidence_index: + duplicate_evidence_indexes.append(evidence_index) + continue + flags = item.get("doc_semantic_flags") if isinstance(item.get("doc_semantic_flags"), list) else [] + component_summary = {key: _component_count(item, key) for key in component_keys} + record = { + "evidence_index": evidence_index, + "doc_uid": item.get("doc_uid"), + "title": item.get("title"), + "title_normalized": item.get("title_normalized") or _norm_space(item.get("title")), + "doc_type": item.get("doc_type"), + "key_facts": item.get("key_facts") or [], + "key_dates": item.get("key_dates") or [], + "key_amounts": item.get("key_amounts") or [], + "key_parties": item.get("key_parties") or [], + "source_pointer": item.get("source_pointer") or {}, + "property_cluster_id": item.get("property_cluster_id"), + "doc_semantic_flags": flags, + "multi_event_registry": item.get("multi_event_registry"), + "component_summary": component_summary, + "component_details": _component_details(item), + } + by_evidence_index[evidence_index] = record + evidence_index_set.append(evidence_index) + by_doc_type.setdefault(str(item.get("doc_type") or "unknown"), []).append(evidence_index) + title_key = _norm_key(record["title_normalized"]) + if title_key: + by_title_norm.setdefault(title_key, []).append(evidence_index) + for flag in flags: + semantic_flag_index.setdefault(str(flag), []).append(evidence_index) + + if malformed_item_refs: + raise RuntimeError(f"Malformed evidence_indexed authority items: {malformed_item_refs[:20]}") + if duplicate_evidence_indexes: + raise RuntimeError(f"Duplicate evidence_index values in evidence_indexed.json: {_uniq(duplicate_evidence_indexes)}") + + return { + "schema_version": "evidence_authority_map.v2", + "evidence_index_set": _sort_evidence_indexes(_uniq(evidence_index_set)), + "by_evidence_index": by_evidence_index, + "by_doc_type": {k: _sort_evidence_indexes(_uniq(v)) for k, v in by_doc_type.items()}, + "by_title_norm": {k: _sort_evidence_indexes(_uniq(v)) for k, v in by_title_norm.items()}, + "semantic_flag_index": {k: _sort_evidence_indexes(_uniq(v)) for k, v in semantic_flag_index.items()}, + } + + def _sort_evidence_indexes(values: list[str]) -> list[str]: + def key(v: str) -> tuple[int, str]: + m = re.search(r"E-(\d+)", str(v)) + return (int(m.group(1)) if m else 10**9, str(v)) + return sorted(values, key=key) + + def _split_examples(value: str) -> list[str]: + parts = re.split(r"[,,、]| / |/|ㆍ|·", value or "") + return [_norm_space(p) for p in parts if _norm_space(p)] + + def _build_juristic_act_map(markdown: str) -> dict[str, Any]: + categories: dict[str, Any] = {} + example_to_gubun: dict[str, list[str]] = {} + for line in markdown.splitlines(): + if "|" not in line: + continue + cells = [_norm_space(c) for c in line.strip().strip("|").split("|")] + if len(cells) < 3: + continue + if cells[0] in ("구분", "---") or set(cells[0]) <= {"-"}: + continue + gubun, content, examples = cells[0], cells[1], cells[2] + if not gubun or gubun.startswith("-"): + continue + example_list = _split_examples(examples) + categories[gubun] = {"content": content, "examples": example_list} + for ex in example_list: + example_to_gubun.setdefault(_norm_key(ex), []).append(gubun) + + axis_groups = { + "structure": ["단독행위", "계약행위", "합동행위"], + "form": ["요식(要式)행위", "불요식행위"], + "time_effect": ["생전 행위", "사후(또는 사인) 행위"], + "legal_object": ["채권행위", "물권행위", "준물권행위"], + "contribution": ["출연행위", "비출연행위"], + "dependency": ["주(主)된 행위", "종(從)된 행위", "유인 행위", "무인 행위"], + "defect_or_condition": ["무효 행위", "취소가능한 법률 행위", "조건부 법률행위", "기한부 법률행위"], + "controversy": ["견해대립있는 행위"], + } + + return { + "schema_version": "juristic_act_taxonomy_map.v1", + "categories": categories, + "example_to_gubun": {k: _uniq(v) for k, v in example_to_gubun.items()}, + "axis_groups": axis_groups, + "source_digest_sha256": _stable_hash(markdown), + } + + def _validate_juristic_act_map(juristic_map: dict[str, Any]) -> None: + categories = juristic_map.get("categories") or {} + example_to_gubun = juristic_map.get("example_to_gubun") or {} + required = ["단독행위", "계약행위", "채권행위", "물권행위"] + missing = [name for name in required if name not in categories] + if not categories or not example_to_gubun: + raise RuntimeError("JuristicAct taxonomy parse produced empty categories or examples") + if missing: + raise RuntimeError(f"JuristicAct taxonomy missing required categories: {missing}") + + def _collect_asset_clusters(meeting_map: dict[str, Any], event_map: dict[str, Any], evidence_map: dict[str, Any]) -> list[str]: + values: list[str] = [] + asset_hint_re = re.compile(r"([가-힣A-Za-z0-9·ㆍ() \-]{1,30}(?:대지|건물|상가|빌라|임야|부동산|지분|영업|상호|채권|대금))") + for clause in meeting_map.get("by_clause_id", {}).values(): + text = str(clause.get("text") or "") + values.extend([_norm_space(m.group(1)) for m in asset_hint_re.finditer(text)]) + values.extend(event_map.get("asset_cluster_values") or []) + for ev in evidence_map.get("by_evidence_index", {}).values(): + if _text(ev.get("property_cluster_id")): + values.append(str(ev.get("property_cluster_id"))) + for fact in ev.get("key_facts") or []: + values.extend([_norm_space(m.group(1)) for m in asset_hint_re.finditer(str(fact))]) + return _uniq([v for v in values if _text(v)]) + + def _build_global_hazard_seed(meeting_map: dict[str, Any], event_map: dict[str, Any], evidence_map: dict[str, Any]) -> dict[str, Any]: + evidence_index_set = evidence_map.get("evidence_index_set") or [] + current_state_ids = event_map.get("state_candidate_ids") or [] + succession_candidate_ids = event_map.get("succession_candidate_ids") or [] + mixed_contract_candidate_ids = event_map.get("mixed_contract_candidate_ids") or [] + + def meeting_expression_records(clause_ids: list[str], tag_key: str) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + by_clause = meeting_map.get("by_clause_id", {}) + for cid in clause_ids: + clause = by_clause.get(cid) or {} + matched = (clause.get("tags") or {}).get(tag_key) or [] + out.append({ + "source": "client_meeting.md", + "clause_id": cid, + "local_clause_id": clause.get("local_clause_id"), + "section_key": clause.get("section_key"), + "matched_patterns": matched, + "text_excerpt": clause.get("text_excerpt") or _short_excerpt(clause.get("text")), + }) + return out + + def event_expression_records(candidate_ids: list[str]) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + by_candidate = event_map.get("by_candidate_id", {}) + for cid in candidate_ids: + cand = by_candidate.get(cid) or {} + text = " ".join(str(cand.get(k) or "") for k in ("action_summary", "event_kind", "event_subkind", "identity_signature")) + matched = _uniq( + _extract_keywords(text, CURRENT_STATE_PATTERNS) + + _extract_keywords(text, CONFLICT_PATTERNS) + + _extract_keywords(text, SUCCESSION_PATTERNS) + + _extract_keywords(text, MIXED_CONTRACT_PATTERNS) + ) + out.append({ + "source": "evidence_event_candidates.json", + "candidate_id": cid, + "source_evidence_index": cand.get("source_evidence_index"), + "event_or_state": cand.get("event_or_state"), + "event_kind": cand.get("event_kind"), + "matched_patterns": matched, + "text_excerpt": _short_excerpt(cand.get("action_summary") or cand.get("identity_signature")), + }) + return out + + conflict_candidate_ids: list[str] = [] + for cid, cand in event_map.get("by_candidate_id", {}).items(): + combined = " ".join( + str(cand.get(k) or "") + for k in ("action_summary", "event_kind", "event_subkind", "identity_signature") + ) + if _extract_keywords(combined, CONFLICT_PATTERNS): + conflict_candidate_ids.append(cid) + + succession_evidence_indexes = [] + mixed_contract_evidence_indexes = [] + for eidx, ev in evidence_map.get("by_evidence_index", {}).items(): + flags = ev.get("doc_semantic_flags") or [] + title_and_facts = " ".join([str(ev.get("title") or "")] + [str(x) for x in ev.get("key_facts") or []]) + if "succession_doc" in flags or _extract_keywords(title_and_facts, SUCCESSION_PATTERNS): + succession_evidence_indexes.append(eidx) + if "mixed_contract_doc" in flags or _extract_keywords(title_and_facts, MIXED_CONTRACT_PATTERNS): + mixed_contract_evidence_indexes.append(eidx) + + return { + "schema_version": "global_hazard_seed.v1", + "meeting_clause_ids": meeting_map.get("clause_order") or [], + "current_state_expressions": { + "meeting_clause_ids": meeting_map.get("current_state_clause_ids") or [], + "event_candidate_ids": current_state_ids, + "meeting_expressions": meeting_expression_records(meeting_map.get("current_state_clause_ids") or [], "current_state"), + "event_expressions": event_expression_records(current_state_ids), + }, + "conflict_pattern_expressions": { + "meeting_clause_ids": meeting_map.get("conflict_pattern_clause_ids") or [], + "event_candidate_ids": _uniq(conflict_candidate_ids), + "meeting_expressions": meeting_expression_records(meeting_map.get("conflict_pattern_clause_ids") or [], "conflict_pattern"), + "event_expressions": event_expression_records(_uniq(conflict_candidate_ids)), + }, + "asset_clusters": _collect_asset_clusters(meeting_map, event_map, evidence_map), + "succession_related_candidates": { + "meeting_clause_ids": meeting_map.get("succession_clause_ids") or [], + "event_candidate_ids": succession_candidate_ids, + "evidence_indexes": _sort_evidence_indexes(_uniq(succession_evidence_indexes)), + "meeting_expressions": meeting_expression_records(meeting_map.get("succession_clause_ids") or [], "succession"), + "event_expressions": event_expression_records(succession_candidate_ids), + }, + "mixed_contract_candidates": { + "meeting_clause_ids": meeting_map.get("mixed_contract_clause_ids") or [], + "event_candidate_ids": mixed_contract_candidate_ids, + "evidence_indexes": _sort_evidence_indexes(_uniq(mixed_contract_evidence_indexes)), + "meeting_expressions": meeting_expression_records(meeting_map.get("mixed_contract_clause_ids") or [], "mixed_contract"), + "event_expressions": event_expression_records(mixed_contract_candidate_ids), + }, + "evidence_index_set": evidence_index_set, + } + + def _read_optional_doc_once(doc_name: str) -> str | None: + try: + payload = _unwrap(_call("read_docs", {"doc_names": [doc_name]}), "read_docs_optional") + except Exception: + return None + if isinstance(payload, dict) and isinstance(payload.get("results"), list): + for item in payload["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + content = item.get("content") or item.get("text") or item.get("markdown") or item.get("raw") + if name and (str(name) == doc_name or str(name).endswith("/" + doc_name) or str(name).split("/")[-1] == doc_name.split("/")[-1]): + return content if isinstance(content, str) else json.dumps(content, ensure_ascii=False) + if isinstance(payload, dict) and doc_name in payload: + val = payload[doc_name] + return val if isinstance(val, str) else json.dumps(val, ensure_ascii=False) + if isinstance(payload, str) and payload.strip(): + return payload + return None + + def _compact_part1_soft_gate_handoff(existence_check: dict[str, Any], input_digests: dict[str, str], meeting_map: dict[str, Any], event_map: dict[str, Any], evidence_map: dict[str, Any]) -> dict[str, Any]: + found_names = existence_check.get("found_names") or [] + base = { + "schema_version": "stage1_part1_soft_gate_handoff.v1", + "status": "ABSENT", + "stage2_auto_progression_allowed": True, + "review_gate_count": 0, + "highest_review_severity": "PASS", + "review_items_compact": [], + "routing_index": {}, + "lawyer_workflow_queues": {}, + "dropped_review_items": [], + "usage_rules": { + "not_factual_authority": True, + "must_not_create_fact_without_stage_a_source": True, + "must_preserve_as_needs_review_if_used": True, + "must_not_override_stage_e_final_progression": True, + "must_not_override_stage_f_single_writer_gate": True, + }, + } + if not _doc_name_matches(found_names, PART1_SOFT_GATE_HANDOFF_FILE): + return base + raw = _read_optional_doc_once(PART1_SOFT_GATE_HANDOFF_FILE) + if not raw: + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": "sidecar listed but unreadable"}) + return base + try: + sidecar = json.loads(raw.strip()) + except Exception as exc: + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": f"sidecar JSON parse failed: {exc}"}) + return base + if not isinstance(sidecar, dict) or sidecar.get("schema_version") != "stage1_part1_soft_gate_handoff.v1": + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": "wrong or missing schema_version"}) + return base + if sidecar.get("handoff_status") == "BLOCKED" or sidecar.get("stage2_auto_progression_allowed") is False: + raise RuntimeError("Part 1 soft gate handoff is BLOCKED; Part 2-6 cannot proceed") + guard = sidecar.get("digest_guard") if isinstance(sidecar.get("digest_guard"), dict) else {} + invalid_guard_fields = [] + stale = [] + for guard_key, doc_name in [("evidence_indexed_sha256", "evidence_indexed.json"), ("evidence_event_candidates_sha256", "evidence_event_candidates.json")]: + val = guard.get(guard_key) + if not isinstance(val, str) or not re.fullmatch(r"[0-9a-f]{64}", val): + invalid_guard_fields.append(guard_key) + elif val != input_digests.get(doc_name): + stale.append(guard_key) + if invalid_guard_fields: + base["status"] = "IGNORED_INVALID" + base["dropped_review_items"].append({"reason": "digest_guard missing real sha256", "fields": invalid_guard_fields}) + return base + if stale: + base["status"] = "IGNORED_STALE" + base["dropped_review_items"].append({"reason": "digest_guard mismatch", "fields": stale}) + return base + evidence_set = set(evidence_map.get("evidence_index_set") or []) + event_set = set(event_map.get("candidate_id_set") or []) + meeting_set = set(meeting_map.get("clause_order") or []) + compact = [] + dropped = [] + for idx, item in enumerate(sidecar.get("review_items") or []): + if len(compact) >= 40: + dropped.append({"index": idx, "reason": "review_items_compact max 40 reached"}) + continue + if not isinstance(item, dict): + dropped.append({"index": idx, "reason": "non-object review item"}) + continue + refs = item.get("source_refs") if isinstance(item.get("source_refs"), dict) else {} + bad_e = [x for x in refs.get("evidence_indexes", []) if x not in evidence_set] + bad_ev = [x for x in refs.get("event_candidate_ids", []) if x not in event_set] + bad_m = [x for x in refs.get("meeting_clause_refs", []) if x not in meeting_set] + if bad_e or bad_ev or bad_m: + dropped.append({"review_id": item.get("review_id"), "reason": "source_refs membership failed", "bad_evidence_indexes": bad_e, "bad_event_candidate_ids": bad_ev, "bad_meeting_clause_refs": bad_m}) + continue + compact.append({ + "review_id": item.get("review_id"), + "severity": item.get("severity"), + "issue_type": item.get("issue_type"), + "lawyer_workflow_role": item.get("lawyer_workflow_role"), + "domain_hints": item.get("domain_hints") or [], + "target_stage_hints": item.get("target_stage_hints") or [], + "target_task_hints": item.get("target_task_hints") or [], + "source_refs": refs, + "candidate_use_policy": item.get("candidate_use_policy"), + "review_instruction": item.get("review_instruction"), + "negative_instruction": item.get("negative_instruction"), + }) + summary = sidecar.get("review_gate_summary") if isinstance(sidecar.get("review_gate_summary"), dict) else {} + base.update({ + "status": sidecar.get("handoff_status") if sidecar.get("handoff_status") in {"READY_WITH_REVIEW", "READY_NO_REVIEW"} else "READY_WITH_REVIEW", + "stage2_auto_progression_allowed": sidecar.get("stage2_auto_progression_allowed") is not False, + "review_gate_count": len(compact), + "highest_review_severity": summary.get("highest_review_severity") or ("HARD_WARNING" if any(i.get("severity") == "HARD_WARNING" for i in compact) else ("SOFT_WARNING" if compact else "PASS")), + "review_items_compact": compact, + "routing_index": sidecar.get("routing_index") if isinstance(sidecar.get("routing_index"), dict) else {}, + "lawyer_workflow_queues": sidecar.get("lawyer_workflow_queues") if isinstance(sidecar.get("lawyer_workflow_queues"), dict) else {}, + "dropped_review_items": dropped, + }) + return base + + + ALL_DOMAIN_TERMS = {config["domain_id"]: config["terms"] for config in DOMAIN_CONFIGS} + PRIMARY_TERMS = { + "B1_Money_Successor": ["대여금", "차용", "물품대금", "공사대금", "원금", "이자", "지연손해금", "변제기", "일부변제", "미변제", "채무승인", "지급각서", "상호속용", "영업양도", "양수인", "채무불승계", "business_transfer", "loan", "admission"], + "B2_Secured_Registry": ["근저당", "저당", "담보", "피담보", "변제충당", "말소", "경매", "배당", "registry", "mortgage", "secured", "auction", "dividend"], + "B3_Land_Valuation": ["토지", "건물", "점유", "수익", "인도", "차임", "감정", "시가", "valuation", "possession", "land", "building"], + "B4_Actio": ["사해행위", "피보전채권", "채무초과", "책임재산", "공동담보", "처분행위", "수익자", "전득자", "가액배상", "actio", "fraudulent"], + "B5_Succession_Notice_Lien_Asset_Defense": ["사망", "상속", "상속포기", "내용증명", "유치권", "별지목록", "동시이행", "상계", "succession", "notice", "lien", "defense"], + } + + MEETING_FIELDS = [ + "clause_id", "local_clause_id", "section_key", "section_label", "major_no", + "sub_no", "heading", "line_start", "line_end", "text_excerpt", "tags", + ] + EVENT_FIELDS = ["candidate_id", "source_evidence_index", "title", "doc_type", "event_or_state", "event_kind", "event_subkind", "action_type_candidate", "action_summary", "participants", "object_spec", "amount", "event_date", "event_end_date", "time_text", "time_precision", "current_state_candidate", "succession_candidate", "legal_effect_candidate", "defense_candidate", "commercial_successor_candidates", "meeting_clause_refs", "actio_relevance_candidates", "identity_signature", "property_cluster_id", "affected_asset_cluster_ids", "doc_semantic_flags", "confidence", "fraction_ref"] + EVIDENCE_FIELDS = ["evidence_index", "doc_uid", "title", "title_normalized", "doc_type", "key_facts", "key_dates", "key_amounts", "key_parties", "source_pointer", "property_cluster_id", "doc_semantic_flags", "multi_event_registry", "component_summary", "component_details"] + + def _parse_json(raw: str, label: str) -> dict[str, Any]: + text = (raw or "").strip() + if not text or text == "null": + raise RuntimeError(f"{label}: empty injected JSON") + decoder = json.JSONDecoder() + obj, idx = decoder.raw_decode(text) + if text[idx:].strip(): + raise RuntimeError(f"{label}: trailing data after JSON object") + if not isinstance(obj, dict): + raise RuntimeError(f"{label}: root must be object") + return obj + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _uniq(values: list[Any]) -> list[Any]: + seen: set[str] = set() + out: list[Any] = [] + for value in values: + marker = json.dumps(value, ensure_ascii=False, sort_keys=True) if isinstance(value, (dict, list)) else str(value) + if marker not in seen: + seen.add(marker) + out.append(value) + return out + + def _natural_key(value: Any) -> list[Any]: + return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", str(value))] + + def _sorted_ids(values: set[str] | list[str]) -> list[str]: + return sorted([str(v) for v in values if str(v)], key=_natural_key) + + def _blob(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True).lower() + + def _matches(value: Any, terms: list[str]) -> bool: + haystack = _blob(value) + return any(str(term).lower() in haystack for term in terms if str(term).strip()) + + def _copy_fields(source: dict[str, Any], fields: list[str]) -> dict[str, Any]: + out: dict[str, Any] = {} + for field in fields: + if field in source: + out[field] = source.get(field) + return out + + def _project_meeting(record: dict[str, Any]) -> dict[str, Any]: + out = _copy_fields(record, MEETING_FIELDS) + if not out.get("text_excerpt") and record.get("text"): + out["text_excerpt"] = str(record.get("text"))[:240] + return out + + def _project_event(record: dict[str, Any]) -> dict[str, Any]: + return _copy_fields(record, EVENT_FIELDS) + + def _project_evidence(record: dict[str, Any]) -> dict[str, Any]: + return _copy_fields(record, EVIDENCE_FIELDS) + + def _accept_stage_a(stage_a: dict[str, Any]) -> None: + checks = [ + ("stage_a_context.schema_version", stage_a.get("schema_version") == "task_c_bo_stage_a_context.v1"), + ("stage_a_context.status", stage_a.get("status") == "READY"), + ("meeting_clause_map.schema_version", (stage_a.get("meeting_clause_map") or {}).get("schema_version") == "meeting_clause_map.v1"), + ("event_candidate_map.schema_version", (stage_a.get("event_candidate_map") or {}).get("schema_version") == "event_candidate_map.v2"), + ("evidence_authority_map.schema_version", (stage_a.get("evidence_authority_map") or {}).get("schema_version") == "evidence_authority_map.v2"), + ("juristic_act_taxonomy_map.schema_version", (stage_a.get("juristic_act_taxonomy_map") or {}).get("schema_version") == "juristic_act_taxonomy_map.v1"), + ("global_hazard_seed.schema_version", (stage_a.get("global_hazard_seed") or {}).get("schema_version") == "global_hazard_seed.v1"), + ("stage_b_worker_contract.context_is_read_only", (stage_a.get("stage_b_worker_contract") or {}).get("context_is_read_only") is True), + ("stage_b_worker_contract.bo_id_finalization_allowed", (stage_a.get("stage_b_worker_contract") or {}).get("bo_id_finalization_allowed") is False), + ("stage_b_worker_contract.candidate_generation_allowed", (stage_a.get("stage_b_worker_contract") or {}).get("candidate_generation_allowed") is True), + ] + failed = [name for name, ok in checks if not ok] + if failed: + raise RuntimeError(f"Stage A acceptance failed: {failed}") + + def _matched_domains(record: dict[str, Any]) -> list[str]: + return [domain_id for domain_id, terms in ALL_DOMAIN_TERMS.items() if _matches(record, terms)] + + def _select(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + meeting_map = stage_a.get("meeting_clause_map") or {} + event_map = stage_a.get("event_candidate_map") or {} + evidence_map = stage_a.get("evidence_authority_map") or {} + by_clause = meeting_map.get("by_clause_id") or {} + by_event = event_map.get("by_candidate_id") or {} + by_evidence = evidence_map.get("by_evidence_index") or {} + + clause_ids: set[str] = set() + event_ids: set[str] = set() + evidence_ids: set[str] = set() + + for key in config["meeting_sources"]: + for value in _as_list(meeting_map.get(key)): + value_id = str(value) + if _matches(by_clause.get(value_id) or {}, config["terms"]): + clause_ids.add(value_id) + for key in config["event_sources"]: + for value in _as_list(event_map.get(key)): + value_id = str(value) + if _matches(by_event.get(value_id) or {}, config["terms"]): + event_ids.add(value_id) + + for clause_id, record in by_clause.items(): + if _matches(record, config["terms"]): + clause_ids.add(str(clause_id)) + for event_id, record in by_event.items(): + if _matches(record, config["terms"]): + event_ids.add(str(event_id)) + for evidence_id, record in by_evidence.items(): + if _matches(record, config["terms"]): + evidence_ids.add(str(evidence_id)) + + for _ in range(2): + for evidence_id in list(evidence_ids): + event_ids.update(str(v) for v in _as_list((event_map.get("by_evidence_index") or {}).get(evidence_id))) + for clause_id in list(clause_ids): + event_ids.update(str(v) for v in _as_list((event_map.get("by_meeting_clause_ref") or {}).get(clause_id))) + for event_id in list(event_ids): + event = by_event.get(event_id) or {} + source_evidence = event.get("source_evidence_index") + if source_evidence: + evidence_ids.add(str(source_evidence)) + for ref in _as_list(event.get("meeting_clause_refs")): + clause_ids.add(str(ref)) + + clause_ids = {v for v in clause_ids if v in by_clause} + event_ids = {v for v in event_ids if v in by_event} + evidence_ids = {v for v in evidence_ids if v in by_evidence} + + selected_meetings = [_project_meeting(by_clause[v]) for v in _sorted_ids(clause_ids)] + selected_events = [_project_event(by_event[v]) for v in _sorted_ids(event_ids)] + selected_evidence = [_project_evidence(by_evidence[v]) for v in _sorted_ids(evidence_ids)] + + shared: list[dict[str, Any]] = [] + for item_type, id_values, source in [ + ("meeting_clause", clause_ids, by_clause), + ("event_candidate", event_ids, by_event), + ("evidence", evidence_ids, by_evidence), + ]: + for item_id in _sorted_ids(id_values): + domains = _matched_domains(source.get(item_id) or {}) + if len(domains) > 1: + shared.append({"item_type": item_type, "id": item_id, "matched_domain_ids": domains}) + + return { + "meeting_clause_ids": _sorted_ids(clause_ids), + "event_candidate_ids": _sorted_ids(event_ids), + "evidence_indexes": _sorted_ids(evidence_ids), + "selected_meetings": selected_meetings, + "selected_events": selected_events, + "selected_evidence": selected_evidence, + "shared": shared, + } + + def _taxonomy_subset(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + taxonomy = stage_a.get("juristic_act_taxonomy_map") or {} + axes = taxonomy.get("axis_groups") or {} + return { + "schema_version": taxonomy.get("schema_version"), + "categories": taxonomy.get("categories") or {}, + "example_to_gubun": taxonomy.get("example_to_gubun") or {}, + "axis_groups": {key: axes.get(key) for key in config["taxonomy_axes"] if key in axes}, + "source_digest_sha256": taxonomy.get("source_digest_sha256"), + } + + def _hazard_subset(stage_a: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + hazard = stage_a.get("global_hazard_seed") or {} + return {key: hazard.get(key) for key in config["hazard_keys"] if key in hazard} + + def _part1_review(stage_a: dict[str, Any], selected: dict[str, Any], config: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]: + handoff = stage_a.get("part1_soft_gate_handoff") or {} + guard = { + "schema_version": handoff.get("schema_version"), + "status": handoff.get("status"), + "stage2_auto_progression_allowed": handoff.get("stage2_auto_progression_allowed"), + "usage_rules": handoff.get("usage_rules") or {}, + } + selected_e = set(selected["evidence_indexes"]) + selected_ev = set(selected["event_candidate_ids"]) + selected_m = set(selected["meeting_clause_ids"]) + relevant: list[dict[str, Any]] = [] + for item in _as_list(handoff.get("review_items_compact")): + refs = item.get("source_refs") if isinstance(item, dict) else {} + domain_text = _blob({ + "domain_hints": item.get("domain_hints"), + "target_task_hints": item.get("target_task_hints"), + "issue_type": item.get("issue_type"), + "review_instruction": item.get("review_instruction"), + }) + ref_hit = bool( + selected_e.intersection(str(v) for v in _as_list(refs.get("evidence_indexes"))) + or selected_ev.intersection(str(v) for v in _as_list(refs.get("event_candidate_ids"))) + or selected_m.intersection(str(v) for v in _as_list(refs.get("meeting_clause_refs"))) + ) + if ref_hit or any(str(term).lower() in domain_text for term in config["terms"]): + relevant.append(item) + return guard, relevant + + + def _build_stage_a_context() -> dict[str, Any]: + existence_check = _preflight_required_files() + docs = _read_required_docs_once() + event_candidates = _parse_json_doc("evidence_event_candidates.json", docs["evidence_event_candidates.json"]) + evidence_indexed = _parse_json_doc("evidence_indexed.json", docs["evidence_indexed.json"]) + meeting_clause_map = _build_meeting_clause_map(docs["client_meeting.md"]) + event_candidate_map = _build_event_candidate_map(event_candidates) + evidence_authority_map = _build_evidence_authority_map(evidence_indexed) + juristic_act_taxonomy_map = _build_juristic_act_map(docs["Default_Agent/Juristic_Act.md"]) + _validate_juristic_act_map(juristic_act_taxonomy_map) + if not evidence_authority_map["evidence_index_set"]: + raise RuntimeError("No finalized evidence_index values available") + input_digests = {name: _sha(docs[name]) for name in REQUIRED_FILES} + part1_handoff = _compact_part1_soft_gate_handoff( + existence_check, input_digests, meeting_clause_map, event_candidate_map, evidence_authority_map + ) + return { + "schema_version": "task_c_bo_stage_a_context.v1", + "stage": "Task_C_BO.Stage_A", + "status": "READY", + "created_at_utc": datetime.now(timezone.utc).isoformat(), + "allowed_input_files": REQUIRED_FILES, + "forbidden_files_attested": ["evidence_all.json", "BO.json", "actio_case_signals.json", "case_liability_signals.json", "legal_effect_signals.json"], + "file_io_policy": { + "list_docs_called_for": REQUIRED_FILES, + "read_docs_called_once_for": REQUIRED_FILES, + "raw_evidence_all_read": False, + }, + "existence_check": existence_check, + "input_digests_sha256": input_digests, + "meeting_clause_map": meeting_clause_map, + "event_candidate_map": event_candidate_map, + "evidence_authority_map": evidence_authority_map, + "juristic_act_taxonomy_map": juristic_act_taxonomy_map, + "global_hazard_seed": _build_global_hazard_seed(meeting_clause_map, event_candidate_map, evidence_authority_map), + "part1_soft_gate_handoff": part1_handoff, + "stage_b_worker_contract": { + "context_is_read_only": True, + "bo_id_finalization_allowed": False, + "candidate_generation_allowed": True, + "must_use_evidence_index_set": evidence_authority_map["evidence_index_set"], + }, + } + + def _tier_ids(selected: dict[str, Any], config: dict[str, Any]) -> dict[str, list[str]]: + primary: list[str] = [] + support: list[str] = [] + review_only: list[str] = [] + records = [ + ("meeting", "clause_id", selected["selected_meetings"]), + ("event", "candidate_id", selected["selected_events"]), + ("evidence", "evidence_index", selected["selected_evidence"]), + ] + primary_terms = PRIMARY_TERMS[config["domain_id"]] + for kind, id_key, items in records: + for item in items: + item_id = str(item.get(id_key) or "") + if kind == "meeting": + direct_value = {key: item.get(key) for key in ("heading", "text_excerpt", "tags")} + elif kind == "event": + direct_value = {key: item.get(key) for key in ("event_kind", "event_subkind", "action_summary", "object_spec", "participants", "amount", "fraction_ref", "doc_semantic_flags")} + else: + direct_value = {key: item.get(key) for key in ("title", "doc_type", "key_facts", "key_dates", "key_amounts", "key_parties", "doc_semantic_flags", "component_summary", "component_details")} + direct_blob = _blob(direct_value) + strong_hits = [term for term in primary_terms if str(term).lower() in direct_blob] + broad_hits = [term for term in config["terms"] if str(term).lower() in direct_blob] + if strong_hits: + primary.append(item_id) + elif broad_hits: + review_only.append(item_id) + else: + support.append(item_id) + return {"primary_ids": _uniq(primary), "support_ids": _uniq(support), "review_only_ids": _uniq(review_only)} + + def _tier_project(items: list[dict[str, Any]], id_key: str, kind: str, tiers: dict[str, list[str]], config: dict[str, Any]) -> list[dict[str, Any]]: + primary = set(tiers["primary_ids"]) + review = set(tiers["review_only_ids"]) + projected: list[dict[str, Any]] = [] + support_fields = { + "meeting": ["clause_id", "section_key", "heading", "text_excerpt", "tags"], + "event": ["candidate_id", "source_evidence_index", "event_or_state", "event_kind", "event_subkind", "action_summary", "participants", "object_spec", "amount", "event_date", "time_text", "meeting_clause_refs", "fraction_ref"], + "evidence": ["evidence_index", "title", "doc_type", "key_dates", "key_amounts", "key_parties", "source_pointer", "doc_semantic_flags", "component_summary"], + } + for item in items: + item_id = str(item.get(id_key) or "") + if item_id in primary: + projected.append({**item, "routing_tier": "primary"}) + continue + if item_id in review: + projected.append({ + id_key: item_id, + "routing_tier": "review_only", + "source_type": kind, + "match_reason": [term for term in config["terms"] if str(term).lower() in _blob(item)][:4], + **({"source_evidence_index": item.get("source_evidence_index")} if kind == "event" else {}), + }) + continue + projected.append({ + **{key: item.get(key) for key in support_fields[kind] if key in item}, + "routing_tier": "support", + }) + return projected + + def _build_slice(stage_a: dict[str, Any], config: dict[str, Any], run_fingerprint: str) -> dict[str, Any]: + _accept_stage_a(stage_a) + selected = _select(stage_a, config) + all_clauses = _as_dict(_as_dict(stage_a.get("meeting_clause_map")).get("by_clause_id")) + selected_clause_ids = {str(item.get("clause_id")) for item in selected["selected_meetings"]} + for clause_id in _as_list(_as_dict(stage_a.get("meeting_clause_map")).get("clause_order")): + if str(clause_id) in selected_clause_ids: + continue + record = _as_dict(all_clauses.get(str(clause_id))) + selected["selected_meetings"].append(_project_meeting(record)) + selected["meeting_clause_ids"].append(str(clause_id)) + selected["meeting_clause_ids"] = _sorted_ids(set(selected["meeting_clause_ids"])) + part1_guard, part1_items = _part1_review(stage_a, selected, config) + tiers = _tier_ids(selected, config) + selected_meetings = _tier_project(selected["selected_meetings"], "clause_id", "meeting", tiers, config) + selected_events = _tier_project(selected["selected_events"], "candidate_id", "event", tiers, config) + selected_evidence = _tier_project(selected["selected_evidence"], "evidence_index", "evidence", tiers, config) + meeting_map = stage_a["meeting_clause_map"] + event_map = stage_a["event_candidate_map"] + evidence_map = stage_a["evidence_authority_map"] + slice_body = { + "schema_version": "task_c_bo_stage_b_domain_slice.v1", + "router_status": "READY", + "domain_id": config["domain_id"], + "domain_label": config["domain_label"], + "allowed_legal_effect_bo_types": config["allowed_legal_effect_bo_types"], + "source_stage_a_schema_version": stage_a["schema_version"], + "source_stage_a_status": stage_a["status"], + "source_stage_a_created_at_utc": stage_a["created_at_utc"], + "run_fingerprint": run_fingerprint, + "input_digests_sha256": stage_a["input_digests_sha256"], + "stage_b_worker_contract": stage_a["stage_b_worker_contract"], + "source_universe": { + "meeting_clause_ids": meeting_map.get("clause_order") or [], + "event_candidate_ids": event_map.get("candidate_id_set") or [], + "evidence_indexes": evidence_map.get("evidence_index_set") or [], + }, + "selected_meeting_clauses": selected_meetings, + "selected_event_candidates": selected_events, + "selected_evidence_authority": selected_evidence, + "juristic_act_taxonomy_subset": _taxonomy_subset(stage_a, config), + "hazard_subset": _hazard_subset(stage_a, config), + "part1_review_guard": part1_guard, + "part1_review_items_relevant": part1_items, + "shared_or_ambiguous_items": selected["shared"], + "routing_audit": { + **tiers, + "activation_reasons": ["deterministic domain terms", "source-reference closure"], + "source_count": { + "meeting": len(selected_meetings), + "event": len(selected_events), + "evidence": len(selected_evidence), + }, + "broadness_ratio": round(len(tiers["review_only_ids"]) / max(1, sum(len(v) for v in tiers.values())), 4), + }, + } + slice_body["routing_audit"]["slice_bytes"] = len(_canonical(slice_body).encode("utf-8")) + slice_body["slice_digest_sha256"] = _sha(slice_body) + return {"stage_b_domain_slice": slice_body} + + def main() -> None: + _init("task-c-bo-a0-context-domain-slice-compiler") + stage_a = _build_stage_a_context() + run_fingerprint = _sha(stage_a["input_digests_sha256"]) + stage_a["run_fingerprint"] = run_fingerprint + slices = {config["domain_id"]: _build_slice(stage_a, config, run_fingerprint) for config in DOMAIN_CONFIGS} + domain_paths = {domain_id: f"stage1_tmp/task_c_bo/domain_slices/{domain_id}.json" for domain_id in slices} + domain_digests = {domain_id: root["stage_b_domain_slice"]["slice_digest_sha256"] for domain_id, root in slices.items()} + domain_sources = { + domain_id: { + "meeting_clause_ids": root["stage_b_domain_slice"]["routing_audit"]["primary_ids"] + root["stage_b_domain_slice"]["routing_audit"]["support_ids"] + root["stage_b_domain_slice"]["routing_audit"]["review_only_ids"], + "event_candidate_ids": [str(item.get("candidate_id")) for item in root["stage_b_domain_slice"]["selected_event_candidates"] if item.get("candidate_id")], + "evidence_indexes": [str(item.get("evidence_index")) for item in root["stage_b_domain_slice"]["selected_evidence_authority"] if item.get("evidence_index")], + "meeting_clause_refs": [str(item.get("clause_id")) for item in root["stage_b_domain_slice"]["selected_meeting_clauses"] if item.get("clause_id")], + } + for domain_id, root in slices.items() + } + source_manifest = { + "schema_version": "task_c_bo_source_universe_manifest.v1", + "status": "READY", + "run_fingerprint": run_fingerprint, + "input_digests_sha256": stage_a["input_digests_sha256"], + "source_universe": { + "meeting_clause_ids": stage_a["meeting_clause_map"].get("clause_order") or [], + "event_candidate_ids": stage_a["event_candidate_map"].get("candidate_id_set") or [], + "evidence_indexes": stage_a["evidence_authority_map"].get("evidence_index_set") or [], + }, + "domain_source_universes": domain_sources, + "counts": { + "meeting_clauses": len(stage_a["meeting_clause_map"].get("clause_order") or []), + "event_candidates": len(stage_a["event_candidate_map"].get("candidate_id_set") or []), + "evidence_indexes": len(stage_a["evidence_authority_map"].get("evidence_index_set") or []), + }, + "domain_slice_digests_sha256": domain_digests, + } + _write_json_verified("stage1_tmp/task_c_bo/stage_a_context.json", {"stage_a_context": stage_a}) + _write_json_verified("stage1_tmp/task_c_bo/source_universe_manifest.json", source_manifest) + for domain_id, root in slices.items(): + _write_json_verified(domain_paths[domain_id], root) + print(json.dumps({"a0_manifest": { + "schema_version": "task_c_bo_a0_manifest.v1", + "status": "READY", + "run_fingerprint": run_fingerprint, + "stage_a_context_path": "stage1_tmp/task_c_bo/stage_a_context.json", + "source_manifest_path": "stage1_tmp/task_c_bo/source_universe_manifest.json", + "domain_slice_paths": domain_paths, + "input_digests_sha256": stage_a["input_digests_sha256"], + "counts": source_manifest["counts"], + }}, ensure_ascii=False, separators=(",", ":"))) + + if __name__ == "__main__": + try: + main() + except Exception as exc: + print(json.dumps({"a0_manifest": {"schema_version": "task_c_bo_a0_manifest.v1", "status": "FAILED", "error": str(exc)}}, ensure_ascii=False)) + raise + - task_name: Task_C_BO_Stage_B_B1_Money_Successor + llm_provider: google + llm_model: gemini-3.5-flash + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + Use localdocs exactly once to call read_docs for only this path: + stage1_tmp/task_c_bo/domain_slices/B1_Money_Successor.json + Do not call list_docs, do not read another path, and do not write any file. + The read result is the only factual input for this task. + + + + + - You MUST use only the JSON object read from . + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B1_Money_Successor"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B1_Money_Successor + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for money claim, payment, notice, admission, and commercial successor liability. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B1:001`, `B1:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - money claim origin: loan, sale price, deposit, settlement, construction price, unpaid balance + - due date, acceleration, demand, notice, payment deadline, or delayed performance + - partial payment, debt admission, payment memorandum, balance confirmation, account statement, or settlement confirmation + - business transfer, trade-name continuation, transferee liability, debt non-assumption clause, or commercial successor liability + - uncertain creditor/debtor/successor linkage that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use domain routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the assigned domain slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split money claim origin, maturity/demand, payment/admission, and successor liability when they are distinct legal-effect seeds. + 4. Preserve payment history, demand/notice hints, admission hints, successor liability hints, and successor-link uncertainty in `extensions.domain_payload`. Preserve agreed interest rate and agreed delay damage rate as literal strings from B1 `loan_components`/`secured_debt_components` (`component_details` of `selected_evidence_authority`) when present; never substitute statutory rates, and keep the supporting evidence indexes in `interest_rate_source_evidence_refs`. + 5. Use `domain_review_queue` for missing amount/date, uncertain creditor/debtor identity, conflicting payment status, or cross-domain successor merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Omit unsupported optional values and route unresolved extractable facts to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + Return exactly one JSON object with this sparse schema. Omit optional keys that are unsupported; preserve meaningful false and zero values. + { + "stage_b_domain_bo_seed_delta": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed_delta.v1", + "domain_id": "B1_Money_Successor", + "status": "READY|READY_WITH_REVIEW|FAILED", + "run_fingerprint": "sha256", + "slice_digest_sha256": "sha256", + "bo_seed_candidates": [ + { + "candidate_ref": "B1:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "Action": "short neutral source-backed draft", + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "money_claim_or_successor_liability" + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B1:R001", + "severity": "SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "candidate_refs": [], + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Part3|Part4|Stage2|human_review" + } + ] + } + } + Optional candidate keys allowed only when supported: JuristicAct, Legal_Keywords, core_field_base, amount, downstream_seed_refs. + Protected domain_payload keys whose source-backed content must be preserved when present: money_claim_details, payment_history, demand_or_notice_hints, admission_hints, successor_liability_hints, successor_link_uncertainty. + Do not emit transport_metadata, transport_candidate_refs, Reason, PriorAct, ReasonRefs, Evidence, EvidenceTitles, BO_ID, id, null-only scaffolding, or empty optional containers. + candidate_ref values must be contiguous in output order with the domain prefix and three-digit ordinal. + Status is FAILED on input guard failure, READY_WITH_REVIEW when the review queue is non-empty, otherwise READY. + + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B2_Secured_Registry + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + Use localdocs exactly once to call read_docs for only this path: + stage1_tmp/task_c_bo/domain_slices/B2_Secured_Registry.json + Do not call list_docs, do not read another path, and do not write any file. + The read result is the only factual input for this task. + + + + + - You MUST use only the JSON object read from . + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B2_Secured_Registry"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B2_Secured_Registry + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for secured debt, registry, payment allocation, auction, distribution, and derivative invalidity issues. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B2:001`, `B2:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - secured debt creation, change, repayment, payment allocation, and secured obligation balance + - mortgage, pledge, collateral security, registration, cancellation, correction, or invalidity chain + - auction, distribution, dividend, satisfaction, or residual balance facts + - derivative invalidity, registry error, title defect, or secured property dispute + - uncertain registry sequence or security/right-holder linkage that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use domain routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the assigned domain slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split secured debt chain, registry act, payment allocation, auction step, dividend distribution, and derivative invalidity when they are distinct legal-effect seeds. + 4. Preserve registry rows, secured debt chain, payment allocation, auction step, dividend distribution hints, derivative invalidity hints, and registry display values in `extensions.domain_payload`. Carry `fraction_ref` from source event candidates unchanged; do not merge or renumber. Populate `fractional_effect_events` as an event-reference list only (fraction-bearing setting/extinguishment/transfer events); never fold fraction states or normalize/reduce fractions (that is Stage 2 work). + 5. Use `domain_review_queue` for missing registry date, unclear secured obligation balance, conflicting registration sequence, or cross-domain property merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Omit unsupported optional values and route unresolved extractable facts to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + Return exactly one JSON object with this sparse schema. Omit optional keys that are unsupported; preserve meaningful false and zero values. + { + "stage_b_domain_bo_seed_delta": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed_delta.v1", + "domain_id": "B2_Secured_Registry", + "status": "READY|READY_WITH_REVIEW|FAILED", + "run_fingerprint": "sha256", + "slice_digest_sha256": "sha256", + "bo_seed_candidates": [ + { + "candidate_ref": "B2:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "Action": "short neutral source-backed draft", + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "secured_registry_or_auction" + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B2:R001", + "severity": "SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "candidate_refs": [], + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Part3|Part4|Stage2|human_review" + } + ] + } + } + Optional candidate keys allowed only when supported: JuristicAct, Legal_Keywords, core_field_base, amount, downstream_seed_refs. + Protected domain_payload keys whose source-backed content must be preserved when present: secured_debt_chain, payment_allocation, registry_rows, fractional_effect_events, mortgage_extinguishment_hints, auction_steps, dividend_distribution_hints, derivative_invalidity_hints. + Do not emit transport_metadata, transport_candidate_refs, Reason, PriorAct, ReasonRefs, Evidence, EvidenceTitles, BO_ID, id, null-only scaffolding, or empty optional containers. + candidate_ref values must be contiguous in output order with the domain prefix and three-digit ordinal. + Status is FAILED on input guard failure, READY_WITH_REVIEW when the review queue is non-empty, otherwise READY. + + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B3_Land_Valuation + llm_provider: google + llm_model: gemini-3.5-flash + llm_reasoning: medium + llm_verbosity: low + max_iterations: 2 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + Use localdocs exactly once to call read_docs for only this path: + stage1_tmp/task_c_bo/domain_slices/B3_Land_Valuation.json + Do not call list_docs, do not read another path, and do not write any file. + The read result is the only factual input for this task. + + + + + - You MUST use only the JSON object read from . + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B3_Land_Valuation"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B3_Land_Valuation + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for land use gain, possession/use, valuation, and asset-value facts. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B3:001`, `B3:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - land or building possession, use, occupation, benefit, rent-equivalent gain, or loss of use + - valuation fact, appraisal, market value, rent value, area, period, and calculation base + - delivery, possession start/end, unauthorized use, co-occupancy, or liability overlap + - asset identity, address, parcel, unit, area, or other value-bearing object description + - uncertain use period, value basis, possession status, or asset binding that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use domain routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the assigned domain slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split possession/use state, use-gain period, valuation fact, and liability overlap when they are distinct legal-effect seeds. + 4. Preserve valuation rows, valuation matrix, use-gain facts, possession state, asset binding hints, and liability overlap hints in `extensions.domain_payload`. Populate the `possession_state` two-track structure (possession facts vs. asserted legal causes) from selected event candidates only; never resolve which legal cause prevails (that is Stage 2 work); preserve conflicting assertions side by side. + 5. Use `domain_review_queue` for missing period, missing value base, unclear possession state, or cross-domain asset merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Omit unsupported optional values and route unresolved extractable facts to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + Return exactly one JSON object with this sparse schema. Omit optional keys that are unsupported; preserve meaningful false and zero values. + { + "stage_b_domain_bo_seed_delta": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed_delta.v1", + "domain_id": "B3_Land_Valuation", + "status": "READY|READY_WITH_REVIEW|FAILED", + "run_fingerprint": "sha256", + "slice_digest_sha256": "sha256", + "bo_seed_candidates": [ + { + "candidate_ref": "B3:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "Action": "short neutral source-backed draft", + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "land_use_gain_or_valuation" + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B3:R001", + "severity": "SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "candidate_refs": [], + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Part3|Part4|Stage2|human_review" + } + ] + } + } + Optional candidate keys allowed only when supported: JuristicAct, Legal_Keywords, core_field_base, amount, downstream_seed_refs. + Protected domain_payload keys whose source-backed content must be preserved when present: use_gain_facts, valuation_object, valuation_rows, valuation_matrix, possession_state, delivery_or_possession_start_hints, unregistered_building_hints, liability_overlap_hints. + Do not emit transport_metadata, transport_candidate_refs, Reason, PriorAct, ReasonRefs, Evidence, EvidenceTitles, BO_ID, id, null-only scaffolding, or empty optional containers. + candidate_ref values must be contiguous in output order with the domain prefix and three-digit ordinal. + Status is FAILED on input guard failure, READY_WITH_REVIEW when the review queue is non-empty, otherwise READY. + + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B4_Actio + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + Use localdocs exactly once to call read_docs for only this path: + stage1_tmp/task_c_bo/domain_slices/B4_Actio.json + Do not call list_docs, do not read another path, and do not write any file. + The read result is the only factual input for this task. + + + + + - You MUST use only the JSON object read from . + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B4_Actio"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B4_Actio + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for actio pauliana, preserved claim, debtor fraudulent act, beneficiary/transferee, remedy, cap, and defenses. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B4:001`, `B4:002`, ... in deterministic order of the candidate seed list. + + + + Create separate seeds when supported for: + - preserved claim existence, claim date, creditor/debtor identity, and claim amount basis + - debtor act, asset decrease, insolvency, common collateral impairment, and harmfulness + - beneficiary, transferee, bad faith/good faith, notice, value compensation, restitution, and remedy cap + - encumbrance, valuation, property transfer, mortgage, sale, gift, collateral provision, or registration act used as fraudulent act basis + - uncertain fraudulent act date, beneficiary chain, asset value, remedy mode, or defense rebuttal that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use domain routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the assigned domain slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split preserved claim, fraudulent transfer, balance-sheet impairment, encumbrance/valuation, beneficiary/transferee state, remedy/cap, and defense rebuttal when they are distinct legal-effect seeds. + 4. Preserve preserved claim bundle, fraudulent transfer facts, balance sheet, encumbrance timeline, valuation matrix, remedy/cap hints, and defense rebuttal in `extensions.domain_payload`. + 5. Use `domain_review_queue` for missing claim date, uncertain fraudulent act date, conflicting value basis, unclear beneficiary chain, or cross-domain asset/claim merge need. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Omit unsupported optional values and route unresolved extractable facts to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + Return exactly one JSON object with this sparse schema. Omit optional keys that are unsupported; preserve meaningful false and zero values. + { + "stage_b_domain_bo_seed_delta": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed_delta.v1", + "domain_id": "B4_Actio", + "status": "READY|READY_WITH_REVIEW|FAILED", + "run_fingerprint": "sha256", + "slice_digest_sha256": "sha256", + "bo_seed_candidates": [ + { + "candidate_ref": "B4:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "Action": "short neutral source-backed draft", + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "actio_fraudulent_transfer" + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B4:R001", + "severity": "SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "candidate_refs": [], + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Part3|Part4|Stage2|human_review" + } + ] + } + } + Optional candidate keys allowed only when supported: JuristicAct, Legal_Keywords, core_field_base, amount, downstream_seed_refs. + Protected domain_payload keys whose source-backed content must be preserved when present: preserved_claim_bundle, fraudulent_transfer_facts, encumbrance_timeline, valuation_matrix, balance_sheet, remedy_mode_hints, value_compensation_cap_hints, defense_rebuttal. + Do not emit transport_metadata, transport_candidate_refs, Reason, PriorAct, ReasonRefs, Evidence, EvidenceTitles, BO_ID, id, null-only scaffolding, or empty optional containers. + candidate_ref values must be contiguous in output order with the domain prefix and three-digit ordinal. + Status is FAILED on input guard failure, READY_WITH_REVIEW when the review queue is non-empty, otherwise READY. + + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + llm_provider: google + llm_model: gemini-3.1-pro-preview + llm_reasoning: high + llm_verbosity: low + max_iterations: 2 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + Use localdocs exactly once to call read_docs for only this path: + stage1_tmp/task_c_bo/domain_slices/B5_Succession_Notice_Lien_Asset_Defense.json + Do not call list_docs, do not read another path, and do not write any file. + The read result is the only factual input for this task. + + + + + - You MUST use only the JSON object read from . + - You MUST NOT read or infer from the full Stage A context. + - You MUST validate that the input root contains `stage_b_domain_slice`. + - You MUST validate `stage_b_domain_slice.schema_version == "task_c_bo_stage_b_domain_slice.v1"`. + - You MUST validate `stage_b_domain_slice.router_status == "READY"`. + - You MUST validate `stage_b_domain_slice.domain_id == "B5_Succession_Notice_Lien_Asset_Defense"`. + - You MUST validate `stage_b_domain_slice.source_stage_a_status == "READY"`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.context_is_read_only == true`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.bo_id_finalization_allowed == false`. + - You MUST validate `stage_b_domain_slice.stage_b_worker_contract.candidate_generation_allowed == true`. + - You MUST return exactly one raw JSON object. No markdown, no code fence, no prose, no terminate marker. + + + + TASK_NAME: B5_Succession_Notice_Lien_Asset_Defense + STAGE: Stage B optimized lean domain seed generator + MISSION: Generate provisional BO seed candidates for succession, notice lifecycle, lien, asset identity, possession, and major defenses. + + + + - Do not create final `BO_ID` or final `id`. + - Do not finalize `Reason`, `PriorAct`, `ReasonRefs`, final `Evidence`, or final `EvidenceTitles`. + - Do not decide final claim group, final liability group, final complaint theory, or final cross-domain merge. + - `candidate_ref` is a transport-only Stage B reference, not a final BO id. + - Use `B5:001`, `B5:002`, ... in deterministic order of the candidate seed list. + - If multiple lien seeds exist, assign `lien_id` values `B5:LIEN:001`, `B5:LIEN:002`, ... ordered by the earliest related event date (ties broken by `candidate_ref` lexicographic order). This id is transport-scope only, not a final BO id. + - Do not produce `violation_candidates` or `status_after_notice_receipt`; route such needs to `domain_review_queue` with `issue_type="legal_theory_required"`. + - `legal_sender_after_resolution.party` must always remain null in Stage 1 (value finalization is Stage 2 legal work); fill only its `status` and `succession_resolution_ref` from directly-read sources. + + + + Create separate seeds when supported for: + - general succession, specific succession, inheritance, assignment, acquisition, transfer, or party-status succession + - notice dispatch, arrival, receipt, refusal, content, timing, and legal effect of notice + - lien, possession, retained object, claim-object connection, asset alias, and asset identity + - simultaneous performance, setoff, payment, prescription, delivery, possession, or other major defense facts + - uncertain notice arrival, lien possession continuity, asset identity, succession scope, or defense linkage that must be preserved for downstream review + + + + - Read only these fields from `stage_b_domain_slice`: + - `domain_id` + - `domain_label` + - `allowed_legal_effect_bo_types` + - `source_stage_a_schema_version` + - `source_stage_a_status` + - `source_stage_a_created_at_utc` + - `input_digests_sha256` + - `stage_b_worker_contract` + - `source_universe` + - `selected_meeting_clauses` + - `selected_event_candidates` + - `selected_evidence_authority` + - `juristic_act_taxonomy_subset` + - `hazard_subset` + - `part1_review_items_relevant` + - `shared_or_ambiguous_items` + - `routing_audit` + - Do not read any field outside `stage_b_domain_slice`. + - Do not reconstruct or request the full Stage A context. + - Do not use domain routing audit as factual authority by itself; use it only for source membership and ambiguity preservation. + + + + 1. Read `selected_event_candidates`, `selected_evidence_authority`, `selected_meeting_clauses`, `hazard_subset`, and `part1_review_items_relevant` from the assigned domain slice only. + 2. Group sources by legal event/state seed. Do not make one seed per evidence item if several sources support the same event. + 3. Split succession state, notice lifecycle, lien/possession state, asset identity, and defense rebuttal when they are distinct legal-effect seeds. + 4. Preserve succession resolution, notice lifecycle, lien state, possession state, asset alias, simultaneous-performance hints, setoff/payment defense hints, and defense rebuttal in `extensions.domain_payload`. Populate the `possession_state` two-track structure (possession facts vs. asserted legal causes) from selected event candidates only; never resolve which legal cause prevails (that is Stage 2 work); preserve conflicting assertions side by side. Assign `lien_id` deterministically and reference it from possession/notice payloads of the same seed group. Inherit `notice_id` from B1 `notice_components[*].notice_id_proposed` (`component_details` of `selected_evidence_authority`); when multiple notices exist, split seeds so each seed carries one notice lifecycle. Inherit `schedule_refs`/`registry_refs` (SCH-*/REG-* keys) and `canonical_label_candidate`/`usable_in_relief` into `asset_alias` unchanged. + 5. Use `domain_review_queue` for missing arrival evidence, uncertain asset identity, unclear possession continuity, conflicting succession scope, or cross-domain defense merge need. When `received_date` cannot be established from selected sources, leave it null and add a `domain_review_queue` item with `issue_type="missing_source"` (do not fabricate); the sealed drafting gate is applied downstream at FL. + 6. Project each seed only to BO target-schema-compatible fields. + 7. Put domain-specific details under `extensions.domain_payload`. + 8. Omit unsupported optional values and route unresolved extractable facts to `domain_review_queue`. + 9. Use `legal_theory_required` for fields this stage must not decide (e.g., lien violation, notice sender standing, possession cause resolution); use `review_required` for extractable-but-unclear facts. + + + + Return exactly one JSON object with this sparse schema. Omit optional keys that are unsupported; preserve meaningful false and zero values. + { + "stage_b_domain_bo_seed_delta": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed_delta.v1", + "domain_id": "B5_Succession_Notice_Lien_Asset_Defense", + "status": "READY|READY_WITH_REVIEW|FAILED", + "run_fingerprint": "sha256", + "slice_digest_sha256": "sha256", + "bo_seed_candidates": [ + { + "candidate_ref": "B5:001", + "BOType": "event|state", + "ActionType": "법률행위(legal acts)|준법률행위(quasi-legal acts)|사실행위(factual acts)|위법행위(unlawful acts)|소송행위(litigation acts)", + "Action": "short neutral source-backed draft", + "source_evidence_indexes": [], + "provenance": { + "source_event_candidate_ids": [], + "source_meeting_clause_ids": [], + "source_evidence_indexes": [] + }, + "extensions": { + "domain_payload": { + "domain_legal_effect_tag": "succession_notice_lien_asset_defense" + } + } + } + ], + "domain_review_queue": [ + { + "review_id": "B5:R001", + "severity": "SOFT_WARNING|HARD_WARNING|BLOCK", + "issue_type": "missing_source|source_conflict|cross_domain_merge_needed|amount_or_date_uncertain|legal_effect_uncertain|review_required|legal_theory_required", + "candidate_refs": [], + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Part3|Part4|Stage2|human_review" + } + ] + } + } + Optional candidate keys allowed only when supported: JuristicAct, Legal_Keywords, core_field_base, amount, downstream_seed_refs. + Protected domain_payload keys whose source-backed content must be preserved when present: succession_resolution, general_succession_status, asset_specific_succession_resolution, notice_lifecycle, lien_state, possession_state, asset_alias, defense_rebuttal, simultaneous_performance_hints, setoff_or_payment_defense_hints. + Do not emit transport_metadata, transport_candidate_refs, Reason, PriorAct, ReasonRefs, Evidence, EvidenceTitles, BO_ID, id, null-only scaffolding, or empty optional containers. + candidate_ref values must be contiguous in output order with the domain prefix and three-digit ordinal. + Status is FAILED on input guard failure, READY_WITH_REVIEW when the review queue is non-empty, otherwise READY. + + + + + - The response MUST begin with the literal byte `{` and end with the literal byte `}`. + - Do NOT emit a leading code-fence opener (a backtick followed by `json` or any language tag, three backticks alone, or any line that starts with backticks). + - Do NOT emit a trailing code-fence closer (any line consisting of backticks). + - Do NOT emit a leading `# Output Messages` or `## ` line, prose, "Here is the JSON", or any text outside the single JSON object. + - Do NOT emit a `**terminate**` marker, a final period, or any trailing whitespace beyond the closing `}`. + - Pre-emit self-check (perform silently before answering): (a) the first non-whitespace character is `{`; (b) the last non-whitespace character is `}`; (c) no backtick character appears anywhere in the output; (d) JSON.parse on the entire output would succeed. If ANY check fails, fix the response before emitting. + - The downstream publisher reads `.json_output` with a strict JSON parser that rejects code fences and prose tails. Violating this rule causes the entire pipeline to fail at the publisher stage even though this task's content is correct. + + + - task_name: Task_C_BO_R0_seed_reducer_exception_planner + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: "#!/usr/bin/env python3\nfrom __future__ import annotations\n\nimport hashlib\nimport + itertools\nimport json\nimport re\nfrom datetime import datetime, timezone\nfrom + typing import Any\n\nimport httpx\n\nLOCALDOCS_URL = \"http://mcp-localdocs:8012/mcp\"\nMCP_HEADERS + = {\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/json, + text/event-stream\",\n}\nCLIENT = httpx.Client(timeout=60)\nMSG_IDS = itertools.count(10)\nJSON_DECODER + = json.JSONDecoder()\n\ndef _next_id() -> int:\n return next(MSG_IDS)\n\ndef + _parse_json_first(raw: str, label: str, allow_trailing: bool = False) -> Any:\n + \ text = (raw or \"\").lstrip(\"\\ufeff\").strip()\n if not text:\n raise + RuntimeError(f\"{label}: empty JSON text\")\n try:\n return json.loads(text)\n + \ except json.JSONDecodeError as first_error:\n try:\n value, + end = JSON_DECODER.raw_decode(text)\n except json.JSONDecodeError as + second_error:\n raise RuntimeError(f\"{label}: invalid JSON\") from + second_error\n if not allow_trailing and text[end:].strip():\n raise + RuntimeError(f\"{label}: trailing non-JSON data\") from first_error\n return + value\n\ndef _parse_rpc(raw: str) -> dict[str, Any]:\n events: list[dict[str, + Any]] = []\n for line in (raw or \"\").splitlines():\n if not line.startswith(\"data:\"):\n + \ continue\n candidate = line[5:].strip()\n if not candidate + or candidate == \"[DONE]\":\n continue\n try:\n parsed + = json.loads(candidate)\n except json.JSONDecodeError:\n continue\n + \ if isinstance(parsed, dict):\n events.append(parsed)\n if + events:\n return events[-1]\n parsed = _parse_json_first(raw, \"MCP + response\", allow_trailing=True)\n if not isinstance(parsed, dict):\n raise + RuntimeError(\"MCP response root must be object\")\n return parsed\n\ndef + _init(task_name: str) -> None:\n response = CLIENT.post(\n LOCALDOCS_URL,\n + \ json={\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n + \ \"method\": \"initialize\",\n \"params\": {\n \"protocolVersion\": + \"2025-03-26\",\n \"capabilities\": {},\n \"clientInfo\": + {\n \"name\": task_name,\n \"version\": + \"1.0\",\n \"user_id\": \"{{__user_hash__}}\",\n \"workspace_id\": + \"{{__workspace_hash__}}\",\n },\n },\n },\n + \ headers=MCP_HEADERS,\n )\n response.raise_for_status()\n session_id + = response.headers.get(\"mcp-session-id\")\n if session_id:\n MCP_HEADERS[\"mcp-session-id\"] + = session_id\n CLIENT.post(\n LOCALDOCS_URL,\n json={\"jsonrpc\": + \"2.0\", \"method\": \"notifications/initialized\"},\n headers=MCP_HEADERS,\n + \ ).raise_for_status()\n\ndef _call(name: str, arguments: dict[str, Any]) + -> dict[str, Any]:\n response = CLIENT.post(\n LOCALDOCS_URL,\n json={\n + \ \"jsonrpc\": \"2.0\",\n \"id\": _next_id(),\n \"method\": + \"tools/call\",\n \"params\": {\"name\": name, \"arguments\": arguments},\n + \ },\n headers=MCP_HEADERS,\n )\n response.raise_for_status()\n + \ payload = _parse_rpc(response.text)\n if payload.get(\"error\"):\n raise + RuntimeError(f\"MCP {name} failed: {payload['error']}\")\n result = payload.get(\"result\")\n + \ if not isinstance(result, dict):\n raise RuntimeError(f\"MCP {name} + returned no result\")\n if result.get(\"isError\") is True:\n raise + RuntimeError(f\"MCP {name} returned isError\")\n return payload\n\ndef _unwrap(payload: + dict[str, Any], label: str) -> Any:\n result = payload.get(\"result\") or + {}\n content = result.get(\"content\") or []\n texts = [block.get(\"text\") + for block in content if isinstance(block, dict) and block.get(\"text\")]\n if + not texts:\n raise RuntimeError(f\"{label}: empty MCP content\")\n raw + = texts[0]\n try:\n return _parse_json_first(raw, label, allow_trailing=True)\n + \ except RuntimeError:\n return raw\n\ndef _extract_doc(payload: dict[str, + Any], doc_name: str) -> str:\n outer = _unwrap(payload, f\"read_docs:{doc_name}\")\n + \ if isinstance(outer, dict) and isinstance(outer.get(\"results\"), list):\n + \ for item in outer[\"results\"]:\n if not isinstance(item, + dict):\n continue\n name = item.get(\"name\") or item.get(\"path\") + or item.get(\"doc_name\") or item.get(\"document_name\")\n if name + and not (str(name) == doc_name or str(name).endswith(\"/\" + doc_name)):\n continue\n + \ value = item.get(\"content\")\n if value is None:\n value + = item.get(\"text\")\n if value is None:\n value = + item.get(\"raw\")\n if value is not None:\n return + value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)\n + \ if isinstance(outer, dict) and doc_name in outer:\n value = outer[doc_name]\n + \ return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)\n + \ if isinstance(outer, str):\n return outer\n return json.dumps(outer, + ensure_ascii=False)\n\ndef _read_text(doc_name: str) -> str:\n return _extract_doc(_call(\"read_docs\", + {\"doc_names\": [doc_name]}), doc_name)\n\ndef _read_json(doc_name: str) -> + Any:\n raw = _read_text(doc_name)\n return _parse_json_first(raw, doc_name, + allow_trailing=True)\n\ndef _canonical(value: Any) -> str:\n return json.dumps(value, + ensure_ascii=False, sort_keys=True, separators=(\",\", \":\"))\n\ndef _sha(value: + Any) -> str:\n raw = value if isinstance(value, str) else _canonical(value)\n + \ return hashlib.sha256(raw.encode(\"utf-8\")).hexdigest()\n\ndef _write_json_verified(doc_name: + str, value: Any) -> str:\n content = json.dumps(value, ensure_ascii=False, + indent=2)\n _call(\"write_file\", {\"path\": doc_name, \"content\": content, + \"overwrite\": True})\n reread = _read_json(doc_name)\n if _canonical(reread) + != _canonical(value):\n raise RuntimeError(f\"{doc_name}: post-write + verification mismatch\")\n return _sha(content)\n\ndef _as_list(value: Any) + -> list[Any]:\n return value if isinstance(value, list) else []\n\ndef _as_dict(value: + Any) -> dict[str, Any]:\n return value if isinstance(value, dict) else {}\n\ndef + _uniq(values: list[Any]) -> list[Any]:\n seen: set[str] = set()\n output: + list[Any] = []\n for value in values:\n marker = _canonical(value) + if isinstance(value, (dict, list)) else str(value)\n if marker in seen:\n + \ continue\n seen.add(marker)\n output.append(value)\n + \ return output\n\ndef _natural_key(value: Any) -> list[Any]:\n return + [int(part) if part.isdigit() else part for part in re.split(r\"(\\d+)\", str(value))]\n\nTASK_NAME + = \"Task_C_BO_R0_seed_reducer_exception_planner\"\nWORKER_RAWS = {\n \"B1_Money_Successor\": + r\"\"\"{{prev.Task_C_BO_Stage_B_B1_Money_Successor.json_output}}\"\"\",\n \"B2_Secured_Registry\": + r\"\"\"{{prev.Task_C_BO_Stage_B_B2_Secured_Registry.json_output}}\"\"\",\n \"B3_Land_Valuation\": + r\"\"\"{{prev.Task_C_BO_Stage_B_B3_Land_Valuation.json_output}}\"\"\",\n \"B4_Actio\": + r\"\"\"{{prev.Task_C_BO_Stage_B_B4_Actio.json_output}}\"\"\",\n \"B5_Succession_Notice_Lien_Asset_Defense\": + r\"\"\"{{prev.Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense.json_output}}\"\"\",\n}\nSEED_PATHS + = {domain: f\"stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/{domain}.json\" + for domain in WORKER_RAWS}\nACTION_TYPES = {\n \"법률행위(legal acts)\", \"준법률행위(quasi-legal + acts)\", \"사실행위(factual acts)\",\n \"위법행위(unlawful acts)\", \"소송행위(litigation + acts)\",\n}\nDOMAIN_PAYLOAD_DEFAULTS = json.loads(r'''{\"B1_Money_Successor\":{\"money_claim_details\":{\"claim_type_candidate\":null,\"principal_amount_hint\":null,\"interest_hint\":null,\"delay_damage_hint\":null,\"agreed_interest_rate\":null,\"agreed_delay_damage_rate\":null,\"interest_accrual_start_hint\":null,\"interest_rate_source_evidence_refs\":[],\"unpaid_balance_hint\":null,\"creditor_candidate\":null,\"debtor_candidate\":null},\"payment_history\":[],\"demand_or_notice_hints\":[],\"admission_hints\":[],\"successor_liability_hints\":[],\"successor_link_uncertainty\":null},\"B2_Secured_Registry\":{\"secured_debt_chain\":[],\"payment_allocation\":[],\"registry_rows\":[],\"fractional_effect_events\":[],\"mortgage_extinguishment_hints\":[],\"auction_steps\":[],\"dividend_distribution_hints\":[],\"derivative_invalidity_hints\":[]},\"B3_Land_Valuation\":{\"use_gain_facts\":[],\"valuation_object\":{},\"valuation_rows\":[],\"valuation_matrix\":{},\"possession_state\":{},\"delivery_or_possession_start_hints\":[],\"unregistered_building_hints\":[],\"liability_overlap_hints\":[]},\"B4_Actio\":{\"preserved_claim_bundle\":{},\"fraudulent_transfer_facts\":{},\"encumbrance_timeline\":[],\"valuation_matrix\":{},\"balance_sheet\":{},\"remedy_mode_hints\":[],\"value_compensation_cap_hints\":[],\"defense_rebuttal\":{}},\"B5_Succession_Notice_Lien_Asset_Defense\":{\"succession_resolution\":{},\"general_succession_status\":{},\"asset_specific_succession_resolution\":{},\"notice_lifecycle\":{},\"lien_state\":{},\"possession_state\":{},\"asset_alias\":{},\"defense_rebuttal\":{},\"simultaneous_performance_hints\":[],\"setoff_or_payment_defense_hints\":[]}}''')\nCOMMON_DOMAIN_DEFAULTS + = {\n \"title_hint\": None, \"summary_hint\": None, \"conflict_patterns\": + [], \"asset_bindings\": [],\n \"parties_hint\": [], \"temporal_hint\": None, + \"hazard_flags\": [], \"legal_effect_tags\": [],\n \"event_state_type\": + None, \"action_summary\": None, \"time_interval_hint\": None,\n \"asset_binding_hint\": + None, \"mixed_contract_split_hint\": None, \"conflict_pattern_hint\": None,\n + \ \"candidate_details\": {}, \"facts\": {}, \"conflict_flags\": [],\n}\n\ndef + _strict_worker_json(raw: str, label: str) -> dict[str, Any]:\n text = (raw + or \"\").lstrip(\"\uFEFF\").strip()\n fence = re.fullmatch(r\"```(?:json)? + *(.*?) *```\", text, flags=re.DOTALL | re.IGNORECASE)\n if fence:\n text + = fence.group(1).strip()\n value = _parse_json_first(text, label, allow_trailing=False)\n + \ for _ in range(3):\n if isinstance(value, dict) and set(value).issubset({\"success\", + \"output\", \"json_output\"}) and any(key in value for key in (\"output\", \"json_output\")):\n + \ value = value.get(\"json_output\", value.get(\"output\"))\n if + isinstance(value, str):\n value = _parse_json_first(value, label, + allow_trailing=False)\n continue\n if isinstance(value, dict) + and set(value).issubset({\"type\", \"text\"}) and isinstance(value.get(\"text\"), + str):\n value = _parse_json_first(value[\"text\"], label, allow_trailing=False)\n + \ continue\n break\n if not isinstance(value, dict):\n raise + RuntimeError(f\"{label}: root must be object\")\n return value\n\ndef _deep_defaults(default: + Any, supplied: Any) -> Any:\n if isinstance(default, dict):\n incoming + = supplied if isinstance(supplied, dict) else {}\n result = {key: _deep_defaults(value, + incoming[key]) if key in incoming else value for key, value in default.items()}\n + \ for key, value in incoming.items():\n if key not in result:\n + \ result[key] = value\n return result\n return supplied\n\ndef + _source_refs(candidate: dict[str, Any]) -> dict[str, list[str]]:\n provenance + = _as_dict(candidate.get(\"provenance\"))\n return {\n \"source_event_candidate_ids\": + _uniq([str(v) for v in _as_list(provenance.get(\"source_event_candidate_ids\")) + if v]),\n \"source_meeting_clause_ids\": _uniq([str(v) for v in _as_list(provenance.get(\"source_meeting_clause_ids\")) + if v]),\n \"source_evidence_indexes\": _uniq([str(v) for v in _as_list(candidate.get(\"source_evidence_indexes\")) + + _as_list(provenance.get(\"source_evidence_indexes\")) if v]),\n }\n\ndef + _expand_candidate(candidate: dict[str, Any], domain: str, manifest: dict[str, + Any]) -> dict[str, Any]:\n refs = _source_refs(candidate)\n domain_payload + = _as_dict(_as_dict(candidate.get(\"extensions\")).get(\"domain_payload\"))\n + \ defaults = {**COMMON_DOMAIN_DEFAULTS, **DOMAIN_PAYLOAD_DEFAULTS[domain]}\n + \ expanded_payload = _deep_defaults(defaults, domain_payload)\n core = + _as_dict(candidate.get(\"core_field_base\"))\n amount = candidate.get(\"amount\") + if isinstance(candidate.get(\"amount\"), dict) else None\n downstream = _as_dict(candidate.get(\"downstream_seed_refs\"))\n + \ return {\n \"candidate_ref\": candidate[\"candidate_ref\"],\n \"BOType\": + candidate[\"BOType\"],\n \"ActionType\": candidate[\"ActionType\"],\n + \ \"JuristicAct\": candidate.get(\"JuristicAct\") if isinstance(candidate.get(\"JuristicAct\"), + dict) else {\"label\": None},\n \"Action\": candidate[\"Action\"],\n + \ \"Reason\": None,\n \"PriorAct\": None,\n \"ReasonRefs\": + [],\n \"Legal_Keywords\": _as_list(candidate.get(\"Legal_Keywords\")),\n + \ \"core_field_base\": {\n \"date\": core.get(\"date\"), \"amount\": + core.get(\"amount\"), \"parties\": _as_list(core.get(\"parties\")),\n \"asset_refs\": + _as_list(core.get(\"asset_refs\")), \"legal_effect_summary\": core.get(\"legal_effect_summary\"),\n + \ },\n \"amount\": amount,\n \"source_evidence_indexes\": + refs[\"source_evidence_indexes\"],\n \"provenance\": refs,\n \"downstream_seed_refs\": + {\n \"legal_effect_structure_seed\": downstream.get(\"legal_effect_structure_seed\"),\n + \ \"claim_group_seed\": downstream.get(\"claim_group_seed\"),\n \"asset_cluster_seed\": + downstream.get(\"asset_cluster_seed\"),\n \"issue_cluster_seed\": + downstream.get(\"issue_cluster_seed\"),\n },\n \"extensions\": + {\"domain_payload\": expanded_payload},\n \"transport_metadata\": {\n + \ \"stage\": \"Task_C_BO.Stage_B.DomainSeed\", \"source_task\": domain,\n + \ \"run_fingerprint\": manifest[\"run_fingerprint\"],\n \"slice_digest_sha256\": + manifest[\"domain_slice_digests_sha256\"][domain],\n },\n \"transport_candidate_refs\": + refs,\n }\n\ndef _validate_refs(candidate: dict[str, Any], domain: str, manifest: + dict[str, Any]) -> list[str]:\n refs = _source_refs(candidate)\n global_u + = _as_dict(manifest.get(\"source_universe\"))\n domain_u = _as_dict(_as_dict(manifest.get(\"domain_source_universes\")).get(domain))\n + \ failures: list[str] = []\n checks = [\n (\"source_event_candidate_ids\", + \"event_candidate_ids\"),\n (\"source_meeting_clause_ids\", \"meeting_clause_refs\"),\n + \ (\"source_evidence_indexes\", \"evidence_indexes\"),\n ]\n for + ref_key, universe_key in checks:\n global_key = \"meeting_clause_ids\" + if ref_key == \"source_meeting_clause_ids\" else universe_key\n for value + in refs[ref_key]:\n if value not in set(str(v) for v in _as_list(global_u.get(global_key))):\n + \ failures.append(f\"{candidate.get('candidate_ref')}:{ref_key}:outside_global:{value}\")\n + \ if value not in set(str(v) for v in _as_list(domain_u.get(universe_key))):\n + \ failures.append(f\"{candidate.get('candidate_ref')}:{ref_key}:outside_slice:{value}\")\n + \ return failures\n\ndef _substantive_key(candidate: dict[str, Any]) -> str:\n + \ return _sha({key: candidate.get(key) for key in (\"BOType\", \"ActionType\", + \"JuristicAct\", \"Action\", \"core_field_base\", \"amount\", \"downstream_seed_refs\", + \"extensions\")})\n\ndef main() -> None:\n _init(\"task-c-bo-r0-seed-reducer-exception-planner\")\n + \ stage_root = _read_json(\"stage1_tmp/task_c_bo/stage_a_context.json\")\n + \ stage_a = _as_dict(_as_dict(stage_root).get(\"stage_a_context\"))\n manifest + = _read_json(\"stage1_tmp/task_c_bo/source_universe_manifest.json\")\n if + stage_a.get(\"status\") != \"READY\" or manifest.get(\"status\") != \"READY\":\n + \ raise RuntimeError(\"A0 artifacts are not READY\")\n if stage_a.get(\"run_fingerprint\") + != manifest.get(\"run_fingerprint\"):\n raise RuntimeError(\"A0 run fingerprint + mismatch\")\n ledger_candidates: list[dict[str, Any]] = []\n reviews: + list[dict[str, Any]] = []\n failures: list[str] = []\n seed_counts: dict[str, + int] = {}\n for domain, raw in WORKER_RAWS.items():\n parsed = _strict_worker_json(raw, + domain)\n delta = _as_dict(parsed.get(\"stage_b_domain_bo_seed_delta\"))\n + \ if delta.get(\"schema_version\") != \"task_c_bo_stage_b_domain_bo_seed_delta.v1\" + or delta.get(\"domain_id\") != domain:\n failures.append(f\"{domain}:delta + schema/domain mismatch\")\n continue\n if delta.get(\"status\") + == \"FAILED\":\n failures.append(f\"{domain}:worker FAILED\")\n if + delta.get(\"run_fingerprint\") != manifest.get(\"run_fingerprint\"):\n failures.append(f\"{domain}:run + fingerprint mismatch\")\n if delta.get(\"slice_digest_sha256\") != manifest.get(\"domain_slice_digests_sha256\", + {}).get(domain):\n failures.append(f\"{domain}:slice digest mismatch\")\n + \ candidates = _as_list(delta.get(\"bo_seed_candidates\"))\n prefix + = domain.split(\"_\", 1)[0]\n expected_refs = [f\"{prefix}:{index:03d}\" + for index in range(1, len(candidates) + 1)]\n actual_refs = [str(_as_dict(candidate).get(\"candidate_ref\") + or \"\") for candidate in candidates]\n if actual_refs != expected_refs:\n + \ failures.append(f\"{domain}:candidate_ref sequence mismatch\")\n + \ expanded: list[dict[str, Any]] = []\n for candidate in candidates:\n + \ if not isinstance(candidate, dict):\n failures.append(f\"{domain}:non-object + candidate\")\n continue\n forbidden_keys = {\"transport_metadata\", + \"transport_candidate_refs\", \"Reason\", \"PriorAct\", \"ReasonRefs\", \"Evidence\", + \"EvidenceTitles\", \"BO_ID\", \"id\"}\n present_forbidden = sorted(forbidden_keys.intersection(candidate))\n + \ if present_forbidden:\n failures.append(f\"{candidate.get('candidate_ref')}:forbidden + sparse keys:{present_forbidden}\")\n continue\n if + candidate.get(\"BOType\") not in {\"event\", \"state\"} or candidate.get(\"ActionType\") + not in ACTION_TYPES or not str(candidate.get(\"Action\") or \"\").strip():\n + \ failures.append(f\"{candidate.get('candidate_ref')}:required + enum/action invalid\")\n continue\n failures.extend(_validate_refs(candidate, + domain, manifest))\n seed = _expand_candidate(candidate, domain, + manifest)\n expanded.append(seed)\n ledger_candidates.append({\"candidate_ref\": + seed[\"candidate_ref\"], \"source_domain\": domain, \"seed\": seed})\n reviews.extend([dict(item, + source_domain=domain) for item in _as_list(delta.get(\"domain_review_queue\")) + if isinstance(item, dict)])\n artifact = {\"stage_b_domain_bo_seed_output\": + {\n \"schema_version\": \"task_c_bo_stage_b_domain_bo_seed.v2\", + \"domain_id\": domain,\n \"status\": \"READY_WITH_REVIEW\" if delta.get(\"domain_review_queue\") + else \"READY\",\n \"run_fingerprint\": manifest[\"run_fingerprint\"], + \"bo_seed_candidates\": expanded,\n \"domain_review_queue\": _as_list(delta.get(\"domain_review_queue\")),\n + \ }}\n _write_json_verified(SEED_PATHS[domain], artifact)\n seed_counts[domain] + = len(expanded)\n\n deterministic: list[dict[str, Any]] = []\n merged_refs: + set[str] = set()\n by_key: dict[str, list[dict[str, Any]]] = {}\n for + item in ledger_candidates:\n by_key.setdefault(_substantive_key(item[\"seed\"]), + []).append(item)\n for group in by_key.values():\n if len(group) < + 2:\n continue\n survivor = group[0][\"candidate_ref\"]\n absorbed + = [item[\"candidate_ref\"] for item in group[1:]]\n merged_refs.update(absorbed)\n + \ deterministic.append({\"decision\": \"MERGE_PROVENANCE_UNION\", \"survivor_ref\": + survivor, \"absorbed_refs\": absorbed})\n for item in ledger_candidates:\n + \ seed = item[\"seed\"]\n refs = _source_refs(seed)\n if + refs[\"source_meeting_clause_ids\"] and not refs[\"source_evidence_indexes\"] + and not refs[\"source_event_candidate_ids\"]:\n reviews.append({\n + \ \"review_id\": f\"R0:{item['candidate_ref']}:MEETING_ONLY\", + \"severity\": \"SOFT_WARNING\",\n \"issue_type\": \"MEETING_ONLY_EVIDENCE_GAP\", + \"candidate_refs\": [item[\"candidate_ref\"]],\n **refs, \"downstream_owner\": + \"human_review\", \"source_domain\": item[\"source_domain\"],\n })\n + \ for left_index, left in enumerate(ledger_candidates):\n for right + in ledger_candidates[left_index + 1:]:\n if left[\"candidate_ref\"] + in merged_refs or right[\"candidate_ref\"] in merged_refs:\n continue\n + \ left_refs = set(_source_refs(left[\"seed\"])[\"source_evidence_indexes\"])\n + \ right_refs = set(_source_refs(right[\"seed\"])[\"source_evidence_indexes\"])\n + \ if left_refs.intersection(right_refs) and left[\"seed\"][\"ActionType\"] + == right[\"seed\"][\"ActionType\"]:\n deterministic.append({\"decision\": + \"KEEP_SEPARATE\", \"cluster_id\": f\"NEAR-{len(deterministic)+1:03d}\", \"candidate_refs\": + [left[\"candidate_ref\"], right[\"candidate_ref\"]]})\n\n packs: list[dict[str, + Any]] = []\n exception_number = 1\n for review in reviews:\n candidate_refs + = [str(value) for value in _as_list(review.get(\"candidate_refs\"))]\n if + review.get(\"issue_type\") != \"cross_domain_merge_needed\" or len(candidate_refs) + < 2:\n continue\n candidates = [item for item in ledger_candidates + if item[\"candidate_ref\"] in candidate_refs][:8]\n exception_id = f\"P2-EXC-{exception_number:03d}\"\n + \ pack_id = f\"P2-PACK-{len(packs)+1:03d}\"\n pack = {\n \"pack_id\": + pack_id, \"exception_type\": \"canonicalization_conflict\", \"exception_ids\": + [exception_id],\n \"items\": [{\n \"exception_id\": + exception_id, \"exception_type\": \"canonicalization_conflict\",\n \"candidate_refs\": + candidate_refs[:8], \"conflicting_field\": \"cross_domain_identity\",\n \"allowed_values\": + [],\n \"compact_candidates\": [{\n \"candidate_ref\": + item[\"candidate_ref\"], \"source_domain\": item[\"source_domain\"],\n \"relevant_fields\": + {key: item[\"seed\"].get(key) for key in (\"BOType\", \"ActionType\", \"Action\")},\n + \ \"source_refs\": _source_refs(item[\"seed\"]),\n } + for item in candidates],\n \"allowed_decisions\": [\"KEEP_SEPARATE\", + \"MERGE_PROVENANCE_UNION\", \"SPLIT\", \"BLOCK_REVIEW\"],\n }],\n + \ \"output_path\": f\"quality_gates/stage1_part2_adjudication_parts/PACK-{len(packs)+1:03d}.json\",\n + \ }\n if len(_canonical(pack).encode(\"utf-8\")) > 20000:\n reviews.append({\"review_id\": + f\"R0:BUDGET:{exception_number:03d}\", \"severity\": \"BLOCK\", \"issue_type\": + \"BUDGET_OVERFLOW_BLOCK_REVIEW\", \"candidate_refs\": candidate_refs, \"downstream_owner\": + \"human_review\"})\n elif len(packs) < 8:\n packs.append(pack)\n + \ exception_number += 1\n\n status = \"BLOCKED\" if failures or any(item.get(\"severity\") + == \"BLOCK\" for item in reviews) else (\"READY_WITH_REVIEW\" if reviews else + \"READY\")\n all_refs = [item[\"candidate_ref\"] for item in ledger_candidates]\n + \ ledger = {\"postb_seed_ledger\": {\n \"schema_version\": \"task_c_bo_postb_seed_ledger.v2\", + \"status\": status,\n \"run_fingerprint\": manifest[\"run_fingerprint\"], + \"source_universe\": manifest[\"source_universe\"],\n \"ledger_candidates\": + ledger_candidates,\n \"candidate_index\": {item[\"candidate_ref\"]: {\"source_domain\": + item[\"source_domain\"], \"ordinal\": index} for index, item in enumerate(ledger_candidates)},\n + \ \"domain_review_queue\": reviews, \"deterministic_decisions\": deterministic,\n + \ \"exception_summary\": {\"pack_count\": len(packs), \"exception_count\": + sum(len(pack[\"items\"]) for pack in packs)},\n \"audit_trace\": {\"failures\": + failures, \"seed_counts\": seed_counts},\n \"conservation\": {\"input_candidate_refs\": + all_refs, \"input_count\": len(all_refs), \"unique_count\": len(set(all_refs))},\n + \ }}\n handoff = {\n \"schema_version\": \"stage1_part2_review_handoff.v1\", + \"status\": status,\n \"run_fingerprint\": manifest[\"run_fingerprint\"], + \"review_count\": len(reviews), \"items\": reviews,\n }\n exception_pack + = {\n \"schema_version\": \"stage1_part2_exception_pack.v1\", \"status\": + status,\n \"run_fingerprint\": manifest[\"run_fingerprint\"], \"pack_count\": + len(packs), \"packs\": packs,\n }\n _write_json_verified(\"stage1_tmp/task_c_bo/postb_seed_ledger.json\", + ledger)\n _write_json_verified(\"quality_gates/stage1_part2_review_handoff.json\", + handoff)\n _write_json_verified(\"quality_gates/stage1_part2_exception_pack.json\", + exception_pack)\n output = {\"r0_manifest\": {\n \"schema_version\": + \"task_c_bo_r0_manifest.v1\", \"status\": status,\n \"run_fingerprint\": + manifest[\"run_fingerprint\"], \"seed_ledger_path\": \"stage1_tmp/task_c_bo/postb_seed_ledger.json\",\n + \ \"review_handoff_path\": \"quality_gates/stage1_part2_review_handoff.json\",\n + \ \"exception_pack_path\": \"quality_gates/stage1_part2_exception_pack.json\",\n + \ \"candidate_count\": len(ledger_candidates), \"review_count\": len(reviews), + \"pack_count\": len(packs),\n }, \"dynamic_fanout\": packs if status != \"BLOCKED\" + else []}\n print(json.dumps(output, ensure_ascii=False, separators=(\",\", + \":\")))\n if status == \"BLOCKED\":\n raise RuntimeError(\"R0 blocked + by schema, source, or budget findings\")\n\nif __name__ == \"__main__\":\n try:\n + \ main()\n except Exception as exc:\n print(json.dumps({\"r0_manifest\": + {\"schema_version\": \"task_c_bo_r0_manifest.v1\", \"status\": \"FAILED\", \"error\": + str(exc)}, \"dynamic_fanout\": []}, ensure_ascii=False))\n raise\n" + - task_name: Task_C_BO_R1_exception_adjudicator_* + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: medium + llm_verbosity: low + max_iterations: 2 + max_concurrency: 2 + cache_control: + mode: auto + ttl: 1h + use_tools: + - localdocs + preflight: false + prompts: + - role: user + content: | + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + You are the conditional, compact exception adjudicator for Stage 1 Part 2. + Use only the item fields below. Do not read any Stage A file, ledger, BO, domain seed, meeting, event, or evidence file. + + pack_id: {{item.pack_id}} + exception_type: {{item.exception_type}} + exception_ids: {{item.exception_ids}} + items: {{item.items}} + output_path: {{item.output_path}} + + For every exception return exactly one decision drawn both from the item's allowed_decisions and this closed enum: + KEEP_SEPARATE, MERGE_PROVENANCE_UNION, SPLIT, DROP_UNSUPPORTED, SELECT_VALUE, SELECT_LINK, NO_LINK, BLOCK_REVIEW. + Never invent a candidate_ref, source ref, exception id, value, or fact. SELECT_VALUE must use allowed_values. When compact data is insufficient, use BLOCK_REVIEW. + Write exactly one JSON file to {{item.output_path}} with localdocs write_file and overwrite=true. Do not write another file. + The file schema is: + { + "schema_version": "stage1_part2_adjudication_part.v1", + "status": "READY|BLOCKED", + "pack_id": "{{item.pack_id}}", + "exception_type": "{{item.exception_type}}", + "decisions": [ + { + "exception_id": "from item", + "decision": "closed enum", + "candidate_refs": [], + "selected_value": null, + "selected_link": null, + "reason_code": "SOURCE_SUPPORT|IDENTITY_MATCH|NO_SAFE_RESOLUTION|INSUFFICIENT_COMPACT_CONTEXT" + } + ] + } + Return only a compact raw JSON object containing pack_id, status, output_path, and decision_count after the write succeeds. + + - task_name: Task_C_BO_F0_final_bo_gate_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_IDS = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + def _next_id() -> int: + return next(MSG_IDS) + + def _parse_json_first(raw: str, label: str, allow_trailing: bool = False) -> Any: + text = (raw or "").lstrip("\ufeff").strip() + if not text: + raise RuntimeError(f"{label}: empty JSON text") + try: + return json.loads(text) + except json.JSONDecodeError as first_error: + try: + value, end = JSON_DECODER.raw_decode(text) + except json.JSONDecodeError as second_error: + raise RuntimeError(f"{label}: invalid JSON") from second_error + if not allow_trailing and text[end:].strip(): + raise RuntimeError(f"{label}: trailing non-JSON data") from first_error + return value + + def _parse_rpc(raw: str) -> dict[str, Any]: + events: list[dict[str, Any]] = [] + for line in (raw or "").splitlines(): + if not line.startswith("data:"): + continue + candidate = line[5:].strip() + if not candidate or candidate == "[DONE]": + continue + try: + parsed = json.loads(candidate) + except json.JSONDecodeError: + continue + if isinstance(parsed, dict): + events.append(parsed) + if events: + return events[-1] + parsed = _parse_json_first(raw, "MCP response", allow_trailing=True) + if not isinstance(parsed, dict): + raise RuntimeError("MCP response root must be object") + return parsed + + def _init(task_name: str) -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": task_name, + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _call(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_rpc(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP {name} failed: {payload['error']}") + result = payload.get("result") + if not isinstance(result, dict): + raise RuntimeError(f"MCP {name} returned no result") + if result.get("isError") is True: + raise RuntimeError(f"MCP {name} returned isError") + return payload + + def _unwrap(payload: dict[str, Any], label: str) -> Any: + result = payload.get("result") or {} + content = result.get("content") or [] + texts = [block.get("text") for block in content if isinstance(block, dict) and block.get("text")] + if not texts: + raise RuntimeError(f"{label}: empty MCP content") + raw = texts[0] + try: + return _parse_json_first(raw, label, allow_trailing=True) + except RuntimeError: + return raw + + def _extract_doc(payload: dict[str, Any], doc_name: str) -> str: + outer = _unwrap(payload, f"read_docs:{doc_name}") + if isinstance(outer, dict) and isinstance(outer.get("results"), list): + for item in outer["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + if name and not (str(name) == doc_name or str(name).endswith("/" + doc_name)): + continue + value = item.get("content") + if value is None: + value = item.get("text") + if value is None: + value = item.get("raw") + if value is not None: + return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False) + if isinstance(outer, dict) and doc_name in outer: + value = outer[doc_name] + return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False) + if isinstance(outer, str): + return outer + return json.dumps(outer, ensure_ascii=False) + + def _read_text(doc_name: str) -> str: + return _extract_doc(_call("read_docs", {"doc_names": [doc_name]}), doc_name) + + def _read_json(doc_name: str) -> Any: + raw = _read_text(doc_name) + return _parse_json_first(raw, doc_name, allow_trailing=True) + + def _canonical(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + def _sha(value: Any) -> str: + raw = value if isinstance(value, str) else _canonical(value) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _write_json_verified(doc_name: str, value: Any) -> str: + content = json.dumps(value, ensure_ascii=False, indent=2) + _call("write_file", {"path": doc_name, "content": content, "overwrite": True}) + reread = _read_json(doc_name) + if _canonical(reread) != _canonical(value): + raise RuntimeError(f"{doc_name}: post-write verification mismatch") + return _sha(content) + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _uniq(values: list[Any]) -> list[Any]: + seen: set[str] = set() + output: list[Any] = [] + for value in values: + marker = _canonical(value) if isinstance(value, (dict, list)) else str(value) + if marker in seen: + continue + seen.add(marker) + output.append(value) + return output + + def _natural_key(value: Any) -> list[Any]: + return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", str(value))] + + TASK_NAME = "Task_C_BO_F0_final_bo_gate_writer" + R0_RAW = r"""{{prev.Task_C_BO_R0_seed_reducer_exception_planner}}""" + BO_KEYS = ["BO_ID", "id", "BOType", "ActionType", "JuristicAct", "Action", "Reason", "PriorAct", "ReasonRefs", "Legal_Keywords", "core_field_base", "amount", "EvidenceTitles", "Evidence", "source_evidence_indexes", "provenance", "downstream_seed_refs", "extensions"] + CORE_KEYS = ["Performer", "PerformerType", "Action_proposal", "Subject", "Object", "BehaviorTime", "TimeText", "TimePrecision", "StatementType", "Perspective"] + DOWNSTREAM_KEYS = ["claim_group_seed_refs_proposed", "canonical_theory_graph_seed_ref_proposed", "legal_effect_structure_seed_ref_proposed"] + EVIDENCE_KEYS = ["evidence_index", "source_title", "priority_class", "relevant_content", "authentication_status", "corroboration", "selection_basis"] + + def _r0_manifest() -> dict[str, Any]: + parsed = _parse_json_first(R0_RAW, "R0_RAW", allow_trailing=False) + if not isinstance(parsed, dict) or not isinstance(parsed.get("r0_manifest"), dict): + raise RuntimeError("R0 stdout manifest missing") + return parsed["r0_manifest"] + + def _decision_parts(exception_pack: dict[str, Any]) -> dict[str, dict[str, Any]]: + output: dict[str, dict[str, Any]] = {} + for pack in _as_list(exception_pack.get("packs")): + path = str(_as_dict(pack).get("output_path") or "") + part = _read_json(path) + if not isinstance(part, dict) or part.get("schema_version") != "stage1_part2_adjudication_part.v1": + raise RuntimeError(f"invalid adjudication part: {path}") + pack_id = str(pack.get("pack_id")) + if part.get("pack_id") != pack_id or pack_id in output: + raise RuntimeError(f"adjudication pack coverage mismatch: {pack_id}") + expected = set(str(v) for v in _as_list(pack.get("exception_ids"))) + actual = set(str(_as_dict(v).get("exception_id")) for v in _as_list(part.get("decisions"))) + if actual != expected: + raise RuntimeError(f"adjudication exception coverage mismatch: {pack_id}") + output[pack_id] = part + return output + + def _merge_refs(target: dict[str, Any], source: dict[str, Any]) -> None: + for key in ("source_event_candidate_ids", "source_meeting_clause_ids", "source_evidence_indexes"): + target["provenance"][key] = _uniq(_as_list(target["provenance"].get(key)) + _as_list(source["provenance"].get(key))) + target["source_evidence_indexes"] = _uniq(_as_list(target.get("source_evidence_indexes")) + _as_list(source.get("source_evidence_indexes"))) + + def _apply_decisions(ledger: dict[str, Any], parts: dict[str, dict[str, Any]]) -> tuple[list[dict[str, Any]], dict[str, Any]]: + candidates = {item["candidate_ref"]: {**item, "seed": json.loads(json.dumps(item["seed"], ensure_ascii=False))} for item in _as_list(ledger.get("ledger_candidates"))} + removed: set[str] = set() + blocked: set[str] = set() + merged: set[str] = set() + for decision in _as_list(ledger.get("deterministic_decisions")): + if decision.get("decision") != "MERGE_PROVENANCE_UNION": + continue + survivor = str(decision.get("survivor_ref")) + if survivor not in candidates: + raise RuntimeError("deterministic merge survivor missing") + for absorbed in _as_list(decision.get("absorbed_refs")): + absorbed = str(absorbed) + if absorbed not in candidates: + raise RuntimeError("deterministic merge absorbed ref missing") + _merge_refs(candidates[survivor]["seed"], candidates[absorbed]["seed"]) + removed.add(absorbed) + merged.add(absorbed) + for part in parts.values(): + for decision in _as_list(part.get("decisions")): + action = decision.get("decision") + refs = [str(v) for v in _as_list(decision.get("candidate_refs"))] + if any(ref not in candidates for ref in refs): + raise RuntimeError("foreign candidate ref in adjudication") + if action == "MERGE_PROVENANCE_UNION" and len(refs) >= 2: + for absorbed in refs[1:]: + _merge_refs(candidates[refs[0]]["seed"], candidates[absorbed]["seed"]) + removed.add(absorbed) + merged.add(absorbed) + elif action == "DROP_UNSUPPORTED": + for ref in refs: + if _as_list(candidates[ref]["seed"].get("source_evidence_indexes")): + raise RuntimeError("source-backed candidate cannot be dropped") + removed.add(ref) + elif action == "SELECT_VALUE": + for ref in refs: + candidates[ref]["seed"]["ActionType"] = decision.get("selected_value") + elif action == "BLOCK_REVIEW": + blocked.update(refs) + elif action not in {"KEEP_SEPARATE", "SPLIT", "SELECT_LINK", "NO_LINK"}: + raise RuntimeError(f"unsupported adjudication decision: {action}") + final_items = [item for ref, item in candidates.items() if ref not in removed and ref not in blocked] + audit = {"input_count": len(candidates), "final_candidate_count": len(final_items), "merged_refs": sorted(merged), "blocked_refs": sorted(blocked), "removed_refs": sorted(removed)} + if audit["input_count"] != audit["final_candidate_count"] + len(set(audit["merged_refs"] + audit["blocked_refs"] + audit["removed_refs"])): + raise RuntimeError("candidate conservation failed") + return final_items, audit + + def _evidence_item(index: str, evidence_map: dict[str, Any]) -> dict[str, Any]: + record = _as_dict(_as_dict(evidence_map.get("by_evidence_index")).get(index)) + return { + "evidence_index": index, "source_title": record.get("title"), "priority_class": record.get("priority_class"), + "relevant_content": record.get("relevant_content"), "authentication_status": record.get("authentication_status"), + "corroboration": record.get("corroboration"), "selection_basis": "source_evidence_indexes membership", + } + + def _final_bo(item: dict[str, Any], ordinal: int, evidence_map: dict[str, Any]) -> dict[str, Any]: + seed = item["seed"] + seed_core = _as_dict(seed.get("core_field_base")) + parties = _as_list(seed_core.get("parties")) + assets = _as_list(seed_core.get("asset_refs")) + source_indexes = sorted(_uniq([str(v) for v in _as_list(seed.get("source_evidence_indexes"))]), key=_natural_key) + evidence = [_evidence_item(index, evidence_map) for index in source_indexes] + amount = _as_dict(seed.get("amount")) + value = amount.get("value") if "value" in amount else amount.get("numeric_value") + value_text = amount.get("basis") if amount.get("basis") is not None else amount.get("value_text") + downstream = _as_dict(seed.get("downstream_seed_refs")) + bo_id = f"bh{ordinal}" + provenance = _as_dict(seed.get("provenance")) + return { + "BO_ID": bo_id, "id": bo_id, "BOType": seed.get("BOType"), "ActionType": seed.get("ActionType"), + "JuristicAct": seed.get("JuristicAct") if isinstance(seed.get("JuristicAct"), dict) else {"label": None}, + "Action": seed.get("Action"), "Reason": "source evidence 및 event candidate에 의해 독립적으로 확인되는 BO", + "PriorAct": None, "ReasonRefs": [], "Legal_Keywords": _as_list(seed.get("Legal_Keywords")), + "core_field_base": { + "Performer": parties[0] if parties else None, "PerformerType": None, "Action_proposal": seed.get("Action"), + "Subject": parties[1] if len(parties) > 1 else None, "Object": assets[0] if assets else None, + "BehaviorTime": seed_core.get("date"), "TimeText": seed_core.get("time_text"), + "TimePrecision": seed_core.get("time_precision"), "StatementType": seed.get("BOType"), "Perspective": "plaintiff", + }, + "amount": {"value_text": value_text, "numeric_value": value, "currency": amount.get("currency") or "KRW"}, + "EvidenceTitles": _uniq([entry.get("source_title") for entry in evidence if entry.get("source_title")]), + "Evidence": evidence, "source_evidence_indexes": source_indexes, + "provenance": { + "source_event_candidate_ids": _uniq(_as_list(provenance.get("source_event_candidate_ids"))), + "source_meeting_clause_ids": _uniq(_as_list(provenance.get("source_meeting_clause_ids"))), + "source_domain": item.get("source_domain"), + }, + "downstream_seed_refs": { + "claim_group_seed_refs_proposed": _as_list(downstream.get("claim_group_seed")), + "canonical_theory_graph_seed_ref_proposed": downstream.get("issue_cluster_seed"), + "legal_effect_structure_seed_ref_proposed": downstream.get("legal_effect_structure_seed"), + }, + "extensions": seed.get("extensions") if isinstance(seed.get("extensions"), dict) else {"domain_payload": {}}, + } + + def _validate_bo(bo_list: list[dict[str, Any]], source_universe: set[str]) -> None: + for index, bo in enumerate(bo_list, 1): + if list(bo) != BO_KEYS or bo.get("BO_ID") != f"bh{index}" or bo.get("id") != f"bh{index}": + raise RuntimeError(f"BO schema/id mismatch at ordinal {index}") + if list(_as_dict(bo.get("core_field_base"))) != CORE_KEYS: + raise RuntimeError(f"BO core schema mismatch at ordinal {index}") + if list(_as_dict(bo.get("downstream_seed_refs"))) != DOWNSTREAM_KEYS: + raise RuntimeError(f"BO downstream schema mismatch at ordinal {index}") + if any(list(_as_dict(item)) != EVIDENCE_KEYS for item in _as_list(bo.get("Evidence"))): + raise RuntimeError(f"BO Evidence schema mismatch at ordinal {index}") + if any(value not in source_universe for value in _as_list(bo.get("source_evidence_indexes"))): + raise RuntimeError(f"BO source membership mismatch at ordinal {index}") + + def main() -> None: + _init("task-c-bo-f0-final-bo-gate-writer") + r0 = _r0_manifest() + stage_a = _as_dict(_as_dict(_read_json("stage1_tmp/task_c_bo/stage_a_context.json")).get("stage_a_context")) + manifest = _read_json("stage1_tmp/task_c_bo/source_universe_manifest.json") + ledger = _as_dict(_as_dict(_read_json("stage1_tmp/task_c_bo/postb_seed_ledger.json")).get("postb_seed_ledger")) + exception_pack = _read_json("quality_gates/stage1_part2_exception_pack.json") + fingerprints = {stage_a.get("run_fingerprint"), manifest.get("run_fingerprint"), ledger.get("run_fingerprint"), exception_pack.get("run_fingerprint"), r0.get("run_fingerprint")} + if len(fingerprints) != 1 or None in fingerprints: + raise RuntimeError("F0 run fingerprint mismatch") + if ledger.get("status") not in {"READY", "READY_WITH_REVIEW"} or r0.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise RuntimeError("R0 ledger is not finalizable") + if r0.get("pack_count") != exception_pack.get("pack_count"): + raise RuntimeError("R0 pack count mismatch") + parts = _decision_parts(exception_pack) + items, conservation = _apply_decisions(ledger, parts) + items.sort(key=lambda item: (_natural_key(item.get("source_domain")), _natural_key(item.get("candidate_ref")))) + evidence_map = _as_dict(stage_a.get("evidence_authority_map")) + bo_list = [_final_bo(item, ordinal, evidence_map) for ordinal, item in enumerate(items, 1)] + source_universe = set(str(v) for v in _as_list(_as_dict(manifest.get("source_universe")).get("evidence_indexes"))) + _validate_bo(bo_list, source_universe) + bo_text = json.dumps(bo_list, ensure_ascii=False, indent=2) + bo_digest = _sha(bo_text) + compiled = {"postb_compiled_bo_bundle": { + "schema_version": "task_c_bo_postb_compiled_bo_bundle.v1", "status": "READY", + "run_fingerprint": manifest["run_fingerprint"], "bo_count": len(bo_list), "behavior_objects": bo_list, + "candidate_conservation": conservation, + }} + _write_json_verified("stage1_tmp/task_c_bo/postb_compiled_bo_bundle.json", compiled) + _write_json_verified("BO.json", bo_list) + reread_bo = _read_json("BO.json") + _validate_bo(reread_bo, source_universe) + if len(reread_bo) != len(bo_list): + raise RuntimeError("BO post-write count mismatch") + gate = { + "schema_version": "stage1_part2_bo_gate.v1", "status": "PASS", + "run_fingerprint": manifest["run_fingerprint"], "bo_count": len(bo_list), + "candidate_conservation": conservation, + "source_membership": {"status": "PASS", "universe_count": len(source_universe)}, + "adjudication_coverage": {"expected_pack_count": exception_pack.get("pack_count"), "actual_pack_count": len(parts)}, + "schema_gate": {"status": "PASS", "bo_keys": BO_KEYS}, + "post_write": {"file_exists": True, "json_parse_pass": True, "sha256": bo_digest}, + "findings": [], + } + _write_json_verified("quality_gates/stage1_part2_bo_gate.json", gate) + print(json.dumps({"bo_write_report": {"schema_version": "stage1_part2_bo_write_report.v1", "status": "PASS", "run_fingerprint": manifest["run_fingerprint"], "bo_count": len(bo_list), "bo_sha256": bo_digest}}, ensure_ascii=False, separators=(",", ":"))) + + if __name__ == "__main__": + try: + main() + except Exception as exc: + print(json.dumps({"bo_write_report": {"schema_version": "stage1_part2_bo_write_report.v1", "status": "FAILED", "error": str(exc)}}, ensure_ascii=False)) + raise + - task_name: Task_C_BO_S0_signal_bundle_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + from datetime import datetime, timezone + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_IDS = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + def _next_id() -> int: + return next(MSG_IDS) + + def _parse_json_first(raw: str, label: str, allow_trailing: bool = False) -> Any: + text = (raw or "").lstrip("\ufeff").strip() + if not text: + raise RuntimeError(f"{label}: empty JSON text") + try: + return json.loads(text) + except json.JSONDecodeError as first_error: + try: + value, end = JSON_DECODER.raw_decode(text) + except json.JSONDecodeError as second_error: + raise RuntimeError(f"{label}: invalid JSON") from second_error + if not allow_trailing and text[end:].strip(): + raise RuntimeError(f"{label}: trailing non-JSON data") from first_error + return value + + def _parse_rpc(raw: str) -> dict[str, Any]: + events: list[dict[str, Any]] = [] + for line in (raw or "").splitlines(): + if not line.startswith("data:"): + continue + candidate = line[5:].strip() + if not candidate or candidate == "[DONE]": + continue + try: + parsed = json.loads(candidate) + except json.JSONDecodeError: + continue + if isinstance(parsed, dict): + events.append(parsed) + if events: + return events[-1] + parsed = _parse_json_first(raw, "MCP response", allow_trailing=True) + if not isinstance(parsed, dict): + raise RuntimeError("MCP response root must be object") + return parsed + + def _init(task_name: str) -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": task_name, + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _call(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_rpc(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP {name} failed: {payload['error']}") + result = payload.get("result") + if not isinstance(result, dict): + raise RuntimeError(f"MCP {name} returned no result") + if result.get("isError") is True: + raise RuntimeError(f"MCP {name} returned isError") + return payload + + def _unwrap(payload: dict[str, Any], label: str) -> Any: + result = payload.get("result") or {} + content = result.get("content") or [] + texts = [block.get("text") for block in content if isinstance(block, dict) and block.get("text")] + if not texts: + raise RuntimeError(f"{label}: empty MCP content") + raw = texts[0] + try: + return _parse_json_first(raw, label, allow_trailing=True) + except RuntimeError: + return raw + + def _extract_doc(payload: dict[str, Any], doc_name: str) -> str: + outer = _unwrap(payload, f"read_docs:{doc_name}") + if isinstance(outer, dict) and isinstance(outer.get("results"), list): + for item in outer["results"]: + if not isinstance(item, dict): + continue + name = item.get("name") or item.get("path") or item.get("doc_name") or item.get("document_name") + if name and not (str(name) == doc_name or str(name).endswith("/" + doc_name)): + continue + value = item.get("content") + if value is None: + value = item.get("text") + if value is None: + value = item.get("raw") + if value is not None: + return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False) + if isinstance(outer, dict) and doc_name in outer: + value = outer[doc_name] + return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False) + if isinstance(outer, str): + return outer + return json.dumps(outer, ensure_ascii=False) + + def _read_text(doc_name: str) -> str: + return _extract_doc(_call("read_docs", {"doc_names": [doc_name]}), doc_name) + + def _read_json(doc_name: str) -> Any: + raw = _read_text(doc_name) + return _parse_json_first(raw, doc_name, allow_trailing=True) + + def _canonical(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + def _sha(value: Any) -> str: + raw = value if isinstance(value, str) else _canonical(value) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _write_json_verified(doc_name: str, value: Any) -> str: + content = json.dumps(value, ensure_ascii=False, indent=2) + _call("write_file", {"path": doc_name, "content": content, "overwrite": True}) + reread = _read_json(doc_name) + if _canonical(reread) != _canonical(value): + raise RuntimeError(f"{doc_name}: post-write verification mismatch") + return _sha(content) + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _uniq(values: list[Any]) -> list[Any]: + seen: set[str] = set() + output: list[Any] = [] + for value in values: + marker = _canonical(value) if isinstance(value, (dict, list)) else str(value) + if marker in seen: + continue + seen.add(marker) + output.append(value) + return output + + def _natural_key(value: Any) -> list[Any]: + return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", str(value))] + + TASK_NAME = "Task_C_BO_S0_signal_bundle_writer" + LIABILITY_TYPES = { + "B1_Money_Successor": "money_claim", "B2_Secured_Registry": "secured_debt", + "B3_Land_Valuation": "land_use_gain", "B4_Actio": "actio_related", + "B5_Succession_Notice_Lien_Asset_Defense": "notice_or_lien_related", + } + LIABILITY_ENUM = {"money_claim", "commercial_successor", "secured_debt", "registry_invalidity", "land_use_gain", "actio_related", "succession_related", "notice_or_lien_related", "defense_or_setoff", "other"} + ACTIO_SCOPES = {"preserved_claim", "fraudulent_act", "beneficiary_or_transferee", "target_asset", "encumbrance", "remedy_or_cap", "defense"} + SUSPICION_LEVELS = {"NONE", "LOW", "MEDIUM", "HIGH", "CRITICAL"} + ACTIO_SCOPE_ENUM = ACTIO_SCOPES + ROLE_ENUM = {"fraudulent_act_date", "debtor_act", "beneficiary", "transferee", "remedy_or_cap", "defense", "encumbrance"} + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + def _first_string(*values: Any) -> str | None: + for value in values: + for item in _strings(value): + return item + return None + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + + def _flatten_strings(value: Any, limit: int = 10) -> list[str]: + out: list[str] = [] + + def visit(node: Any) -> None: + if len(out) >= limit: + return + if node is None: + return + if isinstance(node, (str, int, float, bool)): + text = str(node).strip() + if text and text not in out: + out.append(text) + return + if isinstance(node, list): + for child in node: + visit(child) + return + if isinstance(node, dict): + for key in ( + "asset_label", "asset", "property", "object", "Object", "description", + "value", "name", "party", "beneficiary", "transferee", "debtor_act", + "remedy", "defense", "encumbrance", "date", "claim_type", + ): + if key in node: + visit(node.get(key)) + visit(value) + return out[:limit] + + def _asset_kind_from_text(text: str) -> str: + lowered = text.lower() + if any(key in text for key in ["부동산", "토지", "건물", "아파트", "대지", "등기", "근저당"]): + return "real_property" + if any(key in text for key in ["채권", "대여금", "매매대금", "임대차보증금"]): + return "claim" + if any(key in text for key in ["근저당", "담보", "저당", "질권", "유치권"]): + return "security_right" + if any(key in text for key in ["예금", "현금", "계좌", "deposit", "cash"]) or "bank" in lowered: + return "cash_or_deposit" + if any(key in text for key in ["영업", "사업", "상호", "business"]) or "company" in lowered: + return "business_asset" + if any(key in text for key in ["동산", "차량", "기계", "movable"]): + return "movable" + return "unknown" + + def _target_property_candidates(payload: dict[str, Any], bo_ids: list[str], evidence: list[str]) -> list[dict[str, Any]]: + sources = [ + payload.get("asset_bindings"), + payload.get("asset_binding_hint"), + payload.get("valuation_matrix"), + payload.get("fraudulent_transfer_facts"), + payload.get("encumbrance_timeline"), + payload.get("target_property_candidates"), + ] + out: list[dict[str, Any]] = [] + for source in sources: + for item in _as_list(source): + labels = _flatten_strings(item, limit=4) + if not labels and isinstance(item, dict): + labels = _strings(item.get("asset_label") or item.get("property_label")) + for label in labels: + candidate = { + "asset_label": label, + "asset_kind": _asset_kind_from_text(label), + "related_bo_ids": bo_ids, + "related_evidence_indexes": evidence, + } + key = json.dumps(candidate, ensure_ascii=False, sort_keys=True) + if key not in {json.dumps(x, ensure_ascii=False, sort_keys=True) for x in out}: + out.append(candidate) + return out + + def _preserved_claim_candidates(payload: dict[str, Any], mapping: dict[str, str], bo_ids: list[str], evidence: list[str]) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for item in _as_list(payload.get("preserved_claim_bundle") or payload.get("preserved_claim_candidates")): + obj = _dict(item) + ref = _first_string(obj.get("candidate_ref"), obj.get("bo_candidate_ref")) + bo_id = mapping.get(ref) if ref else _first_string(obj.get("bo_id")) + if bo_id not in bo_ids and bo_ids: + bo_id = bo_id or None + out.append({ + "bo_id": bo_id, + "claim_type_candidate": _first_string(obj.get("claim_type_candidate"), obj.get("claim_type"), obj.get("type"), obj.get("label")), + "creditor_candidate": _first_string(obj.get("creditor_candidate"), obj.get("creditor"), obj.get("plaintiff")), + "debtor_candidate": _first_string(obj.get("debtor_candidate"), obj.get("debtor"), obj.get("defendant")), + "claim_date_candidate": _first_string(obj.get("claim_date_candidate"), obj.get("claim_date"), obj.get("date"), obj.get("BehaviorTime")), + "related_evidence_indexes": _dedupe(evidence + _strings(obj.get("related_evidence_indexes") or obj.get("source_evidence_indexes"))), + }) + if not out and bo_ids: + out.append({ + "bo_id": bo_ids[0], + "claim_type_candidate": None, + "creditor_candidate": None, + "debtor_candidate": None, + "claim_date_candidate": None, + "related_evidence_indexes": evidence, + }) + return out + + def _role_hint(role: str, value: Any, date: Any, bo_ids: list[str], evidence: list[str]) -> dict[str, Any] | None: + if role not in ROLE_ENUM: + return None + value_candidate = _first_string(value) + date_candidate = _first_string(date) + if value_candidate is None and date_candidate is None: + return None + return { + "role": role, + "value_candidate": value_candidate, + "date_candidate": date_candidate, + "related_bo_ids": bo_ids, + "related_evidence_indexes": evidence, + } + + def _actio_role_hints(payload: dict[str, Any], seed: dict[str, Any], bo_ids: list[str], evidence: list[str]) -> list[dict[str, Any]]: + hints: list[dict[str, Any]] = [] + for fact in _as_list(payload.get("fraudulent_transfer_facts")): + obj = _dict(fact) + for hint in ( + _role_hint("fraudulent_act_date", obj.get("date") or obj.get("act_date"), obj.get("date") or obj.get("act_date"), bo_ids, evidence), + _role_hint("debtor_act", obj.get("debtor_act") or obj.get("action") or obj.get("description"), obj.get("date"), bo_ids, evidence), + ): + if hint: + hints.append(hint) + for party in _as_list(payload.get("beneficiary_transferee_hints")): + obj = _dict(party) + role_text = "transferee" if "transferee" in str(obj).lower() or "양수" in str(obj) else "beneficiary" + hint = _role_hint(role_text, obj.get("party") or obj.get("name") or obj.get("value") or obj, obj.get("date"), bo_ids, evidence) + if hint: + hints.append(hint) + for role, key in [ + ("remedy_or_cap", "remedy_mode_hints"), + ("defense", "defense_rebuttal"), + ("encumbrance", "encumbrance_timeline"), + ]: + for item in _as_list(payload.get(key)): + obj = _dict(item) + hint = _role_hint(role, obj or item, obj.get("date") if obj else None, bo_ids, evidence) + if hint: + hints.append(hint) + if not hints: + action = _first_string(seed.get("Action"), _dict(seed.get("core_field_base")).get("Action_proposal")) + hint = _role_hint("debtor_act", action, _dict(seed.get("core_field_base")).get("BehaviorTime"), bo_ids, evidence) + if hint: + hints.append(hint) + deduped: list[dict[str, Any]] = [] + seen: set[str] = set() + for hint in hints: + key = json.dumps(hint, ensure_ascii=False, sort_keys=True) + if key not in seen: + seen.add(key) + deduped.append(hint) + return deduped + + def _scopes(payload: dict[str, Any]) -> list[str]: + scopes: list[str] = [] + if payload.get("preserved_claim_bundle") or payload.get("preserved_claim_candidates"): + scopes.append("preserved_claim") + if payload.get("fraudulent_transfer_facts") or "actio" in " ".join(_strings(payload.get("legal_effect_tags"))).lower(): + scopes.append("fraudulent_act") + if payload.get("beneficiary_transferee_hints"): + scopes.append("beneficiary_or_transferee") + if payload.get("asset_bindings") or payload.get("asset_binding_hint") or payload.get("valuation_matrix"): + scopes.append("target_asset") + if payload.get("encumbrance_timeline"): + scopes.append("encumbrance") + if payload.get("remedy_mode_hints"): + scopes.append("remedy_or_cap") + if payload.get("defense_rebuttal"): + scopes.append("defense") + return [scope for scope in _dedupe(scopes) if scope in ACTIO_SCOPE_ENUM] + + def _suspicion_level(scopes: list[str]) -> str: + core = {"preserved_claim", "fraudulent_act", "beneficiary_or_transferee"} + if core.issubset(set(scopes)): + return "HIGH" + if {"preserved_claim", "fraudulent_act"}.issubset(set(scopes)): + return "MEDIUM" + if scopes: + return "LOW" + return "NONE" + + def _reasons(scopes: list[str], payload: dict[str, Any], b4_review_queue: list[Any]) -> str: + fragments: list[str] = [] + if "preserved_claim" in scopes: + fragments.append("피보전채권 후보가 B4 seed/domain payload에서 확인됨") + if "fraudulent_act" in scopes: + fragments.append("채무자 처분행위 또는 사해행위 후보 사실이 확인됨") + if "beneficiary_or_transferee" in scopes: + fragments.append("수익자 또는 전득자 후보가 확인됨") + if "target_asset" in scopes: + fragments.append("취소·원상회복 대상 재산 후보가 확인됨") + if "encumbrance" in scopes: + fragments.append("담보권·가압류 등 부담 관련 단서가 확인됨") + if "remedy_or_cap" in scopes: + fragments.append("원상회복 방법 또는 가액배상 한도 단서가 확인됨") + if "defense" in scopes: + fragments.append("항변 또는 선의·악의 관련 반박 단서가 확인됨") + for item in b4_review_queue[:2]: + if isinstance(item, dict): + reason = _first_string(item.get("reason"), item.get("issue"), item.get("review_reason")) + if reason: + fragments.append(f"B4 review queue: {reason}") + return ", ".join(fragments) if fragments else "사해행위취소 관련 구조화 단서가 충분히 확인되지 않음" + + + def _payload(bo: dict[str, Any]) -> dict[str, Any]: + return _as_dict(_as_dict(bo.get("extensions")).get("domain_payload")) + + def _actio_scope(payload: dict[str, Any]) -> list[str]: + scopes: list[str] = [] + if payload.get("preserved_claim_bundle"): + scopes.append("preserved_claim") + if payload.get("fraudulent_transfer_facts"): + scopes.append("fraudulent_act") + if payload.get("encumbrance_timeline"): + scopes.append("encumbrance") + if payload.get("remedy_mode_hints") or payload.get("value_compensation_cap_hints"): + scopes.append("remedy_or_cap") + if payload.get("defense_rebuttal"): + scopes.append("defense") + if payload.get("asset_bindings") or payload.get("valuation_matrix"): + scopes.append("target_asset") + return [value for value in _uniq(scopes) if value in ACTIO_SCOPES] + + def _candidate_types(bo: dict[str, Any]) -> list[str]: + payload = _payload(bo) + values = [payload.get("domain_legal_effect_tag"), bo.get("BOType"), bo.get("ActionType"), _as_dict(bo.get("provenance")).get("source_domain")] + values.extend(_as_list(payload.get("legal_effect_tags"))) + label = _as_dict(bo.get("JuristicAct")).get("label") + if label: + values.append(label) + return _uniq([str(value) for value in values if value not in (None, "")]) + + def _liability_type(domain: str, payload: dict[str, Any]) -> str: + if domain == "B1_Money_Successor" and payload.get("successor_liability_hints"): + return "commercial_successor" + if domain == "B2_Secured_Registry" and payload.get("derivative_invalidity_hints"): + return "registry_invalidity" + if domain == "B5_Succession_Notice_Lien_Asset_Defense": + if payload.get("succession_resolution") or payload.get("general_succession_status") or payload.get("asset_specific_succession_resolution"): + return "succession_related" + if payload.get("setoff_or_payment_defense_hints") or payload.get("simultaneous_performance_hints") or payload.get("defense_rebuttal"): + return "defense_or_setoff" + return LIABILITY_TYPES.get(domain, "other") + + def _validate_signal_payloads(actio: dict[str, Any], liability: dict[str, Any], legal: dict[str, Any], bo_ids: set[str]) -> None: + if actio.get("schema_version") != "actio_case_signals.v1" or liability.get("schema_version") != "case_liability_signals.v1" or legal.get("schema_version") != "legal_effect_signals.v1": + raise RuntimeError("signal schema version mismatch") + checks = [ + (actio.get("actio_case_signals"), {"signal_id", "actio_scope", "suspicion_level", "suspicion_reasons", "related_bo_ids", "related_evidence_indexes", "target_property_candidates", "preserved_claim_candidates", "actio_role_hints"}), + (liability.get("case_liability_signals"), {"liability_candidate_type", "related_bo_ids", "related_evidence_indexes", "party_roles", "claim_group_id", "liability_group_id", "linked_structure_ids"}), + (legal.get("bo_legal_effect_routes"), {"bo_id", "candidate_structure_types", "linked_structure_ids", "related_evidence_indexes"}), + ] + for items, keys in checks: + if not isinstance(items, list): + raise RuntimeError("signal item array missing") + for item in items: + if set(_as_dict(item)) != keys: + raise RuntimeError("signal item key mismatch") + related = _as_list(_as_dict(item).get("related_bo_ids")) + if any(value not in bo_ids for value in related): + raise RuntimeError("signal related BO outside BO.json") + if any(item.get("suspicion_level") not in SUSPICION_LEVELS or any(scope not in ACTIO_SCOPES for scope in _as_list(item.get("actio_scope"))) for item in actio["actio_case_signals"]): + raise RuntimeError("actio signal enum mismatch") + if any(item.get("liability_candidate_type") not in LIABILITY_ENUM for item in liability["case_liability_signals"]): + raise RuntimeError("liability signal enum mismatch") + if any(item.get("bo_id") not in bo_ids for item in legal["bo_legal_effect_routes"]): + raise RuntimeError("legal effect route BO outside BO.json") + + def main() -> None: + _init("task-c-bo-s0-signal-bundle-writer") + gate = _read_json("quality_gates/stage1_part2_bo_gate.json") + manifest = _read_json("stage1_tmp/task_c_bo/source_universe_manifest.json") + ledger = _as_dict(_as_dict(_read_json("stage1_tmp/task_c_bo/postb_seed_ledger.json")).get("postb_seed_ledger")) + _read_json("stage1_tmp/task_c_bo_stage_b_domain_seed_outputs/B4_Actio.json") + bo_list = _read_json("BO.json") + if gate.get("status") != "PASS" or gate.get("run_fingerprint") != manifest.get("run_fingerprint") or ledger.get("run_fingerprint") != manifest.get("run_fingerprint"): + raise RuntimeError("F0 gate or run fingerprint invalid") + bo_digest = _sha(json.dumps(bo_list, ensure_ascii=False, indent=2)) + if gate.get("post_write", {}).get("sha256") != bo_digest: + raise RuntimeError("BO digest mismatch") + bo_ids = {str(bo.get("BO_ID")) for bo in bo_list} + actio_items: list[dict[str, Any]] = [] + liability_items: list[dict[str, Any]] = [] + legal_items: list[dict[str, Any]] = [] + for bo in bo_list: + bo_id = str(bo["BO_ID"]) + evidence = _as_list(bo.get("source_evidence_indexes")) + domain = str(_as_dict(bo.get("provenance")).get("source_domain") or "") + payload = _payload(bo) + scopes = _actio_scope(payload) + if domain == "B4_Actio": + scopes = _scopes(payload) + actio_items.append({ + "signal_id": f"actio-signal-{len(actio_items)+1:03d}", "actio_scope": scopes or ["fraudulent_act"], + "suspicion_level": _suspicion_level(scopes), "suspicion_reasons": _reasons(scopes, payload, []), + "related_bo_ids": [bo_id], "related_evidence_indexes": evidence, + "target_property_candidates": _target_property_candidates(payload, [bo_id], evidence), + "preserved_claim_candidates": _preserved_claim_candidates(payload, {}, [bo_id], evidence), + "actio_role_hints": _actio_role_hints(payload, bo, [bo_id], evidence), + }) + downstream = _as_dict(bo.get("downstream_seed_refs")) + liability_items.append({ + "liability_candidate_type": _liability_type(domain, payload), "related_bo_ids": [bo_id], + "related_evidence_indexes": evidence, "party_roles": [], + "claim_group_id": (_as_list(downstream.get("claim_group_seed_refs_proposed")) or [None])[0], + "liability_group_id": None, "linked_structure_ids": [value for value in [downstream.get("legal_effect_structure_seed_ref_proposed")] if value], + }) + legal_items.append({ + "bo_id": bo_id, "candidate_structure_types": _candidate_types(bo), + "linked_structure_ids": [value for value in [downstream.get("legal_effect_structure_seed_ref_proposed")] if value], + "related_evidence_indexes": evidence, + }) + actio = {"schema_version": "actio_case_signals.v1", "status": "READY_WITH_REVIEW" if actio_items else "READY", "actio_case_signals": actio_items} + liability = {"schema_version": "case_liability_signals.v1", "status": "READY_WITH_REVIEW", "case_liability_signals": liability_items} + legal = {"schema_version": "legal_effect_signals.v1", "status": "READY_WITH_REVIEW", "bo_legal_effect_routes": legal_items} + _validate_signal_payloads(actio, liability, legal, bo_ids) + payloads = {"actio_case_signals.json": actio, "case_liability_signals.json": liability, "legal_effect_signals.json": legal} + reports: dict[str, Any] = {} + for path, payload in payloads.items(): + digest = _write_json_verified(path, payload) + reread = _read_json(path) + item_key = "actio_case_signals" if path.startswith("actio") else ("case_liability_signals" if path.startswith("case") else "bo_legal_effect_routes") + reports[path] = {"count": len(reread[item_key]), "status": reread["status"], "sha256": digest} + print(json.dumps({"signal_bundle_write_report": { + "schema_version": "stage1_part2_signal_bundle_write_report.v1", "status": "PASS", + "run_fingerprint": manifest["run_fingerprint"], "files": reports, + }}, ensure_ascii=False, separators=(",", ":"))) + + if __name__ == "__main__": + try: + main() + except Exception as exc: + print(json.dumps({"signal_bundle_write_report": {"schema_version": "stage1_part2_signal_bundle_write_report.v1", "status": "FAILED", "error": str(exc)}}, ensure_ascii=False)) + raise + task_procedure: + IN: + nexts: + - Task_C_BO_A0_context_domain_slice_compiler + wait_until: [] + Task_C_BO_A0_context_domain_slice_compiler: + nexts: + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + wait_until: + - IN + Task_C_BO_Stage_B_B1_Money_Successor: + nexts: + - Task_C_BO_R0_seed_reducer_exception_planner + wait_until: + - Task_C_BO_A0_context_domain_slice_compiler + Task_C_BO_Stage_B_B2_Secured_Registry: + nexts: + - Task_C_BO_R0_seed_reducer_exception_planner + wait_until: + - Task_C_BO_A0_context_domain_slice_compiler + Task_C_BO_Stage_B_B3_Land_Valuation: + nexts: + - Task_C_BO_R0_seed_reducer_exception_planner + wait_until: + - Task_C_BO_A0_context_domain_slice_compiler + Task_C_BO_Stage_B_B4_Actio: + nexts: + - Task_C_BO_R0_seed_reducer_exception_planner + wait_until: + - Task_C_BO_A0_context_domain_slice_compiler + Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense: + nexts: + - Task_C_BO_R0_seed_reducer_exception_planner + wait_until: + - Task_C_BO_A0_context_domain_slice_compiler + Task_C_BO_R0_seed_reducer_exception_planner: + nexts: + - Task_C_BO_R1_exception_adjudicator_* + - Task_C_BO_F0_final_bo_gate_writer + wait_until: + - Task_C_BO_Stage_B_B1_Money_Successor + - Task_C_BO_Stage_B_B2_Secured_Registry + - Task_C_BO_Stage_B_B3_Land_Valuation + - Task_C_BO_Stage_B_B4_Actio + - Task_C_BO_Stage_B_B5_Succession_Notice_Lien_Asset_Defense + Task_C_BO_R1_exception_adjudicator_*: + nexts: + - Task_C_BO_F0_final_bo_gate_writer + wait_until: + - Task_C_BO_R0_seed_reducer_exception_planner + Task_C_BO_F0_final_bo_gate_writer: + nexts: + - Task_C_BO_S0_signal_bundle_writer + wait_until: + - Task_C_BO_R0_seed_reducer_exception_planner + - all Task_C_BO_R1_exception_adjudicator_* + Task_C_BO_S0_signal_bundle_writer: + nexts: + - OUT + wait_until: + - Task_C_BO_F0_final_bo_gate_writer + OUT: + nexts: [] + wait_until: + - Task_C_BO_S0_signal_bundle_writer diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_3_Claude_v2.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_3_Claude_v2.yml new file mode 100644 index 00000000..38cd3b8a --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_3_Claude_v2.yml @@ -0,0 +1,2250 @@ +--- +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_3 + description: 민사소송 원고 송무 초지능 AI변호사 - Legal Effect 생성 + version: v2.0-claude + Stages: + - name: stage1_legal_effect_생성 + description: Legal Effect Structure 작성 + tools: + mcpServers: + localdocs: + type: streamable-http + url: http://mcp-localdocs:8012/mcp + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + task_procedure: + IN: + nexts: + - Task_L0_structure_seed_reducer + wait_until: [] + Task_L0_structure_seed_reducer: + nexts: + - Task_L1_exception_adjudicator + wait_until: + - IN + Task_L1_exception_adjudicator: + nexts: + - Task_L2_final_structure_index_writer + wait_until: + - Task_L0_structure_seed_reducer + Task_L2_final_structure_index_writer: + nexts: + - OUT + wait_until: + - Task_L1_exception_adjudicator + OUT: + nexts: [] + wait_until: + - Task_L2_final_structure_index_writer + + tasks: + - task_name: Task_L0_structure_seed_reducer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + import sys + from collections import defaultdict + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + INPUT_FILES = [ + "BO.json", + "evidence_indexed.json", + "actio_case_signals.json", + "case_liability_signals.json", + "legal_effect_signals.json", + ] + SEED_OUT_PATH = "stage1_tmp/legal_effect_structure_seed_bundle.json" + PACK_OUT_PATH = "stage1_tmp/legal_effect_structure_exception_pack.json" + AUDIT_OUT_PATH = "stage1_tmp/legal_effect_structure_audit.json" + MAX_EXCERPT = 700 + ISSUE_THRESHOLD = 3 + ISSUE_MARGIN = 2 + STRUCTURE_TYPE_PRIORITY = [ + "money_claim", + "commercial_successor", + "secured_debt", + "registry_invalidity", + "land_use_gain", + "valuation", + "fraudulent_transfer", + "preserved_claim", + "actio_remedy_or_cap", + "succession_notice_lien_asset_defense", + "general_legal_effect", + ] + # X-1: closed canonical routing enum — 이 값만 candidate_structure_types에 들어간다. + CANONICAL_TYPE_SET = set(STRUCTURE_TYPE_PRIORITY) + DOWNSTREAM_ROLE_FAMILY = { + "money_claim": "claim_chain_ref", + "commercial_successor": "claim_chain_ref", + "secured_debt": "linked_structures", + "registry_invalidity": "linked_structures", + "land_use_gain": "legal_calculation_object", + "valuation": "legal_calculation_object", + "fraudulent_transfer": "actio_roles", + "preserved_claim": "claim_chain_ref", + "actio_remedy_or_cap": "linked_actio_structures", + "succession_notice_lien_asset_defense": "linked_structures", + "general_legal_effect": "legal_effect_roles", + } + ISSUE_KEYWORDS = { + "money_claim": ["money", "loan", "대여금", "매매대금", "미지급", "채권", "변제"], + "commercial_successor": ["successor", "상호속용", "영업양수", "승계"], + "secured_debt": ["secured", "담보", "근저당", "피담보", "저당"], + "registry_invalidity": ["registry", "등기", "말소", "무효", "소유권이전"], + "land_use_gain": ["land", "사용이익", "임료", "점유", "인도"], + "valuation": ["valuation", "감정", "평가", "가액", "시가"], + "actio": ["fraudulent", "actio", "사해", "수익자", "전득자", "피보전", "가액배상"], + "asset_defense": ["notice", "lien", "통지", "유치권", "항변", "방어"], + } + HARD_ISSUE_KEY_PRIORITY = ["claim_group_ids", "liability_group_ids", + "canonical_theory_graph_seed_ref", "linked_structure_ids"] + # X-4 방어 상한 (자동 차단 금지 — 초과분은 deterministic_review로 라우팅) + PACK_MAX_ITEMS = 30 + PACK_MAX_BYTES = 40000 + PACK_ITEM_CHAR_LIMIT = 2000 + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-legal-effect-l0-structure-seed-reducer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + + def _loads_tolerant(raw: str, label: str) -> Any: + """SKILL §6.4: strict json.loads 실패 시 raw_decode fallback (read_docs Extra-data 대응).""" + try: + return json.loads(raw) + except json.JSONDecodeError: + try: + obj, _ = JSON_DECODER.raw_decode(raw.strip()) + return obj + except json.JSONDecodeError as exc: + raise RuntimeError(f"Content of {label} is not valid JSON: {raw[:300]}") from exc + + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result.get("result", {}).get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + text = content[0].get("text", "") + envelope = _loads_tolerant(text, doc_name) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + item = results[0] + if isinstance(item, dict) and item.get("error"): + raise RuntimeError(f"read_docs error for {doc_name}: {item.get('error')}") + raw = item.get("content", "") + else: + raw = envelope + if isinstance(raw, str): + return _loads_tolerant(raw, doc_name) + return raw + + + def _read_doc(doc_name: str) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + return _extract_json_from_read_docs_result(result, doc_name) + + + def _write_doc(doc_name: str, payload: dict[str, Any]) -> str: + content = json.dumps(payload, ensure_ascii=False, indent=2) + "\n" + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=_next_msg_id()) + return content + + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + if value is None: + continue + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + + def _first_string(*values: Any) -> str | None: + for value in values: + for item in _strings(value): + return item + return None + + + def _short(value: Any, limit: int = MAX_EXCERPT) -> str | None: + if value is None: + return None + text = re.sub(r"\s+", " ", str(value)).strip() + if not text: + return None + return text[:limit] + + + def _norm_text(text: Any) -> str: + value = re.sub(r"\s+", "", str(text or "").lower()) + value = re.sub(r"[()\[\]{}'\"·,.;:]", "", value) + value = re.sub(r"(에관하여|에대한|및|의|을|를|은|는|이|가)$", "", value) + return value + + + def _asset_norm(label: str) -> str: + text = _norm_text(label) + text = re.sub(r"(지분|전체|소유권|목적물)$", "", text) + return text + + + def _legal_unit_tokens(label: str) -> list[str]: + tokens = [] + for token in ["대지", "토지", "건물", "지분", "근저당", "담보", "점유", "사용이익", "상가", "임야", "채권"]: + if token in str(label): + tokens.append(token) + return tokens + + + def _make_id(prefix: str, ordinal: int) -> str: + return f"{prefix}-{ordinal:03d}" + + + # ---------------------------------------------------------------- 컴파일 (v1 LES0 이식) + def _evidence_index_from_item(item: dict[str, Any]) -> str | None: + return _first_string( + item.get("evidence_index"), + item.get("evidence_id"), + item.get("index"), + item.get("id"), + item.get("source_id"), + ) + + + def _evidence_authority_item(index: str, item: dict[str, Any], source: str) -> dict[str, Any]: + return { + "evidence_index": index, + "source_title": _first_string(item.get("source_title"), item.get("title"), item.get("evidence_title"), item.get("document_title"), index), + "priority_class": _first_string(item.get("priority_class"), item.get("priority"), item.get("evidence_priority")), + "authentication_status": _first_string(item.get("authentication_status"), item.get("auth_status"), item.get("authentication")), + "excerpt": _short(item.get("relevant_content") or item.get("excerpt") or item.get("summary") or item.get("content") or item.get("text")), + "source_file": source, + } + + + def _collect_evidence_authority(evidence_indexed: Any, bo_items: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: + """v1 LES0 이식 — evidence_all 입력 제거판 (포함/제외 산출 동일 실측: 개정 보고서 P3-3).""" + out: dict[str, dict[str, Any]] = {} + + def add(index: str | None, item: dict[str, Any], source: str) -> None: + if not index: + return + new = _evidence_authority_item(index, item, source) + old = out.get(index, {}) + merged = {**new, **{k: v for k, v in old.items() if v not in (None, "", [])}} + if old.get("excerpt") and len(str(old.get("excerpt"))) >= len(str(new.get("excerpt") or "")): + merged["excerpt"] = old.get("excerpt") + out[index] = merged + + def visit(node: Any, source: str) -> None: + if isinstance(node, dict): + idx = _evidence_index_from_item(node) + if idx: + add(idx, node, source) + for key in ("items", "evidence", "documents", "records", "results", "events"): + for child in _list(node.get(key)): + visit(child, source) + elif isinstance(node, list): + for child in node: + visit(child, source) + + visit(evidence_indexed, "evidence_indexed.json") + for bo in bo_items: + for ev in _list(bo.get("Evidence")): + if isinstance(ev, dict): + add(_evidence_index_from_item(ev), ev, "BO.json.Evidence") + return out + + + def _domain_payload(bo: dict[str, Any]) -> dict[str, Any]: + return _dict(_dict(bo.get("extensions")).get("domain_payload")) + + + def _compact_bo(bo: dict[str, Any]) -> dict[str, Any]: + payload = _domain_payload(bo) + core = _dict(bo.get("core_field_base")) + evidence_indexes = _dedupe(_strings(bo.get("source_evidence_indexes")) + [_first_string(ev.get("evidence_index")) for ev in _list(bo.get("Evidence")) if isinstance(ev, dict)]) + evidence_indexes = [idx for idx in evidence_indexes if idx] + return { + "BO_ID": str(bo.get("BO_ID") or bo.get("id") or ""), + "id": str(bo.get("id") or bo.get("BO_ID") or ""), + "BOType": bo.get("BOType"), + "ActionType": bo.get("ActionType"), + "JuristicAct": _dict(bo.get("JuristicAct")), + "Action": bo.get("Action"), + "Reason": bo.get("Reason"), + "PriorAct": bo.get("PriorAct"), + "ReasonRefs": _strings(bo.get("ReasonRefs")), + "Legal_Keywords": _strings(bo.get("Legal_Keywords")), + "core_field_base": core, + "amount": bo.get("amount"), + "source_evidence_indexes": evidence_indexes, + "provenance": _dict(bo.get("provenance")), + "downstream_seed_refs": _dict(bo.get("downstream_seed_refs")), + "domain_payload": { + "domain_legal_effect_tag": payload.get("domain_legal_effect_tag"), + "legal_effect_tags": _strings(payload.get("legal_effect_tags")), + "asset_bindings": _as_list(payload.get("asset_bindings")), + "asset_binding_hint": payload.get("asset_binding_hint"), + "candidate_details": payload.get("candidate_details"), + "facts": payload.get("facts"), + "parties_hint": _as_list(payload.get("parties_hint")), + "action_summary": payload.get("action_summary"), + "title_hint": payload.get("title_hint"), + "summary_hint": payload.get("summary_hint"), + "event_state_type": payload.get("event_state_type"), + "asset_alias": _as_list(payload.get("asset_alias")), + }, + } + + + def _actio_signals_by_bo(data: Any) -> dict[str, list[dict[str, Any]]]: + root = _dict(data) + out: dict[str, list[dict[str, Any]]] = {} + for sig in _list(root.get("actio_case_signals")): + if not isinstance(sig, dict): + continue + compact = { + "signal_id": _first_string(sig.get("signal_id")), + "actio_scope": _strings(sig.get("actio_scope")), + "suspicion_level": sig.get("suspicion_level"), + "related_evidence_indexes": _strings(sig.get("related_evidence_indexes")), + "target_property_candidates": _list(sig.get("target_property_candidates")), + "preserved_claim_candidates": _list(sig.get("preserved_claim_candidates")), + "actio_role_hints": _list(sig.get("actio_role_hints")), + } + for bo_id in _strings(sig.get("related_bo_ids")): + out.setdefault(bo_id, []).append(compact) + for item in _list(sig.get("target_property_candidates")): + if isinstance(item, dict): + for bo_id in _strings(item.get("related_bo_ids")): + out.setdefault(bo_id, []).append(compact) + return out + + + def _case_signals_by_bo(data: Any) -> dict[str, list[dict[str, Any]]]: + root = _dict(data) + out: dict[str, list[dict[str, Any]]] = {} + for idx, sig in enumerate(_list(root.get("case_liability_signals")), start=1): + if not isinstance(sig, dict): + continue + compact = { + "signal_ref": f"case-liability-signal-{idx:03d}", + "liability_candidate_type": sig.get("liability_candidate_type"), + "related_evidence_indexes": _strings(sig.get("related_evidence_indexes")), + "party_roles": _list(sig.get("party_roles")), + "claim_group_id": sig.get("claim_group_id"), + "liability_group_id": sig.get("liability_group_id"), + "linked_structure_ids": _strings(sig.get("linked_structure_ids")), + } + bo_ids = _strings(sig.get("related_bo_ids")) + for role in _list(sig.get("party_roles")): + if isinstance(role, dict): + bo_ids.extend(_strings(role.get("bo_ids"))) + for bo_id in _dedupe(bo_ids): + out.setdefault(bo_id, []).append(compact) + return out + + + def _legal_routes_by_bo(data: Any) -> dict[str, list[dict[str, Any]]]: + root = _dict(data) + out: dict[str, list[dict[str, Any]]] = {} + for route in _list(root.get("bo_legal_effect_routes")): + if not isinstance(route, dict): + continue + bo_id = _first_string(route.get("bo_id")) + if not bo_id: + continue + out.setdefault(bo_id, []).append({ + "candidate_structure_types": _strings(route.get("candidate_structure_types")), + "linked_structure_ids": _strings(route.get("linked_structure_ids")), + "related_evidence_indexes": _strings(route.get("related_evidence_indexes")), + }) + return out + + + def _normalize_structure_type(value: str) -> str: + text = str(value).strip().lower() + if any(x in text for x in ["money", "loan", "대여금", "매매대금", "미지급"]): + return "money_claim" + if any(x in text for x in ["successor", "상호속용", "영업양수"]): + return "commercial_successor" + if any(x in text for x in ["secured", "담보", "근저당", "피담보"]): + return "secured_debt" + if any(x in text for x in ["registry", "등기", "말소", "무효"]): + return "registry_invalidity" + if any(x in text for x in ["land", "사용이익", "임료", "점유"]): + return "land_use_gain" + if any(x in text for x in ["valuation", "감정", "평가", "가액", "cap"]): + return "valuation" + if any(x in text for x in ["fraudulent", "actio", "사해", "수익자", "전득자"]): + return "fraudulent_transfer" + if any(x in text for x in ["preserved", "피보전"]): + return "preserved_claim" + if any(x in text for x in ["remedy", "원상회복", "가액배상"]): + return "actio_remedy_or_cap" + if any(x in text for x in ["succession", "notice", "lien", "asset_defense", "승계", "통지", "유치권", "인도", "항변"]): + return "succession_notice_lien_asset_defense" + return text.replace(" ", "_") if text else "general_legal_effect" + + + def _candidate_structure_types(bo: dict[str, Any], signals: dict[str, list[dict[str, Any]]]) -> tuple[list[str], list[str]]: + """X-1: canonical routing enum만 candidate_structure_types로. 비-canonical은 nonrouting_labels로 보존.""" + types: list[str] = [] + for route in signals.get("legal_effect", []): + types.extend(_strings(route.get("candidate_structure_types"))) + for case_sig in signals.get("case_liability", []): + types.extend(_strings(case_sig.get("liability_candidate_type"))) + for actio_sig in signals.get("actio", []): + for scope in _strings(actio_sig.get("actio_scope")): + mapping = { + "fraudulent_act": "fraudulent_transfer", + "preserved_claim": "preserved_claim", + "remedy_or_cap": "actio_remedy_or_cap", + "target_asset": "actio_asset_target", + "beneficiary_or_transferee": "fraudulent_transfer", + "encumbrance": "secured_debt", + "defense": "succession_notice_lien_asset_defense", + } + if scope in mapping: + types.append(mapping[scope]) + payload = _dict(bo.get("domain_payload")) + types.extend(_strings(payload.get("domain_legal_effect_tag"))) + types.extend(_strings(payload.get("legal_effect_tags"))) + source_domain = _first_string(_dict(bo.get("provenance")).get("source_domain")) + if source_domain: + if "B1" in source_domain: + types.append("money_claim") + elif "B2" in source_domain: + types.append("secured_debt") + elif "B3" in source_domain: + types.append("land_use_gain") + elif "B4" in source_domain: + types.append("fraudulent_transfer") + elif "B5" in source_domain: + types.append("succession_notice_lien_asset_defense") + canonical: list[str] = [] + nonrouting: list[str] = [] + for value in types: + if not value: + continue + norm = _normalize_structure_type(value) + if norm in CANONICAL_TYPE_SET: + canonical.append(norm) + else: + nonrouting.append(norm) + return _dedupe(canonical), _dedupe(nonrouting) + + + def _asset_kind(label: str, fallback: Any = None) -> str: + if fallback: + text = str(fallback) + if text in {"real_property", "movable", "claim", "security_right", "cash_or_deposit", "business_asset", "other", "unknown"}: + return text + if any(x in label for x in ["토지", "대지", "건물", "부동산", "아파트", "상가", "임야", "등기"]): + return "real_property" + if any(x in label for x in ["근저당", "담보", "저당", "유치권", "질권"]): + return "security_right" + if any(x in label for x in ["채권", "대여금", "매매대금", "보증금"]): + return "claim" + if any(x in label for x in ["예금", "현금", "계좌"]): + return "cash_or_deposit" + if any(x in label for x in ["영업", "사업", "상호"]): + return "business_asset" + if any(x in label for x in ["동산", "차량", "기계"]): + return "movable" + return "unknown" + + + def _asset_strings(value: Any) -> list[str]: + out: list[str] = [] + + def visit(node: Any) -> None: + if node is None or len(out) >= 20: + return + if isinstance(node, (str, int, float)): + text = str(node).strip() + if text and len(text) <= 180: + out.append(text) + return + if isinstance(node, list): + for child in node: + visit(child) + return + if isinstance(node, dict): + for key in [ + "asset_label", "asset", "assets", "property", "properties", "object", "objects", + "collateral", "collaterals", "target", "official_designation", "alias", + ]: + if key in node: + visit(node.get(key)) + visit(value) + return _dedupe(out) + + + def _dedupe_assets(items: list[dict[str, Any]]) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + seen: set[str] = set() + for item in items: + key = json.dumps({ + "asset_label": item.get("asset_label"), + "asset_kind": item.get("asset_kind"), + "source": item.get("source"), + # X-3: ref는 자산 identity 정보 — 상이 ref 행을 중복으로 소거하지 않는다 + "asset_ref_keys": sorted(_strings(item.get("asset_ref_keys"))), + }, ensure_ascii=False, sort_keys=True) + if key not in seen: + seen.add(key) + out.append(item) + return out + + + def _extract_asset_candidates(bo: dict[str, Any], signals: dict[str, list[dict[str, Any]]]) -> list[dict[str, Any]]: + bo_id = bo.get("BO_ID") + evidence = _strings(bo.get("source_evidence_indexes")) + out: list[dict[str, Any]] = [] + for sig in signals.get("actio", []): + for item in _list(sig.get("target_property_candidates")): + if isinstance(item, dict): + label = _first_string(item.get("asset_label")) + if label: + out.append({ + "asset_label": label, + "asset_kind": _asset_kind(label, item.get("asset_kind")), + "source": "actio_case_signals.target_property_candidates", + "related_bo_ids": _dedupe([bo_id] + _strings(item.get("related_bo_ids"))), + "related_evidence_indexes": _dedupe(evidence + _strings(item.get("related_evidence_indexes"))), + }) + payload = _dict(bo.get("domain_payload")) + for source_key in ["asset_bindings", "asset_binding_hint", "candidate_details", "facts"]: + values = _asset_strings(payload.get(source_key)) + for label in values: + out.append({ + "asset_label": label, + "asset_kind": _asset_kind(label), + "source": f"BO.extensions.domain_payload.{source_key}", + "related_bo_ids": [bo_id] if bo_id else [], + "related_evidence_indexes": evidence, + }) + for alias in _as_list(payload.get("asset_alias")): + if not isinstance(alias, dict): + continue + ref_keys = _strings(alias.get("schedule_refs")) + for reg in _as_list(alias.get("registry_refs")): + if isinstance(reg, dict): + ref_keys.extend(_strings(reg.get("registry_ref"))) + else: + ref_keys.extend(_strings(reg)) + alias_labels = _strings(alias.get("alias_strings")) + _strings(alias.get("canonical_label_candidate")) + for label in alias_labels or ([ref_keys[0]] if ref_keys else []): + out.append({ + "asset_label": label, + "asset_kind": _asset_kind(label), + "source": "BO.extensions.domain_payload.asset_alias", + "asset_ref_keys": _dedupe(ref_keys), + "related_bo_ids": [bo_id] if bo_id else [], + "related_evidence_indexes": evidence, + }) + core_obj = _first_string(_dict(bo.get("core_field_base")).get("Object")) + if core_obj: + out.append({ + "asset_label": core_obj, + "asset_kind": _asset_kind(core_obj), + "source": "BO.core_field_base.Object", + "related_bo_ids": [bo_id] if bo_id else [], + "related_evidence_indexes": evidence, + }) + return _dedupe_assets(out) + + + def _require_signal_root(data: Any, doc_name: str, schema_version: str, array_key: str) -> None: + root = _dict(data) + if root.get("schema_version") != schema_version: + raise ValueError(f"{doc_name} schema_version must be {schema_version}") + if root.get("status") not in {"READY", "READY_WITH_REVIEW", "EMPTY"}: + raise ValueError(f"{doc_name} status must be READY, READY_WITH_REVIEW, or EMPTY") + if not isinstance(root.get(array_key), list): + raise ValueError(f"{doc_name}.{array_key} must be array") + + + # ---------------------------------------------------------------- 결정 로직 (v1 LES1 이식 + X-2/X-3/X-4/X-5) + def _score_issue(candidate: dict[str, Any]) -> list[dict[str, Any]]: + fields = _dict(candidate.get("keyword_fields")) + text = " ".join( + _strings(fields.get("Legal_Keywords")) + + _strings(fields.get("Action")) + + _strings(fields.get("Reason")) + + _strings(fields.get("JuristicAct_label")) + + _strings(fields.get("ActionType")) + + _strings(fields.get("domain_legal_effect_tag")) + + _strings(fields.get("legal_effect_tags")) + + _strings(candidate.get("candidate_structure_types")) + + _strings(candidate.get("source_domain")) + ).lower() + rows = [] + for family, keywords in ISSUE_KEYWORDS.items(): + score = sum(1 for keyword in keywords if keyword.lower() in text) + if score: + rows.append({"candidate_value": family, "score": score, "basis": [kw for kw in keywords if kw.lower() in text]}) + rows.sort(key=lambda row: (-row["score"], row["candidate_value"])) + return rows + + + def _hard_issue_keys(candidate: dict[str, Any]) -> list[dict[str, Any]]: + hard = _dict(candidate.get("hard_issue_keys")) + rows: list[dict[str, Any]] = [] + for field in ["claim_group_ids", "liability_group_ids", "linked_structure_ids", "canonical_theory_graph_seed_ref"]: + for value in _strings(hard.get(field)): + rows.append({"field": field, "value": value}) + return rows + + + def _issue_decision(candidate: dict[str, Any]) -> tuple[str, dict[str, Any], list[str]]: + """v1 이식 + 개정: llm_required 분기를 deterministic_review로 전환 (예외 생성 없음 — X-4 ④ 불충족 계열).""" + bo_id = candidate["bo_id"] + hard_rows = _hard_issue_keys(candidate) + score_rows = _score_issue(candidate) + conflict_flags: list[str] = [] + hard_by_field = {field: _strings(_dict(candidate.get("hard_issue_keys")).get(field)) + for field in HARD_ISSUE_KEY_PRIORITY} + intra_conflict_fields = [field for field, values in hard_by_field.items() if len(values) > 1] + if intra_conflict_fields: + conflict_flags.append("multiple_hard_issue_keys") + else: + for field in HARD_ISSUE_KEY_PRIORITY: + if hard_by_field[field]: + demoted = {f: hard_by_field[f] for f in HARD_ISSUE_KEY_PRIORITY + if f != field and hard_by_field[f]} + return f"hard:{hard_by_field[field][0]}", { + "decision": "hard_key_priority", + "confidence": "HIGH", + "basis_fields": [field], + "demoted_hard_keys": demoted, + "conflict_flags": [], + }, [] + if score_rows: + top = score_rows[0] + second = score_rows[1]["score"] if len(score_rows) > 1 else 0 + if top["score"] >= ISSUE_THRESHOLD and top["score"] - second >= ISSUE_MARGIN and not conflict_flags: + return f"issue:{top['candidate_value']}", { + "decision": "score_confident", + "confidence": "MEDIUM", + "basis_fields": ["Legal_Keywords", "Action", "Reason", "JuristicAct.label", "domain tags"], + "conflict_flags": [], + }, [] + types = set(_strings(candidate.get("candidate_structure_types"))) + if {"money_claim", "fraudulent_transfer"}.issubset(types): + conflict_flags.append("money_claim_and_actio_same_bo") + if {"registry_invalidity", "secured_debt"}.issubset(types): + conflict_flags.append("registry_invalidity_and_secured_debt_same_bo") + candidate_pool = _dedupe([row["value"] for row in hard_rows] + [row["candidate_value"] for row in score_rows]) + if len(candidate_pool) == 1 and not conflict_flags: + return f"issue:{candidate_pool[0]}", { + "decision": "auto_selected_single_candidate", + "confidence": "MEDIUM", + "basis_fields": ["hard_issue_keys", "score_table"], + "conflict_flags": [], + }, [] + if not hard_rows and not score_rows: + return f"review_issue:{bo_id}", { + "decision": "deterministic_review", + "confidence": "LOW", + "basis_fields": [], + "conflict_flags": ["missing_candidate_values"], + }, ["missing_candidate_values"] + # 개정: compact 근거로 확정 불충분(스코어 저신뢰·다후보)한 issue 모호성은 LLM행이 아니라 + # 보존적 defer — 별도 issue cluster로 유지하고 review code만 남긴다 (X-4 ④ 불충족). + return f"review_issue:{bo_id}", { + "decision": "deterministic_review", + "confidence": "LOW", + "basis_fields": ["hard_issue_keys", "Legal_Keywords", "Action", "Reason", "JuristicAct.label", "candidate_structure_types"], + "conflict_flags": conflict_flags or ["insufficient_issue_confidence"], + }, ["ambiguous_issue_cluster_deferred"] + + + def _structure_type_decision(candidate: dict[str, Any]) -> tuple[str, list[str], dict[str, Any]]: + """X-2: canonical 복수 타입은 예외가 아니다 — 전부 보존 + priority 대표. 예외 생성 경로 없음. + (현행 최종 스키마에는 타입 간 상호 배타 필드가 존재하지 않아 X-4 ① 조건을 충족하는 + type 충돌이 성립하지 않는다 — 개정 전략서 §5-4.)""" + types = _dedupe(_strings(candidate.get("candidate_structure_types"))) or ["general_legal_effect"] + roles = {DOWNSTREAM_ROLE_FAMILY.get(t, "legal_effect_roles") for t in types} + representative = sorted(types, key=lambda t: STRUCTURE_TYPE_PRIORITY.index(t) if t in STRUCTURE_TYPE_PRIORITY else 999)[0] + diag = { + "decision": "deterministic_multi_routing" if len(types) > 1 else "deterministic", + "candidate_types": types, + "downstream_roles": sorted(roles), + "representative_candidate_structure_type": representative, + "basis_fields": ["legal_effect_signals", "case_liability_signals", "actio_case_signals", "BO.downstream_seed_refs", "BO.domain_payload"], + } + return representative, types, diag + + + def _build_asset_alias_map(candidates: dict[str, dict[str, Any]]) -> dict[str, list[list[str]]]: + """X-3(b): label(정규화) ↔ ref 계열(lineage) map. 같은 alias 항목에 함께 기재되었거나 + ref가 겹치는 집합은 하나의 lineage로 통합한다 (등기행 다수 + 별지목록 = 동일 자산의 정상 기재). + 한 라벨이 서로소인 lineage 2개 이상을 주장할 때만 진짜 충돌이다.""" + label_to_refsets: dict[str, list[set[str]]] = defaultdict(list) + for bo_id in sorted(candidates): + for asset in _list(candidates[bo_id].get("asset_candidates")): + if not isinstance(asset, dict): + continue + refs = set(_strings(asset.get("asset_ref_keys"))) + label = asset.get("asset_label") + if not refs or not label: + continue + sets = label_to_refsets[_asset_norm(str(label))] + merged = False + for existing in sets: + if existing & refs: + existing |= refs + merged = True + break + if not merged: + sets.append(set(refs)) + out: dict[str, list[list[str]]] = {} + for key, sets in label_to_refsets.items(): + changed = True + while changed: + changed = False + for i in range(len(sets)): + for j in range(i + 1, len(sets)): + if sets[i] & sets[j]: + sets[i] |= sets[j] + del sets[j] + changed = True + break + if changed: + break + out[key] = sorted((sorted(s, key=_ref_sort) for s in sets), key=lambda s: s[0]) + return out + + + def _ref_sort(ref_key: str) -> tuple[int, str]: + return (0 if ref_key.startswith("REG-") else 1 if ref_key.startswith("SCH-") else 2, ref_key) + + + def _asset_decisions(candidate: dict[str, Any], alias_map: dict[str, set[str]], + key_alias: dict[str, str]) -> tuple[list[str], list[dict[str, Any]], list[dict[str, Any]], list[str], list[dict[str, Any]]]: + """X-3 ref-first 규칙: + (a) 동일 REG-*/SCH-* ref 공유 → 동일 클러스터 (v1 keyed 그룹핑 유지) + (b) unkeyed 라벨이 alias map에서 단일 ref 계열로 연결 → 결정론 병합 (deterministic_resolution) + (c) 한 BO에 서로 다른 ref 복수 → 클러스터 분리 + multi_asset_bundle 보존 (예외 아님) + (d) alias 상충(한 라벨이 2+ 상이 ref 계열 주장) → X-4 4중 조건 충족 → pack 예외 + (e) ref/alias 근거 없는 자유문구 동일성 불명(kind/legal unit 상이) → SEPARATE 보존 + review code + 반환: (cluster_keys, bundle_items, pack_exceptions, review_codes, resolutions)""" + bo_id = candidate["bo_id"] + assets = [item for item in _list(candidate.get("asset_candidates")) if isinstance(item, dict) and item.get("asset_label")] + if not assets: + return [], [], [], [], [] + cluster_keys: list[str] = [] + bundle_items: list[dict[str, Any]] = [] + pack_exceptions: list[dict[str, Any]] = [] + review_codes: list[str] = [] + resolutions: list[dict[str, Any]] = [] + + def _root_for_refs(refs: list[str]) -> str: + existing = next((key_alias[r] for r in refs if r in key_alias), None) + root = existing or f"ref:{sorted(refs, key=_ref_sort)[0]}" + for r in refs: + key_alias[r] = root + return root + + groups: dict[str, list[dict[str, Any]]] = defaultdict(list) + packed_labels: set[str] = set() + for asset in assets: + label_norm = _asset_norm(str(asset.get("asset_label"))) + lineages_all = alias_map.get(label_norm) or [] + if len(lineages_all) > 1: + # (d) keyed/unkeyed 불문: 한 라벨이 서로소 ref lineage 2+를 주장 — non-deferrable pack + if label_norm not in packed_labels: + packed_labels.add(label_norm) + pack_exceptions.append({ + "exception_id": "", + "exception_type": "ambiguous_asset_cluster", + "source_bo_ids": [bo_id], + "candidate_values": _dedupe([str(asset.get("asset_label"))] + [lineage[0] for lineage in lineages_all]), + "basis_fields": ["asset_alias", str(asset.get("source"))], + "score_summary": [{"candidate_value": "|".join(lineage), "basis": "disjoint ref lineage"} for lineage in lineages_all[:3]], + "conflict_flags": ["label_claims_disjoint_ref_lineages"], + "allowed_decisions": ["SAME_CLUSTER", "SEPARATE_CLUSTER", "MULTI_ASSET_BUNDLE", "BLOCK_REVIEW"], + "compact_context": {"lineages": lineages_all, "asset_kind": asset.get("asset_kind")}, + }) + cluster_keys.append(f"review_asset:{bo_id}:{len(pack_exceptions)}") + continue + refs = _strings(asset.get("asset_ref_keys")) + if refs: + groups[_root_for_refs(refs)].append(asset) + continue + if lineages_all: + # (b) 단일 lineage — alias 근거 결정론 병합 + root = _root_for_refs(list(lineages_all[0])) + groups[root].append(asset) + resolutions.append({ + "kind": "alias_merged", + "bo_id": bo_id, + "asset_label": asset.get("asset_label"), + "ref_root": root, + "basis": "asset_alias ref-label linkage (X-3b)", + }) + continue + groups[f"{label_norm}::{asset.get('asset_kind') or 'unknown'}"].append(asset) + + for key, group in groups.items(): + if key.startswith("ref:"): + cluster_keys.append(f"asset:{key}") + continue + labels = _dedupe([str(item.get("asset_label")) for item in group]) + kinds = set(_strings([item.get("asset_kind") for item in group])) + legal_units = {tuple(_legal_unit_tokens(label)) for label in labels} + if len(kinds) == 1 and len(legal_units) <= 1: + cluster_keys.append(f"asset:{key}") + else: + # (e) 개정: v1의 LLM 예외 대신 라벨별 SEPARATE 보존 + review code (fuzzy 병합 금지) + for label in labels: + cluster_keys.append(f"asset:{_asset_norm(label)}::{_asset_kind(label)}") + review_codes.append("asset_identity_unresolved_separate_preserved") + resolutions.append({ + "kind": "separate_preserved", + "bo_id": bo_id, + "labels": labels, + "basis": "kind/legal-unit divergence without ref linkage (X-3e)", + }) + normalized_roots: set[str] = set() + for item in assets: + item_refs = _strings(item.get("asset_ref_keys")) + if item_refs and item_refs[0] in key_alias: + normalized_roots.add(key_alias[item_refs[0]]) + else: + label_norm = _asset_norm(str(item.get("asset_label"))) + lineages = alias_map.get(label_norm) or [] + if len(lineages) == 1 and lineages[0] and lineages[0][0] in key_alias: + normalized_roots.add(key_alias[lineages[0][0]]) + else: + normalized_roots.add(label_norm) + if len(normalized_roots) > 1: + bundle_items.append({ + "bundle_ref": f"bundle:{bo_id}", + "bundle_type": "multi_asset_bundle", + "source_bo_ids": [bo_id], + "member_asset_labels": _dedupe([str(item.get("asset_label")) for item in assets]), + }) + return _dedupe(cluster_keys), bundle_items, pack_exceptions, _dedupe(review_codes), resolutions + + + def _merge_exceptions(exceptions: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + """v1 서명 병합 이식 (단일 리스트 형태).""" + merged: dict[str, dict[str, Any]] = {} + for exc in exceptions: + signature = json.dumps({ + "type": exc.get("exception_type"), + "candidates": sorted(_strings(exc.get("candidate_values"))), + "flags": sorted(_strings(exc.get("conflict_flags"))), + "decisions": sorted(_strings(exc.get("allowed_decisions"))), + }, ensure_ascii=False, sort_keys=True) + if signature in merged: + root = merged[signature] + root["source_bo_ids"] = _dedupe(_strings(root.get("source_bo_ids")) + _strings(exc.get("source_bo_ids"))) + root["merged_from_count"] = root.get("merged_from_count", 1) + 1 + else: + merged[signature] = {**exc, "merged_from_count": 1} + ordered = sorted( + merged.values(), + key=lambda item: ( + min(_strings(item.get("source_bo_ids")) or [""]), + json.dumps(sorted(_strings(item.get("candidate_values"))), ensure_ascii=False), + ), + ) + merge_groups: list[dict[str, Any]] = [] + for ordinal, exc in enumerate(ordered, start=1): + exc["exception_id"] = _make_id("LEX", ordinal) + if exc.get("merged_from_count", 1) > 1: + merge_groups.append({ + "representative_exception_id": exc["exception_id"], + "source_bo_ids": _strings(exc.get("source_bo_ids")), + "merged_from_count": exc["merged_from_count"], + }) + return ordered, merge_groups + + + def _add_issue_seed(target: dict[str, dict[str, Any]], issue_id: str, issue_key: str, item: dict[str, Any], candidate: dict[str, Any], diag: dict[str, Any]) -> None: + row = target.setdefault(issue_id, { + "issue_cluster_id": issue_id, + "issue_key": issue_key, + "issue_type": issue_key.split(":", 1)[-1], + "source_bo_ids": [], + "candidate_structure_types": [], + "claim_group_ids": [], + "liability_group_ids": [], + "evidence_indexes": [], + "asset_cluster_ids": [], + "basis_fields": [], + "pending_exception_ids": [], + "deterministic_review_codes": [], + }) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + item["source_bo_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + item["candidate_structure_types"]) + hard = _dict(candidate.get("hard_issue_keys")) + row["claim_group_ids"] = _dedupe(row["claim_group_ids"] + _strings(hard.get("claim_group_ids"))) + row["liability_group_ids"] = _dedupe(row["liability_group_ids"] + _strings(hard.get("liability_group_ids"))) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + item["evidence_indexes"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + item["asset_cluster_ids"]) + row["basis_fields"] = _dedupe(row["basis_fields"] + _strings(diag.get("basis_fields"))) + row["pending_exception_ids"] = _dedupe(row["pending_exception_ids"] + item["pending_exception_ids"]) + row["deterministic_review_codes"] = _dedupe(row["deterministic_review_codes"] + item["deterministic_review_codes"]) + + + def _add_asset_seeds(target: dict[str, dict[str, Any]], asset_ids: list[str], item: dict[str, Any], candidate: dict[str, Any]) -> None: + assets = [asset for asset in _list(candidate.get("asset_candidates")) if isinstance(asset, dict)] + for asset_id in asset_ids: + row = target.setdefault(asset_id, { + "asset_cluster_id": asset_id, + "canonical_asset_label": None, + "asset_kind": None, + "member_asset_mentions": [], + "source_bo_ids": [], + "evidence_indexes": [], + "issue_cluster_ids": [], + "candidate_structure_types": [], + "basis_fields": [], + "pending_exception_ids": [], + "deterministic_review_codes": [], + }) + related_assets = assets or [] + labels = _dedupe([str(asset.get("asset_label")) for asset in related_assets if asset.get("asset_label")]) + if row["canonical_asset_label"] is None and labels: + row["canonical_asset_label"] = labels[0] + kinds = _dedupe([str(asset.get("asset_kind")) for asset in related_assets if asset.get("asset_kind")]) + if row["asset_kind"] is None and kinds: + row["asset_kind"] = kinds[0] + row["member_asset_mentions"] = _dedupe(row["member_asset_mentions"] + labels) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + item["source_bo_ids"]) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + item["evidence_indexes"]) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + item["issue_cluster_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + item["candidate_structure_types"]) + row["basis_fields"] = _dedupe(row["basis_fields"] + [str(asset.get("source")) for asset in related_assets if asset.get("source")]) + row["pending_exception_ids"] = _dedupe(row["pending_exception_ids"] + item["pending_exception_ids"]) + row["deterministic_review_codes"] = _dedupe(row["deterministic_review_codes"] + item["deterministic_review_codes"]) + + + def _add_evidence_index(target: dict[str, dict[str, Any]], evidence_indexes: list[str], item: dict[str, Any], authority: dict[str, Any]) -> None: + for idx in evidence_indexes: + auth = _dict(authority.get(idx)) + row = target.setdefault(idx, { + "evidence_index": idx, + "source_title": auth.get("source_title"), + "priority_class": auth.get("priority_class"), + "authentication_status": auth.get("authentication_status"), + "seed_ids": [], + "source_bo_ids": [], + "issue_cluster_ids": [], + "asset_cluster_ids": [], + "candidate_structure_types": [], + "gap_flags": [], + }) + row["seed_ids"] = _dedupe(row["seed_ids"] + [item["seed_id"]]) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + item["source_bo_ids"]) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + item["issue_cluster_ids"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + item["asset_cluster_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + item["candidate_structure_types"]) + if not auth: + row["gap_flags"] = _dedupe(row["gap_flags"] + ["missing_evidence_authority"]) + elif not auth.get("excerpt"): + row["gap_flags"] = _dedupe(row["gap_flags"] + ["missing_evidence_excerpt"]) + + + def main() -> None: + try: + _init_mcp_session() + docs = {name: _read_doc(name) for name in INPUT_FILES} + bo_items = docs["BO.json"] + if not isinstance(bo_items, list): + raise ValueError("BO.json root must be array") + _require_signal_root(docs["actio_case_signals.json"], "actio_case_signals.json", "actio_case_signals.v1", "actio_case_signals") + _require_signal_root(docs["case_liability_signals.json"], "case_liability_signals.json", "case_liability_signals.v1", "case_liability_signals") + _require_signal_root(docs["legal_effect_signals.json"], "legal_effect_signals.json", "legal_effect_signals.v1", "bo_legal_effect_routes") + evidence_authority = _collect_evidence_authority(docs["evidence_indexed.json"], bo_items) + compact_bo_items = [_compact_bo(bo) for bo in bo_items if isinstance(bo, dict)] + bo_by_id = {bo["BO_ID"]: bo for bo in compact_bo_items if bo.get("BO_ID")} + actio_by_bo = _actio_signals_by_bo(docs["actio_case_signals.json"]) + case_by_bo = _case_signals_by_bo(docs["case_liability_signals.json"]) + legal_by_bo = _legal_routes_by_bo(docs["legal_effect_signals.json"]) + + candidates: dict[str, dict[str, Any]] = {} + nonrouting_by_bo: dict[str, list[str]] = {} + input_warnings: list[dict[str, Any]] = [] + for bo_id, bo in bo_by_id.items(): + signals = { + "actio": actio_by_bo.get(bo_id, []), + "case_liability": case_by_bo.get(bo_id, []), + "legal_effect": legal_by_bo.get(bo_id, []), + } + evidence_indexes = _dedupe( + _strings(bo.get("source_evidence_indexes")) + + [idx for route in signals["legal_effect"] for idx in _strings(route.get("related_evidence_indexes"))] + + [idx for sig in signals["case_liability"] for idx in _strings(sig.get("related_evidence_indexes"))] + + [idx for sig in signals["actio"] for idx in _strings(sig.get("related_evidence_indexes"))] + ) + missing_evidence = [idx for idx in evidence_indexes if idx not in evidence_authority] + if missing_evidence: + input_warnings.append({"bo_id": bo_id, "warning": "missing_evidence_authority", "evidence_indexes": missing_evidence}) + canonical_types, nonrouting = _candidate_structure_types(bo, signals) + nonrouting_by_bo[bo_id] = nonrouting + candidates[bo_id] = { + "bo_id": bo_id, + "source_domain": _first_string(_dict(bo.get("provenance")).get("source_domain")), + "hard_issue_keys": { + "claim_group_ids": _dedupe([x for sig in signals["case_liability"] for x in _strings(sig.get("claim_group_id"))] + _strings(_dict(bo.get("downstream_seed_refs")).get("claim_group_seed_refs_proposed"))), + "liability_group_ids": _dedupe([x for sig in signals["case_liability"] for x in _strings(sig.get("liability_group_id"))]), + "linked_structure_ids": _dedupe( + [x for route in signals["legal_effect"] for x in _strings(route.get("linked_structure_ids"))] + + [x for sig in signals["case_liability"] for x in _strings(sig.get("linked_structure_ids"))] + + _strings(_dict(bo.get("downstream_seed_refs")).get("legal_effect_structure_seed_ref_proposed")) + ), + "canonical_theory_graph_seed_ref": _strings(_dict(bo.get("downstream_seed_refs")).get("canonical_theory_graph_seed_ref_proposed")), + }, + "candidate_structure_types": canonical_types, + "asset_candidates": _extract_asset_candidates(bo, signals), + "evidence_indexes": evidence_indexes, + "keyword_fields": { + "Legal_Keywords": _strings(bo.get("Legal_Keywords")), + "Action": bo.get("Action"), + "Reason": bo.get("Reason"), + "JuristicAct_label": _dict(bo.get("JuristicAct")).get("label"), + "ActionType": bo.get("ActionType"), + "BOType": bo.get("BOType"), + "domain_legal_effect_tag": _dict(bo.get("domain_payload")).get("domain_legal_effect_tag"), + "legal_effect_tags": _strings(_dict(bo.get("domain_payload")).get("legal_effect_tags")), + }, + "signals": signals, + } + + alias_map = _build_asset_alias_map(candidates) + issue_key_to_id: dict[str, str] = {} + asset_key_to_id: dict[str, str] = {} + key_alias: dict[str, str] = {} + issue_seed_map: dict[str, dict[str, Any]] = {} + asset_seed_map: dict[str, dict[str, Any]] = {} + evidence_index_seed: dict[str, dict[str, Any]] = {} + seed_items: list[dict[str, Any]] = [] + raw_exceptions: list[dict[str, Any]] = [] + deterministic_resolutions: list[dict[str, Any]] = [] + multi_asset_bundles: list[dict[str, Any]] = [] + + for bo_id in sorted(candidates): + candidate = _dict(candidates[bo_id]) + issue_key, issue_diag, issue_codes = _issue_decision(candidate) + if issue_key not in issue_key_to_id: + issue_key_to_id[issue_key] = _make_id("IC", len(issue_key_to_id) + 1) + issue_id = issue_key_to_id[issue_key] + asset_keys, bundles, asset_excs, asset_codes, resolutions = _asset_decisions(candidate, alias_map, key_alias) + asset_ids: list[str] = [] + for key in asset_keys: + if key not in asset_key_to_id: + asset_key_to_id[key] = _make_id("AC", len(asset_key_to_id) + 1) + asset_ids.append(asset_key_to_id[key]) + multi_asset_bundles.extend(bundles) + deterministic_resolutions.extend(resolutions) + raw_exceptions.extend(asset_excs) + representative_type, all_types, type_diag = _structure_type_decision(candidate) + if type_diag.get("decision") == "deterministic_multi_routing": + deterministic_resolutions.append({ + "kind": "multi_routing_preserved", + "bo_id": bo_id, + "candidate_structure_types": all_types, + "representative": representative_type, + "basis": "canonical multi-routing preserved deterministically (X-2)", + }) + + seed_id = _make_id("LESS", len(seed_items) + 1) + evidence_indexes = _strings(candidate.get("evidence_indexes")) + signals = _dict(candidate.get("signals")) + case_signals = _list(signals.get("case_liability")) + actio_signals = _list(signals.get("actio")) + item = { + "seed_id": seed_id, + "source_bo_ids": [bo_id], + "representative_candidate_structure_type": representative_type, + "candidate_structure_types": all_types, + "issue_cluster_ids": [issue_id], + "asset_cluster_ids": asset_ids, + "multi_asset_bundle_refs": _strings([bundle.get("bundle_ref") for bundle in bundles]), + "evidence_indexes": evidence_indexes, + "linked_claim_group_ids": _dedupe([x for sig in case_signals if isinstance(sig, dict) for x in _strings(sig.get("claim_group_id"))]), + "linked_liability_group_ids": _dedupe([x for sig in case_signals if isinstance(sig, dict) for x in _strings(sig.get("liability_group_id"))]), + "linked_actio_signal_ids": _dedupe([x for sig in actio_signals if isinstance(sig, dict) for x in _strings(sig.get("signal_id"))]), + "basis_fields": _dedupe(_strings(issue_diag.get("basis_fields")) + _strings(type_diag.get("basis_fields")) + ["BO.source_evidence_indexes"]), + "pending_exception_ids": [], + "deterministic_review_codes": _dedupe(issue_codes + asset_codes), + "diagnostics": { + "issue": {"decision": issue_diag.get("decision"), "confidence": issue_diag.get("confidence")}, + "candidate_structure_type": {"decision": type_diag.get("decision")}, + }, + } + seed_items.append(item) + _add_issue_seed(issue_seed_map, issue_id, issue_key, item, candidate, issue_diag) + _add_asset_seeds(asset_seed_map, asset_ids, item, candidate) + _add_evidence_index(evidence_index_seed, evidence_indexes, item, evidence_authority) + + merged_exceptions, merge_groups = _merge_exceptions(raw_exceptions) + # 병합 후 LEX id를 seed의 pending_exception_ids로 역결합 + exc_by_signature_bo: dict[str, list[str]] = defaultdict(list) + for exc in merged_exceptions: + for exc_bo in _strings(exc.get("source_bo_ids")): + exc_by_signature_bo[exc_bo].append(str(exc["exception_id"])) + # X-4 방어 상한: 자동 차단 금지 — 초과분은 deterministic_review로 전환 (id 유지, 추적 가능) + deterministic_review_overflow: list[dict[str, Any]] = [] + kept_exceptions: list[dict[str, Any]] = [] + total_bytes = 0 + for exc in merged_exceptions: + serialized = len(json.dumps(exc, ensure_ascii=False).encode("utf-8")) + item_chars = len(json.dumps(exc, ensure_ascii=False)) + if item_chars > PACK_ITEM_CHAR_LIMIT or len(kept_exceptions) >= PACK_MAX_ITEMS or total_bytes + serialized > PACK_MAX_BYTES: + deterministic_review_overflow.append({ + "exception_id": exc["exception_id"], + "exception_type": exc.get("exception_type"), + "source_bo_ids": _strings(exc.get("source_bo_ids")), + "reason": "pack_budget_deferred_to_review", + }) + continue + kept_exceptions.append(exc) + total_bytes += serialized + overflow_ids = {str(item["exception_id"]) for item in deterministic_review_overflow} + for item in seed_items: + bo_id = item["source_bo_ids"][0] + ids = exc_by_signature_bo.get(bo_id, []) + item["pending_exception_ids"] = _dedupe([i for i in ids if i not in overflow_ids]) + if any(i in overflow_ids for i in ids): + item["deterministic_review_codes"] = _dedupe(item["deterministic_review_codes"] + ["pack_budget_deferred_to_review"]) + for issue_row in issue_seed_map.values(): + issue_row["pending_exception_ids"] = _dedupe([i for bo in issue_row["source_bo_ids"] for i in exc_by_signature_bo.get(bo, []) if i not in overflow_ids]) + for asset_row in asset_seed_map.values(): + asset_row["pending_exception_ids"] = _dedupe([i for bo in asset_row["source_bo_ids"] for i in exc_by_signature_bo.get(bo, []) if i not in overflow_ids]) + + exception_pack = { + "schema_version": "stage1_legal_structure_exception_pack.v2", + "has_exceptions": bool(kept_exceptions), + "exception_count": len(kept_exceptions), + "exceptions": kept_exceptions, + "budget": {"max_items": PACK_MAX_ITEMS, "max_bytes": PACK_MAX_BYTES, + "max_chars_per_item": PACK_ITEM_CHAR_LIMIT, + "overflow_policy": "DEFER_TO_DETERMINISTIC_REVIEW"}, + } + pack_content = _write_doc(PACK_OUT_PATH, exception_pack) + pack_sha = hashlib.sha256(pack_content.encode("utf-8")).hexdigest() + + audit = { + "schema_version": "stage1_legal_structure_audit.v1", + "nonrouting_labels_by_bo_id": {k: v for k, v in sorted(nonrouting_by_bo.items()) if v}, + "deterministic_resolutions": deterministic_resolutions, + "deterministic_review_items": deterministic_review_overflow + [ + {"seed_id": item["seed_id"], "source_bo_ids": item["source_bo_ids"], + "codes": item["deterministic_review_codes"]} + for item in seed_items if item["deterministic_review_codes"] + ], + "merge_groups": merge_groups, + "input_warnings": input_warnings, + "review_reduction_trace": [], + } + _write_doc(AUDIT_OUT_PATH, audit) + + seed_bundle = { + "legal_structure_seed_bundle": { + "schema_version": "stage1_legal_structure_seed_bundle.v2", + "status": "READY", + "structure_seed_items": seed_items, + "issue_cluster_seed_map": issue_seed_map, + "asset_cluster_seed_map": asset_seed_map, + "evidence_index_seed": evidence_index_seed, + "multi_asset_bundles": multi_asset_bundles, + "exception_pack_ref": { + "path": PACK_OUT_PATH, + "sha256": pack_sha, + "expected_exception_ids": [str(exc["exception_id"]) for exc in kept_exceptions], + }, + } + } + _write_doc(SEED_OUT_PATH, seed_bundle) + + print(json.dumps({ + "status": "READY", + "message": "L0 structure seed reduce 완료: seed bundle/exception pack/audit 생성", + "seed_bundle_path": SEED_OUT_PATH, + "exception_pack_path": PACK_OUT_PATH, + "audit_path": AUDIT_OUT_PATH, + "seed_count": len(seed_items), + "has_exceptions": exception_pack["has_exceptions"], + "exception_count": exception_pack["exception_count"], + "deterministic_resolution_count": len(deterministic_resolutions), + "nonrouting_label_count": sum(len(v) for v in nonrouting_by_bo.values()), + }, ensure_ascii=False)) + except Exception as exc: + print(json.dumps({ + "status": "FAILED", + "message": "L0 structure seed reducer 실패", + "reason": str(exc), + }, ensure_ascii=False)) + raise + + + if __name__ == "__main__": + main() + - task_name: Task_L1_exception_adjudicator + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: low + llm_verbosity: low + max_iterations: 1 + use_tools: [localdocs] + cache_control: + mode: auto + ttl: 20m + preflight: true + preflight_files: + - stage1_tmp/legal_effect_structure_exception_pack.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + + TASK_NAME: Task_L1_exception_adjudicator + STAGE: Stage 1 legal effect structure/index builder — 조건부 예외 판정 (Part 1 v3 GB / Part 2 v3 R1 패턴) + MISSION: 결정론 reducer(L0)가 non-deferrable로 판정한 compact exception만 판정한다. 파일 병합·최종 파일 작성·사실 창작은 하지 않는다. + + + + - 유일한 입력은 preflight로 제공된 `stage1_tmp/legal_effect_structure_exception_pack.json`이다. + - seed bundle, BO.json, evidence, signal 파일, raw 원문을 읽거나 요청하지 않는다. + - pack에 없는 exception_id·BO ID·evidence index·structure/cluster ID·사실·당사자·일자·금액을 창작하지 않는다. + - `legal_effect_structures.json`·seed bundle·audit 파일을 작성하지 않는다. + - 출력 파일은 `stage1_tmp/legal_effect_structure_adjudication.json` 하나뿐이다. + - markdown fence·prose 없이 raw JSON만 파일에 쓴다. + + + + 1. preflight로 제공된 exception pack의 `has_exceptions`를 확인한다. + 2. `has_exceptions == false`이면: `write_file(overwrite=true)`로 아래 no-exception 객체를 `stage1_tmp/legal_effect_structure_adjudication.json`에 저장하고 `"NO_EXCEPTIONS"`만 출력한 뒤 즉시 종료한다(terminate). 다른 어떤 파일도 읽지 않는다. + {"legal_structure_exception_adjudication": {"schema_version": "stage1_legal_structure_exception_adjudication.v1", "status": "READY_NO_EXCEPTIONS", "source_exception_pack_schema_version": "stage1_legal_structure_exception_pack.v2", "decisions": [], "blocked_review_items": [], "adjudication_gate": {"input_exception_count": 0, "decision_count": 0, "blocked_count": 0, "coverage_pass": true, "json_only_contract_pass": true}}} + 3. `has_exceptions == true`이면: `exceptions[]`의 각 항목을 해당 항목의 `compact_context`·`candidate_values`·`basis_fields`·`score_summary`·`conflict_flags`만으로 판정한다. 추가 read는 금지된다. + 4. 판정 규칙: + - `ambiguous_issue_cluster`: `USE_EXISTING_HARD_KEY`, `SELECT_ISSUE_FAMILY`, `SPLIT_ISSUE_CLUSTER`, `BLOCK_REVIEW` 중 하나. + - `ambiguous_asset_cluster`: `SAME_CLUSTER`, `SEPARATE_CLUSTER`, `MULTI_ASSET_BUNDLE`, `BLOCK_REVIEW` 중 하나. 자산 동일성은 등기행·별지목록 ref가 자유문구보다 우선한다. 병합이 권리객체를 훼손할 수 있으면 SEPARATE_CLUSTER 또는 BLOCK_REVIEW. + - `ambiguous_candidate_structure_type`: `SELECT_REPRESENTATIVE_TYPE`, `PRESERVE_MULTI_ROUTING`, `SPLIT_STRUCTURE_SEED`, `BLOCK_REVIEW` 중 하나. 다중 라우팅 보존이 기본적으로 안전하다. + - 각 항목의 `allowed_decisions` 안에서만 선택한다. `selected_value`/`selected_values`/`preserve_candidate_values`/`split_plan`의 값은 반드시 해당 항목의 `candidate_values` 부분집합이어야 한다. + - compact 근거로 확정할 수 없으면 반드시 `BLOCK_REVIEW`로 판정하고 같은 항목을 `blocked_review_items`에 넣는다(확신 없는 확정 금지 — 인간 검토 라우팅). + 5. `write_file(overwrite=true)`로 결과를 저장한다. 루트는 `legal_structure_exception_adjudication`, `schema_version`은 `stage1_legal_structure_exception_adjudication.v1`, `status`는 전 항목 비차단이면 `READY`, 차단 존재 시 `READY_WITH_BLOCKS`다. decision 항목 필드: `exception_id`, `exception_type`, `source_bo_ids`, `decision`, `selected_value|null`, `selected_values[]`, `preserve_candidate_values[]`, `split_plan[]`(split일 때만), `review_required`, `reason_code`, `basis_fields_used[]`, `conflict_flags_acknowledged[]`. 모든 decision은 pack의 `exception_id`를 인용하며, 각 exception마다 정확히 하나의 decision을 만든다. + 6. `"L1 예외 판정 완료 (decisions=<건수>)"`만 출력하고 작업을 끝낸다(terminate). + + + + - Stage 1은 법률효과·청구원인을 확정하지 않는다. 여기 도달한 항목은 final writer가 단일 join 값을 선택해야만 진행되는 사안뿐이다. + - 하나의 라벨이 서로 다른 등기행/별지목록 ref 계열을 주장하는 경우: 원문 ref 근거가 더 구체적인 쪽을 선택하고, 우열을 가릴 수 없으면 BLOCK_REVIEW. + - SEPARATE_CLUSTER(보존)가 권리객체를 지키는 기본값이다. SAME_CLUSTER 병합은 ref 근거가 무손실로 합치될 때만 선택한다. + - 판정은 라우팅/조인 키 결정일 뿐 소유권·담보권·평가액의 실체 판단이 아니다. + + + + - exception pack 부재·파싱 불가: 즉시 중단하고 채팅으로만 보고한다. adjudication 파일은 쓰지 않는다. + - tool 오류: 1회만 재시도. 재실패 시 `FAILED: `만 보고하고 종료한다. + + + - task_name: Task_L2_final_structure_index_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import sys + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + SEED_PATH = "stage1_tmp/legal_effect_structure_seed_bundle.json" + PACK_PATH = "stage1_tmp/legal_effect_structure_exception_pack.json" + ADJUDICATION_PATH = "stage1_tmp/legal_effect_structure_adjudication.json" + AUDIT_PATH = "stage1_tmp/legal_effect_structure_audit.json" + TARGET_NAME = "legal_effect_structures.json" + STRUCTURE_TYPE_ROLE = { + "money_claim": "claim_chain_ref", + "commercial_successor": "claim_chain_ref", + "secured_debt": "linked_structures", + "registry_invalidity": "linked_structures", + "land_use_gain": "legal_calculation_object", + "valuation": "legal_calculation_object", + "fraudulent_transfer": "actio_roles", + "preserved_claim": "claim_chain_ref", + "actio_remedy_or_cap": "linked_actio_structures", + "succession_notice_lien_asset_defense": "linked_structures", + "general_legal_effect": "legal_effect_roles", + } + ISSUE_DECISIONS = {"USE_EXISTING_HARD_KEY", "SELECT_ISSUE_FAMILY", "SPLIT_ISSUE_CLUSTER", "BLOCK_REVIEW"} + ASSET_DECISIONS = {"SAME_CLUSTER", "SEPARATE_CLUSTER", "MULTI_ASSET_BUNDLE", "BLOCK_REVIEW"} + TYPE_DECISIONS = {"SELECT_REPRESENTATIVE_TYPE", "PRESERVE_MULTI_ROUTING", "SPLIT_STRUCTURE_SEED", "BLOCK_REVIEW"} + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-legal-effect-l2-final-structure-index-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + + def _lenient_json(raw: str, label: str) -> Any: + """SKILL §6.4/§6.7: strict 실패 시 코드펜스 제거 + raw_decode salvage.""" + try: + return json.loads(raw) + except json.JSONDecodeError: + text = raw.strip() + if text.startswith("```"): + first_newline = text.find("\n") + if first_newline >= 0: + text = text[first_newline + 1:] + if text.rstrip().endswith("```"): + text = text.rstrip()[:-3] + start = text.find("{") + if start < 0: + raise RuntimeError(f"Content of {label} is not valid JSON: {raw[:300]}") + try: + obj, _ = JSON_DECODER.raw_decode(text[start:].strip()) + return obj + except json.JSONDecodeError as exc: + raise RuntimeError(f"Content of {label} is not valid JSON: {raw[:300]}") from exc + + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result.get("result", {}).get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + text = content[0].get("text", "") + envelope = _lenient_json(text, doc_name) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + item = results[0] + if isinstance(item, dict) and item.get("error"): + raise RuntimeError(f"read_docs error for {doc_name}: {item.get('error')}") + raw = item.get("content", "") + else: + raw = envelope + if isinstance(raw, str): + return _lenient_json(raw, doc_name) + return raw + + + def _read_doc(doc_name: str) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + return _extract_json_from_read_docs_result(result, doc_name) + + + def _write_doc(doc_name: str, payload: dict[str, Any]) -> None: + content = json.dumps(payload, ensure_ascii=False, indent=2) + "\n" + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=_next_msg_id()) + + + def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _strings(value: Any) -> list[str]: + if value is None: + return [] + items = value if isinstance(value, list) else [value] + out: list[str] = [] + for item in items: + if item is None: + continue + text = str(item).strip() + if text and text not in out: + out.append(text) + return out + + + def _dedupe(values: list[str]) -> list[str]: + out: list[str] = [] + for value in values: + if value is None: + continue + text = str(value).strip() + if text and text not in out: + out.append(text) + return out + + + def _make_id(prefix: str, ordinal: int) -> str: + return f"{prefix}-{ordinal:03d}" + + + # ---------------------------------------------------------------- 입력 적재 (파일 채널 일원화 — X-9) + def _load_seed_bundle() -> dict[str, Any]: + outer = _read_doc(SEED_PATH) + root = _dict(_dict(outer).get("legal_structure_seed_bundle")) + if root.get("schema_version") != "stage1_legal_structure_seed_bundle.v2": + raise ValueError("legal_structure_seed_bundle schema mismatch (v2 required)") + if root.get("status") != "READY": + raise ValueError("legal_structure_seed_bundle status invalid") + return root + + + def _load_exception_pack(seed_bundle: dict[str, Any]) -> dict[str, Any]: + ref = _dict(seed_bundle.get("exception_pack_ref")) + if ref.get("path") != PACK_PATH: + raise ValueError("exception_pack_ref path mismatch") + result = _call_tool("read_docs", {"doc_names": [PACK_PATH]}, msg_id=_next_msg_id()) + content = result.get("result", {}).get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {PACK_PATH}") + envelope = _lenient_json(content[0].get("text", ""), PACK_PATH) + raw_text = None + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if results and isinstance(results[0], dict): + raw_text = results[0].get("content") + pack = _lenient_json(raw_text, PACK_PATH) if isinstance(raw_text, str) else (raw_text or envelope) + pack = _dict(pack) + if pack.get("schema_version") != "stage1_legal_structure_exception_pack.v2": + raise ValueError("exception pack schema mismatch (v2 required)") + if isinstance(raw_text, str) and ref.get("sha256"): + digest = hashlib.sha256(raw_text.encode("utf-8")).hexdigest() + if digest != ref.get("sha256"): + raise ValueError("exception pack sha256 mismatch against seed bundle ref") + expected_ids = _strings(ref.get("expected_exception_ids")) + pack_ids = [str(_dict(e).get("exception_id")) for e in _list(pack.get("exceptions"))] + if sorted(expected_ids) != sorted(pack_ids): + raise ValueError("exception pack ids mismatch against seed bundle ref") + return pack + + + def _load_adjudication() -> dict[str, Any]: + outer = _read_doc(ADJUDICATION_PATH) + root = _dict(_dict(outer).get("legal_structure_exception_adjudication")) + if root.get("schema_version") != "stage1_legal_structure_exception_adjudication.v1": + raise ValueError("legal_structure_exception_adjudication schema mismatch") + if root.get("status") not in {"READY", "READY_NO_EXCEPTIONS", "READY_WITH_BLOCKS"}: + raise ValueError("legal_structure_exception_adjudication status is not accepted") + return root + + + # ---------------------------------------------------------------- 판정 검증 (v1 LES3 이식 + coverage 등식) + def _expected_exceptions(pack: dict[str, Any]) -> dict[str, dict[str, Any]]: + out: dict[str, dict[str, Any]] = {} + for item in _list(pack.get("exceptions")): + if not isinstance(item, dict) or not item.get("exception_id"): + raise ValueError("exception item without exception_id in pack") + out[str(item["exception_id"])] = item + return out + + + def _validate_decision_enum(decision: dict[str, Any]) -> None: + exception_type = decision.get("exception_type") + choice = decision.get("decision") + allowed = { + "ambiguous_issue_cluster": ISSUE_DECISIONS, + "ambiguous_asset_cluster": ASSET_DECISIONS, + "ambiguous_candidate_structure_type": TYPE_DECISIONS, + }.get(exception_type) + if allowed is None: + raise ValueError(f"invalid exception_type: {exception_type}") + if choice not in allowed: + raise ValueError(f"invalid decision {choice} for {exception_type}") + + + def _validate_decision_subsets(decision: dict[str, Any], expected: dict[str, Any]) -> None: + candidates = set(_strings(expected.get("candidate_values"))) + basis = set(_strings(expected.get("basis_fields"))) + flags = set(_strings(expected.get("conflict_flags"))) + for field in ["selected_values", "preserve_candidate_values"]: + extras = set(_strings(decision.get(field))) - candidates + if extras: + raise ValueError(f"{decision.get('exception_id')}: {field} contains values outside candidate_values: {sorted(extras)}") + selected = decision.get("selected_value") + if selected not in (None, "") and candidates and str(selected) not in candidates: + raise ValueError(f"{decision.get('exception_id')}: selected_value outside candidate_values") + for split in _list(decision.get("split_plan")): + if isinstance(split, dict): + extras = set(_strings(split.get("candidate_values"))) - candidates + if extras: + raise ValueError(f"{decision.get('exception_id')}: split_plan candidate outside candidate_values") + if basis: + extras = set(_strings(decision.get("basis_fields_used"))) - basis + if extras: + raise ValueError(f"{decision.get('exception_id')}: basis_fields_used outside basis_fields") + if flags: + extras = set(_strings(decision.get("conflict_flags_acknowledged"))) - flags + if extras: + raise ValueError(f"{decision.get('exception_id')}: conflict_flags_acknowledged outside conflict_flags") + + + def _validate_decisions(pack: dict[str, Any], adjudication: dict[str, Any]) -> tuple[dict[str, dict[str, Any]], list[dict[str, Any]], set[str], set[str]]: + """coverage 등식(X-5): expected == resolved ∪ blocked. 반환: (decisions, blocked, resolved_ids, blocked_ids).""" + expected = _expected_exceptions(pack) + decisions: dict[str, dict[str, Any]] = {} + blocked = _list(adjudication.get("blocked_review_items")) + if not expected: + if adjudication.get("status") != "READY_NO_EXCEPTIONS": + raise ValueError("no exceptions expected; adjudication status must be READY_NO_EXCEPTIONS") + if _list(adjudication.get("decisions")): + raise ValueError("no exceptions expected; decisions must be empty") + return decisions, [], set(), set() + for decision in _list(adjudication.get("decisions")): + if not isinstance(decision, dict) or not decision.get("exception_id"): + raise ValueError("decision item missing exception_id") + exception_id = str(decision["exception_id"]) + if exception_id not in expected: + raise ValueError(f"decision references unknown exception_id: {exception_id}") + if exception_id in decisions: + raise ValueError(f"duplicate decision for exception_id: {exception_id}") + _validate_decision_enum(decision) + _validate_decision_subsets(decision, expected[exception_id]) + decisions[exception_id] = decision + missing = sorted(set(expected) - set(decisions)) + if missing: + raise ValueError(f"adjudication missing decisions: {missing}") + blocked_ids = {str(item.get("exception_id")) for item in blocked if isinstance(item, dict)} + for exception_id, decision in decisions.items(): + if decision.get("decision") == "BLOCK_REVIEW" and exception_id not in blocked_ids: + blocked.append({ + "exception_id": exception_id, + "exception_type": decision.get("exception_type"), + "source_bo_ids": _strings(decision.get("source_bo_ids")), + "block_reason": "block_review_decision_without_block_item", + }) + blocked_ids.add(exception_id) + resolved_ids = {eid for eid, d in decisions.items() if d.get("decision") != "BLOCK_REVIEW"} + if resolved_ids | blocked_ids != set(expected): + raise ValueError("coverage equation failed: expected != resolved ∪ blocked") + return decisions, blocked, resolved_ids, blocked_ids + + + # ---------------------------------------------------------------- 구조 빌더 (v1 LES3 이식 + X-5/X-6) + def _decisions_for_bo(decisions: dict[str, dict[str, Any]], bo_id: str, exception_type: str) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for decision in decisions.values(): + if decision.get("exception_type") == exception_type and bo_id in _strings(decision.get("source_bo_ids")): + out.append(decision) + return out + + + def _issue_ids_for_item(item: dict[str, Any], decisions: dict[str, dict[str, Any]], issue_key_ids: dict[str, str]) -> list[str]: + bo_id = _strings(item.get("source_bo_ids"))[0] if _strings(item.get("source_bo_ids")) else "" + issue_decisions = _decisions_for_bo(decisions, bo_id, "ambiguous_issue_cluster") + keys: list[str] = [] + for decision in issue_decisions: + choice = decision.get("decision") + if choice == "BLOCK_REVIEW": + continue + if choice == "SPLIT_ISSUE_CLUSTER": + for split in _list(decision.get("split_plan")): + if isinstance(split, dict): + label = split.get("split_label") or "|".join(_strings(split.get("candidate_values"))) + if label: + keys.append(f"split_issue:{label}") + else: + value = decision.get("selected_value") or (_strings(decision.get("selected_values"))[0] if _strings(decision.get("selected_values")) else None) + if value: + keys.append(f"adjudicated_issue:{value}") + if not keys: + keys = _strings(item.get("issue_cluster_ids")) + out: list[str] = [] + for key in keys: + if key not in issue_key_ids: + issue_key_ids[key] = _make_id("IC", len(issue_key_ids) + 1) + out.append(issue_key_ids[key]) + return _dedupe(out) + + + def _asset_ids_for_item(item: dict[str, Any], decisions: dict[str, dict[str, Any]], asset_key_ids: dict[str, str]) -> tuple[list[str], list[str]]: + bo_id = _strings(item.get("source_bo_ids"))[0] if _strings(item.get("source_bo_ids")) else "" + asset_decisions = _decisions_for_bo(decisions, bo_id, "ambiguous_asset_cluster") + keys = _strings(item.get("asset_cluster_ids")) + bundle_refs = _strings(item.get("multi_asset_bundle_refs")) + for decision in asset_decisions: + choice = decision.get("decision") + if choice == "BLOCK_REVIEW": + continue + if choice == "SAME_CLUSTER": + value = decision.get("selected_value") or "|".join(_strings(decision.get("selected_values")) or _strings(decision.get("preserve_candidate_values"))) + if value: + keys = [f"same_asset:{value}"] + elif choice == "SEPARATE_CLUSTER": + vals = _strings(decision.get("selected_values")) or _strings(decision.get("preserve_candidate_values")) or _strings(decision.get("candidate_values")) + keys = [f"separate_asset:{value}" for value in vals] or keys + elif choice == "MULTI_ASSET_BUNDLE": + value = decision.get("selected_value") or f"bundle:{bo_id}" + if value not in bundle_refs: + bundle_refs.append(str(value)) + out: list[str] = [] + for key in keys: + if key not in asset_key_ids: + asset_key_ids[key] = _make_id("AC", len(asset_key_ids) + 1) + out.append(asset_key_ids[key]) + return _dedupe(out), _dedupe(bundle_refs) + + + def _type_plan_for_item(item: dict[str, Any], decisions: dict[str, dict[str, Any]]) -> list[dict[str, Any]]: + bo_id = _strings(item.get("source_bo_ids"))[0] if _strings(item.get("source_bo_ids")) else "" + original_types = _strings(item.get("candidate_structure_types")) or _strings(item.get("representative_candidate_structure_type")) or ["general_legal_effect"] + representative = item.get("representative_candidate_structure_type") or original_types[0] + type_decisions = _decisions_for_bo(decisions, bo_id, "ambiguous_candidate_structure_type") + if not type_decisions: + return [{"candidate_structure_type": representative, "candidate_structure_types": original_types, "split_label": None}] + plans: list[dict[str, Any]] = [] + for decision in type_decisions: + choice = decision.get("decision") + if choice == "BLOCK_REVIEW": + plans.append({"candidate_structure_type": representative, "candidate_structure_types": original_types, "split_label": None}) + elif choice == "SELECT_REPRESENTATIVE_TYPE": + selected = decision.get("selected_value") or (_strings(decision.get("selected_values"))[0] if _strings(decision.get("selected_values")) else representative) + plans.append({"candidate_structure_type": selected, "candidate_structure_types": _dedupe([selected] + original_types), "split_label": None}) + elif choice == "PRESERVE_MULTI_ROUTING": + vals = _strings(decision.get("selected_values")) or _strings(decision.get("preserve_candidate_values")) or original_types + selected = decision.get("selected_value") or vals[0] + plans.append({"candidate_structure_type": selected, "candidate_structure_types": vals, "split_label": None}) + elif choice == "SPLIT_STRUCTURE_SEED": + for split in _list(decision.get("split_plan")): + if isinstance(split, dict): + vals = _strings(split.get("candidate_values")) or original_types + plans.append({ + "candidate_structure_type": vals[0], + "candidate_structure_types": vals, + "split_label": split.get("split_label"), + }) + return plans or [{"candidate_structure_type": representative, "candidate_structure_types": original_types, "split_label": None}] + + + def _legal_roles(types: list[str]) -> list[str]: + return _dedupe([STRUCTURE_TYPE_ROLE.get(typ, "legal_effect_roles") for typ in types]) + + + def _basis(item: dict[str, Any], types: list[str]) -> dict[str, Any]: + signal_files: list[str] = [] + if _strings(item.get("linked_actio_signal_ids")): + signal_files.append("actio_case_signals.json") + if _strings(item.get("linked_claim_group_ids")) or _strings(item.get("linked_liability_group_ids")): + signal_files.append("case_liability_signals.json") + if types: + signal_files.append("legal_effect_signals.json") + return { + "primary_fields": _strings(item.get("basis_fields")), + "source_signal_files": _dedupe(signal_files), + "source_evidence_indexes": _strings(item.get("evidence_indexes")), + } + + + def _structure_label(candidate_type: str, source_bo_ids: list[str], split_label: Any) -> str: + label = candidate_type.replace("_", " ") + suffix = f" / {split_label}" if split_label else "" + return f"{label} structure for {', '.join(source_bo_ids)}{suffix}" + + + def _build_structures(seed_bundle: dict[str, Any], decisions: dict[str, dict[str, Any]], + blocked_ids: set[str], resolved_ids: set[str]) -> list[dict[str, Any]]: + issue_key_ids: dict[str, str] = {} + asset_key_ids: dict[str, str] = {} + structures: list[dict[str, Any]] = [] + for item in _list(seed_bundle.get("structure_seed_items")): + if not isinstance(item, dict): + continue + source_bo_ids = _strings(item.get("source_bo_ids")) + if not source_bo_ids: + continue + pending = _strings(item.get("pending_exception_ids")) + review_codes = _strings(item.get("deterministic_review_codes")) + item_blocked = sorted(set(pending) & blocked_ids) + item_resolved = sorted(set(pending) & resolved_ids) + issue_ids = _issue_ids_for_item(item, decisions, issue_key_ids) + asset_ids, bundle_refs = _asset_ids_for_item(item, decisions, asset_key_ids) + type_plans = _type_plan_for_item(item, decisions) + for plan in type_plans: + structure_id = _make_id("LES", len(structures) + 1) + types = _strings(plan.get("candidate_structure_types")) or ["general_legal_effect"] + # X-5: 해소된 판정은 review를 발생시키지 않는다 — blocked/deterministic review/split만 review + review_required = bool(item_blocked or review_codes or plan.get("split_label")) + structures.append({ + "structure_id": structure_id, + "candidate_structure_type": str(plan.get("candidate_structure_type") or types[0]), + "candidate_structure_types": types, + "structure_label": _structure_label(str(plan.get("candidate_structure_type") or types[0]), source_bo_ids, plan.get("split_label")), + "source_bo_ids": source_bo_ids, + "issue_cluster_ids": issue_ids, + "asset_cluster_ids": asset_ids, + "evidence_indexes": _strings(item.get("evidence_indexes")), + "linked_claim_group_ids": _strings(item.get("linked_claim_group_ids")), + "linked_liability_group_ids": _strings(item.get("linked_liability_group_ids")), + "linked_actio_signal_ids": _strings(item.get("linked_actio_signal_ids")), + "legal_effect_roles": _legal_roles(types), + "basis": _basis(item, types), + "review_required": review_required, + "pending_exception_state": { + "resolved": item_resolved, + "blocked": item_blocked, + "deterministic_review_codes": review_codes, + }, + "routing_audit": { + "source_seed_id": item.get("seed_id"), + "candidate_structure_types": types, + "multi_asset_bundle_refs": bundle_refs, + }, + }) + return structures + + + def _build_structure_index(structures: list[dict[str, Any]], seed_bundle: dict[str, Any]) -> dict[str, Any]: + by_issue: dict[str, Any] = {} + by_asset: dict[str, Any] = {} + by_evidence: dict[str, Any] = {} + by_type: dict[str, Any] = {} + by_bo: dict[str, Any] = {} + issue_seed_map = _dict(seed_bundle.get("issue_cluster_seed_map")) + asset_seed_map = _dict(seed_bundle.get("asset_cluster_seed_map")) + evidence_seed = _dict(seed_bundle.get("evidence_index_seed")) + + for structure in structures: + sid = structure["structure_id"] + basis = _dict(structure.get("basis")) + routing_audit = _dict(structure.get("routing_audit")) + all_types = _strings(structure.get("candidate_structure_types")) or [structure["candidate_structure_type"]] + for bo_id in _strings(structure.get("source_bo_ids")): + row = by_bo.setdefault(bo_id, { + "bo_id": bo_id, + "structure_ids": [], + "structure_labels": [], + "issue_cluster_ids": [], + "asset_cluster_ids": [], + "evidence_indexes": [], + "candidate_structure_types": [], + "claim_group_ids": [], + "liability_group_ids": [], + "related_actio_signal_ids": [], + "legal_effect_roles": [], + "basis_fields": [], + "multi_asset_bundle_refs": [], + "review_required": False, + }) + row["structure_ids"] = _dedupe(row["structure_ids"] + [sid]) + row["structure_labels"] = _dedupe(row["structure_labels"] + _strings(structure.get("structure_label"))) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + structure["issue_cluster_ids"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + structure["asset_cluster_ids"]) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + structure["evidence_indexes"]) + # X-6: 대표 타입만이 아니라 canonical 전 타입을 인덱스에 반영 (additive) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + all_types) + row["claim_group_ids"] = _dedupe(row["claim_group_ids"] + structure["linked_claim_group_ids"]) + row["liability_group_ids"] = _dedupe(row["liability_group_ids"] + structure["linked_liability_group_ids"]) + row["related_actio_signal_ids"] = _dedupe(row["related_actio_signal_ids"] + structure["linked_actio_signal_ids"]) + row["legal_effect_roles"] = _dedupe(row["legal_effect_roles"] + structure["legal_effect_roles"]) + row["basis_fields"] = _dedupe(row["basis_fields"] + _strings(basis.get("primary_fields"))) + row["multi_asset_bundle_refs"] = _dedupe(row["multi_asset_bundle_refs"] + _strings(routing_audit.get("multi_asset_bundle_refs"))) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + for issue_id in _strings(structure.get("issue_cluster_ids")): + seed = _dict(issue_seed_map.get(issue_id)) + row = by_issue.setdefault(issue_id, { + "issue_cluster_id": issue_id, + "issue_type": seed.get("issue_type"), + "issue_label": seed.get("issue_key") or issue_id, + "source_bo_ids": [], + "candidate_structure_types": [], + "claim_group_ids": [], + "liability_group_ids": [], + "evidence_indexes": [], + "asset_cluster_ids": [], + "structure_ids": [], + "basis_fields": [], + "review_required": False, + }) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + structure["source_bo_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + all_types) + row["claim_group_ids"] = _dedupe(row["claim_group_ids"] + structure["linked_claim_group_ids"]) + row["liability_group_ids"] = _dedupe(row["liability_group_ids"] + structure["linked_liability_group_ids"]) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + structure["evidence_indexes"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + structure["asset_cluster_ids"]) + row["structure_ids"] = _dedupe(row["structure_ids"] + [sid]) + row["basis_fields"] = _dedupe(row["basis_fields"] + _strings(structure.get("basis", {}).get("primary_fields"))) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + for asset_id in _strings(structure.get("asset_cluster_ids")): + seed = _dict(asset_seed_map.get(asset_id)) + row = by_asset.setdefault(asset_id, { + "asset_cluster_id": asset_id, + "canonical_asset_label": seed.get("canonical_asset_label"), + "asset_kind": seed.get("asset_kind"), + "member_asset_mentions": _strings(seed.get("member_asset_mentions")), + "source_bo_ids": [], + "evidence_indexes": [], + "issue_cluster_ids": [], + "candidate_structure_types": [], + "structure_ids": [], + "basis_fields": _strings(seed.get("basis_fields")), + "review_required": bool(_strings(seed.get("deterministic_review_codes"))), + }) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + structure["source_bo_ids"]) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + structure["evidence_indexes"]) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + structure["issue_cluster_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + all_types) + row["structure_ids"] = _dedupe(row["structure_ids"] + [sid]) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + for evidence_index in _strings(structure.get("evidence_indexes")): + seed = _dict(evidence_seed.get(evidence_index)) + row = by_evidence.setdefault(evidence_index, { + "evidence_index": evidence_index, + "source_title": seed.get("source_title"), + "priority_class": seed.get("priority_class"), + "authentication_status": seed.get("authentication_status"), + "source_bo_ids": [], + "structure_ids": [], + "issue_cluster_ids": [], + "asset_cluster_ids": [], + "candidate_structure_types": [], + "related_event_candidate_ids": [], + "related_signal_refs": [], + "support_role": "primary", + "gap_flags": _strings(seed.get("gap_flags")), + }) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + structure["source_bo_ids"]) + row["structure_ids"] = _dedupe(row["structure_ids"] + [sid]) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + structure["issue_cluster_ids"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + structure["asset_cluster_ids"]) + row["candidate_structure_types"] = _dedupe(row["candidate_structure_types"] + all_types) + row["related_signal_refs"] = _dedupe(row["related_signal_refs"] + structure["linked_actio_signal_ids"] + structure["linked_claim_group_ids"] + structure["linked_liability_group_ids"]) + # X-6: by_candidate_structure_type을 전체 canonical 타입에 대해 생성 + for typ in all_types: + row = by_type.setdefault(typ, { + "candidate_structure_type": typ, + "structure_ids": [], + "source_bo_ids": [], + "issue_cluster_ids": [], + "asset_cluster_ids": [], + "evidence_indexes": [], + "claim_group_ids": [], + "liability_group_ids": [], + "related_actio_signal_ids": [], + "downstream_fact_roles": [], + "basis_fields": [], + "review_required": False, + }) + row["structure_ids"] = _dedupe(row["structure_ids"] + [sid]) + row["source_bo_ids"] = _dedupe(row["source_bo_ids"] + structure["source_bo_ids"]) + row["issue_cluster_ids"] = _dedupe(row["issue_cluster_ids"] + structure["issue_cluster_ids"]) + row["asset_cluster_ids"] = _dedupe(row["asset_cluster_ids"] + structure["asset_cluster_ids"]) + row["evidence_indexes"] = _dedupe(row["evidence_indexes"] + structure["evidence_indexes"]) + row["claim_group_ids"] = _dedupe(row["claim_group_ids"] + structure["linked_claim_group_ids"]) + row["liability_group_ids"] = _dedupe(row["liability_group_ids"] + structure["linked_liability_group_ids"]) + row["related_actio_signal_ids"] = _dedupe(row["related_actio_signal_ids"] + structure["linked_actio_signal_ids"]) + row["downstream_fact_roles"] = _dedupe(row["downstream_fact_roles"] + structure["legal_effect_roles"]) + row["basis_fields"] = _dedupe(row["basis_fields"] + _strings(structure.get("basis", {}).get("primary_fields"))) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + + return { + "by_bo_id": by_bo, + "by_issue_cluster_id": by_issue, + "by_asset_cluster_id": by_asset, + "by_evidence_index": by_evidence, + "by_candidate_structure_type": by_type, + } + + + def _quality_gate(structures: list[dict[str, Any]], seed_bundle: dict[str, Any], + blocked: list[dict[str, Any]]) -> dict[str, Any]: + seed_items = [item for item in _list(seed_bundle.get("structure_seed_items")) if isinstance(item, dict)] + seed_bo_ids = sorted({bo for item in seed_items for bo in _strings(item.get("source_bo_ids"))}) + covered_bo_ids = sorted({bo for structure in structures for bo in _strings(structure.get("source_bo_ids"))}) + seed_evidence = sorted({ev for item in seed_items for ev in _strings(item.get("evidence_indexes"))}) + covered_evidence = sorted({ev for structure in structures for ev in _strings(structure.get("evidence_indexes"))}) + review_queue = [] + for structure in structures: + if structure.get("review_required"): + review_queue.append({ + "review_type": "structure_review_required", + "structure_id": structure["structure_id"], + "source_bo_ids": structure["source_bo_ids"], + }) + for item in blocked: + if isinstance(item, dict): + review_queue.append({ + "review_type": "blocked_exception", + "exception_id": item.get("exception_id"), + "exception_type": item.get("exception_type"), + "source_bo_ids": _strings(item.get("source_bo_ids")), + "block_reason": item.get("block_reason"), + }) + # X-5: 자동차단 추적성 복원 — deterministic review 항목을 식별 가능한 큐 항목으로 남긴다 + for item in seed_items: + codes = _strings(item.get("deterministic_review_codes")) + if codes: + review_queue.append({ + "review_type": "deterministic_review", + "seed_id": item.get("seed_id"), + "source_bo_ids": _strings(item.get("source_bo_ids")), + "codes": codes, + }) + return { + "bo_coverage": { + "source_bo_count": len(seed_bo_ids), + "covered_bo_count": len(covered_bo_ids), + "missing_bo_ids": sorted(set(seed_bo_ids) - set(covered_bo_ids)), + "coverage_pass": set(seed_bo_ids) == set(covered_bo_ids), + }, + "evidence_coverage": { + "source_evidence_count": len(seed_evidence), + "covered_evidence_count": len(covered_evidence), + "missing_evidence_indexes": sorted(set(seed_evidence) - set(covered_evidence)), + "coverage_pass": set(seed_evidence).issubset(set(covered_evidence)), + }, + "review_queue": review_queue, + } + + + def _validate_payload(payload: dict[str, Any]) -> None: + if payload.get("schema_version") != "stage1_legal_effect_structures.v1": + raise ValueError("final payload schema_version mismatch") + if payload.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("final payload status invalid") + structures = _list(payload.get("legal_effect_structures")) + index = _dict(payload.get("structure_index")) + for key in ["by_bo_id", "by_issue_cluster_id", "by_asset_cluster_id", "by_evidence_index", "by_candidate_structure_type"]: + if not isinstance(index.get(key), dict): + raise ValueError(f"structure_index.{key} must be object") + structure_ids = {str(item.get("structure_id")) for item in structures if isinstance(item, dict)} + if len(structure_ids) != len(structures): + raise ValueError("structure_id values must be unique") + for item in structures: + if not isinstance(item, dict): + raise ValueError("legal_effect_structures item must be object") + for field in [ + "structure_id", "candidate_structure_type", "candidate_structure_types", "structure_label", + "source_bo_ids", "issue_cluster_ids", "asset_cluster_ids", "evidence_indexes", + "linked_claim_group_ids", "linked_liability_group_ids", "linked_actio_signal_ids", + "legal_effect_roles", "basis", "review_required", "pending_exception_state", + ]: + if field not in item: + raise ValueError(f"structure item missing {field}") + for bo_id in _strings(item.get("source_bo_ids")): + bo_row = _dict(index["by_bo_id"].get(bo_id)) + if not bo_row: + raise ValueError(f"missing BO index for {bo_id}") + if item.get("structure_id") not in _strings(bo_row.get("structure_ids")): + raise ValueError(f"BO index for {bo_id} does not reference {item.get('structure_id')}") + for issue_id in _strings(item.get("issue_cluster_ids")): + if issue_id not in index["by_issue_cluster_id"]: + raise ValueError(f"missing issue index for {issue_id}") + for asset_id in _strings(item.get("asset_cluster_ids")): + if asset_id not in index["by_asset_cluster_id"]: + raise ValueError(f"missing asset index for {asset_id}") + for evidence_index in _strings(item.get("evidence_indexes")): + if evidence_index not in index["by_evidence_index"]: + raise ValueError(f"missing evidence index for {evidence_index}") + for typ in _strings(item.get("candidate_structure_types")) or [item.get("candidate_structure_type")]: + if typ not in index["by_candidate_structure_type"]: + raise ValueError(f"missing candidate type index for {typ}") + gate = _dict(payload.get("quality_gate")) + if not _dict(gate.get("bo_coverage")).get("coverage_pass"): + raise ValueError("BO coverage failed") + if not _dict(gate.get("evidence_coverage")).get("coverage_pass"): + raise ValueError("evidence coverage failed") + + + def _update_audit(resolved_ids: set[str], blocked_ids: set[str], decisions: dict[str, dict[str, Any]]) -> None: + try: + audit = _dict(_read_doc(AUDIT_PATH)) + except Exception: + audit = {"schema_version": "stage1_legal_structure_audit.v1"} + trace = _list(audit.get("review_reduction_trace")) + for eid in sorted(resolved_ids): + decision = _dict(decisions.get(eid)) + trace.append({ + "exception_id": eid, + "resolution": "llm_adjudicated", + "decision": decision.get("decision"), + "reason_code": decision.get("reason_code"), + }) + for eid in sorted(blocked_ids): + trace.append({"exception_id": eid, "resolution": "blocked_to_review"}) + audit["review_reduction_trace"] = trace + _write_doc(AUDIT_PATH, audit) + + + def main() -> None: + try: + _init_mcp_session() + seed_bundle = _load_seed_bundle() + pack = _load_exception_pack(seed_bundle) + adjudication = _load_adjudication() + decisions, blocked, resolved_ids, blocked_ids = _validate_decisions(pack, adjudication) + structures = _build_structures(seed_bundle, decisions, blocked_ids, resolved_ids) + if not structures: + raise ValueError("no legal effect structures generated") + structure_index = _build_structure_index(structures, seed_bundle) + quality_gate = _quality_gate(structures, seed_bundle, blocked) + status = "READY_WITH_REVIEW" if quality_gate["review_queue"] else "READY" + payload = { + "schema_version": "stage1_legal_effect_structures.v1", + "status": status, + "legal_effect_structures": structures, + "structure_index": structure_index, + "quality_gate": quality_gate, + } + _validate_payload(payload) + _write_doc(TARGET_NAME, payload) + reread = _read_doc(TARGET_NAME) + if not isinstance(reread, dict) or reread.get("schema_version") != "stage1_legal_effect_structures.v1": + raise ValueError("post-write reread validation failed") + _update_audit(resolved_ids, blocked_ids, decisions) + print(json.dumps({ + "status": "READY", + "message": "legal_effect_structures.json 작성 완료", + "written_file": TARGET_NAME, + "payload_status": status, + "structure_count": len(structures), + "review_queue_count": len(quality_gate["review_queue"]), + "resolved_exception_count": len(resolved_ids), + "blocked_exception_count": len(blocked_ids), + }, ensure_ascii=False)) + except Exception as exc: + print(json.dumps({ + "status": "FAILED", + "message": "L2 final structure index writer 실패", + "target_file": TARGET_NAME, + "reason": str(exc), + }, ensure_ascii=False)) + raise + + + if __name__ == "__main__": + main() diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_3_Codex_v2.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_3_Codex_v2.yml new file mode 100644 index 00000000..29aa24be --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_3_Codex_v2.yml @@ -0,0 +1,2165 @@ +--- +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_3 + description: 민사소송 원고 송무 초지능 AI변호사 - Legal Effect 생성 + version: v2.0 + Stages: + - name: stage1_legal_effect_생성 + description: Legal Effect Structure 작성 + tools: + mcpServers: + localdocs: + type: streamable-http + url: http://mcp-localdocs:8012/mcp + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + task_procedure: + IN: + nexts: + - Task_LES0_input_pack_compiler + wait_until: [] + Task_LES0_input_pack_compiler: + nexts: + - Task_LES1_deterministic_structure_seed_builder + wait_until: + - IN + Task_LES1_deterministic_structure_seed_builder: + nexts: + - Task_LES2_exception_adjudicator_* + - Task_LES3_final_structure_index_writer + wait_until: + - Task_LES0_input_pack_compiler + Task_LES2_exception_adjudicator_*: + nexts: + - Task_LES3_final_structure_index_writer + wait_until: + - Task_LES1_deterministic_structure_seed_builder + Task_LES3_final_structure_index_writer: + nexts: + - OUT + wait_until: + - Task_LES1_deterministic_structure_seed_builder + - all Task_LES2_exception_adjudicator_* + OUT: + nexts: [] + wait_until: + - Task_LES3_final_structure_index_writer + + tasks: + - task_name: Task_LES0_input_pack_compiler + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: |- + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MESSAGE_IDS = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + INPUT_FILES = [ + "BO.json", + "evidence_indexed.json", + "actio_case_signals.json", + "case_liability_signals.json", + "legal_effect_signals.json", + ] + OUTPUT_PATH = "stage1_tmp/legal_effect_structure_input_pack.json" + CANONICAL_TYPES = [ + "money_claim", + "commercial_successor", + "secured_debt", + "registry_invalidity", + "land_use_gain", + "valuation", + "fraudulent_transfer", + "preserved_claim", + "succession_notice_lien_asset_defense", + "general_legal_effect", + ] + SOURCE_DOMAIN_FALLBACK = { + "B1_Money_Successor": "money_claim", + "B2_Secured_Registry": "secured_debt", + "B3_Land_Valuation": "land_use_gain", + "B4_Actio": "fraudulent_transfer", + "B5_Succession_Notice_Lien_Asset_Defense": "succession_notice_lien_asset_defense", + } + EXACT_TYPE_ALIASES = { + "auction_invalidity": ["registry_invalidity"], + "derivative_bank_mortgage_invalidity": ["secured_debt", "registry_invalidity"], + "mortgage_extinguishment": ["secured_debt", "registry_invalidity"], + "actio_remedy_or_cap": ["fraudulent_transfer"], + "remedy_or_cap": ["fraudulent_transfer"], + "value_compensation": ["fraudulent_transfer", "valuation"], + "insolvency_check": ["fraudulent_transfer"], + "actio_asset_target": ["fraudulent_transfer"], + "target_asset": ["fraudulent_transfer"], + "preserved_claim": ["preserved_claim"], + } + SIGNAL_CONTRACTS = { + "actio_case_signals.json": ("actio_case_signals.v1", "actio_case_signals"), + "case_liability_signals.json": ("case_liability_signals.v1", "case_liability_signals"), + "legal_effect_signals.json": ("legal_effect_signals.v1", "bo_legal_effect_routes"), + } + + def _next_id() -> int: + return next(MESSAGE_IDS) + + def _decode_json(value: Any, label: str) -> Any: + if not isinstance(value, str): + return value + text = value.strip() + if not text: + raise ValueError(f"{label}: empty JSON text") + try: + return json.loads(text) + except json.JSONDecodeError: + try: + obj, _ = JSON_DECODER.raw_decode(text) + return obj + except json.JSONDecodeError as exc: + raise ValueError(f"{label}: invalid JSON: {text[:240]}") from exc + + def _parse_rpc(text: str) -> dict[str, Any]: + candidates = [] + for line in text.splitlines(): + if line.startswith("data:"): + candidates.append(line[5:].strip()) + candidates.append(text.strip()) + for candidate in candidates: + if not candidate: + continue + try: + value = _decode_json(candidate, "MCP response") + except ValueError: + continue + if isinstance(value, dict) and ("result" in value or "error" in value): + return value + raise RuntimeError(f"MCP response could not be parsed: {text[:300]}") + + def _initialize() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage1-part3-les0-v2", + "version": "2.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + initialized = CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ) + initialized.raise_for_status() + + def _call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_rpc(response.text) + if parsed.get("error"): + raise RuntimeError(f"MCP {name} failed: {parsed['error']}") + return parsed + + def _read_doc(path: str) -> Any: + rpc = _call_tool("read_docs", {"doc_names": [path]}) + content = rpc.get("result", {}).get("content", []) + if not content or not isinstance(content[0], dict): + raise RuntimeError(f"{path}: read_docs returned no content") + envelope = _decode_json(content[0].get("text", ""), f"{path} envelope") + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results or not isinstance(results[0], dict): + raise RuntimeError(f"{path}: read_docs results are empty") + if results[0].get("error"): + raise RuntimeError(f"{path}: {results[0]['error']}") + inner = results[0].get("content", "") + else: + inner = envelope + return _decode_json(inner, path) + + def _artifact_text(payload: dict[str, Any]) -> str: + return json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n" + + def _write_doc(path: str, payload: dict[str, Any]) -> None: + _call_tool("write_file", {"path": path, "content": _artifact_text(payload), "overwrite": True}) + + def _digest(value: Any) -> str: + text = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return "sha256:" + hashlib.sha256(text.encode("utf-8")).hexdigest() + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + output = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in output: + output.append(text) + return output + + def _sorted_strings(value: Any) -> list[str]: + return sorted(set(_strings(value))) + + def _first(*values: Any) -> str | None: + for value in values: + items = _strings(value) + if items: + return items[0] + return None + + def _normalized(text: Any) -> str: + return re.sub(r"[^0-9a-zA-Z가-힣]", "", str(text or "").lower()) + + def _evidence_rows(doc: Any) -> list[dict[str, Any]]: + root = _as_dict(doc) + rows = root.get("evidence_indexed") + if not isinstance(rows, list): + rows = root.get("items") + if not isinstance(rows, list): + raise ValueError("evidence_indexed.json must contain evidence_indexed or items array") + return [row for row in rows if isinstance(row, dict)] + + def _canonicalize_label(label: str, source_domain: str | None) -> list[str]: + raw = label.strip() + lower = raw.lower().replace("-", "_").replace(" ", "_") + if lower in CANONICAL_TYPES: + return [lower] + if lower in EXACT_TYPE_ALIASES: + return EXACT_TYPE_ALIASES[lower] + types = [] + text = lower + " " + raw + families = [ + (["money", "loan", "debt_payment", "claim", "대여", "공사대금", "물품대금", "미지급", "채권", "변제"], "money_claim"), + (["successor", "business_transfer", "continued_trade", "상호속용", "영업양수", "승계책임"], "commercial_successor"), + (["secured", "mortgage", "collateral", "담보", "근저당", "피담보", "저당"], "secured_debt"), + (["registry", "auction_invalid", "cancellation", "derivative_invalid", "등기", "말소", "경매무효"], "registry_invalidity"), + (["land_use", "use_gain", "rent", "delivery", "사용이익", "임료", "차임", "인도"], "land_use_gain"), + (["valuation", "appraisal", "value_compensation", "감정", "평가", "가액", "시가"], "valuation"), + (["fraudulent", "beneficiary", "transferee", "insolvency", "remedy_or_cap", "target_asset", "사해", "수익자", "전득자", "무자력", "가액배상"], "fraudulent_transfer"), + (["preserved_claim", "피보전채권"], "preserved_claim"), + (["succession", "notice", "lien", "asset_defense", "상속", "통지", "유치권", "항변"], "succession_notice_lien_asset_defense"), + ] + if "possession" in text or "점유" in text: + if source_domain == "B3_Land_Valuation": + types.append("land_use_gain") + elif source_domain == "B5_Succession_Notice_Lien_Asset_Defense": + types.append("succession_notice_lien_asset_defense") + for needles, canonical in families: + if any(needle in text for needle in needles): + types.append(canonical) + return [item for item in CANONICAL_TYPES if item in types] + + def _signal_rows(doc: Any, name: str) -> list[dict[str, Any]]: + schema, key = SIGNAL_CONTRACTS[name] + root = _as_dict(doc) + if root.get("schema_version") != schema: + raise ValueError(f"{name}: schema_version must be {schema}") + if root.get("status") not in {"READY", "READY_WITH_REVIEW", "EMPTY"}: + raise ValueError(f"{name}: invalid status") + rows = root.get(key) + if not isinstance(rows, list): + raise ValueError(f"{name}.{key} must be array") + return [row for row in rows if isinstance(row, dict)] + + def _signal_bo_ids(row: Any) -> list[str]: + output = [] + if isinstance(row, dict): + for key, value in row.items(): + if key in {"bo_id", "BO_ID"}: + output.extend(_strings(value)) + elif key in {"related_bo_ids", "source_bo_ids"}: + output.extend(_strings(value)) + elif isinstance(value, (dict, list)): + output.extend(_signal_bo_ids(value)) + elif isinstance(row, list): + for item in row: + output.extend(_signal_bo_ids(item)) + return _sorted_strings(output) + + def _group_signals(rows: list[dict[str, Any]], bo_ids: set[str]) -> dict[str, list[dict[str, Any]]]: + output = {bo_id: [] for bo_id in bo_ids} + for row in rows: + refs = _signal_bo_ids(row) + unknown = sorted(set(refs) - bo_ids) + if unknown: + raise ValueError(f"signal references unknown BO IDs: {unknown}") + for bo_id in refs: + output[bo_id].append(row) + return output + + def _add_alias(alias_map: dict[str, dict[str, Any]], ref: str, row: dict[str, Any], review: list[dict[str, Any]]) -> None: + old = alias_map.get(ref) + label = row.get("canonical_label_candidate") + if old and old.get("canonical_label_candidate") and label and old["canonical_label_candidate"] != label: + review.append({ + "review_code": "ASSET_ALIAS_LABEL_CONFLICT", + "source_bo_ids": _sorted_strings(row.get("source_bo_ids")), + "evidence_indexes": _sorted_strings([old.get("evidence_index"), row.get("evidence_index")]), + "related_ref_keys": [ref], + "reason_code": "CONFLICTING_NON_NULL_CANONICAL_LABELS", + }) + return + merged = dict(old or {}) + for key, value in row.items(): + if value not in (None, "", []): + merged[key] = value + merged["source_bo_ids"] = _sorted_strings(_strings((old or {}).get("source_bo_ids")) + _strings(row.get("source_bo_ids"))) + alias_map[ref] = merged + + def _build_evidence_and_aliases(rows: list[dict[str, Any]], bo_items: list[dict[str, Any]]) -> tuple[dict[str, Any], dict[str, Any], list[dict[str, Any]]]: + evidence = {} + aliases = {} + review = [] + for row in rows: + evidence_index = _first(row.get("evidence_index"), row.get("id")) + if not evidence_index: + raise ValueError("evidence row missing evidence_index") + if evidence_index in evidence: + raise ValueError(f"duplicate evidence_index: {evidence_index}") + evidence[evidence_index] = { + "evidence_index": evidence_index, + "source_title": _first(row.get("source_title"), row.get("title")), + "doc_type": _first(row.get("doc_type")), + "source_pointer": _as_dict(row.get("source_pointer")), + "priority_class": _first(row.get("priority_class")), + "authentication_status": _first(row.get("authentication_status")), + } + for schedule in _as_list(row.get("schedule_items")): + if not isinstance(schedule, dict): + continue + ref = _first(schedule.get("schedule_ref")) + if ref: + _add_alias(aliases, ref, { + "ref_type": "schedule", + "canonical_label_candidate": _first(schedule.get("canonical_label_candidate")), + "evidence_index": evidence_index, + "usable_in_relief": _first(schedule.get("usable_in_relief")), + "source_bo_ids": [], + }, review) + for registry in _as_list(row.get("registry_row_components")): + if not isinstance(registry, dict): + continue + ref = _first(registry.get("registry_ref")) + if ref: + _add_alias(aliases, ref, { + "ref_type": "registry_row", + "canonical_label_candidate": None, + "evidence_index": evidence_index, + "section": _first(registry.get("section"), registry.get("kind")), + "row_seq": registry.get("row_seq"), + "source_bo_ids": [], + }, review) + for bo in bo_items: + bo_id = _first(bo.get("BO_ID"), bo.get("id")) + payload = _as_dict(_as_dict(bo.get("extensions")).get("domain_payload")) + for alias in _as_list(payload.get("asset_alias")): + if not isinstance(alias, dict): + continue + labels = _strings(alias.get("canonical_label_candidate")) + _strings(alias.get("alias_strings")) + label = labels[0] if labels else None + for ref in _strings(alias.get("schedule_refs")) + _strings(alias.get("registry_refs")): + _add_alias(aliases, ref, { + "ref_type": "schedule" if ref.startswith("SCH-") else "registry_row", + "canonical_label_candidate": label, + "evidence_index": None, + "source_bo_ids": [bo_id] if bo_id else [], + }, review) + return evidence, aliases, review + + def _asset_kind(label: str, refs: list[str]) -> str: + text = label.lower() + if any(ref.startswith(("REG-", "SCH-")) for ref in refs) or any(word in text for word in ["토지", "대지", "건물", "상가", "빌라", "임야", "부동산"]): + return "real_property" + if any(word in text for word in ["근저당", "저당", "담보권"]): + return "security_right" + if any(word in text for word in ["채권", "대여금", "공사대금", "물품대금"]): + return "claim" + if any(word in text for word in ["예금", "현금", "계좌"]): + return "cash_or_deposit" + return "unknown" + + def _extract_ref_keys(value: Any) -> list[str]: + refs = [] + for text in _strings(value): + refs.extend(re.findall(r"(?:REG|SCH)-\d{3}-\d{2}", text)) + return _sorted_strings(refs) + + def _asset_candidates(bo: dict[str, Any], actio_rows: list[dict[str, Any]], alias_map: dict[str, Any]) -> list[dict[str, Any]]: + bo_id = _first(bo.get("BO_ID"), bo.get("id")) or "" + evidence_indexes = _sorted_strings(bo.get("source_evidence_indexes")) + payload = _as_dict(_as_dict(bo.get("extensions")).get("domain_payload")) + candidates = [] + + def add(label_value: Any, source_code: str, refs_value: Any = None, evidence_value: Any = None) -> None: + for label in _strings(label_value): + if re.fullmatch(r"\d{4}-\d{2}-\d{2}", label): + continue + refs = _sorted_strings(_strings(refs_value) + _extract_ref_keys(label)) + if not refs: + normalized_label = _normalized(label) + matches = [] + if len(normalized_label) >= 4: + for ref, alias in alias_map.items(): + if not ref.startswith("SCH-"): + continue + alias_label = _normalized(_as_dict(alias).get("canonical_label_candidate")) + same_scope = bo_id in _strings(_as_dict(alias).get("source_bo_ids")) or _as_dict(alias).get("evidence_index") in evidence_indexes + if same_scope and normalized_label in alias_label: + matches.append(ref) + if len(matches) == 1: + refs = matches + candidates.append({ + "asset_label": label, + "asset_kind": _asset_kind(label, refs), + "asset_ref_keys": refs, + "source_code": source_code, + "evidence_indexes": _sorted_strings(evidence_value or evidence_indexes), + }) + + add(payload.get("asset_bindings"), "BO_ASSET_BINDINGS") + add(payload.get("asset_binding_hint"), "BO_ASSET_BINDING_HINT") + add(payload.get("registry_object_display"), "BO_REGISTRY_OBJECT_DISPLAY") + add(payload.get("valuation_object"), "BO_VALUATION_OBJECT") + possession_ref = _as_dict(payload.get("possession_state")).get("asset_ref") + if possession_ref: + add(possession_ref, "BO_POSSESSION_ASSET_REF", possession_ref) + for registry in _as_list(payload.get("registry_rows")): + if isinstance(registry, dict) and registry.get("registry_ref"): + add(registry.get("registry_ref"), "BO_REGISTRY_ROW", registry.get("registry_ref")) + for alias in _as_list(payload.get("asset_alias")): + if isinstance(alias, dict): + refs = _strings(alias.get("schedule_refs")) + _strings(alias.get("registry_refs")) + labels = _strings(alias.get("canonical_label_candidate")) + _strings(alias.get("alias_strings")) + add(labels or refs, "BO_EXPLICIT_ASSET_ALIAS", refs) + for signal in actio_rows: + for candidate in _as_list(signal.get("target_property_candidates")): + if isinstance(candidate, dict): + add(candidate.get("asset_label"), "ACTIO_TARGET_PROPERTY", None, candidate.get("related_evidence_indexes")) + output = [] + seen = set() + for item in candidates: + key = (item["asset_label"], tuple(item["asset_ref_keys"]), item["source_code"]) + if key not in seen: + seen.add(key) + output.append(item) + return sorted(output, key=lambda row: (row["asset_ref_keys"], row["asset_label"], row["source_code"])) + + def _candidate_record(bo: dict[str, Any], signals: dict[str, list[dict[str, Any]]], alias_map: dict[str, Any]) -> dict[str, Any]: + bo_id = _first(bo.get("BO_ID"), bo.get("id")) + if not bo_id: + raise ValueError("BO row missing BO_ID and id") + payload = _as_dict(_as_dict(bo.get("extensions")).get("domain_payload")) + source_domain = _first(_as_dict(bo.get("provenance")).get("source_domain")) + raw_labels = [] + for route in signals["legal_effect"]: + raw_labels.extend(_strings(route.get("candidate_structure_types"))) + for row in signals["case_liability"]: + raw_labels.extend(_strings(row.get("liability_candidate_type"))) + for row in signals["actio"]: + raw_labels.extend(_strings(row.get("actio_scope"))) + raw_labels.extend(_strings(payload.get("domain_legal_effect_tag"))) + raw_labels.extend(_strings(payload.get("legal_effect_tags"))) + raw_labels.extend(_strings(bo.get("Legal_Keywords"))) + canonical = [] + nonrouting = [] + for label in _strings(raw_labels): + mapped = _canonicalize_label(label, source_domain) + if mapped: + canonical.extend(mapped) + else: + nonrouting.append(label) + if not canonical: + canonical = [SOURCE_DOMAIN_FALLBACK.get(source_domain, "general_legal_effect")] + canonical = [item for item in CANONICAL_TYPES if item in canonical] + claim_ids = [] + liability_ids = [] + linked_structure_ids = [] + actio_signal_ids = [] + evidence_indexes = _strings(bo.get("source_evidence_indexes")) + for row in signals["case_liability"]: + claim_ids.extend(_strings(row.get("claim_group_id"))) + liability_ids.extend(_strings(row.get("liability_group_id"))) + linked_structure_ids.extend(_strings(row.get("linked_structure_ids"))) + evidence_indexes.extend(_strings(row.get("related_evidence_indexes"))) + for row in signals["legal_effect"]: + linked_structure_ids.extend(_strings(row.get("linked_structure_ids"))) + evidence_indexes.extend(_strings(row.get("related_evidence_indexes"))) + for row in signals["actio"]: + actio_signal_ids.extend(_strings(row.get("signal_id"))) + evidence_indexes.extend(_strings(row.get("related_evidence_indexes"))) + downstream = _as_dict(bo.get("downstream_seed_refs")) + claim_ids.extend(_strings(downstream.get("claim_group_seed_refs_proposed"))) + linked_structure_ids.extend(_strings(downstream.get("legal_effect_structure_seed_ref_proposed"))) + theory_refs = _strings(downstream.get("canonical_theory_graph_seed_ref_proposed")) + basis_codes = ["BO_SOURCE", "EVIDENCE_INDEX", "SOURCE_DOMAIN_FALLBACK"] + if signals["legal_effect"]: + basis_codes.append("LEGAL_EFFECT_SIGNAL") + if signals["case_liability"]: + basis_codes.append("CASE_LIABILITY_SIGNAL") + if signals["actio"]: + basis_codes.append("ACTIO_SIGNAL") + return { + "bo_id": bo_id, + "source_domain": source_domain, + "hard_issue_keys": { + "claim_group_ids": _sorted_strings(claim_ids), + "liability_group_ids": _sorted_strings(liability_ids), + "canonical_theory_graph_seed_refs": _sorted_strings(theory_refs), + "linked_structure_ids": _sorted_strings(linked_structure_ids), + }, + "canonical_structure_types": canonical, + "nonrouting_labels": _sorted_strings(nonrouting), + "asset_candidates": _asset_candidates(bo, signals["actio"], alias_map), + "evidence_indexes": _sorted_strings(evidence_indexes), + "linked_claim_group_ids": _sorted_strings(claim_ids), + "linked_liability_group_ids": _sorted_strings(liability_ids), + "linked_actio_signal_ids": _sorted_strings(actio_signal_ids), + "issue_basis": { + "legal_keywords": _sorted_strings(bo.get("Legal_Keywords")), + "action": _first(bo.get("Action")), + "reason": _first(bo.get("Reason")), + "juristic_act_label": _first(_as_dict(bo.get("JuristicAct")).get("label")), + "action_type": _first(bo.get("ActionType")), + "bo_type": _first(bo.get("BOType")), + }, + "basis_codes": _sorted_strings(basis_codes), + } + + def main() -> None: + fingerprint = None + try: + _initialize() + docs = {name: _read_doc(name) for name in INPUT_FILES} + bo_items = docs["BO.json"] + if not isinstance(bo_items, list): + raise ValueError("BO.json root must be array") + bo_rows = [row for row in bo_items if isinstance(row, dict)] + bo_ids = [_first(row.get("BO_ID"), row.get("id")) for row in bo_rows] + if any(not value for value in bo_ids): + raise ValueError("every BO row must have BO_ID or id") + if len(set(bo_ids)) != len(bo_ids): + raise ValueError("BO IDs must be unique") + bo_universe = set(bo_ids) + evidence_rows = _evidence_rows(docs["evidence_indexed.json"]) + signal_rows = {name: _signal_rows(docs[name], name) for name in SIGNAL_CONTRACTS} + grouped = {name: _group_signals(rows, bo_universe) for name, rows in signal_rows.items()} + evidence, aliases, input_review = _build_evidence_and_aliases(evidence_rows, bo_rows) + records = {} + for bo in sorted(bo_rows, key=lambda row: _first(row.get("BO_ID"), row.get("id")) or ""): + bo_id = _first(bo.get("BO_ID"), bo.get("id")) or "" + signals = { + "actio": grouped["actio_case_signals.json"].get(bo_id, []), + "case_liability": grouped["case_liability_signals.json"].get(bo_id, []), + "legal_effect": grouped["legal_effect_signals.json"].get(bo_id, []), + } + record = _candidate_record(bo, signals, aliases) + missing = sorted(set(record["evidence_indexes"]) - set(evidence)) + if missing: + input_review.append({ + "review_code": "MISSING_EVIDENCE_AUTHORITY", + "source_bo_ids": [bo_id], + "evidence_indexes": missing, + "related_ref_keys": [], + "reason_code": "REFERENCED_EVIDENCE_NOT_INDEXED", + }) + records[bo_id] = record + if set(records) != bo_universe: + raise ValueError("BO-to-candidate conservation failed") + input_digests = {name: _digest(docs[name]) for name in INPUT_FILES} + fingerprint = _digest(input_digests) + status = "READY_WITH_REVIEW" if input_review else "READY" + payload = { + "legal_structure_input_pack": { + "schema_version": "stage1_legal_structure_input_pack.v2", + "status": status, + "run_fingerprint": fingerprint, + "input_digests": input_digests, + "source_counts": { + "bo": len(bo_rows), + "evidence": len(evidence_rows), + "actio_signals": len(signal_rows["actio_case_signals.json"]), + "case_liability_signals": len(signal_rows["case_liability_signals.json"]), + "legal_effect_signals": len(signal_rows["legal_effect_signals.json"]), + }, + "evidence_authority_by_index": evidence, + "asset_alias_by_ref": aliases, + "candidate_records_by_bo_id": records, + "input_review_items": input_review, + } + } + _write_doc(OUTPUT_PATH, payload) + reread = _read_doc(OUTPUT_PATH) + if _digest(reread) != _digest(payload): + raise ValueError("LES0 post-write reread mismatch") + print(json.dumps({ + "les0_manifest": { + "schema_version": "stage1_legal_structure_input_manifest.v2", + "status": status, + "artifact_path": OUTPUT_PATH, + "run_fingerprint": fingerprint, + "bo_count": len(bo_rows), + "candidate_count": len(records), + "review_count": len(input_review), + } + }, ensure_ascii=False, separators=(",", ":"))) + except Exception as exc: + print(json.dumps({ + "les0_manifest": { + "schema_version": "stage1_legal_structure_input_manifest.v2", + "status": "FAILED", + "artifact_path": OUTPUT_PATH, + "run_fingerprint": fingerprint, + "failure_reason": str(exc), + } + }, ensure_ascii=False, separators=(",", ":"))) + raise + + if __name__ == "__main__": + main() + + - task_name: Task_LES1_deterministic_structure_seed_builder + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: |- + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + from collections import defaultdict + from typing import Any + + import httpx + + LES0_MANIFEST_RAW = r'''{{prev.Task_LES0_input_pack_compiler}}''' + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MESSAGE_IDS = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + INPUT_PATH = "stage1_tmp/legal_effect_structure_input_pack.json" + SEED_PATH = "stage1_tmp/legal_effect_structure_seed_bundle.json" + EXCEPTION_PATH = "stage1_tmp/legal_effect_structure_exception_pack.json" + MAX_EXCEPTION_ITEMS_PER_PACK = 4 + TARGET_EXCEPTION_ITEMS_PER_PACK = 3 + MAX_EXCEPTION_PACK_BYTES = 12000 + MAX_EXCEPTION_CANDIDATE_VALUES = 8 + MAX_EXCEPTION_PACKS = 8 + MAX_CONCURRENCY = 2 + CANONICAL_TYPES = [ + "money_claim", + "commercial_successor", + "secured_debt", + "registry_invalidity", + "land_use_gain", + "valuation", + "fraudulent_transfer", + "preserved_claim", + "succession_notice_lien_asset_defense", + "general_legal_effect", + ] + SOURCE_DOMAIN_FALLBACK = { + "B1_Money_Successor": "money_claim", + "B2_Secured_Registry": "secured_debt", + "B3_Land_Valuation": "land_use_gain", + "B4_Actio": "fraudulent_transfer", + "B5_Succession_Notice_Lien_Asset_Defense": "succession_notice_lien_asset_defense", + } + TYPE_ROLES = { + "money_claim": "claim_chain_ref", + "commercial_successor": "claim_chain_ref", + "secured_debt": "linked_structures", + "registry_invalidity": "linked_structures", + "land_use_gain": "legal_calculation_object", + "valuation": "legal_calculation_object", + "fraudulent_transfer": "actio_roles", + "preserved_claim": "claim_chain_ref", + "succession_notice_lien_asset_defense": "linked_structures", + "general_legal_effect": "legal_effect_roles", + } + HARD_ISSUE_KEY_PRIORITY = [ + "claim_group_ids", + "liability_group_ids", + "canonical_theory_graph_seed_refs", + "linked_structure_ids", + ] + ALLOWED_DECISIONS = { + "ambiguous_issue_cluster": ["USE_EXISTING_HARD_KEY", "PRESERVE_MULTI_ISSUE", "SPLIT_ISSUE_CLUSTER", "BLOCK_REVIEW"], + "ambiguous_asset_cluster": ["SAME_CLUSTER", "SEPARATE_CLUSTER", "MULTI_ASSET_BUNDLE", "BLOCK_REVIEW"], + "ambiguous_candidate_structure_type": ["SELECT_REPRESENTATIVE_TYPE", "PRESERVE_MULTI_ROUTING", "SPLIT_STRUCTURE_SEED", "BLOCK_REVIEW"], + } + REVIEW_CODES = { + "ASSET_ALIAS_LABEL_CONFLICT", + "ASSET_IDENTITY_DEFERRED_SEPARATE", + "ISSUE_FAMILY_LOW_MARGIN", + "MISSING_EVIDENCE_AUTHORITY", + "PACK_CONTEXT_TOO_LARGE", + } + + def _next_id() -> int: + return next(MESSAGE_IDS) + + def _decode_json(value: Any, label: str) -> Any: + if not isinstance(value, str): + return value + text = value.strip() + if not text: + raise ValueError(f"{label}: empty JSON text") + try: + return json.loads(text) + except json.JSONDecodeError: + try: + obj, _ = JSON_DECODER.raw_decode(text) + return obj + except json.JSONDecodeError as exc: + raise ValueError(f"{label}: invalid JSON: {text[:240]}") from exc + + def _parse_rpc(text: str) -> dict[str, Any]: + candidates = [line[5:].strip() for line in text.splitlines() if line.startswith("data:")] + candidates.append(text.strip()) + for candidate in candidates: + if not candidate: + continue + try: + value = _decode_json(candidate, "MCP response") + except ValueError: + continue + if isinstance(value, dict) and ("result" in value or "error" in value): + return value + raise RuntimeError(f"MCP response could not be parsed: {text[:300]}") + + def _initialize() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage1-part3-les1-v2", + "version": "2.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_rpc(response.text) + if parsed.get("error"): + raise RuntimeError(f"MCP {name} failed: {parsed['error']}") + return parsed + + def _read_doc(path: str) -> Any: + rpc = _call_tool("read_docs", {"doc_names": [path]}) + content = rpc.get("result", {}).get("content", []) + if not content or not isinstance(content[0], dict): + raise RuntimeError(f"{path}: read_docs returned no content") + envelope = _decode_json(content[0].get("text", ""), f"{path} envelope") + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results or not isinstance(results[0], dict): + raise RuntimeError(f"{path}: read_docs results are empty") + if results[0].get("error"): + raise RuntimeError(f"{path}: {results[0]['error']}") + inner = results[0].get("content", "") + else: + inner = envelope + return _decode_json(inner, path) + + def _artifact_text(payload: dict[str, Any]) -> str: + return json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n" + + def _artifact_hash(payload: dict[str, Any]) -> str: + return "sha256:" + hashlib.sha256(_artifact_text(payload).encode("utf-8")).hexdigest() + + def _write_doc(path: str, payload: dict[str, Any]) -> None: + _call_tool("write_file", {"path": path, "content": _artifact_text(payload), "overwrite": True}) + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + output = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in output: + output.append(text) + return output + + def _sorted_strings(value: Any) -> list[str]: + return sorted(set(_strings(value))) + + def _stable_id(prefix: str, ordinal: int) -> str: + return f"{prefix}-{ordinal:03d}" + + def _normalized(value: Any) -> str: + return re.sub(r"[^0-9a-zA-Z가-힣]", "", str(value or "").lower()) or "unknown" + + def _priority_types(value: Any, source_domain: str | None) -> list[str]: + supplied = _strings(value) + unknown = sorted(set(supplied) - set(CANONICAL_TYPES)) + if unknown: + raise ValueError(f"candidate record contains non-canonical types: {unknown}") + if not supplied: + supplied = [SOURCE_DOMAIN_FALLBACK.get(source_domain, "general_legal_effect")] + return [item for item in CANONICAL_TYPES if item in supplied] + + def _issue_keys(record: dict[str, Any], representative_type: str) -> tuple[list[str], bool]: + hard = _as_dict(record.get("hard_issue_keys")) + keys = [] + for family in HARD_ISSUE_KEY_PRIORITY: + for value in _sorted_strings(hard.get(family)): + keys.append(f"hard:{family}:{value}") + if keys: + return keys, False + return [f"family:{representative_type}"], representative_type == "general_legal_effect" + + def _add_review( + reviews: list[dict[str, Any]], + review_index: dict[tuple[Any, ...], str], + review_by_bo: dict[str, list[str]], + code: str, + bo_ids: Any, + evidence_indexes: Any, + ref_keys: Any, + reason_code: str, + severity: str = "WARNING", + owner: str = "STAGE_2", + ) -> str: + if code not in REVIEW_CODES: + raise ValueError(f"unsupported review code: {code}") + bos = _sorted_strings(bo_ids) + evidence = _sorted_strings(evidence_indexes) + refs = _sorted_strings(ref_keys) + key = (code, tuple(bos), tuple(evidence), tuple(refs), reason_code, severity, owner) + if key in review_index: + return review_index[key] + review_id = _stable_id("LRV", len(reviews) + 1) + item = { + "review_id": review_id, + "review_code": code, + "severity": severity, + "source_bo_ids": bos, + "evidence_indexes": evidence, + "related_ref_keys": refs, + "reason_code": reason_code, + "downstream_owner": owner, + } + reviews.append(item) + review_index[key] = review_id + for bo_id in bos: + review_by_bo[bo_id].append(review_id) + return review_id + + def _explicit_exception_draft(record: dict[str, Any], issue_ids: list[str], asset_ids: list[str], types: list[str]) -> dict[str, Any] | None: + codes = set(_strings(record.get("basis_codes"))) + common = {"FINAL_SINGLE_SELECTION_REQUIRED", "PRESERVE_OR_DEFER_PROHIBITED", "COMPACT_CONTEXT_SUFFICIENT"} + if not common.issubset(codes): + return None + if "AMBIGUOUS_ISSUE_FINAL_REQUIRED" in codes: + exception_type = "ambiguous_issue_cluster" + candidates = issue_ids + elif "AMBIGUOUS_ASSET_FINAL_REQUIRED" in codes: + exception_type = "ambiguous_asset_cluster" + candidates = asset_ids + elif "AMBIGUOUS_TYPE_FINAL_REQUIRED" in codes: + exception_type = "ambiguous_candidate_structure_type" + candidates = types + else: + return None + candidates = _sorted_strings(candidates) + if len(candidates) < 2: + return None + return { + "exception_type": exception_type, + "source_bo_ids": [record["bo_id"]], + "candidate_values": candidates, + "basis_codes": sorted(common | {code for code in codes if code.startswith("AMBIGUOUS_")}), + "score_summary": [], + "conflict_codes": ["FINALIZATION_REQUIRES_IRREVERSIBLE_CHOICE"], + "allowed_decisions": ALLOWED_DECISIONS[exception_type], + "compact_context": { + "bo_id": record["bo_id"], + "source_domain": record.get("source_domain"), + "candidate_values": candidates, + "basis_codes": sorted(common), + }, + } + + def main() -> None: + fingerprint = None + try: + _initialize() + manifest_outer = _decode_json(LES0_MANIFEST_RAW, "LES0 manifest") + manifest = _as_dict(_as_dict(manifest_outer).get("les0_manifest")) + if manifest.get("schema_version") != "stage1_legal_structure_input_manifest.v2": + raise ValueError("LES0 manifest schema mismatch") + if manifest.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("LES0 manifest is not consumable") + if manifest.get("artifact_path") != INPUT_PATH: + raise ValueError("LES0 artifact path mismatch") + input_outer = _read_doc(INPUT_PATH) + input_pack = _as_dict(_as_dict(input_outer).get("legal_structure_input_pack")) + if input_pack.get("schema_version") != "stage1_legal_structure_input_pack.v2": + raise ValueError("input pack schema mismatch") + if input_pack.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("input pack status is not consumable") + fingerprint = input_pack.get("run_fingerprint") + if not fingerprint or fingerprint != manifest.get("run_fingerprint"): + raise ValueError("LES0 fingerprint mismatch") + records = _as_dict(input_pack.get("candidate_records_by_bo_id")) + if len(records) != int(manifest.get("candidate_count", -1)): + raise ValueError("LES0 candidate count mismatch") + evidence_authority = _as_dict(input_pack.get("evidence_authority_by_index")) + alias_map = _as_dict(input_pack.get("asset_alias_by_ref")) + + reviews = [] + review_index = {} + review_by_bo = defaultdict(list) + for source_review in _as_list(input_pack.get("input_review_items")): + if not isinstance(source_review, dict): + continue + code = source_review.get("review_code") + if code not in REVIEW_CODES: + continue + _add_review( + reviews, + review_index, + review_by_bo, + code, + source_review.get("source_bo_ids"), + source_review.get("evidence_indexes"), + source_review.get("related_ref_keys"), + str(source_review.get("reason_code") or "UPSTREAM_REVIEW"), + "HARD_WARNING" if code == "ASSET_ALIAS_LABEL_CONFLICT" else "WARNING", + "HUMAN_REVIEW" if code == "ASSET_ALIAS_LABEL_CONFLICT" else "STAGE_2", + ) + + issue_key_to_id = {} + asset_key_to_id = {} + issue_map = {} + asset_map = {} + seeds = [] + exception_drafts = [] + + for bo_id in sorted(records): + record = _as_dict(records[bo_id]) + if record.get("bo_id") != bo_id: + raise ValueError(f"candidate record key mismatch: {bo_id}") + types = _priority_types(record.get("canonical_structure_types"), record.get("source_domain")) + representative = types[0] + issue_keys, low_margin = _issue_keys(record, representative) + issue_ids = [] + for key in issue_keys: + if key not in issue_key_to_id: + issue_id = _stable_id("IC", len(issue_key_to_id) + 1) + issue_key_to_id[key] = issue_id + issue_map[issue_id] = { + "issue_cluster_id": issue_id, + "cluster_key": key, + "source_bo_ids": [], + "evidence_indexes": [], + "candidate_structure_types": [], + "basis_codes": [], + } + issue_id = issue_key_to_id[key] + issue_ids.append(issue_id) + row = issue_map[issue_id] + row["source_bo_ids"] = _sorted_strings(row["source_bo_ids"] + [bo_id]) + row["evidence_indexes"] = _sorted_strings(row["evidence_indexes"] + _strings(record.get("evidence_indexes"))) + row["candidate_structure_types"] = [item for item in CANONICAL_TYPES if item in row["candidate_structure_types"] + types] + row["basis_codes"] = _sorted_strings(row["basis_codes"] + _strings(record.get("basis_codes"))) + if low_margin: + _add_review( + reviews, review_index, review_by_bo, + "ISSUE_FAMILY_LOW_MARGIN", [bo_id], record.get("evidence_indexes"), [], + "GENERAL_LEGAL_EFFECT_FALLBACK", "WARNING", "STAGE_2", + ) + + asset_ids = [] + free_asset_count = 0 + for candidate in _as_list(record.get("asset_candidates")): + if not isinstance(candidate, dict): + continue + refs = _sorted_strings(candidate.get("asset_ref_keys")) + if refs: + keys = [f"ref:{ref}" for ref in refs] + unknown_refs = sorted(set(refs) - set(alias_map)) + if unknown_refs: + _add_review( + reviews, review_index, review_by_bo, + "ASSET_IDENTITY_DEFERRED_SEPARATE", [bo_id], candidate.get("evidence_indexes"), unknown_refs, + "UNKNOWN_SOURCE_REF", "HARD_WARNING", "HUMAN_REVIEW", + ) + else: + free_asset_count += 1 + keys = [f"free:{bo_id}:{candidate.get('asset_kind') or 'unknown'}:{_normalized(candidate.get('asset_label'))}"] + for key in keys: + if key not in asset_key_to_id: + asset_id = _stable_id("AC", len(asset_key_to_id) + 1) + asset_key_to_id[key] = asset_id + asset_map[asset_id] = { + "asset_cluster_id": asset_id, + "cluster_key": key, + "asset_kind": candidate.get("asset_kind") or "unknown", + "canonical_label_candidate": candidate.get("asset_label"), + "asset_ref_keys": refs, + "source_bo_ids": [], + "evidence_indexes": [], + } + asset_id = asset_key_to_id[key] + asset_ids.append(asset_id) + row = asset_map[asset_id] + row["source_bo_ids"] = _sorted_strings(row["source_bo_ids"] + [bo_id]) + row["evidence_indexes"] = _sorted_strings(row["evidence_indexes"] + _strings(candidate.get("evidence_indexes"))) + asset_ids = _sorted_strings(asset_ids) + if free_asset_count >= 2: + _add_review( + reviews, review_index, review_by_bo, + "ASSET_IDENTITY_DEFERRED_SEPARATE", [bo_id], record.get("evidence_indexes"), [], + "MULTIPLE_REF_FREE_ASSET_MENTIONS", "WARNING", "STAGE_2", + ) + bundle_refs = [_stable_id("BUNDLE", len(seeds) + 1)] if len(asset_ids) > 1 else [] + seed_id = _stable_id("LESS", len(seeds) + 1) + seed = { + "seed_id": seed_id, + "source_bo_ids": [bo_id], + "candidate_structure_type": representative, + "candidate_structure_types": types, + "issue_cluster_ids": _sorted_strings(issue_ids), + "asset_cluster_ids": asset_ids, + "multi_asset_bundle_refs": bundle_refs, + "evidence_indexes": _sorted_strings(record.get("evidence_indexes")), + "linked_claim_group_ids": _sorted_strings(record.get("linked_claim_group_ids")), + "linked_liability_group_ids": _sorted_strings(record.get("linked_liability_group_ids")), + "linked_actio_signal_ids": _sorted_strings(record.get("linked_actio_signal_ids")), + "legal_effect_roles": _sorted_strings([TYPE_ROLES[item] for item in types]), + "basis_codes": _sorted_strings(record.get("basis_codes")), + "confidence": "HIGH", + "pending_exception_ids": [], + "deterministic_review_ids": [], + } + draft = _explicit_exception_draft(record, issue_ids, asset_ids, types) + if draft: + exception_drafts.append((seed, draft)) + seeds.append(seed) + + exceptions = [] + for seed, draft in exception_drafts: + candidates = _strings(draft.get("candidate_values")) + preview = dict(draft) + if len(candidates) > MAX_EXCEPTION_CANDIDATE_VALUES or len(_artifact_text(preview).encode("utf-8")) > MAX_EXCEPTION_PACK_BYTES: + review_id = _add_review( + reviews, review_index, review_by_bo, + "PACK_CONTEXT_TOO_LARGE", draft.get("source_bo_ids"), [], [], + "EXCEPTION_ITEM_EXCEEDS_MICRO_PACK_BUDGET", "HARD_WARNING", "HUMAN_REVIEW", + ) + seed["deterministic_review_ids"].append(review_id) + continue + exception_id = _stable_id("LEX", len(exceptions) + 1) + item = {"exception_id": exception_id, **draft} + exceptions.append(item) + seed["pending_exception_ids"].append(exception_id) + + for seed in seeds: + bo_ids = seed["source_bo_ids"] + seed["deterministic_review_ids"] = _sorted_strings( + seed["deterministic_review_ids"] + [review_id for bo_id in bo_ids for review_id in review_by_bo.get(bo_id, [])] + ) + if seed["deterministic_review_ids"]: + seed["confidence"] = "MEDIUM" + + packs = [] + by_type = defaultdict(list) + for item in exceptions: + by_type[item["exception_type"]].append(item) + for exception_type in sorted(by_type): + rows = by_type[exception_type] + for offset in range(0, len(rows), TARGET_EXCEPTION_ITEMS_PER_PACK): + items = rows[offset:offset + TARGET_EXCEPTION_ITEMS_PER_PACK] + pack_id = _stable_id("LES-PACK", len(packs) + 1) + descriptor = { + "pack_id": pack_id, + "exception_type": exception_type, + "exception_ids": [item["exception_id"] for item in items], + "items": items, + "output_path": f"stage1_tmp/legal_effect_structure_adjudication_parts/PACK-{len(packs) + 1:03d}.json", + "run_fingerprint": fingerprint, + } + if len(_artifact_text(descriptor).encode("utf-8")) > MAX_EXCEPTION_PACK_BYTES: + raise ValueError(f"{pack_id}: serialized micro-pack exceeds {MAX_EXCEPTION_PACK_BYTES} bytes") + packs.append(descriptor) + if len(packs) > MAX_EXCEPTION_PACKS: + blocked_payload = { + "legal_structure_exception_pack": { + "schema_version": "stage1_legal_structure_exception_pack.v2", + "status": "BLOCKED", + "run_fingerprint": fingerprint, + "expected_exception_ids": [item["exception_id"] for item in exceptions], + "exceptions": exceptions, + "deterministic_review_items": reviews, + "pack_count": len(packs), + "packs": packs, + } + } + _write_doc(EXCEPTION_PATH, blocked_payload) + raise ValueError(f"exception pack count {len(packs)} exceeds {MAX_EXCEPTION_PACKS}") + + expected_ids = [item["exception_id"] for item in exceptions] + packed_ids = [exception_id for pack in packs for exception_id in pack["exception_ids"]] + if sorted(expected_ids) != sorted(packed_ids) or len(packed_ids) != len(set(packed_ids)): + raise ValueError("exception-to-pack exact coverage failed") + exception_status = "READY" if exceptions else ("READY_WITH_REVIEW" if reviews else "READY_NO_EXCEPTIONS") + exception_payload = { + "legal_structure_exception_pack": { + "schema_version": "stage1_legal_structure_exception_pack.v2", + "status": exception_status, + "run_fingerprint": fingerprint, + "expected_exception_ids": expected_ids, + "exceptions": exceptions, + "deterministic_review_items": reviews, + "pack_count": len(packs), + "packs": packs, + } + } + exception_hash = _artifact_hash(exception_payload) + referenced_evidence = sorted({evidence_id for seed in seeds for evidence_id in seed["evidence_indexes"]}) + seed_status = "READY_WITH_REVIEW" if reviews else "READY" + seed_payload = { + "legal_structure_seed_bundle": { + "schema_version": "stage1_legal_structure_seed_bundle.v2", + "status": seed_status, + "run_fingerprint": fingerprint, + "source_input_pack": { + "path": INPUT_PATH, + "schema_version": "stage1_legal_structure_input_pack.v2", + "sha256": _artifact_hash(input_outer), + }, + "structure_seed_items": seeds, + "issue_cluster_seed_map": issue_map, + "asset_cluster_seed_map": asset_map, + "evidence_index_seed": {key: evidence_authority[key] for key in referenced_evidence if key in evidence_authority}, + "deterministic_review_items": reviews, + "exception_pack_ref": { + "path": EXCEPTION_PATH, + "schema_version": "stage1_legal_structure_exception_pack.v2", + "sha256": exception_hash, + "expected_exception_ids": expected_ids, + }, + } + } + if {bo_id for seed in seeds for bo_id in seed["source_bo_ids"]} != set(records): + raise ValueError("candidate-to-seed BO conservation failed") + _write_doc(EXCEPTION_PATH, exception_payload) + _write_doc(SEED_PATH, seed_payload) + if _artifact_hash(_read_doc(EXCEPTION_PATH)) != exception_hash: + raise ValueError("exception pack post-write hash mismatch") + if _artifact_hash(_read_doc(SEED_PATH)) != _artifact_hash(seed_payload): + raise ValueError("seed bundle post-write hash mismatch") + manifest_status = "READY" if exceptions and not reviews else ("READY_WITH_REVIEW" if reviews else "READY_NO_EXCEPTIONS") + print(json.dumps({ + "les1_manifest": { + "schema_version": "stage1_legal_structure_les1_manifest.v2", + "status": manifest_status, + "seed_path": SEED_PATH, + "exception_pack_path": EXCEPTION_PATH, + "run_fingerprint": fingerprint, + "seed_count": len(seeds), + "exception_count": len(exceptions), + "review_count": len(reviews), + "pack_count": len(packs), + }, + "dynamic_fanout": packs, + }, ensure_ascii=False, separators=(",", ":"))) + except Exception as exc: + print(json.dumps({ + "les1_manifest": { + "schema_version": "stage1_legal_structure_les1_manifest.v2", + "status": "FAILED", + "seed_path": SEED_PATH, + "exception_pack_path": EXCEPTION_PATH, + "run_fingerprint": fingerprint, + "failure_reason": str(exc), + }, + "dynamic_fanout": [], + }, ensure_ascii=False, separators=(",", ":"))) + raise + + if __name__ == "__main__": + main() + + - task_name: Task_LES2_exception_adjudicator_* + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: medium + llm_verbosity: low + max_iterations: 2 + max_concurrency: 2 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + + TASK_NAME: Task_LES2_exception_adjudicator_* + STAGE: Stage 1 legal effect structure and index builder + ROLE: compact exception-only routing adjudicator + + + + pack_id: {{item.pack_id}} + exception_type: {{item.exception_type}} + exception_ids: {{item.exception_ids}} + items: {{item.items}} + output_path: {{item.output_path}} + run_fingerprint: {{item.run_fingerprint}} + + + + - Act as a senior Korean civil-litigation lawyer and AI systems architect, but make only the narrow routing decision authorized below. + - Use only ASSIGNED_PACK. Do not read, request, or reconstruct BO.json, evidence files, signal files, the LES0 input pack, the LES1 seed bundle, or any other pack. + - Do not create a fact, party, date, amount, BO ID, evidence index, exception ID, candidate value, cluster ID, or structure ID. + - Process every assigned item exactly once. Do not omit, merge, duplicate, or reorder exception IDs. + - Choose only a decision listed in that item's allowed_decisions. + - selected_value, selected_values, preserve_candidate_values, and split_plan candidate_values must be subsets of that item's candidate_values. + - If compact context is insufficient, use BLOCK_REVIEW. Never guess. + - Do not write legal_effect_structures.json, the seed bundle, or the exception pack. + - Write exactly one adjudication part to ASSIGNED_PACK.output_path with localdocs write_file and overwrite=true. + - After the write succeeds, return only the compact manifest JSON specified below. No markdown or prose. + + + + For ambiguous_issue_cluster, use only: + - USE_EXISTING_HARD_KEY when one listed hard-key candidate controls. + - PRESERVE_MULTI_ISSUE when all listed issue routes must remain. + - SPLIT_ISSUE_CLUSTER only when the listed candidates require separate structures and the split is supported by compact context. + - BLOCK_REVIEW otherwise. + + For ambiguous_asset_cluster, use only: + - SAME_CLUSTER when a listed exact ref or explicit alias proves identity. + - SEPARATE_CLUSTER when listed source refs differ or conservative separation is required. + - MULTI_ASSET_BUNDLE when all listed assets remain jointly relevant without identity merger. + - BLOCK_REVIEW otherwise. + + For ambiguous_candidate_structure_type, use only: + - SELECT_REPRESENTATIVE_TYPE when one listed canonical type must control finalization. + - PRESERVE_MULTI_ROUTING when all listed canonical routes remain valid. + - SPLIT_STRUCTURE_SEED only when separate structures are required and supported. + - BLOCK_REVIEW otherwise. + + + + Each decisions[] item must contain exactly: + { + "exception_id": "LEX-001", + "exception_type": "ambiguous_issue_cluster|ambiguous_asset_cluster|ambiguous_candidate_structure_type", + "source_bo_ids": [], + "decision": "one allowed enum", + "selected_value": null, + "selected_values": [], + "preserve_candidate_values": [], + "split_plan": [], + "review_required": false, + "reason_code": "HARD_KEY_CONTROLS|SOURCE_REF_IDENTITY|CONSERVATIVE_SEPARATION|PRESERVE_ALL_ROUTING|SPLIT_REQUIRED|INSUFFICIENT_COMPACT_CONTEXT", + "basis_codes_used": [], + "conflict_codes_acknowledged": [] + } + BLOCK_REVIEW and every split decision must set review_required=true. Other completed decisions set it to false. + + + + Write this raw JSON object to ASSIGNED_PACK.output_path: + { + "schema_version": "stage1_legal_structure_adjudication_part.v2", + "status": "READY|READY_WITH_BLOCKS|FAILED", + "pack_id": "the assigned pack_id", + "run_fingerprint": "the assigned run_fingerprint", + "exception_type": "the assigned exception_type", + "input_exception_ids": ["all assigned exception IDs exactly once"], + "decisions": ["one decision per assigned exception"], + "coverage": { + "expected": 0, + "actual": 0, + "missing_exception_ids": [], + "unknown_exception_ids": [], + "coverage_pass": true + } + } + Use READY_WITH_BLOCKS when any decision is BLOCK_REVIEW; otherwise use READY. + + + + After localdocs confirms the write, return exactly: + { + "pack_id": "the assigned pack_id", + "status": "READY|READY_WITH_BLOCKS|FAILED", + "output_path": "the assigned output_path", + "decision_count": 0 + } + + + + - task_name: Task_LES3_final_structure_index_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: |- + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + from collections import defaultdict + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MESSAGE_IDS = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + SEED_PATH = "stage1_tmp/legal_effect_structure_seed_bundle.json" + EXCEPTION_PATH = "stage1_tmp/legal_effect_structure_exception_pack.json" + TARGET_PATH = "legal_effect_structures.json" + CANONICAL_TYPES = [ + "money_claim", + "commercial_successor", + "secured_debt", + "registry_invalidity", + "land_use_gain", + "valuation", + "fraudulent_transfer", + "preserved_claim", + "succession_notice_lien_asset_defense", + "general_legal_effect", + ] + TYPE_ROLES = { + "money_claim": "claim_chain_ref", + "commercial_successor": "claim_chain_ref", + "secured_debt": "linked_structures", + "registry_invalidity": "linked_structures", + "land_use_gain": "legal_calculation_object", + "valuation": "legal_calculation_object", + "fraudulent_transfer": "actio_roles", + "preserved_claim": "claim_chain_ref", + "succession_notice_lien_asset_defense": "linked_structures", + "general_legal_effect": "legal_effect_roles", + } + ALLOWED_DECISIONS = { + "ambiguous_issue_cluster": {"USE_EXISTING_HARD_KEY", "PRESERVE_MULTI_ISSUE", "SPLIT_ISSUE_CLUSTER", "BLOCK_REVIEW"}, + "ambiguous_asset_cluster": {"SAME_CLUSTER", "SEPARATE_CLUSTER", "MULTI_ASSET_BUNDLE", "BLOCK_REVIEW"}, + "ambiguous_candidate_structure_type": {"SELECT_REPRESENTATIVE_TYPE", "PRESERVE_MULTI_ROUTING", "SPLIT_STRUCTURE_SEED", "BLOCK_REVIEW"}, + } + + def _next_id() -> int: + return next(MESSAGE_IDS) + + def _decode_json(value: Any, label: str) -> Any: + if not isinstance(value, str): + return value + text = value.strip() + if not text: + raise ValueError(f"{label}: empty JSON text") + if text.startswith("```"): + text = re.sub(r"^```(?:json)?\s*", "", text) + text = re.sub(r"\s*```$", "", text) + try: + return json.loads(text) + except json.JSONDecodeError: + try: + obj, _ = JSON_DECODER.raw_decode(text) + return obj + except json.JSONDecodeError as exc: + raise ValueError(f"{label}: invalid JSON: {text[:240]}") from exc + + def _parse_rpc(text: str) -> dict[str, Any]: + candidates = [line[5:].strip() for line in text.splitlines() if line.startswith("data:")] + candidates.append(text.strip()) + for candidate in candidates: + if not candidate: + continue + try: + value = _decode_json(candidate, "MCP response") + except ValueError: + continue + if isinstance(value, dict) and ("result" in value or "error" in value): + return value + raise RuntimeError(f"MCP response could not be parsed: {text[:300]}") + + def _initialize() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage1-part3-les3-v2", + "version": "2.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + def _call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_rpc(response.text) + if parsed.get("error"): + raise RuntimeError(f"MCP {name} failed: {parsed['error']}") + return parsed + + def _read_doc(path: str) -> Any: + rpc = _call_tool("read_docs", {"doc_names": [path]}) + content = rpc.get("result", {}).get("content", []) + if not content or not isinstance(content[0], dict): + raise RuntimeError(f"{path}: read_docs returned no content") + envelope = _decode_json(content[0].get("text", ""), f"{path} envelope") + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results or not isinstance(results[0], dict): + raise RuntimeError(f"{path}: read_docs results are empty") + if results[0].get("error"): + raise RuntimeError(f"{path}: {results[0]['error']}") + inner = results[0].get("content", "") + else: + inner = envelope + return _decode_json(inner, path) + + def _artifact_text(payload: dict[str, Any]) -> str: + return json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n" + + def _artifact_hash(payload: dict[str, Any]) -> str: + return "sha256:" + hashlib.sha256(_artifact_text(payload).encode("utf-8")).hexdigest() + + def _write_doc(path: str, payload: dict[str, Any]) -> None: + _call_tool("write_file", {"path": path, "content": _artifact_text(payload), "overwrite": True}) + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + def _strings(value: Any) -> list[str]: + output = [] + for item in _as_list(value): + if item is None: + continue + text = str(item).strip() + if text and text not in output: + output.append(text) + return output + + def _sorted_strings(value: Any) -> list[str]: + return sorted(set(_strings(value))) + + def _priority_types(value: Any) -> list[str]: + supplied = _strings(value) + unknown = sorted(set(supplied) - set(CANONICAL_TYPES)) + if unknown: + raise ValueError(f"non-canonical structure types: {unknown}") + if not supplied: + supplied = ["general_legal_effect"] + return [item for item in CANONICAL_TYPES if item in supplied] + + def _next_numeric_id(prefix: str, rows: dict[str, Any]) -> str: + numbers = [] + for key in rows: + match = re.fullmatch(re.escape(prefix) + r"-(\d+)", str(key)) + if match: + numbers.append(int(match.group(1))) + return f"{prefix}-{max(numbers, default=0) + 1:03d}" + + def _validate_decision(decision: dict[str, Any], expected: dict[str, Any]) -> None: + if decision.get("exception_id") != expected.get("exception_id"): + raise ValueError("decision exception_id mismatch") + exception_type = expected.get("exception_type") + if decision.get("exception_type") != exception_type: + raise ValueError(f"{expected.get('exception_id')}: exception_type mismatch") + choice = decision.get("decision") + if choice not in ALLOWED_DECISIONS.get(exception_type, set()): + raise ValueError(f"{expected.get('exception_id')}: invalid decision {choice}") + candidates = set(_strings(expected.get("candidate_values"))) + for field in ["selected_values", "preserve_candidate_values"]: + extras = set(_strings(decision.get(field))) - candidates + if extras: + raise ValueError(f"{expected.get('exception_id')}: {field} outside candidates: {sorted(extras)}") + selected = decision.get("selected_value") + if selected is not None and str(selected) not in candidates: + raise ValueError(f"{expected.get('exception_id')}: selected_value outside candidates") + for split in _as_list(decision.get("split_plan")): + values = split.get("candidate_values") if isinstance(split, dict) else split + extras = set(_strings(values)) - candidates + if extras: + raise ValueError(f"{expected.get('exception_id')}: split_plan outside candidates: {sorted(extras)}") + if choice == "BLOCK_REVIEW" and decision.get("review_required") is not True: + raise ValueError(f"{expected.get('exception_id')}: BLOCK_REVIEW must require review") + + def _load_inputs() -> tuple[dict[str, Any], dict[str, Any], dict[str, dict[str, Any]]]: + seed_outer = _read_doc(SEED_PATH) + exception_outer = _read_doc(EXCEPTION_PATH) + seed = _as_dict(_as_dict(seed_outer).get("legal_structure_seed_bundle")) + pack = _as_dict(_as_dict(exception_outer).get("legal_structure_exception_pack")) + if seed.get("schema_version") != "stage1_legal_structure_seed_bundle.v2": + raise ValueError("seed bundle schema mismatch") + if seed.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("seed bundle status is not consumable") + if pack.get("schema_version") != "stage1_legal_structure_exception_pack.v2": + raise ValueError("exception pack schema mismatch") + if pack.get("status") not in {"READY", "READY_NO_EXCEPTIONS", "READY_WITH_REVIEW"}: + raise ValueError("exception pack status is not consumable") + fingerprint = seed.get("run_fingerprint") + if not fingerprint or fingerprint != pack.get("run_fingerprint"): + raise ValueError("seed/exception fingerprint mismatch") + pack_ref = _as_dict(seed.get("exception_pack_ref")) + if pack_ref.get("path") != EXCEPTION_PATH or pack_ref.get("schema_version") != "stage1_legal_structure_exception_pack.v2": + raise ValueError("seed exception_pack_ref path/schema mismatch") + if pack_ref.get("sha256") != _artifact_hash(exception_outer): + raise ValueError("seed exception_pack_ref hash mismatch") + expected_rows = {} + for item in _as_list(pack.get("exceptions")): + if not isinstance(item, dict) or not item.get("exception_id"): + raise ValueError("exception item missing exception_id") + exception_id = str(item["exception_id"]) + if exception_id in expected_rows: + raise ValueError(f"duplicate exception_id: {exception_id}") + expected_rows[exception_id] = item + expected_ids = _strings(pack.get("expected_exception_ids")) + if set(expected_ids) != set(expected_rows) or len(expected_ids) != len(expected_rows): + raise ValueError("expected exception ID set mismatch") + packs = [row for row in _as_list(pack.get("packs")) if isinstance(row, dict)] + if int(pack.get("pack_count", -1)) != len(packs): + raise ValueError("exception pack count mismatch") + packed_ids = [exception_id for row in packs for exception_id in _strings(row.get("exception_ids"))] + if set(packed_ids) != set(expected_ids) or len(packed_ids) != len(set(packed_ids)): + raise ValueError("pack-to-exception exact coverage failed") + decisions = {} + for descriptor in packs: + output_path = descriptor.get("output_path") + if not isinstance(output_path, str) or not output_path: + raise ValueError("pack descriptor missing output_path") + part = _as_dict(_read_doc(output_path)) + if part.get("schema_version") != "stage1_legal_structure_adjudication_part.v2": + raise ValueError(f"{output_path}: adjudication schema mismatch") + if part.get("status") not in {"READY", "READY_WITH_BLOCKS"}: + raise ValueError(f"{output_path}: adjudication status is not consumable") + if part.get("pack_id") != descriptor.get("pack_id") or part.get("run_fingerprint") != fingerprint: + raise ValueError(f"{output_path}: pack ID/fingerprint mismatch") + part_expected = _strings(part.get("input_exception_ids")) + descriptor_expected = _strings(descriptor.get("exception_ids")) + if set(part_expected) != set(descriptor_expected) or len(part_expected) != len(descriptor_expected): + raise ValueError(f"{output_path}: input exception coverage mismatch") + coverage = _as_dict(part.get("coverage")) + if coverage.get("coverage_pass") is not True: + raise ValueError(f"{output_path}: reported coverage failure") + for decision in _as_list(part.get("decisions")): + if not isinstance(decision, dict) or not decision.get("exception_id"): + raise ValueError(f"{output_path}: decision missing exception_id") + exception_id = str(decision["exception_id"]) + if exception_id not in expected_rows: + raise ValueError(f"{output_path}: unknown exception ID {exception_id}") + if exception_id in decisions: + raise ValueError(f"duplicate decision for {exception_id}") + _validate_decision(decision, expected_rows[exception_id]) + decisions[exception_id] = decision + if set(decisions) != set(expected_ids): + missing = sorted(set(expected_ids) - set(decisions)) + unknown = sorted(set(decisions) - set(expected_ids)) + raise ValueError(f"decision exact coverage failed; missing={missing}; unknown={unknown}") + return seed, pack, decisions + + def _decision_groups(seed: dict[str, Any], decisions: dict[str, dict[str, Any]]) -> dict[str, list[dict[str, Any]]]: + output = defaultdict(list) + for exception_id in _strings(seed.get("pending_exception_ids")): + if exception_id not in decisions: + raise ValueError(f"seed references missing decision: {exception_id}") + output[decisions[exception_id]["exception_type"]].append(decisions[exception_id]) + return output + + def _split_values(decision: dict[str, Any]) -> list[list[str]]: + groups = [] + for row in _as_list(decision.get("split_plan")): + values = row.get("candidate_values") if isinstance(row, dict) else row + values = _strings(values) + if values: + groups.append(values) + if not groups: + values = _strings(decision.get("selected_values")) + groups = [[value] for value in values] + return groups + + def _apply_issue_decisions(ids: list[str], rows: list[dict[str, Any]], issue_map: dict[str, Any], bo_ids: list[str]) -> tuple[list[str], bool]: + output = list(ids) + split_review = False + for decision in rows: + choice = decision.get("decision") + if choice == "USE_EXISTING_HARD_KEY": + selected = _strings(decision.get("selected_value")) + _strings(decision.get("selected_values")) + selected = [value for value in selected if value in issue_map] + if selected: + output = selected + elif choice == "SPLIT_ISSUE_CLUSTER": + split_review = True + output = [] + for group in _split_values(decision): + existing = [value for value in group if value in issue_map] + if existing: + output.extend(existing) + else: + issue_id = _next_numeric_id("IC", issue_map) + issue_map[issue_id] = { + "issue_cluster_id": issue_id, + "cluster_key": "adjudicated:" + "|".join(group), + "source_bo_ids": bo_ids, + "evidence_indexes": [], + "candidate_structure_types": [], + "basis_codes": ["LES2_SPLIT_ISSUE_CLUSTER"], + } + output.append(issue_id) + return _sorted_strings(output), split_review + + def _apply_asset_decisions(ids: list[str], rows: list[dict[str, Any]], asset_map: dict[str, Any], bo_ids: list[str]) -> tuple[list[str], list[str], bool]: + output = list(ids) + bundle_refs = [] + split_review = False + for decision in rows: + choice = decision.get("decision") + values = _strings(decision.get("selected_value")) + _strings(decision.get("selected_values")) + existing = [value for value in values if value in asset_map] + if choice == "SAME_CLUSTER" and existing: + output = [existing[0]] + elif choice == "SEPARATE_CLUSTER" and existing: + output = existing + elif choice == "MULTI_ASSET_BUNDLE": + if existing: + output = existing + bundle_refs.append("BUNDLE-ADJ-" + str(decision.get("exception_id"))) + if choice in {"SEPARATE_CLUSTER", "MULTI_ASSET_BUNDLE"}: + split_review = bool(decision.get("review_required")) + return _sorted_strings(output), _sorted_strings(bundle_refs), split_review + + def _type_plans(types: list[str], representative: str, rows: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], bool]: + plans = [{"types": types, "representative": representative, "split_label": None}] + split_review = False + for decision in rows: + choice = decision.get("decision") + if choice == "SELECT_REPRESENTATIVE_TYPE": + selected = str(decision.get("selected_value") or representative) + if selected not in types: + raise ValueError(f"{decision.get('exception_id')}: selected type not in seed types") + plans = [{"types": types, "representative": selected, "split_label": None}] + elif choice == "PRESERVE_MULTI_ROUTING": + plans = [{"types": types, "representative": representative, "split_label": None}] + elif choice == "SPLIT_STRUCTURE_SEED": + split_review = True + groups = _split_values(decision) + plans = [] + for ordinal, group in enumerate(groups, 1): + group_types = _priority_types(group) + plans.append({"types": group_types, "representative": group_types[0], "split_label": f"split-{ordinal}"}) + return plans, split_review + + def _build_structures(seed: dict[str, Any], decisions: dict[str, dict[str, Any]]) -> tuple[list[dict[str, Any]], dict[str, Any], dict[str, Any]]: + issue_map = {key: dict(value) for key, value in _as_dict(seed.get("issue_cluster_seed_map")).items() if isinstance(value, dict)} + asset_map = {key: dict(value) for key, value in _as_dict(seed.get("asset_cluster_seed_map")).items() if isinstance(value, dict)} + structures = [] + for seed_item in _as_list(seed.get("structure_seed_items")): + if not isinstance(seed_item, dict): + raise ValueError("structure seed item must be object") + bo_ids = _sorted_strings(seed_item.get("source_bo_ids")) + if not bo_ids: + raise ValueError("structure seed item missing source BO") + groups = _decision_groups(seed_item, decisions) + issue_ids, issue_split = _apply_issue_decisions( + _strings(seed_item.get("issue_cluster_ids")), groups.get("ambiguous_issue_cluster", []), issue_map, bo_ids + ) + asset_ids, adjudicated_bundles, asset_split = _apply_asset_decisions( + _strings(seed_item.get("asset_cluster_ids")), groups.get("ambiguous_asset_cluster", []), asset_map, bo_ids + ) + types = _priority_types(seed_item.get("candidate_structure_types")) + representative = str(seed_item.get("candidate_structure_type") or types[0]) + if representative not in types: + representative = types[0] + plans, type_split = _type_plans(types, representative, groups.get("ambiguous_candidate_structure_type", [])) + pending_ids = _strings(seed_item.get("pending_exception_ids")) + blocked_ids = [exception_id for exception_id in pending_ids if decisions[exception_id].get("decision") == "BLOCK_REVIEW"] + resolved_ids = [exception_id for exception_id in pending_ids if exception_id not in blocked_ids] + deterministic_review_ids = _sorted_strings(seed_item.get("deterministic_review_ids")) + review_required = bool(deterministic_review_ids or blocked_ids or issue_split or asset_split or type_split) + bundle_refs = _sorted_strings(_strings(seed_item.get("multi_asset_bundle_refs")) + adjudicated_bundles) + for plan in plans: + structure_id = f"LES-{len(structures) + 1:03d}" + plan_types = _priority_types(plan["types"]) + plan_representative = plan["representative"] if plan["representative"] in plan_types else plan_types[0] + suffix = f" ({plan['split_label']})" if plan.get("split_label") else "" + sources = ["BO.json", "legal_effect_signals.json"] + if seed_item.get("linked_claim_group_ids") or seed_item.get("linked_liability_group_ids"): + sources.append("case_liability_signals.json") + if seed_item.get("linked_actio_signal_ids"): + sources.append("actio_case_signals.json") + structures.append({ + "structure_id": structure_id, + "candidate_structure_type": plan_representative, + "candidate_structure_types": plan_types, + "structure_label": f"{plan_representative} structure for {', '.join(bo_ids)}{suffix}", + "source_bo_ids": bo_ids, + "issue_cluster_ids": issue_ids, + "asset_cluster_ids": asset_ids, + "evidence_indexes": _sorted_strings(seed_item.get("evidence_indexes")), + "linked_claim_group_ids": _sorted_strings(seed_item.get("linked_claim_group_ids")), + "linked_liability_group_ids": _sorted_strings(seed_item.get("linked_liability_group_ids")), + "linked_actio_signal_ids": _sorted_strings(seed_item.get("linked_actio_signal_ids")), + "legal_effect_roles": _sorted_strings([TYPE_ROLES[item] for item in plan_types]), + "basis": { + "primary_fields": _sorted_strings(seed_item.get("basis_codes")), + "source_signal_files": sorted(set(sources)), + "source_evidence_indexes": _sorted_strings(seed_item.get("evidence_indexes")), + }, + "review_required": review_required, + "routing_audit": { + "source_seed_id": seed_item.get("seed_id"), + "candidate_structure_types": plan_types, + "multi_asset_bundle_refs": bundle_refs, + "resolved_exception_ids": _sorted_strings(resolved_ids), + "blocked_exception_ids": _sorted_strings(blocked_ids), + "deterministic_review_ids": deterministic_review_ids, + }, + }) + return structures, issue_map, asset_map + + def _merge(row: dict[str, Any], field: str, values: Any) -> None: + row[field] = _sorted_strings(_strings(row.get(field)) + _strings(values)) + + def _build_index(structures: list[dict[str, Any]], issue_map: dict[str, Any], asset_map: dict[str, Any], evidence_seed: dict[str, Any]) -> dict[str, Any]: + by_bo = {} + by_issue = {} + by_asset = {} + by_evidence = {} + by_type = {} + for structure in structures: + sid = structure["structure_id"] + types = structure["candidate_structure_types"] + routing = _as_dict(structure.get("routing_audit")) + basis = _as_dict(structure.get("basis")) + for bo_id in structure["source_bo_ids"]: + row = by_bo.setdefault(bo_id, { + "bo_id": bo_id, "structure_ids": [], "structure_labels": [], "issue_cluster_ids": [], + "asset_cluster_ids": [], "evidence_indexes": [], "candidate_structure_types": [], + "claim_group_ids": [], "liability_group_ids": [], "related_actio_signal_ids": [], + "legal_effect_roles": [], "basis_fields": [], "multi_asset_bundle_refs": [], "review_required": False, + }) + _merge(row, "structure_ids", [sid]) + _merge(row, "structure_labels", [structure["structure_label"]]) + _merge(row, "issue_cluster_ids", structure["issue_cluster_ids"]) + _merge(row, "asset_cluster_ids", structure["asset_cluster_ids"]) + _merge(row, "evidence_indexes", structure["evidence_indexes"]) + row["candidate_structure_types"] = [item for item in CANONICAL_TYPES if item in row["candidate_structure_types"] + types] + _merge(row, "claim_group_ids", structure["linked_claim_group_ids"]) + _merge(row, "liability_group_ids", structure["linked_liability_group_ids"]) + _merge(row, "related_actio_signal_ids", structure["linked_actio_signal_ids"]) + _merge(row, "legal_effect_roles", structure["legal_effect_roles"]) + _merge(row, "basis_fields", basis.get("primary_fields")) + _merge(row, "multi_asset_bundle_refs", routing.get("multi_asset_bundle_refs")) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + for issue_id in structure["issue_cluster_ids"]: + source = _as_dict(issue_map.get(issue_id)) + row = by_issue.setdefault(issue_id, { + "issue_cluster_id": issue_id, "cluster_key": source.get("cluster_key"), "source_bo_ids": [], + "evidence_indexes": [], "candidate_structure_types": [], "structure_ids": [], "review_required": False, + }) + _merge(row, "source_bo_ids", structure["source_bo_ids"]) + _merge(row, "evidence_indexes", structure["evidence_indexes"]) + row["candidate_structure_types"] = [item for item in CANONICAL_TYPES if item in row["candidate_structure_types"] + types] + _merge(row, "structure_ids", [sid]) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + for asset_id in structure["asset_cluster_ids"]: + source = _as_dict(asset_map.get(asset_id)) + row = by_asset.setdefault(asset_id, { + "asset_cluster_id": asset_id, "cluster_key": source.get("cluster_key"), + "asset_kind": source.get("asset_kind"), "canonical_label_candidate": source.get("canonical_label_candidate"), + "asset_ref_keys": _sorted_strings(source.get("asset_ref_keys")), "source_bo_ids": [], + "evidence_indexes": [], "issue_cluster_ids": [], "candidate_structure_types": [], + "structure_ids": [], "review_required": False, + }) + _merge(row, "source_bo_ids", structure["source_bo_ids"]) + _merge(row, "evidence_indexes", structure["evidence_indexes"]) + _merge(row, "issue_cluster_ids", structure["issue_cluster_ids"]) + row["candidate_structure_types"] = [item for item in CANONICAL_TYPES if item in row["candidate_structure_types"] + types] + _merge(row, "structure_ids", [sid]) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + for evidence_index in structure["evidence_indexes"]: + source = _as_dict(evidence_seed.get(evidence_index)) + row = by_evidence.setdefault(evidence_index, { + "evidence_index": evidence_index, "source_title": source.get("source_title"), + "priority_class": source.get("priority_class"), "authentication_status": source.get("authentication_status"), + "source_bo_ids": [], "structure_ids": [], "issue_cluster_ids": [], "asset_cluster_ids": [], + "candidate_structure_types": [], "related_event_candidate_ids": [], "related_signal_refs": [], + "support_role": "primary", "gap_flags": [], + }) + _merge(row, "source_bo_ids", structure["source_bo_ids"]) + _merge(row, "structure_ids", [sid]) + _merge(row, "issue_cluster_ids", structure["issue_cluster_ids"]) + _merge(row, "asset_cluster_ids", structure["asset_cluster_ids"]) + row["candidate_structure_types"] = [item for item in CANONICAL_TYPES if item in row["candidate_structure_types"] + types] + _merge(row, "related_signal_refs", structure["linked_claim_group_ids"] + structure["linked_liability_group_ids"] + structure["linked_actio_signal_ids"]) + for candidate_type in types: + row = by_type.setdefault(candidate_type, { + "candidate_structure_type": candidate_type, "structure_ids": [], "source_bo_ids": [], + "issue_cluster_ids": [], "asset_cluster_ids": [], "evidence_indexes": [], + "claim_group_ids": [], "liability_group_ids": [], "related_actio_signal_ids": [], + "downstream_fact_roles": [], "basis_fields": [], "review_required": False, + }) + _merge(row, "structure_ids", [sid]) + _merge(row, "source_bo_ids", structure["source_bo_ids"]) + _merge(row, "issue_cluster_ids", structure["issue_cluster_ids"]) + _merge(row, "asset_cluster_ids", structure["asset_cluster_ids"]) + _merge(row, "evidence_indexes", structure["evidence_indexes"]) + _merge(row, "claim_group_ids", structure["linked_claim_group_ids"]) + _merge(row, "liability_group_ids", structure["linked_liability_group_ids"]) + _merge(row, "related_actio_signal_ids", structure["linked_actio_signal_ids"]) + _merge(row, "downstream_fact_roles", structure["legal_effect_roles"]) + _merge(row, "basis_fields", basis.get("primary_fields")) + row["review_required"] = bool(row["review_required"] or structure["review_required"]) + return { + "by_bo_id": by_bo, + "by_issue_cluster_id": by_issue, + "by_asset_cluster_id": by_asset, + "by_evidence_index": by_evidence, + "by_candidate_structure_type": by_type, + } + + def _quality_gate(structures: list[dict[str, Any]], index: dict[str, Any], seed: dict[str, Any], pack: dict[str, Any], decisions: dict[str, dict[str, Any]]) -> dict[str, Any]: + seed_items = [item for item in _as_list(seed.get("structure_seed_items")) if isinstance(item, dict)] + expected_bo = sorted({bo for item in seed_items for bo in _strings(item.get("source_bo_ids"))}) + covered_bo = sorted(index["by_bo_id"]) + expected_evidence = sorted({evidence for item in seed_items for evidence in _strings(item.get("evidence_indexes"))}) + covered_evidence = sorted(index["by_evidence_index"]) + expected_exception_ids = _strings(pack.get("expected_exception_ids")) + blocked_ids = sorted(exception_id for exception_id, decision in decisions.items() if decision.get("decision") == "BLOCK_REVIEW") + resolved_ids = sorted(set(expected_exception_ids) - set(blocked_ids)) + reviews = [] + for item in _as_list(seed.get("deterministic_review_items")): + if isinstance(item, dict): + reviews.append({ + "review_type": "deterministic_review", + "review_id": item.get("review_id"), + "review_code": item.get("review_code"), + "source_bo_ids": _sorted_strings(item.get("source_bo_ids")), + "evidence_indexes": _sorted_strings(item.get("evidence_indexes")), + "downstream_owner": item.get("downstream_owner"), + }) + for exception_id in blocked_ids: + decision = decisions[exception_id] + reviews.append({ + "review_type": "blocked_exception", + "exception_id": exception_id, + "exception_type": decision.get("exception_type"), + "source_bo_ids": _sorted_strings(decision.get("source_bo_ids")), + "reason_code": decision.get("reason_code"), + }) + split_structure_ids = [ + structure["structure_id"] for structure in structures + if structure.get("review_required") and ( + _as_dict(structure.get("routing_audit")).get("resolved_exception_ids") + and "split" in structure.get("structure_label", "") + ) + ] + for structure_id in split_structure_ids: + reviews.append({"review_type": "review_sensitive_split", "structure_id": structure_id}) + return { + "bo_coverage": { + "source_bo_count": len(expected_bo), "covered_bo_count": len(covered_bo), + "missing_bo_ids": sorted(set(expected_bo) - set(covered_bo)), + "unknown_bo_ids": sorted(set(covered_bo) - set(expected_bo)), + "coverage_pass": expected_bo == covered_bo, + }, + "evidence_coverage": { + "source_evidence_count": len(expected_evidence), "covered_evidence_count": len(covered_evidence), + "missing_evidence_indexes": sorted(set(expected_evidence) - set(covered_evidence)), + "coverage_pass": set(expected_evidence).issubset(set(covered_evidence)), + }, + "exception_coverage": { + "expected_exception_count": len(expected_exception_ids), + "resolved_exception_count": len(resolved_ids), + "blocked_exception_count": len(blocked_ids), + "missing_exception_ids": sorted(set(expected_exception_ids) - set(decisions)), + "coverage_pass": set(expected_exception_ids) == set(decisions), + }, + "run_fingerprint_gate": {"match": seed.get("run_fingerprint") == pack.get("run_fingerprint")}, + "review_summary": { + "structure_review_count": sum(1 for structure in structures if structure.get("review_required")), + "deterministic_review_count": len(_as_list(seed.get("deterministic_review_items"))), + "blocked_exception_count": len(blocked_ids), + }, + "review_queue": reviews, + } + + def _validate_final(payload: dict[str, Any]) -> None: + if payload.get("schema_version") != "stage1_legal_effect_structures.v1": + raise ValueError("final schema mismatch") + if payload.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("final status invalid") + structures = _as_list(payload.get("legal_effect_structures")) + if not structures: + raise ValueError("final structures must be non-empty") + index = _as_dict(payload.get("structure_index")) + required_indexes = ["by_bo_id", "by_issue_cluster_id", "by_asset_cluster_id", "by_evidence_index", "by_candidate_structure_type"] + for key in required_indexes: + if not isinstance(index.get(key), dict): + raise ValueError(f"structure_index.{key} must be object") + structure_ids = [str(_as_dict(row).get("structure_id")) for row in structures] + if len(structure_ids) != len(set(structure_ids)): + raise ValueError("structure IDs must be unique") + for structure in structures: + if not isinstance(structure, dict): + raise ValueError("structure must be object") + types = _priority_types(structure.get("candidate_structure_types")) + if structure.get("candidate_structure_type") not in types: + raise ValueError(f"{structure.get('structure_id')}: representative type missing from type array") + audit_types = _priority_types(_as_dict(structure.get("routing_audit")).get("candidate_structure_types")) + if types != audit_types: + raise ValueError(f"{structure.get('structure_id')}: top-level/audit type mismatch") + sid = structure.get("structure_id") + for bo_id in _strings(structure.get("source_bo_ids")): + if sid not in _strings(_as_dict(index["by_bo_id"].get(bo_id)).get("structure_ids")): + raise ValueError(f"{sid}: missing BO index link {bo_id}") + for issue_id in _strings(structure.get("issue_cluster_ids")): + if issue_id not in index["by_issue_cluster_id"]: + raise ValueError(f"{sid}: missing issue index {issue_id}") + for asset_id in _strings(structure.get("asset_cluster_ids")): + if asset_id not in index["by_asset_cluster_id"]: + raise ValueError(f"{sid}: missing asset index {asset_id}") + for evidence_index in _strings(structure.get("evidence_indexes")): + if evidence_index not in index["by_evidence_index"]: + raise ValueError(f"{sid}: missing evidence index {evidence_index}") + for candidate_type in types: + if sid not in _strings(_as_dict(index["by_candidate_structure_type"].get(candidate_type)).get("structure_ids")): + raise ValueError(f"{sid}: missing type index {candidate_type}") + gate = _as_dict(payload.get("quality_gate")) + for key in ["bo_coverage", "evidence_coverage", "exception_coverage"]: + if _as_dict(gate.get(key)).get("coverage_pass") is not True: + raise ValueError(f"quality gate failed: {key}") + if _as_dict(gate.get("run_fingerprint_gate")).get("match") is not True: + raise ValueError("quality gate failed: run_fingerprint") + + def main() -> None: + try: + _initialize() + seed, pack, decisions = _load_inputs() + structures, issue_map, asset_map = _build_structures(seed, decisions) + index = _build_index(structures, issue_map, asset_map, _as_dict(seed.get("evidence_index_seed"))) + gate = _quality_gate(structures, index, seed, pack, decisions) + status = "READY_WITH_REVIEW" if gate["review_queue"] else "READY" + payload = { + "schema_version": "stage1_legal_effect_structures.v1", + "status": status, + "legal_effect_structures": structures, + "structure_index": index, + "quality_gate": gate, + } + _validate_final(payload) + _write_doc(TARGET_PATH, payload) + reread = _read_doc(TARGET_PATH) + if _artifact_hash(reread) != _artifact_hash(payload): + raise ValueError("final post-write reread mismatch") + _validate_final(reread) + print(json.dumps({ + "legal_effect_structures_writer": { + "schema_version": "stage1_legal_effect_structures_writer.v2", + "status": "READY", + "written_file": TARGET_PATH, + "payload_status": status, + "structure_count": len(structures), + "exception_count": len(_strings(pack.get("expected_exception_ids"))), + "review_queue_count": len(gate["review_queue"]), + "run_fingerprint": seed.get("run_fingerprint"), + } + }, ensure_ascii=False, separators=(",", ":"))) + except Exception as exc: + print(json.dumps({ + "legal_effect_structures_writer": { + "schema_version": "stage1_legal_effect_structures_writer.v2", + "status": "FAILED", + "target_file": TARGET_PATH, + "failure_reason": str(exc), + } + }, ensure_ascii=False, separators=(",", ":"))) + raise + + if __name__ == "__main__": + main() diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_4_Claude_v3.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_4_Claude_v3.yml new file mode 100644 index 00000000..8e5d71a1 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_4_Claude_v3.yml @@ -0,0 +1,1963 @@ +--- +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_4 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Fact Ledger Generation + version: v3.0-claude + Stages: + - name: stage1_fact_ledger_generation + description: BO, Legal Effect 사용해서 Fact_Ledger_base.json 생성 + tools: + mcpServers: + localdocs: + type: streamable-http + url: http://mcp-localdocs:8012/mcp + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + task_procedure: + IN: + nexts: + - Task_F0_fact_ledger_reducer + wait_until: [] + Task_F0_fact_ledger_reducer: + nexts: + - Task_F1_fact_exception_adjudicator + wait_until: + - IN + Task_F1_fact_exception_adjudicator: + nexts: + - Task_F2_final_fact_ledger_gate_and_writer + wait_until: + - Task_F0_fact_ledger_reducer + Task_F2_final_fact_ledger_gate_and_writer: + nexts: + - OUT + wait_until: + - Task_F1_fact_exception_adjudicator + OUT: + nexts: [] + wait_until: + - Task_F2_final_fact_ledger_gate_and_writer + + tasks: + - task_name: Task_F0_fact_ledger_reducer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import re + import sys + from copy import deepcopy + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + # X-4: evidence_all.json / evidence_event_candidates.json / client_goal.json은 기여 0 실측으로 제거. + INPUT_FILES = { + "bo": "BO.json", + "legal_effect_structures": "legal_effect_structures.json", + "actio_case_signals": "actio_case_signals.json", + "case_liability_signals": "case_liability_signals.json", + "legal_effect_signals": "legal_effect_signals.json", + "evidence_indexed": "evidence_indexed.json", + } + REMOVED_INPUTS = ["evidence_all.json", "evidence_event_candidates.json", "client_goal.json"] + CANDIDATE_OUT_PATH = "stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json" + PACK_OUT_PATH = "stage1_tmp/fact_ledger/fact_exception_pack.json" + MAX_TEXT = 900 + MAX_SIGNAL_ITEMS_PER_BO = 12 + # X-3 방어 상한 (자동 차단 금지 — 초과분은 exception_id 유지한 채 deterministic review로 라우팅) + PACK_MAX_ITEMS = 30 + PACK_MAX_BYTES = 40000 + PACK_ITEM_CHAR_LIMIT = 2000 + FINAL_FIELDS = [ + "fact_id", "source_bo_id", "type", "date", "parties", "object_spec", "amount", "action", + "evidence_refs", "credibility", "legal_centrality", "proof_strength", "legal_effect_roles", + "claim_chain_ref", "linked_structures", "must_not_drop_in_claim_types", "must_consider", + "state_context", "money_claim_effect", "commercial_successor_effect", "actio_roles", + "linked_actio_structures", "downstream_module_candidates", "skeleton_validation_codes", + "derived_fact", "derivation_basis", "legal_calculation_object", + ] + STRUCTURE_TYPE_TO_MODULE = { + "money_claim": "money_claim", + "commercial_successor": "commercial_successor", + "secured_debt": "secured_property", + "registry_invalidity": "registry_invalidity", + "land_use_gain": "land_use_gain", + "valuation": "valuation", + "fraudulent_transfer": "actio", + "preserved_claim": "actio", + "succession_notice_lien_asset_defense": "succession_notice_lien_asset_defense", + "general_legal_effect": "legal_effect", + } + STRUCTURE_TYPE_TO_ROLE = { + "money_claim": "claim_chain_ref", + "commercial_successor": "claim_chain_ref", + "secured_debt": "linked_structures", + "registry_invalidity": "linked_structures", + "land_use_gain": "legal_calculation_object", + "valuation": "legal_calculation_object", + "fraudulent_transfer": "actio_roles", + "preserved_claim": "claim_chain_ref", + "succession_notice_lien_asset_defense": "linked_structures", + "general_legal_effect": "legal_effect_roles", + } + PACK_EXCEPTION_TYPES = [ + "legal_calculation_required", + "unresolved_asset_cluster_projection", + "unresolved_structure_role_projection", + ] + REVIEW_CODE_BY_EXCEPTION_TYPE = { + "legal_calculation_required": "CALCULATION_BASIS_INCOMPLETE_REVIEW", + "unresolved_asset_cluster_projection": "ASSET_CLUSTER_PROJECTION_GAP_REVIEW", + "unresolved_structure_role_projection": "STRUCTURE_ROLE_PROJECTION_GAP_REVIEW", + } + DOMAIN_PAYLOAD_KEEP_KEYS = [ + "money_claim_details", "possession_state", "lien_state", + "notice_lifecycle", "asset_alias", "fractional_effect_events", + ] + POSSESSION_INCOMPATIBLE_CAUSE_PAIRS = [ + ("유치권행사중", "무권원주장"), + ] + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-fact-ledger-f0-reducer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + + def _lenient_json(raw: str, label: str) -> Any: + """SKILL §6.4/§6.7: strict 실패 시 raw_decode fallback (envelope 계층 포함 — X-9).""" + text = (raw or "").strip() + if not text: + raise RuntimeError(f"Empty content for {label}") + try: + return json.loads(text) + except json.JSONDecodeError: + start = text.find("{") + alt = text.find("[") + if alt >= 0 and (start < 0 or alt < start): + start = alt + if start < 0: + raise RuntimeError(f"Content of {label} is not valid JSON: {text[:300]}") + try: + obj, _ = JSON_DECODER.raw_decode(text[start:]) + return obj + except json.JSONDecodeError as exc: + raise RuntimeError(f"Content of {label} is not valid JSON: {text[:300]}") from exc + + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result["result"].get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + text = content[0].get("text", "") + envelope = _lenient_json(text, doc_name) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + first = results[0] + if isinstance(first, dict) and first.get("error"): + raise RuntimeError(f"{doc_name}: {first['error']}") + inner = first.get("content", "") if isinstance(first, dict) else "" + if inner in (None, ""): + raise RuntimeError(f"Empty content for {doc_name}") + if isinstance(inner, str): + return _lenient_json(inner, doc_name) + return inner + return envelope + + + def _read_doc(doc_name: str) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + return _extract_json_from_read_docs_result(result, doc_name) + + + def _write_text_doc(doc_name: str, content: str) -> None: + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=_next_msg_id()) + + + def _read_doc_raw_text(doc_name: str) -> str: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + content = result["result"].get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + envelope = _lenient_json(content[0].get("text", ""), doc_name) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if results and isinstance(results[0], dict): + raw = results[0].get("content") + if isinstance(raw, str): + return raw + raise RuntimeError(f"raw text unavailable for {doc_name}") + + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _text(value: Any, limit: int = MAX_TEXT) -> str: + if value is None: + return "" + if isinstance(value, str): + s = value + else: + s = json.dumps(value, ensure_ascii=False, sort_keys=True) + s = re.sub(r"\s+", " ", s).strip() + return s[:limit] + + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + if value is None: + return out + if isinstance(value, (str, int, float)): + s = str(value).strip() + return [s] if s else [] + if isinstance(value, list): + for item in value: + out.extend(_strings(item)) + return _dedupe(out) + + + def _dedupe(values: list[str]) -> list[str]: + seen: set[str] = set() + out: list[str] = [] + for value in values: + if value and value not in seen: + seen.add(value) + out.append(value) + return out + + + def _first_key(d: dict[str, Any], keys: list[str]) -> Any: + for key in keys: + if key in d and d.get(key) not in (None, "", [], {}): + return d.get(key) + return None + + + def _first_value(*values: Any) -> Any: + for value in values: + if value not in (None, "", [], {}): + return value + return None + + + def _items_from_doc(doc: Any, candidate_keys: list[str]) -> list[dict[str, Any]]: + if isinstance(doc, list): + return [x for x in doc if isinstance(x, dict)] + if isinstance(doc, dict): + for key in candidate_keys: + if isinstance(doc.get(key), list): + return [x for x in doc[key] if isinstance(x, dict)] + if any(k in doc for k in ("BO_ID", "bo_id", "id", "fact_id", "evidence_index", "structure_id")): + return [doc] + return [] + + + def _bo_id(bo: dict[str, Any]) -> str: + return str(_first_key(bo, ["BO_ID", "bo_id", "id", "canonical_bo_id", "bh_id"]) or "").strip() + + + def _bo_items(bo_doc: Any) -> list[dict[str, Any]]: + items = _items_from_doc(bo_doc, ["BO", "bo", "items", "bo_items", "canonical_bo_items", "body"]) + valid = [item for item in items if _bo_id(item)] + if not valid: + raise ValueError("BO.json must contain BO items with BO_ID/id") + return valid + + + def _evidence_refs_from_bo(bo: dict[str, Any]) -> list[str]: + refs: list[str] = [] + for key in ("source_evidence_indexes", "evidence_refs", "Evidence", "EvidenceIndexes", "related_evidence_indexes"): + refs.extend(_strings(bo.get(key))) + core = _as_dict(bo.get("core_field_base")) + refs.extend(_strings(core.get("source_evidence_indexes"))) + return _dedupe(refs) + + + def _compact_bo(bo: dict[str, Any]) -> dict[str, Any]: + core = _as_dict(bo.get("core_field_base")) + return { + "bo_id": _bo_id(bo), + "bo_type": _first_key(bo, ["BOType", "bo_type", "type"]), + "action": _first_key(bo, ["Action", "action"]), + "action_type": _first_key(bo, ["ActionType", "action_type"]), + "juristic_act": _first_key(bo, ["JuristicAct", "juristic_act"]), + "reason": _first_key(bo, ["Reason", "reason"]), + "prior_act": _first_key(bo, ["PriorAct", "prior_act"]), + "legal_keywords": _strings(bo.get("Legal_Keywords")), + "amount": _first_key(bo, ["amount", "proposed_amount", "Amount"]), + "date": _first_key(bo, ["date", "proposed_date", "Date"]), + "core_field_base": { + "BehaviorTime": _first_key(core, ["BehaviorTime", "behavior_time", "date", "Date"]), + "Performer": _first_key(core, ["Performer", "Actor", "actor", "performer"]), + "Subject": _first_key(core, ["Subject", "Counterparty", "subject", "counterparty"]), + "Object": _first_key(core, ["Object", "Property", "Asset", "object", "asset"]), + "Amount": _first_key(core, ["Amount", "amount", "Value", "value"]), + "Action": _first_key(core, ["Action", "action"]), + }, + "source_evidence_indexes": _evidence_refs_from_bo(bo), + "domain_payload_compact": { + k: v + for k, v in _as_dict(_as_dict(bo.get("extensions")).get("domain_payload")).items() + if k in DOMAIN_PAYLOAD_KEEP_KEYS and v not in (None, {}, []) + }, + } + + + def _validate_legal_effect_structures(doc: Any, bo_ids: set[str]) -> tuple[list[dict[str, Any]], dict[str, Any], list[str]]: + warnings: list[str] = [] + if not isinstance(doc, dict): + raise ValueError("legal_effect_structures.json root must be object") + if doc.get("schema_version") != "stage1_legal_effect_structures.v1": + raise ValueError("legal_effect_structures.json schema_version mismatch") + if doc.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("legal_effect_structures.json status must be READY or READY_WITH_REVIEW") + structures = _as_list(doc.get("legal_effect_structures")) + index = _as_dict(doc.get("structure_index")) + if not isinstance(index.get("by_bo_id"), dict): + raise ValueError("legal_effect_structures.structure_index.by_bo_id must be object") + for key in ["by_bo_id", "by_issue_cluster_id", "by_asset_cluster_id", "by_evidence_index", "by_candidate_structure_type"]: + if not isinstance(index.get(key), dict): + raise ValueError(f"legal_effect_structures.structure_index.{key} must be object") + structure_ids = {str(s.get("structure_id")) for s in structures if isinstance(s, dict) and s.get("structure_id")} + for structure in structures: + if not isinstance(structure, dict): + continue + sid = str(structure.get("structure_id") or "") + for bo_id in _strings(structure.get("source_bo_ids")): + if bo_id not in bo_ids: + warnings.append(f"STRUCTURE_SOURCE_BO_NOT_IN_BO_JSON:{sid}:{bo_id}") + bo_row = _as_dict(index["by_bo_id"].get(bo_id)) + if sid and sid not in _strings(bo_row.get("structure_ids")): + warnings.append(f"BY_BO_ID_MISSING_STRUCTURE_REF:{bo_id}:{sid}") + for bo_id, row in index["by_bo_id"].items(): + if bo_id not in bo_ids: + warnings.append(f"BY_BO_ID_UNKNOWN_BO:{bo_id}") + for sid in _strings(_as_dict(row).get("structure_ids")): + if sid not in structure_ids: + warnings.append(f"BY_BO_ID_UNKNOWN_STRUCTURE:{bo_id}:{sid}") + return structures, index, warnings + + + def _compact_structure(structure: dict[str, Any]) -> dict[str, Any]: + return { + "structure_id": structure.get("structure_id"), + "candidate_structure_type": structure.get("candidate_structure_type"), + "structure_label": structure.get("structure_label"), + "source_bo_ids": _strings(structure.get("source_bo_ids")), + "issue_cluster_ids": _strings(structure.get("issue_cluster_ids")), + "asset_cluster_ids": _strings(structure.get("asset_cluster_ids")), + "evidence_indexes": _strings(structure.get("evidence_indexes")), + "linked_claim_group_ids": _strings(structure.get("linked_claim_group_ids")), + "linked_liability_group_ids": _strings(structure.get("linked_liability_group_ids")), + "linked_actio_signal_ids": _strings(structure.get("linked_actio_signal_ids")), + "legal_effect_roles": _strings(structure.get("legal_effect_roles")), + "review_required": bool(structure.get("review_required")), + } + + + def _evidence_id(record: dict[str, Any]) -> str: + return str(_first_key(record, ["evidence_index", "evidence_id", "id", "index", "EvidenceIndex", "doc_id"]) or "").strip() + + + def _flatten_records(value: Any, depth: int = 0) -> list[dict[str, Any]]: + if depth > 3: + return [] + if isinstance(value, list): + out: list[dict[str, Any]] = [] + for item in value: + out.extend(_flatten_records(item, depth + 1)) + return out + if isinstance(value, dict): + if any(k in value for k in ("evidence_index", "evidence_id", "id", "related_bo_ids", "source_bo_ids", "signal_id", "route_id")): + return [value] + out: list[dict[str, Any]] = [] + for child in value.values(): + if isinstance(child, (list, dict)): + out.extend(_flatten_records(child, depth + 1)) + return out + return [] + + + def _excerpt_from_indexed(record: dict[str, Any]) -> str: + """X-8: evidence_indexed.json의 실제 필드(key_facts 우선, key_dates/key_amounts 보조)로 발췌 실질화. + 기존 키(excerpt/summary/content/text/body)는 fallback으로 유지.""" + key_facts = _strings(record.get("key_facts")) + if key_facts: + return _text(" / ".join(key_facts)) + parts: list[str] = [] + key_dates = _strings(record.get("key_dates")) + key_amounts = _strings(record.get("key_amounts")) + if key_dates: + parts.append("dates: " + " / ".join(key_dates)) + if key_amounts: + parts.append("amounts: " + " / ".join(key_amounts)) + if parts: + return _text("; ".join(parts)) + return _text(_first_key(record, ["excerpt", "summary", "content", "text", "body"])) + + + def _build_evidence_authority(evidence_indexed: Any) -> dict[str, Any]: + authority: dict[str, Any] = {} + for record in _flatten_records(evidence_indexed): + eid = _evidence_id(record) + if not eid: + continue + authority[eid] = { + "evidence_index": eid, + "title": _first_key(record, ["title", "Title", "name", "doc_title"]), + "excerpt": _excerpt_from_indexed(record), + "priority": _first_key(record, ["priority", "Priority", "weight"]), + "authentication": _first_key(record, ["authentication", "auth", "source_authentication"]), + "source": "evidence_indexed.json", + } + return authority + + + def _collect_bo_refs(value: Any, bo_ids: set[str]) -> list[str]: + refs: list[str] = [] + if isinstance(value, str): + if value in bo_ids: + refs.append(value) + elif isinstance(value, list): + for item in value: + refs.extend(_collect_bo_refs(item, bo_ids)) + elif isinstance(value, dict): + for key in ("related_bo_ids", "source_bo_ids", "bo_ids", "BO_ID", "bo_id", "source_bo_id"): + refs.extend([x for x in _strings(value.get(key)) if x in bo_ids]) + for child in value.values(): + if isinstance(child, (dict, list)): + refs.extend(_collect_bo_refs(child, bo_ids)) + return _dedupe(refs) + + + def _compact_signal(record: dict[str, Any], signal_type: str) -> dict[str, Any]: + return { + "signal_type": signal_type, + "signal_id": _first_key(record, ["signal_id", "route_id", "id", "structure_id"]), + "candidate_structure_types": _strings(record.get("candidate_structure_types")), + "linked_structure_ids": _strings(record.get("linked_structure_ids")), + "related_evidence_indexes": _strings(_first_key(record, ["related_evidence_indexes", "evidence_refs", "source_evidence_indexes"])), + "party_roles": record.get("party_roles") if isinstance(record.get("party_roles"), list) else [], + "actio_role_hints": record.get("actio_role_hints") if isinstance(record.get("actio_role_hints"), list) else [], + "claim_group_candidates": record.get("claim_group_candidates") if isinstance(record.get("claim_group_candidates"), list) else [], + "liability_group_candidates": record.get("liability_group_candidates") if isinstance(record.get("liability_group_candidates"), list) else [], + "summary": _text(record, 500), + } + + + def _build_signal_hints_by_bo_id(docs: dict[str, Any], bo_ids: set[str]) -> dict[str, dict[str, list[dict[str, Any]]]]: + out: dict[str, dict[str, list[dict[str, Any]]]] = { + bo_id: {"actio": [], "case_liability": [], "legal_effect": []} for bo_id in bo_ids + } + for signal_type, doc in docs.items(): + for record in _flatten_records(doc): + bo_refs = _collect_bo_refs(record, bo_ids) + for bo_id in bo_refs: + bucket = out.setdefault(bo_id, {"actio": [], "case_liability": [], "legal_effect": []}) + key = {"actio_case_signals": "actio", "case_liability_signals": "case_liability", "legal_effect_signals": "legal_effect"}[signal_type] + if len(bucket[key]) < MAX_SIGNAL_ITEMS_PER_BO: + bucket[key].append(_compact_signal(record, key)) + return out + + + def _linked_by_bo(bo_id: str, index: dict[str, Any]) -> dict[str, Any]: + row = _as_dict(index.get("by_bo_id", {}).get(bo_id)) + return { + "structure_ids": _strings(row.get("structure_ids")), + "issue_cluster_ids": _strings(row.get("issue_cluster_ids")), + "asset_cluster_ids": _strings(row.get("asset_cluster_ids")), + "evidence_index_ids": _strings(row.get("evidence_index_ids") or row.get("evidence_indexes")), + "candidate_structure_types": _strings(row.get("candidate_structure_types")), + "claim_group_ids": _strings(row.get("claim_group_ids") or row.get("linked_claim_group_ids")), + "liability_group_ids": _strings(row.get("liability_group_ids") or row.get("linked_liability_group_ids")), + "downstream_fact_roles": _strings(row.get("downstream_fact_roles") or row.get("legal_effect_roles")), + } + + + def _score_evidence(evidence_rows: list[dict[str, Any]]) -> tuple[str, str]: + if not evidence_rows: + return "low", "low" + text = " ".join(_text(row, 300).lower() for row in evidence_rows) + documentary_markers = ["등기", "계약", "판결", "공문", "금융", "송금", "계좌", "registry", "contract", "judgment"] + if any(marker.lower() in text for marker in documentary_markers): + return "high", "documentary" + if len(evidence_rows) >= 2: + return "high", "high" + if "meeting" in text or "회의" in text or "면담" in text: + return "medium", "meeting_only_core" + return "medium", "medium" + + + def _legal_roles(types: list[str], structures: list[dict[str, Any]], signals: dict[str, Any]) -> list[str]: + roles: list[str] = [] + for structure in structures: + roles.extend(_strings(structure.get("legal_effect_roles"))) + roles.extend([STRUCTURE_TYPE_TO_ROLE.get(t, "legal_effect_roles") for t in types]) + if signals.get("actio"): + roles.append("actio_roles") + if signals.get("case_liability"): + roles.append("claim_chain_ref") + if signals.get("legal_effect"): + roles.append("legal_effect_roles") + return _dedupe(roles) + + + def _modules(types: list[str], signals: dict[str, Any]) -> list[str]: + modules = [STRUCTURE_TYPE_TO_MODULE.get(t, "legal_effect") for t in types] + if signals.get("actio"): + modules.append("actio") + if signals.get("case_liability"): + modules.append("claim_liability") + if signals.get("legal_effect"): + modules.append("legal_effect") + return _dedupe(modules) + + + def _linked_structures(linked: dict[str, Any]) -> dict[str, Any]: + return { + "structure_ids": _strings(linked.get("structure_ids")), + "issue_cluster_ids": _strings(linked.get("issue_cluster_ids")), + "asset_cluster_ids": _strings(linked.get("asset_cluster_ids")), + "evidence_index_ids": _strings(linked.get("evidence_index_ids")), + "candidate_structure_types": _strings(linked.get("candidate_structure_types")), + "claim_group_ids": _strings(linked.get("claim_group_ids")), + "liability_group_ids": _strings(linked.get("liability_group_ids")), + } + + + def _parties(bo: dict[str, Any], signals: dict[str, Any]) -> list[dict[str, Any]]: + core = _as_dict(bo.get("core_field_base")) + out: list[dict[str, Any]] = [] + for role, key in [("actor", "Performer"), ("counterparty", "Subject")]: + party = core.get(key) + if party: + out.append({"party": _text(party, 160), "role": role, "source": "BO.core_field_base"}) + for signal in _as_list(signals.get("case_liability")): + for item in _as_list(_as_dict(signal).get("party_roles")): + if isinstance(item, dict) and item.get("party"): + out.append({"party": _text(item.get("party"), 160), "role": _text(item.get("role"), 80), "source": "case_liability_signals"}) + seen: set[tuple[str, str]] = set() + deduped: list[dict[str, Any]] = [] + for row in out: + key = (row.get("party", ""), row.get("role", "")) + if key not in seen: + seen.add(key) + deduped.append(row) + return deduped + + + def _derived_fact(bo: dict[str, Any], row: dict[str, Any]) -> str: + date = row.get("date") or "" + actor = "" + for party in row.get("parties", []): + if isinstance(party, dict) and party.get("role") == "actor": + actor = party.get("party") or "" + break + obj = row.get("object_spec") or "" + action = row.get("action") or bo.get("action") or "" + if date and actor and obj and action: + return f"[{date}] {actor}가 {obj}에 관하여 {action}하였다는 사실" + if action: + return str(action) + return "" + + + def _basis(bo_id: str, evidence_refs: list[str], structures: dict[str, Any]) -> str: + parts = [f"source_bo_id={bo_id}"] + if evidence_refs: + parts.append("evidence_refs=" + ",".join(evidence_refs[:8])) + sids = _strings(structures.get("structure_ids")) + if sids: + parts.append("structure_ids=" + ",".join(sids[:8])) + return "; ".join(parts) + + + def _claim_chain_ref(linked: dict[str, Any], signals: dict[str, Any]) -> dict[str, Any]: + return { + "claim_group_ids": _strings(linked.get("claim_group_ids")), + "liability_group_ids": _strings(linked.get("liability_group_ids")), + "signal_count": len(_as_list(signals.get("case_liability"))), + } + + + def _legal_calculation_object(row: dict[str, Any], types: list[str]) -> dict[str, Any]: + needs_calc = any(t in {"money_claim", "secured_debt", "land_use_gain", "valuation", "fraudulent_transfer"} for t in types) + return { + "calculation_required": needs_calc, + "amount_source": "BO.amount" if row.get("amount") else None, + "amount_text": row.get("amount"), + "object_basis": row.get("object_spec"), + "candidate_structure_types": types, + } + + + def _build_row(idx: int, item: dict[str, Any], structure_by_id: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]: + bo_id = item["source_bo_id"] + bo = _as_dict(item.get("bo_compact")) + linked = _linked_structures(_as_dict(item.get("linked_structures"))) + signals = _as_dict(item.get("signal_hints")) + related_structures = [_as_dict(structure_by_id.get(sid)) for sid in linked["structure_ids"] if isinstance(structure_by_id.get(sid), dict)] + types = _dedupe(linked["candidate_structure_types"] + [str(s.get("candidate_structure_type")) for s in related_structures if s.get("candidate_structure_type")]) + evidence_refs = _dedupe(_strings(item.get("evidence_refs")) + linked["evidence_index_ids"]) + evidence_rows = [_as_dict(_as_dict(item.get("evidence_authority")).get(eid)) for eid in evidence_refs] + evidence_rows = [row for row in evidence_rows if row] + credibility, proof_strength = _score_evidence(evidence_rows) + core = _as_dict(bo.get("core_field_base")) + row = { + "fact_id": f"F-{idx:03d}", + "source_bo_id": bo_id, + "type": _first_value(bo.get("bo_type"), types[0] if types else None), + "date": _first_value(core.get("BehaviorTime"), bo.get("date")), + "parties": _parties(bo, signals), + "object_spec": _first_value(core.get("Object"), _text(linked["asset_cluster_ids"], 160)), + "amount": _text(_first_value(bo.get("amount"), core.get("Amount")), 240) or None, + "action": _first_value(bo.get("action"), core.get("Action")), + "evidence_refs": evidence_refs, + "credibility": credibility, + "legal_centrality": "critical" if linked["structure_ids"] or signals.get("actio") else "medium", + "proof_strength": proof_strength, + "legal_effect_roles": _legal_roles(types, related_structures, signals), + "claim_chain_ref": _claim_chain_ref(linked, signals), + "linked_structures": linked, + "must_not_drop_in_claim_types": _modules(types, signals) if linked["structure_ids"] or signals.get("actio") else [], + "must_consider": bool(linked["structure_ids"] or signals.get("actio")), + "state_context": { + "bo_type": bo.get("bo_type"), + "issue_cluster_ids": linked["issue_cluster_ids"], + "asset_cluster_ids": linked["asset_cluster_ids"], + }, + "money_claim_effect": {"enabled": "money_claim" in types, "source_bo_id": bo_id if "money_claim" in types else None}, + "commercial_successor_effect": {"enabled": "commercial_successor" in types, "source_bo_id": bo_id if "commercial_successor" in types else None}, + "actio_roles": _as_list(signals.get("actio")), + "linked_actio_structures": { + "structure_ids": [s.get("structure_id") for s in related_structures if s.get("candidate_structure_type") in {"fraudulent_transfer", "preserved_claim"}], + "actio_signal_count": len(_as_list(signals.get("actio"))), + }, + "downstream_module_candidates": _modules(types, signals), + "skeleton_validation_codes": [], + "derived_fact": "", + "derivation_basis": "", + "legal_calculation_object": {}, + } + row["legal_calculation_object"] = _legal_calculation_object(row, types) + row["derived_fact"] = _derived_fact(bo, row) + row["derivation_basis"] = _basis(bo_id, evidence_refs, linked) + if not row["derived_fact"]: + row["skeleton_validation_codes"].append("EMPTY_DERIVED_FACT") + if not row["evidence_refs"]: + row["skeleton_validation_codes"].append("NO_EVIDENCE_REFS") + if not linked["structure_ids"]: + row["skeleton_validation_codes"].append("NO_LINKED_LEGAL_EFFECT_STRUCTURE") + return row, related_structures + + + def _mark_row_code(row: dict[str, Any], code: str) -> None: + codes = row.setdefault("skeleton_validation_codes", []) + if isinstance(codes, list) and code not in codes: + codes.append(code) + + + def _domain_payload_compact(bo_by_id: dict[str, Any], row: dict[str, Any]) -> dict[str, Any]: + bo = _as_dict(bo_by_id.get(str(row.get("source_bo_id")))) + return _as_dict(bo.get("domain_payload_compact")) + + + def _clean_str(value: Any) -> str: + return str(value).strip() if value not in (None, "") else "" + + + def _periods_overlap(start_a: str, end_a: str, start_b: str, end_b: str) -> bool: + a0, a1 = start_a or "0000-00-00", end_a or "9999-12-31" + b0, b1 = start_b or "0000-00-00", end_b or "9999-12-31" + return a0 <= b1 and b0 <= a1 + + + def _possession_conflict_scan(rows: list[dict[str, Any]], bo_by_id: dict[str, Any]) -> None: + incompatible = {frozenset(pair) for pair in POSSESSION_INCOMPATIBLE_CAUSE_PAIRS} + entries: list[dict[str, Any]] = [] + for row in rows: + possession = _as_dict(_domain_payload_compact(bo_by_id, row).get("possession_state")) + if not possession: + continue + asset_keys = _strings(possession.get("asset_ref")) or _strings(_as_dict(row.get("state_context")).get("asset_cluster_ids")) + events = [x for x in (_as_dict(e) for e in _as_list(possession.get("possession_events"))) if x] + causes = [x for x in (_as_dict(c) for c in _as_list(possession.get("legal_cause_assertions"))) if _clean_str(x.get("cause_asserted"))] + if events and not causes: + _mark_row_code(row, "LEGAL_CAUSE_STATE_UNKNOWN_REVIEW") + starts = sorted([s for s in (_clean_str(e.get("start_date")) for e in events) if s]) + ends = [_clean_str(e.get("end_date")) for e in events] + open_ended = (not events) or any(not e for e in ends) + period_start = starts[0] if starts else "" + period_end = "" if open_ended else max(e for e in ends if e) + for cause in causes: + entries.append({ + "row": row, + "asset_keys": set(asset_keys), + "cause": _clean_str(cause.get("cause_asserted")), + "start": _clean_str(cause.get("assertion_date")) or period_start, + "end": period_end, + }) + for i in range(len(entries)): + for j in range(i + 1, len(entries)): + first, second = entries[i], entries[j] + if first["row"] is second["row"]: + continue + if not (first["asset_keys"] & second["asset_keys"]): + continue + if frozenset((first["cause"], second["cause"])) not in incompatible: + continue + if not _periods_overlap(first["start"], first["end"], second["start"], second["end"]): + continue + for row in (first["row"], second["row"]): + _mark_row_code(row, "POSSESSION_CAUSE_CONFLICT") + row["must_consider"] = True + + + def _notice_receipt_gate_scan(rows: list[dict[str, Any]], bo_by_id: dict[str, Any]) -> None: + for row in rows: + notice = _as_dict(_domain_payload_compact(bo_by_id, row).get("notice_lifecycle")) + if not notice: + continue + if notice.get("requires_receipt_for_effect") is not True: + continue + received = _clean_str(notice.get("received_date")) + arrival = _clean_str(notice.get("arrival_date")) + if not received and not arrival: + _mark_row_code(row, "NOTICE_RECEIPT_UNVERIFIED") + row["must_consider"] = True + if received and not _strings(notice.get("receipt_evidence_refs")) and not _clean_str(notice.get("recipient_admission_text")): + _mark_row_code(row, "NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW") + + + def _lien_extinction_notice_scan(rows: list[dict[str, Any]], bo_by_id: dict[str, Any]) -> None: + receipt_by_notice_id: dict[str, str] = {} + possession_entries: list[dict[str, Any]] = [] + for row in rows: + payload = _domain_payload_compact(bo_by_id, row) + notice = _as_dict(payload.get("notice_lifecycle")) + notice_id = _clean_str(notice.get("notice_id")) + receipt_date = _clean_str(notice.get("received_date")) or _clean_str(notice.get("arrival_date")) + if notice_id and receipt_date: + receipt_by_notice_id[notice_id] = min(receipt_date, receipt_by_notice_id.get(notice_id, "9999-12-31")) + possession = _as_dict(payload.get("possession_state")) + asset_keys = set(_strings(possession.get("asset_ref"))) + for event in (_as_dict(x) for x in _as_list(possession.get("possession_events"))): + start = _clean_str(event.get("start_date")) + if start and asset_keys: + possession_entries.append({"row": row, "asset_keys": asset_keys, "start": start}) + for row in rows: + lien = _as_dict(_domain_payload_compact(bo_by_id, row).get("lien_state")) + if not lien: + continue + receipt_dates = [receipt_by_notice_id[ref] for ref in _strings(lien.get("extinction_notice_refs")) if ref in receipt_by_notice_id] + lien_assets = set(_strings(lien.get("asset_ref"))) + if not receipt_dates or not lien_assets: + continue + receipt_date = min(receipt_dates) + for entry in possession_entries: + if (lien_assets & entry["asset_keys"]) and entry["start"] > receipt_date: + _mark_row_code(row, "LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW") + _mark_row_code(entry["row"], "LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW") + + + # ---------------------------------------------------------------- X-1/X-2/X-3: 예외의 상류 소거 + def _distinct_values(values: list[Any]) -> list[str]: + out: list[str] = [] + for value in values: + s = _text(value, 240) + if s and s not in out: + out.append(s) + return out + + + def _projection_conflicts_for_row(row: dict[str, Any], bo: dict[str, Any]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + """X-3: v2 트리거 잔여 3유형에 4중 조건(단일 값 선택 필수 / source-backed 상충 후보 2+ / + 보존·defer 불가 / compact 판정 충분)을 적용한다. 후보 2개 미만의 단순 누락은 조건 ② 불충족으로 + deterministic review code로 전환한다. X-1(복수 role/module/그룹 = 정상 다중 라우팅)과 + X-2(파생 사실 문자열 길이 기반)의 trigger는 폐지되었다(배열 보존이 기본값). + 반환: (pack_candidates, deterministic_reviews).""" + core = _as_dict(bo.get("core_field_base")) + linked = _as_dict(row.get("linked_structures")) + roles = _strings(row.get("legal_effect_roles")) + modules = _strings(row.get("downstream_module_candidates")) + calc = _as_dict(row.get("legal_calculation_object")) + pack_candidates: list[dict[str, Any]] = [] + reviews: list[dict[str, Any]] = [] + + def _review(review_code: str, exception_type: str, reason: str) -> None: + reviews.append({ + "fact_id": row["fact_id"], + "source_bo_id": row["source_bo_id"], + "review_code": review_code, + "origin_exception_type": exception_type, + "reason": reason, + }) + + def _candidate(exception_type: str, trigger_reason: str, allowed_output_fields: list[str], + compact_context: dict[str, Any], conflict_flags: list[str]) -> None: + pack_candidates.append({ + "exception_type": exception_type, + "trigger_reason": trigger_reason, + "allowed_output_fields": allowed_output_fields, + "compact_context": compact_context, + "conflict_flags": conflict_flags, + "row": row, + }) + + if calc.get("calculation_required"): + conflict_fields: dict[str, list[str]] = {} + # X-3 ①②: 양 원천(BO.amount vs core.Amount)이 실재하며 상충하면 단일 값 선택이 필수인 진짜 예외. + amount_candidates = _distinct_values([bo.get("amount"), core.get("Amount")]) + if len(amount_candidates) >= 2: + conflict_fields["amount"] = amount_candidates + if not row.get("object_spec"): + object_candidates = _distinct_values([core.get("Object"), ", ".join(_strings(linked.get("asset_cluster_ids")))]) + if len(object_candidates) >= 2: + conflict_fields["object_spec"] = object_candidates + if conflict_fields: + _candidate( + "legal_calculation_required", "calculation_source_conflicting_candidates", + ["amount", "object_spec", "legal_calculation_object", "derivation_basis", "skeleton_validation_codes"], + { + "target_fields": sorted(conflict_fields), + "source_backed_candidates": conflict_fields, + "candidate_structure_types": _strings(calc.get("candidate_structure_types")), + "derivation_basis": row.get("derivation_basis"), + }, + ["CALCULATION_BASIS_CONFLICT"], + ) + elif not row.get("amount") or not row.get("object_spec"): + _review("CALCULATION_BASIS_INCOMPLETE_REVIEW", "legal_calculation_required", + "calculation_source_missing_without_competing_candidates") + asset_ids = _strings(linked.get("asset_cluster_ids")) + if asset_ids and not row.get("object_spec"): + if len(asset_ids) >= 2: + _candidate( + "unresolved_asset_cluster_projection", "asset_cluster_projection_requires_single_object_choice", + ["object_spec", "linked_structures", "legal_calculation_object", "derivation_basis"], + { + "target_fields": ["object_spec"], + "source_backed_candidates": {"object_spec": asset_ids}, + "structure_ids": _strings(linked.get("structure_ids")), + "derivation_basis": row.get("derivation_basis"), + }, + ["ASSET_CLUSTER_PROJECTION_GAP"], + ) + else: + _review("ASSET_CLUSTER_PROJECTION_GAP_REVIEW", "unresolved_asset_cluster_projection", + "single_asset_cluster_without_object_spec") + if _strings(linked.get("structure_ids")) and (not roles or not modules): + types = _strings(linked.get("candidate_structure_types")) + role_candidates = _distinct_values([STRUCTURE_TYPE_TO_ROLE.get(t, "legal_effect_roles") for t in types]) if not roles else [] + module_candidates = _distinct_values([STRUCTURE_TYPE_TO_MODULE.get(t, "legal_effect") for t in types]) if not modules else [] + if len(role_candidates) >= 2 or len(module_candidates) >= 2: + _candidate( + "unresolved_structure_role_projection", "structure_role_projection_requires_single_choice", + ["legal_effect_roles", "downstream_module_candidates", "claim_chain_ref", "actio_roles", "linked_structures"], + { + "target_fields": ["legal_effect_roles", "downstream_module_candidates"], + "source_backed_candidates": {"legal_effect_roles": role_candidates, "downstream_module_candidates": module_candidates}, + "structure_ids": _strings(linked.get("structure_ids")), + "candidate_structure_types": types, + }, + ["STRUCTURE_ROLE_PROJECTION_GAP"], + ) + else: + _review("STRUCTURE_ROLE_PROJECTION_GAP_REVIEW", "unresolved_structure_role_projection", + "structure_role_projection_without_competing_candidates") + return pack_candidates, reviews + + + def _exception_item(exception_id: str, candidate: dict[str, Any]) -> dict[str, Any]: + row = candidate["row"] + return { + "exception_id": exception_id, + "exception_type": candidate["exception_type"], + "fact_id": row["fact_id"], + "source_bo_id": row["source_bo_id"], + "severity": "blocking_projection", + "trigger_reason": candidate["trigger_reason"], + "candidate_fact_row_subset": {key: deepcopy(row.get(key)) for key in candidate["allowed_output_fields"] if key in row}, + "compact_context": candidate["compact_context"], + "allowed_output_fields": candidate["allowed_output_fields"], + "allowed_decisions": ["PATCH", "KEEP_AS_IS", "BLOCK_REVIEW"], + "conflict_flags": candidate["conflict_flags"], + } + + + def _assemble_exception_pack(pack_candidates: list[dict[str, Any]], deterministic_reviews: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], int]: + """FX-### 부여 후 방어 상한 적용. legal_theory 표지는 무조건 LLM 금지 → deterministic review. + 상한 초과분은 자동 차단이 아니라 exception_id를 유지한 채 review로 라우팅한다(§5-6).""" + exceptions: list[dict[str, Any]] = [] + overflow = 0 + total_bytes = 0 + counter = itertools.count(1) + for candidate in pack_candidates: + exception_id = f"FX-{next(counter):03d}" + row = candidate["row"] + review_code = REVIEW_CODE_BY_EXCEPTION_TYPE[candidate["exception_type"]] + if any("legal_theory" in flag for flag in candidate["conflict_flags"]): + deterministic_reviews.append({ + "fact_id": row["fact_id"], "source_bo_id": row["source_bo_id"], + "review_code": "LEGAL_THEORY_REQUIRED_REVIEW", + "origin_exception_type": candidate["exception_type"], + "reason": f"legal_theory_marker_routes_to_stage2:{exception_id}", + }) + _mark_row_code(row, "LEGAL_THEORY_REQUIRED_REVIEW") + row["must_consider"] = True + continue + item = _exception_item(exception_id, candidate) + item_text = json.dumps(item, ensure_ascii=False, sort_keys=True) + oversize = len(item_text) > PACK_ITEM_CHAR_LIMIT + over_budget = len(exceptions) >= PACK_MAX_ITEMS or total_bytes + len(item_text.encode("utf-8")) > PACK_MAX_BYTES + if oversize or over_budget: + overflow += 1 + deterministic_reviews.append({ + "fact_id": row["fact_id"], "source_bo_id": row["source_bo_id"], + "review_code": review_code, + "origin_exception_type": candidate["exception_type"], + "reason": ("pack_item_over_char_limit:" if oversize else "pack_budget_overflow:") + exception_id, + }) + _mark_row_code(row, review_code) + row["must_consider"] = True + continue + total_bytes += len(item_text.encode("utf-8")) + exceptions.append(item) + return exceptions, overflow + + + def _les_review_origins(les_doc: Any, structures: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]: + """X-7: LES 실질 review 상태 수집 — (a) review_required=true, (b) pending_exception_state.blocked + 비공백(Part 3 v2 additive 필드, 부재 시 무시), (c) quality_gate.review_queue 실질 항목.""" + origin_by_sid: dict[str, list[dict[str, Any]]] = {} + for structure in structures: + if not isinstance(structure, dict): + continue + sid = str(structure.get("structure_id") or "") + if not sid: + continue + if structure.get("review_required") is True: + origin_by_sid.setdefault(sid, []).append({"origin": "structure_review_required"}) + blocked = _strings(_as_dict(structure.get("pending_exception_state")).get("blocked")) + if blocked: + origin_by_sid.setdefault(sid, []).append({"origin": "pending_exception_blocked", "blocked_exception_ids": blocked}) + for entry in _as_list(_as_dict(_as_dict(les_doc).get("quality_gate")).get("review_queue")): + item = _as_dict(entry) + sid = str(item.get("structure_id") or "") + review_type = _clean_str(item.get("review_type")) + if not sid or not review_type: + continue + origins = origin_by_sid.setdefault(sid, []) + if review_type == "structure_review_required" and any(o.get("origin") == "structure_review_required" for o in origins): + continue + origins.append({"origin": "quality_gate_review_queue", "review_type": review_type}) + return origin_by_sid + + + def _apply_les_inheritance(rows: list[dict[str, Any]], origin_by_sid: dict[str, list[dict[str, Any]]]) -> dict[str, dict[str, Any]]: + by_fact_id: dict[str, dict[str, Any]] = {} + for row in rows: + sids = sorted(set(_strings(_as_dict(row.get("linked_structures")).get("structure_ids"))) & set(origin_by_sid)) + if not sids: + continue + _mark_row_code(row, "LES_REVIEW_REQUIRED") + row["must_consider"] = True + by_fact_id[row["fact_id"]] = { + "fact_id": row["fact_id"], + "source_bo_id": row["source_bo_id"], + "structure_ids": sids, + "origins": {sid: origin_by_sid[sid] for sid in sids}, + } + return by_fact_id + + + def _validate_candidate_bundle(bundle: dict[str, Any]) -> None: + if bundle.get("schema_version") != "stage1_fact_ledger_candidate_bundle.v2": + raise ValueError("candidate bundle schema_version mismatch") + items = _as_list(bundle.get("candidate_items")) + if not items: + raise ValueError("candidate_items is empty") + for item in items: + missing = [field for field in FINAL_FIELDS if field not in item] + if missing: + raise ValueError(f"candidate item missing fields: {missing}") + extra = [field for field in item if field not in FINAL_FIELDS] + if extra: + raise ValueError(f"candidate item has arbitrary fields: {extra}") + manifest = _as_dict(bundle.get("ref_manifest")) + for key in ("bo_ids", "structure_ids", "evidence_ids"): + if not isinstance(manifest.get(key), list): + raise ValueError(f"ref_manifest.{key} must be list") + ref = _as_dict(bundle.get("exception_pack_ref")) + if ref.get("path") != PACK_OUT_PATH or not ref.get("sha256"): + raise ValueError("exception_pack_ref invalid") + if "exception_pack" in bundle or "created_at_utc" in bundle: + raise ValueError("candidate bundle must not embed exception_pack or created_at_utc") + + + def _validate_exception_pack(pack: dict[str, Any]) -> None: + if pack.get("schema_version") != "stage1_fact_ledger_exception_pack.v2": + raise ValueError("exception pack schema_version mismatch") + for exc in _as_list(pack.get("exceptions")): + if exc.get("exception_type") not in PACK_EXCEPTION_TYPES: + raise ValueError(f"unsupported exception_type: {exc.get('exception_type')}") + + + def main() -> None: + try: + _init_mcp_session() + docs = {key: _read_doc(name) for key, name in INPUT_FILES.items()} + bo_items = _bo_items(docs["bo"]) + bo_by_id = {_bo_id(item): _compact_bo(item) for item in bo_items} + bo_ids = set(bo_by_id) + structures, structure_index, structure_warnings = _validate_legal_effect_structures(docs["legal_effect_structures"], bo_ids) + structure_by_id = { + str(item.get("structure_id")): _compact_structure(item) + for item in structures + if isinstance(item, dict) and item.get("structure_id") + } + evidence_authority = _build_evidence_authority(docs["evidence_indexed"]) + signal_hints = _build_signal_hints_by_bo_id({ + "actio_case_signals": docs["actio_case_signals"], + "case_liability_signals": docs["case_liability_signals"], + "legal_effect_signals": docs["legal_effect_signals"], + }, bo_ids) + warnings = list(structure_warnings) + # per-BO fact input 중간 표현은 메모리에서만 구성한다(§5-3 — 어떤 채널에도 물질화 금지). + rows: list[dict[str, Any]] = [] + pack_candidates: list[dict[str, Any]] = [] + deterministic_reviews: list[dict[str, Any]] = [] + for idx, bo_id in enumerate(sorted(bo_by_id), start=1): + bo = bo_by_id[bo_id] + linked = _linked_by_bo(bo_id, structure_index) + evidence_refs = _dedupe(bo["source_evidence_indexes"] + _strings(linked.get("evidence_index_ids"))) + for eid in evidence_refs: + if eid not in evidence_authority: + warnings.append(f"EVIDENCE_REF_NOT_IN_EVIDENCE_UNIVERSE:{bo_id}:{eid}") + item = { + "source_bo_id": bo_id, + "bo_compact": bo, + "linked_structures": linked, + "signal_hints": signal_hints.get(bo_id, {"actio": [], "case_liability": [], "legal_effect": []}), + "evidence_refs": evidence_refs, + "evidence_authority": {eid: evidence_authority.get(eid, {"evidence_index": eid, "missing": True}) for eid in evidence_refs}, + } + row, _related = _build_row(idx, item, structure_by_id) + rows.append(row) + candidates, reviews = _projection_conflicts_for_row(row, bo) + pack_candidates.extend(candidates) + deterministic_reviews.extend(reviews) + bo_compact_by_id = bo_by_id + _possession_conflict_scan(rows, bo_compact_by_id) + _notice_receipt_gate_scan(rows, bo_compact_by_id) + _lien_extinction_notice_scan(rows, bo_compact_by_id) + for review in deterministic_reviews: + for row in rows: + if row["fact_id"] == review["fact_id"]: + _mark_row_code(row, review["review_code"]) + row["must_consider"] = True + exceptions, overflow = _assemble_exception_pack(pack_candidates, deterministic_reviews) + origin_by_sid = _les_review_origins(docs["legal_effect_structures"], structures) + les_by_fact_id = _apply_les_inheritance(rows, origin_by_sid) + # F2 drafting gate 입력(소스팩 재읽기 폐지 대체): NOTICE_RECEIPT_UNVERIFIED 행의 notice_id. + notice_gate_fact_ids: list[str] = [] + notice_gate_ids: list[str] = [] + for row in rows: + if "NOTICE_RECEIPT_UNVERIFIED" in _as_list(row.get("skeleton_validation_codes")): + notice_gate_fact_ids.append(row["fact_id"]) + notice_id = _clean_str(_as_dict(_domain_payload_compact(bo_compact_by_id, row).get("notice_lifecycle")).get("notice_id")) + if notice_id: + notice_gate_ids.append(notice_id) + pack_payload = { + "schema_version": "stage1_fact_ledger_exception_pack.v2", + "status": "READY", + "has_exceptions": bool(exceptions), + "exception_count": len(exceptions), + "exceptions": exceptions, + "budget": { + "max_items": PACK_MAX_ITEMS, + "max_bytes": PACK_MAX_BYTES, + "item_char_limit": PACK_ITEM_CHAR_LIMIT, + "overflow_routed_to_review_count": overflow, + }, + } + _validate_exception_pack(pack_payload) + pack_text = json.dumps(pack_payload, ensure_ascii=False, indent=2) + "\n" + pack_sha = hashlib.sha256(pack_text.encode("utf-8")).hexdigest() + bundle = { + "schema_version": "stage1_fact_ledger_candidate_bundle.v2", + "status": "READY_WITH_EXCEPTIONS" if exceptions else "READY", + "candidate_items": rows, + "ref_manifest": { + "bo_ids": sorted(bo_ids), + "structure_ids": sorted(structure_by_id), + "evidence_ids": sorted(evidence_authority), + }, + "les_review_inheritance": {"by_fact_id": {fact_id: les_by_fact_id[fact_id] for fact_id in sorted(les_by_fact_id)}}, + "exception_pack_ref": { + "path": PACK_OUT_PATH, + "sha256": pack_sha, + "expected_exception_ids": [exc["exception_id"] for exc in exceptions], + }, + "compile_gate": { + "candidate_count": len(rows), + "exception_count": len(exceptions), + "deterministic_review_count": len(deterministic_reviews), + "les_inheritance_count": len(les_by_fact_id), + "deterministic_review_items": deterministic_reviews, + "notice_gate": {"affected_fact_ids": notice_gate_fact_ids, "notice_ids": _dedupe(notice_gate_ids)}, + "source_integrity_warnings": _dedupe(warnings), + "candidate_schema_closed": True, + "exception_schema_closed": True, + }, + } + _validate_candidate_bundle(bundle) + bundle_text = json.dumps({"fact_ledger_candidate_bundle": bundle}, ensure_ascii=False, indent=2) + "\n" + _write_text_doc(PACK_OUT_PATH, pack_text) + _write_text_doc(CANDIDATE_OUT_PATH, bundle_text) + pack_reread = _read_doc_raw_text(PACK_OUT_PATH) + if hashlib.sha256(pack_reread.encode("utf-8")).hexdigest() != pack_sha: + raise ValueError("post-write reread validation failed: exception pack sha mismatch") + bundle_reread = _read_doc(CANDIDATE_OUT_PATH) + if _as_dict(_as_dict(bundle_reread).get("fact_ledger_candidate_bundle")).get("schema_version") != "stage1_fact_ledger_candidate_bundle.v2": + raise ValueError("post-write reread validation failed: candidate bundle") + print(json.dumps({ + "status": bundle["status"], + "message": "F0 fact ledger reducer complete", + "candidate_path": CANDIDATE_OUT_PATH, + "exception_pack_path": PACK_OUT_PATH, + "fact_count": len(rows), + "has_exceptions": bool(exceptions), + "exception_count": len(exceptions), + "deterministic_review_count": len(deterministic_reviews), + "les_inheritance_count": len(les_by_fact_id), + "removed_inputs": REMOVED_INPUTS, + }, ensure_ascii=False)) + except Exception as exc: + print(json.dumps({ + "status": "FAILED", + "message": str(exc)[:500], + "candidate_path": CANDIDATE_OUT_PATH, + "exception_pack_path": PACK_OUT_PATH, + }, ensure_ascii=False)) + raise + + + if __name__ == "__main__": + main() + - task_name: Task_F1_fact_exception_adjudicator + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: low + llm_verbosity: low + max_iterations: 1 + use_tools: [localdocs] + cache_control: + mode: auto + ttl: 20m + preflight: true + preflight_files: + - stage1_tmp/fact_ledger/fact_exception_pack.json + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + + TASK_NAME: Task_F1_fact_exception_adjudicator + STAGE: Stage 1 Fact Ledger 직접 컴파일러 — 조건부 예외 판정 (Part 1 v3 GB / Part 2 v3 R1 / Part 3 v2 L1 패턴) + MISSION: 결정론 reducer(F0)가 non-deferrable로 판정한 compact 투영 예외만 판정한다. 파일 병합·최종 파일 작성·사실 창작·LES 모호성 재판정은 하지 않는다. + + + + - 20년 이상 경력을 지닌 대한민국 최고 수준 민사송무 변호사이자 세계적 수준의 AI 아키텍트의 관점에서 판정한다. + - 유일한 입력은 preflight로 제공된 `stage1_tmp/fact_ledger/fact_exception_pack.json`이다. + - `Fact_Ledger_base_candidate.json`·`BO.json`·`legal_effect_structures.json`·`client_goal.json`·evidence 파일·signal 파일·raw 원문을 읽거나 요청하지 않는다. + - pack에 없는 exception_id·fact_id·BO ID·evidence index·structure/cluster ID·사실·당사자·일자·금액·출처를 창작하지 않는다. + - LES2가 확정한 issue cluster·asset cluster·candidate structure type 결정을 재판정하지 않는다. BO들이 같은 법적 쟁점·자산군·구조 유형에 속하는지도 판단하지 않는다. + - `Fact_Ledger_base.json`·candidate bundle·writer report를 작성하지 않는다. 출력 파일은 `stage1_tmp/fact_ledger/fact_adjudication.json` 하나뿐이다. + - markdown fence·prose 없이 raw JSON만 파일에 쓴다. + + + + - `unresolved_asset_cluster_projection`은 자산들이 동일한 법적 자산군인지의 판단을 허용하지 않는다. 이미 확정된 asset cluster의 투영 값 선택만 허용한다. + - `unresolved_structure_role_projection`은 candidate structure type의 선택·변경을 허용하지 않는다. 이미 확정된 구조의 role/module 투영 값 선택만 허용한다. + - 판정이 LES2 법적 구조 모호성의 재개를 요구하면 그 항목은 BLOCK_REVIEW로 차단하고 `adjudication_gate.reopened_les_ambiguity`를 true로 표시한다. + - `trigger_reason`·`conflict_flags`에 `legal_theory` 표지가 있으면 PATCH를 금지한다 — BLOCK_REVIEW(하류 법리 모듈 소관). + + + + 1. preflight로 제공된 exception pack의 `has_exceptions`를 확인한다. + 2. `has_exceptions == false`이면: `write_file(overwrite=true)`로 아래 no-exception 판정 객체를 `stage1_tmp/fact_ledger/fact_adjudication.json`에 저장하고 `"NO_EXCEPTIONS"`만 출력한 뒤 즉시 종료한다(terminate). 다른 어떤 파일도 읽지 않는다. + {"fact_exception_adjudication": {"schema_version": "stage1_fact_exception_adjudication.v1", "status": "READY_NO_EXCEPTIONS", "source_exception_pack_schema_version": "stage1_fact_ledger_exception_pack.v2", "decisions": [], "blocked_review_items": [], "adjudication_gate": {"input_exception_count": 0, "decision_count": 0, "blocked_count": 0, "coverage_pass": true, "json_only_contract_pass": true, "reopened_les_ambiguity": false}}} + 3. `has_exceptions == true`이면: `exceptions[]`의 각 항목을 그 항목에 내장된 `candidate_fact_row_subset`·`compact_context`(target_fields·source_backed_candidates)·`allowed_output_fields`·`allowed_decisions`·`conflict_flags`만으로 판정한다. 추가 read는 금지된다. + 4. 판정 규칙: + - `legal_calculation_required`: `compact_context.source_backed_candidates`의 상충 후보 중 문서 근거가 더 구체적인 단일 값을 선택해 PATCH하거나, 현행 행이 적정하면 KEEP_AS_IS. 후보 밖 값 창작 금지. + - `unresolved_asset_cluster_projection`: `source_backed_candidates.object_spec` 후보 중 등기행·별지목록 등 원문 ref 근거가 더 구체적인 값을 선택한다. 우열을 가릴 수 없으면 BLOCK_REVIEW. + - `unresolved_structure_role_projection`: role/module 투영 값을 후보 안에서 선택한다. 배열 보존이 기본값이며 축소 선택을 강제하지 않는다. + - PATCH의 patch 객체 키는 그 항목의 `allowed_output_fields` 안에서만 쓴다(F2가 위반 patch를 hard reject한다). patch 값은 `source_backed_candidates`에 실재하는 값이어야 한다. + - compact 근거로 확정할 수 없으면 반드시 BLOCK_REVIEW로 차단한다(확신 없는 확정 금지 — 인간 검토 라우팅). + - PATCH·KEEP_AS_IS 판정은 `decisions[]`에, BLOCK_REVIEW 판정은 `blocked_review_items[]`에 넣는다. 각 exception_id는 정확히 두 배열 중 한 곳에만 나타난다(F2가 exact coverage 등식으로 검증한다). + 5. `write_file(overwrite=true)`로 결과를 `stage1_tmp/fact_ledger/fact_adjudication.json`에 저장한다. + - 루트는 `fact_exception_adjudication`, `schema_version`은 `stage1_fact_exception_adjudication.v1`, `source_exception_pack_schema_version`은 `stage1_fact_ledger_exception_pack.v2`. + - `status`: 전 항목 비차단이면 `READY`, 차단 존재 시 `READY_WITH_BLOCKS`. + - decision 항목 필드: `exception_id`, `exception_type`(`legal_calculation_required|unresolved_asset_cluster_projection|unresolved_structure_role_projection`), `fact_id`, `source_bo_id`, `decision`(`PATCH|KEEP_AS_IS`), `patch`(PATCH일 때만, allowed_output_fields 키만), `review_required`, `reason_code`(`calculation_basis_selected|asset_projection_selected|structure_role_projected|keep_existing`), `basis_fields_used[]`(항목에 내장된 필드명의 부분집합), `conflict_flags_acknowledged[]`(항목 `conflict_flags`의 부분집합). + - blocked 항목 필드: `exception_id`, `exception_type`, `fact_id`, `source_bo_id`, `block_reason`(`insufficient_compact_record|reopens_les_ambiguity|unsupported_patch|missing_candidate_basis|schema_violation`). + - `adjudication_gate`: `input_exception_count`(pack의 예외 수와 동일), `decision_count`(decisions 배열 길이), `blocked_count`(blocked 배열 길이), `coverage_pass`(모든 exception_id가 정확히 한 곳에 나타날 때만 true), `json_only_contract_pass`, `reopened_les_ambiguity`. + - `exception_id`·`fact_id`·`source_bo_id`·`exception_type`은 pack 항목의 값을 그대로 인용한다(F2가 정체성 불일치를 hard fail 처리한다). + 6. `"F1 예외 판정 완료 (decisions=<건수>)"`만 출력하고 작업을 끝낸다(terminate). + + + + - Stage 1은 법률효과·청구원인을 확정하지 않는다. 여기 도달한 항목은 F2가 진행하려면 단일 투영 값 선택이 필수인 사안뿐이다. + - 판정은 Fact Ledger 행의 투영/조인 값 결정일 뿐 소유권·담보권·평가액·법리의 실체 판단이 아니다. + - 등기행·계약서·판결 등 문서 근거가 자유 문구보다 우선한다. 우열을 가릴 수 없으면 BLOCK_REVIEW. + - 복수 role/module/claim group은 배열형 최종 스키마에 보존되는 정상 상태다. 축소 선택을 강제하지 않는다. + + + + - exception pack 부재·파싱 불가: 즉시 중단하고 `FAILED: <사유>`만 출력한다. 판정 파일은 쓰지 않는다. + - tool 오류: 1회만 재시도. 재실패 시 `FAILED: `만 출력하고 종료한다. + + + - task_name: Task_F2_final_fact_ledger_gate_and_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 180 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import hashlib + import itertools + import json + import sys + from copy import deepcopy + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + CANDIDATE_PATH = "stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json" + PACK_PATH = "stage1_tmp/fact_ledger/fact_exception_pack.json" + ADJUDICATION_PATH = "stage1_tmp/fact_ledger/fact_adjudication.json" + TARGET_PATH = "Fact_Ledger_base.json" + REPORT_PATH = "stage1_tmp/fact_ledger/fact_ledger_writer_report.json" + FINAL_FIELDS = [ + "fact_id", "source_bo_id", "type", "date", "parties", "object_spec", "amount", "action", + "evidence_refs", "credibility", "legal_centrality", "proof_strength", "legal_effect_roles", + "claim_chain_ref", "linked_structures", "must_not_drop_in_claim_types", "must_consider", + "state_context", "money_claim_effect", "commercial_successor_effect", "actio_roles", + "linked_actio_structures", "downstream_module_candidates", "skeleton_validation_codes", + "derived_fact", "derivation_basis", "legal_calculation_object", + ] + FINAL_FIELD_SET = set(FINAL_FIELDS) + FINAL_DEFAULTS = { + "type": None, + "date": None, + "parties": [], + "object_spec": None, + "amount": None, + "action": None, + "evidence_refs": [], + "credibility": "low", + "legal_centrality": "low", + "proof_strength": "low", + "legal_effect_roles": [], + "claim_chain_ref": {}, + "linked_structures": {}, + "must_not_drop_in_claim_types": [], + "must_consider": False, + "state_context": {}, + "money_claim_effect": {}, + "commercial_successor_effect": {}, + "actio_roles": [], + "linked_actio_structures": {}, + "downstream_module_candidates": [], + "skeleton_validation_codes": [], + "derived_fact": "", + "derivation_basis": "", + "legal_calculation_object": {}, + } + GATE_REVIEW_CODES = { + "POSSESSION_CAUSE_CONFLICT", "NOTICE_RECEIPT_UNVERIFIED", "NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW", + "LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW", "LEGAL_CAUSE_STATE_UNKNOWN_REVIEW", + } + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-fact-ledger-f2-final-writer", + "version": "1.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ).raise_for_status() + + + def _parse_mcp_response(text: str) -> dict[str, Any] | None: + for line in text.strip().split("\n"): + if line.startswith("data: "): + try: + return json.loads(line[6:]) + except Exception: + return None + try: + return json.loads(text) + except Exception: + return None + + + def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": msg_id, + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + parsed = _parse_mcp_response(response.text) + if not parsed or "result" not in parsed: + raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}") + return parsed + + + def _strip_code_fence(text: str) -> str: + stripped = text.strip() + if stripped.startswith("```"): + first_newline = stripped.find("\n") + if first_newline >= 0: + stripped = stripped[first_newline + 1:] + if stripped.rstrip().endswith("```"): + stripped = stripped.rstrip()[:-3] + return stripped.strip() + + + def _lenient_json(raw: str, label: str) -> Any: + """SKILL §6.4/§6.7 관용 파서: strict 실패 시 코드펜스 제거 + raw_decode salvage(envelope 계층 포함).""" + text = (raw or "").strip() + if not text: + raise RuntimeError(f"Empty content for {label}") + try: + return json.loads(text) + except json.JSONDecodeError: + pass + text = _strip_code_fence(text) + try: + return json.loads(text) + except json.JSONDecodeError: + start = text.find("{") + alt = text.find("[") + if alt >= 0 and (start < 0 or alt < start): + start = alt + if start < 0: + raise RuntimeError(f"Content of {label} is not valid JSON: {text[:300]}") + try: + obj, _ = JSON_DECODER.raw_decode(text[start:]) + return obj + except json.JSONDecodeError as exc: + raise RuntimeError(f"Content of {label} is not valid JSON: {text[:300]}") from exc + + + def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any: + content = result["result"].get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + text = content[0].get("text", "") + envelope = _lenient_json(text, doc_name) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if not results: + raise RuntimeError(f"No read_docs results for {doc_name}") + first = results[0] + if isinstance(first, dict) and first.get("error"): + raise RuntimeError(f"{doc_name}: {first['error']}") + inner = first.get("content", "") if isinstance(first, dict) else "" + if inner in (None, ""): + raise RuntimeError(f"Empty content for {doc_name}") + if isinstance(inner, str): + return _lenient_json(inner, doc_name) + return inner + return envelope + + + def _read_doc(doc_name: str) -> Any: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + return _extract_json_from_read_docs_result(result, doc_name) + + + def _read_doc_raw_text(doc_name: str) -> str | None: + result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=_next_msg_id()) + content = result["result"].get("content", []) + if not content: + raise RuntimeError(f"No read_docs content for {doc_name}") + envelope = _lenient_json(content[0].get("text", ""), doc_name) + if isinstance(envelope, dict) and "results" in envelope: + results = envelope.get("results") or [] + if results and isinstance(results[0], dict): + raw = results[0].get("content") + if isinstance(raw, str): + return raw + return None + + + def _write_doc(doc_name: str, content: str) -> None: + _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=_next_msg_id()) + + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _strings(value: Any) -> list[str]: + out: list[str] = [] + if value is None: + return out + if isinstance(value, (str, int, float)): + s = str(value).strip() + return [s] if s else [] + if isinstance(value, list): + for item in value: + out.extend(_strings(item)) + return _dedupe(out) + + + def _dedupe(values: list[str]) -> list[str]: + seen: set[str] = set() + out: list[str] = [] + for value in values: + if value and value not in seen: + seen.add(value) + out.append(value) + return out + + + def _load_candidate_bundle() -> dict[str, Any]: + outer = _read_doc(CANDIDATE_PATH) + bundle = _as_dict(_as_dict(outer).get("fact_ledger_candidate_bundle")) + if bundle.get("schema_version") != "stage1_fact_ledger_candidate_bundle.v2": + raise ValueError("candidate bundle schema_version mismatch (v2 required)") + if bundle.get("status") not in {"READY", "READY_WITH_EXCEPTIONS"}: + raise ValueError("candidate bundle status invalid") + items = _as_list(bundle.get("candidate_items")) + if not items: + raise ValueError("candidate_items is empty") + for item in items: + missing = [field for field in FINAL_FIELDS if field not in item] + if missing: + raise ValueError(f"candidate item missing fields: {missing}") + extra = [field for field in item if field not in FINAL_FIELD_SET] + if extra: + raise ValueError(f"candidate item has arbitrary fields: {extra}") + manifest = _as_dict(bundle.get("ref_manifest")) + for key in ("bo_ids", "structure_ids", "evidence_ids"): + if not isinstance(manifest.get(key), list): + raise ValueError(f"ref_manifest.{key} must be list") + return bundle + + + def _load_exception_pack(bundle: dict[str, Any]) -> dict[str, Any]: + """X-6: pack은 bundle의 exception_pack_ref 경유로만 읽고 sha256·expected id를 대조한다.""" + ref = _as_dict(bundle.get("exception_pack_ref")) + if ref.get("path") != PACK_PATH: + raise ValueError("exception_pack_ref path mismatch") + raw_text = _read_doc_raw_text(PACK_PATH) + if isinstance(raw_text, str) and ref.get("sha256"): + digest = hashlib.sha256(raw_text.encode("utf-8")).hexdigest() + if digest != ref.get("sha256"): + raise ValueError("exception pack sha256 mismatch against candidate bundle ref") + pack = _lenient_json(raw_text, PACK_PATH) if isinstance(raw_text, str) else _read_doc(PACK_PATH) + pack = _as_dict(pack) + if pack.get("schema_version") != "stage1_fact_ledger_exception_pack.v2": + raise ValueError("exception pack schema_version mismatch (v2 required)") + expected_ids = _strings(ref.get("expected_exception_ids")) + pack_ids = [str(_as_dict(e).get("exception_id")) for e in _as_list(pack.get("exceptions"))] + if sorted(expected_ids) != sorted(pack_ids): + raise ValueError("exception pack ids mismatch against candidate bundle ref") + return pack + + + def _load_adjudication() -> dict[str, Any]: + outer = _read_doc(ADJUDICATION_PATH) + adjudication = _as_dict(_as_dict(outer).get("fact_exception_adjudication")) + if adjudication.get("schema_version") != "stage1_fact_exception_adjudication.v1": + raise ValueError("fact_exception_adjudication schema_version mismatch") + if adjudication.get("status") not in {"READY", "READY_NO_EXCEPTIONS", "READY_WITH_BLOCKS"}: + raise ValueError(f"fact_exception_adjudication status invalid: {adjudication.get('status')}") + return adjudication + + + def _gate_int(gate: dict[str, Any], key: str) -> int: + value = gate.get(key) + if isinstance(value, bool) or not isinstance(value, int): + return -1 + return value + + + def _check_decision_identity(entry: dict[str, Any], exception: dict[str, Any], label: str) -> None: + for key in ("exception_type", "fact_id", "source_bo_id"): + if str(entry.get(key) or "") != str(exception.get(key) or ""): + raise ValueError(f"{label} {entry.get('exception_id')}: {key} mismatch against exception pack") + + + def _validate_adjudication_coverage(adjudication: dict[str, Any], pack: dict[str, Any]) -> tuple[dict[str, dict[str, Any]], list[dict[str, Any]]]: + """X-6①: exact coverage 등식 — expected == decided ∪ blocked, decided ∩ blocked == ∅. + 중복 decision·미지 ID·예외 0건의 임의 decision·gate 수치 불일치·정체성 불일치는 전부 hard fail.""" + exceptions = { + str(exc.get("exception_id")): exc + for exc in _as_list(pack.get("exceptions")) + if isinstance(exc, dict) and exc.get("exception_id") + } + expected = set(exceptions) + raw_decisions = _as_list(adjudication.get("decisions")) + raw_blocked = _as_list(adjudication.get("blocked_review_items")) + gate = _as_dict(adjudication.get("adjudication_gate")) + if not expected: + if adjudication.get("status") != "READY_NO_EXCEPTIONS": + raise ValueError("no exceptions expected; adjudication status must be READY_NO_EXCEPTIONS") + if raw_decisions or raw_blocked: + raise ValueError("no exceptions expected; decisions and blocked_review_items must be empty") + if gate.get("input_exception_count") not in (0, None): + raise ValueError("adjudication_gate.input_exception_count must be 0 when no exceptions exist") + return {}, [] + blocked_items: list[dict[str, Any]] = [] + blocked_ids: set[str] = set() + for item in raw_blocked: + entry = _as_dict(item) + exception_id = str(entry.get("exception_id") or "") + if not exception_id: + raise ValueError("blocked_review_item missing exception_id") + if exception_id not in expected: + raise ValueError(f"blocked_review_item references unknown exception_id: {exception_id}") + if exception_id in blocked_ids: + raise ValueError(f"duplicate blocked_review_item for exception_id: {exception_id}") + _check_decision_identity(entry, exceptions[exception_id], "blocked_review_item") + blocked_ids.add(exception_id) + blocked_items.append(entry) + decisions_by_id: dict[str, dict[str, Any]] = {} + for item in raw_decisions: + decision = _as_dict(item) + exception_id = str(decision.get("exception_id") or "") + if not exception_id: + raise ValueError("decision missing exception_id") + if exception_id not in expected: + raise ValueError(f"decision references unknown exception_id: {exception_id}") + if exception_id in decisions_by_id: + raise ValueError(f"duplicate decision for exception_id: {exception_id}") + _check_decision_identity(decision, exceptions[exception_id], "decision") + decision_type = decision.get("decision") + if decision_type == "BLOCK_REVIEW": + if exception_id not in blocked_ids: + blocked_ids.add(exception_id) + blocked_items.append({ + "exception_id": exception_id, + "exception_type": decision.get("exception_type"), + "fact_id": decision.get("fact_id"), + "source_bo_id": decision.get("source_bo_id"), + "block_reason": str(decision.get("reason_code") or "insufficient_compact_record"), + }) + continue + if decision_type not in {"PATCH", "KEEP_AS_IS"}: + raise ValueError(f"unknown decision enum: {decision_type}") + decisions_by_id[exception_id] = decision + decided = set(decisions_by_id) + overlap = decided & blocked_ids + if overlap: + raise ValueError(f"coverage violation: decided and blocked overlap: {sorted(overlap)}") + if expected != (decided | blocked_ids): + missing = sorted(expected - (decided | blocked_ids)) + raise ValueError(f"coverage equation failed: missing={missing}") + if _gate_int(gate, "input_exception_count") != len(expected): + raise ValueError("adjudication_gate.input_exception_count mismatch") + if _gate_int(gate, "decision_count") != len(raw_decisions): + raise ValueError("adjudication_gate.decision_count mismatch") + if _gate_int(gate, "blocked_count") != len(raw_blocked): + raise ValueError("adjudication_gate.blocked_count mismatch") + if not gate.get("coverage_pass"): + raise ValueError("adjudication_gate.coverage_pass must be true when exceptions exist") + return decisions_by_id, blocked_items + + + def _exception_allowed_fields(pack: dict[str, Any]) -> dict[str, list[str]]: + out: dict[str, list[str]] = {} + for exc in _as_list(pack.get("exceptions")): + if isinstance(exc, dict) and exc.get("exception_id"): + out[str(exc["exception_id"])] = _strings(exc.get("allowed_output_fields")) + return out + + + def _add_code(row: dict[str, Any], code: str) -> None: + codes = row.setdefault("skeleton_validation_codes", []) + if isinstance(codes, list) and code not in codes: + codes.append(code) + + + def _append_basis(row: dict[str, Any], text: str) -> None: + basis = str(row.get("derivation_basis") or "") + row["derivation_basis"] = (basis + "; " + text).strip("; ") + + + def _apply_patch(row: dict[str, Any], decision: dict[str, Any], allowed_fields: list[str], report: dict[str, Any]) -> bool: + """X-6②: patch key가 allowed_output_fields ∩ FINAL_FIELD_SET 밖이면 decision 전체를 거부하고 + 해당 fact를 BLOCK 처리한다. '경고 후 적용'(v2 방식)은 금지.""" + patch = _as_dict(decision.get("patch")) + exception_id = str(decision.get("exception_id") or "") + allowed = set(allowed_fields) & FINAL_FIELD_SET + bad = sorted(key for key in patch if key not in allowed) + if bad: + report["rejected_patches"].append({ + "exception_id": exception_id, + "fields": bad, + "reason": "PATCH_FIELD_OUTSIDE_ALLOWED_OUTPUT_FIELDS", + }) + _add_code(row, "BLOCK_REVIEW_PRESENT") + row["must_consider"] = True + _append_basis(row, f"rejected_patch={exception_id}") + return False + for key, value in patch.items(): + row[key] = deepcopy(value) + return True + + + def _validate_refs(row: dict[str, Any], bo_ids: set[str], structure_ids: set[str], evidence_ids: set[str], report: dict[str, Any]) -> None: + if row.get("source_bo_id") not in bo_ids: + raise ValueError(f"unknown source_bo_id: {row.get('source_bo_id')}") + for sid in _strings(_as_dict(row.get("linked_structures")).get("structure_ids")): + if sid not in structure_ids: + report["row_warnings"].append({"fact_id": row.get("fact_id"), "warning": "UNKNOWN_LINKED_STRUCTURE_ID", "value": sid}) + _add_code(row, "UNKNOWN_LINKED_STRUCTURE_ID") + for eid in _strings(row.get("evidence_refs")): + if eid not in evidence_ids: + report["row_warnings"].append({"fact_id": row.get("fact_id"), "warning": "EVIDENCE_REF_GAP", "value": eid}) + _add_code(row, "EVIDENCE_REF_GAP") + + + def _normalize_final_row(row: dict[str, Any], ordinal: int) -> dict[str, Any]: + out = {field: deepcopy(row.get(field, FINAL_DEFAULTS.get(field))) for field in FINAL_FIELDS} + out["fact_id"] = f"F-{ordinal:03d}" + out["source_bo_id"] = str(out.get("source_bo_id") or "") + for key in ("parties", "evidence_refs", "legal_effect_roles", "must_not_drop_in_claim_types", "actio_roles", "downstream_module_candidates", "skeleton_validation_codes"): + out[key] = _as_list(out.get(key)) + for key in ("claim_chain_ref", "linked_structures", "state_context", "money_claim_effect", "commercial_successor_effect", "linked_actio_structures", "legal_calculation_object"): + out[key] = _as_dict(out.get(key)) + out["credibility"] = out.get("credibility") if out.get("credibility") in {"high", "medium", "low"} else "low" + out["legal_centrality"] = out.get("legal_centrality") if out.get("legal_centrality") in {"low", "medium", "critical"} else "low" + out["proof_strength"] = out.get("proof_strength") if out.get("proof_strength") in {"low", "medium", "high", "documentary", "meeting_only_core"} else "low" + if not out.get("derived_fact"): + raise ValueError(f"{out['source_bo_id']}: derived_fact is empty") + return out + + + def main() -> None: + try: + _init_mcp_session() + bundle = _load_candidate_bundle() + candidate_items = [deepcopy(x) for x in _as_list(bundle.get("candidate_items")) if isinstance(x, dict)] + exception_pack = _load_exception_pack(bundle) + adjudication = _load_adjudication() + decisions_by_id, blocked_items = _validate_adjudication_coverage(adjudication, exception_pack) + allowed_fields_by_exception = _exception_allowed_fields(exception_pack) + rows_by_fact_id = {str(row.get("fact_id")): row for row in candidate_items} + compile_gate = _as_dict(bundle.get("compile_gate")) + les_inheritance = _as_dict(_as_dict(bundle.get("les_review_inheritance")).get("by_fact_id")) + deterministic_review_items = _as_list(compile_gate.get("deterministic_review_items")) + report: dict[str, Any] = { + "schema_version": "stage1_fact_ledger_writer_report.v1", + "status": "READY", + "blocked_review_items": blocked_items, + "rejected_patches": [], + "patch_policy_warnings": [], + "unapplied_patch_fields": [], + "row_warnings": [], + "possession_conflict_gate": {"status": "PASS", "conflicts": []}, + "drafting_gates": [], + "gate_review_code_summary": {}, + "les_review_inheritance": [les_inheritance[fact_id] for fact_id in sorted(les_inheritance)], + "deterministic_review_items": deterministic_review_items, + } + for fact_id in sorted(les_inheritance): + if fact_id not in rows_by_fact_id: + raise ValueError(f"les_review_inheritance references unknown fact_id: {fact_id}") + for blocked in blocked_items: + fact_id = str(_as_dict(blocked).get("fact_id") or "") + row = rows_by_fact_id.get(fact_id) + if not row: + raise ValueError(f"blocked_review_item references unknown fact_id: {fact_id}") + _add_code(row, "BLOCK_REVIEW_PRESENT") + row["must_consider"] = True + _append_basis(row, "blocked_review=" + str(_as_dict(blocked).get("block_reason") or "unspecified")) + resolved_count = 0 + for exception_id in sorted(decisions_by_id): + decision = decisions_by_id[exception_id] + fact_id = str(decision.get("fact_id") or "") + row = rows_by_fact_id.get(fact_id) + if not row: + raise ValueError(f"decision references unknown fact_id: {fact_id}") + decision_type = decision.get("decision") + if decision_type == "KEEP_AS_IS": + resolved_count += 1 + continue + applied = _apply_patch(row, decision, allowed_fields_by_exception.get(exception_id, []), report) + if applied: + resolved_count += 1 + if decision.get("review_required"): + _add_code(row, "FL2_REVIEW_REQUIRED") + manifest = _as_dict(bundle.get("ref_manifest")) + bo_ids = set(_strings(manifest.get("bo_ids"))) + structure_ids = set(_strings(manifest.get("structure_ids"))) + evidence_ids = set(_strings(manifest.get("evidence_ids"))) + final_rows: list[dict[str, Any]] = [] + for ordinal, row in enumerate(candidate_items, start=1): + normalized = _normalize_final_row(row, ordinal) + _validate_refs(normalized, bo_ids, structure_ids, evidence_ids, report) + extra = [key for key in normalized if key not in FINAL_FIELD_SET] + if extra: + raise ValueError(f"final row has arbitrary fields: {extra}") + final_rows.append(normalized) + # conservation: 후보 BO 집합 == 최종 행 BO 집합, F-### 연속·유일. + candidate_bo_ids = sorted(str(row.get("source_bo_id") or "") for row in candidate_items) + final_bo_ids = sorted(str(row.get("source_bo_id") or "") for row in final_rows) + if candidate_bo_ids != final_bo_ids: + raise ValueError("BO conservation violated between candidate and final rows") + fact_ids = [row["fact_id"] for row in final_rows] + if fact_ids != [f"F-{i:03d}" for i in range(1, len(final_rows) + 1)]: + raise ValueError("fact_id sequence must be continuous and unique") + gate_code_rows: dict[str, list[dict[str, Any]]] = {} + for row in final_rows: + for code in _as_list(row.get("skeleton_validation_codes")): + if code in GATE_REVIEW_CODES: + gate_code_rows.setdefault(str(code), []).append(row) + conflict_rows = gate_code_rows.get("POSSESSION_CAUSE_CONFLICT", []) + report["possession_conflict_gate"] = { + "status": "HARD_WARNING" if conflict_rows else "PASS", + "conflicts": [{"fact_id": row.get("fact_id"), "source_bo_id": row.get("source_bo_id")} for row in conflict_rows], + } + notice_gate_rows = gate_code_rows.get("NOTICE_RECEIPT_UNVERIFIED", []) + if notice_gate_rows: + notice_gate = _as_dict(compile_gate.get("notice_gate")) + report["drafting_gates"].append({ + "gate": "BLOCK_FINAL_DRAFTING_NOTICE_RECEIPT_MISSING", + "row_code": "NOTICE_RECEIPT_UNVERIFIED", + "notice_ids": _strings(notice_gate.get("notice_ids")), + "affected_fact_ids": [row.get("fact_id") for row in notice_gate_rows], + }) + report["gate_review_code_summary"] = {code: [row.get("fact_id") for row in code_rows] for code, code_rows in sorted(gate_code_rows.items())} + # X-6③ status 3단 규칙: blocked/거부 patch/결정론 review/LES 승계/게이트 review 존재 → READY_WITH_REVIEW. + has_review = bool( + blocked_items + or report["rejected_patches"] + or deterministic_review_items + or les_inheritance + or report["gate_review_code_summary"] + ) + report["status"] = "READY_WITH_REVIEW" if has_review else "READY" + _write_doc(TARGET_PATH, json.dumps(final_rows, ensure_ascii=False, indent=2)) + _write_doc(REPORT_PATH, json.dumps(report, ensure_ascii=False, indent=2)) + reread = _read_doc(TARGET_PATH) + if not isinstance(reread, list) or len(reread) != len(final_rows): + raise ValueError("post-write reread validation failed: target is not the expected top-level array") + print(json.dumps({ + "status": report["status"], + "message": "F2 final fact ledger gate and writer complete", + "written_file": TARGET_PATH, + "report_file": REPORT_PATH, + "fact_count": len(final_rows), + "blocked_review_count": len(blocked_items), + "resolved_exception_count": resolved_count, + "deterministic_review_count": len(deterministic_review_items), + "les_inheritance_count": len(les_inheritance), + "drafting_gate_count": len(report["drafting_gates"]), + }, ensure_ascii=False)) + except Exception as exc: + print(json.dumps({ + "status": "FAILED", + "message": str(exc)[:500], + "written_file": TARGET_PATH, + "report_file": REPORT_PATH, + }, ensure_ascii=False)) + raise + + + if __name__ == "__main__": + main() diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_4_Codex_v3.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_4_Codex_v3.yml new file mode 100644 index 00000000..a0586631 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/Stage_1_Part_4_Codex_v3.yml @@ -0,0 +1,2197 @@ +--- +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_4 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Fact Ledger Generation + version: v2.0 + Stages: + - name: stage1_fact_ledger_generation + description: BO, Legal Effect 사용해서 Fact_Ledger_base.json 생성 + tools: + mcpServers: + localdocs: + type: streamable-http + url: http://mcp-localdocs:8012/mcp + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + + task_procedure: + IN: + nexts: + - Task_FL0_fact_source_pack_compiler + wait_until: [] + Task_FL0_fact_source_pack_compiler: + nexts: + - Task_FL1_deterministic_fact_ledger_candidate_builder + wait_until: + - IN + Task_FL1_deterministic_fact_ledger_candidate_builder: + nexts: + - Task_FL2_fact_exception_adjudicator_* + - Task_FL3_final_fact_ledger_gate_and_writer + wait_until: + - Task_FL0_fact_source_pack_compiler + Task_FL2_fact_exception_adjudicator_*: + nexts: + - Task_FL3_final_fact_ledger_gate_and_writer + wait_until: + - Task_FL1_deterministic_fact_ledger_candidate_builder + Task_FL3_final_fact_ledger_gate_and_writer: + nexts: + - OUT + wait_until: + - Task_FL1_deterministic_fact_ledger_candidate_builder + - all Task_FL2_fact_exception_adjudicator_* + OUT: + nexts: [] + wait_until: + - Task_FL3_final_fact_ledger_gate_and_writer + + tasks: + - task_name: Task_FL0_fact_source_pack_compiler + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import copy + import hashlib + import itertools + import json + import re + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _safe_error(exc: BaseException) -> str: + return re.sub(r"\s+", " ", str(exc)).strip()[:300] + + + def _parse_json_value(raw: Any, label: str, allow_trailing: bool = False) -> Any: + if isinstance(raw, (dict, list)): + return raw + if not isinstance(raw, str): + raise ValueError(f"{label} is not JSON text") + text = raw.lstrip("\ufeff").strip() + if not text: + raise ValueError(f"{label} is empty") + try: + return json.loads(text) + except json.JSONDecodeError: + try: + value, end = JSON_DECODER.raw_decode(text) + except json.JSONDecodeError as exc: + raise ValueError(f"{label} invalid JSON: {text[:300]}") from exc + if not allow_trailing and text[end:].strip(): + raise ValueError(f"{label} has trailing content: {text[end:end + 300]}") + return value + + + def _parse_mcp_response(text: str) -> dict[str, Any]: + parsed: dict[str, Any] | None = None + for line in text.strip().splitlines(): + if not line.startswith("data: "): + continue + try: + item = _parse_json_value(line[6:], "MCP SSE data", True) + except ValueError: + continue + if isinstance(item, dict): + parsed = item + if parsed is None: + item = _parse_json_value(text, "MCP response", True) + if not isinstance(item, dict): + raise RuntimeError("MCP response root is not an object") + parsed = item + return parsed + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-fact-ledger-fl0-source-compiler", + "version": "2.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_mcp_response(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP initialize failed: {payload['error']}") + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + initialized = CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ) + initialized.raise_for_status() + + + def _call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_msg_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_mcp_response(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP {name} failed: {payload['error']}") + result = payload.get("result") + if not isinstance(result, dict) or result.get("isError") is True: + raise RuntimeError(f"MCP {name} returned an invalid result") + return result + + + def _extract_doc(result: dict[str, Any], doc_name: str) -> Any: + blocks = result.get("content") or [] + if not blocks or not isinstance(blocks[0], dict): + raise RuntimeError(f"Empty response: {doc_name}") + text = blocks[0].get("text") + if not isinstance(text, str) or not text.strip(): + raise RuntimeError(f"Empty text response: {doc_name}") + outer = _parse_json_value(text, f"{doc_name} outer envelope", True) + inner: Any = outer + if isinstance(outer, dict) and "results" in outer: + results = outer.get("results") or [] + if not results or not isinstance(results[0], dict): + raise RuntimeError(f"Empty results envelope: {doc_name}") + inner = results[0].get("content") + if inner in (None, ""): + inner = results[0].get("text") + if isinstance(inner, (dict, list)): + return inner + return _parse_json_value(inner, f"{doc_name} inner document", False) + + + def _read_json(path: str) -> Any: + return _extract_doc(_call_tool("read_docs", {"doc_names": [path]}), path) + + + def _write_text(path: str, content: str) -> None: + _call_tool("write_file", {"path": path, "content": content, "overwrite": True}) + + + def _canonical(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + + def _digest(value: Any) -> str: + return "sha256:" + hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest() + + + def _write_json(path: str, value: Any) -> None: + _write_text(path, json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2)) + + + def _verify_reread(path: str, expected: Any) -> None: + actual = _read_json(path) + if _digest(actual) != _digest(expected): + raise RuntimeError(f"Post-write verification failed: {path}") + + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _strings(value: Any) -> list[str]: + values = value if isinstance(value, list) else [value] + out: list[str] = [] + for item in values: + if item is None: + continue + if isinstance(item, str): + text = item.strip() + elif isinstance(item, (int, float)) and not isinstance(item, bool): + text = str(item) + else: + continue + if text and text not in out: + out.append(text) + return out + + + def _sorted_strings(value: Any) -> list[str]: + return sorted(set(_strings(value))) + + + def _text(value: Any, limit: int = 500) -> str: + if value in (None, "", [], {}): + return "" + raw = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, sort_keys=True) + return re.sub(r"\s+", " ", raw).strip()[:limit] + + INPUT_FILES = { + "bo": "BO.json", + "structures": "legal_effect_structures.json", + "actio": "actio_case_signals.json", + "liability": "case_liability_signals.json", + "legal_effect": "legal_effect_signals.json", + "evidence": "evidence_indexed.json", + } + OUT_PATH = "stage1_tmp/fact_ledger/fact_source_pack.json" + STRUCTURE_INDEX_KEYS = ( + "by_bo_id", + "by_issue_cluster_id", + "by_asset_cluster_id", + "by_evidence_index", + "by_candidate_structure_type", + ) + DOMAIN_KEYS = ( + "money_claim_details", + "possession_state", + "lien_state", + "notice_lifecycle", + "asset_alias", + "fractional_effect_events", + ) + + + def _bo_id(bo: dict[str, Any]) -> str: + return _text(bo.get("BO_ID") or bo.get("id"), 120) + + + def _evidence_id(row: dict[str, Any]) -> str: + return _text(row.get("evidence_index") or row.get("evidence_id") or row.get("id"), 120) + + + def _sanitize(value: Any, depth: int = 0) -> Any: + if depth > 8: + return None + if isinstance(value, dict): + return {str(k): _sanitize(v, depth + 1) for k, v in sorted(value.items())} + if isinstance(value, list): + out: list[Any] = [] + seen: set[str] = set() + for item in value: + compact = _sanitize(item, depth + 1) + key = _canonical(compact) + if key not in seen: + seen.add(key) + out.append(compact) + return out + if isinstance(value, str): + return _text(value, 500) + if value is None or isinstance(value, (bool, int, float)): + return value + return _text(value, 500) + + + def _bo_evidence_refs(bo: dict[str, Any]) -> list[str]: + refs = _strings(bo.get("source_evidence_indexes")) + for row in _as_list(bo.get("Evidence")): + if isinstance(row, dict): + refs.extend(_strings(row.get("evidence_index"))) + return sorted(set(refs)) + + + def _compact_bo(bo: dict[str, Any]) -> dict[str, Any]: + core = _as_dict(bo.get("core_field_base")) + payload = _as_dict(_as_dict(bo.get("extensions")).get("domain_payload")) + domain = {key: _sanitize(payload.get(key)) for key in DOMAIN_KEYS if payload.get(key) not in (None, {}, [])} + juristic = bo.get("JuristicAct") + return { + "bo_id": _bo_id(bo), + "bo_type": bo.get("BOType") or bo.get("bo_type"), + "action": bo.get("Action") or bo.get("action"), + "action_type": bo.get("ActionType") or bo.get("action_type"), + "juristic_act": _sanitize(juristic), + "reason": _text(bo.get("Reason") or bo.get("reason"), 500) or None, + "prior_act": _sanitize(bo.get("PriorAct") or bo.get("prior_act")), + "legal_keywords": _sorted_strings(bo.get("Legal_Keywords") or bo.get("legal_keywords")), + "amount": _sanitize(bo.get("amount") if bo.get("amount") is not None else core.get("Amount")), + "date": core.get("BehaviorTime") or bo.get("date"), + "core_field_base": { + key: _sanitize(core.get(key)) + for key in ("BehaviorTime", "Performer", "Subject", "Object", "Amount", "Action") + }, + "source_evidence_indexes": _bo_evidence_refs(bo), + "domain_payload_compact": domain, + } + + + def _compact_structure(row: dict[str, Any]) -> dict[str, Any]: + audit = _as_dict(row.get("routing_audit")) + return { + "structure_id": _text(row.get("structure_id"), 120), + "candidate_structure_type": _text(row.get("candidate_structure_type"), 120) or None, + "candidate_structure_types": _sorted_strings(audit.get("candidate_structure_types")), + "structure_label": _text(row.get("structure_label"), 300), + "source_bo_ids": _sorted_strings(row.get("source_bo_ids")), + "issue_cluster_ids": _sorted_strings(row.get("issue_cluster_ids")), + "asset_cluster_ids": _sorted_strings(row.get("asset_cluster_ids")), + "evidence_indexes": _sorted_strings(row.get("evidence_indexes")), + "linked_claim_group_ids": _sorted_strings(row.get("linked_claim_group_ids")), + "linked_liability_group_ids": _sorted_strings(row.get("linked_liability_group_ids")), + "linked_actio_signal_ids": _sorted_strings(row.get("linked_actio_signal_ids")), + "legal_effect_roles": _sorted_strings(row.get("legal_effect_roles")), + "review_required": row.get("review_required") is True, + "routing_audit": { + "blocked_exception_ids": _sorted_strings(audit.get("blocked_exception_ids")), + "deterministic_review_ids": _sorted_strings(audit.get("deterministic_review_ids")), + }, + } + + + def _compact_evidence(row: dict[str, Any]) -> dict[str, Any]: + def compact_values(value: Any) -> list[str]: + out: list[str] = [] + for item in _as_list(value): + text = _text(item, 500) + if text and text not in out: + out.append(text) + return out + + return { + "evidence_index": _evidence_id(row), + "title": _text(row.get("title") or row.get("source_title"), 500), + "doc_type": _text(row.get("doc_type"), 160) or None, + "key_facts": compact_values(row.get("key_facts")), + "key_dates": compact_values(row.get("key_dates")), + "key_amounts": compact_values(row.get("key_amounts")), + "source_pointer": _sanitize(row.get("source_pointer")) if isinstance(row.get("source_pointer"), dict) else {}, + "authentication": _sanitize(row.get("authentication") or row.get("authentication_status")), + } + + + def _signal_rows(doc: dict[str, Any], root: str, schema: str) -> list[dict[str, Any]]: + if doc.get("schema_version") != schema: + raise ValueError(f"{root} schema_version mismatch") + if doc.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError(f"{root} status invalid") + rows = doc.get(root) + if not isinstance(rows, list): + raise ValueError(f"{root} must be an array") + return [row for row in rows if isinstance(row, dict)] + + + def _compact_signal(row: dict[str, Any], signal_type: str) -> dict[str, Any]: + return { + "signal_type": signal_type, + "signal_id": _text(row.get("signal_id") or row.get("id"), 120) or None, + "candidate_structure_types": _sorted_strings(row.get("candidate_structure_types") or row.get("candidate_structure_type")), + "linked_structure_ids": _sorted_strings(row.get("linked_structure_ids")), + "related_evidence_indexes": _sorted_strings(row.get("related_evidence_indexes") or row.get("evidence_indexes")), + "party_roles": _sanitize(_as_list(row.get("party_roles"))), + "actio_role_hints": _sanitize(_as_list(row.get("actio_role_hints"))), + "claim_group_candidates": _sanitize(_as_list(row.get("claim_group_candidates")) + _strings(row.get("claim_group_id"))), + "liability_group_candidates": _sanitize(_as_list(row.get("liability_group_candidates")) + _strings(row.get("liability_group_id"))), + } + + + def _signal_bo_ids(row: dict[str, Any], signal_type: str) -> list[str]: + if signal_type == "legal_effect": + return _strings(row.get("bo_id") or row.get("related_bo_ids")) + return _strings(row.get("related_bo_ids") or row.get("source_bo_ids") or row.get("bo_id")) + + + def _review_for_bo(queue_item: dict[str, Any], bo_id: str) -> dict[str, Any]: + raw_type = _text(queue_item.get("review_type"), 120) + if raw_type == "blocked_exception": + review_type = "blocked_exception" + elif raw_type in {"deterministic_review", "legal_structure_deterministic_review"}: + review_type = "deterministic_review" + elif raw_type in {"review_sensitive_split", "split_review"}: + review_type = "review_sensitive_split" + else: + review_type = "structure_review" + structure_ids = _strings(queue_item.get("structure_ids") or queue_item.get("structure_id")) + return { + "review_type": review_type, + "review_id": _text(queue_item.get("review_id"), 120) or None, + "exception_id": _text(queue_item.get("exception_id"), 120) or None, + "review_code": _text(queue_item.get("review_code"), 160) or None, + "reason_code": _text(queue_item.get("reason_code") or queue_item.get("block_reason"), 160) or None, + "downstream_owner": _text(queue_item.get("downstream_owner"), 80) or ("HUMAN_REVIEW" if review_type == "blocked_exception" else "STAGE_2"), + "structure_ids": sorted(set(structure_ids)), + "blocking": review_type == "blocked_exception" or queue_item.get("blocking") is True, + "source_bo_id": bo_id, + } + + + def main() -> None: + _init_mcp_session() + docs = {key: _read_json(path) for key, path in INPUT_FILES.items()} + input_digests = {path: _digest(docs[key]) for key, path in INPUT_FILES.items()} + run_fingerprint = _digest({"contract": "stage1_fact_ledger_run.v3", "input_digests": input_digests}) + + bo_rows = docs["bo"] + if not isinstance(bo_rows, list) or not bo_rows: + raise ValueError("BO.json must be a non-empty array") + bo_by_id: dict[str, Any] = {} + for row in bo_rows: + if not isinstance(row, dict): + raise ValueError("BO.json contains a non-object item") + bo_id = _bo_id(row) + if not bo_id or bo_id in bo_by_id: + raise ValueError(f"BO ID missing or duplicated: {bo_id}") + bo_by_id[bo_id] = _compact_bo(row) + bo_ids = set(bo_by_id) + + structures_doc = _as_dict(docs["structures"]) + if structures_doc.get("schema_version") != "stage1_legal_effect_structures.v1": + raise ValueError("legal_effect_structures schema_version mismatch") + if structures_doc.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("legal_effect_structures status invalid") + structures_raw = structures_doc.get("legal_effect_structures") + indexes = structures_doc.get("structure_index") + quality_gate = structures_doc.get("quality_gate") + if not isinstance(structures_raw, list) or not isinstance(indexes, dict) or not isinstance(quality_gate, dict): + raise ValueError("legal_effect_structures required roots missing") + for key in STRUCTURE_INDEX_KEYS: + if not isinstance(indexes.get(key), dict): + raise ValueError(f"structure_index.{key} missing") + for gate_name in ("bo_coverage", "evidence_coverage"): + if _as_dict(quality_gate.get(gate_name)).get("coverage_pass") is not True: + raise ValueError(f"quality_gate.{gate_name} failed") + + structure_by_id: dict[str, Any] = {} + expected_by_bo: dict[str, list[str]] = {bo_id: [] for bo_id in bo_ids} + for raw in structures_raw: + if not isinstance(raw, dict): + raise ValueError("legal_effect_structures contains a non-object item") + compact = _compact_structure(raw) + sid = compact["structure_id"] + if not sid or sid in structure_by_id: + raise ValueError(f"structure ID missing or duplicated: {sid}") + unknown_bos = sorted(set(compact["source_bo_ids"]) - bo_ids) + if unknown_bos: + raise ValueError(f"structure {sid} has unknown BO refs: {unknown_bos}") + structure_by_id[sid] = compact + for bo_id in compact["source_bo_ids"]: + expected_by_bo[bo_id].append(sid) + + by_bo = _as_dict(indexes.get("by_bo_id")) + for bo_id in sorted(bo_ids): + indexed = _sorted_strings(_as_dict(by_bo.get(bo_id)).get("structure_ids")) + expected = sorted(set(expected_by_bo.get(bo_id, []))) + if indexed != expected: + raise ValueError(f"structure_index.by_bo_id mismatch: {bo_id}") + + evidence_doc = _as_dict(docs["evidence"]) + evidence_rows = evidence_doc.get("items") or evidence_doc.get("evidence_indexed") + if not isinstance(evidence_rows, list) or not evidence_rows: + raise ValueError("evidence_indexed conservation array missing") + evidence_by_id: dict[str, Any] = {} + for raw in evidence_rows: + if not isinstance(raw, dict): + raise ValueError("evidence_indexed contains a non-object item") + compact = _compact_evidence(raw) + eid = compact["evidence_index"] + if not eid or eid in evidence_by_id: + raise ValueError(f"evidence ID missing or duplicated: {eid}") + evidence_by_id[eid] = compact + evidence_ids = set(evidence_by_id) + for sid, structure in structure_by_id.items(): + unknown = sorted(set(structure["evidence_indexes"]) - evidence_ids) + if unknown: + raise ValueError(f"structure {sid} has unknown evidence refs: {unknown}") + + signal_specs = ( + ("actio", _as_dict(docs["actio"]), "actio_case_signals", "actio_case_signals.v1"), + ("case_liability", _as_dict(docs["liability"]), "case_liability_signals", "case_liability_signals.v1"), + ("legal_effect", _as_dict(docs["legal_effect"]), "bo_legal_effect_routes", "legal_effect_signals.v1"), + ) + signal_hints: dict[str, dict[str, list[dict[str, Any]]]] = { + bo_id: {"actio": [], "case_liability": [], "legal_effect": []} for bo_id in bo_ids + } + source_schema_versions = { + "BO.json": "array", + "legal_effect_structures.json": structures_doc.get("schema_version"), + "evidence_indexed.json": evidence_doc.get("schema_contract_version") or evidence_doc.get("schema_version") or "conservation-array", + } + for signal_type, doc, root, schema in signal_specs: + rows = _signal_rows(doc, root, schema) + source_schema_versions[INPUT_FILES[{"actio": "actio", "case_liability": "liability", "legal_effect": "legal_effect"}[signal_type]]] = schema + for raw in rows: + compact = _compact_signal(raw, signal_type) + for bo_id in _signal_bo_ids(raw, signal_type): + if bo_id in signal_hints: + signal_hints[bo_id][signal_type].append(compact) + for bo_id in signal_hints: + for signal_type in signal_hints[bo_id]: + signal_hints[bo_id][signal_type].sort(key=_canonical) + + structure_refs_by_bo: dict[str, Any] = {} + for bo_id in sorted(bo_ids): + row = _as_dict(by_bo.get(bo_id)) + structure_refs_by_bo[bo_id] = { + "structure_ids": _sorted_strings(row.get("structure_ids")), + "issue_cluster_ids": _sorted_strings(row.get("issue_cluster_ids")), + "asset_cluster_ids": _sorted_strings(row.get("asset_cluster_ids")), + "evidence_index_ids": _sorted_strings(row.get("evidence_indexes")), + "candidate_structure_types": _sorted_strings(row.get("candidate_structure_types")), + "claim_group_ids": _sorted_strings(row.get("claim_group_ids")), + "liability_group_ids": _sorted_strings(row.get("liability_group_ids")), + "downstream_fact_roles": _sorted_strings(row.get("legal_effect_roles") or row.get("downstream_fact_roles")), + } + + upstream_reviews: dict[str, list[dict[str, Any]]] = {bo_id: [] for bo_id in bo_ids} + queue = _as_list(quality_gate.get("review_queue")) + for raw in queue: + if not isinstance(raw, dict): + continue + for bo_id in _strings(raw.get("source_bo_ids") or raw.get("source_bo_id")): + if bo_id in upstream_reviews: + upstream_reviews[bo_id].append(_review_for_bo(raw, bo_id)) + for sid, structure in structure_by_id.items(): + if structure.get("review_required") is not True: + continue + for bo_id in structure["source_bo_ids"]: + upstream_reviews[bo_id].append(_review_for_bo({"review_type": "structure_review", "structure_id": sid}, bo_id)) + for bo_id, rows in upstream_reviews.items(): + unique = {_canonical(row): row for row in rows} + upstream_reviews[bo_id] = sorted(unique.values(), key=lambda row: (_text(row.get("review_type")), _text(row.get("review_id") or row.get("exception_id")), _canonical(row))) + + warnings: list[dict[str, Any]] = [] + for bo_id, bo in bo_by_id.items(): + for eid in bo["source_evidence_indexes"]: + if eid not in evidence_ids: + warnings.append({"code": "UNKNOWN_BO_EVIDENCE_REF", "source_bo_id": bo_id, "evidence_index": eid}) + review_count = sum(len(rows) for rows in upstream_reviews.values()) + blocking_count = sum(1 for rows in upstream_reviews.values() for row in rows if row.get("blocking") is True) + status = "READY_WITH_REVIEW" if review_count or warnings else "READY" + source_pack = { + "schema_version": "stage1_fact_source_pack.v3", + "status": status, + "run_fingerprint": run_fingerprint, + "input_manifest": {"files": input_digests, "source_schema_versions": source_schema_versions}, + "bo_by_id": {bo_id: bo_by_id[bo_id] for bo_id in sorted(bo_by_id)}, + "structure_by_id": {sid: structure_by_id[sid] for sid in sorted(structure_by_id)}, + "structure_refs_by_bo_id": structure_refs_by_bo, + "signal_hints_by_bo_id": {bo_id: signal_hints[bo_id] for bo_id in sorted(signal_hints)}, + "evidence_authority_by_index": {eid: evidence_by_id[eid] for eid in sorted(evidence_by_id)}, + "upstream_review_by_bo_id": {bo_id: upstream_reviews[bo_id] for bo_id in sorted(upstream_reviews)}, + "reference_universe": { + "bo_ids": sorted(bo_ids), + "structure_ids": sorted(structure_by_id), + "evidence_indexes": sorted(evidence_ids), + }, + "source_integrity": { + "warnings": sorted(warnings, key=_canonical), + "warning_count": len(warnings), + "reference_validation_pass": True, + }, + "source_counts": { + "bo_count": len(bo_by_id), + "structure_count": len(structure_by_id), + "evidence_count": len(evidence_by_id), + "review_count": review_count, + "blocking_review_count": blocking_count, + }, + } + payload = {"fact_source_pack": source_pack} + _write_json(OUT_PATH, payload) + _verify_reread(OUT_PATH, payload) + print(_canonical({"fact_source_pack_manifest": { + "schema_version": "stage1_fact_source_pack_manifest.v2", + "status": status, + "path": OUT_PATH, + "sha256": _digest(payload), + "run_fingerprint": run_fingerprint, + "bo_count": len(bo_by_id), + "structure_count": len(structure_by_id), + "evidence_count": len(evidence_by_id), + "review_count": review_count, + }})) + + + try: + main() + except Exception as exc: + print(_canonical({"fact_source_pack_manifest": { + "schema_version": "stage1_fact_source_pack_manifest.v2", + "status": "FAILED", + "error": _safe_error(exc), + }})) + raise + + - task_name: Task_FL1_deterministic_fact_ledger_candidate_builder + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import copy + import hashlib + import itertools + import json + import re + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _safe_error(exc: BaseException) -> str: + return re.sub(r"\s+", " ", str(exc)).strip()[:300] + + + def _parse_json_value(raw: Any, label: str, allow_trailing: bool = False) -> Any: + if isinstance(raw, (dict, list)): + return raw + if not isinstance(raw, str): + raise ValueError(f"{label} is not JSON text") + text = raw.lstrip("\ufeff").strip() + if not text: + raise ValueError(f"{label} is empty") + try: + return json.loads(text) + except json.JSONDecodeError: + try: + value, end = JSON_DECODER.raw_decode(text) + except json.JSONDecodeError as exc: + raise ValueError(f"{label} invalid JSON: {text[:300]}") from exc + if not allow_trailing and text[end:].strip(): + raise ValueError(f"{label} has trailing content: {text[end:end + 300]}") + return value + + + def _parse_mcp_response(text: str) -> dict[str, Any]: + parsed: dict[str, Any] | None = None + for line in text.strip().splitlines(): + if not line.startswith("data: "): + continue + try: + item = _parse_json_value(line[6:], "MCP SSE data", True) + except ValueError: + continue + if isinstance(item, dict): + parsed = item + if parsed is None: + item = _parse_json_value(text, "MCP response", True) + if not isinstance(item, dict): + raise RuntimeError("MCP response root is not an object") + parsed = item + return parsed + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-fact-ledger-fl1-candidate-builder", + "version": "2.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_mcp_response(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP initialize failed: {payload['error']}") + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + initialized = CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ) + initialized.raise_for_status() + + + def _call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_msg_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_mcp_response(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP {name} failed: {payload['error']}") + result = payload.get("result") + if not isinstance(result, dict) or result.get("isError") is True: + raise RuntimeError(f"MCP {name} returned an invalid result") + return result + + + def _extract_doc(result: dict[str, Any], doc_name: str) -> Any: + blocks = result.get("content") or [] + if not blocks or not isinstance(blocks[0], dict): + raise RuntimeError(f"Empty response: {doc_name}") + text = blocks[0].get("text") + if not isinstance(text, str) or not text.strip(): + raise RuntimeError(f"Empty text response: {doc_name}") + outer = _parse_json_value(text, f"{doc_name} outer envelope", True) + inner: Any = outer + if isinstance(outer, dict) and "results" in outer: + results = outer.get("results") or [] + if not results or not isinstance(results[0], dict): + raise RuntimeError(f"Empty results envelope: {doc_name}") + inner = results[0].get("content") + if inner in (None, ""): + inner = results[0].get("text") + if isinstance(inner, (dict, list)): + return inner + return _parse_json_value(inner, f"{doc_name} inner document", False) + + + def _read_json(path: str) -> Any: + return _extract_doc(_call_tool("read_docs", {"doc_names": [path]}), path) + + + def _write_text(path: str, content: str) -> None: + _call_tool("write_file", {"path": path, "content": content, "overwrite": True}) + + + def _canonical(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + + def _digest(value: Any) -> str: + return "sha256:" + hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest() + + + def _write_json(path: str, value: Any) -> None: + _write_text(path, json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2)) + + + def _verify_reread(path: str, expected: Any) -> None: + actual = _read_json(path) + if _digest(actual) != _digest(expected): + raise RuntimeError(f"Post-write verification failed: {path}") + + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _strings(value: Any) -> list[str]: + values = value if isinstance(value, list) else [value] + out: list[str] = [] + for item in values: + if item is None: + continue + if isinstance(item, str): + text = item.strip() + elif isinstance(item, (int, float)) and not isinstance(item, bool): + text = str(item) + else: + continue + if text and text not in out: + out.append(text) + return out + + + def _sorted_strings(value: Any) -> list[str]: + return sorted(set(_strings(value))) + + + def _text(value: Any, limit: int = 500) -> str: + if value in (None, "", [], {}): + return "" + raw = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, sort_keys=True) + return re.sub(r"\s+", " ", raw).strip()[:limit] + + SOURCE_PACK_PATH = "stage1_tmp/fact_ledger/fact_source_pack.json" + CANDIDATE_PATH = "stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json" + EXCEPTION_MANIFEST_PATH = "stage1_tmp/fact_ledger/fact_exception_manifest.json" + INPUT_PART_DIR = "stage1_tmp/fact_ledger/fact_exception_input_parts" + OUTPUT_PART_DIR = "stage1_tmp/fact_ledger/fact_exception_adjudication_parts" + MAX_EXCEPTION_ITEMS_PER_PACK = 4 + TARGET_EXCEPTION_ITEMS_PER_PACK = 3 + MAX_EXCEPTION_ITEM_BYTES = 12000 + MAX_EXCEPTION_PACK_BYTES = 25000 + MAX_EXCEPTION_PACKS = 8 + MAX_CONCURRENCY = 2 + FINAL_FIELDS = [ + "fact_id", "source_bo_id", "type", "date", "parties", "object_spec", "amount", "action", + "evidence_refs", "credibility", "legal_centrality", "proof_strength", "legal_effect_roles", + "claim_chain_ref", "linked_structures", "must_not_drop_in_claim_types", "must_consider", + "state_context", "money_claim_effect", "commercial_successor_effect", "actio_roles", + "linked_actio_structures", "downstream_module_candidates", "skeleton_validation_codes", + "derived_fact", "derivation_basis", "legal_calculation_object", + ] + FINAL_FIELD_SET = set(FINAL_FIELDS) + STRUCTURE_TYPE_TO_MODULE = { + "money_claim": "money_claim", + "commercial_successor": "commercial_successor", + "secured_debt": "secured_registry", + "registry_invalidity": "secured_registry", + "land_use_gain": "land_valuation", + "valuation": "land_valuation", + "fraudulent_transfer": "actio", + "preserved_claim": "actio", + "succession_notice_lien_asset_defense": "succession_notice_lien_asset_defense", + "general_legal_effect": "legal_effect", + } + STRUCTURE_TYPE_TO_ROLE = { + "money_claim": "claim_chain_ref", + "commercial_successor": "claim_chain_ref", + "secured_debt": "linked_structures", + "registry_invalidity": "linked_structures", + "land_use_gain": "legal_calculation_object", + "valuation": "legal_calculation_object", + "fraudulent_transfer": "actio_roles", + "preserved_claim": "actio_roles", + "succession_notice_lien_asset_defense": "linked_structures", + "general_legal_effect": "legal_effect_roles", + } + CLOSED_EXCEPTION_TYPES = { + "conflicting_fact_projection", + "conflicting_calculation_projection", + "conflicting_asset_projection", + "conflicting_structure_role_projection", + } + CLOSED_TARGET_FIELDS = { + "date", "amount", "action", "object_spec", "derived_fact", "claim_chain_ref", + "linked_structures", "state_context", "legal_calculation_object", + } + + + def _first(*values: Any) -> Any: + for value in values: + if value not in (None, "", [], {}): + return value + return None + + + def _add_code(row: dict[str, Any], code: str) -> None: + codes = _strings(row.get("skeleton_validation_codes")) + if code not in codes: + codes.append(code) + row["skeleton_validation_codes"] = sorted(codes) + + + def _score_evidence(rows: list[dict[str, Any]]) -> tuple[str, str, str]: + if not rows: + return "low", "low", "NO_EVIDENCE" + texts = " ".join( + " ".join([_text(row.get("title"), 200), _text(row.get("doc_type"), 100)] + _strings(row.get("key_facts"))) + for row in rows + ).lower() + meeting = any(marker in texts for marker in ("meeting", "회의", "면담")) + documentary = any(marker in texts for marker in ("등기", "계약", "판결", "결정", "공문", "금융", "송금", "계좌", "registry", "contract", "judgment")) + has_structured_fact = any(_as_list(row.get("key_facts")) for row in rows) + has_pointer = any(isinstance(row.get("source_pointer"), dict) and row.get("source_pointer") for row in rows) + if meeting and not documentary and all("meeting" in _text(row.get("doc_type"), 100).lower() or "회의" in _text(row.get("doc_type"), 100) or "면담" in _text(row.get("doc_type"), 100) for row in rows): + return "medium", "meeting_only_core", "MEETING_ONLY" + if documentary and has_structured_fact and has_pointer: + return "high", "documentary", "DOCUMENTARY_STRUCTURED" + if len(rows) >= 2 and has_structured_fact: + return "high", "high", "MULTI_SOURCE_STRUCTURED" + return "medium", "medium", "SINGLE_STRUCTURED_SOURCE" + + + def _linked_view(linked: dict[str, Any]) -> dict[str, Any]: + return { + "structure_ids": _sorted_strings(linked.get("structure_ids")), + "issue_cluster_ids": _sorted_strings(linked.get("issue_cluster_ids")), + "asset_cluster_ids": _sorted_strings(linked.get("asset_cluster_ids")), + "evidence_index_ids": _sorted_strings(linked.get("evidence_index_ids")), + "candidate_structure_types": _sorted_strings(linked.get("candidate_structure_types")), + "claim_group_ids": _sorted_strings(linked.get("claim_group_ids")), + "liability_group_ids": _sorted_strings(linked.get("liability_group_ids")), + } + + + def _legal_roles(types: list[str], structures: list[dict[str, Any]], signals: dict[str, Any]) -> list[str]: + roles: list[str] = [] + for structure in structures: + roles.extend(_strings(structure.get("legal_effect_roles"))) + roles.extend(STRUCTURE_TYPE_TO_ROLE.get(item, "legal_effect_roles") for item in types) + if _as_list(signals.get("actio")): + roles.append("actio_roles") + if _as_list(signals.get("case_liability")): + roles.append("claim_chain_ref") + if _as_list(signals.get("legal_effect")): + roles.append("legal_effect_roles") + return sorted(set(roles)) + + + def _modules(types: list[str], signals: dict[str, Any]) -> list[str]: + modules = [STRUCTURE_TYPE_TO_MODULE.get(item, "legal_effect") for item in types] + if _as_list(signals.get("actio")): + modules.append("actio") + if _as_list(signals.get("case_liability")): + modules.append("claim_liability") + return sorted(set(modules)) + + + def _parties(bo: dict[str, Any], signals: dict[str, Any]) -> list[dict[str, Any]]: + core = _as_dict(bo.get("core_field_base")) + out: list[dict[str, Any]] = [] + for role, field in (("actor", "Performer"), ("counterparty", "Subject")): + value = _text(core.get(field), 160) + if value: + out.append({"party": value, "role": role, "source": "BO.core_field_base"}) + for signal in _as_list(signals.get("case_liability")): + for item in _as_list(_as_dict(signal).get("party_roles")): + if not isinstance(item, dict): + continue + party = _text(item.get("party") or item.get("name"), 160) + if party: + out.append({"party": party, "role": _text(item.get("role"), 80), "source": "case_liability_signals"}) + unique = {(row["party"], row["role"]): row for row in out} + return [unique[key] for key in sorted(unique)] + + + def _claim_groups(linked: dict[str, Any], signals: dict[str, Any]) -> dict[str, Any]: + claims = _strings(linked.get("claim_group_ids")) + liabilities = _strings(linked.get("liability_group_ids")) + for signal in _as_list(signals.get("case_liability")): + claims.extend(_strings(_as_dict(signal).get("claim_group_candidates"))) + liabilities.extend(_strings(_as_dict(signal).get("liability_group_candidates"))) + return { + "claim_group_ids": sorted(set(claims)), + "liability_group_ids": sorted(set(liabilities)), + "signal_count": len(_as_list(signals.get("case_liability"))), + } + + + def _fallback_derived(bo: dict[str, Any], row: dict[str, Any]) -> str: + if _text(row.get("action"), 900): + return _text(row.get("action"), 900) + core = _as_dict(bo.get("core_field_base")) + fields = [core.get(key) for key in ("BehaviorTime", "Performer", "Subject", "Object", "Action", "Amount")] + parts = [_text(value, 240) for value in fields] + return " ".join(value for value in parts if value).strip() + + + def _review_template(code: str, row: dict[str, Any], reason: str, severity: str = "WARNING", owner: str = "STAGE_2") -> dict[str, Any]: + return { + "review_id": None, + "review_code": code, + "severity": severity, + "fact_id": row["fact_id"], + "source_bo_id": row["source_bo_id"], + "structure_ids": _strings(_as_dict(row.get("linked_structures")).get("structure_ids")), + "evidence_refs": _strings(row.get("evidence_refs")), + "reason_code": reason, + "downstream_owner": owner, + } + + + def _build_row(index: int, bo_id: str, source: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]], list[dict[str, Any]], str]: + bo = _as_dict(_as_dict(source.get("bo_by_id")).get(bo_id)) + linked = _linked_view(_as_dict(_as_dict(source.get("structure_refs_by_bo_id")).get(bo_id))) + signals = _as_dict(_as_dict(source.get("signal_hints_by_bo_id")).get(bo_id)) + structure_map = _as_dict(source.get("structure_by_id")) + structures = [_as_dict(structure_map.get(sid)) for sid in linked["structure_ids"] if isinstance(structure_map.get(sid), dict)] + types = sorted(set(linked["candidate_structure_types"] + [str(row.get("candidate_structure_type")) for row in structures if row.get("candidate_structure_type")])) + evidence_refs = sorted(set(_strings(bo.get("source_evidence_indexes")) + linked["evidence_index_ids"])) + evidence_map = _as_dict(source.get("evidence_authority_by_index")) + evidence_rows = [_as_dict(evidence_map.get(eid)) for eid in evidence_refs if isinstance(evidence_map.get(eid), dict)] + credibility, proof_strength, score_basis = _score_evidence(evidence_rows) + core = _as_dict(bo.get("core_field_base")) + modules = _modules(types, signals) + row = { + "fact_id": f"F-{index:03d}", + "source_bo_id": bo_id, + "type": _first(bo.get("bo_type"), types[0] if types else None), + "date": _first(core.get("BehaviorTime"), bo.get("date")), + "parties": _parties(bo, signals), + "object_spec": _first(core.get("Object"), json.dumps(linked["asset_cluster_ids"], ensure_ascii=False, sort_keys=True)), + "amount": _text(_first(bo.get("amount"), core.get("Amount")), 500) or None, + "action": _first(bo.get("action"), core.get("Action")), + "evidence_refs": evidence_refs, + "credibility": credibility, + "legal_centrality": "critical" if linked["structure_ids"] or _as_list(signals.get("actio")) else "medium", + "proof_strength": proof_strength, + "legal_effect_roles": _legal_roles(types, structures, signals), + "claim_chain_ref": _claim_groups(linked, signals), + "linked_structures": linked, + "must_not_drop_in_claim_types": modules if linked["structure_ids"] or _as_list(signals.get("actio")) else [], + "must_consider": bool(linked["structure_ids"] or _as_list(signals.get("actio"))), + "state_context": {"bo_type": bo.get("bo_type"), "issue_cluster_ids": linked["issue_cluster_ids"], "asset_cluster_ids": linked["asset_cluster_ids"]}, + "money_claim_effect": {"enabled": "money_claim" in types, "source_bo_id": bo_id if "money_claim" in types else None}, + "commercial_successor_effect": {"enabled": "commercial_successor" in types, "source_bo_id": bo_id if "commercial_successor" in types else None}, + "actio_roles": copy.deepcopy(_as_list(signals.get("actio"))), + "linked_actio_structures": { + "structure_ids": sorted(s["structure_id"] for s in structures if s.get("candidate_structure_type") in {"fraudulent_transfer", "preserved_claim"}), + "actio_signal_count": len(_as_list(signals.get("actio"))), + }, + "downstream_module_candidates": modules, + "skeleton_validation_codes": [], + "derived_fact": "", + "derivation_basis": "", + "legal_calculation_object": {}, + } + needs_calc = any(item in {"money_claim", "secured_debt", "land_use_gain", "valuation", "fraudulent_transfer"} for item in types) + row["legal_calculation_object"] = { + "calculation_required": needs_calc, + "amount_source": "BO.amount" if row.get("amount") else None, + "amount_text": row.get("amount"), + "object_basis": row.get("object_spec"), + "candidate_structure_types": types, + } + row["derived_fact"] = _fallback_derived(bo, row) + basis = [f"source_bo_id={bo_id}"] + if evidence_refs: + basis.append("evidence_refs=" + ",".join(evidence_refs)) + if linked["structure_ids"]: + basis.append("structure_ids=" + ",".join(linked["structure_ids"])) + row["derivation_basis"] = "; ".join(basis) + reviews: list[dict[str, Any]] = [] + if not row["derived_fact"]: + _add_code(row, "EMPTY_DERIVED_FACT") + _add_code(row, "EMPTY_DERIVED_FACT_AFTER_FALLBACK") + row["must_consider"] = True + reviews.append(_review_template("EMPTY_DERIVED_FACT_AFTER_FALLBACK", row, "NO_SOURCE_BACKED_FALLBACK")) + if not evidence_refs: + _add_code(row, "NO_EVIDENCE_REFS") + if not linked["structure_ids"]: + _add_code(row, "NO_LINKED_LEGAL_EFFECT_STRUCTURE") + if needs_calc and not row.get("amount") and not row.get("object_spec"): + _add_code(row, "CALCULATION_BASIS_MISSING_STAGE2") + reviews.append(_review_template("CALCULATION_BASIS_MISSING_STAGE2", row, "CALCULATION_SOURCE_DEFERRED")) + + upstream = _as_list(_as_dict(source.get("upstream_review_by_bo_id")).get(bo_id)) + for item in upstream: + review = _as_dict(item) + review_type = review.get("review_type") + if review_type == "blocked_exception": + code, severity, owner = "LES_BLOCKED_EXCEPTION_REVIEW", "BLOCK_FINAL_DRAFTING", "HUMAN_REVIEW" + elif review_type == "deterministic_review": + code, severity, owner = "LES_DETERMINISTIC_REVIEW", "WARNING", "STAGE_2" + elif review_type == "review_sensitive_split": + code, severity, owner = "LES_REVIEW_SENSITIVE_SPLIT", "WARNING", "STAGE_2" + else: + code, severity, owner = "LES_REVIEW_REQUIRED", "WARNING", "STAGE_2" + _add_code(row, code) + row["must_consider"] = True + reviews.append(_review_template(code, row, _text(review.get("reason_code") or review_type, 160), severity, owner)) + return row, reviews, evidence_rows, score_basis + + + def _clean_date(value: Any) -> str: + return _text(value, 40) + + + def _periods_overlap(a_start: str, a_end: str, b_start: str, b_end: str) -> bool: + a1, a2 = a_start or "0000-00-00", a_end or "9999-99-99" + b1, b2 = b_start or "0000-00-00", b_end or "9999-99-99" + return max(a1, b1) <= min(a2, b2) + + + def _domain_scans(rows: list[dict[str, Any]], source: dict[str, Any]) -> list[dict[str, Any]]: + reviews: list[dict[str, Any]] = [] + bo_map = _as_dict(source.get("bo_by_id")) + possession_entries: list[tuple[dict[str, Any], set[str], str, str, list[dict[str, Any]]]] = [] + receipt_by_notice: dict[str, str] = {} + for row in rows: + payload = _as_dict(_as_dict(bo_map.get(row["source_bo_id"])).get("domain_payload_compact")) + possession = _as_dict(payload.get("possession_state")) + events = [item for item in _as_list(possession.get("possession_events")) if isinstance(item, dict)] + causes = [item for item in _as_list(possession.get("legal_cause_assertions")) if isinstance(item, dict)] + assets = set(_strings(possession.get("asset_ref")) + _strings(_as_dict(row.get("state_context")).get("asset_cluster_ids"))) + if events and not causes: + _add_code(row, "LEGAL_CAUSE_STATE_UNKNOWN_REVIEW") + reviews.append(_review_template("LEGAL_CAUSE_STATE_UNKNOWN_REVIEW", row, "POSSESSION_WITHOUT_LEGAL_CAUSE")) + cause_names = {_text(item.get("cause_asserted"), 120) for item in causes if _text(item.get("cause_asserted"), 120)} + conflict = any("무권원" in left and any(term in right for term in ("유치권", "임대차", "사용대차")) for left in cause_names for right in cause_names if left != right) + if conflict: + _add_code(row, "POSSESSION_CAUSE_CONFLICT") + reviews.append(_review_template("POSSESSION_CAUSE_CONFLICT", row, "INCOMPATIBLE_LEGAL_CAUSE_ASSERTIONS", "HARD_WARNING", "HUMAN_REVIEW")) + for event in events: + possession_entries.append((row, assets, _clean_date(event.get("start_date")), _clean_date(event.get("end_date")), causes)) + + notice = _as_dict(payload.get("notice_lifecycle")) + notice_id = _text(notice.get("notice_id"), 120) + received = _clean_date(notice.get("received_date")) or _clean_date(notice.get("arrival_date")) + if notice_id and received: + receipt_by_notice[notice_id] = min(received, receipt_by_notice.get(notice_id, "9999-99-99")) + if notice.get("requires_receipt_for_effect") is True and not received: + _add_code(row, "NOTICE_RECEIPT_UNVERIFIED") + reviews.append(_review_template("NOTICE_RECEIPT_UNVERIFIED", row, "RECEIPT_DATE_REQUIRED", "BLOCK_FINAL_DRAFTING", "HUMAN_REVIEW")) + if notice.get("requires_receipt_for_effect") is True and received and not _strings(notice.get("receipt_evidence_refs")) and not _text(notice.get("recipient_admission_text"), 300): + _add_code(row, "NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW") + reviews.append(_review_template("NOTICE_RECEIPT_EVIDENCE_WEAK_REVIEW", row, "RECEIPT_SUPPORT_WEAK")) + + for row in rows: + payload = _as_dict(_as_dict(bo_map.get(row["source_bo_id"])).get("domain_payload_compact")) + lien = _as_dict(payload.get("lien_state")) + if not lien: + continue + dates = [receipt_by_notice[ref] for ref in _strings(lien.get("extinction_notice_refs")) if ref in receipt_by_notice] + lien_assets = set(_strings(lien.get("asset_ref"))) + if not dates or not lien_assets: + continue + receipt = min(dates) + for possession_row, assets, start, _, _ in possession_entries: + if assets & lien_assets and start and start > receipt: + _add_code(possession_row, "LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW") + reviews.append(_review_template("LIEN_ASSERTED_AFTER_EXTINCTION_NOTICE_REVIEW", possession_row, "POSSESSION_AFTER_NOTICE_RECEIPT", "HARD_WARNING", "HUMAN_REVIEW")) + return reviews + + + def _walk_explicit_candidates(value: Any, path: str = "") -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + if isinstance(value, dict): + for key, child in value.items(): + child_path = f"{path}.{key}" if path else str(key) + if key in {"candidate_values", "projection_candidates", "conflict_candidates"} and isinstance(child, list): + for item in child: + if isinstance(item, dict): + candidate = copy.deepcopy(item) + candidate["_path"] = child_path + out.append(candidate) + else: + out.extend(_walk_explicit_candidates(child, child_path)) + elif isinstance(value, list): + for index, child in enumerate(value): + out.extend(_walk_explicit_candidates(child, f"{path}[{index}]")) + return out + + + def _true_exceptions(row: dict[str, Any], source: dict[str, Any]) -> list[dict[str, Any]]: + bo = _as_dict(_as_dict(source.get("bo_by_id")).get(row["source_bo_id"])) + payload = _as_dict(bo.get("domain_payload_compact")) + explicit = _walk_explicit_candidates(payload) + grouped: dict[str, list[dict[str, Any]]] = {} + for item in explicit: + field = _text(item.get("field") or item.get("target_field"), 120) + refs = sorted(set(_strings(item.get("source_refs")) + _strings(item.get("evidence_refs")) + _strings(item.get("structure_refs")))) + if field not in CLOSED_TARGET_FIELDS or not refs or "value" not in item: + continue + grouped.setdefault(field, []).append({ + "field": field, + "value": copy.deepcopy(item.get("value")), + "basis_codes": _sorted_strings(item.get("basis_codes")) or ["EXPLICIT_SOURCE_BACKED_CANDIDATE"], + "source_refs": refs, + }) + out: list[dict[str, Any]] = [] + for field, candidates in sorted(grouped.items()): + unique_values = {_canonical(item["value"]) for item in candidates} + source_sets = {tuple(item["source_refs"]) for item in candidates} + if len(unique_values) < 2 or len(source_sets) < 2: + continue + if field in {"amount", "legal_calculation_object"}: + exception_type = "conflicting_calculation_projection" + elif field in {"object_spec", "state_context"}: + exception_type = "conflicting_asset_projection" + elif field in {"claim_chain_ref", "linked_structures"}: + exception_type = "conflicting_structure_role_projection" + else: + exception_type = "conflicting_fact_projection" + out.append({ + "exception_id": None, + "exception_type": exception_type, + "fact_id": row["fact_id"], + "source_bo_id": row["source_bo_id"], + "target_fields": [field], + "candidate_values": sorted(candidates, key=lambda item: (_canonical(item["value"]), _canonical(item["source_refs"]))), + "candidate_fact_subset": {field: copy.deepcopy(row.get(field))}, + "structure_refs": _strings(_as_dict(row.get("linked_structures")).get("structure_ids")), + "evidence_facts": [], + "allowed_output_fields": [field], + "allowed_decisions": ["KEEP_AS_IS", "PATCH", "BLOCK_REVIEW"], + "conflict_codes": ["EXPLICIT_SOURCE_BACKED_CONFLICT"], + }) + return out + + + def _assign_reviews(items: list[dict[str, Any]]) -> list[dict[str, Any]]: + unique: dict[str, dict[str, Any]] = {} + for item in items: + key = _canonical({key: value for key, value in item.items() if key != "review_id"}) + unique[key] = item + ordered = sorted(unique.values(), key=lambda item: (item["source_bo_id"], item["review_code"], item["reason_code"], item["fact_id"])) + for index, item in enumerate(ordered, start=1): + item["review_id"] = f"FRV-{index:03d}" + return ordered + + + def _pack_exceptions(exceptions: list[dict[str, Any]], reviews: list[dict[str, Any]], rows_by_fact: dict[str, dict[str, Any]], run_fingerprint: str) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]: + eligible: list[dict[str, Any]] = [] + for item in sorted(exceptions, key=lambda value: (value["source_bo_id"], value["exception_type"], value["target_fields"][0])): + if len(_canonical(item).encode("utf-8")) > MAX_EXCEPTION_ITEM_BYTES: + row = rows_by_fact[item["fact_id"]] + _add_code(row, "FL2_REVIEW_REQUIRED") + reviews.append(_review_template("PACK_CONTEXT_TOO_LARGE", row, "EXCEPTION_ITEM_OVER_12000_BYTES", "BLOCK_FINAL_DRAFTING", "HUMAN_REVIEW")) + else: + eligible.append(item) + for index, item in enumerate(eligible, start=1): + item["exception_id"] = f"FX-{index:03d}" + + groups: list[list[dict[str, Any]]] = [] + for exception_type in sorted(CLOSED_EXCEPTION_TYPES): + typed = [item for item in eligible if item["exception_type"] == exception_type] + current: list[dict[str, Any]] = [] + for item in typed: + trial = current + [item] + trial_bytes = len(_canonical({"items": trial}).encode("utf-8")) + if current and (len(trial) > MAX_EXCEPTION_ITEMS_PER_PACK or trial_bytes > MAX_EXCEPTION_PACK_BYTES or len(current) >= TARGET_EXCEPTION_ITEMS_PER_PACK): + groups.append(current) + current = [item] + else: + current = trial + if current: + groups.append(current) + if len(groups) > MAX_EXCEPTION_PACKS: + raise ValueError(f"exception pack count exceeds {MAX_EXCEPTION_PACKS}") + + pack_rows: list[dict[str, Any]] = [] + fanout: list[dict[str, Any]] = [] + for ordinal, items in enumerate(groups, start=1): + file_id = f"PACK-{ordinal:03d}" + pack_id = f"FL-PACK-{ordinal:03d}" + input_path = f"{INPUT_PART_DIR}/{file_id}.json" + output_path = f"{OUTPUT_PART_DIR}/{file_id}.json" + exception_ids = [item["exception_id"] for item in items] + exception_type = items[0]["exception_type"] + part = { + "schema_version": "stage1_fact_exception_input_part.v2", + "status": "READY", + "pack_id": pack_id, + "run_fingerprint": run_fingerprint, + "exception_type": exception_type, + "input_exception_ids": exception_ids, + "items": items, + "output_path": output_path, + } + serialized_bytes = len(_canonical(part).encode("utf-8")) + if serialized_bytes > MAX_EXCEPTION_PACK_BYTES: + raise ValueError(f"serialized exception pack exceeds budget: {pack_id}") + _write_json(input_path, part) + _verify_reread(input_path, part) + pack_rows.append({ + "pack_id": pack_id, + "exception_type": exception_type, + "exception_ids": exception_ids, + "input_path": input_path, + "output_path": output_path, + "serialized_bytes": serialized_bytes, + }) + fanout.append({ + "pack_id": pack_id, + "exception_type": exception_type, + "exception_ids": exception_ids, + "items": items, + "output_path": output_path, + "run_fingerprint": run_fingerprint, + }) + return eligible, pack_rows, fanout + + + def main() -> None: + _init_mcp_session() + source_outer = _read_json(SOURCE_PACK_PATH) + source = _as_dict(_as_dict(source_outer).get("fact_source_pack")) + if source.get("schema_version") != "stage1_fact_source_pack.v3": + raise ValueError("fact_source_pack schema_version mismatch") + if source.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("fact_source_pack status invalid") + run_fingerprint = _text(source.get("run_fingerprint"), 100) + if not run_fingerprint.startswith("sha256:"): + raise ValueError("fact_source_pack run_fingerprint invalid") + bo_ids = _sorted_strings(_as_dict(source.get("reference_universe")).get("bo_ids")) + if not bo_ids or bo_ids != sorted(_as_dict(source.get("bo_by_id")).keys()): + raise ValueError("source BO universe mismatch") + + rows: list[dict[str, Any]] = [] + reviews: list[dict[str, Any]] = [] + evidence_score_basis: dict[str, str] = {} + exceptions: list[dict[str, Any]] = [] + for index, bo_id in enumerate(bo_ids, start=1): + row, row_reviews, _, score_basis = _build_row(index, bo_id, source) + if set(row) != FINAL_FIELD_SET: + raise ValueError(f"candidate row schema mismatch: {bo_id}") + rows.append(row) + reviews.extend(row_reviews) + evidence_score_basis[row["fact_id"]] = score_basis + exceptions.extend(_true_exceptions(row, source)) + reviews.extend(_domain_scans(rows, source)) + rows_by_fact = {row["fact_id"]: row for row in rows} + exceptions, pack_rows, fanout = _pack_exceptions(exceptions, reviews, rows_by_fact, run_fingerprint) + reviews = _assign_reviews(reviews) + + exception_ids = [item["exception_id"] for item in exceptions] + if sorted(item for pack in pack_rows for item in pack["exception_ids"]) != sorted(exception_ids): + raise ValueError("exception pack conservation failed") + manifest_status = "READY" if exceptions else ("READY_WITH_REVIEW" if reviews else "READY_NO_EXCEPTIONS") + manifest = { + "fact_exception_manifest": { + "schema_version": "stage1_fact_exception_manifest.v2", + "status": manifest_status, + "run_fingerprint": run_fingerprint, + "expected_exception_ids": exception_ids, + "exception_count": len(exceptions), + "deterministic_review_items": reviews, + "pack_count": len(pack_rows), + "packs": pack_rows, + } + } + _write_json(EXCEPTION_MANIFEST_PATH, manifest) + _verify_reread(EXCEPTION_MANIFEST_PATH, manifest) + + candidate_bo_ids = [row["source_bo_id"] for row in rows] + missing = sorted(set(bo_ids) - set(candidate_bo_ids)) + unknown = sorted(set(candidate_bo_ids) - set(bo_ids)) + duplicates = sorted({item for item in candidate_bo_ids if candidate_bo_ids.count(item) > 1}) + if missing or unknown or duplicates or len(rows) != len(bo_ids): + raise ValueError("candidate BO conservation failed") + candidate_status = "READY_WITH_EXCEPTIONS" if exceptions else ("READY_WITH_REVIEW" if reviews else "READY") + bundle = { + "fact_ledger_candidate_bundle": { + "schema_version": "stage1_fact_ledger_candidate_bundle.v2", + "status": candidate_status, + "run_fingerprint": run_fingerprint, + "source_pack_ref": { + "path": SOURCE_PACK_PATH, + "schema_version": "stage1_fact_source_pack.v3", + "sha256": _digest(source_outer), + }, + "candidate_items": rows, + "exception_manifest_ref": { + "path": EXCEPTION_MANIFEST_PATH, + "schema_version": "stage1_fact_exception_manifest.v2", + "sha256": _digest(manifest), + "expected_exception_ids": exception_ids, + }, + "compile_gate": { + "source_bo_count": len(bo_ids), + "candidate_count": len(rows), + "source_bo_ids": bo_ids, + "candidate_bo_ids": candidate_bo_ids, + "missing_bo_ids": missing, + "unknown_bo_ids": unknown, + "duplicate_bo_ids": duplicates, + "bo_conservation_pass": True, + "candidate_schema_closed": True, + "reference_validation_pass": True, + "evidence_score_basis": evidence_score_basis, + }, + } + } + _write_json(CANDIDATE_PATH, bundle) + _verify_reread(CANDIDATE_PATH, bundle) + stdout_status = "READY" if exceptions else ("READY_WITH_REVIEW" if reviews else "READY_NO_EXCEPTIONS") + print(_canonical({ + "fact_ledger_fl1_manifest": { + "schema_version": "stage1_fact_ledger_fl1_manifest.v2", + "status": stdout_status, + "candidate_path": CANDIDATE_PATH, + "exception_manifest_path": EXCEPTION_MANIFEST_PATH, + "run_fingerprint": run_fingerprint, + "candidate_count": len(rows), + "exception_count": len(exceptions), + "review_count": len(reviews), + "pack_count": len(pack_rows), + }, + "dynamic_fanout": fanout, + })) + + + try: + main() + except Exception as exc: + print(_canonical({"fact_ledger_fl1_manifest": { + "schema_version": "stage1_fact_ledger_fl1_manifest.v2", + "status": "FAILED", + "error": _safe_error(exc), + }, "dynamic_fanout": []})) + raise + + - task_name: Task_FL2_fact_exception_adjudicator_* + llm_provider: google + llm_model: gemini-3.1-flash-lite + llm_reasoning: medium + llm_verbosity: low + max_iterations: 2 + max_concurrency: 2 + use_tools: + - localdocs + cache_control: + mode: auto + ttl: 1h + preflight: false + prompts: + - role: user + content: | + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + TASK_NAME: Task_FL2_fact_exception_adjudicator_* + STAGE: Stage 1 Fact Ledger generation + ROLE: compact source-backed Fact projection exception adjudicator + + + + pack_id: {{item.pack_id}} + exception_type: {{item.exception_type}} + exception_ids: {{item.exception_ids}} + items: {{item.items}} + output_path: {{item.output_path}} + run_fingerprint: {{item.run_fingerprint}} + + + + - Act as a senior Korean civil-litigation lawyer and AI systems architect, but decide only the assigned compact projection conflicts. + - Use only ASSIGNED_PACK. Do not read or request any file, any other pack, BO, evidence, signal, structure, source pack, candidate bundle, or final ledger. + - Process every assigned exception ID exactly once, without omission, duplication, merge, split, or reorder. + - Select only KEEP_AS_IS, PATCH, or BLOCK_REVIEW when that decision appears in the item's allowed_decisions. + - PATCH keys must be a subset of allowed_output_fields. Every PATCH value must exactly equal one candidate_values[].value for the same field. + - source_refs_used must be a subset of source_refs embedded in that exception item. + - Never create a fact, party, date, amount, asset, BO ID, Fact ID, exception ID, structure ID, evidence ID, candidate value, source ref, or legal theory. + - If compact context is insufficient, use BLOCK_REVIEW. Never guess. + - Do not write Fact_Ledger_base.json or any input, source, candidate, manifest, or other adjudication file. + - The only permitted tool action is localdocs write_file with overwrite=true to ASSIGNED_PACK.output_path. Do not call read_docs, list_docs, or any other tool. + - Write exactly one raw JSON adjudication part, then return only the compact manifest JSON. Do not return markdown, code fences, explanation, or the file body. + + + + - KEEP_AS_IS: preserve the deterministic candidate when its source priority remains supportable; patch must be {}, review_required=false. + - PATCH: choose exactly one source-backed candidate for each patched field; review_required=false. + - BLOCK_REVIEW: use when sources remain materially balanced, compact context is insufficient, or a safe selection requires legal theory; patch must be {}, review_required=true. + - No split decision is allowed. + + + + Each decisions[] item must contain exactly: + { + "exception_id": "FX-001", + "exception_type": "conflicting_fact_projection|conflicting_calculation_projection|conflicting_asset_projection|conflicting_structure_role_projection", + "fact_id": "F-001", + "source_bo_id": "bh1", + "decision": "KEEP_AS_IS|PATCH|BLOCK_REVIEW", + "patch": {}, + "review_required": false, + "reason_code": "KEEP_EXISTING_SOURCE_PRIORITY|SELECT_SOURCE_BACKED_CANDIDATE|INSUFFICIENT_COMPACT_CONTEXT|UNSUPPORTED_PATCH", + "basis_codes_used": [], + "source_refs_used": [], + "conflict_codes_acknowledged": [] + } + + + + Write this raw JSON object to ASSIGNED_PACK.output_path: + { + "schema_version": "stage1_fact_exception_adjudication_part.v2", + "status": "READY|READY_WITH_BLOCKS|FAILED", + "pack_id": "the assigned pack_id", + "run_fingerprint": "the assigned run_fingerprint", + "exception_type": "the assigned exception_type", + "input_exception_ids": ["all assigned exception IDs exactly once"], + "decisions": ["one decision per assigned exception"], + "coverage": { + "expected": 0, + "actual": 0, + "missing_exception_ids": [], + "unknown_exception_ids": [], + "duplicate_exception_ids": [], + "coverage_pass": true + } + } + Use READY_WITH_BLOCKS when any decision is BLOCK_REVIEW; otherwise use READY. expected and actual must equal the assigned exception count. + + + + After localdocs confirms the write, return exactly: + { + "pack_id": "the assigned pack_id", + "status": "READY|READY_WITH_BLOCKS|FAILED", + "output_path": "the assigned output_path", + "decision_count": 0 + } + + + + - task_name: Task_FL3_final_fact_ledger_gate_and_writer + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: httpx + network: agent-network + timeout: 240 + code: | + #!/usr/bin/env python3 + from __future__ import annotations + + import copy + import hashlib + import itertools + import json + import re + from typing import Any + + import httpx + + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + JSON_DECODER = json.JSONDecoder() + + + def _next_msg_id() -> int: + return next(MSG_ID_COUNTER) + + + def _safe_error(exc: BaseException) -> str: + return re.sub(r"\s+", " ", str(exc)).strip()[:300] + + + def _parse_json_value(raw: Any, label: str, allow_trailing: bool = False) -> Any: + if isinstance(raw, (dict, list)): + return raw + if not isinstance(raw, str): + raise ValueError(f"{label} is not JSON text") + text = raw.lstrip("\ufeff").strip() + if not text: + raise ValueError(f"{label} is empty") + try: + return json.loads(text) + except json.JSONDecodeError: + try: + value, end = JSON_DECODER.raw_decode(text) + except json.JSONDecodeError as exc: + raise ValueError(f"{label} invalid JSON: {text[:300]}") from exc + if not allow_trailing and text[end:].strip(): + raise ValueError(f"{label} has trailing content: {text[end:end + 300]}") + return value + + + def _parse_mcp_response(text: str) -> dict[str, Any]: + parsed: dict[str, Any] | None = None + for line in text.strip().splitlines(): + if not line.startswith("data: "): + continue + try: + item = _parse_json_value(line[6:], "MCP SSE data", True) + except ValueError: + continue + if isinstance(item, dict): + parsed = item + if parsed is None: + item = _parse_json_value(text, "MCP response", True) + if not isinstance(item, dict): + raise RuntimeError("MCP response root is not an object") + parsed = item + return parsed + + + def _init_mcp_session() -> None: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "stage-1-fact-ledger-fl3-final-writer", + "version": "2.0", + "user_id": "{{__user_hash__}}", + "workspace_id": "{{__workspace_hash__}}", + }, + }, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_mcp_response(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP initialize failed: {payload['error']}") + session_id = response.headers.get("mcp-session-id") + if session_id: + MCP_HEADERS["mcp-session-id"] = session_id + initialized = CLIENT.post( + LOCALDOCS_URL, + json={"jsonrpc": "2.0", "method": "notifications/initialized"}, + headers=MCP_HEADERS, + ) + initialized.raise_for_status() + + + def _call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + response = CLIENT.post( + LOCALDOCS_URL, + json={ + "jsonrpc": "2.0", + "id": _next_msg_id(), + "method": "tools/call", + "params": {"name": name, "arguments": arguments}, + }, + headers=MCP_HEADERS, + ) + response.raise_for_status() + payload = _parse_mcp_response(response.text) + if payload.get("error"): + raise RuntimeError(f"MCP {name} failed: {payload['error']}") + result = payload.get("result") + if not isinstance(result, dict) or result.get("isError") is True: + raise RuntimeError(f"MCP {name} returned an invalid result") + return result + + + def _extract_doc(result: dict[str, Any], doc_name: str) -> Any: + blocks = result.get("content") or [] + if not blocks or not isinstance(blocks[0], dict): + raise RuntimeError(f"Empty response: {doc_name}") + text = blocks[0].get("text") + if not isinstance(text, str) or not text.strip(): + raise RuntimeError(f"Empty text response: {doc_name}") + outer = _parse_json_value(text, f"{doc_name} outer envelope", True) + inner: Any = outer + if isinstance(outer, dict) and "results" in outer: + results = outer.get("results") or [] + if not results or not isinstance(results[0], dict): + raise RuntimeError(f"Empty results envelope: {doc_name}") + inner = results[0].get("content") + if inner in (None, ""): + inner = results[0].get("text") + if isinstance(inner, (dict, list)): + return inner + return _parse_json_value(inner, f"{doc_name} inner document", False) + + + def _read_json(path: str) -> Any: + return _extract_doc(_call_tool("read_docs", {"doc_names": [path]}), path) + + + def _write_text(path: str, content: str) -> None: + _call_tool("write_file", {"path": path, "content": content, "overwrite": True}) + + + def _canonical(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + + def _digest(value: Any) -> str: + return "sha256:" + hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest() + + + def _write_json(path: str, value: Any) -> None: + _write_text(path, json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2)) + + + def _verify_reread(path: str, expected: Any) -> None: + actual = _read_json(path) + if _digest(actual) != _digest(expected): + raise RuntimeError(f"Post-write verification failed: {path}") + + + def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + + def _as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + + def _strings(value: Any) -> list[str]: + values = value if isinstance(value, list) else [value] + out: list[str] = [] + for item in values: + if item is None: + continue + if isinstance(item, str): + text = item.strip() + elif isinstance(item, (int, float)) and not isinstance(item, bool): + text = str(item) + else: + continue + if text and text not in out: + out.append(text) + return out + + + def _sorted_strings(value: Any) -> list[str]: + return sorted(set(_strings(value))) + + + def _text(value: Any, limit: int = 500) -> str: + if value in (None, "", [], {}): + return "" + raw = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, sort_keys=True) + return re.sub(r"\s+", " ", raw).strip()[:limit] + + SOURCE_PACK_PATH = "stage1_tmp/fact_ledger/fact_source_pack.json" + CANDIDATE_PATH = "stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json" + EXCEPTION_MANIFEST_PATH = "stage1_tmp/fact_ledger/fact_exception_manifest.json" + TARGET_PATH = "Fact_Ledger_base.json" + REPORT_PATH = "stage1_tmp/fact_ledger/fact_ledger_writer_report.json" + FINAL_FIELDS = [ + "fact_id", "source_bo_id", "type", "date", "parties", "object_spec", "amount", "action", + "evidence_refs", "credibility", "legal_centrality", "proof_strength", "legal_effect_roles", + "claim_chain_ref", "linked_structures", "must_not_drop_in_claim_types", "must_consider", + "state_context", "money_claim_effect", "commercial_successor_effect", "actio_roles", + "linked_actio_structures", "downstream_module_candidates", "skeleton_validation_codes", + "derived_fact", "derivation_basis", "legal_calculation_object", + ] + FINAL_FIELD_SET = set(FINAL_FIELDS) + CLOSED_EXCEPTION_TYPES = { + "conflicting_fact_projection", + "conflicting_calculation_projection", + "conflicting_asset_projection", + "conflicting_structure_role_projection", + } + CLOSED_DECISIONS = {"KEEP_AS_IS", "PATCH", "BLOCK_REVIEW"} + + + def _add_code(row: dict[str, Any], code: str) -> None: + row["skeleton_validation_codes"] = sorted(set(_strings(row.get("skeleton_validation_codes")) + [code])) + + + def _validate_rows(bundle: dict[str, Any]) -> list[dict[str, Any]]: + if bundle.get("schema_version") != "stage1_fact_ledger_candidate_bundle.v2": + raise ValueError("candidate bundle schema_version mismatch") + if bundle.get("status") not in {"READY", "READY_WITH_REVIEW", "READY_WITH_EXCEPTIONS"}: + raise ValueError("candidate bundle status invalid") + rows = bundle.get("candidate_items") + if not isinstance(rows, list) or not rows: + raise ValueError("candidate_items missing") + for row in rows: + if not isinstance(row, dict) or set(row) != FINAL_FIELD_SET: + raise ValueError("candidate row schema is not closed") + return copy.deepcopy(rows) + + + def _load_exception_inputs(manifest: dict[str, Any], run_fingerprint: str) -> tuple[dict[str, dict[str, Any]], list[str]]: + items: dict[str, dict[str, Any]] = {} + input_ids: list[str] = [] + packs = _as_list(manifest.get("packs")) + if len(packs) != manifest.get("pack_count"): + raise ValueError("exception manifest pack_count mismatch") + for pack in packs: + row = _as_dict(pack) + part = _as_dict(_read_json(_text(row.get("input_path"), 300))) + if part.get("schema_version") != "stage1_fact_exception_input_part.v2" or part.get("status") != "READY": + raise ValueError("exception input part schema/status invalid") + if part.get("pack_id") != row.get("pack_id") or part.get("run_fingerprint") != run_fingerprint: + raise ValueError("exception input part identity mismatch") + if part.get("output_path") != row.get("output_path") or part.get("exception_type") != row.get("exception_type"): + raise ValueError("exception input part routing mismatch") + part_ids = _strings(part.get("input_exception_ids")) + if part_ids != _strings(row.get("exception_ids")): + raise ValueError("exception input ID mismatch") + for item in _as_list(part.get("items")): + if not isinstance(item, dict): + raise ValueError("exception input item invalid") + exception_id = _text(item.get("exception_id"), 120) + if not exception_id or exception_id in items: + raise ValueError(f"duplicate exception input: {exception_id}") + if item.get("exception_type") not in CLOSED_EXCEPTION_TYPES: + raise ValueError("exception input type invalid") + items[exception_id] = item + input_ids.append(exception_id) + return items, input_ids + + + def _load_decisions(manifest: dict[str, Any], run_fingerprint: str) -> tuple[dict[str, dict[str, Any]], list[str]]: + decisions: dict[str, dict[str, Any]] = {} + decision_ids: list[str] = [] + for pack in _as_list(manifest.get("packs")): + expected_pack = _as_dict(pack) + part = _as_dict(_read_json(_text(expected_pack.get("output_path"), 300))) + if part.get("schema_version") != "stage1_fact_exception_adjudication_part.v2": + raise ValueError("adjudication part schema_version mismatch") + if part.get("status") not in {"READY", "READY_WITH_BLOCKS"}: + raise ValueError("adjudication part failed or has invalid status") + if part.get("pack_id") != expected_pack.get("pack_id") or part.get("run_fingerprint") != run_fingerprint: + raise ValueError("adjudication part identity mismatch") + if part.get("exception_type") != expected_pack.get("exception_type"): + raise ValueError("adjudication part type mismatch") + if _strings(part.get("input_exception_ids")) != _strings(expected_pack.get("exception_ids")): + raise ValueError("adjudication input IDs mismatch") + coverage = _as_dict(part.get("coverage")) + if coverage.get("coverage_pass") is not True or _as_list(coverage.get("missing_exception_ids")) or _as_list(coverage.get("unknown_exception_ids")) or _as_list(coverage.get("duplicate_exception_ids")): + raise ValueError("adjudication part coverage failed") + for decision in _as_list(part.get("decisions")): + if not isinstance(decision, dict): + raise ValueError("adjudication decision invalid") + exception_id = _text(decision.get("exception_id"), 120) + if not exception_id or exception_id in decisions: + raise ValueError(f"duplicate adjudication decision: {exception_id}") + decisions[exception_id] = decision + decision_ids.append(exception_id) + return decisions, decision_ids + + + def _allowed_values(item: dict[str, Any], field: str) -> list[Any]: + return [candidate.get("value") for candidate in _as_list(item.get("candidate_values")) if isinstance(candidate, dict) and candidate.get("field") == field] + + + def _all_source_refs(item: dict[str, Any]) -> set[str]: + refs: set[str] = set() + for candidate in _as_list(item.get("candidate_values")): + if isinstance(candidate, dict): + refs.update(_strings(candidate.get("source_refs"))) + refs.update(_strings(item.get("structure_refs"))) + return refs + + + def _apply_decision(row: dict[str, Any], item: dict[str, Any], decision: dict[str, Any]) -> bool: + for key in ("exception_id", "exception_type", "fact_id", "source_bo_id"): + if decision.get(key) != item.get(key): + raise ValueError(f"decision identity mismatch: {key}") + decision_type = decision.get("decision") + if decision_type not in CLOSED_DECISIONS or decision_type not in _as_list(item.get("allowed_decisions")): + raise ValueError("decision enum invalid") + patch = decision.get("patch") + if not isinstance(patch, dict): + raise ValueError("decision patch must be an object") + used_refs = set(_strings(decision.get("source_refs_used"))) + if not used_refs.issubset(_all_source_refs(item)): + raise ValueError("decision used unknown source refs") + if decision_type == "KEEP_AS_IS": + if patch or decision.get("review_required") is True: + raise ValueError("KEEP_AS_IS contract violated") + return False + if decision_type == "BLOCK_REVIEW": + if patch or decision.get("review_required") is not True: + raise ValueError("BLOCK_REVIEW contract violated") + _add_code(row, "BLOCK_REVIEW_PRESENT") + _add_code(row, "FL2_REVIEW_REQUIRED") + row["must_consider"] = True + row["derivation_basis"] = (str(row.get("derivation_basis") or "") + "; blocked_exception=" + str(item.get("exception_id"))).strip("; ") + return True + if decision.get("review_required") is True or not patch: + raise ValueError("PATCH contract violated") + allowed_fields = set(_strings(item.get("allowed_output_fields"))) + if not set(patch).issubset(allowed_fields) or not set(patch).issubset(FINAL_FIELD_SET): + raise ValueError("PATCH field outside hard whitelist") + for field, value in patch.items(): + if not any(_canonical(value) == _canonical(candidate) for candidate in _allowed_values(item, field)): + raise ValueError("PATCH value is not source-backed") + row[field] = copy.deepcopy(value) + return False + + + def _reference_universes(source: dict[str, Any]) -> dict[str, set[str]]: + structures = _as_dict(source.get("structure_by_id")) + claims: set[str] = set() + liabilities: set[str] = set() + actio_signals: set[str] = set() + for structure in structures.values(): + row = _as_dict(structure) + claims.update(_strings(row.get("linked_claim_group_ids"))) + liabilities.update(_strings(row.get("linked_liability_group_ids"))) + actio_signals.update(_strings(row.get("linked_actio_signal_ids"))) + for by_type in _as_dict(source.get("signal_hints_by_bo_id")).values(): + for signal_type, rows in _as_dict(by_type).items(): + for signal in _as_list(rows): + item = _as_dict(signal) + claims.update(_strings(item.get("claim_group_candidates"))) + liabilities.update(_strings(item.get("liability_group_candidates"))) + if signal_type == "actio": + actio_signals.update(_strings(item.get("signal_id"))) + return {"claim": claims, "liability": liabilities, "actio": actio_signals} + + + def _validate_row_refs(row: dict[str, Any], source: dict[str, Any], group_universes: dict[str, set[str]], report: dict[str, Any]) -> None: + universe = _as_dict(source.get("reference_universe")) + bo_ids = set(_strings(universe.get("bo_ids"))) + structure_ids = set(_strings(universe.get("structure_ids"))) + evidence_ids = set(_strings(universe.get("evidence_indexes"))) + bo_id = _text(row.get("source_bo_id"), 120) + unknown_bo = [] if bo_id in bo_ids else [bo_id] + unknown_structures = sorted(set(_strings(_as_dict(row.get("linked_structures")).get("structure_ids"))) - structure_ids) + unknown_evidence = sorted(set(_strings(row.get("evidence_refs"))) - evidence_ids) + report["reference_gate"]["unknown_bo_refs"].extend(unknown_bo) + report["reference_gate"]["unknown_structure_refs"].extend(unknown_structures) + report["reference_gate"]["unknown_evidence_refs"].extend(unknown_evidence) + claim = _as_dict(row.get("claim_chain_ref")) + if not set(_strings(claim.get("claim_group_ids"))).issubset(group_universes["claim"]): + raise ValueError(f"unknown claim group ref: {bo_id}") + if not set(_strings(claim.get("liability_group_ids"))).issubset(group_universes["liability"]): + raise ValueError(f"unknown liability group ref: {bo_id}") + + + def main() -> None: + _init_mcp_session() + source_outer = _read_json(SOURCE_PACK_PATH) + candidate_outer = _read_json(CANDIDATE_PATH) + manifest_outer = _read_json(EXCEPTION_MANIFEST_PATH) + source = _as_dict(_as_dict(source_outer).get("fact_source_pack")) + bundle = _as_dict(_as_dict(candidate_outer).get("fact_ledger_candidate_bundle")) + manifest = _as_dict(_as_dict(manifest_outer).get("fact_exception_manifest")) + if source.get("schema_version") != "stage1_fact_source_pack.v3" or source.get("status") not in {"READY", "READY_WITH_REVIEW"}: + raise ValueError("fact_source_pack invalid") + if manifest.get("schema_version") != "stage1_fact_exception_manifest.v2" or manifest.get("status") not in {"READY", "READY_NO_EXCEPTIONS", "READY_WITH_REVIEW"}: + raise ValueError("fact_exception_manifest invalid") + rows = _validate_rows(bundle) + run_fingerprint = _text(source.get("run_fingerprint"), 100) + if not run_fingerprint or bundle.get("run_fingerprint") != run_fingerprint or manifest.get("run_fingerprint") != run_fingerprint: + raise ValueError("run_fingerprint mismatch") + source_ref = _as_dict(bundle.get("source_pack_ref")) + manifest_ref = _as_dict(bundle.get("exception_manifest_ref")) + if source_ref.get("path") != SOURCE_PACK_PATH or source_ref.get("schema_version") != "stage1_fact_source_pack.v3" or source_ref.get("sha256") != _digest(source_outer): + raise ValueError("source pack hash/ref mismatch") + if manifest_ref.get("path") != EXCEPTION_MANIFEST_PATH or manifest_ref.get("schema_version") != "stage1_fact_exception_manifest.v2" or manifest_ref.get("sha256") != _digest(manifest_outer): + raise ValueError("exception manifest hash/ref mismatch") + + expected_ids = _strings(manifest.get("expected_exception_ids")) + if expected_ids != _strings(manifest_ref.get("expected_exception_ids")) or len(expected_ids) != manifest.get("exception_count"): + raise ValueError("expected exception IDs mismatch") + if not expected_ids and (_as_list(manifest.get("packs")) or manifest.get("pack_count") != 0): + raise ValueError("no-exception manifest contains packs") + items_by_id, input_ids = _load_exception_inputs(manifest, run_fingerprint) + decisions_by_id, decision_ids = _load_decisions(manifest, run_fingerprint) + missing_ids = sorted(set(expected_ids) - set(decision_ids)) + unknown_ids = sorted(set(decision_ids) - set(expected_ids)) + duplicate_ids = sorted({item for item in decision_ids if decision_ids.count(item) > 1}) + if sorted(expected_ids) != sorted(input_ids) or missing_ids or unknown_ids or duplicate_ids or len(decision_ids) != len(expected_ids): + raise ValueError("exact exception coverage failed") + + rows_by_fact = {row["fact_id"]: row for row in rows} + blocked_items: list[dict[str, Any]] = [] + for exception_id in expected_ids: + item = items_by_id.get(exception_id) + decision = decisions_by_id.get(exception_id) + if not item or not decision: + raise ValueError(f"exception coverage missing: {exception_id}") + row = rows_by_fact.get(_text(item.get("fact_id"), 120)) + if not row or row.get("source_bo_id") != item.get("source_bo_id"): + raise ValueError("exception Fact/BO target mismatch") + blocked = _apply_decision(row, item, decision) + if blocked: + blocked_items.append({ + "exception_id": exception_id, + "exception_type": item.get("exception_type"), + "fact_id": item.get("fact_id"), + "source_bo_id": item.get("source_bo_id"), + "reason_code": decision.get("reason_code"), + }) + + source_bo_ids = _strings(_as_dict(source.get("reference_universe")).get("bo_ids")) + compile_gate = _as_dict(bundle.get("compile_gate")) + candidate_bo_ids = [str(row.get("source_bo_id")) for row in rows] + duplicate_bos = sorted({item for item in candidate_bo_ids if candidate_bo_ids.count(item) > 1}) + missing_bos = sorted(set(source_bo_ids) - set(candidate_bo_ids)) + unknown_bos = sorted(set(candidate_bo_ids) - set(source_bo_ids)) + if source_bo_ids != _strings(compile_gate.get("source_bo_ids")) or candidate_bo_ids != _strings(compile_gate.get("candidate_bo_ids")) or missing_bos or unknown_bos or duplicate_bos or compile_gate.get("bo_conservation_pass") is not True: + raise ValueError("candidate/BO conservation failed") + + deterministic_reviews = _as_list(manifest.get("deterministic_review_items")) + report = { + "schema_version": "stage1_fact_ledger_writer_report.v2", + "status": "READY", + "run_fingerprint": run_fingerprint, + "conservation_gate": { + "source_bo_count": len(source_bo_ids), + "candidate_count": len(candidate_bo_ids), + "final_count": len(rows), + "missing_bo_ids": missing_bos, + "unknown_bo_ids": unknown_bos, + "duplicate_bo_ids": duplicate_bos, + "pass": True, + }, + "exception_gate": { + "expected_count": len(expected_ids), + "decision_count": len(decision_ids), + "blocked_count": len(blocked_items), + "missing_exception_ids": missing_ids, + "unknown_exception_ids": unknown_ids, + "duplicate_exception_ids": duplicate_ids, + "coverage_pass": True, + }, + "reference_gate": {"unknown_bo_refs": [], "unknown_structure_refs": [], "unknown_evidence_refs": [], "pass": True}, + "upstream_les_review_gate": {}, + "blocked_review_items": blocked_items, + "row_warnings": [], + "possession_conflict_gate": {"status": "PASS", "conflicts": []}, + "drafting_gates": [], + "gate_review_code_summary": {}, + "evidence_score_change_summary": [], + } + group_universes = _reference_universes(source) + for row in rows: + _validate_row_refs(row, source, group_universes, report) + for key in ("unknown_bo_refs", "unknown_structure_refs", "unknown_evidence_refs"): + report["reference_gate"][key] = sorted(set(report["reference_gate"][key])) + report["reference_gate"]["pass"] = not any(report["reference_gate"][key] for key in ("unknown_bo_refs", "unknown_structure_refs", "unknown_evidence_refs")) + if not report["reference_gate"]["pass"]: + raise ValueError("reference gate failed") + + code_rows: dict[str, list[str]] = {} + for ordinal, row in enumerate(sorted(rows, key=lambda item: item["source_bo_id"]), start=1): + row["fact_id"] = f"F-{ordinal:03d}" + row["skeleton_validation_codes"] = sorted(set(_strings(row.get("skeleton_validation_codes")))) + for code in row["skeleton_validation_codes"]: + code_rows.setdefault(code, []).append(row["fact_id"]) + rows.sort(key=lambda item: item["fact_id"]) + report["gate_review_code_summary"] = {code: ids for code, ids in sorted(code_rows.items())} + conflicts = code_rows.get("POSSESSION_CAUSE_CONFLICT", []) + report["possession_conflict_gate"] = {"status": "HARD_WARNING" if conflicts else "PASS", "conflicts": conflicts} + + blocking_reviews = [item for item in deterministic_reviews if _as_dict(item).get("severity") == "BLOCK_FINAL_DRAFTING" or _as_dict(item).get("review_code") == "LES_BLOCKED_EXCEPTION_REVIEW"] + affected_facts = sorted(set(_text(_as_dict(item).get("fact_id"), 120) for item in deterministic_reviews if _text(_as_dict(item).get("fact_id"), 120))) + affected_bos = sorted(set(_text(_as_dict(item).get("source_bo_id"), 120) for item in deterministic_reviews if _text(_as_dict(item).get("source_bo_id"), 120))) + upstream_status = "BLOCK_FINAL_DRAFTING" if blocking_reviews else ("REVIEW" if deterministic_reviews else "PASS") + report["upstream_les_review_gate"] = { + "review_count": len(deterministic_reviews), + "blocking_count": len(blocking_reviews), + "affected_fact_ids": affected_facts, + "affected_bo_ids": affected_bos, + "status": upstream_status, + } + if blocking_reviews: + report["drafting_gates"].append({ + "gate": "BLOCK_FINAL_DRAFTING_UPSTREAM_LES_REVIEW", + "affected_fact_ids": sorted(set(_text(_as_dict(item).get("fact_id"), 120) for item in blocking_reviews)), + "affected_bo_ids": sorted(set(_text(_as_dict(item).get("source_bo_id"), 120) for item in blocking_reviews)), + }) + notice_rows = code_rows.get("NOTICE_RECEIPT_UNVERIFIED", []) + if notice_rows: + report["drafting_gates"].append({"gate": "BLOCK_FINAL_DRAFTING_NOTICE_RECEIPT", "affected_fact_ids": notice_rows}) + report["evidence_score_change_summary"] = [ + {"fact_id": fact_id, "basis_code": code} + for fact_id, code in sorted(_as_dict(compile_gate.get("evidence_score_basis")).items()) + ] + has_review = bool(deterministic_reviews or blocked_items or report["drafting_gates"] or conflicts) + report["status"] = "READY_WITH_REVIEW" if has_review else "READY" + + _write_json(TARGET_PATH, rows) + _write_json(REPORT_PATH, report) + _verify_reread(TARGET_PATH, rows) + _verify_reread(REPORT_PATH, report) + print(_canonical({"fact_ledger_final_writer": { + "schema_version": "stage1_fact_ledger_final_writer.v2", + "status": report["status"], + "written_file": TARGET_PATH, + "writer_report_file": REPORT_PATH, + "run_fingerprint": run_fingerprint, + "fact_count": len(rows), + "review_count": len(deterministic_reviews), + "blocked_count": len(blocked_items), + "drafting_gate_count": len(report["drafting_gates"]), + }})) + + + try: + main() + except Exception as exc: + print(_canonical({"fact_ledger_final_writer": { + "schema_version": "stage1_fact_ledger_final_writer.v2", + "status": "FAILED", + "written_file": None, + "error": _safe_error(exc), + }})) + raise +