From 31035d5757b9103c6330fe28491832a62dd61918 Mon Sep 17 00:00:00 2001 From: jhogyu Date: Thu, 20 Aug 2026 15:56:34 +0900 Subject: [PATCH] =?UTF-8?q?fix(stage1):=20execute=20asset-triage=20v.2=20c?= =?UTF-8?q?onfirmed=20items=20=E2=80=94=20P2=20dead=20constants/comment,?= =?UTF-8?q?=20manifest=20+6,=20matrix=20removal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - stage_1_part_2_v.8.yml (195,963 B, sha16 ec407309a5ab43f4): drop two dead R0 path constants (S0's own copies untouched); narrow the legacy_paths_forbidden header comment to old slice/seed paths and state that stage_a_context/source_universe/postb_* keep stage1_tmp/task_c_bo as canonical. R0 fragment synced; prior versions preserved under outdated/ (pre_asset_triage_fix) - Default_Agent/runtime_manifest.json 426->432: register six unanchored Part 1 assets (component union, client_goal schema, two reference catalogs, determinism contract, activation cue digest) with runtime_artifact_count updated and round-trip serialization kept - remove case_kind_domain_matrix.v2.json from deployment origin (byte-identical master kept in assembly tree; empty folders pruned); tree 210->209, listed∩tree 155 with zero hash mismatches - stage_1_2_3_assets_distribution_location.md rewritten as the Default_Agent change record only; full survey kept as *_old.md - Part 1|2 analysis doc: 10 targeted updates (sizes/sha, shifted line ranges A0 62-719 / B 720-849 / R0 850-1793, constant wording removed, §8 history row annotated with a dated note instead of rewriting) - verified: 3-pass dry run PASS with BO.json and legal_effect_structures.json byte-identical to the previous round; adversarial sub-agent check 29 items, findings 1 -> 0 - v.7/MEMORY.md entry 26 Co-Authored-By: Claude Fable 5 --- .../YAML_Prompts/1. Stage_1/v.7/MEMORY.md | 18 + .../Default_Agent/runtime_manifest.json | 26 +- .../routing/case_kind_domain_matrix.v2.json | 1 - ...tage_1_2_3_assets_distribution_location.md | 182 +- ..._1_2_3_assets_distribution_location_old.md | 169 + ...ge_1_part_1_and_2_updated_yaml_analysis.md | 20 +- ..._seed_reducer_and_exception_planner_v4.yml | 2 - ..._seed_reducer_and_exception_planner_v4.yml | 967 +++++ ...tage_1_part_2_v.8.pre_asset_triage_fix.yml | 3315 +++++++++++++++++ .../stage_1_part_2_v.8.yml | 6 +- .../확장_최적워크플로우_연구_프롬프트.txt | 64 +- 11 files changed, 4553 insertions(+), 217 deletions(-) delete mode 100644 Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/Default_Agent/validation_assets/routing/case_kind_domain_matrix.v2.json create mode 100644 Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/stage_1_2_3_assets_distribution_location_old.md create mode 100644 Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/ver_8_yaml_candidates/outdated/_pre_asset_triage_fix/P2-R0_seed_reducer_and_exception_planner_v4.yml create mode 100644 Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/ver_8_yaml_candidates/outdated/stage_1_part_2_v.8.pre_asset_triage_fix.yml diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/MEMORY.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/MEMORY.md index 460a6d73..52d967c0 100644 --- a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/MEMORY.md +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/MEMORY.md @@ -1547,3 +1547,21 @@ task 2(L0 50–605 · L2 607–1,015 — 내용 종행 기준, 임베디드 code 1. **"권장 조치"는 기존 설계 명문과 대조한 뒤에 낸다.** v.1 의 277 복사 권장은 사실 관찰은 정확했으나 4곳의 설계 명문을 확인하지 않아 정반대 처방이 됐다 — 관찰의 정확성과 처방의 타당성은 별개 검증이다. 2. **"훼손 의심"은 git 이력이 즉답한다.** 생성 커밋 바이트 대조 한 번이면 저작/훼손이 갈린다 — 추론(라벨 병렬성)으로 판정하지 말 것. 3. **보안 처방은 실행 가능성(백엔드 지원)과 노출 범위(저장소 전파)를 함께 재야 한다** — YAML 3본 치환은 402파일 노출 앞에서 본질이 아니다. + + +--- + +## 2026-08-20 ㉖ — 자산 정리 회차: v.2 확정 4건 실행 (P2 사문 상수·주석 / manifest +6 / matrix 제거 · sub-agent 2라운드 1→0) + +v.2 §2 의 지정 4건만 수행. 산출: +- **P2 통합본 개정** `stage_1_part_2_v.8.yml` → **195,963 B / 3,315행 / sha 앞 `ec407309a5ab43f4`** (구본 `outdated/stage_1_part_2_v.8.pre_asset_triage_fix.yml`). R0 사문 상수 2줄 삭제(ACTIVATION_MANIFEST_PATH 는 S0 2회만 잔존·REGISTRY_INDEX_PATH 전 파일 0회), L28 주석을 "구 slice/seed 경로 한정 + stage_a/universe/postb_* 5파일 정본 유지" 3줄로 교체. R0 조각 동기화(구본 `outdated/_pre_asset_triage_fix/`). 행 이동: A0 62–719 · B 720–849 · R0 850–1,793(주석 +2 · 상수 −2 상쇄로 R1 이후 무변). +- **runtime_manifest 426→432**: P1 무앵커 자산 6종 직접 행 추가(count 동반 갱신·왕복 보존) — 복원 기준 확보(게이트 강제는 P1 개정 회차 몫). +- **matrix 제거**: `case_kind_domain_matrix.v2.json` 을 배포 트리에서 제거(조립본 원본과 바이트 동일 확인 후 · 빈 폴더 정리) — 트리 209 파일 · 교집합 155/불일치 0 · 실물-미등재 54 · 277 무변. +- **총람 회전**: `stage_1_2_3_assets_distribution_location.md` 를 Default_Agent 변화 기록만으로 재작성(전판 `_old.md` 존치). +- **분석서 파급 10곳** 갱신(수치·행 범위·상수 서술 §3.3/§3.5 + §8 이력 행 시효 주석 — 원문 보존). + +검증: 예행 3패스 PASS + **BO.json·legal_effect_structures.json·리포트 3본 전부 이전 회차와 바이트 동일**(수정이 값에 무영향 실증). sub-agent 검증 29항 — 지적 1건(§8 갱신 누락 — v.2 파급 문면 vs "§8 은 손대지 않는다" 관례 충돌 자리) 반영 후 0건. + +### 교훈 +1. **이력 절은 개서하지 않고 시효 주석으로 닫는다** — §8 라운드 기록의 현재형 서술이 낡았을 때 원문 교체는 이력 위조, 방치는 오독 — "(…는 2026-08-20 회차에서 삭제됐다)" 부가가 정답. +2. **주석 확장(+2)과 코드 삭제(−2)가 상쇄돼도 중간 블록 행 범위는 이동한다** — A0/B 는 +2, R0 는 시작 +2·끝 0, R1 이후 무변. 파일 총행 무변을 "행 범위 무변"으로 착각하지 말 것. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/Default_Agent/runtime_manifest.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/Default_Agent/runtime_manifest.json index aaded1ae..c5a729bf 100644 --- a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/Default_Agent/runtime_manifest.json +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/Default_Agent/runtime_manifest.json @@ -1280,6 +1280,18 @@ "path": "law_versions/source_status/source_repin_status.json", "sha256": "f44e21988306748bfdde5d629d1e955f592d669c2af280eefb88f01c9ae74228" }, + { + "path": "platform/contracts/determinism_scope_contract.json", + "sha256": "8734102d9e2e9b695a3b12e91e7ab1b7208e1c1403729c082d708ee2c4a70532" + }, + { + "path": "platform/reference_catalogs/calculation_ids.json", + "sha256": "fb14965b8315e01d47d694a10a674eda5db606ab9c7bbb6345de98495ba233b8" + }, + { + "path": "platform/reference_catalogs/domain_ids.json", + "sha256": "4740d98e6a31bd0e7d9f792189712a8af1d0a87ec0bbd181deba1e34edb08355" + }, { "path": "platform/router/s3_sg01_router_config.json", "sha256": "4913b119f5ee08f96b52e10bd1f8376326564f1ab4742909fd2d0f07ded4efc6" @@ -1292,6 +1304,10 @@ "path": "platform/router/s3_sg01_router_output.schema.json", "sha256": "4af85d52a60254149a3506cd5aa36cce1360e275b3c195d08307876379114b51" }, + { + "path": "platform/schemas/client_goal_domain_profiles.schema.json", + "sha256": "ae2bfe0d754a09cbae16b2c15bf1518fc23f9e1bda8fa1f5f949606c8e42c010" + }, { "path": "platform/schemas/domain_fanout_plan.schema.json", "sha256": "3b0948613a5996028b9c030a99f0b51d682f6035e019557756b1a15d43971113" @@ -1312,6 +1328,14 @@ "path": "platform/schemas/structure_seed_bundle.schema.json", "sha256": "b7af9e422b6ac3876cffea39ec4f617eea76a631a57dfdfcd57d3785a83c667a" }, + { + "path": "routing/activation_cue_digest.md", + "sha256": "7e98885a50e1880b5ee7e2f459ab47010f90387e5c463a484a152054c2321e97" + }, + { + "path": "routing/evidence_component_union.md", + "sha256": "ecd6e1877f476139012b79fdc04380c067b8e81af86707bcc5c2b311b3fabf79" + }, { "path": "routing/extension_payload_key_declarations.v1.json", "sha256": "1684e13896bc9dfd0e7aa1a3b01354f58b2fa6ed00908bec6f861bce4b47235c" @@ -1706,6 +1730,6 @@ } ], "program_release_status": "STAGE1_NOT_RELEASE_READY", - "runtime_artifact_count": 426, + "runtime_artifact_count": 432, "schema_version": "stage1_runtime_manifest.v1" } diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/Default_Agent/validation_assets/routing/case_kind_domain_matrix.v2.json b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/Default_Agent/validation_assets/routing/case_kind_domain_matrix.v2.json deleted file mode 100644 index 6793ba0c..00000000 --- a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/Default_Agent/validation_assets/routing/case_kind_domain_matrix.v2.json +++ /dev/null @@ -1 +0,0 @@ -{"catalog_role":"test_oracle_only_never_runtime_lookup","excluded_major_categories":["가사소송","가족관계등록","행정소송"],"record_count":137,"records":[{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"대여금 청구","case_kind_id":"CK-001","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"계금 청구","case_kind_id":"CK-002","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"계약금 청구","case_kind_id":"CK-003","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X1"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-06","CE-13"],"candidate_profile_ids":[],"case_kind":"공사대금 청구","case_kind_id":"CK-004","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-07"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-12","CE-13"],"candidate_profile_ids":[],"case_kind":"공제금 청구","case_kind_id":"CK-005","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-19"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-04","CE-11","CE-13"],"candidate_profile_ids":[],"case_kind":"관리비 청구","case_kind_id":"CK-006","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-12"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-13"],"candidate_profile_ids":[],"case_kind":"구상금 청구","case_kind_id":"CK-007","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-03"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-11","CE-13"],"candidate_profile_ids":[],"case_kind":"담보금 청구","case_kind_id":"CK-008","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-10"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"동업반환금 청구","case_kind_id":"CK-009","deferred_calculation_ids":["CE-R2"],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-18"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":["DEFERRED_CALCULATOR_REVIEW"]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"매매대금 청구","case_kind_id":"CK-010","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-04","CE-07","CE-11","CE-13"],"candidate_profile_ids":[],"case_kind":"배당금・배분금 청구","case_kind_id":"CK-011","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-10","E-11"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-03","CE-05","CE-13"],"candidate_profile_ids":[],"case_kind":"배상금 청구","case_kind_id":"CK-012","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-05"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-04","CE-07","CE-13"],"candidate_profile_ids":[],"case_kind":"변상금 청구","case_kind_id":"CK-013","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-04"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"보관금 청구","case_kind_id":"CK-014","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"보상금 청구","case_kind_id":"CK-015","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","E-00"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":["PUBLIC_LAW_NEXUS_REVIEW"]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"보증금 청구","case_kind_id":"CK-016","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-13"],"candidate_profile_ids":[],"case_kind":"보증채무금 청구","case_kind_id":"CK-017","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-03"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-12","CE-13"],"candidate_profile_ids":[],"case_kind":"보험금 및 보험금수익자변경 청구","case_kind_id":"CK-018","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-19"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"부금・불입금 청구","case_kind_id":"CK-019","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-04","CE-07","CE-13"],"candidate_profile_ids":[],"case_kind":"부당이득금・이득상환금 청구","case_kind_id":"CK-020","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-04"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-13"],"candidate_profile_ids":[],"case_kind":"분담금 청구","case_kind_id":"CK-021","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-03"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"분양대금 청구","case_kind_id":"CK-022","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"사용료 청구","case_kind_id":"CK-023","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-04","CE-07","CE-13"],"candidate_profile_ids":[],"case_kind":"선급금・선수금 청구","case_kind_id":"CK-024","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-04"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-03","CE-05","CE-13"],"candidate_profile_ids":[],"case_kind":"손해배상(자) 청구","case_kind_id":"CK-025","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-05"],"required_profile_ids":["SLP-AUTO-DAMAGE"],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-03","CE-05","CE-09","CE-13"],"candidate_profile_ids":[],"case_kind":"손해배상(산) 청구","case_kind_id":"CK-026","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-05","E-17"],"required_profile_ids":["SLP-INDUSTRIAL-ACCIDENT"],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-03","CE-05","CE-13"],"candidate_profile_ids":["SLP-PRODUCT-LIABILITY"],"case_kind":"손해배상(의) 청구","case_kind_id":"CK-027","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-05","E-06"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":["SPECIAL_LAW_PROFILE_ACTIVATION_REVIEW"]},{"calculation_domain_ids":["CE-01","CE-03","CE-05","CE-13"],"candidate_profile_ids":[],"case_kind":"손해배상(지) 청구","case_kind_id":"CK-028","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-05","E-20"],"required_profile_ids":["SLP-IP"],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-03","CE-05","CE-13"],"candidate_profile_ids":[],"case_kind":"손해배상(저) 청구","case_kind_id":"CK-029","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-05","E-20"],"required_profile_ids":["SLP-IP"],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-03","CE-05","CE-13"],"candidate_profile_ids":[],"case_kind":"손해배상(언) 청구","case_kind_id":"CK-030","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-05","E-21"],"required_profile_ids":["SLP-MEDIA-MEDIATION"],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-03","CE-05","CE-06","CE-13"],"candidate_profile_ids":[],"case_kind":"손해배상(건) 청구","case_kind_id":"CK-031","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-05","E-07"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-03","CE-05","CE-13"],"candidate_profile_ids":[],"case_kind":"손해배상(국) 청구","case_kind_id":"CK-032","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-05"],"required_profile_ids":["SLP-STATE-COMPENSATION"],"requires_underlying_leaf":false,"review_codes":["PUBLIC_LAW_NEXUS_REVIEW"]},{"calculation_domain_ids":["CE-01","CE-03","CE-05","CE-13"],"candidate_profile_ids":[],"case_kind":"손해배상(해) 청구","case_kind_id":"CK-033","deferred_calculation_ids":["CE-R3"],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-05"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":["TRANSPORT_MARITIME_DEFERRED_REVIEW","DEFERRED_CALCULATOR_REVIEW"]},{"calculation_domain_ids":["CE-01","CE-03","CE-05","CE-13"],"candidate_profile_ids":["SLP-PRODUCT-LIABILITY"],"case_kind":"손해배상(기) 청구","case_kind_id":"CK-034","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-05","E-06"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":["SPECIAL_LAW_PROFILE_ACTIVATION_REVIEW"]},{"calculation_domain_ids":["CE-01","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"수표금 청구","case_kind_id":"CK-035","deferred_calculation_ids":["CE-R4"],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-16"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":["DEFERRED_CALCULATOR_REVIEW"]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-06","CE-13"],"candidate_profile_ids":[],"case_kind":"설계비・시설대여금 청구","case_kind_id":"CK-036","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02","E-07"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"신용장대금 청구","case_kind_id":"CK-037","deferred_calculation_ids":["CE-R4"],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":["DEFERRED_CALCULATOR_REVIEW"]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"신용카드이용대금 청구","case_kind_id":"CK-038","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-13"],"candidate_profile_ids":[],"case_kind":"압류채권대금 청구","case_kind_id":"CK-039","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-14"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"약속어음금 청구","case_kind_id":"CK-040","deferred_calculation_ids":["CE-R4"],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-16"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":["DEFERRED_CALCULATOR_REVIEW"]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"약정금 청구","case_kind_id":"CK-041","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-13"],"candidate_profile_ids":[],"case_kind":"양도채권금・양수금 청구","case_kind_id":"CK-042","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-03"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"예금(예치금・예탁금・인출금) 청구","case_kind_id":"CK-043","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"운임(운송대금) 청구","case_kind_id":"CK-044","deferred_calculation_ids":["CE-R3"],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":["TRANSPORT_MARITIME_DEFERRED_REVIEW","DEFERRED_CALCULATOR_REVIEW"]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"위약금 청구","case_kind_id":"CK-045","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X1"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"위탁금 청구","case_kind_id":"CK-046","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-08","CE-13"],"candidate_profile_ids":[],"case_kind":"유류분반환・유증 등 청구","case_kind_id":"CK-047","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-15"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-07","CE-13"],"candidate_profile_ids":["SLP-HOUSING-LEASE","SLP-COMMERCIAL-LEASE"],"case_kind":"임대료 청구","case_kind_id":"CK-048","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X1"],"required_primary_domain_ids":["E-08"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":["SPECIAL_LAW_PROFILE_ACTIVATION_REVIEW"]},{"calculation_domain_ids":["CE-01","CE-07","CE-13"],"candidate_profile_ids":["SLP-HOUSING-LEASE","SLP-COMMERCIAL-LEASE"],"case_kind":"임차 및 전세보증금 청구","case_kind_id":"CK-049","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X1"],"required_primary_domain_ids":["E-08"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":["SPECIAL_LAW_PROFILE_ACTIVATION_REVIEW"]},{"calculation_domain_ids":["CE-08","CE-13"],"candidate_profile_ids":[],"case_kind":"재산상속회복 청구","case_kind_id":"CK-050","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-15"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"정산금 청구","case_kind_id":"CK-051","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02","E-18"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-13"],"candidate_profile_ids":[],"case_kind":"전부금 청구","case_kind_id":"CK-052","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-14"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-04","CE-07","CE-11","CE-13"],"candidate_profile_ids":[],"case_kind":"지료 청구","case_kind_id":"CK-053","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-11","E-12"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-05","CE-13"],"candidate_profile_ids":[],"case_kind":"진료비・치료비・의료비 청구","case_kind_id":"CK-054","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02","E-06"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-09","CE-13"],"candidate_profile_ids":[],"case_kind":"청산금・채무인수금・체당금・추심금・출자금 청구 등","case_kind_id":"CK-055","deferred_calculation_ids":["CE-R2"],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-03","E-14","E-17","E-18"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":["DEFERRED_CALCULATOR_REVIEW","COMPOSITE_CASE_KIND_SUBTYPE_REVIEW"]},{"calculation_domain_ids":["CE-01","CE-03","CE-09","CE-13"],"candidate_profile_ids":[],"case_kind":"근로자의 임금 및 퇴직금 청구","case_kind_id":"CK-056","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-17"],"required_profile_ids":["SLP-LABOR-STANDARDS"],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"투자금반환 청구","case_kind_id":"CK-057","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-13"],"candidate_profile_ids":[],"case_kind":"특허권전용실시대금・실용신안권실시대금 청구","case_kind_id":"CK-058","deferred_calculation_ids":["CE-R1"],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-20"],"required_profile_ids":["SLP-IP"],"requires_underlying_leaf":false,"review_codes":["DEFERRED_CALCULATOR_REVIEW"]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-04","CE-06","CE-07","CE-13"],"candidate_profile_ids":[],"case_kind":"하자보수비・할부대금・화해금・확약금・환급금・회원가입비 등","case_kind_id":"CK-059","deferred_calculation_ids":[],"dispute_type":"금전의 지급을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02","E-04","E-07","E-18"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":["COMPOSITE_CASE_KIND_SUBTYPE_REVIEW"]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"[종류물(대체물)의 지급 또는 인도를 구하는 소 일반형]","case_kind_id":"CK-060","deferred_calculation_ids":[],"dispute_type":"종류물(대체물)의 지급 또는 인도를 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-04","CE-07","CE-13"],"candidate_profile_ids":[],"case_kind":"토지의 인도를 구하는 소","case_kind_id":"CK-061","deferred_calculation_ids":[],"dispute_type":"특정물의 인도를 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-11"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-04","CE-07","CE-13"],"candidate_profile_ids":["SLP-HOUSING-LEASE","SLP-COMMERCIAL-LEASE"],"case_kind":"임대인의 건물철거청구와 임차인의 건물매수청구권 행사","case_kind_id":"CK-062","deferred_calculation_ids":[],"dispute_type":"특정물의 인도를 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2","X1"],"required_primary_domain_ids":["E-08","E-11"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":["SPECIAL_LAW_PROFILE_ACTIVATION_REVIEW"]},{"calculation_domain_ids":["CE-01","CE-04","CE-07","CE-13"],"candidate_profile_ids":["SLP-HOUSING-LEASE","SLP-COMMERCIAL-LEASE"],"case_kind":"건물의 명도(인도)의 소","case_kind_id":"CK-063","deferred_calculation_ids":[],"dispute_type":"특정물의 인도를 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2","X1"],"required_primary_domain_ids":["E-08","E-11"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":["SPECIAL_LAW_PROFILE_ACTIVATION_REVIEW"]},{"calculation_domain_ids":["CE-04","CE-07","CE-13"],"candidate_profile_ids":[],"case_kind":"점유회수(반환)청구의 소","case_kind_id":"CK-064","deferred_calculation_ids":[],"dispute_type":"특정물의 인도를 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-11"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-04","CE-07","CE-13"],"candidate_profile_ids":[],"case_kind":"동산 등의 인도 청구","case_kind_id":"CK-065","deferred_calculation_ids":[],"dispute_type":"특정물의 인도를 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-11"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-04","CE-13"],"candidate_profile_ids":[],"case_kind":"매매를 원인으로 한 소유권이전등기 청구","case_kind_id":"CK-066","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-02","E-09"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-04","CE-13"],"candidate_profile_ids":[],"case_kind":"매수인의 잔대금지급의무와 매도인의 소유권이전등기 청구","case_kind_id":"CK-067","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-02","E-09"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-03","CE-04","CE-07","CE-13"],"candidate_profile_ids":[],"case_kind":"계약해제로 인한 원상회복의무와 손해배상의무의 관계","case_kind_id":"CK-068","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2","X1"],"required_primary_domain_ids":["E-01","E-04"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-04","CE-13"],"candidate_profile_ids":[],"case_kind":"교환・교환약정을 원인으로 한 소유권이전등기 청구","case_kind_id":"CK-069","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-02","E-09"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-04","CE-13"],"candidate_profile_ids":[],"case_kind":"대물반환・대물변제 등을 원인으로 한 소유권이전등기 청구","case_kind_id":"CK-070","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-02","E-09"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-04","CE-13"],"candidate_profile_ids":[],"case_kind":"명의신탁해지를 원인으로 한 소유권이전등기 청구","case_kind_id":"CK-071","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-09"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-04","CE-13"],"candidate_profile_ids":[],"case_kind":"취득시효완성을 원인으로 한 소유권이전등기 청구","case_kind_id":"CK-072","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-09"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-04","CE-11","CE-13"],"candidate_profile_ids":[],"case_kind":"양도약정・담보계약해지・이관을 원인으로 한 소유권이전등기","case_kind_id":"CK-073","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2","X1"],"required_primary_domain_ids":["E-09","E-10"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-04","CE-08","CE-13"],"candidate_profile_ids":[],"case_kind":"재산분할・재산승계를 원인으로 한 소유권이전등기 청구","case_kind_id":"CK-074","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-09","E-15"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-04","CE-08","CE-13"],"candidate_profile_ids":[],"case_kind":"증여・유증・유류분반환을 원인으로 한 소유권이전 청구","case_kind_id":"CK-075","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-09","E-15"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-04","CE-13"],"candidate_profile_ids":[],"case_kind":"화해·환매를 원인으로 한 소유권이전등기 청구","case_kind_id":"CK-076","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2","X1"],"required_primary_domain_ids":["E-02","E-09"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-04","CE-13"],"candidate_profile_ids":[],"case_kind":"가등기에 기한 본등기 청구","case_kind_id":"CK-077","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-09"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-04","CE-10","CE-13"],"candidate_profile_ids":[],"case_kind":"대위에 의한 소유권이전등기 청구","case_kind_id":"CK-078","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-09","E-13"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-04","CE-11","CE-13"],"candidate_profile_ids":[],"case_kind":"소유권 이외의 권리 설정등기 및 이전등기 청구","case_kind_id":"CK-079","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-09","E-10"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-04","CE-13"],"candidate_profile_ids":[],"case_kind":"말소등기(소유권이전등기말소) 청구","case_kind_id":"CK-080","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-01","E-09"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-04","CE-13"],"candidate_profile_ids":[],"case_kind":"말소등기(소유권보존등기말소) 청구","case_kind_id":"CK-081","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-09"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-04","CE-11","CE-13"],"candidate_profile_ids":[],"case_kind":"말소등기(근저당권설정등기말소) 청구","case_kind_id":"CK-082","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-09","E-10"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-04","CE-13"],"candidate_profile_ids":[],"case_kind":"말소등기(가등기말소) 청구","case_kind_id":"CK-083","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-09"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-04","CE-11","CE-13"],"candidate_profile_ids":[],"case_kind":"말소등기(지상권설정등기말소) 청구","case_kind_id":"CK-084","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-09","E-10"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-04","CE-11","CE-13"],"candidate_profile_ids":[],"case_kind":"말소등기(전세권설정등기말소) 청구","case_kind_id":"CK-085","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-09","E-10"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-04","CE-10","CE-13"],"candidate_profile_ids":[],"case_kind":"말소등기(대위에의한소유권이전등기말소) 청구","case_kind_id":"CK-086","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-09","E-13"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-04","CE-11","CE-13"],"candidate_profile_ids":[],"case_kind":"말소등기(토지의일부만에관하여말소등기) 청구","case_kind_id":"CK-087","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-09","E-12"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-04","CE-13"],"candidate_profile_ids":[],"case_kind":"말소등기(폐쇄한등기기록에기록된등기의말소) 청구","case_kind_id":"CK-088","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-09"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-04","CE-13"],"candidate_profile_ids":[],"case_kind":"말소등기(사위(詐僞)판결에의한등기의말소) 청구","case_kind_id":"CK-089","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-01","E-09"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-04","CE-13"],"candidate_profile_ids":[],"case_kind":"말소등기(기타등기에관한말소) 청구","case_kind_id":"CK-090","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-09"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-04","CE-13"],"candidate_profile_ids":[],"case_kind":"말소등기의 회복등기 청구","case_kind_id":"CK-091","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-09"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-04","CE-13"],"candidate_profile_ids":[],"case_kind":"경정등기 청구","case_kind_id":"CK-092","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-09"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-04","CE-13"],"candidate_profile_ids":[],"case_kind":"등기상 이해관계 있는 제3자의 승낙의 의사표시","case_kind_id":"CK-093","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-03","E-09"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-04","CE-13"],"candidate_profile_ids":[],"case_kind":"진정명의 회복을 등기원인으로 하는 소유권이전등기 청구","case_kind_id":"CK-094","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-09"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-04","CE-13"],"candidate_profile_ids":[],"case_kind":"명의변경절차에 관한 소송","case_kind_id":"CK-095","deferred_calculation_ids":[],"dispute_type":"의사의 진술을 구하는 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-09"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-13"],"candidate_profile_ids":[],"case_kind":"장래이행의 소","case_kind_id":"CK-096","deferred_calculation_ids":[],"dispute_type":"특수한 유형의 이행의 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X1","E-00"],"required_primary_domain_ids":[],"required_profile_ids":[],"requires_underlying_leaf":true,"review_codes":["CATALOG_UNDERLYING_LEAF_REQUIRED"]},{"calculation_domain_ids":["CE-04","CE-07","CE-13"],"candidate_profile_ids":[],"case_kind":"소유물방해제거·방해예방청구","case_kind_id":"CK-097","deferred_calculation_ids":[],"dispute_type":"특수한 유형의 이행의 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-11"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-05","CE-13"],"candidate_profile_ids":[],"case_kind":"정정보도·반론보도·사과광고 청구","case_kind_id":"CK-098","deferred_calculation_ids":[],"dispute_type":"특수한 유형의 이행의 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X1"],"required_primary_domain_ids":["E-21"],"required_profile_ids":["SLP-MEDIA-MEDIATION"],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-04","CE-13"],"candidate_profile_ids":[],"case_kind":"토지거래허가신청의 협력의무 이행청구","case_kind_id":"CK-099","deferred_calculation_ids":[],"dispute_type":"특수한 유형의 이행의 소","major_category":"이행의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02","E-09"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"채권존재확인 청구","case_kind_id":"CK-100","deferred_calculation_ids":[],"dispute_type":"채권에 관한 확인을 구하는 소","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"채권부존재확인 청구","case_kind_id":"CK-101","deferred_calculation_ids":[],"dispute_type":"채권에 관한 확인을 구하는 소","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"채무부존재확인 청구","case_kind_id":"CK-102","deferred_calculation_ids":[],"dispute_type":"채권에 관한 확인을 구하는 소","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-07","CE-13"],"candidate_profile_ids":["SLP-HOUSING-LEASE","SLP-COMMERCIAL-LEASE"],"case_kind":"임차권 확인 청구","case_kind_id":"CK-103","deferred_calculation_ids":[],"dispute_type":"채권에 관한 확인을 구하는 소","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2","X1"],"required_primary_domain_ids":["E-08"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":["SPECIAL_LAW_PROFILE_ACTIVATION_REVIEW"]},{"calculation_domain_ids":["CE-04","CE-07","CE-13"],"candidate_profile_ids":[],"case_kind":"부동산 소유권 확인","case_kind_id":"CK-104","deferred_calculation_ids":[],"dispute_type":"물권에 관한 확인을 구하는 소","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-11"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-13"],"candidate_profile_ids":[],"case_kind":"도메인 소유권","case_kind_id":"CK-105","deferred_calculation_ids":[],"dispute_type":"물권에 관한 확인을 구하는 소","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-20"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-04","CE-07","CE-13"],"candidate_profile_ids":[],"case_kind":"입목 소유권","case_kind_id":"CK-106","deferred_calculation_ids":[],"dispute_type":"물권에 관한 확인을 구하는 소","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-11"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-13"],"candidate_profile_ids":[],"case_kind":"통상실시권 확인","case_kind_id":"CK-107","deferred_calculation_ids":["CE-R1"],"dispute_type":"물권에 관한 확인을 구하는 소","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-20"],"required_profile_ids":["SLP-IP"],"requires_underlying_leaf":false,"review_codes":["DEFERRED_CALCULATOR_REVIEW"]},{"calculation_domain_ids":["CE-04","CE-07","CE-13"],"candidate_profile_ids":[],"case_kind":"불상·신탁·차등차 등 소유권 확인","case_kind_id":"CK-108","deferred_calculation_ids":[],"dispute_type":"물권에 관한 확인을 구하는 소","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-11"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-11","CE-13"],"candidate_profile_ids":[],"case_kind":"유치권확인 또는 부존재확인","case_kind_id":"CK-109","deferred_calculation_ids":[],"dispute_type":"물권에 관한 확인을 구하는 소","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-10"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-13"],"candidate_profile_ids":[],"case_kind":"[증서의 진정여부를 확인하는 소 일반형]","case_kind_id":"CK-110","deferred_calculation_ids":[],"dispute_type":"증서의 진정여부를 확인하는 소","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","E-00"],"required_primary_domain_ids":[],"required_profile_ids":[],"requires_underlying_leaf":true,"review_codes":["CATALOG_UNDERLYING_LEAF_REQUIRED"]},{"calculation_domain_ids":["CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"대의원회결의무효 등","case_kind_id":"CK-111","deferred_calculation_ids":[],"dispute_type":"각종 무효확인 청구","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-18"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"이사회결의무효","case_kind_id":"CK-112","deferred_calculation_ids":[],"dispute_type":"각종 무효확인 청구","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-18"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"총회결의무효확인","case_kind_id":"CK-113","deferred_calculation_ids":[],"dispute_type":"각종 무효확인 청구","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-18"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"이사회결의부존재","case_kind_id":"CK-114","deferred_calculation_ids":[],"dispute_type":"각종 결의부존재확인 청구","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-18"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"임시주주총회결의부존재 등","case_kind_id":"CK-115","deferred_calculation_ids":[],"dispute_type":"각종 결의부존재확인 청구","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-18"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"기타 결의부존재확인","case_kind_id":"CK-116","deferred_calculation_ids":[],"dispute_type":"각종 결의부존재확인 청구","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-18"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"교원의 지위","case_kind_id":"CK-117","deferred_calculation_ids":[],"dispute_type":"각종 지위확인 청구","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-18"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":["PUBLIC_LAW_NEXUS_REVIEW"]},{"calculation_domain_ids":["CE-01","CE-03","CE-09","CE-13"],"candidate_profile_ids":[],"case_kind":"근로자의 지위 등","case_kind_id":"CK-118","deferred_calculation_ids":[],"dispute_type":"각종 지위확인 청구","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-17"],"required_profile_ids":["SLP-LABOR-STANDARDS"],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"분양권·매수인의 지위 등","case_kind_id":"CK-119","deferred_calculation_ids":[],"dispute_type":"각종 지위확인 청구","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-02","E-18"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"기타 지위확인","case_kind_id":"CK-120","deferred_calculation_ids":[],"dispute_type":"각종 지위확인 청구","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-18"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-13"],"candidate_profile_ids":[],"case_kind":"공탁금출급청구권","case_kind_id":"CK-121","deferred_calculation_ids":[],"dispute_type":"각종 권리확인 청구","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-14"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-13"],"candidate_profile_ids":[],"case_kind":"보상금·수용금수령권","case_kind_id":"CK-122","deferred_calculation_ids":[],"dispute_type":"각종 권리확인 청구","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-14"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":["PUBLIC_LAW_NEXUS_REVIEW"]},{"calculation_domain_ids":["CE-03","CE-04","CE-13"],"candidate_profile_ids":[],"case_kind":"분양권 확인 청구","case_kind_id":"CK-123","deferred_calculation_ids":[],"dispute_type":"각종 권리확인 청구","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-09","E-18"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-04","CE-07","CE-13"],"candidate_profile_ids":[],"case_kind":"시설출입이용권","case_kind_id":"CK-124","deferred_calculation_ids":[],"dispute_type":"각종 권리확인 청구","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-11","E-18"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-04","CE-07","CE-11","CE-13"],"candidate_profile_ids":[],"case_kind":"주위토지통행권","case_kind_id":"CK-125","deferred_calculation_ids":[],"dispute_type":"각종 권리확인 청구","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-11","E-12"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"주주권·주식질권 등","case_kind_id":"CK-126","deferred_calculation_ids":[],"dispute_type":"각종 권리확인 청구","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-18"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-13"],"candidate_profile_ids":[],"case_kind":"기타 권리확인","case_kind_id":"CK-127","deferred_calculation_ids":[],"dispute_type":"각종 권리확인 청구","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","E-00"],"required_primary_domain_ids":[],"required_profile_ids":[],"requires_underlying_leaf":true,"review_codes":["CATALOG_UNDERLYING_LEAF_REQUIRED"]},{"calculation_domain_ids":["CE-13"],"candidate_profile_ids":[],"case_kind":"[기타 확인청구 일반형]","case_kind_id":"CK-128","deferred_calculation_ids":[],"dispute_type":"기타 확인청구","major_category":"확인의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","E-00"],"required_primary_domain_ids":[],"required_profile_ids":[],"requires_underlying_leaf":true,"review_codes":["CATALOG_UNDERLYING_LEAF_REQUIRED"]},{"calculation_domain_ids":["CE-04","CE-11","CE-13"],"candidate_profile_ids":[],"case_kind":"경계확정 청구","case_kind_id":"CK-129","deferred_calculation_ids":[],"dispute_type":"","major_category":"형성의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-12"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-04","CE-11","CE-13"],"candidate_profile_ids":[],"case_kind":"공유물분할 청구","case_kind_id":"CK-130","deferred_calculation_ids":[],"dispute_type":"","major_category":"형성의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","X2"],"required_primary_domain_ids":["E-12"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-13"],"candidate_profile_ids":[],"case_kind":"청구이의","case_kind_id":"CK-131","deferred_calculation_ids":[],"dispute_type":"","major_category":"형성의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-14"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-01","CE-02","CE-13"],"candidate_profile_ids":[],"case_kind":"제3자이의","case_kind_id":"CK-132","deferred_calculation_ids":[],"dispute_type":"","major_category":"형성의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-14"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-13"],"candidate_profile_ids":[],"case_kind":"재심 청구","case_kind_id":"CK-133","deferred_calculation_ids":[],"dispute_type":"","major_category":"형성의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","E-00"],"required_primary_domain_ids":[],"required_profile_ids":[],"requires_underlying_leaf":true,"review_codes":["CATALOG_UNDERLYING_LEAF_REQUIRED"]},{"calculation_domain_ids":["CE-13"],"candidate_profile_ids":[],"case_kind":"준재심 청구","case_kind_id":"CK-134","deferred_calculation_ids":[],"dispute_type":"","major_category":"형성의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","E-00"],"required_primary_domain_ids":[],"required_profile_ids":[],"requires_underlying_leaf":true,"review_codes":["CATALOG_UNDERLYING_LEAF_REQUIRED"]},{"calculation_domain_ids":["CE-13"],"candidate_profile_ids":[],"case_kind":"재권판결에 대한 불복의 소","case_kind_id":"CK-135","deferred_calculation_ids":[],"dispute_type":"","major_category":"형성의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3","E-00"],"required_primary_domain_ids":[],"required_profile_ids":[],"requires_underlying_leaf":true,"review_codes":["CATALOG_UNDERLYING_LEAF_REQUIRED"]},{"calculation_domain_ids":["CE-03","CE-04","CE-10","CE-13"],"candidate_profile_ids":[],"case_kind":"사해행위취소 청구","case_kind_id":"CK-136","deferred_calculation_ids":[],"dispute_type":"","major_category":"형성의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-13"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]},{"calculation_domain_ids":["CE-03","CE-13"],"candidate_profile_ids":[],"case_kind":"주주총회결의취소의 소","case_kind_id":"CK-137","deferred_calculation_ids":[],"dispute_type":"","major_category":"형성의 소","mapping_basis":"ensemble_v2 legal-domain coverage; catalog ordinal mapping; regression only","required_cross_domain_ids":["X3"],"required_primary_domain_ids":["E-18"],"required_profile_ids":[],"requires_underlying_leaf":false,"review_codes":[]}],"schema_version":"s6_case_kind_domain_matrix.v2","source":{"declared_total":148,"equation":"148-7-1-3=137","excluded_count":11,"file_sha256":"27f452f50ad57f9e45e98b9a64d9999f4256fc27db6110c9bf29a207c4deea3e","included_count":137,"path":"case_kinds/case_kinds.md"}} diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/stage_1_2_3_assets_distribution_location.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/stage_1_2_3_assets_distribution_location.md index 2cd155ba..6f29a1e1 100644 --- a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/stage_1_2_3_assets_distribution_location.md +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/stage_1_2_3_assets_distribution_location.md @@ -1,169 +1,37 @@ -# Stage 1 Part 1·2·3 자산 배포 위치 총람 +# Stage 1 Part 1·2·3 자산 배포 — Default_Agent 변화 기록 (2026-08-20 자산 정리 회차) -- 근거: `stage_1_part_1_v.8.yml`(6,668행) · `stage_1_part_2_v.8.yml`(3,315행) · `stage_1_part_3_v.8.yml`(1,038행) 전량 판독 + 배포 트리 실측 (2026-08-20) -- 크기: 배포 원본 트리(`v.7/extension_research/Default_Agent/`)에서 실측한 바이트 수. 실행 중 생성 산출물은 사건마다 달라져 "실행 생성"으로 표기. -- 표기: P1 = Part 1(사건개요파악·기초작업), P2 = Part 2(BO·시그널 생성), P3 = Part 3(법률효과구조 생성) +- 이 문서는 `problems_identified_in_detecting_assets_location_v.2.md` §2-B 작업의 실행 결과로 **배포 원본 `extension_research/Default_Agent/` 에 발생한 변화만**을 기록한다. +- 변화 이전의 전체 배포 위치 총람은 `stage_1_2_3_assets_distribution_location_old.md`(2026-08-20 조사판)에 보존돼 있다 — 아래 두 변화를 제외한 모든 내용은 그 문서가 그대로 유효하다. --- -## 0. 배포 루트 3계층 (세 YAML 공통 규약) +## 1. `runtime_manifest.json` — P1 자산 6종 등재 (426 → 432항) -| 계층 | 위치 | 성격 | +v.2 §2-B "P1 자산 6종 manifest 등재" 의 실행이다. 해시 앵커가 없던 Part 1 소비 자산 6종의 sha256 행을 배포 원본 매니페스트에 **직접 추가**했다(이 회차 관례 — 빌더 등록은 조립본 동기화 회차의 이월 묶음). `runtime_artifact_count` 를 426 → **432** 로 동반 갱신했고(P2·P3 게이트의 count 등식·중복 경성 검사 대응), indent=2 왕복 직렬화를 보존했다. + +| 등재 경로 (`Default_Agent/` 기준) | 크기 | sha256 (앞 16) | |---|---|---| -| ① 정적 자산 루트 (`ASSET_ROOT`) | localdocs 논리 접두 **`Default_Agent/`** ← 배포 원본 **`v.7/extension_research/Default_Agent/`** 트리(210파일, .DS_Store 제외)를 통째로 배포 | P2·P3 remedy 명문: "배포 원본 extension_research/Default_Agent 트리를 서버 Default_Agent/에 통째로 배포하고 runtime_manifest.json의 등재·sha256과 대조. **부분 복사 불허**" | -| ② 사건 워크스페이스 루트 | localdocs 문서 루트 (clientInfo에 `{{__user_hash__}}`/`{{__workspace_hash__}}` 필수 — 빠지면 루트 경로를 봄) | 접두 없는 상대경로 전부: `client_meeting.md`, `BO.json`, `routing/`, `quality_gates/`, `signals/`, `runtime/`, `fanout/`, `stage1_tmp/`, `validation_assets/`, `evidence_*` … | -| ③ 실행 샌드박스 | code-executor 컨테이너 `EXECUTION_ROOT=/tmp/s1` (R0만 `/tmp/s1_r0`) — 모듈 반입처 `_rt/`, S0 signal 패키지 `_sig/`·`_signal_out/` | 임시 스테이징. 배포 대상 아님. 실행 인자 계약 `--asset-root/--execution-root/--logical-root` (D-5 §2.4 0-c-2) | +| `platform/contracts/determinism_scope_contract.json` | 1,473 B | `8734102d9e2e9b69…` | +| `platform/reference_catalogs/calculation_ids.json` | 1,139 B | `fb14965b8315e01d…` | +| `platform/reference_catalogs/domain_ids.json` | 270 B | `4740d98e6a31bd0e…` | +| `platform/schemas/client_goal_domain_profiles.schema.json` | 13,082 B | `ae2bfe0d754a09cb…` | +| `routing/activation_cue_digest.md` | 35,110 B | `7e98885a50e1880b…` | +| `routing/evidence_component_union.md` | 17,593 B | `ecd6e1877f476139…` | -**모듈 미러 규약**: localdocs가 `.py`를 서빙하지 않으므로 모든 런타임 `.py`는 **같은 디렉터리에 바이트 동일 `.txt` 미러**로 배포된다(유일 예외 = 빌더 `build_default_agent_stage1.py`). 반입 시 `Default_Agent/runtime_manifest.json`의 sha256과 대조하고 불일치면 실행 금지. 아래 표의 `.txt` 미러는 전부 `.py` 정본이 같은 디렉터리에 병존한다(크기 동일). +갱신 후 매니페스트: **67,474 B · 432항 · sha256 `cebed7ff4bbbe9b4…`**. 등재는 **복원 기준** 확보다 — 무결성 강제(게이트)까지는 P1 YAML 수정이 필요하며 그것은 별도 설계 판단으로 남는다(v.2 §2-B 명문). -**runtime_manifest.json**: `stage1_runtime_manifest.v1`, 426건 등재. computations/ 등 Stage 1 Part 1~3 밖 자산도 등재되어 있으며 그 물리 실체는 `extension_research/선행구축/Default_Agent_Stage_1/`(1,322파일) 및 `ver_8_yaml_candidates/Default_Agent_Stage_1/`(560파일, 빌드·검증 산출 트리)에 있다. P1~P3 게이트는 각 Part가 선언한 필수 자산 목록에 대해서만 등재·sha256·가독을 검사한다. +## 2. `validation_assets/routing/case_kind_domain_matrix.v2.json` — 배포 원본에서 제거 ---- +v.2 §2-B 선택 항목의 실행이다. 런타임 읽기 금지가 명문(P1 프롬프트 2곳)인 오프라인 회귀 전용 자산(72,969 B)이 배포 트리에 있던 것을 제거해 트리 정의("실행이 읽는 자산 전부이자 그것만")에 정합시켰다. 제거 전 **조립본 원본(`선행구축/Default_Agent_Stage_1/validation_assets/routing/`)과 바이트 동일임을 확인**했다 — 정본은 조립본에 그대로 있다. 빈 폴더(`validation_assets/routing` · `validation_assets`)도 정리했다. -## 1. 공통자산 — 2개 Part 이상이 사용하는 사전배포 정적 자산 (`Default_Agent/`) +## 3. 변화 후 배포 원본 요약 -| 배포 위치 (`Default_Agent/` 이하) | 형태 | 크기(B) | 쓰는 task | 비고 (역할) | -|---|---|---|---|---| -| `runtime_manifest.json` | json | 66,526 | P1 D0 · P2 A0/R0/F0/S0 · P3 L0/L2 | 해시 장부(`stage1_runtime_manifest.v1`, entries[].path+sha256). 모든 모듈 반입·배포 게이트의 단일 근거. entries[].path는 `Default_Agent/` 접두를 뗀 상대경로 | -| `domains/_registry_index.json` | json | 18,921 | P1 T0-02/T0-03/D0/T9 · P2 A0 · P3 L0/L2 | 도메인 registry 색인 정본(registry_version, entries[].domain_id/config_path/config_sha256). 원문 read_raw sha256이 스크리닝·활성 매니페스트·핸드오프에 봉인됨. **매니페스트 미등재 — SG-01 봉인(activation manifest의 registry_index_sha256)이 교체를 잡음**. P2 R0에는 상수로만 선언(미사용) | -| `domains/{domain_id}/domain_config.json` | json ×26 | 2,468~17,324 (총 274,372) | P1 T0-03(후보만)/D0(전 26) · P2 A0 · P3 L0/L2(expected_runnable만) | 도메인별 config: 요건 슬롯·상대사실·증거 구성요소·항변·계산 바인딩·검토코드·structure_types 선언. registry index의 config_path만 따라 열람, config_sha256 사슬 대조. **LLM task(P1 T0-01·T1)는 직접 읽기 금지** | -| `stage1_runtime/runtime_common.txt` | txt(py 미러) | 4,496 | P1 D0 · P2 A0/R0 · P3 L0/L2 | 런타임 공용 모듈. `/tmp/s1/_rt/*.py`로 실체화 후 sys.path 삽입 | -| `stage1_runtime/schema_subset_validator.txt` | txt(py 미러) | 7,442 | P1 D0 · P2 A0/R0 · P3 L0/L2 | 닫힌 스키마 부분집합 검증기 — 산출물 스키마 검증의 공용 엔진 | -| `stage1_runtime/registry_loader.txt` | txt(py 미러) | 6,404 | P1 D0 · P2 A0/R0 · P3 L0 | registry 적재기(load_registry). P3 L0은 compiler 의존 모듈로만 스테이징 | -| `routing/evidence_component_union.md` | md | 17,593 | P1 T3(preflight) · P2 worker(어휘 참조) | 증거 구성요소 어휘 합집합(component_id + accepted_source_kinds + owner). `registry_component_ids`의 유일 허용 어휘 — 목록 밖 이름 생성 금지 | -| `routing/extension_payload_key_declarations.v1.json` | json | 23,071 | P2 A0(게이트)/F0 · P3 L0 | BOType 합집합·확장 payload declared_key_union·legacy_alias_targets 선언표(`stage1_extension_payload_key_declarations.v1`). 생성기 `routing/_build_extension_payload_declarations.py`(배포 시점 접이, 런타임 미실행) | -| `signals/signal_registry.v2.json` | json | 10,907 | P2 A0(게이트)/S0 · P3 L0(게이트만) | signal registry v2 — S0 스키마 $ref 폐포 walk의 출발점. P3에서는 배포 완전성 검사만 받고 파싱되지 않음 | -| `contracts/signals/s5_execution_contract.v2.json` | json | 1,451 | P2 A0(게이트)/S0 · P3 L0(게이트만) | S5 실행 계약 — S0가 `/tmp/s1/_sig/contracts/`에 배치(모듈의 parents[1]/contracts 산술 재현). P3에서는 게이트 전용 | -| `signals/adapters/sg01_activation_adapter.txt` | txt(py 미러) | 3,738 | P1 D0 · P2 S0 | SG-01 활성 어댑터 — D0 산출 매니페스트 자기검증(adapt_and_validate). 미러 위치는 `stage1_runtime/`이 아니라 `signals/adapters/`(YAML 헤더 명시) | -| `signals/schemas/domain_activation_manifest.schema.json` | json | 3,535 | P1 D0(stage, 실패 시 경고) · P2 S0(registry가 결정하는 $ref 폐포에 포함 — 배포된 signal_registry.v2.json 등재 기준) | 활성 매니페스트 스키마 — D0 모듈은 직접 읽지 않음(어댑터가 검증 담당). §3의 폐포 17종 집계(28,476B)에 이 파일이 포함되므로 바이트 합산 시 중복 주의 | +| 축 | 변화 전 | 변화 후 | +|---|---|---| +| 트리 파일 수(.DS_Store 제외) | 210 | **209** (−1: matrix 제거. 등재는 행 추가라 파일 수 무변) | +| `runtime_manifest.json` | 426항 | **432항** (+6) | +| 매니페스트 등재-실물 교집합 | 149 · 불일치 0 | **155 · 불일치 0** (신규 6종 포함 전수 재검) | +| 실물-미등재 | 61 | **54** (P1 자산 6종 등재 + matrix 제거로 −7) | +| 등재-실물없음(상위 장부 몫 — 설계) | 277 | 277 (무변 — v.2 §2-C: 복사 금지 확정) | -## 2. Part 1 전용 정적 자산 - -| 배포 위치 (`Default_Agent/` 이하) | 형태 | 크기(B) | 쓰는 task | 비고 (역할) | -|---|---|---|---|---| -| `routing/activation_cue_digest.md` | md | 35,110 | T0-01(preflight)/T0-02 | 라우팅 단어 사전 — 26개 도메인 활성 단서(P/N/C/OVR)+GLOBAL_DEFAULTS. "사실 원천이 아니다". 머리말에 registry_version·registry_index sha256 → 스냅숏 낡음 감지 | -| `platform/schemas/client_goal_domain_profiles.schema.json` | json | 13,082 | T1(preflight) | client_goal의 domain_profiles 칸 형태를 정하는 닫힌 스키마 — "형식만 정하고 내용은 만들지 않음" | -| `platform/reference_catalogs/domain_ids.json` | json | 270 | T0-03 | 도메인 ID 카탈로그 — 후보 멤버십 검사 | -| `platform/reference_catalogs/calculation_ids.json` | json | 1,139 | T0-03 | 계산 ID 카탈로그(entries[].calculation_id/status) — calc 바인딩 존재·ready 검사 | -| `stage1_runtime/activation_gate.txt` | txt(py 미러) | 66,715 | D0 | **도메인 활성 판정 모듈 본체** — "판정은 이 안에서만 일어난다. 결정 필드는 execution_eligible 하나뿐". main(--asset-root/--execution-root/--logical-root) 호출 | -| `platform/contracts/determinism_scope_contract.json` | json | 1,473 | (헤더 선행조건만) | 결정론 범위 계약 — P1 정적 자산 선행 조건 5종의 하나. task 본문 직접 등장 없음(모듈 내부 소비 추정) | -| `validation_assets/routing/case_kind_domain_matrix.v2.json` | json | 72,969 | **읽기 금지** (T0-01·T1에 금지 명문) | 사건유형 이름을 담은 **오프라인 회귀 전용** 자산 — 런타임 입력 금지 | - -## 3. Part 2 전용 정적 자산 - -| 배포 위치 (`Default_Agent/` 이하) | 형태 | 크기(B) | 쓰는 task | 비고 (역할) | -|---|---|---|---|---| -| `stage1_runtime/registry_validator.txt` | txt(py 미러) | 11,080 | A0 | registry 검증기(validate_registry) — overlay 4개 오류코드만 경성 | -| `stage1_runtime/prompt_compiler.txt` | txt(py 미러) | 15,164 | A0 | 프롬프트 조립기 — FragmentSpec/collect_domain_fragments/compile_fragments, rank 10→50 합성, 상한 96,000B/24,000자 | -| `stage1_runtime/domain_slice_compiler.txt` | txt(py 미러) | 32,300 | A0 | slice 컴파일러(compile_domain_slices) — 낡으면 PART2_COMPILER_STALE | -| `stage1_runtime/domain_fanout_planner.txt` | txt(py 미러) | 12,282 | A0 | fan-out 계획기(build_fanout_plan) | -| `stage1_runtime/stage_a_context_builder.txt` | txt(py 미러) | 25,885 | A0 | stage_a_context/source_universe_manifest 빌더(v3 세 builder 이식) | -| `stage1_runtime/worker_output_validator.txt` | txt(py 미러) | 21,302 | A0(게이트)/R0 | worker seed 산출 검증기(validate_worker_output; schema·slice 주입). 반입 실패는 경고 강등 | -| `stage1_runtime/prompt_composition_policy.json` | json | 1,715 | A0 | 프롬프트 조립 정책(rank 순서·상한) | -| `stage1_runtime/bo_surface_projection_policy.v1.json` | json | 6,355 | A0(게이트)/R0/F0 | BO 투영 정책(`stage1_bo_surface_projection_policy.v1`) — f0_normalization·review_item_projection 규칙 정본 | -| `platform/schemas/domain_slice.schema.v2.json` | json | 17,725 | A0 | slice 스키마 — F-3 능력검사(stage_b_domain_slice.domain_declarations·compiled_prompt.hash_kind 필수) | -| `platform/schemas/domain_fanout_plan.schema.json` | json | 11,148 | A0 | fan-out 계획 스키마(build_fanout_plan에 주입) | -| `platform/schemas/domain_seed_output.schema.v3.json` | json | 21,965 | A0(게이트)/worker(참조)/R0 | worker seed 산출 스키마 v3(additionalProperties:false 봉인) | -| `domains/_common/common_worker_contract.md` | md | 3,766 | A0 | rank 10 공통 worker 계약 조각 | -| `domains/{domain_id}/seed_prompt_overlay.md` | md ×25 | 1,026~5,095 (총 83,177) | A0 | 도메인별 프롬프트 overlay 조각 — config의 `prompt_overlay_ref`가 선언한 경로 추적(실패 시 경고). EC-00은 대신 `common_fragment.md`(1,850B) 보유 | -| `signals/compiler/{common, projections, schema_validator, signal_compiler, signal_gate, transaction_writer, writer_boundary}.txt` | txt(py 미러) ×7 | 1,089~11,095 (총 37,276) | S0 | signal 컴파일러 모듈 — `/tmp/s1/_sig/pkg/signals/compiler/`로 실체화. **파일 기록 권한은 transaction_writer 하나**(canonical_writer_uniqueness) | -| `signals/adapters/{s3_domain_seed_adapter, s3_envelope_migration_adapter, s4_calculation_adapter}.txt` | txt(py 미러) ×3 | 1,071~7,223 (총 9,578) | S0 | 어댑터 모듈(sg01은 공통표 참조 — S0 어댑터 4종 반입에 포함) | -| `signals/emitters/{emitter_runtime, emit_sg02..emit_sg13}.txt` | txt(py 미러) ×13 | 287~7,687 (총 11,131) | S0 | 생성기(emitter) — SG-02~SG-13 12종 + 공용 런타임 | -| `signals/schemas/*.json` (signal_manifest·domain_signal_envelope.v2·SG별 12종 등 15종) + `signals/_common/{signal_item, evidence_slot_status}.schema.json` 2종 | json ×17 | 1,133~3,535 (총 28,476) | S0 | registry 선언에서 출발한 **상대 $ref 폐포 17종** — 목록 상수화 금지(F-4c), registry가 동적으로 결정 | - -## 4. Part 3 전용 정적 자산 - -| 배포 위치 (`Default_Agent/` 이하) | 형태 | 크기(B) | 쓰는 task | 비고 (역할) | -|---|---|---|---|---| -| `stage1_runtime/structure_index_compiler.txt` | txt(py 미러) | 27,309 | L0 | **구조 seed 컴파일 모듈**(compile_structure_seeds) — Θ 조립·구조 seed·signal 조인. "판정 규칙을 YAML에 쓰지 않는다" | -| `stage1_runtime/structure_index_policy.v1.json` | json | 7,032 | L0/L2 | 구조 색인 정책(`stage1_structure_index_policy.v1`) — 불일치 시 PART3_POLICY_INVALID | -| `platform/schemas/structure_seed_bundle.schema.json` | json | 9,128 | L0 | seed bundle 닫힌 스키마 — theta·admission_policy 인지 능력검사(PART3_STRUCTURE_SCHEMA_STALE) | -| `platform/schemas/legal_effect_structures.schema.json` | json | 6,809 | L0(게이트만)/L2 | 최종 색인 닫힌 스키마 — by_module·by_bo_id 인지 능력검사(PART3_INDEX_SCHEMA_STALE) | -| `domains/{domain_id}/structure_types.json` | json ×25 | 390~1,360 (총 21,006) | L0 | 도메인 사이드카 1 — 구조 유형 정의. structure_types 선언 있는 도메인만 반입, 부재·미등재는 경성 실패 | -| `domains/{domain_id}/module_role_projection.json` | json ×25 | 382~1,196 (총 18,248) | L0 | 도메인 사이드카 2 — 모듈 역할 사영. 위와 동일 규율 | - -## 5. 사건 입력 자산 (실행 전 사건 워크스페이스에 존재해야 함) - -| 배포 위치 (워크스페이스 루트 이하) | 형태 | 크기 | 쓰는 task | 비고 (역할) | -|---|---|---|---|---| -| `client_meeting.md` | md | 사건별 | P1 T0-01/T0-02/T1/T3/T4 · P2 A0 | 상담록 — "유일한 사실 원천". source unit(MTG.*) 채번·재산출 대조, meeting_clause_ids의 원천 | -| `evidence_all.json` | json | 사건별 | P1 T2 | 증거 문서 전체 배열 — 배열 순서 = ordinal 부여 기준(array_order, 법적 재정렬 금지) | - -## 6. 실행 중 생성 산출물 (전부 사건 워크스페이스 루트 이하; 크기 = 실행 생성) - -### 6.1 Part 1 산출 (생산 → 소비) - -| 위치 | 형태 | 생산 → 소비 | 비고 (스키마/역할) | -|---|---|---|---| -| `routing/domain_screening_draft.json` | json | T0-01 → T0-02 | 스크리닝 개방형 초안(판단 4종). `stage1_domain_screening_draft.v1` | -| `routing/domain_screening.json` | json | T0-02 → T0-03/T1/D0/T9 · **P2 A0** | 봉인된 닫힌 스키마 스크리닝(`stage1_domain_screening.v1`, 비판단 6종 부착). T9·P2가 원문 sha256 봉인 대조 | -| `routing/candidate_profile_vocabulary.md` | md | T0-03 → T1 · **P2 A0**(부재 허용) | 후보 도메인 낱말 목록(E\|O\|V\|D\|R 줄, 예산 28,000B) — "registry 원문이 정본, 이 파일은 파생본" | -| `client_goal.json` | json | T1 → (Part 1 내 소비 없음, 후속 스테이지용) | 고객 소송목적·당사자·사건군 구조화 + domain_profiles. D0 입력 6종에 불포함 | -| `evidence_shard_plan.json` | json | T2 → T5/T6 | fan-out/배리어/stale 정책 플랜(`evidence_shard_plan.v1`). stdout의 dynamic_fanout을 오케스트레이터가 소비 | -| `evidence_shards/E-{nnn}.json` | json ×N | T2 → T3/T4 (T5 발췌) | 문서별 단일 evidence shard — mapper가 풀 대신 자기 shard만 읽도록 분리 | -| `evidence_indexed_parts/E-{nnn}.json` | json ×N | T3 → T4/T5 | B1 mapper part(`evidence_indexed_part.v4`) | -| `evidence_event_candidate_parts/E-{nnn}.json` | json ×N | T4 → T6 | B2 mapper part(`evidence_event_candidate_part.v4`) | -| `evidence_indexed.json` | json | T5 → D0/T9 · **P2 A0** | 최종 증거 authority catalog(`evidence_indexed.v3`) — 작성은 quality gate 단독 권한 | -| `evidence_event_candidates.json` | json | T6 → D0/T9 · **P2 A0** | 최종 event/state candidate(`evidence_event_candidates.v1`) | -| `stage1_tmp/quality_gate/finalized_evidence_id_manifest.json` | json | T5 → T6 | 확정 evidence id 집합 + dropped_aliases(`finalized_evidence_id_manifest.v1`) | -| `stage1_tmp/quality_gate/B1_precheck.json` | json | T5 → T6/T8 | B1 결정적 검사·suspects(`B1_precheck.v1`) | -| `stage1_tmp/quality_gate/B2_precheck.json` | json | T6 → T8 | B2 결정적 검사·suspects(`B2_precheck.v1`) | -| `stage1_tmp/quality_gate/B12_exception_pack.json` | json | T6 → T7(preflight)/T8 | GB 판정용 compact exception pack(`B12_exception_pack.v1`) — T7의 유일 입력 | -| `stage1_tmp/quality_gate/B12_adjudication_decisions.json` | json | T7 → T8 | exception별 LLM 판정(`B12_adjudication_decisions.v1`) | -| `quality_gates/B1_evidence_indexed_gate.json` | json | T8 → D0/T9 | B1 최종 audit(`B1_quality_gate.v3`) | -| `quality_gates/B2_event_candidates_gate.json` | json | T8 → D0/T9 | B2 최종 audit(`B2_quality_gate.v3`) | -| `routing/domain_activation_manifest.json` | json | D0 → T9 · **P2 A0/S0 · P3 L0/L2** | 도메인 활성 확정 매니페스트 — 모듈 산출 바이트 그대로 업로드(재직렬화 금지). execution_eligible·expected_runnable_domain_ids·registry_index_sha256(SG-01 봉인) | -| `quality_gates/stage1_part1_soft_gate_handoff.json` | json | T9 → OUT · **P2 A0** | soft gate handoff sidecar(`stage1_part1_soft_gate_handoff.v1`) — digest_guard 7종·활성봉인 교차검증·review 큐 | - -### 6.2 Part 2 산출 - -| 위치 | 형태 | 생산 → 소비 | 비고 (스키마/역할) | -|---|---|---|---| -| `runtime/compiled_prompts/{domain_id}.md` | md ×도메인 | A0 → worker(preflight) | 도메인별 조립 프롬프트(rank 10→50 합성) — 재조립 금지. 워크스페이스+로컬 이중 기록 | -| `runtime/domain_slices/{domain_id}.json` | json ×도메인 | A0 → worker/R0/S0 · **P3 L0(옵션)** | 도메인 slice(`task_c_bo_stage_b_domain_slice.v2`) — source_universe·allowed_legal_effect_bo_types의 원천 | -| `runtime/domain_seed_outputs/{domain_id}.json` | json ×도메인 | worker → R0/S0 | worker seed(`task_c_bo_stage_b_domain_bo_seed.v3`) — **기록자는 워커 유일**(R0 되쓰기 금지) | -| `fanout/domain_fanout_plan.json` | json | A0 → R0/S0 (+오케스트레이터 {{item}} 주입원) | fan-out 계획(`domain_fanout_plan.v1`) — task_instances[]가 worker 인스턴스 생성, barrier 기대집합=expected_runnable | -| `stage1_tmp/task_c_bo/stage_a_context.json` | json | A0 → R0/F0 | stage_a_context(`task_c_bo_stage_a_context.v1`) — P-1 개정으로 구 경로 유지 | -| `stage1_tmp/task_c_bo/source_universe_manifest.json` | json | A0 → R0/S0 | 원천 우주 매니페스트(evidence_index_set·event_candidate_ids·meeting_clause_ids) | -| `validation_assets/routing/stage_receipt.json` | json | A0 | 스테이지 영수증(`stage1_stage_receipt.v2`) — schema_injection·deployment_gate 실측 기록 | -| `stage1_tmp/task_c_bo/postb_seed_ledger.json` | json | R0 → F0 | 원장(`task_c_bo_postb_seed_ledger.v1`): 투영 ledger_candidates + deterministic_decisions | -| `quality_gates/stage1_part2_exception_pack.json` | json | R0 → R1(preflight, 유일 입력)/F0 | 예외 pack(`stage1_part2_exception_pack.v1`) — budget 8후보/2,000자 | -| `quality_gates/stage1_part2_review_handoff.json` | json | R0 → F0(읽고 되씀) | 검토 핸드오프(`stage1_part2_review_handoff.v1`): PENDING_FINALIZE → FINALIZED | -| `stage1_tmp/task_c_bo/postb_adjudication_decisions.json` | json | R1 → F0 | R1 판정(`task_c_bo_postb_exception_adjudication.v1`) — 예외 없어도 no-exception 객체 필수 | -| `BO.json` | json | F0(기록+재읽기) → S0 · **P3 L0/L2** | **최종 BO 배열**(bh1..bhN) — 워크스페이스 루트. P3 보존 등식의 BO 계수 원본. 무봉인 입력(P3 receipt에 관찰 기록) | -| `stage1_tmp/task_c_bo/postb_compiled_bundle_compact.json` | json | F0 | compact bundle(`task_c_bo_postb_compiled_bundle_compact.v1`): ref→bo_id 표·정규화 노트 | -| `signals/signal_manifest.json` | json | S0(기록+재읽기 해시 대조) → **P3 L0**(진입 봉인; L2는 재독 금지) | signal 거래 매니페스트 — transaction_id `S5TX-[a-f0-9]{20}`, files[].file_sha256, downstream_read_sets. files[].path에는 `signals/` 접두 없음(붙여서 대조) | -| `signals/**/*.json` (거래 산출 전량) | json | S0 → **P3 L0** | 정본 signal 번들 — `/tmp/s1/_signal_out`에 쓰고 `signals/` 접두로 바이트 그대로 반출. 파일명이 코드에 박힌 것은 SG-05/08/13 셋뿐, 나머지는 emitter 12종+registry가 결정. P3 소비 확인분: `legal_effect_routes.json`(SG-13, L0 조인 주근거+L2 route 보존 등식), `legal_relation_lifecycle_signals.json`(SG-05), `calculation_requirements.json`(SG-11), `procedural_posture_relief_signals.json`(SG-02, 빈 read set), `asset_right_state_signals.json`(SG-07, 빈 read set), `domain_signals/{domain_id}.json`(옵션) | -| `signals/compatibility_views/{actio_case, case_liability, legal_effect}_signals.json` | json ×3 | S0 | 호환 뷰 — actio_case(사영원 없음), case_liability(SG-05·08 사영), legal_effect(SG-13 사영) | -| `actio_case_signals.json` · `case_liability_signals.json` · `legal_effect_signals.json` (워크스페이스 루트) | json ×3 | S0 | 구 이름 별칭 — Part 3·4 최대 호환면, compatibility_views와 바이트 동일 | - -### 6.3 Part 3 산출 - -| 위치 | 형태 | 생산 → 소비 | 비고 (스키마/역할) | -|---|---|---|---| -| `stage1_tmp/task_le/structure_seed_bundle.json` | json | L0 → L2 | seed bundle(`stage1_structure_seed_bundle.v1`): theta·structure_records·counts + signal_manifest_transaction_id 승계 — L0→L2 유일한 중간 계약 | -| `quality_gates/stage1_part3_review_handoff.json` | json | L0 → L2(읽고 되씀) | 검토 핸드오프(`stage1_part3_review_handoff.v1`): OPEN → FINALIZED. review_items 근거 없는 감축 금지 | -| `validation_assets/routing/part3_receipt.json` | json | L0 | P3-L0 실측 영수증(`stage1_part3_receipt.v1`) — "상수 PASS는 증거가 아니다". L2는 receipt 미작성 | -| `legal_effect_structures.json` | json | L2(기록+재읽기 sha256 대조) → OUT | **Stage 1 최종 산출** — 법률효과 구조 색인(`stage1_legal_effect_structures.v1`): theta·structure_records·structure_index(by_domain_id/by_module/by_bo_id)·quality_gate(보존등식 3종). 워크스페이스 루트, canonical JSON | - -## 7. 참조 전용·빌드타임 자산 (런타임 파일 접근 없음) - -| 자산 | 물리 위치 | 형태/크기(B) | 인용처 | 비고 | -|---|---|---|---|---| -| `SKILL.md` | 경로 미명시(유일하게 접두 없는 인용) | md | P1·P2·P3 code-executor 보일러플레이트 주석("5장/5.2장", "§3.3 stdout 규약") | localdocs·code-executor 사용 규약 문서 | -| `platform/schemas/domain_screening.schema.json` | 선행구축 트리(예: `선행구축/Default_Agent_Stage_1/platform/schemas/`) — **extension_research/Default_Agent에는 없음** | json | P1 T0-02 주석 | screening_policy/contract_guards const의 정본 — 값은 코드에 하드코딩, 런타임에 읽지 않음 | -| `tools/build_default_agent_stage1.py` | `ver_8_yaml_candidates/Default_Agent_Stage_1/tools/` | py / 102,559 | P1 헤더(미러 규칙의 유일 예외) | Default_Agent 빌더 | -| `routing/_build_extension_payload_declarations.py` | `ver_8_yaml_candidates/Default_Agent_Stage_1/routing/` | py / 8,842 | P2 F0 주석 | 확장 payload 선언표(공통표 #8)의 생성기 — 배포 시점 접이 | -| `validation_assets/routing/_check_schema_keyword_support.py` | `ver_8_yaml_candidates/Default_Agent_Stage_1/validation_assets/routing/` | py / 8,233 | P2 A0 receipt 필드 | 키워드 지원 점검 스크립트 — receipt에 이름만 인용, 실행하지 않음 | -| 전략·명세 문서(`stage_1_update_strategy.md`, `P1-T2_to_T9_update_strategy.md`, `Part_1/2_Improvement_Strategy_Claude_v1.md`, `stage_1_part_2_optimal_update_strategy_v.2.md`, `stage_1_part_3_개정_신전략서.md` 등) 및 조각 출처 YAML(`Task_A_client_goal_v4.yml` 등) | `v.7/` 및 `ver_8_yaml_candidates/` 일대 | md/yml | 각 YAML 헤더·코드 주석 | 정본 근거 설계 문서 — 런타임 미접근 | - -## 8. 특기사항 (판독 중 확인된 사실) - -1. **게이트 전용 자산**: `signal_registry.v2.json`·`s5_execution_contract.v2.json`은 P3에서 등재·해시·가독 검사만 받고 어느 태스크도 파싱하지 않음. P2 A0의 PART2_REQUIRED_ASSETS는 10종이며, 그중 A0 자신이 소비하는 4종(slice 스키마·fanout 스키마·공통 계약·조립 정책)을 제외한 후속 task용 6종(# R0/S0/F0 주석 표기분)은 A0에서 존재·해시 게이트만 수행(배포 요구 선언). -2. **R0의 죽은 상수 2개**: `routing/domain_activation_manifest.json`·`Default_Agent/domains/_registry_index.json`은 P2 R0에 상수로만 선언되고 main에서 미사용(잔재). -3. **P2 머리말·본문 모순**: "구 경로 `stage1_tmp/task_c_bo/**`는 쓰지 않는다(legacy_paths_forbidden)"고 하나 실제로는 stage_a_context·source_universe(P-1 복귀) + postb_* 3종이 그 경로를 계속 사용. 금지는 사실상 worker slice/seed 구 경로에만 해당. -4. **`domains/_registry_index.json`은 runtime_manifest 미등재** — 교체 탐지는 SG-01 봉인(activation manifest의 registry_index_sha256) 하나가 담당(P3 주석 명시). -5. **L2는 `signals/signal_manifest.json`을 재독하지 않음** — bundle의 signal_manifest_transaction_id를 승계(§4.3 입력 계약 밖). -6. **P3 L1 부재는 의도적** — "판정 부류가 생기면 L0–L2 사이에 끼운다"(task_procedure 주석). P3는 IN→L0→L2→OUT 완전 직렬. -7. **worker는 유일한 LLM-파일기록 task**(P2) — seed 파일 기록자는 워커 하나로 못박혀 있고 R0는 되쓰지 않음. -8. **P3 YAML 헤더 24행 훼손 의심**("`# 137종 —`" 라벨 잔흔) 및 **code-executor Bearer 토큰 평문 하드코딩**(P2·P3 공통 구조, P3 48행 확인) — 배포 전 정리 필요. +같은 회차의 YAML 측 작업(P2 R0 사문 상수 2줄 삭제 · legacy_paths 주석 한정)은 이 문서의 범위 밖이며 `stage_1_part_1_and_2_updated_yaml_analysis.md` 머리말과 v.2 §2-A 에 기록돼 있다. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/stage_1_2_3_assets_distribution_location_old.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/stage_1_2_3_assets_distribution_location_old.md new file mode 100644 index 00000000..2cd155ba --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/stage_1_2_3_assets_distribution_location_old.md @@ -0,0 +1,169 @@ +# Stage 1 Part 1·2·3 자산 배포 위치 총람 + +- 근거: `stage_1_part_1_v.8.yml`(6,668행) · `stage_1_part_2_v.8.yml`(3,315행) · `stage_1_part_3_v.8.yml`(1,038행) 전량 판독 + 배포 트리 실측 (2026-08-20) +- 크기: 배포 원본 트리(`v.7/extension_research/Default_Agent/`)에서 실측한 바이트 수. 실행 중 생성 산출물은 사건마다 달라져 "실행 생성"으로 표기. +- 표기: P1 = Part 1(사건개요파악·기초작업), P2 = Part 2(BO·시그널 생성), P3 = Part 3(법률효과구조 생성) + +--- + +## 0. 배포 루트 3계층 (세 YAML 공통 규약) + +| 계층 | 위치 | 성격 | +|---|---|---| +| ① 정적 자산 루트 (`ASSET_ROOT`) | localdocs 논리 접두 **`Default_Agent/`** ← 배포 원본 **`v.7/extension_research/Default_Agent/`** 트리(210파일, .DS_Store 제외)를 통째로 배포 | P2·P3 remedy 명문: "배포 원본 extension_research/Default_Agent 트리를 서버 Default_Agent/에 통째로 배포하고 runtime_manifest.json의 등재·sha256과 대조. **부분 복사 불허**" | +| ② 사건 워크스페이스 루트 | localdocs 문서 루트 (clientInfo에 `{{__user_hash__}}`/`{{__workspace_hash__}}` 필수 — 빠지면 루트 경로를 봄) | 접두 없는 상대경로 전부: `client_meeting.md`, `BO.json`, `routing/`, `quality_gates/`, `signals/`, `runtime/`, `fanout/`, `stage1_tmp/`, `validation_assets/`, `evidence_*` … | +| ③ 실행 샌드박스 | code-executor 컨테이너 `EXECUTION_ROOT=/tmp/s1` (R0만 `/tmp/s1_r0`) — 모듈 반입처 `_rt/`, S0 signal 패키지 `_sig/`·`_signal_out/` | 임시 스테이징. 배포 대상 아님. 실행 인자 계약 `--asset-root/--execution-root/--logical-root` (D-5 §2.4 0-c-2) | + +**모듈 미러 규약**: localdocs가 `.py`를 서빙하지 않으므로 모든 런타임 `.py`는 **같은 디렉터리에 바이트 동일 `.txt` 미러**로 배포된다(유일 예외 = 빌더 `build_default_agent_stage1.py`). 반입 시 `Default_Agent/runtime_manifest.json`의 sha256과 대조하고 불일치면 실행 금지. 아래 표의 `.txt` 미러는 전부 `.py` 정본이 같은 디렉터리에 병존한다(크기 동일). + +**runtime_manifest.json**: `stage1_runtime_manifest.v1`, 426건 등재. computations/ 등 Stage 1 Part 1~3 밖 자산도 등재되어 있으며 그 물리 실체는 `extension_research/선행구축/Default_Agent_Stage_1/`(1,322파일) 및 `ver_8_yaml_candidates/Default_Agent_Stage_1/`(560파일, 빌드·검증 산출 트리)에 있다. P1~P3 게이트는 각 Part가 선언한 필수 자산 목록에 대해서만 등재·sha256·가독을 검사한다. + +--- + +## 1. 공통자산 — 2개 Part 이상이 사용하는 사전배포 정적 자산 (`Default_Agent/`) + +| 배포 위치 (`Default_Agent/` 이하) | 형태 | 크기(B) | 쓰는 task | 비고 (역할) | +|---|---|---|---|---| +| `runtime_manifest.json` | json | 66,526 | P1 D0 · P2 A0/R0/F0/S0 · P3 L0/L2 | 해시 장부(`stage1_runtime_manifest.v1`, entries[].path+sha256). 모든 모듈 반입·배포 게이트의 단일 근거. entries[].path는 `Default_Agent/` 접두를 뗀 상대경로 | +| `domains/_registry_index.json` | json | 18,921 | P1 T0-02/T0-03/D0/T9 · P2 A0 · P3 L0/L2 | 도메인 registry 색인 정본(registry_version, entries[].domain_id/config_path/config_sha256). 원문 read_raw sha256이 스크리닝·활성 매니페스트·핸드오프에 봉인됨. **매니페스트 미등재 — SG-01 봉인(activation manifest의 registry_index_sha256)이 교체를 잡음**. P2 R0에는 상수로만 선언(미사용) | +| `domains/{domain_id}/domain_config.json` | json ×26 | 2,468~17,324 (총 274,372) | P1 T0-03(후보만)/D0(전 26) · P2 A0 · P3 L0/L2(expected_runnable만) | 도메인별 config: 요건 슬롯·상대사실·증거 구성요소·항변·계산 바인딩·검토코드·structure_types 선언. registry index의 config_path만 따라 열람, config_sha256 사슬 대조. **LLM task(P1 T0-01·T1)는 직접 읽기 금지** | +| `stage1_runtime/runtime_common.txt` | txt(py 미러) | 4,496 | P1 D0 · P2 A0/R0 · P3 L0/L2 | 런타임 공용 모듈. `/tmp/s1/_rt/*.py`로 실체화 후 sys.path 삽입 | +| `stage1_runtime/schema_subset_validator.txt` | txt(py 미러) | 7,442 | P1 D0 · P2 A0/R0 · P3 L0/L2 | 닫힌 스키마 부분집합 검증기 — 산출물 스키마 검증의 공용 엔진 | +| `stage1_runtime/registry_loader.txt` | txt(py 미러) | 6,404 | P1 D0 · P2 A0/R0 · P3 L0 | registry 적재기(load_registry). P3 L0은 compiler 의존 모듈로만 스테이징 | +| `routing/evidence_component_union.md` | md | 17,593 | P1 T3(preflight) · P2 worker(어휘 참조) | 증거 구성요소 어휘 합집합(component_id + accepted_source_kinds + owner). `registry_component_ids`의 유일 허용 어휘 — 목록 밖 이름 생성 금지 | +| `routing/extension_payload_key_declarations.v1.json` | json | 23,071 | P2 A0(게이트)/F0 · P3 L0 | BOType 합집합·확장 payload declared_key_union·legacy_alias_targets 선언표(`stage1_extension_payload_key_declarations.v1`). 생성기 `routing/_build_extension_payload_declarations.py`(배포 시점 접이, 런타임 미실행) | +| `signals/signal_registry.v2.json` | json | 10,907 | P2 A0(게이트)/S0 · P3 L0(게이트만) | signal registry v2 — S0 스키마 $ref 폐포 walk의 출발점. P3에서는 배포 완전성 검사만 받고 파싱되지 않음 | +| `contracts/signals/s5_execution_contract.v2.json` | json | 1,451 | P2 A0(게이트)/S0 · P3 L0(게이트만) | S5 실행 계약 — S0가 `/tmp/s1/_sig/contracts/`에 배치(모듈의 parents[1]/contracts 산술 재현). P3에서는 게이트 전용 | +| `signals/adapters/sg01_activation_adapter.txt` | txt(py 미러) | 3,738 | P1 D0 · P2 S0 | SG-01 활성 어댑터 — D0 산출 매니페스트 자기검증(adapt_and_validate). 미러 위치는 `stage1_runtime/`이 아니라 `signals/adapters/`(YAML 헤더 명시) | +| `signals/schemas/domain_activation_manifest.schema.json` | json | 3,535 | P1 D0(stage, 실패 시 경고) · P2 S0(registry가 결정하는 $ref 폐포에 포함 — 배포된 signal_registry.v2.json 등재 기준) | 활성 매니페스트 스키마 — D0 모듈은 직접 읽지 않음(어댑터가 검증 담당). §3의 폐포 17종 집계(28,476B)에 이 파일이 포함되므로 바이트 합산 시 중복 주의 | + +## 2. Part 1 전용 정적 자산 + +| 배포 위치 (`Default_Agent/` 이하) | 형태 | 크기(B) | 쓰는 task | 비고 (역할) | +|---|---|---|---|---| +| `routing/activation_cue_digest.md` | md | 35,110 | T0-01(preflight)/T0-02 | 라우팅 단어 사전 — 26개 도메인 활성 단서(P/N/C/OVR)+GLOBAL_DEFAULTS. "사실 원천이 아니다". 머리말에 registry_version·registry_index sha256 → 스냅숏 낡음 감지 | +| `platform/schemas/client_goal_domain_profiles.schema.json` | json | 13,082 | T1(preflight) | client_goal의 domain_profiles 칸 형태를 정하는 닫힌 스키마 — "형식만 정하고 내용은 만들지 않음" | +| `platform/reference_catalogs/domain_ids.json` | json | 270 | T0-03 | 도메인 ID 카탈로그 — 후보 멤버십 검사 | +| `platform/reference_catalogs/calculation_ids.json` | json | 1,139 | T0-03 | 계산 ID 카탈로그(entries[].calculation_id/status) — calc 바인딩 존재·ready 검사 | +| `stage1_runtime/activation_gate.txt` | txt(py 미러) | 66,715 | D0 | **도메인 활성 판정 모듈 본체** — "판정은 이 안에서만 일어난다. 결정 필드는 execution_eligible 하나뿐". main(--asset-root/--execution-root/--logical-root) 호출 | +| `platform/contracts/determinism_scope_contract.json` | json | 1,473 | (헤더 선행조건만) | 결정론 범위 계약 — P1 정적 자산 선행 조건 5종의 하나. task 본문 직접 등장 없음(모듈 내부 소비 추정) | +| `validation_assets/routing/case_kind_domain_matrix.v2.json` | json | 72,969 | **읽기 금지** (T0-01·T1에 금지 명문) | 사건유형 이름을 담은 **오프라인 회귀 전용** 자산 — 런타임 입력 금지 | + +## 3. Part 2 전용 정적 자산 + +| 배포 위치 (`Default_Agent/` 이하) | 형태 | 크기(B) | 쓰는 task | 비고 (역할) | +|---|---|---|---|---| +| `stage1_runtime/registry_validator.txt` | txt(py 미러) | 11,080 | A0 | registry 검증기(validate_registry) — overlay 4개 오류코드만 경성 | +| `stage1_runtime/prompt_compiler.txt` | txt(py 미러) | 15,164 | A0 | 프롬프트 조립기 — FragmentSpec/collect_domain_fragments/compile_fragments, rank 10→50 합성, 상한 96,000B/24,000자 | +| `stage1_runtime/domain_slice_compiler.txt` | txt(py 미러) | 32,300 | A0 | slice 컴파일러(compile_domain_slices) — 낡으면 PART2_COMPILER_STALE | +| `stage1_runtime/domain_fanout_planner.txt` | txt(py 미러) | 12,282 | A0 | fan-out 계획기(build_fanout_plan) | +| `stage1_runtime/stage_a_context_builder.txt` | txt(py 미러) | 25,885 | A0 | stage_a_context/source_universe_manifest 빌더(v3 세 builder 이식) | +| `stage1_runtime/worker_output_validator.txt` | txt(py 미러) | 21,302 | A0(게이트)/R0 | worker seed 산출 검증기(validate_worker_output; schema·slice 주입). 반입 실패는 경고 강등 | +| `stage1_runtime/prompt_composition_policy.json` | json | 1,715 | A0 | 프롬프트 조립 정책(rank 순서·상한) | +| `stage1_runtime/bo_surface_projection_policy.v1.json` | json | 6,355 | A0(게이트)/R0/F0 | BO 투영 정책(`stage1_bo_surface_projection_policy.v1`) — f0_normalization·review_item_projection 규칙 정본 | +| `platform/schemas/domain_slice.schema.v2.json` | json | 17,725 | A0 | slice 스키마 — F-3 능력검사(stage_b_domain_slice.domain_declarations·compiled_prompt.hash_kind 필수) | +| `platform/schemas/domain_fanout_plan.schema.json` | json | 11,148 | A0 | fan-out 계획 스키마(build_fanout_plan에 주입) | +| `platform/schemas/domain_seed_output.schema.v3.json` | json | 21,965 | A0(게이트)/worker(참조)/R0 | worker seed 산출 스키마 v3(additionalProperties:false 봉인) | +| `domains/_common/common_worker_contract.md` | md | 3,766 | A0 | rank 10 공통 worker 계약 조각 | +| `domains/{domain_id}/seed_prompt_overlay.md` | md ×25 | 1,026~5,095 (총 83,177) | A0 | 도메인별 프롬프트 overlay 조각 — config의 `prompt_overlay_ref`가 선언한 경로 추적(실패 시 경고). EC-00은 대신 `common_fragment.md`(1,850B) 보유 | +| `signals/compiler/{common, projections, schema_validator, signal_compiler, signal_gate, transaction_writer, writer_boundary}.txt` | txt(py 미러) ×7 | 1,089~11,095 (총 37,276) | S0 | signal 컴파일러 모듈 — `/tmp/s1/_sig/pkg/signals/compiler/`로 실체화. **파일 기록 권한은 transaction_writer 하나**(canonical_writer_uniqueness) | +| `signals/adapters/{s3_domain_seed_adapter, s3_envelope_migration_adapter, s4_calculation_adapter}.txt` | txt(py 미러) ×3 | 1,071~7,223 (총 9,578) | S0 | 어댑터 모듈(sg01은 공통표 참조 — S0 어댑터 4종 반입에 포함) | +| `signals/emitters/{emitter_runtime, emit_sg02..emit_sg13}.txt` | txt(py 미러) ×13 | 287~7,687 (총 11,131) | S0 | 생성기(emitter) — SG-02~SG-13 12종 + 공용 런타임 | +| `signals/schemas/*.json` (signal_manifest·domain_signal_envelope.v2·SG별 12종 등 15종) + `signals/_common/{signal_item, evidence_slot_status}.schema.json` 2종 | json ×17 | 1,133~3,535 (총 28,476) | S0 | registry 선언에서 출발한 **상대 $ref 폐포 17종** — 목록 상수화 금지(F-4c), registry가 동적으로 결정 | + +## 4. Part 3 전용 정적 자산 + +| 배포 위치 (`Default_Agent/` 이하) | 형태 | 크기(B) | 쓰는 task | 비고 (역할) | +|---|---|---|---|---| +| `stage1_runtime/structure_index_compiler.txt` | txt(py 미러) | 27,309 | L0 | **구조 seed 컴파일 모듈**(compile_structure_seeds) — Θ 조립·구조 seed·signal 조인. "판정 규칙을 YAML에 쓰지 않는다" | +| `stage1_runtime/structure_index_policy.v1.json` | json | 7,032 | L0/L2 | 구조 색인 정책(`stage1_structure_index_policy.v1`) — 불일치 시 PART3_POLICY_INVALID | +| `platform/schemas/structure_seed_bundle.schema.json` | json | 9,128 | L0 | seed bundle 닫힌 스키마 — theta·admission_policy 인지 능력검사(PART3_STRUCTURE_SCHEMA_STALE) | +| `platform/schemas/legal_effect_structures.schema.json` | json | 6,809 | L0(게이트만)/L2 | 최종 색인 닫힌 스키마 — by_module·by_bo_id 인지 능력검사(PART3_INDEX_SCHEMA_STALE) | +| `domains/{domain_id}/structure_types.json` | json ×25 | 390~1,360 (총 21,006) | L0 | 도메인 사이드카 1 — 구조 유형 정의. structure_types 선언 있는 도메인만 반입, 부재·미등재는 경성 실패 | +| `domains/{domain_id}/module_role_projection.json` | json ×25 | 382~1,196 (총 18,248) | L0 | 도메인 사이드카 2 — 모듈 역할 사영. 위와 동일 규율 | + +## 5. 사건 입력 자산 (실행 전 사건 워크스페이스에 존재해야 함) + +| 배포 위치 (워크스페이스 루트 이하) | 형태 | 크기 | 쓰는 task | 비고 (역할) | +|---|---|---|---|---| +| `client_meeting.md` | md | 사건별 | P1 T0-01/T0-02/T1/T3/T4 · P2 A0 | 상담록 — "유일한 사실 원천". source unit(MTG.*) 채번·재산출 대조, meeting_clause_ids의 원천 | +| `evidence_all.json` | json | 사건별 | P1 T2 | 증거 문서 전체 배열 — 배열 순서 = ordinal 부여 기준(array_order, 법적 재정렬 금지) | + +## 6. 실행 중 생성 산출물 (전부 사건 워크스페이스 루트 이하; 크기 = 실행 생성) + +### 6.1 Part 1 산출 (생산 → 소비) + +| 위치 | 형태 | 생산 → 소비 | 비고 (스키마/역할) | +|---|---|---|---| +| `routing/domain_screening_draft.json` | json | T0-01 → T0-02 | 스크리닝 개방형 초안(판단 4종). `stage1_domain_screening_draft.v1` | +| `routing/domain_screening.json` | json | T0-02 → T0-03/T1/D0/T9 · **P2 A0** | 봉인된 닫힌 스키마 스크리닝(`stage1_domain_screening.v1`, 비판단 6종 부착). T9·P2가 원문 sha256 봉인 대조 | +| `routing/candidate_profile_vocabulary.md` | md | T0-03 → T1 · **P2 A0**(부재 허용) | 후보 도메인 낱말 목록(E\|O\|V\|D\|R 줄, 예산 28,000B) — "registry 원문이 정본, 이 파일은 파생본" | +| `client_goal.json` | json | T1 → (Part 1 내 소비 없음, 후속 스테이지용) | 고객 소송목적·당사자·사건군 구조화 + domain_profiles. D0 입력 6종에 불포함 | +| `evidence_shard_plan.json` | json | T2 → T5/T6 | fan-out/배리어/stale 정책 플랜(`evidence_shard_plan.v1`). stdout의 dynamic_fanout을 오케스트레이터가 소비 | +| `evidence_shards/E-{nnn}.json` | json ×N | T2 → T3/T4 (T5 발췌) | 문서별 단일 evidence shard — mapper가 풀 대신 자기 shard만 읽도록 분리 | +| `evidence_indexed_parts/E-{nnn}.json` | json ×N | T3 → T4/T5 | B1 mapper part(`evidence_indexed_part.v4`) | +| `evidence_event_candidate_parts/E-{nnn}.json` | json ×N | T4 → T6 | B2 mapper part(`evidence_event_candidate_part.v4`) | +| `evidence_indexed.json` | json | T5 → D0/T9 · **P2 A0** | 최종 증거 authority catalog(`evidence_indexed.v3`) — 작성은 quality gate 단독 권한 | +| `evidence_event_candidates.json` | json | T6 → D0/T9 · **P2 A0** | 최종 event/state candidate(`evidence_event_candidates.v1`) | +| `stage1_tmp/quality_gate/finalized_evidence_id_manifest.json` | json | T5 → T6 | 확정 evidence id 집합 + dropped_aliases(`finalized_evidence_id_manifest.v1`) | +| `stage1_tmp/quality_gate/B1_precheck.json` | json | T5 → T6/T8 | B1 결정적 검사·suspects(`B1_precheck.v1`) | +| `stage1_tmp/quality_gate/B2_precheck.json` | json | T6 → T8 | B2 결정적 검사·suspects(`B2_precheck.v1`) | +| `stage1_tmp/quality_gate/B12_exception_pack.json` | json | T6 → T7(preflight)/T8 | GB 판정용 compact exception pack(`B12_exception_pack.v1`) — T7의 유일 입력 | +| `stage1_tmp/quality_gate/B12_adjudication_decisions.json` | json | T7 → T8 | exception별 LLM 판정(`B12_adjudication_decisions.v1`) | +| `quality_gates/B1_evidence_indexed_gate.json` | json | T8 → D0/T9 | B1 최종 audit(`B1_quality_gate.v3`) | +| `quality_gates/B2_event_candidates_gate.json` | json | T8 → D0/T9 | B2 최종 audit(`B2_quality_gate.v3`) | +| `routing/domain_activation_manifest.json` | json | D0 → T9 · **P2 A0/S0 · P3 L0/L2** | 도메인 활성 확정 매니페스트 — 모듈 산출 바이트 그대로 업로드(재직렬화 금지). execution_eligible·expected_runnable_domain_ids·registry_index_sha256(SG-01 봉인) | +| `quality_gates/stage1_part1_soft_gate_handoff.json` | json | T9 → OUT · **P2 A0** | soft gate handoff sidecar(`stage1_part1_soft_gate_handoff.v1`) — digest_guard 7종·활성봉인 교차검증·review 큐 | + +### 6.2 Part 2 산출 + +| 위치 | 형태 | 생산 → 소비 | 비고 (스키마/역할) | +|---|---|---|---| +| `runtime/compiled_prompts/{domain_id}.md` | md ×도메인 | A0 → worker(preflight) | 도메인별 조립 프롬프트(rank 10→50 합성) — 재조립 금지. 워크스페이스+로컬 이중 기록 | +| `runtime/domain_slices/{domain_id}.json` | json ×도메인 | A0 → worker/R0/S0 · **P3 L0(옵션)** | 도메인 slice(`task_c_bo_stage_b_domain_slice.v2`) — source_universe·allowed_legal_effect_bo_types의 원천 | +| `runtime/domain_seed_outputs/{domain_id}.json` | json ×도메인 | worker → R0/S0 | worker seed(`task_c_bo_stage_b_domain_bo_seed.v3`) — **기록자는 워커 유일**(R0 되쓰기 금지) | +| `fanout/domain_fanout_plan.json` | json | A0 → R0/S0 (+오케스트레이터 {{item}} 주입원) | fan-out 계획(`domain_fanout_plan.v1`) — task_instances[]가 worker 인스턴스 생성, barrier 기대집합=expected_runnable | +| `stage1_tmp/task_c_bo/stage_a_context.json` | json | A0 → R0/F0 | stage_a_context(`task_c_bo_stage_a_context.v1`) — P-1 개정으로 구 경로 유지 | +| `stage1_tmp/task_c_bo/source_universe_manifest.json` | json | A0 → R0/S0 | 원천 우주 매니페스트(evidence_index_set·event_candidate_ids·meeting_clause_ids) | +| `validation_assets/routing/stage_receipt.json` | json | A0 | 스테이지 영수증(`stage1_stage_receipt.v2`) — schema_injection·deployment_gate 실측 기록 | +| `stage1_tmp/task_c_bo/postb_seed_ledger.json` | json | R0 → F0 | 원장(`task_c_bo_postb_seed_ledger.v1`): 투영 ledger_candidates + deterministic_decisions | +| `quality_gates/stage1_part2_exception_pack.json` | json | R0 → R1(preflight, 유일 입력)/F0 | 예외 pack(`stage1_part2_exception_pack.v1`) — budget 8후보/2,000자 | +| `quality_gates/stage1_part2_review_handoff.json` | json | R0 → F0(읽고 되씀) | 검토 핸드오프(`stage1_part2_review_handoff.v1`): PENDING_FINALIZE → FINALIZED | +| `stage1_tmp/task_c_bo/postb_adjudication_decisions.json` | json | R1 → F0 | R1 판정(`task_c_bo_postb_exception_adjudication.v1`) — 예외 없어도 no-exception 객체 필수 | +| `BO.json` | json | F0(기록+재읽기) → S0 · **P3 L0/L2** | **최종 BO 배열**(bh1..bhN) — 워크스페이스 루트. P3 보존 등식의 BO 계수 원본. 무봉인 입력(P3 receipt에 관찰 기록) | +| `stage1_tmp/task_c_bo/postb_compiled_bundle_compact.json` | json | F0 | compact bundle(`task_c_bo_postb_compiled_bundle_compact.v1`): ref→bo_id 표·정규화 노트 | +| `signals/signal_manifest.json` | json | S0(기록+재읽기 해시 대조) → **P3 L0**(진입 봉인; L2는 재독 금지) | signal 거래 매니페스트 — transaction_id `S5TX-[a-f0-9]{20}`, files[].file_sha256, downstream_read_sets. files[].path에는 `signals/` 접두 없음(붙여서 대조) | +| `signals/**/*.json` (거래 산출 전량) | json | S0 → **P3 L0** | 정본 signal 번들 — `/tmp/s1/_signal_out`에 쓰고 `signals/` 접두로 바이트 그대로 반출. 파일명이 코드에 박힌 것은 SG-05/08/13 셋뿐, 나머지는 emitter 12종+registry가 결정. P3 소비 확인분: `legal_effect_routes.json`(SG-13, L0 조인 주근거+L2 route 보존 등식), `legal_relation_lifecycle_signals.json`(SG-05), `calculation_requirements.json`(SG-11), `procedural_posture_relief_signals.json`(SG-02, 빈 read set), `asset_right_state_signals.json`(SG-07, 빈 read set), `domain_signals/{domain_id}.json`(옵션) | +| `signals/compatibility_views/{actio_case, case_liability, legal_effect}_signals.json` | json ×3 | S0 | 호환 뷰 — actio_case(사영원 없음), case_liability(SG-05·08 사영), legal_effect(SG-13 사영) | +| `actio_case_signals.json` · `case_liability_signals.json` · `legal_effect_signals.json` (워크스페이스 루트) | json ×3 | S0 | 구 이름 별칭 — Part 3·4 최대 호환면, compatibility_views와 바이트 동일 | + +### 6.3 Part 3 산출 + +| 위치 | 형태 | 생산 → 소비 | 비고 (스키마/역할) | +|---|---|---|---| +| `stage1_tmp/task_le/structure_seed_bundle.json` | json | L0 → L2 | seed bundle(`stage1_structure_seed_bundle.v1`): theta·structure_records·counts + signal_manifest_transaction_id 승계 — L0→L2 유일한 중간 계약 | +| `quality_gates/stage1_part3_review_handoff.json` | json | L0 → L2(읽고 되씀) | 검토 핸드오프(`stage1_part3_review_handoff.v1`): OPEN → FINALIZED. review_items 근거 없는 감축 금지 | +| `validation_assets/routing/part3_receipt.json` | json | L0 | P3-L0 실측 영수증(`stage1_part3_receipt.v1`) — "상수 PASS는 증거가 아니다". L2는 receipt 미작성 | +| `legal_effect_structures.json` | json | L2(기록+재읽기 sha256 대조) → OUT | **Stage 1 최종 산출** — 법률효과 구조 색인(`stage1_legal_effect_structures.v1`): theta·structure_records·structure_index(by_domain_id/by_module/by_bo_id)·quality_gate(보존등식 3종). 워크스페이스 루트, canonical JSON | + +## 7. 참조 전용·빌드타임 자산 (런타임 파일 접근 없음) + +| 자산 | 물리 위치 | 형태/크기(B) | 인용처 | 비고 | +|---|---|---|---|---| +| `SKILL.md` | 경로 미명시(유일하게 접두 없는 인용) | md | P1·P2·P3 code-executor 보일러플레이트 주석("5장/5.2장", "§3.3 stdout 규약") | localdocs·code-executor 사용 규약 문서 | +| `platform/schemas/domain_screening.schema.json` | 선행구축 트리(예: `선행구축/Default_Agent_Stage_1/platform/schemas/`) — **extension_research/Default_Agent에는 없음** | json | P1 T0-02 주석 | screening_policy/contract_guards const의 정본 — 값은 코드에 하드코딩, 런타임에 읽지 않음 | +| `tools/build_default_agent_stage1.py` | `ver_8_yaml_candidates/Default_Agent_Stage_1/tools/` | py / 102,559 | P1 헤더(미러 규칙의 유일 예외) | Default_Agent 빌더 | +| `routing/_build_extension_payload_declarations.py` | `ver_8_yaml_candidates/Default_Agent_Stage_1/routing/` | py / 8,842 | P2 F0 주석 | 확장 payload 선언표(공통표 #8)의 생성기 — 배포 시점 접이 | +| `validation_assets/routing/_check_schema_keyword_support.py` | `ver_8_yaml_candidates/Default_Agent_Stage_1/validation_assets/routing/` | py / 8,233 | P2 A0 receipt 필드 | 키워드 지원 점검 스크립트 — receipt에 이름만 인용, 실행하지 않음 | +| 전략·명세 문서(`stage_1_update_strategy.md`, `P1-T2_to_T9_update_strategy.md`, `Part_1/2_Improvement_Strategy_Claude_v1.md`, `stage_1_part_2_optimal_update_strategy_v.2.md`, `stage_1_part_3_개정_신전략서.md` 등) 및 조각 출처 YAML(`Task_A_client_goal_v4.yml` 등) | `v.7/` 및 `ver_8_yaml_candidates/` 일대 | md/yml | 각 YAML 헤더·코드 주석 | 정본 근거 설계 문서 — 런타임 미접근 | + +## 8. 특기사항 (판독 중 확인된 사실) + +1. **게이트 전용 자산**: `signal_registry.v2.json`·`s5_execution_contract.v2.json`은 P3에서 등재·해시·가독 검사만 받고 어느 태스크도 파싱하지 않음. P2 A0의 PART2_REQUIRED_ASSETS는 10종이며, 그중 A0 자신이 소비하는 4종(slice 스키마·fanout 스키마·공통 계약·조립 정책)을 제외한 후속 task용 6종(# R0/S0/F0 주석 표기분)은 A0에서 존재·해시 게이트만 수행(배포 요구 선언). +2. **R0의 죽은 상수 2개**: `routing/domain_activation_manifest.json`·`Default_Agent/domains/_registry_index.json`은 P2 R0에 상수로만 선언되고 main에서 미사용(잔재). +3. **P2 머리말·본문 모순**: "구 경로 `stage1_tmp/task_c_bo/**`는 쓰지 않는다(legacy_paths_forbidden)"고 하나 실제로는 stage_a_context·source_universe(P-1 복귀) + postb_* 3종이 그 경로를 계속 사용. 금지는 사실상 worker slice/seed 구 경로에만 해당. +4. **`domains/_registry_index.json`은 runtime_manifest 미등재** — 교체 탐지는 SG-01 봉인(activation manifest의 registry_index_sha256) 하나가 담당(P3 주석 명시). +5. **L2는 `signals/signal_manifest.json`을 재독하지 않음** — bundle의 signal_manifest_transaction_id를 승계(§4.3 입력 계약 밖). +6. **P3 L1 부재는 의도적** — "판정 부류가 생기면 L0–L2 사이에 끼운다"(task_procedure 주석). P3는 IN→L0→L2→OUT 완전 직렬. +7. **worker는 유일한 LLM-파일기록 task**(P2) — seed 파일 기록자는 워커 하나로 못박혀 있고 R0는 되쓰지 않음. +8. **P3 YAML 헤더 24행 훼손 의심**("`# 137종 —`" 라벨 잔흔) 및 **code-executor Bearer 토큰 평문 하드코딩**(P2·P3 공통 구조, P3 48행 확인) — 배포 전 정리 필요. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/stage_1_part_1_and_2_updated_yaml_analysis.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/stage_1_part_1_and_2_updated_yaml_analysis.md index 0b425a50..a7a40971 100644 --- a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/stage_1_part_1_and_2_updated_yaml_analysis.md +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/stage_1_part_1_and_2_updated_yaml_analysis.md @@ -3,7 +3,7 @@ - 문서 위치: `YAML_Prompts/1. Stage_1/v.7/extension_research/stage_1_part_1_and_2_updated_yaml_analysis.md` - 분석 대상 정본 2종 - `ver_8_yaml_candidates/stage_1_part_1_v.8.yml` — 467,668 B / 6,667행 / sha256 `195b43edfaf1df1414acc118c17d544c…` - - `ver_8_yaml_candidates/stage_1_part_2_v.8.yml` — **195,938 B / 3,315행 / sha256 `ec4c43027ad85195…`** (2026-08-19 BO 투영 회차 반영본. 직전 판은 193,933 B / 3,307행 / `73644c5620b1bfbc…` — `outdated/stage_1_part_2_v.8.pre_bo_projection.yml` 로 보존) + - `ver_8_yaml_candidates/stage_1_part_2_v.8.yml` — **195,963 B / 3,315행 / sha256 `ec407309a5ab43f4…`** (2026-08-20 자산 정리 회차 반영본 — R0 사문 상수 2줄 삭제 · legacy_paths 주석 한정. 값 무변, 예행 산출 바이트 동일. 직전 판 195,938 B / `ec4c43027ad85195…` 는 `outdated/stage_1_part_2_v.8.pre_asset_triage_fix.yml`, 그 전 판은 `outdated/stage_1_part_2_v.8.pre_bo_projection.yml` 로 보존) - 근거 자료: `v.7/MEMORY.md` · `stage_1_part_1_개정작업_리포트.md` · `ver_8_yaml_candidates/part_1_remaining_update_report.md` · `stage_1_part_2_레지스트리기반개정완료_보고서.md` · `ver_8_yaml_candidates/8월12_to_16일작업압축요약본.md`(316,036 B) · `Default_Agent_Stage_1/handoffs/stage1_part_interface.v1.json`(34 인계면 선언표) · `stage_1_part_2_여전히남은문제해결방안.md`(v2) · `stage_1_part_2_여전히남은문제해결내역.md` - **Part 2 추가 개정 5회의 작업 내역서** — `stage_1_part_2_추가수정작업.md`(배포 완전성 게이트 F-1~F-7) · `stage_1_part_2_추가수정작업_더.md`(델타 30건 조립본 반영 · R0 라벨 해석) · `stage_1_part_2_추가수정작업_더더.md`(빌더 등록 · 상류 동기화) · `stage_1_part_2_추가수정작업_더더더.md`(스키마 2건 등록 · 예행 seed 보강) · `stage_1_part_2_여전히남은문제해결방안.md`(v2) + `stage_1_part_2_여전히남은문제해결내역.md`(BO 투영 — worker v3 산출이 `BO.json`·검토 handoff 에 닿는 경로 복원) - 실측 기준: 이 문서의 모든 수치·경로·키는 위 두 YAML 원문과 자산 트리를 **직접 파싱·해시**하여 얻은 것이다. 리포트 본문에서 옮겨 온 값은 §7 근거표에 출처를 적었고, 리포트와 실측이 갈린 곳은 그 자리에 명시했다. @@ -256,7 +256,7 @@ YAML이 참조하는 `Default_Agent/…` 자산은 **19개 리터럴**이고, | `stage1_runtime/schema_subset_validator.py` + `.txt` | 7,442 B | py + 미러 | D0 | 스키마 부분집합 검증기 | | `stage1_runtime/registry_loader.py` + `.txt` | 6,404 B | py + 미러 | D0 | registry 적재기 | | `signals/adapters/sg01_activation_adapter.py` + `.txt` | 3,738 B | py + 미러 | D0 | SG-01 어댑터. D0 산출의 자기검증 | -| `validation_assets/routing/case_kind_domain_matrix.v2.json` | 72,969 B | json | **런타임 입력 금지** | 137 사건유형 이름을 담은 오프라인 회귀 전용. T0-01과 T1 두 프롬프트가 각각 읽기를 명시적으로 금지한다(192행 · 1,687행) | +| `validation_assets/routing/case_kind_domain_matrix.v2.json` | 72,969 B (조립본 — 배포 원본 트리에서는 2026-08-20 제거) | json | **런타임 입력 금지** | 137 사건유형 이름을 담은 오프라인 회귀 전용. T0-01과 T1 두 프롬프트가 각각 읽기를 명시적으로 금지한다(192행 · 1,687행) | Part 1 개정이 함께 만든 **검증·회귀 자산**(런타임 입력이 아니다). @@ -335,9 +335,9 @@ Part 1 개정이 함께 만든 **검증·회귀 자산**(런타임 입력이 아 | # | task 명칭 | 실행기 / 설정 | 작업 목적 | 작업 내역 | |---|---|---|---|---| -| P2-A0 | `Task_C_BO_A0_context_and_domain_slice_compiler` | code-executor · timeout 300 · 통합본 60–717행 | Part 1의 봉인된 산출을 받아 **도메인별 작업 재료를 컴파일하고 fan-out을 계획**한다 | 모듈 8종(`runtime_common` · `schema_subset_validator` · `registry_loader` · `registry_validator` · `prompt_compiler` · `domain_slice_compiler` · `domain_fanout_planner` · `stage_a_context_builder`)을 미러로 반입한다. 봉인 3해시를 검증한다. registry 26 config를 적재한다. `prompt_compiler.collect_domain_fragments(extra_specs=…)`로 Part 1의 어휘 사전을 도메인 프롬프트에 붙인다(조립 프롬프트 6,967 B → 24,942 B 실측). 슬라이스에 `domain_declarations` 7갈래를 투영한다. 스크리닝의 `candidates[].requested_calculation_domains`를 파싱해 도메인 `calculation_bindings`와 교집합하고, 교집합 밖은 `CALC_NOT_IN_BINDINGS` 리뷰로 남기며 `router_status`를 `READY_WITH_REVIEW`로 내린다. **도메인 상수를 두지 않는다**. 추가 개정 1회차로 `main()` 첫 문장이 `assert_deployment()`가 되었다 — 필수 자산 10 리터럴과 모듈 미러를 **한 벌로** 검사해(실측 18경로 — 다섯째 회차가 BO 투영 정책 자산을 목록에 더했다) 하나라도 빠지거나 해시가 어긋나면 `PART2_ASSET_DEPLOYMENT_INCOMPLETE`로 세운다. 이어 슬라이스 스키마가 `domain_declarations`와 `compiled_prompt.hash_kind`를 아는지 보고(`PART2_SLICE_SCHEMA_STALE`), 컴파일러 산출에 투영이 실제로 실려 나오는지 본다(`PART2_COMPILER_STALE` — 옛 판본은 알 수 없는 인자로 `TypeError`가 먼저 나므로 같은 코드에 `detail: signature_mismatch`를 붙인다). 프롬프트 오버레이 실패 4코드를 승격 대상으로 강제한다 | -| P2-B | `Task_C_B_domain_worker_*` | LLM `google / gemini-3.1-flash-lite` · reasoning high · verbosity medium · `max_concurrency: 8` · cache 15m · preflight 2 · 통합본 718–847행 | 단일 도메인의 **BO seed 후보**를 만든다 | 읽는 것은 두 파일뿐 — 조립 프롬프트와 슬라이스. **프롬프트를 다시 조립하지 않는다.** 산출 최상위는 `stage_b_domain_bo_seed_output` 한 키, `schema_version`은 `task_c_bo_stage_b_domain_bo_seed.v3` 고정. 다섯 배열(`element_fact_candidates` · `opposing_fact_candidates` · `defense_candidates` · `calculation_requests` · `dependency_refs`) 이름은 스키마가 정한 것이다. `dependency_refs`는 연결만 남기고 의존 도메인의 결론을 복사하지 않는다. 자기검증 6항(최상위 단일 키, `domain_id`·`task_instance_id` 주입값 일치, `source_refs` ⊆ slice source_universe, `bo_type` ∈ `allowed_legal_effect_bo_types`, `registry_component_ids` ⊆ 합집합, 금지 키 `BO_ID`·`Evidence`·`EvidenceTitles`·`final_*` 부재). **결론을 내리지 않는다 — 후보만 남긴다** | -| P2-R0 | `Task_C_BO_R0_seed_reducer_and_exception_planner` | code-executor · timeout 240 · 통합본 848–1,793행 | worker 산출 M개를 **결정론으로 합치고 예외를 좁힌다** | fan-out 계획의 기대집합과 실제 seed 집합을 **정확 일치**로 대조한다(누락·초과·중복 모두 실패). 모듈 4종(`runtime_common` · `schema_subset_validator` · `registry_loader` · `worker_output_validator`)을 반입해 `validate_worker_output`을 **실제로 호출**한다. 슬라이스의 `domain_declarations`로 두 대조를 걸어 `CALCULATION_DOMAIN_NOT_DECLARED`와 `EVIDENCE_SLOT_NOT_DECLARED`를 **review 등급**으로 남긴다. 산출은 셋이다 — seed ledger · 예외 pack · review handoff. **worker seed 파일은 읽기만 하고 다시 쓰지 않는다** — 구판의 제자리 재기록(`transport_metadata`·`handoff_guard` 주입)은 다섯째 회차에서 삭제됐고, 되쓰기와 함께 고아가 된 `_domain_label` · `DOMAIN_LABELS` · `_task_instance_id` · `digest_summary` 도 걷어냈다(R0 의 구 이름 B1~B5 의존 0). 앞 회차의 보강 — `try` 밖 자산 선검사 · `ensure_candidate_ref`(v3 봉인 스키마가 `candidate_ref`를 금지하므로 R0 가 순번에서 만든다) · v3 의 평평한 `source_refs` membership 대조 — 은 그대로다. 다섯째 회차의 처방은 다섯이다 — ① 투영 정책 자산을 `_verify_asset`으로 경성 반입한다(미등재·해시 불일치는 `PART2_ASSET_DEPLOYMENT_INCOMPLETE`, 판본 불일치는 `PART2_PROJECTION_POLICY_INVALID`) ② registry 근거 투영기 `project_to_bo_surface`가 v3 후보를 BO 호환면으로 투영한다 — 반환 키 **17개 고정**(입력 무관), registry 가 말하지 않는 칸은 만들지 않고 review 로 올린다. 투영본은 `projected_candidates`에 **별도 보관**하고 원장 루프가 그것을 읽는다(`seed_objects`는 worker 원본 유지). v2 잔재 `expand_candidate` · `_seed_payload` · `ALLOWED_SEED_KEYS` · `DOMAIN_PAYLOAD_CANON` · `_canon_payload` 는 제거 — 병합·정렬·예외 축소 헬퍼 7종(`_source_refs` · `_duplicate_key` · `_sort_key` · `_normalize_juristic` · `_compact_exception_payload` · `ensure_candidate_ref` · `validate_candidate`)은 한 줄도 고치지 않았다 ③ 검토 채널을 v3 두 키(후보별 `review_items` + 루트 `unknown_or_unrouted_reviews`)로 연결하고, 원본 `review_code`를 덧붙이기 키 `source_review_code`로, `reason`을 handoff `template_note`로 보존한다 ④ `validate_seed_object`가 status 를 v3 enum 5종 전체로 다룬다 — READY 계열 수용, `NO_SUPPORT`는 빈 후보 조건으로 수용 + review, `BLOCKED` · `FAILED` · enum 밖은 경성 중단 ⑤ 죽은 `slice_guard` 검사를 fan-out 계획 행의 `slice_sha256` · `compiled_prompt_sha256` ↔ worker echo 대조로 교체한다(`_seed_docs_from_plan`이 계획 행을 함께 보관). 파생 정합 하나 — 원장 flag 판정의 `{"event","state"}` 하드코딩도 슬라이스의 `allowed_legal_effect_bo_types`(registry 선언)로 교체됐다 | +| P2-A0 | `Task_C_BO_A0_context_and_domain_slice_compiler` | code-executor · timeout 300 · 통합본 62–719행 | Part 1의 봉인된 산출을 받아 **도메인별 작업 재료를 컴파일하고 fan-out을 계획**한다 | 모듈 8종(`runtime_common` · `schema_subset_validator` · `registry_loader` · `registry_validator` · `prompt_compiler` · `domain_slice_compiler` · `domain_fanout_planner` · `stage_a_context_builder`)을 미러로 반입한다. 봉인 3해시를 검증한다. registry 26 config를 적재한다. `prompt_compiler.collect_domain_fragments(extra_specs=…)`로 Part 1의 어휘 사전을 도메인 프롬프트에 붙인다(조립 프롬프트 6,967 B → 24,942 B 실측). 슬라이스에 `domain_declarations` 7갈래를 투영한다. 스크리닝의 `candidates[].requested_calculation_domains`를 파싱해 도메인 `calculation_bindings`와 교집합하고, 교집합 밖은 `CALC_NOT_IN_BINDINGS` 리뷰로 남기며 `router_status`를 `READY_WITH_REVIEW`로 내린다. **도메인 상수를 두지 않는다**. 추가 개정 1회차로 `main()` 첫 문장이 `assert_deployment()`가 되었다 — 필수 자산 10 리터럴과 모듈 미러를 **한 벌로** 검사해(실측 18경로 — 다섯째 회차가 BO 투영 정책 자산을 목록에 더했다) 하나라도 빠지거나 해시가 어긋나면 `PART2_ASSET_DEPLOYMENT_INCOMPLETE`로 세운다. 이어 슬라이스 스키마가 `domain_declarations`와 `compiled_prompt.hash_kind`를 아는지 보고(`PART2_SLICE_SCHEMA_STALE`), 컴파일러 산출에 투영이 실제로 실려 나오는지 본다(`PART2_COMPILER_STALE` — 옛 판본은 알 수 없는 인자로 `TypeError`가 먼저 나므로 같은 코드에 `detail: signature_mismatch`를 붙인다). 프롬프트 오버레이 실패 4코드를 승격 대상으로 강제한다 | +| P2-B | `Task_C_B_domain_worker_*` | LLM `google / gemini-3.1-flash-lite` · reasoning high · verbosity medium · `max_concurrency: 8` · cache 15m · preflight 2 · 통합본 720–849행 | 단일 도메인의 **BO seed 후보**를 만든다 | 읽는 것은 두 파일뿐 — 조립 프롬프트와 슬라이스. **프롬프트를 다시 조립하지 않는다.** 산출 최상위는 `stage_b_domain_bo_seed_output` 한 키, `schema_version`은 `task_c_bo_stage_b_domain_bo_seed.v3` 고정. 다섯 배열(`element_fact_candidates` · `opposing_fact_candidates` · `defense_candidates` · `calculation_requests` · `dependency_refs`) 이름은 스키마가 정한 것이다. `dependency_refs`는 연결만 남기고 의존 도메인의 결론을 복사하지 않는다. 자기검증 6항(최상위 단일 키, `domain_id`·`task_instance_id` 주입값 일치, `source_refs` ⊆ slice source_universe, `bo_type` ∈ `allowed_legal_effect_bo_types`, `registry_component_ids` ⊆ 합집합, 금지 키 `BO_ID`·`Evidence`·`EvidenceTitles`·`final_*` 부재). **결론을 내리지 않는다 — 후보만 남긴다** | +| P2-R0 | `Task_C_BO_R0_seed_reducer_and_exception_planner` | code-executor · timeout 240 · 통합본 850–1,793행 | worker 산출 M개를 **결정론으로 합치고 예외를 좁힌다** | fan-out 계획의 기대집합과 실제 seed 집합을 **정확 일치**로 대조한다(누락·초과·중복 모두 실패). 모듈 4종(`runtime_common` · `schema_subset_validator` · `registry_loader` · `worker_output_validator`)을 반입해 `validate_worker_output`을 **실제로 호출**한다. 슬라이스의 `domain_declarations`로 두 대조를 걸어 `CALCULATION_DOMAIN_NOT_DECLARED`와 `EVIDENCE_SLOT_NOT_DECLARED`를 **review 등급**으로 남긴다. 산출은 셋이다 — seed ledger · 예외 pack · review handoff. **worker seed 파일은 읽기만 하고 다시 쓰지 않는다** — 구판의 제자리 재기록(`transport_metadata`·`handoff_guard` 주입)은 다섯째 회차에서 삭제됐고, 되쓰기와 함께 고아가 된 `_domain_label` · `DOMAIN_LABELS` · `_task_instance_id` · `digest_summary` 도 걷어냈다(R0 의 구 이름 B1~B5 의존 0). 앞 회차의 보강 — `try` 밖 자산 선검사 · `ensure_candidate_ref`(v3 봉인 스키마가 `candidate_ref`를 금지하므로 R0 가 순번에서 만든다) · v3 의 평평한 `source_refs` membership 대조 — 은 그대로다. 다섯째 회차의 처방은 다섯이다 — ① 투영 정책 자산을 `_verify_asset`으로 경성 반입한다(미등재·해시 불일치는 `PART2_ASSET_DEPLOYMENT_INCOMPLETE`, 판본 불일치는 `PART2_PROJECTION_POLICY_INVALID`) ② registry 근거 투영기 `project_to_bo_surface`가 v3 후보를 BO 호환면으로 투영한다 — 반환 키 **17개 고정**(입력 무관), registry 가 말하지 않는 칸은 만들지 않고 review 로 올린다. 투영본은 `projected_candidates`에 **별도 보관**하고 원장 루프가 그것을 읽는다(`seed_objects`는 worker 원본 유지). v2 잔재 `expand_candidate` · `_seed_payload` · `ALLOWED_SEED_KEYS` · `DOMAIN_PAYLOAD_CANON` · `_canon_payload` 는 제거 — 병합·정렬·예외 축소 헬퍼 7종(`_source_refs` · `_duplicate_key` · `_sort_key` · `_normalize_juristic` · `_compact_exception_payload` · `ensure_candidate_ref` · `validate_candidate`)은 한 줄도 고치지 않았다 ③ 검토 채널을 v3 두 키(후보별 `review_items` + 루트 `unknown_or_unrouted_reviews`)로 연결하고, 원본 `review_code`를 덧붙이기 키 `source_review_code`로, `reason`을 handoff `template_note`로 보존한다 ④ `validate_seed_object`가 status 를 v3 enum 5종 전체로 다룬다 — READY 계열 수용, `NO_SUPPORT`는 빈 후보 조건으로 수용 + review, `BLOCKED` · `FAILED` · enum 밖은 경성 중단 ⑤ 죽은 `slice_guard` 검사를 fan-out 계획 행의 `slice_sha256` · `compiled_prompt_sha256` ↔ worker echo 대조로 교체한다(`_seed_docs_from_plan`이 계획 행을 함께 보관). 파생 정합 하나 — 원장 flag 판정의 `{"event","state"}` 하드코딩도 슬라이스의 `allowed_legal_effect_bo_types`(registry 선언)로 교체됐다 | | P2-R1 | `Task_C_BO_R1_exception_adjudicator` | LLM · reasoning low · verbosity low · `max_iterations: 1` · cache 20m · preflight 1 · 통합본 1,794–1,896행 | R0이 non-deferrable로 판정한 **compact exception만 판정**한다 | pack 하나만 읽는다. 병합·최종 파일 작성·사실 창작을 하지 않는다. v3 3,621–3,723행과 바이트 동일(추가 개정 5회에서도 손대지 않았다) | | P2-F0 | `Task_C_BO_F0_final_bo_compiler_gate_writer` | code-executor · timeout 240 · 통합본 1,897–2,685행 | **최종 `BO.json`을 확정**하고 게이트를 쓴다 | PostB_3(final compiler) + PostB_4(final gate/writer) 통합. 입력은 전부 파일 계약(ledger · decisions · stage_a)이다. BOType 어휘를 하드코딩하지 않고 **registry 합집합**에서 해시 검증과 함께 만든다. 확장 payload 키를 `extension_payload_key_declarations.v1.json`과 대조해 미선언 키는 `EXTENSION_PAYLOAD_KEY_UNDECLARED`로 남긴다. R1 산출은 **조건부**로 요구한다(pack의 `exception_count`가 0이면 부재 허용). 다섯째 회차의 처방 둘 — ① 폴백 판정의 `bo_type not in {"event","state"}` 하드코딩을 registry 합집합 `bo_types`(5종)로 교체했다. `claim` · `communication` · `calculation_request` 를 선언한 도메인의 값이 `event`로 **침묵 덮어쓰기**되던 자리이며, 이제 빌드 루프와 검증 루프의 어휘가 같다 ② `_load_projection_policy`로 투영 정책을 반입해(`_load_declarations`와 같은 규율 — `runtime_manifest` sha 대조 후에만 사용) 정규화 기본값을 정책에서 읽고, `normalization_notes` 각 항목을 review handoff 의 `review_items`에도 올린다(`issue_type: schema_field_fallback` + `source_review_code`). Action 대체 체인(seed.Action → action_summary → Action_proposal)은 방어선으로 유지된다 | | P2-S0 | `Task_C_BO_S0_signal_bundle_writer` | code-executor · timeout 300 · 통합본 2,686–3,275행 | **정본 signal 거래 1건**을 기록한다 | 생성기·사영기·기록기를 여기서 만들지 않는다. 조립본 모듈 24종을 반입한다 — compiler 7(`common` · `projections` · `schema_validator` · `signal_compiler` · `signal_gate` · `transaction_writer` · `writer_boundary`) + adapter 4(`s3_domain_seed_adapter` · `s3_envelope_migration_adapter` · `s4_calculation_adapter` · `sg01_activation_adapter`) + emitter 13(`emitter_runtime` + `emit_sg02`~`emit_sg13`). 정본 기록기가 유일한지 검사한다(`S0_CANONICAL_WRITER_NOT_UNIQUE`). `transaction_id`는 `^S5TX-[a-f0-9]{20}$`. 슬라이스의 `emits_signals` 선언과 실제 방출을 대조해 선언 밖 방출은 `SIGNAL_EMISSION_NOT_DECLARED`로 남긴다(**선언보다 적게 나오는 것은 정상**이므로 그 방향은 세지 않는다). 기록 후 `signal_manifest.json`을 다시 읽어 해시 왕복 일치를 확인한다. 추가 개정으로 `signal_registry.v2.json` · `s5_execution_contract.v2.json` · `$ref` 폐포가 닿는 signal 스키마 17종을 `_verify_asset`으로 읽으면서 매니페스트 해시와 대조한다. 다섯째 회차에서는 배포 처방 문구(`PART2_DEPLOYMENT_REMEDY`)만 A0·R0 와 함께 배포 원본 `extension_research/Default_Agent` 기준으로 현행화됐다 | @@ -350,7 +350,7 @@ Part 1 개정이 함께 만든 **검증·회귀 자산**(런타임 입력이 아 |---|---|---|---| | P2-A0 | `Task_C_BO_A0_…slice_compiler` | **Part 1 산출 6종** — `quality_gates/stage1_part1_soft_gate_handoff.json` · `routing/domain_activation_manifest.json` · `routing/domain_screening.json` · `evidence_indexed.json` · `evidence_event_candidates.json` · `routing/candidate_profile_vocabulary.md` + **사건 입력** `client_meeting.md` (md) + **자산** `runtime_manifest.json` · `domains/_registry_index.json` · `domains/_common/common_worker_contract.md` · `domains//domain_config.json` ×26 · `domains//seed_prompt_overlay.md` (config의 `prompt_overlay_ref`가 지목) · `stage1_runtime/prompt_composition_policy.json` · `platform/schemas/domain_slice.schema.v2.json` · `platform/schemas/domain_fanout_plan.schema.json` + 모듈 미러 8종(`.txt`) + **배포 게이트가 존재·해시만 확인하는 자산 6종**(`domain_seed_output.schema.v3.json` · `signals/signal_registry.v2.json` · `contracts/signals/s5_execution_contract.v2.json` · `routing/extension_payload_key_declarations.v1.json` · `stage1_runtime/worker_output_validator.txt` · `stage1_runtime/bo_surface_projection_policy.v1.json` — 하류 R0·F0·S0 이 쓸 것을 A0 가 미리 본다) | `runtime/domain_slices/.json` (json ×M, `task_c_bo_stage_b_domain_slice.v2`, 루트 키 `stage_b_domain_slice`) · `runtime/compiled_prompts/.md` (md ×M) · `fanout/domain_fanout_plan.json` (json, `domain_fanout_plan.v1`) · `stage1_tmp/task_c_bo/stage_a_context.json` (json, 루트 `stage_a_context`) · `stage1_tmp/task_c_bo/source_universe_manifest.json` (json) · `validation_assets/routing/stage_receipt.json` (json, `stage1_stage_receipt.v2`) | | P2-B | `Task_C_B_domain_worker_*` | `{{item.compiled_prompt_path}}` (md) · `{{item.slice_path}}` (json) — **둘뿐** | `{{item.expected_output_path}}` = `runtime/domain_seed_outputs/.json` (json, `task_c_bo_stage_b_domain_bo_seed.v3`) | -| P2-R0 | `Task_C_BO_R0_…exception_planner` | `fanout/domain_fanout_plan.json` · `runtime/domain_slices/.json` ×M · `runtime/domain_seed_outputs/.json` ×M · `stage1_tmp/task_c_bo/stage_a_context.json` · `stage1_tmp/task_c_bo/source_universe_manifest.json` (json) + 자산 `platform/schemas/domain_seed_output.schema.v3.json` · `stage1_runtime/bo_surface_projection_policy.v1.json`(판본까지 검사) · `runtime_manifest.json` + 모듈 미러 4종 · **선언만 하고 읽지 않는 상수 2개**(`ACTIVATION_MANIFEST_PATH` · `REGISTRY_INDEX_PATH` — 각각 대입 1회뿐이고 read 0회). 추가 개정으로 seed 스키마와 검증기 미러는 `_verify_asset`이 **매니페스트 해시와 대조하며** 읽는다 | `stage1_tmp/task_c_bo/postb_seed_ledger.json` (json) · `quality_gates/stage1_part2_exception_pack.json` (json) · `quality_gates/stage1_part2_review_handoff.json` (json) — **셋뿐이다.** 구판의 worker seed 제자리 재기록은 다섯째 회차에서 삭제됐고, seed 파일은 worker 가 쓴 바이트 그대로 남는다 | +| P2-R0 | `Task_C_BO_R0_…exception_planner` | `fanout/domain_fanout_plan.json` · `runtime/domain_slices/.json` ×M · `runtime/domain_seed_outputs/.json` ×M · `stage1_tmp/task_c_bo/stage_a_context.json` · `stage1_tmp/task_c_bo/source_universe_manifest.json` (json) + 자산 `platform/schemas/domain_seed_output.schema.v3.json` · `stage1_runtime/bo_surface_projection_policy.v1.json`(판본까지 검사) · `runtime_manifest.json` + 모듈 미러 4종 (구판의 사문 경로 상수 2개는 2026-08-20 자산 정리 회차에서 삭제됐다). 추가 개정으로 seed 스키마와 검증기 미러는 `_verify_asset`이 **매니페스트 해시와 대조하며** 읽는다 | `stage1_tmp/task_c_bo/postb_seed_ledger.json` (json) · `quality_gates/stage1_part2_exception_pack.json` (json) · `quality_gates/stage1_part2_review_handoff.json` (json) — **셋뿐이다.** 구판의 worker seed 제자리 재기록은 다섯째 회차에서 삭제됐고, seed 파일은 worker 가 쓴 바이트 그대로 남는다 | | P2-R1 | `Task_C_BO_R1_exception_adjudicator` | `quality_gates/stage1_part2_exception_pack.json` (json, preflight) | `stage1_tmp/task_c_bo/postb_adjudication_decisions.json` (json, **조건부**) | | P2-F0 | `Task_C_BO_F0_…gate_writer` | `stage1_tmp/task_c_bo/stage_a_context.json` · `postb_seed_ledger.json` · `postb_adjudication_decisions.json`(조건부) · `quality_gates/stage1_part2_review_handoff.json` · `stage1_part2_exception_pack.json` (json) + 자산 `routing/extension_payload_key_declarations.v1.json` · `stage1_runtime/bo_surface_projection_policy.v1.json`(`_load_projection_policy` — 매니페스트 sha 대조 후 사용) · `runtime_manifest.json` | **`BO.json`** (json) · `stage1_tmp/task_c_bo/postb_compiled_bundle_compact.json` (json) · `quality_gates/stage1_part2_review_handoff.json` (json, `FINALIZED`로 **갱신 재기록** — R0과 공동 기록자) | | P2-S0 | `Task_C_BO_S0_signal_bundle_writer` | `BO.json` · `routing/domain_activation_manifest.json` · `fanout/domain_fanout_plan.json` · `stage1_tmp/task_c_bo/source_universe_manifest.json` · `runtime/domain_slices/.json` ×M (json) + 자산 `signals/signal_registry.v2.json` · `contracts/signals/s5_execution_contract.v2.json` · `runtime_manifest.json` + 모듈 미러 24종 | `signals/…` 아래 정본 signal 집합 — `signals/signal_manifest.json` 포함 + 호환 뷰 3종 `signals/compatibility_views/{actio_case,case_liability,legal_effect}_signals.json` + **루트 별칭 3종** `actio_case_signals.json` · `case_liability_signals.json` · `legal_effect_signals.json` (구 이름 호환면, 같은 바이트) | @@ -378,7 +378,7 @@ YAML이 참조하는 `Default_Agent/…` 자산은 **21개 리터럴**이다(다 | 배포 경로 (`Default_Agent/` 기준) | 크기 | 형태 | 쓰는 task | 역할 | |---|---|---|---|---| -| `domains/_registry_index.json` | 18,921 B | json | A0 | registry 색인. 봉인 대조 대상. (R0에도 경로 상수가 있으나 읽지 않는다) | +| `domains/_registry_index.json` | 18,921 B | json | A0 | registry 색인. 봉인 대조 대상 | | `domains/_common/common_worker_contract.md` | **3,766 B** | md | A0 | 공통 worker 계약문. 사건 종류 이름을 라우팅 키로 쓰지 않는다는 규율을 여기서 선언한다. 다섯째 회차가 출력 규칙 한 문장을 더했다 — 검토는 v3 두 키(`review_items` · `unknown_or_unrouted_reviews`)로만 보내고 `domain_review_queue`(오버레이에 남은 구 이름)는 금지, 방어 후보의 정본 키는 `defense_candidates`. rank 10 계약이 rank 30 오버레이를 이기므로 오버레이 25건 · 색인 · Part 1 봉인은 전부 바이트 무변이다 | | `domains//domain_config.json` ×26 | — | json | A0 | 8선언 가문 원문 (`element_slots` · `opposing_fact_slots` · `evidence_components` · `defense_map` · `calculation_bindings` · `emits_signals` · `structure_types` · `effect_projection`) | | `domains//seed_prompt_overlay.md` | — | md | A0 | 도메인 오버레이 산문. **파일명을 하드코딩하지 않고** config의 `prompt_overlay_ref`가 지목하는 것을 따른다 | @@ -560,7 +560,7 @@ Part 1이 만들어 **Part 2가 읽는** 파일은 여섯이다. | 12 | embedded python | AST 실패 0 · 미정의 이름 0 · 미사용 def 1(`clip`, v3 원문) | | 13 | 인계면 전수 대조 | PASS · finding 0 | | 14 | 스키마 미지원 키워드 | PASS · 3/3 | -| 15 | 개별 6종 ↔ 통합본 | 불일치 0 (다섯째 회차 뒤 재대조 — P2-A0·R0·F0·S0 4종을 통합본 블록과 동기화한 뒤 블록 동일 재확인. R0 블록 946행) | +| 15 | 개별 6종 ↔ 통합본 | 불일치 0 (다섯째 회차 뒤 재대조 — P2-A0·R0·F0·S0 4종을 통합본 블록과 동기화한 뒤 블록 동일 재확인. R0 블록 944행 — 2026-08-20 사문 상수 삭제 후 재동기화·재확인) | | 16 | 26 도메인 전수 compile (스키마 주입) | READY / slice_count 26 / 스키마 실패 0 | | 17 | `calculation_registry` · `computations/` 참조 | 0 · 0 | | 18 | `release_manifest` 전수 해시·크기 | **1,320항** · 불일치 **1** — `tools/build_default_agent_stage1.py`(장부 102,559 B vs 실물 102,997 B). 다섯째 회차의 빌더 제자리 수정(뿌리 6→10)이 조립본 장부에 미반영된 흔적으로, 이월 D-2 의 재생성 대상이다 | @@ -721,7 +721,7 @@ Part 2 개정은 독립 sub-agent 검증 3라운드에서 결함 10건을 닫았 | 주장 | 근거 | |---|---| -| 두 통합본의 크기·행수·sha256 | 원문 바이트 재계산 — Part 1 467,668 B / 6,667행 / `195b43edfaf1df14…`, Part 2 **195,938 B / 3,315행 / `ec4c43027ad85195…`** | +| 두 통합본의 크기·행수·sha256 | 원문 바이트 재계산 — Part 1 467,668 B / 6,667행 / `195b43edfaf1df14…`, Part 2 **195,963 B / 3,315행 / `ec407309a5ab43f4…`** | | YAML 구문 PASS · 최상위 키 `Agent` | `yaml.safe_load` 재실행 | | task 13 / 6 및 행 범위 | `- task_name:` 정규식 전수 추출 | | DAG 노드·edge | `task_procedure` 블록 원문 (Part 1 6,589–6,664행 · Part 2 **3,277–3,312행**) | @@ -754,7 +754,7 @@ Part 2 개정은 독립 sub-agent 검증 3라운드에서 결함 10건을 닫았 | 심각도 | 건수 | 내용 | |---|---|---| | BLOCKER | 1 | §0 요약표가 Part 2의 사건 입력을 "없음"으로 적었다. Part 2 A0는 `client_meeting.md`를 `read_raw`로 직접 읽는다(통합본 515행). 같은 문서의 §3.3·§4.1과 자기모순이었다 | -| MAJOR | 5 | ① R0의 **네 번째 산출** 누락 — worker seed 파일 M개를 제자리에서 다시 쓴다(`transport_metadata`·`handoff_guard` 주입) ② R0의 IN에 적은 `domain_activation_manifest.json`·`_registry_index.json`은 **선언만 하고 읽지 않는 상수**다(각 대입 1회·read 0회) ③ 스테이지 기본 LLM은 Part 1도 `openai / gpt-4o-2024-08-06`으로 같다 ④ T1의 `preflight` 플래그는 `false`다(화이트리스트만 선언) ⑤ 검사 20의 `__pycache__ 0`은 후보 오버레이 기준이고 정본 조립본에는 `.pyc` 3이 남아 있다 | +| MAJOR | 5 | ① R0의 **네 번째 산출** 누락 — worker seed 파일 M개를 제자리에서 다시 쓴다(`transport_metadata`·`handoff_guard` 주입) ② R0의 IN에 적은 `domain_activation_manifest.json`·`_registry_index.json`은 **선언만 하고 읽지 않는 상수**다(각 대입 1회·read 0회 — 그 상수 2개는 2026-08-20 자산 정리 회차에서 삭제됐다) ③ 스테이지 기본 LLM은 Part 1도 `openai / gpt-4o-2024-08-06`으로 같다 ④ T1의 `preflight` 플래그는 `false`다(화이트리스트만 선언) ⑤ 검사 20의 `__pycache__ 0`은 후보 오버레이 기준이고 정본 조립본에는 `.pyc` 3이 남아 있다 | | MINOR | 8 | Part 1 행수 6,668 → **6,667**(리포트에서 옮겨 온 값이 실측과 달랐다) · `task_procedure` 행 범위가 스테이지 층 `prevs`/`nexts`를 삼켰다 · T5가 shard 원문을 조건부로 읽는다 · T3·T4의 `max_concurrency: 12` 누락 · §2.5 표의 행 구성 설명 · `source_copy_map.json`은 두 뿌리 바이트 동일 · 예행의 세 번째 한계(Part 1 code task 8종 중 5종만 실행) · 인계면 선언표의 누락 2줄(`client_meeting.md` 소비자에 Part 2 A0, seed 경로의 `also_written_by`) | 검증이 **깨끗하다고 확인한 것**도 기록해 둔다. task 13/6 전수가 세 표(소작업·IO·연결)에 모두 오른 것, DAG 노드·edge·와일드카드 토큰 전부 일치, §2.5·§3.5·§4.1·§4.3에 인용한 바이트 크기 44개(중복 제외) 전부 정확, 두 뿌리 대조 수치(528/7/23)와 미러 규약 실측, 인계면 34와 구간 분포, task별 행 범위와 v3 대비 바이트 동일성(Part 1 무변경 6종 + Part 2 R1), 그리고 26 도메인 투영 합계 일곱(162/85/112/83/73/267/53)이 `domains/*/domain_config.json` 전수 재계산으로 정확히 재현되는 것. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/ver_8_yaml_candidates/P2-R0_seed_reducer_and_exception_planner_v4.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/ver_8_yaml_candidates/P2-R0_seed_reducer_and_exception_planner_v4.yml index 42c88aa4..44575f6b 100644 --- a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/ver_8_yaml_candidates/P2-R0_seed_reducer_and_exception_planner_v4.yml +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/ver_8_yaml_candidates/P2-R0_seed_reducer_and_exception_planner_v4.yml @@ -65,8 +65,6 @@ "registry_loader": "Default_Agent/stage1_runtime/registry_loader.txt", "worker_output_validator": "Default_Agent/stage1_runtime/worker_output_validator.txt", } - ACTIVATION_MANIFEST_PATH = "routing/domain_activation_manifest.json" - REGISTRY_INDEX_PATH = "Default_Agent/domains/_registry_index.json" def _seed_docs_from_plan(plan): # v5 — 경로만이 아니라 계획 행 전체를 보관한다. validate_seed_object 가 diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/ver_8_yaml_candidates/outdated/_pre_asset_triage_fix/P2-R0_seed_reducer_and_exception_planner_v4.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/ver_8_yaml_candidates/outdated/_pre_asset_triage_fix/P2-R0_seed_reducer_and_exception_planner_v4.yml new file mode 100644 index 00000000..42c88aa4 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/ver_8_yaml_candidates/outdated/_pre_asset_triage_fix/P2-R0_seed_reducer_and_exception_planner_v4.yml @@ -0,0 +1,967 @@ +# ============================================================================= +# P2-R0 / Task_C_BO_R0_seed_reducer_and_exception_planner — v4 (개정본) +# +# 원본 : Claude_YAML/Stage_1_Part_2_Claude_v3.yml 2827~3620행 (794행) +# 근거 : stage_1_part_2_optimal_update_strategy_v.2.md §6.3 / stage_1_update_strategy.md §3.3 +# +# v3 대비 변경 셋. 그 외는 무변경이다 — 병합·전역 출처 검증·예외 축소 로직은 손대지 않았다. +# 1. seed 원천 교체 +# v3 는 SEED_INPUTS 의 {{prev.Task_C_BO_Stage_B_B*.json_output}} 다섯을 읽었다. +# v4 는 A0 가 낸 fanout/domain_fanout_plan.json 의 task_instances[*].expected_output_path +# 를 따라 읽는다. 정적 다섯 이름과 stage1_tmp 구 경로가 함께 사라진다. +# 2. worker_output_validator 호출 +# seed 마다 slice·registry·activation_manifest 를 물려 검증한다. +# domain_seed_output.schema.v3.json 을 주입한다 — 넘기지 않으면 검증이 조용히 건너뛰어진다. +# 3. C-2 — meeting_only_evidence_gap 판정 확장 (P0 판정 B) +# registry_component_ids 로 뒷받침된 seed 는 증거 유래로 인정한다. +# 값이 비면 종전과 완전히 같으므로 승계 5도메인 등가가 깨지지 않는다. +# +# 무변경 확인 +# mcp code-executor / parameters 5키 / CANDIDATE_REF_RE / add_policy_review / +# 전역 출처 universe 재검사 / 예외 축소 / postb_seed_ledger 구조 +# ============================================================================= + - 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 hashlib + 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() + + # v4 — {{prev.Task_C_BO_Stage_B_B*}} 다섯을 걷어냈다. + # worker 산출은 wildcard fan-out 인스턴스가 파일로 남기므로 경로로 읽는다. + # v4 — seed 목록은 상수가 아니라 A0 의 fan-out 계획이 정한다. + FANOUT_PLAN_PATH = "fanout/domain_fanout_plan.json" + SLICE_DIR = "runtime/domain_slices" + SEED_SCHEMA_PATH = "Default_Agent/platform/schemas/domain_seed_output.schema.v3.json" + SEED_SCHEMA_VERSION = "task_c_bo_stage_b_domain_bo_seed.v3" + SLICE_ROOT_KEY = "stage_b_domain_slice" + # R-4 — 머리말이 약속한 worker_output_validator 를 실제로 부른다. 반입은 D0 규약 R-1~R-5. + RUNTIME_MANIFEST = "Default_Agent/runtime_manifest.json" + EXECUTION_ROOT = "/tmp/s1_r0" + VALIDATOR_MIRRORS = { + "runtime_common": "Default_Agent/stage1_runtime/runtime_common.txt", + "schema_subset_validator": "Default_Agent/stage1_runtime/schema_subset_validator.txt", + "registry_loader": "Default_Agent/stage1_runtime/registry_loader.txt", + "worker_output_validator": "Default_Agent/stage1_runtime/worker_output_validator.txt", + } + ACTIVATION_MANIFEST_PATH = "routing/domain_activation_manifest.json" + REGISTRY_INDEX_PATH = "Default_Agent/domains/_registry_index.json" + + def _seed_docs_from_plan(plan): + # v5 — 경로만이 아니라 계획 행 전체를 보관한다. validate_seed_object 가 + # slice_sha256 · compiled_prompt_sha256 기대값을 이 행에서 대조한다(R0-6). + root = plan.get("domain_fanout_plan", plan) + out = {} + rows = {} + for row in root.get("task_instances") or []: + domain_id = row.get("domain_id") + path = row.get("expected_output_path") + if isinstance(domain_id, str) and isinstance(path, str) and domain_id and path: + out[domain_id] = path + rows[domain_id] = row + if not out: + raise RuntimeError("R0_FANOUT_PLAN_EMPTY") + return out, rows + # v4 — 계획이 정하는 두 목록. 상수가 아니므로 비워 두고 main 에서 내용만 채운다. + # 재바인딩하지 않고 갱신만 하므로 아래 도우미들이 같은 객체를 본다. + SEED_DOCS: dict[str, str] = {} + PLAN_ROWS: dict[str, dict[str, Any]] = {} + DOMAIN_ORDER: list[str] = [] + + # DOMAIN_ORDER 는 fan-out 계획의 등재 순서를 그대로 쓴다. 상수 순서를 두지 않는다. + def _domain_order(seed_docs): + return list(seed_docs.keys()) + # v5 — 구 이름 표(DOMAIN_LABELS)와 _domain_label 을 걷어냈다. 유일 소비처가 되쓰기 + # (R0-5 에서 삭제)의 transport_metadata 였다. 이로써 R0 에 구 명세서(B1~B5) 이름 의존이 없다. + 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" + # R0-1 — BO 투영 정책. 투영 규칙의 정본은 코드가 아니라 이 선언 자산이다. + BO_PROJECTION_POLICY = "Default_Agent/stage1_runtime/bo_surface_projection_policy.v1.json" + BO_PROJECTION_SCHEMA_VERSION = "stage1_bo_surface_projection_policy.v1" + # v3 계약이 정본이다. 도메인 ID 는 registry 값(E-00 · EC-00 · X1 …)이고 구 이름도 아직 들어올 수 있으므로 + # 접두사는 도메인에 묶지 않고 형식만 본다 — 도메인 일치는 validate_candidate 의 prefix 검사가 맡는다. + CANDIDATE_REF_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_.-]{0,63}:[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"} + # v5 — ALLOWED_SEED_KEYS(v2 화이트리스트)를 걷어냈다. v3 후보 18필드와의 교집합이 + # extensions 하나뿐이라 워커 산출을 통째로 버리던 자리다(C-1). 원장 payload 의 + # 키 집합은 project_to_bo_surface 의 반환문이 유일한 정의다. + + 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_raw(name: str) -> str: + # 재직렬화 없이 원문 그대로 돌려준다. 미러 해시 대조의 전제다. + p = _call("read_docs", {"doc_names": [name]}) + text = (p["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError("EMPTY_RESPONSE:%s" % name) + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, str): + return outer + if isinstance(outer, dict) and "results" in outer: + r0 = (outer.get("results") or [{}])[0] + inner = r0.get("content") + if inner is None: + inner = r0.get("text") + if not isinstance(inner, str) or not inner: + raise RuntimeError("EMPTY_CONTENT:%s" % name) + return inner + raise RuntimeError("UNEXPECTED_READ_DOCS_ENVELOPE:%s" % name) + + + # ------------------------------------------------------------------ + # F-4 — 자기 입력의 배포 무결성. A0 게이트가 앞에서 걸러 주지만 이 task 는 + # 개별 재실행이 가능하다(예행 하네스가 실제로 task 단위로 돌린다). + # 매니페스트는 task 당 한 번만 읽는다. 호출마다 읽으면 S0 의 $ref 폐포 walk 에서만 19 회다. + # ------------------------------------------------------------------ + PART2_DEPLOYMENT_REMEDY = ( + "배포 원본 extension_research/Default_Agent 트리를 서버 Default_Agent/ 에 통째로 배포하고 " + "runtime_manifest.json 의 등재·sha256 과 대조해 결손 경로를 복원한다. 부분 복사는 허용되지 않는다.") + _MANIFEST_MAP = None + + + def _manifest_map(): + global _MANIFEST_MAP + if _MANIFEST_MAP is None: + _MANIFEST_MAP = {row["path"]: row["sha256"] + for row in (json.loads(read_raw(RUNTIME_MANIFEST)).get("entries") or []) + if isinstance(row, dict)} + return _MANIFEST_MAP + + + def _deployment_error(logical, detail, extra=None): + payload = {"reason_code": "PART2_ASSET_DEPLOYMENT_INCOMPLETE", "path": logical, + "detail": detail, "remedy": PART2_DEPLOYMENT_REMEDY} + if extra: + payload.update(extra) + return RuntimeError(json.dumps(payload, ensure_ascii=False)) + + + def _verify_asset(logical: str) -> str: + """원문을 돌려준다. 반환값을 소비해야 읽기 횟수가 늘지 않는다.""" + body = read_raw(logical) + want = _manifest_map().get(logical[len("Default_Agent/"):]) + if want is None: + raise _deployment_error(logical, "unregistered") + if want != hashlib.sha256(body.encode("utf-8")).hexdigest(): + raise _deployment_error(logical, "hash_mismatch", {"expected": want}) + return body + + + def _assert_mirror_consistent(logical: str) -> None: + """F-4b — 부재는 통과(materialize_validator 의 기존 관용 유지). 미등재·불일치만 막는다. + + 예외 종류를 바꿔 try 를 뚫는 우회(SystemExit 등)는 쓰지 않는다. 그것은 __main__ 가드의 + stdout 출력과 예행 하네스의 단계 기록까지 건너뛴다. 판정을 try 밖으로 옮기는 것이 답이다. + """ + try: + body = read_raw(logical) + except Exception: + return + want = _manifest_map().get(logical[len("Default_Agent/"):]) + if want is None: + raise _deployment_error(logical, "unregistered") + if want != hashlib.sha256(body.encode("utf-8")).hexdigest(): + raise _deployment_error(logical, "hash_mismatch", {"expected": want}) + + + def materialize_validator() -> list[str]: + """worker_output_validator 와 그 의존 셋을 반입한다. 실패는 경고로 남기고 진행한다. + + 이 검증은 덧붙이는 층이다 — 반입이 안 되는 배포에서도 R0 본체는 돌아야 한다. + """ + import hashlib + import os + import pathlib + rt = pathlib.Path(EXECUTION_ROOT) / "_rt" + rt.mkdir(parents=True, exist_ok=True) + manifest_doc = json.loads(read_raw(RUNTIME_MANIFEST)) + expected = {row["path"]: row["sha256"] for row in manifest_doc.get("entries") or []} + staged: list[str] = [] + for name, logical in VALIDATOR_MIRRORS.items(): + body = read_raw(logical) + raw = body.encode("utf-8") + want = expected.get(logical[len("Default_Agent/"):]) + if want is None: + raise RuntimeError("MODULE_MIRROR_UNREGISTERED:%s" % logical) + if want != hashlib.sha256(raw).hexdigest(): + raise RuntimeError("MODULE_MIRROR_HASH_MISMATCH:%s" % logical) + (rt / (name + ".py")).write_bytes(raw) + staged.append(name) + if str(rt) not in sys.path: + sys.path.insert(0, str(rt)) + return staged + + + 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() + "..." + + # v4 — parse_llm_json 을 걷어냈다. worker 가 {{item.expected_output_path}} 에 + # strict JSON 파일을 직접 쓰므로 LLM 원문 관용 파싱 경로가 없어졌다. + # salvage_notes 는 산출 스키마에 남지만 v4 에서는 항상 빈 목록이다 — 구제할 원문이 없다. + # v5 — DOMAIN_PAYLOAD_CANON · _canon_payload 를 걷어냈다. canon 키가 구 이름(B1~B4)뿐이라 + # registry ID 26종 전부에서 no-op 였다(사문 코드). 확장 payload 는 워커 발행 형태 그대로 둔다. + def ensure_candidate_ref(cand: dict[str, Any], domain_id: str, idx: int) -> dict[str, Any]: + """v3 워커 출력에는 candidate_ref 가 없다 — seed 스키마가 additionalProperties: false 로 봉인돼 + 워커가 실을 수 없는 필드다. v3 이 주는 순번에서 R0 내부 식별자를 결정적으로 만든다. + 이미 실려 있으면(구 판본 산출) 그대로 둔다.""" + ref = cand.get("candidate_ref") + if isinstance(ref, str) and ref: + return cand + out = dict(cand) + out["candidate_ref"] = "%s:%03d" % (domain_id, idx + 1) + return out + + def project_to_bo_surface(cand: dict[str, Any], domain_id: str, universe: dict[str, set[str]], + policy: dict[str, Any], allowed_bo_types: set[str], + reviews: list[dict[str, Any]]) -> dict[str, Any]: + """v3 후보를 BO 호환면으로 투영한다. 값의 정본은 registry 이고 규칙은 정책 파일이 선언한다. + + 전임자 둘(expand_candidate + _seed_payload)은 v2 키를 기본값으로 깔고 v2 화이트리스트로 + 걸렀다. v3 후보를 넣으면 워커가 실은 값이 extensions 하나만 남았고, 그 결과 중복 판정 키 + 여덟 성분이 전부 비어 사건 전체가 한 버킷으로 접혔다(C-1·C-2). 여기서는 v3 필드에서 + 끌어오고, registry 가 말해 주지 않는 칸은 채우지 않고 reviews 에 올린다. + 반환 키 집합은 입력과 무관하게 고정이다 — 이 반환문이 원장 payload 키 집합의 유일한 정의다. + """ + ref = str(cand.get("candidate_ref")) + + def note(issue_type: str, field: str, source: str) -> None: + reviews.append({"issue_type": issue_type, "candidate_ref": ref, + "field": field, "source": source}) + + refs = _strings(cand.get("source_refs")) + evidence = sorted(set(refs) & universe["source_evidence_indexes"]) + events = sorted(set(refs) & universe["source_event_candidate_ids"]) + clauses = sorted(set(refs) & universe["source_meeting_clause_ids"]) + + norm = _dict(policy.get("f0_normalization")) + bo_type = cand.get("bo_type") + if allowed_bo_types and bo_type not in allowed_bo_types: + note("legal_effect_uncertain", "BOType", "bo_type") + + ext = dict(_dict(cand.get("extensions"))) + if not isinstance(ext.get("domain_payload"), dict): + ext["domain_payload"] = {} + domain_payload = _dict(ext.get("domain_payload")) + + action_type = domain_payload.get("action_type") + if not (isinstance(action_type, str) and action_type in set(_strings(norm.get("action_type_enum")))): + # registry 근거가 없는 칸이다. 기본값은 선언이며 추정이 아니다 — 반드시 검토로 올린다. + action_type = norm.get("action_type_default") + note("schema_field_fallback", "ActionType", "policy_default") + + effect_type_ids = sorted({str(e.get("type_id")).strip() + for e in _list(cand.get("legal_effect_candidates")) + if isinstance(e, dict) and str(e.get("type_id") or "").strip()}) + action_summary = domain_payload.get("action_summary") + if isinstance(action_summary, str) and action_summary.strip(): + action = action_summary.strip() + elif effect_type_ids: + # 값은 registry token 이지 서술문이 아니다. Stage 2 는 review_handoff 의 action_source 를 함께 읽는다. + action = "%s:%s" % (bo_type, effect_type_ids[0]) + note("schema_field_fallback", "Action", "legal_effect_type_id") + else: + action = str(bo_type) + note("schema_field_fallback", "Action", "bo_type") + + time_facts = [t for t in _list(cand.get("time_facts")) if isinstance(t, dict)] + behavior_time = None + time_text = None + if time_facts: + pick = sorted(time_facts, key=lambda t: (str(t.get("fact_type") or ""), str(t.get("value") or "")))[0] + behavior_time = pick.get("value") + time_text = pick.get("value") + distinct_times = {str(t.get("value") or "").strip() for t in time_facts if str(t.get("value") or "").strip()} + if len(distinct_times) > 1: + note("amount_or_date_uncertain", "core_field_base.BehaviorTime", "time_facts") + + object_refs = sorted(_strings(cand.get("object_refs"))) + + amount_facts = [a for a in _list(cand.get("amount_facts")) if isinstance(a, dict)] + amount = None + if amount_facts: + pick = sorted(amount_facts, key=lambda a: (str(a.get("amount_type") or ""), str(a.get("decimal_value") or "")))[0] + # v3 amount_facts 는 {amount_type, decimal_value, currency, source_refs} 닫힌 스키마다 — + # value_text 필드가 없으므로 정책 규칙대로 decimal_value 원문을 그대로 쓴다. + amount = {"value_text": pick.get("decimal_value"), + "numeric_value": pick.get("decimal_value"), + "currency": pick.get("currency")} + distinct_amounts = {str(a.get("decimal_value") or "").strip() for a in amount_facts if str(a.get("decimal_value") or "").strip()} + if len(distinct_amounts) > 1: + note("amount_or_date_uncertain", "amount", "amount_facts") + + return { + "candidate_ref": ref, + "source_domain": domain_id, + "BOType": bo_type, + "ActionType": action_type, + "JuristicAct": _normalize_juristic(cand.get("juristic_act_type")), + "Action": action, + "Reason": None, + "PriorAct": None, + "ReasonRefs": [], + "Legal_Keywords": effect_type_ids, + "core_field_base": {"BehaviorTime": behavior_time, "TimeText": time_text, + "Object": object_refs[0] if object_refs else None, + "StatementType": bo_type}, + "amount": amount, + "source_evidence_indexes": evidence, + "provenance": {"source_event_candidate_ids": events, + "source_meeting_clause_ids": clauses, + "source_evidence_indexes": evidence, + "source_domain": domain_id}, + "downstream_seed_refs": {}, + "extensions": ext, + "registry_component_ids": _strings(cand.get("registry_component_ids")), + } + + def expand_review_item(item: Any, domain_id: str, idx: int) -> dict[str, Any]: + """v3 검토 항목(후보별 review_items · 루트 unknown_or_unrouted_reviews)을 handoff 항목형으로 사상한다. + + 구판은 v3 루트 13키에 없는 domain_review_queue 를 읽었다 — 봉인(additionalProperties: false)이 + 워커에게 발행을 금지한 키라 워커 검토가 한 건도 도달하지 못했다(C-6). 사상 규칙은 정책 + review_item_projection 이 선언한다. 원본 코드는 덧붙이기 키 source_review_code 로 보존한다. + """ + src = _dict(item) + raw_type = str(src.get("unresolved_type") or "").strip() + raw_code = str(src.get("review_code") or "").strip() + severity = src.get("severity") if src.get("severity") in ("SOFT_WARNING", "HARD_WARNING") else "SOFT_WARNING" + return { + "review_id": str(src.get("review_id") or f"{domain_id}:review:{idx:03d}"), + "issue_type": raw_type if raw_type in REVIEW_ISSUE_ENUM else "review_required", + "severity": severity, + # 원본 review_code(v3 필수 키)를 잃지 않는다 — 정책 additive_keys 의 목적이 그것이다. + "source_review_code": raw_code or raw_type or None, + "reason": str(src.get("reason") or "").strip(), + "source_refs": _strings(src.get("source_refs")), + "recommended_downstream_owner": src.get("recommended_downstream_owner") or "Stage2", + } + + # ---------- 워커 출력 수용 검증 (v3 계약 정본 · 정책 status_policy · 계획 해시 대조) ---------- + def validate_seed_object(seed_obj: dict[str, Any], domain_id: str, plan_row: dict[str, Any], warnings: list[dict[str, Any]]) -> None: + """v3 seed 루트를 검증한다. status 는 v3 enum 5종을 정책 status_policy 로 가른다. + + 구판은 READY 계열 2종만 허용해 계약상 적법한 NO_SUPPORT 가 R0 전체를 중단시켰고(C-11), + 신선도는 워커가 실을 수 없는 transport_metadata.slice_guard 를 읽는 죽은 검사였다. + 신선도의 제 필드는 v3 루트의 slice_sha256 · compiled_prompt_sha256 이고(둘 다 required + — 워커가 반드시 echo 한다), 기대값은 fan-out 계획 행이 든다. + """ + if seed_obj.get("schema_version") != SEED_SCHEMA_VERSION: + 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") + status = seed_obj.get("status") + if status in ("BLOCKED", "FAILED"): + # 워커 실패 신호다. fail-open 은 활성화 판정의 원칙이고, 실패의 침묵 흡수는 금지 원칙이 막는다. + raise ValueError(f"{domain_id}: worker reported {status}") + if status == "NO_SUPPORT": + # 적법한 "실을 것 없음". 후보가 있으면 상태·내용 모순이다. + if _list(seed_obj.get("bo_seed_candidates")): + raise ValueError(f"{domain_id}: NO_SUPPORT with non-empty bo_seed_candidates") + elif status not in ("READY", "READY_WITH_REVIEW"): + raise ValueError(f"{domain_id}: seed status outside v3 enum: {status!r}") + for key in ("slice_sha256", "compiled_prompt_sha256"): + want = plan_row.get(key) + if isinstance(want, str) and want: + if seed_obj.get(key) != want: + raise ValueError(f"{domain_id}: stale seed output: {key} mismatch") + else: + warnings.append({"domain_id": domain_id, "warning": f"fanout plan carries no {key} expectation"}) + + def validate_candidate(cand: dict[str, Any], domain_id: str, idx: int) -> None: + 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 len(DOMAIN_ORDER), + "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 _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() + # F-4a — 자기 정적 입력. try 밖이어야 한다. 안에 넣으면 아래 except Exception 이 + # 삼켜 WORKER_VALIDATOR_UNAVAILABLE 경고로 강등되고 R0 이 계속 돈다. + _seed_schema_body = _verify_asset(SEED_SCHEMA_PATH) + # R0-1 — 투영 정책 반입 (F-4a 와 같은 규율: try 밖 경성). 정책이 없거나 낡았는데 + # 조용히 옛 규칙으로 도는 것이 이번 결손(v2 잔재)의 재발 경로다. + projection_policy = _dict(json.loads(_verify_asset(BO_PROJECTION_POLICY))) + if projection_policy.get("schema_version") != BO_PROJECTION_SCHEMA_VERSION: + raise RuntimeError("PART2_PROJECTION_POLICY_INVALID") + # F-4b — 미러 넷의 무결성. 부재는 통과시키고 미등재·불일치만 막는다. + for _mirror in VALIDATOR_MIRRORS.values(): + _assert_mirror_consistent(_mirror) + salvage_notes: list[dict[str, Any]] = [] + guard_warnings: list[dict[str, Any]] = [] + # v4 — seed 목록과 그 순서는 A0 의 fan-out 계획이 정한다. 이 파일은 목록을 만들지 않는다. + worker_validator = None + seed_schema = None + try: + materialize_validator() + import worker_output_validator as worker_validator + seed_schema = json.loads(_seed_schema_body) + except Exception as exc: + guard_warnings.append({"code": "WORKER_VALIDATOR_UNAVAILABLE", "message": str(exc)[:200]}) + worker_validator = None + _docs, _rows = _seed_docs_from_plan(_dict(read_json_doc(FANOUT_PLAN_PATH))) + SEED_DOCS.update(_docs) + PLAN_ROWS.update(_rows) + DOMAIN_ORDER.extend(_domain_order(SEED_DOCS)) + 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) 워커 출력 수용: 검증 -> 투영. 워커 seed 파일은 손대지 않는다 — + # 선언표(stage1_part_interface.v1)가 기록자를 워커 하나로 정했다(R0-5). + seed_objects: dict[str, dict[str, Any]] = {} + projected_candidates: dict[str, list[dict[str, Any]]] = {} + review_handoff_items: list[dict[str, Any]] = [] + allowed_bo_types_by_domain: dict[str, set[str]] = {} + projection_review_counter = 0 + for domain_id in DOMAIN_ORDER: + # v4 — worker 가 {{item.expected_output_path}} 에 자기 seed 를 직접 쓴다. + # {{prev}} 원문 관용 파싱이 아니라 계획이 정한 경로에서 읽는다. + outer = _dict(read_json_doc(SEED_DOCS[domain_id])) + 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, PLAN_ROWS.get(domain_id) or {}, guard_warnings) + # 슬라이스는 검증기 유무와 무관하게 읽는다 — worker_output_validator 와 + # BOType 허용 어휘(allowed_legal_effect_bo_types, registry 유래)가 이 값을 쓴다. + try: + slice_doc = read_json_doc("%s/%s.json" % (SLICE_DIR, domain_id)) + except Exception: + slice_doc = None + slice_root = _dict(_dict(slice_doc).get(SLICE_ROOT_KEY)) if isinstance(slice_doc, dict) else {} + allowed_bo_types = set(_strings(slice_root.get("allowed_legal_effect_bo_types"))) + allowed_bo_types_by_domain[domain_id] = allowed_bo_types + # R-4 — 스키마와 슬라이스를 실제로 넘긴다. 넘기지 않으면 검증이 조용히 건너뛰어진다. + if worker_validator is not None: + report = worker_validator.validate_worker_output( + {"stage_b_domain_bo_seed_output": seed_obj}, + schema=seed_schema, + expected_domain_id=domain_id, + slice_document=slice_doc) + for item in report.get("errors") or []: + guard_warnings.append({"code": "WORKER_OUTPUT_ERROR", "domain_id": domain_id, + "detail": item}) + for item in report.get("warnings") or []: + guard_warnings.append({"code": "WORKER_OUTPUT_REVIEW", "domain_id": domain_id, + "detail": item}) + cands = _list(seed_obj.get("bo_seed_candidates")) + projected: list[dict[str, Any]] = [] + projection_reviews: 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") + cand = ensure_candidate_ref(cand, domain_id, idx) + validate_candidate(cand, domain_id, idx) + # membership 검사 — v3 의 평평한 source_refs 를 universe 와 대조 (hard BLOCK). + # 이쪽을 보지 않으면 membership 게이트가 v3 산출에서는 통과만 하는 빈 검사가 된다. + known_sources = (universe["source_event_candidate_ids"] | universe["source_meeting_clause_ids"] + | universe["source_evidence_indexes"]) + ref_bad = [v for v in _strings(cand.get("source_refs")) if v not in known_sources] + if ref_bad: + raise RuntimeError(f"BLOCK: {domain_id}.{cand.get('candidate_ref')}: source_refs outside Stage A universe: {ref_bad}") + projected.append(project_to_bo_surface(cand, domain_id, universe, projection_policy, + allowed_bo_types, projection_reviews)) + # R0-5 — 되쓰기 없음. seed_objects 는 워커 원본 그대로다(S0 와 signal adapter 가 + # v3 적합 원본을 읽는다). 투영본은 projected_candidates 가 따로 든다(R0-2 배선). + seed_objects[domain_id] = seed_obj + projected_candidates[domain_id] = projected + # R0-4 — v3 검토 채널: 후보별 review_items + 루트 unknown_or_unrouted_reviews. + # list(...) 복사는 워커 원본 목록을 제자리 변형하지 않기 위한 것이다. + worker_reviews = list(_list(seed_obj.get("unknown_or_unrouted_reviews"))) + for cand in _list(seed_obj.get("bo_seed_candidates")): + worker_reviews.extend(_list(_dict(cand).get("review_items"))) + if seed_obj.get("status") == "NO_SUPPORT": + worker_reviews.append({"review_id": f"{domain_id}:status:NO_SUPPORT", + "review_code": "NO_SUPPORT", + "unresolved_type": "review_required", + "severity": "SOFT_WARNING", + "reason": "worker reported NO_SUPPORT (nothing to carry for this domain)"}) + for idx, item in enumerate(worker_reviews, start=1): + mapped = expand_review_item(item, domain_id, idx) + refs = set(mapped.get("source_refs") or []) + review_handoff_items.append({ + "review_id": mapped["review_id"], + "source_domain": domain_id, + "severity": mapped["severity"], + "issue_type": mapped["issue_type"], + "source_review_code": mapped.get("source_review_code"), + "source_event_candidate_ids": sorted(refs & universe["source_event_candidate_ids"]), + "source_evidence_indexes": sorted(refs & universe["source_evidence_indexes"]), + "source_meeting_clause_ids": sorted(refs & universe["source_meeting_clause_ids"]), + "downstream_owner": mapped["recommended_downstream_owner"] if mapped.get("recommended_downstream_owner") in DOWNSTREAM_OWNER_ENUM else "Stage2", + "template_note": mapped.get("reason") or "후속 단계에서 해당 review 항목의 증거와 법률상 의미를 재검토한다.", + }) + for note_item in projection_reviews: + projection_review_counter += 1 + entry = { + "review_id": "R0:projection:%03d" % projection_review_counter, + "source_domain": domain_id, + "severity": "SOFT_WARNING", + "issue_type": note_item["issue_type"], + "source_review_code": note_item.get("field"), + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Stage2", + "template_note": "투영 규칙이 채우지 못했거나 기본값을 적용한 칸이다: %s ← %s (%s)" % ( + note_item.get("field"), note_item.get("source"), note_item.get("candidate_ref")), + } + if note_item.get("field") == "Action": + entry["action_source"] = note_item.get("source") + review_handoff_items.append(entry) + + # 2) ledger 구성 — 원장은 워커 원본이 아니라 투영본을 읽는다 (R0-2 배선). + # 워커 원본에는 candidate_ref 가 없으므로(봉인 스키마) 원본을 넣으면 아래 검사에서 즉사한다. + input_candidate_total = 0 + seeds: list[dict[str, Any]] = [] + for domain_id in DOMAIN_ORDER: + projected = projected_candidates[domain_id] + input_candidate_total += len(projected) + seeds.extend(projected) + 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): + # C-2 (P0 판정 B) — 증거 단계에서 붙은 registry 구성요소는 증거 유래 근거다. + # _source_refs 가 seed 루트와 provenance 를 모두 보는 관례를 그대로 따른다. + registry_components = [ + str(value) + for value in (seed.get("registry_component_ids") + or _dict(seed.get("provenance")).get("registry_component_ids") + or []) + if isinstance(value, str) and value + ] + if not refs["source_evidence_indexes"] and not registry_components: + flags.append("meeting_only_evidence_gap") + add_policy_review(seed.get("source_domain"), "meeting_only_evidence_gap", refs) + domain_allowed = allowed_bo_types_by_domain.get(str(seed.get("source_domain"))) or set() + if (domain_allowed and seed.get("BOType") not in domain_allowed) 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 \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/ver_8_yaml_candidates/outdated/stage_1_part_2_v.8.pre_asset_triage_fix.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/ver_8_yaml_candidates/outdated/stage_1_part_2_v.8.pre_asset_triage_fix.yml new file mode 100644 index 00000000..9921c4b0 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/ver_8_yaml_candidates/outdated/stage_1_part_2_v.8.pre_asset_triage_fix.yml @@ -0,0 +1,3315 @@ +# ============================================================================= +# Liti-agent Stage 1 Part 2 v.8 — BO 컴파일 (동적 도메인 fan-out) · 통합 실행본 +# +# 정본 근거 +# 전체 DAG : stage_1_update_strategy.md §1 Part 2 블록 · §3 세부 워크플로우 +# 개정 전략 : stage_1_part_2_optimal_update_strategy_v.2.md +# 선결작업 : part_2_우선작업_report.md · part_2_우선작업_2_report.md +# 패치 : part_2_prerequisite/patches/C1_C2_registry_component_ids.patch.md +# +# 구성 — task 6종 +# [개정] Task_C_BO_A0_context_and_domain_slice_compiler 상수 3덩어리 -> 모듈 5개 호출 +# [신설] Task_C_B_domain_worker_* 정적 worker 5개 대체 템플릿 +# [개정] Task_C_BO_R0_seed_reducer_and_exception_planner fan-out 기대집합 + validator + C-2 +# [무변경] Task_C_BO_R1_exception_adjudicator v3 원문 바이트 동일 +# [개정] Task_C_BO_F0_final_bo_compiler_gate_writer BOType 어휘 registry 합집합 +# [개정] Task_C_BO_S0_signal_bundle_writer 인라인 모듈 -> 조립본 모듈 반입 +# +# 삭제 — Task_C_BO_Stage_B_B1~B5 다섯 (v3 1502~2826행, 1,325행) +# §6.7 규율대로 즉시 삭제하지 않는다. 템플릿으로 승계 5도메인을 돌려 같은 BO 가 나오는 +# 것을 확인한 뒤(Q-4) 삭제한다(Q-5). 이 파일은 그 확인이 끝난 상태를 전제한다. +# +# 확정 계약 (stage_1_update_strategy.md §0.3) +# slice runtime/domain_slices/.json task_c_bo_stage_b_domain_slice.v2 +# worker 산출 runtime/domain_seed_outputs/.json task_c_bo_stage_b_domain_bo_seed.v3 +# fan-out fanout/domain_fanout_plan.json domain_fanout_plan.v1 +# worker 이름 Task_C_B_domain_worker_* · 인스턴스 DOMAIN-<도메인ID> +# 실행 인자 --asset-root · --execution-root · --logical-root +# 구 경로 stage1_tmp/task_c_bo/** 는 쓰지 않는다(legacy_paths_forbidden). +# +# 모듈 반입 — Part 1 D0 규약 R-1~R-5 승계 +# .txt 미러를 read_raw 로 읽고 runtime_manifest.json 의 sha256 과 대조한 뒤 +# /tmp/s1/_rt 에 .py 로 기록하고 sys.path 에 넣는다. 미러는 정본 .py 옆에 있다. +# +# 이 파일은 스테이지 하나다. 스테이지 선언 1벌 · task_procedure 1벌 · tasks 1벌. +# 들여쓰기는 Part 2 v3 관례(Stages 2 · tasks 4 · task_name 4)를 유지한다. +# ============================================================================= +--- +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: 300 + code: | + #!/usr/bin/env python3 + # Task_C_BO_A0_context_and_domain_slice_compiler (v4) + # 1) 자산 반입 -> 2) 봉인 검증 -> 3) registry 로드·검증 -> + # 4) 프롬프트 조립 -> 5) slice 컴파일 -> 6) fan-out 계획 -> 7) 기록 + # 도메인 상수를 두지 않는다. 라우팅 판정은 모듈 안에서만 일어난다. + import contextlib + import datetime + import hashlib + import io + import itertools + import json + import os + import posixpath + import pathlib + import sys + import unicodedata + + import httpx + + # ------------------------------------------------------------------ + # localdocs 보일러플레이트 (SKILL.md 5장 / 5.2장) + # clientInfo 에 {{__user_hash__}} / {{__workspace_hash__}} 를 반드시 넣는다. + # 빠지면 localdocs 가 루트 경로를 보므로 사용자 파일을 찾지 못한다. + # Task_A0_domain_screener_02.yml 의 검증 완료본을 그대로 복사했다. + # ------------------------------------------------------------------ + TASK_NAME = "Task_C_BO_A0_context_and_domain_slice_compiler" + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", + "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=120) + MSG_ID_COUNTER = itertools.count(10) + + + def next_msg_id(): + return next(MSG_ID_COUNTER) + + + def _init(): + r = 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) + 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): + 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(name, args, mid): + 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_mcp(r.text) + if not p or "result" not in p: + raise RuntimeError("MCP_CALL_FAILED:%s" % name) + return p + + + def read_raw(name): + # 파일 내용을 재직렬화 없이 원문 그대로 돌려준다. + # registry_index_sha256 과 screening_sha256 은 원문 바이트의 해시여야 + # 하므로 재직렬화를 절대 허용하지 않는다. + p = _call("read_docs", {"doc_names": [name]}, next_msg_id()) + text = (p["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError("EMPTY_RESPONSE:%s" % name) + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, str): + return outer + if isinstance(outer, dict) and "results" in outer: + r0 = (outer.get("results") or [{}])[0] + inner = r0.get("content") + if inner is None: + inner = r0.get("text") + if not isinstance(inner, str) or not inner: + raise RuntimeError("EMPTY_CONTENT:%s" % name) + return inner + raise RuntimeError("UNEXPECTED_READ_DOCS_ENVELOPE:%s" % name) + + + def read_json(name): + raw = read_raw(name) + s = raw.strip() + if s.startswith("```"): + for part in s.split("```"): + part = part.strip() + if part.startswith("json"): + part = part[4:].strip() + if part.startswith("{") or part.startswith("["): + s = part + break + try: + return json.loads(s) + except json.JSONDecodeError: + obj, _ = json.JSONDecoder().raw_decode(s) + return obj + + + def write_doc(path, content): + _call("write_file", {"path": path, "content": content, "overwrite": True}, + next_msg_id()) + # ------------------------------------------------------------------ + # 실행 뿌리 세 개 — D-5 §2.4 0-c-2 확정값. 모듈에는 argv 로만 넘긴다. + # ------------------------------------------------------------------ + ASSET_ROOT = "Default_Agent/" + EXECUTION_ROOT = "/tmp/s1" + LOGICAL_ROOT = "/tmp/s1" + + WORK = pathlib.Path(EXECUTION_ROOT) + RT = WORK / "_rt" + + # 미러는 정본 .py 옆에 놓인다. 이름이 아니라 논리 경로로 지목한다. + MODULE_MIRRORS = { + "runtime_common": "Default_Agent/stage1_runtime/runtime_common.txt", + "schema_subset_validator": "Default_Agent/stage1_runtime/schema_subset_validator.txt", + "registry_loader": "Default_Agent/stage1_runtime/registry_loader.txt", + "registry_validator": "Default_Agent/stage1_runtime/registry_validator.txt", + "prompt_compiler": "Default_Agent/stage1_runtime/prompt_compiler.txt", + "domain_slice_compiler": "Default_Agent/stage1_runtime/domain_slice_compiler.txt", + "domain_fanout_planner": "Default_Agent/stage1_runtime/domain_fanout_planner.txt", + "stage_a_context_builder": "Default_Agent/stage1_runtime/stage_a_context_builder.txt", + } + MODULES = ["runtime_common", "schema_subset_validator", "registry_loader", + "registry_validator", "prompt_compiler", "domain_slice_compiler", + "domain_fanout_planner", "stage_a_context_builder"] + RUNTIME_MANIFEST = "Default_Agent/runtime_manifest.json" + + # 사건 산출물. Part 1 이 낸 것만 읽는다. + HANDOFF = "quality_gates/stage1_part1_soft_gate_handoff.json" + ACTIVATION_MANIFEST = "routing/domain_activation_manifest.json" + SCREENING = "routing/domain_screening.json" + EVIDENCE = "evidence_indexed.json" + EVENTS = "evidence_event_candidates.json" + # meeting_clause_ids 는 evidence·event 문서에 없다. 실측으로 확인했다 + # (구 매니페스트 41개 중 두 문서에서 발견되는 것 0개). 원문을 읽어야 나온다. + MEETING = "client_meeting.md" + # R-3 — Part 1 screener 03 이 낸 어휘 사전. 여덟 갈래 중 여섯을 E|O|V|D|R| 줄로 담는다. + # 네 번째 digest 생성기를 만들지 않는다 — 이미 있는 것을 프롬프트 조각으로 붙인다. + VOCABULARY = "routing/candidate_profile_vocabulary.md" + + # 정적 자산. + REGISTRY_INDEX = "Default_Agent/domains/_registry_index.json" + COMMON_CONTRACT = "Default_Agent/domains/_common/common_worker_contract.md" + POLICY = "Default_Agent/stage1_runtime/prompt_composition_policy.json" + SLICE_SCHEMA = "Default_Agent/platform/schemas/domain_slice.schema.v2.json" + FANOUT_SCHEMA = "Default_Agent/platform/schemas/domain_fanout_plan.schema.json" + + # F-2 — Part 2 가 조립본에서 읽는 정적 자산 중 경로가 고정된 것. 이 목록이 곧 배포 요구 선언이다. + # S0 의 signal 스키마 폐포 17종과 미러 24종은 런타임에 계산되거나 S0 가 이미 경성으로 대조하므로 + # 여기 넣지 않는다. 그 경계는 의도한 것이다. + PART2_REQUIRED_ASSETS = ( + SLICE_SCHEMA, + FANOUT_SCHEMA, + COMMON_CONTRACT, + POLICY, + "Default_Agent/platform/schemas/domain_seed_output.schema.v3.json", # R0 + "Default_Agent/signals/signal_registry.v2.json", # S0 + "Default_Agent/contracts/signals/s5_execution_contract.v2.json", # S0 + "Default_Agent/routing/extension_payload_key_declarations.v1.json", # F0 + "Default_Agent/stage1_runtime/worker_output_validator.txt", # R0 전용 미러 + "Default_Agent/stage1_runtime/bo_surface_projection_policy.v1.json", # R0·F0 — BO 투영 정책 + ) + RUNTIME_MANIFEST_SCHEMA = "stage1_runtime_manifest.v1" + # registry_validator 는 overlay 오류를 모으기만 한다. 네 코드는 배포 문제이므로 경성으로 올린다. + OVERLAY_ERROR_CODES = ("PROMPT_OVERLAY_HASH_MISMATCH", "PROMPT_OVERLAY_NOT_FOUND", + "PROMPT_OVERLAY_PATH_INVALID", "PROMPT_OVERLAY_REFERENCE_DIVERGENCE") + PART2_DEPLOYMENT_REMEDY = ( + "배포 원본 extension_research/Default_Agent 트리를 서버 Default_Agent/ 에 통째로 배포하고 " + "runtime_manifest.json 의 등재·sha256 과 대조해 결손 경로를 복원한다. 부분 복사는 허용되지 않는다.") + + # 산출 경로 — 새 계약만 쓴다. + SLICE_DIR = "runtime/domain_slices" + SLICE_ROOT_KEY = "stage_b_domain_slice" + PROMPT_DIR = "runtime/compiled_prompts" + SEED_DIR = "runtime/domain_seed_outputs" + FANOUT_PATH = "fanout/domain_fanout_plan.json" + # P-1 — v4 개정에서 구 slice 경로를 걷어내며 이 둘의 접두까지 벗겼던 것을 되돌린다. + # 이 둘은 slice 가 아니며 R0·F0·S0 가 여기서 읽는다(v3 1104·1105행). + STAGE_A_PATH = "stage1_tmp/task_c_bo/stage_a_context.json" + SOURCE_MANIFEST_PATH = "stage1_tmp/task_c_bo/source_universe_manifest.json" + RECEIPT_PATH = "validation_assets/routing/stage_receipt.json" + + ERRORS = [] + WARNINGS = [] + + + def warn(code, message): + WARNINGS.append({"code": code, "message": message}) + + + def sha_text(text): + return hashlib.sha256(text.encode("utf-8")).hexdigest() + + + def utc_now(): + # stage_a_context 의 created_at_utc 전용이다. + # 조립 프롬프트 해시에는 들어가지 않으므로 결정성(판정 2)에 영향이 없다. + return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + + def canonical(value): + return json.dumps(value, ensure_ascii=False, sort_keys=True, + separators=(",", ":")) + "\n" + + + def stage_text(logical_name, body): + # 논리 이름을 그대로 실행 뿌리 아래 상대경로로 쓴다. + target = WORK / logical_name + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(body.encode("utf-8")) + return len(body.encode("utf-8")) + + + # ------------------------------------------------------------------ + # 0) 배포 완전성 — F-2. 모듈 반입보다 앞이고 봉인 검증보다도 앞이다. + # 봉인은 사건 산출물의 문제이고 이것은 조립본의 문제라 원인이 다르다. + # 첫 실패에서 멈추지 않고 전부 모은다 — 배포는 한 번에 고쳐야 한다. + # ------------------------------------------------------------------ + def assert_deployment(): + """조립본이 Part 2 개정 델타를 한 벌로 받았는지 본다. 읽기만 한다.""" + manifest = json.loads(read_raw(RUNTIME_MANIFEST)) + if manifest.get("schema_version") != RUNTIME_MANIFEST_SCHEMA: + raise RuntimeError(json.dumps({ + "reason_code": "PART2_RUNTIME_MANIFEST_INVALID", "detail": "schema_version", + "expected": RUNTIME_MANIFEST_SCHEMA, "actual": manifest.get("schema_version"), + }, ensure_ascii=False)) + rows = [row for row in (manifest.get("entries") or []) if isinstance(row, dict)] + paths = [row.get("path") for row in rows] + duplicates = sorted({p for p in paths if paths.count(p) > 1}) + if manifest.get("runtime_artifact_count") != len(rows) or duplicates: + raise RuntimeError(json.dumps({ + "reason_code": "PART2_RUNTIME_MANIFEST_INVALID", "detail": "count_or_duplicate", + "declared_count": manifest.get("runtime_artifact_count"), "actual_count": len(rows), + "duplicate_paths": duplicates, + }, ensure_ascii=False)) + expected = {row["path"]: row["sha256"] for row in rows} + unregistered, mismatch, unreadable = [], [], [] + for logical in sorted(set(list(MODULE_MIRRORS.values()) + list(PART2_REQUIRED_ASSETS))): + rel = logical[len(ASSET_ROOT):] + want = expected.get(rel) + if want is None: + unregistered.append(rel) + try: + body = read_raw(logical) + except Exception: + unreadable.append(rel) + continue + if want is not None and want != sha_text(body): + mismatch.append({"path": rel, "expected": want, "actual": sha_text(body)}) + if unregistered or mismatch or unreadable: + raise RuntimeError(json.dumps({ + "reason_code": "PART2_ASSET_DEPLOYMENT_INCOMPLETE", + "message": "조립본에 Part 2 개정 자산이 한 벌로 반영되지 않았다.", + "unregistered": unregistered, "hash_mismatch": mismatch, "unreadable": unreadable, + "remedy": PART2_DEPLOYMENT_REMEDY, + }, ensure_ascii=False)) + return manifest + + + # ------------------------------------------------------------------ + # 1) 모듈 반입 — R-1~R-5. 해시가 어긋나면 실행하지 않는다. + # ------------------------------------------------------------------ + def materialize_modules(): + RT.mkdir(parents=True, exist_ok=True) + manifest_doc = json.loads(read_raw(RUNTIME_MANIFEST)) + expected = {row["path"]: row["sha256"] + for row in manifest_doc.get("entries") or []} + staged = [] + for name in MODULES: + logical = MODULE_MIRRORS[name] + raw = read_raw(logical).encode("utf-8") + want = expected.get(logical[len("Default_Agent/"):]) + if want is None: + raise RuntimeError("MODULE_MIRROR_UNREGISTERED:%s" % logical) + if want != hashlib.sha256(raw).hexdigest(): + raise RuntimeError("MODULE_MIRROR_HASH_MISMATCH:%s" % logical) + (RT / (name + ".py")).write_bytes(raw) + staged.append(name) + if str(RT) not in sys.path: + sys.path.insert(0, str(RT)) + return staged + + + # ------------------------------------------------------------------ + # 2) 봉인 검증 — 세 해시는 read_raw 원문에서 계산한다 (§6.1-3). + # ------------------------------------------------------------------ + def verify_seal(handoff, screening_raw, manifest_raw, index_raw): + root = handoff.get("stage1_part1_soft_gate_handoff", handoff) + guard = root.get("digest_guard") or {} + pairs = [("screening_sha256", sha_text(screening_raw)), + ("activation_manifest_sha256", sha_text(manifest_raw)), + ("registry_index_sha256", sha_text(index_raw))] + for key, actual in pairs: + declared = guard.get(key) + if declared is None: + raise RuntimeError("SEAL_KEY_MISSING:%s" % key) + if declared != actual: + raise RuntimeError("SEAL_FAILED:%s" % key) + return {key: value for key, value in pairs} + + + # ------------------------------------------------------------------ + # 3) C-1 — evidence authority map 에 registry_component_ids 통과 (P0 판정 B) + # component_keys(문서 추출 구조 이름)와 계층이 다르므로 섞지 않는다. + # ------------------------------------------------------------------ + def evidence_authority_map(evidence_document): + root = evidence_document.get("evidence_indexed", evidence_document) + items = root.get("items") if isinstance(root, dict) else evidence_document + out = {} + for item in items if isinstance(items, list) else []: + if not isinstance(item, dict): + continue + index = item.get("evidence_index") or item.get("evidence_index_proposed") + if not isinstance(index, str) or not index: + continue + out[index] = { + "evidence_index": index, + "doc_uid": item.get("doc_uid"), + "doc_type": item.get("doc_type"), + "source_pointer": item.get("source_pointer") or {}, + "registry_component_ids": [ + str(value) for value in (item.get("registry_component_ids") or []) + if isinstance(value, str) and value + ], + } + return out + + + # ------------------------------------------------------------------ + # 4) 본체 + # ------------------------------------------------------------------ + def main(): + # F-2 — 게이트가 먼저다. 반입도 봉인도 그 뒤다. + gate_manifest = assert_deployment() + staged_modules = materialize_modules() + import registry_loader + import registry_validator + import prompt_compiler + import domain_slice_compiler + import domain_fanout_planner + import stage_a_context_builder + + handoff = read_json(HANDOFF) + screening_raw = read_raw(SCREENING) + manifest_raw = read_raw(ACTIVATION_MANIFEST) + index_raw = read_raw(REGISTRY_INDEX) + seal = verify_seal(handoff, screening_raw, manifest_raw, index_raw) + + stage_text(REGISTRY_INDEX, index_raw) + index_doc = json.loads(index_raw) + index = index_doc.get("domain_registry_index", index_doc) + for entry in index.get("entries") or []: + config_path = entry.get("config_path") + if not isinstance(config_path, str) or not config_path: + raise RuntimeError("REGISTRY_CONFIG_PATH_MISSING:%s" % entry.get("domain_id")) + logical = unicodedata.normalize("NFC", "Default_Agent/domains/" + config_path + if not config_path.startswith("Default_Agent/") + else config_path) + config_text = read_raw(logical) + stage_text(logical, config_text) + # 프롬프트 조각도 함께 반입한다. prompt_compiler 가 도메인별 + # seed_prompt_overlay 를 읽으므로 config 만 실으면 fragment not found 로 멈춘다. + # 파일 이름을 짓지 않는다 — config 가 선언한 prompt_overlay_ref 를 따라간다. + overlay_ref = json.loads(config_text).get("prompt_overlay_ref") + if isinstance(overlay_ref, str) and overlay_ref: + overlay_logical = unicodedata.normalize( + "NFC", overlay_ref if overlay_ref.startswith("Default_Agent/") + else posixpath.join(posixpath.dirname(logical), overlay_ref)) + try: + stage_text(overlay_logical, read_raw(overlay_logical)) + except Exception as exc: + warn("PROMPT_OVERLAY_STAGE_SKIPPED", "%s: %s" % (overlay_logical, exc)) + stage_text(COMMON_CONTRACT, read_raw(COMMON_CONTRACT)) + stage_text(POLICY, read_raw(POLICY)) + + slice_schema = json.loads(read_raw(SLICE_SCHEMA)) + fanout_schema = json.loads(read_raw(FANOUT_SCHEMA)) + + # F-3 — 입력 능력 검사. 장부(F-2)가 아니라 의미를 본다. + # 매니페스트와 스키마를 함께 옛 판본으로 되돌리면 장부는 자기들끼리 맞아 통과한다. + # 그 자리에서 유일하게 남는 검사가 이것이다. + _sb = (slice_schema.get("properties") or {}).get("stage_b_domain_slice") or {} + _props = _sb.get("properties") or {} + _missing = [k for k in ("domain_declarations",) if k not in _props] + if "hash_kind" not in ((_props.get("compiled_prompt") or {}).get("properties") or {}): + _missing.append("compiled_prompt.hash_kind") + if _missing: + raise RuntimeError(json.dumps({ + "reason_code": "PART2_SLICE_SCHEMA_STALE", + "message": "슬라이스 스키마가 컴파일러가 내는 키를 선언하지 않는다. 조립본의 스키마가 개정 전 판본이다.", + "path": SLICE_SCHEMA, "missing_declarations": _missing, + "remedy": "domain_slice.schema.v2.json 을 후보 오버레이 판본으로 덮어쓴다.", + }, ensure_ascii=False)) + + os.chdir(EXECUTION_ROOT) + registry = registry_loader.load_registry(REGISTRY_INDEX) + validation = registry_validator.validate_registry(REGISTRY_INDEX) + # F-4d — overlay 계열 네 코드만 경성으로 올린다. validate_registry 전체를 올리면 + # 지금 통과 중인 다른 review 항목까지 막힌다. 부분 복사에서 흔한 것은 훼손이 아니라 + # 누락이고, 누락은 PROMPT_OVERLAY_NOT_FOUND 로 나온다. + _ovl = [e for e in (validation.get("errors") or []) + if isinstance(e, dict) and e.get("code") in OVERLAY_ERROR_CODES] + if _ovl: + raise RuntimeError(json.dumps({ + "reason_code": "PART2_ASSET_DEPLOYMENT_INCOMPLETE", "detail": "prompt_overlay", + "codes": sorted({str(e.get("code")) for e in _ovl}), + "domains": sorted({str(e.get("domain_id")) for e in _ovl}), + "remedy": PART2_DEPLOYMENT_REMEDY, + }, ensure_ascii=False)) + if validation.get("status") not in ("PASS", "READY", "OK"): + warn("REGISTRY_VALIDATION_NOT_PASS", str(validation.get("status"))) + + manifest = json.loads(manifest_raw) + manifest_root = manifest.get("domain_activation_manifest", manifest) + # D-3 넓은 정의 — execution_eligible 이 유일한 결정 필드다. + eligible = sorted({row.get("domain_id") + for row in manifest_root.get("domain_entries") or [] + if row.get("execution_eligible") is True}) + expected_runnable = sorted(set(manifest_root.get("expected_runnable_domain_ids") or [])) + if eligible != expected_runnable: + raise RuntimeError("A0_EXPECTED_RUNNABLE_SET_MISMATCH") + + evidence_document = json.loads(read_raw(EVIDENCE)) + events_document = json.loads(read_raw(EVENTS)) + authority = evidence_authority_map(evidence_document) + + # 프롬프트 조립 — rank 10 -> 20 -> 30 -> 40 -> 50. 상한 96,000 B / 24,000 자. + policy = json.loads(read_raw(POLICY)) + # R-3 — 어휘 사전을 실행 뿌리에 실어 조각으로 붙인다. 부재는 경고로 남기고 진행한다 + # (Part 1 이 아직 그 파일을 내지 않은 배포에서도 조립은 되어야 한다). + vocabulary_specs = [] + try: + vocabulary_text = read_raw(VOCABULARY) + stage_text(VOCABULARY, vocabulary_text) + vocabulary_specs = [prompt_compiler.FragmentSpec( + fragment_id="candidate_profile_vocabulary", + category="common_dependency", + path=str(pathlib.Path(EXECUTION_ROOT) / VOCABULARY))] + except Exception as exc: + warn("VOCABULARY_FRAGMENT_ABSENT", "%s: %s" % (VOCABULARY, exc)) + prompt_manifests = {} + for domain_id in expected_runnable: + specs = prompt_compiler.collect_domain_fragments( + domain_id, registry, common_contract_path=COMMON_CONTRACT, + extra_specs=vocabulary_specs) + text, manifest_row = prompt_compiler.compile_fragments(specs, policy) + rel = "%s/%s.md" % (PROMPT_DIR, domain_id) + stage_text(rel, text) + write_doc(rel, text) + row = dict(manifest_row) + row["compiled_prompt_path"] = rel + row["compiled_prompt_sha256"] = sha_text(text) + row.setdefault("composition_policy_sha256", sha_text(read_raw(POLICY))) + row["_manifest_dir"] = EXECUTION_ROOT + prompt_manifests[domain_id] = row + + # R-2 — Part 1 screener 02 가 CALC_NOT_IN_BINDINGS 로 이미 검증해 낸 + # requested_calculation_domains 를 통과시킨다. 새 registry 를 적재하지 않는다. + # 봉인용 원문 바이트(screening_raw)는 손대지 않고 파싱만 따로 한다. + # 파싱 실패와 계약 위반을 갈라 둔다. try 로 함께 감싸면 계약 위반이 경고로 + # 강등되어 조용히 통과한다 — 애초에 고치려던 것이 그 조용함이다. + screening_calc = {} + try: + screening_doc = json.loads(screening_raw) + except Exception as exc: + screening_doc = None + warn("SCREENING_CALC_PARSE_SKIPPED", str(exc)) + if screening_doc is not None: + # 루트 래핑을 벗긴다. Part 1 은 {"domain_screening": {...}} 로 쓰고 + # 스키마가 그 키를 required 로 못박는다. 벗기지 않으면 candidates 가 + # 늘 None 이 되어 예외도 없이 아무 일도 일어나지 않는다. + screening_root = screening_doc.get("domain_screening", screening_doc) \ + if isinstance(screening_doc, dict) else None + if not isinstance(screening_root, dict): + raise RuntimeError("SCREENING_ROOT_INVALID") + candidate_rows = screening_root.get("candidates") + if not isinstance(candidate_rows, list) or not candidate_rows: + raise RuntimeError("SCREENING_CANDIDATES_EMPTY") + for row in candidate_rows: + if not isinstance(row, dict): + raise RuntimeError("SCREENING_CANDIDATE_INVALID") + domain_id = row.get("domain_id") + codes = [str(v) for v in (row.get("requested_calculation_domains") or []) + if isinstance(v, str) and v] + if isinstance(domain_id, str) and domain_id and codes: + screening_calc[domain_id] = sorted(set(codes)) + + try: + result = domain_slice_compiler.compile_domain_slices( + manifest, registry, evidence_document, events_document, + manifest_sha256=seal["activation_manifest_sha256"], + evidence_sha256=sha_text(read_raw(EVIDENCE)), + events_sha256=sha_text(read_raw(EVENTS)), + slice_schema=slice_schema, + compiled_prompt_manifests=prompt_manifests, + expected_output_dir=SEED_DIR, + screening_calculation_domains=screening_calc) + except TypeError as exc: + # F-3b 앞단 — 옛 컴파일러는 screening_calculation_domains 를 받지 않는다. 그대로 두면 + # 배포 원인을 말하지 않는 TypeError 로 끝난다. 이름을 붙여 같은 코드로 내보낸다. + raise RuntimeError(json.dumps({ + "reason_code": "PART2_COMPILER_STALE", + "message": "컴파일러가 새 인자를 받지 않는다. 조립본의 domain_slice_compiler 가 개정 전 판본이다.", + "detail": "signature_mismatch", "signature_error": str(exc)[:200], + "remedy": "domain_slice_compiler.py|.txt 를 후보 오버레이 판본으로 덮어쓴다.", + }, ensure_ascii=False)) + + # F-3b — 출력 능력 검사. 기록 루프 앞이다. 여기서 멈추면 슬라이스가 한 벌도 나가지 않는다. + # 새 스키마가 domain_declarations 를 required 로 올리지 않으므로(P0 판정 C) 옛 컴파일러의 + # 산출도 스키마 검증은 26/26 통과한다. 장부가 볼 수 없는 그 자리를 이 검사가 막는다. + _bad = [] + for _did, _obj in sorted((result.get("slices") or {}).items()): + _root = (_obj or {}).get(SLICE_ROOT_KEY) or _obj or {} + if ("domain_declarations" not in _root + or "hash_kind" not in (_root.get("compiled_prompt") or {})): + _bad.append(_did) + if _bad: + raise RuntimeError(json.dumps({ + "reason_code": "PART2_COMPILER_STALE", + "message": "컴파일러가 registry 선언 블록을 싣지 않았다. 조립본의 domain_slice_compiler 가 개정 전 판본이다.", + "domains_without_declarations": _bad, + "remedy": "domain_slice_compiler.py|.txt 를 후보 오버레이 판본으로 덮어쓴다.", + }, ensure_ascii=False)) + + slice_hashes = {} + for domain_id, slice_obj in (result.get("slices") or {}).items(): + rel = "%s/%s.json" % (SLICE_DIR, domain_id) + text = canonical(slice_obj) + stage_text(rel, text) + write_doc(rel, text) + slice_hashes[domain_id] = sha_text(text) + + plan = domain_fanout_planner.build_fanout_plan( + manifest, registry, + activation_manifest_sha256=seal["activation_manifest_sha256"], + slice_dir=SLICE_DIR, seed_output_dir=SEED_DIR, + fanout_schema=fanout_schema) + plan_root = plan.get("domain_fanout_plan", plan) + + # barrier 기대 집합은 expected_runnable_domain_ids 다. active_domain_ids 가 아니다. + planned = sorted({row.get("domain_id") + for row in plan_root.get("task_instances") or []}) + if planned != expected_runnable: + raise RuntimeError("A0_FANOUT_SET_MISMATCH") + + write_doc(FANOUT_PATH, canonical(plan)) + + # P-2 — stage_a_context 와 원천 우주 매니페스트는 R0·F0·S0 의 소비 계약이다. + # v3 의 세 builder 를 그대로 이식한 모듈이 만든다. 여기서 모양을 짓지 않는다. + meeting_raw = read_raw(MEETING) + created_at_utc = utc_now() + input_digests = { + MEETING: sha_text(meeting_raw), + EVIDENCE: sha_text(read_raw(EVIDENCE)), + EVENTS: sha_text(read_raw(EVENTS)), + SCREENING: seal["screening_sha256"], + ACTIVATION_MANIFEST: seal["activation_manifest_sha256"], + REGISTRY_INDEX: seal["registry_index_sha256"], + } + stage_a = stage_a_context_builder.build_stage_a_context( + meeting_text=meeting_raw, + evidence_obj=evidence_document, + event_obj=events_document, + input_digests_sha256=input_digests, + created_at_utc=created_at_utc, + digest_guard=seal, + expected_runnable_domain_ids=expected_runnable) + source_manifest = stage_a_context_builder.build_source_universe_manifest( + stage_a, input_digests_sha256=input_digests, + registry_index_sha256=seal["registry_index_sha256"]) + write_doc(STAGE_A_PATH, canonical({"stage_a_context": stage_a})) + write_doc(SOURCE_MANIFEST_PATH, canonical(source_manifest)) + # P-13 — 판정 7. compile_domain_slices 의 반환에는 검증 수행 여부 필드가 없다. + # 스키마를 안 넘겨도 같은 모양의 성공이 돌아오므로 산출물만으로는 "통과"와 + # "안 함"을 가를 수 없다. 그래서 넘긴 사실과 대상 수를 여기에 적어 둔다. + write_doc(RECEIPT_PATH, canonical({ + "schema_version": "stage1_stage_receipt.v2", + "stage": "P2-A0", + "loader_mode": "registry_modules", + "worker_mode": "template_fanout", + "activation_source": "sg01_manifest", + "slice_sha256_by_domain": slice_hashes, + "schema_injection": { + "slice_schema_path": SLICE_SCHEMA, + "slice_schema_sha256": sha_text(read_raw(SLICE_SCHEMA)), + "slice_schema_argument": "slice_schema", + "fanout_schema_path": FANOUT_SCHEMA, + "fanout_schema_sha256": sha_text(read_raw(FANOUT_SCHEMA)), + "fanout_schema_argument": "fanout_schema", + "validated_slice_count": len(slice_hashes), + "validated_fanout_instance_count": len(plan_root.get("task_instances") or []), + "domain_declarations_projected": sorted( + (result.get("slices") or {}).keys()), + "screening_calculation_domains": screening_calc, + "vocabulary_fragment_injected": bool(vocabulary_specs), + "keyword_support_checker": "validation_assets/routing/_check_schema_keyword_support.py", + "note": "넘김이 곧 검증은 아니다. 대상 수가 0 이면 검증도 0 회다.", + }, + "deployment_gate": { + "checked_count": len(set(list(MODULE_MIRRORS.values()) + list(PART2_REQUIRED_ASSETS))), + "runtime_manifest_sha256": sha_text(read_raw(RUNTIME_MANIFEST)), + "runtime_artifact_count": gate_manifest.get("runtime_artifact_count"), + "schema_capability_checked": ["domain_declarations", "compiled_prompt.hash_kind"], + "compiler_output_checked": True, + "overlay_codes_enforced": list(OVERLAY_ERROR_CODES), + "note": "무엇을 봤는지 적는다. 상수 PASS 는 증거가 아니다.", + }, + "created_by": TASK_NAME, + })) + + return {"status": "READY", "written": True, + "modules": staged_modules, + "expected_runnable_domain_ids": expected_runnable, + "slice_count": len(slice_hashes), + "fanout_instance_count": len(plan_root.get("task_instances") or []), + "digest_guard": seal, + "errors": ERRORS, "warnings": WARNINGS} + + + _sink = io.StringIO() + with contextlib.redirect_stdout(_sink): + _init() + RESULT = main() + print(json.dumps(RESULT, ensure_ascii=False)) + + - task_name: Task_C_B_domain_worker_* + max_concurrency: 8 + preflight_files: + - "{{item.compiled_prompt_path}}" + - "{{item.slice_path}}" + llm_provider: google + llm_model: 'gemini-3.1-flash-lite' + llm_reasoning: high + llm_verbosity: medium + cache_control: + mode: auto + ttl: 15m + prompt: |- + + Task_C_B_domain_worker + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial + litigation counsel. Your role in this task is a **per-domain BO seed worker** within + the Stage 1 Part 2 dynamic fan-out. + + + 본 task 는 오케스트레이터가 runtime parameter 로 주입한 단일 도메인 + `{{item.domain_id}}` 하나만 처리한다. 다른 도메인의 사실을 자기 산출에 넣지 않는다. + 읽어야 할 것은 두 파일뿐이다 — 조립 프롬프트 `{{item.compiled_prompt_path}}` 와 + 도메인 slice `{{item.slice_path}}`. 프롬프트를 다시 조립하지 않는다. + + + + + + - `{{item.compiled_prompt_path}}` (조립 프롬프트. preflight 로 이미 실려 있다) + - `{{item.slice_path}}` (도메인 slice. 최상위 키 stage_b_domain_slice) + + + - `{{item.expected_output_path}}` (본 인스턴스의 seed 파일 1개만) + + + - 다른 도메인의 slice 나 seed 를 읽지 않는다. + - 프롬프트를 재조립하지 않는다. 조각을 다시 이어 붙이지 않는다. + - 최종 청구권을 고르지 않는다. 최종 요건충족을 판단하지 않는다. + - BO 식별자를 확정하지 않는다. BO_ID · Evidence · EvidenceTitles 키를 쓰지 않는다. + - slice 의 source_universe 밖 출처를 인용하지 않는다. + + + + + - 조립 프롬프트는 rank 10(공통 계약) → 20(의존 공통층) → 30(도메인 overlay) + → 40(특별법 overlay) → 50(실행 가드) 순으로 이미 합성되어 있다. + - 그 본문이 이 task 의 실질 지시다. 본 래퍼는 입출력 계약만 규정한다. + - 프롬프트와 slice 가 어긋나 보이면 임의로 고르지 말고 review_items 에 남긴다. + + + + - 모든 근거는 slice 의 `source_universe[*].source_id` 안에 있어야 한다. + - 증거 구성요소 이름은 `Default_Agent/routing/evidence_component_union.md` 의 + component_id 만 쓴다. 목록에 없는 이름을 만들지 않는다(P0 판정 A·B). + - 인용한 component_id 는 각 후보의 `registry_component_ids` 배열에 싣는다. + 그 배열이 비어 있지 않은 후보는 R0 에서 증거 유래로 인정된다. + - 붙일 근거가 slice 안에서 직접 읽히지 않으면 비워 두고 review 로 남긴다. + + + + 최상위는 `stage_b_domain_bo_seed_output` 한 키다. 스키마는 + `Default_Agent/platform/schemas/domain_seed_output.schema.v3.json` 이며 + `schema_version` 은 `task_c_bo_stage_b_domain_bo_seed.v3` 로 고정이다. + + { + "stage_b_domain_bo_seed_output": { + "schema_version": "task_c_bo_stage_b_domain_bo_seed.v3", + "status": "READY", + "task_instance_id": "{{item.task_instance_id}}", + "domain_id": "{{item.domain_id}}", + "registry_version": "", + "registry_index_sha256": "", + "domain_config_sha256": "", + "slice_sha256": "{{item.slice_sha256}}", + "compiled_prompt_sha256": "{{item.compiled_prompt_sha256}}", + "bo_seed_candidates": [ + { + "seed_id": "<도메인슬러그-001 꼴>", + "bo_type": "", + "source_refs": [], + "registry_component_ids": [], + "element_fact_candidates": [], + "opposing_fact_candidates": [], + "defense_candidates": [], + "evidence_slot_status": [], + "calculation_requests": [], + "dependency_refs": [], + "legal_effect_candidates": [], + "review_items": [], + "extensions": {} + } + ], + "unknown_or_unrouted_reviews": [], + "completion_receipt": {}, + "contract_guards": { + "final_conclusion_forbidden": true, + "unknown_values_require_review": true, + "source_membership_required": true, + "strict_json_output": true + } + } + } + + 추가 제약 + - 다섯 배열(element_fact_candidates · opposing_fact_candidates · defense_candidates + · calculation_requests · dependency_refs)의 이름은 스키마가 정한 것이다. 바꾸지 않는다. + - `dependency_refs` 는 연결만 남긴다. 의존 도메인의 결론을 복사하지 않는다. + - 후보를 만들 수 없으면 빈 배열로 두고 review_items 에 사유를 남긴다. + 억지로 채우는 것이 비워 두는 것보다 나쁘다. + + + + - 최상위가 `stage_b_domain_bo_seed_output` 한 키인지 점검한다. + - `domain_id` 와 `task_instance_id` 가 주입값과 정확히 같은지 점검한다. + - 모든 `source_refs` 원소가 slice 의 source_universe 안에 있는지 점검한다. + - `bo_type` 이 slice 의 allowed_legal_effect_bo_types 안에 있는지 점검한다. + - `registry_component_ids` 원소가 합집합 목록 안에 있는지 점검한다. + - 금지 키(BO_ID · Evidence · EvidenceTitles · final_*)가 없는지 점검한다. + + + + - 자기 도메인 밖으로 나가지 않는다. + - 프롬프트를 다시 만들지 않는다. + - 결론을 내리지 않는다. 후보만 남긴다. + - `write_file(overwrite=true)` 로 `{{item.expected_output_path}}` 하나만 쓴다. + + use_tools: + - localdocs + - 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 hashlib + 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() + + # v4 — {{prev.Task_C_BO_Stage_B_B*}} 다섯을 걷어냈다. + # worker 산출은 wildcard fan-out 인스턴스가 파일로 남기므로 경로로 읽는다. + # v4 — seed 목록은 상수가 아니라 A0 의 fan-out 계획이 정한다. + FANOUT_PLAN_PATH = "fanout/domain_fanout_plan.json" + SLICE_DIR = "runtime/domain_slices" + SEED_SCHEMA_PATH = "Default_Agent/platform/schemas/domain_seed_output.schema.v3.json" + SEED_SCHEMA_VERSION = "task_c_bo_stage_b_domain_bo_seed.v3" + SLICE_ROOT_KEY = "stage_b_domain_slice" + # R-4 — 머리말이 약속한 worker_output_validator 를 실제로 부른다. 반입은 D0 규약 R-1~R-5. + RUNTIME_MANIFEST = "Default_Agent/runtime_manifest.json" + EXECUTION_ROOT = "/tmp/s1_r0" + VALIDATOR_MIRRORS = { + "runtime_common": "Default_Agent/stage1_runtime/runtime_common.txt", + "schema_subset_validator": "Default_Agent/stage1_runtime/schema_subset_validator.txt", + "registry_loader": "Default_Agent/stage1_runtime/registry_loader.txt", + "worker_output_validator": "Default_Agent/stage1_runtime/worker_output_validator.txt", + } + ACTIVATION_MANIFEST_PATH = "routing/domain_activation_manifest.json" + REGISTRY_INDEX_PATH = "Default_Agent/domains/_registry_index.json" + + def _seed_docs_from_plan(plan): + # v5 — 경로만이 아니라 계획 행 전체를 보관한다. validate_seed_object 가 + # slice_sha256 · compiled_prompt_sha256 기대값을 이 행에서 대조한다(R0-6). + root = plan.get("domain_fanout_plan", plan) + out = {} + rows = {} + for row in root.get("task_instances") or []: + domain_id = row.get("domain_id") + path = row.get("expected_output_path") + if isinstance(domain_id, str) and isinstance(path, str) and domain_id and path: + out[domain_id] = path + rows[domain_id] = row + if not out: + raise RuntimeError("R0_FANOUT_PLAN_EMPTY") + return out, rows + # v4 — 계획이 정하는 두 목록. 상수가 아니므로 비워 두고 main 에서 내용만 채운다. + # 재바인딩하지 않고 갱신만 하므로 아래 도우미들이 같은 객체를 본다. + SEED_DOCS: dict[str, str] = {} + PLAN_ROWS: dict[str, dict[str, Any]] = {} + DOMAIN_ORDER: list[str] = [] + + # DOMAIN_ORDER 는 fan-out 계획의 등재 순서를 그대로 쓴다. 상수 순서를 두지 않는다. + def _domain_order(seed_docs): + return list(seed_docs.keys()) + # v5 — 구 이름 표(DOMAIN_LABELS)와 _domain_label 을 걷어냈다. 유일 소비처가 되쓰기 + # (R0-5 에서 삭제)의 transport_metadata 였다. 이로써 R0 에 구 명세서(B1~B5) 이름 의존이 없다. + 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" + # R0-1 — BO 투영 정책. 투영 규칙의 정본은 코드가 아니라 이 선언 자산이다. + BO_PROJECTION_POLICY = "Default_Agent/stage1_runtime/bo_surface_projection_policy.v1.json" + BO_PROJECTION_SCHEMA_VERSION = "stage1_bo_surface_projection_policy.v1" + # v3 계약이 정본이다. 도메인 ID 는 registry 값(E-00 · EC-00 · X1 …)이고 구 이름도 아직 들어올 수 있으므로 + # 접두사는 도메인에 묶지 않고 형식만 본다 — 도메인 일치는 validate_candidate 의 prefix 검사가 맡는다. + CANDIDATE_REF_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_.-]{0,63}:[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"} + # v5 — ALLOWED_SEED_KEYS(v2 화이트리스트)를 걷어냈다. v3 후보 18필드와의 교집합이 + # extensions 하나뿐이라 워커 산출을 통째로 버리던 자리다(C-1). 원장 payload 의 + # 키 집합은 project_to_bo_surface 의 반환문이 유일한 정의다. + + 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_raw(name: str) -> str: + # 재직렬화 없이 원문 그대로 돌려준다. 미러 해시 대조의 전제다. + p = _call("read_docs", {"doc_names": [name]}) + text = (p["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError("EMPTY_RESPONSE:%s" % name) + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, str): + return outer + if isinstance(outer, dict) and "results" in outer: + r0 = (outer.get("results") or [{}])[0] + inner = r0.get("content") + if inner is None: + inner = r0.get("text") + if not isinstance(inner, str) or not inner: + raise RuntimeError("EMPTY_CONTENT:%s" % name) + return inner + raise RuntimeError("UNEXPECTED_READ_DOCS_ENVELOPE:%s" % name) + + + # ------------------------------------------------------------------ + # F-4 — 자기 입력의 배포 무결성. A0 게이트가 앞에서 걸러 주지만 이 task 는 + # 개별 재실행이 가능하다(예행 하네스가 실제로 task 단위로 돌린다). + # 매니페스트는 task 당 한 번만 읽는다. 호출마다 읽으면 S0 의 $ref 폐포 walk 에서만 19 회다. + # ------------------------------------------------------------------ + PART2_DEPLOYMENT_REMEDY = ( + "배포 원본 extension_research/Default_Agent 트리를 서버 Default_Agent/ 에 통째로 배포하고 " + "runtime_manifest.json 의 등재·sha256 과 대조해 결손 경로를 복원한다. 부분 복사는 허용되지 않는다.") + _MANIFEST_MAP = None + + + def _manifest_map(): + global _MANIFEST_MAP + if _MANIFEST_MAP is None: + _MANIFEST_MAP = {row["path"]: row["sha256"] + for row in (json.loads(read_raw(RUNTIME_MANIFEST)).get("entries") or []) + if isinstance(row, dict)} + return _MANIFEST_MAP + + + def _deployment_error(logical, detail, extra=None): + payload = {"reason_code": "PART2_ASSET_DEPLOYMENT_INCOMPLETE", "path": logical, + "detail": detail, "remedy": PART2_DEPLOYMENT_REMEDY} + if extra: + payload.update(extra) + return RuntimeError(json.dumps(payload, ensure_ascii=False)) + + + def _verify_asset(logical: str) -> str: + """원문을 돌려준다. 반환값을 소비해야 읽기 횟수가 늘지 않는다.""" + body = read_raw(logical) + want = _manifest_map().get(logical[len("Default_Agent/"):]) + if want is None: + raise _deployment_error(logical, "unregistered") + if want != hashlib.sha256(body.encode("utf-8")).hexdigest(): + raise _deployment_error(logical, "hash_mismatch", {"expected": want}) + return body + + + def _assert_mirror_consistent(logical: str) -> None: + """F-4b — 부재는 통과(materialize_validator 의 기존 관용 유지). 미등재·불일치만 막는다. + + 예외 종류를 바꿔 try 를 뚫는 우회(SystemExit 등)는 쓰지 않는다. 그것은 __main__ 가드의 + stdout 출력과 예행 하네스의 단계 기록까지 건너뛴다. 판정을 try 밖으로 옮기는 것이 답이다. + """ + try: + body = read_raw(logical) + except Exception: + return + want = _manifest_map().get(logical[len("Default_Agent/"):]) + if want is None: + raise _deployment_error(logical, "unregistered") + if want != hashlib.sha256(body.encode("utf-8")).hexdigest(): + raise _deployment_error(logical, "hash_mismatch", {"expected": want}) + + + def materialize_validator() -> list[str]: + """worker_output_validator 와 그 의존 셋을 반입한다. 실패는 경고로 남기고 진행한다. + + 이 검증은 덧붙이는 층이다 — 반입이 안 되는 배포에서도 R0 본체는 돌아야 한다. + """ + import hashlib + import os + import pathlib + rt = pathlib.Path(EXECUTION_ROOT) / "_rt" + rt.mkdir(parents=True, exist_ok=True) + manifest_doc = json.loads(read_raw(RUNTIME_MANIFEST)) + expected = {row["path"]: row["sha256"] for row in manifest_doc.get("entries") or []} + staged: list[str] = [] + for name, logical in VALIDATOR_MIRRORS.items(): + body = read_raw(logical) + raw = body.encode("utf-8") + want = expected.get(logical[len("Default_Agent/"):]) + if want is None: + raise RuntimeError("MODULE_MIRROR_UNREGISTERED:%s" % logical) + if want != hashlib.sha256(raw).hexdigest(): + raise RuntimeError("MODULE_MIRROR_HASH_MISMATCH:%s" % logical) + (rt / (name + ".py")).write_bytes(raw) + staged.append(name) + if str(rt) not in sys.path: + sys.path.insert(0, str(rt)) + return staged + + + 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() + "..." + + # v4 — parse_llm_json 을 걷어냈다. worker 가 {{item.expected_output_path}} 에 + # strict JSON 파일을 직접 쓰므로 LLM 원문 관용 파싱 경로가 없어졌다. + # salvage_notes 는 산출 스키마에 남지만 v4 에서는 항상 빈 목록이다 — 구제할 원문이 없다. + # v5 — DOMAIN_PAYLOAD_CANON · _canon_payload 를 걷어냈다. canon 키가 구 이름(B1~B4)뿐이라 + # registry ID 26종 전부에서 no-op 였다(사문 코드). 확장 payload 는 워커 발행 형태 그대로 둔다. + def ensure_candidate_ref(cand: dict[str, Any], domain_id: str, idx: int) -> dict[str, Any]: + """v3 워커 출력에는 candidate_ref 가 없다 — seed 스키마가 additionalProperties: false 로 봉인돼 + 워커가 실을 수 없는 필드다. v3 이 주는 순번에서 R0 내부 식별자를 결정적으로 만든다. + 이미 실려 있으면(구 판본 산출) 그대로 둔다.""" + ref = cand.get("candidate_ref") + if isinstance(ref, str) and ref: + return cand + out = dict(cand) + out["candidate_ref"] = "%s:%03d" % (domain_id, idx + 1) + return out + + def project_to_bo_surface(cand: dict[str, Any], domain_id: str, universe: dict[str, set[str]], + policy: dict[str, Any], allowed_bo_types: set[str], + reviews: list[dict[str, Any]]) -> dict[str, Any]: + """v3 후보를 BO 호환면으로 투영한다. 값의 정본은 registry 이고 규칙은 정책 파일이 선언한다. + + 전임자 둘(expand_candidate + _seed_payload)은 v2 키를 기본값으로 깔고 v2 화이트리스트로 + 걸렀다. v3 후보를 넣으면 워커가 실은 값이 extensions 하나만 남았고, 그 결과 중복 판정 키 + 여덟 성분이 전부 비어 사건 전체가 한 버킷으로 접혔다(C-1·C-2). 여기서는 v3 필드에서 + 끌어오고, registry 가 말해 주지 않는 칸은 채우지 않고 reviews 에 올린다. + 반환 키 집합은 입력과 무관하게 고정이다 — 이 반환문이 원장 payload 키 집합의 유일한 정의다. + """ + ref = str(cand.get("candidate_ref")) + + def note(issue_type: str, field: str, source: str) -> None: + reviews.append({"issue_type": issue_type, "candidate_ref": ref, + "field": field, "source": source}) + + refs = _strings(cand.get("source_refs")) + evidence = sorted(set(refs) & universe["source_evidence_indexes"]) + events = sorted(set(refs) & universe["source_event_candidate_ids"]) + clauses = sorted(set(refs) & universe["source_meeting_clause_ids"]) + + norm = _dict(policy.get("f0_normalization")) + bo_type = cand.get("bo_type") + if allowed_bo_types and bo_type not in allowed_bo_types: + note("legal_effect_uncertain", "BOType", "bo_type") + + ext = dict(_dict(cand.get("extensions"))) + if not isinstance(ext.get("domain_payload"), dict): + ext["domain_payload"] = {} + domain_payload = _dict(ext.get("domain_payload")) + + action_type = domain_payload.get("action_type") + if not (isinstance(action_type, str) and action_type in set(_strings(norm.get("action_type_enum")))): + # registry 근거가 없는 칸이다. 기본값은 선언이며 추정이 아니다 — 반드시 검토로 올린다. + action_type = norm.get("action_type_default") + note("schema_field_fallback", "ActionType", "policy_default") + + effect_type_ids = sorted({str(e.get("type_id")).strip() + for e in _list(cand.get("legal_effect_candidates")) + if isinstance(e, dict) and str(e.get("type_id") or "").strip()}) + action_summary = domain_payload.get("action_summary") + if isinstance(action_summary, str) and action_summary.strip(): + action = action_summary.strip() + elif effect_type_ids: + # 값은 registry token 이지 서술문이 아니다. Stage 2 는 review_handoff 의 action_source 를 함께 읽는다. + action = "%s:%s" % (bo_type, effect_type_ids[0]) + note("schema_field_fallback", "Action", "legal_effect_type_id") + else: + action = str(bo_type) + note("schema_field_fallback", "Action", "bo_type") + + time_facts = [t for t in _list(cand.get("time_facts")) if isinstance(t, dict)] + behavior_time = None + time_text = None + if time_facts: + pick = sorted(time_facts, key=lambda t: (str(t.get("fact_type") or ""), str(t.get("value") or "")))[0] + behavior_time = pick.get("value") + time_text = pick.get("value") + distinct_times = {str(t.get("value") or "").strip() for t in time_facts if str(t.get("value") or "").strip()} + if len(distinct_times) > 1: + note("amount_or_date_uncertain", "core_field_base.BehaviorTime", "time_facts") + + object_refs = sorted(_strings(cand.get("object_refs"))) + + amount_facts = [a for a in _list(cand.get("amount_facts")) if isinstance(a, dict)] + amount = None + if amount_facts: + pick = sorted(amount_facts, key=lambda a: (str(a.get("amount_type") or ""), str(a.get("decimal_value") or "")))[0] + # v3 amount_facts 는 {amount_type, decimal_value, currency, source_refs} 닫힌 스키마다 — + # value_text 필드가 없으므로 정책 규칙대로 decimal_value 원문을 그대로 쓴다. + amount = {"value_text": pick.get("decimal_value"), + "numeric_value": pick.get("decimal_value"), + "currency": pick.get("currency")} + distinct_amounts = {str(a.get("decimal_value") or "").strip() for a in amount_facts if str(a.get("decimal_value") or "").strip()} + if len(distinct_amounts) > 1: + note("amount_or_date_uncertain", "amount", "amount_facts") + + return { + "candidate_ref": ref, + "source_domain": domain_id, + "BOType": bo_type, + "ActionType": action_type, + "JuristicAct": _normalize_juristic(cand.get("juristic_act_type")), + "Action": action, + "Reason": None, + "PriorAct": None, + "ReasonRefs": [], + "Legal_Keywords": effect_type_ids, + "core_field_base": {"BehaviorTime": behavior_time, "TimeText": time_text, + "Object": object_refs[0] if object_refs else None, + "StatementType": bo_type}, + "amount": amount, + "source_evidence_indexes": evidence, + "provenance": {"source_event_candidate_ids": events, + "source_meeting_clause_ids": clauses, + "source_evidence_indexes": evidence, + "source_domain": domain_id}, + "downstream_seed_refs": {}, + "extensions": ext, + "registry_component_ids": _strings(cand.get("registry_component_ids")), + } + + def expand_review_item(item: Any, domain_id: str, idx: int) -> dict[str, Any]: + """v3 검토 항목(후보별 review_items · 루트 unknown_or_unrouted_reviews)을 handoff 항목형으로 사상한다. + + 구판은 v3 루트 13키에 없는 domain_review_queue 를 읽었다 — 봉인(additionalProperties: false)이 + 워커에게 발행을 금지한 키라 워커 검토가 한 건도 도달하지 못했다(C-6). 사상 규칙은 정책 + review_item_projection 이 선언한다. 원본 코드는 덧붙이기 키 source_review_code 로 보존한다. + """ + src = _dict(item) + raw_type = str(src.get("unresolved_type") or "").strip() + raw_code = str(src.get("review_code") or "").strip() + severity = src.get("severity") if src.get("severity") in ("SOFT_WARNING", "HARD_WARNING") else "SOFT_WARNING" + return { + "review_id": str(src.get("review_id") or f"{domain_id}:review:{idx:03d}"), + "issue_type": raw_type if raw_type in REVIEW_ISSUE_ENUM else "review_required", + "severity": severity, + # 원본 review_code(v3 필수 키)를 잃지 않는다 — 정책 additive_keys 의 목적이 그것이다. + "source_review_code": raw_code or raw_type or None, + "reason": str(src.get("reason") or "").strip(), + "source_refs": _strings(src.get("source_refs")), + "recommended_downstream_owner": src.get("recommended_downstream_owner") or "Stage2", + } + + # ---------- 워커 출력 수용 검증 (v3 계약 정본 · 정책 status_policy · 계획 해시 대조) ---------- + def validate_seed_object(seed_obj: dict[str, Any], domain_id: str, plan_row: dict[str, Any], warnings: list[dict[str, Any]]) -> None: + """v3 seed 루트를 검증한다. status 는 v3 enum 5종을 정책 status_policy 로 가른다. + + 구판은 READY 계열 2종만 허용해 계약상 적법한 NO_SUPPORT 가 R0 전체를 중단시켰고(C-11), + 신선도는 워커가 실을 수 없는 transport_metadata.slice_guard 를 읽는 죽은 검사였다. + 신선도의 제 필드는 v3 루트의 slice_sha256 · compiled_prompt_sha256 이고(둘 다 required + — 워커가 반드시 echo 한다), 기대값은 fan-out 계획 행이 든다. + """ + if seed_obj.get("schema_version") != SEED_SCHEMA_VERSION: + 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") + status = seed_obj.get("status") + if status in ("BLOCKED", "FAILED"): + # 워커 실패 신호다. fail-open 은 활성화 판정의 원칙이고, 실패의 침묵 흡수는 금지 원칙이 막는다. + raise ValueError(f"{domain_id}: worker reported {status}") + if status == "NO_SUPPORT": + # 적법한 "실을 것 없음". 후보가 있으면 상태·내용 모순이다. + if _list(seed_obj.get("bo_seed_candidates")): + raise ValueError(f"{domain_id}: NO_SUPPORT with non-empty bo_seed_candidates") + elif status not in ("READY", "READY_WITH_REVIEW"): + raise ValueError(f"{domain_id}: seed status outside v3 enum: {status!r}") + for key in ("slice_sha256", "compiled_prompt_sha256"): + want = plan_row.get(key) + if isinstance(want, str) and want: + if seed_obj.get(key) != want: + raise ValueError(f"{domain_id}: stale seed output: {key} mismatch") + else: + warnings.append({"domain_id": domain_id, "warning": f"fanout plan carries no {key} expectation"}) + + def validate_candidate(cand: dict[str, Any], domain_id: str, idx: int) -> None: + 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 len(DOMAIN_ORDER), + "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 _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() + # F-4a — 자기 정적 입력. try 밖이어야 한다. 안에 넣으면 아래 except Exception 이 + # 삼켜 WORKER_VALIDATOR_UNAVAILABLE 경고로 강등되고 R0 이 계속 돈다. + _seed_schema_body = _verify_asset(SEED_SCHEMA_PATH) + # R0-1 — 투영 정책 반입 (F-4a 와 같은 규율: try 밖 경성). 정책이 없거나 낡았는데 + # 조용히 옛 규칙으로 도는 것이 이번 결손(v2 잔재)의 재발 경로다. + projection_policy = _dict(json.loads(_verify_asset(BO_PROJECTION_POLICY))) + if projection_policy.get("schema_version") != BO_PROJECTION_SCHEMA_VERSION: + raise RuntimeError("PART2_PROJECTION_POLICY_INVALID") + # F-4b — 미러 넷의 무결성. 부재는 통과시키고 미등재·불일치만 막는다. + for _mirror in VALIDATOR_MIRRORS.values(): + _assert_mirror_consistent(_mirror) + salvage_notes: list[dict[str, Any]] = [] + guard_warnings: list[dict[str, Any]] = [] + # v4 — seed 목록과 그 순서는 A0 의 fan-out 계획이 정한다. 이 파일은 목록을 만들지 않는다. + worker_validator = None + seed_schema = None + try: + materialize_validator() + import worker_output_validator as worker_validator + seed_schema = json.loads(_seed_schema_body) + except Exception as exc: + guard_warnings.append({"code": "WORKER_VALIDATOR_UNAVAILABLE", "message": str(exc)[:200]}) + worker_validator = None + _docs, _rows = _seed_docs_from_plan(_dict(read_json_doc(FANOUT_PLAN_PATH))) + SEED_DOCS.update(_docs) + PLAN_ROWS.update(_rows) + DOMAIN_ORDER.extend(_domain_order(SEED_DOCS)) + 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) 워커 출력 수용: 검증 -> 투영. 워커 seed 파일은 손대지 않는다 — + # 선언표(stage1_part_interface.v1)가 기록자를 워커 하나로 정했다(R0-5). + seed_objects: dict[str, dict[str, Any]] = {} + projected_candidates: dict[str, list[dict[str, Any]]] = {} + review_handoff_items: list[dict[str, Any]] = [] + allowed_bo_types_by_domain: dict[str, set[str]] = {} + projection_review_counter = 0 + for domain_id in DOMAIN_ORDER: + # v4 — worker 가 {{item.expected_output_path}} 에 자기 seed 를 직접 쓴다. + # {{prev}} 원문 관용 파싱이 아니라 계획이 정한 경로에서 읽는다. + outer = _dict(read_json_doc(SEED_DOCS[domain_id])) + 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, PLAN_ROWS.get(domain_id) or {}, guard_warnings) + # 슬라이스는 검증기 유무와 무관하게 읽는다 — worker_output_validator 와 + # BOType 허용 어휘(allowed_legal_effect_bo_types, registry 유래)가 이 값을 쓴다. + try: + slice_doc = read_json_doc("%s/%s.json" % (SLICE_DIR, domain_id)) + except Exception: + slice_doc = None + slice_root = _dict(_dict(slice_doc).get(SLICE_ROOT_KEY)) if isinstance(slice_doc, dict) else {} + allowed_bo_types = set(_strings(slice_root.get("allowed_legal_effect_bo_types"))) + allowed_bo_types_by_domain[domain_id] = allowed_bo_types + # R-4 — 스키마와 슬라이스를 실제로 넘긴다. 넘기지 않으면 검증이 조용히 건너뛰어진다. + if worker_validator is not None: + report = worker_validator.validate_worker_output( + {"stage_b_domain_bo_seed_output": seed_obj}, + schema=seed_schema, + expected_domain_id=domain_id, + slice_document=slice_doc) + for item in report.get("errors") or []: + guard_warnings.append({"code": "WORKER_OUTPUT_ERROR", "domain_id": domain_id, + "detail": item}) + for item in report.get("warnings") or []: + guard_warnings.append({"code": "WORKER_OUTPUT_REVIEW", "domain_id": domain_id, + "detail": item}) + cands = _list(seed_obj.get("bo_seed_candidates")) + projected: list[dict[str, Any]] = [] + projection_reviews: 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") + cand = ensure_candidate_ref(cand, domain_id, idx) + validate_candidate(cand, domain_id, idx) + # membership 검사 — v3 의 평평한 source_refs 를 universe 와 대조 (hard BLOCK). + # 이쪽을 보지 않으면 membership 게이트가 v3 산출에서는 통과만 하는 빈 검사가 된다. + known_sources = (universe["source_event_candidate_ids"] | universe["source_meeting_clause_ids"] + | universe["source_evidence_indexes"]) + ref_bad = [v for v in _strings(cand.get("source_refs")) if v not in known_sources] + if ref_bad: + raise RuntimeError(f"BLOCK: {domain_id}.{cand.get('candidate_ref')}: source_refs outside Stage A universe: {ref_bad}") + projected.append(project_to_bo_surface(cand, domain_id, universe, projection_policy, + allowed_bo_types, projection_reviews)) + # R0-5 — 되쓰기 없음. seed_objects 는 워커 원본 그대로다(S0 와 signal adapter 가 + # v3 적합 원본을 읽는다). 투영본은 projected_candidates 가 따로 든다(R0-2 배선). + seed_objects[domain_id] = seed_obj + projected_candidates[domain_id] = projected + # R0-4 — v3 검토 채널: 후보별 review_items + 루트 unknown_or_unrouted_reviews. + # list(...) 복사는 워커 원본 목록을 제자리 변형하지 않기 위한 것이다. + worker_reviews = list(_list(seed_obj.get("unknown_or_unrouted_reviews"))) + for cand in _list(seed_obj.get("bo_seed_candidates")): + worker_reviews.extend(_list(_dict(cand).get("review_items"))) + if seed_obj.get("status") == "NO_SUPPORT": + worker_reviews.append({"review_id": f"{domain_id}:status:NO_SUPPORT", + "review_code": "NO_SUPPORT", + "unresolved_type": "review_required", + "severity": "SOFT_WARNING", + "reason": "worker reported NO_SUPPORT (nothing to carry for this domain)"}) + for idx, item in enumerate(worker_reviews, start=1): + mapped = expand_review_item(item, domain_id, idx) + refs = set(mapped.get("source_refs") or []) + review_handoff_items.append({ + "review_id": mapped["review_id"], + "source_domain": domain_id, + "severity": mapped["severity"], + "issue_type": mapped["issue_type"], + "source_review_code": mapped.get("source_review_code"), + "source_event_candidate_ids": sorted(refs & universe["source_event_candidate_ids"]), + "source_evidence_indexes": sorted(refs & universe["source_evidence_indexes"]), + "source_meeting_clause_ids": sorted(refs & universe["source_meeting_clause_ids"]), + "downstream_owner": mapped["recommended_downstream_owner"] if mapped.get("recommended_downstream_owner") in DOWNSTREAM_OWNER_ENUM else "Stage2", + "template_note": mapped.get("reason") or "후속 단계에서 해당 review 항목의 증거와 법률상 의미를 재검토한다.", + }) + for note_item in projection_reviews: + projection_review_counter += 1 + entry = { + "review_id": "R0:projection:%03d" % projection_review_counter, + "source_domain": domain_id, + "severity": "SOFT_WARNING", + "issue_type": note_item["issue_type"], + "source_review_code": note_item.get("field"), + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Stage2", + "template_note": "투영 규칙이 채우지 못했거나 기본값을 적용한 칸이다: %s ← %s (%s)" % ( + note_item.get("field"), note_item.get("source"), note_item.get("candidate_ref")), + } + if note_item.get("field") == "Action": + entry["action_source"] = note_item.get("source") + review_handoff_items.append(entry) + + # 2) ledger 구성 — 원장은 워커 원본이 아니라 투영본을 읽는다 (R0-2 배선). + # 워커 원본에는 candidate_ref 가 없으므로(봉인 스키마) 원본을 넣으면 아래 검사에서 즉사한다. + input_candidate_total = 0 + seeds: list[dict[str, Any]] = [] + for domain_id in DOMAIN_ORDER: + projected = projected_candidates[domain_id] + input_candidate_total += len(projected) + seeds.extend(projected) + 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): + # C-2 (P0 판정 B) — 증거 단계에서 붙은 registry 구성요소는 증거 유래 근거다. + # _source_refs 가 seed 루트와 provenance 를 모두 보는 관례를 그대로 따른다. + registry_components = [ + str(value) + for value in (seed.get("registry_component_ids") + or _dict(seed.get("provenance")).get("registry_component_ids") + or []) + if isinstance(value, str) and value + ] + if not refs["source_evidence_indexes"] and not registry_components: + flags.append("meeting_only_evidence_gap") + add_policy_review(seed.get("source_domain"), "meeting_only_evidence_gap", refs) + domain_allowed = allowed_bo_types_by_domain.get(str(seed.get("source_domain"))) or set() + if (domain_allowed and seed.get("BOType") not in domain_allowed) 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 hashlib + 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" + EXCEPTION_PACK_PATH = "quality_gates/stage1_part2_exception_pack.json" + BUNDLE_COMPACT_PATH = "stage1_tmp/task_c_bo/postb_compiled_bundle_compact.json" + TARGET_NAME = "BO.json" + + # v4 신설 — BOType 어휘와 확장 payload 선언의 정본은 registry 다. 코드에 어휘를 두지 않는다. + # registry 를 런타임에 적재하지 않는다. 그러려면 index 1 + domain_config 26 + extension schema 26 + # 을 읽어야 하고 그것은 읽기 53회다. 값이 사건마다 달라지지 않으므로 배포 시점에 한 번 + # 접어 둔 자산 하나만 읽는다. 생성기는 routing/_build_extension_payload_declarations.py 다. + EXTENSION_DECLARATIONS_PATH = "Default_Agent/routing/extension_payload_key_declarations.v1.json" + RUNTIME_MANIFEST_PATH = "Default_Agent/runtime_manifest.json" + DECLARATIONS_SCHEMA_VERSION = "stage1_extension_payload_key_declarations.v1" + BO_TYPE_SOURCE = "registry_union" + UNDECLARED_KEY_REVIEW_CODE = "EXTENSION_PAYLOAD_KEY_UNDECLARED" + # F0-2 — BO 투영 정책 (정규화 기본값의 정본). R0 와 같은 자산을 읽는다. + BO_PROJECTION_POLICY_PATH = "Default_Agent/stage1_runtime/bo_surface_projection_policy.v1.json" + BO_PROJECTION_SCHEMA_VERSION = "stage1_bo_surface_projection_policy.v1" + + 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 read_raw(name: str) -> str: + # 재직렬화 없이 원문 그대로 돌려준다. 해시 대조의 전제다. + # Task_D0_domain_activation_gate.yml 의 검증 완료본을 _call 서명만 맞춰 옮겼다. + p = _call("read_docs", {"doc_names": [name]}) + text = (p["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError("EMPTY_RESPONSE:%s" % name) + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, str): + return outer + if isinstance(outer, dict) and "results" in outer: + r0 = (outer.get("results") or [{}])[0] + inner = r0.get("content") + if inner is None: + inner = r0.get("text") + if not isinstance(inner, str) or not inner: + raise RuntimeError("EMPTY_CONTENT:%s" % name) + return inner + raise RuntimeError("UNEXPECTED_READ_DOCS_ENVELOPE:%s" % name) + + # ---------- v4 신설: registry 선언 조회 ---------- + def _load_declarations() -> dict[str, Any]: + # 어휘의 정본이므로 훼손되면 BOType 검증이 조용히 넓어진다. + # 원문 바이트의 sha256 을 runtime_manifest 와 대조한 뒤에만 쓴다(D0 반입 규약과 같은 규율). + body = read_raw(EXTENSION_DECLARATIONS_PATH) + manifest = json.loads(read_raw(RUNTIME_MANIFEST_PATH)) + want = {row["path"]: row["sha256"] for row in manifest.get("entries") or []}.get( + EXTENSION_DECLARATIONS_PATH[len("Default_Agent/"):]) + if want is None: + raise RuntimeError("F0_EXTENSION_DECLARATIONS_UNREGISTERED") + if want != hashlib.sha256(body.encode("utf-8")).hexdigest(): + raise RuntimeError("F0_EXTENSION_DECLARATIONS_HASH_MISMATCH") + doc = json.loads(body) + if not isinstance(doc, dict) or doc.get("schema_version") != DECLARATIONS_SCHEMA_VERSION: + raise RuntimeError("F0_EXTENSION_DECLARATIONS_SCHEMA_MISMATCH") + if not doc.get("bo_types_union"): + raise RuntimeError("F0_REGISTRY_BO_TYPES_EMPTY") + if not doc.get("declared_key_union"): + raise RuntimeError("F0_EXTENSION_DECLARED_KEYS_EMPTY") + return doc + + def _load_projection_policy() -> dict[str, Any]: + # F0-2 — 정규화 기본값·어휘의 정본. _load_declarations 와 같은 규율로 sha256 대조 후에만 쓴다. + body = read_raw(BO_PROJECTION_POLICY_PATH) + manifest = json.loads(read_raw(RUNTIME_MANIFEST_PATH)) + want = {row["path"]: row["sha256"] for row in manifest.get("entries") or []}.get( + BO_PROJECTION_POLICY_PATH[len("Default_Agent/"):]) + if want is None: + raise RuntimeError("F0_PROJECTION_POLICY_UNREGISTERED") + if want != hashlib.sha256(body.encode("utf-8")).hexdigest(): + raise RuntimeError("F0_PROJECTION_POLICY_HASH_MISMATCH") + doc = json.loads(body) + if not isinstance(doc, dict) or doc.get("schema_version") != BO_PROJECTION_SCHEMA_VERSION: + raise RuntimeError("F0_PROJECTION_POLICY_SCHEMA_MISMATCH") + if not isinstance(doc.get("f0_normalization"), dict): + raise RuntimeError("F0_PROJECTION_POLICY_NORMALIZATION_MISSING") + return doc + + def _declared_bo_types(declarations: dict[str, Any]) -> set[str]: + return {str(v) for v in declarations.get("bo_types_union") or [] if isinstance(v, str) and v} + + def _resolve_domain_ids(declarations: dict[str, Any], source_domain: Any) -> list[str]: + """source_domain 은 도메인 ID 이거나 구 이름(B1~B5)이다. 구 이름은 별칭표 primary 로 옮긴다.""" + name = str(source_domain or "").strip() + if not name: + return [] + known = {str(row.get("domain_id")) for row in declarations.get("domains") or []} + if name in known: + return [name] + targets = _dict(declarations.get("legacy_alias_targets")).get(name) + return [str(v) for v in targets or [] if str(v) in known] + + def _declared_keys_for(declarations: dict[str, Any], domain_ids: list[str]) -> set[str]: + """도메인을 특정하지 못하면 전체 합집합을 상대로 한다. 좁히지 못한 것을 위반으로 세지 않는다.""" + if not domain_ids: + return {str(v) for v in declarations.get("declared_key_union") or []} + wanted = set(domain_ids) + out: set[str] = set() + for row in declarations.get("domains") or []: + if str(row.get("domain_id")) in wanted: + out.update(str(v) for v in row.get("declared_keys") or []) + return out + + def _extension_key_reviews(bo_items: list[dict[str, Any]], declarations: dict[str, Any]) -> list[dict[str, Any]]: + """확장 payload 키를 registry 선언과 대조한다. 선언 밖 키는 review 로 남기고 값은 지우지 않는다.""" + reviews: list[dict[str, Any]] = [] + for item in bo_items: + payload = _dict(_dict(item.get("extensions")).get("domain_payload")) + if not payload: + continue + source_domain = _dict(item.get("provenance")).get("source_domain") + domain_ids = _resolve_domain_ids(declarations, source_domain) + undeclared = sorted(set(payload) - _declared_keys_for(declarations, domain_ids)) + if undeclared: + reviews.append({ + "bo_id": item.get("BO_ID"), + "source_domain": source_domain, + "resolved_domain_ids": domain_ids, + "resolution": "registry_domain_ids" if domain_ids else "declared_key_union_fallback", + "undeclared_keys": undeclared, + "review_code": UNDECLARED_KEY_REVIEW_CODE, + }) + return reviews + + 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]], + bo_types: 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}") + # v4 — 어휘의 정본은 registry 합집합이다. 코드에 {"event","state"} 를 두지 않는다. + if item.get("BOType") not in bo_types: + 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]] = [] + # v4 — registry 선언을 한 번 읽는다. BOType 어휘와 확장 payload 선언이 여기서 나온다. + declarations = _load_declarations() + f0_norm = _dict(_load_projection_policy().get("f0_normalization")) + bo_types = _declared_bo_types(declarations) + 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"))]) + # P-11 — R1 산출은 조건부다. R1 은 예외가 없어도 no-exception 객체를 반드시 쓰므로 + # 파일 부재는 "예외 없음"이 아니라 "R1 이 돌지 않았거나 실패했다"를 뜻한다. + # 종전의 무조건 fallback 은 그 둘을 가르지 못하고 판정을 조용히 삼켰다. + # 예외 팩의 exception_count 가 필수 여부를 정한다. + try: + pack = _dict(read_json_doc(EXCEPTION_PACK_PATH)) + except Exception: + pack = {} + pack_root = _dict(pack.get("postb_exception_pack") or pack) + declared_exceptions = pack_root.get("exception_count") + if not isinstance(declared_exceptions, int): + declared_exceptions = len(_list(pack_root.get("exceptions"))) + r1_state = "READ" + try: + adj_doc = read_json_doc(DECISIONS_PATH) + except Exception as exc: + if declared_exceptions > 0: + _fail("R1 adjudication decisions required but unreadable", gates, + ["exception_count=%d" % declared_exceptions, str(exc)]) + r1_state = "R1_SKIPPED" + 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": []}} + _add_gate(gates, "r1_decision_presence", True, + "exception_count=%d state=%s" % (declared_exceptions, r1_state)) + 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")) + # F0-1 — 어휘의 정본은 registry 합집합(bo_types)이다. {"event","state"} 하드코딩은 + # claim 등 여덟 도메인의 선언값을 침묵 덮어쓰던 자리다(C-5). 기본값은 정책 f0_normalization 이 선언한다. + if bo_type not in bo_types: + normalization_notes.append({"candidate_ref": candidate_ref, "field": "BOType", "received": bo_type, "fallback": f0_norm.get("bo_type_default")}) + bo_type = f0_norm.get("bo_type_default") + if action_type not in ACTION_TYPE_ENUM: + normalization_notes.append({"candidate_ref": candidate_ref, "field": "ActionType", "received": action_type, "fallback": f0_norm.get("action_type_default")}) + action_type = f0_norm.get("action_type_default") + if not isinstance(action, str) or not action.strip(): + normalization_notes.append({"candidate_ref": candidate_ref, "field": "Action", "fallback": "source-backed BO"}) + action = str(f0_norm.get("action_default_template") or " source-backed BO").replace("", candidate_ref) + + 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, bo_types)) + _add_gate(gates, "bo_schema_and_reference_validation", not errors, "BO_JSON_Schema target validation") + # v4 신설 — 확장 payload 키를 registry 선언과 대조한다. + # 실패로 세지 않는다. 선언 밖 키는 review 로 남기고 값은 그대로 둔다. + extension_key_reviews = _extension_key_reviews(bo_items, declarations) + _add_gate(gates, "extension_payload_key_declaration_check", True, + f"bo_type_source={BO_TYPE_SOURCE} bo_types={len(bo_types)} " + f"declared_keys={len(declarations.get('declared_key_union') or [])} " + f"undeclared_records={len(extension_key_reviews)}") + 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, + "extension_key_reviews": extension_key_reviews, + }, 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 review in extension_key_reviews: + handoff.setdefault("review_items", []).append({ + "review_id": f"F0:extension_key:{review['bo_id']}", + "source_domain": review["source_domain"], + "severity": "SOFT_WARNING", + "issue_type": UNDECLARED_KEY_REVIEW_CODE, + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Stage2", + "template_note": "확장 payload 에 registry 선언 밖 키가 있다: " + + ", ".join(review["undeclared_keys"][:12]), + }) + 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로 보존하였다.", + }) + # F0-2 — 정규화는 노트로 끝내지 않고 handoff 에도 올린다. 침묵하는 폴백과 + # 선언된 기본값의 차이는 관측 가능성이다 (M-f 관측점). + for note in normalization_notes: + handoff.setdefault("review_items", []).append({ + "review_id": "F0:normalization:%s:%s" % (note.get("candidate_ref"), note.get("field")), + "source_domain": str(note.get("candidate_ref") or "").split(":")[0] or None, + "severity": "SOFT_WARNING", + "issue_type": "schema_field_fallback", + "source_review_code": note.get("field"), + "source_event_candidate_ids": [], + "source_evidence_indexes": [], + "source_meeting_clause_ids": [], + "downstream_owner": "Stage2", + "template_note": "F0 정규화가 적용된 칸이다. 값의 출처와 타당성을 재검토한다.", + }) + 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, + "bo_type_source": BO_TYPE_SOURCE, + "registry_version": declarations.get("generated_from", {}).get("registry_version"), + "extension_key_review_count": len(extension_key_reviews), + "r1_decision_state": r1_state, + "declared_exception_count": declared_exceptions, + }, 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: 300 + code: |- + #!/usr/bin/env python3 + # Task_C_BO_S0_signal_bundle_writer (v4) + # 정본 signal 거래 1건을 기록한다. 생성기·사영기·기록기는 조립본 모듈이며 여기서 만들지 않는다. + # Spec: stage_1_part_2_optimal_update_strategy_v.2.md §6.5 + from __future__ import annotations + import contextlib + import hashlib + import io + import itertools + import json + import pathlib + import posixpath + 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() + + # ---- 실행 뿌리 셋 — D-5 §2.4 0-c-2 확정값 ---- + ASSET_ROOT = "Default_Agent/" + EXECUTION_ROOT = "/tmp/s1" + LOGICAL_ROOT = "/tmp/s1" + WORK = pathlib.Path(EXECUTION_ROOT) + # 모듈의 디렉터리 산술을 그대로 재현한다(머리말 '반입 배치' 참조). + # SIGNALS_ROOT.parents[1] == ANCHOR 이므로 계약은 ANCHOR/contracts 아래다. + ANCHOR = WORK / "_sig" + SIGNALS_ROOT = ANCHOR / "pkg" / "signals" + CONTRACT_DIR = ANCHOR / "contracts" + OUTPUT_DIR = WORK / "_signal_out" + + # ---- 반입 대상 ---- + RUNTIME_MANIFEST = "Default_Agent/runtime_manifest.json" + COMPILER_MODULES = ["common", "projections", "schema_validator", "signal_compiler", + "signal_gate", "transaction_writer", "writer_boundary"] + ADAPTER_MODULES = ["s3_domain_seed_adapter", "s3_envelope_migration_adapter", + "s4_calculation_adapter", "sg01_activation_adapter"] + EMITTER_MODULES = ["emitter_runtime"] + ["emit_sg%02d" % n for n in range(2, 14)] + SIGNAL_REGISTRY = "Default_Agent/signals/signal_registry.v2.json" + EXECUTION_CONTRACT = "Default_Agent/contracts/signals/s5_execution_contract.v2.json" + + # ---- 사건 입력 ---- + # v4 — 구 경로·정적 이름을 걷어냈다. seed 는 fan-out 계획의 expected_output_path 로 읽는다. + ACTIVATION_MANIFEST_PATH = "routing/domain_activation_manifest.json" + FANOUT_PLAN_PATH = "fanout/domain_fanout_plan.json" + SOURCE_UNIVERSE_PATH = "stage1_tmp/task_c_bo/source_universe_manifest.json" + BO_PATH = "BO.json" + SEED_SCHEMA_VERSION = "task_c_bo_stage_b_domain_bo_seed.v3" + # R-5 — 도메인이 선언한 방출 signal 집합. A0 가 슬라이스에 실어 둔 것을 읽는다. + # registry 를 여기서 다시 적재하지 않는다. + SLICE_DIR = "runtime/domain_slices" + SLICE_ROOT_KEY = "stage_b_domain_slice" + DECLARED_EMISSION_REVIEW_CODE = "SIGNAL_EMISSION_NOT_DECLARED" + + # ---- 산출 ---- + SIGNAL_OUTPUT_PREFIX = "signals/" + TRANSACTION_ID_RE = r"^S5TX-[a-f0-9]{20}$" + CANONICAL_WRITER_MODULE = "compiler/transaction_writer.py" + COMPATIBILITY_ROOT_ALIASES = { + "compatibility_views/actio_case_signals.json": "actio_case_signals.json", + "compatibility_views/case_liability_signals.json": "case_liability_signals.json", + "compatibility_views/legal_effect_signals.json": "legal_effect_signals.json", + } + # 각 호환 뷰가 어느 정본 signal 의 사영인지. projections.py 의 서명이 정본이다. + COMPATIBILITY_VIEW_SOURCES = { + "compatibility_views/actio_case_signals.json": [], + "compatibility_views/case_liability_signals.json": ["SG-05", "SG-08"], + "compatibility_views/legal_effect_signals.json": ["SG-13"], + } + SIGNAL_FILE_BY_CODE = { + "SG-05": "legal_relation_lifecycle_signals.json", + "SG-08": "liability_causation_damage_signals.json", + "SG-13": "legal_effect_routes.json", + } + COMPATIBILITY_EMPTY_REVIEW_CODE = "COMPATIBILITY_VIEW_EMPTY" + + + 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}) + + + def read_raw(name: str) -> str: + # 파일 내용을 재직렬화 없이 원문 그대로 돌려준다. + # 모듈 미러의 sha256 은 원문 바이트의 해시여야 하므로 재직렬화를 허용하지 않는다. + # Task_D0_domain_activation_gate.yml 의 검증 완료본을 _call 서명만 맞춰 옮겼다. + p = _call("read_docs", {"doc_names": [name]}) + text = (p["result"].get("content") or [{}])[0].get("text", "") + if not text: + raise RuntimeError("EMPTY_RESPONSE:%s" % name) + try: + outer = json.loads(text) + except Exception: + return text + if isinstance(outer, str): + return outer + if isinstance(outer, dict) and "results" in outer: + r0 = (outer.get("results") or [{}])[0] + inner = r0.get("content") + if inner is None: + inner = r0.get("text") + if not isinstance(inner, str) or not inner: + raise RuntimeError("EMPTY_CONTENT:%s" % name) + return inner + raise RuntimeError("UNEXPECTED_READ_DOCS_ENVELOPE:%s" % name) + + + # ------------------------------------------------------------------ + # F-4 — 자기 입력의 배포 무결성. A0 게이트가 앞에서 걸러 주지만 이 task 는 + # 개별 재실행이 가능하다(예행 하네스가 실제로 task 단위로 돌린다). + # 매니페스트는 task 당 한 번만 읽는다. 호출마다 읽으면 S0 의 $ref 폐포 walk 에서만 19 회다. + # ------------------------------------------------------------------ + PART2_DEPLOYMENT_REMEDY = ( + "배포 원본 extension_research/Default_Agent 트리를 서버 Default_Agent/ 에 통째로 배포하고 " + "runtime_manifest.json 의 등재·sha256 과 대조해 결손 경로를 복원한다. 부분 복사는 허용되지 않는다.") + _MANIFEST_MAP = None + + + def _manifest_map(): + global _MANIFEST_MAP + if _MANIFEST_MAP is None: + _MANIFEST_MAP = {row["path"]: row["sha256"] + for row in (json.loads(read_raw(RUNTIME_MANIFEST)).get("entries") or []) + if isinstance(row, dict)} + return _MANIFEST_MAP + + + def _deployment_error(logical, detail, extra=None): + payload = {"reason_code": "PART2_ASSET_DEPLOYMENT_INCOMPLETE", "path": logical, + "detail": detail, "remedy": PART2_DEPLOYMENT_REMEDY} + if extra: + payload.update(extra) + return RuntimeError(json.dumps(payload, ensure_ascii=False)) + + + def _verify_asset(logical: str) -> str: + """원문을 돌려준다. 반환값을 소비해야 읽기 횟수가 늘지 않는다.""" + body = read_raw(logical) + want = _manifest_map().get(logical[len("Default_Agent/"):]) + if want is None: + raise _deployment_error(logical, "unregistered") + if want != hashlib.sha256(body.encode("utf-8")).hexdigest(): + raise _deployment_error(logical, "hash_mismatch", {"expected": want}) + return body + + + # ------------------------------------------------------------------ + # 1) 자산 반입 — D0 반입 규약 R-1~R-5 를 그대로 따른다. + # + # 디렉터리 산술을 흉내내야 하는 이유(실측). + # signals/compiler/*.py 는 _SIGNALS_ROOT = Path(__file__).resolve().parents[1] + # 로 signals 뿌리를 잡고, 실행 계약을 _SIGNALS_ROOT.parents[1]/contracts/ + # s5_execution_contract.v2.json 에서 읽는다. 즉 계약은 signals 의 조부모 아래다. + # 조립본은 계약을 Default_Agent/contracts/signals/ 에 두므로 그 산술이 조립본 + # 배치로는 풀리지 않는다. 반입 시에는 우리가 배치를 정하므로 모듈이 기대하는 + # 산술을 그대로 재현한다 — signals 를 /pkg/signals 에 두고 계약을 + # /contracts 에 둔다. 모듈 원문은 한 글자도 고치지 않는다. + # ------------------------------------------------------------------ + def _relative_refs(node: Any) -> list[str]: + """상대 파일 $ref 만 모은다. 로컬 포인터(#/...)는 검증기가 스스로 푼다.""" + out: list[str] = [] + if isinstance(node, dict): + ref = node.get("$ref") + if isinstance(ref, str) and ref and not ref.startswith("#"): + out.append(ref.split("#", 1)[0]) + for value in node.values(): + out.extend(_relative_refs(value)) + elif isinstance(node, list): + for value in node: + out.extend(_relative_refs(value)) + return [item for item in out if item] + + + def _stage_bytes(target, body: str) -> int: + raw = body.encode("utf-8") + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(raw) + return len(raw) + + + def materialize() -> dict[str, Any]: + SIGNALS_ROOT.mkdir(parents=True, exist_ok=True) + CONTRACT_DIR.mkdir(parents=True, exist_ok=True) + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + manifest_doc = json.loads(read_raw(RUNTIME_MANIFEST)) + expected = {row["path"]: row["sha256"] for row in manifest_doc.get("entries") or []} + + staged: dict[str, Any] = {"modules": [], "schemas": [], "unregistered": []} + for sub, names in (("compiler", COMPILER_MODULES), + ("adapters", ADAPTER_MODULES), + ("emitters", EMITTER_MODULES)): + for name in names: + logical = "%ssignals/%s/%s.txt" % (ASSET_ROOT, sub, name) + body = read_raw(logical) + raw = body.encode("utf-8") + want = expected.get(logical[len(ASSET_ROOT):]) + got = hashlib.sha256(raw).hexdigest() + if want is None: + raise RuntimeError("MODULE_MIRROR_UNREGISTERED:%s" % logical) + if want != got: + raise RuntimeError("MODULE_MIRROR_HASH_MISMATCH:%s" % logical) + _stage_bytes(SIGNALS_ROOT / sub / (name + ".py"), body) + staged["modules"].append("%s/%s" % (sub, name)) + + _stage_bytes(SIGNALS_ROOT / "signal_registry.v2.json", _verify_asset(SIGNAL_REGISTRY)) + _stage_bytes(CONTRACT_DIR / "s5_execution_contract.v2.json", _verify_asset(EXECUTION_CONTRACT)) + + # 스키마 목록을 이 코드가 만들지 않는다. registry 가 선언한 참조에서 출발해 + # 상대 파일 $ref 를 따라간다. _common/ 아래 조각도 그렇게 저절로 딸려 온다. + registry = json.loads((SIGNALS_ROOT / "signal_registry.v2.json").read_text(encoding="utf-8")) + pending = list(dict.fromkeys( + [str(row["schema"]) for row in registry["entries"] if row.get("schema")] + + [str(registry["domain_envelope"]), str(registry["manifest_schema"])])) + seen: set[str] = set() + while pending: + rel = posixpath.normpath(pending.pop(0)) + if rel in seen or rel.startswith(".."): + continue + seen.add(rel) + # F-4c — 이 한 줄이 폐포가 끌어오는 signal 스키마 전부를 덮는다. + # 목록을 상수로 굳히지 않는다 — registry 가 바뀌면 조용히 어긋난다. + body = _verify_asset("%ssignals/%s" % (ASSET_ROOT, rel)) + _stage_bytes(SIGNALS_ROOT / rel, body) + staged["schemas"].append(rel) + for child in _relative_refs(json.loads(body)): + pending.append(posixpath.join(posixpath.dirname(rel), child)) + + sys.path.insert(0, str(SIGNALS_ROOT)) + staged["signals_root"] = str(SIGNALS_ROOT) + staged["module_count"] = len(staged["modules"]) + staged["schema_count"] = len(staged["schemas"]) + return staged + + + # ------------------------------------------------------------------ + # 2) 입력 조립 — 정적 어휘를 두지 않는다. 계획서와 매니페스트가 목록을 정한다. + # ------------------------------------------------------------------ + def build_inputs() -> tuple[dict[str, Any], dict[str, Any]]: + activation = read_json_doc(ACTIVATION_MANIFEST_PATH) + if not isinstance(activation, dict) or not isinstance( + activation.get("domain_activation_manifest"), dict): + raise RuntimeError("SG01_INPUT_REQUIRED: Part 1 activation gate output is required") + + plan = read_json_doc(FANOUT_PLAN_PATH) + plan_root = plan.get("domain_fanout_plan") if isinstance(plan, dict) else None + plan_root = plan_root if isinstance(plan_root, dict) else (plan if isinstance(plan, dict) else {}) + instances = [x for x in (plan_root.get("task_instances") or []) if isinstance(x, dict)] + if not instances: + raise RuntimeError("S0_FANOUT_PLAN_EMPTY") + + seeds: dict[str, Any] = {} + seed_paths: list[str] = [] + declared_emissions: dict[str, list[str]] = {} + for instance in instances: + path = instance.get("expected_output_path") + domain_id = str(instance.get("domain_id") or "") + if not isinstance(path, str) or not path or not domain_id: + raise RuntimeError("S0_FANOUT_INSTANCE_INVALID:%s" % json.dumps(instance, ensure_ascii=False)[:120]) + document = read_json_doc(path) + root = document.get("stage_b_domain_bo_seed_output") if isinstance(document, dict) else None + if not isinstance(root, dict): + raise RuntimeError("S0_SEED_ROOT_MISSING:%s" % path) + if root.get("schema_version") != SEED_SCHEMA_VERSION: + raise RuntimeError("S3_SEED_SCHEMA_VERSION_MISMATCH:%s" % path) + if root.get("domain_id") != domain_id: + raise RuntimeError("S0_SEED_DOMAIN_MISMATCH:%s" % path) + seeds[domain_id] = document + seed_paths.append(path) + # R-5 — 같은 도메인의 슬라이스에서 emits_signals 선언을 읽는다. 부재는 조용히 넘긴다. + try: + slice_doc = read_json_doc("%s/%s.json" % (SLICE_DIR, domain_id)) + slice_root = slice_doc.get(SLICE_ROOT_KEY) if isinstance(slice_doc, dict) else None + slice_root = slice_root if isinstance(slice_root, dict) else (slice_doc if isinstance(slice_doc, dict) else {}) + declarations = slice_root.get("domain_declarations") + codes = [str(v) for v in ((declarations or {}).get("emits_signals") or []) + if isinstance(v, str) and v] + if codes: + declared_emissions[domain_id] = sorted(set(codes)) + except Exception: + pass + + universe_doc = read_json_doc(SOURCE_UNIVERSE_PATH) + universe_doc = universe_doc if isinstance(universe_doc, dict) else {} + bo_items = read_json_doc(BO_PATH) + bo_ids = sorted({str(x.get("BO_ID")) for x in bo_items + if isinstance(x, dict) and x.get("BO_ID")}) if isinstance(bo_items, list) else [] + evidence_ids = sorted({str(v) for v in (universe_doc.get("evidence_index_set") or [])}) + event_ids = sorted({str(v) for v in (universe_doc.get("event_candidate_ids") or [])}) + meeting_ids = sorted({str(v) for v in (universe_doc.get("meeting_clause_ids") or [])}) + if not (evidence_ids or event_ids or meeting_ids): + raise RuntimeError("S0_SOURCE_UNIVERSE_EMPTY:%s" % SOURCE_UNIVERSE_PATH) + + # fact_ids 와 law_version_ids 는 이 매니페스트가 선언하지 않는다. + # 비워 둔다. 레코드가 그 종류를 실으면 게이트의 source_membership 이 잡는다. + # 조용히 통과시키지 않는 쪽이 맞다. + source_universe = { + "bo_ids": bo_ids, + "fact_ids": [], + "evidence_ids": evidence_ids, + "meeting_clause_ids": meeting_ids, + "law_version_ids": [], + "event_ids": event_ids, + "all_source_refs": sorted(set(bo_ids) | set(evidence_ids) | set(event_ids) | set(meeting_ids)), + "unrouted_evidence_count": int(len( + activation["domain_activation_manifest"].get("unrouted_material") or [])), + } + inputs = { + "declared_emissions": declared_emissions, + "domain_activation_manifest": activation, + "domain_seed_outputs": seeds, + "source_universe": source_universe, + # v4 — 구 signal 원문을 넣지 않는다. 세 호환 뷰는 정본 signal 의 사영일 뿐이다. + "legacy_signals": {}, + "signal_candidates": {}, + } + receipt = { + "seed_count": len(seeds), + "declared_emission_domains": sorted(declared_emissions), + "seed_paths": seed_paths, + "bo_id_count": len(bo_ids), + "evidence_count": len(evidence_ids), + "event_count": len(event_ids), + "meeting_count": len(meeting_ids), + "fact_ids_declared": False, + "law_version_ids_declared": False, + } + return inputs, receipt + + + # ------------------------------------------------------------------ + # 3) 생성기 12 · 사영기 3 · 단일 기록기 호출 + # 호출 본문은 이 한 함수뿐이다. 생성기와 사영기는 순수 함수이며 파일을 쓰지 않는다. + # 실행기 안에서 파일을 쓰는 것은 compiler/transaction_writer.py 하나다 — + # signal_gate 의 canonical_writer_uniqueness 가 그것을 강제한다. + # ------------------------------------------------------------------ + def compile_and_validate(inputs: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]: + from compiler.signal_compiler import compile_transaction + from compiler.signal_gate import validate_output + + buf = io.StringIO() + with contextlib.redirect_stdout(buf): + manifest = compile_transaction(inputs, OUTPUT_DIR) + gate = validate_output(inputs, OUTPUT_DIR, SIGNALS_ROOT) + if not re.fullmatch(TRANSACTION_ID_RE, str(manifest.get("transaction_id") or "")): + raise RuntimeError("S0_TRANSACTION_ID_PATTERN:%s" % manifest.get("transaction_id")) + if gate.get("canonical_writer_modules") != [CANONICAL_WRITER_MODULE]: + raise RuntimeError("S0_CANONICAL_WRITER_NOT_UNIQUE:%s" + % json.dumps(gate.get("canonical_writer_modules"), ensure_ascii=False)) + if gate.get("status") != "PASS": + raise RuntimeError("S0_SIGNAL_GATE_FAILED:%s" + % json.dumps(gate.get("errors")[:8], ensure_ascii=False)) + return manifest, gate + + + # ------------------------------------------------------------------ + # 4) 반출 — 거래가 낸 바이트를 그대로 옮긴다. 재직렬화하지 않는다. + # ------------------------------------------------------------------ + def publish(manifest: dict[str, Any]) -> dict[str, Any]: + written: list[dict[str, Any]] = [] + local: dict[str, bytes] = {} + for path in sorted(OUTPUT_DIR.rglob("*.json")): + rel = path.relative_to(OUTPUT_DIR).as_posix() + raw = path.read_bytes() + local[rel] = raw + write_doc(SIGNAL_OUTPUT_PREFIX + rel, raw.decode("utf-8")) + written.append({"path": SIGNAL_OUTPUT_PREFIX + rel, + "sha256": hashlib.sha256(raw).hexdigest(), "bytes": len(raw)}) + + # 구 이름 세 개는 Part 3·4 가 읽는 최대 호환면이다. 같은 바이트를 그대로 한 벌 더 놓는다. + # 두 번째 생산자가 아니라 운반이다 — 내용은 거래가 낸 것과 바이트 동일하다. + aliases: list[dict[str, Any]] = [] + for canonical_rel, alias in COMPATIBILITY_ROOT_ALIASES.items(): + raw = local.get(canonical_rel) + if raw is None: + raise RuntimeError("S0_COMPATIBILITY_VIEW_MISSING:%s" % canonical_rel) + write_doc(alias, raw.decode("utf-8")) + aliases.append({"alias": alias, "canonical": SIGNAL_OUTPUT_PREFIX + canonical_rel, + "sha256": hashlib.sha256(raw).hexdigest()}) + + # 기록 후 재읽기. 봉인 파일 하나를 원문 바이트로 되읽어 해시를 대조한다. + reread = read_raw(SIGNAL_OUTPUT_PREFIX + "signal_manifest.json").encode("utf-8") + if hashlib.sha256(reread).hexdigest() != hashlib.sha256(local["signal_manifest.json"]).hexdigest(): + raise RuntimeError("S0_POST_WRITE_MANIFEST_HASH_MISMATCH") + return {"written": written, "compatibility_root_aliases": aliases, + "file_count": len(written)} + + + def emission_notices(manifest: dict[str, Any], + declared_emissions: dict[str, list[str]]) -> list[dict[str, Any]]: + """도메인이 선언한 emits_signals 와 기록이 실린 정본 signal 을 대조한다. + + 실패로 세지 않는다. 선언은 registry 의 것이고 실제 방출은 사건 재료에 달려 있어 + 선언보다 적게 나오는 것은 정상이다. 반대로 **선언 밖에서 기록이 나오면** 어휘 밖의 + 산출이므로 지목한다 — 137종 일반성은 그 어휘 안에서 성립해야 한다. + """ + if not declared_emissions: + return [] + union: set[str] = set() + for codes in declared_emissions.values(): + union.update(codes) + by_path = {row["path"]: row for row in manifest.get("files") or []} + emitted: set[str] = set() + for code, filename in SIGNAL_FILE_BY_CODE.items(): + if (by_path.get(filename) or {}).get("record_count"): + emitted.add(code) + undeclared = sorted(code for code in emitted if code not in union) + if not undeclared: + return [] + return [{ + "review_code": DECLARED_EMISSION_REVIEW_CODE, + "undeclared_signals": undeclared, + "declared_union": sorted(union), + "declared_by_domain": {k: v for k, v in sorted(declared_emissions.items())}, + "note": "선언 밖 signal 에 기록이 실렸다. registry 의 emits_signals 를 넓히거나 산출을 좁힌다.", + }] + + + def compatibility_notices(manifest: dict[str, Any]) -> list[dict[str, Any]]: + """호환 뷰가 비었는데 정본 signal 에는 기록이 있으면 조용히 넘기지 않고 지목한다. + + v3 은 세 파일을 BO.json 에서 직접 만들었고, v4 는 정본 signal 의 사영으로 만든다. + 사영 대상은 compatibility_key/compatibility_route 를 단 기록뿐이며 그 표식은 + 구 signal 원문에서만 붙는다. 따라서 구 원문을 넣지 않는 v4 에서는 뷰가 빌 수 있다. + Part 3·4 는 signal_manifest.downstream_read_sets 가 선언한 정본 집합으로 옮겨야 한다. + 그 이관은 Part 3·4 개정의 몫이므로 여기서는 사실만 남긴다. + """ + by_path = {row["path"]: row for row in manifest.get("files") or []} + notices: list[dict[str, Any]] = [] + for canonical_rel, alias in COMPATIBILITY_ROOT_ALIASES.items(): + view = by_path.get(canonical_rel) or {} + if view.get("state") != "empty": + continue + sources = COMPATIBILITY_VIEW_SOURCES[canonical_rel] + populated = sorted(code for code in sources + if (by_path.get(SIGNAL_FILE_BY_CODE.get(code, "")) or {}).get("record_count")) + if populated: + notices.append({ + "review_code": COMPATIBILITY_EMPTY_REVIEW_CODE, + "alias": alias, + "canonical_view": canonical_rel, + "populated_canonical_signals": populated, + "downstream_read_sets": manifest.get("downstream_read_sets"), + "note": "구 이름 파일이 비었다. Part 3·4 는 정본 signal 집합으로 읽어야 한다.", + }) + return notices + + + def main() -> None: + _init() + staged = materialize() + inputs, input_receipt = build_inputs() + manifest, gate = compile_and_validate(inputs) + published = publish(manifest) + notices = compatibility_notices(manifest) + notices.extend(emission_notices(manifest, inputs.get("declared_emissions") or {})) + + print(json.dumps({ + "status": "READY_WITH_REVIEW" if notices else "READY", + "message": "정본 signal 거래 1건 기록 완료 (파일 %d종)" % published["file_count"], + "schema_version": "stage1_canonical_signal_writer.v1", + "transaction_id": manifest.get("transaction_id"), + "manifest_status": manifest.get("status"), + "signal_manifest_path": SIGNAL_OUTPUT_PREFIX + "signal_manifest.json", + "module_import": { + "module_count": staged["module_count"], + "schema_count": staged["schema_count"], + "hash_source": RUNTIME_MANIFEST, + "signals_root": staged["signals_root"], + }, + "inputs": input_receipt, + "gate": { + "status": gate.get("status"), + "error_count": gate.get("error_count"), + "canonical_writer_modules": gate.get("canonical_writer_modules"), + "source_membership_pass": gate.get("source_membership_pass"), + "domain_source_membership_pass": gate.get("domain_source_membership_pass"), + "meeting_only_promotion_pass": gate.get("meeting_only_promotion_pass"), + "negative_conflict_preservation_pass": gate.get("negative_conflict_preservation_pass"), + "compatibility_projection_pass": gate.get("compatibility_projection_pass"), + "manifest_hash_pass": gate.get("manifest_hash_pass"), + "forbidden_conclusion_key_pass": gate.get("forbidden_conclusion_key_pass"), + }, + "published": published, + "active_domains": manifest.get("active_domains"), + "unrouted_counts": manifest.get("unrouted_counts"), + "compatibility_notices": notices, + }, 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: + # A0 가 fan-out 계획을 낸 뒤에야 worker 인스턴스가 생긴다. 그래서 직렬이다. + 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_B_domain_worker_*"] + wait_until: ["IN"] + + # 활성 도메인 병렬 x M. 인스턴스는 domain_fanout_plan.task_instances[] 가 만든다. + Task_C_B_domain_worker_*: + nexts: ["Task_C_BO_R0_seed_reducer_and_exception_planner"] + wait_until: ["Task_C_BO_A0_context_and_domain_slice_compiler"] + + # barrier — 정확 일치. 누락·초과·중복 모두 실패다. + Task_C_BO_R0_seed_reducer_and_exception_planner: + nexts: ["Task_C_BO_R1_exception_adjudicator"] + wait_until: ["all Task_C_B_domain_worker_*"] + + # 조건부. 예외 pack 이 비면 통과만 한다. + 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"] + + prevs: [] + nexts: [] diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/ver_8_yaml_candidates/stage_1_part_2_v.8.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/ver_8_yaml_candidates/stage_1_part_2_v.8.yml index 9921c4b0..257a6fb8 100644 --- a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/ver_8_yaml_candidates/stage_1_part_2_v.8.yml +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/ver_8_yaml_candidates/stage_1_part_2_v.8.yml @@ -25,7 +25,9 @@ # fan-out fanout/domain_fanout_plan.json domain_fanout_plan.v1 # worker 이름 Task_C_B_domain_worker_* · 인스턴스 DOMAIN-<도메인ID> # 실행 인자 --asset-root · --execution-root · --logical-root -# 구 경로 stage1_tmp/task_c_bo/** 는 쓰지 않는다(legacy_paths_forbidden). +# 구 slice/seed 경로(stage1_tmp/task_c_bo/domain_slices|domain_seed_outputs)는 쓰지 않는다 +# (legacy_paths_forbidden). stage_a_context·source_universe_manifest(P-1 복귀)와 +# postb_* 3종은 stage1_tmp/task_c_bo/ 를 정본 경로로 유지한다. # # 모듈 반입 — Part 1 D0 규약 R-1~R-5 승계 # .txt 미러를 read_raw 로 읽고 runtime_manifest.json 의 sha256 과 대조한 뒤 @@ -890,8 +892,6 @@ Agent: "registry_loader": "Default_Agent/stage1_runtime/registry_loader.txt", "worker_output_validator": "Default_Agent/stage1_runtime/worker_output_validator.txt", } - ACTIVATION_MANIFEST_PATH = "routing/domain_activation_manifest.json" - REGISTRY_INDEX_PATH = "Default_Agent/domains/_registry_index.json" def _seed_docs_from_plan(plan): # v5 — 경로만이 아니라 계획 행 전체를 보관한다. validate_seed_object 가 diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/확장_최적워크플로우_연구_프롬프트.txt b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/확장_최적워크플로우_연구_프롬프트.txt index 5b13ea63..f29020cf 100644 --- a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/확장_최적워크플로우_연구_프롬프트.txt +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/확장_최적워크플로우_연구_프롬프트.txt @@ -2262,6 +2262,25 @@ stage_1_2_3_assets_distribution_location.md`의 `§8. 특기사항 (판독 중 ================================== + +`problems_identified_in_detecting_assets_location_v.2.md`--`§2. 확정 작업 분류`의 `A. YAML 수정으로 해결`에서 `P2 L893–894 사문 상수 2줄 삭제`, `P2 L28 주석 1줄` +`B. Default_Agent 자산 등재/삭제로 해결`에서 `**P1 자산 6종 manifest 등재**`, ``validation_assets/routing/case_kind_domain_matrix.v2.json` 제거` 작업들을 수행한다. + + + +1. 에 기재된 작업을 수행한다. +2. 작업 수행 후, sub-agent를 띄워 지시사항을 100% 정확히 준수하여 작업이 진행되었는지 검증한다. 이 검증을 통과할 때까지 작업수행-검증 과정을 반복한다. +3. 검증 과정을 완전히 통과했을 때, 수정된 yaml 파일들은 overwrite하여 기존 위치에 저장하고, 기존 yaml들은 outdated 폴더로 옮긴다. +4. `B. Default_Agent 자산 등재/삭제로 해결` 작업 수행 결과로 Default_Agent 폴더에 변화가 발생한 내용만을 `stage_1_2_3_assets_distribution_location.md` 동일 파일명으로 overwrite하여 저장하고 기존 파일은`stage_1_2_3_assets_distribution_location_old.md`로 파일명만 바꾸어서 유지한다. + + + +- `v.7/CLAUDE.md`가 제시하는 Fable 5 행동 규칙을 준수한다. +- 작업 완료 시 작업 내역을 압축 요약하여 `v.7/MEMORY.md`에 추가 기입한다. +- MEMORY.md 기입 후 연결된 git repository에 commit한다. + + +================================== @@ -2273,34 +2292,8 @@ stage_1_2_3_assets_distribution_location.md`의 `§8. 특기사항 (판독 중 - - - - - - - -Stage 1 - part 1|2|3 배포 폴더 위치 v.2 작성 - - - - - - - - - - - - - - - -stage_1_part_3_개정작업내역서.md -stage_1_part_1_v.8.yml - - - +--> stage 1 -- part 4 개정 작업 최적 워크플로우 탐구 --> 작업 명세서 작성 +--> stage 1 -- part 4 개정 --> 남은 작업 check --> 추가작업 명세서 생성 및 작업 수행 --> part 4 개정 마무리 --> stage 1 -- part 3 개정 작업 최적 워크플로우 탐구 --> 작업 명세서 생성 --> stage 1 -- part 3 개정 --> 남은 작업 check --> 추가작업 명세서 생성 및 작업 수행 --> part 3 개정 마무리 @@ -2309,21 +2302,6 @@ stage_1_part_1_v.8.yml - - - - - - - - - - - - - - -