작업이 끝나지 않았는데 완료처럼 읽히는 문장은 프로그램을 잘못 쓰게 할 수 있다. 기능 등록과 실제 실행, 전달과 결과 확인은 다른 사건이었다. 북극성에서 정리한 상태 구분을 바탕으로 선언된 합성 상태를 한영 문구로 바꾸는 독립 모듈을 만들었다.
문구를 만들기 전에 상태의 뜻을 고정한다
status는 등록·준비·실행·대기·확인 필요·성공·실패·중지 중 하나다. delivery와result·receipt를 따로 받는다. 등록이나 준비인데 이미 전달됐다고 선언하면 거절한다. succeeded에는 전달·맞는 결과·마지막 기록 조건이 모두 필요하다.
이 함수가 실제 실행기를 읽어 사실을 입증하는 것은 아니다. 입력된 상태의 모순을 거절하고 그 선언에 맞는 문구를 고르는 역할이다. original_evidence_verified는 항상 False다. 불리언을 받았다고 원래 사이트 결과까지 인증한 것처럼 쓰지 않는다. 운영에 연결하려면 어떤 주체가 상태를 공급하고 어떤 검증을 마쳤는지 별도 계약이 필요하다.
등록과 대기에 완료라는 말을 빌려주지 않는다
등록 문구는 정의가 저장됐으며 아직 실행하지 않았다고 말한다. 준비는 아직 전달하지 않은 입력 상태다. 실행 중과 결과 대기는 기존 실행을 읽으라는 다음 행동을 제공한다. 버튼이 생겼거나 응답을 기다린다는 사실에 완료라는 라벨을 붙이지 않는다.
결과 확인 필요도 같은 요청을 다시 보내라는 안내가 아니다. 기존 전달과 결과를 먼저 읽도록 한다. 이 예제는 resend_authorized를 어떤 상태에서도 True로 바꾸지 않는다. 문구가 새로운 실행 권한을 만들지 않도록 한 것이다. 실제 새 요청을 할지는 현재 자료와 사용자의 의도, 전달 상태를 확인한 별도 결정이어야 한다.
중지 문구에는 전달 경계가 남는다
전달 전 중지는 전달 전에 멈췄다고 쓴다. 전달 뒤에는 이미 전달한 행동을 되돌리지 않았다고 말하고, 전달 여부가unknown이면 아직 확인되지 않았다고 알린다. 모두 중지됨이라는 제목을 가져도 본문이 남은 효과의 차이를 보여 준다.
관찰 실행기가 중지된 뒤 결과가 나중에 확인되는 경우도 있다. cancelled에 맞는 결과 선언을 함께 줄 수 있지만 원래 상태를 성공으로 바꾸지 않는다. needs_review도 후속 맞는 관찰을 받았다고 자동 해제하지 않는다. 이 함수는 상태 전이기가 아니다. 글자를 바꾸는 기능이 운영 기록의 이력을 고치지 못하게 역할을 작게 뒀다.
코드와 구현 살펴보기
독립 합성 전체 구현
"""Localize a declared synthetic state. Does not execute or certify any action."""
STATES=frozenset({'registered','prepared','running','waiting','needs_review','succeeded','failed','cancelled'})
DELIVERY=frozenset({'none','sent','unknown'})
RESULTS=frozenset({'unchecked','match','mismatch','unavailable'})
RECEIPTS=frozenset({'missing','recorded'})
COPY={
'ko':{
'registered':('등록됨','기능 정의가 저장됐습니다. 아직 실행하지 않았습니다.','이번 입력과 대상을 확인하세요.'),
'prepared':('실행 준비','입력이 준비됐습니다. 아직 전달하지 않았습니다.','이번 작업의 실행 범위를 확인하세요.'),
'running':('실행 중','기존 실행이 진행 중입니다.','같은 실행의 상태를 읽으세요.'),
'waiting':('결과 대기','기존 실행의 결과를 기다리고 있습니다.','기존 결과를 읽으며 기다리세요.'),
'needs_review':('결과 확인 필요','완료 근거가 확정되지 않았습니다.','기존 전달과 결과를 먼저 읽어 확인하세요.'),
'succeeded':('결과 확인됨','선언된 결과 확인과 마지막 기록 조건이 맞습니다.','확인한 결과를 살펴보세요.'),
'failed':('전달 전 실패','전달하기 전에 실행이 끝났습니다.','실패 단계와 현재 입력을 확인하세요.'),
'cancelled':('중지됨','실행을 멈췄습니다.','현재 남아 있는 결과를 확인하세요.'),
'history':'이전 실행 · ',
'cancel_none':'전달 전에 실행을 멈췄습니다.',
'cancel_sent':'실행을 멈췄습니다. 이미 전달한 행동은 되돌리지 않았습니다.',
'cancel_unknown':'실행을 멈췄습니다. 전달 여부는 아직 확인되지 않았습니다.',
},
'en':{
'registered':('Registered','The capability definition is saved. It has not run.','Check the current inputs and target.'),
'prepared':('Prepared','Inputs are prepared and have not been delivered.','Check the requested execution scope.'),
'running':('Running','The existing execution is progressing.','Read the same run status.'),
'waiting':('Waiting for result','The existing execution is awaiting its result.','Wait by reading the existing result.'),
'needs_review':('Result needs review','Completion evidence is not settled.','Read the original delivery and result first.'),
'succeeded':('Result checked','The declared result and final-record conditions match.','Inspect the checked result.'),
'failed':('Failed before delivery','Execution ended before delivery.','Check the failed stage and current inputs.'),
'cancelled':('Stopped','Execution was stopped.','Inspect the outcome that remains.'),
'history':'Previous run · ',
'cancel_none':'Execution stopped before delivery.',
'cancel_sent':'Execution stopped. Previously delivered actions were not undone.',
'cancel_unknown':'Execution stopped. Delivery is still unconfirmed.',
}}
def describe(*,status,delivery='none',result='unchecked',receipt='missing',history=False,lang='ko'):
for value,allowed in [(status,STATES),(delivery,DELIVERY),(result,RESULTS),(receipt,RECEIPTS),(lang,frozenset(COPY))]:
if type(value) is not str or value not in allowed:raise ValueError('INVALID_STATE')
if type(history) is not bool:raise ValueError('HISTORY_BOOLEAN_REQUIRED')
if status in {'registered','prepared'} and (delivery!='none' or result!='unchecked' or receipt!='missing'):raise ValueError('PREPARATION_CONTRADICTION')
if result in {'match','mismatch'} and delivery!='sent':raise ValueError('RESULT_WITHOUT_DELIVERY')
if receipt=='recorded' and not(delivery=='sent' and result=='match'):raise ValueError('RECEIPT_WITHOUT_MATCH')
if status=='succeeded' and not(delivery=='sent' and result=='match' and receipt=='recorded'):raise ValueError('SUCCESS_UNPROVEN')
if status=='failed' and delivery!='none':raise ValueError('DELIVERED_FAILURE_NEEDS_REVIEW')
if status in {'running','waiting'} and (result!='unchecked' or receipt!='missing'):raise ValueError('ACTIVE_RESULT_CONTRADICTION')
c=COPY[lang];label,message,next_step=c[status]
if status=='cancelled':message=c['cancel_'+{'none':'none','sent':'sent','unknown':'unknown'}[delivery]]
return dict(status=status,label=(c['history'] if history else '')+label,message=message,next_step=next_step,
aria_live='off' if history else 'polite',history=history,lang=lang,
state_changed=False,action_executed=False,resend_authorized=False,
original_evidence_verified=False,localization_scope='declared_synthetic_state')
과거 실패를 현재 진행처럼 읽히게 하지 않는다
history가 True이면 이전 실행이라는 접두어를 붙이고 aria_live를 off로 둔다. 현재 상태와 같은 문구를 반복 발표하지 않게 하는 제안이다. history 여부는 호출자의 선언이며 실제 기록의 시각과 작업 ID를 자동 대조하는 기능은 없다.
한국어와 영어 문구를 함께 제공하고 원문 error_message는 받지 않는다. 실제 로그를 자동 번역하거나 민감한 값을 지워 주는 도구가 아니다. 독립 시험28개에는 모순된 완료 선언, 중지 경계, 과거 표시와 반환값 독립성을 넣었다. 다섯 기록은 실제 로컬 함수 실행에서 저장했다. ZIP 추출 뒤에도 시험하지만 실제 스크린리더나 렌더링 화면의 접근성 검수를 했다는 뜻은 아니다.
다른 업무에서는 상태 계약부터 다시 맞춘다
코드·시험·기록 생성기는 수정하고 재사용할 수 있다. 실제 주문·발행·ERP 업무에 붙이려면 전달과 확인, 기록의 의미를 그 업무에 맞춰 정해야 한다. 원래 앱의 상태 이름이 같다는 이유만으로 이 합성 계약에 그대로 대입하면 안 된다.
예제는 네트워크·파일·인증·실행·수리·영속 기록을 다루지 않는다. state_changed와 action_executed도 항상 False다. 각 상태를 구분해 사용자가 다음에 확인할 일을 알 수 있게 해야 한다. 나는 친절한 문장을 만들기 위해서도 먼저 완료의 근거와 중지 뒤 남는 효과를 정확히 설명해야 한다고 생각한다. 그 기준까지 함께 공유한다.