trim-1로 줄인 로그에 원본 줄 번호 연결하기

AI에게 긴 로그의 일부만 보여 준 뒤, 답변에 나온 문장을 원본에서 다시 찾으려면 줄 번호가 필요합니다. trim-1은 남긴 줄을 다시 쓰지 않지만, 발췌본의 줄 번호가 원본의 줄 번호와 같아지는 것은 아닙니다. 생략 표시에 적힌 줄 수와 보관한 원문을 함께 읽으면 원본 위치를 계산할 수 있습니다. 여기서는 같은 로그 문장이 두 번 등장하는 예제로 그 연결을 만들고, 맞지 않는 발췌본은 거부하는 작은 Python 함수를 작성합니다.

확인일: 2026년 10월 16일. trim-1 v0.1.0의 공개 소스와 모델 카드를 검토했습니다. 아래에 직접 작성한 Python 예제와 합성 데이터 테스트는 Python 3.12.14에서 실행했습니다. trim-1 설치·모델 실행·압축 성능 측정은 하지 않았습니다.

trim-1은 어떤 입력을 줄이는 도구인가요?

trim-1은 질문과 입력 문서를 받아, 질문에 필요하다고 판단한 줄을 원래 순서대로 남기는 도구입니다. 줄 사이에서 없앤 부분은 … 5 lines …처럼 표시합니다. 요약 문장을 새로 만드는 방식이 아니라, 줄별 특징을 작은 트리 모델에 넣어 선택하는 방식입니다.

첫 공개 버전인 v0.1.0은 2026년 10월 14일에 나왔습니다. Go 1.23 이상을 선언한 모듈이며, 모델이 코드에 포함되어 있어 이 모듈의 추론에 별도 모델 API 호출은 필요하지 않습니다. 코드와 모델은 Apache-2.0으로 제공되며, 복사해 사용한 Go 수학 함수에는 별도의 BSD 고지가 있습니다. 코드 라이선스와 추가 고지를 함께 확인해야 합니다.

공개 직후의 관심도 확인됩니다. 10월 16일 확인한 Trendshift의 날짜별 집계는 10월 14일 별 증가를 69개, 15일을 38개로 표시했습니다. 이는 해당 제공자의 날짜별 관측이며 정확한 최근 24시간 수치나 품질 평가가 아닙니다. 저장소에는 입력을 읽기 전에 크기를 제한하는 기여와 CRLF 줄바꿈 처리에 관한 보고도 있습니다. 아직 이틀 된 프로젝트인 만큼 이런 활동과 운영 안정성은 구분해서 봐야 합니다.

같은 문장이 두 번 나오면 문자열 검색만으로는 부족합니다

아래 예제의 원문에는 INFO waiting이 1행과 6행에 각각 있습니다. 발췌본은 첫 다섯 줄을 생략한 뒤 6~8행을 남기고, 마지막 두 줄을 생략했다고 가정합니다. 발췌본에 남은 INFO waiting을 원문 처음부터 검색하면 1행을 찾을 수 있습니다. 하지만 생략 수를 따라가면 인용할 위치는 6행입니다.

v0.1.0의 출력 코드는 남긴 줄의 순서를 유지하고, 연속해서 빠진 줄 수를 표시합니다. 이를 이용해 원문을 가리키는 위치를 하나씩 이동시킵니다. 생략 표시는 그 수만큼 건너뛰고, 일반 줄은 바로 그 위치의 원문 바이트와 비교합니다. 같은 문장이 앞에 있어도 다시 검색하지 않습니다.

다음 예제는 10행짜리 원문에서 사람이 고른 합성 발췌본을 사용합니다. trim-1의 실제 출력이나 모델 예측이 아닙니다. 이처럼 2,000바이트보다 작은 입력은 기본 설정의 trim-1에서 그대로 반환됩니다. 짧게 만든 이유는 압축률을 보여 주려는 것이 아니라, 반복 문장과 줄 번호 계산을 눈으로 확인하기 위해서입니다.

원문과 발췌본을 대조하는 Python 예제

