문제의 출발점
앞 편에서 실행과 기록의 루프를 만들더라도 작업 목록은 설계해야 한다는 문제를 남겼어요. AI에게 목록까지 만들게 하면 이 구간도 자동화할 수 있을 것 같아요. 계산 사례라면 금액 변환을 구현하는 작업과 합계를 구현하는 작업으로 나누고, 앞 작업의 결과를 뒤 작업이 사용하도록 연결할 수 있어요.
Task를 입력과 결과, 검사 조건을 가진 함수처럼 연결한다고 생각해 볼게요. 각 단계의 검사가 통과하면 원래 요청도 해결될 것 같아요. 하지만 검사가 기대한 반환값을 확인했다는 사실과, 요청의 모든 조건을 확인했다는 사실은 달라요. 어떤 목표 조건을 어느 검사로 확인했는지까지 연결해야 해요. 이 편에서는 반환값이 맞아도 함수 재사용 조건을 놓치는 사례로 그 차이를 살펴봐요.
작업을 나누기 전에 요청의 기준을 남기기
VibeMarshal은 요청을 바로 Task 이름으로 바꾸기보다 GoalContractRevision으로 정리해요. 사용자 원문, 관찰할 결과, 반드시 충족할 수용 기준인 Hard AC, 제약과 비목표, 허용 효과를 담아요. 정규화한 목표를 독립적으로 검토하는 경로도 있어요. 목표 준비 구현
계산 사례의 기준을 세 가지로 정리할 수 있어요. 아래 값과 재사용 조건은 실제 fixture의 검사에서 가져온 것이고, 표는 완전한 Engine 입력 JSON이 아닌 설명용 요약이에요.
| 기준 | 확인할 결과 |
|---|---|
| AC-1 금액 변환 | cents("12.34") == 1234, cents("0.05") == 5, cents("3") == 300 |
| AC-2 합계 | total(["1.50", "2.25"]) == 375, total([]) == 0 |
| AC-3 함수 재사용 | 두 가격을 합산할 때 invoice에 바인딩된 cents가 두 번 호출됨 |
Goal은 결과를 판단할 기준이고, Plan은 그 결과를 만들 작업과 순서예요. T1을 cents 구현과 검사, T2를 total 구현과 검사로 나누면 T2는 T1에 의존해요. 이 선후관계를 Task DAG로 표현할 수 있어요. DAG가 있다고 작업을 모두 동시에 실행하는 것은 아니에요. 현재 운영 계약은 같은 프로젝트의 Attempt를 직렬로 제한해요.
반환값은 맞지만 요청은 어기는 경로
total이 cents를 재사용하지 않고 금액을 직접 변환한다고 가정해 볼게요.
from decimal import Decimal
from money import cents
def total(prices: list[str]) -> int:
return sum(int(Decimal(price) * 100) for price in prices)이 구현은 예제의 합계 반환값을 맞출 수 있어요. cents 자체도 올바르게 구현돼 있다면 두 함수의 반환값만 보는 검사는 통과할 수 있어요. 그러나 AC-3은 만족하지 않아요. 가격을 변환하는 정책이 두 곳에 생겼고, 기존 함수의 호출도 빠졌어요.
이를 원본 fixture의 격리 복사본에서 확인했어요. 테스트 파일은 그대로 두고, 먼저 반환값을 확인하는 5개 검사만 선택해 실행했어요. 이어서 재사용 조건을 포함한 전체 6개를 실행했어요. 마지막에는 total이 cents를 사용하도록 고친 뒤 같은 전체 검사를 다시 실행했어요.
| 구현과 검사 범위 | 실행 결과 | 해석 |
|---|---|---|
직접 변환하는 total, 반환값 검사 5개 선택 | 5개 통과, 종료 코드 0 | 선택한 반환값은 맞음 |
| 같은 구현, 전체 6개 검사 | 5개 통과·1개 실패, 종료 코드 1 | 함수 재사용 조건을 어김 |
cents 재사용으로 수정, 전체 6개 검사 | 6개 통과, 종료 코드 0 | fixture의 모든 검사 충족 |
실패한 검사는 mock.patch.object(invoice, "cents", wraps=invoice.cents)로 합계 모듈에 바인딩된 함수를 감시해요. 기대 호출 수는 2지만 직접 변환하는 구현의 호출 수는 0이었어요. ‘함수 반환값이 맞다’와 ‘요청한 방식으로 동작한다’가 다른 기준임을 보여줘요. 변환 검사, 재사용 검사
처음 선택한 5개 검사는 AC-1과 AC-2의 반환값만 확인했어요. AC-3을 확인하는 재사용 검사는 포함하지 않았어요. 따라서 이 실행은 불완전한 검사 범위를 비교하기 위한 관측이며 작업의 완료 근거로 채택하지 않았어요. 실제 Core에서 모든 Task가 완료됐거나 잘못된 Plan이 승인됐다는 기록도 아니에요. 확인한 것은 선택한 검사들의 통과를 전체 Goal의 충족으로 확대할 때 놓칠 수 있는 조건이에요.
Task의 성공과 Goal의 완료를 나누기
Engine은 개별 Task validation과 Plan 수준의 integration validation, 전체 Goal의 판정을 구분해요. complete_task는 해당 Task의 필수 검사 ID 집합과 최신 결과를 대조해요. 빠진 검사나 실패가 있으면 완료를 거부하고, 미확정 실행이나 효과도 확인해요. Task 완료 조건
전체 목표는 별도의 기준으로 확인해요. runtime은 최신 Goal 검사 근거를 모아 판정에 사용하고, record_goal_verdict는 제출된 판정이 같은 Goal·Plan에 연결됐는지와 criterion coverage, Task 완료, integration 결과 및 실제 evidence를 대조해요. 개별 작업이 끝났다는 사실을 전체 목표 달성으로 바로 바꾸지 않아요. Goal 결과 집계, Goal 판정 수용 조건
그렇다고 이 구조가 원래 의도를 완벽히 알아낸다는 뜻은 아니에요. Goal 정규화에서 중요한 요구를 빠뜨리거나 의미 검사가 잘못 판단하면, 기록과 연결이 정확해도 문제는 남아요. 코드가 강제하는 것은 계약과 근거의 일치이고, 계약의 내용까지 항상 옳다는 보장은 아니에요.
Task를 함수처럼 연결하는 생각은 작업 사이의 입출력을 설계하는 데 도움이 돼요. 하지만 파일 수정이나 외부 효과를 가진 실행은 순수 함수가 아니며, 이 사례가 함수 합성의 수학적 보증을 입증하는 것도 아니에요. 원래 목표와 제약을 전체 경로에 다시 대조해야 해요. 다음 편에서는 Task 목록을 상세히 만들기 전에 어떤 해결 경로를 검토할지 살펴봐요.