feat(stage1-part3): build Part 3 spec from registry per new strategy; deploy structure assets

- ver_8_yaml_candidates/stage_1_part_3_v.8.yml (52,878 B): two-task stage
  (Task_C_LE_L0_structure_seed_reducer -> Task_C_LE_L2_final_structure_index_writer,
  no L1) built solely from registry and Part 1|2 outputs (zero Claude_YAML
  dependency); fragments P3-L0/L2 v1 block-identical to the integrated file
- Default_Agent deployment origin: new structure_index_compiler.py(+mirror),
  structure_index_policy.v1.json, two sealed schemas (unversioned filenames,
  pattern-only), 50 sidecar files copied byte-identical from assembly;
  runtime_manifest 371->426 (delta exactly 55, zero existing-row edits)
- interface table 34->45 rows (closes C-7: legacy signal names dropped from
  Part 3 consumers; closes C-8: client_goal registered, client_meeting
  consumer added in both duplicated rows), stage_chain Part 3 section,
  upper strategy chain/§4.2 updated for L1 absence
- verified: 3-pass dry run on deployment origin produces
  legal_effect_structures.json (theta 4, 6 records, SG-13 6/6 attached,
  BO 2/2 attached via source_domain+Legal_Keywords join, 3 conservation
  equations from source counts, 3 reverse indexes); 26-domain synthetic
  theta 56 / records 73 / modules 34, deterministic; regressions 22+2
  supplementary all PASS on copied variant trees; interface checker 0
  findings; adversarial sub-agent loop 5 findings -> 0
- P3-0a (D-4 approval) treated as granted by the user's execution order;
  assembly-side sync (builder registration, ledgers, receipt) deferred as
  one bundle with Part 2 carry-overs
