제4장

제4장: SDK와 CLI — 레코드 생산·검증 API

WIA-BIO-014의 참조 SDK와 명령줄 도구를 오직 "레코드를 생산하고 점검하는 서비스"로 읽는다. 요청·응답 계약, 오류 코드, 그리고 모든 호출에 기관이 덧붙여야 하는 출처 정보를 다룬다.

제2장과 제3장은 프로토콜 레코드가 무엇을 담으며 어떻게 직렬화되고 식별되는지를 다루었다. 이 장이 다루는 것은 그 레코드를 실제로 만들어 내고 점검하는 기계, 즉 @wia/bio-014 패키지로 배포되는 참조 TypeScript SDK(CRISPRProtocolSDK 클래스)와 참조 셸 도구 wia-bio-014이다. 관점은 의도적으로 고정되어 있고 어느 절에서도 바뀌지 않는다. 이 표면의 모든 진입점은 레코드를 생산하거나 레코드를 점검하는 서비스로 취급하며, 각각에 대해 오직 세 가지만 묻는다. 무슨 레코드를 입력으로 받는가, 무슨 레코드를 출력하는가, 그리고 그 레코드가 1년 뒤 그 자리에 없었던 심사자 앞에서도 방어될 수 있으려면 어떤 출처 정보가 함께 따라가야 하는가.

그 레코드 안의 숫자가 어떻게 계산되는지는 과학의 질문이다. 제1장 §1.3에서 밝힌 대로 본서는 설계·점수 산출 방법론을 재현하지 않는다. 응답 필드가 그런 숫자를 담고 있을 때 본서가 기술하는 것은 필드 자체 — 이름, 타입, 개수, 반드시 동반되어야 하는 출처 — 뿐이며, 그 값의 해석은 연구책임자와 기관생물안전위원회(IBC)·기관생명윤리위원회(IRB)에 맡긴다.

4.1 SDK 표면 — 클래스 하나, 함수 다섯, 기억 없음

참조 SDK는 클래스 하나와 모듈 수준 함수 다섯 개를 노출한다. CRISPRProtocolSDK 클래스는 getVersion()과 함께 다섯 개의 실동작 메서드 designGuideRNA(), predictOffTargets(), calculateEditingEfficiency(), validateProtocol(), simulateEditing()을 가진다. 클래스와 나란히 모듈은 독립 함수 designGuideRNA, predictOffTargets, calculateEditingEfficiency, validateCRISPRProtocol, simulateCRISPREditing을 내보내며, 이들은 각각 새 SDK 인스턴스를 만들어 대응 메서드에 위임한다. 기관이 자체 래퍼를 짜기 전에 짚어 둘 이름 불일치가 두 곳 있다. 클래스 메서드는 validateProtocol인데 독립 함수는 validateCRISPRProtocol이고, 클래스 메서드는 simulateEditing인데 독립 함수는 simulateCRISPREditing이다. 한쪽 이름으로 짠 코드와 다른 쪽 이름으로 쓴 문서는 조용히 어긋난다.

인스턴스가 보유하는 것은 버전 문자열과 초기화 플래그뿐이다. 세션도, 연결도, 캐시도, 호출을 넘어 살아남는 상태도 없다. 기록 체계의 관점에서 이 한 가지 성질은 좋은 소식이자 동시에 나쁜 소식이다. 이 표면은 동시 호출에 안전하고 숨은 순서 효과가 없다. 그러나 같은 이유로 SDK는 누가 호출하는지, 이것이 어떤 요청인지, 호출자가 어느 버전의 프로토콜을 다루고 있다고 믿는지 알지 못하며, 어디에도 아무것도 쓰지 않는다. 그 사실 하나하나가 기관의 레코드 저장소가 채워야 하는 필드다.

표 4-1. 참조 SDK 표면을 레코드 서비스로 읽기
진입점 입력 레코드 출력 레코드 예외 출력 외에 호출자가 반드시 저장할 것
getVersion()—버전 문자열—없음. 단 이 문자열은 아래 모든 봉투에 들어가야 한다
designGuideRNA()GuideRNARequestGuideRNAC004요청 전문, 도구 버전, 행위자, 타임스탬프 출처
predictOffTargets()OffTargetRequestOffTargetSite[]C004탐색 파라미터, 구현체 신원, 행 수, 절단 여부
calculateEditingEfficiency()EfficiencyRequestEfficiencyResponseC007두 분모 전부, 라벨을 만든 임계값 설정
validateProtocol()ProtocolValidationValidationResult—요청 플래그 3개, 검증된 프로토콜 버전의 다이제스트, 도구 버전
simulateEditing()CRISPRProtocolEditingSimulation—멱등 키, 명시적 "예측" 라벨, 도구 버전

이 표의 마지막 열이 이 장의 본론이며, 모든 절에 되풀이되는 하나의 구조를 요구한다. 바로 증거 봉투(evidence envelope)다. 봉투는 기관이 실제로 저장하는 레코드이고, SDK 응답은 그 적재물일 뿐이다. 봉투는 최소한 적재물을 만든 도구의 이름과 버전, 입력의 정규화 스냅샷 또는 다이제스트, 그 호출이 누구의 권한으로 이루어졌는지를 나타내는 행위자 신원, 타임스탬프의 출처, 그리고 적재물의 의미를 바꾼 요청 플래그를 담는다. SDK 안의 어떤 것도 봉투를 만들지 않는다. 기관이 만들지 않으면 저장된 응답은 이력 없는 숫자로 남고, 이력 없는 숫자는 방어할 수 없다.

predictOffTargets()의 출력을 어떻게 저장해야 하는지를 결정하는 사실. 참조 구현에서 이 메서드는 시연용 스텁이다. 소스 주석 자체가 실제 구현이라면 유전체 데이터베이스를 탐색할 것이며 시연을 위해 모의 부위를 생성한다고 적고 있고, 행은 generateMockCoordinates·generateMockAnnotation·generateOffTargetSequence라는 이름의 헬퍼가 채우며 불일치 수와 염색질 상태는 Math.random()에서 나온다. 여기서 두 가지 결론이 따라오고, 둘 다 견해의 문제가 아니다. 첫째, 출력은 재현 불가능하다. 바이트까지 같은 입력으로 두 번 호출하면 다른 행이 돌아오므로, 과거 레코드를 되짚기 위해 호출을 재생할 수 없다. 둘째, 이 행들은 훗날의 독자가 측정값 또는 실제 유전체 전역 탐색의 결과로 오해할 수 있는 어떤 필드에도 저장되어서는 안 된다. 이 인터페이스를 채택하는 기관은 같은 계약 뒤에 실제 예측기를 놓고, 행 묶음마다 어느 구현체가 그것을 만들었는지 기록해야 한다.

