Compare commits
42
Commits
5bba75d1e8
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af6ce99364 | ||
|
|
1a443c562d | ||
|
|
73e913e776 | ||
|
|
1c99f60719 | ||
|
|
7c927f5e12 | ||
|
|
a37fdd4abd | ||
|
|
333c651491 | ||
|
|
0dc7933077 | ||
|
|
dccc5f095f | ||
|
|
1414a7d725 | ||
|
|
49b023ceaa | ||
|
|
e8fe11bfa1 | ||
|
|
18e060d390 | ||
|
|
cceb9a3e53 | ||
|
|
cd502e260f | ||
|
|
44035a7915 | ||
|
|
76406e2f02 | ||
|
|
46905b1e79 | ||
|
|
63da6e77d8 | ||
|
|
54f292b08e | ||
|
|
2fa910ed74 | ||
|
|
bc2d33f856 | ||
|
|
142357a440 | ||
|
|
26df6edc06 | ||
|
|
a80f5ea215 | ||
|
|
5b671976d5 | ||
|
|
66a594a378 | ||
|
|
76c349fa0c | ||
|
|
c31360a772 | ||
|
|
6d0eba9346 | ||
|
|
8e7d21534d | ||
|
|
e1aee6c92c | ||
|
|
d7a0f99cb4 | ||
|
|
ea7bf01e72 | ||
|
|
12a47fe822 | ||
|
|
ebe1434860 | ||
|
|
69d9c73fff | ||
|
|
a175ee3c53 | ||
|
|
2eb2867c13 | ||
|
|
fdd3c7def1 | ||
|
|
d858361586 | ||
|
|
8c47e547b4 |
@@ -0,0 +1,98 @@
|
|||||||
|
---
|
||||||
|
name: decide
|
||||||
|
description: 設計上の決定を記録する。メインドキュメントの更新・検討済み案の記録・journal作成・コミットを一度に行う。
|
||||||
|
disable-model-invocation: true
|
||||||
|
argument-hint: [決定の要旨]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 決定を記録する
|
||||||
|
|
||||||
|
共通の運用ルールは、AGENTS.mdの参照先であるCLAUDE.mdを正とする。
|
||||||
|
現在の仕様はメインドキュメント、判断の経緯はjournal、変更の要旨はコミットに記録する。
|
||||||
|
「検討済み・非採用」には、将来の再提案を防ぐために必要な案を選んで残す。
|
||||||
|
|
||||||
|
## 手順
|
||||||
|
|
||||||
|
### 0. 決定内容を確認する
|
||||||
|
|
||||||
|
以下が揃っているか確認し、足りなければユーザーに聞く。推測で埋めない。
|
||||||
|
|
||||||
|
- 採用した案は何か
|
||||||
|
- それによって**今後効く制約**は何か(却下理由ではなく、現在の状態として書ける文)
|
||||||
|
- 落とした案があるか
|
||||||
|
- この決定が未確認の推測に依存していないか
|
||||||
|
|
||||||
|
### 1. メインドキュメントを更新する
|
||||||
|
|
||||||
|
該当する直下の `*.md` を更新する。
|
||||||
|
|
||||||
|
- 採用した案・現在の設定を書く。「なぜAなのか」は1〜3行まで。
|
||||||
|
- 「なぜBではないのか」は書かない。代わりに**制約文**として書く。
|
||||||
|
- ✗ `B案はKLEパーサーが壊れるので却下した`
|
||||||
|
- ○ `クラスター情報はKLE JSONに出力しない(KLE互換性の制約)`
|
||||||
|
- 比較表・検討過程・ベンチマーク結果をここに書かない。
|
||||||
|
- 他の担当の詳細を外へ移したあとも、この文書だけで担当範囲が分かるか確認する。
|
||||||
|
境界の理解に必要な一文と参照は残す。参照先の仕様は複製しない。
|
||||||
|
|
||||||
|
### 2. 非採用一覧に残す案を選ぶ
|
||||||
|
|
||||||
|
各案について、この記録が将来のどんな再提案を防ぐかを判断する。
|
||||||
|
本文の制約だけで十分なら一覧へ重複記載しない。実際に検討した別方針で、
|
||||||
|
再提案されうるものは、経緯への入口として末尾の `## 検討済み・非採用` に1案1行で残す。
|
||||||
|
|
||||||
|
```
|
||||||
|
- labels[9] へのID埋め込み (journal/2026-09-16-cluster-metadata.md)
|
||||||
|
```
|
||||||
|
|
||||||
|
- 理由は書かない。journalファイル名だけ。
|
||||||
|
- 目安は5行まで。件数や古さだけで機械的に削除せず、再提案を防ぐ必要性で掲載を見直す。
|
||||||
|
- 掲載する案がなければセクションを設けない。一覧から外しても経緯はjournalに残す。
|
||||||
|
|
||||||
|
### 3. journal を書く
|
||||||
|
|
||||||
|
`journal/YYYY-MM-DD-<topic>.md` を作る。トピックを詰め込まない(1決定1ファイル)。
|
||||||
|
|
||||||
|
構成は上から順に:
|
||||||
|
|
||||||
|
1. **結論** — 何を採用したか
|
||||||
|
2. **案の比較** — 簡潔な表。メリット/デメリットを1行ずつ
|
||||||
|
3. 検証ログ・根拠・捨てた案の詳細
|
||||||
|
|
||||||
|
冒頭20行で結論と選択肢が分かること。整形は最低限でよい。
|
||||||
|
他ファイルへのリンクも、他ファイルからのリンクも張らない。
|
||||||
|
|
||||||
|
### 4. 推測と未決の問いを扱う
|
||||||
|
|
||||||
|
この決定が未確認の推測に依存する場合:
|
||||||
|
|
||||||
|
- メインドキュメント本文に `[推測:P-n]` を付ける
|
||||||
|
- 末尾の `## 未確認事項` に項目を足し、**何を確認できれば事実に格上げできるか**を書く
|
||||||
|
|
||||||
|
逆に、この決定で検証が済んだ推測があれば、タグを外して `## 未確認事項` から削除する。
|
||||||
|
本文タグと末尾項目は必ず対で操作する(片方だけ残さない)。
|
||||||
|
|
||||||
|
本文の未決記述は、読者が現在の対応範囲を理解するために必要かを判断する。
|
||||||
|
対応する問いがあればCLAUDE.mdのルール4に従ってリンクし、問いの詳細は複製しない。
|
||||||
|
必要な問いが未登録なら未確認事項を追加し、実装にも現在の説明にも不要な言及なら本文から外す。
|
||||||
|
今は答えに依存せず進められる課題はルール5に従う。「未決」という単語だけで分類しない。
|
||||||
|
|
||||||
|
### 5. コミットする
|
||||||
|
|
||||||
|
メインドキュメントと journal を**同じコミット**に入れる。1コミット1決定。
|
||||||
|
|
||||||
|
メッセージ:
|
||||||
|
|
||||||
|
```
|
||||||
|
<何を変えたか(1行)>
|
||||||
|
|
||||||
|
<なぜそうしたか 数行>
|
||||||
|
|
||||||
|
Journal: journal/2026-09-16-cluster-metadata.md
|
||||||
|
```
|
||||||
|
|
||||||
|
「なぜ」の詳細はコミットメッセージに書ききらない。journal 側にある。
|
||||||
|
|
||||||
|
## 注意
|
||||||
|
|
||||||
|
- 決定が固まっていない段階では使わない。調査中は journal だけ書けばよい。
|
||||||
|
- 複数の決定をまとめてコミットしない。分ける。
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
---
|
||||||
|
name: doc-lint
|
||||||
|
description: ドキュメントがAGENTS.mdの運用ルール1〜5に沿っているか検査する。推測タグと未確認事項の対応漏れ、保留課題の書式・参照・必須欄、journalへのリンク、比較表の混入などを検出する。
|
||||||
|
argument-hint: [ファイル...]
|
||||||
|
---
|
||||||
|
|
||||||
|
# ドキュメントのルール違反を検査する
|
||||||
|
|
||||||
|
機械的に判定できる項目はスクリプトで検出し、判断が要る項目だけ目視で見る。
|
||||||
|
共通の運用ルールは、AGENTS.mdの参照先であるCLAUDE.mdを正とする。
|
||||||
|
**検出して報告するだけで、勝手に直さない。** 修正は内容を確認してもらってから。
|
||||||
|
|
||||||
|
## 1. スクリプトを走らせる
|
||||||
|
|
||||||
|
```
|
||||||
|
python .agents/skills/doc-lint/doc-lint.py [ファイル...]
|
||||||
|
```
|
||||||
|
|
||||||
|
Python 3.9以上で実行する。引数なしでリポジトリ直下の `*.md`、`journal/*.md`、`backlog/**/*.md` を全部見る。
|
||||||
|
実行場所にかかわらずリポジトリを基準に分類し、明示したパスは実行ディレクトリから解決する。
|
||||||
|
ERROR があれば終了コード 1。存在しない引数のファイルもERRORにする。
|
||||||
|
|
||||||
|
検出する項目:
|
||||||
|
|
||||||
|
| 種別 | 内容 |
|
||||||
|
|---|---|
|
||||||
|
| ERROR | 本文の `[推測:P-n]` に対応する `## 未確認事項` の項目がない |
|
||||||
|
| ERROR | 本文の推測タグに対応する項目に `- 格上げ条件:` の内容がない |
|
||||||
|
| ERROR | タグのない未確認事項に `- 格上げ条件:` または `- 完了条件:` の内容がない |
|
||||||
|
| ERROR | `## 未確認事項` のIDが重複している |
|
||||||
|
| ERROR | 未確認事項へのローカルリンクの参照先ファイル・見出しがない、または個別項目へのリンクが未確認事項の項目を指していない |
|
||||||
|
| ERROR | リンクラベルの課題番号と参照先の課題番号が一致しない |
|
||||||
|
| ERROR | 仕様書の `## 保留課題` がリンク箇条書き以外を含む、空である、参照先ファイル・見出しがない |
|
||||||
|
| ERROR | 末尾の管理セクションが重複する、保留課題 → 検討済み・非採用 → 未確認事項の順でない |
|
||||||
|
| ERROR | backlogの項目IDが不正・検査対象内で重複する、必須5欄がない・空である |
|
||||||
|
| ERROR | backlogに推測タグがある、参照先ファイルが存在しない |
|
||||||
|
| ERROR | journal へのリンク / journal から他ファイルへのリンク |
|
||||||
|
| ERROR | `## 検討済み・非採用` の項目が journal ファイル名を参照していない |
|
||||||
|
| ERROR | journal のファイル名が `YYYY-MM-DD-<topic>.md` 形式でない |
|
||||||
|
| WARN | `## 検討済み・非採用` が5件を超えた |
|
||||||
|
| WARN | 本文の段落・箇条書き項目に、リンクのない課題番号や、課題への参照を伴わない「未決」「未定」がある |
|
||||||
|
| WARN | journal の冒頭20行に結論が見当たらない |
|
||||||
|
| WARN | メインドキュメントに比較表・優先度評価・却下案の記述らしきもの |
|
||||||
|
|
||||||
|
**本文タグのない `## 未確認事項` の項目はエラーにしない。** 実装前に判断が必要な未解決の問いとして正当。
|
||||||
|
設計判断は「完了条件」でよい。タグなしの項目が事実確認なのか設計判断なのかは、目視で確認する。
|
||||||
|
|
||||||
|
backlogでは `### PHY-P-14 題名` のように区分付きIDを使い、関連仕様・未決事項・保留できる理由・再開条件・完了条件を必須とする。
|
||||||
|
仕様書側の書式は `- [PHYの保留課題](backlog/README.md#phy)`。
|
||||||
|
backlogは短い候補・制約を保持できるため、メインドキュメント向けの散文ヒューリスティックは適用しない。
|
||||||
|
|
||||||
|
WARN の散文検出は当たりが粗い。取りこぼしも誤検出もあるので、出た/出ないを結論にしない。
|
||||||
|
未確認事項リンクの検査対象は `[P-3](#p-3-...)`・`[MAPのP-3](MAPレイヤー.md#p-3-...)` 等の
|
||||||
|
インラインリンク。Markdown全般のリンク検査ではない。
|
||||||
|
「未決」「未定」のWARNは、次行の参照も同じ段落として扱う。コード例、末尾の管理セクション、
|
||||||
|
journal、backlog、ルールブック自体はこのWARNの対象外とする。
|
||||||
|
WARNから課題の追加や本文の削除を自動決定しない。
|
||||||
|
|
||||||
|
## 2. スクリプトで見られない項目を確認する
|
||||||
|
|
||||||
|
こちらが本体。ファイルを読んで判断する。
|
||||||
|
|
||||||
|
- **「なぜBではないのか」が理由として残っていないか。**
|
||||||
|
却下理由ではなく、現在効いている制約として書かれているか。
|
||||||
|
- ✗ `階層構造はKLEが非対応なので諦めた`
|
||||||
|
- ○ `階層クラスターはKLE JSONに出力しない(KLE互換性の制約)`
|
||||||
|
- **本文が前提にする未確認の記述にタグが付いているか。** 検査項目の中で最も重要で、
|
||||||
|
機械では判定できない。「〜できるはず」「〜と考えられる」「実現可能」で実際に試していない
|
||||||
|
記述を探し、`[推測:P-n]` が付いているか確認する。
|
||||||
|
- **同じ事実が複数ドキュメントに複製されていないか。** 片方だけ古くなる。
|
||||||
|
- **他の担当の説明を移した結果、責務の境界まで消えていないか。**
|
||||||
|
この文書だけで担当範囲が分かるための一文と参照は残し、他の担当の詳細仕様は参照先で管理する。
|
||||||
|
- **非採用一覧の各項目は、将来のどんな再提案を防ぐか。**
|
||||||
|
本文の制約で十分か、実際に検討した別方針として経緯への入口が必要かを判断する。
|
||||||
|
件数・古さ・重複だけで一律に追加・削除しない。
|
||||||
|
- **本文の未決記述は、読者に何を伝えるためにあるか。**
|
||||||
|
現在の対応範囲は本文に残し、問いの詳細は未確認事項へ集約する。
|
||||||
|
対応項目へのリンク、必要な課題の追加、不要な言及の削除のいずれかを判断する。
|
||||||
|
リンクが存在しても、その問いが本文の内容に対応するかは目視で確認する。
|
||||||
|
- **保留中の対応範囲が仕様本文にあるか。** 現在の実装が答えに依存する前提をbacklogへ隠していないか。
|
||||||
|
- **再開条件・完了条件が具体的か。** 仕様書の保留課題リンクは該当区分に向き、解決済みの課題が残っていないか。
|
||||||
|
- **journal の冒頭20行で、結論と選択肢が分かるか。** 見出しの有無ではなく中身を読む。
|
||||||
|
- **コミット規約**(必要なら `git log --oneline` を見る)
|
||||||
|
- 1コミット1決定になっているか
|
||||||
|
- journal を伴う決定のメッセージに `Journal:` 行があるか
|
||||||
|
|
||||||
|
## 3. 報告する
|
||||||
|
|
||||||
|
機械検査のERROR / WARNと、目視で確認した内容・未確認の範囲を分けて報告する。
|
||||||
|
機械検査が0件でも、内容の分類・責務の境界・記録の必要性が適切だった証明にはしない。
|
||||||
|
目視確認を省いた場合はその旨を明記する。
|
||||||
|
修正案は出してよいが、適用は確認を取ってから。事実関係の誤りを見つけた場合も、直さず指摘する。
|
||||||
|
|
||||||
|
## 注意
|
||||||
|
|
||||||
|
- 既存ドキュメントへのルール適用は段階的に進めている。
|
||||||
|
WARN が大量に出るのは想定内で、再編(`/restructure-doc`)のタイミングで解消する。
|
||||||
|
再編前のファイルの WARN を個別に直そうとしない。
|
||||||
|
|
||||||
|
スクリプトを変更した場合の回帰検査:
|
||||||
|
|
||||||
|
```
|
||||||
|
python -B -m unittest discover -s .agents/skills/doc-lint -p test_doc_lint.py
|
||||||
|
```
|
||||||
@@ -0,0 +1,382 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""CLAUDE.md の運用ルール1〜5のうち、機械的に判定できる項目を検査する。
|
||||||
|
|
||||||
|
使い方:
|
||||||
|
python .agents/skills/doc-lint/doc-lint.py [ファイル...]
|
||||||
|
引数なしならリポジトリ直下の *.md、journal/*.md、backlog/**/*.md が対象。
|
||||||
|
明示したパスは実行ディレクトリから解決する。
|
||||||
|
ERROR が1件でもあれば終了コード 1。
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import unquote, urlsplit
|
||||||
|
|
||||||
|
TAG = re.compile(r"\[推測:(P-\d+)\]")
|
||||||
|
ENTRY = re.compile(r"^###\s+(P-\d+)\b")
|
||||||
|
H2 = re.compile(r"^##\s+(.*?)\s*$")
|
||||||
|
MDLINK = re.compile(r"\]\(([^)]+\.md[^)]*)\)")
|
||||||
|
INLINE_LINK = re.compile(r"(?<!!)\[([^\]\n]+)\]\(\s*(<[^>\n]+>|[^\s)]+)\s*\)")
|
||||||
|
INLINE_CODE = re.compile(r"(\x60+).*?\1", re.S)
|
||||||
|
ISSUE_ID = re.compile(r"(?<![A-Za-z0-9_-])P-\d+(?![A-Za-z0-9_-])")
|
||||||
|
ISSUE_ANCHOR = re.compile(r"^p-\d+(?:-|$)")
|
||||||
|
HEADING = re.compile(r"^#{1,6}\s+(.+?)\s*#*\s*$")
|
||||||
|
JOURNALFN= re.compile(r"^\d{4}-\d{2}-\d{2}-.+\.md$")
|
||||||
|
PROMOTE = re.compile(r"^\s*[-*]\s*格上げ条件\s*[::]\s*\S")
|
||||||
|
COMPLETE = re.compile(r"^\s*[-*]\s*完了条件\s*[::]\s*\S")
|
||||||
|
BACKLOG_ENTRY = re.compile(r"^###\s+([A-Za-z][A-Za-z0-9_-]*-P-\d+)\s+\S")
|
||||||
|
BACKLOG_LINK = re.compile(r"^- \[[^\]]+の保留課題\]\(([^)]+)\)$")
|
||||||
|
BACKLOG_FIELDS = ("関連仕様", "未決事項", "保留できる理由", "再開条件", "完了条件")
|
||||||
|
MANAGEMENT_SECTIONS = ("保留課題", "検討済み・非採用", "未確認事項")
|
||||||
|
ROOT = Path(__file__).resolve().parents[3]
|
||||||
|
|
||||||
|
# ルールブック自身は、アンチパターンを例として引用するため散文ヒューリスティックの対象外
|
||||||
|
PROSE_EXEMPT = {"CLAUDE.md"}
|
||||||
|
|
||||||
|
issues = []
|
||||||
|
def add(sev, path, line, msg):
|
||||||
|
issues.append((sev, path, line, msg))
|
||||||
|
|
||||||
|
def load(path):
|
||||||
|
"""コードフェンス内を空行に潰した (行番号, 本文) の列を返す。"""
|
||||||
|
raw = Path(path).read_text(encoding="utf-8").splitlines()
|
||||||
|
out, fence = [], None
|
||||||
|
for i, l in enumerate(raw, 1):
|
||||||
|
marker = re.match(r"^ {0,3}(\x60{3,}|~{3,})(.*)$", l)
|
||||||
|
if fence is not None:
|
||||||
|
out.append((i, ""))
|
||||||
|
if (marker and marker.group(1)[0] == fence[0]
|
||||||
|
and len(marker.group(1)) >= fence[1] and not marker.group(2).strip()):
|
||||||
|
fence = None
|
||||||
|
continue
|
||||||
|
if marker:
|
||||||
|
fence = (marker.group(1)[0], len(marker.group(1)))
|
||||||
|
out.append((i, ""))
|
||||||
|
continue
|
||||||
|
out.append((i, l))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def section(lines, title):
|
||||||
|
"""## <title> 配下の (行番号, 本文) を返す。無ければ None。"""
|
||||||
|
start = None
|
||||||
|
for idx, (n, l) in enumerate(lines):
|
||||||
|
m = H2.match(l)
|
||||||
|
if m and m.group(1) == title:
|
||||||
|
start = idx + 1
|
||||||
|
break
|
||||||
|
if start is None:
|
||||||
|
return None
|
||||||
|
body = []
|
||||||
|
for n, l in lines[start:]:
|
||||||
|
if H2.match(l):
|
||||||
|
break
|
||||||
|
body.append((n, l))
|
||||||
|
return body
|
||||||
|
|
||||||
|
def local_target(path, target):
|
||||||
|
"""相対リンクをリンク元基準で解決する。外部URLは対象外。"""
|
||||||
|
parsed = urlsplit(target)
|
||||||
|
if parsed.scheme or parsed.netloc:
|
||||||
|
return None, ""
|
||||||
|
linked = (path.parent / unquote(parsed.path)).resolve() if parsed.path else path
|
||||||
|
return linked, unquote(parsed.fragment)
|
||||||
|
|
||||||
|
|
||||||
|
def heading_index(lines):
|
||||||
|
"""ATX見出しのアンカーを作る。同じ見出しの連番も扱う。"""
|
||||||
|
headings = {}
|
||||||
|
for n, line in lines:
|
||||||
|
match = HEADING.match(line)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
title = match.group(1)
|
||||||
|
slug = re.sub(r"[^\w\s-]", "", title).lower().replace(" ", "-")
|
||||||
|
anchor, suffix = slug, 0
|
||||||
|
while anchor in headings:
|
||||||
|
suffix += 1
|
||||||
|
anchor = f"{slug}-{suffix}"
|
||||||
|
headings[anchor] = (n, title)
|
||||||
|
return headings
|
||||||
|
|
||||||
|
|
||||||
|
def body_paragraphs(lines):
|
||||||
|
"""改行をまたぐ本文段落を返す。見出し・空行・次の箇条書きで区切る。"""
|
||||||
|
paragraph = []
|
||||||
|
in_management = False
|
||||||
|
for n, line in lines:
|
||||||
|
if HEADING.match(line):
|
||||||
|
if paragraph:
|
||||||
|
yield paragraph
|
||||||
|
paragraph = []
|
||||||
|
h2 = H2.match(line)
|
||||||
|
if h2:
|
||||||
|
in_management = h2.group(1) in MANAGEMENT_SECTIONS
|
||||||
|
elif line.startswith("# "):
|
||||||
|
in_management = False
|
||||||
|
continue
|
||||||
|
if in_management:
|
||||||
|
continue
|
||||||
|
if not line.strip() or re.match(r"^\s*(?:[-*+]|\d+[.)])\s+", line):
|
||||||
|
if paragraph:
|
||||||
|
yield paragraph
|
||||||
|
paragraph = []
|
||||||
|
if line.strip():
|
||||||
|
paragraph.append((n, line))
|
||||||
|
if paragraph:
|
||||||
|
yield paragraph
|
||||||
|
|
||||||
|
|
||||||
|
def check_issue_references(path, rel, lines):
|
||||||
|
"""課題リンクの構造はERROR、本文の参照漏れの候補はWARNにする。"""
|
||||||
|
targets = {}
|
||||||
|
valid_link_lines = set()
|
||||||
|
# インラインコードも位置を変えずにマスクする。
|
||||||
|
text = "\n".join(line for _, line in lines)
|
||||||
|
masked = INLINE_CODE.sub(lambda m: re.sub(r"[^\n]", " ", m.group()), text).split("\n")
|
||||||
|
prose_lines = [(n, line) for (n, _), line in zip(lines, masked)]
|
||||||
|
for n, line in prose_lines:
|
||||||
|
for match in INLINE_LINK.finditer(line):
|
||||||
|
label, target = match.groups()
|
||||||
|
target = target.strip("<>")
|
||||||
|
linked, anchor = local_target(path, target)
|
||||||
|
if linked is None:
|
||||||
|
continue
|
||||||
|
label_ids = ISSUE_ID.findall(label)
|
||||||
|
if not (label_ids or ISSUE_ANCHOR.match(anchor) or anchor == "未確認事項"):
|
||||||
|
continue
|
||||||
|
if not linked.is_file():
|
||||||
|
add("ERROR", rel, n, "未確認事項リンクの参照先ファイルがない: %s" % target)
|
||||||
|
continue
|
||||||
|
if linked not in targets:
|
||||||
|
target_lines = load(linked)
|
||||||
|
entries = {
|
||||||
|
ln: ENTRY.match(value).group(1)
|
||||||
|
for ln, value in (section(target_lines, "未確認事項") or [])
|
||||||
|
if ENTRY.match(value)
|
||||||
|
}
|
||||||
|
targets[linked] = (heading_index(target_lines), entries)
|
||||||
|
headings, entries = targets[linked]
|
||||||
|
if not anchor or anchor not in headings:
|
||||||
|
add("ERROR", rel, n, "未確認事項リンクの参照先見出しがない: %s" % target)
|
||||||
|
continue
|
||||||
|
target_line, _ = headings[anchor]
|
||||||
|
if anchor == "未確認事項" and not label_ids:
|
||||||
|
continue
|
||||||
|
target_id = entries.get(target_line)
|
||||||
|
if target_id is None:
|
||||||
|
add("ERROR", rel, n, "課題リンクが未確認事項の個別項目を指していない: %s" % target)
|
||||||
|
elif any(pid != target_id for pid in label_ids):
|
||||||
|
add("ERROR", rel, n, "リンクラベルの課題番号と参照先が一致しない: %s → %s" % (label, target_id))
|
||||||
|
else:
|
||||||
|
valid_link_lines.add(n)
|
||||||
|
|
||||||
|
for paragraph in body_paragraphs(prose_lines):
|
||||||
|
n = paragraph[0][0]
|
||||||
|
text = "\n".join(line for _, line in paragraph)
|
||||||
|
# リンクと推測タグには別の構造検査がある。裸の課題番号だけを探す。
|
||||||
|
plain = TAG.sub("", INLINE_LINK.sub("", text))
|
||||||
|
bare_ids = sorted(set(ISSUE_ID.findall(plain)))
|
||||||
|
if bare_ids:
|
||||||
|
add("WARN", rel, n, "本文にリンクのない課題番号がある(要確認): %s" % ", ".join(bare_ids))
|
||||||
|
if (re.search(r"未決|未定", plain)
|
||||||
|
and not any(ln in valid_link_lines for ln, _ in paragraph)
|
||||||
|
and not TAG.search(text)):
|
||||||
|
add("WARN", rel, n, "本文の未決・未定に課題への参照がない(要確認)。リンク・課題追加・言及削除を判断する")
|
||||||
|
|
||||||
|
|
||||||
|
def check_backlog(path, rel, lines, seen_ids):
|
||||||
|
blocks = []
|
||||||
|
current = None
|
||||||
|
for n, line in lines:
|
||||||
|
if H2.match(line):
|
||||||
|
current = None
|
||||||
|
elif re.match(r"^###\s", line):
|
||||||
|
match = BACKLOG_ENTRY.match(line)
|
||||||
|
current = None
|
||||||
|
if not match:
|
||||||
|
add("ERROR", rel, n, "保留課題の見出しは ### <区分>-P-<番号> <題名> とする")
|
||||||
|
continue
|
||||||
|
pid = match.group(1)
|
||||||
|
if pid in seen_ids:
|
||||||
|
add("ERROR", rel, n, "保留課題のID %s が重複している(初出: %s)" % (pid, seen_ids[pid]))
|
||||||
|
seen_ids[pid] = "%s:%d" % (rel, n)
|
||||||
|
current = (pid, n, [])
|
||||||
|
blocks.append(current)
|
||||||
|
elif current is not None:
|
||||||
|
current[2].append(line)
|
||||||
|
|
||||||
|
for pid, n, body in blocks:
|
||||||
|
for field in BACKLOG_FIELDS:
|
||||||
|
pattern = re.compile(r"^[-*]\s+" + re.escape(field) + r"\s*[::]\s*\S")
|
||||||
|
if not any(pattern.match(line) for line in body):
|
||||||
|
add("ERROR", rel, n, "%s に「- %s:」の内容がない" % (pid, field))
|
||||||
|
|
||||||
|
for n, line in lines:
|
||||||
|
if TAG.search(line):
|
||||||
|
add("ERROR", rel, n, "本文の前提となる推測タグはbacklogではなく仕様書に置く")
|
||||||
|
for target in MDLINK.findall(line):
|
||||||
|
linked, _ = local_target(path, target)
|
||||||
|
if linked is not None and not linked.is_file():
|
||||||
|
add("ERROR", rel, n, "backlogの参照先ファイルがない: %s" % target)
|
||||||
|
|
||||||
|
|
||||||
|
def check_backlog_section(path, rel, lines):
|
||||||
|
positions = []
|
||||||
|
for title in MANAGEMENT_SECTIONS:
|
||||||
|
headings = [n for n, line in lines if H2.match(line) and H2.match(line).group(1) == title]
|
||||||
|
if len(headings) > 1:
|
||||||
|
add("ERROR", rel, headings[1], "## %s が重複している" % title)
|
||||||
|
if headings:
|
||||||
|
positions.append(headings[0])
|
||||||
|
if positions != sorted(positions):
|
||||||
|
add("ERROR", rel, min(positions), "末尾の管理セクションは保留課題 → 検討済み・非採用 → 未確認事項の順に置く")
|
||||||
|
|
||||||
|
deferred = section(lines, "保留課題")
|
||||||
|
if deferred is None:
|
||||||
|
return
|
||||||
|
contents = [(n, line) for n, line in deferred if line.strip()]
|
||||||
|
if not contents:
|
||||||
|
add("ERROR", rel, 1, "空の ## 保留課題 はセクションごと削除する")
|
||||||
|
for n, line in contents:
|
||||||
|
match = BACKLOG_LINK.fullmatch(line)
|
||||||
|
if not match:
|
||||||
|
add("ERROR", rel, n, "保留課題には - [<区分>の保留課題](backlogのファイル#見出し) だけを書く")
|
||||||
|
continue
|
||||||
|
target = match.group(1)
|
||||||
|
linked, anchor = local_target(path, target)
|
||||||
|
if (linked is None or not linked.is_relative_to(ROOT / "backlog")
|
||||||
|
or linked.suffix != ".md" or not anchor):
|
||||||
|
add("ERROR", rel, n, "保留課題はbacklog内のMarkdownファイルの見出しへリンクする: %s" % target)
|
||||||
|
continue
|
||||||
|
if not linked.is_file():
|
||||||
|
add("ERROR", rel, n, "保留課題の参照先ファイルがない: %s" % target)
|
||||||
|
continue
|
||||||
|
headings = heading_index(load(linked))
|
||||||
|
if anchor not in headings:
|
||||||
|
add("ERROR", rel, n, "保留課題の参照先見出しがない: %s" % target)
|
||||||
|
|
||||||
|
|
||||||
|
def check(path, seen_backlog_ids=None):
|
||||||
|
path = Path(path).resolve()
|
||||||
|
rel = path.relative_to(ROOT).as_posix() if path.is_relative_to(ROOT) else path.as_posix()
|
||||||
|
base = path.name
|
||||||
|
is_journal = path.is_relative_to(ROOT / "journal")
|
||||||
|
is_backlog = path.is_relative_to(ROOT / "backlog")
|
||||||
|
lines = load(path)
|
||||||
|
|
||||||
|
if is_backlog:
|
||||||
|
check_backlog(path, rel, lines, seen_backlog_ids if seen_backlog_ids is not None else {})
|
||||||
|
elif not is_journal:
|
||||||
|
check_backlog_section(path, rel, lines)
|
||||||
|
if base not in PROSE_EXEMPT:
|
||||||
|
check_issue_references(path, rel, lines)
|
||||||
|
|
||||||
|
unconf = section(lines, "未確認事項")
|
||||||
|
rejected = section(lines, "検討済み・非採用")
|
||||||
|
unconf_span = set(n for n, _ in unconf) if unconf else set()
|
||||||
|
tagged = {pid for n, line in lines if n not in unconf_span for pid in TAG.findall(line)}
|
||||||
|
|
||||||
|
# --- ルール4: 推測タグと未確認事項の対応 ---
|
||||||
|
entries = set()
|
||||||
|
if unconf is not None:
|
||||||
|
blocks = [] # [(pid, 行番号, [本文行])] 重複IDも個別に検査する
|
||||||
|
for n, l in unconf:
|
||||||
|
m = ENTRY.match(l)
|
||||||
|
if m:
|
||||||
|
pid = m.group(1)
|
||||||
|
if pid in entries:
|
||||||
|
add("ERROR", rel, n, "未確認事項のID %s が重複している" % pid)
|
||||||
|
entries.add(pid)
|
||||||
|
blocks.append((pid, n, []))
|
||||||
|
elif blocks:
|
||||||
|
blocks[-1][2].append(l)
|
||||||
|
for pid, ln, body in blocks:
|
||||||
|
has_promote = any(PROMOTE.match(x) for x in body)
|
||||||
|
has_complete = any(COMPLETE.match(x) for x in body)
|
||||||
|
if pid in tagged and not has_promote:
|
||||||
|
add("ERROR", rel, ln,
|
||||||
|
"%s に「- 格上げ条件:」の行がない(何を確認すれば事実に格上げできるか)" % pid)
|
||||||
|
elif not has_promote and not has_complete:
|
||||||
|
add("ERROR", rel, ln,
|
||||||
|
"%s に「- 格上げ条件:」または「- 完了条件:」の内容がない" % pid)
|
||||||
|
|
||||||
|
for n, l in lines:
|
||||||
|
if n in unconf_span:
|
||||||
|
continue
|
||||||
|
for pid in TAG.findall(l):
|
||||||
|
if unconf is None or pid not in entries:
|
||||||
|
add("ERROR", rel, n,
|
||||||
|
"本文の [推測:%s] に対応する項目が ## 未確認事項 にない" % pid)
|
||||||
|
# 逆向き(本文タグのない未確認事項)は「未解決の問い」として正当なのでエラーにしない
|
||||||
|
|
||||||
|
# --- ルール3: journal への/からのリンク禁止 ---
|
||||||
|
for n, l in lines:
|
||||||
|
for target in MDLINK.findall(l):
|
||||||
|
linked, _ = local_target(path, target)
|
||||||
|
if linked is not None and linked.is_relative_to(ROOT / "journal"):
|
||||||
|
add("ERROR", rel, n, "journal へのリンクが張られている: %s" % target)
|
||||||
|
elif is_journal and linked is not None:
|
||||||
|
add("ERROR", rel, n, "journal から他ファイルへのリンクが張られている: %s" % target)
|
||||||
|
|
||||||
|
# --- ルール1: 検討済み・非採用 ---
|
||||||
|
if rejected is not None:
|
||||||
|
items = [(n, l) for n, l in rejected if l.strip().startswith(("-", "*"))]
|
||||||
|
for n, l in items:
|
||||||
|
if not re.search(r"\(journal/[^)]+\.md\)", l):
|
||||||
|
add("ERROR", rel, n,
|
||||||
|
"検討済み・非採用の項目が journal ファイル名を参照していない")
|
||||||
|
if len(items) > 5:
|
||||||
|
add("WARN", rel, items[5][0],
|
||||||
|
"検討済み・非採用が %d 件(目安5件)。再提案を防ぐ必要性で掲載を見直す" % len(items))
|
||||||
|
|
||||||
|
# --- journal 固有 ---
|
||||||
|
if is_journal:
|
||||||
|
if not JOURNALFN.match(base):
|
||||||
|
add("ERROR", rel, 1, "journal のファイル名が YYYY-MM-DD-<topic>.md 形式でない")
|
||||||
|
head = " ".join(l for n, l in lines[:20])
|
||||||
|
if "結論" not in head:
|
||||||
|
add("WARN", rel, 1, "冒頭20行に結論が見当たらない(結論を先頭に置く)")
|
||||||
|
|
||||||
|
# --- ルール1: 調査・比較の混入(要目視確認) ---
|
||||||
|
if not is_journal and not is_backlog and base not in PROSE_EXEMPT:
|
||||||
|
# 検討済み・非採用 は却下案を列挙する場所なので対象外
|
||||||
|
skip = set(n for n, _ in rejected) if rejected else set()
|
||||||
|
for n, l in lines:
|
||||||
|
if n in skip:
|
||||||
|
continue
|
||||||
|
if "メリット" in l and "デメリット" in l:
|
||||||
|
add("WARN", rel, n, "比較表らしき記述(メリット/デメリット)。journal 行きでは?")
|
||||||
|
elif re.search(r"却下|不採用|見送っ|も検討したが|案[A-Z]|[A-Z]案", l):
|
||||||
|
add("WARN", rel, n, "却下案の検討過程らしき記述。制約文に書き換えるか journal へ")
|
||||||
|
elif re.search(r"優先度", l) and l.lstrip().startswith("#"):
|
||||||
|
add("WARN", rel, n, "優先度評価は journal 行きでは?")
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
issues.clear()
|
||||||
|
targets = [Path(arg).resolve() for arg in argv[1:]]
|
||||||
|
if not targets:
|
||||||
|
targets = (sorted(ROOT.glob("*.md")) + sorted((ROOT / "journal").glob("*.md"))
|
||||||
|
+ sorted((ROOT / "backlog").rglob("*.md")))
|
||||||
|
targets = list(dict.fromkeys(targets))
|
||||||
|
if not targets:
|
||||||
|
print("対象ファイルなし")
|
||||||
|
return 0
|
||||||
|
seen_backlog_ids = {}
|
||||||
|
for t in targets:
|
||||||
|
if t.is_file():
|
||||||
|
check(t, seen_backlog_ids)
|
||||||
|
else:
|
||||||
|
add("ERROR", t.as_posix(), 1, "対象ファイルが見つからない")
|
||||||
|
|
||||||
|
errs = [i for i in issues if i[0] == "ERROR"]
|
||||||
|
warns = [i for i in issues if i[0] == "WARN"]
|
||||||
|
for sev, path, line, msg in errs + warns:
|
||||||
|
print("%-5s %s:%d %s" % (sev, path, line, msg))
|
||||||
|
print("\n%d files / ERROR %d / WARN %d" % (len(targets), len(errs), len(warns)))
|
||||||
|
return 1 if errs else 0
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if hasattr(sys.stdout, "reconfigure"):
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.exit(main(sys.argv))
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
"""文書の分類・参照・完了条件の回帰検査。標準ライブラリだけで実行する。"""
|
||||||
|
import contextlib
|
||||||
|
import importlib.util
|
||||||
|
import io
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import quote
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location("doc_lint", Path(__file__).with_name("doc-lint.py"))
|
||||||
|
lint = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(lint)
|
||||||
|
|
||||||
|
|
||||||
|
def backlog_item(pid="PHY-P-14"):
|
||||||
|
return (
|
||||||
|
f"### {pid} 原点の吸着\n\n"
|
||||||
|
"- 関連仕様: [PHY](../PHY.md)\n"
|
||||||
|
"- 未決事項: 吸着が必要か\n"
|
||||||
|
"- 保留できる理由: 数値入力で編集できる\n"
|
||||||
|
"- 再開条件: 原点移動を試作するとき\n"
|
||||||
|
"- 完了条件: 吸着の要否を決める\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def issue_section():
|
||||||
|
return (
|
||||||
|
"## 未確認事項\n\n"
|
||||||
|
"### P-1 動作の範囲\n\n- 完了条件: 範囲を決める\n\n"
|
||||||
|
"### P-3 対応・状態管理\n\n- 完了条件: 管理方法を決める\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DocLintTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
temporary = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(temporary.cleanup)
|
||||||
|
self.root = Path(temporary.name).resolve()
|
||||||
|
root_patch = patch.object(lint, "ROOT", self.root)
|
||||||
|
root_patch.start()
|
||||||
|
self.addCleanup(root_patch.stop)
|
||||||
|
self.write("PHY.md", "# PHY\n")
|
||||||
|
|
||||||
|
def write(self, relative, content):
|
||||||
|
path = self.root / relative
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(content, encoding="utf-8")
|
||||||
|
return path
|
||||||
|
|
||||||
|
def run_lint(self, *paths):
|
||||||
|
output = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(output):
|
||||||
|
status = lint.main(["doc-lint.py", *(str(path) for path in paths)])
|
||||||
|
return status, output.getvalue()
|
||||||
|
|
||||||
|
def test_default_scan_includes_backlog_and_accepts_fixed_links(self):
|
||||||
|
self.write("PHY.md", "# PHY\n\n## 保留課題\n\n- [PHYの保留課題](backlog/README.md#phy)\n")
|
||||||
|
self.write("backlog/README.md", "# 保留課題\n\n## PHY\n\n" + backlog_item())
|
||||||
|
self.write("journal/2026-09-22-decision.md", "# 決定\n\n## 結論\n\n採用した。\n")
|
||||||
|
status, output = self.run_lint()
|
||||||
|
self.assertEqual(status, 0, output)
|
||||||
|
self.assertIn("3 files / ERROR 0", output)
|
||||||
|
|
||||||
|
def test_backlog_fields_must_have_values(self):
|
||||||
|
for field in lint.BACKLOG_FIELDS:
|
||||||
|
with self.subTest(field=field):
|
||||||
|
text = backlog_item()
|
||||||
|
text = "\n".join(f"- {field}: " if line.startswith(f"- {field}:") else line
|
||||||
|
for line in text.splitlines())
|
||||||
|
path = self.write("backlog/README.md", "## PHY\n" + text)
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1)
|
||||||
|
self.assertIn(f"- {field}:", output)
|
||||||
|
|
||||||
|
def test_duplicate_backlog_ids_across_nested_files(self):
|
||||||
|
self.write("backlog/one.md", "## PHY\n" + backlog_item())
|
||||||
|
self.write("backlog/sub/two.md", "## PHY\n" + backlog_item().replace("../PHY.md", "../../PHY.md"))
|
||||||
|
status, output = self.run_lint()
|
||||||
|
self.assertEqual(status, 1)
|
||||||
|
self.assertIn("保留課題のID PHY-P-14 が重複", output)
|
||||||
|
|
||||||
|
def test_incomplete_last_item_cannot_borrow_fields_from_next_section(self):
|
||||||
|
self.write("backlog/README.md", "## PHY\n### PHY-P-14 吸着\n\n## ELC\n" + backlog_item("ELC-P-1"))
|
||||||
|
status, output = self.run_lint()
|
||||||
|
self.assertEqual(status, 1)
|
||||||
|
self.assertIn("PHY-P-14 に「- 完了条件:」", output)
|
||||||
|
|
||||||
|
def test_invalid_backlog_heading_and_missing_reference(self):
|
||||||
|
path = self.write("backlog/README.md", "## PHY\n### P-14 吸着\n" + backlog_item().replace("../PHY.md", "../missing.md"))
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1)
|
||||||
|
self.assertIn("保留課題の見出し", output)
|
||||||
|
self.assertIn("参照先ファイルがない", output)
|
||||||
|
|
||||||
|
def test_spec_backlog_section_rejects_prose_empty_and_broken_targets(self):
|
||||||
|
self.write("backlog/README.md", "## PHY\n" + backlog_item())
|
||||||
|
cases = {
|
||||||
|
"内容をここにも要約する。": "だけを書く",
|
||||||
|
"": "空の ## 保留課題",
|
||||||
|
"- [PHYの保留課題](backlog/missing.md#phy)": "参照先ファイルがない",
|
||||||
|
"- [PHYの保留課題](backlog/README.md#missing)": "参照先見出しがない",
|
||||||
|
"- [PHYの保留課題](backlog/README.md)": "見出しへリンク",
|
||||||
|
"- [PHYの保留課題](PHY.md#phy)": "backlog内",
|
||||||
|
}
|
||||||
|
for content, expected in cases.items():
|
||||||
|
with self.subTest(content=content):
|
||||||
|
path = self.write("PHY.md", "# PHY\n\n## 保留課題\n\n" + content + "\n")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1)
|
||||||
|
self.assertIn(expected, output)
|
||||||
|
|
||||||
|
def test_management_sections_have_one_order(self):
|
||||||
|
path = self.write("PHY.md", "## 未確認事項\n\n(なし)\n\n## 検討済み・非採用\n\n## 未確認事項\n")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1)
|
||||||
|
self.assertIn("順に置く", output)
|
||||||
|
self.assertIn("## 未確認事項 が重複", output)
|
||||||
|
|
||||||
|
def test_decisions_accept_completion_but_tagged_assumptions_require_evidence(self):
|
||||||
|
text = "## 未確認事項\n\n### P-1 操作方式\n- 完了条件: 操作方式を決める\n"
|
||||||
|
path = self.write("PHY.md", text)
|
||||||
|
self.assertEqual(self.run_lint(path)[0], 0)
|
||||||
|
path.write_text("本文 [推測:P-1]\n\n" + text, encoding="utf-8")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1)
|
||||||
|
self.assertIn("格上げ条件", output)
|
||||||
|
path.write_text("本文 [推測:P-1]\n\n" + text.replace("完了条件", "格上げ条件"), encoding="utf-8")
|
||||||
|
self.assertEqual(self.run_lint(path)[0], 0)
|
||||||
|
|
||||||
|
def test_absolute_journal_paths_keep_journal_checks(self):
|
||||||
|
path = self.write("journal/bad.md", "# 調査\n\n[仕様](../PHY.md)\n")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1)
|
||||||
|
self.assertIn("journal のファイル名", output)
|
||||||
|
self.assertIn("journal から他ファイルへのリンク", output)
|
||||||
|
|
||||||
|
def test_backlog_is_not_prose_linted_but_cannot_link_to_journal(self):
|
||||||
|
self.write("journal/2026-09-22-decision.md", "## 結論\n")
|
||||||
|
path = self.write("backlog/README.md", "## PHY\n" + backlog_item() + "\n候補はA案。\n[履歴](../journal/2026-09-22-decision.md)\n")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1)
|
||||||
|
self.assertIn("journal へのリンク", output)
|
||||||
|
self.assertIn("WARN 0", output)
|
||||||
|
|
||||||
|
def test_code_examples_do_not_create_real_sections_or_tags(self):
|
||||||
|
path = self.write("PHY.md", "```markdown\n## 保留課題\n[推測:P-9]\n```\n")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 0, output)
|
||||||
|
|
||||||
|
def test_missing_explicit_file_is_an_error(self):
|
||||||
|
status, output = self.run_lint(self.root / "missing.md")
|
||||||
|
self.assertEqual(status, 1)
|
||||||
|
self.assertIn("対象ファイルが見つからない", output)
|
||||||
|
|
||||||
|
def test_issue_links_accept_same_file_cross_file_and_encoded_anchors(self):
|
||||||
|
self.write("MAPレイヤー.md", "# MAP\n\n" + issue_section())
|
||||||
|
encoded = quote("MAPレイヤー.md") + "#" + quote("p-3-対応状態管理")
|
||||||
|
path = self.write(
|
||||||
|
"PHY.md",
|
||||||
|
"# PHY\n\n範囲は未決。\n[P-1](#p-1-動作の範囲)で扱う。\n\n"
|
||||||
|
"[MAPのP-3](MAPレイヤー.md#p-3-対応状態管理)へ。\n\n"
|
||||||
|
f"[MAPのP-3]({encoded})へ。\n\n" + issue_section(),
|
||||||
|
)
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 0, output)
|
||||||
|
self.assertIn("ERROR 0 / WARN 0", output)
|
||||||
|
|
||||||
|
def test_issue_links_reject_missing_file_heading_and_fragment(self):
|
||||||
|
self.write("MAP.md", "# MAP\n\n" + issue_section())
|
||||||
|
for target, expected in (
|
||||||
|
("missing.md#p-3-対応状態管理", "参照先ファイルがない"),
|
||||||
|
("MAP.md#p-3-古い題名", "参照先見出しがない"),
|
||||||
|
("MAP.md", "参照先見出しがない"),
|
||||||
|
):
|
||||||
|
with self.subTest(target=target):
|
||||||
|
path = self.write("PHY.md", f"[P-3]({target})\n")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1, output)
|
||||||
|
self.assertIn(expected, output)
|
||||||
|
|
||||||
|
def test_issue_label_must_match_real_entry_in_unconfirmed_section(self):
|
||||||
|
self.write("MAP.md", "# MAP\n\n### P-2 本文の見出し\n\n" + issue_section())
|
||||||
|
for link, expected in (
|
||||||
|
("[P-3](MAP.md#p-1-動作の範囲)", "課題番号と参照先が一致しない"),
|
||||||
|
("[MAPのP-3](MAP.md#p-1-動作の範囲)", "課題番号と参照先が一致しない"),
|
||||||
|
("[P-2](MAP.md#p-2-本文の見出し)", "個別項目を指していない"),
|
||||||
|
("[P-3](MAP.md#未確認事項)", "個別項目を指していない"),
|
||||||
|
):
|
||||||
|
with self.subTest(link=link):
|
||||||
|
path = self.write("PHY.md", link + "\n")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1, output)
|
||||||
|
self.assertIn(expected, output)
|
||||||
|
|
||||||
|
def test_duplicate_heading_slug_resolves_actual_target(self):
|
||||||
|
self.write("MAP.md", "# MAP\n\n### P-1 動作の範囲\n\n" + issue_section())
|
||||||
|
path = self.write("PHY.md", "[P-1](MAP.md#p-1-動作の範囲-1)\n")
|
||||||
|
self.assertEqual(self.run_lint(path)[0], 0)
|
||||||
|
path.write_text("[P-1](MAP.md#p-1-動作の範囲)\n", encoding="utf-8")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1, output)
|
||||||
|
self.assertIn("個別項目を指していない", output)
|
||||||
|
|
||||||
|
def test_unresolved_prose_warns_without_failing_or_modifying_documents(self):
|
||||||
|
text = "# PHY\n\n方式は未定。P-3で検討する。\n\n" + issue_section()
|
||||||
|
path = self.write("PHY.md", text)
|
||||||
|
before = path.read_bytes()
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 0, output)
|
||||||
|
self.assertIn("リンクのない課題番号", output)
|
||||||
|
self.assertIn("未決・未定に課題への参照がない", output)
|
||||||
|
self.assertEqual(path.read_bytes(), before)
|
||||||
|
|
||||||
|
def test_another_issue_link_does_not_hide_bare_id(self):
|
||||||
|
path = self.write(
|
||||||
|
"PHY.md", "P-3と[P-1](#p-1-動作の範囲)を参照。\n\n" + issue_section()
|
||||||
|
)
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 0, output)
|
||||||
|
self.assertIn("リンクのない課題番号がある(要確認): P-3", output)
|
||||||
|
|
||||||
|
def test_unrelated_links_and_section_links_do_not_hide_unresolved_prose(self):
|
||||||
|
for link in ("[仕様](PHY.md)", "[一覧](#未確認事項)", "[外部](https://example.com/)"):
|
||||||
|
with self.subTest(link=link):
|
||||||
|
path = self.write("PHY.md", f"範囲は未決。{link}\n\n" + issue_section())
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 0, output)
|
||||||
|
self.assertIn("未決・未定に課題への参照がない", output)
|
||||||
|
|
||||||
|
def test_broken_link_does_not_count_as_resolved_reference(self):
|
||||||
|
path = self.write("PHY.md", "範囲は未決。\n[P-1](#p-1-missing)\n")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1, output)
|
||||||
|
self.assertIn("参照先見出しがない", output)
|
||||||
|
self.assertIn("未決・未定に課題への参照がない", output)
|
||||||
|
|
||||||
|
def test_paragraph_and_list_boundaries_do_not_borrow_links(self):
|
||||||
|
for body in (
|
||||||
|
"範囲は未決。\n\n[P-1](#p-1-動作の範囲)\n",
|
||||||
|
"- 範囲は未決。\n- [P-1](#p-1-動作の範囲)\n",
|
||||||
|
"1. 範囲は未決。\n2. [P-1](#p-1-動作の範囲)\n",
|
||||||
|
"範囲は未決。\n## 次の話\n[P-1](#p-1-動作の範囲)\n",
|
||||||
|
):
|
||||||
|
with self.subTest(body=body):
|
||||||
|
path = self.write("PHY.md", body + "\n" + issue_section())
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 0, output)
|
||||||
|
self.assertIn("未決・未定に課題への参照がない", output)
|
||||||
|
path = self.write(
|
||||||
|
"PHY.md", "- 範囲は未決。\n [P-1](#p-1-動作の範囲)\n\n" + issue_section()
|
||||||
|
)
|
||||||
|
self.assertIn("ERROR 0 / WARN 0", self.run_lint(path)[1])
|
||||||
|
|
||||||
|
def test_code_examples_external_links_and_assumptions_are_not_prose_warnings(self):
|
||||||
|
path = self.write(
|
||||||
|
"PHY.md",
|
||||||
|
"\x60未決 P-8 [P-8](#missing)\x60\n\n"
|
||||||
|
"\x60\x60\x60markdown\n未定 P-8 [P-8](#missing)\n\x60\x60\x60\n\n"
|
||||||
|
"~~~markdown\n未定 P-8 [P-8](#missing)\n~~~\n\n"
|
||||||
|
"[P-99](https://example.com/#p-99)\n\n"
|
||||||
|
"範囲は未定 [推測:P-1]。\n\n"
|
||||||
|
"## 未確認事項\n\n### P-1 調査\n\n- 格上げ条件: 検証する\n",
|
||||||
|
)
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 0, output)
|
||||||
|
self.assertIn("ERROR 0 / WARN 0", output)
|
||||||
|
|
||||||
|
def test_management_sections_and_non_spec_files_are_exempt_from_prose_warnings(self):
|
||||||
|
self.write("CLAUDE.md", "未決 P-8 [P-8](#missing)\n")
|
||||||
|
self.write("journal/2026-09-22-decision.md", "## 結論\n\n未定 P-8\n")
|
||||||
|
self.write("backlog/README.md", "## PHY\n\n" + backlog_item() + "\n未定 P-8\n")
|
||||||
|
self.write("PHY.md", "# PHY\n\n" + issue_section() + "\n未定 P-8\n")
|
||||||
|
status, output = self.run_lint()
|
||||||
|
self.assertEqual(status, 0, output)
|
||||||
|
self.assertIn("WARN 0", output)
|
||||||
|
|
||||||
|
def test_links_inside_unconfirmed_items_are_still_checked(self):
|
||||||
|
path = self.write("PHY.md", issue_section() + "\n[P-8](#missing)\n")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1, output)
|
||||||
|
self.assertIn("参照先見出しがない", output)
|
||||||
|
|
||||||
|
def test_rejected_count_warning_does_not_select_entries_for_deletion(self):
|
||||||
|
path = self.write(
|
||||||
|
"PHY.md", "## 検討済み・非採用\n\n"
|
||||||
|
+ "".join(f"- 案{i} (journal/2026-09-22-decision.md)\n" for i in range(6))
|
||||||
|
)
|
||||||
|
before = path.read_bytes()
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 0, output)
|
||||||
|
self.assertIn("6 件(目安5件)", output)
|
||||||
|
self.assertIn("再提案を防ぐ必要性", output)
|
||||||
|
self.assertNotIn("古いものから落とす", output)
|
||||||
|
self.assertEqual(path.read_bytes(), before)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
---
|
||||||
|
name: restructure-doc
|
||||||
|
description: 既存ドキュメント1ファイルをAGENTS.mdのルール1〜4に沿って仕分ける。事実・推測・採用案を残し、調査過程をjournalへ移す。
|
||||||
|
disable-model-invocation: true
|
||||||
|
argument-hint: <ファイル名>
|
||||||
|
---
|
||||||
|
|
||||||
|
# ドキュメントを再編する
|
||||||
|
|
||||||
|
運用ルール策定前に書かれたドキュメントを、AGENTS.md のルール1〜4に沿った形に作り直す。
|
||||||
|
**1回の実行で1ファイルだけ**扱う。
|
||||||
|
|
||||||
|
## 最重要の制約
|
||||||
|
|
||||||
|
**情報を失わせない。** この作業は「移動と再配置」であって、要約でも削除でもない。
|
||||||
|
|
||||||
|
- 内容を短くしない。判断に迷った記述は、消さずに残してユーザーに聞く。
|
||||||
|
- メインドキュメントから外す記述は、必ず journal に移す。捨てない。
|
||||||
|
- 元の主張を勝手に言い換えない。特に断定度を上げ下げしない。
|
||||||
|
|
||||||
|
## 手順
|
||||||
|
|
||||||
|
### 1. 全体を読む
|
||||||
|
|
||||||
|
対象ファイルを最初から最後まで読む。部分的に読んで判断しない。
|
||||||
|
|
||||||
|
### 2. 4分類する
|
||||||
|
|
||||||
|
各ブロック(見出し単位、または段落単位)を次のどれかに分ける。
|
||||||
|
|
||||||
|
| 分類 | 行き先 |
|
||||||
|
|---|---|
|
||||||
|
| a. 採用した案 / 現在の設定 / 設計に影響する制約 | メインドキュメントに残す |
|
||||||
|
| b. 裏取りの済んだ事実 | メインドキュメントに残す(文中のどこでもよい) |
|
||||||
|
| c. 未確認の推測・未解決の問い | メインドキュメント末尾の `## 未確認事項` |
|
||||||
|
| d. 調査過程 / 案の比較 / 却下案 / 優先度評価 | `journal/` へ移す |
|
||||||
|
|
||||||
|
判定のヒント:
|
||||||
|
|
||||||
|
- メリット/デメリット表、優先度リスト、「〜も検討したが」 → d
|
||||||
|
- 「〜できるはず」「〜と考えられる」「実現可能」で、実際に試していない → c
|
||||||
|
- 外部仕様の記述(KLEのプロパティ定義など)は、出典を確認できていれば b、していなければ c
|
||||||
|
|
||||||
|
### 3. 分類結果を提示して承認を得る
|
||||||
|
|
||||||
|
**ここで必ず止まる。** ファイルを書き換える前に、分類の一覧をユーザーに見せて確認する。
|
||||||
|
特に b と c の境目(裏取りできているか)は自分で判断せず聞く。
|
||||||
|
|
||||||
|
### 4. メインドキュメントを書き直す
|
||||||
|
|
||||||
|
- a を中心に再構成する。b は文中に自然に置く。
|
||||||
|
- 「なぜBではないのか」の記述が残っていたら、**制約文**に書き換える。
|
||||||
|
- ✗ `階層構造はKLEが非対応なので諦めた`
|
||||||
|
- ○ `階層クラスターはKLE JSONに出力しない(KLE互換性の制約)`
|
||||||
|
- 落とした案があれば末尾 `## 検討済み・非採用` に1案1行(理由なし、journalファイル名を参照)。
|
||||||
|
|
||||||
|
### 5. 未確認事項を立てる
|
||||||
|
|
||||||
|
c に分類したものを末尾の `## 未確認事項` にまとめる。
|
||||||
|
|
||||||
|
- 本文が前提にしている推測 → 本文に `[推測:P-n]` を付け、対応する項目を置く
|
||||||
|
- まだ本文に書けない未解決の問い → 本文タグなしで項目だけ置く
|
||||||
|
- 各項目に**格上げ条件**を必ず書く。思いつかないものはユーザーに聞く(空欄で残さない)
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### P-1 KLEはlabels[9]の内容を保存時に保持する
|
||||||
|
- 位置づけ: 本文「クラスター管理」が前提にしている
|
||||||
|
- 格上げ条件: KLE本家でインポート→エクスポートし、labels[9]が残ることを確認する
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. journal へ移す
|
||||||
|
|
||||||
|
d を `journal/YYYY-MM-DD-<元ファイル名>.md` に移す。
|
||||||
|
|
||||||
|
- 冒頭に**結論**(このファイルの再編で何が確定し、何を落としたか)
|
||||||
|
- 次に案の比較(元ファイルの表をそのまま持ってきてよい)
|
||||||
|
- 以下に元の調査記述
|
||||||
|
- 整形は最低限でよい
|
||||||
|
|
||||||
|
### 7. コミットする
|
||||||
|
|
||||||
|
1ファイル1コミット。メインドキュメントと journal を同じコミットに入れる。
|
||||||
|
|
||||||
|
```
|
||||||
|
<ファイル名> を運用ルールに沿って再編
|
||||||
|
|
||||||
|
<何を残し、何をjournalへ移したか 数行>
|
||||||
|
|
||||||
|
Journal: journal/2026-09-16-<元ファイル名>.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意
|
||||||
|
|
||||||
|
- 複数ファイルを一度に触らない。ファイル間で内容が重複していると気づいた場合は、
|
||||||
|
勝手に統合せず、指摘だけしてそのファイルの再編を終える。
|
||||||
|
- 再編の途中で事実関係の誤りを見つけても、勝手に直さない。指摘する。
|
||||||
@@ -7,8 +7,9 @@ argument-hint: [決定の要旨]
|
|||||||
|
|
||||||
# 決定を記録する
|
# 決定を記録する
|
||||||
|
|
||||||
CLAUDE.md のルール1〜4に従い、決定を4箇所に分散して記録する。
|
共通の運用ルールは CLAUDE.md を正とする。
|
||||||
どれか1つでも欠けると後から追えなくなるので、全部やる。
|
現在の仕様はメインドキュメント、判断の経緯はjournal、変更の要旨はコミットに記録する。
|
||||||
|
「検討済み・非採用」には、将来の再提案を防ぐために必要な案を選んで残す。
|
||||||
|
|
||||||
## 手順
|
## 手順
|
||||||
|
|
||||||
@@ -30,17 +31,22 @@ CLAUDE.md のルール1〜4に従い、決定を4箇所に分散して記録す
|
|||||||
- ✗ `B案はKLEパーサーが壊れるので却下した`
|
- ✗ `B案はKLEパーサーが壊れるので却下した`
|
||||||
- ○ `クラスター情報はKLE JSONに出力しない(KLE互換性の制約)`
|
- ○ `クラスター情報はKLE JSONに出力しない(KLE互換性の制約)`
|
||||||
- 比較表・検討過程・ベンチマーク結果をここに書かない。
|
- 比較表・検討過程・ベンチマーク結果をここに書かない。
|
||||||
|
- 他の担当の詳細を外へ移したあとも、この文書だけで担当範囲が分かるか確認する。
|
||||||
|
境界の理解に必要な一文と参照は残す。参照先の仕様は複製しない。
|
||||||
|
|
||||||
### 2. 落とした案を1行残す
|
### 2. 非採用一覧に残す案を選ぶ
|
||||||
|
|
||||||
メインドキュメント末尾の `## 検討済み・非採用` に追記する。
|
各案について、この記録が将来のどんな再提案を防ぐかを判断する。
|
||||||
|
本文の制約だけで十分なら一覧へ重複記載しない。実際に検討した別方針で、
|
||||||
|
再提案されうるものは、経緯への入口として末尾の `## 検討済み・非採用` に1案1行で残す。
|
||||||
|
|
||||||
```
|
```
|
||||||
- labels[9] へのID埋め込み (journal/2026-09-16-cluster-metadata.md)
|
- labels[9] へのID埋め込み (journal/2026-09-16-cluster-metadata.md)
|
||||||
```
|
```
|
||||||
|
|
||||||
- 理由は書かない。journalファイル名だけ。
|
- 理由は書かない。journalファイル名だけ。
|
||||||
- セクションが5行を超えたら、古いものから落とす。長くなるのは書きすぎのサイン。
|
- 目安は5行まで。件数や古さだけで機械的に削除せず、再提案を防ぐ必要性で掲載を見直す。
|
||||||
|
- 掲載する案がなければセクションを設けない。一覧から外しても経緯はjournalに残す。
|
||||||
|
|
||||||
### 3. journal を書く
|
### 3. journal を書く
|
||||||
|
|
||||||
@@ -55,7 +61,7 @@ CLAUDE.md のルール1〜4に従い、決定を4箇所に分散して記録す
|
|||||||
冒頭20行で結論と選択肢が分かること。整形は最低限でよい。
|
冒頭20行で結論と選択肢が分かること。整形は最低限でよい。
|
||||||
他ファイルへのリンクも、他ファイルからのリンクも張らない。
|
他ファイルへのリンクも、他ファイルからのリンクも張らない。
|
||||||
|
|
||||||
### 4. 推測を扱う
|
### 4. 推測と未決の問いを扱う
|
||||||
|
|
||||||
この決定が未確認の推測に依存する場合:
|
この決定が未確認の推測に依存する場合:
|
||||||
|
|
||||||
@@ -65,6 +71,11 @@ CLAUDE.md のルール1〜4に従い、決定を4箇所に分散して記録す
|
|||||||
逆に、この決定で検証が済んだ推測があれば、タグを外して `## 未確認事項` から削除する。
|
逆に、この決定で検証が済んだ推測があれば、タグを外して `## 未確認事項` から削除する。
|
||||||
本文タグと末尾項目は必ず対で操作する(片方だけ残さない)。
|
本文タグと末尾項目は必ず対で操作する(片方だけ残さない)。
|
||||||
|
|
||||||
|
本文の未決記述は、読者が現在の対応範囲を理解するために必要かを判断する。
|
||||||
|
対応する問いがあればCLAUDE.mdのルール4に従ってリンクし、問いの詳細は複製しない。
|
||||||
|
必要な問いが未登録なら未確認事項を追加し、実装にも現在の説明にも不要な言及なら本文から外す。
|
||||||
|
今は答えに依存せず進められる課題はルール5に従う。「未決」という単語だけで分類しない。
|
||||||
|
|
||||||
### 5. コミットする
|
### 5. コミットする
|
||||||
|
|
||||||
メインドキュメントと journal を**同じコミット**に入れる。1コミット1決定。
|
メインドキュメントと journal を**同じコミット**に入れる。1コミット1決定。
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
---
|
---
|
||||||
name: doc-lint
|
name: doc-lint
|
||||||
description: ドキュメントがCLAUDE.mdの運用ルール1〜4に沿っているか検査する。推測タグと未確認事項の対応漏れ、journalへのリンク、比較表の混入などを検出する。
|
description: ドキュメントがCLAUDE.mdの運用ルール1〜5に沿っているか検査する。推測タグと未確認事項の対応漏れ、保留課題の書式・参照・必須欄、journalへのリンク、比較表の混入などを検出する。
|
||||||
argument-hint: [ファイル...]
|
argument-hint: [ファイル...]
|
||||||
---
|
---
|
||||||
|
|
||||||
# ドキュメントのルール違反を検査する
|
# ドキュメントのルール違反を検査する
|
||||||
|
|
||||||
機械的に判定できる項目はスクリプトで検出し、判断が要る項目だけ目視で見る。
|
機械的に判定できる項目はスクリプトで検出し、判断が要る項目だけ目視で見る。
|
||||||
|
共通の運用ルールは CLAUDE.md を正とする。
|
||||||
**検出して報告するだけで、勝手に直さない。** 修正は内容を確認してもらってから。
|
**検出して報告するだけで、勝手に直さない。** 修正は内容を確認してもらってから。
|
||||||
|
|
||||||
## 1. スクリプトを走らせる
|
## 1. スクリプトを走らせる
|
||||||
@@ -15,25 +16,45 @@ argument-hint: [ファイル...]
|
|||||||
python .claude/skills/doc-lint/doc-lint.py [ファイル...]
|
python .claude/skills/doc-lint/doc-lint.py [ファイル...]
|
||||||
```
|
```
|
||||||
|
|
||||||
引数なしで直下の `*.md` と `journal/*.md` を全部見る。ERROR があれば終了コード 1。
|
Python 3.9以上で実行する。引数なしでリポジトリ直下の `*.md`、`journal/*.md`、`backlog/**/*.md` を全部見る。
|
||||||
|
実行場所にかかわらずリポジトリを基準に分類し、明示したパスは実行ディレクトリから解決する。
|
||||||
|
ERROR があれば終了コード 1。存在しない引数のファイルもERRORにする。
|
||||||
|
|
||||||
検出する項目:
|
検出する項目:
|
||||||
|
|
||||||
| 種別 | 内容 |
|
| 種別 | 内容 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| ERROR | 本文の `[推測:P-n]` に対応する `## 未確認事項` の項目がない |
|
| ERROR | 本文の `[推測:P-n]` に対応する `## 未確認事項` の項目がない |
|
||||||
| ERROR | `## 未確認事項` の項目に `- 格上げ条件:` の行がない |
|
| ERROR | 本文の推測タグに対応する項目に `- 格上げ条件:` の内容がない |
|
||||||
|
| ERROR | タグのない未確認事項に `- 格上げ条件:` または `- 完了条件:` の内容がない |
|
||||||
| ERROR | `## 未確認事項` のIDが重複している |
|
| ERROR | `## 未確認事項` のIDが重複している |
|
||||||
|
| ERROR | 未確認事項へのローカルリンクの参照先ファイル・見出しがない、または個別項目へのリンクが未確認事項の項目を指していない |
|
||||||
|
| ERROR | リンクラベルの課題番号と参照先の課題番号が一致しない |
|
||||||
|
| ERROR | 仕様書の `## 保留課題` がリンク箇条書き以外を含む、空である、参照先ファイル・見出しがない |
|
||||||
|
| ERROR | 末尾の管理セクションが重複する、保留課題 → 検討済み・非採用 → 未確認事項の順でない |
|
||||||
|
| ERROR | backlogの項目IDが不正・検査対象内で重複する、必須5欄がない・空である |
|
||||||
|
| ERROR | backlogに推測タグがある、参照先ファイルが存在しない |
|
||||||
| ERROR | journal へのリンク / journal から他ファイルへのリンク |
|
| ERROR | journal へのリンク / journal から他ファイルへのリンク |
|
||||||
| ERROR | `## 検討済み・非採用` の項目が journal ファイル名を参照していない |
|
| ERROR | `## 検討済み・非採用` の項目が journal ファイル名を参照していない |
|
||||||
| ERROR | journal のファイル名が `YYYY-MM-DD-<topic>.md` 形式でない |
|
| ERROR | journal のファイル名が `YYYY-MM-DD-<topic>.md` 形式でない |
|
||||||
| WARN | `## 検討済み・非採用` が5件を超えた |
|
| WARN | `## 検討済み・非採用` が5件を超えた |
|
||||||
|
| WARN | 本文の段落・箇条書き項目に、リンクのない課題番号や、課題への参照を伴わない「未決」「未定」がある |
|
||||||
| WARN | journal の冒頭20行に結論が見当たらない |
|
| WARN | journal の冒頭20行に結論が見当たらない |
|
||||||
| WARN | メインドキュメントに比較表・優先度評価・却下案の記述らしきもの |
|
| WARN | メインドキュメントに比較表・優先度評価・却下案の記述らしきもの |
|
||||||
|
|
||||||
**本文タグのない `## 未確認事項` の項目はエラーにしない。** 「まだ本文に書けない未解決の問い」として正当。
|
**本文タグのない `## 未確認事項` の項目はエラーにしない。** 実装前に判断が必要な未解決の問いとして正当。
|
||||||
|
設計判断は「完了条件」でよい。タグなしの項目が事実確認なのか設計判断なのかは、目視で確認する。
|
||||||
|
|
||||||
|
backlogでは `### PHY-P-14 題名` のように区分付きIDを使い、関連仕様・未決事項・保留できる理由・再開条件・完了条件を必須とする。
|
||||||
|
仕様書側の書式は `- [PHYの保留課題](backlog/README.md#phy)`。
|
||||||
|
backlogは短い候補・制約を保持できるため、メインドキュメント向けの散文ヒューリスティックは適用しない。
|
||||||
|
|
||||||
WARN の散文検出は当たりが粗い。取りこぼしも誤検出もあるので、出た/出ないを結論にしない。
|
WARN の散文検出は当たりが粗い。取りこぼしも誤検出もあるので、出た/出ないを結論にしない。
|
||||||
|
未確認事項リンクの検査対象は `[P-3](#p-3-...)`・`[MAPのP-3](MAPレイヤー.md#p-3-...)` 等の
|
||||||
|
インラインリンク。Markdown全般のリンク検査ではない。
|
||||||
|
「未決」「未定」のWARNは、次行の参照も同じ段落として扱う。コード例、末尾の管理セクション、
|
||||||
|
journal、backlog、ルールブック自体はこのWARNの対象外とする。
|
||||||
|
WARNから課題の追加や本文の削除を自動決定しない。
|
||||||
|
|
||||||
## 2. スクリプトで見られない項目を確認する
|
## 2. スクリプトで見られない項目を確認する
|
||||||
|
|
||||||
@@ -43,10 +64,21 @@ WARN の散文検出は当たりが粗い。取りこぼしも誤検出もある
|
|||||||
却下理由ではなく、現在効いている制約として書かれているか。
|
却下理由ではなく、現在効いている制約として書かれているか。
|
||||||
- ✗ `階層構造はKLEが非対応なので諦めた`
|
- ✗ `階層構造はKLEが非対応なので諦めた`
|
||||||
- ○ `階層クラスターはKLE JSONに出力しない(KLE互換性の制約)`
|
- ○ `階層クラスターはKLE JSONに出力しない(KLE互換性の制約)`
|
||||||
- **裏取りしていない記述にタグが付いていないか。** 検査項目の中で最も重要で、
|
- **本文が前提にする未確認の記述にタグが付いているか。** 検査項目の中で最も重要で、
|
||||||
機械では判定できない。「〜できるはず」「〜と考えられる」「実現可能」で実際に試していない
|
機械では判定できない。「〜できるはず」「〜と考えられる」「実現可能」で実際に試していない
|
||||||
記述を探し、`[推測:P-n]` が付いているか確認する。
|
記述を探し、`[推測:P-n]` が付いているか確認する。
|
||||||
- **同じ事実が複数ドキュメントに複製されていないか。** 片方だけ古くなる。
|
- **同じ事実が複数ドキュメントに複製されていないか。** 片方だけ古くなる。
|
||||||
|
- **他の担当の説明を移した結果、責務の境界まで消えていないか。**
|
||||||
|
この文書だけで担当範囲が分かるための一文と参照は残し、他の担当の詳細仕様は参照先で管理する。
|
||||||
|
- **非採用一覧の各項目は、将来のどんな再提案を防ぐか。**
|
||||||
|
本文の制約で十分か、実際に検討した別方針として経緯への入口が必要かを判断する。
|
||||||
|
件数・古さ・重複だけで一律に追加・削除しない。
|
||||||
|
- **本文の未決記述は、読者に何を伝えるためにあるか。**
|
||||||
|
現在の対応範囲は本文に残し、問いの詳細は未確認事項へ集約する。
|
||||||
|
対応項目へのリンク、必要な課題の追加、不要な言及の削除のいずれかを判断する。
|
||||||
|
リンクが存在しても、その問いが本文の内容に対応するかは目視で確認する。
|
||||||
|
- **保留中の対応範囲が仕様本文にあるか。** 現在の実装が答えに依存する前提をbacklogへ隠していないか。
|
||||||
|
- **再開条件・完了条件が具体的か。** 仕様書の保留課題リンクは該当区分に向き、解決済みの課題が残っていないか。
|
||||||
- **journal の冒頭20行で、結論と選択肢が分かるか。** 見出しの有無ではなく中身を読む。
|
- **journal の冒頭20行で、結論と選択肢が分かるか。** 見出しの有無ではなく中身を読む。
|
||||||
- **コミット規約**(必要なら `git log --oneline` を見る)
|
- **コミット規約**(必要なら `git log --oneline` を見る)
|
||||||
- 1コミット1決定になっているか
|
- 1コミット1決定になっているか
|
||||||
@@ -54,11 +86,19 @@ WARN の散文検出は当たりが粗い。取りこぼしも誤検出もある
|
|||||||
|
|
||||||
## 3. 報告する
|
## 3. 報告する
|
||||||
|
|
||||||
ERROR / WARN / 目視で見つけたもの を分けて出す。
|
機械検査のERROR / WARNと、目視で確認した内容・未確認の範囲を分けて報告する。
|
||||||
|
機械検査が0件でも、内容の分類・責務の境界・記録の必要性が適切だった証明にはしない。
|
||||||
|
目視確認を省いた場合はその旨を明記する。
|
||||||
修正案は出してよいが、適用は確認を取ってから。事実関係の誤りを見つけた場合も、直さず指摘する。
|
修正案は出してよいが、適用は確認を取ってから。事実関係の誤りを見つけた場合も、直さず指摘する。
|
||||||
|
|
||||||
## 注意
|
## 注意
|
||||||
|
|
||||||
- 運用ルール策定前に書かれた既存ドキュメントは、まだルール1〜4が適用されていない。
|
- 既存ドキュメントへのルール適用は段階的に進めている。
|
||||||
WARN が大量に出るのは想定内で、再編(`/restructure-doc`)のタイミングで解消する。
|
WARN が大量に出るのは想定内で、再編(`/restructure-doc`)のタイミングで解消する。
|
||||||
再編前のファイルの WARN を個別に直そうとしない。
|
再編前のファイルの WARN を個別に直そうとしない。
|
||||||
|
|
||||||
|
スクリプトを変更した場合の回帰検査:
|
||||||
|
|
||||||
|
```
|
||||||
|
python -B -m unittest discover -s .claude/skills/doc-lint -p test_doc_lint.py
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,23 +1,35 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""CLAUDE.md の運用ルール1〜4のうち、機械的に判定できる項目を検査する。
|
"""CLAUDE.md の運用ルール1〜5のうち、機械的に判定できる項目を検査する。
|
||||||
|
|
||||||
使い方:
|
使い方:
|
||||||
python .claude/skills/doc-lint/doc-lint.py [ファイル...]
|
python .claude/skills/doc-lint/doc-lint.py [ファイル...]
|
||||||
引数なしなら リポジトリ直下の *.md と journal/*.md を対象にする。
|
引数なしならリポジトリ直下の *.md、journal/*.md、backlog/**/*.md が対象。
|
||||||
|
明示したパスは実行ディレクトリから解決する。
|
||||||
ERROR が1件でもあれば終了コード 1。
|
ERROR が1件でもあれば終了コード 1。
|
||||||
"""
|
"""
|
||||||
import sys, re, os, io, glob
|
import re
|
||||||
|
import sys
|
||||||
if hasattr(sys.stdout, "reconfigure"):
|
from pathlib import Path
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
from urllib.parse import unquote, urlsplit
|
||||||
|
|
||||||
TAG = re.compile(r"\[推測:(P-\d+)\]")
|
TAG = re.compile(r"\[推測:(P-\d+)\]")
|
||||||
ENTRY = re.compile(r"^###\s+(P-\d+)\b")
|
ENTRY = re.compile(r"^###\s+(P-\d+)\b")
|
||||||
H2 = re.compile(r"^##\s+(.*?)\s*$")
|
H2 = re.compile(r"^##\s+(.*?)\s*$")
|
||||||
MDLINK = re.compile(r"\]\(([^)]+\.md[^)]*)\)")
|
MDLINK = re.compile(r"\]\(([^)]+\.md[^)]*)\)")
|
||||||
|
INLINE_LINK = re.compile(r"(?<!!)\[([^\]\n]+)\]\(\s*(<[^>\n]+>|[^\s)]+)\s*\)")
|
||||||
|
INLINE_CODE = re.compile(r"(\x60+).*?\1", re.S)
|
||||||
|
ISSUE_ID = re.compile(r"(?<![A-Za-z0-9_-])P-\d+(?![A-Za-z0-9_-])")
|
||||||
|
ISSUE_ANCHOR = re.compile(r"^p-\d+(?:-|$)")
|
||||||
|
HEADING = re.compile(r"^#{1,6}\s+(.+?)\s*#*\s*$")
|
||||||
JOURNALFN= re.compile(r"^\d{4}-\d{2}-\d{2}-.+\.md$")
|
JOURNALFN= re.compile(r"^\d{4}-\d{2}-\d{2}-.+\.md$")
|
||||||
PROMOTE = re.compile(r"^\s*[-*]\s*格上げ条件\s*[::]\s*\S")
|
PROMOTE = re.compile(r"^\s*[-*]\s*格上げ条件\s*[::]\s*\S")
|
||||||
|
COMPLETE = re.compile(r"^\s*[-*]\s*完了条件\s*[::]\s*\S")
|
||||||
|
BACKLOG_ENTRY = re.compile(r"^###\s+([A-Za-z][A-Za-z0-9_-]*-P-\d+)\s+\S")
|
||||||
|
BACKLOG_LINK = re.compile(r"^- \[[^\]]+の保留課題\]\(([^)]+)\)$")
|
||||||
|
BACKLOG_FIELDS = ("関連仕様", "未決事項", "保留できる理由", "再開条件", "完了条件")
|
||||||
|
MANAGEMENT_SECTIONS = ("保留課題", "検討済み・非採用", "未確認事項")
|
||||||
|
ROOT = Path(__file__).resolve().parents[3]
|
||||||
|
|
||||||
# ルールブック自身は、アンチパターンを例として引用するため散文ヒューリスティックの対象外
|
# ルールブック自身は、アンチパターンを例として引用するため散文ヒューリスティックの対象外
|
||||||
PROSE_EXEMPT = {"CLAUDE.md"}
|
PROSE_EXEMPT = {"CLAUDE.md"}
|
||||||
@@ -28,14 +40,21 @@ def add(sev, path, line, msg):
|
|||||||
|
|
||||||
def load(path):
|
def load(path):
|
||||||
"""コードフェンス内を空行に潰した (行番号, 本文) の列を返す。"""
|
"""コードフェンス内を空行に潰した (行番号, 本文) の列を返す。"""
|
||||||
raw = io.open(path, encoding="utf-8").read().splitlines()
|
raw = Path(path).read_text(encoding="utf-8").splitlines()
|
||||||
out, fence = [], False
|
out, fence = [], None
|
||||||
for i, l in enumerate(raw, 1):
|
for i, l in enumerate(raw, 1):
|
||||||
if l.lstrip().startswith("```"):
|
marker = re.match(r"^ {0,3}(\x60{3,}|~{3,})(.*)$", l)
|
||||||
fence = not fence
|
if fence is not None:
|
||||||
|
out.append((i, ""))
|
||||||
|
if (marker and marker.group(1)[0] == fence[0]
|
||||||
|
and len(marker.group(1)) >= fence[1] and not marker.group(2).strip()):
|
||||||
|
fence = None
|
||||||
|
continue
|
||||||
|
if marker:
|
||||||
|
fence = (marker.group(1)[0], len(marker.group(1)))
|
||||||
out.append((i, ""))
|
out.append((i, ""))
|
||||||
continue
|
continue
|
||||||
out.append((i, "" if fence else l))
|
out.append((i, l))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def section(lines, title):
|
def section(lines, title):
|
||||||
@@ -55,15 +74,208 @@ def section(lines, title):
|
|||||||
body.append((n, l))
|
body.append((n, l))
|
||||||
return body
|
return body
|
||||||
|
|
||||||
def check(path):
|
def local_target(path, target):
|
||||||
rel = path.replace(os.sep, "/")
|
"""相対リンクをリンク元基準で解決する。外部URLは対象外。"""
|
||||||
base = os.path.basename(rel)
|
parsed = urlsplit(target)
|
||||||
is_journal = rel.startswith("journal/")
|
if parsed.scheme or parsed.netloc:
|
||||||
|
return None, ""
|
||||||
|
linked = (path.parent / unquote(parsed.path)).resolve() if parsed.path else path
|
||||||
|
return linked, unquote(parsed.fragment)
|
||||||
|
|
||||||
|
|
||||||
|
def heading_index(lines):
|
||||||
|
"""ATX見出しのアンカーを作る。同じ見出しの連番も扱う。"""
|
||||||
|
headings = {}
|
||||||
|
for n, line in lines:
|
||||||
|
match = HEADING.match(line)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
title = match.group(1)
|
||||||
|
slug = re.sub(r"[^\w\s-]", "", title).lower().replace(" ", "-")
|
||||||
|
anchor, suffix = slug, 0
|
||||||
|
while anchor in headings:
|
||||||
|
suffix += 1
|
||||||
|
anchor = f"{slug}-{suffix}"
|
||||||
|
headings[anchor] = (n, title)
|
||||||
|
return headings
|
||||||
|
|
||||||
|
|
||||||
|
def body_paragraphs(lines):
|
||||||
|
"""改行をまたぐ本文段落を返す。見出し・空行・次の箇条書きで区切る。"""
|
||||||
|
paragraph = []
|
||||||
|
in_management = False
|
||||||
|
for n, line in lines:
|
||||||
|
if HEADING.match(line):
|
||||||
|
if paragraph:
|
||||||
|
yield paragraph
|
||||||
|
paragraph = []
|
||||||
|
h2 = H2.match(line)
|
||||||
|
if h2:
|
||||||
|
in_management = h2.group(1) in MANAGEMENT_SECTIONS
|
||||||
|
elif line.startswith("# "):
|
||||||
|
in_management = False
|
||||||
|
continue
|
||||||
|
if in_management:
|
||||||
|
continue
|
||||||
|
if not line.strip() or re.match(r"^\s*(?:[-*+]|\d+[.)])\s+", line):
|
||||||
|
if paragraph:
|
||||||
|
yield paragraph
|
||||||
|
paragraph = []
|
||||||
|
if line.strip():
|
||||||
|
paragraph.append((n, line))
|
||||||
|
if paragraph:
|
||||||
|
yield paragraph
|
||||||
|
|
||||||
|
|
||||||
|
def check_issue_references(path, rel, lines):
|
||||||
|
"""課題リンクの構造はERROR、本文の参照漏れの候補はWARNにする。"""
|
||||||
|
targets = {}
|
||||||
|
valid_link_lines = set()
|
||||||
|
# インラインコードも位置を変えずにマスクする。
|
||||||
|
text = "\n".join(line for _, line in lines)
|
||||||
|
masked = INLINE_CODE.sub(lambda m: re.sub(r"[^\n]", " ", m.group()), text).split("\n")
|
||||||
|
prose_lines = [(n, line) for (n, _), line in zip(lines, masked)]
|
||||||
|
for n, line in prose_lines:
|
||||||
|
for match in INLINE_LINK.finditer(line):
|
||||||
|
label, target = match.groups()
|
||||||
|
target = target.strip("<>")
|
||||||
|
linked, anchor = local_target(path, target)
|
||||||
|
if linked is None:
|
||||||
|
continue
|
||||||
|
label_ids = ISSUE_ID.findall(label)
|
||||||
|
if not (label_ids or ISSUE_ANCHOR.match(anchor) or anchor == "未確認事項"):
|
||||||
|
continue
|
||||||
|
if not linked.is_file():
|
||||||
|
add("ERROR", rel, n, "未確認事項リンクの参照先ファイルがない: %s" % target)
|
||||||
|
continue
|
||||||
|
if linked not in targets:
|
||||||
|
target_lines = load(linked)
|
||||||
|
entries = {
|
||||||
|
ln: ENTRY.match(value).group(1)
|
||||||
|
for ln, value in (section(target_lines, "未確認事項") or [])
|
||||||
|
if ENTRY.match(value)
|
||||||
|
}
|
||||||
|
targets[linked] = (heading_index(target_lines), entries)
|
||||||
|
headings, entries = targets[linked]
|
||||||
|
if not anchor or anchor not in headings:
|
||||||
|
add("ERROR", rel, n, "未確認事項リンクの参照先見出しがない: %s" % target)
|
||||||
|
continue
|
||||||
|
target_line, _ = headings[anchor]
|
||||||
|
if anchor == "未確認事項" and not label_ids:
|
||||||
|
continue
|
||||||
|
target_id = entries.get(target_line)
|
||||||
|
if target_id is None:
|
||||||
|
add("ERROR", rel, n, "課題リンクが未確認事項の個別項目を指していない: %s" % target)
|
||||||
|
elif any(pid != target_id for pid in label_ids):
|
||||||
|
add("ERROR", rel, n, "リンクラベルの課題番号と参照先が一致しない: %s → %s" % (label, target_id))
|
||||||
|
else:
|
||||||
|
valid_link_lines.add(n)
|
||||||
|
|
||||||
|
for paragraph in body_paragraphs(prose_lines):
|
||||||
|
n = paragraph[0][0]
|
||||||
|
text = "\n".join(line for _, line in paragraph)
|
||||||
|
# リンクと推測タグには別の構造検査がある。裸の課題番号だけを探す。
|
||||||
|
plain = TAG.sub("", INLINE_LINK.sub("", text))
|
||||||
|
bare_ids = sorted(set(ISSUE_ID.findall(plain)))
|
||||||
|
if bare_ids:
|
||||||
|
add("WARN", rel, n, "本文にリンクのない課題番号がある(要確認): %s" % ", ".join(bare_ids))
|
||||||
|
if (re.search(r"未決|未定", plain)
|
||||||
|
and not any(ln in valid_link_lines for ln, _ in paragraph)
|
||||||
|
and not TAG.search(text)):
|
||||||
|
add("WARN", rel, n, "本文の未決・未定に課題への参照がない(要確認)。リンク・課題追加・言及削除を判断する")
|
||||||
|
|
||||||
|
|
||||||
|
def check_backlog(path, rel, lines, seen_ids):
|
||||||
|
blocks = []
|
||||||
|
current = None
|
||||||
|
for n, line in lines:
|
||||||
|
if H2.match(line):
|
||||||
|
current = None
|
||||||
|
elif re.match(r"^###\s", line):
|
||||||
|
match = BACKLOG_ENTRY.match(line)
|
||||||
|
current = None
|
||||||
|
if not match:
|
||||||
|
add("ERROR", rel, n, "保留課題の見出しは ### <区分>-P-<番号> <題名> とする")
|
||||||
|
continue
|
||||||
|
pid = match.group(1)
|
||||||
|
if pid in seen_ids:
|
||||||
|
add("ERROR", rel, n, "保留課題のID %s が重複している(初出: %s)" % (pid, seen_ids[pid]))
|
||||||
|
seen_ids[pid] = "%s:%d" % (rel, n)
|
||||||
|
current = (pid, n, [])
|
||||||
|
blocks.append(current)
|
||||||
|
elif current is not None:
|
||||||
|
current[2].append(line)
|
||||||
|
|
||||||
|
for pid, n, body in blocks:
|
||||||
|
for field in BACKLOG_FIELDS:
|
||||||
|
pattern = re.compile(r"^[-*]\s+" + re.escape(field) + r"\s*[::]\s*\S")
|
||||||
|
if not any(pattern.match(line) for line in body):
|
||||||
|
add("ERROR", rel, n, "%s に「- %s:」の内容がない" % (pid, field))
|
||||||
|
|
||||||
|
for n, line in lines:
|
||||||
|
if TAG.search(line):
|
||||||
|
add("ERROR", rel, n, "本文の前提となる推測タグはbacklogではなく仕様書に置く")
|
||||||
|
for target in MDLINK.findall(line):
|
||||||
|
linked, _ = local_target(path, target)
|
||||||
|
if linked is not None and not linked.is_file():
|
||||||
|
add("ERROR", rel, n, "backlogの参照先ファイルがない: %s" % target)
|
||||||
|
|
||||||
|
|
||||||
|
def check_backlog_section(path, rel, lines):
|
||||||
|
positions = []
|
||||||
|
for title in MANAGEMENT_SECTIONS:
|
||||||
|
headings = [n for n, line in lines if H2.match(line) and H2.match(line).group(1) == title]
|
||||||
|
if len(headings) > 1:
|
||||||
|
add("ERROR", rel, headings[1], "## %s が重複している" % title)
|
||||||
|
if headings:
|
||||||
|
positions.append(headings[0])
|
||||||
|
if positions != sorted(positions):
|
||||||
|
add("ERROR", rel, min(positions), "末尾の管理セクションは保留課題 → 検討済み・非採用 → 未確認事項の順に置く")
|
||||||
|
|
||||||
|
deferred = section(lines, "保留課題")
|
||||||
|
if deferred is None:
|
||||||
|
return
|
||||||
|
contents = [(n, line) for n, line in deferred if line.strip()]
|
||||||
|
if not contents:
|
||||||
|
add("ERROR", rel, 1, "空の ## 保留課題 はセクションごと削除する")
|
||||||
|
for n, line in contents:
|
||||||
|
match = BACKLOG_LINK.fullmatch(line)
|
||||||
|
if not match:
|
||||||
|
add("ERROR", rel, n, "保留課題には - [<区分>の保留課題](backlogのファイル#見出し) だけを書く")
|
||||||
|
continue
|
||||||
|
target = match.group(1)
|
||||||
|
linked, anchor = local_target(path, target)
|
||||||
|
if (linked is None or not linked.is_relative_to(ROOT / "backlog")
|
||||||
|
or linked.suffix != ".md" or not anchor):
|
||||||
|
add("ERROR", rel, n, "保留課題はbacklog内のMarkdownファイルの見出しへリンクする: %s" % target)
|
||||||
|
continue
|
||||||
|
if not linked.is_file():
|
||||||
|
add("ERROR", rel, n, "保留課題の参照先ファイルがない: %s" % target)
|
||||||
|
continue
|
||||||
|
headings = heading_index(load(linked))
|
||||||
|
if anchor not in headings:
|
||||||
|
add("ERROR", rel, n, "保留課題の参照先見出しがない: %s" % target)
|
||||||
|
|
||||||
|
|
||||||
|
def check(path, seen_backlog_ids=None):
|
||||||
|
path = Path(path).resolve()
|
||||||
|
rel = path.relative_to(ROOT).as_posix() if path.is_relative_to(ROOT) else path.as_posix()
|
||||||
|
base = path.name
|
||||||
|
is_journal = path.is_relative_to(ROOT / "journal")
|
||||||
|
is_backlog = path.is_relative_to(ROOT / "backlog")
|
||||||
lines = load(path)
|
lines = load(path)
|
||||||
|
|
||||||
|
if is_backlog:
|
||||||
|
check_backlog(path, rel, lines, seen_backlog_ids if seen_backlog_ids is not None else {})
|
||||||
|
elif not is_journal:
|
||||||
|
check_backlog_section(path, rel, lines)
|
||||||
|
if base not in PROSE_EXEMPT:
|
||||||
|
check_issue_references(path, rel, lines)
|
||||||
|
|
||||||
unconf = section(lines, "未確認事項")
|
unconf = section(lines, "未確認事項")
|
||||||
rejected = section(lines, "検討済み・非採用")
|
rejected = section(lines, "検討済み・非採用")
|
||||||
unconf_span = set(n for n, _ in unconf) if unconf else set()
|
unconf_span = set(n for n, _ in unconf) if unconf else set()
|
||||||
|
tagged = {pid for n, line in lines if n not in unconf_span for pid in TAG.findall(line)}
|
||||||
|
|
||||||
# --- ルール4: 推測タグと未確認事項の対応 ---
|
# --- ルール4: 推測タグと未確認事項の対応 ---
|
||||||
entries = set()
|
entries = set()
|
||||||
@@ -80,9 +292,14 @@ def check(path):
|
|||||||
elif blocks:
|
elif blocks:
|
||||||
blocks[-1][2].append(l)
|
blocks[-1][2].append(l)
|
||||||
for pid, ln, body in blocks:
|
for pid, ln, body in blocks:
|
||||||
if not any(PROMOTE.match(x) for x in body):
|
has_promote = any(PROMOTE.match(x) for x in body)
|
||||||
|
has_complete = any(COMPLETE.match(x) for x in body)
|
||||||
|
if pid in tagged and not has_promote:
|
||||||
add("ERROR", rel, ln,
|
add("ERROR", rel, ln,
|
||||||
"%s に「- 格上げ条件:」の行がない(何を確認すれば事実に格上げできるか)" % pid)
|
"%s に「- 格上げ条件:」の行がない(何を確認すれば事実に格上げできるか)" % pid)
|
||||||
|
elif not has_promote and not has_complete:
|
||||||
|
add("ERROR", rel, ln,
|
||||||
|
"%s に「- 格上げ条件:」または「- 完了条件:」の内容がない" % pid)
|
||||||
|
|
||||||
for n, l in lines:
|
for n, l in lines:
|
||||||
if n in unconf_span:
|
if n in unconf_span:
|
||||||
@@ -96,9 +313,10 @@ def check(path):
|
|||||||
# --- ルール3: journal への/からのリンク禁止 ---
|
# --- ルール3: journal への/からのリンク禁止 ---
|
||||||
for n, l in lines:
|
for n, l in lines:
|
||||||
for target in MDLINK.findall(l):
|
for target in MDLINK.findall(l):
|
||||||
if target.lstrip("./").startswith("journal/"):
|
linked, _ = local_target(path, target)
|
||||||
|
if linked is not None and linked.is_relative_to(ROOT / "journal"):
|
||||||
add("ERROR", rel, n, "journal へのリンクが張られている: %s" % target)
|
add("ERROR", rel, n, "journal へのリンクが張られている: %s" % target)
|
||||||
elif is_journal:
|
elif is_journal and linked is not None:
|
||||||
add("ERROR", rel, n, "journal から他ファイルへのリンクが張られている: %s" % target)
|
add("ERROR", rel, n, "journal から他ファイルへのリンクが張られている: %s" % target)
|
||||||
|
|
||||||
# --- ルール1: 検討済み・非採用 ---
|
# --- ルール1: 検討済み・非採用 ---
|
||||||
@@ -110,7 +328,7 @@ def check(path):
|
|||||||
"検討済み・非採用の項目が journal ファイル名を参照していない")
|
"検討済み・非採用の項目が journal ファイル名を参照していない")
|
||||||
if len(items) > 5:
|
if len(items) > 5:
|
||||||
add("WARN", rel, items[5][0],
|
add("WARN", rel, items[5][0],
|
||||||
"検討済み・非採用が %d 件(目安5件)。古いものから落とす" % len(items))
|
"検討済み・非採用が %d 件(目安5件)。再提案を防ぐ必要性で掲載を見直す" % len(items))
|
||||||
|
|
||||||
# --- journal 固有 ---
|
# --- journal 固有 ---
|
||||||
if is_journal:
|
if is_journal:
|
||||||
@@ -121,7 +339,7 @@ def check(path):
|
|||||||
add("WARN", rel, 1, "冒頭20行に結論が見当たらない(結論を先頭に置く)")
|
add("WARN", rel, 1, "冒頭20行に結論が見当たらない(結論を先頭に置く)")
|
||||||
|
|
||||||
# --- ルール1: 調査・比較の混入(要目視確認) ---
|
# --- ルール1: 調査・比較の混入(要目視確認) ---
|
||||||
if not is_journal and base not in PROSE_EXEMPT:
|
if not is_journal and not is_backlog and base not in PROSE_EXEMPT:
|
||||||
# 検討済み・非採用 は却下案を列挙する場所なので対象外
|
# 検討済み・非採用 は却下案を列挙する場所なので対象外
|
||||||
skip = set(n for n, _ in rejected) if rejected else set()
|
skip = set(n for n, _ in rejected) if rejected else set()
|
||||||
for n, l in lines:
|
for n, l in lines:
|
||||||
@@ -135,16 +353,21 @@ def check(path):
|
|||||||
add("WARN", rel, n, "優先度評価は journal 行きでは?")
|
add("WARN", rel, n, "優先度評価は journal 行きでは?")
|
||||||
|
|
||||||
def main(argv):
|
def main(argv):
|
||||||
targets = argv[1:]
|
issues.clear()
|
||||||
|
targets = [Path(arg).resolve() for arg in argv[1:]]
|
||||||
if not targets:
|
if not targets:
|
||||||
targets = sorted(glob.glob("*.md")) + sorted(glob.glob("journal/*.md"))
|
targets = (sorted(ROOT.glob("*.md")) + sorted((ROOT / "journal").glob("*.md"))
|
||||||
|
+ sorted((ROOT / "backlog").rglob("*.md")))
|
||||||
|
targets = list(dict.fromkeys(targets))
|
||||||
if not targets:
|
if not targets:
|
||||||
print("対象ファイルなし"); return 0
|
print("対象ファイルなし")
|
||||||
|
return 0
|
||||||
|
seen_backlog_ids = {}
|
||||||
for t in targets:
|
for t in targets:
|
||||||
if os.path.isfile(t):
|
if t.is_file():
|
||||||
check(t)
|
check(t, seen_backlog_ids)
|
||||||
else:
|
else:
|
||||||
print("見つからない: %s" % t)
|
add("ERROR", t.as_posix(), 1, "対象ファイルが見つからない")
|
||||||
|
|
||||||
errs = [i for i in issues if i[0] == "ERROR"]
|
errs = [i for i in issues if i[0] == "ERROR"]
|
||||||
warns = [i for i in issues if i[0] == "WARN"]
|
warns = [i for i in issues if i[0] == "WARN"]
|
||||||
@@ -154,4 +377,6 @@ def main(argv):
|
|||||||
return 1 if errs else 0
|
return 1 if errs else 0
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
if hasattr(sys.stdout, "reconfigure"):
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.exit(main(sys.argv))
|
sys.exit(main(sys.argv))
|
||||||
|
|||||||
@@ -0,0 +1,300 @@
|
|||||||
|
"""文書の分類・参照・完了条件の回帰検査。標準ライブラリだけで実行する。"""
|
||||||
|
import contextlib
|
||||||
|
import importlib.util
|
||||||
|
import io
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import quote
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location("doc_lint", Path(__file__).with_name("doc-lint.py"))
|
||||||
|
lint = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(lint)
|
||||||
|
|
||||||
|
|
||||||
|
def backlog_item(pid="PHY-P-14"):
|
||||||
|
return (
|
||||||
|
f"### {pid} 原点の吸着\n\n"
|
||||||
|
"- 関連仕様: [PHY](../PHY.md)\n"
|
||||||
|
"- 未決事項: 吸着が必要か\n"
|
||||||
|
"- 保留できる理由: 数値入力で編集できる\n"
|
||||||
|
"- 再開条件: 原点移動を試作するとき\n"
|
||||||
|
"- 完了条件: 吸着の要否を決める\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def issue_section():
|
||||||
|
return (
|
||||||
|
"## 未確認事項\n\n"
|
||||||
|
"### P-1 動作の範囲\n\n- 完了条件: 範囲を決める\n\n"
|
||||||
|
"### P-3 対応・状態管理\n\n- 完了条件: 管理方法を決める\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DocLintTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
temporary = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(temporary.cleanup)
|
||||||
|
self.root = Path(temporary.name).resolve()
|
||||||
|
root_patch = patch.object(lint, "ROOT", self.root)
|
||||||
|
root_patch.start()
|
||||||
|
self.addCleanup(root_patch.stop)
|
||||||
|
self.write("PHY.md", "# PHY\n")
|
||||||
|
|
||||||
|
def write(self, relative, content):
|
||||||
|
path = self.root / relative
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(content, encoding="utf-8")
|
||||||
|
return path
|
||||||
|
|
||||||
|
def run_lint(self, *paths):
|
||||||
|
output = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(output):
|
||||||
|
status = lint.main(["doc-lint.py", *(str(path) for path in paths)])
|
||||||
|
return status, output.getvalue()
|
||||||
|
|
||||||
|
def test_default_scan_includes_backlog_and_accepts_fixed_links(self):
|
||||||
|
self.write("PHY.md", "# PHY\n\n## 保留課題\n\n- [PHYの保留課題](backlog/README.md#phy)\n")
|
||||||
|
self.write("backlog/README.md", "# 保留課題\n\n## PHY\n\n" + backlog_item())
|
||||||
|
self.write("journal/2026-09-22-decision.md", "# 決定\n\n## 結論\n\n採用した。\n")
|
||||||
|
status, output = self.run_lint()
|
||||||
|
self.assertEqual(status, 0, output)
|
||||||
|
self.assertIn("3 files / ERROR 0", output)
|
||||||
|
|
||||||
|
def test_backlog_fields_must_have_values(self):
|
||||||
|
for field in lint.BACKLOG_FIELDS:
|
||||||
|
with self.subTest(field=field):
|
||||||
|
text = backlog_item()
|
||||||
|
text = "\n".join(f"- {field}: " if line.startswith(f"- {field}:") else line
|
||||||
|
for line in text.splitlines())
|
||||||
|
path = self.write("backlog/README.md", "## PHY\n" + text)
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1)
|
||||||
|
self.assertIn(f"- {field}:", output)
|
||||||
|
|
||||||
|
def test_duplicate_backlog_ids_across_nested_files(self):
|
||||||
|
self.write("backlog/one.md", "## PHY\n" + backlog_item())
|
||||||
|
self.write("backlog/sub/two.md", "## PHY\n" + backlog_item().replace("../PHY.md", "../../PHY.md"))
|
||||||
|
status, output = self.run_lint()
|
||||||
|
self.assertEqual(status, 1)
|
||||||
|
self.assertIn("保留課題のID PHY-P-14 が重複", output)
|
||||||
|
|
||||||
|
def test_incomplete_last_item_cannot_borrow_fields_from_next_section(self):
|
||||||
|
self.write("backlog/README.md", "## PHY\n### PHY-P-14 吸着\n\n## ELC\n" + backlog_item("ELC-P-1"))
|
||||||
|
status, output = self.run_lint()
|
||||||
|
self.assertEqual(status, 1)
|
||||||
|
self.assertIn("PHY-P-14 に「- 完了条件:」", output)
|
||||||
|
|
||||||
|
def test_invalid_backlog_heading_and_missing_reference(self):
|
||||||
|
path = self.write("backlog/README.md", "## PHY\n### P-14 吸着\n" + backlog_item().replace("../PHY.md", "../missing.md"))
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1)
|
||||||
|
self.assertIn("保留課題の見出し", output)
|
||||||
|
self.assertIn("参照先ファイルがない", output)
|
||||||
|
|
||||||
|
def test_spec_backlog_section_rejects_prose_empty_and_broken_targets(self):
|
||||||
|
self.write("backlog/README.md", "## PHY\n" + backlog_item())
|
||||||
|
cases = {
|
||||||
|
"内容をここにも要約する。": "だけを書く",
|
||||||
|
"": "空の ## 保留課題",
|
||||||
|
"- [PHYの保留課題](backlog/missing.md#phy)": "参照先ファイルがない",
|
||||||
|
"- [PHYの保留課題](backlog/README.md#missing)": "参照先見出しがない",
|
||||||
|
"- [PHYの保留課題](backlog/README.md)": "見出しへリンク",
|
||||||
|
"- [PHYの保留課題](PHY.md#phy)": "backlog内",
|
||||||
|
}
|
||||||
|
for content, expected in cases.items():
|
||||||
|
with self.subTest(content=content):
|
||||||
|
path = self.write("PHY.md", "# PHY\n\n## 保留課題\n\n" + content + "\n")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1)
|
||||||
|
self.assertIn(expected, output)
|
||||||
|
|
||||||
|
def test_management_sections_have_one_order(self):
|
||||||
|
path = self.write("PHY.md", "## 未確認事項\n\n(なし)\n\n## 検討済み・非採用\n\n## 未確認事項\n")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1)
|
||||||
|
self.assertIn("順に置く", output)
|
||||||
|
self.assertIn("## 未確認事項 が重複", output)
|
||||||
|
|
||||||
|
def test_decisions_accept_completion_but_tagged_assumptions_require_evidence(self):
|
||||||
|
text = "## 未確認事項\n\n### P-1 操作方式\n- 完了条件: 操作方式を決める\n"
|
||||||
|
path = self.write("PHY.md", text)
|
||||||
|
self.assertEqual(self.run_lint(path)[0], 0)
|
||||||
|
path.write_text("本文 [推測:P-1]\n\n" + text, encoding="utf-8")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1)
|
||||||
|
self.assertIn("格上げ条件", output)
|
||||||
|
path.write_text("本文 [推測:P-1]\n\n" + text.replace("完了条件", "格上げ条件"), encoding="utf-8")
|
||||||
|
self.assertEqual(self.run_lint(path)[0], 0)
|
||||||
|
|
||||||
|
def test_absolute_journal_paths_keep_journal_checks(self):
|
||||||
|
path = self.write("journal/bad.md", "# 調査\n\n[仕様](../PHY.md)\n")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1)
|
||||||
|
self.assertIn("journal のファイル名", output)
|
||||||
|
self.assertIn("journal から他ファイルへのリンク", output)
|
||||||
|
|
||||||
|
def test_backlog_is_not_prose_linted_but_cannot_link_to_journal(self):
|
||||||
|
self.write("journal/2026-09-22-decision.md", "## 結論\n")
|
||||||
|
path = self.write("backlog/README.md", "## PHY\n" + backlog_item() + "\n候補はA案。\n[履歴](../journal/2026-09-22-decision.md)\n")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1)
|
||||||
|
self.assertIn("journal へのリンク", output)
|
||||||
|
self.assertIn("WARN 0", output)
|
||||||
|
|
||||||
|
def test_code_examples_do_not_create_real_sections_or_tags(self):
|
||||||
|
path = self.write("PHY.md", "```markdown\n## 保留課題\n[推測:P-9]\n```\n")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 0, output)
|
||||||
|
|
||||||
|
def test_missing_explicit_file_is_an_error(self):
|
||||||
|
status, output = self.run_lint(self.root / "missing.md")
|
||||||
|
self.assertEqual(status, 1)
|
||||||
|
self.assertIn("対象ファイルが見つからない", output)
|
||||||
|
|
||||||
|
def test_issue_links_accept_same_file_cross_file_and_encoded_anchors(self):
|
||||||
|
self.write("MAPレイヤー.md", "# MAP\n\n" + issue_section())
|
||||||
|
encoded = quote("MAPレイヤー.md") + "#" + quote("p-3-対応状態管理")
|
||||||
|
path = self.write(
|
||||||
|
"PHY.md",
|
||||||
|
"# PHY\n\n範囲は未決。\n[P-1](#p-1-動作の範囲)で扱う。\n\n"
|
||||||
|
"[MAPのP-3](MAPレイヤー.md#p-3-対応状態管理)へ。\n\n"
|
||||||
|
f"[MAPのP-3]({encoded})へ。\n\n" + issue_section(),
|
||||||
|
)
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 0, output)
|
||||||
|
self.assertIn("ERROR 0 / WARN 0", output)
|
||||||
|
|
||||||
|
def test_issue_links_reject_missing_file_heading_and_fragment(self):
|
||||||
|
self.write("MAP.md", "# MAP\n\n" + issue_section())
|
||||||
|
for target, expected in (
|
||||||
|
("missing.md#p-3-対応状態管理", "参照先ファイルがない"),
|
||||||
|
("MAP.md#p-3-古い題名", "参照先見出しがない"),
|
||||||
|
("MAP.md", "参照先見出しがない"),
|
||||||
|
):
|
||||||
|
with self.subTest(target=target):
|
||||||
|
path = self.write("PHY.md", f"[P-3]({target})\n")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1, output)
|
||||||
|
self.assertIn(expected, output)
|
||||||
|
|
||||||
|
def test_issue_label_must_match_real_entry_in_unconfirmed_section(self):
|
||||||
|
self.write("MAP.md", "# MAP\n\n### P-2 本文の見出し\n\n" + issue_section())
|
||||||
|
for link, expected in (
|
||||||
|
("[P-3](MAP.md#p-1-動作の範囲)", "課題番号と参照先が一致しない"),
|
||||||
|
("[MAPのP-3](MAP.md#p-1-動作の範囲)", "課題番号と参照先が一致しない"),
|
||||||
|
("[P-2](MAP.md#p-2-本文の見出し)", "個別項目を指していない"),
|
||||||
|
("[P-3](MAP.md#未確認事項)", "個別項目を指していない"),
|
||||||
|
):
|
||||||
|
with self.subTest(link=link):
|
||||||
|
path = self.write("PHY.md", link + "\n")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1, output)
|
||||||
|
self.assertIn(expected, output)
|
||||||
|
|
||||||
|
def test_duplicate_heading_slug_resolves_actual_target(self):
|
||||||
|
self.write("MAP.md", "# MAP\n\n### P-1 動作の範囲\n\n" + issue_section())
|
||||||
|
path = self.write("PHY.md", "[P-1](MAP.md#p-1-動作の範囲-1)\n")
|
||||||
|
self.assertEqual(self.run_lint(path)[0], 0)
|
||||||
|
path.write_text("[P-1](MAP.md#p-1-動作の範囲)\n", encoding="utf-8")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1, output)
|
||||||
|
self.assertIn("個別項目を指していない", output)
|
||||||
|
|
||||||
|
def test_unresolved_prose_warns_without_failing_or_modifying_documents(self):
|
||||||
|
text = "# PHY\n\n方式は未定。P-3で検討する。\n\n" + issue_section()
|
||||||
|
path = self.write("PHY.md", text)
|
||||||
|
before = path.read_bytes()
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 0, output)
|
||||||
|
self.assertIn("リンクのない課題番号", output)
|
||||||
|
self.assertIn("未決・未定に課題への参照がない", output)
|
||||||
|
self.assertEqual(path.read_bytes(), before)
|
||||||
|
|
||||||
|
def test_another_issue_link_does_not_hide_bare_id(self):
|
||||||
|
path = self.write(
|
||||||
|
"PHY.md", "P-3と[P-1](#p-1-動作の範囲)を参照。\n\n" + issue_section()
|
||||||
|
)
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 0, output)
|
||||||
|
self.assertIn("リンクのない課題番号がある(要確認): P-3", output)
|
||||||
|
|
||||||
|
def test_unrelated_links_and_section_links_do_not_hide_unresolved_prose(self):
|
||||||
|
for link in ("[仕様](PHY.md)", "[一覧](#未確認事項)", "[外部](https://example.com/)"):
|
||||||
|
with self.subTest(link=link):
|
||||||
|
path = self.write("PHY.md", f"範囲は未決。{link}\n\n" + issue_section())
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 0, output)
|
||||||
|
self.assertIn("未決・未定に課題への参照がない", output)
|
||||||
|
|
||||||
|
def test_broken_link_does_not_count_as_resolved_reference(self):
|
||||||
|
path = self.write("PHY.md", "範囲は未決。\n[P-1](#p-1-missing)\n")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1, output)
|
||||||
|
self.assertIn("参照先見出しがない", output)
|
||||||
|
self.assertIn("未決・未定に課題への参照がない", output)
|
||||||
|
|
||||||
|
def test_paragraph_and_list_boundaries_do_not_borrow_links(self):
|
||||||
|
for body in (
|
||||||
|
"範囲は未決。\n\n[P-1](#p-1-動作の範囲)\n",
|
||||||
|
"- 範囲は未決。\n- [P-1](#p-1-動作の範囲)\n",
|
||||||
|
"1. 範囲は未決。\n2. [P-1](#p-1-動作の範囲)\n",
|
||||||
|
"範囲は未決。\n## 次の話\n[P-1](#p-1-動作の範囲)\n",
|
||||||
|
):
|
||||||
|
with self.subTest(body=body):
|
||||||
|
path = self.write("PHY.md", body + "\n" + issue_section())
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 0, output)
|
||||||
|
self.assertIn("未決・未定に課題への参照がない", output)
|
||||||
|
path = self.write(
|
||||||
|
"PHY.md", "- 範囲は未決。\n [P-1](#p-1-動作の範囲)\n\n" + issue_section()
|
||||||
|
)
|
||||||
|
self.assertIn("ERROR 0 / WARN 0", self.run_lint(path)[1])
|
||||||
|
|
||||||
|
def test_code_examples_external_links_and_assumptions_are_not_prose_warnings(self):
|
||||||
|
path = self.write(
|
||||||
|
"PHY.md",
|
||||||
|
"\x60未決 P-8 [P-8](#missing)\x60\n\n"
|
||||||
|
"\x60\x60\x60markdown\n未定 P-8 [P-8](#missing)\n\x60\x60\x60\n\n"
|
||||||
|
"~~~markdown\n未定 P-8 [P-8](#missing)\n~~~\n\n"
|
||||||
|
"[P-99](https://example.com/#p-99)\n\n"
|
||||||
|
"範囲は未定 [推測:P-1]。\n\n"
|
||||||
|
"## 未確認事項\n\n### P-1 調査\n\n- 格上げ条件: 検証する\n",
|
||||||
|
)
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 0, output)
|
||||||
|
self.assertIn("ERROR 0 / WARN 0", output)
|
||||||
|
|
||||||
|
def test_management_sections_and_non_spec_files_are_exempt_from_prose_warnings(self):
|
||||||
|
self.write("CLAUDE.md", "未決 P-8 [P-8](#missing)\n")
|
||||||
|
self.write("journal/2026-09-22-decision.md", "## 結論\n\n未定 P-8\n")
|
||||||
|
self.write("backlog/README.md", "## PHY\n\n" + backlog_item() + "\n未定 P-8\n")
|
||||||
|
self.write("PHY.md", "# PHY\n\n" + issue_section() + "\n未定 P-8\n")
|
||||||
|
status, output = self.run_lint()
|
||||||
|
self.assertEqual(status, 0, output)
|
||||||
|
self.assertIn("WARN 0", output)
|
||||||
|
|
||||||
|
def test_links_inside_unconfirmed_items_are_still_checked(self):
|
||||||
|
path = self.write("PHY.md", issue_section() + "\n[P-8](#missing)\n")
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 1, output)
|
||||||
|
self.assertIn("参照先見出しがない", output)
|
||||||
|
|
||||||
|
def test_rejected_count_warning_does_not_select_entries_for_deletion(self):
|
||||||
|
path = self.write(
|
||||||
|
"PHY.md", "## 検討済み・非採用\n\n"
|
||||||
|
+ "".join(f"- 案{i} (journal/2026-09-22-decision.md)\n" for i in range(6))
|
||||||
|
)
|
||||||
|
before = path.read_bytes()
|
||||||
|
status, output = self.run_lint(path)
|
||||||
|
self.assertEqual(status, 0, output)
|
||||||
|
self.assertIn("6 件(目安5件)", output)
|
||||||
|
self.assertIn("再提案を防ぐ必要性", output)
|
||||||
|
self.assertNotIn("古いものから落とす", output)
|
||||||
|
self.assertEqual(path.read_bytes(), before)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Beyond KLE
|
# Beyond KLE
|
||||||
|
|
||||||
キーボード設計のデータモデルとワークフローを再設計するプロジェクト。
|
キーボード設計のデータモデルとワークフローを再設計するプロジェクト。
|
||||||
PHY(物理配置) / ELC(電気配線) / IDX(キー識別) / MAP(キーマップ) / IME(入力変換) の5層に分離する。
|
PHY(物理配置) / ELC(電気配線) / IDX(キー識別) / MAP(キーマップ) / HST(ホスト) の5層に分離する。
|
||||||
|
|
||||||
- 応答・ドキュメントは日本語。
|
- 応答・ドキュメントは日本語。
|
||||||
- 現状はコード未着手、設計ドキュメントのみ。
|
- 現状はコード未着手、設計ドキュメントのみ。
|
||||||
@@ -12,19 +12,27 @@ PHY(物理配置) / ELC(電気配線) / IDX(キー識別) / MAP(キーマップ)
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `keyboard-design-layers.md` | レイヤー構造とツール分割方針(最上位) |
|
| `keyboard-design-layers.md` | レイヤー構造とツール分割方針(最上位) |
|
||||||
| `PHYレイヤー.md` | PHY層の仕様 |
|
| `PHYレイヤー.md` | PHY層の仕様 |
|
||||||
|
| `ELCレイヤー.md` | ELC層の仕様 |
|
||||||
|
| `IDXレイヤー.md` | IDX層の仕様 |
|
||||||
|
| `MAPレイヤー.md` | MAP層とエディタの責務・実機との読み書き方針 |
|
||||||
|
| `HSTレイヤー.md` | HST層(ホストによるキーコードの解釈)の仕様 |
|
||||||
| `phase1-json-schema.md` | 各層のJSON Schema |
|
| `phase1-json-schema.md` | 各層のJSON Schema |
|
||||||
| `keyboard-making-workflow.md` | 既存の自作キーボード制作フローとの対応 |
|
| `keyboard-making-workflow.md` | 既存の自作キーボード制作フローとの対応 |
|
||||||
| `implementation-plan.md` | 実装方針の決定事項・検証用サンプル |
|
| `implementation-plan.md` | 実装方針の決定事項・検証用サンプル |
|
||||||
|
| [`backlog/README.md`](backlog/README.md) | 後回しにできる問い・検証・改善候補 |
|
||||||
|
| `samples/` | 検証用サンプル(KLE JSON、VIA定義)。出典とテストケースは `implementation-plan.md` |
|
||||||
|
|
||||||
**既存ドキュメントは下記ルールの策定前に書かれたもので、ルール1〜4は未適用。**
|
既存ドキュメントには下記ルールを段階的に適用する。PHY・ELC・IDXの保留課題はbacklogへ分離済み。
|
||||||
全体の再編は別タスクとして行う。それまで各ファイルをメイン/journalのどちらに分類するかは未確定なので、
|
全体の再編は別タスクとして行い、指示なく他の内容を移動・分割・書き直ししない。
|
||||||
指示なく移動・分割・書き直しをしない。
|
|
||||||
|
|
||||||
初期コミット以前の経緯は追跡できない。現在のドキュメント群を「初期検討段階」のベースラインとして扱い、
|
初期コミット以前の経緯は追跡できない。現在のドキュメント群を「初期検討段階」のベースラインとして扱い、
|
||||||
それ以前の決定理由を掘り起こそうとしない。
|
それ以前の決定理由を掘り起こそうとしない。
|
||||||
|
|
||||||
## ドキュメント運用ルール
|
## ドキュメント運用ルール
|
||||||
|
|
||||||
|
現在の仕様・将来取り組む課題・過去の検討を分け、仕様を読む際に判断状況が混ざらないようにする。
|
||||||
|
未確認の前提は確定事項と区別し、保留課題は再開するまで見つけられる状態に保つ。
|
||||||
|
|
||||||
### 1. メインドキュメント(リポジトリ直下の `*.md`)
|
### 1. メインドキュメント(リポジトリ直下の `*.md`)
|
||||||
|
|
||||||
書くもの: **採用した案 / 現在の設定 / 設計に影響する制約**。
|
書くもの: **採用した案 / 現在の設定 / 設計に影響する制約**。
|
||||||
@@ -34,10 +42,11 @@ PHY(物理配置) / ELC(電気配線) / IDX(キー識別) / MAP(キーマップ)
|
|||||||
- 却下案の再提案を防ぐ情報は、比較ではなく**制約文**として書く。
|
- 却下案の再提案を防ぐ情報は、比較ではなく**制約文**として書く。
|
||||||
- ✗ `B案はKLEパーサーが壊れるので却下した`
|
- ✗ `B案はKLEパーサーが壊れるので却下した`
|
||||||
- ○ `クラスター情報はKLE JSONに出力しない(KLE互換性の制約)`
|
- ○ `クラスター情報はKLE JSONに出力しない(KLE互換性の制約)`
|
||||||
- 末尾に `## 検討済み・非採用` を置き、落とした案を**1案1行**で残す。理由は書かず、journalファイル名を参照する。
|
- 落とした案があれば、末尾に `## 検討済み・非採用` を置き、**1案1行**で残す。理由は書かず、journalファイル名を参照する。
|
||||||
- 例: `- labels[9] へのID埋め込み (journal/2026-09-16-cluster-metadata.md)`
|
- 例: `- labels[9] へのID埋め込み (journal/2026-09-16-cluster-metadata.md)`
|
||||||
- コミットは `git log --oneline -- journal/2026-09-16-cluster-metadata.md` で引ける(journal削除後も追跡可)
|
- コミットは `git log --oneline -- journal/2026-09-16-cluster-metadata.md` で引ける(journal削除後も追跡可)
|
||||||
- 目安5行まで。増えたら古いものから落とす。ここが長くなるのは書きすぎのサイン。
|
- 載せるのは、実際に検討した別方針のうち再提案されうるものに限る。本文の制約文で足りるなら重複させない。
|
||||||
|
- 目安5行まで。増えたら、再提案を防ぐ必要性が低いものから落とす。長くなるのは書きすぎのサイン。
|
||||||
|
|
||||||
### 2. 履歴と根拠
|
### 2. 履歴と根拠
|
||||||
|
|
||||||
@@ -60,10 +69,15 @@ PHY(物理配置) / ELC(電気配線) / IDX(キー識別) / MAP(キーマップ)
|
|||||||
|
|
||||||
- **裏取りの済んだ事実**は、ドキュメント内のどこに書いてもよい。文中に自然に混ぜてよい。
|
- **裏取りの済んだ事実**は、ドキュメント内のどこに書いてもよい。文中に自然に混ぜてよい。
|
||||||
ただし同じ事実を複数ドキュメントに複製しない。必要ならリンクする。
|
ただし同じ事実を複数ドキュメントに複製しない。必要ならリンクする。
|
||||||
- **未確認の推測**だけを末尾に隔離する。2種類ある:
|
- **現在の仕様・実装が答えに依存する未確認事項**は、仕様書の末尾に残す。2種類ある:
|
||||||
- 本文が前提にしている推測 → 本文に `[推測:P-n]` を付け、末尾の `## 未確認事項` に対応項目を置く
|
- 本文が前提にしている推測 → 本文に `[推測:P-n]` を付け、末尾の `## 未確認事項` に対応項目を置く
|
||||||
- まだ本文に書けない未解決の問い → 本文タグなしで `## 未確認事項` に直接置く
|
- 実装前に判断が必要な未解決の問い → 本文タグなしで `## 未確認事項` に直接置く
|
||||||
- `## 未確認事項` の各項目には、**何を確認できれば事実に格上げできるか**を必ず書く。
|
- 本文が未決の問いに触れるときは、「未決」と書いて終わらせず該当項目へリンクする。
|
||||||
|
同一ファイル内は `[P-3](#p-3-...)`、他ファイルからは `[MAPのP-3](MAPレイヤー.md#p-3-...)` の形とする。
|
||||||
|
問いの内容は `## 未確認事項` にだけ書き、本文へ写さない(二重管理を避ける)。
|
||||||
|
- 未確認の推測には、**何を確認できれば事実に格上げできるか**を「格上げ条件」として書く。
|
||||||
|
設計判断には、**何を決めれば解決するか**を「完了条件」として書く。意思決定を事実確認と混同しない。
|
||||||
|
- 答えに依存せず進められる課題は `backlog/` に置く(ルール5)。保留中の対応範囲・暫定仕様は仕様書に残す。
|
||||||
- 検証できたらタグを外し、該当項目を削除する。
|
- 検証できたらタグを外し、該当項目を削除する。
|
||||||
- 「今回検討して捨てた仮説」は `## 未確認事項` に書かない。journal に残して消す。
|
- 「今回検討して捨てた仮説」は `## 未確認事項` に書かない。journal に残して消す。
|
||||||
- 推測に基づいて実装しない。先に検証するか、ユーザーに確認する。
|
- 推測に基づいて実装しない。先に検証するか、ユーザーに確認する。
|
||||||
@@ -78,9 +92,39 @@ PHY(物理配置) / ELC(電気配線) / IDX(キー識別) / MAP(キーマップ)
|
|||||||
- 格上げ条件: KLE本家でインポート→エクスポートし、labels[9]が残ることを確認する
|
- 格上げ条件: KLE本家でインポート→エクスポートし、labels[9]が残ることを確認する
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 5. 保留課題(`backlog/`)
|
||||||
|
|
||||||
|
書くもの: **今は解決しなくても進められる問い・検証・改善候補**。
|
||||||
|
|
||||||
|
- 当面は `backlog/README.md` にまとめる。増えて読みにくくなったらレイヤー別・トピック別に分ける。
|
||||||
|
- 項目には、関連仕様・未決事項・保留できる理由・再開条件・完了条件を書く。
|
||||||
|
再開条件は「いつ考え直すか」、完了条件は「何を確認または決定すれば終わるか」を示す。
|
||||||
|
- 仕様書から移す項目は、元の番号にレイヤー名などを付けて識別する(例: `PHY-P-14`)。
|
||||||
|
判断に必要な候補や制約は短く残す。長い比較・調査ログは `journal/` に置く。
|
||||||
|
- 問いの本文は二重管理しない。仕様書には保留中の対応範囲・暫定仕様を残し、必要に応じてbacklogへリンクする。
|
||||||
|
- 仕様書から保留課題を分離した場合、末尾の管理セクションを `## 保留課題` → `## 検討済み・非採用` →
|
||||||
|
`## 未確認事項` の順に置く(存在するものだけ)。`## 保留課題` には下記のリンク箇条書きだけを書く。
|
||||||
|
項目名の列挙・問いの要約・保留理由・対応範囲は書かない。対応範囲は仕様本文、問いの詳細はbacklogに置く。
|
||||||
|
- リンクは `- [<区分>の保留課題](backlog/README.md#<区分の見出し>)` の形とする。
|
||||||
|
分割後は該当するbacklogファイルへ向ける。複数区分を参照する場合は1リンク1行とし、参照先の見出しまで指定する。
|
||||||
|
該当課題がなくなったらリンクを削除し、空になった `## 保留課題` はセクションごと削除する。
|
||||||
|
- 現在の仕様が依存する未確認の前提は、backlogへ移して隠さない。仕様書の `## 未確認事項` に残す。
|
||||||
|
- backlogへの移動は仮説の採用や実装の許可を意味しない。実装は採用済みの仕様・対応範囲に従う。
|
||||||
|
- 解決したら必要な結論を仕様書へ反映し、項目と不要になった参照を削除する。取り下げた項目も削除し、完了一覧は蓄積しない。
|
||||||
|
調査・判断の経緯はルール2・3に従って履歴に残す。
|
||||||
|
- 未解決の課題は作業ツリーに残す。結論反映後に削除できるjournalとは寿命が異なる。
|
||||||
|
|
||||||
|
仕様書側の書式:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 保留課題
|
||||||
|
|
||||||
|
- [PHYの保留課題](backlog/README.md#phy)
|
||||||
|
```
|
||||||
|
|
||||||
## Claude への作業指示
|
## Claude への作業指示
|
||||||
|
|
||||||
- ドキュメントを更新するときは、上のルール1〜4のどこに属する情報かを判断してから書く。分類に迷うものは書く前に確認する。
|
- ドキュメントを更新するときは、上のルール1〜5のどこに属する情報かを判断してから書く。分類に迷うものは書く前に確認する。
|
||||||
- 調査タスクの結果は、結論だけをメインドキュメントに書き、調査過程は `journal/` に落とす。
|
- 調査タスクの結果は、結論だけをメインドキュメントに書き、調査過程は `journal/` に落とす。
|
||||||
- 既存ドキュメントの記述と食い違う事実を見つけたら、勝手に上書きせず指摘する。
|
- 既存ドキュメントの記述と食い違う事実を見つけたら、勝手に上書きせず指摘する。
|
||||||
|
|
||||||
|
|||||||
+32
-20
@@ -25,6 +25,9 @@ ELCが扱う「ピン」は物理的なマイコンピンに限らない。**ピ
|
|||||||
現時点で具体形を定めるのは、マイコンとアナログマルチプレクサの2種別。
|
現時点で具体形を定めるのは、マイコンとアナログマルチプレクサの2種別。
|
||||||
デジタル側でピンを増やす部品も提供者として扱うが、種別ごとの記述は必要になった時点で定める。
|
デジタル側でピンを増やす部品も提供者として扱うが、種別ごとの記述は必要になった時点で定める。
|
||||||
|
|
||||||
|
同じ `PinRef` が複数の提供者の `select` に現れることは一律には禁止しない。
|
||||||
|
複数mux構成の走査出力は、実配線と制御方法を確認した構成から対応する。
|
||||||
|
|
||||||
### キーの端子
|
### キーの端子
|
||||||
|
|
||||||
キーは検出方式(detect)を持ち、検出方式ごとに端子の構成が決まる。各端子は接続先の論理ピンを持つ。
|
キーは検出方式(detect)を持ち、検出方式ごとに端子の構成が決まる。各端子は接続先の論理ピンを持つ。
|
||||||
@@ -32,7 +35,7 @@ ELCが扱う「ピン」は物理的なマイコンピンに限らない。**ピ
|
|||||||
- 接点(contact): a, b の2端子。端子は無極性で、a と b に順序の意味はない
|
- 接点(contact): a, b の2端子。端子は無極性で、a と b に順序の意味はない
|
||||||
- アナログ(analog): out 1端子。加えて電源の vcc, gnd。ホールセンサがこれにあたる
|
- アナログ(analog): out 1端子。加えて電源の vcc, gnd。ホールセンサがこれにあたる
|
||||||
|
|
||||||
光学スイッチの端子構成は未定 [推測:P-1]。
|
光学スイッチは当面未対応とする。端子構成を確認するまで、既存の検出方式に仮置きしない。
|
||||||
|
|
||||||
### 配線トポロジを区分として持たない
|
### 配線トポロジを区分として持たない
|
||||||
|
|
||||||
@@ -46,6 +49,12 @@ ELCが扱う「ピン」は物理的なマイコンピンに限らない。**ピ
|
|||||||
row / col はELCの正本に含めない。charlieplexing で便宜的に行と列の形で書くことはあるが、
|
row / col はELCの正本に含めない。charlieplexing で便宜的に行と列の形で書くことはあるが、
|
||||||
それは表記の都合であって配線の事実ではない。QMKなどが要求する row / col は、出力時に割り当てる派生情報とする。
|
それは表記の都合であって配線の事実ではない。QMKなどが要求する row / col は、出力時に割り当てる派生情報とする。
|
||||||
|
|
||||||
|
走査方向の決定と追加指定はFirmware Generatorが受け持ち、ELCには保存しない。
|
||||||
|
接点マトリクスでは、どちら側のピン群を駆動し、どちら側から読み取るかを指す。
|
||||||
|
配線・ダイオードの向きと整合する方向を出力器が選び、追加指定が必要な場合は出力設定で受け取る。
|
||||||
|
配線だけから全方式の走査手順が一意に決まることは前提にしない。
|
||||||
|
方式ごとの必要な設定は、その方式の出力に対応する際に確認し、未対応の構成は出力しない。
|
||||||
|
|
||||||
### ダイオード
|
### ダイオード
|
||||||
|
|
||||||
接点キーはダイオードの向きを持つ。向きは端子名で表す(`a-to-b` はアノードがa側)。省略時はダイオードなし。
|
接点キーはダイオードの向きを持つ。向きは端子名で表す(`a-to-b` はアノードがa側)。省略時はダイオードなし。
|
||||||
@@ -61,6 +70,23 @@ per-key LEDを載せたキーか、LEDの部品ノードを指す。並びの位
|
|||||||
|
|
||||||
現時点で定めるのはチェーン方式(WS2812系)のみとする。ドライバICでアドレスを与える方式は未定。
|
現時点で定めるのはチェーン方式(WS2812系)のみとする。ドライバICでアドレスを与える方式は未定。
|
||||||
|
|
||||||
|
### レイアウトバリアント
|
||||||
|
|
||||||
|
PHYがレイアウトバリアントを持つ場合も、ELCは選択に関係なく全バリアントのキーを配線する。
|
||||||
|
基板上のフットプリントは選ばれなかった側も実在するため。
|
||||||
|
|
||||||
|
互いに排他なキーは、同じ接続先の組を共有してよい。排他でないキー同士が同じ接続先の組を持つのは
|
||||||
|
設計エラーとする。接点マトリクスなら、どちらが押されたかを区別できないため。
|
||||||
|
排他かどうかはPHYの木から判る。
|
||||||
|
|
||||||
|
ファームウェア向けのキー番号と接続先の対応表は、バリアント選択後のIDXの並びから生成する。
|
||||||
|
同じ接続先を共有する排他キーも、選択後に同時には残らない。ELCの保存データは選択によって書き換えない。
|
||||||
|
|
||||||
|
LEDチェーンもバリアントごとに分けない。排他なキー位置の両方にLEDが載る基板も、
|
||||||
|
バリアント固有のキーにLEDが載らない基板もあるが、どちらもチェーンは1本で、選択によって並びが変わらない。
|
||||||
|
載らないキーは、PHYがキーごとに持つper-key LEDの有無でそのまま表せる。
|
||||||
|
キー配列の出力で行うバリアントの取捨・番号の詰め直しは、LEDチェーンには適用しない。
|
||||||
|
|
||||||
### 他レイヤーへの参照
|
### 他レイヤーへの参照
|
||||||
|
|
||||||
キーとLEDの参照には、PHYのノードIDをそのまま使う。ELCはIDXを参照しない。
|
キーとLEDの参照には、PHYのノードIDをそのまま使う。ELCはIDXを参照しない。
|
||||||
@@ -117,6 +143,10 @@ type ELC = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 保留課題
|
||||||
|
|
||||||
|
- [ELCの保留課題](backlog/README.md#elc)
|
||||||
|
|
||||||
## 検討済み・非採用
|
## 検討済み・非採用
|
||||||
|
|
||||||
- `matrix.type` による配線方式の列挙 (journal/2026-09-20-elc-pin-provider.md)
|
- `matrix.type` による配線方式の列挙 (journal/2026-09-20-elc-pin-provider.md)
|
||||||
@@ -125,22 +155,4 @@ type ELC = {
|
|||||||
|
|
||||||
## 未確認事項
|
## 未確認事項
|
||||||
|
|
||||||
### P-1 光学スイッチの端子構成
|
(なし)
|
||||||
|
|
||||||
- 位置づけ: 本文「キーの端子」が扱えていない残り
|
|
||||||
- 現状の認識: 発光側と受光側が独立した端子を持つのか、接点と同じ2端子として扱えるのかが分からない
|
|
||||||
- 格上げ条件: 光学スイッチを使った実在のPCBの回路図を読み、端子数と配線の形を確認する
|
|
||||||
|
|
||||||
### P-2 複数マルチプレクサ構成の作り
|
|
||||||
|
|
||||||
- 位置づけ: 本文「ピンとピン提供者」が前提にしている。`select` を提供者ごとに持たせてよいかに関わる
|
|
||||||
- 現状の認識: 選択ピンを複数のmuxで共有して共通ピンだけ分ける形と、イネーブルピンで切り替える形が考えられるが、
|
|
||||||
どちらが一般的か確認していない。前者なら同じ `PinRef` が複数の提供者の `select` に現れる
|
|
||||||
- 格上げ条件: 複数muxを使った実在のキーボードの配線を確認し、共有の形を確かめる
|
|
||||||
|
|
||||||
### P-3 駆動側と検出側の役割をELCが明示的に持つ必要があるか
|
|
||||||
|
|
||||||
- 位置づけ: 本文「配線トポロジを区分として持たない」の残り
|
|
||||||
- 現状の認識: 接点マトリクスではダイオードの向きから、directでは電源レールの参照から、
|
|
||||||
どちらを駆動しどちらを検出するかが決まりそうに見えるが、全方式で導出できるか確かめていない
|
|
||||||
- 格上げ条件: 各方式について、端子の接続先とダイオードの向きだけから走査手順が一意に決まるかを確認する
|
|
||||||
|
|||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
# HSTレイヤー
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
HST(ホスト)レイヤーは、接続先のホストがキーボードから受け取ったキーコードをどう解釈するかを記述する。
|
||||||
|
MAPが出力するのはHIDのキーコードであり、画面上の文字や動作は、ホストのOSとキーボード配列の設定で決まる。
|
||||||
|
HSTはこの対応を持ち、MAPエディタとFirmware Generatorが参照する。入力がHSTを通過するわけではない。
|
||||||
|
|
||||||
|
名前は、HIDでキーボード(device)の接続相手をhostと呼ぶことに合わせる。
|
||||||
|
IMEのかな漢字変換はOSの仕事であり、HSTの対象外とする。
|
||||||
|
|
||||||
|
## プロファイル
|
||||||
|
|
||||||
|
HSTのデータは、ホスト環境ごとのプロファイルの集まりとする。
|
||||||
|
プロファイルはキーボードに依存しないため、プロジェクトの外にある共有カタログとして版を管理する。
|
||||||
|
プロジェクトは、参照するプロファイルのIDと、利用者ごとの差分(XKBのオプションなど)だけを持つ。
|
||||||
|
|
||||||
|
### 識別
|
||||||
|
|
||||||
|
プロファイルは、OSとホスト配列の組で識別する。必要な場合だけ、入力方式やその設定を加える。
|
||||||
|
|
||||||
|
- ホスト配列は、OSがキーコードを解釈するための配列の設定である。キーキャップの刻印やOSの表示言語とは独立に決まる
|
||||||
|
(日本語のWindowsで英語配列を設定する、など)。
|
||||||
|
- Linux(XKB)では、配列(layout)・変種(variant)・オプション(options)の組になる。
|
||||||
|
- `win-jp` のような短い名前は、人が選ぶためのプリセット名として付ける。
|
||||||
|
- 各OSの識別子(WindowsのKLID、XKBの名前、macOSの入力ソースID)と対応付けられるようにする。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "os": "windows", "layout": "jp106" }
|
||||||
|
{ "os": "macos", "layout": "us" }
|
||||||
|
{ "os": "linux", "layout": "jp", "options": ["ctrl:nocaps"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
「日本語入力を使う環境」のような設定は、プロファイルの識別に含める。
|
||||||
|
IMEが今ONか、のような実行時の状態は識別に含めない(次節の引数として扱う)。
|
||||||
|
|
||||||
|
### 記述する内容
|
||||||
|
|
||||||
|
プロファイルは、順方向の対応だけを記述する。
|
||||||
|
|
||||||
|
```txt
|
||||||
|
(キーコード, 修飾キー, ホスト状態) → 文字 / 動作 / 未定義 / 不明
|
||||||
|
```
|
||||||
|
|
||||||
|
- ホスト状態: CapsLock、かなロック、IMEのON/OFFなど、同じキーコードの結果を変える状態。
|
||||||
|
HSTは状態の種類を引数として記述するだけで、実行時の状態を観測しない。
|
||||||
|
- OS固有のキーの意味: `LANG1`/`LANG2`(Macのかな/英数)、変換/無変換、Cmd/Ctrlの役割など。
|
||||||
|
- 成立の前提: 動作に必要なOS側の設定(Unicode入力の方式など)。HSTは設定を変更する機能を持たない。
|
||||||
|
- 対応可否: 対応済み・未対応・不明の3値とする。カタログに載っていない動作を「使えない」と扱わない。
|
||||||
|
|
||||||
|
## 使う側の責務
|
||||||
|
|
||||||
|
HSTは逆引き(文字から、それを入力するキー操作を求めること)を持たない。
|
||||||
|
逆引きは、使う側が順方向の表から求める[推測:P-1]。
|
||||||
|
|
||||||
|
- 順方向の表から導けるので、両方を持つと二重管理になる。
|
||||||
|
- 逆引きの候補には、ファームウェアの機能に依存するものがある。
|
||||||
|
例えば、US刻印のキーキャップをJIS配列のホストで使う場合、Shiftの有無で文字の組が合わないキーには、
|
||||||
|
Shift時に別のキーコードを送る機能(QMKのKey Override)が必要になる。
|
||||||
|
これはホストの知識ではないため、HSTに持たせない。
|
||||||
|
|
||||||
|
使う側の担当は次のとおり。
|
||||||
|
|
||||||
|
- MAPエディタ: キーコードの表示、逆引きによる入力補助、対象ホストで意味を持たないキーコードの警告
|
||||||
|
- Firmware Generator: `send_string` が前提にする配列、Unicode入力の方式など、ホストに依存する生成設定
|
||||||
|
|
||||||
|
## MAPとの境界
|
||||||
|
|
||||||
|
- MAPは具体的なキーコードを保存する。逆引きは編集時の入力補助であり、選んだ候補のキーコードをMAPへ書く一方向の変換とする。
|
||||||
|
実機に残るのはキーコードだけなので、この形なら実機との往復を単純に保てる。
|
||||||
|
- HSTは割り当てを所有・変更しない。PHYのキーIDも参照しない。
|
||||||
|
|
||||||
|
## 対象外
|
||||||
|
|
||||||
|
- 実行時の状態の観測・同期(今どのOSに接続しているか、IMEがONか)
|
||||||
|
- OS設定の変更、IMEの変換処理
|
||||||
|
- ホスト側での再割り当て(AutoHotkey、Karabiner、IMEのローマ字テーブルなど)
|
||||||
|
- アプリごとのショートカット(「コピー」のような操作の汎用的な辞書)
|
||||||
|
|
||||||
|
## 検討済み・非採用
|
||||||
|
|
||||||
|
- 名前をSYSとする (journal/2026-09-24-hst-layer-scope.md)
|
||||||
|
- 逆引きの候補をプロファイルが持つ (journal/2026-09-24-hst-layer-scope.md)
|
||||||
|
|
||||||
|
## 未確認事項
|
||||||
|
|
||||||
|
### P-1 使う側が逆引きの候補と実現可否を判断できる
|
||||||
|
|
||||||
|
- 位置づけ: 本文「使う側の責務」が前提にしている。HSTにファームウェアの知識を持たせない線引きは、これが成り立つことに依存する。
|
||||||
|
- 格上げ条件: US刻印のキーキャップをJIS配列のホストで使う構成で、Shiftの有無で文字の組が合わないキーについて、
|
||||||
|
HSTの順方向の表と、Generatorのビルド設定(有効な機能)だけから、MAPエディタがKey Overrideを含む候補を作れることを試作で確認する。
|
||||||
|
|
||||||
|
### P-2 対象ホストを複数持つか
|
||||||
|
|
||||||
|
- 位置づけ: 未解決の問い。Win/Mac両用のキーボードでは、表示と検証を複数のホストに対して行う必要がある。
|
||||||
|
OSの切り替えをMAPのレイヤーで表す既存の慣習と、どう対応付けるかも関わる。
|
||||||
|
- 完了条件: プロジェクトが参照するプロファイルの数と、MAPのレイヤーとの対応付けの有無をユーザーが決める。
|
||||||
+138
@@ -0,0 +1,138 @@
|
|||||||
|
# IDXレイヤー
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
IDXレイヤーは、全バリアントにわたるPHYのキーの並び順を持つ。
|
||||||
|
番号を使う形式への出力時に、選択したバリアントで使うキーだけを残し、0から連続するキー番号を与える。
|
||||||
|
保存した並びの位置と、選択後の出力でのキー番号は区別する。
|
||||||
|
|
||||||
|
並び順は他のどの情報からも導出できない。設計者が決める情報なので、IDXが正本として持つ。
|
||||||
|
ELCのrow/colが「配線から決まるので出力時に導出する」のと、理由が裏返しの関係にある。
|
||||||
|
|
||||||
|
IDXが持つのは、番号を振る対象のキーと、その並び順だけとする。バリアントの選択は保存しない。
|
||||||
|
|
||||||
|
## キー番号は公開インターフェース
|
||||||
|
|
||||||
|
基板を公開すると、他人が書いたキーマップが、出力したレイアウトのキー番号に依存する。
|
||||||
|
同じレイアウトで番号を振り直すと、既存のキーマップが別のキーを指す。
|
||||||
|
|
||||||
|
そのため並び順は正本として保存し、出力のたびに幾何や配線から生成し直さない。
|
||||||
|
同じ並びと同じバリアント選択からは、同じ出力番号を得る。
|
||||||
|
番号の互換性は出力レイアウトごとに扱い、異なるバリアントの同じ番号が同じキーを指すとは限らない。
|
||||||
|
並びへの挿入・削除・並べ替えでは、影響する出力レイアウトの番号が変わることを考慮する。
|
||||||
|
|
||||||
|
## 幾何から導出しない
|
||||||
|
|
||||||
|
PHYの座標・ツリー順をキー番号に使わない。
|
||||||
|
|
||||||
|
クラスターは幾何の編集単位であって論理的な行ではない。親指キーをまとめて回転させるために
|
||||||
|
クラスターへ出し入れすると、ツリー順が変わる。ツリー順が番号なら、そこでキーマップがずれる。
|
||||||
|
|
||||||
|
座標から行を判定する方法は、カラムスタッガードや曲線配列で破綻する。これはKLEの問題点として
|
||||||
|
`keyboard-design-layers.md` に挙げたものと同じ性質の話である。
|
||||||
|
|
||||||
|
## 配線から導出しない
|
||||||
|
|
||||||
|
ELCのマトリクス位置をキー番号に使わない。
|
||||||
|
|
||||||
|
- マトリクスの並びとキー番号の並びは一致しない。分割キーボードでは、マトリクスは片方の半身の行を
|
||||||
|
並べてからもう片方の行を並べるが、キー番号は視覚的な行ごとに左右を交互に取るのが通例である
|
||||||
|
- マトリクスには欠番がある。キー番号は実在するキーに対して連続だが、マトリクスは疎である
|
||||||
|
- 基板リビジョンでピンを差し替えたときに、キー番号が動いてはならない
|
||||||
|
|
||||||
|
row / col 自体がELCの正本ではなく出力時の派生情報である。派生情報から番号を導くと二重に不安定になる。
|
||||||
|
|
||||||
|
## 出力先が要求する形
|
||||||
|
|
||||||
|
3つの処理系はいずれも「マトリクス位置の順序付きリスト」を要求し、その並びの位置をキー番号とする。
|
||||||
|
どれも人手で書かれており、他の情報から導出されていない。Firmware Generatorはこれらを出力する。
|
||||||
|
|
||||||
|
| 処理系 | キー番号を決めているもの |
|
||||||
|
| --- | --- |
|
||||||
|
| QMK | `LAYOUT` マクロの引数順 / `keyboard.json` の `layouts.<NAME>.layout[]` の配列順 |
|
||||||
|
| ZMK | matrix transform の `map` の並び順 |
|
||||||
|
| KMK | `coord_mapping` の並び順 |
|
||||||
|
|
||||||
|
## レイアウトバリアント
|
||||||
|
|
||||||
|
IDXは、番号を振る対象のキーを全バリアントにわたって1つの `order` に持つ。
|
||||||
|
各PHYキーIDを別々の要素として並べ、同じIDは重複させない。同じ接続先に配線された排他キーも
|
||||||
|
別々の要素にする。物理キーごとに、出力先が要求する並びの中の位置を与えられるようにするため。
|
||||||
|
|
||||||
|
出力時は、PHYのVariantGroupごとにバリアントを選び、次の手順で出力用の並びを得る。
|
||||||
|
|
||||||
|
1. 選択したバリアントの配下と、バリアントに属さない共通のキーを対象にする。
|
||||||
|
入れ子の場合は、祖先のバリアントも選択されているキーだけを対象にする
|
||||||
|
2. `order` から対象外のキーを除く。残ったキー同士の順序は変えない
|
||||||
|
3. 残った並びの位置を、0から連続する出力用のキー番号とする
|
||||||
|
|
||||||
|
保存した `order` は書き換えない。レイアウトごとに独立したIDXや並び順も保存しない。
|
||||||
|
同じ接続先の排他キーは選択後に同時には残らないため、別々の要素でも出力の対応表に重複しない。
|
||||||
|
番号を使う形式への出力は、バリアントを選択した構成を単位とする。
|
||||||
|
|
||||||
|
例えば、ANSIのバックスラッシュとISOの `#` が同じ接続先を使う場合も、次のように並べられる。
|
||||||
|
QとAは共通キー、バックスラッシュと `#` は排他キーとする。
|
||||||
|
|
||||||
|
| キー | 保存したorderでの位置 | ANSI出力の番号 | ISO出力の番号 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Q | 0 | 0 | 0 |
|
||||||
|
| バックスラッシュ | 1 | 1 | — |
|
||||||
|
| A | 2 | 2 | 1 |
|
||||||
|
| # | 3 | — | 2 |
|
||||||
|
|
||||||
|
全体の並びは `Q → バックスラッシュ → A → #`、ANSI出力は `Q → バックスラッシュ → A`、
|
||||||
|
ISO出力は `Q → A → #` になる。
|
||||||
|
|
||||||
|
既存コミュニティレイアウトとの互換出力は、要求順を再現できることを確認したものを対象とする。
|
||||||
|
単一の `order` で既存レイアウト全般を再現できることは、現行仕様の前提にしない。
|
||||||
|
|
||||||
|
## 他レイヤーとの関係
|
||||||
|
|
||||||
|
IDXはPHYのキーIDだけを参照する。ELCを参照しない。
|
||||||
|
|
||||||
|
MAPはIDXを参照しない。MAPもキーの参照にPHYのキーIDを使う。
|
||||||
|
キーマップを配列として出力するとき、バリアント選択後のIDXの並び順で並べ替える。
|
||||||
|
ELCから作るキー番号と接続先の対応表にも、同じ選択後の並びを使う。
|
||||||
|
これにより、IDXの編集やバリアント選択で出力番号が変わっても、MAP内のキーIDへの割り当ては書き換えずに済む。
|
||||||
|
PHYのIDで参照するELC・MAPの保存データ自体には、番号を詰める処理はない。
|
||||||
|
|
||||||
|
MAPが番号を持たないキーにキーコードを割り当てていた場合、そのキーは出力に現れない。
|
||||||
|
IDXの `order` と突き合わせて検出できる。`order` に含まれていても、選ばれなかったバリアントの
|
||||||
|
キーへの割り当ては、その出力には含めない。
|
||||||
|
|
||||||
|
## 持たないもの
|
||||||
|
|
||||||
|
- マトリクス座標 (row / col)。ELCの配線から出力時に割り当てる
|
||||||
|
- LEDの識別番号。ELCが正本を持つ
|
||||||
|
- キーコード (MAPが持つ)
|
||||||
|
- キーの物理的な位置とサイズ (PHYが持つ)
|
||||||
|
|
||||||
|
## 保存形式
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type IDX = {
|
||||||
|
order: string[] // 全バリアントの対象キーのPHY ID。各IDは1回だけ現れる。
|
||||||
|
// 出力時に対象外のキーを除き、残った並びの位置をキー番号とする
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
番号は個別のフィールドとして持たず、出力時の並びの位置で表すため、出力番号に欠番や飛びは生じない。
|
||||||
|
キーIDの重複は許さない。
|
||||||
|
|
||||||
|
出力番号の対象は `order` に挙げたキーに限る。PHYの部品ノードと `decal` のキーは対象にならない。
|
||||||
|
|
||||||
|
## 保留課題
|
||||||
|
|
||||||
|
- [IDXの保留課題](backlog/README.md#idx)
|
||||||
|
|
||||||
|
## 検討済み・非採用
|
||||||
|
|
||||||
|
- PHYのツリー順をキー番号とする (journal/2026-09-21-idx-necessity.md)
|
||||||
|
- ELCのマトリクス順からキー番号を導出する (journal/2026-09-21-idx-necessity.md)
|
||||||
|
- 排他キーで番号を共有し、選択後も番号を詰めない (journal/2026-09-22-idx-variant-output.md)
|
||||||
|
- MAPがキー番号を参照する (journal/2026-09-21-idx-necessity.md)
|
||||||
|
- レイアウトごとに別のIDXを持つ (journal/2026-09-21-multi-layout.md)
|
||||||
|
|
||||||
|
## 未確認事項
|
||||||
|
|
||||||
|
(なし)
|
||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
# MAPレイヤー
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
MAPレイヤーは、キーマップレイヤーごとのキーへの割り当てを持つ。
|
||||||
|
MAPエディタは、その設計・編集と、実機との読み書きを提供する。
|
||||||
|
ここでいうキーマップレイヤーは、PHY・ELC等の設計レイヤーとは区別する。
|
||||||
|
|
||||||
|
主眼は、新しく作成するキーボードのワークフローにある。
|
||||||
|
キーマップを設計し、実機へ反映し、使って調整した結果をプロジェクトへ戻せるようにする。
|
||||||
|
既存キーボード全般への接続・取り込みを、初期対応の前提にはしない。
|
||||||
|
|
||||||
|
## MAPデータと他レイヤーの関係
|
||||||
|
|
||||||
|
キー参照と出力時の並べ替えは、[IDXレイヤー「他レイヤーとの関係」](IDXレイヤー.md#他レイヤーとの関係)に従う。
|
||||||
|
MAPのキー参照にはPHYのキーIDを使い、実機との通信に使うアドレスを正本のキー参照にはしない。
|
||||||
|
|
||||||
|
現行の保存形式は、[JSON SchemaのMAP節](phase1-json-schema.md#4-mapレイヤー-json-schema)にある
|
||||||
|
レイヤー別のキーコード割り当てを出発点とする。
|
||||||
|
キーコードはQMKの表記を基本とする。当面の対象ファームウェアがQMKであるため。
|
||||||
|
レイヤーとStaticの動作定義への参照は、QMKの番号ではなく名前で書く([DynamicとStatic](#dynamicとstatic))。
|
||||||
|
|
||||||
|
ファームウェアの設定値は、キーボード全体に効くものをFirmware Generatorの設定とし、
|
||||||
|
キーごとに上書きする値(tapping termのper key設定等)をMAPが持つ。
|
||||||
|
QMKの機能ごとにどちらに属するかを個別に判断しないで済むようにするため。
|
||||||
|
|
||||||
|
動作定義とキーごとの設定の保存形式は、[P-1](#p-1-動作定義とキーごとの設定の保存形式)で扱う。
|
||||||
|
|
||||||
|
新規作成したMAPは、すべてのキーが未割り当ての状態から始める。
|
||||||
|
PHYのラベルからの自動適用は行わない([PHY「ラベル」](PHYレイヤー.md#ラベル))。
|
||||||
|
|
||||||
|
## キーマップレイヤーの識別
|
||||||
|
|
||||||
|
キーマップレイヤーは、並び順と独立した名前で識別する。名前はMAP内で一意とする。
|
||||||
|
レイヤーを切り替える動作も名前でレイヤーを参照し、番号へは出力時に並び順から変換する。
|
||||||
|
レイヤーの並べ替えや挿入で参照先がずれないようにするため。IDXがキー番号を正本にしないのと同じ理由による。
|
||||||
|
|
||||||
|
## MAPエディタの責務
|
||||||
|
|
||||||
|
- PHYの配置を参照し、キーマップレイヤーごとの割り当てを編集・保存する。
|
||||||
|
- 対応する実機へ割り当てを書き込み、実機の現在の割り当てを読み出す。
|
||||||
|
- 実機で使いながら調整し、その結果をMAPへ取り込むワークフローを提供する。
|
||||||
|
|
||||||
|
読み書きは一組として対応する。読み出しには、現在値の確認に加え、Remap等で調整した結果を
|
||||||
|
プロジェクトへ戻す用途も含む。保存と実機への反映の操作、差分表示、自動同期の有無は、
|
||||||
|
[P-3](#p-3-プロジェクトと実機の対応状態管理)で扱う。
|
||||||
|
|
||||||
|
## DynamicとStatic
|
||||||
|
|
||||||
|
Dynamicは、対応ファームウェアが公開している通信プロトコルを使って、再ビルドせずに設定を読み書きする経路。
|
||||||
|
Staticは、設定をファームウェアへ組み込むために出力する経路とする。
|
||||||
|
Staticの出力は[全体方針のFirmware Generator](keyboard-design-layers.md#firmware-generator)が担い、MAPエディタは持たない。
|
||||||
|
|
||||||
|
MAPエディタはDynamicの読み書きを提供する。Dynamicで変更できる範囲は、
|
||||||
|
対象ファームウェアと既存プロトコルが公開する範囲に従う。
|
||||||
|
|
||||||
|
当面の対象ファームウェアはQMKとし、対応範囲を次のように分ける。
|
||||||
|
|
||||||
|
- Dynamic: QMK本家のVIAプロトコルが公開する項目に限る。
|
||||||
|
- Static: QMK本家masterの公式機能のうちVIAで変更できないものは、Firmware Generatorで生成する対象とする。
|
||||||
|
- 次のものには当面対応しない。
|
||||||
|
- Vial等のフォーク
|
||||||
|
- XAP、developブランチ等、master以外にある機能
|
||||||
|
- Community Modules(VIAモジュールを除く)
|
||||||
|
- キーボード・キーマップ固有のコード(VIAのキーボード独自メニューを含む)には、原則として対応しない。
|
||||||
|
|
||||||
|
QMKはVIAをCommunity Modulesの形式へ移し、従来の実装を将来のリリースで削除すると告知している(QMK 0.34.4のChangeLog「VIA Feature Migration」)。
|
||||||
|
VIAがモジュールへ移っても、QMKとVIAの両方に追従する。そのため、移行がmasterに入る前に、VIAモジュールを有効にした出力と読み書きに対応する。
|
||||||
|
|
||||||
|
Staticで定義する動作(タップダンス等)をキーへ割り当てるときは、定義を名前で参照し、出力時に番号へ変換する。
|
||||||
|
割り当て自体はVIAで読み書きできても、参照先の定義はStaticにしかないため、
|
||||||
|
[レイヤーの参照](#キーマップレイヤーの識別)と同じく並び順に依存させない。
|
||||||
|
|
||||||
|
QMK本家のVIAには、VIAの管理外にある実機の保存設定(Magic設定、保存されたデフォルトレイヤー、Unicodeの入力モード等)を読み出す手段がない。
|
||||||
|
これらを実機で変更しても、MAPエディタはプロジェクトへ取り込めない。
|
||||||
|
|
||||||
|
プロジェクトのMAP、Staticで組み込んだ初期キーマップ、実機に保存された現在の割り当ては区別する。
|
||||||
|
実機で調整してMAPへ取り込んだ割り当ては、Firmware Generatorへの入力としてStaticの初期キーマップへ反映できるようにする。
|
||||||
|
実機の保存内容を初期化しても、調整後の割り当てに戻せるようにするため。
|
||||||
|
|
||||||
|
## 対応プロトコル
|
||||||
|
|
||||||
|
最初に対応する通信プロトコルはVIAとし、対応プロトコルを絞って段階的に増やす。
|
||||||
|
ユーザーが好むRemapと同じ通信経路を使い、新規キーボードの設計から実機での調整へつなぐため。
|
||||||
|
|
||||||
|
- QMKやVial側にBeyond KLE独自のプロトコルを追加しない。そのためのファームウェアのフォークも行わない。
|
||||||
|
- VIA対応では、対象ファームウェアの既存のVIA機能を使って読み書きする。
|
||||||
|
- VIAへの対応は、Remapというアプリ全体の機能を再現することを意味しない。
|
||||||
|
- 初期の実装対象はVIAに限る。
|
||||||
|
|
||||||
|
対応環境は、QMKのコミットと、使うモジュールの一覧(各モジュールの出所とコミット)で固定し、
|
||||||
|
更新するタイミングはユーザーが個別に判断する。QMKに同梱されたモジュールは、QMKのコミットで決まる。
|
||||||
|
VIAもモジュールの1つとして扱い、QMKリポジトリの外へ移った場合にも同じ形で指定できるようにするため。
|
||||||
|
MAPエディタのUIには、対応しているVIAプロトコルとキーコード体系のバージョンを明示する。
|
||||||
|
接続した実機とはこの2つのバージョンで照合し、一致しない実機には対応しない。
|
||||||
|
実機から取得できるバージョン情報がこの2つに限られるため。
|
||||||
|
|
||||||
|
最初に対応する環境は、QMK 0.34.4(commit `08c662f`)とする。このときキーコード体系は0.0.9、VIAプロトコルは0x000D。
|
||||||
|
VIAは同梱の従来実装(`quantum/via.c`)で、追加のモジュールは使わない。
|
||||||
|
|
||||||
|
## VIAで読み書きする項目
|
||||||
|
|
||||||
|
VIAプロトコル0x000Dのコマンドのうち、MAPエディタが扱うものを次のように分ける[推測:P-2]。
|
||||||
|
|
||||||
|
- 対応する:
|
||||||
|
- VIAプロトコルとキーコード体系のバージョンの読み出し(実機との照合に使う)
|
||||||
|
- レイヤー数の読み出し
|
||||||
|
- レイヤー別のキーコード割り当ての読み書き
|
||||||
|
- マクロの読み書き(個数・容量の読み出しとリセットを含む)
|
||||||
|
- 別の課題で扱う:
|
||||||
|
- エンコーダの割り当て([MAP-P-4](backlog/README.md#map-p-4-onoff以外の入力への割り当て))
|
||||||
|
- layout option([P-3](#p-3-プロジェクトと実機の対応状態管理))
|
||||||
|
- 当面対応しない: キーマップ・EEPROMのリセット、ブートローダへの移行、デバイスの識別表示、
|
||||||
|
マトリクス状態の読み出し(キーテスター)、custom channel(照明・オーディオ・キーボード独自メニュー)
|
||||||
|
|
||||||
|
実機のファームウェアでどの機能が有効かは、VIAでは読み出せない。
|
||||||
|
新規キーボードではFirmware Generatorのビルド設定を正とする。既存キーボードでは、利用者が同じ形式で申告した内容を信用する。
|
||||||
|
レイヤー数、マクロの個数と容量は実機から読み出し、書き込む前にMAPの内容が収まるかを確認する。
|
||||||
|
|
||||||
|
## 持たないもの
|
||||||
|
|
||||||
|
物理配置、配線、キーの出力番号は、それぞれPHY・ELC・IDXで扱う。
|
||||||
|
ホストによるキーコードの解釈は[HSTレイヤー](HSTレイヤー.md)、IMEの入力変換はOSの責務とし、外部ツールで編集できる照明等の設定も、
|
||||||
|
VIAで通信できるという理由だけでMAPの所有データには含めない。
|
||||||
|
MAPエディタは、キーコードを当面US配列の文字で表示する。
|
||||||
|
|
||||||
|
アナログ入力の解釈の所属は、[全体方針のP-1](keyboard-design-layers.md#p-1-アナログ値の解釈をどの層が持つか)で扱う。
|
||||||
|
|
||||||
|
## 保留課題
|
||||||
|
|
||||||
|
- [MAPの保留課題](backlog/README.md#map)
|
||||||
|
- [PHYの保留課題](backlog/README.md#phy)
|
||||||
|
|
||||||
|
## 検討済み・非採用
|
||||||
|
|
||||||
|
- MAPエディタをStaticに閉じる (journal/2026-09-22-map-via-workflow.md)
|
||||||
|
|
||||||
|
## 未確認事項
|
||||||
|
|
||||||
|
### P-1 動作定義とキーごとの設定の保存形式
|
||||||
|
|
||||||
|
- 位置づけ: MAPのデータモデルを定めるための設計判断。
|
||||||
|
- 未決事項:
|
||||||
|
- マクロ(VIAで読み書きするため内容をMAPが持つ)と、タップダンス・コンボ等のStaticで定義する動作を、MAPにどう保存するか。
|
||||||
|
- キーごとに上書きする設定を、PHYのキー、キーとレイヤーの組、キーコードのどれに付けるか。
|
||||||
|
- 完了条件: 動作定義とキーごとの設定の保存形式をユーザーが決める。
|
||||||
|
|
||||||
|
### P-2 VIAのコマンドはQMK 0.34.4のソースどおりに動作する
|
||||||
|
|
||||||
|
- 位置づけ: 本文「VIAで読み書きする項目」が前提にしている。コマンドの内容はソース(`quantum/via.c`)と公式文書から確認したもので、実機では検証していない。
|
||||||
|
- 格上げ条件: QMK 0.34.4でビルドした新規キーボードの構成で、対応するコマンドの読み書きと、レイヤー数・マクロの個数と容量の読み出しを実機で確認する。
|
||||||
|
|
||||||
|
### P-3 プロジェクトと実機の対応・状態管理
|
||||||
|
|
||||||
|
- 位置づけ: 実機への読み書きをMAPへ結び付けるための設計判断。
|
||||||
|
- 未決事項:
|
||||||
|
- 出力した構成とPHYキーID・通信上のアドレスの対応をどこに保持し、接続した実機とどう照合するか。
|
||||||
|
- VIAの割り当ては接続先(row/col)単位で保存されるため、同じ接続先に配線された排他キーは、実機では1つの割り当て先を共有する。書き込み時に選択外のバリアントのキーの割り当てをどう扱うか、読み出した値をどのPHYキーへ戻すか(実機側のlayout optionの選択に従うか)。
|
||||||
|
- バリアントや配線の変更後の扱い。
|
||||||
|
- プロジェクトと実機、プロジェクトとStaticで組み込んだ初期キーマップの差分表示・保存・反映の操作。
|
||||||
|
- 完了条件: 対応情報の生成・保存・照合方法と状態管理を定め、設計から実機へ反映し、Remap等での変更を元のPHYキーIDへの割り当てとして取り込む手順を確認する。排他キーを含む構成で、書き込みと読み出しがバリアントの選択に従って元のPHYキーへ対応することを確認する。
|
||||||
+127
-55
@@ -3,7 +3,7 @@
|
|||||||
## 概要
|
## 概要
|
||||||
|
|
||||||
PHYレイヤーは、キースイッチやキーキャップ、その他周辺部品の物理的な配置に関する情報を提供する。
|
PHYレイヤーは、キースイッチやキーキャップ、その他周辺部品の物理的な配置に関する情報を提供する。
|
||||||
付加的な情報として、matrix情報(ELCレイヤー)や入力キー情報(MAP/IMEレイヤー)を含ませることができるが、必須ではない。
|
付加的な情報として、matrix情報(ELCレイヤー)や入力キー情報(MAP/HSTレイヤー)を含ませることができるが、必須ではない。
|
||||||
|
|
||||||
## 構造
|
## 構造
|
||||||
|
|
||||||
@@ -16,9 +16,16 @@ Stepped Caps Lockのようにスイッチがキャップ中心からずれるキ
|
|||||||
|
|
||||||
### プロファイル設定
|
### プロファイル設定
|
||||||
|
|
||||||
1ファイル内に複数のプロファイルを持たせることができる。
|
1ファイル内に複数のプロファイルを持たせることができる。プロファイルはノードではなくファイル単位に置き、
|
||||||
ユニットサイズ(1Uが何mmか、x軸・y軸それぞれ)を規定。
|
キーはIDで参照する。同じ定義を複数のキーに複製しないため、実体ではなく参照とする。
|
||||||
ビジュアル表現として、内側へのオフセット位置及びオフセット距離(mm or U単位)・R(U単位)を規定。
|
|
||||||
|
プロファイルが規定するのは次の2つ。
|
||||||
|
|
||||||
|
- ユニットサイズ: 1Uが何mmか。x軸・y軸それぞれ
|
||||||
|
- ビジュアル表現: キー上面の矩形を、外形の各辺(上下左右)から内側への距離と、角のRで表す
|
||||||
|
|
||||||
|
上面の距離を4辺それぞれ持つのは、上面が外形の中心からずれるプロファイルを表せるようにするため。
|
||||||
|
プロファイル内の距離はすべてmmで持つ。実寸を規定する定義なので、U単位の値を混ぜない。
|
||||||
|
|
||||||
### 座標と回転
|
### 座標と回転
|
||||||
|
|
||||||
@@ -94,6 +101,8 @@ LED・OLED・ロータリーエンコーダ・ネジ穴のように、設計者
|
|||||||
フットプリントの選択には使わない。予約名以外および省略時は既定の形で描く。
|
フットプリントの選択には使わない。予約名以外および省略時は既定の形で描く。
|
||||||
ユーザーが付ける名前は `@` で始めない限り予約と衝突しない。
|
ユーザーが付ける名前は `@` で始めない限り予約と衝突しない。
|
||||||
|
|
||||||
|
当面は表裏の描き分けを行わない。描画用の予約グループ名を、PCB上の実装面の指定としては使わない。
|
||||||
|
|
||||||
部品ノードはクラスターの子になれる。クラスターを動かせば配下の部品も追随する。
|
部品ノードはクラスターの子になれる。クラスターを動かせば配下の部品も追随する。
|
||||||
|
|
||||||
LEDの配線はPHYが持たない。LEDの識別番号もPHYは持たない。番号は配線で決まるため、ELCが持つ。
|
LEDの配線はPHYが持たない。LEDの識別番号もPHYは持たない。番号は配線で決まるため、ELCが持つ。
|
||||||
@@ -106,6 +115,7 @@ IDXはLEDの番号を持たない。RGB効果が必要とする位置と区分
|
|||||||
### ラベル
|
### ラベル
|
||||||
|
|
||||||
キーはラベルを参考情報として持つ。KLE JSONの取り込みでは照合に使う。
|
キーはラベルを参考情報として持つ。KLE JSONの取り込みでは照合に使う。
|
||||||
|
当面はラベルからMAPへの自動適用は行わない。
|
||||||
|
|
||||||
### グルーピング
|
### グルーピング
|
||||||
|
|
||||||
@@ -124,6 +134,48 @@ IDXはLEDの番号を持たない。RGB効果が必要とする位置と区分
|
|||||||
|
|
||||||
- クラスター構造はBeyond KLEのデータにのみ存在する。外部形式へは階層を畳んだキー配置だけを出力する
|
- クラスター構造はBeyond KLEのデータにのみ存在する。外部形式へは階層を畳んだキー配置だけを出力する
|
||||||
|
|
||||||
|
### レイアウトバリアント
|
||||||
|
|
||||||
|
一部のキークラスタを別の構成と交換できるようにした基板がある。
|
||||||
|
`\ |` と `Enter` を ISO Enter と1Uキーに変える、スペースキー周りのキー数を変える、といったもの。
|
||||||
|
交換にはスイッチの差し替えが伴い、同時に成立するのは1つだけである。
|
||||||
|
|
||||||
|
PHYは全バリアントのキーを同時に持つ。基板上のフットプリントは選ばれなかった側も実在するため、
|
||||||
|
どれか1つに畳んだ状態を正本にできない。
|
||||||
|
|
||||||
|
排他関係は木構造で表す。バリアントグループ(VariantGroup)と、その子であるバリアント(Variant)の
|
||||||
|
2種類のノードを置く。Variantの配下にあるノードが、そのバリアントを選んだときに実装されるものになる。
|
||||||
|
|
||||||
|
- VariantGroupの子はVariantだけ。VariantはVariantGroupの直下にしか現れない
|
||||||
|
- 選択肢が3つ以上ある場合はVariantを3つ以上置く
|
||||||
|
- 配下にノードを持たないVariantを置いてよい(「エンコーダーを載せない」など)
|
||||||
|
- 部品ノードもVariantの配下に置ける。1Uキーとロータリーエンコーダーの選択がこれにあたる
|
||||||
|
- Variantの中に別のVariantGroupを置ける。あるバリアントを選んだときだけ現れる選択肢を表す
|
||||||
|
- 選択箇所が複数ある場合はVariantGroupを複数置く。同じ親の下に並べてよい
|
||||||
|
- 選択が互いに独立なら、VariantGroupを分けて選択肢の数を減らしてよい。
|
||||||
|
ANSI/ISOとBackspaceの分割のように、独立な2つの選択が1つの選択肢リストに畳まれている基板がある
|
||||||
|
|
||||||
|
VariantGroupはクラスターと同じ変換を持つ。Variantは変換を持たない。
|
||||||
|
バリアントは同じ場所を占める代替物であり、選択によって配下の絶対配置が動く構造を作らないため。
|
||||||
|
1つのバリアントの中で回転中心が要る場合は、その中にクラスターを置く。
|
||||||
|
|
||||||
|
サイズや形が変わるキーは、同じキーを書き換えるのではなく、別のキーノードとして各Variantに置く。
|
||||||
|
ANSI EnterとISO Enterは別のIDを持つ2つのキーであり、同じ場所に重なる。
|
||||||
|
|
||||||
|
同じ位置・同じサイズのキーが複数のバリアントで成立する場合も同じで、Variantごとに別のキーノードを置く。
|
||||||
|
隣り合うキャップが同じフットプリントを食い合う基板があり、選択を分解できるとは限らないため。
|
||||||
|
どのキーノードが同じ物理キーを指すかはBeyond KLEが判定しない。設計者が知っている情報として扱う。
|
||||||
|
|
||||||
|
VariantGroupは既定のバリアントを1つ持つ。KLE JSONのように単一の配置を要求する出力は、
|
||||||
|
VariantGroupごとにバリアントを1つ選んでから階層を畳んで生成する。既定値はそのときの初期選択になる。
|
||||||
|
編集中にどのバリアントを表示しているかはエディタの状態であり、PHYには保存しない。
|
||||||
|
|
||||||
|
Variantをまたぐノードの移動は、クラスター間の移動と同じreparentとして扱う。絶対配置は変わらない。
|
||||||
|
組み替えの対象を同一の親に限る規則がそのまま効くため、複数のVariantにまたがる選択は組み替えできない。
|
||||||
|
|
||||||
|
ELC・IDX・MAPの保存データは全バリアントのPHYノードIDを参照し、選択によって書き換えない。
|
||||||
|
選択は、PHYを単一の配置に畳むときや、IDX・MAPから選択した構成のキー配列を生成するときに使う。
|
||||||
|
|
||||||
### クラスターの組み替え
|
### クラスターの組み替え
|
||||||
|
|
||||||
クラスター構造は、配置を作ったあとから組み替える。KLE JSONから取り込んだ平坦なキー配置に
|
クラスター構造は、配置を作ったあとから組み替える。KLE JSONから取り込んだ平坦なキー配置に
|
||||||
@@ -149,12 +201,28 @@ Transform は回転と平行移動だけを持つ剛体変換であり、合成
|
|||||||
ただし選択したノード全部が同じ回転角度を持つ場合はその角度を基準角度に採り、境界矩形もその角度の座標系で取る
|
ただし選択したノード全部が同じ回転角度を持つ場合はその角度を基準角度に採り、境界矩形もその角度の座標系で取る
|
||||||
- 組み替えで生じた非グリッド値はそのまま保持する。クラスターを解除して親に戻ったノードも同じ
|
- 組み替えで生じた非グリッド値はそのまま保持する。クラスターを解除して親に戻ったノードも同じ
|
||||||
|
|
||||||
|
当面、reframeは通常の移動・回転と別の明示的な操作とし、原点の指定は数値入力で行う。
|
||||||
|
原点の吸着やフォーカス状態・ハンドルに連動した操作は、試作後に検討する。
|
||||||
|
|
||||||
## 保存形式
|
## 保存形式
|
||||||
|
|
||||||
各ノードは親から見た変換を持つ。キー・クラスター・部品ノードを混在させた木構造として保持する。
|
各ノードは親から見た変換を持つ。キー・クラスター・部品ノード・バリアントノードを混在させた木構造として保持する。
|
||||||
各ノードはIDを持つ。木の中のノードを一意に指すために持ち、キーのIDは他レイヤーからの参照にも使う。
|
各ノードはIDを持つ。木の中のノードを一意に指すために持ち、キーのIDは他レイヤーからの参照にも使う。
|
||||||
|
プロファイルは木の外に置き、ファイル単位の配列として持つ。
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
type Profile = {
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
unitSize: { x: number, y: number }, // 1Uのmm数
|
||||||
|
visualOffset?: { // キー上面。単位はmm
|
||||||
|
top: number, // 外形の各辺から内側への距離
|
||||||
|
right: number,
|
||||||
|
bottom: number,
|
||||||
|
left: number,
|
||||||
|
radius: number
|
||||||
|
}
|
||||||
|
}
|
||||||
type Transform = {
|
type Transform = {
|
||||||
x: number, // 親座標系での位置
|
x: number, // 親座標系での位置
|
||||||
y: number,
|
y: number,
|
||||||
@@ -172,7 +240,7 @@ type Key = {
|
|||||||
id: string,
|
id: string,
|
||||||
transform: Transform,
|
transform: Transform,
|
||||||
size: Size,
|
size: Size,
|
||||||
profile: Profile,
|
profile: string, // Profile.id を参照
|
||||||
decal?: boolean,
|
decal?: boolean,
|
||||||
ghost?: boolean,
|
ghost?: boolean,
|
||||||
labels?: string[], // 参考情報
|
labels?: string[], // 参考情報
|
||||||
@@ -186,12 +254,36 @@ type Component = {
|
|||||||
transform: Transform,
|
transform: Transform,
|
||||||
partGroup?: string // `@` 始まりは描画用の予約名
|
partGroup?: string // `@` 始まりは描画用の予約名
|
||||||
}
|
}
|
||||||
|
type Node = Key | Cluster | Component | VariantGroup
|
||||||
type Cluster = {
|
type Cluster = {
|
||||||
id: string,
|
id: string,
|
||||||
transform: Transform,
|
transform: Transform,
|
||||||
children: (Key | Cluster | Component)[]
|
children: Node[]
|
||||||
|
}
|
||||||
|
type VariantGroup = {
|
||||||
|
id: string,
|
||||||
|
name: string, // UI上の選択肢の見出し
|
||||||
|
transform: Transform,
|
||||||
|
default: string, // 既定のVariantのid
|
||||||
|
children: Variant[] // 2つ以上。並びが表示順
|
||||||
|
}
|
||||||
|
type Variant = {
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
children: Node[] // 空でもよい
|
||||||
|
}
|
||||||
|
type PhyFile = {
|
||||||
|
version: string,
|
||||||
|
metadata: {
|
||||||
|
name?: string,
|
||||||
|
author?: string,
|
||||||
|
description?: string,
|
||||||
|
created?: string,
|
||||||
|
modified?: string
|
||||||
|
},
|
||||||
|
profiles: Profile[], // 1つ以上
|
||||||
|
root: Node[]
|
||||||
}
|
}
|
||||||
const root: (Key | Cluster | Component)[] = []
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 連携用出力形式
|
## 連携用出力形式
|
||||||
@@ -243,77 +335,57 @@ const kleData = (ConfigObject | KeyLabel)[][]
|
|||||||
```
|
```
|
||||||
|
|
||||||
- 回転角度は正規化せずそのまま出力する。KLEのデータは任意の角度を受け付け、15度刻みはKLEのUI上の操作単位にすぎない
|
- 回転角度は正規化せずそのまま出力する。KLEのデータは任意の角度を受け付け、15度刻みはKLEのUI上の操作単位にすぎない
|
||||||
- rx, ry はPHYに持たないので、出力時に選ぶ
|
- rx, ry はPHYに持たないので、出力時に選ぶ。同じ絶対角度を持つキーには共通の値を与える
|
||||||
|
(クラスターの原点が使える)。KLEが1行にまとめるのは r, rx, ry が一致し y が等しいキーだけという制約による
|
||||||
|
- 書き出す x, y は丸める。行のまとまりは y の厳密一致で決まるので、絶対座標への変換で入った誤差が残ると行が割れる
|
||||||
- ユニットサイズ(mm)、ビジュアルオフセット、クラスター構造、部品ノード、per-key LEDはKLE JSONに出力されない
|
- ユニットサイズ(mm)、ビジュアルオフセット、クラスター構造、部品ノード、per-key LEDはKLE JSONに出力されない
|
||||||
|
- バリアント構造はKLE JSONに出力しない。VariantGroupごとにバリアントを1つ選び、その配下だけを畳んで出力する
|
||||||
- 部品グループ・スイッチの向きはKLE JSONに出力しない。sm, sb, st には取り込み時に保持した値をそのまま書き戻す
|
- 部品グループ・スイッチの向きはKLE JSONに出力しない。sm, sb, st には取り込み時に保持した値をそのまま書き戻す
|
||||||
|
- r, rx, ry は行の先頭にのみ置く。KLE本家はそれ以外の位置に置かれたものを読まない
|
||||||
|
- p, sm, sb, st は空文字に戻す指定がKLE側で無視される。値を持つキーと持たないキーが混在する配置は、
|
||||||
|
KLEで開いた時点で後続のキーに前の値が残る
|
||||||
- KLEの `p` はキーキャップのプロファイル名(SA、DCSなど)であり、PHYのプロファイルとは対応させない
|
- KLEの `p` はキーキャップのプロファイル名(SA、DCSなど)であり、PHYのプロファイルとは対応させない
|
||||||
|
|
||||||
### KLE JSONの取り込み
|
### KLE JSONの取り込み
|
||||||
|
|
||||||
|
当面は新規PHYとしての読み込みを対象とする。既存PHYへの照合・更新は、照合規則を検証するまで未対応とする。
|
||||||
|
以下は照合・更新に対応する際の制約であり、曖昧な対応を自動確定しない。
|
||||||
|
誤照合で既存IDを別のキーに割り当てると、他レイヤーの参照にも影響するため。
|
||||||
|
|
||||||
既存のPHYデータを畳んだ配置に対して、KLE JSONの各キーを形状(位置・サイズ・回転)とラベルで照合する。
|
既存のPHYデータを畳んだ配置に対して、KLE JSONの各キーを形状(位置・サイズ・回転)とラベルで照合する。
|
||||||
|
|
||||||
- 照合できたキーは元のノードとして扱う。KLE側で配置が変わっていれば、元の親から見た相対座標に戻して反映する
|
- 照合できたキーは元のノードとして扱う。KLE側で配置が変わっていれば、元の親から見た相対座標に戻して反映する
|
||||||
- 照合できないキーは root 直下に新規キーとして置く。クラスターへの割り当てはユーザーが行う
|
- 照合できないキーは root 直下に新規キーとして置く。クラスターとバリアントへの割り当てはユーザーが行う
|
||||||
|
- 照合の対象は、出力時に選んだバリアントの配下にあるキーに限る。選ばれなかったバリアントのキーは照合にも削除にも関与しない
|
||||||
- 構造の復元は保証しない。KLE側での編集が大きいほど、クラスター構造は失われる
|
- 構造の復元は保証しない。KLE側での編集が大きいほど、クラスター構造は失われる
|
||||||
- 配列の順番は照合に使わない。KLEは保存時にキーを「角度 → rx → ry → y → x」の順に並べ替えるため、順番は形状から決まる以上の情報を持たない
|
- 配列の順番は照合に使わない。KLEは保存時にキーを「角度 → rx → ry → y → x」の順に並べ替えるため、順番は形状から決まる以上の情報を持たない
|
||||||
|
|
||||||
|
r, rx, ry は行の先頭以外に置かれていても受け入れ、そのキー以降に適用する。
|
||||||
|
rx, ry を持つキーでは、座標を (rx, ry) に戻してから x, y を加える。
|
||||||
|
KLE本家はこの位置の r, rx, ry をエラーにするが、VIAとRemapは読む。Remapで扱える基板を取り込めない状態にしない。
|
||||||
|
|
||||||
KLEの rx, ry, r はキーの絶対配置 (x, y, rotation) に変換して取り込む。元の rx, ry は保持しない。
|
KLEの rx, ry, r はキーの絶対配置 (x, y, rotation) に変換して取り込む。元の rx, ry は保持しない。
|
||||||
|
|
||||||
見た目の属性(c, t, p, a, f, f2, fa, l, n)は、KLE JSONへ書き戻せるように保持する。どの層で持つかは決まっていない。
|
見た目の属性(c, t, p, a, f, f2, fa, l, n)は、KLE JSONへ書き戻せるように保持する。
|
||||||
|
当面はキー単位の `kleCompatibility` に不透明なデータとして退避し、見た目の編集機能は持たない。
|
||||||
|
|
||||||
KLEの sm, sb, st(ファイル単位では switchMount, switchBrand, switchType)も同様に、不透明なまま保持して書き戻す。
|
KLEの sm, sb, st(ファイル単位では switchMount, switchBrand, switchType)も同様に、不透明なまま保持して書き戻す。
|
||||||
Beyond KLEはスイッチの品種を持たないため、対応づける先がない。
|
Beyond KLEはスイッチの品種を持たないため、対応づける先がない。
|
||||||
|
ファイル単位の属性(switchMount, switchBrand, switchType, backcolor, radii, background, css)は、
|
||||||
|
ファイル単位の `kleCompatibility` に退避する。正式な所属層を決めるまでは、この退避先で保持する。
|
||||||
|
|
||||||
|
## 保留課題
|
||||||
|
|
||||||
|
- [PHYの保留課題](backlog/README.md#phy)
|
||||||
|
|
||||||
## 検討済み・非採用
|
## 検討済み・非採用
|
||||||
|
|
||||||
- スイッチのブランドをPHYで持つ (journal/2026-09-20-switch-terminal.md)
|
|
||||||
- スイッチの品種・ソケット方式をPHYで持つ (journal/2026-09-20-switch-terminal-reversal.md)
|
- スイッチの品種・ソケット方式をPHYで持つ (journal/2026-09-20-switch-terminal-reversal.md)
|
||||||
- LED専用ノード (journal/2026-09-20-led-node.md)
|
- LED専用ノード (journal/2026-09-20-led-node.md)
|
||||||
- per-key LEDに座標を持たせる (journal/2026-09-20-led-node.md)
|
- per-key LEDに座標を持たせる (journal/2026-09-20-led-node.md)
|
||||||
- KLE取り込み時のクラスター自動推定 (journal/2026-09-20-cluster-manual-grouping.md)
|
- KLE取り込み時のクラスター自動推定 (journal/2026-09-20-cluster-manual-grouping.md)
|
||||||
|
- レイアウトバリアントをキーごとのタグとファイル単位の宣言で持つ (journal/2026-09-21-multi-layout.md)
|
||||||
|
|
||||||
## 未確認事項
|
## 未確認事項
|
||||||
|
|
||||||
### P-1 KLE JSONへ書き出した配置が行のまとまりを保てるか
|
(なし)
|
||||||
|
|
||||||
- 位置づけ: 本文が前提にしているものではない。連携用出力形式の設計に関わる未解決の問い
|
|
||||||
- 現状の認識: 階層を畳んでKLEに読み込ませると行のまとまりが崩れる。他レイヤーの情報を使ってでも行ごとにまとめたいが、方法は未検討
|
|
||||||
- 手がかり: KLEは r, rx, ry が同じで y が等しいキーを1行にまとめる。同じ絶対角度を持つキーに共通の rx, ry(例えばクラスターの原点)を選べば、行にまとめられる見込み
|
|
||||||
- 格上げ条件: 回転を含むPHYデータをKLE JSONに変換してKLE本家に読み込ませ、rotation groupと行配列がどう再構成されるかを確認する
|
|
||||||
|
|
||||||
### P-2 KLE JSON取り込み時の照合基準
|
|
||||||
|
|
||||||
- 位置づけ: 本文「KLE JSONの取り込み」の具体化に必要な未解決の問い
|
|
||||||
- 現状の認識: 形状の許容誤差、ラベルの扱い、複数候補がある場合の決め方が未定
|
|
||||||
- 格上げ条件: 照合規則を決めたうえで、KLE本家でキーの移動・追加・削除をしたJSONを取り込み、意図どおりに対応づくことを確認する
|
|
||||||
|
|
||||||
### P-6 キーキャップの見た目情報を持つ層
|
|
||||||
|
|
||||||
- 位置づけ: 本文「見た目の属性は保持する。どの層で持つかは決まっていない」の未解決部分
|
|
||||||
- 現状の認識: c, t, p, a, f系に加え、l, n もキーキャップの見た目属性であり、PHYとは別の層が要りそう。退避だけして書き戻す sm, sb, st も同じ置き場の問題を持つ。そもそもキーキャップは設計時に決める必要が薄く、カスタムキーボードの文脈に属するため、Beyond KLEの扱う範囲かどうかも未定。ファイル単位の backcolor, radii, background, css も同じ問いに含まれる
|
|
||||||
- 格上げ条件: キーキャップの情報をBeyond KLEの範囲に含めるかをユーザーが決め、保持する層を決める
|
|
||||||
|
|
||||||
### P-8 ラベルをMAPに適用するか
|
|
||||||
|
|
||||||
- 位置づけ: 未解決の問い。本文ではラベルを参考情報として持つことだけを決めている
|
|
||||||
- 現状の認識: どのラベルがMAPに適用できるかは、おそらく機械的には判断できない。自動で適用してユーザーが不要なものを落とす方式も考えられる
|
|
||||||
- 格上げ条件: 適用するか、する場合は自動適用とユーザーによる取捨の流れをユーザーが決める
|
|
||||||
|
|
||||||
### P-12 部品の表裏を描画でどう扱うか
|
|
||||||
|
|
||||||
- 位置づけ: 本文「部品ノードは表裏を持たない」の残余
|
|
||||||
- 現状の認識: どちらの面に載るかはPCB設計ツールが決める情報なのでPHYには持たない。ただしエディタ上で裏面の部品を描き分けたくなる可能性があり、`@` 始まりの予約グループ名だけで表現できるかは未検証
|
|
||||||
- 格上げ条件: 予約グループ名による描画を実装し、表裏の描き分けが必要になるか、必要なら予約名で足りるかを確認する
|
|
||||||
|
|
||||||
### P-13 reframeと通常の移動・回転をエディタ上でどう区別するか
|
|
||||||
|
|
||||||
- 位置づけ: 本文「クラスターの組み替え」の残余。データモデル側は決まっており、操作系だけが未定
|
|
||||||
- 現状の認識: クラスターへのフォーカス状態に紐づけ(中に入っている間は配下の絶対配置が変わらない)、
|
|
||||||
そのうえで原点ハンドルと通常の移動・回転ハンドルを分ける案が有力。専用モードを設ける案もある
|
|
||||||
- 格上げ条件: 操作系をユーザーが決める
|
|
||||||
|
|
||||||
### P-14 クラスター原点の吸着先
|
|
||||||
|
|
||||||
- 位置づけ: 未解決の問い。数値入力以外に、原点をどこへ吸着させるか
|
|
||||||
- 現状の認識: キーのローカル原点(左上角)・キー中心・境界矩形の角と中心が候補。相対グリッドとは別の機構になる
|
|
||||||
- 格上げ条件: 原点移動を実装し、吸着なしで足りるか、足りない場合どの候補が要るかを確認する
|
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
# 保留課題
|
||||||
|
|
||||||
|
現在の実装が答えに依存せず進められる問い・検証・改善候補をまとめる。
|
||||||
|
保留中の対応範囲は各仕様書を正とし、運用は[ドキュメント運用ルール](../CLAUDE.md)に従う。
|
||||||
|
項目の識別子は移動元のレイヤー名と番号を引き継ぐ。
|
||||||
|
|
||||||
|
## PHY
|
||||||
|
|
||||||
|
### PHY-P-2 KLE JSON取り込み時の照合基準
|
||||||
|
|
||||||
|
- 関連仕様: [PHYレイヤー](../PHYレイヤー.md)「KLE JSONの取り込み」
|
||||||
|
- 未決事項: 形状の許容誤差、ラベルの扱い、複数候補がある場合の決め方。
|
||||||
|
- 保留できる理由: 新規PHYとしての読み込みは、既存ノードとの照合なしで実装できる。
|
||||||
|
- 再開条件: 既存PHYへのKLE JSONの照合・更新に着手するとき。
|
||||||
|
- 完了条件: 照合規則と曖昧な候補の扱いを決め、KLE本家でキーの移動・追加・削除をしたJSONを取り込み、
|
||||||
|
意図したIDに対応づき、他レイヤーの参照を誤って付け替えないことを確認する。
|
||||||
|
|
||||||
|
### PHY-P-6 キーキャップの見た目情報を持つ層
|
||||||
|
|
||||||
|
- 関連仕様: [PHYレイヤー](../PHYレイヤー.md)「KLE JSONの取り込み」、[JSON Schema](../phase1-json-schema.md)の `kleCompatibility`
|
||||||
|
- 未決事項: キーキャップの見た目編集をBeyond KLEの範囲に含めるか、正式にはどの層で情報を持つか。
|
||||||
|
対象はキー単位の c, t, p, a, f系, l, n と、退避する sm, sb, st、ファイル単位の
|
||||||
|
switchMount, switchBrand, switchType, backcolor, radii, background, css。
|
||||||
|
- 保留できる理由: 現行Schemaの `kleCompatibility` で、正式な所属層や編集機能を決めずに保持・書き戻しができる。
|
||||||
|
- 再開条件: 見た目の編集機能、または退避先から正式な所属層への移行を検討するとき。
|
||||||
|
- 完了条件: 扱う範囲と所属層をユーザーが決め、既存の退避情報を失わない保持・移行方法を定める。
|
||||||
|
|
||||||
|
### PHY-P-8 ラベルをMAPに適用するか
|
||||||
|
|
||||||
|
- 関連仕様: [PHYレイヤー](../PHYレイヤー.md)「ラベル」
|
||||||
|
- 未決事項: ラベルをMAPへ適用するか。機械的に適用先を判断できるかは未確認で、候補を自動適用してユーザーが不要なものを落とす案もある。
|
||||||
|
- 保留できる理由: ラベルは参考情報として保持でき、MAPへの自動適用なしで各レイヤーを扱える。
|
||||||
|
- 再開条件: MAPのキーコード割り当てを補助する機能を検討するとき。
|
||||||
|
- 完了条件: 適用の要否と、適用する場合の候補提示・ユーザーによる取捨の流れをユーザーが決める。
|
||||||
|
|
||||||
|
### PHY-P-12 部品の表裏を描画でどう扱うか
|
||||||
|
|
||||||
|
- 関連仕様: [PHYレイヤー](../PHYレイヤー.md)「部品ノード」
|
||||||
|
- 未決事項: エディタ上で表裏の描き分けが必要か。必要なら `@` 始まりの予約グループ名だけで足りるか。
|
||||||
|
- 保留できる理由: 表裏を描き分けなくても配置は編集でき、実装面はPCB設計ツール側で決められる。
|
||||||
|
- 再開条件: 部品ノードと予約グループ名による描画を試作し、表示を評価するとき。
|
||||||
|
- 完了条件: 描き分けの要否を確認し、必要なら実装面の指定と混同しない表示方法を決める。
|
||||||
|
|
||||||
|
### PHY-P-13 reframeと通常の移動・回転をエディタ上でどう区別するか
|
||||||
|
|
||||||
|
- 関連仕様: [PHYレイヤー](../PHYレイヤー.md)「クラスターの組み替え」
|
||||||
|
- 未決事項: reframeの操作系。クラスターへのフォーカス状態に紐づけて原点ハンドルと通常のハンドルを分ける案と、専用モードを設ける案がある。
|
||||||
|
- 保留できる理由: データ変換は定義済みで、通常の移動・回転とは別の明示的な操作として試作できる。
|
||||||
|
- 再開条件: クラスター編集を試作し、操作の分かりやすさを評価するとき。
|
||||||
|
- 完了条件: 配下が追随する操作と絶対配置を保つ操作を区別できる操作系をユーザーが決める。
|
||||||
|
|
||||||
|
### PHY-P-14 クラスター原点の吸着先
|
||||||
|
|
||||||
|
- 関連仕様: [PHYレイヤー](../PHYレイヤー.md)「クラスターの組み替え」
|
||||||
|
- 未決事項: 吸着が必要か。候補はキーのローカル原点(左上角)・キー中心・境界矩形の角と中心で、相対グリッドとは別の機構になる。
|
||||||
|
- 保留できる理由: 数値入力だけで原点移動は成立する。
|
||||||
|
- 再開条件: 原点移動を試作し、操作性を評価するとき。
|
||||||
|
- 完了条件: 吸着の要否と、必要な場合の候補を決める。
|
||||||
|
|
||||||
|
## ELC
|
||||||
|
|
||||||
|
### ELC-P-1 光学スイッチの端子構成
|
||||||
|
|
||||||
|
- 関連仕様: [ELCレイヤー](../ELCレイヤー.md)「キーの端子」
|
||||||
|
- 未決事項: 発光側と受光側が独立した端子を持つのか、接点と同じ2端子として扱えるのか。
|
||||||
|
- 保留できる理由: 光学スイッチを対応範囲から外したまま、接点・アナログの配線を扱える。
|
||||||
|
- 再開条件: 光学スイッチの配線に対応するとき。
|
||||||
|
- 完了条件: 実在のPCBの回路図で端子数と配線の形を確認し、必要な端子構成を定める。
|
||||||
|
|
||||||
|
### ELC-P-2 複数マルチプレクサ構成の作り
|
||||||
|
|
||||||
|
- 関連仕様: [ELCレイヤー](../ELCレイヤー.md)「ピンとピン提供者」
|
||||||
|
- 未決事項: 選択ピンを共有して共通ピンを分ける構成と、enableで切り替える構成の実配線・制御方法。
|
||||||
|
どちらが一般的かは未確認。前者は複数providerの `select` に同じ `PinRef` を書く形で表現できる。
|
||||||
|
- 保留できる理由: providerごとの参照を保存する形は維持でき、複数muxの走査出力を検証済みの構成に限定できる。
|
||||||
|
- 再開条件: 複数muxを使う構成のファームウェア出力に対応するとき。
|
||||||
|
- 完了条件: 実在のキーボードの配線でピン共有とenableの使い方を確認し、現行モデルでの表現と必要な制御を確かめる。
|
||||||
|
|
||||||
|
### ELC-P-3 方式ごとの走査設定の確認
|
||||||
|
|
||||||
|
- 関連仕様: [ELCレイヤー](../ELCレイヤー.md)「配線トポロジを区分として持たない」
|
||||||
|
- 未決事項: 各方式で出力器が決められる走査方法と、ユーザーの追加指定が必要になる範囲。
|
||||||
|
接点マトリクスではダイオードの向き、directでは電源レールの参照から役割を判断できる範囲を確認する。
|
||||||
|
- 保留できる理由: 走査方向の決定・追加指定の責任は出力器側と決まり、ELCの配線保存はそのまま進められる。
|
||||||
|
- 再開条件: 各方式のファームウェア出力に着手するとき。
|
||||||
|
- 完了条件: 対象方式の配線と整合する走査方法を確認し、必要な出力設定と対応範囲を定める。
|
||||||
|
全方式で走査手順が一意に決まることの証明は求めない。
|
||||||
|
|
||||||
|
## IDX
|
||||||
|
|
||||||
|
### IDX-P-1 単一のorderから既存コミュニティレイアウトの並びを得られる範囲
|
||||||
|
|
||||||
|
- 関連仕様: [IDXレイヤー](../IDXレイヤー.md)「レイアウトバリアント」
|
||||||
|
- 未決事項: 採用済みの方式で既存コミュニティレイアウトの要求順を再現できる範囲の実測。
|
||||||
|
同じPHYキーIDを使う共通キー同士の要求順がレイアウト間で逆転する場合は、単一の `order` からの取捨では表せないが、その実例の有無は未確認。
|
||||||
|
- 保留できる理由: 全バリアントのキーを別々の要素として並べ、選択後に番号を詰める仕様は決定済み。
|
||||||
|
既存レイアウト全般との互換性を前提にせず実装できる。
|
||||||
|
- 再開条件: 既存コミュニティレイアウトとの互換出力を検証するとき。
|
||||||
|
- 完了条件: 実データのキーを共通キーとバリアント別のPHYキーに対応づけ、全体の `order` から各レイアウトの要求順を取捨で再現できるか確認する。
|
||||||
|
再現できないものが残る場合は、対応範囲と扱いを決める。
|
||||||
|
|
||||||
|
## MAP
|
||||||
|
|
||||||
|
### MAP-P-4 ON/OFF以外の入力への割り当て
|
||||||
|
|
||||||
|
- 関連仕様: [MAPレイヤー](../MAPレイヤー.md)「MAPデータと他レイヤーの関係」、[PHYレイヤー](../PHYレイヤー.md)「部品ノード」
|
||||||
|
- 未決事項: ロータリーエンコーダの回転のように、ON/OFFのキーではない入力へ割り当てる方法。
|
||||||
|
MAPはPHYのキーIDを参照するが、エンコーダはPHYで部品ノードとして置かれる。
|
||||||
|
参照先の種類、入力ごとの割り当て先(回転方向など)、VIAでの読み書きの対象にするかが未定。
|
||||||
|
アナログ値の解釈の所属は[全体方針のP-1](../keyboard-design-layers.md#p-1-アナログ値の解釈をどの層が持つか)で扱い、この項目とは分ける。
|
||||||
|
- 保留できる理由: キーへの割り当てと実機との読み書きは、キー以外の入力を扱わなくても実装できる。
|
||||||
|
- 再開条件: エンコーダ等を載せたキーボードの割り当てをMAPで扱うとき。
|
||||||
|
- 完了条件: 対象とする入力の種類と、MAPからの参照方法・保存形式をユーザーが決め、VIAで読み書きする場合は必要なコマンドを確認する。
|
||||||
|
|
||||||
|
### MAP-P-5 キーコードを表示するOS配列
|
||||||
|
|
||||||
|
- 関連仕様: [MAPレイヤー](../MAPレイヤー.md)「持たないもの」、[HSTレイヤー](../HSTレイヤー.md)
|
||||||
|
- 未決事項: MAPエディタがキーコードをどのOS配列の文字で表示するか。HSTのどのプロファイルを使うか。
|
||||||
|
少なくともUS/JISの切り替えは欲しい。利用者が増えれば他言語への対応も求められる。
|
||||||
|
- 保留できる理由: キーコードの割り当てと実機との読み書きは、表示に使うOS配列と独立して実装できる。
|
||||||
|
- 再開条件: MAPエディタの表示を実装するとき、またはHSTの[P-2](../HSTレイヤー.md#p-2-対象ホストを複数持つか)を決めるとき。
|
||||||
|
- 完了条件: 表示に使うHSTプロファイルの指定方法と置き場所をユーザーが決める。
|
||||||
|
|
||||||
|
### MAP-P-6 QMKのdevelopブランチ・Community Modulesへの対応
|
||||||
|
|
||||||
|
- 関連仕様: [MAPレイヤー](../MAPレイヤー.md)「DynamicとStatic」
|
||||||
|
- 未決事項: 当面非対応としたdevelopブランチの機能とCommunity Modules(VIAモジュール以外)を、DynamicとStaticの対象に含めるか。
|
||||||
|
含める場合、developブランチのコミットの指定方法と、UIでの対応バージョンの示し方。
|
||||||
|
- 保留できる理由: QMK本家masterの公式機能だけで、Dynamicの読み書きとStaticの生成を実装できる。
|
||||||
|
- 再開条件: masterの機能だけでは足りない動作や設定が必要になったとき。
|
||||||
|
または、VIAのモジュール移行([qmk_firmware#26315](https://github.com/qmk/qmk_firmware/pull/26315))がdevelopに入ったとき。
|
||||||
|
VIAモジュールへの対応はmasterに入る前に済ませる([MAPレイヤー「DynamicとStatic」](../MAPレイヤー.md#dynamicとstatic))。
|
||||||
|
- 完了条件: 対象に含める範囲と、対応バージョンの管理方法をユーザーが決める。
|
||||||
|
|
||||||
|
### MAP-P-7 再ビルド時に実機の調整を失わないワークフロー
|
||||||
|
|
||||||
|
- 関連仕様: [MAPレイヤー](../MAPレイヤー.md)「DynamicとStatic」、[MAPのP-3](../MAPレイヤー.md#p-3-プロジェクトと実機の対応状態管理)
|
||||||
|
- 未決事項: QMK本家のVIAは、実機に保存したVIAのデータ(キーマップ・マクロ・layout option)を、ビルド日時をもとにした値で検証し、一致しなければ初期化する(`quantum/via.c` の `via_eeprom_is_valid()`。実機では未検証)。
|
||||||
|
再ビルドしたファームウェアを書き込む前に、実機の調整をプロジェクトへ読み出す手順を、ワークフローでどう保証するか。警告、読み出しの強制、Firmware Generatorとの連携などが候補。
|
||||||
|
- 保留できる理由: 実機からの読み出しとStaticの初期キーマップへの反映はすでに仕様にある。手作業の手順で運用できる。
|
||||||
|
- 再開条件: Firmware Generatorでの書き込みや、プロジェクトと実機の状態管理(P-3)を設計するとき。
|
||||||
|
- 完了条件: 再ビルドの前に実機の調整を取り込めるワークフローをユーザーが決め、書き込み後にVIAのデータが初期化されることを実機で確認する。
|
||||||
|
|
||||||
|
### MAP-P-8 QMK以外のファームウェアでのキーコード表記
|
||||||
|
|
||||||
|
- 関連仕様: [MAPレイヤー](../MAPレイヤー.md)「MAPデータと他レイヤーの関係」、[全体方針のFirmware Generator](../keyboard-design-layers.md#firmware-generator)
|
||||||
|
- 未決事項: MAPはQMKのキーコード表記を基本とする。KMK等のQMK以外のファームウェアへ出力するとき、QMK表記から変換するか、共通の表記を別に設けるか。
|
||||||
|
- 保留できる理由: 当面の対象ファームウェアはQMKだけで、QMK表記のまま保存・読み書き・出力できる。
|
||||||
|
- 再開条件: QMK以外のファームウェアへの出力や、実機との通信に着手するとき。
|
||||||
|
- 完了条件: QMK以外のファームウェアで使うキーコードの表現と、QMK表記との対応方法をユーザーが決める。
|
||||||
|
|
||||||
|
### MAP-P-9 Cコードの生成とユーザーのCの組み込み
|
||||||
|
|
||||||
|
- 関連仕様: [MAPレイヤー](../MAPレイヤー.md)「MAPデータと他レイヤーの関係」「DynamicとStatic」、[全体方針のFirmware Generator](../keyboard-design-layers.md#firmware-generator)
|
||||||
|
- 未決事項: QMKのkeymap.jsonでは、タップダンス・コンボ・キーオーバーライドや、キーごとの設定(`get_tapping_term` 等のコールバック)を表現できない。
|
||||||
|
JSONを正本にしたままこれらをCとして生成する方法と、ユーザーが用意したC/hで一部を上書きする方法。
|
||||||
|
QMKのコールバックは1つの関数として定義するため、生成したCとユーザーのCが同じ関数を定義するとビルドできない。候補:
|
||||||
|
- 生成した関数の先頭でユーザー用のフックを呼び、フックが処理しなかった場合はJSONの値を使う(有力)
|
||||||
|
- ユーザーが引き受ける関数をJSONまたはhのプロトタイプで宣言し、Generatorはその関数を出力しない
|
||||||
|
`ACTION_TAP_DANCE_FN*` やリーダーキーのように、本質的にCで書く動作をJSONから参照する方法も含む。
|
||||||
|
MAPの「キーマップ固有のコードには原則として対応しない」との関係も整理する。
|
||||||
|
- 保留できる理由: レイヤー別の割り当てとVIAでの読み書きは、keymap.jsonで表現できる範囲だけで実装できる。
|
||||||
|
- 再開条件: Firmware Generatorでタップダンス・コンボ・キーごとの設定等を出力するとき、またはP-1で動作定義とキーごとの設定の保存形式を決めるとき。
|
||||||
|
- 完了条件: 生成するCの構成と、ユーザーのC/hを組み込む方法をユーザーが決め、両方を含む構成でQMKのビルドが通ることを確認する。
|
||||||
+45
-18
@@ -13,16 +13,41 @@ PHYの正本は相対座標の木だが、既存エディタ(KLE, kle-ng)の移
|
|||||||
|
|
||||||
### テストケース
|
### テストケース
|
||||||
|
|
||||||
1. **60% ANSI** - 標準的なレイアウト
|
入力はKLE JSONとし、取り込みを通してPHYへ変換する。
|
||||||
2. **Split keyboard** - 左右分離型
|
|
||||||
3. **Ergonomic layout** - カラムスタッガード + 回転
|
| ケース | サンプル | 主に確かめること |
|
||||||
4. **Macro pad** - シンプルな4x4
|
|---|---|---|
|
||||||
|
| 60% | Cr0wn60 | 行スタッガード、幅の違うキー、左右対称配列の中央の隙間 |
|
||||||
|
| 分割 | crkbd、jisplit89 | 左右の分離、カラムスタッガードと回転した親指キー(crkbd)、キー数の多いJIS配列の分割(jisplit89) |
|
||||||
|
| 分割 + バリアント | Kyria、Iris Rev.7 | 回転したクラスター内のバリアント(Kyria)、部品(エンコーダ)を含む3択のバリアント(Iris Rev.7) |
|
||||||
|
| マクロパッド | 4x4 | 最小構成 |
|
||||||
|
| charlieplexing | Cr0wn60(VIA定義) | 行と列が同じピンを共有する配線を、ELCの接続先で表せるか |
|
||||||
|
|
||||||
|
リポジトリに置くサンプルは `samples/` にある。
|
||||||
|
|
||||||
|
- `kle/cr0wn60.json`: [EwdErna/Cr0wn60](https://github.com/EwdErna/Cr0wn60) の `keyboard-layout.json`(919cc9e、MIT)
|
||||||
|
- `via/cr0wn60.json`: 同リポジトリの `cr0wn60-via.json`。ラベルにマトリクス位置を持つ
|
||||||
|
- `kle/macropad-4x4.json`: このリポジトリで作成
|
||||||
|
|
||||||
|
Cr0wn60はcharlieplexingで、行と列に同じ9本のピンを割り当て、使わない位置をマスクしている。
|
||||||
|
ピンの割り当ては、ファームウェア [EwdErna/qmk_firmware](https://github.com/EwdErna/qmk_firmware) の
|
||||||
|
`dev/ewderna_22` ブランチ(68d9249)の `keyboards/ewderna/cr0wn/cr0wn60/` にある
|
||||||
|
(`info.json` の `matrix_pins`、`cr0wn60.c` の `matrix_mask`)。
|
||||||
|
|
||||||
|
その他はコピーせず、[the-via/keyboards](https://github.com/the-via/keyboards)(GPL-3.0)から、コミット 9e3e9f4 の次のファイルの `layouts.keymap` を取得して使う。
|
||||||
|
|
||||||
|
- crkbd: `src/crkbd/crkbd.json`
|
||||||
|
- jisplit89: `src/jisplit89/jisplit89.json`
|
||||||
|
- Kyria: `src/splitkb/kyria.json`
|
||||||
|
- Iris Rev.7: `src/keebio/iris/iris-rev7.json`
|
||||||
|
|
||||||
|
VIAの定義に含まれるKLE JSONは、ラベルにキー名ではなくマトリクス位置(`行,列`)を持つ。
|
||||||
|
バリアントは、ラベルの別の欄に `選択肢の番号,候補の番号` の形で示す。
|
||||||
|
キー名のラベルによる照合の検証には使えない。ELCの接続先とバリアントの検証に使う。
|
||||||
|
|
||||||
### データ例
|
### データ例
|
||||||
|
|
||||||
以下は旧形式(`keys` 配列、`position` / `size.width`)で書かれており、現行のPHYスキーマ(`phase1-json-schema.md`)に合わせた変換が必要。
|
#### 60% ANSI PHY Layer (抜粋)
|
||||||
|
|
||||||
#### 60% ANSI PHY Layer
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -38,20 +63,22 @@ PHYの正本は相対座標の木だが、既存エディタ(KLE, kle-ng)の移
|
|||||||
"unitSize": { "x": 19.05, "y": 19.05 }
|
"unitSize": { "x": 19.05, "y": 19.05 }
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"keys": [
|
"root": [
|
||||||
// Escape key
|
|
||||||
{
|
{
|
||||||
"id": "k00", "profile": "standard",
|
"type": "key",
|
||||||
"position": { "x": 0, "y": 0 },
|
"id": "k00",
|
||||||
"size": { "width": 1, "height": 1 },
|
"profile": "standard",
|
||||||
"metadata": { "label": "Esc" }
|
"transform": { "x": 0, "y": 0, "rotation": 0 },
|
||||||
|
"size": { "w": 1, "h": 1 },
|
||||||
|
"labels": ["Esc"]
|
||||||
},
|
},
|
||||||
// Space bar
|
|
||||||
{
|
{
|
||||||
"id": "k60", "profile": "standard",
|
"type": "key",
|
||||||
"position": { "x": 5, "y": 4 },
|
"id": "k60",
|
||||||
"size": { "width": 6.25, "height": 1 },
|
"profile": "standard",
|
||||||
"metadata": { "label": "Space" }
|
"transform": { "x": 5, "y": 4, "rotation": 0 },
|
||||||
|
"size": { "w": 6.25, "h": 1 },
|
||||||
|
"labels": ["Space"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# コミュニティレイアウトの引数順がキーの取捨だけで得られるか
|
||||||
|
|
||||||
|
IDX P-1 の検証。
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
得られない場合がある。ISO系のコミュニティレイアウトで、`LAYOUT_all` の並びからキーを落としただけでは
|
||||||
|
引数順が合わない。QMKの実データで 173組中30組。全部ISO系だった。
|
||||||
|
|
||||||
|
原因は、同じマトリクス位置が、レイアウトによって別の物理キーになるため。
|
||||||
|
|
||||||
|
| レイアウト | matrix (2,12) の位置とサイズ |
|
||||||
|
| --- | --- |
|
||||||
|
| `LAYOUT_all` | (13.5, 1) 1.5U — ANSIのバックスラッシュ |
|
||||||
|
| `LAYOUT_60_iso` | (12.75, 2) 1U — ISOの `#~` |
|
||||||
|
|
||||||
|
キーが物理的に別の場所へ移るので、読み順での位置も変わる。`LAYOUT_all` では2行目に、
|
||||||
|
ISOでは3行目に現れる。取捨では並べ替えられない。
|
||||||
|
|
||||||
|
PHYの用語では、この2つは別のキーノードである(サイズや形が変わるキーは別のIDを持つ)。
|
||||||
|
「選んだバリアントに属さないキーを落とす」だけでは、残ったキーの並びが変わらないので、
|
||||||
|
コミュニティレイアウトが要求する順にならない。
|
||||||
|
|
||||||
|
## 調べ方
|
||||||
|
|
||||||
|
QMKのキーボード一覧3759件から400件を無作為抽出し、`keyboards.qmk.fm` の info.json を取得した。
|
||||||
|
`LAYOUT_all`(無ければ `LAYOUT`)を基準に、同じボードの他のレイアウトと比較した。
|
||||||
|
|
||||||
|
| 対象 | 件数 |
|
||||||
|
| --- | --- |
|
||||||
|
| 取得したボード | 400 |
|
||||||
|
| 複数のレイアウトを持つボード | 146 |
|
||||||
|
| community_layouts に載っているレイアウトとの組 | 173 |
|
||||||
|
| うち基準の並びの部分列になっている | 143 |
|
||||||
|
| うち部分列にならない | 30 (すべてISO系) |
|
||||||
|
| うち同じマトリクス位置で形状が違うキーを含む | 172 |
|
||||||
|
|
||||||
|
部分列にならない例: alf/dc60, argyle, artifact/stratum, cannonkeys/brutalv2_65, era/linx3/n87,
|
||||||
|
kapcave/gskt00, karlb/kbic65, kbdfans/kbd67/rev2, lucid/phantom_solder, phdesign/ph60/multi,
|
||||||
|
rmi_kb/tkl_ff/v2 の各ISOレイアウト。
|
||||||
|
|
||||||
|
era/linx3/n87 は `LAYOUT_tkl_ansi` に `LAYOUT_all` へ無いマトリクス位置が1つある。
|
||||||
|
QMKの `LAYOUT_all` は全バリアントの重ね合わせではなく、それ自体が1つの選択でしかない
|
||||||
|
(1つのマトリクス位置を2回書けないため)。PHYが全バリアントを同時に持つのとは別物である。
|
||||||
|
|
||||||
|
## 172/173 が「形状違い」を含むことについて
|
||||||
|
|
||||||
|
ANSI系のレイアウトでも、Backspaceが2Uか1U×2か、右Shiftの幅、Tsanganの下段など、
|
||||||
|
同じマトリクス位置のキーの形が変わる組がほとんどである。これらは並び順は保たれるが、
|
||||||
|
PHY上は別のキーノードになる。IDXが「キーIDの並び」で番号を持つ以上、
|
||||||
|
落とすだけでなく差し替えが要ることは変わらない。
|
||||||
|
|
||||||
|
## 検証スクリプト
|
||||||
|
|
||||||
|
```py
|
||||||
|
# QMKのinfo.jsonを無作為抽出で取得する。qmk_list.json は
|
||||||
|
# curl -o qmk_list.json https://keyboards.qmk.fm/v1/keyboard_list.json
|
||||||
|
import json, random, urllib.request, concurrent.futures, os
|
||||||
|
kbs=json.load(open('qmk_list.json'))['keyboards']
|
||||||
|
random.seed(42)
|
||||||
|
os.makedirs('qmk', exist_ok=True)
|
||||||
|
def fetch(kb):
|
||||||
|
p='qmk/'+kb.replace('/','__')+'.json'
|
||||||
|
if os.path.exists(p): return True
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(f'https://keyboards.qmk.fm/v1/keyboards/{kb}/info.json', timeout=30) as r:
|
||||||
|
open(p,'wb').write(r.read())
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
return str(e)[:60]
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(8) as ex:
|
||||||
|
res=list(ex.map(fetch, random.sample(kbs, 400)))
|
||||||
|
print('fetched', sum(1 for r in res if r is True), 'failed', sum(1 for r in res if r is not True))
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
# LAYOUT_all の並びから、他のレイアウトがキーの取捨だけで得られるか
|
||||||
|
import json, glob, collections
|
||||||
|
st=collections.Counter(); rows=[]
|
||||||
|
for f in glob.glob('qmk/*.json'):
|
||||||
|
d=json.load(open(f,encoding='utf-8'))
|
||||||
|
for kb,v in (d.get('keyboards') or {}).items():
|
||||||
|
lays=v.get('layouts') or {}
|
||||||
|
if not lays: continue
|
||||||
|
st['boards']+=1
|
||||||
|
if len(lays)>1: st['multi-layout boards']+=1
|
||||||
|
base=next((n for n in ('LAYOUT_all','LAYOUT') if n in lays), None)
|
||||||
|
if base is None or len(lays)<2: continue
|
||||||
|
shape=lambda k:(k.get('x'),k.get('y'),k.get('w',1),k.get('h',1),k.get('r',0))
|
||||||
|
shA={}
|
||||||
|
for k in lays[base]['layout']: shA.setdefault(tuple(k['matrix']),[]).append(shape(k))
|
||||||
|
A=[tuple(k['matrix']) for k in lays[base]['layout']]
|
||||||
|
for name,l in lays.items():
|
||||||
|
if name==base: continue
|
||||||
|
B=[tuple(k['matrix']) for k in l['layout']]
|
||||||
|
it=iter(A); subseq=all(any(a==b for a in it) for b in B) # 部分列か
|
||||||
|
rows.append(dict(kb=kb, name=name, subseq=subseq,
|
||||||
|
missing=sum(1 for m in B if m not in set(A)),
|
||||||
|
shapediff=sum(1 for k in l['layout']
|
||||||
|
if tuple(k['matrix']) in shA and shape(k) not in shA[tuple(k['matrix'])]),
|
||||||
|
community=name.replace('LAYOUT_','') in (v.get('community_layouts') or [])))
|
||||||
|
print(st)
|
||||||
|
comm=[r for r in rows if r['community']]
|
||||||
|
print('community layout pairs:', len(comm))
|
||||||
|
print(' 部分列になる:', sum(1 for r in comm if r['subseq']))
|
||||||
|
print(' 部分列にならない:', sum(1 for r in comm if not r['subseq']))
|
||||||
|
print(' baseに無いキーを含む:', sum(1 for r in comm if r['missing']))
|
||||||
|
print(' 同じmatrixで形状が違うキーを含む:', sum(1 for r in comm if r['shapediff']))
|
||||||
|
for r in comm:
|
||||||
|
if not r['subseq']: print(' NG', r['kb'], r['name'])
|
||||||
|
```
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# IDXレイヤーは必要か (schema P-1解消)
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
必要。ただし「最も重要な抽象化層」「エコシステムで最も欠けている層」という位置づけは誤りで、
|
||||||
|
ドキュメントから落とす。
|
||||||
|
|
||||||
|
IDXの中身は「どのキーに、どの順で番号を振るか」という順列だけ。情報ではなく決めごとで、
|
||||||
|
PHYからもELCからも導出できない。導出できないから正本として置く。
|
||||||
|
|
||||||
|
あわせて2点決めた。
|
||||||
|
|
||||||
|
- MAPはキー番号ではなくPHYのキーIDを参照する
|
||||||
|
- IDXの保存形式は `order: string[]` (並びの位置が番号)
|
||||||
|
|
||||||
|
## 発端
|
||||||
|
|
||||||
|
ユーザーの指摘: 「PHYのidやELCの接続がそのままキー番号になってしまうのでは?
|
||||||
|
わざわざIDXを振る意味が薄れてきている」。
|
||||||
|
|
||||||
|
薄く見える原因は特定できた。IDXが持つと思われていたものが順次出ていったため。
|
||||||
|
|
||||||
|
| 元々IDXが持つとされたもの | 現在の置き場 | コミット |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| row / col | ELCの配線から出力時に導出 | 5bba75d |
|
||||||
|
| LEDの識別番号 | ELC (チェーン順序) | eb96ff0 |
|
||||||
|
|
||||||
|
引き算の結果、キー番号との対応だけが残った。薄いという認識自体は正しい。
|
||||||
|
|
||||||
|
## 案の比較
|
||||||
|
|
||||||
|
| 案 | キー番号の出どころ | 判定 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| IDXが正本として持つ | 設計者が決める並び順 | 採用 |
|
||||||
|
| PHYのツリー順 | 幾何の編集順 | reparentで番号が動きキーマップがずれる |
|
||||||
|
| ELCのマトリクス順 | row/col | 分割で並びが一致しない。欠番がある。再配線で動く |
|
||||||
|
| そもそも番号を持たない | (MAPがPHYのキーIDを参照) | 公開インターフェースが固定できない |
|
||||||
|
|
||||||
|
最後の案が一番惜しかった。row/colを出力時に割り当てる判断と揃えるなら、キー番号も
|
||||||
|
Firmware Generatorが生成する派生情報にできそうに見える。しかし番号が再生成のたびに動くと、
|
||||||
|
公開した基板に対して他人が書いたキーマップが別のキーを指す。公開インターフェースは固定が要る。
|
||||||
|
|
||||||
|
結果として、MAPの参照だけをPHYのキーIDに移し、番号の並びはIDXに残す形にした。
|
||||||
|
番号がずれてもMAPは無傷で、番号は固定される。
|
||||||
|
|
||||||
|
## 根拠: 3つの処理系はどれもこの順列を手書きしている
|
||||||
|
|
||||||
|
- QMK: `LAYOUT` マクロの引数順 / `keyboard.json` の `layouts.<NAME>.layout[]` の配列順
|
||||||
|
- ZMK: matrix transform の `map` の並び順
|
||||||
|
- KMK: `coord_mapping` の並び順
|
||||||
|
|
||||||
|
QMK Configuratorの説明:
|
||||||
|
> The Configurator's API uses the layout macro and the JSON file we've given it to create a visual
|
||||||
|
> representation of the keyboard that has each visual object tied to a specific key, in sequence.
|
||||||
|
> When a user selects the top-left key in the Configurator, and assigns Num Lock to it, the
|
||||||
|
> Configurator builds a keymap file with `KC_NUM` as the first key, and so on as the keymap is built.
|
||||||
|
|
||||||
|
> The names themselves actually don't matter, as long as they match between the top section, which
|
||||||
|
> receives the keycodes from the keymap, and the bottom half which designates where each key is in the matrix.
|
||||||
|
|
||||||
|
ZMKの matrix transform `map`:
|
||||||
|
> It should have one item per logical position in the keymap. Each item should list the physical row
|
||||||
|
> and column that trigger the key in that position.
|
||||||
|
|
||||||
|
つまりIDXは「欠けている層」ではない。Cのマクロの引数順として存在していて、形式化されていないだけ。
|
||||||
|
引数の数え間違いでキーが1つずれるのが定番のバグであることも、この層が手書きである傍証になる。
|
||||||
|
|
||||||
|
出典:
|
||||||
|
- https://docs.qmk.fm/reference_configurator_support
|
||||||
|
- https://docs.qmk.fm/reference_info_json
|
||||||
|
- https://zmk.dev/docs/config/layout
|
||||||
|
- https://github.com/KMKfw/kmk_firmware/blob/main/docs/en/porting_to_kmk.md
|
||||||
|
|
||||||
|
## 根拠: マトリクス順 ≠ キー番号順 の実物
|
||||||
|
|
||||||
|
KMKのCorne向け `coord_mapping`。
|
||||||
|
|
||||||
|
```python
|
||||||
|
coord_mapping = [
|
||||||
|
0, 1, 2, 3, 4, 5, 24, 25, 26, 27, 28, 29,
|
||||||
|
6, 7, 8, 9, 10, 11, 30, 31, 32, 33, 34, 35,
|
||||||
|
12, 13, 14, 15, 16, 17, 36, 37, 38, 39, 40, 41,
|
||||||
|
21, 22, 23, 42, 43, 44,
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
配列の位置がキー番号、値がマトリクス上の通し番号。左半身が0〜23、右半身が24〜47。
|
||||||
|
キー番号の並びは視覚的な行ごとに左右交互で、マトリクス順とは一致しない。18〜20は欠番。
|
||||||
|
1つの例で「並びが違う」と「穴がある」の両方が出ている。
|
||||||
|
|
||||||
|
KMKの説明:
|
||||||
|
> Split keyboards count as the total keyboard, not per side, the right side being offset by the
|
||||||
|
> number of keys on the left side, as if the rows were stacked.
|
||||||
|
|
||||||
|
QMKの分割も同じ構造。
|
||||||
|
> Since the matrix scanning procedure operates on entire rows, it first populates the left half's rows,
|
||||||
|
> then the right half's. Thus, the matrix as QMK views it has double the rows instead of double the columns.
|
||||||
|
|
||||||
|
出典: https://docs.qmk.fm/features/split_keyboard
|
||||||
|
|
||||||
|
## 保存形式を `order: string[]` にした理由
|
||||||
|
|
||||||
|
旧案 `keyMap: [{keyId, index}]` は、欠番・重複・非連続をスキーマが許してしまう。
|
||||||
|
`{k-a, 0}, {k-b, 0}` が通る。
|
||||||
|
|
||||||
|
ELCの `LEDChain.order` で既に「並びの位置が番号」の形を採っている (eb96ff0)。同じ形に揃えた。
|
||||||
|
不変条件が構造として保証され、バリデーションが不要になる。
|
||||||
|
|
||||||
|
## 捨てた仮説
|
||||||
|
|
||||||
|
- 「IDXはdecal/ghostの区別を持つべきではないか」。decalはPHYが `decal` フラグを持っているので
|
||||||
|
そこから分かる。ghostは何を意味するかがKLE由来で曖昧なので、IDXの側で規定しない。
|
||||||
|
`order` に挙げたキーだけが番号を持つ、で足りる
|
||||||
|
- 「VIAとの往復でキー番号が要るのでは」。VIAのプロトコルはrow/col指定なので、キー番号は出てこない。
|
||||||
|
ただし裏取りしていないので、ドキュメントには書かなかった
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
# 公開KLE JSONの収集とPHYへの変換可否
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
公開されているKLE JSON 1505件(キー111,619個)を集め、PHYへ変換して書き戻す往復を通した。
|
||||||
|
|
||||||
|
- 幾何(位置・サイズ・回転・2つ目の矩形・decal・ghost)は全件で無損失。PHYのデータモデルで表せないKLEの
|
||||||
|
幾何情報は無かった
|
||||||
|
- 置き場がないのは見た目の属性だけ(c, t, p, a, f, f2, fa, n, l と sm, sb, st、ファイル単位の
|
||||||
|
backcolor, radii, background, css)。検証では退避用のフィールドに逃がした。PHY P-6 の対象そのもの
|
||||||
|
- KLE本家のデシリアライザが読めないJSONが実在する(1505件中1件)
|
||||||
|
- `l`(stepped)が付いたキーが16個あった。KLEはstepped Caps Lockに「段差がある」ことしか持たず、
|
||||||
|
スイッチ位置のオフセットは持たない。PHYの `switchOffset` は取り込みでは決まらない
|
||||||
|
|
||||||
|
## コーパス
|
||||||
|
|
||||||
|
| 出所 | 件数 | 備考 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| KLE本家のプリセット (`layouts.json`) | 12 | ANSI 104, ISO 105, ErgoDox, Kinesis Advantage, Atreus など |
|
||||||
|
| KLE本家のサンプル (`samples/`) | 9 | Symbolics Space Cadet, Apple Wireless, Commodore VIC-20 など |
|
||||||
|
| VIA キーボード定義 (the-via/keyboards の `layouts.keymap`) | 1484 | 市販・自作基板の実データ |
|
||||||
|
|
||||||
|
Geekhackやgistは当たっていない。VIAの定義はKLE JSONそのもの(`layouts.keymap`)で、
|
||||||
|
実際に流通している基板をほぼ網羅しているため、これで足りると判断した。
|
||||||
|
|
||||||
|
## 検証方法
|
||||||
|
|
||||||
|
KLE本家のリポジトリ (ijprest/keyboard-layout-editor) から `serial.js` と `extensions.js` を
|
||||||
|
そのままNodeで読み込み、本家の `deserialize` / `serialize` を使った。自前の再実装ではない。
|
||||||
|
|
||||||
|
1. 元データ → `deserialize` → キー配列A
|
||||||
|
2. A → PHY(root直下に平坦なKey木、絶対座標を `Translate·Rotate` に分解)
|
||||||
|
3. PHY → KLEのキー配列 → 本家 `serialize` → `deserialize` → キー配列B
|
||||||
|
4. AとBを、絶対位置・角度・サイズ・ラベル・見た目属性のフィンガープリントの多重集合で比較
|
||||||
|
|
||||||
|
比較対象は「Aをそのまま本家で往復させた結果」とした(本家自身の往復の癖を分離するため)。
|
||||||
|
|
||||||
|
## 結果
|
||||||
|
|
||||||
|
- 1504/1504 で配置が一致(1件は手順1で本家が例外を投げるため対象外)
|
||||||
|
- 直列化結果そのものが本家の出力と完全一致: 1408件。小数4桁に丸めると一致(誤差のみ): 20件。
|
||||||
|
残り76件は行の切り方が違うだけで、読み直した配置は一致する
|
||||||
|
|
||||||
|
出現頻度(キー単位、111,619個中):
|
||||||
|
|
||||||
|
| 属性 | 件数 |
|
||||||
|
| --- | --- |
|
||||||
|
| c (色) | 35,888 |
|
||||||
|
| 0.25U刻みでない座標・サイズ | 6,824 |
|
||||||
|
| 回転あり | 4,583 (うち15度の倍数でない 4,155) |
|
||||||
|
| rx/ry あり | 2,567 |
|
||||||
|
| f/f2/fa | 893 |
|
||||||
|
| d (decal) | 830 |
|
||||||
|
| 2つ目の矩形 (x2,y2,w2,h2) | 570 |
|
||||||
|
| sm/sb/st | 142 |
|
||||||
|
| t (文字色) | 61 |
|
||||||
|
| n (ホームポジションの突起) | 52 |
|
||||||
|
| l (stepped) | 16 |
|
||||||
|
| g (ghost) | 8 |
|
||||||
|
|
||||||
|
ファイル単位のメタデータは name 21件、backcolor 7件、author 2件、radii 2件、
|
||||||
|
background/switchMount/switchBrand/switchType 各1件。
|
||||||
|
|
||||||
|
座標・サイズ・回転は正規化されていない値が普通に出てくる。PHYが「スナップするが正規化しない」と
|
||||||
|
決めているのと整合する。
|
||||||
|
|
||||||
|
## 本家が読めないJSON
|
||||||
|
|
||||||
|
`owlab/spring/spring.json` の `layouts.keymap` は `{"r":12,"y":-0.8,"x":0.1}` を行の先頭以外に置いており、
|
||||||
|
本家の `deserialize` は `'r' can only be used on the first key in a row` で例外を投げる。
|
||||||
|
VIA側のパーサーは通してしまうため、このまま公開されている。
|
||||||
|
|
||||||
|
取り込みでこれをどう扱うか(本家と同じく拒否するか、行の途中の r を受け入れるか)は未定。
|
||||||
|
PHY の未確認事項に置いた。
|
||||||
|
|
||||||
|
## 見た目属性の扱い
|
||||||
|
|
||||||
|
退避して書き戻す方針は、次の制約とセットでないと成立しない。
|
||||||
|
|
||||||
|
本家の `deserialize` は `p` / `sm` / `sb` / `st` を `if(key.p)` の形で読む。空文字への書き戻しが無視されるため、
|
||||||
|
一度値を持ったキーの後ろに値を持たないキーが来る並びでは、前の値が後続に残る。
|
||||||
|
|
||||||
|
```txt
|
||||||
|
入力: [[{p:'SA',sm:'cherry'},'A',{p:'',sm:''},'B']]
|
||||||
|
結果: A → p=SA sm=cherry / B → p=SA sm=cherry (Bの空文字指定は無視される)
|
||||||
|
```
|
||||||
|
|
||||||
|
実コーパスでは、クラスターの原点を rx,ry に選ぶ出力では1件も発生しなかった。
|
||||||
|
キーごとに rx,ry を選ぶ出力では1件発生した(floookay/adelheid、`p:"SPACE"` が11キーに漏れる)。
|
||||||
|
並び順に依存する事故なので、出力側で回避しきれない。
|
||||||
|
|
||||||
|
## 検証スクリプト
|
||||||
|
|
||||||
|
コーパスと本家のコードを用意する。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone --depth 1 https://github.com/ijprest/keyboard-layout-editor.git kle
|
||||||
|
git clone --depth 1 https://github.com/the-via/keyboards.git via
|
||||||
|
# 本家の serial.js を Node から使えるようにする(angular は toJson しか使われない)
|
||||||
|
{ echo 'global.angular = { toJson: (o) => JSON.stringify(o) };'; cat kle/extensions.js; cat kle/serial.js; } > kleio.js
|
||||||
|
```
|
||||||
|
|
||||||
|
`convert.js` — KLEキーとPHYノードの相互変換と比較用フィンガープリント。
|
||||||
|
|
||||||
|
```js
|
||||||
|
// KLE JSON <-> Beyond KLE PHY の変換(検証用)
|
||||||
|
const $serial = require('./kleio.js');
|
||||||
|
const R = (d) => d * Math.PI / 180;
|
||||||
|
|
||||||
|
// KLE key -> PHY Key (root直下の平坦な木)
|
||||||
|
function kleKeyToPhy(k, i) {
|
||||||
|
const th = R(k.rotation_angle), c = Math.cos(th), s = Math.sin(th);
|
||||||
|
const dx = k.x - k.rotation_x, dy = k.y - k.rotation_y;
|
||||||
|
const node = {
|
||||||
|
id: 'k' + i,
|
||||||
|
transform: {
|
||||||
|
x: k.rotation_x + dx * c - dy * s,
|
||||||
|
y: k.rotation_y + dx * s + dy * c,
|
||||||
|
rotation: k.rotation_angle,
|
||||||
|
},
|
||||||
|
size: { w: k.width, h: k.height },
|
||||||
|
profile: 'default',
|
||||||
|
};
|
||||||
|
if (k.width2 !== k.width || k.height2 !== k.height || k.x2 !== 0 || k.y2 !== 0) {
|
||||||
|
Object.assign(node.size, { x2: k.x2, y2: k.y2, w2: k.width2, h2: k.height2 });
|
||||||
|
}
|
||||||
|
if (k.decal) node.decal = true;
|
||||||
|
if (k.ghost) node.ghost = true;
|
||||||
|
const labels = k.labels.map(x => x === undefined ? '' : x);
|
||||||
|
if (labels.some(x => x !== '')) node.labels = labels;
|
||||||
|
// PHYに置き場のない属性(P-6)。検証用に退避
|
||||||
|
node._kle = { c: k.color, t: k.textColor, p: k.profile, n: k.nub, l: k.stepped,
|
||||||
|
sm: k.sm, sb: k.sb, st: k.st, ts: k.textSize, dflt: k.default };
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PHY Key -> KLE key (rx,ry の選び方は出力側の裁量)
|
||||||
|
function phyToKleKey(n, opts = {}) {
|
||||||
|
const k = $serial.defaultKeyProps();
|
||||||
|
const rot = n.transform.rotation;
|
||||||
|
const [rx, ry] = opts.pivot ? opts.pivot(n) : (rot === 0 ? [0, 0] : [n.transform.x, n.transform.y]);
|
||||||
|
const th = R(rot), c = Math.cos(th), s = Math.sin(th);
|
||||||
|
// 絶対座標 -> KLE の (x,y): 逆回転
|
||||||
|
const dx = n.transform.x - rx, dy = n.transform.y - ry;
|
||||||
|
k.x = rx + dx * c + dy * s;
|
||||||
|
k.y = ry + -dx * s + dy * c;
|
||||||
|
k.rotation_angle = rot; k.rotation_x = rx; k.rotation_y = ry;
|
||||||
|
k.width = n.size.w; k.height = n.size.h;
|
||||||
|
k.width2 = n.size.w2 !== undefined ? n.size.w2 : n.size.w;
|
||||||
|
k.height2 = n.size.h2 !== undefined ? n.size.h2 : n.size.h;
|
||||||
|
k.x2 = n.size.x2 || 0; k.y2 = n.size.y2 || 0;
|
||||||
|
k.decal = !!n.decal; k.ghost = !!n.ghost;
|
||||||
|
k.labels = (n.labels || []).slice();
|
||||||
|
const e = n._kle || {};
|
||||||
|
k.color = e.c !== undefined ? e.c : k.color;
|
||||||
|
k.textColor = e.t || []; k.profile = e.p || ''; k.nub = !!e.n; k.stepped = !!e.l;
|
||||||
|
k.sm = e.sm || ''; k.sb = e.sb || ''; k.st = e.st || '';
|
||||||
|
k.textSize = e.ts || []; if (e.dflt) k.default = e.dflt;
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 比較用: キーの見た目を決める絶対量だけを取り出す
|
||||||
|
function fingerprint(k) {
|
||||||
|
const th = R(k.rotation_angle), c = Math.cos(th), s = Math.sin(th);
|
||||||
|
const dx = k.x - k.rotation_x, dy = k.y - k.rotation_y;
|
||||||
|
const ax = k.rotation_x + dx * c - dy * s, ay = k.rotation_y + dx * s + dy * c;
|
||||||
|
const r6 = (v) => Math.round(v * 1e6) / 1e6;
|
||||||
|
const ang = ((k.rotation_angle % 360) + 360) % 360;
|
||||||
|
return JSON.stringify([r6(ax), r6(ay), r6(ang), k.width, k.height, k.width2, k.height2, k.x2, k.y2,
|
||||||
|
k.decal, k.ghost, k.stepped, k.nub, (k.labels || []).map(x => x || '').join('\u0001'),
|
||||||
|
k.color, k.profile, k.sm, k.sb, k.st,
|
||||||
|
(k.textColor || []).map(x => x || '').join('\u0001'),
|
||||||
|
(k.textSize || []).map(x => x || 0).join('\u0001'), k.default && k.default.textSize]);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { $serial, kleKeyToPhy, phyToKleKey, fingerprint };
|
||||||
|
```
|
||||||
|
|
||||||
|
`run2.js` — コーパス全件の往復。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const fs=require('fs'), path=require('path');
|
||||||
|
const { $serial, kleKeyToPhy, phyToKleKey, fingerprint } = require('./convert.js');
|
||||||
|
const rnd=v=>Math.round(v*1e6)/1e6;
|
||||||
|
function walk(d,out=[]){for(const e of fs.readdirSync(d,{withFileTypes:true})){const p=path.join(d,e.name); e.isDirectory()?walk(p,out):e.name.endsWith('.json')&&out.push(p);}return out;}
|
||||||
|
const corpus=[];
|
||||||
|
for(const p of JSON.parse(fs.readFileSync('kle/layouts.json','utf8')).presets) if(p.data.length) corpus.push({name:'preset:'+p.name,data:p.data});
|
||||||
|
for(const f of fs.readdirSync('kle/samples')) { try{ corpus.push({name:'sample:'+f,data:JSON.parse(fs.readFileSync('kle/samples/'+f,'utf8'))});}catch(e){} }
|
||||||
|
for(const p of walk('via/src')){ try{ const d=JSON.parse(fs.readFileSync(p,'utf8')); if(d.layouts&&Array.isArray(d.layouts.keymap)&&d.layouts.keymap.length) corpus.push({name:'via:'+path.relative('via/src',p),data:d.layouts.keymap}); }catch(e){} }
|
||||||
|
|
||||||
|
let ok=0, sameSer=0, n=0, parseFail=0;
|
||||||
|
let stickyA=0, stickyB=0; const stickyBoards=[];
|
||||||
|
const RESET=/"(p|sm|sb|st|t)":""/;
|
||||||
|
const rowsOf=s=>s.filter(Array.isArray).length;
|
||||||
|
let rowsOrig=0,rowsA=0,rowsB=0, rotBoards=0;
|
||||||
|
for(const b of corpus){
|
||||||
|
let A; try{ A=$serial.deserialize(b.data).keys; }catch(e){ parseFail++; continue; }
|
||||||
|
n++;
|
||||||
|
const orig=$serial.serialize({meta:{},keys:JSON.parse(JSON.stringify(A))});
|
||||||
|
const phy=A.map((k,i)=>{const nd=kleKeyToPhy(k,i); nd._pivot=[k.rotation_x,k.rotation_y]; return nd;});
|
||||||
|
const mk=(opts)=>$serial.serialize({meta:{},keys:phy.map(nd=>{const k=phyToKleKey(nd,opts); k.x=rnd(k.x); k.y=rnd(k.y); return k;})});
|
||||||
|
const sA=mk(), sB=mk({pivot:nd=>nd._pivot});
|
||||||
|
const g=s=>$serial.deserialize(s).keys.map(fingerprint).sort().join('|');
|
||||||
|
if(g(sB)===g(orig)) ok++;
|
||||||
|
if(JSON.stringify(sB)===JSON.stringify(orig)) sameSer++;
|
||||||
|
if(RESET.test(JSON.stringify(sA))) stickyA++;
|
||||||
|
if(RESET.test(JSON.stringify(sB))) { stickyB++; stickyBoards.push(b.name); }
|
||||||
|
if(A.some(k=>k.rotation_angle)){ rotBoards++; rowsOrig+=rowsOf(orig); rowsA+=rowsOf(sA); rowsB+=rowsOf(sB); }
|
||||||
|
}
|
||||||
|
console.log(`boards=${n} parseFail=${parseFail}`);
|
||||||
|
console.log(`PHY経由(クラスター原点を軸+丸め)で元と同じ配置: ${ok}/${n}`);
|
||||||
|
console.log(`直列化結果が元のKLE出力と完全一致: ${sameSer}/${n}`);
|
||||||
|
console.log(`回転を含む板: ${rotBoards} 行数合計 orig=${rowsOrig} A(キーごと軸)=${rowsA} B(クラスター軸)=${rowsB}`);
|
||||||
|
console.log(`sticky属性の""リセットを含む出力: A=${stickyA} B=${stickyB}`, JSON.stringify(stickyBoards.slice(0,6)));
|
||||||
|
```
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# 行の先頭以外の r, rx, ry を受け入れるか
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
受け入れる。位置を問わず、そのキー以降に適用する。rx, ry は座標を (rx, ry) に戻してから x, y を加える
|
||||||
|
(KLE本家のコードから行頭チェックだけを外したときの挙動と同じ)。
|
||||||
|
|
||||||
|
決め手は Remap が読めること。ユーザーの判断: 「Remapで扱えるものを拒否したくない」。
|
||||||
|
|
||||||
|
| 案 | 取り込める基板 | 既存の解釈への影響 | 出力への影響 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 本家に合わせて拒否 | 1505件中1504件 | なし | なし |
|
||||||
|
| **r だけ受け入れ** | 1505件 | なし | なし |
|
||||||
|
| **r, rx, ry とも受け入れ(採用)** | 1505件 | なし | なし |
|
||||||
|
|
||||||
|
3案とも出力は変わらない。本家の serialize はクラスターが変わると必ず行を割るので、
|
||||||
|
r, rx, ry は常に行頭にしか出ない。寛容化しても非互換なJSONを生む側には回らない。
|
||||||
|
|
||||||
|
## 実データ
|
||||||
|
|
||||||
|
コーパス1505件(KLE本家プリセット12 + samples 9 + the-via/keyboards 1484)で、
|
||||||
|
行の先頭以外の r, rx, ry を持つのは `owlab/spring/spring.json` の1件だけ。内訳は r が15回、rx/ry は0回。
|
||||||
|
|
||||||
|
spring は Alice系。物理行ごとに配列を1つ書き、行の途中で r を 0 → 12 → -12 → 0 と切り替えている。
|
||||||
|
行頭チェックを外して読むと素直に開く(67キー、左右±12度のスプレイ + 回転なしの外側2列)。
|
||||||
|
|
||||||
|
```
|
||||||
|
0,3 x=3.25 r=0 0,4 x=4.44 r=12 0,8 x=8.72 r=-12 0,12 x=12.85 r=0
|
||||||
|
1,2 x=2.50 r=0 1,3 x=3.70 r=12 1,7 x=8.48 r=-12 1,11 x=12.60 r=0
|
||||||
|
```
|
||||||
|
|
||||||
|
## 寛容化が既存の解釈を変えないこと
|
||||||
|
|
||||||
|
本家 serial.js の該当箇所は throw だけで、意味づけは残りの行が決めている。
|
||||||
|
|
||||||
|
```js
|
||||||
|
if(key.r != null) { if(k!=0) {deserializeError(...)} current.rotation_angle = key.r; }
|
||||||
|
if(key.rx != null) { if(k!=0) {deserializeError(...)} current.rotation_x = cluster.x = key.rx; extend(current, cluster); }
|
||||||
|
```
|
||||||
|
|
||||||
|
throw を3つ消したパーサーと本家で全1505件を読み比べ、本家が読めた1504件は**キー配列が完全一致**。
|
||||||
|
変わるのは今エラーになる1件だけ。
|
||||||
|
|
||||||
|
## 他のツールの受け入れ
|
||||||
|
|
||||||
|
| ツール | 行の途中の r/rx/ry | 根拠 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| KLE本家 `serial.js` | 拒否 | `'r' can only be used on the first key in a row` |
|
||||||
|
| kle-serial (ijprest, TS移植) | 拒否 | 同じチェックを移植 |
|
||||||
|
| Vial `kle_serial.py` | 拒否 | `k != 0 and ("r" in item or ...)` |
|
||||||
|
| kicad-kbplacer | 拒否 | 同じチェックあり |
|
||||||
|
| VIA `@the-via/reader` | 許容 | reduceで位置チェックなし |
|
||||||
|
| Remap `KeyboardModel.ts` | 許容 | `Current.setOp()` に位置チェックなし |
|
||||||
|
| QMK `kle2xy.py` | 無関係 | r/rx/ry を読まない(回転を捨てる) |
|
||||||
|
|
||||||
|
Remap の `Current` を位置計算だけ移植して、throw を外した本家と突き合わせた。行の途中の r、
|
||||||
|
行の途中の rx/ry、途中で r=0 に戻す、spring 全体、いずれも一致する。VIA reader はコードを読んだだけで、
|
||||||
|
`cursor.y = ry` としか書いていないため rx/ry の扱いは本家と違う可能性がある(spring は rx=ry=0 なので影響なし)。
|
||||||
|
|
||||||
|
同じ突き合わせをコーパス全件でやると19件が不一致になるが、いずれも行頭側の ry の解釈やラベルなしキーの扱いで、
|
||||||
|
今回の話とは別件。簡易移植での比較なので Remap 本体で再現したわけでもない。追っていない。
|
||||||
|
|
||||||
|
## 取り込んで書き戻す
|
||||||
|
|
||||||
|
spring を寛容パーサーで読み、PHY経由で本家の serialize に通すと、5行 → 26行のJSONになる。
|
||||||
|
本家の deserialize が読め、配置も元と一致した。寛容化は本家で開けないファイルの修復経路になる。
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# KLE JSON出力での rx, ry の選び方と行のまとまり
|
||||||
|
|
||||||
|
PHY P-1「KLE JSONへ書き出した配置が行のまとまりを保てるか」の検証。
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
保てる。条件は2つ。
|
||||||
|
|
||||||
|
1. 同じ絶対角度を持つキーには共通の rx, ry を与える(クラスターの原点が使える)
|
||||||
|
2. 書き出す x, y を丸める
|
||||||
|
|
||||||
|
この2つを満たすと、ErgoDox・Kinesis Advantage では元のKLE JSONとバイト単位で一致する出力になった。
|
||||||
|
|
||||||
|
KLEが行を切る条件は本家 `serial.js` の `serialize` にそのまま書かれている。
|
||||||
|
`(r, rx, ry)` のいずれかが変わるか、`y` が前のキーと**厳密に**異なると新しい行になる。
|
||||||
|
キーは書き出し前に `角度 → rx → ry → y → x` で並べ替えられる。
|
||||||
|
|
||||||
|
## 案の比較
|
||||||
|
|
||||||
|
回転を含む実データをPHY(絶対座標)に変換し、rx, ry の選び方だけを変えて書き出した。
|
||||||
|
行数が少ないほど、元の行のまとまりに近い。
|
||||||
|
|
||||||
|
| 軸の選び方 | 回転を含む153枚の行数合計 | 備考 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 元のKLE JSON | 3465 | 比較の基準 |
|
||||||
|
| キーごとに自分の原点を軸にする | 6369 | 回転キーが1つずつ別の回転グループになり、1キー1行に分解される |
|
||||||
|
| 同じ角度のキーで軸を共有する(クラスター原点) | 3526 | ほぼ基準どおり |
|
||||||
|
|
||||||
|
丸めなしだと、軸を共有しても行が余分に割れる。ErgoDox で 26行 → 28行。
|
||||||
|
`serialize` の行判定が `key.y !== current.y` の厳密比較で、絶対座標への変換で入った
|
||||||
|
1e-16 程度の誤差が残るため。丸めれば 1e-4 でも 1e-9 でも結果は同じだった。
|
||||||
|
|
||||||
|
| ボード | 元の行数 | 軸共有+丸め | 一致 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| ErgoDox (KLEプリセット) | 26 | 26 | 完全一致 |
|
||||||
|
| ErgoDox EZ (VIA) | 26 | 26 | 完全一致 |
|
||||||
|
| Kinesis Advantage | 17 | 17 | 完全一致 |
|
||||||
|
| Atreus | 34 | 34 | 数値の誤差のみ(本家は保存時に小数4桁へ丸めるので、その時点で一致) |
|
||||||
|
|
||||||
|
## 検証の範囲
|
||||||
|
|
||||||
|
本家のWebアプリのUIに読み込ませたわけではなく、本家リポジトリの `serial.js` をそのままNodeで実行した。
|
||||||
|
行の再構成と回転グループの判定はこのファイルに閉じているので、UIを経由しても同じ結果になるはずだが、
|
||||||
|
描画そのものは確認していない。
|
||||||
|
|
||||||
|
## 副作用
|
||||||
|
|
||||||
|
キーごとに軸を選ぶ出力は、行が割れるだけでなく、見た目属性の書き戻しも壊しやすい。
|
||||||
|
KLEは `p` / `sm` / `sb` / `st` の空文字への書き戻しを無視するため、並びが変わると前のキーの値が漏れる。
|
||||||
|
軸を共有した出力では1504件中0件、キーごとに軸を選んだ出力では1件発生した。
|
||||||
|
|
||||||
|
## 検証スクリプト
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone --depth 1 https://github.com/ijprest/keyboard-layout-editor.git kle
|
||||||
|
git clone --depth 1 https://github.com/the-via/keyboards.git via
|
||||||
|
{ echo 'global.angular = { toJson: (o) => JSON.stringify(o) };'; cat kle/extensions.js; cat kle/serial.js; } > kleio.js
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 使い方: 下の setup の手順でコーパスを用意してから node rowgroup.js
|
||||||
|
const fs=require('fs');
|
||||||
|
const $serial=require('./kleio.js'); // 本家 extensions.js + serial.js を連結したもの
|
||||||
|
const R=d=>d*Math.PI/180, rnd=v=>Math.round(v*1e6)/1e6;
|
||||||
|
const rowsOf=s=>s.filter(Array.isArray).length;
|
||||||
|
|
||||||
|
// KLEキー -> 絶対配置(PHYのTransformと同じ意味)。rx,ry は元の値をクラスター原点として覚えておく
|
||||||
|
function toPhy(k){
|
||||||
|
const th=R(k.rotation_angle), c=Math.cos(th), s=Math.sin(th);
|
||||||
|
const dx=k.x-k.rotation_x, dy=k.y-k.rotation_y;
|
||||||
|
return {x:k.rotation_x+dx*c-dy*s, y:k.rotation_y+dx*s+dy*c, rot:k.rotation_angle,
|
||||||
|
pivot:[k.rotation_x,k.rotation_y], src:k};
|
||||||
|
}
|
||||||
|
// 絶対配置 -> KLEキー。pivot の選び方だけを差し替えて比べる
|
||||||
|
function toKle(n, pivot){
|
||||||
|
const k=JSON.parse(JSON.stringify(n.src));
|
||||||
|
const [rx,ry]=pivot, th=R(n.rot), c=Math.cos(th), s=Math.sin(th);
|
||||||
|
const dx=n.x-rx, dy=n.y-ry;
|
||||||
|
k.x=rnd(rx+dx*c+dy*s); k.y=rnd(ry-dx*s+dy*c);
|
||||||
|
k.rotation_angle=n.rot; k.rotation_x=rx; k.rotation_y=ry;
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
const load=p=>{const d=JSON.parse(fs.readFileSync(p,'utf8')); return d.layouts?d.layouts.keymap:d;};
|
||||||
|
const targets=[];
|
||||||
|
for(const p of JSON.parse(fs.readFileSync('kle/layouts.json','utf8')).presets)
|
||||||
|
if(['ErgoDox','Kinesis Advantage','Atreus'].includes(p.name)) targets.push({name:p.name,data:p.data});
|
||||||
|
targets.push({name:'ergodox_ez(via)',data:load('via/src/ergodox_ez/ergodox_ez.json')});
|
||||||
|
|
||||||
|
for(const t of targets){
|
||||||
|
const A=$serial.deserialize(t.data).keys;
|
||||||
|
const orig=$serial.serialize({meta:{},keys:JSON.parse(JSON.stringify(A))});
|
||||||
|
const phy=A.map(toPhy);
|
||||||
|
const ser=(pick)=>$serial.serialize({meta:{},keys:phy.map(n=>toKle(n,pick(n)))});
|
||||||
|
const perKey=ser(n=>n.rot?[n.x,n.y]:[0,0]); // 回転キーはキーごとに自分の原点を軸にする
|
||||||
|
const cluster=ser(n=>n.pivot); // 同じ角度のキーで軸を共有する
|
||||||
|
console.log(t.name.padEnd(20),'元',rowsOf(orig),
|
||||||
|
'| キーごと',rowsOf(perKey),
|
||||||
|
'| 軸共有',rowsOf(cluster), JSON.stringify(cluster)===JSON.stringify(orig)?'(元と完全一致)':'');
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# LEDチェーンとレイアウトバリアント
|
||||||
|
|
||||||
|
ELC P-4 の検証。
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
チェーンはレイアウトの選択と無関係に固定されている。バリアントごとにチェーンの並びを分ける必要はない。
|
||||||
|
|
||||||
|
理由は、per-key LEDがスイッチとは独立に基板へ実装されるため。排他な2つのキー位置の両方にLEDが載る。
|
||||||
|
QMKの `rgb_matrix.layout` はボードに1つしかなく、レイアウトごとに分かれていない。
|
||||||
|
|
||||||
|
| ボード | レイアウト数 | LED数 | バリアント固有のマトリクス位置 | そこへのLED割り当て |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| dp60 | 7 | 72 | 8 | 8個すべてにあり |
|
||||||
|
| cherrybstudio/cb87rgb | 9 | 92 | 6 | 6個すべてにあり |
|
||||||
|
| crkbd/r2g | 2 | 54 | 6 (外側の列) | 6個すべてにあり |
|
||||||
|
| annepro2/c18 | 2 | 61 | 1 (ISOの追加キー) | なし |
|
||||||
|
| boardsource/sessanta | 2 | 72 | 4 (分割スペース) | なし |
|
||||||
|
|
||||||
|
annepro2/c18 と boardsource/sessanta は、バリアント固有のキーにLEDが無い。
|
||||||
|
チェーンが切れるのではなく、そのキーにper-key LEDが載っていないだけである。
|
||||||
|
PHYがper-key LEDの有無をキーごとに持つ形でそのまま表せる。
|
||||||
|
|
||||||
|
どちらの型でも、LEDの番号はレイアウトの選択で動かない。
|
||||||
|
|
||||||
|
## 調べ方
|
||||||
|
|
||||||
|
QMKのinfo.json 400件から、レイアウトを2つ以上持ち `rgb_matrix.layout` を持つ19件を抽出。
|
||||||
|
全レイアウトのマトリクス位置の和集合から共通部分を引いた「バリアント固有の位置」に、
|
||||||
|
LEDが割り当てられているかを見た。11件は割り当てあり、12件は一部に無し(両方に該当する板がある)。
|
||||||
|
|
||||||
|
## 限界
|
||||||
|
|
||||||
|
回路図そのものではなく、ファームウェアが持つLED配列を見ている。
|
||||||
|
LEDが基板に実装されているかどうかまでは確認できない。ただしQMKの `rgb_matrix.layout` は
|
||||||
|
レイアウトごとに分けられない構造なので、「選択によってチェーンの並びが変わる基板」は
|
||||||
|
少なくともQMKでは成立しない。
|
||||||
|
|
||||||
|
## 検証スクリプト
|
||||||
|
|
||||||
|
```py
|
||||||
|
# QMKのinfo.jsonを無作為抽出で取得する。qmk_list.json は
|
||||||
|
# curl -o qmk_list.json https://keyboards.qmk.fm/v1/keyboard_list.json
|
||||||
|
import json, random, urllib.request, concurrent.futures, os
|
||||||
|
kbs=json.load(open('qmk_list.json'))['keyboards']
|
||||||
|
random.seed(42)
|
||||||
|
os.makedirs('qmk', exist_ok=True)
|
||||||
|
def fetch(kb):
|
||||||
|
p='qmk/'+kb.replace('/','__')+'.json'
|
||||||
|
if os.path.exists(p): return True
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(f'https://keyboards.qmk.fm/v1/keyboards/{kb}/info.json', timeout=30) as r:
|
||||||
|
open(p,'wb').write(r.read())
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
return str(e)[:60]
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(8) as ex:
|
||||||
|
res=list(ex.map(fetch, random.sample(kbs, 400)))
|
||||||
|
print('fetched', sum(1 for r in res if r is True), 'failed', sum(1 for r in res if r is not True))
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
# レイアウトバリアントとper-key LEDの関係。rgb_matrix.layout はボードに1つしかない
|
||||||
|
import json, glob, collections
|
||||||
|
st=collections.Counter()
|
||||||
|
for f in glob.glob('qmk/*.json'):
|
||||||
|
d=json.load(open(f,encoding='utf-8'))
|
||||||
|
for kb,v in (d.get('keyboards') or {}).items():
|
||||||
|
lays=v.get('layouts') or {}; rm=(v.get('rgb_matrix') or {}).get('layout')
|
||||||
|
if not rm or len(lays)<2: continue
|
||||||
|
st['対象ボード']+=1
|
||||||
|
used=[set(tuple(k['matrix']) for k in l['layout']) for l in lays.values()]
|
||||||
|
variant_only=set().union(*used)-set.intersection(*used) # バリアント固有の位置
|
||||||
|
led=set(tuple(l['matrix']) for l in rm if 'matrix' in l)
|
||||||
|
if not variant_only: continue
|
||||||
|
if variant_only & led: st['固有位置にLEDあり']+=1
|
||||||
|
if variant_only - led: st['固有位置にLEDなし']+=1
|
||||||
|
print(kb, 'レイアウト', len(lays), 'LED', len(rm),
|
||||||
|
'固有位置', len(variant_only), 'うちLEDあり', len(variant_only & led))
|
||||||
|
print(st)
|
||||||
|
```
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# マルチレイアウト(交換可能なキークラスタ)の扱い
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
- PHYは全バリアントのキーを同時に持つ(superset)。どれか1つに畳んだ状態を正本にしない
|
||||||
|
- 排他関係は木で表す。VariantGroup / Variant の2ノードを追加する
|
||||||
|
- ELCは変更なし。supersetをそのまま配線する。排他キー同士は同じ接続先を共有してよい
|
||||||
|
- IDXの番号空間は1つ。排他キー同士は同じ番号を共有できるので `order` の要素にキーIDの配列を許す
|
||||||
|
- MAPは変更なし
|
||||||
|
|
||||||
|
## PHYでの表現
|
||||||
|
|
||||||
|
| 案 | 形 | 判断 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 木にバリアントノード | VariantGroup > Variant > ノード | 採用 |
|
||||||
|
| キーにタグ + ファイル単位で選択肢を宣言 | `variant?: {option, choice}` + `layoutOptions[]` | 不採用 |
|
||||||
|
| 別レイヤーに切り出す | PHYはsupersetだけ持ち、選択箇所は新レイヤー | 不採用 |
|
||||||
|
|
||||||
|
タグ案の利点は、reparentでキーを出し入れしても所属が変わらないこと。
|
||||||
|
木案はそこが弱いが、次の点で勝る。
|
||||||
|
|
||||||
|
- 条件付きの選択肢(あるバリアントを選んだときだけ現れる選択肢)が入れ子で自然に出る。
|
||||||
|
タグ案では選択肢の依存関係を別に持つ必要がある
|
||||||
|
- 「どのキーがどの選択肢に属するか」を木の上で直接見られる。タグ案は全ノードを走査しないと分からない
|
||||||
|
- 空のVariant(「エンコーダーを載せない」)が、子のいないノードとしてそのまま置ける。
|
||||||
|
タグ案ではファイル単位の宣言が必須になる(キーのタグから選択肢を復元できないため)
|
||||||
|
|
||||||
|
別レイヤー案は、選択箇所が「どのスイッチを実装するか」という物理の話であり、
|
||||||
|
PHY以外の3レイヤーがすべてPHYのノードIDを参照する構造からも外れるので落とした。
|
||||||
|
|
||||||
|
## VariantがTransformを持たない理由
|
||||||
|
|
||||||
|
バリアントは同じ場所を占める代替物なので、選択によって配下の絶対配置が動く構造にしたくない。
|
||||||
|
1つのバリアントの中で回転中心が要る場合は、その中にクラスターを置けば足りる。
|
||||||
|
VariantGroup側はクラスターと同じくTransformを持つ。
|
||||||
|
|
||||||
|
## IDXの番号共有
|
||||||
|
|
||||||
|
ELCで同じマトリクス交点に配線された排他キーは、同じキー番号を共有しなければならない。
|
||||||
|
|
||||||
|
出力先3つ(QMK `LAYOUT` マクロ / ZMK matrix transform の `map` / KMK `coord_mapping`)は、
|
||||||
|
どれも「番号の位置 → マトリクス位置」の1対1の対応表である。
|
||||||
|
同じ交点に別々の番号を与えると、その表に同じ交点が2回現れることになる。
|
||||||
|
逆に、別々の交点のキーに同じ番号を与えることもできない。
|
||||||
|
|
||||||
|
よって `order` の配列要素は「同じ交点に配線された排他キーの集合」と一致する。
|
||||||
|
|
||||||
|
### レイアウトごとに別のIDXを持つ案
|
||||||
|
|
||||||
|
不採用。キー番号が公開インターフェースであることと相性が悪い。
|
||||||
|
番号空間が複数あると、キーマップがどの空間のものかを指定しないと解釈できない。
|
||||||
|
また選択箇所が3つあれば組み合わせの数だけ空間が要る。
|
||||||
|
|
||||||
|
QMKの `LAYOUT_60_ansi` のような特定レイアウト向けの並びは、
|
||||||
|
supersetの並びから選択外のキーを落とした派生情報として出す。
|
||||||
|
ただし既存のコミュニティレイアウトの引数順と一致するかは未確認(IDXレイヤー.md P-1)。
|
||||||
|
|
||||||
|
## 捨てた検討
|
||||||
|
|
||||||
|
- キーごとに `variants: string[]` を持たせて複数の選択肢にまたがらせる形。
|
||||||
|
そもそも複数選択肢で共通のキーが実在するかを確認できていないので、必要になってから決める
|
||||||
|
(PHYレイヤー.md P-15)
|
||||||
|
- 選択状態(どのバリアントを選んでいるか)をPHYとは別ファイルに持つ案。
|
||||||
|
既定のバリアントをVariantGroupに持たせれば足りる。編集中の表示切り替えはエディタの状態
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# プロファイル定義の置き場
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
- ファイル単位の器 `PhyFile` を保存形式に書き、`profiles: Profile[]` をそこに置く
|
||||||
|
- `Key.profile` は Profile 実体ではなく ID 参照にする
|
||||||
|
- ビジュアル表現は上面矩形の4辺インセット + R
|
||||||
|
- プロファイル内の距離はすべて mm
|
||||||
|
|
||||||
|
phase1-json-schema.md には最初からファイル単位の器(version/metadata/profiles/root)と profile 定義があり、
|
||||||
|
PHYレイヤー.md の保存形式だけが `const root: Node[] = []` のまま取り残されていた。今回は実質その追認。
|
||||||
|
ただし visualOffset の形だけはスキーマ側も書き換えた。
|
||||||
|
|
||||||
|
## 案の比較
|
||||||
|
|
||||||
|
### ビジュアル表現の形
|
||||||
|
|
||||||
|
| 案 | 内容 | 採否 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 4辺インセット + R | top/right/bottom/left/radius | 採用。非対称な上面を表せる |
|
||||||
|
| 位置 + サイズ差 | x, y, dw, dh, radius | 不採用。表現力は同じだが、非対称のとき値の読み取りが直感的でない |
|
||||||
|
| スキーマ現状のまま | x, y, radius | 不採用。本文が言う「オフセット距離」に当たる項目がなく、上面のサイズが決まらない |
|
||||||
|
|
||||||
|
kbd-editor の stores/displayConfig.ts が top{l,r,t,b} + cornerRadius で、4辺独立と同じ形だった
|
||||||
|
(過去の kbd-editor 調査による。概念の参考は可としている)。
|
||||||
|
|
||||||
|
### 単位
|
||||||
|
|
||||||
|
| 案 | 採否 |
|
||||||
|
| --- | --- |
|
||||||
|
| すべて mm | 採用 |
|
||||||
|
| unitSize は mm、オフセットと R は U | 不採用 |
|
||||||
|
| 値ごとに mm/U を指定 | 不採用 |
|
||||||
|
|
||||||
|
元の本文は「オフセット距離(mm or U単位)・R(U単位)」と単位が混在していた。
|
||||||
|
プロファイルは実寸を規定する定義なので、中で U 換算を持つと unitSize への依存が入る。
|
||||||
|
|
||||||
|
### 参照方法
|
||||||
|
|
||||||
|
| 案 | 採否 |
|
||||||
|
| --- | --- |
|
||||||
|
| ID 参照 | 採用。スキーマが既にこの形。定義の複製が起きない |
|
||||||
|
| Key が Profile 実体を持つ | 不採用 |
|
||||||
|
|
||||||
|
座標モデルを決めたときに「今回の決定の範囲外」として実体のまま残されていた項目。
|
||||||
|
今回で決着。
|
||||||
|
|
||||||
|
## 今回触っていないもの
|
||||||
|
|
||||||
|
ファイル単位のデータとして他に候補が出ていたが、置き場の決定は保留:
|
||||||
|
|
||||||
|
- KLE の switchMount / switchBrand / switchType、backcolor / radii / background / css → P-6 待ち
|
||||||
|
- KLE 出力時の rx, ry の選び方、行のまとまり → P-1。ファイルではなく出力設定の単位
|
||||||
|
|
||||||
|
implementation-plan.md の 60% ANSI サンプルは keys/position/size.width という古い平坦な形のままで、
|
||||||
|
今回の保存形式と合っていない。プロファイル部分だけは unitSize のみなので矛盾しない。別タスク。
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# 複数のバリアントで共通のキーを持つ基板があるか
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
実在する。そのうえで、共通キーはVariantごとに複製して置く。同じ物理キーかどうかはBeyond KLEが判定しない。
|
||||||
|
|
||||||
|
分解して複製を避けられる基板もあるが、避けられない基板もある。
|
||||||
|
|
||||||
|
| ボード | 選択肢 | 分解 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| LW67 | ANSI / ANSI Split Backspace / ISO / ISO Split Backspace | 独立した2つの選択の積。VariantGroupを2つに分ければ複製は要らない |
|
||||||
|
| MACRO1 | Numpad / Southpaw / Macropad | 分解できない。独立な選択に割ると、キャップが重なる組み合わせが作れてしまう |
|
||||||
|
|
||||||
|
複製を許す判断の根拠は、複製しても他レイヤーが壊れないこと。
|
||||||
|
ELCは排他キーで接続先の組を共有でき、IDXは `order` の配列形式で番号を共有できる。
|
||||||
|
MAPは同じキーコードを2回書くことになるが、どれが同じ物理キーかは設計者が知っている。
|
||||||
|
位置で自動判定しようとしても、排他キーは位置もサイズも違ってよい(2UのBackspaceと分割後の1U)ので、
|
||||||
|
判定の基準にならない。
|
||||||
|
|
||||||
|
## 調べ方
|
||||||
|
|
||||||
|
VIAのキーボード定義1484件のうち、レイアウトオプションを持つ824件を対象にした。
|
||||||
|
VIAは1つのキーを1つの選択肢にしか置けないので、複数の選択肢で成立するキーは定義側で複製されている。
|
||||||
|
生ラベルの4番目(`"行,列"` の形)がオプション番号と選択肢番号で、これで選択肢ごとのキー集合を作り、
|
||||||
|
マトリクス位置と形状(x, y, w, h, r)が一致するキーが複数の選択肢に現れるものを探した。
|
||||||
|
|
||||||
|
3択以上のオプション415件のうち、該当するのは2件だけだった。
|
||||||
|
|
||||||
|
## LW67
|
||||||
|
|
||||||
|
ANSI/ISO と Backspace分割の2択の積で、4つの選択肢はその全組み合わせ。
|
||||||
|
ISO Enter は ISO 側の2つ、分割Backspaceのキーは分割側の2つに現れる。
|
||||||
|
QMKも `LAYOUT_ansi` / `LAYOUT_ansi_splitbs` / `LAYOUT_iso` / `LAYOUT_iso_splitbs` の4つを持つ。
|
||||||
|
VariantGroupを「ANSI/ISO」と「Backspace分割の有無」に分ければ、キーの複製は要らない。
|
||||||
|
|
||||||
|
## MACRO1
|
||||||
|
|
||||||
|
6行4列。VIAは3択、QMK (`laneware/macro1`) も `LAYOUT_numpad` / `LAYOUT_southpaw` / `LAYOUT_macropad` の
|
||||||
|
3つしか持たない。`LAYOUT_macropad` は全キー1Uで23キーあり、他の2つが使うフットプリントを全部含む。
|
||||||
|
配線はすべての位置に来ていて、組み合わせを3つに絞っているのはファームウェア側である。
|
||||||
|
|
||||||
|
| | 左列 (2,0 / 3,0 / 4,0) | 下段左 (5,0 / 5,1) | 右列 (2,3 / 3,3 / 4,3) | 下段右 (5,2 / 5,3) |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| Numpad | 1U×3 | `5,0` が2U幅。`5,1` は潰れる | 2U縦×2 (`3,3` なし) | `5,2` 1U。`5,3` は `4,3` が潰す |
|
||||||
|
| Southpaw | 2U縦×2 (`3,0` なし) | `5,0` は `4,0` が潰す。`5,1` 1U | 1U×3 | `5,2` が2U幅。`5,3` は潰れる |
|
||||||
|
| Macropad | 1U×3 | 両方1U | 1U×3 | 両方1U |
|
||||||
|
|
||||||
|
共通キーは8個。`3,0` などの左列1Uが {Numpad, Macropad}、`5,1` と右列1Uが {Southpaw, Macropad} に現れる。
|
||||||
|
出現集合が交差するので入れ子では表せない。
|
||||||
|
|
||||||
|
左右の列だけを見ると独立な2択に見えるが、下段が絡むと分解できない。
|
||||||
|
「左列と下段左」を1つの選択、「右列と下段右」をもう1つの選択として2つのVariantGroupに割ると、
|
||||||
|
`4,0` が2U縦のまま `5,0` に1Uキーを置く組み合わせが作れる。これは提供されないどころか、
|
||||||
|
キャップが重なって物理的に成立しない。
|
||||||
|
|
||||||
|
## 捨てた見方
|
||||||
|
|
||||||
|
- 入れ子のVariantGroupで表す: 出現集合が交差するので不可能
|
||||||
|
- 独立したVariantGroupに分けて、成立しない組み合わせを禁止する仕組みを足す:
|
||||||
|
複製を許せば要らない。禁止の表現を足すほうが重い
|
||||||
|
- 同じ位置のキーに違う番号が振られたら警告する: 排他キーは位置もサイズも違ってよいので基準にならない
|
||||||
|
|
||||||
|
## この調査の限界
|
||||||
|
|
||||||
|
VIAの表現力に引きずられている。VIAは選択肢ごとのキー集合しか書けないので、
|
||||||
|
設計者が最初から「2つの独立した選択」として設計した基板は、VIAの定義でも2つのオプションに分かれ、
|
||||||
|
この検索には引っかからない。引っかかった2件は「1つのオプションに畳んでしまった」定義だけである。
|
||||||
|
|
||||||
|
## 検証スクリプト
|
||||||
|
|
||||||
|
`rawkle.js` — VIAのオプションタグを読むため、並べ替え前の生ラベルを残すデシリアライザ。
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 生ラベル(並べ替え前)を保持する最小デシリアライザ。幾何情報は serial.js と同じ規則。
|
||||||
|
function deserializeRaw(rows){
|
||||||
|
const cur={x:0,y:0,w:1,h:1,x2:0,y2:0,w2:0,h2:0,r:0,rx:0,ry:0,d:false};
|
||||||
|
const keys=[];
|
||||||
|
for(const row of rows){
|
||||||
|
if(!Array.isArray(row)){ continue; }
|
||||||
|
for(const item of row){
|
||||||
|
if(typeof item==='string'){
|
||||||
|
keys.push({x:cur.x,y:cur.y,w:cur.w,h:cur.h,
|
||||||
|
w2:cur.w2===0?cur.w:cur.w2,h2:cur.h2===0?cur.h:cur.h2,x2:cur.x2,y2:cur.y2,
|
||||||
|
r:cur.r,rx:cur.rx,ry:cur.ry,d:cur.d,raw:item.split('\n')});
|
||||||
|
cur.x+=cur.w; cur.w=cur.h=1; cur.x2=cur.y2=cur.w2=cur.h2=0; cur.d=false;
|
||||||
|
} else {
|
||||||
|
if(item.r!=null) cur.r=item.r;
|
||||||
|
if(item.rx!=null){ cur.rx=item.rx; cur.x=cur.rx; cur.y=cur.ry; }
|
||||||
|
if(item.ry!=null){ cur.ry=item.ry; cur.x=cur.rx; cur.y=cur.ry; }
|
||||||
|
if(item.x) cur.x+=item.x;
|
||||||
|
if(item.y) cur.y+=item.y;
|
||||||
|
if(item.w){ cur.w=cur.w2=item.w; }
|
||||||
|
if(item.h){ cur.h=cur.h2=item.h; }
|
||||||
|
if(item.x2) cur.x2=item.x2;
|
||||||
|
if(item.y2) cur.y2=item.y2;
|
||||||
|
if(item.w2) cur.w2=item.w2;
|
||||||
|
if(item.h2) cur.h2=item.h2;
|
||||||
|
if(item.d) cur.d=item.d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cur.y++; cur.x=cur.rx;
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
module.exports={deserializeRaw};
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
const fs=require('fs'), path=require('path');
|
||||||
|
const {deserializeRaw}=require('./rawkle.js');
|
||||||
|
function walk(d,out=[]){for(const e of fs.readdirSync(d,{withFileTypes:true})){const p=path.join(d,e.name); e.isDirectory()?walk(p,out):e.name.endsWith('.json')&&out.push(p);}return out;}
|
||||||
|
const OPT=/^(\d+),(\d+)$/;
|
||||||
|
const rows=[];
|
||||||
|
for(const f of walk('via/src')){
|
||||||
|
let d; try{ d=JSON.parse(fs.readFileSync(f,'utf8')); }catch(e){ continue; }
|
||||||
|
const lay=d.layouts; if(!lay||!Array.isArray(lay.keymap)||!lay.keymap.length) continue;
|
||||||
|
let keys; try{ keys=deserializeRaw(lay.keymap); }catch(e){ continue; }
|
||||||
|
const byOpt={};
|
||||||
|
for(const k of keys){
|
||||||
|
const tag=k.raw[3]; if(!tag||!OPT.test(tag)) continue;
|
||||||
|
const [o,c]=tag.split(',').map(Number);
|
||||||
|
const id=[k.raw[0],k.x,k.y,k.w,k.h,k.r].join('|'); // マトリクス位置+形状で「同じキー」とみなす
|
||||||
|
((byOpt[o]=byOpt[o]||{})[c]=byOpt[o][c]||new Set()).add(id);
|
||||||
|
}
|
||||||
|
for(const [o,ch] of Object.entries(byOpt)){
|
||||||
|
const cs=Object.keys(ch).map(Number).sort((a,b)=>a-b);
|
||||||
|
if(cs.length<3) continue;
|
||||||
|
// 各キーが現れる選択肢集合
|
||||||
|
const occ={};
|
||||||
|
for(const c of cs) for(const id of ch[c]) (occ[id]=occ[id]||[]).push(c);
|
||||||
|
const multi=Object.entries(occ).filter(([,v])=>v.length>1 && v.length<cs.length);
|
||||||
|
if(!multi.length) continue;
|
||||||
|
// 出現集合の族が laminar(入れ子で表せる)か
|
||||||
|
const sets=[...new Set(multi.map(([,v])=>v.join(',')))].map(s=>new Set(s.split(',').map(Number)));
|
||||||
|
let laminar=true;
|
||||||
|
for(let i=0;i<sets.length;i++) for(let j=i+1;j<sets.length;j++){
|
||||||
|
const inter=[...sets[i]].filter(x=>sets[j].has(x)).length;
|
||||||
|
if(inter>0 && inter!==Math.min(sets[i].size,sets[j].size)) laminar=false;
|
||||||
|
}
|
||||||
|
rows.push({f:path.relative('via/src',f),name:d.name,opt:+o,
|
||||||
|
label: Array.isArray(lay.labels&&lay.labels[o])? lay.labels[o] : (lay.labels?lay.labels[o]:null),
|
||||||
|
choices:cs.length, sharedKeys:multi.length,
|
||||||
|
patterns:[...new Set(multi.map(([,v])=>'{'+v.join(',')+'}'))].join(' '), laminar});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('3択以上で「一部の選択肢だけに現れる共通キー」を持つオプション:',rows.length);
|
||||||
|
rows.forEach(r=>console.log(JSON.stringify(r)));
|
||||||
|
```
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# 保留課題をbacklogへ分離する
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
文書を、現行仕様・保留課題・検討履歴の3種類に分ける。
|
||||||
|
後回しにできる問い・検証・改善候補はbacklog/README.mdにまとめる。
|
||||||
|
現在の仕様が依存する未確認の前提は、引き続き仕様書の末尾に残す。
|
||||||
|
|
||||||
|
## 案の比較
|
||||||
|
|
||||||
|
| 置き場 | 判断 |
|
||||||
|
| --- | --- |
|
||||||
|
| 各仕様書の未確認事項にすべて残す | 現行実装の前提と、将来考えればよい課題が混ざる |
|
||||||
|
| journalへ移す | 結論反映後に削除できる履歴と、再開まで残す課題では寿命が異なる |
|
||||||
|
| 直下にbacklog.mdを置く | 直下へ追加したくないというユーザーの意向で採らない |
|
||||||
|
| backlog/README.mdへまとめる | 採用。役割をフォルダで区別し、必要になれば分割できる |
|
||||||
|
|
||||||
|
## 運用の意図
|
||||||
|
|
||||||
|
既存ルールの狙いは、採用した仕様に過去の検討を混ぜず、未確認の前提を事実と誤認して実装しないこと。
|
||||||
|
保留課題は現在の仕様でも解決済みの履歴でもないため、第3の文書種別として扱う。
|
||||||
|
|
||||||
|
移動の基準は未確認かどうかではなく、現在の実装がその答えに依存するかどうか。
|
||||||
|
保留中の対応範囲・暫定仕様は仕様書に明記し、その範囲で進められる問いだけを移す。
|
||||||
|
backlogへの移動自体は、仮説の採用や実装の許可を意味しない。
|
||||||
|
|
||||||
|
各項目は元の番号にレイヤー名を付け、関連仕様・未決事項・保留理由・再開条件・完了条件を持つ。
|
||||||
|
再開条件で「いつ考え直すか」を、完了条件で「何を確認または決定すれば終わるか」を残す。
|
||||||
|
事実確認と設計上の意思決定をともに「格上げ」と呼ぶことはやめる。
|
||||||
|
解決または取り下げた項目は削除し、完了済み課題の一覧は作らない。
|
||||||
|
|
||||||
|
仕様書側の「保留課題」は、対応するbacklogの区分へのリンク箇条書きだけに固定した。
|
||||||
|
問いの要約や項目名の列挙を重ねると片方だけ古くなるため、詳細はbacklogで一元管理する。
|
||||||
|
末尾は保留課題・検討済み非採用・未確認事項の順とし、空の保留課題セクションは残さない。
|
||||||
|
|
||||||
|
## 適用範囲
|
||||||
|
|
||||||
|
PHY・ELC・IDXの10件を整理した。ELC-P-3は責任分担の決定後に残る方式別の確認として扱い、
|
||||||
|
IDX-P-1は別途決定済みの仕様を維持して既存レイアウトとの互換性の実測だけを残した。
|
||||||
|
実機確認や既存コミュニティレイアウトの再調査は行っていない。
|
||||||
|
他の既存文書の全体再編は、この変更には含めない。
|
||||||
|
|
||||||
|
doc-lintもルール5に対応させる。backlogを検査対象に加え、項目の必須欄、
|
||||||
|
仕様書側の固定書式と参照先、事実確認と設計判断の完了条件を検査する。
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# ELCの走査設定を出力器に置く
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
ELCは配線を保存し、走査方向の決定と追加指定はFirmware Generatorの出力設定に置く。
|
||||||
|
接点マトリクスでいう走査方向は、どちら側のピン群を駆動して、どちら側から読み取るか。
|
||||||
|
全方式で走査手順が配線から一意に決まることは、ELCの成立条件にしない。
|
||||||
|
|
||||||
|
## 案の比較
|
||||||
|
|
||||||
|
| 案 | 判断 |
|
||||||
|
| --- | --- |
|
||||||
|
| 全方式で配線から走査手順を一意に導出できることを先に検証する | 配線保存の実装まで止める条件としては強すぎる |
|
||||||
|
| ELCに走査方向の指定を持たせる | 配線と出力時の選択の責任が混ざるため採らない |
|
||||||
|
| 出力器が走査方向を選び、必要なら出力設定で指定を受ける | 採用。ELCの責任を配線に保てる |
|
||||||
|
|
||||||
|
## 判断の根拠と残件
|
||||||
|
|
||||||
|
旧ELC P-3は「端子の接続先とダイオードの向きだけから走査手順が一意に決まるか」を格上げ条件にしていた。
|
||||||
|
しかし、複数の適切な方法から出力器が選べるなら、一意性は必要ない。
|
||||||
|
先に決める必要があったのは、配線だけでは決まらない場合の指定をどこが受け持つかである。
|
||||||
|
|
||||||
|
この変更は責任分担の決定であり、各方式の実配線や走査方法を新たに検証した結果ではない。
|
||||||
|
出力器は配線・ダイオードの向きと整合する方法を選び、必要な追加指定は出力設定から受け取る。
|
||||||
|
未対応の構成は出力しない。
|
||||||
|
|
||||||
|
方式ごとの導出可能な範囲と必要な追加設定は、その方式の出力に着手するときに確認する。
|
||||||
|
残件はbacklogのELC-P-3「方式ごとの走査設定の確認」に引き継いだ。
|
||||||
|
PHYの編集やELCの配線保存は、この確認に依存せず進められる。
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# IDXの全体の並びとバリアント選択後の出力番号
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
IDXは全バリアントの対象キーを `order: string[]` に持つ。同じ配線を使う排他キーも別々に並べる。
|
||||||
|
出力時に選択した構成のキーだけを残し、残った並びの位置を0から連続する出力番号とする。
|
||||||
|
保存したorderは変更しない。公開番号の互換性は出力レイアウトごとに扱う。
|
||||||
|
ユーザーがこの方針を採用した。
|
||||||
|
|
||||||
|
## 案の比較
|
||||||
|
|
||||||
|
| 案 | 扱い |
|
||||||
|
| --- | --- |
|
||||||
|
| 1つのorderに全キーを置き、出力時に取捨して詰める | 採用 |
|
||||||
|
| 同じ配線の排他キーは番号を共有し、選択後も番号を詰めない | 今回置き換えた旧仕様 |
|
||||||
|
| レイアウトごとに独立したIDXを保存する | 採用しない |
|
||||||
|
|
||||||
|
## 決定理由と例
|
||||||
|
|
||||||
|
ユーザーは、ANSIのバックスラッシュとISOの # が同じ配線を使っても、IDXでは別々の位置を持つことを希望した。
|
||||||
|
QとAが共通キーなら、全体のorderを `Q, backslash, A, hash` と置くことで、
|
||||||
|
ANSIでは `Q, backslash, A`、ISOでは `Q, A, hash` を取捨だけで得られる。
|
||||||
|
ISO出力の番号は0, 1, 2であり、保存したorderでの位置0, 2, 3とは区別する。
|
||||||
|
|
||||||
|
旧仕様は、全バリアントを含む番号と配線の対応表に同じ接続先が重複することを理由に、番号共有を必須としていた。
|
||||||
|
今回の出力単位はバリアント選択済みの構成であり、排他キーは同時に残らないため、その理由は当てはまらない。
|
||||||
|
全バリアントを一度に含むキー番号の対応表は、今回の出力方式には含めない。
|
||||||
|
|
||||||
|
MAPはPHYキーIDへの割り当てを保持する。出力時に配線の対応表とMAPの配列へ同じ選択後の並びを適用する。
|
||||||
|
ELCの保存データは全バリアントの配線を保持し、LEDチェーンも選択によって取捨・詰め直しをしない。
|
||||||
|
|
||||||
|
## 未検証の範囲
|
||||||
|
|
||||||
|
例での解消は、既存コミュニティレイアウト全般に対応できることの検証ではない。
|
||||||
|
従来の実データ調査は、既存のLAYOUT_all等の並びから他の並びを取捨できるかを調べたものだった。
|
||||||
|
今回の、別々のPHYキーIDを使って全体のorderを構成する方式については改めて確認が必要。
|
||||||
|
共通キー同士の要求順がレイアウト間で逆転する場合には単一のorderで表せない。
|
||||||
|
その実例の有無と互換出力の対応範囲を、IDX P-1の残余として残す。
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# MAPエディタの実機との往復とVIA対応
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
新規作成するキーボードのワークフローを主眼とし、MAPエディタは実機の読み書きを一組で提供する。
|
||||||
|
最初の通信プロトコルはVIA。対応プロトコルを絞り、段階的に増やす。
|
||||||
|
独自プロトコルの追加や、そのためのQMK・Vial等のフォークは行わない。
|
||||||
|
高度な動作定義、Staticの対応範囲、同期方式はまだ決めていない。
|
||||||
|
|
||||||
|
## 案の比較
|
||||||
|
|
||||||
|
| 案 | 扱い |
|
||||||
|
| --- | --- |
|
||||||
|
| 既存プロトコルで実機の読み書きを提供し、VIAから始める | 採用 |
|
||||||
|
| MAPエディタをStaticに閉じる | 採用しない |
|
||||||
|
| Dynamicの対応範囲を読み出しだけに限定する | ユーザーが読み書きの両対応を指定 |
|
||||||
|
| 独自プロトコルのためにファームウェアをフォークする | ユーザーが意図しないと明示 |
|
||||||
|
|
||||||
|
## 判断の根拠
|
||||||
|
|
||||||
|
ユーザーは、既存キーボード全般への対応よりも、新しく作るキーボードのワークフローを重視している。
|
||||||
|
VIAを話すなら読み書きの両方を提供し、利用者が好むRemapと同じ通信経路から始める方針となった。
|
||||||
|
当初のアシスタントによる「互換性」の説明はファイル変換に寄りすぎており、実機との通信を中心に修正した。
|
||||||
|
VIA向けのキーボード定義生成と、MAPの割り当ての読み書きは別の責務として整理した。
|
||||||
|
|
||||||
|
## 調査で確認した範囲
|
||||||
|
|
||||||
|
公式文書・実装を参照した。実機による読み書きの検証は行っていない。
|
||||||
|
|
||||||
|
- RemapはWebHIDを使い、QMKのVIAクライアント向け機能へアクセスする。Workbenchにはソース編集・ビルド・書き込みもある。
|
||||||
|
参照: https://github.com/remap-keys/remap#references
|
||||||
|
参照: https://remap-keys.app/docs/workbench
|
||||||
|
- VIAのキーボード定義には配置とmatrix座標の対応がある。QMKのVIA実装はキー単位の読み書きにlayer・row・colを使う。
|
||||||
|
参照: https://caniusevia.com/docs/specification/
|
||||||
|
参照: https://github.com/qmk/qmk_firmware/blob/master/quantum/via.c
|
||||||
|
- Vialではタップダンスやコンボを実機へ保存できるが、枠数等にはビルド時の制約がある。
|
||||||
|
参照: https://get.vial.today/manual/tap-dance.html
|
||||||
|
参照: https://get.vial.today/manual/combos.html
|
||||||
|
- ZMK Studioは既存behaviorの割り当てを変更できるが、新しいbehavior定義等は対象外。実機に保存した変更とソース側の設定の扱いにも注意が必要。
|
||||||
|
参照: https://zmk.dev/docs/features/studio
|
||||||
|
|
||||||
|
## 未採用の提案との区別
|
||||||
|
|
||||||
|
共通の動作モデル、ファームウェア固有表現の保持、Staticを先に実装する順序、差分を選んで反映するUIは、
|
||||||
|
アシスタントの提案段階にとどまる。今回の合意として固定しない。
|
||||||
|
Vial・ZMK Studioを次にどの順序で実装するかも決めていない。
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# QMKでのDynamic/Staticの対応範囲
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
当面の対象ファームウェアはQMK。
|
||||||
|
- Dynamic: QMK本家のVIAプロトコルが公開する項目のみ
|
||||||
|
- Static: QMK本家の公式機能のうちVIAで変更できないものは、Firmware Generatorで生成する対象
|
||||||
|
- 非公式機能(Vial等のフォーク、XAP等のmaster以外のブランチ)は当面非対応
|
||||||
|
- developブランチ・Community Modulesは当面非対応(将来広げたい→backlog MAP-P-6)。キーボード・キーマップ固有コードは原則非対応
|
||||||
|
- 対応QMKは特定コミットに固定し、更新タイミングはユーザーが個別に判断する
|
||||||
|
- UIにはVIAプロトコルとキーコード体系のバージョンを表示し、実機とはこの2つで照合。不一致の実機は非対応
|
||||||
|
(当初はUIにコミットを出す案だったが、実機から取れるのがこの2つだけなのでユーザーがバージョン表記に変更)
|
||||||
|
- Staticで定義する動作(タップダンス等)はキーから名前で参照し、出力時に番号へ変換する
|
||||||
|
- VIA管理外のEEPROM設定は読み出せない旨を制約として明記
|
||||||
|
|
||||||
|
どこまでを初期対応するか(P-1)、最初に対応するコミット(P-2)は未決。
|
||||||
|
|
||||||
|
## 案の比較
|
||||||
|
|
||||||
|
| 案 | 扱い |
|
||||||
|
| --- | --- |
|
||||||
|
| DynamicはQMK本家VIAのみ、他の公式機能はStatic | 採用(ユーザー指定) |
|
||||||
|
| VialでDynamicの範囲を広げる(タップダンス・コンボ・QMK Settings等) | 当面非対応(フォークのため) |
|
||||||
|
| XAP | 当面非対応(master外) |
|
||||||
|
| QMKの最新へ自動追従 | 採用しない。ユーザーが恣意的に更新時期を決める |
|
||||||
|
|
||||||
|
## 調査結果(2026-09-23時点のソース)
|
||||||
|
|
||||||
|
実機での検証はしていない。
|
||||||
|
|
||||||
|
### A. QMK本家 VIA
|
||||||
|
参照: https://raw.githubusercontent.com/qmk/qmk_firmware/master/quantum/via.h
|
||||||
|
VIA_PROTOCOL_VERSION = 0x000D
|
||||||
|
|
||||||
|
command:
|
||||||
|
- 0x01 get_protocol_version
|
||||||
|
- 0x02/0x03 get/set_keyboard_value
|
||||||
|
- 0x01 uptime(get), 0x02 layout_options(get/set), 0x03 switch_matrix_state(get),
|
||||||
|
0x04 firmware_version(get), 0x05 device_indication(set), 0x06 keycodes_version(get)
|
||||||
|
- 0x04/0x05 dynamic_keymap_get/set_keycode (layer,row,col)
|
||||||
|
- 0x06 dynamic_keymap_reset
|
||||||
|
- 0x07/0x08/0x09 custom_set/get_value, custom_save
|
||||||
|
- channel 0 custom(キーボード独自メニュー)
|
||||||
|
- 1 backlight: brightness, effect
|
||||||
|
- 2 rgblight: brightness, effect, effect_speed, color
|
||||||
|
- 3 rgb_matrix: brightness, effect, effect_speed, color
|
||||||
|
- 4 audio: enable, clicky_enable
|
||||||
|
- 5 led_matrix: brightness, effect, effect_speed
|
||||||
|
- 0x0A eeprom_reset
|
||||||
|
- 0x0B bootloader_jump
|
||||||
|
- 0x0C..0x10 dynamic_keymap_macro_get_count / get_buffer_size / get_buffer / set_buffer / reset
|
||||||
|
- 0x11 dynamic_keymap_get_layer_count
|
||||||
|
- 0x12/0x13 dynamic_keymap_get/set_buffer
|
||||||
|
- 0x14/0x15 dynamic_keymap_get/set_encoder
|
||||||
|
|
||||||
|
MAPに関係するもの: キーコード、レイヤー数、エンコーダ、マクロ、layout_options。
|
||||||
|
照明・オーディオはMAPの所有データ外(MAPレイヤー.md「持たないもの」)。
|
||||||
|
|
||||||
|
### B. Vial(フォーク、今回非対応)
|
||||||
|
参照: https://raw.githubusercontent.com/vial-kb/vial-qmk/vial/quantum/vial.h
|
||||||
|
参照: https://raw.githubusercontent.com/vial-kb/vial-qmk/vial/quantum/qmk_settings.c
|
||||||
|
VIAL_PROTOCOL_VERSION = 6
|
||||||
|
|
||||||
|
- keyboard_id、圧縮キーボード定義JSONの取得(get_size/get_def)
|
||||||
|
- unlock/lock(物理キー押下による解除)
|
||||||
|
- dynamic entry: tap dance, combo, key override, alt repeat key
|
||||||
|
- 拡張マクロ(EXT_TAP/DOWN/UP)
|
||||||
|
- QMK Settings (QSID):
|
||||||
|
1 grave_esc_override, 2 combo_term, 3 auto_shift, 4 auto_shift_timeout,
|
||||||
|
5 osk_tap_toggle, 6 osk_timeout, 7 tapping_term, 9-17 mousekey各種(MK_3_SPEED以外),
|
||||||
|
18 tap_code_delay, 19 tap_hold_caps_delay, 20 tapping_toggle,
|
||||||
|
21 magic_settings(Ctrl/Caps, Alt/GUI, Ctrl/GUI, Grave/Esc, BSpc/BSlash入替, GUI無効, NKRO),
|
||||||
|
22 permissive_hold, 23 hold_on_other_key_press, 24 retro_tapping,
|
||||||
|
25 quick_tap_term, 26 chordal_hold, 27 flow_tap_term
|
||||||
|
- VialRGB(RGB Matrixエフェクト列挙・設定)。ソース未確認
|
||||||
|
|
||||||
|
### C. プロトコル外で実機上だけ変わるもの(Dynamicの定義外)
|
||||||
|
当初「すべてEEPROMに保存される」と書いたが誤り。確認結果:
|
||||||
|
- 保存されない(再起動で消える): ダイナミックマクロ(docs/features/dynamic_macros.md
|
||||||
|
"lost when the keyboard is unplugged or otherwise rebooted")、DT_UP/DT_DOWN(docs/tap_hold.md "*not* persistent")
|
||||||
|
- EEPROMに保存される(quantum/eeconfig.h): keymap config(Magic)、default layer、unicode mode、audio、backlight、
|
||||||
|
rgblight/rgb_matrix/led_matrix、haptic、steno mode、handedness、connection、kb/user固有データ
|
||||||
|
- QMK本家via.cにはこれらを読み出すコマンドがない。照明・オーディオはcustom channel 1〜5で読めるがMAPの所有外。
|
||||||
|
custom channel 0はweakの via_custom_value_command_kb() へ流れる(キーボード固有コード、原則非対応)。
|
||||||
|
- 参考: via_eeprom_is_valid() はビルド日時由来のmagicで検証し、不一致ならVIAのデータ(キーマップ・マクロ・layout options)を
|
||||||
|
初期化する。再ビルドして書き込むとDynamicの変更は消える前提になる。
|
||||||
|
|
||||||
|
### XAP
|
||||||
|
QMK公式の次世代プロトコルとして開発されていたが、masterには入っていない認識。未確認。
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# GeneratorのC出力とユーザーのCによる上書き
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
未決。backlog MAP-P-9 として保留(ユーザー指定)。フック方式が有力(ユーザー「フック方式いいかも」)。
|
||||||
|
JSONを第一級にしてCを自動生成し、ユーザーのC/hで一部を上書きできるようにしたい、というのがユーザーの意向。
|
||||||
|
|
||||||
|
## 案の比較
|
||||||
|
|
||||||
|
| 案 | 内容 | 所見 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| (b) フック | 生成関数の先頭でユーザーのフックを呼び、未処理ならJSONの値 | キー単位の例外だけ書ける。JSONが正本のまま |
|
||||||
|
| (a) 関数ごと置き換え(JSONで宣言) | 宣言された関数はGeneratorが出力しない | 単純。その関数の範囲ではJSONの値が無効になる |
|
||||||
|
| (a') 関数ごと置き換え(hのプロトタイプで宣言) | ユーザーのhにあるプロトタイプの関数を出力しない(ユーザー案) | (a)と同じ性質。Generatorがhを解析する必要 |
|
||||||
|
| weak属性で上書き | 生成側をweakにしてユーザーの定義で上書き | 使えない(下記) |
|
||||||
|
| ユーザーのCを解析して自動判定 | — | 壊れやすい |
|
||||||
|
|
||||||
|
## 根拠(QMK 0.34.4)
|
||||||
|
|
||||||
|
- keymap.jsonで書けるもの: keyboard, keymap, layout, layers, encoders, dip_switches, macros, keycodes(名前宣言), config, modules,
|
||||||
|
host_language 等(data/schemas/keymap.jsonschema)。タップダンス・コンボ・キーオーバーライド・キーごとの設定はない。
|
||||||
|
- キーごとの設定は `*_PER_KEY` を定義した上での `get_tapping_term(keycode, record)` 等のコールバック。keyrecordを受けるので
|
||||||
|
キーコード単位でも位置単位でも書ける。
|
||||||
|
- keymap.jsonとkeymap.cを並べると、json2cで生成したkeymap.cがユーザーのkeymap.cを取り込む
|
||||||
|
(builddefs/build_keyboard.mk:148-157、OTHER_KEYMAP_C)。QMK自体がJSON + ユーザーCの共存に対応している。
|
||||||
|
- 取り込みは同一翻訳単位なので、生成側weak + ユーザー側strongは再定義エラーになる。QMK本体のweak既定実装もあるため、
|
||||||
|
別ファイルにしても生成側をweakにする方式は選ばれる定義が不定。
|
||||||
|
- プロトタイプの再宣言はCでは合法(同一シグネチャなら何度でも)。QMKが宣言済みの関数をユーザーのhに書いてもよい。
|
||||||
|
シグネチャ違いはコンパイルエラーになるので検出にも使える。
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# IMEレイヤーの再定義(HST)と線引き
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
- IMEレイヤーをHST(ホスト)レイヤーに改める。役割は、ホストがキーコードをどう解釈するかの記述。IMEの変換はOSの仕事で対象外。
|
||||||
|
- データは、ホスト環境ごとのプロファイルを集めた共有カタログ。キーボードに依存しない。プロジェクトはプロファイルのIDと差分だけを持つ。
|
||||||
|
- 識別はOS×ホスト配列(+必要なら入力方式)。`win-jp` などはプリセット名。
|
||||||
|
- プロファイルは順方向の表 f(キーコード, 修飾キー, ホスト状態) → 文字/動作/未定義/不明、成立の前提、対応可否(3値)だけを持つ。
|
||||||
|
- 逆引き・候補の生成・ファームウェアでの実現はMAPエディタとGeneratorが行う。ユーザーは、これが実現できるかに不安がある → HSTのP-1。
|
||||||
|
- MAPはキーコードを保存する。逆引きは入力補助で、一方向の変換。
|
||||||
|
- 複数ホストの扱いは未決 → HSTのP-2。
|
||||||
|
|
||||||
|
## 案の比較
|
||||||
|
|
||||||
|
| 論点 | 当初の提案(人との議論) | Claude | Codex | 採用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 名前 | SYS | HOST/HST | SYS → 最終的にHOST/HSTに同意 | HST |
|
||||||
|
| 識別子 | WIN-JAP (OS×言語) | OS×配列の構造 | OS×配列×入力方式+プロファイル名 | OS×配列(+入力方式) |
|
||||||
|
| 範囲 | OS・言語ごとのキーの振る舞い・表示 | ホストの解釈の記述 | 意図を環境ごとの出力に変換、まで候補 → 最終的に「対応関係を提供、変更はしない」 | 記述のみ |
|
||||||
|
| 逆引き | - | 使う側で順方向の表から求める | 候補と成立条件をプロファイルが持つ | 使う側 |
|
||||||
|
| ホスト状態 | - | 関数の引数として記述 | 設定と実行時の状態を分ける | 両方(矛盾しない) |
|
||||||
|
|
||||||
|
## 根拠・経緯
|
||||||
|
|
||||||
|
- SYSは、HIDのSystem Control用途ページ(電源・スリープ)や「システム設定全般」と紛らわしい。hostはHIDでキーボードの接続相手を指す用語で、QMKも「host keyboard layout」と呼ぶ。
|
||||||
|
- 言語は軸として誤り: 日本語Windowsで英語配列を使える。MacではUS/JISが入力ソースとキーボード種別で決まる。XKBはlayout/variant/optionsの3要素。
|
||||||
|
「英語」にもUS・UKがある。`JAP` はISOコードでもない(ja/jpn)。
|
||||||
|
- 逆引きをプロファイルに持たせない理由: (1) 順方向の表から導けるので二重管理になる。
|
||||||
|
(2) 候補がファームウェアの機能に依存する。例: US刻印+JISホストでShiftの組が合わないキーにはKey Overrideが要る。HSTにQMKの知識が混ざる。
|
||||||
|
- Codexの指摘で採用したもの: 表示の切り替えと送信内容の変更を混ぜない。知らない動作を「使えない」と扱わない(対応可否を3値にする)。OS設定への依存は記述する。
|
||||||
|
- MAPの正本をキーコードにする理由の修正: Claudeは当初「VIAでは実機にキーコードしか残らず、意図を復元できないので、キーコードでなければならない」とした。
|
||||||
|
Codexが、意図をプロジェクト側に残す設計も可能で、難しくなるのは実機で変更された値の取り込みだけだと反論し、これが正しい。
|
||||||
|
採用する理由は「実機との往復を単純に保てるから」とする。
|
||||||
|
- 「コピー」のような操作の辞書は、アプリやユーザー設定まで相手が広がるので対象外。
|
||||||
|
- 未採用のアイデア: キートップの刻印生成にHSTを使う(PHYのラベルへ流す)。検討していないので仕様には書かない。
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
# QMK 0.34.4 の機能棚卸し(MAP P-2 の下調べ)
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
- 対象: QMK `0.34.4`(tag, commit `08c662f286ddfd12a985f57b584b02eca5af0ae6`, 2026-09-05)。masterの最新リリース。
|
||||||
|
ユーザーが最初の対応環境として0.34.4を採用(2026-09-24)。
|
||||||
|
- キーコード体系 `QMK_KEYCODES_VERSION` = `0.0.9`(quantum/keycodes.h)
|
||||||
|
- `VIA_PROTOCOL_VERSION` = `0x000D`(quantum/via.h。9/23時点の調査と同じ)
|
||||||
|
- MAPから見たQMKの機能は次の6種類に分かれる(下の表)。スキーマ・編集機能への落とし込みは未着手。
|
||||||
|
- 既存ドキュメントに影響しうる事実が2つある:
|
||||||
|
1. **VIAのモジュール化**(20260830 ChangeLog「VIA Feature Migration」#26315)。VIA統合の保守をVIAチームへ移し、
|
||||||
|
旧来のfeature実装は将来のリリースで削除予定と告知済み。PR本体はdevelop向けのDraftで、0.34.4時点のVIAは従来の
|
||||||
|
`quantum/via.c`。MAPレイヤー.md は「Dynamic = QMK本家のVIA」「Community Modules は当面非対応」としているため、
|
||||||
|
VIAがモジュール側へ移った時点でこの2つが衝突する。
|
||||||
|
2. **VIAのマトリクス状態取得の無効化**(20250831 ChangeLog #25414)。`id_switch_matrix_state` は既定で全0を返す。
|
||||||
|
`VIA_INSECURE` 定義時、または `SECURE_ENABLE` でアンロック中のみ実値。Remap等のキーテスターは既定では動かない。
|
||||||
|
- QMKのキーごとの設定(tapping term等)はデータテーブルではなく、keycode/keyrecordを受けるCのコールバック関数で表現される。
|
||||||
|
keymap.json(data-driven)で書けるのはレイヤー・エンコーダ・DIPスイッチ・マクロ・カスタムキーコード・config・modules まで。
|
||||||
|
タップダンス・コンボ・キーオーバーライド・キーごとの設定はCでしか書けない。
|
||||||
|
|
||||||
|
## 分類の概要
|
||||||
|
|
||||||
|
| 区分 | 主なもの | 実機への保存 | VIAで読み書き |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| A. キーコード(単体で完結) | 基本キー、修飾付き、MT/LT/OSM等、レイヤー操作、Magic、メディア、マウス等 | キーマップ | ○(16bit値として) |
|
||||||
|
| B. 定義を参照するキーコード | TD(n)、QK_MACRO_n、UM/UP、QK_KB_n/QK_USER_n | 参照はキーマップ、定義はファームウェア | 参照のみ○。定義はマクロのみ○ |
|
||||||
|
| C. キー以外の起動条件を持つ動作 | コンボ、キーオーバーライド、リーダー、オートコレクト、Tri Layer | ファームウェア | × |
|
||||||
|
| D. タップ・ホールド等の挙動設定 | TAPPING_TERM等 + キーごとのコールバック | ファームウェア(DTTの変更は揮発) | × |
|
||||||
|
| E. キー以外の入力への割り当て | エンコーダ、DIPスイッチ、ポインティング、ジョイスティック等 | エンコーダはキーマップ | エンコーダのみ○ |
|
||||||
|
| F. 実機の永続状態 | Magic設定、保存されたデフォルトレイヤー、Unicodeモード等 | EEPROM | × |
|
||||||
|
|
||||||
|
MAPの所有外(照明・オーディオ・ディスプレイ・ハードウェア構成)は末尾にまとめた。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A. キーコード体系 0.0.9
|
||||||
|
|
||||||
|
data/constants/keycodes/*.hjson を 0.0.1〜0.0.9 の順にマージして得た範囲。
|
||||||
|
|
||||||
|
| 範囲 | 名前 | 内容 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 0x0000–0x00FF | QK_BASIC | KC_NO/KC_TRNS、HID基本キー(basic 161件)、system 3、media 27、mouse 19(QK_MOUSE_*)、修飾8 |
|
||||||
|
| 0x0100–0x1FFF | QK_MODS | `LCTL(kc)` 等。kcは基本キーのみ、修飾は5bit(左右混在不可) |
|
||||||
|
| 0x2000–0x3FFF | QK_MOD_TAP | `MT(mod, kc)`。kcは基本キーのみ |
|
||||||
|
| 0x4000–0x4FFF | QK_LAYER_TAP | `LT(layer, kc)`。layer 0–15、kcは基本キーのみ |
|
||||||
|
| 0x5000–0x51FF | QK_LAYER_MOD | `LM(layer, mod)`。layer 0–15 |
|
||||||
|
| 0x5200– | QK_TO / MOMENTARY / DEF_LAYER / TOGGLE_LAYER / ONE_SHOT_LAYER / ONE_SHOT_MOD / LAYER_TAP_TOGGLE / PERSISTENT_DEF_LAYER | TO/MO/DF/TG/OSL/OSM/TT/PDF。各32(layer 0–31) |
|
||||||
|
| 0x5600–0x56FF | QK_SWAP_HANDS | SH_T(kc)、SH_TOGG等7件 |
|
||||||
|
| 0x5700–0x57FF | QK_TAP_DANCE | TD(0–255) |
|
||||||
|
| 0x7000–0x70FF | QK_MAGIC | Ctrl/Caps、Alt/GUI、Ctl/GUI、Grave/Esc、BSpc/BSlash、Esc/Caps入替、GUI無効、NKRO、EE_HANDS 等35件 |
|
||||||
|
| 0x7100–0x71FF | QK_MIDI | ノート・オクターブ・移調・ベロシティ・チャンネル等 |
|
||||||
|
| 0x7200–0x73FF | QK_SEQUENCER | 9件 |
|
||||||
|
| 0x7400–0x743F | QK_JOYSTICK | ボタン0–31 |
|
||||||
|
| 0x7440–0x747F | QK_PROGRAMMABLE_BUTTON | 1–32 |
|
||||||
|
| 0x7480–0x74BF | QK_AUDIO | Audio/Clicky/Music 15件 |
|
||||||
|
| 0x74C0–0x751F | QK_STENO | 0.0.9で拡張・改名(STN_* → QK_STENO_* / ST_*、旧名はエイリアス) |
|
||||||
|
| 0x7700–0x777F | QK_MACRO | QK_MACRO_0–31(名前付き)。VIA/keymap.jsonのマクロ |
|
||||||
|
| 0x7780–0x77BF | QK_CONNECTION | 出力先切替(USB/2.4GHz/BT)、BTプロファイル1–5等15件 |
|
||||||
|
| 0x77C0–0x77FF | QK_COMMUNITY_MODULE | Community Modules用 |
|
||||||
|
| 0x7800–0x78FF | QK_LIGHTING | Backlight 7、LED Matrix 11、RGBLight(underglow)11 + RGB_MODE_* 10、RGB Matrix 15 |
|
||||||
|
| 0x7C00–0x7DFF | QK_QUANTUM | 下記 |
|
||||||
|
| 0x7E00–0x7E3F | QK_KB | QK_KB_0–31(キーボード固有) |
|
||||||
|
| 0x7E40–0x7FFF | QK_USER | QK_USER_0–31(キーマップ固有。SAFE_RANGE以降) |
|
||||||
|
| 0x8000–0xFFFF | QK_UNICODE / UNICODEMAP / UNICODEMAP_PAIR | UC(cp≤0x7FFF)、UM(i)、UP(i,j) |
|
||||||
|
|
||||||
|
QK_QUANTUM の内訳(73件): Bootloader/Reboot/Debug/EEPROMクリア/Make、Auto Shift操作6、Grave Escape、
|
||||||
|
Velocikey、Space Cadet 7、Unicodeモード切替8、Haptic 13、Combo on/off/toggle、Dynamic Macro 5、Leader、
|
||||||
|
Key Lock、One Shot on/off/toggle、Key Override on/off/toggle、Secure 4、Dynamic Tapping Term 3、Caps Word、
|
||||||
|
Autocorrect 3、Tri Layer Lower/Upper、Repeat/Alt Repeat、Layer Lock。
|
||||||
|
|
||||||
|
補足:
|
||||||
|
- 20250831で旧RGB・マウスの非推奨キーコードを削除(#25444)。
|
||||||
|
- VIAはキーマップを16bit値で読み書きするため、上のどの値もキーへ書ける。ファームウェアで無効な機能の値は何もしない。
|
||||||
|
|
||||||
|
## B. 定義を参照するキーコード
|
||||||
|
|
||||||
|
| 機能 | キーコード | 定義の中身 | 定義の置き場所 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Tap Dance | TD(n) | `ACTION_TAP_DANCE_DOUBLE(kc1,kc2)` / `_LAYER_MOVE(kc,layer)` / `_LAYER_TOGGLE(kc,layer)` / `_FN` / `_FN_ADVANCED` / `_FN_ADVANCED_WITH_RELEASE`。組み込みは前3つで、残りはC関数 | C(`tap_dance_actions[]`) |
|
||||||
|
| マクロ | QK_MACRO_n | tap/down/up/delay(+keymap.jsonではbeep)と文字列。VIAの保存形式は SS_TAP/DOWN/UP/DELAY_CODE を含むバイト列 | keymap.json `macros` またはVIA(EEPROM)。VIA既定16個 |
|
||||||
|
| Unicode Map | UM(i), UP(i,j) | コードポイント表 | C(`unicode_map[]`) |
|
||||||
|
| 固有キーコード | QK_KB_n, QK_USER_n | 任意のC処理 | C(`process_record_*`)。keymap.json `keycodes` で名前宣言のみ可 |
|
||||||
|
| Send String | (キーコードなし) | 文字列送信。マクロやC処理から呼ぶ | C |
|
||||||
|
|
||||||
|
## C. キー以外の起動条件を持つ動作
|
||||||
|
|
||||||
|
| 機能 | 定義 | 主な設定 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Combos | キーコード列(最大8、EXTRA_LONGで16)→出力キーコード | COMBO_TERM、per-combo: term/must_hold/must_tap/must_press_in_order、COMBO_STRICT_TIMER、COMBO_ONLY_FROM_LAYER / COMBO_REF_DEFAULT(参照レイヤー)、Modifier Combos、キー解放・再押下の扱い |
|
||||||
|
| Key Overrides | trigger, trigger_mods, layers(ビットマスク), negative_mod_mask, suppressed_mods, replacement, options(ko_option_t), custom_action, enabled | MODS_TO_NEUTRALIZE |
|
||||||
|
| Leader Key | QK_LEADER の後のキー列(1〜5キー)→C処理 | LEADER_TIMEOUT、LEADER_PER_KEY_TIMING、LEADER_NO_TIMEOUT、STRICT_KEY_PROCESSING |
|
||||||
|
| Autocorrect | 誤字→訂正の辞書(ビルド時にtrieへ変換) | 辞書ファイル、MIN/MAX_LENGTH |
|
||||||
|
| Tri Layer | lower+upper → adjust レイヤー | 3レイヤーの番号 |
|
||||||
|
| Caps Word / Repeat Key / Layer Lock / Key Lock / Swap Hands | 定義データはほぼなし。Swap Handsは `hand_swap_config`(マトリクス対応表)が必要 | Caps Wordの継続キー判定等はC |
|
||||||
|
|
||||||
|
## D. タップ・ホールド等の挙動設定(docs/tap_hold.md 他)
|
||||||
|
|
||||||
|
全体設定(config.h / keyboard.jsonのconfig)とキーごとのコールバックの組で提供される。
|
||||||
|
|
||||||
|
| 設定 | 全体 | キーごと |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Tapping Term | TAPPING_TERM | TAPPING_TERM_PER_KEY → get_tapping_term(keycode, record) |
|
||||||
|
| Dynamic Tapping Term | DYNAMIC_TAPPING_TERM_ENABLE(キーで増減、揮発) | — |
|
||||||
|
| Permissive Hold | PERMISSIVE_HOLD | get_permissive_hold |
|
||||||
|
| Hold On Other Key Press | HOLD_ON_OTHER_KEY_PRESS | get_hold_on_other_key_press |
|
||||||
|
| Quick Tap Term | QUICK_TAP_TERM | get_quick_tap_term |
|
||||||
|
| Flow Tap(2025-05追加) | FLOW_TAP_TERM | is_flow_tap_key、get_flow_tap_term(keycode, record, prev_keycode) |
|
||||||
|
| Chordal Hold(2025-02追加) | CHORDAL_HOLD + 左右手の割り当て(chordal_hold_layout) | get_chordal_hold(tap_hold_keycode, …, other_keycode, …) |
|
||||||
|
| Retro Tapping | RETRO_TAPPING | get_retro_tapping |
|
||||||
|
| Speculative Hold(2025-11追加) | SPECULATIVE_HOLD | get_speculative_hold |
|
||||||
|
| Tapping Force Hold | — | get_tapping_force_hold |
|
||||||
|
| Tapping Toggle | TAPPING_TOGGLE(TTのタップ回数) | — |
|
||||||
|
| Auto Shift | AUTO_SHIFT_TIMEOUT 等 | get_autoshift_timeout、get_auto_shifted_key、REPEAT/NO_AUTO_REPEAT_PER_KEY |
|
||||||
|
| One Shot | ONESHOT_TAP_TOGGLE、ONESHOT_TIMEOUT | — |
|
||||||
|
| Combo | COMBO_TERM | get_combo_term 等(コンボごと) |
|
||||||
|
| その他 | TAP_CODE_DELAY、TAP_HOLD_CAPS_DELAY、DEBOUNCE | — |
|
||||||
|
|
||||||
|
- キーごとのコールバックは keycode と keyrecord(押された位置・レイヤーを含む)を受ける。
|
||||||
|
つまり「キーコード単位」「物理位置単位」のどちらでも書ける(docs/tap_hold.md「Why do we include the key record」)。
|
||||||
|
- Chordal Holdの手の割り当ては物理位置(マトリクス)単位のデータ。
|
||||||
|
|
||||||
|
## E. キー以外の入力への割り当て
|
||||||
|
|
||||||
|
| 機能 | MAP相当のデータ | 備考 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Encoders | ENCODER_MAP: レイヤーごと・エンコーダごとの ccw/cw キーコード | keymap.json `encoders`。VIAで読み書き可(0x14/0x15) |
|
||||||
|
| DIP Switch | DIP_SWITCH_MAP: スイッチごとの on/off キーコード | keymap.json `dip_switches`(2025-11追加)。VIA不可 |
|
||||||
|
| Pointing Device | Auto Mouse Layer(レイヤー番号・タイムアウト)、ドラッグスクロール等はC | センサー設定はハードウェア寄り |
|
||||||
|
| Joystick / Digitizer / MIDI / Steno / Programmable Button / Sequencer | キーコードでボタン・音等を割り当て。軸・プロトコル設定は全体 | Stenoは0.0.9でキーコード改名 |
|
||||||
|
|
||||||
|
## F. 実機の永続状態(quantum/eeconfig.h)
|
||||||
|
|
||||||
|
VIA管理外でEEPROMに保存されるもの: debug、keymap config(Magic: Ctrl/Caps入替、Caps→Ctrl、Alt/GUI入替、GUI無効、Grave/Esc、BSpc/BSlash、NKRO 他)、
|
||||||
|
default layer(PDF)、Unicode入力モード、audio、backlight、rgblight/rgb_matrix/led_matrix、haptic、steno mode、
|
||||||
|
handedness(EE_HANDS)、connection、kb/user固有データ、Community Modules の永続データ(1.1.3で追加)。
|
||||||
|
QMK本家VIAにはこれらを読み出すコマンドがない(9/23調査の再確認)。
|
||||||
|
|
||||||
|
## MAP所有外の機能(参考の一覧)
|
||||||
|
|
||||||
|
- 照明: Backlight、RGBLight、RGB Matrix、LED Matrix、LED Indicators
|
||||||
|
- 表示: OLED、Quantum Painter(LVGL含む)、ST7565、HD44780
|
||||||
|
- 音・触覚: Audio、Haptic Feedback
|
||||||
|
- ハードウェア構成: Split Keyboard、Custom Matrix、Debounce、Converters、PS/2 Mouse、Wireless、Battery、USBPD
|
||||||
|
- ホスト連携: Raw HID、OS Detection、Secure、Command、Bootmagic、WPM、Deferred Exec、Virtser、Plover HID
|
||||||
|
- 拡張: Community Modules(1.1.3。modules/qmk/ に hello_world, super_alt_tab, split_data_sync, nvm_test, flow_*_matrix_effect)
|
||||||
|
|
||||||
|
## VIA 0x000D の読み書き範囲(0.34.4で再確認)
|
||||||
|
|
||||||
|
9/23の journal(qmk-dynamic-scope)と同じコマンド構成。差分・補足:
|
||||||
|
- `id_switch_matrix_state` は既定で0を返す(上記)。
|
||||||
|
- `id_eeprom_reset` は `VIA_EEPROM_ALLOW_RESET` 定義時のみ有効。
|
||||||
|
- エンコーダは `ENCODER_MAP_ENABLE` 時のみ。
|
||||||
|
- 既定: DYNAMIC_KEYMAP_LAYER_COUNT 4、DYNAMIC_KEYMAP_MACRO_COUNT 16。上限はEEPROM容量に従う。
|
||||||
|
|
||||||
|
## 調査方法
|
||||||
|
|
||||||
|
- `git clone --depth 1 --branch 0.34.4 --filter=blob:none --sparse` で docs / quantum / data / builddefs を取得。
|
||||||
|
- 機能一覧: docs/_sidebar.json、builddefs/generic_features.mk の GENERIC_FEATURES。
|
||||||
|
- キーコード: data/constants/keycodes/keycodes_*.hjson をバージョン順にマージ。
|
||||||
|
- 変更点: docs/ChangeLog/20250223〜20260830。
|
||||||
|
- VIA移行PR: https://github.com/qmk/qmk_firmware/pull/26315(develop向けDraft。VIA_EEPROM_MAGIC_ADDR 以外は後方互換を維持する方針と記載)
|
||||||
|
- 実機での検証はしていない。
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# VIAのモジュール化への追従
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
- QMKはVIA統合をCommunity Modules形式へ移し、旧実装を将来削除すると告知(20260830 ChangeLog「VIA Feature Migration」#26315)。
|
||||||
|
- 方針「QMKとVIAの両方に追従する」は変えない(ユーザー判断)。
|
||||||
|
→ Community Modulesは当面非対応のままだが、VIAモジュールだけは例外として、移行がmasterに入る前に対応する。
|
||||||
|
最悪、Community Modulesへの対応はVIAだけでもよい(ユーザー発言)。
|
||||||
|
- キーテスター(switch_matrix_state)が既定で動かない件は気にしない(ユーザー判断)。
|
||||||
|
|
||||||
|
## 案の比較
|
||||||
|
|
||||||
|
| 案 | 扱い |
|
||||||
|
| --- | --- |
|
||||||
|
| VIAモジュールだけCommunity Modules非対応の例外にする | 採用 |
|
||||||
|
| Community Modules全般に対応を広げる | MAP-P-6のまま保留 |
|
||||||
|
| 旧VIA実装が残るQMKに固定し続ける | 追従方針に反するので採用しない |
|
||||||
|
|
||||||
|
## 根拠(2026-09-24時点)
|
||||||
|
|
||||||
|
- docs/ChangeLog/20260830.md(QMK 0.34.4):
|
||||||
|
"upkeep of the VIA integration will transition to the VIA team" /
|
||||||
|
"in a future release, the old VIA feature-based implementation is expected to be removed."
|
||||||
|
- PR https://github.com/qmk/qmk_firmware/pull/26315 : develop向けDraft。追加ファイルは
|
||||||
|
modules/qmk/via/{qmk_module.json, via.c, via.h, post_config.h, post_rules.mk}。
|
||||||
|
qmk_module.json にバージョン欄なし。VIA_EEPROM_MAGIC_ADDR 以外は後方互換を維持すると記載。
|
||||||
|
- 0.34.4時点のVIAは従来どおり quantum/via.c、VIA_PROTOCOL_VERSION 0x000D。
|
||||||
|
- 同梱モジュールの有効化は keymap.json の `"modules": ["qmk/via"]` の形(data/schemas/keymap.jsonschema の modules、
|
||||||
|
builddefs/build_keyboard.mk の generate-community-modules-*)。対応コストは小さい見込み。
|
||||||
|
- 保守がVIAチームへ移るため、将来QMKリポジトリ外へ出る可能性はあるが未確認。
|
||||||
|
|
||||||
|
## 追記: 対応環境の指定方法
|
||||||
|
|
||||||
|
結論: 対応環境 = QMKのコミット + 使うモジュールの一覧(出所とコミット)。VIAは一覧の1項目。
|
||||||
|
QMK同梱モジュールはQMKのコミットで決まる。実機との照合は従来どおりVIAプロトコル・キーコード体系のバージョン。
|
||||||
|
|
||||||
|
| 案 | 扱い |
|
||||||
|
| --- | --- |
|
||||||
|
| QMKのコミット + モジュール一覧 | 採用。VIAが外部へ出てもMAP-P-6の他モジュールでも同じ形 |
|
||||||
|
| QMKのコミット + VIAのバージョンだけ追加 | VIAの特別扱いになるため採用しない |
|
||||||
|
| QMKのコミットだけ(現状) | VIAがリポジトリ外へ出ると実装が決まらない |
|
||||||
|
|
||||||
|
- きっかけ: ユーザーが「QMKに加えてVIAのバージョンも明記したほうがよいか」と相談。
|
||||||
|
- #26315の案ではVIAは modules/qmk/via(QMK同梱)で、qmk_module.json にバージョン欄がない。
|
||||||
|
この案のままならQMKのコミットだけで決まるが、保守移管後に外へ出る可能性に備える。
|
||||||
|
- モジュールのバージョンは実機から取得できないため、照合には使わない。
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# VIAで読み書きする項目と機能・容量の確認方法
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
- 対応: バージョン読み出し、レイヤー数、キーコード割り当て(まとめ読み書き含む)、マクロ
|
||||||
|
- 別課題: エンコーダ(MAP-P-4)、layout option(P-3)
|
||||||
|
- 当面非対応: リセット系・ブートローダ移行、デバイスの識別表示、キーテスター、custom channel
|
||||||
|
- 有効な機能: 新規キーボードはGeneratorのビルド設定を正とする。既存キーボードは利用者の申告を信用する(ユーザー案)。
|
||||||
|
申告はGeneratorの設定と同じ形式にし、新規と既存を1つのモデルで扱う。
|
||||||
|
- 容量(レイヤー数・マクロ個数・容量)は実機から読んで照合する。
|
||||||
|
- P-2は判断が済んだので、実機で未検証という推測(P-2)に置き換えた。
|
||||||
|
|
||||||
|
## 案の比較(VIA 0x000D, QMK 0.34.4 quantum/via.c)
|
||||||
|
|
||||||
|
| 項目 | コマンド | 扱い | 理由 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| プロトコル・キーコード体系バージョン | 0x01, 0x02 id 0x06 | 対応 | 実機照合に必須 |
|
||||||
|
| レイヤー数 | 0x11 | 対応 | 容量確認 |
|
||||||
|
| キーコード | 0x04/0x05, 0x12/0x13 | 対応 | MAPの本体 |
|
||||||
|
| マクロ | 0x0C–0x10 | 対応 | P-1で内容をMAPが持つとした |
|
||||||
|
| エンコーダ | 0x14/0x15 | 別課題 | MAP-P-4 |
|
||||||
|
| layout options | 0x02/0x03 id 0x02 | 別課題 | P-3の排他キーの読み戻しと一体 |
|
||||||
|
| リセット・ブートローダ | 0x06, 0x0A, 0x0B | 当面非対応 | 操作でありMAPのデータではない。0x0Aは VIA_EEPROM_ALLOW_RESET 時のみ |
|
||||||
|
| デバイスの識別表示 | 0x03 id 0x05 | 当面非対応 | 任意の補助機能 |
|
||||||
|
| キーテスター | 0x02 id 0x03 | 非対応 | 既定で全0(#25414)。ユーザーも不要と判断 |
|
||||||
|
| custom channel | 0x07–0x09 | 非対応 | 照明・オーディオはMAP所有外、channel 0は固有コード |
|
||||||
|
|
||||||
|
## 機能・容量の確認
|
||||||
|
|
||||||
|
- VIAから読めるのは容量(レイヤー数、マクロ個数、マクロバッファサイズ)とバージョンだけ。
|
||||||
|
どの機能を有効にしてビルドしたか(タップダンス、コンボ等)は読めない。キーボード固有のVIA定義JSONにも含まれない。
|
||||||
|
- 無効な機能のキーコードを書いてもエラーにならず、実機で何もしないだけ。申告の誤りは壊れる方向には働かない。
|
||||||
|
- 既存キーボードは初期対応の前提ではない(MAPレイヤー.md「概要」)が、申告方式ならモデルを増やさずに扱える。
|
||||||
+77
-43
@@ -11,42 +11,60 @@
|
|||||||
### 設計思想
|
### 設計思想
|
||||||
|
|
||||||
- 「Better KLE」ではなく「Beyond KLE」: UIではなくデータモデルとワークフロー全体の再設計
|
- 「Better KLE」ではなく「Beyond KLE」: UIではなくデータモデルとワークフロー全体の再設計
|
||||||
- UNIX哲想の適用: 小さな役割のツールがそれぞれしっかり機能する
|
- UNIX哲学の適用: 小さな役割のツールがそれぞれしっかり機能する
|
||||||
- 統合環境(IDE)の提供: 独立したツール群をオーケストレート
|
- 統合環境(IDE)の提供: 独立したツール群をオーケストレート
|
||||||
|
|
||||||
## レイヤー構造
|
## レイヤー構造
|
||||||
|
|
||||||
|
レイヤーは一列に積み上がらない。ELC・IDX・MAPはそれぞれ独立にPHYを参照する。
|
||||||
|
ELCはIDXを参照せず、IDXもELCを参照しない。MAPもIDXを参照しない。
|
||||||
|
他レイヤーからキーや部品を指す参照には、PHYのノードIDを使う。
|
||||||
|
|
||||||
### 1. PHYレイヤー (物理配置)
|
### 1. PHYレイヤー (物理配置)
|
||||||
|
|
||||||
- **役割**: キーの物理的な位置、サイズ、回転
|
- **役割**: キーと部品の物理的な位置、サイズ、回転
|
||||||
- **対応ツール**: PCBエディタ、KLE
|
- **対応ツール**: PCBエディタ、KLE
|
||||||
- **出力**: 座標とキーサイズの配列
|
- **出力**: 相対座標の木(キー・クラスター・部品ノード・バリアントノード)。各ノードはIDを持つ
|
||||||
|
- **備考**: 交換可能なレイアウト(ISO Enter等)は全パターンを同時に持ち、外部出力時に1つ選んで畳む
|
||||||
|
|
||||||
### 2. ELCレイヤー (電気配線)
|
### 2. ELCレイヤー (電気配線)
|
||||||
|
|
||||||
- **役割**: マイコンのpinと各キーの対応
|
- **役割**: ピン提供者が提供する論理ピンと、各キーの端子・LEDの対応
|
||||||
|
- **入力**: PHYのノードID
|
||||||
- **対応ツール**: 回路図エディタ、ファームウェア
|
- **対応ツール**: 回路図エディタ、ファームウェア
|
||||||
- **出力**: ピン配列定義、マトリックス配線情報
|
- **出力**: 提供者定義、キーごとの端子の接続先、LEDチェーン
|
||||||
- **備考**: charlieplexingなど非標準的な実装にも対応
|
- **備考**: 配線方式(マトリクス / direct / charlieplexing)を区分として選ばせず、端子の接続先の組み合わせとして表すため、非標準的な実装も同じ形で書ける
|
||||||
|
- **備考**: row / col は正本に持たない。QMKなどが要求する形は出力時に割り当てる
|
||||||
|
|
||||||
### 3. IDXレイヤー (キー識別)
|
### 3. IDXレイヤー (キー識別)
|
||||||
|
|
||||||
- **役割**: 物理位置と"キー番号"の対応付け
|
- **役割**: 全バリアントにわたるPHYのキーの並び順を保存し、選択した構成の出力用キー番号を決める
|
||||||
- **重要性**: 物理世界と論理世界を繋ぐ最も重要な抽象化層
|
- **入力**: PHYのキーID
|
||||||
- **出力**: `{x, y, matrix:[row,col], label}[]`
|
- **対応ツール**: QMKの `LAYOUT` マクロ、ZMKの matrix transform、KMKの `coord_mapping`
|
||||||
- **備考**: 現状のエコシステムで最も欠けている層
|
- **出力**: 全バリアントの対象キーIDを重複なく並べた `order`。出力時に対象外のキーを除き、0から連続する番号を与える
|
||||||
|
- **備考**: キー番号はPHYの幾何からもELCの配線からも導出しない(導出すると、幾何の編集や再配線でキーマップがずれる)
|
||||||
|
- **備考**: 並び順を正本として保存する。出力番号は公開インターフェースであり、互換性は出力レイアウトごとに扱う
|
||||||
|
- **備考**: マトリックス座標とLEDの識別番号はIDXが持たない(どちらも配線で決まるためELCが持つ)
|
||||||
|
- **備考**: 全バリアントで1つのIDXを持つ。同じ接続先に配線された排他キーも別々の要素とし、選択後の出力で番号を詰める。保存した `order` は変えない
|
||||||
|
- **備考**: 既存のエコシステムでは、この並びが `LAYOUT` マクロの引数順などとして手書きされている。欠けている層ではなく、形式化されていない層
|
||||||
|
|
||||||
### 4. MAPレイヤー (キーマップ)
|
### 4. MAPレイヤー (キーマップ)
|
||||||
|
|
||||||
- **役割**: キー番号とキーコードの対応
|
- **役割**: キーマップレイヤーごとのキーへの割り当て
|
||||||
- **対応ツール**: ファームウェア、VIA、Remap
|
- **入力**: PHYのキーID
|
||||||
- **出力**: レイヤー別キーコード配列
|
- **対応ツール**: MAPエディタ (Keymap Designer)
|
||||||
|
- **出力**: MAP JSON (現行形式はレイヤー別キーコード割り当て)
|
||||||
|
- **備考**: キーの参照にPHYのキーIDを使う。配列としての並べ替えは、出力時にバリアント選択後のIDXの並び順で行う
|
||||||
|
- **詳細**: [MAPレイヤー](MAPレイヤー.md)。動作定義とキーごとの設定の保存形式は、[MAPのP-1](MAPレイヤー.md#p-1-動作定義とキーごとの設定の保存形式)で扱う
|
||||||
|
|
||||||
### 5. IMEレイヤー (入力変換)
|
### 5. HSTレイヤー (ホスト)
|
||||||
|
|
||||||
- **役割**: キーコードと実際の入力文字の対応
|
- **役割**: 接続先のホスト(OSとキーボード配列の設定)が、キーコードをどう解釈するかの記述
|
||||||
- **対応ツール**: OS
|
- **入力**: なし。どの層も参照せず、MAPエディタとFirmware Generatorから参照される
|
||||||
- **備考**: MAPレイヤー設計時に考慮が必要
|
- **対応ツール**: OSのキーボード配列、QMKの言語別キーコード(`keymap_japanese.h` など)
|
||||||
|
- **出力**: ホスト環境ごとのプロファイル。キーボードに依存しない共有カタログとする
|
||||||
|
- **備考**: IMEのかな漢字変換はOSの責務で、HSTの対象外
|
||||||
|
- **詳細**: [HSTレイヤー](HSTレイヤー.md)
|
||||||
|
|
||||||
## ツール設計方針
|
## ツール設計方針
|
||||||
|
|
||||||
@@ -55,34 +73,38 @@
|
|||||||
#### PHY Editor
|
#### PHY Editor
|
||||||
|
|
||||||
- 物理配置の視覚的編集
|
- 物理配置の視覚的編集
|
||||||
- KLE互換フォーマット対応
|
- KLE JSONの読み込み・書き出し
|
||||||
- 出力: PHY JSON
|
- 出力: PHY JSON
|
||||||
|
|
||||||
#### Matrix Mapper
|
#### Key Indexer
|
||||||
|
|
||||||
- PHY Editorの出力を入力
|
- PHY JSONを入力
|
||||||
- キーにrow/colを割り当て
|
- 全バリアントの対象キーを1つの並びに配置
|
||||||
- 視覚的にマトリックス配線を確認
|
|
||||||
- 出力: IDX JSON
|
- 出力: IDX JSON
|
||||||
|
|
||||||
#### Pin Configurator
|
#### Wiring Editor
|
||||||
|
|
||||||
- マトリックス定義を入力
|
- PHY JSONを入力
|
||||||
- マイコンのピン配置を指定
|
- ピン提供者(マイコン、マルチプレクサ等)を置き、各キーの端子の接続先を指定
|
||||||
- 配線図を出力
|
- 視覚的に配線を確認
|
||||||
- 出力: ELC JSON
|
- 出力: ELC JSON
|
||||||
|
|
||||||
#### Keymap Designer
|
#### Keymap Designer (MAPエディタ)
|
||||||
|
|
||||||
- キー識別子リストを入力
|
- PHYの配置を参照し、キーマップレイヤーごとの割り当てを編集・保存する
|
||||||
- レイヤー別キーコード割り当て
|
- 新規キーボードの設計から実機での調整までを扱い、割り当ての読み書きを一組で提供する
|
||||||
- VIA/Remapとの互換性
|
- 実機との通信は既存のVIAプロトコルから対応し、対象を段階的に増やす
|
||||||
- 出力: MAP JSON
|
- 出力: MAP JSON
|
||||||
|
|
||||||
|
具体的な責務と通信上の制約は、[MAPレイヤー](MAPレイヤー.md#mapエディタの責務)に従う。
|
||||||
|
|
||||||
#### Firmware Generator
|
#### Firmware Generator
|
||||||
|
|
||||||
- 全レイヤーの情報を統合
|
- 全レイヤーの情報を統合
|
||||||
|
- バリアントを選択し、IDXから対象外のキーを除いた並びで、キー番号の対応表とMAPの配列を生成
|
||||||
|
- row / col など、ターゲットが要求する派生情報をここで割り当てる
|
||||||
- QMK/VIA/KMK等へ変換
|
- QMK/VIA/KMK等へ変換
|
||||||
|
- VIA向けキーボード定義は、配置と接続先の対応などを統合して生成する
|
||||||
- 出力: ターゲット固有の設定ファイル
|
- 出力: ターゲット固有の設定ファイル
|
||||||
|
|
||||||
### 統合環境 (IDE)
|
### 統合環境 (IDE)
|
||||||
@@ -92,24 +114,28 @@
|
|||||||
- **バージョン管理統合**: テキストベース定義でgit管理
|
- **バージョン管理統合**: テキストベース定義でgit管理
|
||||||
- **ワークフローサポート**: ガイド付きウィザード、テンプレート
|
- **ワークフローサポート**: ガイド付きウィザード、テンプレート
|
||||||
|
|
||||||
|
ビルド・ファームウェアの書き込みは、MAP編集とは別の実行機能として扱う。
|
||||||
|
同じ画面から各ツールを利用できることと、各レイヤーがそのデータや処理を所有することは区別する。
|
||||||
|
|
||||||
## プロトコル設計
|
## プロトコル設計
|
||||||
|
|
||||||
### レイヤー間インターフェース
|
### レイヤー間インターフェース
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
|
PHY → ELC
|
||||||
|
Input: ノードID + 配置
|
||||||
|
Output: 提供者定義 + 端子の接続先 + LEDチェーン
|
||||||
|
|
||||||
PHY → IDX
|
PHY → IDX
|
||||||
Input: KLE JSON (or compatible)
|
Input: キーID + 配置
|
||||||
Output: Augmented layout (coordinates + key IDs)
|
Output: 全バリアントの対象キーIDの並び
|
||||||
|
|
||||||
IDX → ELC
|
PHY → MAP
|
||||||
Input: Key ID list
|
Input: キーID
|
||||||
Output: Matrix definition + Pin mapping
|
Output: MAP JSON (現行形式はレイヤー別キーコード割り当て)
|
||||||
|
|
||||||
IDX → MAP
|
|
||||||
Input: Key ID list
|
|
||||||
Output: Keymap arrays
|
|
||||||
|
|
||||||
All → Firmware
|
All → Firmware
|
||||||
|
Input: 各レイヤーのデータ + バリアント選択
|
||||||
Output: QMK keyboard.json / VIA JSON / KMK .py
|
Output: QMK keyboard.json / VIA JSON / KMK .py
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -124,7 +150,7 @@ All → Firmware
|
|||||||
### Phase 1: プロトコル策定
|
### Phase 1: プロトコル策定
|
||||||
|
|
||||||
- JSON Schemaでフォーマット定義
|
- JSON Schemaでフォーマット定義
|
||||||
- TypeScriptでリファレンス実装
|
- リファレンス実装(言語・構成は `implementation-plan.md` で選定)
|
||||||
- CLIツールとして動作確認
|
- CLIツールとして動作確認
|
||||||
|
|
||||||
### Phase 2: 個別ツール開発
|
### Phase 2: 個別ツール開発
|
||||||
@@ -141,8 +167,8 @@ All → Firmware
|
|||||||
|
|
||||||
## 既存ツールとの関係
|
## 既存ツールとの関係
|
||||||
|
|
||||||
- **KLE**: PHY Editorの一実装、インポート対応
|
- **KLE**: PHYデータを表示できる既存のビュワー。KLE JSONの読み書きで連携する
|
||||||
- **VIA/Remap**: MAP Layerクライアントとして連携
|
- **VIA/Remap**: 同じVIA対応実機の割り当てを編集する外部クライアント。外部で調整した結果もMAPエディタで読み出してプロジェクトへ戻す。読み書きする項目は[MAP「VIAで読み書きする項目」](MAPレイヤー.md#viaで読み書きする項目)、実機との対応・状態管理は[MAPのP-3](MAPレイヤー.md#p-3-プロジェクトと実機の対応状態管理)で扱う
|
||||||
- **QMK Configurator**: Firmware Generatorのターゲット
|
- **QMK Configurator**: Firmware Generatorのターゲット
|
||||||
|
|
||||||
## 配慮事項
|
## 配慮事項
|
||||||
@@ -155,7 +181,7 @@ All → Firmware
|
|||||||
|
|
||||||
### 互換性
|
### 互換性
|
||||||
|
|
||||||
- 既存KLE JSONの完全サポート
|
- 既存KLE JSONの読み込み(PHYデータがなくても読める)
|
||||||
- 段階的移行パスの提供
|
- 段階的移行パスの提供
|
||||||
- レガシーツールとの共存
|
- レガシーツールとの共存
|
||||||
|
|
||||||
@@ -165,3 +191,11 @@ All → Firmware
|
|||||||
2. GitHubでRFCリポジトリ立ち上げ
|
2. GitHubでRFCリポジトリ立ち上げ
|
||||||
3. 小規模コミュニティでフィードバック収集
|
3. 小規模コミュニティでフィードバック収集
|
||||||
4. リファレンス実装の開発
|
4. リファレンス実装の開発
|
||||||
|
|
||||||
|
## 未確認事項
|
||||||
|
|
||||||
|
### P-1 アナログ値の解釈をどの層が持つか
|
||||||
|
|
||||||
|
- 位置づけ: 未解決の問い。ELCは配線だけを持つと決めたため、生値から押下深さを出す変換の置き場が空いている
|
||||||
|
- 現状の認識: センサの品種と出力特性はPCB設計ツール側、キャリブレーションは実機ごとの実測値で設計データではない、とまでは決まっている。残りをMAPに寄せるか、別の層を立てるかは未定
|
||||||
|
- 格上げ条件: アナログ入力のキーボードを1つ通しで設計し、変換に必要な情報と、それを置く層をユーザーが決める
|
||||||
|
|||||||
+16
-70
@@ -1,5 +1,8 @@
|
|||||||
# 自作キーボード制作ワークフロー分析
|
# 自作キーボード制作ワークフロー分析
|
||||||
|
|
||||||
|
既存の制作フローの各工程が、どのレイヤーに当たるかを見る文書。
|
||||||
|
レイヤーの定義と役割は `keyboard-design-layers.md` を正とする。
|
||||||
|
|
||||||
## 現在の一般的な制作手順
|
## 現在の一般的な制作手順
|
||||||
|
|
||||||
### 1. デザイン・設計段階
|
### 1. デザイン・設計段階
|
||||||
@@ -23,7 +26,7 @@
|
|||||||
- ダイオード配置の検討
|
- ダイオード配置の検討
|
||||||
- **出力**: 回路図、ピン配置表
|
- **出力**: 回路図、ピン配置表
|
||||||
- **現在の課題**: KLEからマトリックス情報への変換が手動
|
- **現在の課題**: KLEからマトリックス情報への変換が手動
|
||||||
- **Beyond KLE対応**: **ELCレイヤー** + **IDXレイヤー**
|
- **Beyond KLE対応**: **ELCレイヤー**。行列構成という工程は持たず、キーの端子とピンの対応として書く
|
||||||
|
|
||||||
### 2. PCB設計段階
|
### 2. PCB設計段階
|
||||||
|
|
||||||
@@ -35,7 +38,7 @@
|
|||||||
- スイッチフットプリントの配置
|
- スイッチフットプリントの配置
|
||||||
- マイコン・コネクタ等の部品配置
|
- マイコン・コネクタ等の部品配置
|
||||||
- **現在の課題**: KLEとPCBツール間の座標変換
|
- **現在の課題**: KLEとPCBツール間の座標変換
|
||||||
- **Beyond KLE対応**: **PHYレイヤー** → PCBツール連携
|
- **Beyond KLE対応**: **PHYレイヤー** → PCBツール連携。渡すのは配置であって、フットプリントの選択はPCBツール側に残る
|
||||||
|
|
||||||
#### b) 配線・ルーティング
|
#### b) 配線・ルーティング
|
||||||
|
|
||||||
@@ -54,10 +57,13 @@
|
|||||||
- マトリックス定義(ROW/COL)
|
- マトリックス定義(ROW/COL)
|
||||||
- ピン配置設定
|
- ピン配置設定
|
||||||
- 物理レイアウト定義
|
- 物理レイアウト定義
|
||||||
- **現在の課題**:
|
- キーマップ配列の並び順を決める(QMKなら `LAYOUT` マクロの引数順、ZMKなら matrix transform の
|
||||||
- KLEとファームウェア設定の手動変換
|
`map`、KMKなら `coord_mapping`)
|
||||||
- VIAではlabelフィールドに"1,5"等を記入してmatrix座標を表現
|
- **現在の課題**: KLEとファームウェア設定の手動変換。並び順は手書きで、引数の数え間違いがそのまま
|
||||||
- **Beyond KLE対応**: **IDXレイヤー** + **ELCレイヤー** → ファームウェア自動生成
|
キーのずれになる
|
||||||
|
- **Beyond KLE対応**: **PHY** + **IDX** + **ELC** → ファームウェア自動生成。
|
||||||
|
ROW/COLはELCの配線から出力時に割り当てる。並び順は**IDXレイヤー**が全バリアントにわたって正本として持ち、
|
||||||
|
出力時に選択したバリアントで使うキーだけを残して、キー番号を0から連続する番号に詰める
|
||||||
|
|
||||||
#### b) キーマップ設定
|
#### b) キーマップ設定
|
||||||
|
|
||||||
@@ -66,14 +72,14 @@
|
|||||||
- 各キーへのキーコード割り当て
|
- 各キーへのキーコード割り当て
|
||||||
- レイヤー設計
|
- レイヤー設計
|
||||||
- マクロ・特殊機能の設定
|
- マクロ・特殊機能の設定
|
||||||
- **Beyond KLE対応**: **MAPレイヤー**
|
- **Beyond KLE対応**: **MAPレイヤー**。キーの参照にはPHYのキーIDを使い、配列への並べ替えは出力時にバリアント選択後のIDXの順で行う
|
||||||
|
|
||||||
### 4. 製造・組み立て段階
|
### 4. 製造・組み立て段階
|
||||||
|
|
||||||
#### a) PCB製造
|
#### a) PCB製造
|
||||||
|
|
||||||
- **出力**: Gerberファイル、BOM、Pick&Place
|
- **出力**: Gerberファイル、BOM、Pick&Place
|
||||||
- **Beyond KLE対応**: PHY/ELCレイヤーからの自動生成
|
- **Beyond KLE対応**: なし。部品の品種とフットプリントを持たないため、この段階はPCB設計ツールが担う
|
||||||
|
|
||||||
#### b) 部品調達・組み立て
|
#### b) 部品調達・組み立て
|
||||||
|
|
||||||
@@ -86,72 +92,12 @@
|
|||||||
- **問題**: KLE → PCB → ファームウェアで毎回手動変換
|
- **問題**: KLE → PCB → ファームウェアで毎回手動変換
|
||||||
- **影響**: 一箇所変更時の全体への反映が困難
|
- **影響**: 一箇所変更時の全体への反映が困難
|
||||||
|
|
||||||
### 2. マトリックス情報の欠落
|
### 2. 設計変更時のトレーサビリティ不足
|
||||||
|
|
||||||
- **問題**: KLEは物理配置のみ、論理構造(マトリックス)は別管理
|
|
||||||
- **影響**: VIAでのhackyな対応(labelフィールド悪用)
|
|
||||||
|
|
||||||
### 3. 設計変更時のトレーサビリティ不足
|
|
||||||
|
|
||||||
- **問題**: どのレイヤーでの変更が他に影響するかが不明確
|
- **問題**: どのレイヤーでの変更が他に影響するかが不明確
|
||||||
- **影響**: デバッグ・修正作業の複雑化
|
- **影響**: デバッグ・修正作業の複雑化
|
||||||
|
|
||||||
### 4. ツール間連携の脆弱性
|
### 3. ツール間連携の脆弱性
|
||||||
|
|
||||||
- **問題**: 手動変換によるヒューマンエラー
|
- **問題**: 手動変換によるヒューマンエラー
|
||||||
- **影響**: 設計ミス、製造後の修正コスト
|
- **影響**: 設計ミス、製造後の修正コスト
|
||||||
|
|
||||||
## Beyond KLEによる解決策
|
|
||||||
|
|
||||||
### 1. 統一データモデル
|
|
||||||
|
|
||||||
```
|
|
||||||
物理設計(PHY) → キー識別(IDX) → 電気設計(ELC) → キーマップ(MAP)
|
|
||||||
↓ ↓ ↓ ↓
|
|
||||||
PCBツール ファームウェア 回路図 VIA/Remap
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. レイヤー間の明確な責任分離
|
|
||||||
|
|
||||||
- **PHY**: 物理配置のみに集中
|
|
||||||
- **IDX**: 物理と論理の橋渡し(最重要レイヤー)
|
|
||||||
- **ELC**: 電気的接続の明確化
|
|
||||||
- **MAP**: キーマップロジックの分離
|
|
||||||
|
|
||||||
### 3. 双方向の依存関係管理
|
|
||||||
|
|
||||||
- 上流レイヤーの変更が下流に自動反映
|
|
||||||
- 下流での制約が上流設計にフィードバック
|
|
||||||
|
|
||||||
### 4. 既存ツールとの互換性維持
|
|
||||||
|
|
||||||
- KLE JSONの完全サポート
|
|
||||||
- VIA/QMK等への出力対応
|
|
||||||
- 段階的移行の実現
|
|
||||||
|
|
||||||
## Phase 1で策定すべきプロトコル
|
|
||||||
|
|
||||||
### 1. レイヤー間インターフェース仕様
|
|
||||||
|
|
||||||
- 各レイヤーの入出力JSON Schema
|
|
||||||
- 依存関係の明確化
|
|
||||||
- エラーハンドリング規則
|
|
||||||
|
|
||||||
### 2. 既存フォーマットとの変換仕様
|
|
||||||
|
|
||||||
- KLE JSON → PHY変換
|
|
||||||
- QMK設定 ← IDX+ELC+MAP変換
|
|
||||||
- VIA JSON ← 全レイヤー統合
|
|
||||||
|
|
||||||
### 3. 拡張性設計
|
|
||||||
|
|
||||||
- 非標準マトリックス(charlieplexing等)対応
|
|
||||||
- 3D配置への拡張可能性
|
|
||||||
- カスタムキーコードサポート
|
|
||||||
|
|
||||||
## 次のアクション
|
|
||||||
|
|
||||||
1. **JSON Schema策定**: 各レイヤーのデータ構造定義
|
|
||||||
2. **変換ロジック設計**: レイヤー間の変換アルゴリズム
|
|
||||||
3. **リファレンス実装**: TypeScript/Rust等でのプロトタイプ
|
|
||||||
4. **テストケース作成**: 代表的なキーボード設計での検証
|
|
||||||
|
|||||||
+285
-105
@@ -2,11 +2,16 @@
|
|||||||
|
|
||||||
## レイヤー間データフロー
|
## レイヤー間データフロー
|
||||||
|
|
||||||
|
```txt
|
||||||
|
PHY (物理配置)
|
||||||
|
├─→ ELC (電気配線)
|
||||||
|
├─→ IDX (キー識別)
|
||||||
|
└─→ MAP (キーマップ)
|
||||||
```
|
```
|
||||||
PHY (物理配置) → IDX (キー識別) → ELC (電気配線) + MAP (キーマップ)
|
|
||||||
↓ ↓
|
ELC・IDX・MAPはどれもPHYを参照する。ELCはIDXを参照せず、MAPもIDXを参照しない。
|
||||||
ファームウェア生成 VIA/Remap出力
|
ファームウェアやVIA/Remap向けの出力は、各レイヤーの情報を統合して生成する。
|
||||||
```
|
他レイヤーからキーや部品を指す参照には、PHYのノードIDを使う。
|
||||||
|
|
||||||
## 1. PHYレイヤー JSON Schema
|
## 1. PHYレイヤー JSON Schema
|
||||||
|
|
||||||
@@ -38,23 +43,10 @@ PHY (物理配置) → IDX (キー識別) → ELC (電気配線) + MAP (キー
|
|||||||
"modified": { "type": "string", "format": "date-time" }
|
"modified": { "type": "string", "format": "date-time" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compatibility": {
|
"kleCompatibility": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Information for external tool compatibility",
|
"description": "File-level opaque KLE fields kept for round-trip output (switchMount, switchBrand, switchType, backcolor, radii, background, css). Kept here for now; the eventual owning layer is deferred in backlog/README.md, PHY-P-6",
|
||||||
"properties": {
|
"additionalProperties": true
|
||||||
"kleJson": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"source": { "type": "string" },
|
|
||||||
"checksum": { "type": "string" },
|
|
||||||
"preservedFields": { "type": "array", "items": { "type": "string" } }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"originalFormat": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["kle", "via", "qmk", "native"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"profiles": {
|
"profiles": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@@ -69,7 +61,8 @@ PHY (物理配置) → IDX (キー識別) → ELC (電気配線) + MAP (キー
|
|||||||
"oneOf": [
|
"oneOf": [
|
||||||
{ "$ref": "#/$defs/key" },
|
{ "$ref": "#/$defs/key" },
|
||||||
{ "$ref": "#/$defs/cluster" },
|
{ "$ref": "#/$defs/cluster" },
|
||||||
{ "$ref": "#/$defs/component" }
|
{ "$ref": "#/$defs/component" },
|
||||||
|
{ "$ref": "#/$defs/variantGroup" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,6 +86,7 @@ PHY (物理配置) → IDX (キー識別) → ELC (電気配線) + MAP (キー
|
|||||||
"name": { "type": "string" },
|
"name": { "type": "string" },
|
||||||
"unitSize": {
|
"unitSize": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"description": "Millimeters per 1U",
|
||||||
"required": ["x", "y"],
|
"required": ["x", "y"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"x": { "type": "number", "minimum": 0 },
|
"x": { "type": "number", "minimum": 0 },
|
||||||
@@ -101,9 +95,13 @@ PHY (物理配置) → IDX (キー識別) → ELC (電気配線) + MAP (キー
|
|||||||
},
|
},
|
||||||
"visualOffset": {
|
"visualOffset": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"description": "Key top rectangle, in millimeters: inward distance from each edge of the outline, plus corner radius",
|
||||||
|
"required": ["top", "right", "bottom", "left", "radius"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"x": { "type": "number" },
|
"top": { "type": "number" },
|
||||||
"y": { "type": "number" },
|
"right": { "type": "number" },
|
||||||
|
"bottom": { "type": "number" },
|
||||||
|
"left": { "type": "number" },
|
||||||
"radius": { "type": "number", "minimum": 0 }
|
"radius": { "type": "number", "minimum": 0 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,7 +170,7 @@ PHY (物理配置) → IDX (キー識別) → ELC (電気配線) + MAP (キー
|
|||||||
},
|
},
|
||||||
"kleCompatibility": {
|
"kleCompatibility": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Opaque KLE fields kept for round-trip output (c, t, p, a, f, f2, fa, l, n, sm, sb, st). The owning layer is undecided; see P-6 in PHYレイヤー.md",
|
"description": "Opaque KLE fields kept for round-trip output (c, t, p, a, f, f2, fa, l, n, sm, sb, st). Kept here for now; the eventual owning layer is deferred in backlog/README.md, PHY-P-6",
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,9 +192,61 @@ PHY (物理配置) → IDX (キー識別) → ELC (電気配線) + MAP (キー
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"variantGroup": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Mutually exclusive layout alternatives. Children are variants only",
|
||||||
|
"required": ["type", "id", "name", "transform", "default", "children"],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "variantGroup"
|
||||||
|
},
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Heading shown for the choice in the editor"
|
||||||
|
},
|
||||||
|
"transform": { "$ref": "#/$defs/transform" },
|
||||||
|
"default": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "id of the variant used when a single layout must be chosen"
|
||||||
|
},
|
||||||
|
"children": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 2,
|
||||||
|
"description": "Display order",
|
||||||
|
"items": { "$ref": "#/$defs/variant" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"variant": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "One alternative. Appears only directly under a variantGroup. Carries no transform",
|
||||||
|
"required": ["type", "id", "name", "children"],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "variant"
|
||||||
|
},
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"children": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "May be empty",
|
||||||
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{ "$ref": "#/$defs/key" },
|
||||||
|
{ "$ref": "#/$defs/cluster" },
|
||||||
|
{ "$ref": "#/$defs/component" },
|
||||||
|
{ "$ref": "#/$defs/variantGroup" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"cluster": {
|
"cluster": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["type", "id", "name", "transform", "children"],
|
"required": ["type", "id", "transform", "children"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -211,7 +261,8 @@ PHY (物理配置) → IDX (キー識別) → ELC (電気配線) + MAP (キー
|
|||||||
"oneOf": [
|
"oneOf": [
|
||||||
{ "$ref": "#/$defs/key" },
|
{ "$ref": "#/$defs/key" },
|
||||||
{ "$ref": "#/$defs/cluster" },
|
{ "$ref": "#/$defs/cluster" },
|
||||||
{ "$ref": "#/$defs/component" }
|
{ "$ref": "#/$defs/component" },
|
||||||
|
{ "$ref": "#/$defs/variantGroup" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,7 +288,7 @@ PHY (物理配置) → IDX (キー識別) → ELC (電気配線) + MAP (キー
|
|||||||
"id": "standard",
|
"id": "standard",
|
||||||
"name": "Standard Cherry MX",
|
"name": "Standard Cherry MX",
|
||||||
"unitSize": { "x": 19.05, "y": 19.05 },
|
"unitSize": { "x": 19.05, "y": 19.05 },
|
||||||
"visualOffset": { "x": 1, "y": 1, "radius": 1 }
|
"visualOffset": { "top": 3, "right": 3, "bottom": 3, "left": 3, "radius": 1 }
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"root": [
|
"root": [
|
||||||
@@ -303,15 +354,62 @@ PHY (物理配置) → IDX (キー識別) → ELC (電気配線) + MAP (キー
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "variantGroup",
|
||||||
|
"id": "vg_enter",
|
||||||
|
"name": "Enter",
|
||||||
|
"transform": { "x": 6, "y": 0, "rotation": 0 },
|
||||||
|
"default": "v_ansi",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "variant",
|
||||||
|
"id": "v_ansi",
|
||||||
|
"name": "ANSI",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "key",
|
||||||
|
"id": "k_ansi_bslash",
|
||||||
|
"profile": "standard",
|
||||||
|
"transform": { "x": 1, "y": 0, "rotation": 0 },
|
||||||
|
"size": { "w": 1.5, "h": 1 },
|
||||||
|
"labels": ["|", "\\"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "key",
|
||||||
|
"id": "k_ansi_enter",
|
||||||
|
"profile": "standard",
|
||||||
|
"transform": { "x": 0.25, "y": 1, "rotation": 0 },
|
||||||
|
"size": { "w": 2.25, "h": 1 },
|
||||||
|
"labels": ["Enter"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "variant",
|
||||||
|
"id": "v_iso",
|
||||||
|
"name": "ISO",
|
||||||
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "key",
|
"type": "key",
|
||||||
"id": "k_iso_enter",
|
"id": "k_iso_enter",
|
||||||
"profile": "standard",
|
"profile": "standard",
|
||||||
"transform": { "x": 6, "y": 1, "rotation": 0 },
|
"transform": { "x": 1, "y": 0, "rotation": 0 },
|
||||||
"size": { "w": 1.5, "h": 1, "x2": 0.25, "y2": 1, "w2": 1.25, "h2": 1 },
|
"size": { "w": 1.5, "h": 1, "x2": 0.25, "y2": 1, "w2": 1.25, "h2": 1 },
|
||||||
"switchOffset": { "x": 0, "y": 0.5 },
|
"switchOffset": { "x": 0, "y": 0.5 },
|
||||||
"labels": ["Enter"]
|
"labels": ["Enter"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "key",
|
||||||
|
"id": "k_iso_hash",
|
||||||
|
"profile": "standard",
|
||||||
|
"transform": { "x": 0.25, "y": 1, "rotation": 0 },
|
||||||
|
"size": { "w": 1, "h": 1 },
|
||||||
|
"labels": ["~", "#"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "component",
|
"type": "component",
|
||||||
"id": "screw0",
|
"id": "screw0",
|
||||||
@@ -324,6 +422,12 @@ PHY (物理配置) → IDX (キー識別) → ELC (電気配線) + MAP (キー
|
|||||||
|
|
||||||
## 2. IDXレイヤー JSON Schema
|
## 2. IDXレイヤー JSON Schema
|
||||||
|
|
||||||
|
各フィールドの意味は `IDXレイヤー.md` を正とする。
|
||||||
|
|
||||||
|
全バリアントの対象キーIDを、重複なく1つの並びに持つ。同じ配線を使う排他キーも別々の要素とする。
|
||||||
|
出力時は選択したバリアントで使わないキーを除き、残った並びの位置を0から連続するキー番号とする。
|
||||||
|
保存した `order` は変更しない。
|
||||||
|
|
||||||
### 基本構造
|
### 基本構造
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -331,9 +435,9 @@ PHY (物理配置) → IDX (キー識別) → ELC (電気配線) + MAP (キー
|
|||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://beyond-kle.org/schemas/idx-layer.json",
|
"$id": "https://beyond-kle.org/schemas/idx-layer.json",
|
||||||
"title": "IDX Layer - Key Identification",
|
"title": "IDX Layer - Key Identification",
|
||||||
"description": "Mapping between physical keys and logical identifiers",
|
"description": "Ordered PHY key IDs across all variants. Output key numbers are contiguous positions after filtering by variant selection",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["version", "phyReference", "keyMap"],
|
"required": ["version", "phyReference", "order"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"version": { "type": "string" },
|
"version": { "type": "string" },
|
||||||
"phyReference": {
|
"phyReference": {
|
||||||
@@ -344,37 +448,13 @@ PHY (物理配置) → IDX (キー識別) → ELC (電気配線) + MAP (キー
|
|||||||
"checksum": { "type": "string" }
|
"checksum": { "type": "string" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keyMap": {
|
"order": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
"uniqueItems": true,
|
||||||
|
"description": "Each PHY key has its own entry, including mutually exclusive keys wired to the same terminals. Variant selection does not modify this list",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/$defs/keyMapping"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"$defs": {
|
|
||||||
"keyMapping": {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["physicalId", "logicalId"],
|
|
||||||
"properties": {
|
|
||||||
"physicalId": {
|
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Reference to PHY layer key ID"
|
"description": "Reference to PHY layer key ID"
|
||||||
},
|
|
||||||
"logicalId": {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["index"],
|
|
||||||
"properties": {
|
|
||||||
"index": { "type": "integer", "minimum": 0 },
|
|
||||||
"matrix": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"row": { "type": "integer", "minimum": 0 },
|
|
||||||
"col": { "type": "integer", "minimum": 0 }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"label": { "type": "string" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -383,19 +463,21 @@ PHY (物理配置) → IDX (キー識別) → ELC (電気配線) + MAP (キー
|
|||||||
|
|
||||||
## 3. ELCレイヤー JSON Schema
|
## 3. ELCレイヤー JSON Schema
|
||||||
|
|
||||||
|
各フィールドの意味は `ELCレイヤー.md` を正とする。
|
||||||
|
|
||||||
### 基本構造
|
### 基本構造
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://beyond-kle.org/schemas/elc-layer.json",
|
"$id": "https://beyond-kle.org/schemas/elc-layer.json",
|
||||||
"title": "ELC Layer - Electrical Configuration",
|
"title": "ELC Layer - Electrical Wiring",
|
||||||
"description": "Electrical wiring and pin mapping",
|
"description": "Wiring between provider pins and key terminals",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["version", "idxReference", "microcontroller", "matrix"],
|
"required": ["version", "phyReference", "providers", "keys"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"version": { "type": "string" },
|
"version": { "type": "string" },
|
||||||
"idxReference": {
|
"phyReference": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["source", "checksum"],
|
"required": ["source", "checksum"],
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -403,42 +485,125 @@ PHY (物理配置) → IDX (キー識別) → ELC (電気配線) + MAP (キー
|
|||||||
"checksum": { "type": "string" }
|
"checksum": { "type": "string" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"microcontroller": {
|
"providers": {
|
||||||
"type": "object",
|
|
||||||
"required": ["type", "pinMapping"],
|
|
||||||
"properties": {
|
|
||||||
"type": { "type": "string" },
|
|
||||||
"pinMapping": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"rows": {
|
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": { "type": "string" }
|
"items": { "$ref": "#/$defs/provider" }
|
||||||
},
|
},
|
||||||
"cols": {
|
"keys": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": { "type": "string" }
|
"items": { "$ref": "#/$defs/keyWiring" }
|
||||||
}
|
},
|
||||||
}
|
"ledChains": {
|
||||||
}
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/ledChain" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"matrix": {
|
"$defs": {
|
||||||
"type": "object",
|
"pinRef": {
|
||||||
"required": ["type", "dimensions"],
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["standard", "charlieplexing", "direct"]
|
"pattern": "^(VCC|GND|[^.]+\\.[^.]+)$",
|
||||||
|
"description": "\"<provider id>.<pin name>\", or the reserved power rails VCC / GND"
|
||||||
},
|
},
|
||||||
"dimensions": {
|
"provider": {
|
||||||
|
"oneOf": [
|
||||||
|
{ "$ref": "#/$defs/mcuProvider" },
|
||||||
|
{ "$ref": "#/$defs/analogMuxProvider" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mcuProvider": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["rows", "cols"],
|
"required": ["id", "kind", "part"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"rows": { "type": "integer", "minimum": 1 },
|
"id": { "type": "string" },
|
||||||
"cols": { "type": "integer", "minimum": 1 }
|
"kind": { "const": "mcu" },
|
||||||
|
"part": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Part number. Used to interpret pin names"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"analogMuxProvider": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "kind", "channels", "select", "common"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"kind": { "const": "analog-mux" },
|
||||||
|
"channels": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"description": "Number of logical pins provided. Pin names are \"0\"..\"channels-1\""
|
||||||
|
},
|
||||||
|
"select": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/pinRef" },
|
||||||
|
"description": "Select pins, least significant bit first"
|
||||||
|
},
|
||||||
|
"common": { "$ref": "#/$defs/pinRef" },
|
||||||
|
"enable": { "$ref": "#/$defs/pinRef" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"keyWiring": {
|
||||||
|
"oneOf": [
|
||||||
|
{ "$ref": "#/$defs/contactKey" },
|
||||||
|
{ "$ref": "#/$defs/analogKey" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"contactKey": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["key", "detect", "terminals"],
|
||||||
|
"properties": {
|
||||||
|
"key": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Reference to PHY layer key ID"
|
||||||
|
},
|
||||||
|
"detect": { "const": "contact" },
|
||||||
|
"terminals": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Non-polar. a and b carry no ordering meaning",
|
||||||
|
"required": ["a", "b"],
|
||||||
|
"properties": {
|
||||||
|
"a": { "$ref": "#/$defs/pinRef" },
|
||||||
|
"b": { "$ref": "#/$defs/pinRef" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"diode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["a-to-b", "b-to-a"],
|
||||||
|
"description": "Anode side comes first. Omitted means no diode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"analogKey": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["key", "detect", "terminals"],
|
||||||
|
"properties": {
|
||||||
|
"key": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Reference to PHY layer key ID"
|
||||||
|
},
|
||||||
|
"detect": { "const": "analog" },
|
||||||
|
"terminals": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["out", "vcc", "gnd"],
|
||||||
|
"properties": {
|
||||||
|
"out": { "$ref": "#/$defs/pinRef" },
|
||||||
|
"vcc": { "$ref": "#/$defs/pinRef" },
|
||||||
|
"gnd": { "$ref": "#/$defs/pinRef" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ledChain": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "data", "order"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"data": { "$ref": "#/$defs/pinRef" },
|
||||||
|
"order": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "PHY node IDs. The position in the array is the LED number"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -447,6 +612,10 @@ PHY (物理配置) → IDX (キー識別) → ELC (電気配線) + MAP (キー
|
|||||||
|
|
||||||
## 4. MAPレイヤー JSON Schema
|
## 4. MAPレイヤー JSON Schema
|
||||||
|
|
||||||
|
キーの参照にはPHYのキーIDを使う。キー番号は参照しない。
|
||||||
|
配列としての並べ替えは、ファームウェア出力時にバリアント選択後のIDXの並び順で行う。
|
||||||
|
キー番号と接続先の対応表も同じ並びで生成し、MAPの保存データは変更しない。
|
||||||
|
|
||||||
### 基本構造
|
### 基本構造
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -456,10 +625,10 @@ PHY (物理配置) → IDX (キー識別) → ELC (電気配線) + MAP (キー
|
|||||||
"title": "MAP Layer - Keymap Configuration",
|
"title": "MAP Layer - Keymap Configuration",
|
||||||
"description": "Key code assignments and layers",
|
"description": "Key code assignments and layers",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["version", "idxReference", "layers"],
|
"required": ["version", "phyReference", "layers"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"version": { "type": "string" },
|
"version": { "type": "string" },
|
||||||
"idxReference": {
|
"phyReference": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["source", "checksum"],
|
"required": ["source", "checksum"],
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -477,10 +646,12 @@ PHY (物理配置) → IDX (キー識別) → ELC (電気配線) + MAP (キー
|
|||||||
"$defs": {
|
"$defs": {
|
||||||
"layer": {
|
"layer": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["id", "name", "keymap"],
|
"required": ["name", "keymap"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": { "type": "integer", "minimum": 0 },
|
"name": {
|
||||||
"name": { "type": "string" },
|
"type": "string",
|
||||||
|
"description": "Layer identifier, unique within the MAP. Layer order is the array order"
|
||||||
|
},
|
||||||
"keymap": {
|
"keymap": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
@@ -491,9 +662,12 @@ PHY (物理配置) → IDX (キー識別) → ELC (電気配線) + MAP (キー
|
|||||||
},
|
},
|
||||||
"keyAssignment": {
|
"keyAssignment": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["keyIndex", "keycode"],
|
"required": ["keyId", "keycode"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"keyIndex": { "type": "integer" },
|
"keyId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Reference to PHY layer key ID"
|
||||||
|
},
|
||||||
"keycode": { "type": "string" }
|
"keycode": { "type": "string" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -505,25 +679,31 @@ PHY (物理配置) → IDX (キー識別) → ELC (電気配線) + MAP (キー
|
|||||||
|
|
||||||
### PHY → IDX変換
|
### PHY → IDX変換
|
||||||
|
|
||||||
1. PHYレイヤーから物理キーリストを取得
|
1. PHYレイヤーから全バリアントのキーのリストを取得
|
||||||
2. 各キーに論理IDを割り当て
|
2. 番号を振る対象キーを1つの `order` に重複なく並べる。同じ配線の排他キーも別々の要素にする
|
||||||
3. マトリックス座標の自動生成(行列最適化)
|
|
||||||
|
|
||||||
### IDX → ELC変換
|
### PHY → ELC変換
|
||||||
|
|
||||||
1. マトリックス次元を算出
|
1. ピン提供者を置き、各提供者が消費する上位ピンを指定
|
||||||
2. ピン配置の最適化提案
|
2. 各キーの端子の接続先を指定
|
||||||
3. 配線効率の検証
|
3. LEDチェーンの並びを指定
|
||||||
|
|
||||||
### IDX + ELC → MAP変換
|
### IDX + ELC + MAP → ファームウェア出力
|
||||||
|
|
||||||
1. キーインデックス配列の生成
|
1. PHYのVariantGroupごとにバリアントを選び、共通キーと、祖先を含めて選択されたバリアントのキーを特定
|
||||||
2. デフォルトキーマップの提案
|
2. IDXの `order` から対象外のキーを除き、残った順序のまま出力用のキー番号を0から連続して割り当て
|
||||||
3. VIA/QMK形式への出力
|
3. ELCの配線から、ターゲットが要求する row / col を割り当て、手順2の並びでキー番号との対応表を生成
|
||||||
|
4. 同じ並びでMAPのキーコードを配列へ並べ替え、VIA/QMK形式へ出力
|
||||||
|
|
||||||
|
保存したPHY・IDX・ELC・MAPは書き換えない。ELCのLEDチェーンの並び・番号は、このキーの取捨では変わらない。
|
||||||
|
|
||||||
## 次のステップ
|
## 次のステップ
|
||||||
|
|
||||||
1. **TypeScriptでの型定義作成**
|
1. **型定義の作成**(言語は `implementation-plan.md` で選定)
|
||||||
2. **変換ロジックのプロトタイプ実装**
|
2. **変換ロジックのプロトタイプ実装**
|
||||||
3. **テストケース(60%、Split等)での検証**
|
3. **テストケース(60%、Split等)での検証**
|
||||||
4. **既存KLE JSONからの変換機能**
|
4. **既存KLE JSONからの変換機能**
|
||||||
|
|
||||||
|
## 未確認事項
|
||||||
|
|
||||||
|
(なし)
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
[
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"p": "DCS R1"
|
||||||
|
},
|
||||||
|
"Esc",
|
||||||
|
{
|
||||||
|
"x": 0.25
|
||||||
|
},
|
||||||
|
"!\n1",
|
||||||
|
"@\n2",
|
||||||
|
"#\n3",
|
||||||
|
"$\n4",
|
||||||
|
"%\n5",
|
||||||
|
{
|
||||||
|
"x": 1
|
||||||
|
},
|
||||||
|
"^\n6",
|
||||||
|
"&\n7",
|
||||||
|
"*\n8",
|
||||||
|
"(\n9",
|
||||||
|
")\n0",
|
||||||
|
"_\n-",
|
||||||
|
{
|
||||||
|
"x": 0.75
|
||||||
|
},
|
||||||
|
"+\n="
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"p": "DCS R2",
|
||||||
|
"a": 0,
|
||||||
|
"w": 1.5
|
||||||
|
},
|
||||||
|
"Tab\n\n\n\n\n1.5",
|
||||||
|
{
|
||||||
|
"a": 4
|
||||||
|
},
|
||||||
|
"Q",
|
||||||
|
"W",
|
||||||
|
"E",
|
||||||
|
"R",
|
||||||
|
"T",
|
||||||
|
{
|
||||||
|
"x": 0.5
|
||||||
|
},
|
||||||
|
"Y",
|
||||||
|
"U",
|
||||||
|
"I",
|
||||||
|
"O",
|
||||||
|
"P",
|
||||||
|
"{\n[",
|
||||||
|
{
|
||||||
|
"x": 0.5,
|
||||||
|
"a": 0,
|
||||||
|
"w": 1.5
|
||||||
|
},
|
||||||
|
"Backspace\n\n\n\n\n1.5"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"p": "DCS R3",
|
||||||
|
"w": 1.75
|
||||||
|
},
|
||||||
|
"Ctrl\n\n\n\n\n1.75",
|
||||||
|
{
|
||||||
|
"a": 4
|
||||||
|
},
|
||||||
|
"A",
|
||||||
|
"S",
|
||||||
|
"D",
|
||||||
|
"F",
|
||||||
|
"G",
|
||||||
|
"H",
|
||||||
|
"J",
|
||||||
|
"K",
|
||||||
|
"L",
|
||||||
|
":\n;",
|
||||||
|
"\"\n'",
|
||||||
|
{
|
||||||
|
"a": 0,
|
||||||
|
"w": 2.25
|
||||||
|
},
|
||||||
|
"Enter\n\n\n\n\n2.25"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"p": "DCS R4",
|
||||||
|
"w": 1.25
|
||||||
|
},
|
||||||
|
"Shift\n\n\n\n\n1.25",
|
||||||
|
{
|
||||||
|
"x": 0.25,
|
||||||
|
"a": 4
|
||||||
|
},
|
||||||
|
"Z",
|
||||||
|
"X",
|
||||||
|
"C",
|
||||||
|
"V",
|
||||||
|
"B",
|
||||||
|
{
|
||||||
|
"x": 0.5
|
||||||
|
},
|
||||||
|
"N",
|
||||||
|
"M",
|
||||||
|
"<\n,",
|
||||||
|
">\n.",
|
||||||
|
"?\n/",
|
||||||
|
"|\n\\",
|
||||||
|
{
|
||||||
|
"x": 0.25,
|
||||||
|
"a": 0,
|
||||||
|
"w": 1.75
|
||||||
|
},
|
||||||
|
"Shift\n\n\n\n\n1.75"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"x": 1.25,
|
||||||
|
"w": 1.25
|
||||||
|
},
|
||||||
|
"Win\n\n\n\n\n1.25",
|
||||||
|
{
|
||||||
|
"w": 1.25
|
||||||
|
},
|
||||||
|
"Alt\n\n\n\n\n1.25",
|
||||||
|
{
|
||||||
|
"x": 6.5,
|
||||||
|
"w": 1.25
|
||||||
|
},
|
||||||
|
"Alt\n\n\n\n\n1.25",
|
||||||
|
{
|
||||||
|
"w": 1.25
|
||||||
|
},
|
||||||
|
"Ctrl\n\n\n\n\n1.25"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"r": 180,
|
||||||
|
"rx": 4.5,
|
||||||
|
"ry": 4.5,
|
||||||
|
"y": -0.5,
|
||||||
|
"x": -0.25,
|
||||||
|
"a": 4
|
||||||
|
},
|
||||||
|
"\n\n\nFn"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"rx": 6.75,
|
||||||
|
"y": -0.5,
|
||||||
|
"x": -2.5,
|
||||||
|
"p": "DCS SPACE",
|
||||||
|
"a": 3,
|
||||||
|
"w": 2.25
|
||||||
|
},
|
||||||
|
"\n\n\n\n\n2.25",
|
||||||
|
{
|
||||||
|
"w": 2.25
|
||||||
|
},
|
||||||
|
"\n\n\n\n\n2.25"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"rx": 9.5,
|
||||||
|
"y": -0.5,
|
||||||
|
"x": -0.75,
|
||||||
|
"p": "DCS R4",
|
||||||
|
"a": 4
|
||||||
|
},
|
||||||
|
"\n\n\nCode"
|
||||||
|
]
|
||||||
|
]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
["7", "8", "9", "/"],
|
||||||
|
["4", "5", "6", "*"],
|
||||||
|
["1", "2", "3", "-"],
|
||||||
|
["0", ".", "Enter", "+"]
|
||||||
|
]
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
{
|
||||||
|
"name": "cr0wn60",
|
||||||
|
"vendorId": "0xADE1",
|
||||||
|
"productId": "0x0001",
|
||||||
|
"lighting": "qmk_rgblight",
|
||||||
|
"keycodes": [],
|
||||||
|
"matrix": { "rows": 9, "cols": 9 },
|
||||||
|
"layouts": {
|
||||||
|
"keymap": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"p": "DCS R1"
|
||||||
|
},
|
||||||
|
"1,0",
|
||||||
|
{
|
||||||
|
"x": 0.25
|
||||||
|
},
|
||||||
|
"2,0",
|
||||||
|
"3,0",
|
||||||
|
"4,0",
|
||||||
|
"5,0",
|
||||||
|
"6,0",
|
||||||
|
{
|
||||||
|
"x": 1
|
||||||
|
},
|
||||||
|
"0,1",
|
||||||
|
"2,1",
|
||||||
|
"3,1",
|
||||||
|
"4,1",
|
||||||
|
"5,1",
|
||||||
|
"6,1",
|
||||||
|
{
|
||||||
|
"x": 0.75
|
||||||
|
},
|
||||||
|
"7,1"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"p": "DCS R2",
|
||||||
|
"a": 0,
|
||||||
|
"w": 1.5
|
||||||
|
},
|
||||||
|
"0,2",
|
||||||
|
{
|
||||||
|
"a": 4
|
||||||
|
},
|
||||||
|
"1,2",
|
||||||
|
"3,2",
|
||||||
|
"4,2",
|
||||||
|
"5,2",
|
||||||
|
"6,2",
|
||||||
|
{
|
||||||
|
"x": 0.5
|
||||||
|
},
|
||||||
|
"0,3",
|
||||||
|
"1,3",
|
||||||
|
"2,3",
|
||||||
|
"4,3",
|
||||||
|
"5,3",
|
||||||
|
"6,3",
|
||||||
|
{
|
||||||
|
"x": 0.5,
|
||||||
|
"a": 0,
|
||||||
|
"w": 1.5
|
||||||
|
},
|
||||||
|
"7,3"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"p": "DCS R3",
|
||||||
|
"w": 1.75
|
||||||
|
},
|
||||||
|
"0,4",
|
||||||
|
{
|
||||||
|
"a": 4
|
||||||
|
},
|
||||||
|
"1,4",
|
||||||
|
"2,4",
|
||||||
|
"3,4",
|
||||||
|
"5,4",
|
||||||
|
"6,4",
|
||||||
|
"0,5",
|
||||||
|
"1,5",
|
||||||
|
"2,5",
|
||||||
|
"3,5",
|
||||||
|
"4,5",
|
||||||
|
"6,5",
|
||||||
|
{
|
||||||
|
"a": 0,
|
||||||
|
"w": 2.25
|
||||||
|
},
|
||||||
|
"7,5"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"p": "DCS R4",
|
||||||
|
"w": 1.25
|
||||||
|
},
|
||||||
|
"0,6",
|
||||||
|
{
|
||||||
|
"x": 0.25,
|
||||||
|
"a": 4
|
||||||
|
},
|
||||||
|
"1,6",
|
||||||
|
"2,6",
|
||||||
|
"3,6",
|
||||||
|
"4,6",
|
||||||
|
"5,6",
|
||||||
|
{
|
||||||
|
"x": 0.5
|
||||||
|
},
|
||||||
|
"0,7",
|
||||||
|
"1,7",
|
||||||
|
"2,7",
|
||||||
|
"3,7",
|
||||||
|
"4,7",
|
||||||
|
"5,7",
|
||||||
|
{
|
||||||
|
"x": 0.25,
|
||||||
|
"a": 0,
|
||||||
|
"w": 1.75
|
||||||
|
},
|
||||||
|
"6,7"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"x": 1.25,
|
||||||
|
"w": 1.25
|
||||||
|
},
|
||||||
|
"0,8",
|
||||||
|
{
|
||||||
|
"w": 1.25
|
||||||
|
},
|
||||||
|
"1,8",
|
||||||
|
{
|
||||||
|
"x": 6.5,
|
||||||
|
"w": 1.25
|
||||||
|
},
|
||||||
|
"6,8",
|
||||||
|
{
|
||||||
|
"w": 1.25
|
||||||
|
},
|
||||||
|
"7,8"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"r": 180,
|
||||||
|
"rx": 4.5,
|
||||||
|
"ry": 4.5,
|
||||||
|
"y": -0.5,
|
||||||
|
"x": -0.25,
|
||||||
|
"a": 4
|
||||||
|
},
|
||||||
|
"2,8"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"rx": 6.75,
|
||||||
|
"y": -0.5,
|
||||||
|
"x": -2.5,
|
||||||
|
"p": "DCS SPACE",
|
||||||
|
"a": 3,
|
||||||
|
"w": 2.25
|
||||||
|
},
|
||||||
|
"4,8",
|
||||||
|
{
|
||||||
|
"w": 2.25
|
||||||
|
},
|
||||||
|
"3,8"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"rx": 9.5,
|
||||||
|
"y": -0.5,
|
||||||
|
"x": -0.75,
|
||||||
|
"p": "DCS R4",
|
||||||
|
"a": 4
|
||||||
|
},
|
||||||
|
"5,8"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user