미완료라는 표시만으로 다음 일을 고르면 이미 확인한 자료를 다시 읽을 수 있다. 현재 자료와 이전 검토 기록을 비교해 새 검토·기존 후속·완료 기록을 나누는 독립 합성 함수를 공개한다.

현재 자료와 이전 검토 기록을 따로 받는다

이 함수는 실제 파일 대신 current와 records라는 두 합성 목록을 받는다. current에는 별칭 ID·수정 번호·선언한 SHA·현재 보류 이유를 넣는다. records에는 같은 ID의 검토 당시 수정 번호·SHA·전체 프레임 확인 표시·검토 상태를 넣는다. 자료가 지금 어떤 상태인지와 예전에 무엇을 확인했는지를 서로 다른 입력으로 남겼다.

각 목록은 64행까지이며 수정 번호는 양의 정수, 전체 확인 표시는 정확한 참·거짓이어야 한다. ID와 보류 이유는 짧은 ASCII 별칭만 받는다. 실제 사진·원고·주소·계정·개인 경로를 읽거나 자동 익명화하지 않는다. 공개 예시의 별칭을 실제 업체 자료로 착각하지 않도록 모두 독립 합성 값으로 작성했다.

새 검토가 필요한 이유를 먼저 돌려준다

이전 기록이 없거나 수정 번호가 달라졌거나 SHA가 다르면 new_inspections에 들어간다. 같은 수정 번호와 SHA가 있어도 전체 확인 근거가 없으면 새 검토 대상으로 남는다. 이유를 no_record, revision_changed, fingerprint_changed, inspection_evidence_missing으로 구별해, 담당자가 무엇부터 대조해야 하는지 읽을 수 있게 했다.

여기서 SHA는 전달받은 문자열을 비교한 것이다. 함수가 디스크의 현재 파일을 찾아 해시를 계산하지 않는다. 수정 번호를 새로 붙이거나 SHA만 옮겨 적어 실제 검토를 갱신할 수 있다고 설명하지 않았다. 신뢰할 수 있는 수집기에서 현재 파일을 고정하고 검토자가 근거를 남기는 과정이 이 함수 앞에 있어야 한다.

검토한 보류 자료는 후속 목록에 남긴다

현재 수정 번호·SHA·전체 확인 근거가 맞는 보류나 follow_up 기록은 existing_followups로 간다. 새로 봐야 할 목록에서 빠지더라도 기록과 보류 이유를 잃지 않는다. 현재 holds가 남아 있으면 과거 상태가 complete여도 완료 목록에 넣지 않는다. 이 조건은 새 합성 예제에서 명시한 정책이며 원래 선택기의 모든 필드와 상태를 그대로 옮긴 것은 아니다.

반대로 현재 보류 이유가 비었다고 과거 held 기록을 자동 완료로 승격하지도 않는다. 후속 조치가 끝났는지 기록을 다시 확인해야 한다. 이 함수에는 보류를 해제하는 명령이 없고 원래 입력을 고치지도 않는다. 새 검토를 고르는 목록과 보류를 해결하는 업무를 나눈 상태 계산기로 보면 된다.

코드와 구현 살펴보기

독립 합성 전체 구현

"""Independent synthetic workset; declared metadata, no I/O or authorization."""
from copy import deepcopy
import re

ALIAS = re.compile(r'[A-Za-z0-9_][A-Za-z0-9_.-]{0,47}\Z')


def _index(rows, recorded):
    if not isinstance(rows, list) or len(rows) > 64:
        raise ValueError('Expected at most 64 rows')
    required = {'id', 'revision', 'sha256'} | ({'full_frame_checked', 'status'} if recorded else {'holds'})
    indexed = {}
    for source in rows:
        if not isinstance(source, dict) or set(source) != required:
            raise ValueError('Unexpected or missing fields')
        row = deepcopy(source)
        if not isinstance(row['id'], str) or not ALIAS.fullmatch(row['id']):
            raise ValueError('Synthetic alias required')
        if row['id'] in indexed:
            raise ValueError('Duplicate id; preserve rejected evidence separately')
        if type(row['revision']) is not int or row['revision'] < 1:
            raise ValueError('Positive revision required')
        if not isinstance(row['sha256'], str) or not re.fullmatch(r'[0-9a-f]{64}', row['sha256']):
            raise ValueError('Lowercase SHA-256 declaration required')
        if recorded:
            if type(row['full_frame_checked']) is not bool or row['status'] not in ('complete', 'held', 'follow_up'):
                raise ValueError('Explicit inspection evidence and known status required')
        else:
            holds = row['holds']
            if not isinstance(holds, list) or len(holds) > 8:
                raise ValueError('At most eight hold aliases required')
            if any(not isinstance(x, str) or not ALIAS.fullmatch(x) for x in holds) or len(holds) != len(set(holds)):
                raise ValueError('Unique hold aliases required')
        indexed[row['id']] = row
    return indexed