4.2 요청·응답 계약

다섯 호출 중 셋은 표준의 다른 어디에도 존재하지 않는 레코드를 주고받는 요청–응답 계약이다. 아래에서 이들을 레코드로서 필드 단위로 기술한다. CRISPRProtocol 전체를 다루는 나머지 두 호출은 §4.3과 §4.4에서 따로 다룬다.

4.2.1 GuideRNARequest → GuideRNA

요청은 표적 서열 문자열, PAMType 통제 어휘의 PAM 종류, Organism 통제 어휘의 생물종, 그리고 선택 위치 필드 다섯(염색체, 위치, 가닥, 유전자 이름, 사용자 지정 유전체 참조)을 담는다. 응답은 서열과 PAM 문자열, 둘을 합친 전체 표적 문자열, 네 개의 숫자(온타깃 점수, GC 함량 백분율, 예측 효율 백분율, 특이도 점수), 경고 문자열 목록, 그리고 선택 구조 둘(유전체 좌표, 예측 오프타깃 부위 목록)을 담는다.

레코드 규칙은 한 줄로 말할 수 있지만 현장에서 가장 자주 깨진다. 응답의 네 숫자는 그것을 만든 구현체에 의해서만 척도와 의미가 정해진다. 어느 것도 단독으로 저장되면 의미가 없다. 레지스트리에 "온타깃 점수 0.72"만 적혀 있다면 그것은 가이드에 관한 사실이 아니라 아무도 기록하지 않은 프로그램 실행에 관한 사실이다. 요청과 응답은 getVersion()이 준 도구 버전과 함께 하나의 봉투에 담아 같이 저장한다.

이 계약에는 함정이 두 군데 있다. 첫째는 warnings: string[]이다. 이들은 사람이 읽도록 쓰인 문장이며 코드가 아니다. 집계·필터·기관 간 비교가 불가능하고, 적재물 안에 아무 버전 신호도 없이 도구 버전에 따라 문구가 바뀔 수 있다. 기관은 접수 시점에 각 경고를 자체 코드 어휘로 사상(寫像)하고 원문 문자열을 그 옆에 원형 그대로 보존해야 한다. 그러면 훗날의 독자가 도구가 실제로 한 말과 기관이 그것을 어떻게 분류했는지를 둘 다 볼 수 있다. 둘째 함정은 coordinates.assembly다. 참조 구현에서 이 값은 호출자가 준 것이 아니라 organism 필드로부터 내부 헬퍼가 파생한 것이다. 즉 저장된 레코드에는 제출자가 말한 적 없는 어셈블리 라벨이 들어간다. 적합한 레코드는 제출자 자신의 어셈블리 선언을 권위 있는 필드로 유지하고 파생 라벨은 도구 산출물로 취급한다. 둘이 일치하는지 보는 참조 검사는 제3장 §3.4의 검증 계층에 속한다.

4.2.2 OffTargetRequest → OffTargetSite[]

요청은 가이드 서열 문자열, 유전체 어셈블리 문자열, 최대 불일치 정수, 그리고 탐색 범위를 넓히거나 좁히는 선택 불리언 넷(DNA 벌지 포함, RNA 벌지 포함, PAM 변이 포함, 염색질 접근성 필터)을 담는다. 반환되는 각 행은 유전체 좌표, 서열 문자열, 불일치 수, 불일치 위치 배열, 선택 벌지 수, 공개된 점수법의 이름을 딴 점수 둘, 주석 구조, 선택 염색질 상태, 그리고 예측 활성 백분율을 담는다. 주석 구조 자체는 여섯 항 통제 어휘(exon·intron·promoter·intergenic·UTR·enhancer)의 영역 값과, 선택 항목인 유전자 이름·유전자 식별자·전사체 식별자·자유 서술 결과 필드를 가진다.

행 묶음은 탐색 파라미터의 함수이므로, 파라미터 없이 저장된 행 묶음은 해석이 불가능하다. 같은 가이드를 다른 최대 불일치 값으로 탐색하면 행 수가 정당하게 달라지고, 행만 본 독자는 좁은 탐색과 깨끗한 유전체를 구별할 수 없다. 여기서 따라오는 규칙은 기록 체계에서 절대적이다. 서로 다른 파라미터로 만들어진 행 묶음은 결코 하나의 표로 합치지 않으며, 행 묶음은 그것을 만든 요청 없이 저장하지 않는다. 두 점수 필드는 공개된 방법의 이름을 따르고 있으므로, 레코드는 그 값을 만든 구현체와 빌드를 명시해야 한다. 본서는 어떤 점수 산출식도 재현하거나 지지하지 않는다.

이 계약은 자동화 파이프라인이 필요로 하는 세 가지를 빠뜨리고 있으며, 봉투가 그 셋을 모두 보충해야 한다. 응답은 총 개수가 없는 맨 배열이어서 절단된 결과와 완전한 결과를 구별할 수 없고, 실제로 탐색된 어셈블리를 되울려 주지 않아 요청의 오타를 응답만으로는 찾을 수 없으며, 선택 불리언 중 어느 것이 구현체에 의해 실제로 반영되었는지 알 수 없다.

4.2.3 EfficiencyRequest → EfficiencyResponse

이것은 표면에서 유일하게 호출자가 준 값만으로 계산되는 순수 산술 호출이며, 그래서 기록 규율을 정확하게 말할 수 있는 유일한 호출이다. 요청은 편집된 리드 수, 총 리드 수, EditType 어휘의 편집 유형, 선택 항목인 시퀀싱 방법 라벨을 담는다. 응답은 효율 백분율, 두 원소 신뢰구간, 세 값 품질 라벨, 표본 크기 충분성 불리언, 권고 문자열, 그리고 표준오차를 담은 선택 통계 객체를 담는다.

