メインコンテンツまでスキップ

正準 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.0minor はキーが増えたとき(増えたキーを持たない文書のバイトは変わらない)、major は既存の綴りが変わったとき
koyu原本の言語版の素通し。書かれていなければ出ない言語の意味論が変わったとき

二つが別なのは、同じ意味論を別のキーで綴り直すことがありうるからである。境界の向きを保存する a キーの追加は、言語を一語も変えずに綴りを変えた。逆に、言語版が上がっても綴りが変わらないことはある。

版宣言の無い原本には版を刻まない。言語版の既定は「そのツールの最新版」なので、刻めば著者の書いていない版を名乗ることになり、しかもツールの既定が動いた日に同じ入力のバイトが変わる。決定性はこの形式の側の約束であって、ツールの既定に預けるものではない。意味を固定したい原本は koyu 1.0 と書く。

三つの安定性の規則

1. 同じ構成からは常にバイト同一の JSON が出る

記録の形のキー(最上位・レベル・空間・境界・開口・seg・柱)はこのスキーマが定める固定順で、原本に由来するキー(レベル名・パス・アセット名・属性キー)は照合順に並ぶ。

  • spaces — パス順
  • boundariesbetween の辞書順(同一 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.300.3 として出る。丸めも桁揃えも単位変換もしない。指数が要る大きさ(1e+231e-7)は指数表記になる。

隣り合う頁