Navigation

Readable Docs from AI Markdown

AI에게 설계 문서나 조사 기록을 맡기면 분량은 금방 차지만 사람이 읽을 순서는 비어 있는 경우가 많습니다. 직접 만들어 공개한 정적 문서 도구 for-humanity로 AI가 HTML로 만든 업무 설계 문서와 개인 프로젝트의 핸드북을 Markdown으로 옮겼습니다. 그때 정한 원본 보존 순서, 읽는 구조를 입히는 기준, 생성된 문서의 검증 방법을 도구 0.21.0 기준으로 정리합니다.

AI 문서에서 반복해서 본 문제

옮기기 전에 업무 설계 문서 HTML 5개와 핸드북 HTML 11개를 다시 읽었습니다. 두 묶음 모두 표가 많았지만 같은 종류의 표는 아니었습니다.

항목업무 설계 문서핸드북
표121개170개
칸 안에 목록, 문단, 코드가 든 표99개0개
가장 넓은 표의 열 수1014
원본 이미지23개0개

설계 문서의 표는 비교보다 상세 설명을 담는 데 쓰였습니다. 한 칸에 문단과 코드가 들어간 10열 표는 본문 폭을 넓혀도 읽히지 않습니다. 표의 개수만 보고 새 표현 부품이 필요하다고 판단할 수 없었던 이유입니다.

내용에서도 같은 문제가 반복됐습니다.

원본은 고치지 않고 보존부터 합니다

AI 문서를 옮길 때 가장 먼저 정한 원칙은 원본을 고치지 않는 것이었습니다. 옮기는 순서는 다음과 같습니다.

  1. 원본 목록, SHA256, 이미지, 앵커, 표의 열 수와 병합 셀을 기록합니다.
  2. 원본은 그대로 두고 목적지의 문서 경계와 부모 관계, 정본 위치를 설계합니다.
  3. 첫 변환에서는 현재, 제안, 미확인 상태와 코드, 계약을 보존합니다. 반복된 개정 기록도 이 단계에서는 줄이지 않습니다.
  4. 원본과 결과를 문장과 값 단위로 대조합니다. 빠진 것은 원문을 다시 읽어 복원합니다.
  5. 최신 동작만 추린 편집본은 별도 작업으로 만듭니다.

대조는 정규화한 글자 비교로 했습니다. 실제 이전에서는 코드 블록 110개와 본문, 목록, 표 칸 5,631개를 원문과 맞춰 봤습니다. 아래는 같은 방식을 Markdown 사이에 적용하도록 다시 쓴 예시입니다.

compare-units.ts, 원본의 줄과 표 칸이 결과에 남았는지 대조ts
import { createHash } from 'node:crypto'
import { readFileSync } from 'node:fs'