참조 구현은 백분율을 편집된 리드를 총 리드로 나눈 뒤 100을 곱해 계산하고, 구간은 그 비율 주위의 정규근사 구간으로 만든다. 비율의 표준오차와 양측 95 % 표준정규 분위수 1.96을 쓴다. 이 둘은 본 표준의 주장이 아니라 확립된 통계학적 사실이며, 출력을 저장하는 사람에게는 확립된 성질 두 가지가 더 중요하다. 정규근사는 총 수가 작거나 비율이 0 또는 1에 가까울 때 성질이 나빠진다는 것이 알려져 있고, 참조 구현은 구간 끝점을 0에서 100 사이로 잘라 넣는다 — 근사가 실패한 바로 그 경우를 가려 버리는 처리다. 따라서 저장된 구간에는 항상 원래의 개수가 동반되어야 한다. 독자가 잘린 두 숫자를 믿는 대신 스스로 다시 계산하거나 더 나은 구간을 계산할 수 있어야 한다.

이것이 제2장 §2.5에서 도입한 규칙을 API 계층에서 다시 말한 것이다. 계산된 백분율은 분모 없이 저장하지 않는다. 함수 자체는 입력 경계에서 그 역(逆)을 강제한다. 편집 리드가 총 리드보다 크면 거부하고 총 리드가 0이면 거부하며, 둘 다 오류 코드 C007이다. 이 비대칭은 시사적이다. 도구는 모순된 개수를 받아들이지 않지만, 호출자가 개수를 버리고 백분율만 저장하는 것은 아무렇지 않게 허용한다. 그 순간 이후로는 체계 안의 어떤 검증기도 그 값을 다시 점검할 수 없다.

품질 라벨과 충분성 불리언은 총 리드 수를 참조 구현에 하드코딩된 정수 임계값과 비교해 만들어진다. 제3장 §3.5는 그 상수들을 다루면서, 그것이 과학적 권고가 아니라 출처 없는·기관별로 설정 가능한 소프트웨어 기본값임을 분명히 밝힌다. API 계층에서는 규칙 하나면 된다. 라벨은 그것을 만든 임계값 설정과 함께 저장한다. 그러지 않으면 기본값이 한 번 바뀌는 순간 그 라벨은 감사 불가능해진다. 권고 문자열은 더 강한 처분을 받아야 한다. 이 문자열은 표본 크기 문장과, 편집 유형별 하드코딩된 "전형적" 효율과의 비교를 이어 붙여 만든다. 즉 파싱 불가능하면서 동시에 출처 없는 상수에 의존한다. 기관은 저장 레코드에서 이를 생략하거나, 도구와 버전을 명시한 불투명한 벤더 문구로만 저장해야 하며, 어떤 후속 보고서에서도 이를 기관의 판단으로 제시해서는 안 된다.

표 4-2. 명세 §8.2 인터페이스와 SDK 타입의 차이
요소 명세 §8.2 SDK types.ts 레코드 측 결과
설계 응답 타입 이름GuideRNAResponseGuideRNA같은 레코드를 문서와 코드가 서로 다른 이름으로 부른다
PAM 어휘세 값"없음" 항목을 포함해 더 넓은 PAMTypeSDK에서 유효한 값이 명세대로 만든 검증기에서 거부된다
오프타깃 위치평면 chromosome+position가닥·어셈블리를 가진 coordinates 객체명세 형태는 가닥·어셈블리를 담지 못한다 — 양방향 손실
불일치 표현위치를 담은 배열 필드 mismatchesmismatches 개수 + mismatchPositions 배열같은 필드명·다른 타입·다른 의미 — 조용한 손상 위험
오프타깃 주석단일 문자열영역 어휘를 가진 OffTargetAnnotation 객체질의가 가능한 것은 SDK 형태뿐. 문자열 형태는 이행 시 파싱이 필요하다
효율 응답네 필드충분성 불리언·통계 객체 추가명세만 구현한 소비자는 모르는 필드를 버린다(제3장 §3.6)
설계 응답의 오프타깃 목록필수선택부재가 중의적이다 — 요청하지 않았는가, 요청했는데 비었는가

실무적 결론은 이렇다. 구현자는 두 형태 중 어느 것을 따르는지 선언하고 둘 사이의 어댑터를 공표해야 한다. 본서는 SDK 형태를 레코드 형태로 권한다. 가닥·어셈블리·질의 가능한 주석을 담을 수 있는 쪽이 그것이기 때문이며, 명세 형태는 문서화된 — 한 방향으로는 무손실인 — 어댑터로 지원한다. JSON 문서와 SDK 타입 사이의 필드명 사상 표는 제3장 §3.1에 있으므로 여기서 되풀이하지 않는다.

4.3 validateCRISPRProtocol — 검증 실행을 증거로 만들기

검증 호출은 ProtocolValidation 레코드를 받는다. 프로토콜 하나와, 참조 구현에서 모두 true가 기본값인 선택 불리언 셋(윤리 점검, 규제 점검, 오프타깃 분석 수행)이다. 반환되는 ValidationResult는 유효성 불리언, 차단성 오류 문자열 배열, 비차단 경고 문자열 배열, 오프타깃 위험 수준·전체 위험 수준·준수 불리언 둘을 담은 안전 하위 객체, 숫자 점수 셋을 담은 품질 하위 객체, 권고 문자열 배열, 그리고 타임스탬프를 담는다.

이 레코드가 증거로 쓰일 수 있는지를 결정하는 성질이 셋 있고, 셋 다 참조 구현에서 눈으로 확인된다.

첫째, 유효성 불리언은 "오류 배열이 비었다"로 정의된다. 그러므로 그것은 프로토콜의 속성이 아니라 특정 설정으로 수행된 1회 실행의 요약이다. 같은 검증기가 같은 프로토콜을 다른 플래그로 두 번 실행하면 정당하게 서로 다른 답을 낼 수 있는데, 반환된 레코드만으로는 어느 설정이 어느 답을 냈는지 독자가 알 방법이 없다.

