Skip to content

Commit 6efce28

Browse files
authored
fix(v2): references reference_kind 도입 — 'index↔count' QA 모순 해소 (#176)
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 '누적↔단위' 용어 드리프트)는 후속 후보.
1 parent 8034eab commit 6efce28

4 files changed

Lines changed: 112 additions & 7 deletions

File tree

‎ipe/v1/schema/blueprint.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,19 @@ class IOFieldSpec(BaseModel):
160160
"구조적 해소 — 차원이 데이터 의존이라 정적 range 로 표현 불가하기 때문."
161161
),
162162
)
163+
reference_kind: Literal["index", "cardinality"] = Field(
164+
default="index",
165+
description=(
166+
"references 스칼라의 **의미** (서술 분기 — 생성 바이트는 두 경우 동일). "
167+
"index=collection 의 특정 원소/정점을 가리키는 **위치 번호**(현행·기본; "
168+
"graph 출발/도착 s·t, 질의 인덱스). cardinality=collection 크기에 묶인 "
169+
"**개수/수량**으로, 위치 인덱스가 아니다(예: binary_search '적어도 K개' "
170+
"에서 K). 둘 다 생성 범위는 [base, base+크기-1] 로 byte-identical 하나 "
171+
"input_format/constraints 서술이 '가리키는 번호' ↔ '~ 이하의 개수' 로 "
172+
"갈린다. references 미설정 시 무의미. narrative 가 K 를 '개수'로 서술하는데 "
173+
"여기서 index 로 두면 'index↔count' 모순으로 QA reject 되던 결함의 해소."
174+
),
175+
)
163176
cols_range: ConstraintRange | None = Field(
164177
default=None,
165178
description=(

‎ipe/v2/generation/input_gen.py‎

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,14 @@ def _int_array_format(field: IOFieldSpec) -> str:
182182

183183
def _render_field(field: IOFieldSpec, indexing: int) -> str:
184184
if _is_reference(field):
185-
# 참조 스칼라 — 가리키는 collection 의 원소/정점 번호 (indexing base).
185+
# 참조 스칼라 — 가리키는 collection 의 크기에 묶인 값 (indexing base).
186186
lo, bound = ("0", "크기 미만") if indexing == 0 else ("1", "크기 이하")
187+
if field.reference_kind == "cardinality":
188+
# 개수/수량 (위치 인덱스 아님) — narrative 의 '개수' 서술과 정합.
189+
return (
190+
f"{field.name}: 한 줄에 정수 하나 — {field.references} 의 크기에 묶인 "
191+
f"개수/수량 ({lo} 이상 {field.references} 의 {bound}). 위치 인덱스가 아니다."
192+
)
187193
label = "0-indexed" if indexing == 0 else "1-indexed"
188194
return (
189195
f"{field.name}: 한 줄에 정수 하나 — {field.references} 의 원소/정점을 가리키는 "
@@ -231,7 +237,8 @@ def describe_io_field(field: IOFieldSpec) -> str:
231237
"""
232238
head = f"{field.name}:{field.type}"
233239
if _is_reference(field):
234-
return f"{head} →refs {field.references}(1..|{field.references}|)"
240+
kind = "개수" if field.reference_kind == "cardinality" else "위치번호"
241+
return f"{head} →refs {field.references}(1..|{field.references}|, {kind})"
235242
rng = ""
236243
if field.size_range is not None:
237244
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]:
284291
)
285292
if symbolic is not None and base == 0:
286293
symbolic = f"{symbolic}-1" # 0-indexed → '크기 미만' = [0, V-1]
287-
label = "0-indexed" if base == 0 else "1-indexed"
288294
bound = "크기 미만" if base == 0 else "크기 이하"
295+
if f.reference_kind == "cardinality":
296+
# 개수/수량 (위치 인덱스 아님) — input_format·narrative 와 같은 의미.
297+
desc = (
298+
f"{f.references} 의 크기에 묶인 개수/수량 "
299+
f"({base} 이상 {f.references} 의 {bound})"
300+
)
301+
else:
302+
label = "0-indexed" if base == 0 else "1-indexed"
303+
desc = (
304+
f"{f.references} 의 {label} 번호 "
305+
f"({base} 이상 {f.references} 의 {bound})"
306+
)
289307
out.append(
290308
ConstraintRange(
291309
name=f.name,
292310
min_value=base,
293311
max_value=base + size_hi - 1,
294312
symbolic_max=symbolic,
295-
description=(
296-
f"{f.references} 의 {label} 번호 "
297-
f"({base} 이상 {f.references} 의 {bound})"
298-
),
313+
description=desc,
299314
)
300315
)
301316
continue

‎ipe/v2/nodes/formalizer.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@
5151
**trivial 퇴화**(QA difficulty reject), ``[1,V상한]`` 으로 잡으면 작은 그래프에서
5252
**V 초과 범위밖 입력**(정해 IndexError → fail_synthesis)이 된다. ``references`` 가
5353
둘 다 구조적으로 차단한다 (정점 질의는 거의 항상 이 방식).
54+
- **reference_kind (의미 구분)**: 그 스칼라가 collection 의 **특정 원소/정점을 가리키는
55+
위치 번호**이면 ``reference_kind="index"``(기본; graph s·t, 질의 인덱스). 반대로
56+
collection **크기에 묶인 개수/수량**(위치가 아님)이면 반드시
57+
``reference_kind="cardinality"`` 로 둔다 — 예: binary_search '적어도 K개를 만족하는
58+
최소값' 에서 K(1≤K≤N 인 목표 **개수**, N번째 원소가 아님), '상위 K개 선택'의 K.
59+
두 경우 생성 범위는 [1, 크기] 로 동일하지만, cardinality 인데 index(기본)로 두면
60+
형식 계약이 'fields 를 가리키는 1-indexed 번호'로 서술돼 narrative 의 '목표 개수'
61+
서술과 **'index↔count' 모순**을 일으켜 QA reject 된다.
5462
- **중복 카운트 금지** (위 graph 규율을 모든 collection 으로 일반화): collection
5563
필드(int_array/int_matrix/grid/weighted_edges/tree_edges)는 canonical 직렬화에
5664
**자기 크기 헤더**(원소 개수 N / 행·열 R C / 정점·간선 V E)를 **자기접두**로 자체

‎tests/v2/test_input_gen.py‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,75 @@ def test_reference_into_int_array_bound_to_element_count() -> None:
637637
assert 1 <= k <= 6 # 원소 개수 이내 1-indexed
638638

639639

640+
# ---------- reference_kind: index(위치) vs cardinality(개수) 서술 분기 ----------
641+
642+
643+
def _cardinality_schema(kind: str) -> IOSchema:
644+
"""[int_array fields, int K→fields(reference_kind=kind)] — binary_search '적어도 K개' 형상."""
645+
return IOSchema(
646+
inputs=(
647+
IOFieldSpec(
648+
name="fields",
649+
type="int_array",
650+
size_range=ConstraintRange(name="fields", min_value=4, max_value=4),
651+
value_range=ConstraintRange(name="v", min_value=1, max_value=9),
652+
),
653+
IOFieldSpec(
654+
name="K", type="int", references="fields", reference_kind=kind # type: ignore[arg-type]
655+
),
656+
),
657+
output_type="int",
658+
output_format="x",
659+
)
660+
661+
662+
def test_cardinality_reference_renders_count_prose() -> None:
663+
"""cardinality 참조는 '개수/수량'·'위치 인덱스가 아니다' 로 서술 — index 의 '가리키는
664+
번호' 와 갈려 narrative '목표 개수' 서술과 정합(index↔count 모순 해소)."""
665+
text = render_input_format(_cardinality_schema("cardinality"))
666+
assert "개수" in text and "위치 인덱스가 아니다" in text
667+
assert "가리키는" not in text # 위치 인덱스 단정 안 함
668+
assert "fields 의 크기 이하" in text # 데이터 의존 범위는 보존
669+
670+
671+
def test_index_reference_renders_pointer_prose_unchanged() -> None:
672+
"""index(기본) 참조는 현행 '가리키는 1-indexed 번호' 서술 유지 — graph 무회귀."""
673+
text = render_input_format(_cardinality_schema("index"))
674+
assert "가리키는" in text and "1-indexed" in text
675+
assert "위치 인덱스가 아니다" not in text
676+
677+
678+
def test_cardinality_reference_constraint_description() -> None:
679+
cons_card = {c.name: c for c in render_constraints(_cardinality_schema("cardinality"))}
680+
cons_idx = {c.name: c for c in render_constraints(_cardinality_schema("index"))}
681+
assert "개수" in cons_card["K"].description
682+
assert "번호" not in cons_card["K"].description
683+
assert "번호" in cons_idx["K"].description # index 는 현행 유지
684+
# 숫자 범위·기호는 두 경우 동일 (의미만 갈림, 바인딩 동일)
685+
assert cons_card["K"].min_value == cons_idx["K"].min_value == 1
686+
assert cons_card["K"].symbolic_max == cons_idx["K"].symbolic_max == "N"
687+
688+
689+
def test_cardinality_reference_generation_byte_identical_to_index() -> None:
690+
"""reference_kind 는 서술만 가른다 — 생성 입력 바이트는 index 와 완전 동일."""
691+
contract = GeneratorContract(scale_families=(ScaleFamily(name="s", case_count=12),))
692+
idx = [c.input_text for c in generate_inputs(contract, _cardinality_schema("index"), seed=7)]
693+
card = [
694+
c.input_text
695+
for c in generate_inputs(contract, _cardinality_schema("cardinality"), seed=7)
696+
]
697+
assert idx == card # byte-identical
698+
699+
700+
def test_describe_io_field_marks_reference_kind() -> None:
701+
card = describe_io_field(
702+
IOFieldSpec(name="K", type="int", references="fields", reference_kind="cardinality")
703+
)
704+
idx = describe_io_field(IOFieldSpec(name="s", type="int", references="grid"))
705+
assert "개수" in card # cardinality 마킹
706+
assert "위치번호" in idx # index(기본) 마킹
707+
708+
640709
def test_reference_resolves_regardless_of_field_order() -> None:
641710
"""참조 스칼라가 collection 보다 **앞**에 선언돼도 실제 크기에 바인딩."""
642711
schema = IOSchema(

0 commit comments

Comments
 (0)