ADR-0036 は glossary の SSoT モデルを「folio が vocabulary.yaml に canonical 定義を上流で author し、 そこから hover tooltip と glossary.html の 2 面を derive する」と定めた。 当時 vocabulary.yaml は唯一の編集起点であり、 単一 SSoT からの derive という構造は自己整合していた。
その後 ADR-0050 の SSoT-first 転換で、 glossary は generator contract (folio-glossary.glossary.yaml) を持つ pack になった。 contract は各語に formal_def (正式定義) を持ち、 ADR-0051 の語彙では正本 (edit-SSoT) である。 ここで定義本文は vocabulary.yaml の definition と contract の formal_def に二重保持されることになった — vocabulary.yaml は folio fix の tooltip populate 等が読む runtime SSoT ゆえ撤去できないからである。
folio-9inj はこの二重保持を「手動同鏡 (片側編集禁止 MUST)」という規律で塞ぎ、 集合完全一致 + 定義逐語一致を機械強制した。 しかしこれは導出ではなく規律であり、 2 つの構造的な綻びを残した。 第一に、 vocabulary.yaml ⊃ contract の drift が実際に発生した — vocab 側にだけ 3 語を足した編集が contract を stale のまま残し、 治癒 ticket folio-7ts2 が 33→36 語の収録で塞いだ (folio-7ts2 は drift の *治癒* であって発生源ではない)。 第二に、 同鏡 gate は集合と定義本文しか見ないため、 語順・domain・header・整形といった面は誰も検査していなかった。 二重保持を人間規律で守る限り、 「片側だけ編集する」という操作が物理的に可能なまま残る。
同時に、 提示層にも取り残しがあった。 folio 自身の design-intent/glossary.html は bin/folio の内部 emitter が出力する定義リスト (<dt>/<dd> の平坦な列) のままで、 glossary-pack が生成するリッチな dual-audience 提示層 (domain 区分・平易定義・機械層 term レコード) は成果物として存在しながらcanonical には成っていなかった。 folio の北極星 (非エンジニアが読める文書) に対して、 folio 自身の用語集だけが旧型で取り残されている状態である。 これは ADR-0049 / vision で確立した「out-of-scan canonical を pack pipeline の生成物にする」型を glossary へ適用すれば解消する。
glossary の SSoT を次の 3 層に再指定する。 (1) glossary contract YAML = 唯一の edit-SSoT — 語の追加・削除・定義改訂はここだけを編集する (ADR-0051 の「正本 (edit-SSoT)」であり、 P-11 の隔離先 harness に居住し続ける)。 (2) design-intent/vocabulary.yaml = contract からの生成 projection — 全域関数 (canonical ← canonical / domain ← domain / definition ← formal_def) で機械導出する runtime SSoT。 folio fix の tooltip populate・folio validate の glossary gate・Layer 1 consumer 向けの内部 derive がこれを読む役割は不変で、 変わるのは誰が書くかだけである (人間 → projection script)。 (3) design-intent/glossary.html = canonical WHAT (提示層) — folio build が pack pipeline へ委譲して生成する (§2.2)。
これにより folio-9inj の「手動同鏡」は導出に置換される。 同鏡は「2 つの正本が一致していることを人間が保つ」規律だったが、 projection では正本は 1 つだけで、 他方は毎回そこから作り直される派生物である。 片側編集は禁止事項ではなく意味を持たない操作になる (次の再生成で消える)。 これは double-SSoT を「導出」で構造保証する DITA / literate programming の prior art と同型で、 folio が既に index.html・chrome・glossary の tooltip で採っている型の水平展開である。
folio build は folio self-host root において、 glossary.html の生成を generator の pack pipeline (assemble-glossary.sh + inject-prose.sh) へ委譲する。 CLI は「何を生成するか」を所有し続け、 「どう組み立てるか」は pack が持つ — この関係を build-orchestrates-pack と呼ぶ。 Layer 1 consumer root では従来どおり内部 emitter が vocabulary.yaml から derive する (consumer は generator を持たないため。 ADR-0036 §2.1 の ship→derive はこの経路で不変)。
委譲の設計には 3 つの不変条件を課す。 (a) 単一述語による self-host 検出: 「sibling generator に grandfather manifest と contract dir が両実在する」という gate (s) spec-contract-pairing と同一の述語を共通 helper へ抽出し、 全消費点がそれを呼ぶ。 marker の有無・vocab の非空・環境変数を self-host 信号に代用しない (代用は 2 つの「self-host とは何か」を作り、 必ず drift する)。 (b) 3 消費点の lockstep: build の書込 path・build --check の対称 gate・validate の nav-regen-drift gate は、 いずれも単一の emit dispatcher だけを呼ぶ。 これは可読性のためではなく構造的な要請である — 書込 path だけが旧経路に残ると、 folio build 1 回でリッチな提示層が素朴版へ silent revert し、 以後 gate は「committed (素朴) == emit (素朴)」で恒真 PASS になって劣化を隠す。 (c) 前提崩れは hard-fail: 委譲入力の不在、 contract の graph 前方照会 edge や doc_id の欠落、 pipeline 失敗、 出力の前提崩れ (空・構造マーカー欠如・marker 欠如・下限サイズ割れ) は全て hard-fail であり、 内部 emitter へ fallback しない。 fallback は (b) と同じ silent revert を招く。
提示層に必要な folio scanner 互換 head (生成 marker・doc-type・status・相対 @id) は、 contract の canonical_page block 由来の条件 emit とする。 同 block を持たない contract (generator の demo suite 等) の出力には marker を付けない — 無条件に付けると demo 成果物が「folio build の生成物」という虚偽の出自を表示し、 「marker 持ち page だけを drift 検査する」という marker keystone の設計自体を汚染するからである。 scanner 側は無改修とする。
projection stale gate: verify-glossary-parity.sh の同鏡節を、 「committed vocabulary.yaml == fresh projection」の whole-file byte 比較へ置換する。 byte 一致は旧検査 (集合一致 + 定義逐語一致) を構造的に含意する上位集合であり、 加えて旧検査が見なかった header・語順・domain・整形の drift も捕捉する。 gate は既に CI の deterministic floor へ blocking 配線済ゆえ、 配線を継承する。 非循環性を守るため、 生成物 warning header は projection script 内のリテラル定数とする — committed から header を読み戻すと、 header 改竄が fresh 側へも伝播して byte 比較が恒真 PASS 化する。
nav-regen-drift の再配線: diff 対象を emit dispatcher の出力へ切替える (self-host では pack pipeline 出力)。 pack の footer は生成時刻を含み毎分変化するため、 比較前に生成時刻だけを正規化する。 正規化の scope は接頭辞付き部分文字列に厳密に限る — 裸の日付を全域置換したり行ごと削除したりすると、 版日付・承認・検証状態を潰す drift masking になる。 build --check も同一 normalize で対称化する。 term 集合の対称差診断は、 pack (見出し形) と内部 emitter (span 形) の両 shape を抽出対象にする (片方だけ対応させると他方の経路で診断が黙って空になる)。
配信 asset の同期 gate: glossary-pack は他 pack と違い style を inline せず <link rel="stylesheet" href="srs.css"> で外部参照するため、 配信 root (design-intent/) に srs.css の実体が要る。 Leg 0 は源泉 (design-system/srs.css) を残したまま配信 root へ copy を置く option 1 を採った (pack の href 無改変)。 ただし放置された copy は源泉から drift するのが本 repo の前科であり (folio-u7oc: srs.css の WCAG fix が canonical へ未伝播のまま 10 日 silent drift)、 かつ実害の質が悪い — render-gate は配信 root の copy を読んで contrast/幾何を検査するため、 copy が stale だと「古い token で緑」という vacuous-green を作り、 WCAG token drift クラスを再生産する。 したがって copy を許す前提条件として verify-asset-sync.sh (登録対の whole-file byte 比較・REGISTRY が default-block の SSoT) を併設し、 CI の deterministic floor へ blocking 配線する。 inline する canonical (vision.html) の drift は canonical-drift gate が byte に含めて捕捉するが、 外部参照される asset の中身は byte 比較の外側にあるため本 gate が唯一の番人であり、 未配線の gate は前提条件を discharge しない (gate は tree に在るだけでは gate でなく、 走って初めて gate)。
本 § は glossary に限らず全 pack に適用される一般宣言である。 生成 pack の入力は 2 つある: contract YAML = 機械層の edit-SSoT (構造・識別子・正式定義など機械が消費する値) と、 prose manifest = 人間層の edit-SSoT (平易な散文。 opus が著作する)。 prose manifest は contract からの導出物ではない — 平易化は意味の営みであり、 機械的に導出できない (自由文の fidelity は独立 reviewer の領分であって決定的プログラムの領分ではない)。 したがって pack は双 edit-SSoT 構造を持つ。
ADR-0050 §3 は SSoT-first 転換の帰結として「authoring の単線化 — 編集は contract YAML の 1 箇所になる」を挙げた。 本 § はこれを否定するのではなく精緻化する (refine): 機械層 normative の編集は contract YAML 単線のままで、 単線化の主張はその射程で成立し続ける。 本 § が別 edit-SSoT として切り出すのは人間層の平易 prose だけであり、 これは機械層の値ではなく「機械が導出できない意味の営み」の置き場である (両者は対象が異なる = 矛盾でなく層の精緻化)。
双 edit-SSoT は SSoT 唯一性 (P-5) と衝突しない。 両者は異なる対象の正本だからである (機械層の値 / 人間層の散文)。 両者の整合は 3 層で束縛する: (1) inject の集合一致 — HTML のスロット id 集合と manifest の key 集合が完全一致でなければ注入は fail-closed (未充填も orphan も拒否)。 (2) repro-build byte — 同一入力からの再生成が byte 一致する決定性。 (3) fidelity ceiling — 散文が contract を歪めず捏造していないかを独立 reviewer が検査する。 (1)(2) は機械が、 (3) は LLM が担う — これが folio の機械/LLM 境界の適用である。 本 § は帰属の明文化のみで、 prose の物理配置・経路・実装は一切変更しない。
本 ADR は以下を supersede する (部分。 ADR-0050 §2.5 / ADR-0026 先例に従い、 当該 ADR の他の決定は不変ゆえ dc:replaces でなく dc:references + 本 § / §5 で trace する。 frozen 本文は P-6 のもと不触で、 supersede 関係は本 ADR 側にのみ記す):
vocabulary.yaml SSoT + HTML view は生成 projection) — ADR-0036 §2.1 は自身を「ADR-0034 §2.8 の extend」と位置づけ、 その SSoT モデルを継承した。 本 ADR の再指定はその継承鎖をさかのぼり、 ADR-0034 §2.8 の「vocabulary.yaml が SSoT」の部分にも及ぶ (folio self-host に限る。 「HTML view は生成 projection」という §2.8 の骨格自体は不変で、 本 ADR は projection の *源* を vocabulary.yaml から contract へ 1 段さかのぼらせたに過ぎない)。 Layer 1 consumer では §2.8 はそのまま有効。vocabulary.yaml に canonical 定義を上流で author する」条項 — folio 自身では vocabulary.yaml はもはや author されず、 glossary contract からの生成 projection である (§2.1)。 同 § の他の決定は存続する: consumer が folio-closed 語を derive で受け取り自身では folio 語の定義を書かないこと、 plugin-resident SSoT として同梱し consumer に vocabulary.yaml を置かないこと (copy 配布の却下)、 探索 candidate の設計はいずれも不変。glossary.html は build が vocabulary.yaml から derive する生成 projection」条項 — folio self-host では canonical WHAT (提示層) は glossary contract から pack pipeline 委譲で生成される (§2.2)。 Layer 1 consumer ではこの条項はそのまま有効 (内部 emitter が vocabulary.yaml から derive する)。 同 § の他の決定は存続する: 描画が hover tooltip のみで用語 mark から glossary へ deep-link しないこと (folio-ovi の routing 衝突 amend)、 tooltip の供給元が vocabulary.yaml であること、 定義を 2 箇所に author しない double-SSoT 排除の原理はいずれも不変 — 本 ADR はその原理をより強く適用する (規律から導出へ)。glossary.html 全体が vocabulary.yaml からの regen と byte 一致」条項 — 比較の右辺が emit dispatcher の出力になり (self-host では pack pipeline)、 生成時刻の正規化が加わる (§2.3)。 gate の位置と性格は不変: nav-regen-drift へ畳む設計、 byte 一致で定義 drift と構造 drift の双方を捕捉すること、 term 集合の対称差を報告して drift の所在を surface すること、 slug 一意性を単一 SoT で fail-closed 検出することはいずれも存続する (slug の供給元が self-host では contract の明示 slug になる)。 term-marked の best-effort tier・keystone 設計も不変。vocabulary.yaml を直接開けば済んだ素朴さは失われる)。 projection stale gate が忘却を検出するため事故にはならないが、 手順は増えるvocabulary.yaml は生成物になったため、 そこに書かれていた rationale は保持できない。 語ごとの編集判断に効くものは edit-SSoT である contract へ、 語彙運用に効くものは projection の header template へ移設したvocabulary.yaml) は完全に不変 — 本 ADR の転換は self-host root にのみ効く| 案 | 採用可否 |
|---|---|
| contract = edit-SSoT / vocabulary.yaml = 生成 projection (採用) | 二重保持を導出へ置換し drift クラスを構造的に消す。 runtime SSoT としての vocabulary.yaml の役割は保つため consumer 経路・tooltip 機構は無改修 |
| 手動同鏡を維持し gate を強化する | 不採用 — gate をいくら強めても「片側だけ編集できる」構造は残り、 検出は常に事後。 folio-7ts2 が治癒した drift は gate 不在下の実害だが、 二重保持を残す限り同型の穴 (gate が見ていない面) が再生産される (実際 header・語順・整形は無防備だった) |
vocabulary.yaml を撤去し contract を直接読ませる | 不採用 — folio fix / validate / consumer derive が読む runtime SSoT を消すと、 consumer は generator を持たないため folio-closed 語を受け取れなくなる (ADR-0036 §2.1 の ship→derive が壊れる)。 projection なら両立する |
| 提示層を内部 emitter の出力のまま据置く | 不採用 — folio 自身の用語集だけが非エンジニアに届かない旧型で残り、 self-dogfood が glossary で成立しない。 pack の成果物は既に存在しており、 canonical に成っていないだけだった |
| prose manifest を contract へ統合する (案A) | 不採用 — 平易 prose を機械層 contract に同居させ edit-SSoT を単一化する案。 8dkl の F6 裁定 (案E 採択) で却下: 平易化は機械が導出できない意味の営みゆえ、 統合しても「1 つの正本」にはならず contract 内に別性質の層が同居するだけで、 機械層 normative の単線性 (ADR-0050 §3) を濁す。 §2.4 の双 edit-SSoT + 3 層束縛 (inject 集合一致 / repro-build byte / fidelity ceiling) を採る |
| 委譲時に前提が崩れたら内部 emitter へ fallback する | 不採用 — folio build 1 回で提示層が素朴版へ silent revert し、 以後 gate は「committed (素朴) == emit (素朴)」で恒真 PASS になって劣化を隠す。 hard-fail の方が loud で安全 (§2.2 (c)) |
vocabulary.yaml) の規範反映を要する。 本 ADR は決定を記録するが規範文 (rules / REQ) は書き換えない — 反映 cell = bd folio-fc0s (起票済)。 ADR-0050 §2.3 → 規範反映先確定の先例と同型。folio validate 3 gate clean (nav-regen-drift / glossary / spec-contract-pairing)、 folio build --check clean、 glossary-parity 敵対回帰、 glossary pack の floor battery、 per-shape MK self-test (self-host = 見出し形 / consumer = span 形 / header / 定義 の各構造クラスに実弾)。 gate I/J ceiling と本 ADR 本文の Phase F ceiling は Leg B (admin) が担う。