둘째, 두 준수 불리언은 프로토콜로도 충족되고 호출자가 점검을 꺼서도 충족된다. 윤리 준수는 "윤리 승인이 기록되어 있거나, 윤리 점검이 비활성화되었다"로, 규제 준수는 "규제 배열이 비어 있지 않거나, 규제 점검이 비활성화되었다"로 계산된다. 따라서 윤리 점검을 끈 호출자는 승인이 전혀 없는 프로토콜에 대해 "윤리 준수"라고 읽히는 결과를 받으며, 그 플래그는 반환 레코드 어디에도 되울려지지 않는다. 이 장에서 가장 단단한 규칙이 여기서 나온다. 요청 플래그 없이 저장된 ValidationResult는 증거가 아니다. 플래그는 봉투에 들어가야 하고, 레코드 저장소는 플래그를 달지 않은 검증 결과의 수용을 거부해야 한다. 아울러 여기서 규제 준수는 배열 항목의 존재로 충족되며 내용으로 충족되지 않는다. 제5장 §5.2가 정하는 대로, 확인 플래그는 참조된 승인 기록을 결코 대신하지 못한다.

셋째, 타임스탬프는 검증기를 돌린 쪽 기계의 시계이며, 레코드에는 도구 버전도 검증된 프로토콜의 다이제스트도 없다. 단독으로 저장하면 심사자가 언제나 묻는 두 질문에 답할 수 없다. 어느 버전의 레코드가 검증되었는가, 그리고 어느 빌드의 검증기가 했는가. 감사 사건을 순서 지을 전제로서의 시각 동기화는 제5장 §5.4가 다룬다. API 계층의 요구는 더 좁지만 타협할 수 없다 — 봉투는 타임스탬프가 어디서 왔는지를 기록한다.

결과를 색인하는 방식을 좌우하는 제약이 하나 더 있다. 오류·경고 배열은 코드도 필드 경로도 없는 평문 문자열이다. "이번 분기에 승인 누락으로 차단된 프로토콜이 몇 건인가"를 묻고 싶은 저장소는 문자열 일치에 의존하지 않고는 답할 수 없고, 문구가 바뀌는 순간 그 방식은 깨진다. 권장하는 접수 처리는 각 문자열을 기관 고유 코드와 필드 포인터로 사상하고 — 경로 표기로는 RFC 6901이 정의하는 JSON Pointer가 적절하다 — 원문은 원형 그대로 보존하는 것이다. 개별 점검이 어느 검증 계층에 속하는지는 제3장 §3.4의 주제다. 여기서 중요한 것은 참조 검증기가 구조 점검과 소수의 정책 점검을 수행하며, 참조된 승인 문서가 실제로 존재하는지는 확인하지 않는다는 점이다.

4.3.1 코드로 본 증거 봉투

아래 발췌는 검증 호출을 저장 가능한 레코드로 바꾸는 래퍼의 형태를 보여 준다. 과학적인 것은 아무것도 더하지 않고, SDK가 담지 않는 출처 정보만 더한다. 반환되는 Result 타입은 §4.5에서 논한다.

import { createHash } from 'node:crypto';
import {
  CRISPRProtocolSDK,
  validateCRISPRProtocol,
  type CRISPRProtocol,
  type ProtocolValidation,
  type ValidationResult,
} from '@wia/bio-014';

interface EvidenceEnvelope<T> {
  envelopeVersion: 'wia-bio-014-env/1';
  tool: { name: string; version: string };
  actor: { subject: string; authority: string };
  input: { protocolId: string; protocolVersion: string; digest: string };
  flags: Record<string, boolean>;
  clock: { source: 'ntp-sync-host'; recordedAt: string };
  payload: T;
}

function canonicalDigest(value: unknown): string {
  // 제3장 §3.7은 해시 전에 RFC 8785 정규 JSON을 요구한다.
  // 여기서는 간결함을 위해 JSON.stringify를 썼으나 키 순서가 안정적이지 않다.
  return createHash('sha256')
    .update(JSON.stringify(value))
    .digest('hex');
}

export function validateAsEvidence(
  protocol: CRISPRProtocol,
  actor: { subject: string; authority: string },
): EvidenceEnvelope<ValidationResult> {
  const sdk = new CRISPRProtocolSDK();

  const request: ProtocolValidation = {
    protocol,
    checkEthics: true,
    checkRegulatory: true,
    performOffTargetAnalysis: true,
  };

  const payload = validateCRISPRProtocol(request);

  return {
    envelopeVersion: 'wia-bio-014-env/1',
    tool: { name: '@wia/bio-014', version: sdk.getVersion() },
    actor,
    input: {
      protocolId: protocol.id,
      protocolVersion: protocol.version,
      digest: canonicalDigest(protocol),
    },
    // 적재물이 플래그를 담지 않으므로 봉투가 되울려 준다.
    flags: {
      checkEthics: request.checkEthics === true,
      checkRegulatory: request.checkRegulatory === true,
      performOffTargetAnalysis: request.performOffTargetAnalysis === true,
    },
    clock: { source: 'ntp-sync-host', recordedAt: new Date().toISOString() },
    payload,
  };
}

직렬화하면 저장되는 레코드는 아래 문서처럼 된다. 보이는 모든 값은 자리표시자이며 그렇게 표시해 두었다. 여기에 권장 설정은 하나도 없다.

{
  "envelopeVersion": "wia-bio-014-env/1",
  "tool": { "name": "@wia/bio-014", "version": "<sdk-version>" },
  "actor": { "subject": "<기관 사용자 식별자>", "authority": "role:submitter" },
  "input": {
    "protocolId": "<프로토콜 식별자 자리표시자>",
    "protocolVersion": "<버전 자리표시자>",
    "digest": "sha256:<16진 다이제스트 자리표시자>"
  },
  "flags": {
    "checkEthics": true,
    "checkRegulatory": true,
    "performOffTargetAnalysis": true
  },
  "clock": { "source": "ntp-sync-host", "recordedAt": "<ISO-8601 타임스탬프>" },
  "payload": {
    "isValid": false,
    "errors": ["<검증기 원문 메시지>"],
    "warnings": ["<검증기 원문 메시지>"],
    "safety": {
      "offTargetRisk": "<low|medium|high>",
      "overallRisk": "<low|medium|high>",
      "ethicsCompliant": false,
      "regulatoryCompliant": false
    },
    "quality": {
      "gRNAQuality": 0,
      "specificityScore": 0,
      "protocolCompleteness": 0
    },
    "recommendations": ["<검증기 원문 메시지>"],
    "validated": "<ISO-8601 타임스탬프>"
  },
  "localCodes": [
    { "code": "<기관 고유 코드>", "pointer": "/safety/ethicsApproval" }
  ]
}

