dsh-perm-gate 2.1.2 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.ja.md +108 -0
  2. package/CHANGELOG.ko.md +104 -0
  3. package/CHANGELOG.md +236 -0
  4. package/INSTALL.ja.md +41 -21
  5. package/INSTALL.ko.md +37 -19
  6. package/INSTALL.md +46 -23
  7. package/INSTALL.zh.md +33 -17
  8. package/README.ja.md +52 -12
  9. package/README.ko.md +53 -13
  10. package/README.md +68 -10
  11. package/README.zh.md +41 -10
  12. package/assets/allow1.png +0 -0
  13. package/assets/allow2.png +0 -0
  14. package/assets/gateoff.png +0 -0
  15. package/assets/llms.png +0 -0
  16. package/assets/permlist.png +0 -0
  17. package/assets/setting-head.png +0 -0
  18. package/assets/setting-rules.png +0 -0
  19. package/cordis.patch.yml +11 -8
  20. package/docs/baseline-guard.md +56 -0
  21. package/docs/plan-rules-to-settings.md +217 -0
  22. package/docs/rules-format.en.md +294 -0
  23. package/docs/rules-format.md +279 -0
  24. package/docs/ui-gap-analysis-and-branch-permissions.md +338 -0
  25. package/dsh.plugin.json +2 -2
  26. package/examples/permissions.example.yaml +16 -1
  27. package/lib/allowlist.d.ts +27 -0
  28. package/lib/allowlist.js +103 -1
  29. package/lib/cli.js +11 -14
  30. package/lib/client.js +484 -6
  31. package/lib/client.js.map +1 -1
  32. package/lib/command-dispatcher.d.ts +22 -0
  33. package/lib/command-dispatcher.js +97 -0
  34. package/lib/command-semantics.d.ts +62 -0
  35. package/lib/command-semantics.js +6 -0
  36. package/lib/compiler.d.ts +11 -0
  37. package/lib/compiler.js +16 -0
  38. package/lib/config.d.ts +73 -12
  39. package/lib/config.js +56 -14
  40. package/lib/dry-run.d.ts +87 -0
  41. package/lib/dry-run.js +110 -0
  42. package/lib/engine.js +52 -2
  43. package/lib/evaluate.js +76 -2
  44. package/lib/events.d.ts +56 -2
  45. package/lib/events.js +92 -0
  46. package/lib/index.js +106 -26
  47. package/lib/parsers/git.d.ts +43 -0
  48. package/lib/parsers/git.js +404 -0
  49. package/lib/parsers/shell-cmds.d.ts +32 -0
  50. package/lib/parsers/shell-cmds.js +293 -0
  51. package/lib/preset.d.ts +13 -0
  52. package/lib/preset.js +17 -0
  53. package/lib/risk.d.ts +17 -3
  54. package/lib/risk.js +17 -3
  55. package/lib/rule-chain.js +13 -22
  56. package/lib/rule-dims.d.ts +35 -0
  57. package/lib/rule-dims.js +36 -0
  58. package/lib/rule.d.ts +36 -1
  59. package/lib/rule.js +56 -8
  60. package/lib/rules-view.d.ts +52 -0
  61. package/lib/rules-view.js +126 -0
  62. package/lib/runtime.d.ts +121 -7
  63. package/lib/runtime.js +204 -25
  64. package/lib/shell.d.ts +14 -0
  65. package/lib/shell.js +17 -0
  66. package/package.json +19 -2
  67. package/scripts/patch-permission-glyph.mjs +225 -42
