From 5e6c0a285ce76fe9da22ba1e9db71d3bfb206e7e Mon Sep 17 00:00:00 2001 From: LsMin124 Date: Thu, 25 Jun 2026 16:57:53 +0900 Subject: [PATCH] =?UTF-8?q?fix(v2):=20sequence=20write-side=20N=3D0/sorted?= =?UTF-8?q?ness=20=EB=8B=A8=EC=9D=BC=EC=86=8C=EC=8A=A4=ED=99=94=20?= =?UTF-8?q?=E2=80=94=20'N=3D0=E2=86=94constraints'=20=EB=AA=A8=EC=88=9C=20?= =?UTF-8?q?=ED=95=B4=EC=86=8C=20(=EC=9E=91=EC=97=85=20B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit int_array 입력형식 prose 가 size_range.min 과 무관하게 'N=0 이면 0 한 줄'을 무조건 방출 → render_constraints 의 N∈[min,max](min≥1)와 정면 충돌해 QA ambiguity reject (loop_accumulate 3/3·binary_search 3/3·lis 3/3 run 실측). graph empty bias 가 size_range.min 을 존중하는 것과 달리 sequence 프로즈만 단일소스화를 못 받은 Phase-1b-for-sequence 갭. - _int_array_format(field): N=0 절을 size_range.min==0 일 때만 방출(단일소스). _FORMAT_TEXT 에서 'int_array' 제거 — 빈 수열 절은 상수가 아니라 size_range 파생. - SORTEDNESS_LABEL/DUPLICATES_LABEL 단일소스: format prose(_sequence_clause)와 narrative DATA(SequenceBackbone.structural_facts)가 같은 라벨 READ → 드리프트 불가. non_decreasing 을 수식형(a[i] ≤ a[i+1])으로 — 평문 '오름차순' 병기가 순증가로 읽혀 '중복 값 가능'과 충돌하던 모호성 제거(binary_search 실측). serializer(_serialize_int_array/_sequence_values) 무수정 = 직렬화 바이트 byte-identical. 게이트: 899 passed/4 skipped, mypy --strict 100, ruff green. code-review APPROVE 0C0H0M. --- ipe/v2/backbone/sequence.py | 21 ++++-------- ipe/v2/generation/input_gen.py | 58 ++++++++++++++++++++----------- tests/v2/test_input_gen.py | 62 ++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 34 deletions(-) diff --git a/ipe/v2/backbone/sequence.py b/ipe/v2/backbone/sequence.py index bfabe9b..f31656a 100644 --- a/ipe/v2/backbone/sequence.py +++ b/ipe/v2/backbone/sequence.py @@ -22,18 +22,16 @@ from typing import TYPE_CHECKING -from ..generation.input_gen import derive_degenerate_inputs +from ..generation.input_gen import ( + DUPLICATES_LABEL, + SORTEDNESS_LABEL, + derive_degenerate_inputs, +) from .base import DegenerateInput if TYPE_CHECKING: from ipe.v1.schema import IOSchema -_SORTEDNESS_FACT = { - "unsorted": "무정렬(임의 순서)", - "non_decreasing": "비내림차 정렬(a[i] ≤ a[i+1])", - "strictly_increasing": "순증가 정렬(a[i] < a[i+1], 중복 없음)", -} - class SequenceBackbone: """Sequence-family backbone. Owns a schema iff some ``int_array`` field carries a @@ -65,14 +63,9 @@ def structural_facts(self, io_schema: IOSchema) -> list[str]: if f.type != "int_array" or shape is None: continue prefix = f"{f.name}(수열)" - facts.append(f"{prefix}: {_SORTEDNESS_FACT[shape.sortedness]}") + facts.append(f"{prefix}: {SORTEDNESS_LABEL[shape.sortedness]}") if shape.sortedness != "strictly_increasing": - dup = ( - "중복 값 가능" - if shape.duplicates_allowed - else "중복 값 없음(서로 다른 값)" - ) - facts.append(f"{prefix}: {dup}") + facts.append(f"{prefix}: {DUPLICATES_LABEL[shape.duplicates_allowed]}") return facts def derive_edge_inputs(self, io_schema: IOSchema) -> tuple[DegenerateInput, ...]: diff --git a/ipe/v2/generation/input_gen.py b/ipe/v2/generation/input_gen.py index 73c5827..8107a5a 100644 --- a/ipe/v2/generation/input_gen.py +++ b/ipe/v2/generation/input_gen.py @@ -102,13 +102,12 @@ def seed_from_run_id(run_id: str) -> int: "bool": "한 줄에 0 또는 1.", "float": "한 줄에 실수 하나 (소수 4자리).", "string": "한 줄에 영소문자 문자열.", - "int_array": ( - "첫 줄에 원소 개수 N, 다음 줄에 N 개의 공백구분 정수 " - "(N=0 이면 '0' 한 줄만)." - ), "int_matrix": "첫 줄에 'R C'(행 수, 열 수), 이어서 R 줄에 각 C 개의 공백구분 정수.", "grid": "첫 줄에 'R C'(행 수, 열 수), 이어서 R 줄에 각 C 개의 공백구분 정수.", } +# int_array 는 빈 수열(N=0) 절을 size_range.min 에서 파생해야 하므로 상수가 아니라 +# ``_int_array_format(field)`` 로 렌더한다 — min≥1 스키마에 'N=0 이면…' 을 무조건 달면 +# render_constraints 의 N∈[min,max](min≥1) 와 정면 충돌해 QA ambiguity reject 된다(실측). def _vertex_index_phrase(indexing: int) -> str: @@ -134,20 +133,28 @@ def _structural_clause(field: IOFieldSpec) -> str: return ", ".join(parts) +# 정렬성/중복 라벨 — **단일 진실원천**. format prose(``_sequence_clause``)와 narrative +# DATA(``SequenceBackbone.structural_facts``)가 둘 다 여기서 READ → 두 곳이 드리프트 불가. +# non_decreasing 은 수식형(a[i] ≤ a[i+1])으로만 표기 — 평문 '오름차순' 병기는 순증가 +# (strictly ascending)로 읽혀 ``duplicates_allowed=True`` 와 충돌, QA ambiguity reject +# 됐다(binary_search 실측). 두 곳이 같은 라벨을 보므로 이 모호성이 한 곳에서만 닫힌다. +SORTEDNESS_LABEL = { + "unsorted": "무정렬(임의 순서)", + "non_decreasing": "비내림차순 정렬(a[i] ≤ a[i+1])", + "strictly_increasing": "순증가 정렬(a[i] < a[i+1], 중복 없음)", +} +DUPLICATES_LABEL = {True: "중복 값 가능", False: "중복 값 없음(서로 다른 값)"} + + def _sequence_clause(shape: SequenceShape) -> str: - """int_array 정렬/중복 사실 prose (sequence_shape 단일 진실 투영). format prose 는 - 직렬화 바이트와 **드리프트 금지** 라 serializer 동거 — ``_serialize_int_array`` 가 - 같은 shape 를 READ 해 실제 정렬/distinct 배열을 방출한다(SequenceBackbone. - structural_facts 의 *의미* 사실과 짝, 이쪽은 *형식* 기술).""" - sort = { - "unsorted": "무정렬(임의 순서)", - "non_decreasing": "비내림차(오름차순) 정렬", - "strictly_increasing": "순증가 정렬(중복 없음)", - }[shape.sortedness] + """int_array 정렬/중복 사실 prose (sequence_shape 단일 진실 투영 — ``SORTEDNESS_LABEL``/ + ``DUPLICATES_LABEL`` READ). format prose 는 직렬화 바이트와 **드리프트 금지** 라 + serializer 동거 — ``_serialize_int_array`` 가 같은 shape 를 READ 해 실제 정렬/distinct + 배열을 방출한다. structural_facts 와 같은 라벨을 공유 = 형식↔의미 무드리프트.""" + sort = SORTEDNESS_LABEL[shape.sortedness] if shape.sortedness == "strictly_increasing": return sort # distinct 함축 — 중복 절 생략 - dup = "중복 값 가능" if shape.duplicates_allowed else "중복 값 없음(서로 다른 값)" - return f"{sort}, {dup}" + return f"{sort}, {DUPLICATES_LABEL[shape.duplicates_allowed]}" def _string_clause(shape: StringShape) -> str: @@ -162,6 +169,17 @@ def _string_clause(shape: StringShape) -> str: }[shape.alphabet] +def _int_array_format(field: IOFieldSpec) -> str: + """int_array 입력 형식 prose. 빈 수열(N=0) 절은 size_range 가 N=0 을 **실제 허용**할 + 때만(min==0) 방출 — graph empty bias 가 size_range.min 을 존중하는 것과 동형으로, + 빈/최소 입력 의미를 ``size_range.min`` 단일소스에서 파생한다. min≥1 이면 절 없음(= + render_constraints 의 N≥min 과 정합). 'N=0 이면…' 을 무조건 달면 N≥1 스키마에서 + constraints 와 모순돼 QA ambiguity reject 됐다(loop_accumulate/binary_search/lis 실측).""" + base = "첫 줄에 원소 개수 N, 다음 줄에 N 개의 공백구분 정수" + lo = field.size_range.min_value if field.size_range is not None else _DEFAULT_SIZE[0] + return f"{base} (N=0 이면 '0' 한 줄만)." if lo == 0 else f"{base}." + + def _render_field(field: IOFieldSpec, indexing: int) -> str: if _is_reference(field): # 참조 스칼라 — 가리키는 collection 의 원소/정점 번호 (indexing base). @@ -183,11 +201,11 @@ def _render_field(field: IOFieldSpec, indexing: int) -> str: f"{field.name}: 첫 줄에 정점 수 V, 이어서 V-1 줄에 {edge_line}. " f"정점 번호는 {_vertex_index_phrase(indexing)}, 트리(연결·무사이클) 보장." ) - if field.type == "int_array" and field.sequence_shape is not None: - return ( - f"{field.name}: {_FORMAT_TEXT['int_array']} " - f"{_sequence_clause(field.sequence_shape)}." - ) + if field.type == "int_array": + prose = _int_array_format(field) + if field.sequence_shape is not None: + return f"{field.name}: {prose} {_sequence_clause(field.sequence_shape)}." + return f"{field.name}: {prose}" if field.type == "string" and field.string_shape is not None: return f"{field.name}: 한 줄에 {_string_clause(field.string_shape)} 문자열." if field.type in ("int_matrix", "grid") and field.cols_range is not None: diff --git a/tests/v2/test_input_gen.py b/tests/v2/test_input_gen.py index 1440cbc..fbe27e7 100644 --- a/tests/v2/test_input_gen.py +++ b/tests/v2/test_input_gen.py @@ -896,6 +896,68 @@ def test_render_multi_field_preserves_order() -> None: assert "1)" in text and "2)" in text # 필드 순번 명시 +# ---------- int_array N=0 절 / sortedness 단일소스 (Task B — sequence write-side) ---------- + + +def test_render_int_array_omits_empty_clause_when_min_positive() -> None: + # min≥1 → 'N=0' 절 없음 (render_constraints 의 N≥min 과 정합). loop_accumulate(3/3)· + # binary_search(3/3)·lis(3/3) 가 'N=0↔constraints' 모순으로 reject 되던 결함의 해소. + text = render_input_format(_io_schema(_int_array_field())) # min_value=1 + assert "N=0" not in text + # size_range=None (방어 기본 _DEFAULT_SIZE min=1) 도 절 없음 — 명시 확인. + none_size = render_input_format(_io_schema(IOFieldSpec(name="a", type="int_array"))) + assert "N=0" not in none_size + + +def test_render_int_array_strictly_increasing_uses_math_form() -> None: + # 순증가도 수식형(a[i] < a[i+1])으로 — structural_facts 와 동일 라벨(단일소스) 확인. + field = _int_array_field().model_copy( + update={"sequence_shape": SequenceShape(sortedness="strictly_increasing")} + ) + text = render_input_format(_io_schema(field)) + assert "a[i] < a[i+1]" in text + assert "중복 없음" in text + + +def test_render_int_array_states_empty_clause_when_min_zero() -> None: + # size_range 가 N=0 을 실제 허용(min==0)할 때만 빈 수열 절 방출 — size_range.min 단일소스. + field = _int_array_field().model_copy( + update={"size_range": ConstraintRange(name="arr", min_value=0, max_value=20)} + ) + text = render_input_format(_io_schema(field)) + assert "N=0" in text # 빈 수열 허용 → 절 명시 (constraints N∈[0,20] 과 정합) + + +def test_render_int_array_sortedness_is_unambiguous() -> None: + # non_decreasing 은 수식형(a[i] ≤ a[i+1])으로만 표기 — 평문 '오름차순' 병기는 순증가로 + # 읽혀 '중복 값 가능' 과 충돌해 QA ambiguity reject 됐다(binary_search 실측). + field = _int_array_field().model_copy( + update={ + "sequence_shape": SequenceShape( + sortedness="non_decreasing", duplicates_allowed=True + ) + } + ) + text = render_input_format(_io_schema(field)) + assert "오름차순" not in text + assert "a[i] ≤ a[i+1]" in text + assert "중복 값 가능" in text + + +def test_sortedness_label_single_sourced_with_structural_facts() -> None: + # format prose(render_input_format)와 narrative DATA(SequenceBackbone.structural_facts)가 + # 같은 SORTEDNESS_LABEL 을 공유 → 한 곳에서 닫힌 모호성이 다른 곳에서 되살아날 수 없다. + from ipe.v2.backbone import SequenceBackbone + + field = _int_array_field().model_copy( + update={"sequence_shape": SequenceShape(sortedness="non_decreasing")} + ) + fmt = render_input_format(_io_schema(field)) + facts = " | ".join(SequenceBackbone().structural_facts(_io_schema(field))) + assert "비내림차순 정렬(a[i] ≤ a[i+1])" in fmt + assert "비내림차순 정렬(a[i] ≤ a[i+1])" in facts + + # ---------- GraphShape: 구조 사실 IR 필드 (Phase 1 F6~F8) ----------