localCodes 배열은 기관이 덧붙인 것이다. 자유 서술 배열을 코드화해 질의 가능하게 만든 형태이며, 각 항목은 자신이 가리키는 필드를 지목한다. 적재물은 그 옆에 바이트 단위로 보존한다. 기관의 분류와 도구의 문구는 서로 다른 두 주장이고, 훗날의 심사자는 둘 다 볼 권리가 있다. 숫자 필드 하나는 특히 주의가 필요하다. 품질 객체의 완전성 점수는 참조 구현에서 다섯 하위 구조가 "존재하는가"에 고정 가중치를 주어 계산된다. 즉 객체의 존재를 재는 것이지 내용이 충실한지를 재는 것이 아니며, 레코드는 이를 품질 지표로 제시해서는 안 된다.

4.4 simulateCRISPREditing — 계획 산출물과 측정값을 분리하기

시뮬레이션 호출은 프로토콜 전체를 받아 EditingSimulation을 돌려준다. 식별자, 원형 그대로 내장된 입력 프로토콜, 예측 결과 객체, 가이드 레코드에서 복사된 오프타깃 목록, 0과 1 사이의 성공 확률, 다섯 부분으로 된 문자열 일정, 그리고 통화를 가진 선택 비용 추정이다. 레코드로 읽으면 이 객체는 계획 산출물이며, 이에 관한 가장 중요한 규칙 하나는 독자가 측정값으로 오해할 수 있는 자리에 절대 저장하지 않는다는 것이다.

참조 구현은 그 규칙의 근거를 쉽게 만들어 준다. 일정은 모든 입력에 대해 동일한 다섯 개의 하드코딩된 문자열이고, 예측 인델 분포 역시 모든 입력에 대해 동일한 세 개의 하드코딩된 정수다. 즉 이 객체는 계산 필드 몇 개가 꿰어진 템플릿이며, 어떤 의미로도 예측이 아니다. 여기서 레코드로 옮겨지는 것은 무엇이든 명시된 도구 버전의 산출물로 라벨되어야 하고, 특히 일정 문자열은 결코 사업 계획의 일정 약속으로 전사되어서는 안 된다. 그것은 참조 코드에서 물려받은 예시용 자리표시자다.

성공 확률은 제3장 §3.5가 다루는 출처 없는 상수에서 파생된 값들과, 프로토콜이 생략했을 때 구현체가 기본값으로 대체하는 전달 효율 필드로부터 계산된다. 도구와 버전을 붙여서만 저장하고, 제2장 §2.5의 측정 결과 레코드와 같은 필드 계열에는 결코 두지 않는다. 규칙은 일반화된다. 예측값과 측정값은 서로 다른 레코드 타입에 살고, 보고 시점에 프로토콜 식별자와 버전으로 결합되며, 둘을 함께 보여 주는 보고서는 열 제목에서 어느 것이 무엇인지 밝힌다. 기관이 심사자의 질문에 답할 능력을 잃는 가장 흔한 경로는, 훗날 측정값이 들어갈 필드에 예측값을 써 버리는 것이다.

레코드에 직접 영향을 주는 작은 성질이 셋 더 있다. 시뮬레이션 식별자는 밀리초 타임스탬프와 짧은 난수 접미사로 구성되어 입력에서 파생되지 않는다. 같은 입력을 다시 돌리면 새 식별자가 나오고, 두 레코드가 같은 실행을 기술한다는 사실을 탐지할 방법이 없다. 그러므로 시뮬레이션을 저장하는 기관은 SDK 식별자 옆에 자체 멱등 키를 — 정규 입력과 도구 버전의 다이제스트로 — 함께 실어야 한다. §4.3에서 쓰고 §4.7에서 다시 다루는 것과 같은 구성이다. 비용 추정은 선택 항목이고 참조 구현은 이를 채우지 않으므로, 부재는 "제공되지 않음"으로 기록하고 0으로 강제 변환하지 않는다. 그리고 입력 프로토콜 전체가 반환 객체에 내장되므로 프로토콜 레코드가 복제된다. 제2장 §2.7이 정리한 위험, 즉 서술 필드로 새어 든 개인정보까지 함께 복제된다. 내장 사본 대신 참조와 다이제스트를 저장하는 편이 더 나은 기본값이며, 내장 사본을 유지하는 경우 그것은 프로토콜 자체의 접근통제를 그대로 상속한다. 접근통제는 제6장 §6.3이 규정한다.

4.5 오류 코드 C001–C010과 호출자의 의무

SDK의 오류는 CRISPRProtocolError로 제기된다. 플랫폼 오류 타입의 하위 클래스이며 CRISPRErrorCode 어휘의 코드, 메시지, 선택 세부 맵을 담는다. 어휘는 C001부터 C010까지 열 개 코드를 정의한다. 표에 앞서 이 어휘에 관한 세 가지 사실을 밝혀야 한다. 셋 다 구현자가 오류 처리를 계획하는 방식을 바꾼다.

명세 §8.4의 오류 표는 C001–C006 여섯 개만 열거한다. SDK 열거형은 넷을 더한다. 잘못된 파라미터, 유전체 미발견, 오프타깃 분석 실패, 검증 실패다. 명세만 읽은 구현자는 추가된 네 코드를 처리하지 않게 되는데, 그중 하나는 참조 구현이 실제로 제기하는 단 두 코드 가운데 하나다.

열 코드 가운데 참조 구현 어디에서든 실제로 제기되는 것은 둘뿐이다. 두 곳의 입력 점검에서 제기되는 잘못된 서열 코드와, 또 다른 두 곳의 입력 점검에서 제기되는 잘못된 파라미터 코드다. 나머지 여덟은 선언되어 있으나 도달 불가능하다. 이것은 문서로 덮을 결함이 아니라 적합성의 형태를 바꾸는 사실이다. 오류 처리에 관한 주장은 코드별로 하고 실제로 그 코드를 유발하는 시험 벡터로 뒷받침해야 하며(제7장 §7.2가 그 방법을 정한다), 표준을 채택하는 기관은 현재 쓰이지 않는 여덟 코드를 어느 구성요소가 제기하는지 결정해 문서화해야 한다. 그 결정이 적히기 전까지 오류 처리는 시험 불가능하다. 오류를 만들어 낼 합의된 방법이 없기 때문이다.

