From ea6cfa3b662069ec9ec776b76d4a1d3d80f8b98c Mon Sep 17 00:00:00 2001 From: LsMin124 Date: Fri, 26 Jun 2026 03:28:00 +0900 Subject: [PATCH] =?UTF-8?q?fix(v2):=20references=20reference=5Fkind=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85=20=E2=80=94=20'index=E2=86=94count'=20QA=20?= =?UTF-8?q?=EB=AA=A8=EC=88=9C=20=ED=95=B4=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit binary_search '적어도 K개' 류에서 K가 collection 크기에 묶인 cardinality(개수) 인데 references 메커니즘이 위치 인덱스 prose('가리키는 1-indexed 번호')만 방출해, narrative 의 '목표 개수(인덱스 아님)' 서술과 'index↔count' 모순 → QA ambiguity reject 되던 결함(taskB-run1 fail_qa 실측)의 해소. - IOFieldSpec.reference_kind: Literal["index","cardinality"] 추가 (default "index"). index=위치 번호(현행·graph s/t·질의 인덱스), cardinality=크기에 묶인 개수/수량. - _render_field / render_constraints: cardinality 분기('개수/수량 … 위치 인덱스가 아니다'). index 경로 무변경 → 기존 graph io_contract byte-identical, 무회귀. - 생성기(_is_reference/_serialize_reference) 무수정 → 두 kind 생성 입력 byte-identical (범위·symbolic_max 동일, 의미 prose 만 분기). - DB 스키마/migration 영향 0: reference_kind 는 internal_meta 미포함=비-persist, input_format(Text)·constraints[].description(기존 JSON 키) 문자열만 변동. - formalizer 가이드에 cardinality 판별 규율 추가. describe_io_field 에 kind 마킹. - 테스트 5종(prose 분기·byte-identical·default·describe). 게이트 903 passed/ mypy --strict 100/ruff green. 측정: binary_search P1 N=3 에서 cardinality references 설계 0회 발생(설계 변동성, taskB 1/3→이번 0/3)이라 해당 실패 클래스 live 미재현 — 결정론적 렌더링은 단위 입증, LLM 의 cardinality emit 은 가이드+back-route QA 가 처리. 별개 실패 모드 (output_format↔description '누적↔단위' 용어 드리프트)는 후속 후보. --- ipe/v1/schema/blueprint.py | 13 +++++++ ipe/v2/generation/input_gen.py | 29 ++++++++++---- ipe/v2/nodes/formalizer.py | 8 ++++ tests/v2/test_input_gen.py | 69 ++++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 7 deletions(-) diff --git a/ipe/v1/schema/blueprint.py b/ipe/v1/schema/blueprint.py index c7621a2..89b8100 100644 --- a/ipe/v1/schema/blueprint.py +++ b/ipe/v1/schema/blueprint.py @@ -160,6 +160,19 @@ class IOFieldSpec(BaseModel): "구조적 해소 — 차원이 데이터 의존이라 정적 range 로 표현 불가하기 때문." ), ) + reference_kind: Literal["index", "cardinality"] = Field( + default="index", + description=( + "references 스칼라의 **의미** (서술 분기 — 생성 바이트는 두 경우 동일). " + "index=collection 의 특정 원소/정점을 가리키는 **위치 번호**(현행·기본; " + "graph 출발/도착 s·t, 질의 인덱스). cardinality=collection 크기에 묶인 " + "**개수/수량**으로, 위치 인덱스가 아니다(예: binary_search '적어도 K개' " + "에서 K). 둘 다 생성 범위는 [base, base+크기-1] 로 byte-identical 하나 " + "input_format/constraints 서술이 '가리키는 번호' ↔ '~ 이하의 개수' 로 " + "갈린다. references 미설정 시 무의미. narrative 가 K 를 '개수'로 서술하는데 " + "여기서 index 로 두면 'index↔count' 모순으로 QA reject 되던 결함의 해소." + ), + ) cols_range: ConstraintRange | None = Field( default=None, description=( diff --git a/ipe/v2/generation/input_gen.py b/ipe/v2/generation/input_gen.py index 8107a5a..23761d0 100644 --- a/ipe/v2/generation/input_gen.py +++ b/ipe/v2/generation/input_gen.py @@ -182,8 +182,14 @@ def _int_array_format(field: IOFieldSpec) -> str: def _render_field(field: IOFieldSpec, indexing: int) -> str: if _is_reference(field): - # 참조 스칼라 — 가리키는 collection 의 원소/정점 번호 (indexing base). + # 참조 스칼라 — 가리키는 collection 의 크기에 묶인 값 (indexing base). lo, bound = ("0", "크기 미만") if indexing == 0 else ("1", "크기 이하") + if field.reference_kind == "cardinality": + # 개수/수량 (위치 인덱스 아님) — narrative 의 '개수' 서술과 정합. + return ( + f"{field.name}: 한 줄에 정수 하나 — {field.references} 의 크기에 묶인 " + f"개수/수량 ({lo} 이상 {field.references} 의 {bound}). 위치 인덱스가 아니다." + ) label = "0-indexed" if indexing == 0 else "1-indexed" return ( f"{field.name}: 한 줄에 정수 하나 — {field.references} 의 원소/정점을 가리키는 " @@ -231,7 +237,8 @@ def describe_io_field(field: IOFieldSpec) -> str: """ head = f"{field.name}:{field.type}" if _is_reference(field): - return f"{head} →refs {field.references}(1..|{field.references}|)" + kind = "개수" if field.reference_kind == "cardinality" else "위치번호" + return f"{head} →refs {field.references}(1..|{field.references}|, {kind})" rng = "" if field.size_range is not None: rng += f" size[{field.size_range.min_value}..{field.size_range.max_value}]" @@ -284,18 +291,26 @@ def render_constraints(io_schema: IOSchema) -> list[ConstraintRange]: ) if symbolic is not None and base == 0: symbolic = f"{symbolic}-1" # 0-indexed → '크기 미만' = [0, V-1] - label = "0-indexed" if base == 0 else "1-indexed" bound = "크기 미만" if base == 0 else "크기 이하" + if f.reference_kind == "cardinality": + # 개수/수량 (위치 인덱스 아님) — input_format·narrative 와 같은 의미. + desc = ( + f"{f.references} 의 크기에 묶인 개수/수량 " + f"({base} 이상 {f.references} 의 {bound})" + ) + else: + label = "0-indexed" if base == 0 else "1-indexed" + desc = ( + f"{f.references} 의 {label} 번호 " + f"({base} 이상 {f.references} 의 {bound})" + ) out.append( ConstraintRange( name=f.name, min_value=base, max_value=base + size_hi - 1, symbolic_max=symbolic, - description=( - f"{f.references} 의 {label} 번호 " - f"({base} 이상 {f.references} 의 {bound})" - ), + description=desc, ) ) continue diff --git a/ipe/v2/nodes/formalizer.py b/ipe/v2/nodes/formalizer.py index 0ee07eb..4490572 100644 --- a/ipe/v2/nodes/formalizer.py +++ b/ipe/v2/nodes/formalizer.py @@ -51,6 +51,14 @@ **trivial 퇴화**(QA difficulty reject), ``[1,V상한]`` 으로 잡으면 작은 그래프에서 **V 초과 범위밖 입력**(정해 IndexError → fail_synthesis)이 된다. ``references`` 가 둘 다 구조적으로 차단한다 (정점 질의는 거의 항상 이 방식). + - **reference_kind (의미 구분)**: 그 스칼라가 collection 의 **특정 원소/정점을 가리키는 + 위치 번호**이면 ``reference_kind="index"``(기본; graph s·t, 질의 인덱스). 반대로 + collection **크기에 묶인 개수/수량**(위치가 아님)이면 반드시 + ``reference_kind="cardinality"`` 로 둔다 — 예: binary_search '적어도 K개를 만족하는 + 최소값' 에서 K(1≤K≤N 인 목표 **개수**, N번째 원소가 아님), '상위 K개 선택'의 K. + 두 경우 생성 범위는 [1, 크기] 로 동일하지만, cardinality 인데 index(기본)로 두면 + 형식 계약이 'fields 를 가리키는 1-indexed 번호'로 서술돼 narrative 의 '목표 개수' + 서술과 **'index↔count' 모순**을 일으켜 QA reject 된다. - **중복 카운트 금지** (위 graph 규율을 모든 collection 으로 일반화): collection 필드(int_array/int_matrix/grid/weighted_edges/tree_edges)는 canonical 직렬화에 **자기 크기 헤더**(원소 개수 N / 행·열 R C / 정점·간선 V E)를 **자기접두**로 자체 diff --git a/tests/v2/test_input_gen.py b/tests/v2/test_input_gen.py index fbe27e7..84ca176 100644 --- a/tests/v2/test_input_gen.py +++ b/tests/v2/test_input_gen.py @@ -637,6 +637,75 @@ def test_reference_into_int_array_bound_to_element_count() -> None: assert 1 <= k <= 6 # 원소 개수 이내 1-indexed +# ---------- reference_kind: index(위치) vs cardinality(개수) 서술 분기 ---------- + + +def _cardinality_schema(kind: str) -> IOSchema: + """[int_array fields, int K→fields(reference_kind=kind)] — binary_search '적어도 K개' 형상.""" + return IOSchema( + inputs=( + IOFieldSpec( + name="fields", + type="int_array", + size_range=ConstraintRange(name="fields", min_value=4, max_value=4), + value_range=ConstraintRange(name="v", min_value=1, max_value=9), + ), + IOFieldSpec( + name="K", type="int", references="fields", reference_kind=kind # type: ignore[arg-type] + ), + ), + output_type="int", + output_format="x", + ) + + +def test_cardinality_reference_renders_count_prose() -> None: + """cardinality 참조는 '개수/수량'·'위치 인덱스가 아니다' 로 서술 — index 의 '가리키는 + 번호' 와 갈려 narrative '목표 개수' 서술과 정합(index↔count 모순 해소).""" + text = render_input_format(_cardinality_schema("cardinality")) + assert "개수" in text and "위치 인덱스가 아니다" in text + assert "가리키는" not in text # 위치 인덱스 단정 안 함 + assert "fields 의 크기 이하" in text # 데이터 의존 범위는 보존 + + +def test_index_reference_renders_pointer_prose_unchanged() -> None: + """index(기본) 참조는 현행 '가리키는 1-indexed 번호' 서술 유지 — graph 무회귀.""" + text = render_input_format(_cardinality_schema("index")) + assert "가리키는" in text and "1-indexed" in text + assert "위치 인덱스가 아니다" not in text + + +def test_cardinality_reference_constraint_description() -> None: + cons_card = {c.name: c for c in render_constraints(_cardinality_schema("cardinality"))} + cons_idx = {c.name: c for c in render_constraints(_cardinality_schema("index"))} + assert "개수" in cons_card["K"].description + assert "번호" not in cons_card["K"].description + assert "번호" in cons_idx["K"].description # index 는 현행 유지 + # 숫자 범위·기호는 두 경우 동일 (의미만 갈림, 바인딩 동일) + assert cons_card["K"].min_value == cons_idx["K"].min_value == 1 + assert cons_card["K"].symbolic_max == cons_idx["K"].symbolic_max == "N" + + +def test_cardinality_reference_generation_byte_identical_to_index() -> None: + """reference_kind 는 서술만 가른다 — 생성 입력 바이트는 index 와 완전 동일.""" + contract = GeneratorContract(scale_families=(ScaleFamily(name="s", case_count=12),)) + idx = [c.input_text for c in generate_inputs(contract, _cardinality_schema("index"), seed=7)] + card = [ + c.input_text + for c in generate_inputs(contract, _cardinality_schema("cardinality"), seed=7) + ] + assert idx == card # byte-identical + + +def test_describe_io_field_marks_reference_kind() -> None: + card = describe_io_field( + IOFieldSpec(name="K", type="int", references="fields", reference_kind="cardinality") + ) + idx = describe_io_field(IOFieldSpec(name="s", type="int", references="grid")) + assert "개수" in card # cardinality 마킹 + assert "위치번호" in idx # index(기본) 마킹 + + def test_reference_resolves_regardless_of_field_order() -> None: """참조 스칼라가 collection 보다 **앞**에 선언돼도 실제 크기에 바인딩.""" schema = IOSchema(