본문으로 건너뛰기

canopy — 라이브 대규모 프로젝트

가상의 라이브 서비스 MMO 하나입니다. 워크북 42개 · 정의된 이름 549개이고, 테이블의 경계가 워크북의 정의된 이름인 레이아웃으로 읽습니다 — named-range.

dotnet run --project src/Tabbit.csproj -- --recipe samples/canopy/recipe.jsonc

값은 전부 합성입니다. 재현하는 것은 규모와 표기입니다 — 테이블 수, 컬럼 수, 중첩의 비율, 그리고 이 레이아웃이 시트 안에 선언해 두는 제약의 종류. 근거는 샘플 색인에 있습니다.

여기 있는 것

무엇어디
워크북 42개xlsx/생성물입니다. 손으로 고치지 않습니다
같은 워크북의 .xlsbxlsb/ — 대조용. 아래 「두 컨테이너」
격자 40개schema/정본. 표기가 여기 있습니다
생성기gen/
변환 설정recipe.jsonc · recipe-xlsb.jsonc
산출물out/ — 바이너리 · compact JSON · 인코딩 보고서 · 스키마 기준선

전 언어 산출물은 여기 없습니다. 「변환하면 무엇이 나오는가」는 sprout가 답하고, 이 샘플은 「테이블 515개에서 무엇이 일어나는가」에 답합니다. 같은 답을 17개 언어로 100 MB 커밋하는 것은 값이 없습니다.

같은 이유로 htmlsummary도 없습니다. html은 이 코퍼스에서 84 MB가 되고, summary중첩 필드가 있는 모델을 거부합니다 — 컬럼의 10분의 1이 중첩인 것이 이 샘플의 요점이므로, 그 거부는 여기서 결함이 아니라 기록입니다.

규모

항목smalllive
워크북42개 · 5.1 MB42개
정의된 이름549개549개
변환되는 테이블515개515개
22,863735,680
1,235,6258,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/TableFaction 하나
:requiredInObject중첩 레코드의 멤버 전부
:asset에셋 경로 컬럼
key:min~max · strkey정수 인덱스와 문자열 인덱스
number · string · text · bool · bit · floatbit는 2진 문자열입니다
중첩 — slot[0]스칼라 배열. 빈 자리가 섞입니다 (AllowArrayGaps)
중첩 — entry[0]["ItemId"]레코드의 배열. 접히면 entry: [{...}, {...}]가 됩니다
이름 없는 컬럼시트가 데이터 사이에 끼우는 설명 칸. 변환에서 빠집니다
셀 값 -「없음」. 빈 칸은 오류입니다tabbit과 정반대입니다
지역 변종 _BCGL · _BCCN95개. 이름을 자르지 않고 그대로 둡니다
매트릭스 표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:NN자리 2진 문자열
?N <규칙>N%의 행이 -가 됩니다 — 이 레이아웃이 「없음」을 적는 방식