라이프스타일

JSON 출력과 결과 검증

이벤트 스트림과 최종 결과를 구분하고 구조화 출력을 해석해 누락과 잘못된 형식을 거부합니다.

읽는 데 약 20분 · 실습 25 분

직접 제작한 흐름도이며 제품 화면이 아닙니다.
사진: Mokaair (© Mokaair)
이 글의 목차
  1. 목표와 자료
  2. 1단계: 이벤트와 최종 답변 분리
  3. 2단계: 고정 입력과 계약 준비
  4. 3단계: 한 번 실행하고 단계별 검증하는 프로그램
  5. 4단계: 실행하고 내용 확인
  6. 5단계: 모델 사용량 없이 실패 세 가지 실습
  7. 복원·제한·다음 단계

목표와 자료

이 단락의 학습 자료:

1단계: 이벤트와 최종 답변 분리

--json은 stdout을 한 줄마다 독립된 JSON 객체인 JSONL로 바꿉니다. 하나의 배열이나 최종 답변만이 아니므로 events.jsonl 전체에 json.loads를 한 번 호출하면 안 됩니다. --output-schema는 최종 답변 구조를 지정하고 -o는 이를 final.json에 저장합니다. 한 번 실행으로 이벤트·구조화 답변을 모두 보관하며 stderr는 진단을 저장합니다. 확장자는 이름이며 실제 형식은 플래그와 내용이 결정합니다.

파일내용승인 조건
status.json래퍼의 종료 기록exited이며 exit_code 0
events.jsonl줄 단위 CLI 이벤트각 줄 해석 가능, 완료 존재, 실패 없음
final.json최종 구조화 답변필드·타입·버전·값 검증
stderr.log진행·진단조사 자료이며 답변 아님
schema.json이번에 요구한 형식예상 계약과 함께 보존

이 파일명은 실습 규칙이며 Codex가 항상 자동 생성하는 파일 목록이 아닙니다.

2단계: 고정 입력과 계약 준비

자신의 빈 exec-json-lab에서 git init을 실행하고 아래 UTF-8 tasks.md를 만드세요. 이전 자료를 재사용하면 완전히 같은지 확인합니다. 버전 혼동을 막기 위해 검증기는 exec-practice-1만 허용합니다. 전체 3, 완료 1, 미완료 2를 손으로 확인하세요. 실제 자료에는 새 버전·승인 계약을 정의하며 모든 출력이 통과하도록 검사를 지우지 않습니다.

tasks.md · markdown
# Practice tasks
Revision: exec-practice-1

- [x] Read the guide
- [ ] Create a practice file
- [ ] Verify the result

3단계: 한 번 실행하고 단계별 검증하는 프로그램

아래 전체 프로그램을 같은 폴더의 run_summary.py로 저장하세요. Python 표준 라이브러리만 쓰므로 pip install이 필요 없습니다. 새 출력 폴더·schema·running 상태를 저장한 뒤 인수 배열로 Codex를 시작하며 모델 텍스트로 shell 명령을 조립하지 않습니다. 120초는 교육용 제한이며 Codex 고정 한도가 아닙니다. 시간 초과·시작 실패·0이 아닌 종료는 증거를 남기고 답변을 거부하며 자동 재시도하지 않습니다.

Windows에서는 독립 설치판 codex.exe를 사용합니다. PowerShell에서 Get-Command codex -All을 실행해 설치된 .exe인지 확인합니다. npm .cmd/.ps1 래퍼나 별칭만 있으면 Python은 PowerShell의 명령 검색 방식을 적용하지 않습니다. 의 공식 독립 설치 방법을 따르고 새 터미널에서 다시 확인하세요. 이를 해결하려고 shell=True나 셸 문자열 연결로 바꾸지 마세요. macOS/Linux에서는 터미널과 Python 프로세스의 PATH가 같은지 확인합니다.

run_summary.py (전체 코드) · python
"""Run once, retain evidence, and accept only a validated practice summary."""
import argparse
import json
from pathlib import Path
import subprocess
import sys

SCHEMA = {
    "type": "object",
    "properties": {
        "revision": {"type": "string"},
        "total": {"type": "integer", "minimum": 0},
        "completed": {"type": "integer", "minimum": 0},
        "pending": {"type": "integer", "minimum": 0},
    },
    "required": ["revision", "total", "completed", "pending"],
    "additionalProperties": False,
}
PROMPT = (
    "Read only tasks.md. Return its Revision marker and checkbox counts "
    "as revision, total, completed and pending. Do not edit input files "
    "or use external services."
)