// 링크 주소, 기호, 공백을 지우고 글자만 비교합니다
const normalize = (text: string) =>
  text.replace(/\]\([^)]*\)/g, ']').replace(/[\s`*_>#[\]-]/g, '').toLowerCase()

// 줄과 표 칸을 비교 단위로 씁니다. 표를 문단으로 풀어도 칸 단위로 찾을 수 있습니다
const units = (text: string) =>
  text.split(/\n|\|/).map(normalize).filter((unit) => unit.length > 1)

export const compareUnits = (sourcePath: string, targetPaths: string[]) => {
  const source = readFileSync(sourcePath, 'utf8')
  const target = normalize(targetPaths.map((path) => readFileSync(path, 'utf8')).join('\n'))
  const all = units(source)

  return {
    sha256: createHash('sha256').update(source).digest('hex'),
    units: all.length,
    missing: all.filter((unit) => !target.includes(unit)),
  }
}

missing에는 지운 예외 문장뿐 아니라 이름을 바꾼 제목과 열 이름도 함께 나옵니다. 그래서 목록을 사람이 한 번 걸러야 합니다. 이 검사는 글자가 남았는지를 볼 뿐이고 문장이 맞는지, 업무 규칙이 빠짐없이 옮겨졌는지는 증명하지 못합니다.

문법도 원본을 해치지 않는 쪽을 골랐습니다. for-humanity는 일반 Markdown에 frontmatter를 붙인 파일을 읽습니다. 설계 문서 전용 타입을 두지 않기로 했고 .mdx 파일도 Markdown으로 읽으며 JSX는 실행하지 않습니다. 추가 문법은 :::note, :::details, ::part 정도라 도구 없이 저장소에서 열어도 문서가 읽힙니다. AI가 만든 HTML 프로토타입의 상자와 대화상자를 모두 부품으로 만들지 않고 제목, 목록, 비교표, 그림, 명시적 링크로 먼저 옮겼습니다.

사이드바, 제목, 표로 읽는 순서를 만듭니다

읽는 구조는 세 가지로 입힙니다. 사이드바는 문서의 경계를, 제목은 문서 안의 순서를, 표는 같은 기준의 비교를 맡습니다.

사이드바는 frontmatter의 group과 parent로 만듭니다. group은 클릭할 수 없는 분류 경로이고 parent는 같은 분류 안에서 문서를 다른 문서 아래에 놓습니다.

frontmatter, 분류와 부모 문서yaml
---
name: Upload screen
label: 업로드 화면의 상태, 질문, API와 검증 조건
group: [Projects, Design]
parent: ./overview
order: 20
---
flowchart TB
    G["group: Projects / Design"] --> O["overview: 범위와 핵심 결정"]
    O --> A["upload screen: 상태, 질문, API"]
    O --> B["list screen: 상태, 질문, API"]
    O --> R["공통 참조: 오류 코드"]

문서를 나누는 기준은 본문 폭이 아니라 독자입니다. 한 화면을 이해하는 데 필요한 구성, 상태, 질문, API, 검증은 한 문서의 ##와 ### 제목으로 잇습니다. 다른 독자, 다른 작업, 여러 문서가 함께 찾아볼 공통 참조만 하위 문서로 나눕니다. 제목은 페이지 목차가 되므로 번호보다 안정적인 이름을 붙입니다.

표는 내용의 모양을 보고 고릅니다.

내용쓰는 표현피하는 표현
같은 기준의 짧은 비교2~5열 표칸 안에 문단과 목록을 압축
항목마다 긴 이유와 조건제목과 문단, 목록숫자 표처럼 보이는 큰 행렬
API 요청, 응답, 오류항목별 제목과 코드, 짧은 표요청과 오류를 한 표에 모두 넣기
확인할 질문ID, 상태, 선택지, 근거를 적은 절번호만 보고 의미를 추측
화면의 위치설명을 붙인 그림작은 그림 속 글씨에만 의존

5열은 렌더러의 제한이 아니라 보수적인 분류 기준입니다. 짧은 6열 표는 읽히고 긴 3열 표는 읽히지 않을 수 있습니다. 실제 이전에서는 원본 표 291개 중 단순 비교 175개를 표로 남기고 116개는 열 이름을 제목 삼은 설명으로 풀었습니다. 병합 셀은 공통 열의 제목을 함께 적거나 공유 값을 행마다 다시 적어 관계를 지켰습니다. 넓은 표와 코드는 상자 안에서만 가로로 스크롤되게 하고 페이지 전체가 밀리지 않는지 좁은 화면에서 확인합니다.

AI에게 줄 작성 규칙

AI에게 형식 하나를 강제하지 않았습니다. 대신 독자가 찾을 질문, 근거의 종류, 현재 사실과 제안과 미확인 항목의 구분, 읽는 순서를 먼저 정하게 했습니다. 목적에 따라 앞에 둘 것과 뒤에서 찾을 것이 다릅니다.

목적앞에서 읽을 것뒤에서 찾을 것
설계범위, 핵심 결정, 남은 질문, 화면 흐름항목별 계약, 상태, 검증 조건
참조지원 범위, 버전, 정본값, 오류, 예외, 짧은 예, 출처
조사확인할 질문, 관찰, 결론과 한계자료, 재현 절차, 미확인 항목
검증 기록무엇을 어디까지 확인했는가환경, 명령, 관찰, 확인하지 않은 것

이 규칙은 저장소의 에이전트 지침 파일이 작성 가이드를 가리키는 방식으로 연결할 수 있습니다. 사용자 저장소의 지침 전체를 덮어쓰지 않고 문서 작성에 필요한 부분만 잇습니다. 언어와 출처 우선순위는 각 프로젝트가 정합니다.

AGENTS.md, 문서 작성 부분만 발췌한 예시md
## Writing documents

- Start with the question the reader needs answered and the scope in one sentence.
- Mark each claim as current fact, decision, proposal or unverified.
- Do not report a successful render as a verified behavior.
- Keep numbers, exceptions, sources and IDs from the source.
- Keep one canonical place per fact and link to it explicitly.
- Prefer headings, lists and short tables before inventing new syntax.
- Put unimplemented directives only inside code blocks.
- Follow docs/writing.md for terms and tone.

체크박스와 상태 색은 수행 증거를 대신하지 않습니다. 질문에 답이 없다는 이유로 추천안을 채택 상태로 바꾸지 않게 한 것도 같은 이유입니다.

생성된 문서를 빌드에서 검증합니다

AI가 문서를 한꺼번에 고치면 사람이 모든 링크와 frontmatter를 읽어 볼 수 없습니다. 그래서 검사는 빌드가 맡게 했습니다. 0.21.0에서 빌드를 멈추는 입력은 다음과 같습니다.

없는 문서를 가리키는 링크는 다릅니다. 파일과 줄 번호를 경고로 보여 주지만 종료 코드는 0입니다. 작성 중에는 편하지만 배포 전에는 놓치기 쉬우므로 경고도 실패로 다루는 감싸기 스크립트를 둡니다.

build-strict.ts, 경고가 하나라도 있으면 실패ts
import { spawnSync } from 'node:child_process'

const result = spawnSync('pnpm', ['exec', 'for-humanity', 'build'], { encoding: 'utf8' })
const output = `${result.stdout}${result.stderr}`
process.stdout.write(output)

// 터미널에서는 " WARN ", CI에서는 "[warn]"으로 찍힙니다
const warnings = output.split('\n').filter((line) => /\sWARN\s|\[warn\]/.test(line))

if (result.status !== 0 || warnings.length > 0) {
  console.error(`build failed: exit ${result.status}, ${warnings.length} warning(s)`)
  process.exit(1)
}

도구가 모르는 규칙은 저장소 쪽 검사로 보탭니다. 이 사이트는 빌드 뒤에 별도 스크립트로 가운뎃점, 용어 통일표, 연결어미 뒤 쉼표, 공개 범위의 비공개 명칭을 찾습니다. 빌드는 홈과 공개 문서만 검사하므로 내부 문서의 frontmatter 누락도 이 스크립트가 봅니다.

개발 서버도 검증 대상이었습니다. AI가 문서를 대량으로 고치는 동안 0.3.0 개발 서버는 전체 렌더를 겹쳐 돌리다 메모리 부족으로 멈췄습니다. 0.4.1에서 연속 저장을 합치고 갱신을 하나씩 처리하도록 바꾼 뒤 35회 연속 저장, 늦게 추가한 이미지, 잘못된 문서의 복구를 실제 개발 서버에서 확인했습니다.

자동 검사가 증명하지 못하는 범위도 적어 둡니다. 렌더링이 성공했다고 문서의 주장이 맞는 것은 아닙니다. 마지막에는 사람이 다음 질문으로 읽습니다.

참고 자료