입력이 실패한 이유를 설명하려고 원고와 화면 전체를 넘길 필요는 없었다. 어떤 단계에서 어떤 종류의 컨트롤이 막혔는지를 더 좁게 남길 수 있다. 북극성의 구조 진단에서 출발해 원문 필드를 아예 받지 않는 독립 합성 보고서 모듈을 만들었다.

임의 로그를 지워 주는 도구는 아니다

이 예제는 로그 문자열이나 HTML을 받아 민감한 부분을 알아서 익명화하지 않는다. 허용한 구조만 받는 새 계약이다. event에 message·url·path·account를 추가하거나 노드에 value·name·placeholder 원문을 넣으면 거절한다. 조용히 일부를 빼고 나머지를 안전하다고 반환하지 않는다.

단계·함수 별칭·오류 코드는 고정된 예제 목록에서 고른다. 실제 원본 경로나 작업 ID도 넣지 않는다. 허용 목록으로 필드를 줄였다는 사실이 어떤 공개 자료든 자동 안전하다는 보장은 아니다. 실제 자료를 연결할 때는 공급 단계와 최종 편집 검토가 여전히 필요하며 safe_to_publish_verified는 항상 False다.

컨트롤은 모양과 상태만 작게 남긴다

노드는 제한된 tag, 편집 가능 여부, 라벨 속성의 존재, 폭·높이와 조상의 visible·hidden·inert boolean을 받는다. 라벨이 있다는 사실과 라벨 글자를 보내는 일은 다르다. 조상은 여섯 개 이하, 노드는 여덟 개 이하로 묶는다. 호출자가 sensitive로 선언한 노드는 결과에서 제외하고 개수만 남긴다.

폭과 높이는 정수 범위를 검사한 뒤 8픽셀 단위로 양자화하고 최대 4096으로 표시한다. 원래 진단의 정확한 측정과 다른 독립 예제의 선택이다. 화면 구조 자체도 정보를 줄 수 있으므로 이런 축약을 완전한 익명성 보장이라고 부르지 않는다. 비밀번호와 OTP를 실제로 찾아내는 기능도 이 코드에는 없다.

원래 진단의 바깥 봉투까지 안전하다고 하지 않는다

원래 probeInput은 고정된 함수로 실제 입력 노드의 구조를 읽고 값·텍스트·접근성 이름을 반환하지 않는다. 비밀번호와 민감한 autocomplete, 다른 문서의 노드를 제외한다. 이 작은 함수의 범위는 실제 코드와 시험에서 읽을 수 있다.

하지만 ControlProbe의 프레임 봉투에는 origin이 있고 더 넓은 RepairDiagnostic에는 URL이나 message 같은 필드가 있다. 노드 함수가 원문을 빼므로 전체 진단도 그대로 공개해도 된다고 추론하면 틀린다. 새 모듈이 원래 봉투를 통째로 받지 않는 이유다. 구조 수집, 내부 조사, 공개 편집은 서로 다른 경계로 남겼다.

코드와 구현 살펴보기

독립 합성 전체 구현

"""Strict structural synthetic reports, not arbitrary-log redaction or repair."""
from copy import deepcopy

STAGES={'lookup':'resolve_target','prepare':'prepare_input','dispatch':'send_native_input','verify':'verify_input'}
CODES={'TARGET_MISSING','INPUT_UNSUPPORTED','TARGET_COVERED','DOCUMENT_CHANGED','OUTCOME_UNKNOWN','INPUT_MISMATCH','CONTEXT_LIMIT'}
NODE_KEYS={'tag','editable','width','height','label_present','sensitive','parents'}


def exact(value,keys):
    if type(value) is not dict or set(value)!=set(keys):raise ValueError('EXACT_STRUCTURAL_FIELDS_REQUIRED')


def boolean(value):
    if type(value) is not bool:raise ValueError('BOOLEAN_REQUIRED')
    return value


