docs: analyze Fact Ledger output schema drift
This commit is contained in:
+468
@@ -0,0 +1,468 @@
|
||||
# Fact_Ledger_base.json 출력 형식 불일치 원인 분석
|
||||
|
||||
## 1. 분석 대상과 결론
|
||||
|
||||
### 1.1 분석 대상
|
||||
|
||||
이 보고서는 다음 파일을 상호 대조하여 작성한 것이다.
|
||||
|
||||
- 실행 작업명세서: `Stage_1_Part_1_Codex_v3.yml`부터 `Stage_1_Part_4_Codex_v3.yml`까지
|
||||
- 실제 실행 결과: `Results_00_7_21_Codex/Fact_Ledger_base.json`
|
||||
- 중간 산출물:
|
||||
- `Results_00_7_21_Codex/stage1_tmp/fact_ledger/Fact_Ledger_base_candidate.json`
|
||||
- `Results_00_7_21_Codex/stage1_tmp/fact_ledger/fact_exception_manifest.json`
|
||||
- `Results_00_7_21_Codex/stage1_tmp/fact_ledger/fact_ledger_writer_report.json`
|
||||
- 비교 기준 문서: `Analysis_Stage_1_Codex_v1.md`의 4.5.4 `Fact_Ledger_base.json`
|
||||
|
||||
### 1.2 최종 결론
|
||||
|
||||
실제 `Fact_Ledger_base.json`이 분석 문서 4.5.4의 예시와 다른 주된 이유는 LLM의 자유 추론이나 실행 중 우발적 직렬화 오류가 아니다. **Part 4 YAML의 FL1 Python 코드가 처음부터 분석 문서와 다른 자료형으로 candidate row를 만들고, FL3 Python writer가 필드명 집합만 검사한 뒤 그 값을 그대로 최종 파일에 기록하도록 구현되어 있기 때문**이다.
|
||||
|
||||
정확한 판정은 다음과 같다.
|
||||
|
||||
1. **최상위 형식과 필드명은 일치한다.** 실제 파일은 wrapper 없는 JSON 배열이고, 48개 row 모두 정확히 27개 필드를 가진다. 누락 필드와 추가 필드는 0개이다.
|
||||
2. **필드 자료형과 ID 표기 규칙은 다르다.** 분석 문서가 배열ㆍ객체ㆍ불리언으로 표시한 여러 필드가 실제 결과에서는 객체ㆍ문자열ㆍ불리언 등 다른 자료형을 사용한다.
|
||||
3. **실제 최종 파일은 FL1 candidate와 완전히 동일하다.** 이번 실행의 exception 수는 0개이므로 FL2 LLM adjudicator가 값을 바꾸지 않았다.
|
||||
4. **FL3의 `candidate row schema is closed` 검사는 JSON Schema 검사가 아니다.** `set(row) == FINAL_FIELD_SET`만 확인하므로 키 이름이 맞으면 잘못된 자료형도 통과한다.
|
||||
5. **`amount`와 `object_spec`에는 이중 인코딩 결함이 존재한다.** upstream 객체ㆍ배열이 JSON 객체ㆍ배열로 유지되지 않고 JSON 문법을 담은 문자열로 변환되었다.
|
||||
6. 따라서 이번 차이는 두 층으로 나누어 보아야 한다.
|
||||
- 객체ㆍ배열ㆍ불리언의 설계 차이는 분석 문서와 실행 코드 사이의 계약 drift이다.
|
||||
- `amount`와 `object_spec`의 JSON 문자열화는 별도의 실질적 구현 결함이다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 실제 출력 형식 조사
|
||||
|
||||
### 2.1 최상위 구조와 보존 상태
|
||||
|
||||
실제 파일을 기계적으로 검사한 결과는 다음과 같다.
|
||||
|
||||
| 검사 항목 | 실제 결과 | 판정 |
|
||||
|---|---:|---|
|
||||
| 최상위 JSON type | Array | 4.5.4와 일치 |
|
||||
| 최종 Fact row 수 | 48 | BO 48건과 일치 |
|
||||
| row별 필드 수 | 모든 row가 27개 | 4.5.4와 일치 |
|
||||
| 27개 필드 중 누락 | 0개 | 일치 |
|
||||
| 지정 외 추가 필드 | 0개 | 일치 |
|
||||
| BO 누락ㆍ중복 | 0개 | conservation gate 통과 |
|
||||
| unknown BOㆍ증거ㆍ구조 참조 | 0개 | reference gate 통과 |
|
||||
| FL2 exception | 0개 | LLM 판정 미실행 |
|
||||
| writer status | `READY_WITH_REVIEW` | 형식 차이를 오류로 인식하지 않음 |
|
||||
|
||||
따라서 문제를 “JSON 배열 형식이 완전히 틀렸다”거나 “27개 필드가 지켜지지 않았다”고 평가해서는 안 된다. **구조적 외형은 일치하지만 field-level type contract가 불일치하는 문제**이다.
|
||||
|
||||
### 2.2 실제 row의 자료형
|
||||
|
||||
48개 row 전체에서 관찰된 자료형은 다음과 같다.
|
||||
|
||||
| 필드 | 실제 자료형 | 실제 값의 성격 |
|
||||
|---|---|---|
|
||||
| `fact_id` | String | `F-001` 형식 |
|
||||
| `source_bo_id` | String | `bh1`, `bh10` 등 upstream BO ID |
|
||||
| `type` | String | `event` 등 |
|
||||
| `date` | null | 48건 모두 null |
|
||||
| `parties` | Array | 당사자 role 객체 배열 |
|
||||
| `object_spec` | String | `"[]"`처럼 JSON 배열 문법을 담은 문자열 |
|
||||
| `amount` | String | JSON 객체 문법을 담은 문자열 |
|
||||
| `action` | String | 사실ㆍ행위 설명 |
|
||||
| `evidence_refs` | Array | `E-###` 목록 |
|
||||
| `credibility` | String | `high`, `medium`, `low` |
|
||||
| `legal_centrality` | String | `critical`, `medium` 등 |
|
||||
| `proof_strength` | String | `high`, `medium`, `low` |
|
||||
| `legal_effect_roles` | Array | role 이름 목록 |
|
||||
| `claim_chain_ref` | Object | claimㆍliability group 및 signal count |
|
||||
| `linked_structures` | Object | structureㆍissueㆍassetㆍevidence index 묶음 |
|
||||
| `must_not_drop_in_claim_types` | Array | downstream module 목록 |
|
||||
| `must_consider` | Boolean | true 또는 false |
|
||||
| `state_context` | Object | BO type 및 issueㆍasset cluster |
|
||||
| `money_claim_effect` | Object | enabled와 source BO ID |
|
||||
| `commercial_successor_effect` | Object | enabled와 source BO ID |
|
||||
| `actio_roles` | Array | actio signal projection |
|
||||
| `linked_actio_structures` | Object | structure ID와 signal count |
|
||||
| `downstream_module_candidates` | Array | Stage 2 module 후보 |
|
||||
| `skeleton_validation_codes` | Array | 검증 code 목록 |
|
||||
| `derived_fact` | String | source-backed 사실 문장 |
|
||||
| `derivation_basis` | String | 세미콜론 연결 provenance 문장 |
|
||||
| `legal_calculation_object` | Object | 계산 필요 여부와 amountㆍobject basis |
|
||||
|
||||
### 2.3 분석 문서 4.5.4와의 차이
|
||||
|
||||
분석 문서 4.5.4는 최종 row 예시를 다음과 같은 자료형으로 표현한다.
|
||||
|
||||
- `object_spec`: Object
|
||||
- `credibility`: Object
|
||||
- `claim_chain_ref`: Array
|
||||
- `linked_structures`: Array
|
||||
- `must_consider`: Array
|
||||
- `linked_actio_structures`: Array
|
||||
- `derived_fact`: Boolean
|
||||
- `derivation_basis`: Array
|
||||
- `amount`: null 예시
|
||||
- `legal_calculation_object`: null 예시
|
||||
|
||||
실제 결과와 직접 비교하면 다음과 같다.
|
||||
|
||||
| 필드 | 4.5.4 예시 | 실제 결과 | 불일치 성격 |
|
||||
|---|---|---|---|
|
||||
| `fact_id` | `F-0001` | `F-001` | ID 자릿수 규칙 차이 |
|
||||
| `source_bo_id` | `BO-0001` | `bh1` | upstream ID 보존 규칙 차이 |
|
||||
| `object_spec` | Object | String | 명백한 type 불일치 및 JSON 이중 인코딩 |
|
||||
| `amount` | null | String | null 예시와 다르며 upstream Object가 JSON 문자열로 변환됨 |
|
||||
| `credibility` | Object | String | 명백한 type 불일치 |
|
||||
| `claim_chain_ref` | Array | Object | 명백한 type 불일치 |
|
||||
| `linked_structures` | Array | Object | 명백한 type 불일치 |
|
||||
| `must_consider` | Array | Boolean | 명백한 type 불일치 |
|
||||
| `linked_actio_structures` | Array | Object | 명백한 type 불일치 |
|
||||
| `derived_fact` | Boolean | String | 명백한 typeㆍ의미 불일치 |
|
||||
| `derivation_basis` | Array | String | 명백한 type 불일치 |
|
||||
| `legal_calculation_object` | null | Object | 기본값 예시와 실제 projection 차이 |
|
||||
|
||||
`amount`와 `legal_calculation_object`의 null은 예시 기본값일 수 있으므로 null 자체만으로 고정 type을 단정하기는 어렵다. 그러나 `amount`가 upstream Object에서 JSON 문자열로 바뀐 것은 명백한 직렬화 문제이다.
|
||||
|
||||
---
|
||||
|
||||
## 3. Part 4 실행 경로별 원인 추적
|
||||
|
||||
### 3.1 FL0: upstream 값은 아직 구조화되어 있음
|
||||
|
||||
`Task_FL0_fact_source_pack_compiler`는 `BO.json`, `legal_effect_structures.json`, 세 signal 파일, `evidence_indexed.json`을 BO ID 중심의 source pack으로 편성한다.
|
||||
|
||||
실제 `BO.json`과 `fact_source_pack.json`을 확인하면 첫 BO의 `amount`는 다음과 같은 Object이다.
|
||||
|
||||
```json
|
||||
{
|
||||
"currency": "KRW",
|
||||
"numeric_value": null,
|
||||
"value_text": null
|
||||
}
|
||||
```
|
||||
|
||||
즉, `amount`가 문자열이 된 원인은 Part 2나 FL0가 아니다. FL0 단계까지는 구조화된 Object가 보존되어 있다.
|
||||
|
||||
### 3.2 FL1: 실제 row format을 결정하는 직접 원인
|
||||
|
||||
`Task_FL1_deterministic_fact_ledger_candidate_builder`의 `_build_row()`가 최종 format을 사실상 결정한다.
|
||||
|
||||
#### 3.2.1 `FINAL_FIELDS`는 필드명만 지정함
|
||||
|
||||
FL1은 27개 필드명을 `FINAL_FIELDS`로 선언하지만 각 필드의 JSON type, null 허용 여부, enum, nested property를 선언하지 않는다.
|
||||
|
||||
```python
|
||||
FINAL_FIELDS = [
|
||||
"fact_id", "source_bo_id", "type", "date", "parties",
|
||||
"object_spec", "amount", "action", ...
|
||||
]
|
||||
FINAL_FIELD_SET = set(FINAL_FIELDS)
|
||||
```
|
||||
|
||||
후속 검사는 다음 조건뿐이다.
|
||||
|
||||
```python
|
||||
if set(row) != FINAL_FIELD_SET:
|
||||
raise ValueError(...)
|
||||
```
|
||||
|
||||
따라서 `amount`가 Object인지 String인지, `derived_fact`가 Boolean인지 String인지와 무관하게 키가 27개이면 통과한다.
|
||||
|
||||
#### 3.2.2 `_text()`가 Object를 JSON 문자열로 변환함
|
||||
|
||||
FL1의 `_text()`는 String이 아닌 값을 `json.dumps()`한 뒤 String으로 반환한다.
|
||||
|
||||
```python
|
||||
def _text(value, limit=500):
|
||||
raw = value if isinstance(value, str) else json.dumps(value, ...)
|
||||
return ...
|
||||
```
|
||||
|
||||
`amount` 생성 코드는 이 함수를 직접 사용한다.
|
||||
|
||||
```python
|
||||
"amount": _text(_first(bo.get("amount"), core.get("Amount")), 500) or None
|
||||
```
|
||||
|
||||
그 결과 upstream의 amount Object가 다음처럼 JSON String이 된다.
|
||||
|
||||
```json
|
||||
{
|
||||
"amount": "{\"currency\": \"KRW\", \"numeric_value\": null, \"value_text\": null}"
|
||||
}
|
||||
```
|
||||
|
||||
이는 단순 표시 차이가 아니라 **double-encoded JSON**이다. 실제 48개 row의 `amount` String은 모두 다시 JSON parse가 가능한 문자열이다. 즉, 객체를 문자열로 바꾼 사실이 데이터 전체에서 일관되게 확인된다.
|
||||
|
||||
#### 3.2.3 `object_spec` fallback도 JSON 문자열을 직접 생성함
|
||||
|
||||
FL1은 `core_field_base.Object`가 없으면 다음 fallback을 사용한다.
|
||||
|
||||
```python
|
||||
"object_spec": _first(
|
||||
core.get("Object"),
|
||||
json.dumps(linked["asset_cluster_ids"], ...)
|
||||
)
|
||||
```
|
||||
|
||||
따라서 빈 asset cluster는 Array `[]`가 아니라 String `"[]"`가 된다. 이번 결과의 48개 `object_spec`은 모두 다시 JSON parse가 가능한 String이다.
|
||||
|
||||
#### 3.2.4 나머지 type 차이는 `_build_row()`에 명시된 설계임
|
||||
|
||||
다음 필드는 런타임이 우연히 바꾼 것이 아니라 FL1 코드가 명시적으로 해당 type을 생성한다.
|
||||
|
||||
| 필드 | FL1 생성 방식 | 실제 type |
|
||||
|---|---|---|
|
||||
| `credibility` | `_score_evidence()`의 enum 반환 | String |
|
||||
| `claim_chain_ref` | `_claim_groups()`의 named map 반환 | Object |
|
||||
| `linked_structures` | `_linked_view()` 반환 | Object |
|
||||
| `must_consider` | `bool(...)` | Boolean |
|
||||
| `linked_actio_structures` | structure IDs와 count를 담은 literal | Object |
|
||||
| `derived_fact` | `_fallback_derived()`의 사실 문장 | String |
|
||||
| `derivation_basis` | `"; ".join(basis)` | String |
|
||||
| `legal_calculation_object` | 계산 관련 named map | Object |
|
||||
|
||||
따라서 분석 문서의 ArrayㆍBoolean 예시는 FL1의 실제 구현을 반영하지 못한 것이다.
|
||||
|
||||
#### 3.2.5 ID 형식도 FL1에서 결정됨
|
||||
|
||||
FL1은 다음 코드로 Fact ID를 생성한다.
|
||||
|
||||
```python
|
||||
"fact_id": f"F-{index:03d}"
|
||||
```
|
||||
|
||||
따라서 실제 ID는 `F-001`이며 분석 문서의 `F-0001`과 다르다. `source_bo_id`는 새 ID로 변환하지 않고 upstream BO의 `bh1` 등을 그대로 보존한다. 분석 문서의 `BO-0001`은 실제 ID 정책과 일치하지 않는 예시이다.
|
||||
|
||||
### 3.3 FL2: 이번 format 차이의 원인이 아님
|
||||
|
||||
실제 `fact_exception_manifest.json`은 다음 상태를 보인다.
|
||||
|
||||
- `exception_count`: 0
|
||||
- `pack_count`: 0
|
||||
- LLM decision 수: 0
|
||||
|
||||
또한 `Fact_Ledger_base_candidate.json`의 `candidate_items`와 최종 `Fact_Ledger_base.json`은 완전히 동일하다.
|
||||
|
||||
그러므로 이번 자료형 차이를 FL2 LLM의 오판, 자유 생성, schema 이탈로 설명할 수 없다. FL2는 이번 실행에서 row format에 전혀 관여하지 않았다.
|
||||
|
||||
### 3.4 FL3: 잘못된 type을 차단하지 못한 원인
|
||||
|
||||
`Task_FL3_final_fact_ledger_gate_and_writer`는 최종 파일의 단일 writer이지만, `_validate_rows()`는 다음만 검사한다.
|
||||
|
||||
```python
|
||||
if not isinstance(row, dict) or set(row) != FINAL_FIELD_SET:
|
||||
raise ValueError("candidate row schema is not closed")
|
||||
```
|
||||
|
||||
검사하지 않는 항목은 다음과 같다.
|
||||
|
||||
- `object_spec`이 Object인지 여부
|
||||
- `amount`가 ObjectㆍNumberㆍnull인지 여부
|
||||
- `claim_chain_ref`가 Array인지 Object인지 여부
|
||||
- `must_consider`가 Boolean인지 여부
|
||||
- `derived_fact`가 Boolean인지 String인지 여부
|
||||
- nested field의 required key와 type
|
||||
- ID 정규식과 자릿수
|
||||
- JSON String 안에 다시 JSON이 들어가는 이중 인코딩 여부
|
||||
|
||||
FL3는 BO 보존, exception coverage, 참조 universe, review gate는 엄격하게 검사한다. 그러나 **field-level JSON type contract는 검사하지 않는다.** 마지막 `_verify_reread()`도 작성 전후 digest가 같은지만 확인한다. 잘못된 type을 그대로 쓰고 다시 같은 값으로 읽으면 검사를 통과한다.
|
||||
|
||||
### 3.5 최종 실행 흐름
|
||||
|
||||
이번 실행에서 format이 결정된 경로는 다음과 같다.
|
||||
|
||||
```text
|
||||
BO.json amount = Object
|
||||
|
|
||||
v
|
||||
FL0 source pack = Object 유지
|
||||
|
|
||||
v
|
||||
FL1 _build_row()
|
||||
- amount: _text(Object) -> JSON String
|
||||
- object_spec: json.dumps(Array) -> JSON String
|
||||
- 나머지 필드: FL1 코드가 Object/Boolean/String으로 명시 생성
|
||||
|
|
||||
v
|
||||
Fact_Ledger_base_candidate.json
|
||||
|
|
||||
+--> exception_count = 0, FL2 미실행
|
||||
|
|
||||
v
|
||||
FL3 _validate_rows()
|
||||
- 27개 key 존재 여부만 검사
|
||||
- field type은 검사하지 않음
|
||||
|
|
||||
v
|
||||
Fact_Ledger_base.json에 candidate를 그대로 기록
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 근본 원인 분석
|
||||
|
||||
### 4.1 단일한 machine-readable JSON Schema가 없음
|
||||
|
||||
Part 4에는 `FINAL_FIELDS` 배열은 있으나 `type`, `required`, `properties`, `items`, `enum`, `pattern`, `additionalProperties`를 포함하는 정식 JSON Schema가 없다. 분석 문서와 FL1ㆍFL3가 공유하는 하나의 schema artifact도 없다.
|
||||
|
||||
그 결과 다음 세 계약이 독립적으로 변했다.
|
||||
|
||||
1. 분석 문서가 설명한 개념적 format
|
||||
2. FL1이 실제 생성하는 Python value type
|
||||
3. FL3가 통과시키는 최소 조건
|
||||
|
||||
현재 가장 강한 실행 계약은 “27개 key가 정확히 존재해야 한다”는 조건뿐이다.
|
||||
|
||||
### 4.2 분석 문서가 실행 코드의 value type을 잘못 추상화함
|
||||
|
||||
`Analysis_Stage_1_Codex_v1.md`의 4.5.4는 필드명과 의미를 잘 정리했지만, `_build_row()`의 실제 반환 type을 그대로 옮기지 않았다. 예를 들어 다음 오해가 발생했다.
|
||||
|
||||
- “고려 대상 목록”이라는 의미로 `must_consider`를 Array로 표현했으나 코드는 Boolean flag를 사용한다.
|
||||
- “파생 사실 여부”라는 의미로 `derived_fact`를 Boolean으로 표현했으나 코드는 파생 사실 문장을 저장한다.
|
||||
- 구조 참조를 목록으로 단순화했으나 코드는 여러 종류의 ID를 보존하기 위해 Object를 사용한다.
|
||||
|
||||
따라서 4.5.4는 **필드명 목록으로는 정확하지만 JSON type 명세로는 부정확한 문서**이다.
|
||||
|
||||
### 4.3 “closed schema”라는 이름과 실제 검사의 불일치
|
||||
|
||||
FL1은 compile gate에 `candidate_schema_closed: true`를 기록하고 FL3는 “candidate row schema is not closed”라는 오류문을 사용한다. 그러나 실제로 닫혀 있는 것은 key set뿐이다.
|
||||
|
||||
일반적인 closed JSON Schema라면 다음을 함께 검증해야 한다.
|
||||
|
||||
- 각 property의 type
|
||||
- nested required property
|
||||
- enum과 nullability
|
||||
- array item type
|
||||
- ID pattern
|
||||
- `additionalProperties: false`
|
||||
|
||||
현재 명칭은 강한 schema 검증을 수행하는 것처럼 보이지만 실제 보증 범위는 훨씬 좁다. 이 naming mismatch가 분석 문서 작성 시 과도한 신뢰를 유발한 것으로 판단된다.
|
||||
|
||||
### 4.4 형식 회귀 테스트가 없음
|
||||
|
||||
현행 gate는 다음을 잘 검증한다.
|
||||
|
||||
- BO 48건과 Fact 48건의 보존
|
||||
- 중복ㆍ누락 BO
|
||||
- exception coverage
|
||||
- unknown evidenceㆍstructure reference
|
||||
- upstream review와 drafting gate
|
||||
|
||||
그러나 expected field type과 실제 field type을 비교하는 fixture test가 없다. 그 결과 48개 row 전체의 `amount`와 `object_spec`이 JSON String이어도 writer status가 `READY_WITH_REVIEW`가 된다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 법률ㆍdownstream 관점의 영향
|
||||
|
||||
### 5.1 `amount` 이중 인코딩
|
||||
|
||||
금전청구, 담보채무, 사해행위 가액배상 한도 계산은 통화, 원금, 잔액, 이율, 기준일을 구조적으로 읽어야 한다. `amount`가 Object가 아니라 JSON String이면 Stage 2가 추가 parse를 수행해야 하며, 이를 모르는 consumer는 금액을 단순 문장으로 취급하거나 계산에서 누락할 수 있다.
|
||||
|
||||
### 5.2 `object_spec` 이중 인코딩
|
||||
|
||||
부동산ㆍ지분ㆍ담보 목적물 연결은 asset ID와 fraction ID를 안정적으로 비교해야 한다. `"[]"`은 빈 Array가 아니라 truthy String이므로 PythonㆍJavaScript 조건문에서 “목적물이 존재한다”고 오판할 수 있다.
|
||||
|
||||
### 5.3 `derived_fact` 의미 충돌
|
||||
|
||||
분석 문서는 Boolean으로 설명하지만 실제 값은 사실 문장이다. Stage 2가 `derived_fact == true`를 기대하면 파생 사실을 인식하지 못한다. 반대로 String의 truthiness만 사용하면 모든 non-empty 사실을 “파생 사실”로 오인할 수 있다.
|
||||
|
||||
### 5.4 참조 필드의 Array/Object 차이
|
||||
|
||||
`claim_chain_ref`, `linked_structures`, `linked_actio_structures`는 실제 Object 구조가 법률적으로 더 유용하다. claimㆍliabilityㆍstructureㆍissueㆍasset을 named key로 분리할 수 있기 때문이다. 다만 Stage 2가 분석 문서의 Array를 기준으로 구현되어 있으면 iterationㆍmembershipㆍ`.get()` 처리에서 즉시 contract error가 발생한다.
|
||||
|
||||
### 5.5 provenance의 구조화 부족
|
||||
|
||||
`derivation_basis`가 세미콜론 연결 String이므로 source BO, evidence, structure를 다시 파싱해야 한다. 법률 감사 가능성과 기계 검증 측면에서는 reference object 배열이 더 안전하다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 수정 방향
|
||||
|
||||
### 6.1 먼저 authoritative schema를 결정해야 함
|
||||
|
||||
두 선택지가 존재한다.
|
||||
|
||||
#### 선택지 A: 분석 문서 4.5.4를 절대 기준으로 삼음
|
||||
|
||||
FL1의 모든 field type을 4.5.4 예시에 맞게 변경하고 FL3에서 해당 type을 강제한다. 그러나 `claim_chain_ref`와 `linked_structures`를 단순 Array로 바꾸면 현재 Object가 보존하는 named 관계가 손실될 수 있다.
|
||||
|
||||
#### 선택지 B: 실행 코드의 의미 구조를 기준으로 schema를 재정의함
|
||||
|
||||
현재 ObjectㆍBoolean 구조 중 합리적인 것은 유지하고, 이중 인코딩과 모호한 필드만 수정한다. 법률정보 보존성과 Stage 2 확장성 면에서 이 선택지가 더 적합하다.
|
||||
|
||||
**권고안은 선택지 B이다.** 다만 분석 문서가 아니라 별도의 machine-readable schema를 단일 진실 원천으로 삼아야 한다.
|
||||
|
||||
### 6.2 권고하는 canonical type
|
||||
|
||||
| 필드 | 권고 type | 조치 |
|
||||
|---|---|---|
|
||||
| `fact_id` | String, `^F-\d{3}$` | 현행 `F-001`을 공식화하거나 자릿수를 명시적으로 변경함 |
|
||||
| `source_bo_id` | String, upstream BO universe member | `bh#`를 그대로 보존하고 문서 예시를 수정함 |
|
||||
| `object_spec` | Object 또는 null | JSON String 사용을 금지함 |
|
||||
| `amount` | Object 또는 null | upstream amount Object를 deep copy함 |
|
||||
| `credibility` | String enum | `high|medium|low`를 공식화함 |
|
||||
| `claim_chain_ref` | Object | claimㆍliability group을 named key로 유지함 |
|
||||
| `linked_structures` | Object | structureㆍissueㆍassetㆍevidence key를 유지함 |
|
||||
| `must_consider` | Boolean | 현행 flag 의미를 공식화함 |
|
||||
| `linked_actio_structures` | Object | 구조 ID와 signal count를 유지함 |
|
||||
| `derived_fact` | String 또는 null | 사실 문장 필드로 공식화하거나 `derived_fact_text`로 rename함 |
|
||||
| `is_derived` | Boolean | 파생 여부가 필요하면 별도 필드로 추가함 |
|
||||
| `derivation_basis` | Array of Object | BOㆍevidenceㆍstructure ref를 구조화함 |
|
||||
| `legal_calculation_object` | Object 또는 null | nested required key를 정의함 |
|
||||
|
||||
### 6.3 FL1 수정 사항
|
||||
|
||||
최소한 다음 두 코드는 변경해야 한다.
|
||||
|
||||
```python
|
||||
# 현재
|
||||
"amount": _text(_first(bo.get("amount"), core.get("Amount")), 500) or None
|
||||
|
||||
# 권고
|
||||
"amount": copy.deepcopy(_first(bo.get("amount"), core.get("Amount")))
|
||||
```
|
||||
|
||||
```python
|
||||
# 현재
|
||||
"object_spec": _first(
|
||||
core.get("Object"),
|
||||
json.dumps(linked["asset_cluster_ids"], ...)
|
||||
)
|
||||
|
||||
# 권고 예시
|
||||
"object_spec": {
|
||||
"source_object": copy.deepcopy(core.get("Object")),
|
||||
"asset_cluster_ids": linked["asset_cluster_ids"]
|
||||
}
|
||||
```
|
||||
|
||||
`derivation_basis`도 사람이 읽는 String과 기계가 검증하는 reference 배열을 분리하는 것이 적합하다.
|
||||
|
||||
### 6.4 FL3 수정 사항
|
||||
|
||||
FL3는 `FINAL_FIELD_SET` 검사 외에 다음을 수행해야 한다.
|
||||
|
||||
1. 정식 JSON Schema로 모든 candidate row를 검증한다.
|
||||
2. `amount`ㆍ`object_spec`이 JSON처럼 보이는 String이면 hard failure 처리한다.
|
||||
3. nested reference Object의 required key와 array item type을 검증한다.
|
||||
4. `fact_id` pattern과 `source_bo_id` universe membership을 모두 검증한다.
|
||||
5. FL2 patch 적용 후 schema를 다시 검증한다.
|
||||
6. 최종 write 후 reread한 값에도 같은 schema 검증을 반복한다.
|
||||
|
||||
### 6.5 문서와 테스트 수정 사항
|
||||
|
||||
- `Analysis_Stage_1_Codex_v1.md` 4.5.4를 authoritative schema와 동일하게 수정해야 한다.
|
||||
- 48개 실제 row의 type profile을 fixture로 저장하고 regression test에 포함해야 한다.
|
||||
- `candidate_items == final rows`만 검사하지 말고 각 field type과 nested contract를 검사해야 한다.
|
||||
- Stage 2 consumer가 Array/Object와 Boolean/String을 어떤 방식으로 읽는지 함께 점검해야 한다.
|
||||
|
||||
---
|
||||
|
||||
## 7. 최종 판정
|
||||
|
||||
`Results_00_7_21_Codex/Fact_Ledger_base.json`은 Part 4 YAML의 **현재 실행 코드에는 부합**한다. 48개 BO를 보존하고, 27개 key를 정확히 유지하며, 참조 gate도 통과했다. 그러므로 실행 결과가 FL3 writer에 의해 임의로 망가진 것은 아니다.
|
||||
|
||||
그러나 이 파일은 `Analysis_Stage_1_Codex_v1.md` 4.5.4가 암시한 field-level JSON type contract에는 부합하지 않는다. 가장 중요한 원인은 다음 세 가지이다.
|
||||
|
||||
1. 분석 문서와 실행 코드가 공유하는 정식 JSON Schema가 없다.
|
||||
2. FL1이 분석 문서와 다른 type을 명시적으로 생성하고 `amount`ㆍ`object_spec`을 JSON String으로 이중 인코딩한다.
|
||||
3. FL3가 key set만 검사하고 field type을 검사하지 않는다.
|
||||
|
||||
따라서 문제의 본질은 **LLM 출력 불안정성이 아니라 문서-코드 간 schema drift와 불완전한 deterministic validator**이다. 해결 역시 LLM prompt 강화가 아니라 canonical JSON Schema 도입, FL1의 type-safe projection, FL3의 field-level 검증 강화로 이루어져야 한다.
|
||||
Reference in New Issue
Block a user