Python 3.12 환경에서 아래 내용을 UTF-8 파일 trim_excerpt.py로 저장하고 python3 trim_excerpt.py 명령으로 실행하세요. 외부 패키지는 필요하지 않습니다. 축약된 입력은 LF 줄바꿈으로 범위를 제한합니다. CRLF를 임의로 바꿔 비교하지 않으며, 원문과 발췌본이 바이트 단위로 완전히 같을 때만 줄바꿈 형식과 관계없이 그대로 연결합니다.

"""Original demonstration: map a trim-1-format excerpt to exact source lines.

No trim-1 code or model is imported or executed. Non-identity excerpts are
restricted to LF input/output and the v0.1.0 marker grammar. The returned
hash binds line numbers to these source bytes, not to a mutable file path.
This verifies alignment only, not provenance authenticity or evidence recall.
"""

import hashlib
import re

MARKER = re.compile("… ([1-9][0-9]*) (line|lines) …".encode())
COLLISION = re.compile("[ \\t]*… [0-9]+ lines? …[ \\t\\r]*".encode())


def lf_lines(data):
    if not data:
        return []
    return data.removesuffix(b"\n").split(b"\n")


def map_excerpt(original, excerpt):
    source = lf_lines(original)
    if original == excerpt:
        ranges = [("keep", 1, len(source))] if source else []
        return {"source_sha256": hashlib.sha256(original).hexdigest(),
                "ranges": ranges}
    if b"\r" in original or b"\r" in excerpt:
        raise ValueError("This demonstration accepts LF-only shortened text")
    if original.endswith(b"\n") != excerpt.endswith(b"\n"):
        raise ValueError("Final newline differs")
    if any(COLLISION.fullmatch(line) for line in source):
        raise ValueError("Original contains a marker-like line")
    ranges, cursor = [], 0
    for line in lf_lines(excerpt):
        marker = MARKER.fullmatch(line)
        if marker:
            count = int(marker[1])
            if marker[2] != (b"line" if count == 1 else b"lines"):
                raise ValueError("Invalid marker grammar")
            end = cursor + count
            kind = "omit"
        else:
            if cursor >= len(source) or line != source[cursor]:
                raise ValueError(f"Retained line differs at source line {cursor + 1}")
            end, kind = cursor + 1, "keep"
        if end > len(source):
            raise ValueError("Marker runs past the original")
        ranges.append((kind, cursor + 1, end))
        cursor = end
    if cursor != len(source):
        raise ValueError("Excerpt does not account for every source line")
    return {"source_sha256": hashlib.sha256(original).hexdigest(),
            "ranges": ranges}


if __name__ == "__main__":
    # Hand-selected synthetic fixture; not trim-1 output or a prediction.
    original = (b"INFO waiting\nINFO startup\nDEBUG cache=A\nDEBUG cache=B\n"
                b"DEBUG cache=C\nINFO waiting\nERROR checkout timeout request=R7\n"
                b"    at charge(payment.py:88)\n\nINFO shutdown\n")
    excerpt = ("… 5 lines …\nINFO waiting\nERROR checkout timeout request=R7\n"
               "    at charge(payment.py:88)\n… 2 lines …\n").encode()
    import json
    print(json.dumps(map_excerpt(original, excerpt), indent=2))

실행 결과의 값은 다음과 같습니다. JSON의 공백과 줄바꿈만 줄여 표시했습니다.

{
  "source_sha256": "dd802860561951b1e30858a64695e8c00ae79a0d7d9be6b1d43a16803c536452",
  "ranges": [
    ["omit", 1, 5],
    ["keep", 6, 6],
    ["keep", 7, 7],
    ["keep", 8, 8],
    ["omit", 9, 10]
  ]
}

각 범위는 1부터 시작하며 끝 행을 포함합니다. 이제 오류 문장을 인용할 때는 이 원문의 7행을 가리킬 수 있습니다. 앞선 처리 과정이 궁금하면 생략된 1~5행을, 종료 상황이 필요하면 9~10행을 같은 원문에서 다시 읽으면 됩니다. 발췌본만 가지고 삭제된 내용을 복원하는 기능은 아닙니다.

