canopy — 라이브 대규모 프로젝트
가상의 라이브 서비스 MMO 하나입니다. 워크북 42개 · 정의된 이름 549개이고, 테이블의 경계가
워크북의 정의된 이름인 레이아웃으로 읽습니다 — named-range.
dotnet run --project src/Tabbit.csproj -- --recipe samples/canopy/recipe.jsonc
값은 전 부 합성입니다. 재현하는 것은 규모와 표기입니다 — 테이블 수, 컬럼 수, 중첩의 비율, 그리고 이 레이아웃이 시트 안에 선언해 두는 제약의 종류. 근거는 샘플 색인에 있습니다.
여기 있는 것
| 무엇 | 어디 |
|---|---|
| 워크북 42개 | xlsx/ — 생성물입니다. 손으로 고치지 않습니다 |
같은 워크북의 .xlsb | xlsb/ — 대조용. 아래 「두 컨테이너」 |
| 격자 40개 | schema/ — 정본. 표기가 여기 있습니다 |
| 생성기 | gen/ |
| 변환 설정 | recipe.jsonc · recipe-xlsb.jsonc |
| 산출물 | out/ — 바이너리 · compact JSON · 인코딩 보고서 · 스키마 기준선 |
전 언어 산출물은 여기 없습니다. 「변환하면 무엇이 나오는가」는 sprout가 답하고, 이 샘플은 「테이블 515개에서 무엇이 일어나는가」에 답합니다. 같은 답을 17개 언어로 100 MB 커밋하는 것은 값이 없습니다.
같은 이유로 html과 summary도 없습니다. html은 이 코퍼스에서 84 MB가 되고, summary는
중첩 필드가 있는 모델을 거부합니다 — 컬럼의 10분의 1이 중첩인 것이 이 샘플의 요점이므로,
그 거부는 여기서 결함이 아니라 기록입니다.
규모
| 항목 | small | live |
|---|---|---|
| 워크북 | 42개 · 5.1 MB | 42개 |
| 정의된 이름 | 549개 | 549개 |
| 변환되는 테이블 | 515개 | 515개 |
| 행 | 22,863 | 735,680 |
| 셀 | 1,235,625 | 8,730,058 |
| 최상위 컬럼 | 5,838개 | 같음 |
| 작업용 시트 | 12개 | 12개 |
dotnet run --project samples/canopy/gen -- --scale small # 커밋된 것
dotnet run --project samples/canopy/gen -- --scale live --out xlsx-live # 전량
재현하는 표기
이 레이아웃에는 tabbit에 없는 것이 모여 있습니다. 격자에 자리가 없으면 그 분기에 게이트가
없습니다.
| 표기 | 어디에 |
|---|---|
| 테이블 = 정의된 이름 | 549개 전부 |
| 헤더 2행 + 개수가 다른 제약 행 | 격자마다 5~8줄. 데이터 시작 행이 테이블마다 다릅니다 |
:required · :min · :max | 거의 전 컬럼 |
:enum | 문자열 컬럼의 따옴표 목록. 숫자 컬럼에서는 값 하나로 읽히므로 쓰지 않습니다 |
:links — 단일·복수 대상 | 복수는 한 셀 안에서 줄바꿈으로 나눕니다 (LootLine) |
:link — 구형 Json/Table | Faction 하나 |
:requiredInObject | 중첩 레코드의 멤버 전부 |
:asset | 에셋 경로 컬럼 |
key:min~max · strkey | 정수 인덱스와 문자열 인덱스 |
number · string · text · bool · bit · float | bit는 2진 문자열입니다 |
중첩 — slot[0] | 스칼라 배열. 빈 자리가 섞입니다 (AllowArrayGaps) |
중첩 — entry[0]["ItemId"] | 레코드의 배열. 접히면 entry: [{...}, {...}]가 됩니다 |
| 이름 없는 컬럼 | 시트가 데이터 사이에 끼우는 설명 칸. 변환에서 빠집니다 |
셀 값 - | 「없음」. 빈 칸은 오류입니다 — tabbit과 정반대입니다 |
지역 변종 _BCGL · _BCCN | 95개. 이름을 자르지 않고 그대로 둡니다 |
| 매트릭스 표 | SystemMatrix — 219행 × 482컬럼. 짝 테이블 SystemMatrixColumn이 컬럼 id를 돌려줍니다 |
| 정의된 이름이 없는 시트 | 12개. 로그 한 줄로 지나갑니다 |
| 읽지 않는 격자 | Tile.xlsx의 34개. recipe가 이름으로 제외합니다 |
Tile.xlsx를 남겨 둔 것은 의도입니다. 헤더도 키도 없는 정수 격자는 그쪽 에디터가 쓰고 그쪽
익스포터가 읽는 것이고 이 레이아웃에는 읽을 방법이 없으므로, 조용히 실패하게 두는 대신
recipe가 이름을 부릅니다. 언젠가 그 워크북에 정의된 이름이 하나 생기면 그때 눈에 띕니다.
두 컨테이너
xlsx/와 xlsb/는 같은 값을 담은 다른 파일 형식입니다. 임포터가 둘을 완전히 다른
경로로 읽습니다 — 한쪽은 XML의 zip이고, 다른 쪽은 BIFF12 레코드와 xl/workbook.bin에서
직접 읽는 정의된 이름입니다(설계).
그래서 이 쌍은 시연이 아니라 게이트입니다.
dotnet run --project src/Tabbit.csproj -- --recipe samples/canopy/recipe.jsonc
dotnet run --project src/Tabbit.csproj -- --recipe samples/canopy/recipe-xlsb.jsonc
diff -r samples/canopy/out/json samples/canopy/out-xlsb/json
컨테이너 말고는 다른 것이 없으므로 차이가 나오면 그것은 리더의 결함입니다. 2026-08-26 실측 — 테이블 515개의 JSON과 바이너리가 전부 일치하고, 다른 것은 매니페스트의 타임스탬프뿐입니다.
.xlsb는 생성기가 쓰지 못합니다. gen/to-xlsb.ps1이 Excel에게 다시 저장하게 하며, 그래서
빌드의 일부가 아닙니다. .xlsb는 커밋되어 있습니다 — 워크북을 다시 만들었을 때만 이
스크립트를 돌리면 됩니다.
격자 40개와 테이블 515개
한 격자를 여러 테이블이 씁니다. 규모를 만드는 것은 테이블의 수이고 표기를 증명하는 것은 격자이므로, 515개의 서로 다른 컬럼 구성을 지어내는 것은 어느 쪽에도 보태지 않습니다.
schema/<격자>.tsv 하나가 사각형 하나의 규격입니다. 이 레이아웃이 사각형 안에 헤더 2행과
개수가 정해지지 않은 제약 행을 두므로, 격자 파일도 같은 순서로 적혀 있습니다.
@name 프로퍼티 이름 → 사각형 1행
@type 타입 → 사각형 2행
:required 컬럼별 셀 → 사각형 3행
:min … 그리고 이 테이블이 선언하는 만큼
@gen 생성 규칙 → 생성기가 소비합니다
제약 행이 위치가 아니라 첫 칸의 이름으로 식별되는 것이 이 레이아웃의 특징이고, 그래서 격자마다 줄 수가 다릅니다. 전 테이블이 같은 줄 수를 가지면 위치로 읽는 구현도 통과합니다.
어느 격자가 어느 워크북의 어느 이름이 되는지는 gen/tables.tsv가 정합니다. 생성 규칙은 sprout의 것과 같고, 여기에 둘이 더 있습니다.
| 규칙 | 무엇 |
|---|---|
bit:N | N자리 2진 문자열 |
?N <규칙> | N%의 행이 -가 됩니다 — 이 레이아웃이 「없음」을 적는 방식 |