def verify(run):
    status = json.loads((run / "status.json").read_text(encoding="utf-8"))
    if (not isinstance(status, dict)
            or status != {"state": "exited", "exit_code": 0}
            or type(status.get("exit_code")) is not int):
        raise ValueError("Process did not exit successfully")
    events = []
    for number, line in enumerate((run / "events.jsonl").read_text(encoding="utf-8-sig").splitlines(), 1):
        if not line.strip():
            continue
        event = json.loads(line)
        if not isinstance(event, dict) or not isinstance(event.get("type"), str):
            raise ValueError(f"Invalid event on line {number}")
        events.append(event["type"])
    if (events.count("turn.completed") != 1 or events.count("turn.started") != 1
            or events.index("turn.started") > events.index("turn.completed")):
        raise ValueError("Missing or ambiguous completed turn")
    if "turn.failed" in events or "error" in events:
        raise ValueError("Failure event requires investigation")
    result = json.loads((run / "final.json").read_text(encoding="utf-8-sig"))
    if not isinstance(result, dict) or set(result) != set(SCHEMA["required"]):
        raise ValueError("Unexpected or missing final fields")
    if result["revision"] != "exec-practice-1":
        raise ValueError("Unexpected input revision")
    if any(type(result[key]) is not int or result[key] < 0 for key in ("total", "completed", "pending")):
        raise ValueError("Counts must be nonnegative integers, not booleans")
    if result["total"] != result["completed"] + result["pending"]:
        raise ValueError("Inconsistent count arithmetic")
    return result


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("run_name")
    parser.add_argument("--verify-only", action="store_true")
    args = parser.parse_args()
    root = Path.cwd().resolve()
    if Path(args.run_name).name != args.run_name or args.run_name in {".", ".."}:
        parser.error("Use a folder name, not a path")
    run = root / args.run_name
    if not args.verify_only:
        if not (root / "tasks.md").is_file():
            parser.error("Missing tasks.md in the current folder")
        run.mkdir()  # Refuse to overwrite an earlier run.
        schema_path = run / "schema.json"
        schema_path.write_text(json.dumps(SCHEMA, indent=2) + "\n", encoding="utf-8")
        status_path = run / "status.json"
        status_path.write_text(json.dumps({"state": "running"}) + "\n", encoding="utf-8")
        command = ["codex", "exec", "--sandbox", "read-only", "--ephemeral", "--json",
                   "--output-schema", str(schema_path), "-o", str(run / "final.json"), PROMPT]
        try:
            with (run / "events.jsonl").open("xb") as stdout, (run / "stderr.log").open("xb") as stderr:
                process = subprocess.run(command, cwd=root, stdout=stdout, stderr=stderr, timeout=120, check=False)
            status = {"state": "exited", "exit_code": process.returncode}
        except subprocess.TimeoutExpired:
            status = {"state": "timeout"}
        except OSError:
            status = {"state": "launch_failed"}
        status_path.write_text(json.dumps(status) + "\n", encoding="utf-8")
    result = verify(run)
    # Output data only: never execute text returned by the model.
    print(json.dumps(result, ensure_ascii=False))


if __name__ == "__main__":
    try:
        main()
    except (OSError, ValueError) as error:
        print(f"Not accepted: {error}", file=sys.stderr)
        sys.exit(1)

4단계: 실행하고 내용 확인

실습 폴더의 Windows PowerShell · powershell
py -3 --version
py -3 run_summary.py run-01
$practiceExit = $LASTEXITCODE
$practiceExit
실습 폴더의 macOS / Linux · sh
python3 --version
python3 run_summary.py run-01
practice_exit=$?
printf '%s\n' "$practice_exit"

Windows에 py가 없고 python --version이 Python 3이면 py -3를 python으로 바꿉니다. 버전 exec-practice-1, total 3, completed 1, pending 2인 객체와 종료 0을 기대하고 run-01의 다섯 파일을 확인하세요. 검증기는 필드 누락·추가, 음수, 정수 대신 true, 산술 불일치를 거부합니다. 그러나 3/2/1은 합이 맞아도 틀린 내용이므로 목록과 비교해야 합니다. schema 준수만으로 사실 정확성을 증명하지 못합니다.

5단계: 모델 사용량 없이 실패 세 가지 실습

파일 관리 화면에서 run-01 전체를 run-bad-fields로 복사하고 복사본 final.json의 pending만 삭제합니다. 아래 verify-only는 stderr에 Not accepted와 0이 아닌 종료를 보여야 합니다. 원본을 별도로 run-bad-events로 복사해 이벤트 객체 하나의 마지막 닫는 중괄호를 지운 후 해당 폴더를 검증하면 역시 실패해야 합니다. 세 번째 run-bad-status에서는 status.json의 exit_code만 7로 바꿉니다. final.json이 맞아도 거부되어야 합니다. 오류를 누적하지 말고 매번 변경 없는 원본에서 복사하세요.