끝으로 명세의 오류 표에 있는 조치 열은 실험대 앞의 과학자를 향해 쓰여 있다. 그 조치들은 본서의 범위 밖이다. 표 4-3은 각 코드에 대해 기록·운영 측 대응만을 다시 쓴다. 접수 서비스가 무엇을 하고, 무엇이 기록되며, 누구에게 통지되는가. 모든 과학적 판단은 연구책임자와 IBC·IRB에 넘긴다.

표 4-3. 오류 코드 → 의미 → 호출자의 기록·운영 조치
코드 열거형 이름 참조 SDK에서 제기되는가? 기록·운영 조치
C001NO_PAM_FOUND아니오실패한 접수 시도로 취급한다. 요청 다이제스트와 실패를 기록하고, 프로토콜 레코드는 만들지 않으며, 요청을 제출자에게 돌려보낸다. 과학적 질문은 연구책임자에게 간다.
C002HIGH_OFF_TARGET_RISK아니오 — 검증기가 대신 문자열 오류를 낸다검증 봉투를 보존하고, 실행 단계로의 생애주기 전이를 차단하며(제5장 §5.1), 레코드를 생물안전관리책임자 검토로 보낸다.
C003LOW_PREDICTED_EFFICIENCY아니오경고 등급이다. 차단하지 않고 값을 도구 버전과 함께 저장하며, 승인 경로가 아니라 기관 대시보드에 드러낸다.
C004INVALID_SEQUENCE예 — 입력 점검 두 곳경계에서의 입력 검증 실패다. 문제 필드를 명시해 제출자에게 반환하고, 요청 다이제스트를 기록하고, 레코드를 만들지 않으며, 호출을 부분 저장하지 않는다.
C005DELIVERY_INCOMPATIBLE아니오해당 필드를 지목한 접수 반려. 양립성 판단 자체는 범위 밖이며 연구책임자의 몫이다.
C006ETHICS_VIOLATION아니오 — 검증기가 대신 문자열 오류를 낸다강한 차단이다. IBC·IRB 사무국에 통지하고 시도를 감사 사건으로 보존하며, 어떤 예외 처리도 승인 참조를 요구한다(제8장 §8.3).
C007INVALID_PARAMETERS예 — 효율 호출의 입력 점검 두 곳제출을 거부한다. 도구가 거부한 입력으로 계산된 백분율은 결코 저장하지 않으며, 거부된 개수 자체를 기록한다.
C008GENOME_NOT_FOUND아니오두 원인을 구분한다. 어셈블리 라벨이 해석되지 않는 것은 문서의 참조 계층 실패(제3장 §3.4)이므로 제출자에게 되돌린다. 운영 측 참조 데이터가 없거나 손상된 것은 설정 장애이므로 운영자에게 알리고, 참조 데이터 수정 후 재시도하며, 프로토콜의 결함으로 기록하지 않는다.
C009OFF_TARGET_ANALYSIS_FAILED아니오분석을 "수행되지 않음"으로 기록한다 — "수행했고 비었음"과는 다른 사실이다. 실패한 실행이 대응 확인 플래그를 true로 남기게 두어서는 안 된다.
C010VALIDATION_FAILED아니오"검증기가 실행되지 못했다"에만 엄격히 예약한다. 실행되어 프로토콜이 무효로 판정된 경우는 검증 결과가 보고하며, 이 코드가 아니다. 둘을 뭉개면 보고서에서 장애와 미준수를 구별할 수 없게 된다.

마지막 행이 그은 구분은 일반화할 값이 있다. 준수 보고를 가장 자주 망치는 구분이기 때문이다. "점검이 실행되지 않았다"와 "점검이 실행되어 실패했다"는 결과가 다른 서로 다른 사실이며, 이 둘을 같은 필드에 저장하는 기록 체계는 1년이 지나면 어느 방향으로도 신뢰할 수 없는 대시보드를 내놓는다.

4.5.1 Result 대 예외 던지기

SDK는 판별 유니온 결과 타입과 그 비동기 변형을 내보낸다. 그러나 참조 구현의 어느 함수도 이를 쓰지 않는다. 모든 진입점은 예외를 던져 실패를 알린다. 레코드 접수에 대한 결과는 구체적이다. 제출된 프로토콜 200건을 검증하는 일괄 처리는 호출을 개별로 감싸지 않으면 첫 불량에서 중단되고, 절반만 처리된 일괄은 아무도 출처를 되짚을 수 없는 레코드를 남기는 전형적 경로다. 어댑터는 사소하며, 기관의 경계가 그 자리다.

import type { Result } from '@wia/bio-014';
import { CRISPRProtocolError } from '@wia/bio-014';

export function tryCall<T>(fn: () => T): Result<T, CRISPRProtocolError> {
  try {
    return { success: true, data: fn() };
  } catch (err) {
    if (err instanceof CRISPRProtocolError) {
      return { success: false, error: err };
    }
    throw err; // 우리 계약이 아니다 — 미지의 결함을 위장하지 않는다.
  }
}

catch 블록을 지배하는 규칙은 둘이다. 준수에 영향을 주는 실패는 결코 삼키지 않는다. 검증 호출을 맨 catch로 감싸면, 차단되어야 했던 프로토콜이 검증 기록이 아예 없는 프로토콜로 바뀐다. 성공처럼 보이기 때문에 장애보다 나쁘다. 그리고 실패 항목은 성공 항목과 같은 봉투를 — 같은 도구 버전, 행위자, 입력 다이제스트, 시계 출처를 — 지닌다. 특정 날짜에 무슨 일이 있었는지 묻는 심사자는 레코드를 만들지 못한 시도까지 볼 수 있어야 한다. 이 표면을 HTTP로 노출하는 경우 오류 코드는 산문 본문이 아니라 구조화된 문제 문서에 실어야 하며, RFC 7807을 대체하는 RFC 9457이 그 형식을 정의한다. 제6장 §6.2가 이를 전송 프로파일에 배치한다.

