P29 비율 CSV의 명시적 percent / ratio 배율 계약
버전 1.0.0

목적
합성 입력 R1=10%/percent, R2=0.1/ratio를 모두 정확한 문자열 0.1로 변환한다.
R3=10/unknown은 배율을 알 수 없으므로 unknown_scale와 null로 보류한다.
값의 크기, % 문자만으로 배율을 추측하지 않는다. 실제 업무 데이터가 아니다.

실행 환경
코드는 Python 3.12 이상 표준 라이브러리만 사용한다. 추가 설치와 API 키가 필요 없다.
실제 검수는 Linux CPython 3.12.14에서 수행했다.
Windows/macOS/Excel의 실행과 화면·가져오기 동작은 시험하지 않았다.
하드 링크를 지원하는 일반 로컬 저장소와 개인 소유의 격리된 연습 폴더를 쓴다.
다른 프로그램이 파일이나 폴더를 동시에 바꾸지 않는 동안 한 작업씩 실행한다.

실행
ZIP을 새 빈 폴더에 직접 풀고 이 README가 있는 루트로 이동한다.
다음은 각각 별도 명령이다. 의도한 종료 1 때문에 &&로 이어 실행하지 않는다.
python --version
python tests/verify_checksums.py
python -m unittest discover -s tests -v
python tests/run_fixture_checks.py
python code/explicit_rates.py --input fixtures/main.csv --out main_result.json

main_result.json은 아직 없어야 한다. 출력 부모 폴더는 미리 있어야 한다.
main 실행은 HOLD 입력 3 / PASS 2 / HOLD 1 / 원본 쓰기 0, 종료 1이다.
expected/main.json과 모든 필드를 대조한다. source_sha256만 기대 JSON에서 제외했다.
해시는 현재 입력 파일 바이트에서 별도로 SHA-256을 계산해 대조한다.

추가 명령
python code/explicit_rates.py --input fixtures/normal.csv --out normal_result.json
python code/explicit_rates.py --input fixtures/boundary.csv --out boundary_result.json
python code/explicit_rates.py --input fixtures/holds.csv --out holds_result.json
python code/explicit_rates.py --input fixtures/recovery.csv --out recovery_result.json
normal=2 PASS/종료0; boundary=10 PASS/종료0; holds=25 HOLD/종료1; recovery=3 PASS/종료0.
recovery는 공급자가 R3의 percent 배율과 10% 표기를 확인했다는 가상 전제의 별도 사본이다.
프로그램이 unknown을 자동으로 고치지 않는다.

입력 계약
UTF-8 또는 시작 BOM, 쉼표 CSV, 헤더 id,raw_rate,scale 순서, 모든 레코드 세 열
1..10,000 데이터 레코드, 1,000,000바이트까지, CSV 필드 4,096자까지
ID는 ASCII 영문으로 시작, 영문/숫자/_/-만, 1..32자, 대소문자 구분, 중복 거부
scale는 정확한 percent 또는 ratio만. 빈 scale은 missing_scale, 그 밖은 unknown_scale
percent: 숫자 뒤 ASCII % 필수, 값 0..100; ratio: % 금지, 값 0..1
숫자 부분은 ASCII [0-9]+(?:\.[0-9]+)? 전체 일치, 앞자리0 허용
전체 숫자 최대24자리(앞자리0도 셈), 소수 최대18자리; 음수/부호+/지수/NaN/Inf/공백 불허
문자열 Decimal과 독립 localcontext precision32, Inexact trap. float/round/quantize/clamp 없음
canonical_ratio는 JSON 문자열(0.1은 따옴표 있음), 보류는 null; 원문 raw_rate와 scale 보존
보고서 raw_rate는 CSV 파서의 필드 값이다. CSV 구분용 따옴표 자체를 보존한 바이트는 아니다.
source_record는 헤더를1로 센 논리 CSV 레코드 번호이다.

종료코드
0 새 완성 JSON, 모든 행 PASS
1 새 완성 JSON, 하나 이상 HOLD. 원문·배율·사유를 확인할 것
2 옵션/파일/구조/경로/인코딩/읽기/저장 오류. 이번 실행의 새 최종 JSON 없음
기존 파일/폴더/끊어진 심볼릭 링크가 있으면 종료2이고 기존 내용을 유지한다.
재실행할 때 새 이름 main_second.json을 사용한다. 입력과 출력의 같은 경로도 거부한다.

실패와 복구
unknown_scale/missing_scale: 제공자가 확인한 배율을 사본에 명시할 때까지 계산 보류
missing_percent_suffix/unit_scale_conflict: 숫자를 바꾸기 전에 배율과 표기 계약의 모순 확인
out_of_range/negative_not_allowed: 0이나1로 맞추지 말고 업무 범위·음수의 의미 확인
rate_format/whitespace/digit_limit/fraction_limit: 원자료와 표기·정밀도 정책 확인, 조용한 절삭 금지
failures 폴더10개는 전체 오류, 원본 근거로 정정된 새 사본을 준비해 전체 재검사
저장 실패: 허용된 일반 로컬 폴더·여유 공간·하드 링크 지원 확인 후 새 이름으로 재시험

보존과 한계
입력은 읽기만 하고 새 JSON은 같은 출력 폴더 임시파일에 끝까지 저장·fsync·재읽기한다.
그 뒤 os.link로 새 최종 이름을 만든다. 기존 파일을 덮는 fallback은 없다.
심볼릭 링크·검출 가능한 reparse 표식·하드 링크 입력·.. 경로를 거부한다.
처리된 저장 오류에서는 새 최종 결과가 없다. 임시 정리까지 실패하면 .p29- 파일이 남을 수 있다.
확정 후 임시파일 정리만 실패하면 완성 결과를 보존하고 stderr 경고가 나온다.
강제 종료/정전/적대적 동시 경로 변경/동기화·네트워크 저장소를 보호하는 도구가 아니다.
원본 해시는 읽은 바이트의 지문이며 파일 잠금·지속 감시·작성자 인증이 아니다.
0..1 범위는 이 실습 계약이다. 증가율이나 모든 업무 비율이 항상 이 범위인 것은 아니다.

재검산
53 unittest와 CLI22건을 제공한다. fixture16개 전후 바이트, 독립 기대 JSON6개를 비교한다.
저장 실패·이름 선점·읽기 거부·정밀도 부족은 시험에서 통제해 만든 오류이다.
expected/hand_check.txt는 배율 나눗셈·최대 자릿수·복구 전제를 독립 명시한다.
SHA256SUMS.txt는 그 파일 자신을 제외한 배포 파일 목록/지문이다.
ZIP에는 코드/가상 입력/독립 기대값/시험/README/체크섬만 있다. 실제 출력과 검수 로그는 없다.

공식 기술 근거
https://docs.python.org/3.12/library/decimal.html
https://docs.python.org/3.12/library/csv.html
https://docs.python.org/3.12/library/os.html#os.link
공식 문서 확인일 2026-10-07 UTC. 3.12 계열 문서 표제3.12.15, 실행3.12.14는 구분한다.