검사를 통과했다는 말의 범위

이 함수는 남긴 문장이 지정한 원문 위치와 같은지, 생략 수를 더했을 때 원문 전체 행 수와 맞는지 확인합니다. 원문에 이미 생략 표시처럼 보이는 줄이 있으면 축약된 결과의 해석을 거부합니다. 끝의 줄바꿈이 달라지거나, 남긴 문장이 수정되거나, 생략 수가 범위를 넘거나, 마지막 구간이 설명 없이 빠져도 오류를 냅니다. 반복 문장·빈 줄·UTF-8 문자·변조·잘못된 줄 수 등을 포함한 13개 단위 테스트를 실행해 통과를 확인했습니다.

다만 이것은 보관한 원문과 발췌본 사이의 위치 일치 검사입니다. trim-1이 실제로 만들었는지 인증하지 않으며, 모든 trim-1 출력 규칙을 판별하는 검증기도 아닙니다. 중요한 원인이나 예외가 생략됐어도 위치는 정확히 맞을 수 있습니다. SHA-256 역시 어느 원문 바이트를 기준으로 계산했는지 식별하는 값이지, 생성 주체나 내용의 진실성을 보증하는 서명은 아닙니다.

실제 파일에 연결할 때는 이름만 보관하지 말고, 변경되지 않는 원문 사본이나 저장소 객체와 그 해시를 함께 남기는 편이 좋습니다. 압축에 사용한 버전, 질문, 설정 이름도 같은 기록에 두어야 나중에 선택 조건을 확인할 수 있습니다. 파일을 읽을 때는 Path.read_bytes()처럼 바이트를 그대로 읽는 방식을 쓰고, 비교 전에 줄바꿈이나 공백을 정리하지 않습니다.

원본을 다시 읽을 수 있을 때 도입합니다

공식 모델 카드는 학습·평가 데이터가 영어 텍스트와 소스 코드라고 밝힙니다. 질문과 직접 단어가 겹치지 않는 후속 단서가 빠질 수 있으며, 긴 한 줄을 더 짧게 나누지는 않습니다. 따라서 위 Python 함수가 한글 바이트를 대조할 수 있다는 사실을 trim-1의 한글 검색·선택 정확도로 해석하면 안 됩니다. 벤치마크의 토큰 감소량도 실제 청구 비용 절감액은 아닙니다.

긴 문서에서 질문에 맞는 후보 구간을 먼저 보고, 필요할 때 원문으로 돌아가는 작업에는 이런 연결이 유용할 수 있습니다. 반면 오류 코드가 이미 정해져 있다면 줄 번호를 붙이는 일반 텍스트 검색만으로 충분할 수 있습니다. JSON이나 소스 코드를 다시 파싱·실행하려는 작업이라면 줄을 빼서 만든 읽기용 발췌본을 원본 파일처럼 사용하지 않는 편이 낫습니다.

실험을 시작한다면 공개 예제나 민감하지 않은 유한한 파일부터 다루고, 원본을 대체하지 않는 별도 출력으로 살펴보세요. 특히 출시된 v0.1.0 CLI는 입력 전체를 먼저 읽습니다. 앞서 언급한 입력 크기 제한 수정이 기본 브랜치에 병합됐다고 해서 이 릴리스에 포함됐다고 가정하면 안 됩니다. 이 글의 Python 함수 역시 큰 스트림을 처리하는 수집기가 아니라, 이미 확보한 작은 두 바이트열을 대조하는 예제입니다.

핵심은 발췌본을 더 짧게 만드는 데서 끝내지 않는 것입니다. 이 예제처럼 남은 문장이 원문 6행인지 1행인지 구분하고, 빠진 구간을 다시 열 수 있어야 검토 과정에서 쓸 수 있는 발췌본이 됩니다.

공개 저장소·모델 카드·관심도 기록을 바탕으로 AI의 도움을 받아 조사하고 작성했습니다. 프로젝트의 실제 압축 결과와 성능은 측정하지 않았으며, 실행 확인 범위는 이 글의 자체 Python 예제와 합성 데이터 테스트입니다.

댓글