package/CHANGELOG.ja.md CHANGED
@@ -5,6 +5,114 @@
5
5
  形式は [Keep a Changelog](https://keepachangelog.com/ja/1.1.0/) に従い、
6
6
  このプロジェクトは [Semantic Versioning](https://semver.org/spec/v2.0.0.html) に準拠します。
7
7
 
8
+ ## [2.4.1] - 2026-09-17
9
+
10
+ ### 修正
11
+
12
+ - 承認履歴が、フラグ付きの呼び出しがなぜレビューされずに実行されたのかを説明する唯一のイベントについて、生の `preset-passthrough` 文字列を表示していました(ティアは `approval: ask` を宣言しているのに、セッションが `never` で上書きされ、ゲートの ask がパススルーに降格したためです)。他の判定と同じく読みやすいラベルで表示されるようになりました。
13
+
14
+ ## [2.6.1] - 2026-09-25
15
+
16
+ ### 修正
17
+
18
+ - **入力欄グリフのパッチが DSH 0.1.5 で再び機能します。** `scripts/patch-permission-glyph.mjs`
19
+ は `["permissive",` の直後にエントリを挿入していましたが、そのキーは DSH 0.1.2 がグリフを
20
+ 描いていた値で、0.1.5-rc.2 のバンドルにはどこにも存在しません。そのためパッチは
21
+ `anchor not found` で失敗し、両ティアはテキストのみのままでした。取得元はティアごとに
22
+ `GLYPH_TARGETS` で宣言し、アンカーは**解決**します —— まず `["<key>",`、次にバンドルが使う
23
+ 定数別名(`danger-full-access` は `[FULL_ACCESS,`)。ホストが描かないキーは、Map が実際に
24
+ 持つキーと一緒に報告されます。
25
+ - **UI を提供していない DSH インストールにパッチを当てなくなりました。** 候補は profile
26
+ スコープを先、CLI インストールを次に並べ、同じバンドルを持つ**他の**インストールは黙って
27
+ 見送らず出力に列挙します。このプロジェクトのホストで実測:グローバル CLI インストールが
28
+ ランチャー管理のランタイムを隠していたため、パッチは成功と報告しつつピッカーは
29
+ アイコンなしのままでした。
30
+
31
+ ### 変更
32
+
33
+ - **各ティアは、共有するファイルサンドボックスのグリフを付けます(共通の盾+目ではなく)。**
34
+ 「自动审查」は `workspace-write` のグリフを、「自动审查(高权限)」は `danger-full-access` の
35
+ グリフをコピーするため、アイコンはそのティアが実際に与えるアクセスと一致します。同一アイコンの
36
+ 旧挙動は、`permissive` のグリフを持っていた 0.1.2 の Map に由来します。
37
+ - **2 つのグリフは両方適用するか、両方とも適用しません。** 解決できない取得元キーは何も書かない
38
+ 拒否であり、部分的にパッチされたバンドルにはなりません。
39
+
40
+ ## [2.4.0] - 2026-09-17
41
+
42
+ ### 変更
43
+
44
+ - **P3 LLM 分類器はエスカレート専用になり、拒否できなくなりました。** ハードカテゴリ
45
+ (`deletion` / `credential` / `remote` / `system` / `bulk`) の `risky` 判定はこれまでパネルなしで
46
+ **自動拒否**していましたが、現在は**人間への確認(ask)を維持**します。拒否は決定論的レイヤー
47
+ (P0 ハード拒否、拒否キーワードのブラックリスト、明示的な `deny:` ルール)だけに属します。
48
+ 確率的な判定が、異議を唱えられないブロックを下してはならないからです。実測: グレーダーは無害な
49
+ `git commit -F …` を **`remote`** と判定し、自動拒否は承認するパネルも再試行する grant も残しませんでした
50
+ (たまたま `safe` と判定された手動リトライだけが通りました)。これは「高リスク操作は**決定論的に**
51
+ 遮断し、LLM の判断には委ねない」というプロジェクト自身のルールとも整合します。
52
+
53
+ ### 追加
54
+
55
+ - ハードリスクカテゴリは `neutral` との重要な違いを一つだけ保ちます: **決して学習されません**。
56
+ 人間の承認を繰り返しても `deletion`/`credential`/`remote`/`system`/`bulk` の判定が自動許可に
57
+ 沈殿することはありません(従来は自動拒否の副作用としてのみ成立していましたが、現在は明示的な性質です)。
58
+
59
+ ### 動作上の注意
60
+
61
+ - `approval: never` では、ゲートが配信できない ask は従来どおりパススルーに降格するため、分類器が
62
+ フラグを立てた呼び出しは**実行されます**(以前は自動拒否でした)。これは「決定論的に危険なものだけを
63
+ 拒否し、それ以外は交渉する」の直接の帰結です —— 交渉には人間が必要で、`never` は人間がいないことを
64
+ 意味します。フラグを実際に受け取りたい場合は `approval` が `ask` のティアで運用してください。
65
+
66
+ ## [2.3.0] - 2026-09-17
67
+
68
+ ### 追加
69
+
70
+ - **ゲートの停止はもはや沈黙しません。** セッションの権限プリセットが `gatePresets` の外にあるとき、
71
+ ゲートは停止して何も記録しませんでした —— そのためツール呼び出しは、ゲートが検査して許可した
72
+ 呼び出しと見分けがつきませんでした。現在は (session, preset) の遷移ごとに **1 件だけ**
73
+ `stand-down` 通知を記録し(呼び出しごとではありません)、プリセット・スコープ・P0 ハード拒否が
74
+ 無効である事実を明示します。ブラウザは入力欄の上に常駐の **GATE OFF** ストリップとして表示し、
75
+ 承認履歴には `ゲート停止` タグが出ます。
76
+ - 自动审查 設定カードがゲート自身のプリセットスコープ(`gatePresets`、既定は
77
+ `permissive` / `permissive-full`)とその外側で何が起きるかを明示するため、スコープはティアを
78
+ 設定する場所で確認できます。
79
+
80
+ ### 変更
81
+
82
+ - **P0 の文書上の位置づけはグローバルではなくスコープ付きです。** P0 ハード拒否は*ゲートのプリセット
83
+ スコープ内*で単調かつ交渉不能です。プリセットをまたぐとゲートは P0 を含めて完全に停止します ——
84
+ 選択されたティア自身のポリシーがそのセッションを所有するためです。コードは常にこう動作していましたが、
85
+ `AGENTS.md` と 4 つの README は逆を主張しており、`danger-full-access` が「P0 は依然適用される」と
86
+ 読めてしまいました。`gatePresets: ['*']` を設定すると P0 は再びグローバルになります。
87
+
88
+ ### 修正
89
+
90
+ - `DEFAULT_GATE_PRESETS` / `resolveGatePresets` を `config.ts`(schemastery を import しています)から
91
+ 依存のない `preset.ts` へ移し、ブラウザ側が node 専用依存をクライアントバンドルに引き込まずに
92
+ スコープを描画できるようにしました。`config.ts` は両方を再エクスポートするため、既存の import は
93
+ 変更ありません。
94
+
95
+ ## [2.2.0] - 2026-09-17
96
+
97
+ ### 修正
98
+
99
+ - **P0 ハード拒否が、4 つのシェルツール以外をすべて素通りさせていました。** `hardDenyReason` はシェル検査をローカルの正規表現 `/^(?:bash|pwsh|sh|cmd)$/` で門番していましたが、これは `shell` / `terminal` / `powershell` に**一致しません**。`shell` は DSH の主要シェルツールであるため、P0 のシェル検査(保護パスへのリダイレクト)は**最も一般的な呼び出し形に対して無効**でした。実測、同一コマンド `echo x > /etc/passwd`:`bash` 経由は遮断、`shell` 経由は**通過**。`engine.ts` は `evaluate.ts` から `SHELL_TOOLS` を取り込むよう変更しました——同じ事実の複製が 3 か所にあり、完全だったのは 1 つだけでした。
100
+ - **`git push --delete` が通常の push として解析されていました。** `hasDelete` は `analyzeSubcommand` で計算されながら `branch` ケースでしか読まれず、`git push --delete origin main` は通常 push 分岐(`destructiveness: 4`)に落ちていました。`DESTRUCTIVENESS_MAP` の `'push-delete': 5` は死んだデータでした。
101
+ - **保護ブランチ判定が、スラッシュを含む名前に誤反応していました。** 述語は最後の `/` より前を落としていたため、`backup/main` や `feat/release` が保護扱いになっていました。既知の ref 接頭辞(`refs/heads/`、`refs/tags/`、`refs/remotes/<remote>/`)のみを剥がすよう変更しました。方向が重要です——この述語は**取り消し不能な** P0 に供給され、見逃しにはキーワード/ルール/LLM 層という受け皿がありますが、誤検出は正当なワークフローを出口なく塞いでしまいます。
102
+
103
+ ### 追加
104
+
105
+ - **保護ブランチに対するリモート履歴改変の P0 ハード拒否。** `main` / `master` / `production` / `release` / `stable` を強制上書きまたは削除する `git push` を、LLM 呼び出しより前に決定的に拒否します。従来の保護は、フラットでブランチを見ず、名前空間で上書き可能なキーワード `'push --force'` でした。これはその下に敷く交渉不能な床です。
106
+ - **「エスカレーションのみ」は慣習ではなく構造です**: ヘルパーは `string | undefined` を返し、呼び出し側はそれを deny / 未決定として読みます。allow を表現する手段がないため、P0 に配線してもゲートが許可する範囲を広げることはできません。
107
+ - 意図的に**対象外**(引き続きキーワード/ルール/LLM 層が担当): 非保護ブランチへの force push、ブランチ名を書かない `git push --force`、通常の push。
108
+ - コマンドパーサ(`command-dispatcher` / `command-semantics` / `parsers/git` / `parsers/shell-cmds`)の初の本番利用です。これまで自身のテストファイルからしか参照されていませんでした。
109
+
110
+ ### テスト
111
+
112
+ - 新規 `test/git-protected-push.spec.ts`(13 件)。
113
+ - **反証**: 3 つの修正をそれぞれ戻すと、それを守るケースだけが赤くなります(計 7 件)。意図的に許可するケースは緑のままです。
114
+ - 全量 **40 ファイル / 459 件**、`typecheck` クリーン。
115
+
8
116
  ## [2.0.0] - 2026-09-11
9
117
 
10
118
  ### 修正
package/CHANGELOG.ko.md CHANGED
@@ -5,6 +5,110 @@
5
5
  형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)을 따르며,
6
6
  이 프로젝트는 [Semantic Versioning](https://semver.org/spec/v2.0.0.html)을 준수합니다.
7
7
 
8
+ ## [2.6.1] - 2026-09-25
9
+
10
+ ### 수정됨
11
+
12
+ - **입력창 글리프 패치가 DSH 0.1.5에서 다시 동작합니다.** `scripts/patch-permission-glyph.mjs`는
13
+ `["permissive",` 뒤에 항목을 끼워 넣었지만, 그 키는 DSH 0.1.2가 글리프를 그리던 값이고
14
+ 0.1.5-rc.2 번들에는 어디에도 없습니다. 그래서 패치는 `anchor not found`로 실패했고 두 티어는
15
+ 텍스트만 표시했습니다. 이제 원본은 티어별로 `GLYPH_TARGETS`에 선언하고, 앵커는 **해석**합니다
16
+ —— 먼저 `["<key>",`, 다음으로 번들이 쓰는 상수 별칭(`danger-full-access`는 `[FULL_ACCESS,`).
17
+ 호스트가 그리지 않는 키는 Map이 실제로 가진 키와 함께 보고됩니다.
18
+ - **UI를 제공하지 않는 DSH 설치에는 더 이상 패치하지 않습니다.** 후보는 profile 스코프를
19
+ 먼저, CLI 설치를 다음으로 정렬하고, 같은 번들을 가진 **다른** 설치는 조용히 넘기지 않고
20
+ 출력에 나열합니다. 이 프로젝트의 호스트에서 실측: 전역 CLI 설치가 런처가 관리하는
21
+ 런타임을 가려서, 패치는 성공을 보고했지만 선택기는 아이콘 없이 남아 있었습니다.
22
+
23
+ ### 변경됨
24
+
25
+ - **각 티어는 공유하는 파일 샌드박스의 글리프를 답니다(공통 방패+눈이 아니라).** 「自动审查」는
26
+ `workspace-write` 글리프를, 「自动审查(高权限)」는 `danger-full-access` 글리프를 복사하므로
27
+ 아이콘이 그 티어가 실제로 부여하는 접근 권한과 일치합니다. 동일 아이콘 쌍은 `permissive`
28
+ 글리프를 갖고 있던 0.1.2의 Map에서 온 동작입니다.
29
+ - **두 글리프는 모두 적용되거나 모두 적용되지 않습니다.** 해석할 수 없는 원본 키는 아무것도 쓰지
30
+ 않는 거부이며, 일부만 패치된 번들이 되지 않습니다.
31
+
32
+ ## [2.4.1] - 2026-09-17
33
+
34
+ ### 수정됨
35
+
36
+ - 승인 이력이, 플래그된 호출이 왜 검토 없이 실행되었는지 설명하는 유일한 이벤트에 대해 원시 `preset-passthrough` 문자열을 표시했습니다(티어는 `approval: ask`를 선언했지만 세션이 `never`로 재정의되어 게이트의 ask가 패스스루로 강등된 경우입니다). 이제 다른 판정과 마찬가지로 읽기 쉬운 라벨로 표시됩니다.
37
+
38
+ ## [2.4.0] - 2026-09-17
39
+
40
+ ### 변경됨
41
+
42
+ - **P3 LLM 분류기는 에스컬레이션 전용이 되어 더 이상 거부할 수 없습니다.** 하드 카테고리
43
+ (`deletion` / `credential` / `remote` / `system` / `bulk`)의 `risky` 판정은 이전에는 패널 없이
44
+ **자동 거부**했지만, 이제는 **사람 확인(ask)을 유지**합니다. 거부는 결정론적 계층(P0 하드 거부,
45
+ 거부 키워드 블랙리스트, 명시적 `deny:` 규칙)에만 속합니다. 확률적 판정이 이의를 제기할 수 없는
46
+ 차단을 내려서는 안 되기 때문입니다. 실측: 그레이더가 무해한 `git commit -F …`를 **`remote`**로
47
+ 판정했고, 자동 거부는 승인할 패널도 재시도할 grant도 남기지 않았습니다(우연히 `safe`로 판정된
48
+ 수동 재시도만 통과했습니다). 이는 "고위험 작업은 **결정론적으로** 차단하고 LLM 판단에 맡기지
49
+ 않는다"는 프로젝트 자체 규칙과도 일치합니다.
50
+
51
+ ### 추가됨
52
+
53
+ - 하드 위험 카테고리는 `neutral`과의 중요한 차이를 하나만 유지합니다: **결코 학습되지 않습니다**.
54
+ 사람의 승인을 반복해도 `deletion`/`credential`/`remote`/`system`/`bulk` 판정이 자동 허용으로
55
+ 침전되지 않습니다(이전에는 자동 거부의 부수 효과로만 성립했지만, 이제는 명시적 속성입니다).
56
+
57
+ ### 동작 주의
58
+
59
+ - `approval: never`에서는 게이트가 전달할 수 없는 ask가 여전히 패스스루로 강등되므로, 분류기가
60
+ 플래그한 호출은 이제 **실행됩니다**(이전에는 자동 거부였습니다). 이는 "결정론적으로 위험한 것만
61
+ 거부하고 나머지는 협상한다"의 직접적 귀결입니다 —— 협상에는 사람이 필요하고, `never`는 사람이
62
+ 없다는 뜻입니다. 플래그를 실제로 받으려면 `approval`이 `ask`인 티어에서 운용하십시오.
63
+
64
+ ## [2.3.0] - 2026-09-17
65
+
66
+ ### 추가됨
67
+
68
+ - **게이트 정지는 더 이상 침묵하지 않습니다.** 세션 권한 프리셋이 `gatePresets` 밖에 있을 때 게이트는
69
+ 정지하고 아무것도 기록하지 않았습니다 —— 그래서 그 도구 호출은 게이트가 검사하고 허용한 호출과
70
+ 구별되지 않았습니다. 이제 (session, preset) 전환마다 **1건만** `stand-down` 알림을 기록하고
71
+ (호출마다가 아닙니다), 프리셋·범위·P0 하드 거부가 비활성이라는 사실을 명시합니다. 브라우저는
72
+ 입력창 위에 상시 **GATE OFF** 표시줄로 렌더링하고, 승인 이력에는 `게이트 정지` 태그가 표시됩니다.
73
+ - 自动审查 설정 카드가 게이트 자체의 프리셋 범위(`gatePresets`, 기본값
74
+ `permissive` / `permissive-full`)와 그 밖에서 벌어지는 일을 명시하므로, 범위를 티어를 설정하는
75
+ 곳에서 확인할 수 있습니다.
76
+
77
+ ### 변경됨
78
+
79
+ - **P0의 문서상 위치는 전역이 아니라 범위 한정입니다.** P0 하드 거부는 *게이트의 프리셋 범위 안에서*
80
+ 단조롭고 협상 불가능합니다. 프리셋을 넘어가면 게이트는 P0를 포함해 완전히 정지합니다 —— 선택된
81
+ 티어 자체의 정책이 그 세션을 소유하기 때문입니다. 코드는 항상 이렇게 동작했지만 `AGENTS.md`와
82
+ 4개 README는 반대로 주장해, `danger-full-access`가 "P0는 여전히 적용된다"로 읽혔습니다.
83
+ `gatePresets: ['*']`로 설정하면 P0는 다시 전역이 됩니다.
84
+
85
+ ### 수정됨
86
+
87
+ - `DEFAULT_GATE_PRESETS` / `resolveGatePresets`를 `config.ts`(schemastery를 import합니다)에서
88
+ 의존성 없는 `preset.ts`로 옮겨, 브라우저 쪽이 node 전용 의존성을 클라이언트 번들로 끌어오지 않고
89
+ 범위를 렌더링할 수 있게 했습니다. `config.ts`는 둘 다 재수출하므로 기존 import는 변경이 없습니다.
90
+
91
+ ## [2.2.0] - 2026-09-17
92
+
93
+ ### 수정
94
+
95
+ - **P0 하드 거부가 4개의 셸 도구를 제외한 나머지를 모두 통과시키고 있었습니다.** `hardDenyReason`은 셸 검사를 로컬 정규식 `/^(?:bash|pwsh|sh|cmd)$/`로 막고 있었는데, 이는 `shell` / `terminal` / `powershell`에 **일치하지 않습니다**. `shell`은 DSH의 주 셸 도구이므로, P0의 셸 검사(보호 경로로의 리다이렉트)는 **가장 흔한 호출 형태에 대해 무력**했습니다. 실측, 동일 명령 `echo x > /etc/passwd`: `bash` 경유는 차단, `shell` 경유는 **통과**. `engine.ts`가 이제 `evaluate.ts`에서 `SHELL_TOOLS`를 가져옵니다——같은 사실의 사본이 세 곳에 있었고, 완전한 것은 하나뿐이었습니다.
96
+ - **`git push --delete`가 일반 push로 파싱되고 있었습니다.** `hasDelete`는 `analyzeSubcommand`에서 계산되지만 `branch` 케이스에서만 읽혔고, 그 결과 `git push --delete origin main`은 일반 push 분기(`destructiveness: 4`)로 떨어졌습니다. `DESTRUCTIVENESS_MAP`의 `'push-delete': 5`는 죽은 데이터였습니다.
97
+ - **보호 브랜치 판정이 슬래시를 포함한 이름에 오작동하고 있었습니다.** 술어가 마지막 `/` 앞부분을 잘라내어 `backup/main`과 `feat/release`가 보호 대상으로 읽혔습니다. 이제 알려진 ref 접두사(`refs/heads/`, `refs/tags/`, `refs/remotes/<remote>/`)만 제거합니다. 방향이 중요합니다——이 술어는 **되돌릴 수 없는** P0에 공급되며, 놓침에는 키워드/규칙/LLM 계층이라는 안전망이 있지만 오탐은 정당한 워크플로를 출구 없이 막습니다.
98
+
99
+ ### 추가
100
+
101
+ - **보호 브랜치에 대한 원격 이력 재작성의 P0 하드 거부.** `main` / `master` / `production` / `release` / `stable`을 강제 덮어쓰기하거나 삭제하는 `git push`를 LLM 호출 이전에 결정론적으로 거부합니다. 기존 보호는 평면적이고 브랜치를 보지 않으며 네임스페이스로 덮어쓸 수 있는 키워드 `'push --force'`였습니다. 이것은 그 아래에 깔리는 협상 불가능한 바닥입니다.
102
+ - **"에스컬레이션 전용"은 관례가 아니라 구조입니다**: 헬퍼는 `string | undefined`를 반환하고 호출자는 이를 deny / 미결로 읽습니다. allow를 표현할 수단이 없으므로 P0에 배선해도 게이트가 허용하는 범위를 넓힐 수 없습니다.
103
+ - 의도적으로 **대상에서 제외**(키워드/규칙/LLM 계층이 계속 담당): 비보호 브랜치로의 force push, 브랜치명을 쓰지 않은 `git push --force`, 일반 push.
104
+ - 커맨드 파서(`command-dispatcher` / `command-semantics` / `parsers/git` / `parsers/shell-cmds`)의 첫 프로덕션 사용입니다. 지금까지는 자체 테스트 파일에서만 참조되고 있었습니다.
105
+
106
+ ### 테스트
107
+
108
+ - 신규 `test/git-protected-push.spec.ts`(13건).
109
+ - **반증**: 세 수정을 각각 되돌리면 그것을 지키는 케이스만 빨개집니다(총 7건). 의도적으로 허용하는 케이스는 초록으로 남습니다.
110
+ - 전체 **40 파일 / 459건**, `typecheck` 클린.
111
+
8
112
  ## [2.0.0] - 2026-09-11
9
113
 
10
114
  ### 수정
package/CHANGELOG.md CHANGED
@@ -5,6 +5,242 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.6.1] - 2026-09-25
9
+
10
+ ### Fixed
11
+
12
+ - **The composer glyph patch works on DSH 0.1.5 again.** `scripts/patch-permission-glyph.mjs`
13
+ spliced its entry after `["permissive",` — a key DSH 0.1.2 rendered a glyph for and 0.1.5-rc.2
14
+ no longer contains anywhere in the bundle — so the patch died with `anchor not found` and both
15
+ plugin tiers stayed text-only. The sources are now declared per tier in `GLYPH_TARGETS` and each
16
+ anchor is **resolved**: `["<key>",` first, then the constant alias the bundle uses instead
17
+ (`danger-full-access` is keyed `[FULL_ACCESS,`). A source key this host does not render is
18
+ reported together with the keys the map does carry.
19
+ - **The probe no longer patches a DSH install that is not serving the UI.** Candidate bundles are
20
+ ordered profile scope first, CLI install second, and any *other* install carrying the same
21
+ bundle is listed in the output instead of being silently passed over. Measured on this
22
+ project's own host: a global CLI install shadowed the launcher-managed runtime, so the patch
23
+ reported success while the picker stayed icon-free.
24
+
25
+ ### Changed
26
+
27
+ - **Each tier wears the glyph of the file sandbox it shares, not one shared shield+eye.** 自动审查
28
+ copies the `workspace-write` glyph and 自动审查(高权限) the `danger-full-access` one, so the icon
29
+ matches the access the tier actually grants. The old identical-icon pair came from 0.1.2, whose
30
+ map still keyed a `permissive` glyph that 0.1.5 dropped.
31
+ - **Both glyphs are applied, or neither.** A source key the script cannot resolve is a refusal that
32
+ writes nothing, rather than a half-patched bundle.
33
+
34
+ ## [2.6.0] - 2026-09-17
35
+
36
+ ### Added
37
+
38
+ - **Rule test (dry-run) in the settings card.** A new panel evaluates one would-be tool call
39
+ against the ruleset the gate currently has loaded and shows the verdict, the matched rule (index
40
+ and action), the dimensions that rule constrains, and the reason. Written for the question the
41
+ YAML cannot answer by inspection: *what will this actually do?* — `args` is an OR over tokens, an
42
+ empty dimension constrains nothing, and `command` matches the decomposed command **word** only, so
43
+ a plausible-looking rule can be dead on arrival.
44
+
45
+ The report keeps two answers apart on purpose. **`verdict`** is the policy result of the whole
46
+ chain (P0 hard-deny → deny-keyword → P1 grant → P2 rules → P4 ask → permissive).
47
+ **`ruleLayer`** is what the `permissions` chain decides on its own, and only that layer can name a
48
+ rule index: a P0 hard-deny or a preset deny-keyword fires *before* it and leaves no rule behind, so
49
+ the panel says so instead of attributing an unrelated rule. `matchedDimensions` likewise lists the
50
+ dimensions the matched rule *constrains* rather than the one that "caused" the match — dimensions
51
+ are ANDed, so a single cause cannot honestly be named. A `ruleCount` of 0 is surfaced as "0 rules
52
+ loaded, check the rulesFile path" rather than as "nothing matched".
53
+
54
+ - **`POST /api/dsh-perm-gate/dry-run`** — the panel's endpoint. **Read-only by construction**: it
55
+ has no write form, unknown body keys are dropped rather than forwarded, and it never touches
56
+ rules, grants, learning state or the settings namespace. Testing a rule must not be able to change
57
+ it. It evaluates against the **live** runtime rather than a fresh one, so the panel tests the rules
58
+ in force — a fresh runtime would re-resolve the chain from a different root and could silently
59
+ answer about a different file.
60
+
61
+ - **`src/dry-run.ts`** — `runDryRun` / `createDryRunRuntime`, the shared evaluator for the CLI and
62
+ the route, plus `PermGateRuntime.explainRules` (read-only rule-layer report) and
63
+ `PermGateRuntime.explainCall` (pure policy evaluation). `src/cli.ts` is now a thin wrapper over it;
64
+ its output is unchanged across nine replayed invocations, verified byte for byte (including the
65
+ `--list`, no-tool, bad-JSON and write-path cases).
66
+
67
+ ### Fixed
68
+
69
+ - **The rule-test panel reported `allow` for calls the rules send to a human.** The first version
70
+ evaluated through the host-facing path, which applies the session layers — the preset stand-down
71
+ and the `approval: never` ask degradation. A session-less dry-run has no session, so the policy
72
+ reader answered "never", every `ask` degraded to a passthrough, and the panel showed **allow** for
73
+ exactly the rules someone opens it to review. Measured on the live host: `shell ls -la` showed
74
+ `allow` while the rule layer on the same card said `ask`.
75
+
76
+ `runDryRun` now reports the **pure policy** verdict from `explainCall`: the same
77
+ P0 → deny-keyword → P1 → P2 → P4 chain with the session-state layers excluded rather than guessed,
78
+ and with `reason` never collapsed to a bare `(default/passthrough)`. The host-facing view remains
79
+ available under `input.hostView`; the CLI asks for it, so its historic output stays byte for byte
80
+ identical (re-verified: 9/9 cases, SHA256 per case).
81
+
82
+ - **The "read-only" route was not read-only.** It ran the host-facing decision path against the live
83
+ runtime, which appends audit entries and records decision events — opening the rule-test panel
84
+ wrote to the live decision feed. The pure path appends nothing, and `test/dry-run.spec.ts` now
85
+ asserts that a call the host path *would* record leaves the audit mirror at zero.
86
+
87
+ ## [2.5.0] - 2026-09-17
88
+
89
+ ### Added
90
+
91
+ - **`branch` rule dimension** — git branch / remote / protected-branch matching, so a rule can
92
+ finally say "no push to a protected branch" without catching unrelated commands. The decisive
93
+ case is the one `args` can never express: `git push --force origin main` (dangerous) and
94
+ `git checkout --force main` (routine) carry the *same tokens*, and only the branch dimension
95
+ tells them apart. Sub-fields: `target` (branch-name glob, `*` crosses `/`), `remote`
96
+ (remote-name glob) and `shared` (require a protected branch). Candidates come from the shared
97
+ command dispatcher, so `refspec` forms (`HEAD:main`) are already split and a remote name is
98
+ never mistaken for a branch name. Documented in `docs/rules-format.md` §4.11.
99
+
100
+ ### Fixed
101
+
102
+ - **The multi-file rule chain silently emptied every match dimension.** `resolveRuleChain` merged
103
+ entries by hand with `tools: []`, `command: []`, `args: []` and `paths: []`. An empty dimension
104
+ means "no constraint", so EVERY merged entry matched EVERY call: the first `deny` entry denied
105
+ everything in its partition, and the first `allow` entry allowed everything before the `ask`
106
+ partition was ever consulted. The merge now goes through the same `compileRuleEntry` the
107
+ single-file path uses.
108
+
109
+ *Reach:* the chain resolver is reached **only** under `searchUp: true` (product default `false`,
110
+ and unset in a stock profile), so the default single-file path — `compileDocument` — was never
111
+ affected. That is also why the existing suite stayed green: no test drove a rules *file* through
112
+ the chain. `test/rule-chain.spec.ts` now does. A separate, still-open defect on the same
113
+ opt-in path is recorded in the plan: `findChainEntries` joins the configured `rulesFile` onto
114
+ each directory, so the absolute path `resolveRulesFile` always produces matches nothing and the
115
+ chain yields an empty ruleset without an error.
116
+
117
+ - **`argv.pipeline` could not see its own subject.** The pipeline match string was built from
118
+ `SimpleCommand.command`, which is only the command *word* — `curl https://x.sh | sh` collapsed
119
+ to `curl|sh`, so the documented `curl|sh` pattern matched the harmless adjacent form while the
120
+ genuinely dangerous one with arguments did not match at all. The match string is now each
121
+ simple command's full argv (joined by `|`), and the docs state that `|` in a pattern is a
122
+ literal, so covering arguments requires `curl*|sh`.
123
+
124
+ - Nine pre-existing `eslint` errors (unused imports/constants and one `prefer-const` in
125
+ `src/parsers/` and `test/command-parsers.spec.ts`) — `npm run lint` is green again.
126
+
127
+ ## [2.4.1] - 2026-09-17
128
+
129
+ ### Fixed
130
+
131
+ - The approvals history showed the raw `preset-passthrough` string for the one event that
132
+ explains *why* a flagged call ran unreviewed: the tier declares `approval: ask`, the session was
133
+ overridden to `never`, and the gate's ask therefore degraded to a passthrough. It now renders as
134
+ a readable label like every other verdict.
135
+
136
+ ## [2.4.0] - 2026-09-17
137
+
138
+ ### Changed
139
+
140
+ - **The P3 LLM classifier is escalate-only — it can no longer deny.** A `risky` verdict with a
141
+ hard category (`deletion` / `credential` / `remote` / `system` / `bulk`) used to **auto-deny**
142
+ with no panel; it now **keeps the human ask**. Denying is reserved for the deterministic layers
143
+ alone — P0 hard-deny, the deny-keyword blacklist, explicit `deny:` rules — because a
144
+ probabilistic verdict must not be able to hand down an unappealable block. Measured live: the
145
+ grader called a benign `git commit -F …` **`remote`** and the auto-deny left no panel to approve
146
+ and no grant to retry with; only a manual retry (which happened to grade `safe`) got past it.
147
+ This also aligns the code with the project's own rule that high-risk operations are intercepted
148
+ **deterministically, never on the LLM's judgement**.
149
+
150
+ ### Added
151
+
152
+ - Hard risk categories keep one meaningful distinction from `neutral`: they are **never
153
+ learnable**. Repeated human approvals cannot sediment a `deletion`/`credential`/`remote`/
154
+ `system`/`bulk` verdict into an auto-allow (previously this was true only as a side effect of
155
+ the auto-deny; it is now an explicit property).
156
+
157
+ ### Behaviour notes
158
+
159
+ - Under `approval: never` an ask the gate cannot deliver still degrades to passthrough, so a
160
+ classifier-flagged call now **runs** where it used to be auto-denied. That is the direct
161
+ consequence of "deny only what is deterministically dangerous, negotiate everything else": a
162
+ negotiation needs a human, and `never` means there is none. Run the gate in a tier whose
163
+ `approval` is `ask` for the flags to reach you.
164
+
165
+ ## [2.3.0] - 2026-09-17
166
+
167
+ ### Added
168
+
169
+ - **A stand-down is no longer silent.** When the session's permission preset is outside
170
+ `gatePresets`, the gate used to stand down and record nothing — so the tool call looked
171
+ exactly like one the gate had inspected and allowed. It now records **one** `stand-down`
172
+ notice per (session, preset) transition (never per call), naming the preset, the scope and
173
+ the fact that P0 hard-deny is inactive. The browser renders it as a sticky **GATE OFF**
174
+ strip above the input, and the approvals history shows a `Gate off` tag.
175
+ - The 自动审查 settings card states the gate's own preset scope (`gatePresets`, default
176
+ `permissive` / `permissive-full`) and what happens outside it, so the scope is visible
177
+ where the tier is configured.
178
+
179
+ ### Changed
180
+
181
+ - **P0's documented positioning is scoped, not global.** P0 hard-deny is monotonic and
182
+ non-negotiable *within the gate's preset scope*; across presets the gate stands down
183
+ entirely — P0 included — because the selected tier's own policy owns that session. The
184
+ code always behaved this way; `AGENTS.md` and the four READMEs claimed otherwise, which
185
+ made `danger-full-access` read as "P0 still applies". Set `gatePresets: ['*']` to make P0
186
+ global again.
187
+
188
+ ### Fixed
189
+
190
+ - `DEFAULT_GATE_PRESETS` / `resolveGatePresets` moved from `config.ts` (which imports
191
+ schemastery) into the dependency-free `preset.ts`, so the browser half can render the scope
192
+ without pulling a node-only dependency into the client bundle. `config.ts` re-exports both,
193
+ so existing imports are unchanged.
194
+
195
+ ## [2.2.0] - 2026-09-17
196
+
197
+ ### Fixed
198
+
199
+ - **P0 hard-deny skipped every shell tool but four.** `hardDenyReason` gated its shell
200
+ inspection on a locally defined regex, `/^(?:bash|pwsh|sh|cmd)$/`, which does **not**
201
+ match `shell`, `terminal` or `powershell`. `shell` is DSH's primary shell tool, so the
202
+ whole P0 shell check — the protected-path redirect guard — was **inert for the most
203
+ common call shape**. Measured, same command `echo x > /etc/passwd`: blocked via `bash`,
204
+ **allowed** via `shell`. `engine.ts` now imports `SHELL_TOOLS` from `evaluate.ts`; the
205
+ gate held three copies of that one fact and only one of them was complete.
206
+ - **`git push --delete` was parsed as an ordinary push.** `hasDelete` was computed in
207
+ `analyzeSubcommand` but only ever read in the `branch` case, so `git push --delete
208
+ origin main` fell through to the plain-push branch (`destructiveness: 4`) and the
209
+ `DESTRUCTIVENESS_MAP` entry `'push-delete': 5` was dead data.
210
+ - **Protected-branch detection mis-fired on any name containing a slash.** The predicate
211
+ stripped everything before the last `/`, so `backup/main` and `feat/release` were read
212
+ as protected. It now strips only known ref prefixes (`refs/heads/`, `refs/tags/`,
213
+ `refs/remotes/<remote>/`). The direction matters: this predicate feeds an **unremovable**
214
+ P0, where a miss still has the keyword / rule / LLM layers behind it, while a false
215
+ positive blocks a legitimate workflow with no recourse.
216
+
217
+ ### Added
218
+
219
+ - **P0 hard-deny for remote-history rewrite on a protected branch.** A `git push` that
220
+ force-overwrites or deletes `main` / `master` / `production` / `release` / `stable` is
221
+ rejected deterministically, before any LLM call. The pre-existing protection was the
222
+ flat, branch-blind, namespace-overridable keyword `'push --force'`; this is the
223
+ non-negotiable floor beneath it.
224
+ - **Escalate-only by construction, not by convention**: the helper returns
225
+ `string | undefined`, which the caller reads as deny / undecided. It has no way to
226
+ express allow, so wiring it into P0 cannot widen what the gate permits.
227
+ - Deliberately **not** covered (still left to the keyword / rule / LLM layers):
228
+ force-push to a non-protected branch, `git push --force` with no branch named, and
229
+ ordinary pushes.
230
+ - First production use of the command parser (`command-dispatcher`, `command-semantics`,
231
+ `parsers/git`, `parsers/shell-cmds`), which until now was referenced only by its own
232
+ test file.
233
+
234
+ ### Tests
235
+
236
+ - New suite `test/git-protected-push.spec.ts` (13 cases): parser variants, the
237
+ protected-branch predicate in both directions, full `SHELL_TOOLS` coverage, the four
238
+ deliberately-allowed shapes, and compound-command segmentation
239
+ (`git status && git push --force origin main`).
240
+ - **Falsified**: reverting each of the three fixes turns exactly the cases guarding it red
241
+ (7 failures in total), while the deliberate-allow cases stay green.
242
+ - Full suite **40 files / 459 cases**; `typecheck` clean.
243
+
8
244
  ## [2.1.2] - 2026-09-15