- work record stage_1_part_3_개정작업내역서.md; v.7/MEMORY.md entry 23;
  확장_최적워크플로우_연구_프롬프트.txt session prompt log

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 17:10:08 +09:00
co-authored by Claude Fable 5
parent d8e5b241ac
commit 6c7c793b2a
65 changed files with 4953 additions and 247 deletions
@@ -1474,3 +1474,26 @@ D-4 결정안 승계(Θ=expected_runnable 합집합 · order_key 3키 · by_modu
2. **회귀 시나리오는 자기 문서의 읽기 규율과 대조해야 한다.** `_verify_asset` 로 읽으라고 지시한 자산의 "내용만 변조" 회귀는 해시 게이트에 먼저 걸려 의미 검사에 도달하지 못한다 — 변조 회귀는 장부 동반/미동반 두 변종이 짝이다.
3. **용어 하나("활성")가 필수 회귀를 뒤집는다.** 정의역이 둘(`active` ⊊ `expected_runnable`) 있는 시스템에서는 집합 이름을 축약하지 말 것.
4. 문서 검증자는 편집 파급 검증에서 가장 세다 — 라운드 2~6 의 지적 전부가 "반영이 만든 새 모순"이었다. 반영 후 재검증을 생략하면 수정이 결함을 재생산한다.
---
## 2026-08-19 ㉓ — Part 3 신전략서 실행 완료: stage_1_part_3_v.8.yml 생성 (예행 3패스 · 회귀 24/24 · 적대 검증 2라운드)
산출: 통합본 `ver_8_yaml_candidates/stage_1_part_3_v.8.yml`(52,878 B · task 2 · IN→L0→L2→OUT) + 조각 P3-L0/L2 v1, 배포 원본 신설 5종(`structure_index_compiler.py`+미러 · `structure_index_policy.v1.json` · 스키마 2 — 파일명 무판본·패턴만) + 사이드카 50 복사(조립본과 바이트 동일), `runtime_manifest` **371→426**(델타 55·기존 행 수정 0), 선언표 34→**45**(C-7 구 이름 소비자 제거·C-8 폐쇄), stage_chain·상위 전략서 2곳, 내역서 `stage_1_part_3_개정작업내역서.md`. P3-0a 는 사용자의 실행 지시를 승인으로 처리.
### 핵심 실측
- 예행 3패스(asset root = 배포 원본): pass1 6/45 · pass2 3/29 · **pass3 2/5 — `legal_effect_structures.json` 실생성**. Θ 4종·레코드 6((도메인,type_id) 쌍)·SG-13 6/6 부착·**BO 2/2 부착**(source_domain+Legal_Keywords 결정론 조인 — BO 투영 회차가 실은 registry 토큰이 여기서 회수됨)·보존 등식 셋(L2 가 config·SG-13·BO 원본에서 바깥 수 직접 계수)·역인덱스 셋(by_domain_id·by_module·by_bo_id).
- 26 도메인 합성: Θ 56 · 구조 73 · module 34 · 머리 4줄(rank1 money_claim×2 비병합) · 결정론. 계산 탐침: signal_id 조인·dangling 검토·미연결 형식화 각 실증.
- 회귀 L-a~L-s 22종 + 보충 2종 = 24/24(필수 8종 포함). 변조는 전부 사본 트리 — 배포 원본 무변.
- 선언표 검사기 finding 0(3 yaml 전수). 트리 210 파일·매니페스트 실재분 전수 일치·__pycache__ 0.
### 검증 루프 (지적 5 → 0)
1차 MAJOR 2: ① 선언표 `client_meeting` 행이 **두 곳에 중복**(entry_inputs·handoffs)인데 절반만 갱신 — 검사기는 handoffs 만 읽으므로 기계 검증면 미반영 ② 도메인 봉투 부재가 침묵 스킵(실주행에서 감시 도메인 봉투 부재는 상시 경로) → `audit.input_gaps` 기록+L-k 단언. MINOR 3: L2 의 계약 밖 signal_manifest 읽기(→ L0 가 transaction_id 를 bundle 에 부착해 승계) · 합성/탐침 증적 미보존(→ SUPP 2종 영구 기록) · `Stage_1.Part_3` 약칭 잔존. 재검증 **지적 0건**.
### 교훈
1. **중복 선언 행은 전수 갱신 대상이다.** 같은 계약 문서 안에 같은 행이 두 배열에 있으면(entry_inputs·handoffs) 하나만 고친 갱신은 검사기가 읽는 쪽에 따라 무효가 된다 — 갱신 전에 대상 행을 grep 으로 전수 나열할 것.
2. **"실재하는 것만 읽는다"는 규칙에는 부재 기록이 한 벌이다.** 예행에서 전부 실재하는 입력은 부재 경로가 한 번도 발화하지 않아 침묵 스킵이 숨는다 — 부재가 상시인 실주행 조건을 회귀(L-k)로 강제해야 드러난다.
3. **하류가 상류 봉인값을 쓰려면 재독이 아니라 승계다.** L2 가 signal_manifest 를 다시 읽는 것은 계약 밖 읽기이자 봉인 우회 여지 — L0 가 검증한 값을 bundle 로 나르는 것이 계약에 맞다.
4. **검증 스크립트의 부산물도 배포 트리 오염원이다.** sys.path 반입이 만든 __pycache__ 가 트리에 두 번 생겼다 — 실측 스크립트에는 dont_write_bytecode 를 상수로 둘 것.
5. 검사기의 가시 범위에 맞춰 코드를 쓴다 — read_json 직호출 상수만 보는 U4 앞에서 간접 호출 래퍼는 "선언은 했으나 실측 불가"를 만든다(L0 read-plumbing 재작성으로 해소).
@@ -0,0 +1,20 @@
{
"schema_version": "stage1_s2_module_role_projection.v1",
"domain_id": "E-00",
"legacy_projection_source": {
"path": "S_0_GPT/legacy/part4/structure_projection_contract.json",
"sha256": "9e615dda7e26e23fcaae381ed05763d16e4887909400277c9996919e09398398"
},
"structure_type_to_projection": {
"general_legal_effect": {
"module": "legal_effect",
"role_projection": "legal_effect_roles"
}
},
"default_projection": {
"module": "legal_effect",
"role_projection": "legal_effect_roles",
"review_code": "UNREGISTERED_EFFECT_TYPE_REVIEW"
},
"closure_status": "CLOSED_WITH_LOCAL_FILES"
}
@@ -0,0 +1,20 @@
{
"schema_version": "stage1_s2_structure_types.v1",
"domain_id": "E-00",
"legacy_contract": {
"canonical_type_set_frozen": true,
"source": "1. Stage_1/v.7/extension_research/선행구축/S_0_GPT/legacy/part3/structure_type_contract.json"
},
"entries": [
{
"type_id": "general_legal_effect",
"priority_rank": 11,
"legacy_source": "Part3 fallback",
"module": "legal_effect",
"role_projection": "legal_effect_roles",
"admission_rule": "registered route only; E-00 unregistered material remains review"
}
],
"noncanonical_preservation": "nonrouting_labels",
"ordering_preserved": true
}
@@ -0,0 +1 @@
{"default_projection":null,"domain_id":"E-01","mappings":[{"module":"juristic_act","role_projection":"act_party_object_roles","source":"s6_registered","structure_type":"juristic_act_validity"},{"module":"juristic_act","role_projection":"declarant_counterparty_roles","source":"s6_registered","structure_type":"declaration_defect"},{"module":"juristic_act","role_projection":"canceller_counterparty_third_party_roles","source":"s6_registered","structure_type":"cancellation_effect"},{"module":"agency","role_projection":"principal_agent_counterparty_roles","source":"s6_registered","structure_type":"unauthorized_agency"},{"module":"agency","role_projection":"principal_apparent_agent_relying_party_roles","source":"s6_registered","structure_type":"apparent_agency"},{"module":"juristic_act","role_projection":"condition_beneficiary_obligor_roles","source":"s6_registered","structure_type":"condition_term_effect"}],"missing_mapping_policy":{"emit_review_code":"UNREGISTERED_EFFECT_TYPE_REVIEW","silent_fallback_forbidden":true},"schema_version":"stage1_domain_module_role_projection.v1","status":"ACTIVE"}
@@ -0,0 +1 @@
{"admission_rules":{"multiple_types_allowed":true,"nonregistered_type_review_code":"UNREGISTERED_EFFECT_TYPE_REVIEW","registered_membership_required":true,"silent_general_legal_effect_fallback_forbidden":true,"source_order_deduplicated":true},"domain_id":"E-01","schema_version":"stage1_domain_structure_types.v1","status":"ACTIVE","structure_types":[{"module":"juristic_act","priority_rank":71,"role_projection":"act_party_object_roles","type_id":"juristic_act_validity"},{"module":"juristic_act","priority_rank":72,"role_projection":"declarant_counterparty_roles","type_id":"declaration_defect"},{"module":"juristic_act","priority_rank":73,"role_projection":"canceller_counterparty_third_party_roles","type_id":"cancellation_effect"},{"module":"agency","priority_rank":74,"role_projection":"principal_agent_counterparty_roles","type_id":"unauthorized_agency"},{"module":"agency","priority_rank":75,"role_projection":"principal_apparent_agent_relying_party_roles","type_id":"apparent_agency"},{"module":"juristic_act","priority_rank":76,"role_projection":"condition_beneficiary_obligor_roles","type_id":"condition_term_effect"}]}
@@ -0,0 +1,17 @@
{
"schema_version": "s2_module_role_projection.v1",
"domain_id": "E-02",
"projections": [
{
"type_id": "money_claim",
"module": "money_claim",
"role_projection": "claim_chain_ref"
},
{
"type_id": "general_legal_effect",
"module": "legal_effect",
"role_projection": "legal_effect_roles"
}
],
"closure_status": "CLOSED"
}
@@ -0,0 +1,23 @@
{
"schema_version": "s2_domain_structure_types.v1",
"domain_id": "E-02",
"types": [
{
"type_id": "money_claim",
"priority_rank": 1,
"legacy_source": "B1_Money_Successor",
"module": "money_claim",
"role_projection": "claim_chain_ref",
"registered": true
},
{
"type_id": "general_legal_effect",
"priority_rank": 11,
"legacy_source": "B1_Money_Successor",
"module": "legal_effect",
"role_projection": "legal_effect_roles",
"registered": true
}
],
"fallback_policy": "unregistered_effect_type_review; silent_generalization_forbidden"
}
@@ -0,0 +1,22 @@
{
"schema_version": "s2_module_role_projection.v1",
"domain_id": "E-03",
"projections": [
{
"type_id": "money_claim",
"module": "money_claim",
"role_projection": "claim_chain_ref"
},
{
"type_id": "commercial_successor",
"module": "commercial_successor",
"role_projection": "claim_chain_ref"
},
{
"type_id": "general_legal_effect",
"module": "legal_effect",
"role_projection": "legal_effect_roles"
}
],
"closure_status": "CLOSED"
}
@@ -0,0 +1,31 @@
{
"schema_version": "s2_domain_structure_types.v1",
"domain_id": "E-03",
"types": [
{
"type_id": "money_claim",
"priority_rank": 1,
"legacy_source": "B1_Money_Successor",
"module": "money_claim",
"role_projection": "claim_chain_ref",
"registered": true
},
{
"type_id": "commercial_successor",
"priority_rank": 2,
"legacy_source": "B1_Money_Successor",
"module": "commercial_successor",
"role_projection": "claim_chain_ref",
"registered": true
},
{
"type_id": "general_legal_effect",
"priority_rank": 11,
"legacy_source": "B1_Money_Successor",
"module": "legal_effect",
"role_projection": "legal_effect_roles",
"registered": true
}
],
"fallback_policy": "unregistered_effect_type_review; silent_generalization_forbidden"
}
@@ -0,0 +1,30 @@
{
"schema_version": "stage1_domain_module_role_projection.v1",
"domain_id": "E-04",
"status": "ACTIVE",
"mappings": [
{
"structure_type": "land_use_gain",
"module": "land_use_gain",
"role_projection": "legal_calculation_object",
"source": "legacy_preserved_or_additive_registered"
},
{
"structure_type": "valuation",
"module": "valuation",
"role_projection": "legal_calculation_object",
"source": "legacy_preserved_or_additive_registered"
},
{
"structure_type": "unjust_enrichment",
"module": "unjust_enrichment",
"role_projection": "restitution_roles",
"source": "legacy_preserved_or_additive_registered"
}
],
"default_projection": null,
"missing_mapping_policy": {
"emit_review_code": "UNREGISTERED_EFFECT_TYPE_REVIEW",
"silent_fallback_forbidden": true
}
}
@@ -0,0 +1,33 @@
{
"schema_version": "stage1_domain_structure_types.v1",
"domain_id": "E-04",
"status": "ACTIVE",
"legacy_source_domain": "B3",
"structure_types": [
{
"type_id": "land_use_gain",
"priority_rank": 5,
"module": "land_use_gain",
"role_projection": "legal_calculation_object"
},
{
"type_id": "valuation",
"priority_rank": 6,
"module": "valuation",
"role_projection": "legal_calculation_object"
},
{
"type_id": "unjust_enrichment",
"priority_rank": 12,
"module": "unjust_enrichment",
"role_projection": "restitution_roles"
}
],
"admission_rules": {
"registered_membership_required": true,
"multiple_types_allowed": true,
"source_order_deduplicated": true,
"nonregistered_type_review_code": "UNREGISTERED_EFFECT_TYPE_REVIEW",
"silent_general_legal_effect_fallback_forbidden": true
}
}
@@ -0,0 +1,42 @@
{
"schema_version": "stage1_domain_module_role_projection.v1",
"domain_id": "E-05",
"status": "ACTIVE",
"mappings": [
{
"structure_type": "tort_liability",
"module": "tort",
"role_projection": "tort_liability_roles",
"source": "s3_registered"
},
{
"structure_type": "joint_tort_liability",
"module": "tort",
"role_projection": "joint_tort_roles",
"source": "s3_registered"
},
{
"structure_type": "vicarious_liability",
"module": "tort",
"role_projection": "employer_employee_roles",
"source": "s3_registered"
},
{
"structure_type": "structure_liability",
"module": "tort",
"role_projection": "possessor_owner_roles",
"source": "s3_registered"
},
{
"structure_type": "tort_damage_extent",
"module": "damages",
"role_projection": "damage_causation_roles",
"source": "s3_registered"
}
],
"default_projection": null,
"missing_mapping_policy": {
"emit_review_code": "UNREGISTERED_EFFECT_TYPE_REVIEW",
"silent_fallback_forbidden": true
}
}
@@ -0,0 +1,44 @@
{
"schema_version": "stage1_domain_structure_types.v1",
"domain_id": "E-05",
"status": "ACTIVE",
"structure_types": [
{
"type_id": "tort_liability",
"priority_rank": 20,
"module": "tort",
"role_projection": "tort_liability_roles"
},
{
"type_id": "joint_tort_liability",
"priority_rank": 21,
"module": "tort",
"role_projection": "joint_tort_roles"
},
{
"type_id": "vicarious_liability",
"priority_rank": 22,
"module": "tort",
"role_projection": "employer_employee_roles"
},
{
"type_id": "structure_liability",
"priority_rank": 23,
"module": "tort",
"role_projection": "possessor_owner_roles"
},
{
"type_id": "tort_damage_extent",
"priority_rank": 24,
"module": "damages",
"role_projection": "damage_causation_roles"
}
],
"admission_rules": {
"registered_membership_required": true,
"multiple_types_allowed": true,
"source_order_deduplicated": true,
"nonregistered_type_review_code": "UNREGISTERED_EFFECT_TYPE_REVIEW",
"silent_general_legal_effect_fallback_forbidden": true
}
}
@@ -0,0 +1 @@
{"default_projection":null,"domain_id":"E-06","mappings":[{"module":"professional_liability","role_projection":"professional_client_roles","source":"s6_registered","structure_type":"professional_duty_liability"},{"module":"medical_liability","role_projection":"patient_provider_roles","source":"s6_registered","structure_type":"medical_treatment_fault"},{"module":"medical_liability","role_projection":"patient_explainer_consent_roles","source":"s6_registered","structure_type":"medical_disclosure_duty"},{"module":"product_liability","role_projection":"injured_manufacturer_supplier_roles","source":"s6_registered","structure_type":"product_defect_liability"},{"module":"damages","role_projection":"conduct_damage_alternative_cause_roles","source":"s6_registered","structure_type":"professional_damage_causation"}],"missing_mapping_policy":{"emit_review_code":"UNREGISTERED_EFFECT_TYPE_REVIEW","silent_fallback_forbidden":true},"schema_version":"stage1_domain_module_role_projection.v1","status":"ACTIVE"}
@@ -0,0 +1 @@
{"admission_rules":{"multiple_types_allowed":true,"nonregistered_type_review_code":"UNREGISTERED_EFFECT_TYPE_REVIEW","registered_membership_required":true,"silent_general_legal_effect_fallback_forbidden":true,"source_order_deduplicated":true},"domain_id":"E-06","schema_version":"stage1_domain_structure_types.v1","status":"ACTIVE","structure_types":[{"module":"professional_liability","priority_rank":71,"role_projection":"professional_client_roles","type_id":"professional_duty_liability"},{"module":"medical_liability","priority_rank":72,"role_projection":"patient_provider_roles","type_id":"medical_treatment_fault"},{"module":"medical_liability","priority_rank":73,"role_projection":"patient_explainer_consent_roles","type_id":"medical_disclosure_duty"},{"module":"product_liability","priority_rank":74,"role_projection":"injured_manufacturer_supplier_roles","type_id":"product_defect_liability"},{"module":"damages","priority_rank":75,"role_projection":"conduct_damage_alternative_cause_roles","type_id":"professional_damage_causation"}]}
@@ -0,0 +1,32 @@
{
"schema_version": "s3_module_role_projection.v1",
"domain_id": "E-07",
"projections": [
{
"type_id": "construction_payment_claim",
"module": "construction_contract",
"role_projection": "construction_payment_roles"
},
{
"type_id": "construction_progress_settlement",
"module": "construction_contract",
"role_projection": "construction_progress_roles"
},
{
"type_id": "construction_defect_remedy",
"module": "construction_defect",
"role_projection": "construction_defect_roles"
},
{
"type_id": "construction_defect_setoff",
"module": "construction_defect",
"role_projection": "construction_setoff_roles"
},
{
"type_id": "general_legal_effect",
"module": "legal_effect",
"role_projection": "legal_effect_roles"
}
],
"closure_status": "CLOSED"
}
@@ -0,0 +1,42 @@
{
"schema_version": "s3_domain_structure_types.v1",
"domain_id": "E-07",
"types": [
{
"type_id": "construction_payment_claim",
"priority_rank": 1,
"module": "construction_contract",
"role_projection": "construction_payment_roles",
"registered": true
},
{
"type_id": "construction_progress_settlement",
"priority_rank": 2,
"module": "construction_contract",
"role_projection": "construction_progress_roles",
"registered": true
},
{
"type_id": "construction_defect_remedy",
"priority_rank": 3,
"module": "construction_defect",
"role_projection": "construction_defect_roles",
"registered": true
},
{
"type_id": "construction_defect_setoff",
"priority_rank": 4,
"module": "construction_defect",
"role_projection": "construction_setoff_roles",
"registered": true
},
{
"type_id": "general_legal_effect",
"priority_rank": 11,
"module": "legal_effect",
"role_projection": "legal_effect_roles",
"registered": true
}
],
"fallback_policy": "unregistered_effect_type_review; silent_generalization_forbidden"
}
@@ -0,0 +1,37 @@
{
"schema_version": "s3_module_role_projection.v1",
"domain_id": "E-08",
"projections": [
{
"type_id": "lease_deposit_return",
"module": "lease_contract",
"role_projection": "lease_deposit_return_roles"
},
{
"type_id": "lease_rent_payment",
"module": "lease_contract",
"role_projection": "lease_rent_roles"
},
{
"type_id": "lease_termination",
"module": "lease_lifecycle",
"role_projection": "lease_termination_roles"
},
{
"type_id": "lease_delivery_concurrent_performance",
"module": "lease_lifecycle",
"role_projection": "lease_concurrent_performance_roles"
},
{
"type_id": "lease_attached_property_purchase",
"module": "lease_contract",
"role_projection": "lease_purchase_claim_roles"
},
{
"type_id": "general_legal_effect",
"module": "legal_effect",
"role_projection": "legal_effect_roles"
}
],
"closure_status": "CLOSED"
}
@@ -0,0 +1,49 @@
{
"schema_version": "s3_domain_structure_types.v1",
"domain_id": "E-08",
"types": [
{
"type_id": "lease_deposit_return",
"priority_rank": 1,
"module": "lease_contract",
"role_projection": "lease_deposit_return_roles",
"registered": true
},
{
"type_id": "lease_rent_payment",
"priority_rank": 2,
"module": "lease_contract",
"role_projection": "lease_rent_roles",
"registered": true
},
{
"type_id": "lease_termination",
"priority_rank": 3,
"module": "lease_lifecycle",
"role_projection": "lease_termination_roles",
"registered": true
},
{
"type_id": "lease_delivery_concurrent_performance",
"priority_rank": 4,
"module": "lease_lifecycle",
"role_projection": "lease_concurrent_performance_roles",
"registered": true
},
{
"type_id": "lease_attached_property_purchase",
"priority_rank": 5,
"module": "lease_contract",
"role_projection": "lease_purchase_claim_roles",
"registered": true
},
{
"type_id": "general_legal_effect",
"priority_rank": 11,
"module": "legal_effect",
"role_projection": "legal_effect_roles",
"registered": true
}
],
"fallback_policy": "unregistered_effect_type_review; silent_generalization_forbidden"
}
@@ -0,0 +1,17 @@
{
"schema_version": "s2_module_role_projection.v1",
"domain_id": "E-09",
"projections": [
{
"type_id": "registry_invalidity",
"module": "registry_invalidity",
"role_projection": "linked_structures"
},
{
"type_id": "general_legal_effect",
"module": "legal_effect",
"role_projection": "legal_effect_roles"
}
],
"closure_status": "CLOSED"
}
@@ -0,0 +1,23 @@
{
"schema_version": "s2_domain_structure_types.v1",
"domain_id": "E-09",
"types": [
{
"type_id": "registry_invalidity",
"priority_rank": 4,
"legacy_source": "B2_Secured_Registry",
"module": "registry_invalidity",
"role_projection": "linked_structures",
"registered": true
},
{
"type_id": "general_legal_effect",
"priority_rank": 11,
"legacy_source": "B2_Secured_Registry",
"module": "legal_effect",
"role_projection": "legal_effect_roles",
"registered": true
}
],
"fallback_policy": "unregistered_effect_type_review; silent_generalization_forbidden"
}
@@ -0,0 +1,27 @@
{
"schema_version": "s2_module_role_projection.v1",
"domain_id": "E-10",
"projections": [
{
"type_id": "secured_debt",
"module": "secured_property",
"role_projection": "linked_structures"
},
{
"type_id": "registry_invalidity",
"module": "registry_invalidity",
"role_projection": "linked_structures"
},
{
"type_id": "succession_notice_lien_asset_defense",
"module": "succession_notice_lien_asset_defense",
"role_projection": "linked_structures"
},
{
"type_id": "general_legal_effect",
"module": "legal_effect",
"role_projection": "legal_effect_roles"
}
],
"closure_status": "CLOSED"
}
@@ -0,0 +1,39 @@
{
"schema_version": "s2_domain_structure_types.v1",
"domain_id": "E-10",
"types": [
{
"type_id": "secured_debt",
"priority_rank": 3,
"legacy_source": "B2_Secured_Registry",
"module": "secured_property",
"role_projection": "linked_structures",
"registered": true
},
{
"type_id": "registry_invalidity",
"priority_rank": 4,
"legacy_source": "B2_Secured_Registry",
"module": "registry_invalidity",
"role_projection": "linked_structures",
"registered": true
},
{
"type_id": "succession_notice_lien_asset_defense",
"priority_rank": 10,
"legacy_source": "B2_Secured_Registry",
"module": "succession_notice_lien_asset_defense",
"role_projection": "linked_structures",
"registered": true
},
{
"type_id": "general_legal_effect",
"priority_rank": 11,
"legacy_source": "B2_Secured_Registry",
"module": "legal_effect",
"role_projection": "legal_effect_roles",
"registered": true
}
],
"fallback_policy": "unregistered_effect_type_review; silent_generalization_forbidden"
}
@@ -0,0 +1,30 @@
{
"schema_version": "stage1_domain_module_role_projection.v1",
"domain_id": "E-11",
"status": "ACTIVE",
"mappings": [
{
"structure_type": "possession_vindication",
"module": "possession_vindication",
"role_projection": "linked_structures",
"source": "legacy_preserved_or_additive_registered"
},
{
"structure_type": "land_use_gain",
"module": "land_use_gain",
"role_projection": "legal_calculation_object",
"source": "legacy_preserved_or_additive_registered"
},
{
"structure_type": "succession_notice_lien_asset_defense",
"module": "succession_notice_lien_asset_defense",
"role_projection": "linked_structures",
"source": "legacy_preserved_or_additive_registered"
}
],
"default_projection": null,
"missing_mapping_policy": {
"emit_review_code": "UNREGISTERED_EFFECT_TYPE_REVIEW",
"silent_fallback_forbidden": true
}
}
@@ -0,0 +1,33 @@
{
"schema_version": "stage1_domain_structure_types.v1",
"domain_id": "E-11",
"status": "ACTIVE",
"legacy_source_domain": "B3",
"structure_types": [
{
"type_id": "possession_vindication",
"priority_rank": 5,
"module": "possession_vindication",
"role_projection": "linked_structures"
},
{
"type_id": "land_use_gain",
"priority_rank": 6,
"module": "land_use_gain",
"role_projection": "legal_calculation_object"
},
{
"type_id": "succession_notice_lien_asset_defense",
"priority_rank": 10,
"module": "succession_notice_lien_asset_defense",
"role_projection": "linked_structures"
}
],
"admission_rules": {
"registered_membership_required": true,
"multiple_types_allowed": true,
"source_order_deduplicated": true,
"nonregistered_type_review_code": "UNREGISTERED_EFFECT_TYPE_REVIEW",
"silent_general_legal_effect_fallback_forbidden": true
}
}
@@ -0,0 +1 @@
{"default_projection":null,"domain_id":"E-12","mappings":[{"module":"coownership","role_projection":"coowner_act_consent_roles","source":"s6_registered","structure_type":"coownership_governance"},{"module":"partition","role_projection":"partitioner_coowner_object_roles","source":"s6_registered","structure_type":"coownership_partition"},{"module":"boundary","role_projection":"adjacent_owner_parcel_boundary_roles","source":"s6_registered","structure_type":"boundary_determination"},{"module":"coownership","role_projection":"using_coowner_nonusing_coowner_roles","source":"s6_registered","structure_type":"exclusive_use_settlement"},{"module":"aggregate_building","role_projection":"management_body_unit_owner_roles","source":"s6_registered","structure_type":"aggregate_building_management"}],"missing_mapping_policy":{"emit_review_code":"UNREGISTERED_EFFECT_TYPE_REVIEW","silent_fallback_forbidden":true},"schema_version":"stage1_domain_module_role_projection.v1","status":"ACTIVE"}
@@ -0,0 +1 @@
{"admission_rules":{"multiple_types_allowed":true,"nonregistered_type_review_code":"UNREGISTERED_EFFECT_TYPE_REVIEW","registered_membership_required":true,"silent_general_legal_effect_fallback_forbidden":true,"source_order_deduplicated":true},"domain_id":"E-12","schema_version":"stage1_domain_structure_types.v1","status":"ACTIVE","structure_types":[{"module":"coownership","priority_rank":71,"role_projection":"coowner_act_consent_roles","type_id":"coownership_governance"},{"module":"partition","priority_rank":72,"role_projection":"partitioner_coowner_object_roles","type_id":"coownership_partition"},{"module":"boundary","priority_rank":73,"role_projection":"adjacent_owner_parcel_boundary_roles","type_id":"boundary_determination"},{"module":"coownership","priority_rank":74,"role_projection":"using_coowner_nonusing_coowner_roles","type_id":"exclusive_use_settlement"},{"module":"aggregate_building","priority_rank":75,"role_projection":"management_body_unit_owner_roles","type_id":"aggregate_building_management"}]}
@@ -0,0 +1,30 @@
{
"schema_version": "stage1_domain_module_role_projection.v1",
"domain_id": "E-13",
"status": "ACTIVE",
"mappings": [
{
"structure_type": "fraudulent_transfer",
"module": "actio",
"role_projection": "actio_roles",
"source": "legacy_preserved_or_additive_registered"
},
{
"structure_type": "preserved_claim",
"module": "actio",
"role_projection": "claim_chain_ref",
"source": "legacy_preserved_or_additive_registered"
},
{
"structure_type": "actio_remedy_or_cap",
"module": "actio",
"role_projection": "linked_actio_structures",
"source": "legacy_preserved_or_additive_registered"
}
],
"default_projection": null,
"missing_mapping_policy": {
"emit_review_code": "UNREGISTERED_EFFECT_TYPE_REVIEW",
"silent_fallback_forbidden": true
}
}
@@ -0,0 +1,33 @@
{
"schema_version": "stage1_domain_structure_types.v1",
"domain_id": "E-13",
"status": "ACTIVE",
"legacy_source_domain": "B4",
"structure_types": [
{
"type_id": "fraudulent_transfer",
"priority_rank": 7,
"module": "actio",
"role_projection": "actio_roles"
},
{
"type_id": "preserved_claim",
"priority_rank": 8,
"module": "actio",
"role_projection": "claim_chain_ref"
},
{
"type_id": "actio_remedy_or_cap",
"priority_rank": 9,
"module": "actio",
"role_projection": "linked_actio_structures"
}
],
"admission_rules": {
"registered_membership_required": true,
"multiple_types_allowed": true,
"source_order_deduplicated": true,
"nonregistered_type_review_code": "UNREGISTERED_EFFECT_TYPE_REVIEW",
"silent_general_legal_effect_fallback_forbidden": true
}
}
@@ -0,0 +1,16 @@
{
"schema_version": "stage1_s3_module_role_projection.v1",
"domain_id": "E-14",
"structure_type_to_projection": {
"execution_linked_claim": {
"module": "execution_linked_claims",
"role_projection": "linked_execution_claims"
}
},
"default_projection": {
"module": "unregistered_effect_review",
"role_projection": "unregistered_effect_roles",
"review_code": "UNREGISTERED_EFFECT_TYPE_REVIEW"
},
"closure_status": "CLOSED_WITH_LOCAL_FILES"
}
@@ -0,0 +1,15 @@
{
"schema_version": "stage1_s3_structure_types.v1",
"domain_id": "E-14",
"entries": [
{
"type_id": "execution_linked_claim",
"priority_rank": 14,
"module": "execution_linked_claims",
"role_projection": "linked_execution_claims",
"admission_rule": "source-backed SG-13 route and registered active domain required"
}
],
"unregistered_effect_policy": "UNREGISTERED_EFFECT_TYPE_REVIEW",
"ordering_integration_required": true
}
@@ -0,0 +1,24 @@
{
"schema_version": "stage1_s2_module_role_projection.v1",
"domain_id": "E-15",
"legacy_projection_source": {
"path": "S_0_GPT/legacy/part4/structure_projection_contract.json",
"sha256": "9e615dda7e26e23fcaae381ed05763d16e4887909400277c9996919e09398398"
},
"structure_type_to_projection": {
"succession_notice_lien_asset_defense": {
"module": "succession_notice_lien_asset_defense",
"role_projection": "linked_structures"
},
"general_legal_effect": {
"module": "legal_effect",
"role_projection": "legal_effect_roles"
}
},
"default_projection": {
"module": "legal_effect",
"role_projection": "legal_effect_roles",
"review_code": "UNREGISTERED_EFFECT_TYPE_REVIEW"
},
"closure_status": "CLOSED_WITH_LOCAL_FILES"
}
@@ -0,0 +1,28 @@
{
"schema_version": "stage1_s2_structure_types.v1",
"domain_id": "E-15",
"legacy_contract": {
"canonical_type_set_frozen": true,
"source": "1. Stage_1/v.7/extension_research/선행구축/S_0_GPT/legacy/part3/structure_type_contract.json"
},
"entries": [
{
"type_id": "succession_notice_lien_asset_defense",
"priority_rank": 10,
"legacy_source": "B5",
"module": "succession_notice_lien_asset_defense",
"role_projection": "linked_structures",
"admission_rule": "domain payload or SG-13 candidate must be source-backed"
},
{
"type_id": "general_legal_effect",
"priority_rank": 11,
"legacy_source": "Part3 fallback",
"module": "legal_effect",
"role_projection": "legal_effect_roles",
"admission_rule": "registered route only; E-00 unregistered material remains review"
}
],
"noncanonical_preservation": "nonrouting_labels",
"ordering_preserved": true
}
@@ -0,0 +1 @@
{"default_projection":null,"domain_id":"E-16","mappings":[{"module":"negotiable_instrument","role_projection":"issuer_acceptor_holder_roles","source":"s6_registered","structure_type":"bill_primary_liability"},{"module":"negotiable_instrument","role_projection":"drawer_payee_holder_roles","source":"s6_registered","structure_type":"check_issuer_liability"},{"module":"negotiable_instrument","role_projection":"endorser_endorsee_holder_roles","source":"s6_registered","structure_type":"endorsement_holder_status"},{"module":"negotiable_instrument","role_projection":"presenter_drawee_recourse_party_roles","source":"s6_registered","structure_type":"presentment_recourse_liability"},{"module":"negotiable_instrument","role_projection":"grantor_completer_holder_roles","source":"s6_registered","structure_type":"blank_instrument_completion"},{"module":"negotiable_instrument","role_projection":"instrument_debtor_holder_cause_party_roles","source":"s6_registered","structure_type":"cause_relation_defense"}],"missing_mapping_policy":{"emit_review_code":"UNREGISTERED_EFFECT_TYPE_REVIEW","silent_fallback_forbidden":true},"schema_version":"stage1_domain_module_role_projection.v1","status":"ACTIVE"}
@@ -0,0 +1 @@
{"admission_rules":{"multiple_types_allowed":true,"nonregistered_type_review_code":"UNREGISTERED_EFFECT_TYPE_REVIEW","registered_membership_required":true,"silent_general_legal_effect_fallback_forbidden":true,"source_order_deduplicated":true},"domain_id":"E-16","schema_version":"stage1_domain_structure_types.v1","status":"ACTIVE","structure_types":[{"module":"negotiable_instrument","priority_rank":71,"role_projection":"issuer_acceptor_holder_roles","type_id":"bill_primary_liability"},{"module":"negotiable_instrument","priority_rank":72,"role_projection":"drawer_payee_holder_roles","type_id":"check_issuer_liability"},{"module":"negotiable_instrument","priority_rank":73,"role_projection":"endorser_endorsee_holder_roles","type_id":"endorsement_holder_status"},{"module":"negotiable_instrument","priority_rank":74,"role_projection":"presenter_drawee_recourse_party_roles","type_id":"presentment_recourse_liability"},{"module":"negotiable_instrument","priority_rank":75,"role_projection":"grantor_completer_holder_roles","type_id":"blank_instrument_completion"},{"module":"negotiable_instrument","priority_rank":76,"role_projection":"instrument_debtor_holder_cause_party_roles","type_id":"cause_relation_defense"}]}
@@ -0,0 +1,16 @@
{
"schema_version": "stage1_s3_module_role_projection.v1",
"domain_id": "E-17",
"structure_type_to_projection": {
"labor_wage_claim": {
"module": "labor_wage_claims",
"role_projection": "linked_labor_claims"
}
},
"default_projection": {
"module": "unregistered_effect_review",
"role_projection": "unregistered_effect_roles",
"review_code": "UNREGISTERED_EFFECT_TYPE_REVIEW"
},
"closure_status": "CLOSED_WITH_LOCAL_FILES"
}
@@ -0,0 +1,15 @@
{
"schema_version": "stage1_s3_structure_types.v1",
"domain_id": "E-17",
"entries": [
{
"type_id": "labor_wage_claim",
"priority_rank": 17,
"module": "labor_wage_claims",
"role_projection": "linked_labor_claims",
"admission_rule": "source-backed SG-13 route and registered active domain required"
}
],
"unregistered_effect_policy": "UNREGISTERED_EFFECT_TYPE_REVIEW",
"ordering_integration_required": true
}
@@ -0,0 +1 @@
{"closure_status":"CLOSED_WITH_LOCAL_FILES","default_projection":{"module":"unregistered_effect_review","review_code":"UNREGISTERED_EFFECT_TYPE_REVIEW","role_projection":"unregistered_effect_roles"},"domain_id":"E-18","schema_version":"stage1_s6_module_role_projection.v1","structure_type_to_projection":{"org_resolution_status_relation":{"module":"org_resolution_status","role_projection":"resolution_status_roles"}}}
@@ -0,0 +1 @@
{"domain_id":"E-18","entries":[{"admission_rule":"source-backed SG-13 route and registered active domain required","module":"org_resolution_status","priority_rank":18,"role_projection":"resolution_status_roles","type_id":"org_resolution_status_relation"}],"ordering_integration_required":true,"schema_version":"stage1_s6_structure_types.v1","unregistered_effect_policy":"UNREGISTERED_EFFECT_TYPE_REVIEW"}
@@ -0,0 +1 @@
{"closure_status":"CLOSED_WITH_LOCAL_FILES","default_projection":{"module":"unregistered_effect_review","review_code":"UNREGISTERED_EFFECT_TYPE_REVIEW","role_projection":"unregistered_effect_roles"},"domain_id":"E-19","schema_version":"stage1_s6_module_role_projection.v1","structure_type_to_projection":{"insurance_claim_relation":{"module":"insurance_claims","role_projection":"insurance_relation_roles"}}}
@@ -0,0 +1 @@
{"domain_id":"E-19","entries":[{"admission_rule":"source-backed SG-13 route and registered active domain required","module":"insurance_claims","priority_rank":19,"role_projection":"insurance_relation_roles","type_id":"insurance_claim_relation"}],"ordering_integration_required":true,"schema_version":"stage1_s6_structure_types.v1","unregistered_effect_policy":"UNREGISTERED_EFFECT_TYPE_REVIEW"}
@@ -0,0 +1 @@
{"closure_status":"CLOSED_WITH_LOCAL_FILES","default_projection":{"module":"unregistered_effect_review","review_code":"UNREGISTERED_EFFECT_TYPE_REVIEW","role_projection":"unregistered_effect_roles"},"domain_id":"E-20","schema_version":"stage1_s6_module_role_projection.v1","structure_type_to_projection":{"ip_right_claim_relation":{"module":"ip_claims","role_projection":"ip_right_infringement_roles"}}}
@@ -0,0 +1 @@
{"domain_id":"E-20","entries":[{"admission_rule":"source-backed SG-13 route and registered active domain required","module":"ip_claims","priority_rank":20,"role_projection":"ip_right_infringement_roles","type_id":"ip_right_claim_relation"}],"ordering_integration_required":true,"schema_version":"stage1_s6_structure_types.v1","unregistered_effect_policy":"UNREGISTERED_EFFECT_TYPE_REVIEW"}
@@ -0,0 +1 @@
{"closure_status":"CLOSED_WITH_LOCAL_FILES","default_projection":{"module":"unregistered_effect_review","review_code":"UNREGISTERED_EFFECT_TYPE_REVIEW","role_projection":"unregistered_effect_roles"},"domain_id":"E-21","schema_version":"stage1_s6_module_role_projection.v1","structure_type_to_projection":{"media_personality_claim_relation":{"module":"media_personality_rights","role_projection":"publication_personality_roles"}}}
@@ -0,0 +1 @@
{"domain_id":"E-21","entries":[{"admission_rule":"source-backed SG-13 route and registered active domain required","module":"media_personality_rights","priority_rank":21,"role_projection":"publication_personality_roles","type_id":"media_personality_claim_relation"}],"ordering_integration_required":true,"schema_version":"stage1_s6_structure_types.v1","unregistered_effect_policy":"UNREGISTERED_EFFECT_TYPE_REVIEW"}
@@ -0,0 +1,24 @@
{
"schema_version": "stage1_s2_module_role_projection.v1",
"domain_id": "X1",
"legacy_projection_source": {
"path": "S_0_GPT/legacy/part4/structure_projection_contract.json",
"sha256": "9e615dda7e26e23fcaae381ed05763d16e4887909400277c9996919e09398398"
},
"structure_type_to_projection": {
"succession_notice_lien_asset_defense": {
"module": "succession_notice_lien_asset_defense",
"role_projection": "linked_structures"
},
"general_legal_effect": {
"module": "legal_effect",
"role_projection": "legal_effect_roles"
}
},
"default_projection": {
"module": "legal_effect",
"role_projection": "legal_effect_roles",
"review_code": "UNREGISTERED_EFFECT_TYPE_REVIEW"
},
"closure_status": "CLOSED_WITH_LOCAL_FILES"
}
@@ -0,0 +1,28 @@
{
"schema_version": "stage1_s2_structure_types.v1",
"domain_id": "X1",
"legacy_contract": {
"canonical_type_set_frozen": true,
"source": "1. Stage_1/v.7/extension_research/선행구축/S_0_GPT/legacy/part3/structure_type_contract.json"
},
"entries": [
{
"type_id": "succession_notice_lien_asset_defense",
"priority_rank": 10,
"legacy_source": "B5",
"module": "succession_notice_lien_asset_defense",
"role_projection": "linked_structures",
"admission_rule": "domain payload or SG-13 candidate must be source-backed"
},
{
"type_id": "general_legal_effect",
"priority_rank": 11,
"legacy_source": "Part3 fallback",
"module": "legal_effect",
"role_projection": "legal_effect_roles",
"admission_rule": "registered route only; E-00 unregistered material remains review"
}
],
"noncanonical_preservation": "nonrouting_labels",
"ordering_preserved": true
}
@@ -0,0 +1,24 @@
{
"schema_version": "stage1_domain_module_role_projection.v1",
"domain_id": "X2",
"status": "ACTIVE",
"mappings": [
{
"structure_type": "asset_state",
"module": "asset_state",
"role_projection": "asset_state_projection",
"source": "legacy_preserved_or_additive_registered"
},
{
"structure_type": "valuation",
"module": "valuation",
"role_projection": "legal_calculation_object",
"source": "legacy_preserved_or_additive_registered"
}
],
"default_projection": null,
"missing_mapping_policy": {
"emit_review_code": "UNREGISTERED_EFFECT_TYPE_REVIEW",
"silent_fallback_forbidden": true
}
}
@@ -0,0 +1,27 @@
{
"schema_version": "stage1_domain_structure_types.v1",
"domain_id": "X2",
"status": "ACTIVE",
"legacy_source_domain": "B3",
"structure_types": [
{
"type_id": "asset_state",
"priority_rank": 4,
"module": "asset_state",
"role_projection": "asset_state_projection"
},
{
"type_id": "valuation",
"priority_rank": 6,
"module": "valuation",
"role_projection": "legal_calculation_object"
}
],
"admission_rules": {
"registered_membership_required": true,
"multiple_types_allowed": true,
"source_order_deduplicated": true,
"nonregistered_type_review_code": "UNREGISTERED_EFFECT_TYPE_REVIEW",
"silent_general_legal_effect_fallback_forbidden": true
}
}
@@ -0,0 +1,24 @@
{
"schema_version": "stage1_s2_module_role_projection.v1",
"domain_id": "X3",
"legacy_projection_source": {
"path": "S_0_GPT/legacy/part4/structure_projection_contract.json",
"sha256": "9e615dda7e26e23fcaae381ed05763d16e4887909400277c9996919e09398398"
},
"structure_type_to_projection": {
"succession_notice_lien_asset_defense": {
"module": "succession_notice_lien_asset_defense",
"role_projection": "linked_structures"
},
"general_legal_effect": {
"module": "legal_effect",
"role_projection": "legal_effect_roles"
}
},
"default_projection": {
"module": "legal_effect",
"role_projection": "legal_effect_roles",
"review_code": "UNREGISTERED_EFFECT_TYPE_REVIEW"
},
"closure_status": "CLOSED_WITH_LOCAL_FILES"
}
@@ -0,0 +1,28 @@
{
"schema_version": "stage1_s2_structure_types.v1",
"domain_id": "X3",
"legacy_contract": {
"canonical_type_set_frozen": true,
"source": "1. Stage_1/v.7/extension_research/선행구축/S_0_GPT/legacy/part3/structure_type_contract.json"
},
"entries": [
{
"type_id": "succession_notice_lien_asset_defense",
"priority_rank": 10,
"legacy_source": "B5",
"module": "succession_notice_lien_asset_defense",
"role_projection": "linked_structures",
"admission_rule": "domain payload or SG-13 candidate must be source-backed"
},
{
"type_id": "general_legal_effect",
"priority_rank": 11,
"legacy_source": "Part3 fallback",
"module": "legal_effect",
"role_projection": "legal_effect_roles",
"admission_rule": "registered route only; E-00 unregistered material remains review"
}
],
"noncanonical_preservation": "nonrouting_labels",
"ordering_preserved": true
}
@@ -0,0 +1,150 @@
{
"$id": "https://schemas.liti-agent.local/stage1/part3/legal_effect_structures.schema.json",
"title": "stage1_legal_effect_structures.v1",
"description": "Part 3 L2 최종 산출 — 법률효과 구조와 역인덱스 셋(by_domain_id · by_module · by_bo_id). by_claim_form 은 만들지 않는다(claim-form 어휘가 registry 에 없다 — D-4 라). 열거를 하드코딩하지 않는다.",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "status", "expected_runnable_domain_ids", "theta",
"structure_records", "structure_index", "quality_gate", "created_by"],
"properties": {
"schema_version": {"const": "stage1_legal_effect_structures.v1"},
"status": {"enum": ["READY", "READY_WITH_REVIEW"]},
"registry_version": {"type": ["string", "null"]},
"registry_index_sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
"activation_manifest_sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
"signal_manifest_transaction_id": {"type": "string", "pattern": "^S5TX-[a-f0-9]{20}$"},
"expected_runnable_domain_ids": {
"type": "array",
"items": {"type": "string", "pattern": "^(?:(?:EC|E)-[A-Z0-9]{2,8}|X[A-Z0-9]{1,8})$"}
},
"theta": {
"type": "array",
"items": {"type": "string", "pattern": "^[a-z][a-z0-9_]{1,127}$"}
},
"structure_records": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["structure_id", "domain_id", "type_id", "priority_rank", "order_key",
"module", "role_projection", "admission_policy", "routes", "route_count",
"evidence_refs", "meeting_clause_refs", "calculation_request_refs",
"source_bo_ids", "review_codes", "review_required"],
"properties": {
"structure_id": {"type": "string", "pattern": "^LES-[A-Za-z0-9._:-]{2,64}-[a-z][a-z0-9_]{1,127}$"},
"domain_id": {"type": "string", "pattern": "^(?:(?:EC|E)-[A-Z0-9]{2,8}|X[A-Z0-9]{1,8})$"},
"type_id": {"type": "string", "pattern": "^[a-z][a-z0-9_]{1,127}$"},
"priority_rank": {"type": ["integer", "null"]},
"order_key": {"type": "array", "minItems": 3, "maxItems": 3},
"module": {"type": ["string", "null"]},
"role_projection": {"type": ["string", "null"]},
"admission_policy": {"type": "string", "minLength": 1},
"admission_rule": {"type": "string"},
"registered_declared": {"type": "boolean"},
"extension_key": {"type": "string"},
"routes": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["signal_id", "source_seed_id", "registered", "status"],
"properties": {
"signal_id": {"type": ["string", "null"]},
"source_seed_id": {"type": ["string", "null"]},
"registered": {"type": ["boolean", "null"]},
"status": {"type": ["string", "null"]}
}
}
},
"route_count": {"type": "integer", "minimum": 0},
"evidence_refs": {"type": "array", "items": {"type": "string"}},
"meeting_clause_refs": {"type": "array", "items": {"type": "string"}},
"calculation_request_refs": {"type": "array", "items": {"type": "string"}},
"source_bo_ids": {"type": "array", "items": {"type": "string"}},
"review_codes": {
"type": "array",
"items": {"type": "string", "pattern": "^[A-Z][A-Z0-9_]{2,127}$"}
},
"review_required": {"type": "boolean"}
}
}
},
"structure_index": {
"type": "object",
"additionalProperties": false,
"required": ["by_domain_id", "by_module", "by_bo_id"],
"properties": {
"by_domain_id": {
"type": "object",
"additionalProperties": {"type": "array", "items": {"type": "string"}}
},
"by_module": {
"type": "object",
"additionalProperties": {"type": "array", "items": {"type": "string"}}
},
"by_bo_id": {
"type": "object",
"additionalProperties": {"type": "array", "items": {"type": "string"}}
}
}
},
"quality_gate": {
"type": "object",
"additionalProperties": false,
"required": ["declaration_conservation", "route_conservation", "bo_conservation",
"review_queue"],
"properties": {
"declaration_conservation": {
"type": "object",
"additionalProperties": false,
"required": ["structure_records", "declared_total_from_config", "conservation_pass"],
"properties": {
"structure_records": {"type": "integer", "minimum": 0},
"declared_total_from_config": {"type": "integer", "minimum": 0},
"conservation_pass": {"type": "boolean"}
}
},
"route_conservation": {
"type": "object",
"additionalProperties": false,
"required": ["routes_attached", "routes_unattached", "sg13_route_total_from_source",
"conservation_pass"],
"properties": {
"routes_attached": {"type": "integer", "minimum": 0},
"routes_unattached": {"type": "integer", "minimum": 0},
"sg13_route_total_from_source": {"type": "integer", "minimum": 0},
"conservation_pass": {"type": "boolean"}
}
},
"bo_conservation": {
"type": "object",
"additionalProperties": false,
"required": ["bo_attached", "bo_unattached", "bo_total_from_source",
"conservation_pass"],
"properties": {
"bo_attached": {"type": "integer", "minimum": 0},
"bo_unattached": {"type": "integer", "minimum": 0},
"bo_total_from_source": {"type": "integer", "minimum": 0},
"conservation_pass": {"type": "boolean"}
}
},
"review_queue": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["review_code", "severity"],
"properties": {
"review_code": {"type": "string", "pattern": "^[A-Z][A-Z0-9_]{2,127}$"},
"severity": {"enum": ["SOFT_WARNING", "HARD_WARNING"]},
"domain_id": {"type": ["string", "null"]},
"structure_id": {"type": ["string", "null"]},
"count": {"type": "integer", "minimum": 1}
}
}
}
}
},
"created_by": {"const": "Task_C_LE_L2_final_structure_index_writer"}
}
}
@@ -0,0 +1,331 @@
{
"$id": "https://schemas.liti-agent.local/stage1/part3/structure_seed_bundle.schema.json",
"title": "stage1_structure_seed_bundle.v1",
"description": "Part 3 L0 산출 — 구조 seed 묶음. 레코드는 Θ 의 (도메인, type_id) 쌍마다 하나. 열거를 하드코딩하지 않는다 — type_id·도메인 ID·검토 코드는 패턴만 강제한다(도메인 증설 시 스키마 무수정).",
"type": "object",
"additionalProperties": false,
"required": [
"structure_seed_bundle"
],
"properties": {
"structure_seed_bundle": {
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"status",
"expected_runnable_domain_ids",
"theta",
"structure_records",
"unattached_routes",
"unattached_bo",
"counts",
"created_by"
],
"properties": {
"schema_version": {
"const": "stage1_structure_seed_bundle.v1"
},
"status": {
"enum": [
"READY",
"READY_WITH_REVIEW"
]
},
"registry_version": {
"type": [
"string",
"null"
]
},
"expected_runnable_domain_ids": {
"type": "array",
"items": {
"type": "string",
"pattern": "^(?:(?:EC|E)-[A-Z0-9]{2,8}|X[A-Z0-9]{1,8})$"
}
},
"theta": {
"type": "array",
"items": {
"type": "string",
"pattern": "^[a-z][a-z0-9_]{1,127}$"
}
},
"structure_records": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"structure_id",
"domain_id",
"type_id",
"priority_rank",
"order_key",
"module",
"role_projection",
"admission_policy",
"routes",
"route_count",
"evidence_refs",
"meeting_clause_refs",
"calculation_request_refs",
"source_bo_ids",
"review_codes",
"review_required"
],
"properties": {
"structure_id": {
"type": "string",
"pattern": "^LES-[A-Za-z0-9._:-]{2,64}-[a-z][a-z0-9_]{1,127}$"
},
"domain_id": {
"type": "string",
"pattern": "^(?:(?:EC|E)-[A-Z0-9]{2,8}|X[A-Z0-9]{1,8})$"
},
"type_id": {
"type": "string",
"pattern": "^[a-z][a-z0-9_]{1,127}$"
},
"priority_rank": {
"type": [
"integer",
"null"
]
},
"order_key": {
"type": "array",
"minItems": 3,
"maxItems": 3
},
"module": {
"type": [
"string",
"null"
]
},
"role_projection": {
"type": [
"string",
"null"
]
},
"admission_policy": {
"type": "string",
"minLength": 1
},
"admission_rule": {
"type": "string"
},
"registered_declared": {
"type": "boolean"
},
"extension_key": {
"type": "string"
},
"routes": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"signal_id",
"source_seed_id",
"registered",
"status"
],
"properties": {
"signal_id": {
"type": [
"string",
"null"
]
},
"source_seed_id": {
"type": [
"string",
"null"
]
},
"registered": {
"type": [
"boolean",
"null"
]
},
"status": {
"type": [
"string",
"null"
]
}
}
}
},
"route_count": {
"type": "integer",
"minimum": 0
},
"evidence_refs": {
"type": "array",
"items": {
"type": "string"
}
},
"meeting_clause_refs": {
"type": "array",
"items": {
"type": "string"
}
},
"calculation_request_refs": {
"type": "array",
"items": {
"type": "string"
}
},
"source_bo_ids": {
"type": "array",
"items": {
"type": "string"
}
},
"review_codes": {
"type": "array",
"items": {
"type": "string",
"pattern": "^[A-Z][A-Z0-9_]{2,127}$"
}
},
"review_required": {
"type": "boolean"
}
}
}
},
"unattached_routes": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"signal_id",
"type_id",
"reason"
],
"properties": {
"signal_id": {
"type": [
"string",
"null"
]
},
"type_id": {
"type": [
"string",
"null"
]
},
"reason": {
"type": "string"
}
}
}
},
"unattached_bo": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"bo_id",
"reason"
],
"properties": {
"bo_id": {
"type": [
"string",
"null"
]
},
"reason": {
"type": "string"
}
}
}
},
"counts": {
"type": "object",
"additionalProperties": false,
"required": [
"structure_records",
"declared_structure_types_total",
"sg13_route_total",
"routes_attached",
"routes_unattached",
"bo_total",
"bo_attached",
"bo_unattached",
"sg11_total",
"sg11_linked",
"sg11_unlinked"
],
"properties": {
"structure_records": {
"type": "integer",
"minimum": 0
},
"declared_structure_types_total": {
"type": "integer",
"minimum": 0
},
"sg13_route_total": {
"type": "integer",
"minimum": 0
},
"routes_attached": {
"type": "integer",
"minimum": 0
},
"routes_unattached": {
"type": "integer",
"minimum": 0
},
"bo_total": {
"type": "integer",
"minimum": 0
},
"bo_attached": {
"type": "integer",
"minimum": 0
},
"bo_unattached": {
"type": "integer",
"minimum": 0
},
"sg11_total": {
"type": "integer",
"minimum": 0
},
"sg11_linked": {
"type": "integer",
"minimum": 0
},
"sg11_unlinked": {
"type": "integer",
"minimum": 0
}
}
},
"created_by": {
"const": "Task_C_LE_L0_structure_seed_reducer"
},
"signal_manifest_transaction_id": {
"type": "string",
"pattern": "^S5TX-[a-f0-9]{20}$"
}
}
}
}
}
@@ -824,6 +824,206 @@
"path": "contracts/signals/s5_execution_contract.v2.json",
"sha256": "311ac8721ec297bc86e54c83f0866d61629bd99f15746f86bbbff32914d35e02"
},
{
"path": "domains/E-00/module_role_projection.json",
"sha256": "278ecdd4ac6c004f9994d6357ef439d089456a5e65bed0f74f49170ba3315932"
},
{
"path": "domains/E-00/structure_types.json",
"sha256": "ae4a94fdbb0e2a8e0273a259e38c28774a3f2f24985878286f18a9ae05cbabaa"
},
{
"path": "domains/E-01/module_role_projection.json",
"sha256": "cb263ca339f7b749a51b57fa12d915fda3644f09e648d15a84202651a23a84fb"
},
{
"path": "domains/E-01/structure_types.json",
"sha256": "f2c423a0346a347292b3ca33d3a9f33a82d540e83cbb5f79bb8dae7161c68b26"
},
{
"path": "domains/E-02/module_role_projection.json",
"sha256": "e21465cf806fb080d75f89526f9c142010c392ee87454a5a19248b9a80eb7c28"
},
{
"path": "domains/E-02/structure_types.json",
"sha256": "e470dd3c96f2f9b8bfa6c2c97b05259281005a84b6eea24c38d322abdf16ad8e"
},
{
"path": "domains/E-03/module_role_projection.json",
"sha256": "f693146dfb3ba2ac5fa6a6221c6b2dc7e32084acb06823b1f46f423a56546704"
},
{
"path": "domains/E-03/structure_types.json",
"sha256": "70c579a03c41226e6383fe7b4aa5e60f4b0f5a723db30f6292dd2a0bde4d17bb"
},
{
"path": "domains/E-04/module_role_projection.json",
"sha256": "4c4bd473a12ad1dfcc8c9392503cd057017385a21287a89cb7ea147f3cf7d2d6"
},
{
"path": "domains/E-04/structure_types.json",
"sha256": "e8b15e9e5a39a488bf9ef660d69c7bc2db832248eb33788ba2ae54a9b5200e73"
},
{
"path": "domains/E-05/module_role_projection.json",
"sha256": "7fabfa3893a138dccb4e5bc797444f5f999c0e51ec6b0c8fbe0211eff3f03a6c"
},
{
"path": "domains/E-05/structure_types.json",
"sha256": "22707fbce849991c099a08885cac929d1ba842aa896945f9921c3214e4c701d0"
},
{
"path": "domains/E-06/module_role_projection.json",
"sha256": "805b73b2b15cb720b62cd83455de85a67bda434255b69d588eb98f2527f88b71"
},
{
"path": "domains/E-06/structure_types.json",
"sha256": "a6520f72813caeaf72d62fd63eca10a1f46fd4fb3ffd97f97f222f37c53e8663"
},
{
"path": "domains/E-07/module_role_projection.json",
"sha256": "9920ed415f9768beb517100e97fa7d2eb862ca90ca83f802ea460f8f6bbf2445"
},
{
"path": "domains/E-07/structure_types.json",
"sha256": "fd4946e24cacc3bcc74c7fee5ce7fca99d3e5f2339fe3a9d698c54e2626a6c01"
},
{
"path": "domains/E-08/module_role_projection.json",
"sha256": "4a8c284b5d3bbf8ca224e9f08398e6f563b00cec6de72f5128acc4cd8425eced"
},
{
"path": "domains/E-08/structure_types.json",
"sha256": "a7ee6782c6541d0c6f6f6107a1d3615c2e7003a979741f2afc9d5d686a59e51d"
},
{
"path": "domains/E-09/module_role_projection.json",
"sha256": "ab93656808a832cfd73a6d75ed28d75e50e6cc66c67d59d765fcb8bbf638205f"
},
{
"path": "domains/E-09/structure_types.json",
"sha256": "c374b8b912679cc765b152c58ecd8320898f947b799e44ca509aa622669d7f4c"
},
{
"path": "domains/E-10/module_role_projection.json",
"sha256": "80f760d9d8516cc81ebe503a2967418c43e11f8b8410231081111483dfa6477d"
},
{
"path": "domains/E-10/structure_types.json",
"sha256": "07be97d40e422e14e570574c4e1da875307fe487c39e2ba9862ffd4b1623b748"
},
{
"path": "domains/E-11/module_role_projection.json",
"sha256": "0a55ce91b1845d64d085710110aec22bfcea66967ab5fce3a4acf5179faf65f1"
},
{
"path": "domains/E-11/structure_types.json",
"sha256": "6e565bab389f59866a0eafa8b7b7ff89f90ef2363ced3d9a4240100d3b3c3b90"
},
{
"path": "domains/E-12/module_role_projection.json",
"sha256": "207ef7d4a1300c12aa54cb1defa1110a6a9d18850d0fc4e144baebe6ff2b3c27"
},
{
"path": "domains/E-12/structure_types.json",
"sha256": "ef97f4b0ed05637165bfb211fc9be0cb3bcf0523e6b5a645c007e8dc2825bea9"
},
{
"path": "domains/E-13/module_role_projection.json",
"sha256": "412df6800be576d7f789bc7fc2045091eb9c49fd346e442ba42d6f29300f7313"
},
{
"path": "domains/E-13/structure_types.json",
"sha256": "cc18073ca443e98a34e139c08d11121ceb7d4ecd366ad3b3d5cb65ddb53e36f7"
},
{
"path": "domains/E-14/module_role_projection.json",
"sha256": "cead5634615b6b417390b37a948a050ea89cba37485bb79617bd1296c06829a5"
},
{
"path": "domains/E-14/structure_types.json",
"sha256": "5b0025a720a7b9f1ad7c74ce8d79f0969b282690abc0e3171e28dbf41933153b"
},
{
"path": "domains/E-15/module_role_projection.json",
"sha256": "873f921bbbc86fc07a0c1ecfcbcacee67b1d9592b2efd7319290a10e8c1ba018"
},
{
"path": "domains/E-15/structure_types.json",
"sha256": "784ceabd17c6de6d95a30f52dd5fd4036946fbedc556f2bad536225057a0f5fe"
},
{
"path": "domains/E-16/module_role_projection.json",
"sha256": "24ad172b32d4942be6f4031c582f928d3c6feb6e442027e3ee02700519744b11"
},
{
"path": "domains/E-16/structure_types.json",
"sha256": "b0bb30602ec424d35359c1a676ccca2bb2eda4a676997753da44d1dbbcc1f0b3"
},
{
"path": "domains/E-17/module_role_projection.json",
"sha256": "498e2320ad04a907680b8588ee1b1b9ee7487e131bece96a601979d60796f5a4"
},
{
"path": "domains/E-17/structure_types.json",
"sha256": "d306dbdf583453d51f643eb8398920efb1bb5bfa82b3a600caa79f45f790b2eb"
},
{
"path": "domains/E-18/module_role_projection.json",
"sha256": "b0699a793cdc52e6f74e9df3254f403858e83080be9e402bf3c7dae3cd8ffd0e"
},
{
"path": "domains/E-18/structure_types.json",
"sha256": "ce4eae3a54bfe7ac31f16b657cc0a891580e025f7bfecb1812c015e7fd0c2500"
},
{
"path": "domains/E-19/module_role_projection.json",
"sha256": "00950a8c76d4367ccd6dc000476c810df33e49b07fce124545e350530b008b99"
},
{
"path": "domains/E-19/structure_types.json",
"sha256": "58b9503558bcab3fb7ae76532659d54694208d813197e97bac148d50795feb22"
},
{
"path": "domains/E-20/module_role_projection.json",
"sha256": "7aef9706b50161693005ee61fc576a1562929fdf20830a3187fdda2a94fae8e3"
},
{
"path": "domains/E-20/structure_types.json",
"sha256": "6766546fd61d3f46f0d0b71bc3c41d5c63a525954b2568d8003c9f5e90234074"
},
{
"path": "domains/E-21/module_role_projection.json",
"sha256": "de6b0f29ad8100744a1a4fe8393a44a8a921d604b5244f832256450cb547722a"
},
{
"path": "domains/E-21/structure_types.json",
"sha256": "b027ff04f21cc4a86c8285e2cde6f8fd23e0ff3a3aeb3fa61a9bd10142814e36"
},
{
"path": "domains/X1/module_role_projection.json",
"sha256": "1bceacd99f408384fec9547262017d8c857dbdb58ae906446c45ce8f1e322920"
},
{
"path": "domains/X1/structure_types.json",
"sha256": "3344a27beb548c7d0b083b6e442a8f70ca89ce02d22614fda83203d3ff53e7e9"
},
{
"path": "domains/X2/module_role_projection.json",
"sha256": "9282422d06035a78b592c110c197c1afa58e78294566eecd2666f0c61820cd5f"
},
{
"path": "domains/X2/structure_types.json",
"sha256": "f51c0404bab1c5f4506012460ba02faf2cee6c958d49ee353095366defacd2ec"
},
{
"path": "domains/X3/module_role_projection.json",
"sha256": "93d3709c7d01132b82f0445d18eab7e666ea8d7fe2afe9b308bf87af5477be10"
},
{
"path": "domains/X3/structure_types.json",
"sha256": "91f7ef0a971b519bd7fea8fb2efc61770afeb83b3a6ee774f153aea28a9de913"
},
{
"path": "domains/_common/common_worker_contract.md",
"sha256": "f133c54c5a9353ae14efc4d7a27d9bee18872b6727713a9a107778eef8990fb1"
@@ -1104,6 +1304,14 @@
"path": "platform/schemas/domain_slice.schema.v2.json",
"sha256": "212a405088e7cf7ba2c65528a1c716938c946df7fe3bae3256b613051ed31aa3"
},
{
"path": "platform/schemas/legal_effect_structures.schema.json",
"sha256": "e2879c7a6d7f163c0729e8b863c5ef79b1094482eace78e6b8e1c7d69ce201e3"
},
{
"path": "platform/schemas/structure_seed_bundle.schema.json",
"sha256": "b7af9e422b6ac3876cffea39ec4f617eea76a631a57dfdfcd57d3785a83c667a"
},
{
"path": "routing/extension_payload_key_declarations.v1.json",
"sha256": "1684e13896bc9dfd0e7aa1a3b01354f58b2fa6ed00908bec6f861bce4b47235c"
@@ -1460,6 +1668,18 @@
"path": "stage1_runtime/stage_a_context_builder.txt",
"sha256": "68d984f9d565034c73721dc97c5aab0f9cdb8a8b46e1bf13eabd8c136ec4bcc0"
},
{
"path": "stage1_runtime/structure_index_compiler.py",
"sha256": "c400adfb7c58e340a17e67aebcdc4a7cf92c35c422f418451c79ce7c531d201f"
},
{
"path": "stage1_runtime/structure_index_compiler.txt",
"sha256": "c400adfb7c58e340a17e67aebcdc4a7cf92c35c422f418451c79ce7c531d201f"
},
{
"path": "stage1_runtime/structure_index_policy.v1.json",
"sha256": "823a7e734a7bc9433966a5629a5a0b153a067c92d8570072fa14a5c42bb722bd"
},
{
"path": "stage1_runtime/worker_output_validator.py",
"sha256": "e5b46f929082faf621731ffb8f4864ccb9bba461ab23caa56d1bcfe5b073fd9d"
@@ -1486,6 +1706,6 @@
}
],
"program_release_status": "STAGE1_NOT_RELEASE_READY",
"runtime_artifact_count": 371,
"runtime_artifact_count": 426,
"schema_version": "stage1_runtime_manifest.v1"
}
@@ -0,0 +1,529 @@
#!/usr/bin/env python3
"""structure_index_compiler — Stage 1 Part 3 구조 seed 조립기.
정본 근거
전략 stage_1_part_3_개정_신전략서.md §2(D-4) · §4.2(L0-1~L0-8)
정책 Default_Agent/stage1_runtime/structure_index_policy.v1.json
계약 Θ = SG-01.expected_runnable_domain_ids 에 대한 domain_config.structure_types 합집합
구조 레코드 = (도메인, type_id) 쌍마다 하나 · 도메인 간 병합 금지
전순서 order_key = (priority_rank, registry_index entries 위치, type_id)
이 모듈은 순수하다 — 파일을 읽지도 쓰지도 않는다. 호출자가 적재한 문서를 받아
{structure_seed_bundle, review_items, audit} 를 돌려준다. 시각·난수를 쓰지 않으므로
같은 입력이면 같은 출력이다(L-n).
사건 종류 이름·도메인 목록·유형 열거를 고정하지 않는다(137종 일반성 §8.1).
"""
from __future__ import annotations
from typing import Any
SCHEMA_VERSION = "stage1_structure_seed_bundle.v1"
POLICY_SCHEMA_VERSION = "stage1_structure_index_policy.v1"
# 검토 코드 — registry 선언(UNREGISTERED_EFFECT_TYPE_REVIEW)과 신전략서 §4.2 의 신설분.
REVIEW_UNREGISTERED = "UNREGISTERED_EFFECT_TYPE_REVIEW"
REVIEW_SLICE_DIVERGENCE = "PART3_SLICE_PROJECTION_DIVERGENCE"
REVIEW_SIDECAR_MISMATCH = "PART3_REGISTRY_SIDECAR_MISMATCH"
REVIEW_CROSS_DOMAIN = "PART3_EFFECT_TYPE_CROSS_DOMAIN"
REVIEW_CALC_DANGLING = "PART3_CALCULATION_REF_DANGLING"
REVIEW_BO_UNATTACHED = "PART3_BO_UNATTACHED"
REVIEW_BO_UNRESOLVED = "PART3_BO_DOMAIN_UNRESOLVED"
SG13_ROUTE_RECORD_TYPE = "legal_effect_route"
def _strings(value: Any) -> list[str]:
if not isinstance(value, list):
return []
return [item for item in value if isinstance(item, str) and item]
def _dict(value: Any) -> dict:
return value if isinstance(value, dict) else {}
# ---------------------------------------------------------------------------
# 사이드카 정규화 리더 — 방언 지도(신전략서 §1.2): 목록 키 3종 · 투영 키 3종 · 정책 자리 4갈래.
# 방언을 다 읽지 않은 리더는 결손을 만들어 낸다(구 전략서 §1.5 의 실패 기록) —
# 세 방언 전부를 인정하고, 세지 않은 필드를 공통 필드로 승격하지 않는다.
# ---------------------------------------------------------------------------
_LIST_KEYS = ("entries", "structure_types", "types")
_PROJECTION_KEYS = ("projections", "mappings", "structure_type_to_projection")
_FILE_POLICY_KEYS = ("admission_rules", "unregistered_effect_policy", "fallback_policy")
def read_sidecar_types(doc: Any) -> dict[str, Any]:
"""structure_types.json 방언을 정규화한다.
반환: {"list_key": str|None, "schema_version": str|None,
"items": [{"type_id", "admission_rule"?, "registered"?}],
"file_policy_key": str|None, "file_policy": dict|None,
"legacy_contract_style": bool}
"""
root = _dict(doc)
list_key = None
rows: list[Any] = []
for key in _LIST_KEYS:
if isinstance(root.get(key), list):
list_key = key
rows = root[key]
break
items = []
for row in rows:
row = _dict(row)
type_id = row.get("type_id") or row.get("structure_type") or row.get("id")
if not isinstance(type_id, str) or not type_id:
continue
item: dict[str, Any] = {"type_id": type_id}
if isinstance(row.get("admission_rule"), str) and row.get("admission_rule"):
item["admission_rule"] = row["admission_rule"]
if isinstance(row.get("registered"), bool):
item["registered"] = row["registered"]
items.append(item)
file_policy_key = None
file_policy = None
for key in _FILE_POLICY_KEYS:
if root.get(key) is not None:
file_policy_key = key
file_policy = root[key]
break
legacy_style = file_policy_key is None and any(
key in root for key in ("legacy_contract", "noncanonical_preservation", "ordering_preserved"))
return {"list_key": list_key,
"schema_version": root.get("schema_version"),
"items": items,
"file_policy_key": file_policy_key,
"file_policy": file_policy,
"legacy_contract_style": legacy_style}
def read_sidecar_projection(doc: Any) -> dict[str, Any]:
"""module_role_projection.json 방언을 정규화한다.
반환: {"projection_key": str|None, "mappings": {type_id: {"module", "role_projection"}},
"default_projection": dict|None, "missing_mapping_policy": dict|None}
"""
root = _dict(doc)
mappings: dict[str, dict[str, Any]] = {}
projection_key = None
for key in _PROJECTION_KEYS:
value = root.get(key)
if isinstance(value, list):
projection_key = key
for row in value:
row = _dict(row)
type_id = row.get("structure_type") or row.get("type_id")
if isinstance(type_id, str) and type_id:
mappings[type_id] = {"module": row.get("module"),
"role_projection": row.get("role_projection")}
break
if isinstance(value, dict):
projection_key = key
for type_id, row in value.items():
row = _dict(row)
if isinstance(type_id, str) and type_id:
mappings[type_id] = {"module": row.get("module"),
"role_projection": row.get("role_projection")}
break
default_projection = root.get("default_projection")
if not isinstance(default_projection, dict):
default_projection = None
missing_policy = root.get("missing_mapping_policy")
if not isinstance(missing_policy, dict):
missing_policy = None
return {"projection_key": projection_key, "mappings": mappings,
"default_projection": default_projection,
"missing_mapping_policy": missing_policy}
def admission_policy_for(sidecar_types: dict[str, Any], item: dict[str, Any]) -> str:
"""두 단 정의(신전략서 §1.2) — 파일 단위 정책(3키 중 있는 것) → 없으면 항목 admission_rule.
73/73 이 채워지는 값이다. 어느 단도 없으면 빈 문자열이 아니라 명시 문자열을 남긴다 —
없는 것을 있는 것처럼 채우지 않되, 부재 자체는 보이게 한다.
"""
key = sidecar_types.get("file_policy_key")
if key:
return "file:%s" % key
rule = item.get("admission_rule")
if isinstance(rule, str) and rule:
return "item:admission_rule"
return "absent"
def resolve_source_domains(value: Any, alias_targets: dict[str, Any],
known_domain_ids: set[str]) -> list[str]:
"""BO provenance.source_domain 을 registry 도메인 ID 목록으로 해소한다(L0-7b ①).
registry ID 면 [값]. 아니면 별칭표의 목록(다중 타깃 실재 — 단일 primary 개념 없음).
별칭표에도 없으면 빈 목록 — 호출자가 PART3_BO_DOMAIN_UNRESOLVED 로 격리한다.
"""
if not isinstance(value, str) or not value:
return []
if value in known_domain_ids:
return [value]
targets = _strings(_dict(alias_targets).get(value))
return [d for d in targets if d in known_domain_ids]
# ---------------------------------------------------------------------------
# 본체
# ---------------------------------------------------------------------------
def compile_structure_seeds(activation_manifest: Any,
registry_index: Any,
domain_configs: dict[str, Any],
domain_sidecars: dict[str, Any],
policy: Any,
alias_declarations: Any,
sg13_records: list[Any],
sg05_records: list[Any],
sg11_records: list[Any],
domain_envelopes: dict[str, Any],
bo_records: list[Any],
slice_documents: dict[str, Any]) -> dict[str, Any]:
"""L0-1 ~ L0-8. 반환 dict 는 theta 와 order_key 를 반드시 싣는다(PART3_COMPILER_STALE 의 대조면)."""
policy_root = _dict(policy)
if policy_root.get("schema_version") != POLICY_SCHEMA_VERSION:
raise ValueError("PART3_POLICY_INVALID:%s" % policy_root.get("schema_version"))
manifest_root = _dict(activation_manifest).get("domain_activation_manifest",
_dict(activation_manifest))
expected_runnable = _strings(_dict(manifest_root).get("expected_runnable_domain_ids"))
if not expected_runnable:
raise ValueError("PART3_EXPECTED_RUNNABLE_EMPTY")
index_root = _dict(_dict(registry_index).get("domain_registry_index", registry_index))
index_entries = [row for row in (index_root.get("entries") or []) if isinstance(row, dict)]
index_position = {row.get("domain_id"): position
for position, row in enumerate(index_entries)
if isinstance(row.get("domain_id"), str)}
known_domain_ids = set(index_position)
review_items: list[dict[str, Any]] = []
audit: dict[str, Any] = {"sidecar_dialects": {}, "input_gaps": [],
"unlinked_sg11": [], "dangling_calculation_refs": {}}
def review(code: str, severity: str, domain_id: str | None, detail: dict[str, Any]) -> None:
item = {"review_code": code, "severity": severity}
if domain_id is not None:
item["domain_id"] = domain_id
item.update(detail)
review_items.append(item)
# ---------------- L0-1 · Θ 조립 ----------------
declared_by_domain: dict[str, list[dict[str, Any]]] = {}
for domain_id in expected_runnable:
if domain_id not in known_domain_ids:
raise ValueError("PART3_DOMAIN_NOT_IN_REGISTRY:%s" % domain_id)
config_root = _dict(domain_configs.get(domain_id))
rows = [row for row in (config_root.get("structure_types") or [])
if isinstance(row, dict) and isinstance(row.get("type_id"), str)]
declared_by_domain[domain_id] = rows
theta = sorted({row["type_id"] for rows in declared_by_domain.values() for row in rows})
declaring_domains: dict[str, list[str]] = {}
for domain_id in expected_runnable:
for row in declared_by_domain[domain_id]:
declaring_domains.setdefault(row["type_id"], []).append(domain_id)
# ---------------- L0-2c · 사이드카 교차 검증 (레코드 생성보다 앞 — admission_policy 재료) ----------------
sidecar_norm: dict[str, dict[str, Any]] = {}
projection_norm: dict[str, dict[str, Any]] = {}
for domain_id in expected_runnable:
pair = _dict(domain_sidecars.get(domain_id))
declared_ids = sorted({row["type_id"] for row in declared_by_domain[domain_id]})
if not pair:
# 부재 규칙(신전략서 §5.5) — 선언 0 도메인의 부재만 정상이다.
if declared_ids:
raise ValueError("PART3_SIDECAR_ABSENT_FOR_DECLARING_DOMAIN:%s" % domain_id)
audit["sidecar_dialects"][domain_id] = {"absent": True, "declared": 0}
continue
types_doc = read_sidecar_types(pair.get("structure_types"))
projection_doc = read_sidecar_projection(pair.get("module_role_projection"))
sidecar_norm[domain_id] = types_doc
projection_norm[domain_id] = projection_doc
audit["sidecar_dialects"][domain_id] = {
"list_key": types_doc.get("list_key"),
"schema_version": types_doc.get("schema_version"),
"projection_key": projection_doc.get("projection_key"),
"file_policy_key": types_doc.get("file_policy_key"),
"legacy_contract_style": types_doc.get("legacy_contract_style"),
"item_count": len(types_doc.get("items") or []),
}
sidecar_ids = sorted({item["type_id"] for item in types_doc.get("items") or []})
if sidecar_ids != declared_ids:
review(REVIEW_SIDECAR_MISMATCH, "HARD_WARNING", domain_id, {
"detail": "type_set_mismatch",
"config_only": sorted(set(declared_ids) - set(sidecar_ids)),
"sidecar_only": sorted(set(sidecar_ids) - set(declared_ids)),
})
for type_id, mapping in sorted((projection_doc.get("mappings") or {}).items()):
config_row = next((row for row in declared_by_domain[domain_id]
if row["type_id"] == type_id), None)
if config_row is None:
continue
if (mapping.get("module") != config_row.get("module")
or mapping.get("role_projection") != config_row.get("role_projection")):
review(REVIEW_SIDECAR_MISMATCH, "HARD_WARNING", domain_id, {
"detail": "projection_mismatch", "type_id": type_id,
"config": {"module": config_row.get("module"),
"role_projection": config_row.get("role_projection")},
"sidecar": mapping,
})
# ---------------- L0-2 · 구조 레코드 — (도메인, type_id) 쌍마다 하나 ----------------
e13_extension_key = None
contract = _dict(policy_root.get("extension_key_rule"))
e13_extension_key = contract.get("extension_key") if isinstance(
contract.get("extension_key"), str) else None
e13_domain = contract.get("domain_id") if isinstance(contract.get("domain_id"), str) else None
records: dict[tuple[str, str], dict[str, Any]] = {}
for domain_id in expected_runnable:
types_doc = sidecar_norm.get(domain_id, {"items": []})
item_by_type = {item["type_id"]: item for item in types_doc.get("items") or []}
for row in declared_by_domain[domain_id]:
type_id = row["type_id"]
item = item_by_type.get(type_id, {})
record: dict[str, Any] = {
"structure_id": "LES-%s-%s" % (domain_id, type_id),
"domain_id": domain_id,
"type_id": type_id,
"priority_rank": row.get("priority_rank"),
"order_key": [row.get("priority_rank"), index_position[domain_id], type_id],
"module": row.get("module"),
"role_projection": row.get("role_projection"),
"admission_policy": admission_policy_for(types_doc, item),
"routes": [],
"route_count": 0,
"evidence_refs": [],
"meeting_clause_refs": [],
"calculation_request_refs": [],
"source_bo_ids": [],
"review_codes": [],
"review_required": False,
}
if "admission_rule" in item:
record["admission_rule"] = item["admission_rule"]
if "registered" in item:
record["registered_declared"] = item["registered"]
if domain_id == e13_domain and e13_extension_key:
record["extension_key"] = e13_extension_key
if domain_id == "E-00":
# E-00 특칙 — 잔여 수집기 유래 구조는 검토 필수(신전략서 §4.2).
record["review_required"] = True
records[(domain_id, type_id)] = record
# ---------------- L0-2b · 슬라이스 투영 정합 ----------------
for domain_id in expected_runnable:
slice_doc = slice_documents.get(domain_id)
if slice_doc is None:
audit["input_gaps"].append({"kind": "slice_absent", "domain_id": domain_id})
continue
slice_root = _dict(_dict(slice_doc).get("stage_b_domain_slice", slice_doc))
declarations = _dict(slice_root.get("domain_declarations"))
slice_ids = sorted(_strings(declarations.get("structure_type_ids")))
config_ids = sorted({row["type_id"] for row in declared_by_domain[domain_id]})
if slice_ids != config_ids:
review(REVIEW_SLICE_DIVERGENCE, "HARD_WARNING", domain_id, {
"slice_only": sorted(set(slice_ids) - set(config_ids)),
"config_only": sorted(set(config_ids) - set(slice_ids)),
})
# ---------------- L0-3 · SG-13 조인 + L0-4 · 도메인 소속 ----------------
routes = [_dict(row) for row in sg13_records
if _dict(row).get("record_type") == SG13_ROUTE_RECORD_TYPE]
attached_route_count = 0
unattached_routes: list[dict[str, Any]] = []
for route in routes:
details = _dict(route.get("details"))
type_id = details.get("type_id")
route_domains = _strings(route.get("domain_ids"))
route_domain = route_domains[0] if route_domains else None
registered = details.get("registered")
route_ref = {
"signal_id": route.get("signal_id"),
"source_seed_id": details.get("source_seed_id"),
"registered": registered if isinstance(registered, bool) else None,
"status": route.get("status"),
}
if not isinstance(type_id, str) or type_id not in theta:
# Θ 밖 — 버리지 않는다. default_projection 이 있으면 검토 항목에 실어 함께 나른다.
projection = None
if route_domain and route_domain in projection_norm:
projection = projection_norm[route_domain].get("default_projection")
review(REVIEW_UNREGISTERED, "HARD_WARNING", route_domain, {
"type_id": type_id, "signal_id": route.get("signal_id"),
"default_projection": projection,
})
unattached_routes.append({"signal_id": route.get("signal_id"),
"type_id": type_id, "reason": "type_outside_theta"})
continue
target_keys = []
if route_domain and (route_domain, type_id) in records:
target_keys = [(route_domain, type_id)]
else:
# Θ 안이지만 자기 도메인이 선언하지 않은 유형 — 선언 도메인 전부에 붙이고 검토(L0-4).
target_keys = [(d, type_id) for d in declaring_domains.get(type_id, [])
if (d, type_id) in records]
if target_keys:
review(REVIEW_CROSS_DOMAIN, "HARD_WARNING", route_domain, {
"type_id": type_id, "signal_id": route.get("signal_id"),
"attached_to": ["%s:%s" % key for key in target_keys],
})
if not target_keys:
unattached_routes.append({"signal_id": route.get("signal_id"),
"type_id": type_id, "reason": "no_declaring_domain"})
review(REVIEW_UNREGISTERED, "HARD_WARNING", route_domain, {
"type_id": type_id, "signal_id": route.get("signal_id"),
"default_projection": None,
})
continue
attached_route_count += 1
for key in target_keys:
record = records[key]
record["routes"].append(dict(route_ref))
for field in ("evidence_refs", "meeting_clause_refs"):
merged = set(record[field]) | set(_strings(route.get(field)))
record[field] = sorted(merged)
if registered is False:
# 붙은 채로 검토 꼬리표를 단다(L-m2 — 분할은 부착 여부로만 가른다).
if REVIEW_UNREGISTERED not in record["review_codes"]:
record["review_codes"].append(REVIEW_UNREGISTERED)
record["review_required"] = True
review(REVIEW_UNREGISTERED, "SOFT_WARNING", key[0], {
"type_id": type_id, "signal_id": route.get("signal_id"),
"detail": "registered_false_route_attached",
})
declared_flag = record.get("registered_declared")
if isinstance(declared_flag, bool) and isinstance(registered, bool) \
and declared_flag != registered:
# types 방언의 registered ↔ SG-13 registered 대조(L0-2c 둘째 갈래).
if REVIEW_SIDECAR_MISMATCH not in record["review_codes"]:
record["review_codes"].append(REVIEW_SIDECAR_MISMATCH)
record["review_required"] = True
review(REVIEW_SIDECAR_MISMATCH, "HARD_WARNING", key[0], {
"detail": "registered_flag_mismatch", "type_id": type_id,
"sidecar": declared_flag, "route": registered,
})
# ---------------- L0-5 · module 부여는 L0-2 에서 config 원문으로 끝났다 ----------------
# ---------------- L0-6 · 계산 조인 — SG-11 signal_id ↔ 도메인 봉투 calculation_refs ----------------
# 봉투 부재는 침묵 스킵이 아니라 영수증 계수다(I-5 — expected_runnable 중 실재하는 것만 읽되
# 부재를 기록한다). 실주행에서 감시 도메인 봉투가 없는 것은 상시 경로다.
for domain_id in expected_runnable:
if domain_id not in domain_envelopes:
audit["input_gaps"].append({"kind": "domain_envelope_absent",
"domain_id": domain_id})
sg11_rows = [_dict(row) for row in sg11_records]
sg11_ids = {row.get("signal_id") for row in sg11_rows if isinstance(row.get("signal_id"), str)}
linked_sg11: set[str] = set()
for domain_id in expected_runnable:
envelope = _dict(_dict(domain_envelopes.get(domain_id)).get("domain_signal_envelope",
domain_envelopes.get(domain_id)))
refs = _strings(_dict(envelope).get("calculation_refs"))
if not refs:
continue
matched = sorted(set(refs) & sg11_ids)
dangling = sorted(set(refs) - sg11_ids)
if dangling:
audit["dangling_calculation_refs"][domain_id] = dangling
review(REVIEW_CALC_DANGLING, "HARD_WARNING", domain_id, {"dangling": dangling})
if matched:
linked_sg11.update(matched)
for (record_domain, _type_id), record in records.items():
if record_domain == domain_id:
merged = set(record["calculation_request_refs"]) | set(matched)
record["calculation_request_refs"] = sorted(merged)
# 봉투 참조에 연결되지 않은 SG-11(합성 id 포함)은 구조에 싣지 않되 형식화 기록(신전략서 L0-6).
audit["unlinked_sg11"] = sorted(sg11_ids - linked_sg11)
# ---------------- L0-7 · SG-05 근거 refs (도메인 단위 합집합) ----------------
for row in sg05_records:
row = _dict(row)
for domain_id in _strings(row.get("domain_ids")):
for (record_domain, _type_id), record in records.items():
if record_domain == domain_id:
for field in ("evidence_refs", "meeting_clause_refs"):
merged = set(record[field]) | set(_strings(row.get(field)))
record[field] = sorted(merged)
# ---------------- L0-7b · BO 부착 조인 ----------------
alias_targets = _dict(_dict(alias_declarations).get("legacy_alias_targets"))
bo_rows = [_dict(row) for row in bo_records]
bo_attached: list[str] = []
bo_unattached: list[dict[str, Any]] = []
for bo in bo_rows:
bo_id = bo.get("BO_ID") or bo.get("id")
provenance = _dict(bo.get("provenance"))
source_domain = provenance.get("source_domain")
resolved = resolve_source_domains(source_domain, alias_targets, known_domain_ids)
if not resolved:
review(REVIEW_BO_UNRESOLVED, "HARD_WARNING", None, {
"bo_id": bo_id, "source_domain": source_domain,
})
bo_unattached.append({"bo_id": bo_id, "reason": "source_domain_unresolved"})
continue
keywords = _strings(bo.get("Legal_Keywords"))
attached_here = False
for domain_id in resolved:
for type_id in keywords:
record = records.get((domain_id, type_id))
if record is not None:
attached_here = True
if isinstance(bo_id, str) and bo_id not in record["source_bo_ids"]:
record["source_bo_ids"].append(bo_id)
record["source_bo_ids"].sort()
if attached_here:
bo_attached.append(bo_id if isinstance(bo_id, str) else "")
else:
# 부착 실패는 침묵 금지(L0-7b ③).
review(REVIEW_BO_UNATTACHED, "HARD_WARNING", None, {
"bo_id": bo_id, "source_domain": source_domain,
"legal_keywords": keywords, "resolved_domains": resolved,
})
bo_unattached.append({"bo_id": bo_id, "reason": "no_matching_structure"})
# ---------------- 정렬 · 검토 재료 반영 · 산출 ----------------
ordered = sorted(records.values(),
key=lambda record: (record["order_key"][0] if isinstance(
record["order_key"][0], int) else 999,
record["order_key"][1], record["order_key"][2]))
for record in ordered:
record["route_count"] = len(record["routes"])
record["routes"].sort(key=lambda ref: (str(ref.get("signal_id")),
str(ref.get("source_seed_id"))))
record["review_codes"].sort()
if record["review_codes"]:
record["review_required"] = True
status = "READY_WITH_REVIEW" if review_items else "READY"
bundle = {
"schema_version": SCHEMA_VERSION,
"status": status,
"registry_version": index_root.get("registry_version"),
"expected_runnable_domain_ids": list(expected_runnable),
"theta": theta,
"structure_records": ordered,
"unattached_routes": sorted(unattached_routes,
key=lambda row: (str(row.get("signal_id")),
str(row.get("type_id")))),
"unattached_bo": sorted(bo_unattached, key=lambda row: str(row.get("bo_id"))),
"counts": {
"structure_records": len(ordered),
"declared_structure_types_total": sum(
len(rows) for rows in declared_by_domain.values()),
"sg13_route_total": len(routes),
"routes_attached": attached_route_count,
"routes_unattached": len(unattached_routes),
"bo_total": len(bo_rows),
"bo_attached": len(bo_attached),
"bo_unattached": len(bo_unattached),
"sg11_total": len(sg11_rows),
"sg11_linked": len(linked_sg11),
"sg11_unlinked": len(audit["unlinked_sg11"]),
},
"created_by": "Task_C_LE_L0_structure_seed_reducer",
}
return {"structure_seed_bundle": bundle, "review_items": review_items, "audit": audit}
@@ -0,0 +1,529 @@
#!/usr/bin/env python3
"""structure_index_compiler — Stage 1 Part 3 구조 seed 조립기.
정본 근거
전략 stage_1_part_3_개정_신전략서.md §2(D-4) · §4.2(L0-1~L0-8)
정책 Default_Agent/stage1_runtime/structure_index_policy.v1.json
계약 Θ = SG-01.expected_runnable_domain_ids 에 대한 domain_config.structure_types 합집합
구조 레코드 = (도메인, type_id) 쌍마다 하나 · 도메인 간 병합 금지
전순서 order_key = (priority_rank, registry_index entries 위치, type_id)
이 모듈은 순수하다 — 파일을 읽지도 쓰지도 않는다. 호출자가 적재한 문서를 받아
{structure_seed_bundle, review_items, audit} 를 돌려준다. 시각·난수를 쓰지 않으므로
같은 입력이면 같은 출력이다(L-n).
사건 종류 이름·도메인 목록·유형 열거를 고정하지 않는다(137종 일반성 §8.1).
"""
from __future__ import annotations
from typing import Any
SCHEMA_VERSION = "stage1_structure_seed_bundle.v1"
POLICY_SCHEMA_VERSION = "stage1_structure_index_policy.v1"
# 검토 코드 — registry 선언(UNREGISTERED_EFFECT_TYPE_REVIEW)과 신전략서 §4.2 의 신설분.
REVIEW_UNREGISTERED = "UNREGISTERED_EFFECT_TYPE_REVIEW"
REVIEW_SLICE_DIVERGENCE = "PART3_SLICE_PROJECTION_DIVERGENCE"
REVIEW_SIDECAR_MISMATCH = "PART3_REGISTRY_SIDECAR_MISMATCH"
REVIEW_CROSS_DOMAIN = "PART3_EFFECT_TYPE_CROSS_DOMAIN"
REVIEW_CALC_DANGLING = "PART3_CALCULATION_REF_DANGLING"
REVIEW_BO_UNATTACHED = "PART3_BO_UNATTACHED"
REVIEW_BO_UNRESOLVED = "PART3_BO_DOMAIN_UNRESOLVED"
SG13_ROUTE_RECORD_TYPE = "legal_effect_route"
def _strings(value: Any) -> list[str]:
if not isinstance(value, list):
return []
return [item for item in value if isinstance(item, str) and item]
def _dict(value: Any) -> dict:
return value if isinstance(value, dict) else {}
# ---------------------------------------------------------------------------
# 사이드카 정규화 리더 — 방언 지도(신전략서 §1.2): 목록 키 3종 · 투영 키 3종 · 정책 자리 4갈래.
# 방언을 다 읽지 않은 리더는 결손을 만들어 낸다(구 전략서 §1.5 의 실패 기록) —
# 세 방언 전부를 인정하고, 세지 않은 필드를 공통 필드로 승격하지 않는다.
# ---------------------------------------------------------------------------
_LIST_KEYS = ("entries", "structure_types", "types")
_PROJECTION_KEYS = ("projections", "mappings", "structure_type_to_projection")
_FILE_POLICY_KEYS = ("admission_rules", "unregistered_effect_policy", "fallback_policy")
def read_sidecar_types(doc: Any) -> dict[str, Any]:
"""structure_types.json 방언을 정규화한다.
반환: {"list_key": str|None, "schema_version": str|None,
"items": [{"type_id", "admission_rule"?, "registered"?}],
"file_policy_key": str|None, "file_policy": dict|None,
"legacy_contract_style": bool}
"""
root = _dict(doc)
list_key = None
rows: list[Any] = []
for key in _LIST_KEYS:
if isinstance(root.get(key), list):
list_key = key
rows = root[key]
break
items = []
for row in rows:
row = _dict(row)
type_id = row.get("type_id") or row.get("structure_type") or row.get("id")
if not isinstance(type_id, str) or not type_id:
continue
item: dict[str, Any] = {"type_id": type_id}
if isinstance(row.get("admission_rule"), str) and row.get("admission_rule"):
item["admission_rule"] = row["admission_rule"]
if isinstance(row.get("registered"), bool):
item["registered"] = row["registered"]
items.append(item)
file_policy_key = None
file_policy = None
for key in _FILE_POLICY_KEYS:
if root.get(key) is not None:
file_policy_key = key
file_policy = root[key]
break
legacy_style = file_policy_key is None and any(
key in root for key in ("legacy_contract", "noncanonical_preservation", "ordering_preserved"))
return {"list_key": list_key,
"schema_version": root.get("schema_version"),
"items": items,
"file_policy_key": file_policy_key,
"file_policy": file_policy,
"legacy_contract_style": legacy_style}
def read_sidecar_projection(doc: Any) -> dict[str, Any]:
"""module_role_projection.json 방언을 정규화한다.
반환: {"projection_key": str|None, "mappings": {type_id: {"module", "role_projection"}},
"default_projection": dict|None, "missing_mapping_policy": dict|None}
"""
root = _dict(doc)
mappings: dict[str, dict[str, Any]] = {}
projection_key = None
for key in _PROJECTION_KEYS:
value = root.get(key)
if isinstance(value, list):
projection_key = key
for row in value:
row = _dict(row)
type_id = row.get("structure_type") or row.get("type_id")
if isinstance(type_id, str) and type_id:
mappings[type_id] = {"module": row.get("module"),
"role_projection": row.get("role_projection")}
break
if isinstance(value, dict):
projection_key = key
for type_id, row in value.items():
row = _dict(row)
if isinstance(type_id, str) and type_id:
mappings[type_id] = {"module": row.get("module"),
"role_projection": row.get("role_projection")}
break
default_projection = root.get("default_projection")
if not isinstance(default_projection, dict):
default_projection = None
missing_policy = root.get("missing_mapping_policy")
if not isinstance(missing_policy, dict):
missing_policy = None
return {"projection_key": projection_key, "mappings": mappings,
"default_projection": default_projection,
"missing_mapping_policy": missing_policy}
def admission_policy_for(sidecar_types: dict[str, Any], item: dict[str, Any]) -> str:
"""두 단 정의(신전략서 §1.2) — 파일 단위 정책(3키 중 있는 것) → 없으면 항목 admission_rule.
73/73 이 채워지는 값이다. 어느 단도 없으면 빈 문자열이 아니라 명시 문자열을 남긴다 —
없는 것을 있는 것처럼 채우지 않되, 부재 자체는 보이게 한다.
"""
key = sidecar_types.get("file_policy_key")
if key:
return "file:%s" % key
rule = item.get("admission_rule")
if isinstance(rule, str) and rule:
return "item:admission_rule"
return "absent"
def resolve_source_domains(value: Any, alias_targets: dict[str, Any],
known_domain_ids: set[str]) -> list[str]:
"""BO provenance.source_domain 을 registry 도메인 ID 목록으로 해소한다(L0-7b ①).
registry ID 면 [값]. 아니면 별칭표의 목록(다중 타깃 실재 — 단일 primary 개념 없음).
별칭표에도 없으면 빈 목록 — 호출자가 PART3_BO_DOMAIN_UNRESOLVED 로 격리한다.
"""
if not isinstance(value, str) or not value:
return []
if value in known_domain_ids:
return [value]
targets = _strings(_dict(alias_targets).get(value))
return [d for d in targets if d in known_domain_ids]
# ---------------------------------------------------------------------------
# 본체
# ---------------------------------------------------------------------------
def compile_structure_seeds(activation_manifest: Any,
registry_index: Any,
domain_configs: dict[str, Any],
domain_sidecars: dict[str, Any],
policy: Any,
alias_declarations: Any,
sg13_records: list[Any],
sg05_records: list[Any],
sg11_records: list[Any],
domain_envelopes: dict[str, Any],
bo_records: list[Any],
slice_documents: dict[str, Any]) -> dict[str, Any]:
"""L0-1 ~ L0-8. 반환 dict 는 theta 와 order_key 를 반드시 싣는다(PART3_COMPILER_STALE 의 대조면)."""
policy_root = _dict(policy)
if policy_root.get("schema_version") != POLICY_SCHEMA_VERSION:
raise ValueError("PART3_POLICY_INVALID:%s" % policy_root.get("schema_version"))
manifest_root = _dict(activation_manifest).get("domain_activation_manifest",
_dict(activation_manifest))
expected_runnable = _strings(_dict(manifest_root).get("expected_runnable_domain_ids"))
if not expected_runnable:
raise ValueError("PART3_EXPECTED_RUNNABLE_EMPTY")
index_root = _dict(_dict(registry_index).get("domain_registry_index", registry_index))
index_entries = [row for row in (index_root.get("entries") or []) if isinstance(row, dict)]
index_position = {row.get("domain_id"): position
for position, row in enumerate(index_entries)
if isinstance(row.get("domain_id"), str)}
known_domain_ids = set(index_position)
review_items: list[dict[str, Any]] = []
audit: dict[str, Any] = {"sidecar_dialects": {}, "input_gaps": [],
"unlinked_sg11": [], "dangling_calculation_refs": {}}
def review(code: str, severity: str, domain_id: str | None, detail: dict[str, Any]) -> None:
item = {"review_code": code, "severity": severity}
if domain_id is not None:
item["domain_id"] = domain_id
item.update(detail)
review_items.append(item)
# ---------------- L0-1 · Θ 조립 ----------------
declared_by_domain: dict[str, list[dict[str, Any]]] = {}
for domain_id in expected_runnable:
if domain_id not in known_domain_ids:
raise ValueError("PART3_DOMAIN_NOT_IN_REGISTRY:%s" % domain_id)
config_root = _dict(domain_configs.get(domain_id))
rows = [row for row in (config_root.get("structure_types") or [])
if isinstance(row, dict) and isinstance(row.get("type_id"), str)]
declared_by_domain[domain_id] = rows
theta = sorted({row["type_id"] for rows in declared_by_domain.values() for row in rows})
declaring_domains: dict[str, list[str]] = {}
for domain_id in expected_runnable:
for row in declared_by_domain[domain_id]:
declaring_domains.setdefault(row["type_id"], []).append(domain_id)
# ---------------- L0-2c · 사이드카 교차 검증 (레코드 생성보다 앞 — admission_policy 재료) ----------------
sidecar_norm: dict[str, dict[str, Any]] = {}
projection_norm: dict[str, dict[str, Any]] = {}
for domain_id in expected_runnable:
pair = _dict(domain_sidecars.get(domain_id))
declared_ids = sorted({row["type_id"] for row in declared_by_domain[domain_id]})
if not pair:
# 부재 규칙(신전략서 §5.5) — 선언 0 도메인의 부재만 정상이다.
if declared_ids:
raise ValueError("PART3_SIDECAR_ABSENT_FOR_DECLARING_DOMAIN:%s" % domain_id)
audit["sidecar_dialects"][domain_id] = {"absent": True, "declared": 0}
continue
types_doc = read_sidecar_types(pair.get("structure_types"))
projection_doc = read_sidecar_projection(pair.get("module_role_projection"))
sidecar_norm[domain_id] = types_doc
projection_norm[domain_id] = projection_doc
audit["sidecar_dialects"][domain_id] = {
"list_key": types_doc.get("list_key"),
"schema_version": types_doc.get("schema_version"),
"projection_key": projection_doc.get("projection_key"),
"file_policy_key": types_doc.get("file_policy_key"),
"legacy_contract_style": types_doc.get("legacy_contract_style"),
"item_count": len(types_doc.get("items") or []),
}
sidecar_ids = sorted({item["type_id"] for item in types_doc.get("items") or []})
if sidecar_ids != declared_ids:
review(REVIEW_SIDECAR_MISMATCH, "HARD_WARNING", domain_id, {
"detail": "type_set_mismatch",
"config_only": sorted(set(declared_ids) - set(sidecar_ids)),
"sidecar_only": sorted(set(sidecar_ids) - set(declared_ids)),
})
for type_id, mapping in sorted((projection_doc.get("mappings") or {}).items()):
config_row = next((row for row in declared_by_domain[domain_id]
if row["type_id"] == type_id), None)
if config_row is None:
continue
if (mapping.get("module") != config_row.get("module")
or mapping.get("role_projection") != config_row.get("role_projection")):
review(REVIEW_SIDECAR_MISMATCH, "HARD_WARNING", domain_id, {
"detail": "projection_mismatch", "type_id": type_id,
"config": {"module": config_row.get("module"),
"role_projection": config_row.get("role_projection")},
"sidecar": mapping,
})
# ---------------- L0-2 · 구조 레코드 — (도메인, type_id) 쌍마다 하나 ----------------
e13_extension_key = None
contract = _dict(policy_root.get("extension_key_rule"))
e13_extension_key = contract.get("extension_key") if isinstance(
contract.get("extension_key"), str) else None
e13_domain = contract.get("domain_id") if isinstance(contract.get("domain_id"), str) else None
records: dict[tuple[str, str], dict[str, Any]] = {}
for domain_id in expected_runnable:
types_doc = sidecar_norm.get(domain_id, {"items": []})
item_by_type = {item["type_id"]: item for item in types_doc.get("items") or []}
for row in declared_by_domain[domain_id]:
type_id = row["type_id"]
item = item_by_type.get(type_id, {})
record: dict[str, Any] = {
"structure_id": "LES-%s-%s" % (domain_id, type_id),
"domain_id": domain_id,
"type_id": type_id,
"priority_rank": row.get("priority_rank"),
"order_key": [row.get("priority_rank"), index_position[domain_id], type_id],
"module": row.get("module"),
"role_projection": row.get("role_projection"),
"admission_policy": admission_policy_for(types_doc, item),
"routes": [],
"route_count": 0,
"evidence_refs": [],
"meeting_clause_refs": [],
"calculation_request_refs": [],
"source_bo_ids": [],
"review_codes": [],
"review_required": False,
}
if "admission_rule" in item:
record["admission_rule"] = item["admission_rule"]
if "registered" in item:
record["registered_declared"] = item["registered"]
if domain_id == e13_domain and e13_extension_key:
record["extension_key"] = e13_extension_key
if domain_id == "E-00":
# E-00 특칙 — 잔여 수집기 유래 구조는 검토 필수(신전략서 §4.2).
record["review_required"] = True
records[(domain_id, type_id)] = record
# ---------------- L0-2b · 슬라이스 투영 정합 ----------------
for domain_id in expected_runnable:
slice_doc = slice_documents.get(domain_id)
if slice_doc is None:
audit["input_gaps"].append({"kind": "slice_absent", "domain_id": domain_id})
continue
slice_root = _dict(_dict(slice_doc).get("stage_b_domain_slice", slice_doc))
declarations = _dict(slice_root.get("domain_declarations"))
slice_ids = sorted(_strings(declarations.get("structure_type_ids")))
config_ids = sorted({row["type_id"] for row in declared_by_domain[domain_id]})
if slice_ids != config_ids:
review(REVIEW_SLICE_DIVERGENCE, "HARD_WARNING", domain_id, {
"slice_only": sorted(set(slice_ids) - set(config_ids)),
"config_only": sorted(set(config_ids) - set(slice_ids)),
})
# ---------------- L0-3 · SG-13 조인 + L0-4 · 도메인 소속 ----------------
routes = [_dict(row) for row in sg13_records
if _dict(row).get("record_type") == SG13_ROUTE_RECORD_TYPE]
attached_route_count = 0
unattached_routes: list[dict[str, Any]] = []
for route in routes:
details = _dict(route.get("details"))
type_id = details.get("type_id")
route_domains = _strings(route.get("domain_ids"))
route_domain = route_domains[0] if route_domains else None
registered = details.get("registered")
route_ref = {
"signal_id": route.get("signal_id"),
"source_seed_id": details.get("source_seed_id"),
"registered": registered if isinstance(registered, bool) else None,
"status": route.get("status"),
}
if not isinstance(type_id, str) or type_id not in theta:
# Θ 밖 — 버리지 않는다. default_projection 이 있으면 검토 항목에 실어 함께 나른다.
projection = None
if route_domain and route_domain in projection_norm:
projection = projection_norm[route_domain].get("default_projection")
review(REVIEW_UNREGISTERED, "HARD_WARNING", route_domain, {
"type_id": type_id, "signal_id": route.get("signal_id"),
"default_projection": projection,
})
unattached_routes.append({"signal_id": route.get("signal_id"),
"type_id": type_id, "reason": "type_outside_theta"})
continue
target_keys = []
if route_domain and (route_domain, type_id) in records:
target_keys = [(route_domain, type_id)]
else:
# Θ 안이지만 자기 도메인이 선언하지 않은 유형 — 선언 도메인 전부에 붙이고 검토(L0-4).
target_keys = [(d, type_id) for d in declaring_domains.get(type_id, [])
if (d, type_id) in records]
if target_keys:
review(REVIEW_CROSS_DOMAIN, "HARD_WARNING", route_domain, {
"type_id": type_id, "signal_id": route.get("signal_id"),
"attached_to": ["%s:%s" % key for key in target_keys],
})
if not target_keys:
unattached_routes.append({"signal_id": route.get("signal_id"),
"type_id": type_id, "reason": "no_declaring_domain"})
review(REVIEW_UNREGISTERED, "HARD_WARNING", route_domain, {
"type_id": type_id, "signal_id": route.get("signal_id"),
"default_projection": None,
})
continue
attached_route_count += 1
for key in target_keys:
record = records[key]
record["routes"].append(dict(route_ref))
for field in ("evidence_refs", "meeting_clause_refs"):
merged = set(record[field]) | set(_strings(route.get(field)))
record[field] = sorted(merged)
if registered is False:
# 붙은 채로 검토 꼬리표를 단다(L-m2 — 분할은 부착 여부로만 가른다).
if REVIEW_UNREGISTERED not in record["review_codes"]:
record["review_codes"].append(REVIEW_UNREGISTERED)
record["review_required"] = True
review(REVIEW_UNREGISTERED, "SOFT_WARNING", key[0], {
"type_id": type_id, "signal_id": route.get("signal_id"),
"detail": "registered_false_route_attached",
})
declared_flag = record.get("registered_declared")
if isinstance(declared_flag, bool) and isinstance(registered, bool) \
and declared_flag != registered:
# types 방언의 registered ↔ SG-13 registered 대조(L0-2c 둘째 갈래).
if REVIEW_SIDECAR_MISMATCH not in record["review_codes"]:
record["review_codes"].append(REVIEW_SIDECAR_MISMATCH)
record["review_required"] = True
review(REVIEW_SIDECAR_MISMATCH, "HARD_WARNING", key[0], {
"detail": "registered_flag_mismatch", "type_id": type_id,
"sidecar": declared_flag, "route": registered,
})
# ---------------- L0-5 · module 부여는 L0-2 에서 config 원문으로 끝났다 ----------------
# ---------------- L0-6 · 계산 조인 — SG-11 signal_id ↔ 도메인 봉투 calculation_refs ----------------
# 봉투 부재는 침묵 스킵이 아니라 영수증 계수다(I-5 — expected_runnable 중 실재하는 것만 읽되
# 부재를 기록한다). 실주행에서 감시 도메인 봉투가 없는 것은 상시 경로다.
for domain_id in expected_runnable:
if domain_id not in domain_envelopes:
audit["input_gaps"].append({"kind": "domain_envelope_absent",
"domain_id": domain_id})
sg11_rows = [_dict(row) for row in sg11_records]
sg11_ids = {row.get("signal_id") for row in sg11_rows if isinstance(row.get("signal_id"), str)}
linked_sg11: set[str] = set()
for domain_id in expected_runnable:
envelope = _dict(_dict(domain_envelopes.get(domain_id)).get("domain_signal_envelope",
domain_envelopes.get(domain_id)))
refs = _strings(_dict(envelope).get("calculation_refs"))
if not refs:
continue
matched = sorted(set(refs) & sg11_ids)
dangling = sorted(set(refs) - sg11_ids)
if dangling:
audit["dangling_calculation_refs"][domain_id] = dangling
review(REVIEW_CALC_DANGLING, "HARD_WARNING", domain_id, {"dangling": dangling})
if matched:
linked_sg11.update(matched)
for (record_domain, _type_id), record in records.items():
if record_domain == domain_id:
merged = set(record["calculation_request_refs"]) | set(matched)
record["calculation_request_refs"] = sorted(merged)
# 봉투 참조에 연결되지 않은 SG-11(합성 id 포함)은 구조에 싣지 않되 형식화 기록(신전략서 L0-6).
audit["unlinked_sg11"] = sorted(sg11_ids - linked_sg11)
# ---------------- L0-7 · SG-05 근거 refs (도메인 단위 합집합) ----------------
for row in sg05_records:
row = _dict(row)
for domain_id in _strings(row.get("domain_ids")):
for (record_domain, _type_id), record in records.items():
if record_domain == domain_id:
for field in ("evidence_refs", "meeting_clause_refs"):
merged = set(record[field]) | set(_strings(row.get(field)))
record[field] = sorted(merged)
# ---------------- L0-7b · BO 부착 조인 ----------------
alias_targets = _dict(_dict(alias_declarations).get("legacy_alias_targets"))
bo_rows = [_dict(row) for row in bo_records]
bo_attached: list[str] = []
bo_unattached: list[dict[str, Any]] = []
for bo in bo_rows:
bo_id = bo.get("BO_ID") or bo.get("id")
provenance = _dict(bo.get("provenance"))
source_domain = provenance.get("source_domain")
resolved = resolve_source_domains(source_domain, alias_targets, known_domain_ids)
if not resolved:
review(REVIEW_BO_UNRESOLVED, "HARD_WARNING", None, {
"bo_id": bo_id, "source_domain": source_domain,
})
bo_unattached.append({"bo_id": bo_id, "reason": "source_domain_unresolved"})
continue
keywords = _strings(bo.get("Legal_Keywords"))
attached_here = False
for domain_id in resolved:
for type_id in keywords:
record = records.get((domain_id, type_id))
if record is not None:
attached_here = True
if isinstance(bo_id, str) and bo_id not in record["source_bo_ids"]:
record["source_bo_ids"].append(bo_id)
record["source_bo_ids"].sort()
if attached_here:
bo_attached.append(bo_id if isinstance(bo_id, str) else "")
else:
# 부착 실패는 침묵 금지(L0-7b ③).
review(REVIEW_BO_UNATTACHED, "HARD_WARNING", None, {
"bo_id": bo_id, "source_domain": source_domain,
"legal_keywords": keywords, "resolved_domains": resolved,
})
bo_unattached.append({"bo_id": bo_id, "reason": "no_matching_structure"})
# ---------------- 정렬 · 검토 재료 반영 · 산출 ----------------
ordered = sorted(records.values(),
key=lambda record: (record["order_key"][0] if isinstance(
record["order_key"][0], int) else 999,
record["order_key"][1], record["order_key"][2]))
for record in ordered:
record["route_count"] = len(record["routes"])
record["routes"].sort(key=lambda ref: (str(ref.get("signal_id")),
str(ref.get("source_seed_id"))))
record["review_codes"].sort()
if record["review_codes"]:
record["review_required"] = True
status = "READY_WITH_REVIEW" if review_items else "READY"
bundle = {
"schema_version": SCHEMA_VERSION,
"status": status,
"registry_version": index_root.get("registry_version"),
"expected_runnable_domain_ids": list(expected_runnable),
"theta": theta,
"structure_records": ordered,
"unattached_routes": sorted(unattached_routes,
key=lambda row: (str(row.get("signal_id")),
str(row.get("type_id")))),
"unattached_bo": sorted(bo_unattached, key=lambda row: str(row.get("bo_id"))),
"counts": {
"structure_records": len(ordered),
"declared_structure_types_total": sum(
len(rows) for rows in declared_by_domain.values()),
"sg13_route_total": len(routes),
"routes_attached": attached_route_count,
"routes_unattached": len(unattached_routes),
"bo_total": len(bo_rows),
"bo_attached": len(bo_attached),
"bo_unattached": len(bo_unattached),
"sg11_total": len(sg11_rows),
"sg11_linked": len(linked_sg11),
"sg11_unlinked": len(audit["unlinked_sg11"]),
},
"created_by": "Task_C_LE_L0_structure_seed_reducer",
}
return {"structure_seed_bundle": bundle, "review_items": review_items, "audit": audit}
@@ -0,0 +1,98 @@
{
"schema_version": "stage1_structure_index_policy.v1",
"status": "ACTIVE",
"purpose": "Part 3 구조 seed 조립·색인 규칙을 코드 밖에 선언한다. 값의 정본은 registry(domain_config.structure_types)이고, registry 가 말하지 않는 값은 만들지 않고 검토로 올린다. 선언의 목적문은 적대 검증의 오라클이다.",
"authority": {
"theta_source": "SG-01 domain_activation_manifest.expected_runnable_domain_ids",
"structure_type_source": "Default_Agent/domains/<id>/domain_config.json#structure_types",
"sidecar_source": "Default_Agent/domains/<id>/structure_types.json · module_role_projection.json",
"alias_source": "Default_Agent/routing/extension_payload_key_declarations.v1.json#legacy_alias_targets",
"case_kind_names_forbidden": true
},
"theta_definition": {
"formula": "Θ(사건) = ⋃ { domain_config(d).structure_types[].type_id | d ∈ expected_runnable_domain_ids }",
"note": "active_domain_ids 가 아니다(D-3 넓은 정의). 좁히면 감시·상시 도메인의 효과가 전부 검토로 쓸려 들어가고 커버리지 등식은 그래도 통과한다 — 조용히 틀리는 경로다."
},
"record_identity": {
"rule": "구조 레코드는 Θ 의 (도메인, type_id) 쌍마다 정확히 하나다. 도메인 간에 유형을 합치지 않는다.",
"structure_id": "LES-<domain_id>-<type_id>",
"note": "SG-13 route·BO 부착은 레코드 위에 붙는 근거이지 레코드 생성 주체가 아니다. 근거 없는 유형은 '선언됐으나 근거 없음'으로 보이고, 선언 0 도메인(EC-00)은 자연히 0 이다."
},
"total_order": {
"order_key": [
"그 도메인이 선언한 priority_rank",
"domains/_registry_index.json entries[] 의 도메인 위치",
"type_id"
],
"note": "min(rank) 병합 금지 — 도메인 내부 rank 는 26/26 전부 엄격 오름차순이라 이 3키가 각 도메인의 선언 순서를 보존한다."
},
"admission_policy": {
"definition": "두 단 — 그 도메인 사이드카의 파일 단위 정책 키(admission_rules · unregistered_effect_policy · fallback_policy 중 있는 것, 값 'file:<키>') → 없으면 항목 단위 admission_rule(값 'item:admission_rule') → 어느 단도 없으면 'absent'.",
"note": "admission_rule 은 entries 방언 13건에만 있는 선택 필드다 — 게이트 기준으로 쓰지 않는다. 원본 보존을 위해 있는 것만 admission_rule 필드로 함께 싣는다."
},
"fallback_policy": {
"silent_general_legal_effect_fallback_forbidden": true,
"rule": "빈 입력은 structure_count 0 으로 기록한다. Θ 밖·registered:false 유형은 버리지도 일반화하지도 않고 UNREGISTERED_EFFECT_TYPE_REVIEW 를 붙인다. 사이드카 default_projection 이 있으면 그 투영을 검토 항목에 실어 나르되 검토 코드를 반드시 동반한다."
},
"attachment_partition": {
"rule": "route 의 분할은 부착 여부로만 가른다 — 붙은(어떤 레코드에 근거로 실림) / 미부착(어느 레코드에도 닿지 못함). 검토 여부는 분할과 무관하다. registered:false 라도 type_id 가 Θ 안·선언 도메인이 있으면 붙은 채로 검토 꼬리표를 단다(이중 계상 금지)."
},
"bo_attachment": {
"alias_resolution": "provenance.source_domain 이 registry 도메인 ID 면 [그 값], 아니면 legacy_alias_targets[값] 목록(다중 타깃 실재 — 단일 primary 개념 없음), 어디에도 없으면 PART3_BO_DOMAIN_UNRESOLVED 검토로 격리.",
"join": "해소된 모든 도메인 d 에 대해 t ∈ BO.Legal_Keywords 인 구조 레코드 (d, t) 에 source_bo_ids 로 부착을 시도한다.",
"silence_forbidden": "Legal_Keywords 가 비었거나 어느 (d, t) 에도 닿지 않는 BO 는 PART3_BO_UNATTACHED 검토로 싣는다.",
"purpose": "원본 review_code 와 마찬가지로 — BO 연결을 잃지 않기 위해서다. SG 층의 source_bo_ids 가 비어 있어도 이 조인이 BO↔구조 연결을 세운다."
},
"sg11_join": {
"key": "SG-11 레코드의 signal_id (worker request_id 승계값 — details.request_id 는 보장되지 않는 키라 쓰지 않는다)",
"counterpart": "domain_signal_envelope.calculation_refs",
"unlinked_rule": "봉투 참조에 연결되지 않은 SG-11 레코드는 구조에 싣지 않되 영수증에 미연결 계수로 형식화 기록한다.",
"dangling_rule": "봉투에 있는데 SG-11 에 없는 참조는 PART3_CALCULATION_REF_DANGLING 검토."
},
"extension_key_rule": {
"domain_id": "E-13",
"extension_key": "actio_case_signals.v2",
"note": "별도 매핑표를 만들지 않는다 — s5_execution_contract.v2.json 의 e13_extension_domains 선언을 그대로 싣는다."
},
"conservation_equations": {
"declaration": "구조 레코드 수 == Σ(expected_runnable 도메인의 structure_types 개수) — 어긋나면 PART3_STRUCTURE_COUNT_BROKEN",
"route": "붙은 route 수 + 미부착 route 수 == SG-13 legal_effect_route 총수 — 어긋나면 PART3_ROUTE_CONSERVATION_BROKEN",
"bo": "부착 BO 수 + 미부착 BO 수 == BO.json 레코드 총수 — 어긋나면 PART3_BO_CONSERVATION_BROKEN",
"note": "세 등식 다 L2 가 원본 파일(domain_config · legal_effect_routes · BO.json)에서 직접 센 바깥 수와 비교한다. bundle 에 실린 수와 비교하면 항등식으로 퇴행한다."
},
"structure_index": {
"indexes": [
"by_domain_id",
"by_module",
"by_bo_id"
],
"by_claim_form_forbidden": "claim-form 어휘가 registry 에 없고 (module, role_projection) 은 type_id 와 일대일이라 색인이 되지 못한다. 필요하면 registry 에 어휘를 새로 선언하는 일이며 Part 3 범위 밖이다."
},
"review_codes": [
"UNREGISTERED_EFFECT_TYPE_REVIEW",
"PART3_SLICE_PROJECTION_DIVERGENCE",
"PART3_REGISTRY_SIDECAR_MISMATCH",
"PART3_EFFECT_TYPE_CROSS_DOMAIN",
"PART3_CALCULATION_REF_DANGLING",
"PART3_BO_UNATTACHED",
"PART3_BO_DOMAIN_UNRESOLVED"
],
"hard_error_codes": [
"PART3_ASSET_DEPLOYMENT_INCOMPLETE",
"PART3_POLICY_INVALID",
"PART3_STRUCTURE_SCHEMA_STALE",
"PART3_COMPILER_STALE",
"PART3_INDEX_SCHEMA_STALE",
"PART3_SIGNAL_SEAL_FAILED",
"PART3_REGISTRY_INDEX_SEAL_FAILED",
"PART3_DOMAIN_NOT_IN_REGISTRY",
"PART3_STRUCTURE_COUNT_BROKEN",
"PART3_ROUTE_CONSERVATION_BROKEN",
"PART3_BO_CONSERVATION_BROKEN",
"PART3_FINAL_SCHEMA_VALIDATION_FAILED",
"PART3_EXPECTED_RUNNABLE_EMPTY",
"PART3_SIDECAR_ABSENT_FOR_DECLARING_DOMAIN"
],
"e00_rule": "E-00(잔여 수집기)에서 온 구조 레코드는 review_required true 를 강제한다.",
"sidecar_absence_rule": "structure_types 선언이 0 인 도메인(오늘 EC-00 하나)의 사이드카 부재는 정상 — 스킵하고 영수증에 기록한다. 선언이 있는 도메인의 부재·미등재는 경성 실패다."
}
@@ -0,0 +1,58 @@
# Stage 1 — Part 3 개정 작업 내역서
- 문서 판본: v1 (2026-08-19)
- 지시 원문: `stage_1_part_3_개정_신전략서.md` (§3 입력 계약 · §4 작업 명세 · §5 자산 · §6 실행 단계 · §7 검증 · §10 완료 판정)
- 판정 기준(완수): 실행 완주가 아니라 **의도한 결과물이 올바른 방식으로 생성 + 소작업 간 upstream–downstream consistency 유지**.
- P3-0a(D-4 승인): 사용자의 "신전략서를 실행하라"는 지시를 승인으로 처리하고 착수했다.
- 결과: **§4·§5 처방 전건 반영 + 회귀 22종 및 보충 실측 2종 전건 통과 + 적대적 sub-agent 검증 2라운드(1차 지적 5건 전부 반영 · 재검증 "지적 0건")** — Part 3 는 예행 전 사슬(3패스 11단계)에서 완수 기준을 충족했다.
---
## 0. 요약
Part 3 작업명세서를 **registry 와 Part 1·2 실산출 계약만으로 새로 지었다**(구 Part 3 v2 무참조 — 제약 1). task 는 둘이다: `Task_C_LE_L0_structure_seed_reducer`(Θ 조립 → 구조 레코드 → SG-13·SG-11·BO 조인 → 검토) → `Task_C_LE_L2_final_structure_index_writer`(보존 등식 셋 → 역인덱스 셋 → 최종 기록). **L1 은 두지 않았다** — LLM 이 판정할 예외 부류가 없다(신전략서 §4.1). 조립·판정 로직은 신설 모듈 `structure_index_compiler` 에 있고 YAML 은 반입·게이트·봉인·기록만 한다.
가장 중요한 실측: **예행 전 사슬에서 `legal_effect_structures.json` 이 실제로 선다** — Θ 4종 · 구조 레코드 6건(레코드는 (도메인, type_id) 쌍마다 하나) · SG-13 route 6/6 부착 · **BO 2/2 부착**(`provenance.source_domain` + `Legal_Keywords` 결정론 조인 — SG 층의 `source_bo_ids` 가 비어 있어도 BO↔구조 연결이 섬) · 보존 등식 셋 전부 성립(바깥 수는 L2 가 원본에서 직접 계수) · 역인덱스 셋(`by_domain_id` · `by_module` · `by_bo_id`). 26 도메인 합성 실측은 **Θ 56종 · 구조 73건 · module 34종 · 결정론**이다.
## 1. 작업 분류 (method 1)
**병렬(독립) 수행분** — 서로 파일이 겹치지 않음: 정책 자산 작성 · 스키마 2종 작성 · 컴파일러 모듈 작성 · 사이드카 50 복사.
**순차 수행분** — 상하류 의존: 매니페스트 등재(자산 확정 후, 원자적 1회) → L0/L2 yaml 조각 → 통합본 병합 → 선언표·stage_chain·상위 전략서 → 예행 3패스 → 회귀 → 검증 루프 → 내역서.
## 2. 산출물 목록
| 파일 | 상태 |
| --- | --- |
| `ver_8_yaml_candidates/stage_1_part_3_v.8.yml` | **신규** (52,878 B · task 2 · `IN→L0→L2→OUT` · 선언 1벌) |
| `ver_8_yaml_candidates/P3-L0_structure_seed_reducer_v1.yml` · `P3-L2_final_structure_index_writer_v1.yml` | **신규** 조각 (통합본과 블록 동일) |
| `Default_Agent/stage1_runtime/structure_index_compiler.py` + `.txt` | **신규** (순수 모듈 — I/O·시각·난수 없음. 방언 3·3 정규화 리더 · Θ · 전순서 · 조인 · 검토) |
| `Default_Agent/stage1_runtime/structure_index_policy.v1.json` | **신규** (`stage1_structure_index_policy.v1` — Θ 정의 · 3키 전순서 · admission 2단 · 부착 분할 · BO 조인 · SG-11 키 · 등식 셋 · 코드 목록. 검증 오라클) |
| `Default_Agent/platform/schemas/structure_seed_bundle.schema.json` · `legal_effect_structures.schema.json` | **신규** (파일명 무판본 · 열거 하드코딩 없이 패턴 3종만) |
| `Default_Agent/domains/<id>/structure_types.json` · `module_role_projection.json` ×25 | **복사 배포** (조립본과 50/50 바이트 동일) |
| `Default_Agent/runtime_manifest.json` | 갱신 (**371 → 426** · 델타 정확히 55 · 기존 행 수정 0 · 왕복 보존) |
| `선행구축/.../handoffs/stage1_part_interface.v1.json` | 갱신 (신규 11줄 + 기존 갱신 — `handoff_count` 34→**45** · C-7 구 이름 3종 Part 3 소비자 제거 · C-8 `client_goal` 등재·`client_meeting` 소비자 · compact 직렬화 보존) |
| `선행구축/.../handoffs/stage1_stage_chain.v1.json` | 갱신 (Part 3 구간 — yaml·requires·produces·이관 완료 note) |
| `stage_1_update_strategy.md` | 갱신 두 곳 (§1 사슬 `S0→L0→L2` · §4.2 L1 부재 주석) |
| 예행 증거 | `/tmp/dry_p3/` (3패스 산출 전부) · 회귀 결과 scratchpad `part3_regression_results.json` |
배포 트리 최종 210 파일(155+55) · 매니페스트 등재-실물(트리 실재 149건) 전수 해시 일치 · `__pycache__` 0.
## 3. 검증 (method 3)
**정적** — yaml parse PASS · 선언 1벌 · AST 미정의 이름 0 · 개별↔통합 블록 동일 · 비주석 금지어(Claude_YAML·구 signal 이름·case_kind matrix·조립본 경로) 0 · code-executor parameters 5키 · 선언표 검사기 **finding 0**(3 yaml 전수).
**예행 3패스** (asset root = 배포 원본 `extension_research/Default_Agent` · 실행 루트 신선): pass1 PASS 6단계/45건 · seed 대역 3건 · pass2 PASS 3단계/29건 · **pass3 PASS 2단계/5건**. 산출 실측 — 최종 status READY · 봉인 승계(`S5TX-…`) · 레코드 6 · 게이트 3 전부 pass · handoff FINALIZED · 영수증(게이트 10 자산/실측·봉인 둘·조인 계수·SG-02/07 공백 형식화·사이드카 방언 지도·BO sha·전순서표).
**회귀 22종(L-a~L-s) + 보충 2종 = 24/24 PASS.** 필수 8종(L-b·b2·c·d2·k·m2·p·q) 포함 전건 기대대로 발화. 변조는 전부 사본 트리/실행 루트에서 수행(배포 원본 무변). 보충: SUPP-synth26(Θ56·73·34·머리 4줄·결정론) · SUPP-calc-probe(정상 조인·dangling 검토·미연결 형식화).
**적대적 sub-agent 2라운드**: 1차 지적 5건(MAJOR 2 — ① 선언표 `handoffs` 배열의 `client_meeting` 행 미갱신(중복 행 절반만 반영) ② 도메인 봉투 부재의 침묵 스킵(영수증 계수 부재) / MINOR 3 — L2 의 계약 밖 `signal_manifest` 읽기 · 합성/탐침 증적 미보존 · `Stage_1.Part_3` 약칭 잔존) — **전부 반영**(봉투 부재는 `audit.input_gaps` 기록 + L-k 단언, transaction_id 는 L0 가 bundle 에 부착해 L2 가 승계). 재검증: **"지적 0건"**(반영 5 + 파급 12 = 17건 실측 전건 통과).
## 4. 한계 (정직 선언)
- 예행은 LLM 대역 기반이다 — 실체 도메인 활성·실서버 실행은 범위 밖(행동 검증 **미측정** 명시, 신전략서 §7.1).
- 예행 Θ 는 4종에 그치므로 26 도메인 합성·계산 조인 탐침으로 따로 실측했다(§7.3 처방 그대로).
- P3-2 의 "정책·스키마 자기 검증" 별도 기록은 남기지 않았다 — 예행에서 두 스키마가 실주입 검증됐고(L0·L2 닫힌 스키마 통과) 회귀 L-b/L-b2 가 스키마 능력 검사의 발화를 실증한다.
## 5. 이월 (조립본 동기화 회차 — 신전략서 §6.1)
① Part 3 신설 자산 5종(+미러)·사이드카 등재의 조립본 반영 + 빌더 `PART3_ASSEMBLY_AUTHORED` 등록 + 사이드카 sweep 규칙(파일 반영과 등록은 한 벌) ② `release_manifest`·`source_copy_map` 재생성 ③ 신선 빌드의 Part 3 자산 재현 확인 ④ `dry_run_receipt` 정본화(Part 2 M-* + Part 3 L-* 합본) ⑤ `effect_projection.schema_ref` dangling 해소(registry 소유자). Part 2 의 D-1~D-6 과 한 벌로 처리한다. Part 3 실행 정합성 무영향은 배포 원본 단독 예행 전 사슬로 실증돼 있다.
@@ -173,7 +173,7 @@ signal 파일명은 다음과 같다. SG-01 `domain_activation_manifest.json`, S
└─▶ Fact_Ledger_base.json + writer report ──▶ Stage 2
의존: T0→{T1,D0} · {T5,T6,T8}→D0 · D0→T9 · T9→A0 · fanout_plan→B*→R0→R1→F0→S0
S0→L0→L1→L2→P4-F0→F1→F2
S0→L0→L2→P4-F0→F1→F2 (L1 은 두지 않는다 — stage_1_part_3_개정_신전략서.md §4.1)
```
LLM 호출 구조: Part 1은 스크리너 1 + Task_A 1 + 매퍼 2N + 조건부 판정(대개 즉시 종료). Part 2는 worker M개 + 조건부 판정. Part 3·4는 조건부 판정만. 순증 비용은 스크리너 1회와 활성 도메인 증가분이며, 상시 도메인과 감시 도메인은 낮은 등급 모델로 돈다.
@@ -287,7 +287,7 @@ signal마다 받아들이는 조건이 registry에 선언되어 있으므로 그
### 4.2 P3-L1 예외 판정
무변경이다. 허용 결정 목록과 후보값 부분집합 규율을 유지하고, 예외 묶음에 동적 열거 목록이 동봉될 뿐이다.
(개정 실행에서 L1 은 두지 않는 것으로 확정됐다 — LLM 이 판정할 예외 부류가 없다. 되돌림 경로와 근거는 `stage_1_part_3_개정_신전략서.md` §4.1. 아래 원문은 기록으로 남긴다.) 무변경이다. 허용 결정 목록과 후보값 부분집합 규율을 유지하고, 예외 묶음에 동적 열거 목록이 동봉될 뿐이다.
### 4.3 P3-L2 최종 색인 기록 [개정]
@@ -0,0 +1,567 @@
# =============================================================================
# P3-L0 / Task_C_LE_L0_structure_seed_reducer — v1 (신설)
#
# 정본 근거 : stage_1_part_3_개정_신전략서.md §4.2 (L0-1 ~ L0-8) · §3 (I-1 ~ I-14) · §5
# 계약 : Θ = SG-01.expected_runnable_domain_ids 의 domain_config.structure_types 합집합
# 구조 레코드 = (도메인, type_id) 쌍마다 하나 · order_key 3키 전순서
# 조립·판정 로직은 stage1_runtime/structure_index_compiler 모듈에 있다.
# 이 task 는 자산 반입 → 게이트 → 봉인 → 적재 → 모듈 호출 → 기록만 한다.
# 제약 1 : v.7/Claude_YAML 의 어떤 파일도 읽지 않는다. 구 Part 3 에 의존하지 않는다.
# 137종 : 사건 이름·도메인 목록·유형 열거를 코드에 두지 않는다.
# =============================================================================
- task_name: Task_C_LE_L0_structure_seed_reducer
mcp: code-executor
tool_name: run_code
parameters:
language: python
requirements: "httpx"
network: "agent-network"
timeout: 240
code: |-
#!/usr/bin/env python3
# Task_C_LE_L0_structure_seed_reducer (v1)
# 0) 배포 게이트 -> 1) 모듈 반입 -> 2) 정책 반입 -> 3) 의미 검사 둘 ->
# 4) 진입 봉인 둘 -> 5) 입력 적재 -> 6) compile_structure_seeds ->
# 7) bundle · review handoff · receipt 기록
import contextlib
import hashlib
import io
import itertools
import json
import pathlib
import re
import sys
import httpx
# ------------------------------------------------------------------
# localdocs 보일러플레이트 (SKILL.md 5장 / 5.2장) — Part 2 A0 검증 완료본 복사.
# clientInfo 에 {{__user_hash__}} / {{__workspace_hash__}} 를 반드시 넣는다.
# ------------------------------------------------------------------
TASK_NAME = "Task_C_LE_L0_structure_seed_reducer"
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):
# 재직렬화 없이 원문 그대로. 봉인·미러 해시 대조의 전제다.
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()
try:
return json.loads(s)
except json.JSONDecodeError:
obj, _ = json.JSONDecoder().raw_decode(s)
return obj
def try_read_json(name):
try:
return read_json(name)
except Exception:
return None
def write_doc(path, content):
_call("write_file", {"path": path, "content": content, "overwrite": True},
next_msg_id())
# ------------------------------------------------------------------
# 실행 뿌리 — /tmp/s1 공유(신전략서 §4.2 도입부. 새 뿌리를 만들지 않는다).
# ------------------------------------------------------------------
ASSET_ROOT = "Default_Agent/"
EXECUTION_ROOT = "/tmp/s1"
RT = pathlib.Path(EXECUTION_ROOT) / "_rt"
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",
"structure_index_compiler": "Default_Agent/stage1_runtime/structure_index_compiler.txt",
}
MODULES = ["runtime_common", "schema_subset_validator", "registry_loader",
"structure_index_compiler"]
RUNTIME_MANIFEST = "Default_Agent/runtime_manifest.json"
RUNTIME_MANIFEST_SCHEMA = "stage1_runtime_manifest.v1"
# 정적 자산 (I-9 ~ I-14)
REGISTRY_INDEX = "Default_Agent/domains/_registry_index.json"
POLICY = "Default_Agent/stage1_runtime/structure_index_policy.v1.json"
POLICY_SCHEMA_VERSION = "stage1_structure_index_policy.v1"
ALIAS_DECLARATIONS = "Default_Agent/routing/extension_payload_key_declarations.v1.json"
SEED_SCHEMA = "Default_Agent/platform/schemas/structure_seed_bundle.schema.json"
INDEX_SCHEMA = "Default_Agent/platform/schemas/legal_effect_structures.schema.json"
# 사건 산출물 (I-1 ~ I-8) — Part 1·2 가 낸 것만 읽는다.
SIGNAL_MANIFEST = "signals/signal_manifest.json"
SG13_PATH = "signals/legal_effect_routes.json"
SG05_PATH = "signals/legal_relation_lifecycle_signals.json"
SG11_PATH = "signals/calculation_requirements.json"
SG02_PATH = "signals/procedural_posture_relief_signals.json"
SG07_PATH = "signals/asset_right_state_signals.json"
DOMAIN_SIGNAL_DIR = "signals/domain_signals"
ACTIVATION_MANIFEST = "routing/domain_activation_manifest.json"
BO_PATH = "BO.json"
SLICE_DIR = "runtime/domain_slices"
# 산출
BUNDLE_PATH = "stage1_tmp/task_le/structure_seed_bundle.json"
HANDOFF_PATH = "quality_gates/stage1_part3_review_handoff.json"
RECEIPT_PATH = "validation_assets/routing/part3_receipt.json"
# F-2 상당 — 배포 게이트가 한 벌로 검사하는 필수 자산 10 리터럴(신전략서 §5.5).
# 사이드카 50 은 여기 넣지 않는다 — expected_runnable 도메인 것만 읽는 시점에
# _verify_asset 으로 지킨다. domains/_registry_index.json 은 매니페스트 등재
# 대상이 아니므로 SG-01 봉인이 지킨다(I-11 · §1.10 규율).
PART3_REQUIRED_ASSETS = (
SEED_SCHEMA,
INDEX_SCHEMA,
POLICY,
ALIAS_DECLARATIONS,
"Default_Agent/signals/signal_registry.v2.json",
"Default_Agent/contracts/signals/s5_execution_contract.v2.json",
"Default_Agent/stage1_runtime/structure_index_compiler.txt",
"Default_Agent/stage1_runtime/runtime_common.txt",
"Default_Agent/stage1_runtime/schema_subset_validator.txt",
"Default_Agent/stage1_runtime/registry_loader.txt",
)
PART3_DEPLOYMENT_REMEDY = (
"배포 원본 extension_research/Default_Agent 트리를 서버 Default_Agent/ 에 통째로 배포하고 "
"runtime_manifest.json 의 등재·sha256 과 대조해 결손 경로를 복원한다. 부분 복사는 허용되지 않는다.")
TRANSACTION_ID_RE = re.compile(r"^S5TX-[a-f0-9]{20}$")
ERRORS = []
WARNINGS = []
def sha_text(text):
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def canonical(value):
return json.dumps(value, ensure_ascii=False, sort_keys=True,
separators=(",", ":")) + "\n"
# ------------------------------------------------------------------
# 0) 배포 게이트 — 등재·sha256·가독을 한 벌로. 첫 실패에서 멈추지 않고 모은다.
# ------------------------------------------------------------------
def assert_deployment():
manifest = json.loads(read_raw(RUNTIME_MANIFEST))
if manifest.get("schema_version") != RUNTIME_MANIFEST_SCHEMA:
raise RuntimeError(json.dumps({
"reason_code": "PART3_RUNTIME_MANIFEST_INVALID", "detail": "schema_version",
"expected": RUNTIME_MANIFEST_SCHEMA, "actual": manifest.get("schema_version"),
}, ensure_ascii=False))
expected = {row["path"]: row["sha256"]
for row in (manifest.get("entries") or []) if isinstance(row, dict)}
unregistered, mismatch, unreadable = [], [], []
for logical in sorted(set(list(MODULE_MIRRORS.values()) + list(PART3_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": "PART3_ASSET_DEPLOYMENT_INCOMPLETE",
"message": "배포 원본에 Part 3 자산이 한 벌로 반영되지 않았다.",
"unregistered": unregistered, "hash_mismatch": mismatch,
"unreadable": unreadable, "remedy": PART3_DEPLOYMENT_REMEDY,
}, ensure_ascii=False))
return manifest
_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": "PART3_ASSET_DEPLOYMENT_INCOMPLETE", "path": logical,
"detail": detail, "remedy": PART3_DEPLOYMENT_REMEDY}
if extra:
payload.update(extra)
return RuntimeError(json.dumps(payload, ensure_ascii=False))
def _verify_asset(logical):
"""원문을 돌려준다. 등재·해시 대조를 통과해야만 소비된다."""
body = read_raw(logical)
want = _manifest_map().get(logical[len(ASSET_ROOT):])
if want is None:
raise _deployment_error(logical, "unregistered")
if want != sha_text(body):
raise _deployment_error(logical, "hash_mismatch", {"expected": want})
return body
# ------------------------------------------------------------------
# 1) 모듈 반입 — R-1~R-5.
# ------------------------------------------------------------------
def materialize_modules():
RT.mkdir(parents=True, exist_ok=True)
expected = _manifest_map()
staged = []
for name in MODULES:
logical = MODULE_MIRRORS[name]
raw = read_raw(logical).encode("utf-8")
want = expected.get(logical[len(ASSET_ROOT):])
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
# ------------------------------------------------------------------
# 3) 의미 검사 — 장부 검사만으로는 닫히지 않는다(Part 2 R-b/R-g2 교훈).
# 각 검사는 자기가 쓰는 산출물의 스키마만 본다. by_module 은 L2 몫이다.
# ------------------------------------------------------------------
def assert_seed_schema_capability(seed_schema):
bundle_props = (((seed_schema.get("properties") or {})
.get("structure_seed_bundle") or {}).get("properties") or {})
record_props = ((((bundle_props.get("structure_records") or {})
.get("items") or {}).get("properties") or {}))
missing = []
if "theta" not in bundle_props:
missing.append("theta")
if "admission_policy" not in record_props:
missing.append("structure_records[].admission_policy")
if missing:
raise RuntimeError(json.dumps({
"reason_code": "PART3_STRUCTURE_SCHEMA_STALE",
"message": "seed 스키마가 Part 3 계약 필드를 모른다. 배포 원본의 스키마가 개정 전 판본이다.",
"missing": missing, "remedy": PART3_DEPLOYMENT_REMEDY,
}, ensure_ascii=False))
def assert_compiler_capability(result):
bundle = (result or {}).get("structure_seed_bundle") or {}
records = bundle.get("structure_records")
bad = ("theta" not in bundle
or not isinstance(records, list)
or any("order_key" not in record for record in records
if isinstance(record, dict)))
if bad:
raise RuntimeError(json.dumps({
"reason_code": "PART3_COMPILER_STALE",
"message": "컴파일러 산출에 theta·order_key 가 실려 나오지 않는다. "
"배포 원본의 structure_index_compiler 가 개정 전 판본이다.",
"remedy": PART3_DEPLOYMENT_REMEDY,
}, ensure_ascii=False))
# ------------------------------------------------------------------
# 4) 진입 봉인 둘 (신전략서 §4.2-5)
# ------------------------------------------------------------------
def verify_signal_seal():
doc = read_json(SIGNAL_MANIFEST)
root = doc.get("signal_manifest", doc) if isinstance(doc, dict) else {}
transaction_id = root.get("transaction_id")
if not isinstance(transaction_id, str) or not TRANSACTION_ID_RE.match(transaction_id):
raise RuntimeError(json.dumps({
"reason_code": "PART3_SIGNAL_SEAL_FAILED", "detail": "transaction_id",
"actual": transaction_id}, ensure_ascii=False))
checked = 0
for row in root.get("files") or []:
if not isinstance(row, dict):
continue
# files[].path 에는 signals/ 접두사가 없다(I-1 주의). 붙여서 대조한다.
rel = str(row.get("path") or "")
actual = sha_text(read_raw("signals/" + rel))
if actual != row.get("file_sha256"):
raise RuntimeError(json.dumps({
"reason_code": "PART3_SIGNAL_SEAL_FAILED", "detail": "file_sha256",
"path": rel, "expected": row.get("file_sha256"), "actual": actual,
}, ensure_ascii=False))
checked += 1
return {"transaction_id": transaction_id, "files_checked": checked, "root": root}
def verify_registry_index_seal(activation_root, index_raw):
declared = activation_root.get("registry_index_sha256")
actual = sha_text(index_raw)
if declared != actual:
# 전순서 둘째 키가 이 색인의 배열 위치에 걸려 있다. 매니페스트 등재 대상이
# 아니므로 이 봉인만이 색인 교체를 잡는다(L-d2).
raise RuntimeError(json.dumps({
"reason_code": "PART3_REGISTRY_INDEX_SEAL_FAILED",
"declared": declared, "actual": actual}, ensure_ascii=False))
return actual
def main():
gate_manifest = assert_deployment()
staged = materialize_modules()
import structure_index_compiler
# 2) 정책 반입 — 판본 불일치는 PART3_POLICY_INVALID.
policy = json.loads(_verify_asset(POLICY))
if policy.get("schema_version") != POLICY_SCHEMA_VERSION:
raise RuntimeError(json.dumps({
"reason_code": "PART3_POLICY_INVALID",
"expected": POLICY_SCHEMA_VERSION,
"actual": policy.get("schema_version")}, ensure_ascii=False))
# 3) 의미 검사 (입력 쪽)
seed_schema = json.loads(_verify_asset(SEED_SCHEMA))
assert_seed_schema_capability(seed_schema)
# 4) 진입 봉인 둘
seal = verify_signal_seal()
activation_doc = read_json(ACTIVATION_MANIFEST)
activation_root = activation_doc.get("domain_activation_manifest", activation_doc)
index_raw = read_raw(REGISTRY_INDEX)
registry_index_sha = verify_registry_index_seal(activation_root, index_raw)
registry_index = json.loads(index_raw)
index_root = registry_index.get("domain_registry_index", registry_index)
expected_runnable = [d for d in (activation_root.get("expected_runnable_domain_ids") or [])
if isinstance(d, str)]
# 5) 입력 적재
# I-9 domain_config — 무결성은 SG-01 봉인 -> _registry_index -> config_sha256 사슬.
config_sha = {row.get("domain_id"): row.get("config_sha256")
for row in (index_root.get("entries") or []) if isinstance(row, dict)}
domain_configs = {}
domain_sidecars = {}
for domain_id in expected_runnable:
raw = read_raw("Default_Agent/domains/%s/domain_config.json" % domain_id)
if config_sha.get(domain_id) and sha_text(raw) != config_sha[domain_id]:
raise RuntimeError(json.dumps({
"reason_code": "PART3_REGISTRY_INDEX_SEAL_FAILED",
"detail": "config_sha256", "domain_id": domain_id}, ensure_ascii=False))
config = json.loads(raw)
domain_configs[domain_id] = config
config_root = config.get("domain_config", config)
declared = [row for row in (config_root.get("structure_types") or [])
if isinstance(row, dict)]
if declared:
# 사이드카 부재 규칙(§5.5) — 선언 있는 도메인의 부재·미등재는 경성 실패.
domain_sidecars[domain_id] = {
"structure_types": json.loads(_verify_asset(
"Default_Agent/domains/%s/structure_types.json" % domain_id)),
"module_role_projection": json.loads(_verify_asset(
"Default_Agent/domains/%s/module_role_projection.json" % domain_id)),
}
alias_declarations = json.loads(_verify_asset(ALIAS_DECLARATIONS))
def records_of(doc):
if not isinstance(doc, dict):
return []
return [row for row in (doc.get("records") or []) if isinstance(row, dict)]
# 경로 상수를 read_json 에 직접 넘긴다 — 선언표 검사기(U4)가 읽기를 코드에서
# 확인할 수 있는 형태다. 간접 호출로 감싸면 소비 선언이 실측 불가가 된다.
sg13_records = records_of(read_json(SG13_PATH))
sg05_records = records_of(read_json(SG05_PATH))
sg11_records = records_of(read_json(SG11_PATH))
# 읽되 비어 있음을 정상 처리(§3) — 조인 코드를 두지 않고 형식화된 사실로 남긴다.
sg02_rows = records_of(read_json(SG02_PATH))
sg07_rows = records_of(read_json(SG07_PATH))
empty_read_set = [
{"signal_id": "SG-02", "transform": "passthrough", "producer_count": 0,
"record_count": len(sg02_rows),
"reason": "closed schema declares no claim-form vocabulary"},
{"signal_id": "SG-07", "transform": "passthrough", "producer_count": 0,
"record_count": len(sg07_rows),
"reason": "closed schema declares no claim-form vocabulary"},
]
domain_envelopes = {}
for domain_id in expected_runnable:
doc = try_read_json("%s/%s.json" % (DOMAIN_SIGNAL_DIR, domain_id))
if doc is not None:
domain_envelopes[domain_id] = doc
bo_doc = try_read_json(BO_PATH)
bo_records = bo_doc if isinstance(bo_doc, list) else []
bo_raw = read_raw(BO_PATH) if bo_doc is not None else ""
slice_documents = {}
for domain_id in expected_runnable:
doc = try_read_json("%s/%s.json" % (SLICE_DIR, domain_id))
if doc is not None:
slice_documents[domain_id] = doc
# 6) 모듈 호출 — 판정 규칙을 이 파일에 쓰지 않는다.
try:
result = structure_index_compiler.compile_structure_seeds(
activation_doc, registry_index, domain_configs, domain_sidecars,
policy, alias_declarations, sg13_records, sg05_records, sg11_records,
domain_envelopes, bo_records, slice_documents)
except TypeError as exc:
raise RuntimeError(json.dumps({
"reason_code": "PART3_COMPILER_STALE", "detail": "signature_mismatch",
"signature_error": str(exc)[:200],
"remedy": PART3_DEPLOYMENT_REMEDY}, ensure_ascii=False))
assert_compiler_capability(result)
bundle = result["structure_seed_bundle"]
review_items = result["review_items"]
audit = result["audit"]
# 진입 봉인의 거래 식별자를 bundle 에 부착한다 — L2 는 signal_manifest 를
# 다시 읽지 않고(§4.3 입력 계약 밖) 이 값을 승계한다.
bundle["signal_manifest_transaction_id"] = seal["transaction_id"]
# 자기 산출을 닫힌 스키마로 검증한다.
import schema_subset_validator
schema_errors = schema_subset_validator.validate(
{"structure_seed_bundle": bundle}, seed_schema)
if schema_errors:
raise RuntimeError(json.dumps({
"reason_code": "PART3_FINAL_SCHEMA_VALIDATION_FAILED",
"stage": "L0", "errors": schema_errors[:10]}, ensure_ascii=False))
# 7) 기록 — bundle · handoff(OPEN) · receipt(실측값. 상수 PASS 금지).
write_doc(BUNDLE_PATH, canonical({"structure_seed_bundle": bundle}))
write_doc(HANDOFF_PATH, canonical({"stage1_part3_review_handoff": {
"schema_version": "stage1_part3_review_handoff.v1",
"status": "OPEN",
"review_items": review_items,
"counts": {"review_items": len(review_items)},
"created_by": TASK_NAME,
}}))
order_table = ["%s|%s|%s|%s|%s" % (
record.get("priority_rank"), record.get("domain_id"),
record.get("type_id"), record.get("module"),
record.get("admission_policy"))
for record in bundle.get("structure_records") or []]
write_doc(RECEIPT_PATH, canonical({
"schema_version": "stage1_part3_receipt.v1",
"stage": "P3-L0",
"deployment_gate": {
"checked_count": len(set(list(MODULE_MIRRORS.values())
+ list(PART3_REQUIRED_ASSETS))),
"runtime_manifest_sha256": sha_text(read_raw(RUNTIME_MANIFEST)),
"runtime_artifact_count": gate_manifest.get("runtime_artifact_count"),
"schema_capability_checked": ["theta", "structure_records[].admission_policy"],
"compiler_output_checked": ["theta", "structure_records[].order_key"],
"policy_schema_version": policy.get("schema_version"),
"note": "무엇을 봤는지 적는다. 상수 PASS 는 증거가 아니다.",
},
"entry_seals": {
"signal_manifest_transaction_id": seal["transaction_id"],
"signal_files_checked": seal["files_checked"],
"registry_index_sha256": registry_index_sha,
"config_sha256_chain_checked": len(domain_configs),
},
"join_counts": bundle.get("counts"),
"empty_read_set_signals": empty_read_set,
"sidecar_dialects": audit.get("sidecar_dialects"),
"unlinked_sg11": audit.get("unlinked_sg11"),
"dangling_calculation_refs": audit.get("dangling_calculation_refs"),
"input_gaps": audit.get("input_gaps"),
"bo_input": {"sha256": sha_text(bo_raw) if bo_raw else None,
"record_count": len(bo_records),
"note": "BO.json 은 무봉인 입력이다 — signal 5종은 I-1 봉인, "
"registry 는 SG-01 봉인이 지키는 비대칭을 관찰로 남긴다."},
"resolved_order_table": order_table,
"created_by": TASK_NAME,
}))
return {"status": bundle.get("status"),
"modules": staged,
"expected_runnable_domain_ids": expected_runnable,
"theta_count": len(bundle.get("theta") or []),
"structure_record_count": len(bundle.get("structure_records") or []),
"review_item_count": len(review_items),
"counts": bundle.get("counts"),
"errors": ERRORS, "warnings": WARNINGS}
_sink = io.StringIO()
with contextlib.redirect_stdout(_sink):
_init()
RESULT = main()
print(json.dumps(RESULT, ensure_ascii=False))
@@ -0,0 +1,420 @@
# =============================================================================
# P3-L2 / Task_C_LE_L2_final_structure_index_writer — v1 (신설)
#
# 정본 근거 : stage_1_part_3_개정_신전략서.md §4.3 · §2(D-4)
# 계약 : 구조 레코드 = Θ 의 (도메인, type_id) 쌍마다 하나 — 레코드 생성 주체는
# registry 선언이지 SG-13·BO 가 아니다.
# 보존 등식 셋(선언·근거·BO) — 바깥 수는 이 task 가 원본 파일에서 직접 센다
# (bundle 수와 비교하면 항등식으로 퇴행한다).
# 역인덱스 셋 by_domain_id · by_module · by_bo_id. by_claim_form 금지(D-4 라).
# 판정 파일은 없다 — L1 부재. F0 의 조건부 규칙을 흉내 내지 않는다(§4.1).
# =============================================================================
- task_name: Task_C_LE_L2_final_structure_index_writer
mcp: code-executor
tool_name: run_code
parameters:
language: python
requirements: "httpx"
network: "agent-network"
timeout: 240
code: |-
#!/usr/bin/env python3
# Task_C_LE_L2_final_structure_index_writer (v1)
# 1) 게이트·정책·의미 검사 -> 2) bundle·handoff·원본 적재 ->
# 3) 보존 등식 셋(바깥 수 직접 계수) -> 4) 역인덱스 셋 ->
# 5) 닫힌 스키마 검증 -> 6) 기록·재읽기 -> 7) handoff FINALIZED
import contextlib
import hashlib
import io
import itertools
import json
import pathlib
import sys
import httpx
TASK_NAME = "Task_C_LE_L2_final_structure_index_writer"
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):
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()
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())
ASSET_ROOT = "Default_Agent/"
EXECUTION_ROOT = "/tmp/s1"
RT = pathlib.Path(EXECUTION_ROOT) / "_rt"
MODULE_MIRRORS = {
"runtime_common": "Default_Agent/stage1_runtime/runtime_common.txt",
"schema_subset_validator": "Default_Agent/stage1_runtime/schema_subset_validator.txt",
}
MODULES = ["runtime_common", "schema_subset_validator"]
RUNTIME_MANIFEST = "Default_Agent/runtime_manifest.json"
POLICY = "Default_Agent/stage1_runtime/structure_index_policy.v1.json"
POLICY_SCHEMA_VERSION = "stage1_structure_index_policy.v1"
INDEX_SCHEMA = "Default_Agent/platform/schemas/legal_effect_structures.schema.json"
REGISTRY_INDEX = "Default_Agent/domains/_registry_index.json"
BUNDLE_PATH = "stage1_tmp/task_le/structure_seed_bundle.json"
HANDOFF_PATH = "quality_gates/stage1_part3_review_handoff.json"
ACTIVATION_MANIFEST = "routing/domain_activation_manifest.json"
SG13_PATH = "signals/legal_effect_routes.json"
BO_PATH = "BO.json"
TARGET_PATH = "legal_effect_structures.json"
PART3_DEPLOYMENT_REMEDY = (
"배포 원본 extension_research/Default_Agent 트리를 서버 Default_Agent/ 에 통째로 배포하고 "
"runtime_manifest.json 의 등재·sha256 과 대조해 결손 경로를 복원한다. 부분 복사는 허용되지 않는다.")
_MANIFEST_MAP = None
def sha_text(text):
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def canonical(value):
return json.dumps(value, ensure_ascii=False, sort_keys=True,
separators=(",", ":")) + "\n"
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": "PART3_ASSET_DEPLOYMENT_INCOMPLETE", "path": logical,
"detail": detail, "remedy": PART3_DEPLOYMENT_REMEDY}
if extra:
payload.update(extra)
return RuntimeError(json.dumps(payload, ensure_ascii=False))
def _verify_asset(logical):
body = read_raw(logical)
want = _manifest_map().get(logical[len(ASSET_ROOT):])
if want is None:
raise _deployment_error(logical, "unregistered")
if want != sha_text(body):
raise _deployment_error(logical, "hash_mismatch", {"expected": want})
return body
def materialize_modules():
RT.mkdir(parents=True, exist_ok=True)
expected = _manifest_map()
staged = []
for name in MODULES:
logical = MODULE_MIRRORS[name]
raw = read_raw(logical).encode("utf-8")
want = expected.get(logical[len(ASSET_ROOT):])
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 assert_index_schema_capability(index_schema):
# 의미 검사(자기 스키마) — by_module · by_bo_id 를 아는가(PART3_INDEX_SCHEMA_STALE).
index_props = (((index_schema.get("properties") or {})
.get("structure_index") or {}).get("properties") or {})
missing = [key for key in ("by_module", "by_bo_id") if key not in index_props]
if missing:
raise RuntimeError(json.dumps({
"reason_code": "PART3_INDEX_SCHEMA_STALE",
"message": "색인 스키마가 by_module·by_bo_id 를 모른다. "
"배포 원본의 legal_effect_structures.schema.json 이 개정 전 판본이다.",
"missing": missing, "remedy": PART3_DEPLOYMENT_REMEDY,
}, ensure_ascii=False))
def main():
staged = materialize_modules()
import schema_subset_validator
policy = json.loads(_verify_asset(POLICY))
if policy.get("schema_version") != POLICY_SCHEMA_VERSION:
raise RuntimeError(json.dumps({
"reason_code": "PART3_POLICY_INVALID",
"expected": POLICY_SCHEMA_VERSION,
"actual": policy.get("schema_version")}, ensure_ascii=False))
index_schema = json.loads(_verify_asset(INDEX_SCHEMA))
assert_index_schema_capability(index_schema)
# 입력은 전부 파일 계약. 판정 파일은 없다(L1 부재 — §4.1).
bundle_doc = read_json(BUNDLE_PATH)
bundle = bundle_doc.get("structure_seed_bundle", bundle_doc)
records = [row for row in (bundle.get("structure_records") or [])
if isinstance(row, dict)]
handoff_doc = read_json(HANDOFF_PATH)
handoff = handoff_doc.get("stage1_part3_review_handoff", handoff_doc)
activation_doc = read_json(ACTIVATION_MANIFEST)
activation_root = activation_doc.get("domain_activation_manifest", activation_doc)
expected_runnable = [d for d in (activation_root.get("expected_runnable_domain_ids") or [])
if isinstance(d, str)]
# ------------------------------------------------------------------
# 보존 등식 셋 — 바깥 수를 원본에서 직접 센다.
# ------------------------------------------------------------------
# (1) 선언 보존 — Σ(expected_runnable 도메인의 structure_types 개수).
# domain_config 무결성은 SG-01 봉인 -> _registry_index -> config_sha256 사슬 재사용.
index_raw = read_raw(REGISTRY_INDEX)
if activation_root.get("registry_index_sha256") != sha_text(index_raw):
raise RuntimeError(json.dumps({
"reason_code": "PART3_REGISTRY_INDEX_SEAL_FAILED",
"stage": "L2"}, ensure_ascii=False))
registry_index = json.loads(index_raw)
index_root = registry_index.get("domain_registry_index", registry_index)
config_sha = {row.get("domain_id"): row.get("config_sha256")
for row in (index_root.get("entries") or []) if isinstance(row, dict)}
declared_total = 0
for domain_id in expected_runnable:
raw = read_raw("Default_Agent/domains/%s/domain_config.json" % domain_id)
if config_sha.get(domain_id) and sha_text(raw) != config_sha[domain_id]:
raise RuntimeError(json.dumps({
"reason_code": "PART3_REGISTRY_INDEX_SEAL_FAILED",
"detail": "config_sha256", "domain_id": domain_id}, ensure_ascii=False))
config = json.loads(raw)
config_root = config.get("domain_config", config)
declared_total += len([row for row in (config_root.get("structure_types") or [])
if isinstance(row, dict)])
declaration_pass = (len(records) == declared_total)
# (2) 근거 보존 — SG-13 원본에서 legal_effect_route 총수를 직접 센다.
# 붙은 route 는 레코드에서 재계수한다(bundle counts 를 믿지 않는다).
sg13_doc = read_json(SG13_PATH)
sg13_total = len([row for row in (sg13_doc.get("records") or [])
if isinstance(row, dict)
and row.get("record_type") == "legal_effect_route"])
attached_signal_ids = set()
for record in records:
for ref in record.get("routes") or []:
if isinstance(ref, dict) and isinstance(ref.get("signal_id"), str):
attached_signal_ids.add(ref["signal_id"])
routes_unattached = len(bundle.get("unattached_routes") or [])
route_pass = (len(attached_signal_ids) + routes_unattached == sg13_total)
# (3) BO 보존 — BO.json 원본에서 총수를 직접 센다.
bo_doc = read_json(BO_PATH)
bo_total = len(bo_doc) if isinstance(bo_doc, list) else 0
attached_bo = set()
for record in records:
for bo_id in record.get("source_bo_ids") or []:
if isinstance(bo_id, str):
attached_bo.add(bo_id)
bo_unattached = len(bundle.get("unattached_bo") or [])
bo_pass = (len(attached_bo) + bo_unattached == bo_total)
failures = []
if not declaration_pass:
failures.append({"reason_code": "PART3_STRUCTURE_COUNT_BROKEN",
"records": len(records), "declared_total": declared_total})
if not route_pass:
failures.append({"reason_code": "PART3_ROUTE_CONSERVATION_BROKEN",
"attached": len(attached_signal_ids),
"unattached": routes_unattached, "sg13_total": sg13_total})
if not bo_pass:
failures.append({"reason_code": "PART3_BO_CONSERVATION_BROKEN",
"attached": len(attached_bo),
"unattached": bo_unattached, "bo_total": bo_total})
if failures:
raise RuntimeError(json.dumps({"conservation_failures": failures},
ensure_ascii=False))
# ------------------------------------------------------------------
# 역인덱스 셋 — ID 목록만 담는다. 본문을 복제하지 않는다.
# ------------------------------------------------------------------
by_domain_id = {}
by_module = {}
by_bo_id = {}
for record in records:
structure_id = record.get("structure_id")
by_domain_id.setdefault(record.get("domain_id"), []).append(structure_id)
if record.get("module"):
by_module.setdefault(record["module"], []).append(structure_id)
for bo_id in record.get("source_bo_ids") or []:
by_bo_id.setdefault(bo_id, []).append(structure_id)
for index in (by_domain_id, by_module, by_bo_id):
for key in index:
index[key] = sorted(set(index[key]))
# 검토 재산정 — handoff 항목을 코드·심각도로 묶는다. review 는 근거 없이 줄지 않는다.
review_queue = []
queue_keys = {}
for item in handoff.get("review_items") or []:
if not isinstance(item, dict):
continue
key = (item.get("review_code"), item.get("severity"))
if key in queue_keys:
queue_keys[key]["count"] += 1
else:
row = {"review_code": key[0], "severity": key[1] or "SOFT_WARNING",
"count": 1}
queue_keys[key] = row
review_queue.append(row)
review_queue.sort(key=lambda row: (row["review_code"], row["severity"]))
status = "READY_WITH_REVIEW" if (review_queue or any(
record.get("review_required") for record in records)) else "READY"
final_doc = {
"schema_version": "stage1_legal_effect_structures.v1",
"status": status,
"registry_version": bundle.get("registry_version"),
"registry_index_sha256": sha_text(index_raw),
"activation_manifest_sha256": sha_text(read_raw(ACTIVATION_MANIFEST)),
"signal_manifest_transaction_id": bundle.get("signal_manifest_transaction_id"),
"expected_runnable_domain_ids": expected_runnable,
"theta": bundle.get("theta") or [],
"structure_records": records,
"structure_index": {"by_domain_id": by_domain_id,
"by_module": by_module,
"by_bo_id": by_bo_id},
"quality_gate": {
"declaration_conservation": {
"structure_records": len(records),
"declared_total_from_config": declared_total,
"conservation_pass": declaration_pass,
},
"route_conservation": {
"routes_attached": len(attached_signal_ids),
"routes_unattached": routes_unattached,
"sg13_route_total_from_source": sg13_total,
"conservation_pass": route_pass,
},
"bo_conservation": {
"bo_attached": len(attached_bo),
"bo_unattached": bo_unattached,
"bo_total_from_source": bo_total,
"conservation_pass": bo_pass,
},
"review_queue": review_queue,
},
"created_by": TASK_NAME,
}
# 닫힌 스키마 검증 -> 기록 -> 재읽기.
schema_errors = schema_subset_validator.validate(final_doc, index_schema)
if schema_errors:
raise RuntimeError(json.dumps({
"reason_code": "PART3_FINAL_SCHEMA_VALIDATION_FAILED",
"stage": "L2", "errors": schema_errors[:10]}, ensure_ascii=False))
text = canonical(final_doc)
write_doc(TARGET_PATH, text)
reread = read_raw(TARGET_PATH)
if sha_text(reread) != sha_text(text):
raise RuntimeError("PART3_WRITE_REREAD_MISMATCH")
# handoff FINALIZED 갱신(공동 기록자). 항목을 줄이지 않는다.
handoff["status"] = "FINALIZED"
handoff["finalized_by"] = TASK_NAME
write_doc(HANDOFF_PATH, canonical({"stage1_part3_review_handoff": handoff}))
return {"status": status, "modules": staged,
"structure_record_count": len(records),
"theta_count": len(bundle.get("theta") or []),
"index_bucket_counts": {"by_domain_id": len(by_domain_id),
"by_module": len(by_module),
"by_bo_id": len(by_bo_id)},
"quality_gate_pass": {"declaration": declaration_pass,
"route": route_pass, "bo": bo_pass},
"review_queue_count": len(review_queue)}
_sink = io.StringIO()
with contextlib.redirect_stdout(_sink):
_init()
RESULT = main()
print(json.dumps(RESULT, ensure_ascii=False))
@@ -1 +1 @@
{"enforcement":{"declared":true,"note":"강제와 선언은 다르다. 이 파일이 선언을 맡는다.","runtime_guard":"Task_C_BO_A0 가 봉인 세 해시를 대조하므로 Part 1 미수행 시 SEAL_FAILED 로 멈춘다."},"interface_table":"handoffs/stage1_part_interface.v1.json","purpose":"Part 1~4 의 실행 순서와 인계 파일을 선언한다. 지금은 각 통합본의 prevs/nexts 가 비어 있어 순서가 A0 의 파일 부재 실패로 사실상 강제될 뿐 선언되어 있지 않다.","schema_version":"stage1_stage_chain.v1","stages":[{"order":1,"part":"Stage_1.Part_1","produces":["routing/domain_activation_manifest.json","quality_gates/stage1_part1_soft_gate_handoff.json","routing/domain_screening.json","evidence_indexed.json","evidence_event_candidates.json"],"requires":["client_meeting.md","evidence_all.json"],"yaml":"stage_1_part_1_v.8.yml"},{"order":2,"part":"Stage_1.Part_2","produces":["BO.json","signals/signal_manifest.json","actio_case_signals.json","case_liability_signals.json","legal_effect_signals.json","stage1_tmp/task_c_bo/postb_compiled_bundle_compact.json"],"requires":["routing/domain_activation_manifest.json","quality_gates/stage1_part1_soft_gate_handoff.json","routing/domain_screening.json","evidence_indexed.json","evidence_event_candidates.json"],"yaml":"stage_1_part_2_v.8.yml"},{"note":"정본 signal 집합으로의 이관은 Part 3 개정의 몫이다.","order":3,"part":"Stage_1.Part_3","produces":[],"requires":["BO.json","signals/signal_manifest.json"],"yaml":"Stage_1_Part_3_Claude_v2.yml"},{"note":"같음.","order":4,"part":"Stage_1.Part_4","produces":[],"requires":["BO.json","signals/signal_manifest.json"],"yaml":"Stage_1_Part_4_Claude_v3.yml"}],"status":"ACTIVE"}
{"enforcement":{"declared":true,"note":"강제와 선언은 다르다. 이 파일이 선언을 맡는다.","runtime_guard":"Task_C_BO_A0 가 봉인 세 해시를 대조하므로 Part 1 미수행 시 SEAL_FAILED 로 멈춘다."},"interface_table":"handoffs/stage1_part_interface.v1.json","purpose":"Part 1~4 의 실행 순서와 인계 파일을 선언한다. 지금은 각 통합본의 prevs/nexts 가 비어 있어 순서가 A0 의 파일 부재 실패로 사실상 강제될 뿐 선언되어 있지 않다.","schema_version":"stage1_stage_chain.v1","stages":[{"order":1,"part":"Stage_1.Part_1","produces":["routing/domain_activation_manifest.json","quality_gates/stage1_part1_soft_gate_handoff.json","routing/domain_screening.json","evidence_indexed.json","evidence_event_candidates.json"],"requires":["client_meeting.md","evidence_all.json"],"yaml":"stage_1_part_1_v.8.yml"},{"order":2,"part":"Stage_1.Part_2","produces":["BO.json","signals/signal_manifest.json","actio_case_signals.json","case_liability_signals.json","legal_effect_signals.json","stage1_tmp/task_c_bo/postb_compiled_bundle_compact.json"],"requires":["routing/domain_activation_manifest.json","quality_gates/stage1_part1_soft_gate_handoff.json","routing/domain_screening.json","evidence_indexed.json","evidence_event_candidates.json"],"yaml":"stage_1_part_2_v.8.yml"},{"note":"정본 signal 집합(downstream_read_sets.part3_L0) 이관 완료. L1 없음 — 2 task 직렬.","order":3,"part":"Stage_1.Part_3","produces":["legal_effect_structures.json","stage1_tmp/task_le/structure_seed_bundle.json","quality_gates/stage1_part3_review_handoff.json","validation_assets/routing/part3_receipt.json"],"requires":["BO.json","signals/signal_manifest.json","routing/domain_activation_manifest.json"],"yaml":"stage_1_part_3_v.8.yml"},{"note":"같음.","order":4,"part":"Stage_1.Part_4","produces":[],"requires":["BO.json","signals/signal_manifest.json"],"yaml":"Stage_1_Part_4_Claude_v3.yml"}],"status":"ACTIVE"}
@@ -2110,10 +2110,6 @@ MEMORY.md 파일에는 지금까지의 작업 내역들이 압축적으로 요
--> 이 기준을 prompt에 명시해야 한다.
┌────────────────────────────────────────────┐
│ │
│ 1st Execution of Update Strategy of Part 3 │
@@ -2125,21 +2121,21 @@ MEMORY.md 파일에는 지금까지의 작업 내역들이 압축적으로 요
</goal>
<context>
지금까지 stage 1 -- part 1|2 개정작업을 진행했다.
`stage_1_part_1_v.8.yml`, `stage_1_part_2_v.8.yml`를 검증하여 두 작업명세서가 모두 의도한 작업 과정을 실행하는 것을 확인했다.
지금까지 stage 1 -- part 1|2 개정작업을 진행하여 yaml 작업명세서 `stage_1_part_1_v.8.yml`와 `stage_1_part_2_v.8.yml`를 생성했다.
두 yaml 작업명세서는 검증을 통해 모두 의도한 작업 과정을 실행함을 확인했다.
이제 `stage_1_part_3_개정_신전략서.md`가 제시하는 방식을 실행하여 part 3 작업의 궁극적 목적(목표/목적물)을 '최소 작업으로 최고 품질로 달성'하는 원칙을 기반으로 두고 최적 워크플로우를 구성하여 작업명세서(yaml & related assets)를 개정 생성하고자 한다.
이제 `stage_1_part_3_개정_신전략서.md`가 제시하는 방식을 실행하여 part 3 작업의 궁극적 목적(목표/목적물)을 '최소 작업으로 최고 품질로 달성'하는 원칙을 달성하는 part 3 작업명세서(yaml & related assets)를 생성하고자 한다.
</context>
<method>
1. `stage_1_part_3_개정_신전략서.md`의 작업 내용들을 독립적으로 동시 실행가능한 task들과, 직렬로 sequential 방식으로 실행해야 할 task들로 분류한다.
2. 1의 결과를 바탕으로 독립 동시 실행 가능한 task들은 병렬로 동시 실행하고, sequential 방식으로 실행해야 하는 task들은 upstream-downstream 작업 consistency를 유지하면서 실행한다.
3. 2번 작업을 수행한 후 sub-agent를 띄워서 2번 작업이 `stage_1_part_3_개정_신전략서.md`가 지시하는 사항들을 100% 정확하게 준수하여 작업을 `완수`했는지 검증한다. Sub-agent가 100% 검증 통과라고 판정할 때까지, 2번 작업을 반복한다.
2. 1의 결과를 바탕으로 독립 동시 실행 가능한 task들은 병렬로 동시 실행하고, sequential 방식으로 실행해야 하는 task들은 upstream-downstream 작업 consistency를 유지하면서 실행하여 part 3의 yaml 작업명세서를 작성한다.
3. 2번 작업을 수행한 후 sub-agent를 띄워서 2번 작업이 `stage_1_part_3_개정_신전략서.md`가 지시하는 사항들을 100% 정확하게 준수하여 작업을 수행했는지 검증한다. Sub-agent가 100% 검증 통과라고 판정할 때까지, 2번 작업(yaml 작성 & 검증)을 반복한다.
4. 3번 작업이 완료되면, 작업 결과물 중 저장해야 할 자산들(assets)을 `extension_research/Default_Agent` 폴더에 배포 위치를 정확히 지켜서 저장하고, part 3 작업명세서를 `extension_research/ver_8_yaml_candidates/stage_1_part_3_v.8.yml`로 생성한다.
<constraints>
1. 작업 시, 기존 yaml 작업명세서('v.7/Claude_YAML'의 yaml 파일들)와 관련한 dependency는 완전히 제거한다. 즉, 기존 yaml 작업 명세서가 지시하는 내용에 전혀 의존하지 않도록 한다.
2. Stage 1 -- part 1|2 작업이 사용하는 자산들의 명칭과 배포 위치 그리고 포맷은 `extension_research/Default_Agent` 폴더에 저장되어 있으며, 앞으로 Part 3 개정 작업을 통해 생성될 Stage 1 (part 상관없이) 실행 시 사용될 모든 자산(md, .py, .txt, .json)들은 이 폴더에 정확한 배포 위치 서브 폴더에 저장한다.
2. Stage 1 -- part 1|2 작업이 사용하는 자산들의 명칭과 배포 위치 그리고 포맷은 `extension_research/Default_Agent` 폴더에 저장되어 있으며, 앞으로 개정 생성될 Part 3 yaml 작업명세서가 실행되어 생성될 모든 자산(md, .py, .txt, .json)들은 이 폴더의 정확한 배포 위치(서브 폴더)에 저장한다.
3. Liti-agent가 stage # yaml 작업명세서를 실행하여 작업을 `완수`했다는 표현을 사용할 때, `완수`의 기준은 단순히 yaml 작업명세서를 끝까지 실행 완주할 수 있다는 것이 아니다. Yaml 작업명세서를 실행했을 때, 원래 의도한 결과물들이 올바른 방식으로 생성되어야 하고, 소작업(sub-task)들의 upstream-downstream 간에 consistency(넘겨받은 파일을 올바로 읽고, handoff로 넘겨주는 파일도 정해진 포맷에 맞게 넘겨줌)를 유지하여 작업이 끝까지 이어진 경우를 작업이 `완수`되었다고 평가한다.
4. Stage 1 전체 작업에 대한 아래 내용을 절대 규칙으로 삼는다.
<absolute_principle_for_whole_stage_1>
@@ -2156,6 +2152,15 @@ MEMORY.md 파일에는 지금까지의 작업 내역들이 압축적으로 요
- yaml 작성을 할 때는 <v.7/SKILL.md>의 규칙을 엄격하게 준수하며, python code를 작성할 때는 <v.7/extension_research/test_code_executor.ipynb>가 제시하는 Python code 작성 규칙을 엄수한다.
</global_constraints>
==================================
--> stage 1 -- part 3 개정 작업 최적 워크플로우 탐구 --> 작업 명세서 생성
--> stage 1 -- part 3 개정 --> 남은 작업 check --> 추가작업 명세서 생성 및 작업 수행 --> part 3 개정 마무리
@@ -2188,102 +2193,6 @@ MEMORY.md 파일에는 지금까지의 작업 내역들이 압축적으로 요
┌────────────────────────────────────────────┐
│ │
│ 1st Execution of Update Strategy of Part 3 │
│ │
└────────────────────────────────────────────┘
<goal>
`stage_1_part_3_개정방안전략서.md`를 실행하여 stage 1 -- part 3 작업명세서를 개정한다.
</goal>
<context>
지금까지 stage 1 -- part 1|2 개정작업을 진행했다.
`stage_1_part_1_v.8.yml`, `stage_1_part_2_v.8.yml`를 검증하여 두 작업명세서가 모두 의도한 작업 과정을 실행하는 것을 확인했다.
이제 `stage_1_part_3_개정방안전략서.md`가 제시하는 방식을 실행하여 part 3 작업명세서(yaml & related assets)를 개정하고자 한다.
</context>
<method>
1. 구 part 3 yaml('v.7/Claude_YAML/Stage_1_Part_3_Claude_v2.yml')를 확인한다.
2. `stage_1_part_3_개정방안전략서.md`가 제시하는 개정 방법을 읽고, 독립 실행 가능한 task들과 sequential 작업이 필요한 task들로 분류한다.
3. 2번 과정에서 분류한 task들에 대해서 독립 실행 가능한 task들은 병렬로 작업을 수행하고, sequential 작업이 필요한 task들은 upstream-downstream consistency를 유지하면서 작업을 수행한다. 작업 수행 시 아래 제시한 <constraints>를 준수한다.
4. 3번 작업 과정에서 얻은 결과물들은 기본적으로 `v.7/extension_research/ver_8_yaml_candidates` 폴더에 '정확한 배포 위치'로 저장하는 것을 원칙으로 한다. 작업 수행 과정에서 기존 stage 1 -- part 3의 개별 작업용 yaml 파일들, md 파일들, python code (.py|.txt)들을 수정하거나 재작성하면 기존 파일들은 outdated 폴더로 옮기고, 수정 및 재작성한 파일은 기존 파일들을 대체하는 방식으로 정확한 배포 위치에 생성한다. 신규 작성한 자산이 있다면 그 역시 정확한 배포 위치에 저장한다.
5. 작업을 완료하면 독립적으로 sub-agent를 띄워서 `stage_1_part_3_개정방안전략서.md`가 제시하는 개정 방법에 따라 작업이 100% 수행되었는지 검증한다. 만일 검증을 통과하지 못한 작업 내용이 존재하면 그 작업 내역만을 incremental 수정한다. 이 과정은 sub-agent의 검증을 통과할 때까지 반복한다.
6. 5번 검증까지 통과하여 작업을 완료하면 작업 내역서를 문서로 작성하여 'v.7/extension_research/stage_1_part_3_1차개정작업.md'로 생성한다.
<constraints>
1. 작업 시, 기존 yaml 작업명세서('v.7/Claude_YAML'의 yaml 파일들)와 관련한 dependency가 없도록 한다. 즉, 기존 yaml 작업 명세서가 지시하는 내용에 절대 의존하면 안된다. 새로 작성하는 yaml이나 자산들은 기존 작업명세서에서 지시하는 내용과는 별개로 독립적인 결과물이어야 한다.
2. Stage 1 -- part 1|2 작업이 사용하는 자산들의 명칭과 배포 위치 그리고 포맷들을 정확히 활용한다.
3. Stage 1 전체 작업에 대한 아래의 절대 규칙을 'conerstone'으로 삼는다.
<absolute_principle_for_whole_stage_1>
- Liti-agent는 대한민국 민사소송 사건종류 137종을 모두 다룰 수 있도록 개발되어야 한다.
- Stage 1 에서는 Liti-agent가 137종 중 그 어떤 사건을 다루더라도 `레지스트리 기반` 방식으로 사건 종류의 이름이 아니라 증거 구성요소·요건 슬롯·단서로 구조화된 정보를 확보하는 것을 목표로 한다. 이를 위해 사용할 자산들이 '법리 도메인, 계산 도메인, signal'이며 이에 대한 정의는 `v.7/extension_research/ensemble_v2.md`에 제시되어 있다.
</absolute_principle_for_whole_stage_1>
</constraints>
</method>
<global_constraints>
- `v.7/CLAUDE.md`가 제시하는 Opus 5 행동 규칙을 준수한다.
- 작업 완료 시 작업 내역을 압축 요약하여 `v.7/MEMORY.md`에 추가 기입한다.
- yaml 작성을 할 때는 <v.7/SKILL.md>의 규칙을 엄격하게 준수하며, python code를 작성할 때는 <v.7/extension_research/test_code_executor.ipynb>가 제시하는 Python code 작성 규칙을 엄수한다.
</global_constraints>
--> stage 1 -- part 3 개정 --> 남은 작업 check --> 추가작업 명세서 생성 및 작업 수행 --> part 3 개정 마무리
[다음]
- stage 1 -- part 2에서 더 개정할 작업이 남아있는가?
- if no -> 오늘 stage 1 -- part 2 개정 작업 결과물을 읽어들여서 `stage_1_part_1_and_2_updated_yaml_analysis.md`를 incremental update하여 `stage_1_part_1_and_2_updated_yaml_analysis.md`로 생성 (기존 파일은 `stage_1_part_1_and_2_updated_yaml_analysis_old.md`로 파일명을 바꿔서 outdated 폴더로 이송)
- `stage_1_part_1_and_2_updated_yaml_analysis.md` 내용을 기반으로 part 3 최적 개정 방식 탐구
- 기존 stage_1_update_strategy.md가 제시한 part 3 개정 작업 내용 및 DAG 형식 참조
Part 3 — 법률효과 구조
P3-L0 -> P3-L1 -> P3-L2
§4. Part 3 세부 워크플로우
- §4.1 P3-L0 구조 seed 축약 [개정]
- §4.2 P3-L1 예외 판정
- §4.3 P3-L2 최종 색인 기록 [개정]
최적 개정 작업 전략서 작성
┌─────────────────────────────┐
│ Must Task │
└─────────────────────────────┘
MEMORY.md에 기록된 {{실측이 리포트와 갈린 곳 · 문서가 처음 기록한 것}}에 제시된 내용을 참조한 리포트는 파일명 마지막에 '_outdated'를 붙여서 남기고, {{실측이 리포트와 갈린 곳 · 문서가 처음 기록한 것}} 내용을 반영하여 기존 리포트에 반영하여 새 리포트를 동일한 파일명으로 생성하라.
=> 리포트에 반영하여, 리포트 개정 요망
==================================
## 개정 작업에서 지켜야 할 절대적인 기준(cornerstone):
@@ -2314,13 +2223,6 @@ Opus 5에 명시적으로 검증 작업을 시킬 때, 검증 100% 통과할 때
[2] [1]의 md 문서를 기준으로
--> stage 1 -- part 3 개정 작업 최적 워크플로우 탐구 --> 작업 명세서 생성
--> stage 1 -- part 3 개정 --> 남은 작업 check --> 추가작업 명세서 생성 및 작업 수행 --> part 3 개정 마무리
[3]
Stage 1 -- part 1, 2, 3 개정 작업 요약
기존 stage 1 -- part 1, 2 개정 작업 요약본에 stage 1 -- part 3 개정 작업본을 추가 입력하여 업데이트
@@ -2342,42 +2244,6 @@ Stage 1 -- part 1, 2, 3 개정 작업 요약
Part 2 작업 검증
Part 3 개정 작업 리서치
Part 3 개정 yaml 작성
@@ -2387,6 +2253,13 @@ Part 4 개정 yaml 작성
--> 안종선이 step by step으로 실행하고, 결과물을 내가 체크하여 수정, 추후 다시 실행
--> stage 1 4개 yaml 파일들이 문제없이 실행되도록 완료
┌────────────────────────────────────────────┐
│ Sketch of How to Update Stage 2 │
└────────────────────────────────────────────┘
[Stage 1 작업 내역들 + DAG + stage 2 작업 목표 + 기존 stage 2 yaml] 종합하여 stage 2 최적 워크플로우 탐구
-->
큰 덩어리:
@@ -2412,10 +2285,14 @@ Part 4 개정 yaml 작성
`stage_1_part_2_optimal_update_strategy.md`를 수행한다.
==========================================================================
┌────────────────────────────────────────────┐
│ Structure of Prompt │
└────────────────────────────────────────────┘
<goal>
</goal>
@@ -2463,98 +2340,6 @@ Part 4 개정 yaml 작성
[질문]
1. `registry_component_ids`를 `evidence_indexed` 내부의 항목으로 표현되도록 `evidence_indexed` json schema에 포함시키면 되나?
만일 답이 '예'라면, 'part_2_update_addition.md'의 "§1. `registry_component_ids`는 파일이 아니다"에 제시된 내용을 반영하여 작업을 실행하면 `registry_component_ids`를 `evidence_indexed` 내부의 항목으로 표현되게 하는가?
2. `evidence_indexed`를 검증하는 json schema가 반드시 필요한가? 예, 아니오로 답하고 이유를 한 문단 이내로 설명하라.
조건
Stage 1 part 1 단일 yaml 파일 생성
DAG 표현 정확히 기입
Python code 표현 정확히
케이무크 작업 실행 계획 세우기
4 + 2 + 3 + 0.5 = 9.5
1 + 1 + 0.5 + 0.5 = 3
12.5 1억 2천 5백
총 2억 모음 --> tips 진행