def dimension(value):
    if type(value) is not int or not 0<=value<=32768:raise ValueError('DIMENSION_INVALID')
    return min(4096,((value+4)//8)*8)


def node(raw):
    exact(raw,NODE_KEYS)
    if raw['tag'] not in ('input','textarea','div','select','button'):raise ValueError('TAG_NOT_ALLOWED')
    for key in ('editable','label_present','sensitive'):boolean(raw[key])
    width,height=dimension(raw['width']),dimension(raw['height'])
    parents=raw['parents']
    if type(parents) is not list or len(parents)>6:raise ValueError('PARENT_LIMIT')
    clean=[]
    for p in parents:
        exact(p,{'visible','hidden','inert'})
        clean.append({k:boolean(p[k]) for k in ('visible','hidden','inert')})
    if raw['sensitive']:return None
    return {'tag':raw['tag'],'editable':raw['editable'],'width':width,'height':height,
            'label_present':raw['label_present'],'parents':clean}


def report(raw):
    exact(raw,{'stage','code','function','outcome_uncertain','nodes'})
    stage=raw['stage']
    if type(stage) is not str or stage not in STAGES or raw['function']!=STAGES[stage]:raise ValueError('STAGE_FUNCTION_INVALID')
    if type(raw['code']) is not str or raw['code'] not in CODES:raise ValueError('ERROR_CODE_NOT_ALLOWED')
    boolean(raw['outcome_uncertain'])
    if type(raw['nodes']) is not list or len(raw['nodes'])>8:raise ValueError('NODE_LIMIT')
    converted=[node(n) for n in raw['nodes']]
    return {'stage':stage,'code':raw['code'],'function':raw['function'],'outcome_uncertain':raw['outcome_uncertain'],
            'nodes':[n for n in converted if n is not None],'excluded_sensitive_nodes':sum(n is None for n in converted),
            'automatic_replay':False,'source_fix_verified':False}


def bundle(events):
    if type(events) is not list or not 1<=len(events)<=20:raise ValueError('EVENT_LIMIT')
    reports=[report(e) for e in events]
    return {'version':1,'reports':deepcopy(reports),'events':len(reports),
            'stage_counts':{s:sum(r['stage']==s for r in reports) for s in STAGES},
            'safe_to_publish_verified':False,'scope_identity_authenticated':False,
            'automatic_replay':False,'arbitrary_log_redaction':False}

오류 분류가 재전송 허용으로 바뀌지 않게 한다

보고서에는 lookup·prepare·dispatch·verify 같은 예제 단계와 오류 코드, 결과 불확실 여부가 남는다. 함수 별칭은 단계와 맞아야 하며 임의 문장을 함수 이름으로 끼워 넣지 못한다. 단계별 개수는 조사한 사건의 수이지 성공한 기능 수가 아니다.

OUTCOME_UNKNOWN을 기록해도 이 모듈이 기존 서버 결과를 확인하거나 다시 제출하지 않는다. automatic_replay와 source_fix_verified는 항상 False다. 원래 내부 자동수리도 이미 제거된 역사이며 이 공유 코드가 복구한 기능이 아니다. 진단이 자세해졌다는 이유만으로 수정이나 재실행까지 허용하지 않는 계약을 필드로 보여 준다.

서른두 시험에는 거절한 자료도 포함한다

원문 필드와 임의 오류·함수·태그를 거절하는지, boolean 자리에 문자열이 들어오는지, 부모와 노드·이벤트 한도를 넘기는지 시험한다. 민감 노드 제외와 크기 양자화, 결과 사본의 독립성도 확인한다. 거절 오류 문장에 입력했던 민감한 문자열을 그대로 되풀이하지 않는 시험도 있다.

독립 원본 32개와 ZIP 추출본의 같은 32개 시험을 실행한다. 다섯 기록은 실제 로컬 함수 호출로 만들었고 아래 선택기는 그 결과만 읽는다. 실제 고객의 오류 로그를 수집하거나 원래 브라우저에서 진단을 실행한 검수가 아니다. 시험 입력이 합성이라는 점과 검증한 범위를 함께 남겼다.

합성 로컬 기록의 읽기 뷰어

공유할수록 받아들이는 입력을 더 분명히 한다

코드·시험·기록 생성기와 설명은 수정하고 재사용할 수 있다. 운영에서는 실제 관찰과 사건의 소속을 안전하게 연결하고 무엇을 공개할지 다시 판단해야 한다. 이 모듈은 신원 인증, 영속 원장, DOM 진단, 수리 패치나 모델 호출을 구현하지 않는다.

누군가 원문 로그를 그대로 이 함수에 넣으면 정상 입력이 아니라 거절 대상이다. 진단이 유용하려면 모든 것을 모으는 것보다 오류를 설명하는 데 필요한 구조가 무엇인지 먼저 정해야 한다. 나는 공개용 작은 계약의 한계까지 함께 남기는 방식으로 실제 제작의 조사 원칙을 공유한다.

코드·시험·실행 기록 ZIP 받기
Python 표준 라이브러리 · 합성 자료 · 수정과 재사용 가능