4.6 CLI — 자동화의 이음새

참조 명령줄 도구는 일곱 명령을 노출한다. 가이드 설계, 오프타깃 예측, 효율 계산, 프로토콜 검증, 템플릿 생성, 버전 배너, 도움말이다. 옵션은 도구 자신의 도움말이 열거하는 것들이다. 설계에는 표적 서열과 PAM 종류, 예측에는 가이드·유전체·최대 불일치, 효율에는 편집·총 개수, 검증에는 프로토콜 파일, 템플릿에는 출력 경로다. 자동화의 이음새로서 중요한 것은 기능 목록이 아니라 거동이며, 그중 다섯이 하중을 받는다.

인식되는 모든 명령이 종료 코드 0으로 끝난다. 스크립트는 조건 없는 성공 종료 한 줄로 끝나며, 검증 루틴은 프로토콜 파일이 없을 때 실패 상태를 반환하지만 그 상태는 루틴을 호출한 분기 블록에서 버려진다. 0이 아닌 값으로 끝나는 것은 인식되지 않은 명령 이름뿐이다. POSIX(IEEE Std 1003.1)가 성문화한 관례상 종료 코드 0은 성공을 뜻하므로, 이 도구를 게이트로 삼은 지속적통합 작업은 검증하라고 지시한 파일이 존재하지 않을 때조차 통과한다. 운영 규칙은 단호하다. 어떤 파이프라인도 이 도구의 종료 코드로 게이트하지 않는다. SDK로 게이트하거나 도구 출력 파싱으로 게이트하며, 상태를 전달하는 릴리스가 나오기 전까지 그렇게 한다 — 수정 방향은 조건 없이 0으로 끝내는 대신 분기 블록이 루틴의 상태를 반환하게 하는 것이다.

모든 명령이 내장 기본값을 가지며, 인식되지 않은 옵션은 조용히 버려진다. 인자 루프는 문서화된 옵션에 일치시키고 그 밖의 것은 맨 shift로 흘려보낸다. 따라서 옵션 이름을 잘못 적으면 내장 기본값이 그대로 남고 진단은 전혀 나오지 않아, 총 개수를 잘못 적어 물은 효율 명령은 기본 개수로 계산된 값을 자신 있게 보고한다. 접수 자동화의 규칙은 호출 전에 인자를 검증하고, 실효 파라미터를 레코드에 되울려 적는 것이다. 그러면 독자가 운영자의 의도가 아니라 도구가 실제로 받은 값을 볼 수 있다.

출력은 사람용 색상 텍스트이며 기계 형식이 아니다. 도구는 단말 이스케이프 시퀀스를 섞은 절 제목과 상태 줄을 표준출력에 쓴다. JSON 모드는 없다. 그러므로 본서가 말하는 구조화 출력 요건은 현재 거동의 서술이 아니라 권고다. 향후 릴리스는 호출 1회당 정확히 하나의 JSON 문서를 — RFC 8259를 따르고 §4.1의 봉투 필드를 포함하여 — 내보내는 옵션을 제공해야 한다. 그때까지 색상 출력 파싱은 취약하며, 올바른 자동화 표면은 SDK다.

검증 명령은 시연용 스텁이다. 지정된 파일이 존재하는지만 확인한 뒤, 파일 내용과 무관하게 고정된 점검 줄 묶음 — 고정된 생물안전 등급 줄과 고정된 종합 판정까지 — 을 출력한다. 소스 주석 자체가 실제 구현이라면 JSON을 파싱할 것이라고 적고 있다. 따라서 그 출력은 프로토콜에 관한 어떤 것의 증거도 아니며, 승인 패키지·적합성 주장·감사 대응에 결코 첨부해서는 안 된다. 이 장에서 한 문장만 기관의 운영 절차에 닿는다면 그것은 이 문장이어야 한다.

템플릿 생성은 모든 값이 자리표시자인 고정 골격을 쓴다. JSON 문서 자체에 대해서는 제3장이 같은 점을 밝힌다. 그럼에도 "템플릿 생성 후 채우기"는 여전히 옳은 접수 절차인데, 이유는 값과 전혀 무관하다. 템플릿이 필드 집합과 직렬화 형태의 snake_case 표기를 고정하므로 제출자가 필드 이름을 지어낼 수 없다. 채워진 템플릿은 CLI 검증기가 아니라 SDK 검증기로 간다.

표 4-4. 참조 CLI 명령을 자동화 표면으로 읽기
명령 옵션을 생략했을 때의 거동 출력 형태 종료 코드 증거로 쓸 수 있는가?
design-grna내장 자리표시자 서열로 실행색상 텍스트0아니오 — 자리표시자 입력, 봉투 없음
predict-offtargets내장 자리표시자 가이드·어셈블리로 실행색상 텍스트0아니오 — 시연용 출력
calc-efficiency내장 기본 개수로 실행색상 텍스트0아니오 — 그 개수가 운영자의 것이 아닐 수 있다
validate기본 파일명을 찾는다색상 텍스트, 내용 고정파일이 없어도 0아니오 — 고정 출력, 파싱 없음
generate-template기본 파일명에 쓴다자리표시자로 된 JSON 파일0증거는 아니나 유효한 접수 출발점
version—색상 텍스트 배너0버전 문자열을 봉투에 담는 경우에만
help—색상 텍스트0아니오

4.6.1 실행 기록 하나, 그리고 그것이 증명하지 못하는 것

아래 실행 기록은 접수 절차와 운영자의 판단을 주석으로 함께 보여 준다. 값은 자리표시자다.

$ wia-bio-014 generate-template --output intake/PROTO-PLACEHOLDER.json
# 골격을 쓴다. 안의 모든 값은 자리표시자이며 예시 유전자·좌표·전달
# 파라미터도 마찬가지다. 그중 어느 것도 권장 설정이 아니다.

$ vi intake/PROTO-PLACEHOLDER.json
# 제출자가 자리표시자를 과제의 값으로 바꾼다.
# 필드 이름과 snake_case 표기는 바꾸지 않는다 — 필드 집합의 권위는
# 템플릿에 있다(제3장 §3.1).

