diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/case_kinds.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/case_kinds.md new file mode 100644 index 00000000..fc186b6f --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/case_kinds.md @@ -0,0 +1,22 @@ +# 사건 종류(Case Kinds) + +| 소송 대분류 | 분쟁 유형 | 사건 종류 | +|------------|----------|----------| +| 이행의 소 | 금전의 지급을 구하는 소 | 대여금 청구, 계금 청구, 계약금 청구, 공사대금 청구, 공제금 청구, 관리비 청구, 구상금 청구, 담보금 청구, 동업반환금 청구, 매매대금 청구, 배당금・배분금 청구, 배상금 청구, 변상금 청구, 보관금 청구, 보상금 청구, 보증금 청구, 보증채무금 청구, 보험금 및 보험금수익자변경 청구, 부금・불입금 청구, 부당이득금・이득상환금 청구, 분담금 청구, 분양대금 청구, 사용료 청구, 선급금・선수금 청구, 손해배상(자) 청구, 손해배상(산) 청구, 손해배상(의) 청구, 손해배상(지) 청구, 손해배상(저) 청구, 손해배상(언) 청구, 손해배상(건) 청구, 손해배상(국) 청구, 손해배상(해) 청구, 손해배상(기) 청구, 수표금 청구, 설계비・시설대여금 청구, 신용장대금 청구, 신용카드이용대금 청구, 압류채권대금 청구, 약속어음금 청구, 약정금 청구, 양도채권금・양수금 청구, 예금(예치금・예탁금・인출금) 청구, 운임(운송대금) 청구, 위약금 청구, 위탁금 청구, 유류분반환・유증 등 청구, 임대료 청구, 임차 및 전세보증금 청구, 재산상속회복 청구, 정산금 청구, 전부금 청구, 지료 청구, 진료비・치료비・의료비 청구, 청산금・채무인수금・체당금・추심금・출자금 청구 등, 근로자의 임금 및 퇴직금 청구, 투자금반환 청구, 특허권전용실시대금・실용신안권실시대금 청구, 하자보수비・할부대금・화해금・확약금・환급금・회원가입비 등 | +| 이행의 소 | 종류물(대체물)의 지급 또는 인도를 구하는 소 | | +| 이행의 소 | 특정물의 인도를 구하는 소 | 토지의 인도를 구하는 소, 임대인의 건물철거청구와 임차인의 건물매수청구권 행사, 건물의 명도(인도)의 소, 점유회수(반환)청구의 소, 동산 등의 인도 청구 | +| 이행의 소 | 의사의 진술을 구하는 소 | 매매를 원인으로 한 소유권이전등기 청구, 매수인의 잔대금지급의무와 매도인의 소유권이전등기 청구, 계약해제로 인한 원상회복의무와 손해배상의무의 관계, 교환・교환약정을 원인으로 한 소유권이전등기 청구, 대물반환・대물변제 등을 원인으로 한 소유권이전등기 청구, 명의신탁해지를 원인으로 한 소유권이전등기 청구, 취득시효완성을 원인으로 한 소유권이전등기 청구, 양도약정・담보계약해지・이관을 원인으로 한 소유권이전등기, 재산분할・재산승계를 원인으로 한 소유권이전등기 청구, 증여・유증・유류분반환을 원인으로 한 소유권이전 청구, 화해·환매를 원인으로 한 소유권이전등기 청구, 가등기에 기한 본등기 청구, 대위에 의한 소유권이전등기 청구, 소유권 이외의 권리 설정등기 및 이전등기 청구, 말소등기(소유권이전등기말소) 청구, 말소등기(소유권보존등기말소) 청구, 말소등기(근저당권설정등기말소) 청구, 말소등기(가등기말소) 청구, 말소등기(지상권설정등기말소) 청구, 말소등기(전세권설정등기말소) 청구, 말소등기(대위에의한소유권이전등기말소) 청구, 말소등기(토지의일부만에관하여말소등기) 청구, 말소등기(폐쇄한등기기록에기록된등기의말소) 청구, 말소등기(사위(詐僞)판결에의한등기의말소) 청구, 말소등기(기타등기에관한말소) 청구, 말소등기의 회복등기 청구, 경정등기 청구, 등기상 이해관계 있는 제3자의 승낙의 의사표시, 진정명의 회복을 등기원인으로 하는 소유권이전등기 청구, 명의변경절차에 관한 소송 | +| 이행의 소 | 특수한 유형의 이행의 소 | 장래이행의 소, 소유물방해제거·방해예방청구, 정정보도·반론보도·사과광고 청구, 토지거래허가신청의 협력의무 이행청구 | +| 확인의 소 | 채권에 관한 확인을 구하는 소 | 채권존재확인 청구, 채권부존재확인 청구, 채무부존재확인 청구, 임차권 확인 청구 | +| 확인의 소 | 물권에 관한 확인을 구하는 소 | 부동산 소유권 확인, 도메인 소유권, 입목 소유권, 통상실시권 확인, 불상·신탁·자동차 등 소유권 확인, 유치권확인 또는 부존재확인 | +| 확인의 소 | 증서의 진정여부를 확인하는 소 | | +| 확인의 소 | 각종 무효확인 청구 | 대의원회결의무효 등, 이사회결의무효, 총회결의무효확인 | +| 확인의 소 | 각종 결의부존재확인 청구 | 이사회결의부존재, 임시주주총회결의부존재 등, 기타 결의부존재확인 | +| 확인의 소 | 각종 지위확인 청구 | 교원의 지위, 근로자의 지위 등, 분양권·매수인의 지위 등, 기타 지위확인 | +| 확인의 소 | 각종 권리확인 청구 | 공탁금출급청구권, 보상금·수용금수령권, 분양권 확인 청구, 시설출입이용권, 주위토지통행권, 주주권·주식질권 등, 기타 권리확인 | +| 확인의 소 | 기타 확인청구 | | +| 형성의 소 | | 경계확정 청구, 공유물분할 청구, 청구이의, 제3자이의, 재심 청구, 준재심 청구, 제권판결에 대한 불복의 소, 사해행위취소 청구, 주주총회결의취소의 소 | +| 가사소송 | 가사소송 | 혼인관계 소송, 부모와 자 관계 소송, 다른 법령의 규정에 의한 가사 소송, 손해배상청구・원상회복청구 | +| 가사소송 | 가사비송 | 라류 가사비송, 마류 가사비송, 다른 법령의 규정에 의한 가사비송사건 | +| 가족관계등록 | | | +| 행정소송 | | 행정처분취소 등 청구, 행정처분무효확인 등 청구, 행정처분 집행정지신청 | 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 3fccc3c8..b46fa45d 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 @@ -1,8 +1,9 @@ -# MEMORY — 청구취지작성규칙 작업 메모리 +# MEMORY — Stage 1 개정 작업 메모리 > 이 파일은 CLAUDE.md의 Memory System 규칙에 따라 유지한다. task당 하나의 교훈, 상단 한 줄 요약. ## 2026-08-07 법리 도메인·signal 앙상블 (ensemble.md) + 한 줄 요약: 독립 연구 2건(a: Claude 기준, b: GPT 비교)을 대조해 실체 21 + 횡단 3(X1 통지·X2 자산·X3 절차) + 폴백 1 + 계산 11(+후순위 3) 도메인, 공통 signal 10종 + 조건부 domain_signals/.json 2층 구조로 확정 — 산출물 `extension_research/ensemble.md`. - 판정에서 반복 사용한 기준: ① 요건사실 체계·증거 component가 다르면 분리, 같으면 통합(계약 일반/원인/등기 3분할 기각, 도급·전문책임·보험·지재·언론 분리 채택), ② 소의 유형(이행/확인/형성)은 실체 도메인이 아니라 X3 횡단 차원, ③ 정교한 계산은 개별 계산 도메인 + Stage 1은 operand 완전성 검증까지만. diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/Default_Agent/stage1_runtime/__pycache__.zip b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/Default_Agent/stage1_runtime/__pycache__.zip new file mode 100644 index 00000000..eda88cd2 Binary files /dev/null and b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/Default_Agent/stage1_runtime/__pycache__.zip differ diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/Liti-agent Stage 1 Part 3 개정용 registry·signal 재료 실물 조사.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/Liti-agent Stage 1 Part 3 개정용 registry·signal 재료 실물 조사.md new file mode 100644 index 00000000..d8b60b66 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/Liti-agent Stage 1 Part 3 개정용 registry·signal 재료 실물 조사.md @@ -0,0 +1,423 @@ +# Liti-agent Stage 1 Part 3 개정용 registry·signal 재료 실물 조사 + +BASE = `.../v.7/extension_research` (이하 `$BASE`) + +------ + +## 1. registry `structure_types` 실측 (26개 `domain_config.json` 전수) + +**경로**: `$BASE/Default_Agent/domains/<도메인>/domain_config.json` — 26개 (E-00~E-21, EC-00, X1, X2, X3). `domains/` 하위에는 `_registry_index.json`과 `_common/`도 있음. + +### 1.1 필드 구성 — 실제 키는 정확히 4개 + +73건 전 레코드가 예외 없이 동일한 4키만 가짐 (추가 키 0건): + +| 키 | 출현 | 값 성격 | +| ----------------- | ----- | ----------- | +| `type_id` | 73/73 | 유형 식별자 | +| `priority_rank` | 73/73 | 정수 | +| `module` | 73/73 | 모듈명 | +| `role_projection` | 73/73 | 역할군명 | + +`domain_config.json` 최상위 키 순서(E-00 기준): `schema_version, domain_id, label_ko, slug, kind, status, execution_policy, depends_on, special_law_profiles, activation_cues, routing_policy, element_slots, opposing_fact_slots, evidence_components, defense_map, calculation_bindings, emits_signals, bo_types, structure_types, effect_projection, gates, review_codes, contract_guards, prompt_overlay_ref, profile_schema_ref, extension_schema_ref, runtime_activation_from_legacy_alias_forbidden, case_kind_name_runtime_lookup_forbidden, removed_noncanonical_signal_aliases`. + +### 1.2 합집합 — **선언 73건 · distinct `type_id` 56종** + +전략서 §1.1의 "선언 73건 · 고유 56종" 수치는 실측과 **정확히 일치**. `_registry_index.json` 기준 도메인 순서는 `E-00 … E-21 · EC-00 · X1 · X2 · X3`. + +전략서·원안이 말한 **"전역 열넷"은 56종의 부분집합**이며, 동결계약 `canonical_type_set` 11종 + 3종으로 구성됨: + +- 동결계약 11종(§6 참조): `money_claim, commercial_successor, secured_debt, registry_invalidity, land_use_gain, valuation, fraudulent_transfer, preserved_claim, actio_remedy_or_cap, succession_notice_lien_asset_defense, general_legal_effect` +- 추가 3종: `unjust_enrichment`(E-04, rank 12), `possession_vindication`(E-11, rank 5), `asset_state`(X2, rank 4) +- 나머지 **42종**은 E-01·E-05·E-06·E-07·E-08·E-12·E-14·E-16~E-21이 선언한 것으로 어느 문서의 "열넷"에도 없음. + +전 56종(rank, module, role_projection, 선언 도메인 수) — rank 오름차순: + + + +``` +1 construction_payment_claim ×1 construction_contract / construction_payment_roles +1 lease_deposit_return ×1 lease_contract / lease_deposit_return_roles +1 money_claim ×2 money_claim / claim_chain_ref +2 commercial_successor ×1 commercial_successor / claim_chain_ref +2 construction_progress_settlement ×1 construction_contract / construction_progress_roles +2 lease_rent_payment ×1 lease_contract / lease_rent_roles +3 construction_defect_remedy ×1 construction_defect / construction_defect_roles +3 lease_termination ×1 lease_lifecycle / lease_termination_roles +3 secured_debt ×1 secured_property / linked_structures +4 asset_state ×1 asset_state / asset_state_projection +4 construction_defect_setoff ×1 construction_defect / construction_setoff_roles +4 lease_delivery_concurrent_performance ×1 lease_lifecycle / lease_concurrent_performance_roles +4 registry_invalidity ×2 registry_invalidity / linked_structures +5,6 land_use_gain ×2 land_use_gain / legal_calculation_object ← rank 불일치 +5 lease_attached_property_purchase ×1 lease_contract / lease_purchase_claim_roles +5 possession_vindication ×1 possession_vindication / linked_structures +6 valuation ×2 valuation / legal_calculation_object +7 fraudulent_transfer ×1 actio / actio_roles +8 preserved_claim ×1 actio / claim_chain_ref +9 actio_remedy_or_cap ×1 actio / linked_actio_structures +10 succession_notice_lien_asset_defense ×5 (동명 module) / linked_structures +11 general_legal_effect ×10 legal_effect / legal_effect_roles +12 unjust_enrichment ×1 unjust_enrichment / restitution_roles +14 execution_linked_claim ×1 execution_linked_claims / linked_execution_claims +17 labor_wage_claim ×1 labor_wage_claims / linked_labor_claims +18 org_resolution_status_relation ×1 org_resolution_status / resolution_status_roles +19 insurance_claim_relation ×1 insurance_claims / insurance_relation_roles +20 ip_right_claim_relation ×1 · 20 tort_liability ×1 +21 joint_tort_liability ×1 · 21 media_personality_claim_relation ×1 +22 vicarious_liability ×1 · 23 structure_liability ×1 · 24 tort_damage_extent ×1 +71 bill_primary_liability · coownership_governance · juristic_act_validity · professional_duty_liability +72 check_issuer_liability · coownership_partition · declaration_defect · medical_treatment_fault +73 boundary_determination · cancellation_effect · endorsement_holder_status · medical_disclosure_duty +74 exclusive_use_settlement · presentment_recourse_liability · product_defect_liability · unauthorized_agency +75 aggregate_building_management · apparent_agency · blank_instrument_completion · professional_damage_causation +76 cause_relation_defense · condition_term_effect +``` + +`land_use_gain`만 도메인 간 rank가 갈림(E-04=5, E-11=6). 전략서 §2.2의 "합치지 않는다" 근거가 이 1건임을 확인. + +### 1.3 도메인별 개수 분포 (합 73) + +| n | 도메인 | +| ---- | ---------------------------------------------- | +| 0 | EC-00 | +| 1 | E-00, E-14, E-17, E-18, E-19, E-20, E-21 (7개) | +| 2 | E-02, E-09, E-15, X1, X2, X3 (6개) | +| 3 | E-03, E-04, E-11, E-13 (4개) | +| 4 | E-10 | +| 5 | E-05, E-06, E-07, E-12 (4개) | +| 6 | E-01, E-08, E-16 (3개) | + +`structure_type_ids` 총 73 · 빈 도메인은 EC-00 하나 — 이는 `$BASE/stage_1_part_1_and_2_updated_yaml_analysis.md` §6.3의 registry 투영 실측표와 정확히 일치. + +### 1.4 `priority_rank` 값 분포 (범위 1~~76, 결측 rank 13·15·16·25~~70) + + + +``` +rank 1:4 2:3 3:3 4:5 5:3 6:3 7:1 8:1 9:1 10:5 11:10 12:1 + 14:1 17:1 18:1 19:1 20:2 21:2 22:1 23:1 24:1 + 71:4 72:4 73:4 74:4 75:4 76:2 +``` + +rank 11(`general_legal_effect`) 10건이 최다. rank 11이 최댓값이 아님(최댓값 76) — 전략서 §1.2의 "폴백 조항은 애초에 성립하지 않는다" 근거를 실측 확인. + +### 1.5 `general_legal_effect`의 존재와 위치 + +**10/26 도메인만 선언**: E-00, E-02, E-03, E-07, E-08, E-09, E-10, E-15, X1, X3. 전부 `priority_rank: 11 / module: legal_effect / role_projection: legal_effect_roles`로 동일. 나머지 16 도메인(E-01, E-04~~E-06, E-11~~E-14, E-16~E-21, EC-00, X2)에는 내려보낼 자리가 없음. E-00은 이 유형 **하나만** 선언(n=1). + +### 1.6 `effect_projection` 구조 — 3키, 26/26 도메인 전수 선언 + + + +```json +{ "schema_ref": "Default_Agent/domains/E-13/effect_projection.schema.json", + "module": "actio", + "role_projection": "actio_roles" } +``` + +- `schema_ref` — 도메인별 `effect_projection.schema.json` 논리 경로 +- `module` / `role_projection` — 그 도메인의 **기본** 모듈·역할군 1쌍 (E-13은 rank 7 `fraudulent_transfer`의 값과 동일) +- EC-00 포함 26개 전부 존재. 즉 `structure_types`가 0인 EC-00도 `effect_projection`은 가짐. + +### 1.7 ⚠ 배포 트리에 사이드카가 없다 (Part 3 착수 리스크) + +전략서 I-10이 입력으로 잡은 `structure_types.json` · `module_role_projection.json` ×25는 **`$BASE/Default_Agent/domains/` 아래에 0건**이다. + +- 배포 트리 `$BASE/Default_Agent/` 전체 = **160 파일**. `domains/` 하위 파일은 `domain_config.json` 26 + `seed_prompt_overlay.md` 25 + `_common/` 2 (+ `_registry_index.json`) 뿐. +- 사이드카는 조립본 `$BASE/선행구축/Default_Agent_Stage_1/domains/`(전체 1,323 파일)에만 존재: `structure_types.json` 25 · `module_role_projection.json` 25 · `effect_projection.schema.json` 25 · `gates.json` 25 등. +- 따라서 배포 트리 `domain_config.json`의 `effect_projection.schema_ref`가 가리키는 `Default_Agent/domains/E-13/effect_projection.schema.json`은 **배포 트리에 실재하지 않는 경로**다. + +조립본 사이드카 실물(E-13): + +- `structure_types.json`: `{schema_version: "stage1_domain_structure_types.v1", domain_id, status, legacy_source_domain: "B4", structure_types[4키 동일], admission_rules{registered_membership_required, multiple_types_allowed, source_order_deduplicated, nonregistered_type_review_code: "UNREGISTERED_EFFECT_TYPE_REVIEW", silent_general_legal_effect_fallback_forbidden: true}}` +- `module_role_projection.json`: `{schema_version: "stage1_domain_module_role_projection.v1", domain_id, status, mappings[{structure_type, module, role_projection, source: "legacy_preserved_or_additive_registered"}], default_projection: null, missing_mapping_policy{emit_review_code: "UNREGISTERED_EFFECT_TYPE_REVIEW", silent_fallback_forbidden: true}}` + +------ + +## 2. SG signal 스키마·레지스트리 선언 + +**스키마 위치**: `$BASE/Default_Agent/signals/schemas/` — **15개** + `$BASE/Default_Agent/signals/_common/` **2개**(`signal_item.schema.json`, `evidence_slot_status.schema.json`) = 합 17종. schemas/ 15개 중 SG 계열은 12개(SG-02~SG-13 중 SG-01 제외)이고 나머지 3개는 `domain_activation_manifest`(=SG-01), `domain_signal_envelope.schema.v2`, `signal_manifest`. + +### 2.1 공통 봉투 구조 (5종 전부 동일) + +최상위 4키, `additionalProperties: false`, 전부 required: + +- `schema_version` (const, 예: `stage1_signal_sg13.v1`) +- `signal_id` (const, 예: `"SG-13"`) +- `status` (enum: `READY / READY_WITH_REVIEW / EMPTY / BLOCKED / FAILED`) +- `records` (array) + +### 2.2 레코드 필드 — `_common/signal_item.schema.json` 15키 전부 required + +``` +signal_id, record_type, status, issue_cluster_ids, domain_ids, source_bo_ids, source_fact_ids, evidence_refs, meeting_clause_refs, negative_or_conflicting_refs, law_version_refs, confidence, proof_grade, review_required, details +``` + +- `status` enum: `observed / inferred / contested / missing_required / review` +- `confidence` enum: `high / medium / low` +- `proof_grade` enum: `document_direct / document_indirect / corroborated / client_statement / ungraded` +- `record_type` 패턴: `^[a-z][a-z0-9_.-]{1,127}$` +- `details`: `type: object` (자유 스키마) — **각 SG가 `details` 안 필드를 스키마로 강제하지 않는다**(예외 2건은 아래) + +### 2.3 5종별 `record_type` 허용 패턴 · 조건부 제약 + +| SG | 파일 | `record_type` 패턴 | `details` 조건부 required | +| ----- | ----------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| SG-02 | `procedural_posture_relief_signals.schema.json` | `^sg02_…$` | `^generic_procedure$` | 없음 | +| SG-05 | `legal_relation_lifecycle_signals.schema.json` | `^sg05_…$` | `^legacy_liability_relation$` | `^domain_relation_atom$` | `legacy_liability_relation`일 때 `compatibility_key, compatibility_order, relation_component` | +| SG-07 | `asset_right_state_signals.schema.json` | `^sg07_…$` | `^generic_asset$` | 없음 | +| SG-11 | `calculation_requirements.schema.json` | `^sg11_…$` | `^calculation_requirement$` | 없음 | +| SG-13 | `legal_effect_routes.schema.json` | `^sg13_…$` | `^generic_route$` | `^legacy_legal_effect_route$` | `^legal_effect_route$` | `legacy_legal_effect_route`일 때 `compatibility_order, compatibility_route` | + +`$id`는 전부 `https://schemas.liti-agent.local/stage1/s5/.schema.json`. + +### 2.4 `signal_registry.v2.json` 선언 + +**경로**: `$BASE/Default_Agent/signals/signal_registry.v2.json` 최상위 6키: `compatibility_views, domain_envelope, entries, manifest_schema, schema_version("signal_registry.v2"), writer("compiler/transaction_writer.py")` `entries` = **13개 리스트** (SG-01 ~ SG-13). `compatibility_views` = `["actio_case_signals.json","case_liability_signals.json","legal_effect_signals.json"]` `manifest_schema` = `schemas/signal_manifest.schema.json` · `domain_envelope` = `schemas/domain_signal_envelope.schema.v2.json` + +entry 공통 키: `signal_id, file, schema, emitter, producer, input_sources, transform, lifecycle, downstream_consumers, admission_conditions{record_type_patterns, required_detail_keys, source_requirements}` + +| SG | file | emitter | transform | required_detail_keys | +| ----- | ---------------------------------------- | ----------------------- | ------------------------------- | -------------------- | +| SG-02 | `procedural_posture_relief_signals.json` | `emitters/emit_sg02.py` | `passthrough` | `[]` | +| SG-05 | `legal_relation_lifecycle_signals.json` | `emitters/emit_sg05.py` | `legacy_relation_decomposition` | `[]` | +| SG-07 | `asset_right_state_signals.json` | `emitters/emit_sg07.py` | `passthrough` | `[]` | +| SG-11 | `calculation_requirements.json` | `emitters/emit_sg11.py` | `calculation_merge` | `[]` | +| SG-13 | `legal_effect_routes.json` | `emitters/emit_sg13.py` | `legacy_route_wrap` | `[]` | + +5종 전부 공통: `producer: "central_emitter_runtime"`, `input_sources: ["signal_candidates","source_universe"]`, `lifecycle: "active"`, `downstream_consumers: ["Part3","Part4","Stage2"]`, `source_requirements: ["sealed_source_membership","meeting_only_hard_rule"]`. + +**수용 조건에 `required_detail_keys`가 5종 모두 빈 배열**이므로, Part 3가 `details.type_id`(SG-13)·`details.request_id`(SG-11) 같은 필드를 조인 키로 쓰려면 **registry가 보장해 주지 않는다** — Part 3 쪽에서 방어해야 함. + +참고 SG-01: `file: "domain_activation_manifest.json"`, `emitter: null`, `producer: "part1_activation_gate_adapter"`, `transform: "activation_validation"`, `input_sources: ["domain_activation_manifest"]`. + +------ + +## 3. Part 2 실산출 signal 실물 (`/tmp/dry_v6/`) + +### 3.1 `/tmp/dry_v6/signals/` 파일 20개 전량 + +| 파일 | 크기 B | 정체 | +| ------------------------------------------------- | ------ | --------------------------- | +| `signal_manifest.json` | 8,921 | 기술 index (아래 §3.2) | +| `domain_activation_manifest.json` | 34,686 | SG-01, 26 레코드 | +| `party_capacity_standing_signals.json` | 110 | SG-03, **빈 봉투** | +| `governing_law_version_signals.json` | 110 | SG-04, 빈 봉투 | +| `legal_relation_lifecycle_signals.json` | 3,994 | **SG-05, 6 레코드** | +| `timeline_notice_condition_signals.json` | 110 | SG-06, 빈 봉투 | +| `asset_right_state_signals.json` | 110 | **SG-07, 0 레코드 (EMPTY)** | +| `liability_causation_damage_signals.json` | 110 | SG-08, 빈 봉투 | +| `defense_exception_signals.json` | 110 | SG-09, 빈 봉투 | +| `evidence_proof_conflict_signals.json` | 110 | SG-10, 빈 봉투 | +| `calculation_requirements.json` | 3,349 | **SG-11, 5 레코드** | +| `remedy_enforcement_signals.json` | 110 | SG-12, 빈 봉투 | +| `legal_effect_routes.json` | 4,570 | **SG-13, 6 레코드** | +| `procedural_posture_relief_signals.json` | 110 | **SG-02, 0 레코드 (EMPTY)** | +| `domain_signals/X1.json` | 3,415 | 도메인 봉투, 2 레코드 | +| `domain_signals/X2.json` | 3,675 | 도메인 봉투, 2 레코드 | +| `domain_signals/X3.json` | 3,675 | 도메인 봉투, 2 레코드 | +| `compatibility_views/actio_case_signals.json` | 97 | 구 이름 투영, EMPTY | +| `compatibility_views/case_liability_signals.json` | 105 | 구 이름 투영, EMPTY | +| `compatibility_views/legal_effect_signals.json` | 103 | 구 이름 투영, EMPTY | + +**`domain_signals/.json` 형태는 존재한다** — 단 활성 도메인 3개(X1·X2·X3)만. 최상위 키는 `domain_signal_envelope` 하나이고, 그 안에 `calculation_refs, defense_candidates, dependency_refs, domain_id, domain_registry_version("Stage1.Assembly.2026-08-10.v1"), element_fact_candidates[{source_id, source_kind}], evidence_slot_status[{slot_id, status, review_code, evidence_refs, meeting_clause_refs, negative_or_conflicting_refs, source_fact_ids}], extensions, opposing_fact_candidates, registry_index_sha256, review_items[…]`. X1의 슬롯 7개(`x1.el01~el05`, `x1.op01~op02`)는 전부 `status: "missing"` + `review_code: "REPLAY_STUB_NO_FACT"`. `review_items`에 `REPLAY_STUB_NO_LEGACY_SEED`(생성기 = `validation_assets/replay/_build_domain_seed_v4_stub.py`). + +### 3.2 `signal_manifest.json` — `downstream_read_sets` 실제 내용 + +최상위 11키: `active_calculations, active_domains, compatibility_views, domain_signal_files, downstream_read_sets, files, schema_version("signal_manifest.v2"), status("READY_WITH_REVIEW"), transaction_guards, transaction_id("S5TX-18c97eb1f1ee2e117f0c"), writer("Task_C_BO_S0_canonical_signal_compiler")` + + + +```json +"downstream_read_sets": { + "part3_L0": ["SG-02","SG-05","SG-07","SG-11","SG-13"], + "part4_F0": ["SG-04","SG-06","SG-07","SG-10","SG-11"], + "stage2": ["ALL"] +} +``` + +→ Part 3가 읽으라고 선언된 집합 = 정확히 조사 대상 5종. **다만 그중 SG-02·SG-07은 실산출에서 0 레코드**다. + +기타: `active_domains: ["X1","X2","X3"]`, `active_calculations: []`, `unrouted_counts: {evidence:0, issues:0, operands:5}`, `transaction_guards: {canonical_writer_count:1, compatibility_views_are_projections:true, meeting_only_evidence_promotion_forbidden:true, negative_conflict_preservation_required:true, source_membership_required:true}`. + +`files[]` = **19 항목**(20 파일 중 `signal_manifest.json` 자신 제외). 항목 키: `path, file_sha256, hash_kind("raw_file_sha256"), kind(canonical|compatibility_view|domain_signal), lifecycle("active"), record_count, schema_version, state(empty|ready|review), writer`. ⚠ `files[].path`에 `signals/` 접두사가 없음(전략서 I-1 주의사항과 일치). + +state 분포: `empty` 12 · `review` 5(SG-11, SG-01 manifest, X1·X2·X3) · `ready` 2(SG-05, SG-13). + +### 3.3 SG-02·05·07·11·13 실물 레코드 (키만) + +레코드 15키는 5종 모두 `signal_item` 그대로 동일: `confidence, details, domain_ids, evidence_refs, issue_cluster_ids, law_version_refs, meeting_clause_refs, negative_or_conflicting_refs, proof_grade, record_type, review_required, signal_id, source_bo_ids, source_fact_ids, status`. 차이는 `record_type`과 `details` 키뿐: + +| SG | status | n | `record_type` (전건 동일) | `details` 키 | +| ----- | ------------------- | ---- | ------------------------- | ------------------------------------------------------------ | +| SG-02 | `EMPTY` | 0 | — | — (`{"records":[],"schema_version":"stage1_signal_sg02.v1","signal_id":"SG-02","status":"EMPTY"}`) | +| SG-05 | `READY` | 6 | `domain_relation_atom` | `source_id, source_kind, source_seed_id` | +| SG-07 | `EMPTY` | 0 | — | — | +| SG-11 | `READY_WITH_REVIEW` | 5 | `calculation_requirement` | `calculation_domain, completeness, review_code, source_refs` | +| SG-13 | `READY` | 6 | `legal_effect_route` | `registered, source_refs, source_seed_id, type_id` | + +- SG-13 record[0]: `signal_id: "sg13-2a0f9b89d6427860"`, `status: "inferred"`, `details: {registered: true, source_refs: ["E-001","E-002"], source_seed_id: "X2-001", type_id: "valuation"}`, `domain_ids: ["X2"]`, `proof_grade: "document_indirect"`. → **`details.type_id`가 존재하고 `registered: true`**. 전략서 §2.5의 "`types` 방언 `registered`를 SG-13 `details.registered`와 대조" 조인이 실물에서 가능. +- SG-11 record[0]: `signal_id: "calculation-request-0000"`, `details: {calculation_domain: "CE-03", completeness: "deferred", review_code: "REPLAY_STUB_NO_OPERAND", source_refs: []}`, `review_required: true`. ⚠ **`details.request_id`가 없다** — 전략서 I-4가 "`details.request_id`로 조인"이라 적었으나 실산출에는 그 키가 없고 `signal_id`가 그 역할을 함. +- SG-05 record[0]: `signal_id: "sg05-13dbc22c322d4f8a"`, `details: {source_id: "E-002", source_kind: "evidence", source_seed_id: "X2-001"}`. ⚠ 5종 모두 `source_bo_ids: []`, `issue_cluster_ids: []`이므로 I-7(BO 연결)은 실물에서 비어 있음. + +### 3.4 루트 별칭 3종 — `compatibility_views/`와 바이트 동일 + +| 파일 | 최상위 구조 | +| ----------------------------------------- | ------------------------------------------------------------ | +| `/tmp/dry_v6/actio_case_signals.json` | `{"schema_version":"actio_case_signals.v1","status":"EMPTY","actio_case_signals":[]}` | +| `/tmp/dry_v6/case_liability_signals.json` | `{"schema_version":"case_liability_signals.v1","status":"EMPTY","case_liability_signals":[]}` | +| `/tmp/dry_v6/legal_effect_signals.json` | `{"schema_version":"legal_effect_signals.v1","status":"EMPTY","bo_legal_effect_routes":[]}` | + +`signals/compatibility_views/` 아래 동명 3파일과 **내용 동일**. 3종 모두 최상위 3키이고 데이터 배열 이름이 파일마다 다름(특히 `legal_effect_signals.json`은 `bo_legal_effect_routes`) — 동결계약의 ingress 필드 `bo_legal_effect_routes[].candidate_structure_types`가 바로 이 배열이며 **현재 비어 있음**. + +### 3.5 `/tmp/dry_v6/BO.json` — 리스트, 레코드 2건, 18키 + + + +``` +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 +``` + +record[0] 실물 요점: + +- `BO_ID: "bh1"`, `BOType: "event"`, `Action: "event:general_legal_effect"` +- `Legal_Keywords: ["general_legal_effect","succession_notice_lien_asset_defense"]` ← **structure type_id가 여기 실려 있음** +- `core_field_base` 10키: `Performer, PerformerType, Action_proposal, Subject, Object, BehaviorTime, TimeText, TimePrecision, StatementType, Perspective` +- `provenance`: `{source_event_candidate_ids, source_meeting_clause_ids, source_domain: "X1"}` +- `downstream_seed_refs`: `{claim_group_seed_refs_proposed: [], canonical_theory_graph_seed_ref_proposed: null, legal_effect_structure_seed_ref_proposed: null}` ← **Part 3 구조 seed 자리가 예약돼 있으나 전부 null** +- `extensions`: `{"domain_payload": {}}` ← 동결계약 ingress의 `domain_legal_effect_tag`·`legal_effect_tags` 자리가 비어 있음 + +------ + +## 4. D-4 결선 — **원 결정 문서에는 없고, Part 3 전략서에서 결정안이 제시된 뒤 미승인 상태** + +### 4.1 사실관계 + +- `$BASE/S1-D-3_S2-D-6_decision.md`(222행)에는 **"D-4" 문자열이 0회**. 이 문서는 제목 그대로 D-3·D-6 전용이며 "상태: 확정". +- `$BASE/D1_D6_결선확정설명.md` §D4(63~~79행)는 **결정 설명이 아니라 미결 쟁점의 해설**이다. `$BASE/개정작업실행전_정지작업_정보.txt` 64~~75행에 동일 문단이 중복 존재. +- 실제 결정안은 **`$BASE/stage_1_part_3_개정방안전략서.md` §2 "D-4 결정안"**(136~215행)에 있고, 같은 문서 §7의 **P3-0a가 "D-4 결정(§2) 승인 … 승인 없이는 착수하지 않는다"**로 사용자 승인을 게이트로 걸어 두었다. `$BASE/8월17_18_작업.md` 1189행: *"실행 착수를 막고 있는 선행 조건이 둘입니다. **P3-0a**는 D-4 관련 네 건의 결정에 대한 대표님 승인이고 …"* + +### 4.2 원안(미결 상태)의 D-4 정의 — 원문 + +`$BASE/stage_1_update_strategy.md` 341행: + +> | D-4 | 효과 유형 범위(사건별 합집합 대 전역 열넷), `actio_remedy_or_cap` 매핑, 폴백 조항 정리 | Part 3·4 | + +`$BASE/stage_1_update_strategy.md` 282행: + +> 열거 범위는 D-4의 결정을 따른다. 사건별 합집합으로 좁히거나 전역 열넷으로 고정하거나 둘 중 하나이며, 이 선택이 색인 산출까지 좌우하므로 결정 전에는 착수하지 않는다. + +`$BASE/stage_1_workflow_update_v2.md` 146행: + +> | D-4 효과 유형 범위 | Part 3 열거를 사건별 합집합으로 할지 전역 14종으로 할지. 아울러 `actio_remedy_or_cap`의 모듈·역할 누락 1건과 일반 법률효과 폴백 조항을 함께 정리 | Part 3·4 | + +### 4.3 전략서 §2가 제시한 D-4 결정안 — 원문 인용 + +**(가) 범위 — `expected_runnable_domain_ids`에 대한 합집합** (§2.1) + +> ``` +> Θ(사건) = ⋃ { domain_config(d).structure_types[].type_id +> | d ∈ SG-01.expected_runnable_domain_ids } +> ``` +> +> **`active_domain_ids` 가 아니다.** D-3 은 넓은 정의로 확정됐다 … **이 선택이 중요한 이유** — SG-13 레코드는 `expected_runnable` 전체에 대해 돌린 worker 산출에서 나온다. Θ 를 `active_domain_ids` 로 좁히면 감시·상시 도메인이 낸 효과가 전부 Θ 밖으로 떨어져 **검토로 쓸려 들어가고, 커버리지 등식은 그래도 통과한다.** 조용히 틀리는 경로다. 전역 56종 고정을 쓰지 않는 이유는 셋이다. 비활성 도메인의 유형을 실으면 하류가 "이 사건에 없는 것"과 "이 사건에서 비어 있는 것"을 구별하지 못한다. 도메인이 늘면 색인이 사건과 무관하게 커진다. 그리고 137종 원칙이 요구하는 것은 사건 이름 없는 라우팅이지 전역 열거가 아니다 … + +같은 문서 §0 비교표 ① 및 §1.1: + +> | ① | 열거 범위는 D-4 결정 전 착수 금지 | **D-4 를 지금 결정한다** — `expected_runnable_domain_ids` 에 대한 합집합 | 선택지("사건별 합집합 대 전역 열넷")의 후자는 대상이 없다. 유형은 **56종**이다(§1.1) | +> +> ### 1.1 효과 유형은 열넷이 아니라 쉰여섯 +> +> 26개 `domain_config.json` 의 `structure_types` 전수 = **선언 73건 · 고유 `type_id` 56종**. D-4 의 "전역 열넷 고정"은 대상이 없어졌다. + +**(나) `actio_remedy_or_cap` 매핑** (§2.3, 전문) + +> `E-13` 이 rank 9 · `module: actio` · `role_projection: linked_actio_structures` 로 선언한다. **별도 매핑표를 만들지 않는다** — `s5_execution_contract.v2.json` 의 `e13_extension_domains` 가 선언한 확장 키 `actio_case_signals.v2` 를 `extension_key` 로 함께 싣는다. + +→ §1에서 실측 확인: E-13 `domain_config.json`이 정확히 그 3값을 선언하고 있음. 즉 D-4 둘째 갈래(누락)는 **registry 층에서는 이미 해소된 상태**이고 남은 것은 Part 4 상수지도의 registry 참조 전환. + +**(다) 폴백 조항 정리** (§2.5) + +> **폴백 없음.** `general_legal_effect` 는 보편이 아니므로(§1.2) 폴백 대상이 되지 못한다. 입력이 비면 `structure_count: 0` 을 기록한다. … 다만 10 도메인의 `default_projection` 은 무시하지 않는다. 그 값은 **침묵 폴백이 아니라 검토 꼬리표가 붙은 투영**이다(넷은 `{legal_effect, legal_effect_roles, review_code: UNREGISTERED_EFFECT_TYPE_REVIEW}`, 여섯은 `{unregistered_effect_review, unregistered_effect_roles, …}`). Θ 밖 유형을 만나면 이 투영을 싣되 **반드시 `UNREGISTERED_EFFECT_TYPE_REVIEW` 를 함께 붙인다.** + +비교표 ④: + +> | ④ | 일반 법률효과를 최후순위(11)로, 빈 입력은 그것으로 정규화 | **폴백 없음.** 구조 0 을 정상값으로 기록 | `general_legal_effect` 는 26 중 **10** 도메인만 선언하고 rank 11 은 최댓값(76)이 아니다(§1.2) | + +**(라) 부수 결정 2건** — §2.2 전순서 `order_key = (그 도메인이 선언한 priority_rank, registry_index.entries 위치, type_id)` · 도메인 간 병합 금지 / §2.4 `by_claim_form` 폐기 → `by_module`(34 버킷). + +### 4.4 D-2 · D-3 · D-6 확정값 (각 한 줄) + +- **D-2 (배포 기준점)** — `$BASE/D2_release_baseline_procedure.md` §2: *"봉인 완료 시점 `release_manifest.json`의 **sha256 한 값**이 배포 기준점이다."* 현 상태는 `program_release_status: STAGE1_NOT_RELEASE_READY` · `seal_approval: false` · blocker 2건(`PENDING_INDEPENDENT_ASSEMBLY_REVIEW`, `UNBOUND_S7_DEPLOYMENT_CONSUMER`)이며, 세 뿌리 인자 값은 **확정**(`Default_Agent/` · `/tmp/s1` · `/tmp/s1`). +- **D-3 (실행 대상 정의)** — `$BASE/S1-D-3_S2-D-6_decision.md` §0: *"실행 대상은 `execution_eligible is True`인 `domain_entries`의 집합이다(넓은 정의). `activation_status`는 서술 라벨이며 실행 판정에 쓰지 않는다."* (상태별: active·active_with_review → true / monitor·review·supporting → false / X1·X2·X3 무조건 true / E-00은 `unrouted_count > 0`일 때만 / EC-00 무조건 false) +- **D-6 (worker 상한)** — 같은 문서 §0: *"`execution_policy.max_instances`는 **선언 존치·강제 유예**. 도메인당 인스턴스 1개로 고정하고 상한을 강제하지 않는다."* (실측 분포 1×16 · 3×2(E-07·E-08) · 8×7(E-14·15·17~21) · 0×1(EC-00)) + +------ + +## 5. Part 3 몫으로 이월된 것 — `stage_1_part_1_and_2_updated_yaml_analysis.md` (822행) 내 "Part 3" 언급 **4곳 전부** + +| 행 | 절 | 원문 요지 | Part 3 몫 | +| ------- | -------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| **373** | §3.4 upstream–downstream 연결 (P2-S0 → Part 3·4·Stage 2) | *"현재 Part 3·4는 구 이름 3종(`actio_case_signals.json` 등)을 읽고 있고, **정본 집합으로의 이관은 Part 3·4 개정의 몫으로 남아 있다.** 그래서 S0이 루트 별칭 3종을 같은 바이트로 함께 쓴다"* | ① 구 이름 3종 읽기 → `downstream_read_sets.part3_L0` 5종으로 전환 | +| **486** | §4.4 선언표가 덮지 않는 산출 | `client_goal.json` — *"T1의 최종 산출이고 **Part 1 밖(Part 3 이후)에서 읽힌다** … 선언표 갱신 후보"* | ② `client_goal.json` 소비자로서 Part 3의 인계면 선언 등재 (선언표 갱신 후보, C-8) | +| **487** | §4.4 동 | `signals/` 아래 정본 signal 개별 파일(SG-02~SG-13 계열) — *"표에는 `signal_manifest.json`과 구 이름 호환 3종만 오른다 … **Part 3·4 이관이 끝나면 개별 정본도 선언 후보**"* | ③ 이관 완료 후 개별 정본 signal 파일의 선언표 등재 | +| **702** | §6.6 남은 것 · C. 이월 목록 | *"\| C-7 \| Part 3·4 를 정본 signal 집합(`downstream_read_sets`)으로 이관 \| **Part 3·4 개정 범위** \|"* | ④ C-7 = ①의 정식 이월 티켓 | + +즉 Part 3 개정에 이월된 것은 실질 **두 덩어리**: (a) **C-7 — 구 이름 3종 → 정본 5종 이관**(§3.4·§6.6가 같은 건), (b) **인계면 선언표 갱신 2종**(`client_goal.json` 등재 + 개별 정본 signal 등재). §6.6의 나머지 C 항목(C-1~~C-6, C-8~~C-10)은 Part 1·2 또는 registry 소유자 몫으로 명시돼 Part 3 범위가 아니다. + +------ + +## 6. Part 3용 기존 검증·회귀 자산 + +### 6.1 `compatibility/legacy_contracts/part3/` — **존재. 파일 1개** + +**경로**: `$BASE/선행구축/Default_Agent_Stage_1/compatibility/legacy_contracts/part3/structure_type_contract.json` (5,383 B) + +`legacy_contracts/` 하위 전량: `part2/r0_f0_contract.json` · **`part3/structure_type_contract.json`** · `part4/deterministic_gates_contract.json` · `part4/structure_projection_contract.json`. (그 외 `compatibility/legacy_alias_map.json`, `contracts/signals/compatibility_projection_contract.json`) + +내용 — Part 3 개정이 정면으로 상대할 동결 계약: + +- `schema_version: "s0_legacy_part3_structure_type_contract.v1"`, `status: "FROZEN"`, `frozen_on: "2026-08-08"` +- `source`: `YAML_Prompts/1. Stage_1/v.7/Claude_YAML/Stage_1_Part_3_Claude_v2.yml`, `sha256: 7213bbb1…d886e`, `source_lines: ["88-115","484-506","509-554"]` ← 원안이 말한 "우선순위 상수 88~101행 · 정본 집합 102행"의 출처 +- `structure_type_priority`: **11건**, 각 `{priority_rank, type_id, downstream_role_family}` (registry의 `role_projection`과 이름이 다름) +- `canonical_type_set`: **11종** (rank 1~11 그대로) +- `contract_rules`: `enum_closed: true` / `candidate_admission`(정규화 후 membership 통과분만 `candidate_structure_types`에 보존) / `noncanonical_preservation`(비-canonical은 `nonrouting_labels`에 보존) / **`fallback`: "빈 입력은 general_legal_effect로 정규화되며, 일반 문자열은 공백을 밑줄로 바꾼 뒤 membership을 검사한다."** ← D-4 셋째 갈래가 걷어내야 할 조항 실물 / `ordering` / `multiple_types` +- `ingress_contract`: `legal_effect_signal_field: "bo_legal_effect_routes[].candidate_structure_types"`, `case_liability_signal_field: "liability_candidate_type"`, **`actio_scope_map` 7키**(`fraudulent_act→fraudulent_transfer, preserved_claim→preserved_claim, remedy_or_cap→actio_remedy_or_cap, target_asset→actio_asset_target, beneficiary_or_transferee→fraudulent_transfer, encumbrance→secured_debt, defense→succession_notice_lien_asset_defense`) — ⚠ `actio_asset_target`은 `canonical_type_set`에도 registry 56종에도 **없음**, `domain_payload_fields: ["domain_legal_effect_tag","legal_effect_tags"]`, `legacy_source_domain_map: {B1→money_claim, B2→secured_debt, B3→land_use_gain, B4→fraudulent_transfer, B5→succession_notice_lien_asset_defense}` +- `normalization_keyword_groups`: 10 유형별 한/영 cue 배열(`general_legal_effect` 제외) + +### 6.2 `validation_assets/` — **structure 전용 기준선은 없다** + +`$BASE/선행구축/Default_Agent_Stage_1/validation_assets/` 3개 하위(`integration/`, `replay/`, `routing/`) 전수 확인 결과: + +- **"구조 59건 baseline"에 해당하는 파일은 없다.** 유일하게 실재하는 구조 수치는 **73**이며, `validation_assets/replay/dry_run_receipt.v1.json`의 `declaration_projection_totals_26_domains.structure_type_ids = 73`(검사 ID **R-f**: *"투영 합계 element 162 · opposing 85 · component 112 · defense 83 · **structure 73** · emits 267 · calc 53"*, `result: PASS`)와 §6.3 분석표가 같은 값. 전략서 §4.3·§9도 "구조 레코드 73건(= 26 도메인의 `structure_types` 선언 합)"을 목표 수치로 삼는다. +- **legacy 등가 계약은 seed/slice 층이지 structure 층이 아니다.** `routing/legacy_seed_equivalence_contract.json`의 `compared_fields`는 G1~~G5(`source_refs`, `evidence_refs`, `meeting_clause_refs`, `event_candidate_refs`, `bo_types`)뿐이고 **structure_type 비교 필드가 없다**. `routing/legacy_slice_equivalence_contract.json`도 F1~~F… source universe/selected sources 계열. +- `routing/legacy_baseline_manifest.json`: `status: FROZEN`, `baseline_count: 5` (B1~B5, 각 sha256+size), `policy: READ_ONLY_NEVER_RENAMED_NEVER_MOVED`, `produced_by: Stage_1_Part_2_Claude_v3.yml Task_C_BO_A0`. +- `routing/` 기타 회귀 자산: `case_kind_domain_matrix.v2.json`(72,969 B, 137 레코드), `routing_regression_contract.json`, `gate_regression_contract.json`, `expected_runnable_oracle.v1.json`, `metamorphic_probe_receipts.v2.json`, `test_oracles/`, `runtime_payloads/`, `negative_runtime_payloads/`, `schemas/` — 전부 **라우팅/활성화(D-3) 회귀용**이지 Part 3 구조용이 아니다. +- `replay/`: `_dry_run_stage1.py`, `_build_domain_seed_v4_stub.py`, `_build_screening_draft_stub.py`, `_promote_seed_v2_to_v3.py`, `dry_run_receipt.v1.json`, `replay_manifest.v1.json`. + +**결론: Part 3(구조·legal_effect) 전용 회귀 기준선은 아직 없다.** 전략서가 회귀 18건(L-a…L-p)과 "26 도메인 합성 실측(Θ 56종 · 구조 레코드 73건 · `by_module` 34 버킷)"을 **신규로** 만들라고 적은 이유가 이것. + +------ + +## 7. `ensemble_v2.md`의 structure/legal_effect/SG-13 관련 절 (228행) + +관련 언급은 **4곳**: + +- **39행 (채택 2 — 폴백에 의한 침묵 커버 금지)**: E-00을 잔여 수집·미분류 검출기로 재정의하며, 세 조치 중 ③이 Part 3 직결 — *"③ **Part 3에서 미등록 효과 유형을 `general_legal_effect`로 침묵 강등하는 경로를 금지하고 `unregistered_effect_type` review로 보낸다.** '커버'의 정의가 '흡수'에서 '가시화'로 바뀌는 것이 핵심이다."* ← D-4 셋째 갈래의 상위 근거. +- **45행 (채택 5 — SG-05·SG-08 코어 편입)**: *"SG-05는 b의 legal_relation과 contract_obligation_lifecycle을 record subtype으로 통합한 1파일로 하고, **초기 구현은 BO·legal_effect_routes로부터의 결정론 파생으로 시작**하여 중복 writer 없이 계약을 안정화한 뒤 **Part 3 그래프 전환 시 정식 승격**한다."* ← SG-05가 지금 파생 신호이고 Part 3 전환이 승격 시점임을 명시. +- **184행 (SG 목록표 SG-13 행)**: *"| SG-13 | `legal_effect_routes.json` **(v2 정식 편입)** | BO·사실이 생성·변경·소멸시키는 권리·의무 후보와 **Part 3 라우팅** (다중 route, 미등록 유형은 `unregistered_effect_type` review) | 현행 `legal_effect_signals`의 canonical 후계를 코어 목록에 정식 등재 |"* +- **202·204행 (구 signal 처리 / 매니페스트 성격)**: *"`actio_case_signals.json` → `domain_signals/E-13.json`의 extensions로 이전. `case_liability_signals.json` → SG-05·SG-08로 분해(BO당 1행 강제 폐지). **`legal_effect_signals.json` → SG-13으로 승계.**"* / *"`signal_manifest.json`은 signal이 아닌 기술 index다: 파일 목록·schema version·record 수·sha256·writer task·활성 도메인/계산 목록·empty/blocked/review 상태·미분류 잔량·**downstream read set. Part 3·4와 Stage 2의 진입 계약.**"* + +`ensemble_v2.md`에는 `structure_types`·`priority_rank`·`by_module`에 대한 직접 규정은 없다 — 구조 유형 층은 registry(`domain_config.json`)와 동결계약이 전담하고, ensemble_v2는 signal 층만 규정한다. + +------ + +## 8. Part 3 개정 착수 시 실물이 드러낸 불일치 4건 + +1. **사이드카 미배포** — 전략서 I-10이 요구하는 `structure_types.json`·`module_role_projection.json` ×25는 배포 트리 `$BASE/Default_Agent/domains/`에 0건이며 조립본에만 있음. `domain_config.json`의 `effect_projection.schema_ref` 경로도 배포 트리에서 dangling. (§1.7) +2. **SG-11 `details.request_id` 부재** — 전략서 I-4의 조인 키가 실산출에 없고 `signal_id`가 대체 (§3.3). registry의 `required_detail_keys: []`가 이를 강제하지 않음. +3. **Part 3 입력 5종 중 2종이 실측 0 레코드** — SG-02·SG-07이 `EMPTY`이고 생산자가 없음(전략서 §1.6이 이미 "조인 코드를 지금 쓰지 않는다"로 처리). SG-05·SG-13은 `source_bo_ids: []`이라 I-7(BO 연결)이 실물에서 끊겨 있음. (§3.3) +4. **동결계약의 `actio_asset_target`** — `ingress_contract.actio_scope_map`이 매핑하는 이 값이 `canonical_type_set` 11종에도 registry 56종에도 없음 (§6.1). D-4 (나)/(다) 정리 시 함께 처리 대상. \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/stage_1_part_2_실행실패원인기록.md b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/stage_1_part_2_실행실패원인기록.md new file mode 100644 index 00000000..afdb9e84 --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.7/extension_research/stage_1_part_2_실행실패원인기록.md @@ -0,0 +1,101 @@ +전략서를 문서로만 남기지 않고 실제로 실행해 완료했습니다. 결론부터: **stage_1_part_2_v.8.yml** **실행을 막던** + + **유일한 원인이 해소됐고, 조립·검증·봉인 스모크가 전부 통과했습니다.** + + + + **무엇을 만들었는가** + + + + Default_Agent/ 아래에 자산 5건을 신설했습니다. + +![image-20260821022239347](/Users/allen_jhang/Library/Application%20Support/typora-user-images/image-20260821022239347.png) + + runtime_manifest.json 은 카탈로그 1건만 등재하고 항목 수를 432에서 433으로 올렸습니다. overlay 와 SLP + + 인덱스는 등재하지 않았는데, 현행 장부가 도메인 설정 파일과 도메인 프롬프트를 원래 싣지 않는 관례와 맞추기 + + 위해서입니다. + + + + 세 profile 은 동일한 4절 골격(식별 단서 / 고유 요건 슬롯·증거 component / review code 발행 규칙 / SG-04 + + 위임 경계)으로 썼고, 법령 조문 번호·요율·기간 상수는 검색 결과 0건입니다. + + + + **작성 중 발견한 함정 하나** + + + + routing/extension_payload_key_declarations.v1.json 을 확인해 보니 special_law_profile_candidates 라는 + + 출력 항목을 선언하는 도메인은 **E-06 하나뿐**이었습니다. E-20 과 E-21 은 선언하지 않는데 세 도메인 모두 + + 미선언 항목을 금지합니다. 세 profile 에 같은 문장을 넣었다면 두 도메인의 산출물이 스키마 위반이 됐을 + + 것이므로, 각 파일이 그 도메인에 실제로 선언된 항목만 지목하도록 썼습니다. + + + + **검증 결과** + + + + \- **조립**: E-06·E-20·E-21 모두 정상 종료(rc=0), profile 조각이 제 위치에 1건씩 붙고 중복 탈락 0건, 필수 마커 + + 검증 통과. 특별법을 선언하지 않는 E-05 도 회귀 대조로 정상 종료. + + \- **참조 무결성**: 카탈로그를 주입하면 세 도메인 모두 누락 0으로 닫히고 경고가 54개에서 51개로 줄었습니다. + + \- **봉인**: SLP 인덱스 3건 재계산 불일치 0, manifest 전수 재계산 일치 156·불일치 0·파일없음 277(직전 회차 + + 155/0/277에 카탈로그 1건이 더해진 값과 정확히 일치). + + + + **하지 못한 것 두 가지** + + + + **절단 문자열 조사**는 이 컴퓨터에서 불가능합니다. 조사 대상 경로인 /Users/jsahn 자체가 존재하지 않습니다. + + 실행 워크스페이스를 가진 쪽에서만 확인할 수 있습니다. + + + + **레지스트리 인덱스 값 채우기는 일부러 보류**했습니다. 실행 경로에서 읽히지 않아 이득이 없는 반면, 실행 + + 워크스페이스의 인덱스 검증 스키마가 slice 스키마와 같은 소문자 패턴 결함을 갖고 있으면 지금 통과 중인 + + 검증이 새로 깨질 수 있습니다. 그 스키마를 확인한 뒤 처리하는 편이 안전합니다. + + + + 별건으로 넘긴 항목도 기록해 두었습니다. 참조본 A0가 --reference-catalog profile= 로 쓰는 표기는 실제로 + + 배선되지 않아 special_law_profiles= 여야 하고(실측 확인), A0가 요구하는 자산 중 3종이 아직 릴리스에 + + 없습니다. + + + + **문서** + + + + \- extension_research/assets_special_law_problem_v.2.md — 검증 리포트에 실행 후속 상태를 부기 + + \- extension_research/special_law_profile_update.md — 전략서에 §4 실행 결과(자산·sha·검증·별건·보류 + + 사유·다음 행동) 추가 + + \- MEMORY.md — 검증·전략·실행 3연속을 한 항목으로 압축 기록 + + + + 다음은 신설 파일 5개와 갱신된 manifest 를 실행 워크스페이스에 반영한 뒤 stage_1_part_2_v.8.yml 을 + + 재실행하는 것인데, 그 워크스페이스는 이 컴퓨터에서 접근할 수 없어 제가 진행할 수 없습니다. \ No newline at end of file diff --git a/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.8/stage_1_part_1_v.8.yml b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.8/stage_1_part_1_v.8.yml new file mode 100644 index 00000000..5adb6eaf --- /dev/null +++ b/Case_02_Comparison_Research/YAML_Prompts/1. Stage_1/v.8/stage_1_part_1_v.8.yml @@ -0,0 +1,6667 @@ +# ============================================================================= +# Liti-agent Stage 1 Part 1 v.8 — 증거 기반 · 목표 · 도메인 확정 (통합 실행본) +# +# 정본 근거 +# 전체 DAG : stage_1_update_strategy.md §1 Part 1 블록 +# 개정 명세 : P1-T2_to_T9_update_strategy.md §1~§4 +# 조립 명세 : stage_1_part_1_완성용_작업명세서.md §3 (16 슬라이스) +# 설계 판정 : P1-T2_to_T9_선결작업수행결과/01_설계판정/P0_설계판정_결정문서.md +# 계측 재정의 : 같은 폴더 02_계측재정의/P2_계측통로_재정의.md +# +# 구성 — task 13종 (v3 대비 +4 신설 · 3 개정 · 6 무변경) +# [신설] Task_A0_domain_screener_01 / 02 / 03 ver_8 후보본 +# [개정] Task_A_client_goal Task_A_client_goal_v4.yml +# [무변경] Task_Evidence_shard_planner v3 622-945 +# [개정] Task_B1_map_doc_* T3_Task_B1_map_doc_v4.yml +# [무변경] Task_B2_map_events_* v3 1540-2204 +# [무변경] Task_B1_quality_gate_evidence_indexed v3 2205-2802 +# [무변경] Task_B2_quality_gate_event_candidates v3 2803-3519 +# [무변경] Task_B12_gate_llm_adjudicator v3 3520-3679 +# [무변경] Task_B12_gate_audit_finalizer v3 3680-4041 +# [신설] Task_D0_domain_activation_gate ver_8 후보본 (정정본) +# [개정] Task_B2_SHA256_soft_gate_handoff_writer T9_..._v4.yml +# +# 개정 요지 +# T0 3종 스크리너 사슬 신설. 02 가 01 의 초안을 봉인하고 03 이 후보 어휘를 압축한다. +# T1 domain_profiles 를 도메인ID 키 개방형으로. 스키마는 정적 자산으로 분리. +# T3 증거 구성요소 어휘를 registry 합집합 참조로. v3 구조 어휘와 병존(P0 판정 A)하며 +# part 파일에 registry_component_ids 슬롯 1개를 additive 로 더한다(P0 판정 B). +# schema_contract_version 은 evidence_indexed_part.v4 유지 — 순수 additive 다. +# D0 판정 로직을 YAML 에 쓰지 않는다. stage1_runtime/activation_gate.py 를 반입해 호출하고 +# signals/adapters/sg01_activation_adapter.py 로 자기검증한다. 결정 필드는 +# execution_eligible 하나뿐이다(D-3 넓은 정의). +# T9 봉인 사전에 세 해시 추가(screening / activation_manifest / registry_index). +# 원문 sha256 이므로 read_raw 를 쓴다. unwrap_text 재직렬화 금지. +# schema_version 은 stage1_part1_soft_gate_handoff.v1 유지 — 역시 additive 다. +# +# 모듈 반입 규약 (localdocs 가 .py 를 서빙하지 않아 도입한 우회책) +# 모든 .py 에는 같은 디렉터리에 바이트 동일한 .txt 형제 미러가 배포된다. +# 빌더 규칙이며 예외는 build_default_agent_stage1.py 하나다. 미러는 정본 옆에 놓이므로 +# sg01_activation_adapter 의 미러는 signals/adapters/ 에 있다(stage1_runtime/ 이 아니다). +# 반입 시 sha256 을 Default_Agent/runtime_manifest.json 과 대조하고, 불일치면 실행하지 않는다. +# +# 정적 자산 선행 조건 (P1-T2_to_T9_update_strategy.md §0.2 — 전부 충족) +# Default_Agent/stage1_runtime/activation_gate.py +# Default_Agent/platform/schemas/client_goal_domain_profiles.schema.json +# Default_Agent/routing/activation_cue_digest.md +# Default_Agent/routing/evidence_component_union.md +# Default_Agent/platform/contracts/determinism_scope_contract.json +# +# 이 파일은 스테이지 하나다. 스테이지 선언 1벌 · task_procedure 1벌 · tasks 1벌. +# 조각 후보본들이 각자 갖고 있던 Agent/Stages 머리와 자기 task_procedure 는 전부 걷어냈다. +# mcpServers 합집합은 {localdocs, code-executor} 이며 v3 머리가 이미 둘 다 선언한다. +# ============================================================================= +Agent: + name: Liti-agent_Civil_Suit_Plaintiff_Stage_1_Part_1 + description: 민사소송 원고 송무 초지능 AI변호사 - Stage 1 Part 1 기초작업 + version: v.2 + + Stages: + - name: stage1_사건개요파악_기초작업 + description: 사건개요 파악, 기초정보 추출 + llm_provider: openai + llm_model: gpt-4o-2024-08-06 + tools: + mcpServers: + localdocs: + type: streamable-http + url: "http://mcp-localdocs:8012/mcp" + description: Get the content of local documents + code-executor: + type: streamable-http + url: https://code-executor.mcp.eroomai.com/mcp + description: Run scripts of programming languages + headers: + Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM= + + tasks: + - task_name: Task_A0_domain_screener_01 + llm_provider: google + llm_model: 'gemini-3.1-flash-lite' + llm_reasoning: high + llm_verbosity: low + cache_control: + mode: auto + ttl: 1h + use_tools: + - localdocs + # preflight: true + preflight_files 화이트리스트. + # 화이트리스트를 명시하면 프롬프트 본문 regex 스캔이 꺼지고 이 두 경로만 + # 미리 읽힌다. 입출력 통제는 유지되면서, 정적 경로인 단어 사전이 캐시 + # 공유 영역에 inline 되어 매 실행 35KB 를 다시 태우지 않는다. + # 도구 오용 방지는 프롬프트의 / 이 맡는다. + preflight: true + preflight_files: + - Default_Agent/routing/activation_cue_digest.md + - client_meeting.md + prompts: + - role: user + content: |- + + + - You are executing one LLM sub-task inside Stage 1 of a Korean civil-litigation complaint-generation pipeline. + - The current task's static block, role overlay, assigned inputs, output schema, and writer boundary control. + - This common prefix cannot expand the current task's input set, output set, legal domain, validation authority, writer authority, or reasoning depth. + - If any common rule appears broader than the current task, apply only the narrower current-task version. + - Stage 1 prepares verified structured artifacts. Do not draft complaint prose or final counsel-level conclusions unless the current task explicitly authorizes a validation or gate conclusion. + + + + - Use only assigned files, provided context inputs, prior outputs, and allowed tools. + - Do not import facts, law, procedural history, parties, dates, amounts, IDs, document contents, or source meanings from memory, outside knowledge, or unassigned files. + - Treat prior outputs as authority only to the extent the current task names them or provides them as context. + - If a value is unsupported, missing, conflicting, stale, or out of scope, use only the current schema's allowed null, empty, unknown, warning, blocked, or needs_review path. + + + + - Preserve exact source identifiers required by the current schema. + - Maintain separation among raw fact, inferred fact, legal signal, evidence support, fact support, validation issue, and final gate decision when the current schema distinguishes them. + - Do not upgrade meeting-only or indirect material into direct proof. + - Do not silently resolve material conflicts. If the current schema has a conflict or uncertainty field, use it; otherwise stay within the task's allowed warning or review path. + + + + - Follow required JSON shape, key names, enum values, ordering, file names, and status strings exactly. + - Do not add arbitrary keys, prose, markdown fences, alternative files, unauthorized repair, or explanatory material outside allowed fields. + - Create, mutate, normalize, merge, or finalize IDs only when the current task explicitly authorizes it. + - Write final files only when the current task is the authorized writer. Validators and guards report issues in their own authorized schema and do not silently repair unless instructed. + + + + - Prefer the current prompt and schema, assigned structured upstream artifacts, compact indexes, ledgers, manifests, bundles, and gates. + - Read raw evidence or meeting text only when the current task requires direct provenance, ambiguity resolution, or a schema-required value missing from structured artifacts. + - For map or projection tasks, process only the assigned item, domain, or batch. Reducers aggregate only the inputs assigned to them. + - Do not restate, summarize, cite, or copy this common prefix in any output. + + + + - Return only the requested structured artifact, concise allowed rationale fields, validation notes, or status object. + - Keep chain-of-thought private. + - Stop when the current schema is complete and safe. + + + + + - Fill only slots that the current extraction, mapping, support, or projection schema asks for. + - Treat legal labels, party assertions, meeting narrative, and upstream candidates as routing signals only, not proof by themselves. + - Preserve the strongest source IDs and locators required by the current schema; do not quote long passages unless asked. + - Separate raw fact, inferred fact, support signal, and review note when the schema distinguishes them. + - If a source conflict is visible, preserve it only through the current schema's allowed uncertainty, warning, or review field. Do not classify full contradiction severity here. + - Do not perform norm search, final legal-effect classification, merits judgment, cross-domain merge, or final writer repair unless the current task explicitly requires it. + - Stop after the assigned item, evidence shard, domain, batch, support file, or fact-support projection is complete. + + + + + Task_A0_domain_screener_01 + + You are an MCP-enabled LLM agent assisting plaintiff-side Korean civil/commercial litigation counsel. + + + `client_meeting.md`와 `Default_Agent/routing/activation_cue_digest.md` 두 개만 읽고 `routing/domain_screening_draft.json`을 생성한다. + 이 task가 답해야 할 질문은 "이 사건이 137종 사건유형 중 무엇인가"가 아니라 "registry의 26개 법리 렌즈 중 어느 것이 켜지는가"이다. + 산출물은 확정이 아니라 가설이다. 확정은 하류의 `Task_D0_domain_activation_gate`가 registry 모듈을 호출하여 결정론적으로 수행한다. + 본 task는 판단 4종(`status`, `candidates`, `unmatched_issue_refs`, `review_items`)과 그 판단을 검증 가능하게 만드는 최소 부속 정보만 생성한다. + 닫힌 스키마 `stage1_domain_screening.v1`의 비판단 6종(`schema_version`, `registry_version`, `registry_index_sha256`, `source_document_refs`, `screening_policy`, `contract_guards`)은 본 task가 만들지 않는다. 후속 task `Task_A0_domain_screener_02`가 registry에서 직접 읽어 붙이고 스키마 검증까지 수행한다. + 본 task의 산출물은 개방형 초안이므로 닫힌 스키마의 필드 집합에 구속되지 않는다. 다만 `candidates` 배열의 각 원소는 닫힌 스키마의 후보 객체와 키 구성이 정확히 같아야 한다. 후속 task가 변형 없이 그대로 복사하기 때문이다. + + + + + - In this task, `provisional_status` and `confidence` are meeting-grounded routing hypotheses, not legal conclusions. + - The EXTRACTION overlay's prohibition on final legal-effect classification does not prohibit the schema-authorized `provisional_status`, `confidence`, or `requested_calculation_domains` fields. + - The overlay's prohibition on norm search remains absolute here. 법령·판례·조문 검색과 인용은 어떤 형태로도 금지한다. + - 본 task는 final writer가 아니다. `FINAL_WRITER_REDUCER` 오버레이는 본 task에 적용되지 않으며, 어떤 최종 산출물도 확정하지 않는다. + + + + + - `client_meeting.md` — 유일한 사실 원천 + - `Default_Agent/routing/activation_cue_digest.md` — 단어 사전(라우팅 어휘). 사실 원천이 아니다. + + + - `routing/domain_screening_draft.json` + + + - `list_docs` : 입력 2종의 존재 확인과 출력 1종의 생성 확인에만 쓴다. + - `write_file` : `routing/domain_screening_draft.json` 한 경로에만 1회 쓴다. + - `read_docs` : 원칙적으로 쓰지 않는다. 두 입력은 preflight로 이미 제공되어 있다. preflight 내용이 제공되지 않은 경우에 한하여 위 두 경로에만 1회 쓴다. + + + - 위 화이트리스트 밖의 어떤 파일도 읽지 않는다. 특히 `Default_Agent/domains/*/domain_config.json` 원본 26개를 직접 읽지 않는다. + - `Default_Agent/validation_assets/routing/case_kind_domain_matrix.v2.json`을 읽지 않는다. 사건유형 이름을 담은 오프라인 회귀 전용 자산이며 런타임 입력이 금지되어 있다. + - `routing/domain_screening.json`을 만들거나 건드리지 않는다. 그 파일은 후속 task의 산출물이다. + - 중간 파일 생성, 산출물 재읽기, `write_file` 2회 이상 호출. + - 사건유형 명칭, 당사자 이름, 사건번호를 라우팅 판단의 근거로 삼기. + - 최종 법률효과 판단, 단일 도메인 확정, 승패 예측, 청구금액 산정, 법령·판례 검색. + - 닫힌 스키마에 없는 필드명 사용: `track_hypothesis`, `domain_hypotheses`, `relief_mode_hints`, `computation_hints`, `matched_cues`, `notes_kr`. + - 비판단 6종(`schema_version`, `registry_version`, `registry_index_sha256`, `source_document_refs`, `screening_policy`, `contract_guards`)을 초안에 넣기. + - 단어 사전에만 있고 상담록에 없는 표현을 사실처럼 근거로 삼기. + + + + + 1. `list_docs`로 `client_meeting.md`와 `Default_Agent/routing/activation_cue_digest.md`의 존재만 확인한다. + 2. preflight로 제공된 두 파일의 내용을 쓴다. 제공되지 않은 경우에 한하여 `read_docs`를 1회 호출한다. + 3. ``에 따라 상담록을 source unit으로 나누고 각 unit에 `MTG.*` 식별자를 부여한다. + 4. ``로 사전을 해석한다. + 5. `` → `` → `` → `` → `` 순서로 판단 4종을 만든다. + 6. ``를 적용하고 ``를 자기점검한다. + 7. 메모리에서 초안 객체를 만들고 `write_file(overwrite=true)`로 `routing/domain_screening_draft.json`에 저장한다. + 8. `list_docs`로 `routing/domain_screening_draft.json` 존재만 확인한다. 절대 읽지 않는다. + 9. 정확히 `"도메인 스크리닝 초안 생성 완료"`만 출력하고 종료한다. + + + + - 필수 입력 누락, 빈 파일, 파싱 불가: 즉시 중단하고 채팅으로만 보고한다. 파일은 쓰지 않는다. + - 단어 사전의 머리말에 `registry_version`이 없거나 `## DOMAINS` 아래 도메인 절이 26개가 아니면 중단하고 `FAILED: activation_cue_digest`만 보고한다. + - tool 오류: 원인 확인 후 1회만 재시도한다. 재실패 시 `FAILED: `만 보고하고 종료한다. + - 판단이 어려운 사안을 억지로 확정하지 않는다. 판단 불가는 실패가 아니라 `review` 상태와 검토 항목으로 표현한다. + + + + - 사실 원천은 `client_meeting.md` 하나뿐이다. + - `Default_Agent/routing/activation_cue_digest.md`는 사실 원천이 아니다. 무엇을 찾을지 알려주는 라우팅 어휘 목록이다. 사전의 `match_value` 문구를 상담록의 대응 서술 없이 근거로 인용하면 그것은 환각이다. + - 상담록의 문장들을 조합해 새로운 사실을 만들지 않는다. 상대방 주장은 주장으로만, 희망사항은 희망으로만 취급한다. + - 상담록에만 있는 내용은 증거가 아니다. 증거 승격은 하류 단계의 권한이며 본 task는 그 판단을 하지 않는다. + - 사건유형 이름을 사실 권위로 쓰지 않는다. 라우팅은 구조화된 단서와 서술로만 한다. + + + + - 사전은 머리말, `## LEGEND`, `## GLOBAL_DEFAULTS`, `## USAGE`, `## DOMAINS` 다섯 부분이다. 판단에 쓰는 것은 `GLOBAL_DEFAULTS`와 `DOMAINS`이다. + - 도메인 절 머리 형식: `### | | kind= | ao=<0|1> | dep= | calc=` + - 단서 줄 형식: + `P|||||||` + `N|||||||` + `C|||` + `OVR|=|...` + - `` 코드는 두 개뿐이다. `DR`=confidence_downrank_only, `MR`=monitor_review. + - `` 코드 `SP`/`SU`는 하류 단계의 조회 위치 표시다. 본 task는 상담록만 보므로 `src` 값을 이유로 단서를 배제하지 않는다. + - `OVR` 줄이 있으면 그 도메인에 한하여 `GLOBAL_DEFAULTS`를 덮어쓴다. 값 `absent`는 원본에 그 키가 없다는 뜻이므로 그 임계값 판정을 적용하지 않는다. + - 모든 충돌 단서의 효과는 `hard_review`이며 그래서 `C` 줄에서 생략되어 있다. + - 사전에 없는 도메인 ID, 단서 ID, 검토 코드, 계산 ID는 만들지 않는다. + + + + - 상담록을 문단 단위 source unit으로 나누고 각 unit에 다음 형식의 식별자를 부여한다. + `MTG.
.<사건군 2자리>.<항 2자리>` + - `
`은 세 값만 쓴다. 【상담내용】=`FACT`, 【사건관계인의 주장】=`OPP`, 【의뢰인의 희망사항】=`WISH`. + - `<사건군 2자리>`는 그 섹션 안의 `1.`~`9.` 번호를 두 자리로 채운 값이다. 사건군에 속하지 않는 문단은 `00`. + - `<항 2자리>`는 `가.`=`01`, `나.`=`02`, `다.`=`03`, `라.`=`04`, `마.`=`05`, `바.`=`06`, `사.`=`07`, `아.`=`08` 순서다. 항 구분이 없는 문단은 `00`. + - 상담일지 머리표(사무소·연락처·접수번호·상담일자·의뢰인 표)는 문단마다 자르지 않고 `MTG.META.01` 한 단위로 접는다. 상담일자와 의뢰인 값은 그 안에 보존된 것으로 본다. + - 예시: 【상담내용】 `2.` 사건군의 `라.` 문단 → `MTG.FACT.02.04`. 【사건관계인의 주장】 `5.`의 항 구분 없는 문단 → `MTG.OPP.05.00`. 【의뢰인의 희망사항】 `3.` → `MTG.WISH.03.00`. 【상담내용】의 사건군 앞 머리 문단 → `MTG.FACT.00.00`. + - 식별자는 반드시 `^[A-Za-z][A-Za-z0-9_.:/-]{0,255}$`를 만족해야 한다. 한글, 공백, 대괄호, `#`을 쓰지 않는다. + - 어떤 문단도 버리지 않는다. 위 규칙에 맞지 않는 문단은 `MTG.RAW.<3자리 일련번호>`로 부여하고 `review_items`에 `SCREENING_SOURCE_UNIT_FALLBACK`를 남긴다. + - 부여한 전체 목록을 `source_unit_index`에 문서 등장 순서대로 기록한다. 이 목록이 본 task가 살펴본 원천의 전부이며, 후속 task가 같은 규칙으로 독립 재산출하여 대조한다. + - 후속 task가 `source_document_refs`를 만든다. 본 task는 그 필드를 만들지 않는다. + + + + - 각 source unit을 읽고 사전의 긍정 단서 `match_value`에 대응하는 서술이 그 unit에 있는지 판단한다. + - 대응이 인정되면 그 도메인 후보의 `positive_refs`에 해당 unit 식별자를 넣는다. 단서 ID(`E13-P01` 같은 값)는 `positive_refs`에 넣지 않는다. 그 배열은 원천 식별자만 담는다. + - 어떤 단서가 근거였는지는 `reasoning_summary`에 단서 ID로 적는다. + - 단서 하나에만 걸린 상태로 후보를 세우지 않는다(`single_keyword_activation_forbidden`). 서로 다른 단서 두 개 이상 또는 서로 다른 source unit 두 개 이상이 필요하다. + - `cue_type`이 `evidence_component`인 단서는 그 자료의 존재가 상담록에 직접 언급된 경우에만 인정한다. 있을 법하다는 추론으로 인정하지 않는다. + - 한 unit이 여러 도메인에 동시에 걸릴 수 있다. 배타적으로 하나만 고르지 않는다. + - 사건유형 명칭, 당사자 이름, 사건번호는 어떤 경우에도 매칭 근거가 되지 않는다. + + + + - 도메인별로 걸린 긍정 단서의 `weight`를 합산한다. 같은 단서가 여러 unit에서 걸려도 1회만 계산한다. + - 합이 `leaf_activation_threshold` 이상이면 `candidate`, `monitor_threshold` 이상이면 `monitor`로 본다. 기본값은 각각 1.0과 0.5이며 `OVR` 줄이 있으면 그 값을 쓴다. + - 임계값은 registry가 정한 참고값이며 판정 권한이 없다. 임계값에 못 미쳐도 강한 서술이 있으면 `monitor` 또는 `review`로 보존한다. 임계값을 넘었다는 사실만으로 `candidate`를 확정하지도 않는다. + - 네 상태의 뜻은 다음과 같다. + `candidate` — 이 도메인이 독자 쟁점으로 켜질 만한 서술이 있다. + `supporting` — 독자 쟁점은 아니나 다른 도메인의 요건을 뒷받침한다. 다른 도메인의 `dep=`에 등장하는 선행 도메인이 주로 여기에 온다. + `monitor` — 가능성은 있으나 서술이 얇다. 삭제하지 않고 감시한다. + `review` — 판단이 갈리거나 충돌 단서가 걸렸다. + - `confidence`는 `high|medium|low` 세 값이다. 강한 단서 두 개 이상이면 `high`, 강한 단서 하나이거나 보조 단서 위주이면 `medium`, 부정 단서로 낮춰졌거나 서술이 얇으면 `low`로 본다. + - `ao=1`인 도메인(E-00, X1, X2, X3)은 상시 활성 대상이다. 단서가 걸리지 않아도 후보 목록에서 누락시키지 않으며, 대응 서술이 없으면 `monitor`와 `low`로 남긴다. + - `reasoning_summary`는 1자 이상 1200자 이하다. 어떤 단서 ID가 어느 unit에서 걸렸는지와 상태를 그렇게 정한 이유만 적는다. 법률 결론, 승패 전망, 조문 인용을 적지 않는다. + - `final_determination_forbidden`은 모든 후보에서 `true`이다. + + + + - 부정 단서가 걸리면 해당 unit 식별자를 `negative_refs`에 넣는다. + - `DR`은 `confidence`만 한 단계 낮춘다. `high`→`medium`, `medium`→`low`, `low`는 그대로 둔다. + - `MR`은 `provisional_status`를 `monitor`로 옮긴다. + - 어느 경우에도 후보를 목록에서 삭제하지 않고, `provisional_status`를 `candidate`에서 두 단계 넘게 내리지 않는다(`candidate_downgrade_forbidden`). + - 충돌 단서가 걸리면 해당 unit 식별자를 `conflict_refs`에 넣고, 그 `C` 줄의 `review_code`를 그대로 써서 `review_items`에 항목을 추가한다. 검토 코드를 새로 만들지 않는다. + - 충돌 단서가 걸린 후보의 `provisional_status`는 `review`로 하고 `confidence`는 `low`를 넘지 않는다. + - 충돌을 조용히 해소하지 않는다. 어느 쪽이 맞는지 고르지 않고 양쪽 근거를 모두 남긴다. + + + + - `source_unit_index`의 unit 중 어떤 후보의 `positive_refs`·`negative_refs`·`conflict_refs`에도 들어가지 않은 것을 모두 `unmatched_issue_refs`에 넣는다. 하나도 버리지 않는다. + - `unmatched_issue_refs`가 비어 있지 않으면 `review_items`에 `UNROUTED_ISSUE_REVIEW` 항목을 하나 만들고 그 unit들을 `source_refs`에 담는다. `severity`는 `review`, `domain_ids`는 `["E-00"]`로 한다. + - `review_items` 각 항목의 필수 키는 `review_code`, `severity`, `reason`, `source_refs` 넷이다. 선택 키는 `domain_ids`, `unresolved_type`, `blocks_final_drafting` 셋이다. 다른 키를 만들지 않는다. + - `review_code`는 `^[A-Z][A-Z0-9_]{2,127}$`를 만족해야 하고, 사전의 `C` 줄에 적힌 코드이거나 `UNROUTED_ISSUE_REVIEW` 또는 `SCREENING_SOURCE_UNIT_FALLBACK` 중 하나여야 한다. + - `severity`는 `info|review|hard_warning|block` 네 값이다. 본 task는 `block`을 쓰지 않는다. 실행을 막는 판정은 하류 게이트의 권한이다. + - `status`는 다음 순서로 정한다. + 입력 자체가 판정 불가이면 `BLOCKED`. + 하드 실패가 있으면 `FAILED`. + `review_items`가 하나라도 있으면 `READY_WITH_REVIEW`. + 그 외에는 `READY`. + - 본 task는 `status`를 제안할 뿐이다. 후속 task는 이 값을 올릴 수 있어도 내릴 수 없다. + + + + - 금액 계산이 필요해 보이는 서술이 있으면 그 후보의 `requested_calculation_domains`에 계산 ID를 넣는다. + - 넣을 수 있는 값은 사전의 그 도메인 머리에 적힌 `calc=` 목록 안에 있는 것뿐이다. 목록 밖 값은 넣지 않는다. + - 계산 ID 형식은 `^CE-[A-Z0-9][A-Z0-9-]{0,15}$`이다. + - 본 task는 어떤 금액도 계산하지 않고 이율·기산일·기간·원금을 정하지 않는다. 계산이 필요하다는 표시만 남긴다. + - 필요 없으면 빈 배열로 둔다. + + + + - 인코딩은 UTF-8, 줄바꿈은 LF, 문자열은 NFC로 정규화한다. + - 모든 참조 배열은 중복 없이 문자열 오름차순으로 정렬한다. + - `candidates`는 `domain_id` 문자열 오름차순으로 정렬한다. + - `source_unit_index`는 문서 등장 순서를 유지한다. + - 숫자를 문자열로 감싸지 않고 불리언은 `true`/`false`로 쓴다. + - 값이 없으면 빈 배열을 쓰고 `null`이나 빈 문자열을 쓰지 않는다. + - 출력은 순수 JSON 하나다. 마크다운 코드펜스, 머리말, 꼬리 설명을 붙이지 않는다. + + + + 최상위 키는 `domain_screening_draft` 하나다. 아래 키 구성을 정확히 지킨다. + + { + "domain_screening_draft": { + "draft_schema_version": "stage1_domain_screening_draft.v1", + "produced_by": "Task_A0_domain_screener_01", + "source_document_code": "MTG", + "source_document_name": "client_meeting.md", + "unit_numbering_rule": "MTG.
..", + "source_unit_index": [ + { "unit_id": "MTG.FACT.01.01", "gist": "60자 이내 요지. 판단이 아니라 그 문단이 무엇을 다루는지만 적는다." } + ], + "status": "READY | READY_WITH_REVIEW | BLOCKED | FAILED 중 하나", + "candidates": [ + { + "domain_id": "E-13", + "provisional_status": "candidate | supporting | monitor | review 중 하나", + "positive_refs": ["MTG.FACT.03.01"], + "negative_refs": [], + "conflict_refs": [], + "confidence": "high | medium | low 중 하나", + "reasoning_summary": "E13-P01은 MTG.FACT.03.01에서, E13-P02는 MTG.FACT.03.03에서 대응 서술을 확인함. 합 2.0으로 활성 임계값을 넘어 candidate로 둠.", + "final_determination_forbidden": true, + "requested_calculation_domains": ["CE-03"] + } + ], + "unmatched_issue_refs": ["MTG.WISH.05.00"], + "review_items": [ + { + "review_code": "UNROUTED_ISSUE_REVIEW", + "severity": "review", + "reason": "어느 도메인 단서에도 걸리지 않은 상담록 단위를 잔여로 보존함.", + "source_refs": ["MTG.WISH.05.00"], + "domain_ids": ["E-00"], + "unresolved_type": "unrouted_meeting_unit", + "blocks_final_drafting": true + } + ] + } + } + + 고정값 규칙: + - `draft_schema_version`, `produced_by`, `source_document_code`, `source_document_name`, `unit_numbering_rule` 다섯 값은 위에 적힌 문자열을 글자 그대로 쓴다. + - `candidates`의 각 원소는 위 아홉 개 키만 쓴다. `requested_calculation_domains`가 빈 배열이어도 키는 남긴다. + - `review_items`가 없으면 빈 배열로 둔다. + + + + - `source_unit_index`의 모든 `unit_id`가 `^[A-Za-z][A-Za-z0-9_.:/-]{0,255}$`를 만족하고 중복이 없는지 점검한다. + - 상담록의 모든 문단이 `source_unit_index`에 하나씩 대응하는지, 빠뜨린 문단이 없는지 점검한다. + - 모든 `positive_refs`·`negative_refs`·`conflict_refs`·`review_items[].source_refs`·`unmatched_issue_refs`의 원소가 전부 `source_unit_index`의 `unit_id` 집합 안에 있는지 점검한다. 목록 밖 식별자가 하나라도 있으면 고친다. + - `source_unit_index`의 개수가 (어느 후보엔가 인용된 unit 수) + (`unmatched_issue_refs` 수)와 같은지 점검한다. 어긋나면 인용 관계를 다시 본다. + - 모든 `domain_id`가 사전 `## DOMAINS`에 실제로 있는 값이고 `^(?:(?:EC|E)-[A-Z0-9]{2,8}|X[A-Z0-9]{1,8})$`를 만족하는지 점검한다. + - 같은 `domain_id`가 `candidates`에 두 번 등장하지 않는지 점검한다. + - 모든 후보에 `final_determination_forbidden: true`가 있는지 점검한다. + - 부정 단서가 걸린 후보가 목록에서 사라지지 않았는지, `candidate`에서 두 단계 넘게 내려가지 않았는지 점검한다. + - 충돌 단서가 걸린 후보의 `provisional_status`가 `review`이고 `confidence`가 `low`인지 점검한다. + - 모든 `review_code`가 사전의 `C` 줄에 있는 값이거나 `UNROUTED_ISSUE_REVIEW` 또는 `SCREENING_SOURCE_UNIT_FALLBACK`인지 점검한다. + - 각 후보의 `requested_calculation_domains`가 그 도메인 머리의 `calc=` 목록의 부분집합인지 점검한다. + - `ao=1`인 E-00, X1, X2, X3 네 도메인이 `candidates`에 모두 있는지 점검한다. + - 금지 필드명 여섯 개와 비판단 6종이 초안에 하나도 없는지 점검한다. + - `reasoning_summary`에 사건유형 명칭, 당사자 이름, 사건번호가 판단 근거로 쓰이지 않았는지 점검한다. + - `reasoning_summary`가 1200자를 넘지 않는지 점검한다. + - 출력 파일 경로와 성공 메시지가 정확한지 점검한다. + + + + + + [ ] 1) `list_docs`로 `client_meeting.md`와 `Default_Agent/routing/activation_cue_digest.md` 존재 확인(내용은 preflight로 이미 제공됨) + [ ] 2) source unit 채번 → 단서 매칭 → 판단 4종 생성 + [ ] 3) `` 자기점검 + [ ] 4) `write_file` 사용해서 `routing/domain_screening_draft.json` 생성 + [ ] 5) `list_docs` 사용해서 `routing/domain_screening_draft.json` 존재만 확인(절대 읽기 금지) + [ ] 6) `"도메인 스크리닝 초안 생성 완료"` 출력하고 작업을 끝낸다(terminate) + + + + + - `client_meeting.md` + - `Default_Agent/routing/activation_cue_digest.md` + + + - `routing/domain_screening_draft.json` + + + + + - 이 산출물은 가설이다. 어떤 도메인도 확정하지 않는다. + - `routing/domain_screening.json`은 후속 task의 산출물이다. 만들지 않는다. + - 비판단 6종은 초안에 넣지 않는다. + - 초안을 재읽지 않는다. + - 성공 시 마지막 채팅 출력은 정확히 `"도메인 스크리닝 초안 생성 완료"` 한 줄만 쓴다. + + + - task_name: Task_A0_domain_screener_02 + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + timeout: 180 + code: | + #!/usr/bin/env python3 + # Task_A0_domain_screener_02 + # domain_screening_draft.json (LLM 판단 4종) 를 받아 + # 비판단 6종을 registry 에서 직접 읽어 붙이고, + # 멤버십과 보존 등식을 검증하여 routing/domain_screening.json 을 기록한다. + # 판단은 하지 않는다. 상태를 올릴 수는 있어도 내리지 않는다. + import hashlib + import itertools + import json + import re + import unicodedata + + import httpx + + # -------------------------------------------------------------------------- + # localdocs 보일러플레이트 (SKILL.md 5장 / 5.2장) + # clientInfo 에 {{__user_hash__}} / {{__workspace_hash__}} 를 반드시 넣는다. + # 빠지면 localdocs 가 루트 경로를 보므로 사용자 파일을 찾지 못한다. + # -------------------------------------------------------------------------- + TASK_NAME = "Task_A0_domain_screener_02" + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", + "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + + + 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 %s failed" % name) + return p + + + def read_raw(name): + # 파일 내용을 재직렬화 없이 원문 그대로 돌려준다. + # registry_index_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()) + + + # -------------------------------------------------------------------------- + # 경로와 상수 + # -------------------------------------------------------------------------- + DRAFT_PATH = "routing/domain_screening_draft.json" + MEETING_PATH = "client_meeting.md" + REGISTRY_INDEX_PATH = "Default_Agent/domains/_registry_index.json" + DIGEST_PATH = "Default_Agent/routing/activation_cue_digest.md" + OUTPUT_PATH = "routing/domain_screening.json" + + SCHEMA_VERSION = "stage1_domain_screening.v1" + + # platform/schemas/domain_screening.schema.json 이 const 로 못박은 값이다. + # 하나라도 다르면 스키마 검증에서 떨어지므로 판단 대상이 아니다. + SCREENING_POLICY = { + "case_name_lookup_forbidden": True, + "single_keyword_activation_forbidden": True, + "negative_cue_downrank_only": True, + "evidence_refinement_required": True, + "unknown_review_code": "UNROUTED_ISSUE_REVIEW", + } + CONTRACT_GUARDS = { + "final_conclusion_forbidden": True, + "unknown_values_require_review": True, + "source_membership_required": True, + "strict_json_output": True, + "meeting_only_evidence_promotion_forbidden": True, + } + + STATUS_RANK = {"READY": 0, "READY_WITH_REVIEW": 1, "BLOCKED": 2, "FAILED": 3} + PROVISIONAL_STATUS = ("candidate", "supporting", "monitor", "review") + CONFIDENCE = ("high", "medium", "low") + SEVERITY = ("info", "review", "hard_warning", "block") + + REF_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_.:/-]{0,255}$") + DOMAIN_RE = re.compile(r"^(?:(?:EC|E)-[A-Z0-9]{2,8}|X[A-Z0-9]{1,8})$") + CALC_RE = re.compile(r"^CE-[A-Z0-9][A-Z0-9-]{0,15}$") + CODE_RE = re.compile(r"^[A-Z][A-Z0-9_]{2,127}$") + REG_VER_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_.-]{2,127}$") + SHA_RE = re.compile(r"^[a-f0-9]{64}$") + + CANDIDATE_REQUIRED = ("domain_id", "provisional_status", "positive_refs", + "negative_refs", "conflict_refs", "confidence", + "reasoning_summary", "final_determination_forbidden") + CANDIDATE_ALLOWED = CANDIDATE_REQUIRED + ("requested_calculation_domains",) + REVIEW_REQUIRED = ("review_code", "severity", "reason", "source_refs") + REVIEW_ALLOWED = REVIEW_REQUIRED + ("domain_ids", "unresolved_type", + "blocks_final_drafting") + SCREENING_REQUIRED = ("schema_version", "status", "registry_version", + "registry_index_sha256", "source_document_refs", + "candidates", "unmatched_issue_refs", "review_items", + "screening_policy", "contract_guards") + + # 01 이 쓰면 안 되는 필드. 하나라도 있으면 초안이 계약을 벗어난 것이다. + FORBIDDEN_DRAFT_FIELDS = ("track_hypothesis", "domain_hypotheses", + "relief_mode_hints", "computation_hints", + "matched_cues", "notes_kr") + NONJUDGMENT_FIELDS = ("schema_version", "registry_version", + "registry_index_sha256", "source_document_refs", + "screening_policy", "contract_guards") + + GANADA = "가나다라마바사아자차카타파하" + SECTION_MARKERS = (("상담내용", "FACT"), + ("사건관계인의주장", "OPP"), + ("의뢰인의희망사항", "WISH")) + + ERRORS = [] + WARNINGS = [] + ADDED_REVIEWS = [] + + + def fail(code, message): + ERRORS.append({"code": code, "message": message}) + + + def warn(code, message): + WARNINGS.append({"code": code, "message": message}) + + + # -------------------------------------------------------------------------- + # 1) source unit 독립 재산출 + # Task_A0_domain_screener_01 의 와 같은 규칙을 + # 코드로 다시 구현한다. LLM 의 채번을 신뢰하지 않고 대조하기 위한 것이다. + # -------------------------------------------------------------------------- + def mint_source_units(meeting_text): + lines = unicodedata.normalize("NFC", meeting_text).split("\n") + section = None + cluster = 0 + units = [] + meta_seen = False + raw_seq = 0 + for line in lines: + t = line.strip() + if not t: + continue + squashed = re.sub(r"\s+", "", t) + if "【" in t and "】" in t: + hit = None + for needle, code in SECTION_MARKERS: + if needle in squashed: + hit = code + break + if hit: + section = hit + cluster = 0 + continue + if section is None: + # 상담일지 머리표는 문단마다 자르지 않고 MTG.META.01 한 단위로 접는다. + if set(t) <= set("| -"): + continue + if not meta_seen: + units.append("MTG.META.01") + meta_seen = True + continue + m = re.match(r"^(\d{1,2})\.\s", t) + if m: + cluster = int(m.group(1)) + continue + m2 = re.match(r"^([" + GANADA + r"])\.\s", t) + if m2: + item = GANADA.index(m2.group(1)) + 1 + else: + item = 0 + unit_id = "MTG.%s.%02d.%02d" % (section, cluster, item) + if unit_id in units: + raw_seq += 1 + unit_id = "MTG.RAW.%03d" % raw_seq + units.append(unit_id) + return units, raw_seq + + + # -------------------------------------------------------------------------- + # 2) 단어 사전 파싱 + # 도메인 화이트리스트, 도메인별 계산 허용목록, 충돌 검토코드, 상시활성 목록, + # 그리고 사전이 어느 registry 스냅숏에서 나왔는지를 뽑는다. + # -------------------------------------------------------------------------- + def parse_digest(md): + text = unicodedata.normalize("NFC", md) + info = {"registry_version": None, "registry_index_sha256": None, + "domains": {}, "always_on": [], "review_codes": set()} + m = re.search(r"^registry_version:\s*`([^`]+)`", text, re.M) + if m: + info["registry_version"] = m.group(1).strip() + m = re.search(r"^registry_index:.*sha256=`([a-f0-9]{64})`", text, re.M) + if m: + info["registry_index_sha256"] = m.group(1) + current = None + for line in text.split("\n"): + t = line.rstrip() + if t.startswith("### "): + parts = [x.strip() for x in t[4:].split("|")] + if len(parts) < 6: + continue + did = parts[0] + kv = {} + for seg in parts[2:]: + if "=" in seg: + k, v = seg.split("=", 1) + kv[k.strip()] = v.strip() + calc = [] if kv.get("calc", "-") == "-" else kv["calc"].split(",") + dep = [] if kv.get("dep", "-") == "-" else kv["dep"].split(",") + info["domains"][did] = {"calc": calc, "dep": dep, + "kind": kv.get("kind"), + "ao": kv.get("ao") == "1"} + if kv.get("ao") == "1": + info["always_on"].append(did) + current = did + elif t.startswith("C|") and current: + fields = t.split("|") + if len(fields) >= 4: + info["review_codes"].add(fields[3].strip()) + return info + + + # -------------------------------------------------------------------------- + # 3) 최종 산출물 자기검증 — 닫힌 스키마의 형태를 코드로 다시 확인한다. + # -------------------------------------------------------------------------- + def self_check(doc): + problems = [] + if list(doc.keys()) != ["domain_screening"]: + problems.append("root key must be exactly domain_screening") + return problems + ds = doc["domain_screening"] + missing = [k for k in SCREENING_REQUIRED if k not in ds] + extra = [k for k in ds if k not in SCREENING_REQUIRED] + if missing: + problems.append("missing required: %s" % ",".join(missing)) + if extra: + problems.append("additionalProperties: %s" % ",".join(extra)) + if ds.get("schema_version") != SCHEMA_VERSION: + problems.append("schema_version const mismatch") + if ds.get("status") not in STATUS_RANK: + problems.append("status enum invalid") + if not REG_VER_RE.match(str(ds.get("registry_version", ""))): + problems.append("registry_version pattern invalid") + if not SHA_RE.match(str(ds.get("registry_index_sha256", ""))): + problems.append("registry_index_sha256 pattern invalid") + for key in ("source_document_refs", "unmatched_issue_refs"): + arr = ds.get(key, []) + if len(set(arr)) != len(arr): + problems.append("%s not unique" % key) + for v in arr: + if not REF_RE.match(v): + problems.append("%s ref pattern invalid: %s" % (key, v)) + for c in ds.get("candidates", []): + for k in CANDIDATE_REQUIRED: + if k not in c: + problems.append("candidate missing %s" % k) + for k in c: + if k not in CANDIDATE_ALLOWED: + problems.append("candidate extra key %s" % k) + if c.get("final_determination_forbidden") is not True: + problems.append("final_determination_forbidden must be true") + for r in ds.get("review_items", []): + for k in REVIEW_REQUIRED: + if k not in r: + problems.append("review_item missing %s" % k) + for k in r: + if k not in REVIEW_ALLOWED: + problems.append("review_item extra key %s" % k) + if r.get("severity") not in SEVERITY: + problems.append("review_item severity invalid") + if not CODE_RE.match(str(r.get("review_code", ""))): + problems.append("review_code pattern invalid") + reason = r.get("reason", "") + if not (1 <= len(reason) <= 1000): + problems.append("review_item reason length invalid") + if ds.get("screening_policy") != SCREENING_POLICY: + problems.append("screening_policy const mismatch") + if ds.get("contract_guards") != CONTRACT_GUARDS: + problems.append("contract_guards const mismatch") + return problems + + + # -------------------------------------------------------------------------- + # main + # -------------------------------------------------------------------------- + def main(): + _init() + + # ---- 입력 4종 ---- + try: + draft_doc = read_json(DRAFT_PATH) + except Exception as exc: + fail("DRAFT_UNREADABLE", "%s: %s" % (DRAFT_PATH, exc)) + return None + try: + meeting_raw = read_raw(MEETING_PATH) + except Exception as exc: + fail("MEETING_UNREADABLE", "%s: %s" % (MEETING_PATH, exc)) + return None + try: + registry_raw = read_raw(REGISTRY_INDEX_PATH) + except Exception as exc: + fail("REGISTRY_INDEX_UNREADABLE", "%s: %s" % (REGISTRY_INDEX_PATH, exc)) + return None + try: + digest_raw = read_raw(DIGEST_PATH) + except Exception as exc: + fail("DIGEST_UNREADABLE", "%s: %s" % (DIGEST_PATH, exc)) + return None + + if not isinstance(draft_doc, dict) or "domain_screening_draft" not in draft_doc: + fail("DRAFT_ROOT_KEY_INVALID", "root key domain_screening_draft not found") + return None + draft = draft_doc["domain_screening_draft"] + + for k in FORBIDDEN_DRAFT_FIELDS: + if k in draft: + fail("FORBIDDEN_FIELD_PRESENT", "draft contains forbidden field %s" % k) + for k in NONJUDGMENT_FIELDS: + if k in draft: + fail("NONJUDGMENT_FIELD_IN_DRAFT", + "draft must not carry non-judgment field %s" % k) + + # ---- registry 원문 해시. 재직렬화하지 않은 원문 바이트로 계산한다. ---- + registry_index_sha256 = hashlib.sha256(registry_raw.encode("utf-8")).hexdigest() + try: + registry_doc = json.loads(registry_raw) + registry = registry_doc["domain_registry_index"] + except Exception as exc: + fail("REGISTRY_INDEX_UNREADABLE", "parse failed: %s" % exc) + return None + registry_version = registry.get("registry_version") + registry_domain_ids = sorted(e["domain_id"] for e in registry.get("entries", [])) + + # ---- 단어 사전 ---- + digest = parse_digest(digest_raw) + if len(digest["domains"]) != len(registry_domain_ids): + fail("DIGEST_MALFORMED", + "digest domains=%d registry entries=%d" + % (len(digest["domains"]), len(registry_domain_ids))) + return None + unknown_in_digest = sorted(set(digest["domains"]) - set(registry_domain_ids)) + if unknown_in_digest: + fail("DIGEST_DOMAIN_SET_MISMATCH", + "digest has domains absent from registry: %s" % ",".join(unknown_in_digest)) + return None + # 사전 머리말은 있으면 낡음 감지에 쓰고, 없으면 경고만 남긴다. + # registry_version 과 registry_index_sha256 의 정본은 _registry_index.json 이지 + # 사전이 아니다. 사전 머리말이 편집되어 사라져도 산출물은 정확해야 한다. + if digest["registry_index_sha256"] is None: + warn("DIGEST_HEADER_ABSENT", + "activation_cue_digest.md has no registry_index sha256 header; " + "staleness cross-check skipped") + elif digest["registry_index_sha256"] != registry_index_sha256: + # 사전이 만들어진 뒤 registry 가 바뀌었거나, 읽는 과정에서 원문이 변형된 것이다. + # 사전이 낡은 단서로 판단한 초안을 봉인하면 안 되므로 여기서 멈춘다. + fail("REGISTRY_INDEX_SHA_MISMATCH", + "digest=%s computed=%s" % (digest["registry_index_sha256"], + registry_index_sha256)) + return None + if digest["registry_version"] is None: + warn("DIGEST_HEADER_ABSENT", + "activation_cue_digest.md has no registry_version header") + elif digest["registry_version"] != registry_version: + fail("REGISTRY_VERSION_MISMATCH", + "digest=%s registry=%s" % (digest["registry_version"], registry_version)) + return None + + # ---- source unit 독립 재산출 ---- + universe, raw_fallback = mint_source_units(meeting_raw) + if raw_fallback: + warn("SCREENING_SOURCE_UNIT_FALLBACK", + "%d unit(s) fell back to MTG.RAW.* numbering" % raw_fallback) + if not universe: + fail("SOURCE_UNIT_SET_EMPTY", "no source unit minted from %s" % MEETING_PATH) + return None + universe_set = set(universe) + if len(universe_set) != len(universe): + fail("SOURCE_UNIT_DUPLICATE", "duplicate unit id after minting") + return None + for u in universe: + if not REF_RE.match(u): + fail("REF_PATTERN_INVALID", "minted unit id invalid: %s" % u) + source_document_refs = sorted(universe_set) + + # 초안이 스스로 적어둔 목록과 대조한다. 어긋나면 재산출본을 정본으로 삼고 검토를 남긴다. + draft_units = [] + for row in draft.get("source_unit_index", []) or []: + if isinstance(row, dict) and row.get("unit_id"): + draft_units.append(row["unit_id"]) + if draft_units and set(draft_units) != universe_set: + only_draft = sorted(set(draft_units) - universe_set) + only_mint = sorted(universe_set - set(draft_units)) + ADDED_REVIEWS.append({ + "review_code": "E00_MEMBERSHIP_CONFLICT_REVIEW", + "severity": "review", + "reason": ("초안의 source_unit_index 와 재산출한 원천 단위 집합이 다르다. " + "재산출본을 정본으로 삼았다. 초안에만 있음=%s, 재산출본에만 있음=%s" + % (",".join(only_draft) or "-", ",".join(only_mint) or "-"))[:1000], + "source_refs": sorted(set(only_mint))[:64], + "domain_ids": ["E-00"], + "unresolved_type": "source_unit_index_divergence", + "blocks_final_drafting": False, + }) + + # ---- 후보 검증 ---- + candidates = draft.get("candidates") + if not isinstance(candidates, list): + fail("DRAFT_FIELD_MISSING", "candidates must be a list") + return None + seen_domains = set() + cited = set() + clean_candidates = [] + for c in candidates: + if not isinstance(c, dict): + fail("CANDIDATE_KEY_INVALID", "candidate is not an object") + continue + did = c.get("domain_id") + for k in CANDIDATE_REQUIRED: + if k not in c: + fail("CANDIDATE_KEY_INVALID", "%s missing %s" % (did, k)) + for k in c: + if k not in CANDIDATE_ALLOWED: + fail("CANDIDATE_KEY_INVALID", "%s extra key %s" % (did, k)) + if not DOMAIN_RE.match(str(did)) or did not in digest["domains"]: + fail("DOMAIN_ID_UNKNOWN", "unknown domain_id: %r" % did) + continue + if did in seen_domains: + fail("DOMAIN_ID_DUPLICATE", "duplicate domain_id: %s" % did) + continue + seen_domains.add(did) + if c.get("provisional_status") not in PROVISIONAL_STATUS: + fail("ENUM_INVALID", "%s provisional_status=%r" % (did, c.get("provisional_status"))) + if c.get("confidence") not in CONFIDENCE: + fail("ENUM_INVALID", "%s confidence=%r" % (did, c.get("confidence"))) + summary = c.get("reasoning_summary", "") + if not isinstance(summary, str) or not (1 <= len(summary) <= 1200): + fail("REASONING_SUMMARY_LENGTH", "%s length=%d" % (did, len(str(summary)))) + if c.get("final_determination_forbidden") is not True: + fail("FINAL_DETERMINATION_FLAG_INVALID", "%s" % did) + for key in ("positive_refs", "negative_refs", "conflict_refs"): + arr = c.get(key) or [] + if not isinstance(arr, list): + fail("CANDIDATE_KEY_INVALID", "%s %s must be a list" % (did, key)) + continue + for ref in arr: + if not REF_RE.match(str(ref)): + fail("REF_PATTERN_INVALID", "%s %s %r" % (did, key, ref)) + elif ref not in universe_set: + # source_membership_required 위반. 조용히 지우지 않고 실패로 처리한다. + fail("REF_NOT_IN_SOURCE_UNIVERSE", "%s %s %s" % (did, key, ref)) + else: + cited.add(ref) + allowed_calc = set(digest["domains"][did]["calc"]) + calc = c.get("requested_calculation_domains") or [] + if not isinstance(calc, list): + fail("CANDIDATE_KEY_INVALID", "%s requested_calculation_domains must be a list" % did) + calc = [] + for cid in calc: + if not CALC_RE.match(str(cid)): + fail("CALC_PATTERN_INVALID", "%s %r" % (did, cid)) + elif cid not in allowed_calc: + fail("CALC_NOT_IN_BINDINGS", "%s requested %s but calc=%s" + % (did, cid, ",".join(sorted(allowed_calc)) or "-")) + out = {} + for k in CANDIDATE_REQUIRED: + out[k] = c.get(k) + for key in ("positive_refs", "negative_refs", "conflict_refs"): + out[key] = sorted(set(out.get(key) or [])) + if "requested_calculation_domains" in c: + out["requested_calculation_domains"] = sorted(set(calc)) + clean_candidates.append(out) + clean_candidates.sort(key=lambda x: str(x.get("domain_id"))) + + # 상시 활성 4종은 D0 게이트의 보장 대상이다. 여기서 후보를 만들어내지 않고 경고만 남긴다. + missing_ao = [d for d in sorted(digest["always_on"]) if d not in seen_domains] + if missing_ao: + warn("ALWAYS_ON_NOT_IN_DRAFT", + "always_on domains absent from draft candidates: %s" % ",".join(missing_ao)) + + # ---- 검토 항목 검증 ---- + allowed_codes = set(digest["review_codes"]) | { + "UNROUTED_ISSUE_REVIEW", "SCREENING_SOURCE_UNIT_FALLBACK", + "E00_MEMBERSHIP_CONFLICT_REVIEW", "E00_FALSE_UNROUTED_REVIEW", + "E00_CONSERVATION_FAILURE"} + clean_reviews = [] + for r in draft.get("review_items") or []: + if not isinstance(r, dict): + fail("REVIEW_ITEM_KEY_INVALID", "review_item is not an object") + continue + for k in REVIEW_REQUIRED: + if k not in r: + fail("REVIEW_ITEM_KEY_INVALID", "missing %s" % k) + for k in r: + if k not in REVIEW_ALLOWED: + fail("REVIEW_ITEM_KEY_INVALID", "extra key %s" % k) + code = str(r.get("review_code", "")) + if not CODE_RE.match(code): + fail("REVIEW_CODE_PATTERN_INVALID", code) + elif code not in allowed_codes: + fail("REVIEW_CODE_UNKNOWN", code) + if r.get("severity") not in SEVERITY: + fail("ENUM_INVALID", "review severity=%r" % r.get("severity")) + for ref in r.get("source_refs") or []: + if ref not in universe_set: + fail("REF_NOT_IN_SOURCE_UNIVERSE", "review_items %s" % ref) + for did in r.get("domain_ids") or []: + if not DOMAIN_RE.match(str(did)): + fail("DOMAIN_ID_UNKNOWN", "review_items %r" % did) + item = {} + for k in REVIEW_ALLOWED: + if k in r: + item[k] = r[k] + item["source_refs"] = sorted(set(item.get("source_refs") or [])) + clean_reviews.append(item) + + # ---- 보존 등식. 초안의 미분류를 신뢰하지 않고 다시 계산한다. ---- + computed_unmatched = sorted(universe_set - cited) + draft_unmatched = sorted(set(draft.get("unmatched_issue_refs") or [])) + if draft_unmatched != computed_unmatched: + only_draft = sorted(set(draft_unmatched) - set(computed_unmatched)) + only_calc = sorted(set(computed_unmatched) - set(draft_unmatched)) + ADDED_REVIEWS.append({ + "review_code": "E00_FALSE_UNROUTED_REVIEW", + "severity": "review", + "reason": ("초안의 unmatched_issue_refs 가 재계산 결과와 다르다. 재계산본으로 대체했다. " + "초안에만 있음=%s, 재계산본에만 있음=%s" + % (",".join(only_draft) or "-", ",".join(only_calc) or "-"))[:1000], + "source_refs": (only_draft + only_calc)[:64], + "domain_ids": ["E-00"], + "unresolved_type": "unmatched_set_divergence", + "blocks_final_drafting": False, + }) + unmatched = computed_unmatched + if computed_unmatched and not any( + r.get("review_code") == "UNROUTED_ISSUE_REVIEW" for r in clean_reviews): + ADDED_REVIEWS.append({ + "review_code": "UNROUTED_ISSUE_REVIEW", + "severity": "review", + "reason": "어느 도메인 단서에도 걸리지 않은 상담록 단위를 잔여로 보존한다.", + "source_refs": computed_unmatched[:64], + "domain_ids": ["E-00"], + "unresolved_type": "unrouted_meeting_unit", + "blocks_final_drafting": True, + }) + clean_reviews.extend(ADDED_REVIEWS) + + if len(universe_set) != len(cited) + len(unmatched): + fail("CONSERVATION_FAILED", + "universe=%d cited=%d unmatched=%d" + % (len(universe_set), len(cited), len(unmatched))) + + # ---- 상태. 올릴 수는 있어도 내리지 않는다. ---- + draft_status = draft.get("status") + if draft_status not in STATUS_RANK: + fail("ENUM_INVALID", "draft status=%r" % draft_status) + draft_status = "READY_WITH_REVIEW" + status = draft_status + if clean_reviews and STATUS_RANK[status] < STATUS_RANK["READY_WITH_REVIEW"]: + status = "READY_WITH_REVIEW" + + if ERRORS: + return None + + # ---- 비판단 6종을 붙여 조립한다. 키 순서는 스키마의 required 순서를 따른다. ---- + screening = { + "schema_version": SCHEMA_VERSION, + "status": status, + "registry_version": registry_version, + "registry_index_sha256": registry_index_sha256, + "source_document_refs": source_document_refs, + "candidates": clean_candidates, + "unmatched_issue_refs": unmatched, + "review_items": clean_reviews, + "screening_policy": dict(SCREENING_POLICY), + "contract_guards": dict(CONTRACT_GUARDS), + } + doc = {"domain_screening": screening} + + problems = self_check(doc) + if problems: + for p in problems: + fail("OUTPUT_SCHEMA_SELF_CHECK_FAILED", p) + return None + + body = json.dumps(doc, ensure_ascii=False, indent=2) + "\n" + write_doc(OUTPUT_PATH, body) + try: + _call("list_docs", {"path": "routing", "pattern": "domain_screening.json"}, + next_msg_id()) + except Exception as exc: + warn("OUTPUT_LIST_CHECK_SKIPPED", str(exc)) + + return { + "screening_sha256": hashlib.sha256(body.encode("utf-8")).hexdigest(), + "status": status, + "source_unit_count": len(universe_set), + "cited_unit_count": len(cited), + "unmatched_unit_count": len(unmatched), + "candidate_count": len(clean_candidates), + "review_item_count": len(clean_reviews), + "review_items_added_by_02": [r["review_code"] for r in ADDED_REVIEWS], + "registry_version": registry_version, + "registry_index_sha256": registry_index_sha256, + } + + + try: + RESULT = main() + except Exception as exc: + fail("UNHANDLED_EXCEPTION", "%s: %s" % (type(exc).__name__, exc)) + RESULT = None + + if RESULT is None: + print(json.dumps({"status": "FAILED", "written": False, + "output_path": OUTPUT_PATH, + "errors": ERRORS, "warnings": WARNINGS}, + ensure_ascii=False)) + else: + payload = {"status": RESULT["status"], "written": True, + "output_path": OUTPUT_PATH, "errors": [], "warnings": WARNINGS} + payload.update(RESULT) + print(json.dumps(payload, ensure_ascii=False)) + - task_name: Task_A0_domain_screener_03 + mcp: code-executor + tool_name: run_code + parameters: + language: python + requirements: "httpx" + network: "agent-network" + # 후보 15개면 도구 호출이 4(고정 읽기)+15(후보 config)+1(쓰기)+1(확인)=21회다. + # 후보가 26개까지 늘어도 여유가 남도록 240초로 둔다. + timeout: 240 + code: | + #!/usr/bin/env python3 + # Task_A0_domain_screener_03 + # routing/domain_screening.json 의 후보 도메인 목록으로 + # Default_Agent/domains/<후보>/domain_config.json 에서 7개 항목만 발췌하여 + # routing/candidate_profile_vocabulary.md 를 기록한다. + # Task_A_client_goal 이 domain_profiles 를 채울 때 쓸 낱말 목록이며, + # 판단은 하지 않는다. registry 원문이 정본이고 이 파일은 파생본이다. + import hashlib + import itertools + import json + import re + import unicodedata + + import httpx + + # -------------------------------------------------------------------------- + # localdocs 보일러플레이트 (SKILL.md 5장 / 5.2장) + # clientInfo 에 {{__user_hash__}} / {{__workspace_hash__}} 를 반드시 넣는다. + # 빠지면 localdocs 가 루트 경로를 보므로 사용자 파일을 찾지 못한다. + # -------------------------------------------------------------------------- + TASK_NAME = "Task_A0_domain_screener_03" + LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp" + MCP_HEADERS = {"Content-Type": "application/json", + "Accept": "application/json, text/event-stream"} + CLIENT = httpx.Client(timeout=60) + MSG_ID_COUNTER = itertools.count(10) + + + 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 %s failed" % name) + return p + + + def read_raw(name): + # 원문을 재직렬화 없이 그대로 돌려준다. + # domain_config 의 sha256 을 registry 색인과 대조하므로 바이트 보존이 필수다. + 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()) + + + # -------------------------------------------------------------------------- + # 경로와 상수 + # -------------------------------------------------------------------------- + SCREENING_PATH = "routing/domain_screening.json" + REGISTRY_INDEX_PATH = "Default_Agent/domains/_registry_index.json" + DOMAIN_CATALOG_PATH = "Default_Agent/platform/reference_catalogs/domain_ids.json" + CALC_CATALOG_PATH = "Default_Agent/platform/reference_catalogs/calculation_ids.json" + CONFIG_PATH_FMT = "Default_Agent/domains/%s/domain_config.json" + OUTPUT_PATH = "routing/candidate_profile_vocabulary.md" + + DIGEST_SCHEMA = "candidate_profile_vocabulary.v1" + + # Task_A 프롬프트의 잔여 예산. §2.2 로 고정 4-profile 블록(약 9,000 B)이 + # 삭제되는 것을 반영한 값이다. 초과하면 라벨을 단계적으로 줄인다. + MAX_OUTPUT_BYTES = 28000 + # 라벨 길이 사다리. 앞에서부터 시도하고 예산에 맞는 첫 단계를 쓴다. + LABEL_TIERS = [("full", None), ("60", 60), ("40", 40), ("24", 24), ("none", 0)] + + DOMAIN_RE = re.compile(r"^(?:(?:EC|E)-[A-Z0-9]{2,8}|X[A-Z0-9]{1,8})$") + CALC_RE = re.compile(r"^CE-[A-Z0-9][A-Z0-9-]{0,15}$") + REF_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_.:/-]{0,255}$") + + ERRORS = [] + WARNINGS = [] + + + def fail(code, message): + ERRORS.append({"code": code, "message": message}) + + + def warn(code, message): + WARNINGS.append({"code": code, "message": message}) + + + def cut(text, limit): + # 라벨 축약. 파이프는 구분자이므로 슬래시로 치환한다. + if text is None: + return "-" + t = unicodedata.normalize("NFC", str(text)).replace("\n", " ").replace("|", "/").strip() + if not t: + return "-" + if limit is None: + return t + if limit == 0: + return "" + return t if len(t) <= limit else t[:limit] + + + def render(rows, refs, meta, limit): + # rows: [(domain_id, cfg)] 순서 고정. limit 은 라벨 길이 상한. + L = [] + a = L.append + a("# CANDIDATE_PROFILE_VOCABULARY") + a("") + a("digest_schema: `%s`" % DIGEST_SCHEMA) + a("purpose: `client_goal_domain_profiles.schema.json` 을 채울 때 쓰는 낱말 목록이다. " + "사실 원천이 아니며 registry 원문의 파생본이다.") + a("source: `Default_Agent/domains//domain_config.json` 에서 7개 항목만 발췌 " + "(label_ko·kind, element_slots, opposing_fact_slots, evidence_components, " + "defense_map, calculation_bindings, review_codes)") + a("registry_version: `%s`" % meta["registry_version"]) + a("registry_index_sha256: `%s`" % meta["registry_index_sha256"]) + a("screening_sha256: `%s`" % meta["screening_sha256"]) + a("candidate_count: %d" % len(rows)) + a("label_tier: `%s`" % meta["label_tier"]) + a("") + a("## LEGEND") + a("") + a("도메인 머리: `### ||kind=|calc=`") + a("- `E||||