9
245
 
10
246
  ### Added
package/INSTALL.ja.md CHANGED
@@ -12,7 +12,7 @@
12
12
  - [日本語 changelog](./CHANGELOG.ja.md)
13
13
  - [한국어 changelog](./CHANGELOG.ko.md)
14
14
 
15
- `dsh-perm-gate` バージョン **2.1.2**。判定チェーン、ルールファイル形式、自动审查
15
+ `dsh-perm-gate` バージョン **2.6.1**。判定チェーン、ルールファイル形式、自动审查
16
16
  ティアについては [日本語 README](./README.ja.md) を参照してください。
17
17
 
18
18
  ## 必要条件
@@ -23,19 +23,20 @@
23
23
  - インストール先の profile。ブラウザ側は `web` profile 向けにのみビルドされます
24
24
  (`package.json` の `dsh.client.platform = "web"`)。
25
25
 
26
- ## DSH のバージョンに合う tag を選ぶ
26
+ ## DSH のバージョンに合うラインを選ぶ
27
27
 
28
- 単一ビルドが DSH 両ラインをカバーするため、どちらの tag を選んでも動作するコードが
29
- インストールされます —— tag を残しておくのは、あなたがピン留めするバージョンが
30
- 各ラインで意味を持つようにするためです。
28
+ 2 つの長期ブランチが 2 つの DSH ラインを担当します:`main`(2.x)と
29
+ `sync/0.1.5-from-main`(3.x、DSH 0.1.5 のパッケージング変体 —— `compat/0.1.5` は
30
+ 同じコードツリーを指します)。
31
31
 