$ wia-bio-014 validate --protocol intake/PROTO-PLACEHOLDER.json
# 고정된 점검 줄 묶음과 고정된 종합 판정을 출력한다.
# 파일을 파싱하지 않았다. 이 출력을 기록하지 말 것.

$ echo $?
0
# 파일이 없어도 0이다 — 상태가 전달되지 않는다.
# 따라서 이 명령으로 파이프라인을 게이트하지 말 것.

$ node ./intake/validate-as-evidence.mjs intake/PROTO-PLACEHOLDER.json
# §4.3의 래퍼가 SDK 검증기를 호출하고, 도구 버전·행위자·입력
# 다이제스트·플래그를 담은 봉투 하나를 레코드 저장소에 쓴다.
# 승인 절차가 소비하는 산출물은 이것이다.

4.6.2 호출이 놓이는 자리

아래 순서도는 SDK를 기관 접수 경로 안에 놓은 것이다. SDK는 상태가 없는 프로세스 내 라이브러리이므로 저장을 하지 않는 동기 단계로 나타나며, 무언가를 영속화하는 모든 화살표는 기관의 것이다.

제출자            접수 서비스           SDK(프로세스 내)      레코드 저장소     감사 로그
  |                   |                      |                   |               |
  |-- 채운 JSON ---->|                      |                   |               |
  |                   |-- 파싱 + 스키마 --->|                   |               |
  |                   |   (구조 계층, 제3장 §3.4)                |               |
  |                   |<-- 타입된 프로토콜 ---|                   |               |
  |                   |                      |                   |               |
  |                   |-- validateProtocol(플래그) -->|          |               |
  |                   |<-- ValidationResult (플래그 미반향) ------|               |
  |                   |                      |                   |               |
  |                   |-- 봉투 구성: 도구+버전, 행위자,            |               |
  |                   |   입력 다이제스트, 플래그, 시계 출처 ----->|              |
  |                   |                      |      봉투 저장(append)            |
  |                   |                      |                   |-- 사건 ---->|
  |                   |                      |                   |   누가/무엇/언제
  |<-- 판정 ----------|                      |                   |               |
  |   (유효 | 차단, 인용 근거는 봉투 식별자)                       |               |

이 그림에 없는 것을 보라. SDK는 레코드 저장소에 닿지 않고, 행위자를 읽지 않으며, 감사 사건을 쓰지 않는다. 그 세 화살표는 기관이 그렸기 때문에만 존재한다.

4.7 API 계층의 함정 일곱 가지

아래 각 항목은 위에서 기술한 계약들에서 관찰된 것이며, 하나같이 통상적인 코드 리뷰는 통과하고 감사에서 무너지는 레코드를 만든다.

  1. 입력 스냅샷 없이 점수를 저장한다. 요청이 버려진 숫자는 재계산도 비교도 방어도 불가능하다. 봉투의 입력 다이제스트가 최소선이고, 정규화된 요청 전문이 더 낫다.
  2. 자유 서술을 기계 판독 가능한 것처럼 다룬다. 경고·오류·권고 문자열은 버전에 따라 문구가 바뀔 수 있는 사람의 문장이다. 접수 시점에 기관 코드로 사상하고 원문을 그 옆에 원형으로 보존한다.
  3. 멱등 키 없이 레코드를 생성한다. 이 표면의 어느 것도 멱등이 아니며, 시뮬레이션 식별자는 시계 값과 난수 접미사로 만들어진다. 타임아웃 후 재시도는 어떤 질의로도 중복으로 인식되지 않는 두 번째 레코드를 만든다. 생성은 정규 입력·도구 버전·행위자의 다이제스트로 키를 잡는다.
  4. 파생 기본값을 호출자의 진술처럼 신뢰한다. 생물종에서 파생된 어셈블리 라벨과, 기본이 "켬"인 검증 플래그는 둘 다 사람이 주장한 것처럼 레코드에 남는다. 제출자가 말한 것을 기록하고, 도구가 추론한 것은 따로 기록한다.
  5. 준수 점검 주변에서 예외를 삼킨다. 잡아서 무시된 검증 실패는 검증 기록이 없는 프로토콜을 만들고, 후단에서는 결함이 아니라 부재로 읽힌다. 실패는 성공과 같은 봉투로 기록한다.
  6. 예측값과 측정값을 한 필드 계열에 섞는다. 시뮬레이션 산출물과 시퀀싱 기반 결과는 서로 다른 레코드 타입을 차지하며, 보고를 위해 결합할 뿐 저장에서 합치지 않는다.
  7. 시연용 스텁을 상대로, 또는 도구가 설정하지도 않는 종료 코드를 상대로 자동화한다. 두 실패 모두 초록색 파이프라인처럼 보인다. 어떤 도구든 승인 경로에 배선하기 전에, 입력을 실제로 파싱하는지와 파이프라인이 관측할 수 있는 방식으로 실패를 알리는지를 확인한다.

장 요약. 참조 SDK는 다섯 개 레코드 서비스로 이루어진 상태 없는 라이브러리이고, 참조 CLI는 그 위의 시연용 껍데기다. 어느 쪽도 신원·영속성·출처·감사 훅을 담지 않으며, 타입된 오류를 던지는 것 외에는 기계가 소비할 수 있는 방식으로 실패를 보고하지 않는다. 호출 결과를 레코드로 만드는 모든 것 — 도구 버전, 행위자, 입력 다이제스트, 요청 플래그, 시계 출처, 멱등 키, 코드화된 발견 — 은 기관이 만든다. §4.1에서 정의하고 §4.3에서 실물로 보인 봉투가 그 형태다. 이어지는 장들은 그 봉투를 전제로 한다. 제5장은 봉투를 승인 게이트와 감사 추적이 있는 생애주기에 넣고, 제6장은 전송과 접근통제 위에 놓으며, 제7장은 봉투의 묶음을 적합성 증거 패키지로 바꾼다.

과학·실험실 안전 관련 내용: 소속 기관의 IBC·IRB와 공식 생물안전·규제 당국(질병관리청 및 LMO법 소관 당국, WHO, NIH 가이드라인, 해당 국가 규제기관)에 문의할 것. 이 장은 레코드 필드·호출 계약·오류 처리만을 기술하며, 실험·설계·안전에 관한 지침은 제공하지 않는다.