Windows: Codex 호출 없이 복사본 검증 · powershell
py -3 run_summary.py run-bad-fields --verify-only
$LASTEXITCODE
macOS / Linux: 복사본만 검증 · sh
python3 run_summary.py run-bad-fields --verify-only
echo $?

확장: 형식은 통과하지만 내용은 틀림

원본 run-01을 비어 있는 이름 run-wrong-counts로 복사하고 final.json만 아래 객체로 바꿉니다. status와 events는 유지하고 해당 폴더에 verify-only를 실행하세요. 타입·버전·합계가 맞아 종료 0이지만 tasks.md와 비교하면 답을 거부해야 합니다. 이 검증기는 입력을 다시 읽거나 최종 파일과 이벤트 답의 일치를 확인하지 않으므로 일반적인 사실 검증기가 아닙니다.

run-wrong-counts/final.json: 의도적으로 틀린 개수 · json
{
  "revision": "exec-practice-1",
  "total": 3,
  "completed": 2,
  "pending": 1
}

구조 통과/내용 거부로 기록하고 원본 기록이나 후속 처리에 적용하지 않습니다. 미변경 run-01과 3/1/2를 다시 확인하세요. 다음 CI 글은 고정 실습값을 정확히 비교해 이 승인 조건을 실행 가능한 검사로 만듭니다.

복원·제한·다음 단계

복원을 위해 Codex에 JSON 수정을 요청하지 않습니다. 실패 복사본은 증거로 남기고 원본 run-01을 재검증하면 통과해야 합니다. 다음 실제 실행은 run-02를 사용하며 기존 run-01 덮어쓰기는 거부됩니다. launch_failed는 같은 터미널에서 codex --version을 확인하고 timeout은 를 읽은 뒤 재시도를 결정합니다. 래퍼는 한 번의 호출만 관리하며 호스트의 모든 자손 프로세스나 분산 실행 잠금은 관리하지 않습니다.

CLI 이벤트 종류가 늘 수 있어 프로그램은 알 수 없는 이벤트를 보존하며 고정 item 수로 성공을 정하지 않습니다. 미완료·실패 회차는 조사하도록 보수적으로 거부합니다. 한 회차를 검증하며 재개된 여러 회차 기록에 그대로 적용하지 않습니다. Windows·macOS·Linux는 같은 Python 소스를 쓰고 휴대전화는 로컬 스크립트를 실행하지 않습니다. Python/CLI 버전·입력 버전·파일·수동 개수를 기록한 뒤 로 진행하세요. 참고 검증은 합성 이벤트 시험과 실제 모델 출력을 구분하며 교육용 JSON을 실제 API 답변이라 부르지 않습니다.

직접 제작한 흐름도이며 제품 화면이 아닙니다.
직접 제작한 흐름도이며 제품 화면이 아닙니다. · 사진: Mokaair (© Mokaair)
자세한 설명 보기

Three numbered stages: identify the starting point, perform the exercise, and verify the result. Original illustration, not a product screenshot.

전체 목차

  • 라이프스타일

    Codex 학습 센터: 전체 튜토리얼 목차

    설치와 첫 작업부터 MD 지침과 고급 연동까지 60개 강의, 열 개 단원을 계획합니다. 수준, 환경, 목표, 명령으로 다음 글을 찾고 미게시 항목의 상태를 확인할 수 있습니다.

  • 라이프스타일

    Worktree와 작업 격리

    Worktree는 하나의 Git 저장소에 다른 브랜치의 작업 폴더를 만듭니다. 파일이 분리되어도 DB, 포트와 외부 서비스는 공유될 수 있습니다.

  • 라이프스타일

    실습: 작은 웹사이트 만들기

    brief.md에서 Small Steps 할 일 사이트를 계획하고 추가·완료·삭제·필터·로컬 저장을 구현합니다. HTML·CSS·데이터 함수·화면 이벤트·시험을 분리하고 Node와 브라우저로 검증한 뒤 재시작·복원 인계 기록을 남깁니다.

  • 라이프스타일

    사용량과 효율: 재작업 줄이기

    조건, 모델 선택, 시간, 결과를 기록해 불필요한 재시도와 과도한 문맥을 줄입니다.

최신 여행 소식·가이드

출처

라이프스타일