def plan(current, records):
    current, records = _index(current, False), _index(records, True)
    inspections, followups, complete = [], [], []
    for key, item in sorted(current.items()):
        previous = records.get(key)
        if previous is None:
            reason = 'no_record'
        elif previous['revision'] != item['revision']:
            reason = 'revision_changed'
        elif previous['sha256'] != item['sha256']:
            reason = 'fingerprint_changed'
        elif not previous['full_frame_checked']:
            reason = 'inspection_evidence_missing'
        else:
            reason = None
        if reason:
            inspections.append({'id': key, 'reason': reason, 'holds': item['holds']})
        elif previous['status'] == 'complete' and not item['holds']:
            complete.append(key)
        else:
            followups.append({'id': key, 'recorded_status': previous['status'], 'holds': item['holds']})
    return {'new_inspections': inspections, 'next_inspection': inspections[0] if inspections else None,
            'existing_followups': followups, 'complete_ids': complete,
            'historical_only_record_ids': sorted(set(records) - set(current)),
            'records_authenticated': False, 'performs_execution': False,
            'changes_holds': False, 'goal_complete': False}

다음 항목이 없어도 전체 목표는 끝나지 않는다

complete_ids에는 현재 조건과 맞고 보류 없는 완료 기록만 들어간다. 현재 자료에서 빠진 과거 ID는 historical_only_record_ids로 남긴다. 이것은 삭제 목록이 아니다. 현재 범위에서 더 볼 자료가 없더라도 후속 조치, 빠진 범위, 운영자의 확인이 남아 있을 수 있어 goal_complete는 항상 false로 둔다.

next_inspection은 별칭 ID 순서의 첫 항목이다. 같은 입력의 순서를 바꿔도 결과를 재현하기 위한 작은 예제의 규칙이며, 업무 가치나 마감·위험 순위를 최적화한 결과가 아니다. 실제 프로그램에는 우선순위·담당자·기한을 별도로 연결할 수 있다. 선택 순서가 곧 실행 권한이 되지 않도록 경계를 유지해야 한다.

시험과 다섯 기록을 함께 재현한다

29개 시험 메서드는 같은 지문의 새 수정 번호, 같은 수정 번호의 바뀐 지문, 현재 보류와 과거 완료의 충돌, 전체 확인 표시 누락과 과거 기록 보존을 다룬다. 중복 ID·잘못된 형식·모르는 필드·상한 초과도 거절한다. 입력을 보존하고 출력의 목록을 고쳐도 입력에 영향이 없는지 확인했다. 하위 조합을 별도 시험 수로 더하지 않았다.

다섯 기록은 이 함수를 로컬 Python으로 실행한 결과다. 아래 선택기는 저장된 JSON만 보여 주며 방문자 파일을 검사하거나 브라우저에서 Python을 실행하지 않는다. ZIP을 새 폴더에 풀어 unittest와 make_records.py를 실행하면 같은 계약을 확인할 수 있다. 원래 프로그램의 24개 시험 기록과 이 독립 예제의 29개 시험은 서로 다른 검증이다.

합성 로컬 기록의 읽기 뷰어

실제 운영에는 파일 고정과 실행 관리가 더 필요하다

압축을 푼 폴더에서 workset의 plan 함수를 가져와 두 합성 목록을 넘기면 된다. 새 검토 목록의 reason과 기존 후속 목록의 holds·recorded_status부터 읽으면 된다. 검사 범위나 보류 이유를 시스템에 맞춰 늘릴 때는 기록이 의미하는 실제 확인을 먼저 정한다. 표시를 참으로 만드는 코드가 검토 자체를 대신해서는 안 된다.

이 패키지는 파일 수집·해시 계산·인증·잠금·지속 저장·실행·발행을 제공하지 않는다. records_authenticated, performs_execution, changes_holds와 goal_complete는 false다. 사진을 어디에 공개할지 계산하려면 아래의 기존 갤러리·전후 비교 모듈을 별도로 볼 수 있다. 두 모듈의 자동 연결을 구현했다고 주장하지 않고 각각의 책임을 남겼다.

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