32
32
  | DSH のバージョン | インストール方法 |
33
33
  |-----------------|-----------------|
34
- | `0.1.2-alpha.1` 以上(`0.1.5-rc.2` を含む) | `dsh plugin --profile web add dsh-perm-gate`(tag `latest`) |
34
+ | `0.1.2-alpha.1` – `0.1.5-rc.0` | `dsh plugin --profile web add dsh-perm-gate`(tag `latest`、2.x ライン) |
35
+ | `0.1.5-rc.1` 以上 | `dsh plugin --profile web add github:drscrewdriver/dsh-perm-gate#sync/0.1.5-from-main`(3.x ライン;npm の dist-tag は未公開 —— `@latest` は同じコードの 2.x パッケージングをインストールします) |
35
36
  | `0.1.1-rc.2` まで | `dsh plugin --profile web add dsh-perm-gate@legacy` |
36
37
 
37
- DSH は `engines.dsh` を強制しないため、tag が選択メカニズムであり、互換性ゲート
38
- そのものではありません。単一ビルドが両ラインをカバーする理由と tag の公開方法については、
38
+ DSH は `engines.dsh` を強制しないため、tag と ref は選択メカニズムであり、互換性
39
+ ゲートそのものではありません。ラインと tag の公開レイアウトについては
39
40
  [RELEASING.md](./RELEASING.md) を参照してください。
