正準 JSON
これは「同じ建物とは何か」の定義を書き下したものである。
原本 (.muro) は行順も綴りも自由なので、二つのファイルが同じ建物かどうかはテキストでは決まらない。正準形はその問いに一意な答えを与える — 同じ構成なら常に同じバイト列、違う構成なら必ず違うバイト列。
npx tsx src/cli.ts json examples/two-rooms.muro
{
"format": "koyu-canonical/1.0",
"koyu": "1.0",
"name": "二室",
"unit": "mm",
"grid": {
"X": [
0,
3600,
7200
],
"Y": [
0,
4500
]
},
"levels": {
"L1": {
"z": 0,
"h": 2400,
"slab": 150
}
},
"spaces": {
"/L1/a": {
"type": "room",
"at": [
"X1",
"Y1",
"X2",
"Y2"
],
"attrs": {
"daylight": 1,
"name": "居室A"
}
}
}
}
(実際の出力は続きを持つ。キーごとの一覧はスキーマにある。)
使い道は二つ
一つ。意味差分が正しいことを確かめる物差しである。koyu diff 自身に「あなたは正しいか」と聞いても意味が無いので、独立した判定が要る。テストはこれで書かれている。
二つ。.muro のパーサを持たないプログラムへの出口である。外部のビュアーやエクスポータ、MCP の canonical_json がここを読む。
土台ではない。意味差分は Model を受け取り、正準形を経由しない。層の合成は .muro の層に対して行われ、正準化はその後である。読み込む側は無く、系への入口は parse だけである — 原本は一つでよい、という選択である。
二つの版 — 形式の版と言語の版
文書が最初に名乗るのはこの形式自身の版である。koyu はその次に来る言語の版であって、形式の版ではない。
| キー | 何の版か | 上がるとき |
|---|---|---|
format | 正準 JSON の綴り — キーの集合・並び・照合順・正規化・数の綴り。現在は koyu-canonical/1.0 | minor はキーが増えたとき(増えたキーを持たない文書のバイトは変わらない)、major は既存の綴りが変わったとき |
koyu | 原本の言語版の素通し。書かれていなければ出ない | 言語の意味論が変わったとき |
二つが別なのは、同じ意味論を別のキーで綴り直すことがありうるからである。境界の向きを保存する a キーの追加は、言語を一語も変えずに綴りを変えた。逆に、言語版が上がっても綴りが変わらないことはある。
版宣言の無い原本には版を刻まない。言語版の既定は「そのツールの最新版」なので、刻めば著者の書いていない版を名乗ることになり、しかもツールの既定が動いた日に同じ入力のバイトが変わる。決定性はこの形式の側の約束であって、ツールの既定に預けるものではない。意味を固定したい原本は koyu 1.0 と書く。
三つの安定性の規則
1. 同じ構成からは常にバイト同一の JSON が出る
記録の形のキー(最上位・レベル・空間・境界・開口・seg・柱)はこのスキーマが定める固定順で、原本に由来するキー(レベル名・パス・アセット名・属性キー)は照合順に並ぶ。
spaces— パス順boundaries—betweenの辞書順(同一betweenは内容の正準順)zones/assets/polygons— キー順
宣言順に意味の無い配列も内容の正準順に並ぶ — 開口・seg・領域の合併・柱の通り名 x/y・描かれた線の端点。同じ構成を別の行順で書いても同じバイト列になる。
例外は columns である。柱の宣言順は意味だから並べ替えない。同じ通りの交点に二本は立たず先の宣言が勝つので、二行を入れ替えると実際に立つ柱が変わる。並べ替えると別の建物が同一のバイト列になり、この形式の存在理由が柱について失われる。順序が意味を持つ配列の正準順は、宣言順そのものである。
並べ替えを掛ける前に問うべきことは一つである — この配列の順序を入れ替えたら別の構成になるか。なるなら掛けてはならない。並べ替えは整形ではなく「順序に意味が無い」ことの表明である。
2. 合成後の、書かれた構成である
import・スパン・stack・帯は展開済みで残らない。
既定境界は出ない。接する空間の既定は壁だが、正準 JSON は書かれた構成のみを持ち、意味(既定壁を含む)は導出後のモデルが持つ。消費者は deriveDefaultBoundaries を適用してから意味を読むこと。
だから koyu check の「境界 1」と koyu json の "boundaries": [] は矛盾しない。前者は導出された意味の側の数、後者は書かれた構成の側の数である。
3. 書かれた表記を保存する
位置は通り参照のまま("at": "Y2+1820")、領域は通り名 4 つ組、境界の向きは a キー(先に書いた空間 — edge/swing はこの側から読む)。正準形は語り直さない。
例外は意味を持たない綴りだけである。
- 領域の逆順表記(
X2..X1)は座標昇順に正規化される - 描かれた線の端点の対は解決座標の昇順に正準化される(線分は向きを持たない)。綴りは通り参照のまま保つ
polygonの頂点列は幾何(巡回)なので並べ替えない
帯が導出する内側の切り位置は、書かれた綴りが存在しないため床規則で綴られる — その座標以下で最も大きい通り芯からのオフセット(オフセット 0 なら通り名だけ)。帯の両端と直交方向の両端は書かれた綴りのままである。
バイトの規範
規則 1 の「バイト同一」は、次の四つが定まっていて初めて意味を持つ。別の言語で書かれた実装も、これに従えば同じバイト列を出す。
符号化
UTF-8、改行は LF、字下げは空白 2 つ、文書の末尾に改行が一つ。非 ASCII はエスケープせず生のまま出る("name": "居室")。エスケープするのは JSON が要求するもの(" \ 制御文字)だけである。
照合順
符号位置の昇順であり、これは出力される UTF-8 バイトの昇順に等しい。ロケールの照合(localeCompare の類)は使わない。
JavaScript の < と既定の sort はここでは使えない。あれは UTF-16 コード単位順で、符号位置順と一致しない。
𠮟 (U+20B9F) 代用対 D842 DF9F UTF-8: F0 A0 AE 9F
﨑 (U+FA11) 単一の単位 UTF-8: EF A8 91
< では 𠮟 が 﨑 より小さいが、UTF-8 では 﨑 が前である。どちらも日本語の実在の字であり、差は理論上のものではない。基準を「この形式自身のバイト」に置くのは、素直に書かれた他言語の実装と一致する側だからである。実装は compareCanonical。
正規化
文字は NFC である。原本は読み込みのときに NFC へ正規化され、同一性(パス・uid・名)もそこで決まる — が を「か + 濁点」と綴った空間は、合成済みの が と綴った空間と同じ空間であり、両方書けばパスの重複エラーになる。正規化しなければ、見分けのつかない二つのキーが並ぶ文書が出る。
NFKC は採らない。㎡ を m2 に、① を 1 に書き換えてしまい、それは規則 3(書かれた表記の保存)に反する。
数
最短往復表記で綴る。0.30 は 0.3 として出る。丸めも桁揃えも単位変換もしない。指数が要る大きさ(1e+23・1e-7)は指数表記になる。