40
41
 
41
42
  ## 公式 CLI でインストール
@@ -72,6 +73,11 @@ profile の `cordis.yml` にプラグインを追加します:
72
73
  defaultAction: ask # allow | ask | deny
73
74
  ```
74
75
 
76
+ 2.6.0 から、ルール本体は `dsh-perm-gate-rules` settings 名前空間に保存され、変更の
77
+ たびにゲートがその場で再コンパイルされます。上記の YAML ファイルは任意になりました。
78
+ 名前空間が未設定の間だけ読み込まれ、プラグインが最初にルールを書き込む際(許可リスト
79
+ の承認、または設定 UI での編集)にファイルのルールが自動的に取り込まれます。
80
+
75
81
  まずは [examples/permissions.example.yaml](./examples/permissions.example.yaml)
76
82
  から始め、profile を再読込してください。
77
83
 
@@ -89,13 +95,21 @@ dsh profile reload --profile web
89
95
 
90
96
  ## **DSH** アップグレード後:入力欄アイコンのパッチを再適用
91
97
 
92
- 「自动审查(高权限)」が「自动审查」と同じ盾+目のアイコンを表示するのは、
98
+ 「自动审查」と「自动审查(高权限)」の 2 つのティアにアイコンが付くのは、
93
99
  `scripts/patch-permission-glyph.mjs` が **DSH ホストパッケージ内の閉じた Map** に
94
- その項目を追加したからにすぎません。DSH はプラグイン提供ティアに**設計上アイコンを
95
- 与えません** —— その Map 自身のコメントが *"host-configured names outside the design
96
- set get none"* と述べています。プラグインが影響できる option オブジェクトは
97
- `{value, name, description}` しか運ばないため、代わりに使えるプラグイン側のシームは
98
- 存在しません。
100
+ それらを追加したからにすぎません ——「自动审查」は `workspace-write` の盾+鉛筆を、
101
+ 「自动审查(高权限)」は `danger-full-access` の盾+感嘆符をコピーしており、それぞれが
102
+ 実際に共有するファイルサンドボックスのグリフになります。DSH はプラグイン提供ティアに
103
+ **設計上アイコンを与えません** —— その Map 自身のコメントが *"host-configured names
104
+ outside the design set get none"* と述べています。プラグインが影響できる option
105
+ オブジェクトは `{value, name, description}` しか運ばないため、代わりに使えるプラグイン側の
106
+ シームは存在しません。
107
+
108
+ スクリプトは取得元のキーを**解決**します(`["<key>",` を先に、次にホストが使う定数別名 ——
109
+ 0.1.5 の `danger-full-access` は `[FULL_ACCESS,`)。DSH はこの設計セットを一度変更して
110
+ います —— 0.1.2 には `permissive` のグリフがあり、0.1.5 には `read-only` /
111
+ `workspace-write` / `danger-full-access` しかありません。解決できないキーは、Map が実際に
112
+ 持つキーと一緒に報告されるため、ティアの付け替えは `GLYPH_TARGETS` の 1 行で済みます。
99
113
 
100
114
  このパッチは**ホスト**のファイルを書き換えるので、**DSH のアップグレードや再インストールで
101
115
  失われます**。*本プラグイン*のアップグレードでは失われません。アイコンは元々プラグインの
@@ -114,11 +128,15 @@ dsh profile reload --profile web
114
128
  プラグインが許可なく自分の harness を書き換えてよい理由はありません。適用の有無にかかわらず
115
129
  DSH の動作に影響はなく、ティア自体はそのまま機能します。
116
130
 
117
- このスクリプトは冪等(2 回目は no-op)で、バックアップは 1 度だけ取り、壊れたスライスを
118
- 書き込もうとしません —— 結果に対して `node --check` を実行し、失敗すればバックアップを
119
- 復元します —— したがって DSH アップグレードのたびに無条件で再実行して安全です。
120
- 実行中の `node` バイナリからパッケージを逆算するため、nvm のバージョン変更や
121
- インストール symlink の貼り替えでも壊れません。
131
+ このスクリプトは冪等(2 回目は no-op)で、バックアップは 1 度だけ取り、2 つのグリフは
132
+ 両方適用するか両方とも適用しないか(解決できない取得元キーは拒否であり、部分適用では
133
+ ありません)のいずれかで、壊れたスライスを書き込もうとしません —— 結果に対して
134
+ `node --check` を実行し、失敗すればバックアップを復元します —— したがって DSH
135
+ アップグレードのたびに無条件で再実行して安全です。呼び出し元の profile スコープを先に、
136
+ CLI インストールを次に探すため、nvm のバージョン変更やインストール symlink の貼り替えでも
137
+ 壊れません。マシンに複数の DSH インストールがある場合、**パッチを当てなかった**方は出力に
138
+ 列挙されます —— 目の前の UI を提供しているのはそのうち 1 つだけなので、必要ならそのパスを
139
+ 引数で明示してください。
122
140
 
123
141
  **プラグインを再インストールしてもアイコンは戻りません。**
124
142
  `dsh plugin --profile web add …` は profile ディレクトリ内の pnpm に引数を転送し、
@@ -194,10 +212,12 @@ DSH の bundle patch は `permission.config.presets` を key 単位でマージ
194
212
  します。profile を再読込して `cordis.patch.yml` を再適用し、後から読み込まれる
195
213
  プラグインが `presets` を上書きしていないか確認してください。
196
214
 
197
- **「自动审查(高权限)」の入力欄アイコンが消えた。**
215
+ **「自动审查」ティアの入力欄アイコンが消えた。**
198
216
  DSH のアップグレードまたは再インストールが、パッチを当てたホスト bundle を置き換え
199
217
  ました。プラグインを再インストールしても戻りません。
200
- `npx dsh-perm-gate-patch-glyph` を実行して再読込してください。
218
+ `npx dsh-perm-gate-patch-glyph` を実行して再読込してください。取得元キーが無いと
219
+ 報告された場合は DSH がグリフ表を再度変更しています —— エラーが現在のキー一覧を
220
+ 示すので、`GLYPH_TARGETS` の該当ティアを付け替えてください。
201
221
  ティア自体は影響を受けません —— パッチなしでもラベルとゲートは動作します。
202
222
 
203
223
  **ルールファイルがあるのに `--list` が `ruleCount: 0` を返す。**