@unlaxer/dde-toolkit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 unlaxer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,25 @@
1
+ ## DDE — Document Deficit Extraction
2
+
3
+ When the user says "DDE", "DDE して", "ドキュメントレビュー", "用語集を作って", "図が足りない", "リンクが足りない":
4
+
5
+ 1. Read `.claude/skills/dde-session.md` for the full methodology
6
+ 2. Read `dde/method.md` (or `kit/method.md`) for background
7
+ 3. Read `dde/flows/*.yaml` (or `kit/flows/*.yaml`) for session structure
8
+ 4. Identify the target document (argument or README.md)
9
+ 5. Collect project context: existing `docs/glossary/` articles, directory structure
10
+ 6. Detect reader level (expert / beginner / grandma)
11
+ 7. Analyze the document for: undefined terms, missing diagrams, reader gaps
12
+ 8. Output a Gap list with 3 categories (A. terms / B. diagrams / C. gaps)
13
+ 9. Save Gap list to `dde/sessions/`
14
+ 10. Show numbered next-action choices
15
+
16
+ When the user says "用語集記事を生成して" or selects action 1:
17
+ - Generate `docs/glossary/<term>.md` for each undefined term
18
+ - Use `dde-tool save docs/glossary/<term>.md` to save
19
+
20
+ When the user says "dde-link して" or selects action 4:
21
+ ```bash
22
+ npx dde-link <file>
23
+ ```
24
+
25
+ Details: `dde/method.md`, `dde/flows/`, `.claude/skills/dde-session.md`
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ // dde-install — DDE toolkit をプロジェクトにインストールする
3
+
4
+ import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, readdirSync } from 'fs';
5
+ import { join, dirname } from 'path';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const PKG_ROOT = join(__dirname, '..');
10
+ const TARGET = process.cwd();
11
+
12
+ const pkg = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'));
13
+ const VERSION = pkg.version;
14
+ const lang = detectLang();
15
+
16
+ function detectLang() {
17
+ const env = process.env.LANG || process.env.LC_ALL || '';
18
+ if (env.startsWith('en')) return 'en';
19
+ return 'ja';
20
+ }
21
+
22
+ function cp(src, dst) {
23
+ if (!existsSync(src)) return false;
24
+ mkdirSync(dirname(dst), { recursive: true });
25
+ copyFileSync(src, dst);
26
+ return true;
27
+ }
28
+
29
+ function cpDir(srcDir, dstDir) {
30
+ if (!existsSync(srcDir)) return;
31
+ mkdirSync(dstDir, { recursive: true });
32
+ for (const f of readdirSync(srcDir)) {
33
+ const src = join(srcDir, f);
34
+ const dst = join(dstDir, f);
35
+ cp(src, dst);
36
+ }
37
+ }
38
+
39
+ function appendSection(filePath, section, marker, label) {
40
+ if (existsSync(filePath)) {
41
+ const content = readFileSync(filePath, 'utf8');
42
+ if (content.includes(marker)) {
43
+ console.log(` ${label} already has DDE section`);
44
+ return;
45
+ }
46
+ writeFileSync(filePath, content + '\n' + section);
47
+ console.log(` ${label} updated`);
48
+ } else {
49
+ writeFileSync(filePath, section);
50
+ console.log(` ${label} created`);
51
+ }
52
+ }
53
+
54
+ // --- agents セクション(agents-dde-section.md から読む)---
55
+ const agentsSectionPath = join(PKG_ROOT, 'agents-dde-section.md');
56
+ const AGENTS_SECTION = existsSync(agentsSectionPath)
57
+ ? readFileSync(agentsSectionPath, 'utf8')
58
+ : '';
59
+
60
+ // --- .cursorrules セクション ---
61
+ const CURSORRULES_SECTION = `
62
+ # DDE — Document Deficit Extraction
63
+ # https://github.com/unlaxer/dde-toolkit
64
+
65
+ When the user says "DDE して", "ドキュメントレビュー", "用語集を作って":
66
+ - Read .claude/skills/dde-session.md for instructions
67
+ - Target document: argument or README.md
68
+ - Output Gap list (A. terms / B. diagrams / C. reader gaps)
69
+ - Save to dde/sessions/
70
+ - Generate docs/glossary/<term>.md for undefined terms
71
+ - Run: npx dde-link <file>
72
+ `.trimStart();
73
+
74
+ // =============================================================================
75
+ console.log(`DDE toolkit — installing to ${TARGET} (lang=${lang})\n`);
76
+
77
+ // 1. dde/ ディレクトリをプロジェクトに展開(DGE の dge/ と同じ構造)
78
+ const ddeDir = join(TARGET, 'dde');
79
+ mkdirSync(ddeDir, { recursive: true });
80
+ console.log(` dde/ created`);
81
+
82
+ // method.md
83
+ if (cp(join(PKG_ROOT, 'method.md'), join(ddeDir, 'method.md'))) {
84
+ console.log(` dde/method.md created`);
85
+ }
86
+
87
+ // flows/
88
+ const flowsSrc = join(PKG_ROOT, 'flows');
89
+ const flowsDst = join(ddeDir, 'flows');
90
+ if (existsSync(flowsSrc)) {
91
+ cpDir(flowsSrc, flowsDst);
92
+ console.log(` dde/flows/ created`);
93
+ }
94
+
95
+ // templates/
96
+ const tmplSrc = join(PKG_ROOT, 'templates');
97
+ const tmplDst = join(ddeDir, 'templates');
98
+ if (existsSync(tmplSrc) && readdirSync(tmplSrc).length > 0) {
99
+ cpDir(tmplSrc, tmplDst);
100
+ console.log(` dde/templates/ created`);
101
+ }
102
+
103
+ // bin/
104
+ const binSrc = join(PKG_ROOT, 'bin');
105
+ const binDst = join(ddeDir, 'bin');
106
+ mkdirSync(binDst, { recursive: true });
107
+ if (cp(join(binSrc, 'dde-tool.js'), join(binDst, 'dde-tool.js'))) {
108
+ console.log(` dde/bin/dde-tool.js created`);
109
+ }
110
+
111
+ // sessions/ (空ディレクトリ)
112
+ const sessionsDir = join(ddeDir, 'sessions');
113
+ if (!existsSync(sessionsDir)) {
114
+ mkdirSync(sessionsDir, { recursive: true });
115
+ writeFileSync(join(sessionsDir, '.gitkeep'), '');
116
+ console.log(` dde/sessions/ created`);
117
+ }
118
+
119
+ // version.txt
120
+ writeFileSync(join(ddeDir, 'version.txt'), VERSION + '\n');
121
+ console.log(` dde/version.txt created (v${VERSION})`);
122
+
123
+ // 2. docs/glossary/
124
+ const glossaryDir = join(TARGET, 'docs', 'glossary');
125
+ if (!existsSync(glossaryDir)) {
126
+ mkdirSync(glossaryDir, { recursive: true });
127
+ console.log(` docs/glossary/ created`);
128
+ } else {
129
+ console.log(` docs/glossary/ already exists`);
130
+ }
131
+
132
+ // 3. .claude/skills/
133
+ const skillsTarget = join(TARGET, '.claude', 'skills');
134
+ mkdirSync(skillsTarget, { recursive: true });
135
+
136
+ for (const file of ['dde-session.md', 'dde-update.md']) {
137
+ const src = join(PKG_ROOT, 'skills', file);
138
+ const dst = join(skillsTarget, file);
139
+ if (cp(src, dst)) {
140
+ console.log(` .claude/skills/${file} created`);
141
+ }
142
+ }
143
+
144
+ // 4. AGENTS.md
145
+ if (AGENTS_SECTION) {
146
+ appendSection(join(TARGET, 'AGENTS.md'), AGENTS_SECTION, '## DDE —', 'AGENTS.md');
147
+ }
148
+
149
+ // 5. GEMINI.md
150
+ if (AGENTS_SECTION) {
151
+ appendSection(join(TARGET, 'GEMINI.md'), AGENTS_SECTION, '## DDE —', 'GEMINI.md');
152
+ }
153
+
154
+ // 6. .cursorrules
155
+ appendSection(join(TARGET, '.cursorrules'), CURSORRULES_SECTION, '# DDE —', '.cursorrules');
156
+
157
+ // =============================================================================
158
+ console.log(`\nDone! DDE toolkit is ready. (v${VERSION}, lang=${lang})`);
159
+ console.log(`\n Claude Code で「DDE して」と言えば起動します。`);
160
+ console.log(` Codex (AGENTS.md), Gemini CLI (GEMINI.md), Cursor (.cursorrules) にも対応。`);
161
+ console.log(` Try: "README.md を DDE して"`);
162
+ console.log(`\nMIT License.`);
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ // dde-link CLI — Markdown ファイルに用語リンクを自動付与する
3
+
4
+ import { parseArgs } from 'node:util';
5
+ import { resolve } from 'node:path';
6
+ import { link } from '../lib/linker.js';
7
+
8
+ const { values, positionals } = parseArgs({
9
+ args: process.argv.slice(2),
10
+ options: {
11
+ check: { type: 'boolean', default: false },
12
+ fix: { type: 'boolean', default: false },
13
+ 'dry-run': { type: 'boolean', default: false },
14
+ glossary: { type: 'string' },
15
+ lang: { type: 'string', default: 'auto' },
16
+ dictionary: { type: 'string' },
17
+ help: { type: 'boolean', short: 'h', default: false },
18
+ },
19
+ allowPositionals: true,
20
+ });
21
+
22
+ if (values.help || positionals.length === 0) {
23
+ console.log(`
24
+ Usage: dde-link <file> [options]
25
+
26
+ Options:
27
+ --check リンク漏れを検出(exit code 1 で失敗)
28
+ --fix ファイルを上書き(デフォルト動作)
29
+ --dry-run 変更プレビュー(stdout に diff 出力)
30
+ --glossary 用語集ディレクトリ(デフォルト: docs/glossary/)
31
+ --lang 言語: auto / en / ja(デフォルト: auto)
32
+ --dictionary 辞書ファイルパス(デフォルト: docs/glossary/dictionary.yaml)
33
+ -h, --help このヘルプを表示
34
+ `.trim());
35
+ process.exit(0);
36
+ }
37
+
38
+ const filePath = resolve(positionals[0]);
39
+
40
+ try {
41
+ const result = link(filePath, {
42
+ glossaryDir: values.glossary ? resolve(values.glossary) : undefined,
43
+ dictionaryPath: values.dictionary ? resolve(values.dictionary) : undefined,
44
+ lang: values.lang,
45
+ check: values.check,
46
+ dryRun: values['dry-run'],
47
+ });
48
+
49
+ if (values.check) {
50
+ if (result.unlinked.length === 0) {
51
+ console.log('✓ リンク漏れなし');
52
+ process.exit(0);
53
+ } else {
54
+ console.log(`リンク漏れ: ${result.unlinked.length} 件`);
55
+ for (const u of result.unlinked) {
56
+ console.log(` "${u.term}" (${u.count} 箇所)`);
57
+ }
58
+ process.exit(1);
59
+ }
60
+ }
61
+
62
+ if (values['dry-run']) {
63
+ if (result.changeCount === 0) {
64
+ console.log('変更なし');
65
+ } else {
66
+ console.log(`変更予定: ${result.changeCount} 件\n`);
67
+ console.log(result.diff);
68
+ }
69
+ process.exit(0);
70
+ }
71
+
72
+ // --fix or default
73
+ if (result.changeCount === 0) {
74
+ console.log('変更なし');
75
+ } else {
76
+ console.log(`${result.changeCount} 件のリンクを追加しました: ${filePath}`);
77
+ }
78
+ process.exit(0);
79
+
80
+ } catch (err) {
81
+ console.error(`エラー: ${err.message}`);
82
+ process.exit(1);
83
+ }
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ // dde-tool — DDE MUST enforcement CLI
3
+
4
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
5
+ import { dirname, join } from 'path';
6
+
7
+ const VERSION = JSON.parse(
8
+ readFileSync(new URL('../package.json', import.meta.url), 'utf8')
9
+ ).version;
10
+
11
+ const command = process.argv[2];
12
+ const arg = process.argv[3];
13
+
14
+ function cmdSave() {
15
+ const file = arg;
16
+ if (!file) {
17
+ console.error('ERROR: file path required. Usage: echo "content" | dde-tool save <file>');
18
+ process.exit(1);
19
+ }
20
+
21
+ const dir = dirname(file);
22
+ mkdirSync(dir, { recursive: true });
23
+
24
+ let content = '';
25
+ process.stdin.setEncoding('utf8');
26
+ process.stdin.on('data', chunk => { content += chunk; });
27
+ process.stdin.on('end', () => {
28
+ writeFileSync(file, content);
29
+ const bytes = Buffer.byteLength(content);
30
+ console.log(`SAVED: ${file} (${bytes} bytes)`);
31
+ });
32
+ }
33
+
34
+ function cmdPrompt() {
35
+ const flow = arg || 'quick';
36
+
37
+ // dde/flows/ → kit/flows/ の順で探す
38
+ const candidates = [
39
+ join(process.cwd(), 'dde', 'flows', `${flow}.yaml`),
40
+ join(process.cwd(), 'kit', 'flows', `${flow}.yaml`),
41
+ ];
42
+
43
+ for (const yamlFile of candidates) {
44
+ if (existsSync(yamlFile)) {
45
+ const content = readFileSync(yamlFile, 'utf8');
46
+ const lines = content.split('\n');
47
+ let inPostActions = false;
48
+ const actions = [];
49
+
50
+ for (const line of lines) {
51
+ if (line.match(/^post_actions:/)) { inPostActions = true; continue; }
52
+ if (inPostActions && line.match(/^\S/) && !line.match(/^\s/)) break;
53
+ if (inPostActions) {
54
+ const match = line.match(/display_name:\s*"(.+?)"/);
55
+ if (match) actions.push(match[1]);
56
+ }
57
+ }
58
+
59
+ if (actions.length > 0) {
60
+ actions.forEach((a, i) => console.log(` ${i + 1}. ${a}`));
61
+ return;
62
+ }
63
+ }
64
+ }
65
+
66
+ // デフォルト選択肢
67
+ console.log(' 1. 用語集記事を全部生成する');
68
+ console.log(' 2. mermaid 図を生成する');
69
+ console.log(' 3. 読者ギャップを修正提案する');
70
+ console.log(' 4. dde-link でリンクを付ける');
71
+ console.log(' 5. 後で');
72
+ }
73
+
74
+ function cmdVersion() {
75
+ console.log(`dde-tool v${VERSION}`);
76
+ }
77
+
78
+ function cmdHelp() {
79
+ console.log(`dde-tool v${VERSION} — DDE MUST enforcement CLI
80
+
81
+ Commands:
82
+ save <file> Save stdin to file (MUST: always save Gap list)
83
+ prompt [flow] Show numbered choices from flow YAML (MUST: show choices)
84
+ version Show version
85
+ help Show this help
86
+
87
+ Examples:
88
+ echo "gap content" | dde-tool save dde/sessions/2026-01-01-readme.md
89
+ dde-tool prompt quick`);
90
+ }
91
+
92
+ switch (command) {
93
+ case 'save': cmdSave(); break;
94
+ case 'prompt': cmdPrompt(); break;
95
+ case 'version':
96
+ case '-v':
97
+ case '--version': cmdVersion(); break;
98
+ case 'help':
99
+ case '-h':
100
+ case '--help':
101
+ case undefined: cmdHelp(); break;
102
+ default:
103
+ console.error(`ERROR: unknown command "${command}". Run "dde-tool help" for usage.`);
104
+ process.exit(1);
105
+ }
@@ -0,0 +1,78 @@
1
+ name: quick
2
+ display_name: "⚡ クイック — 用語を抽出して単語帳を作る"
3
+
4
+ trigger_keywords: ["DDE して", "DDE", "ドキュメントレビュー", "用語集を作って", "単語帳を作って", "リンクが足りない"]
5
+
6
+ defaults:
7
+ exclude_dirs: ["dde", "dge", "node_modules", ".git", ".claude"]
8
+ reader_level: beginner
9
+ article_length: medium # short / medium / long / custom
10
+
11
+ workflow:
12
+ steps:
13
+ - id: select_docs
14
+ display_name: "対象ドキュメント群を選択"
15
+ mode: confirm
16
+ note: "md 一覧(多い場合はフォルダ一覧)を出してユーザーが除外指定"
17
+ - id: level
18
+ display_name: "読者レベル設定"
19
+ mode: confirm
20
+ note: "LLM がドキュメントから推定して提案、ユーザーが確認"
21
+ - id: article_length
22
+ display_name: "記事の分量設定"
23
+ mode: confirm
24
+ note: "short(〜200字)/ medium(〜500字)/ long(〜1000字)/ 文字数直指定"
25
+ - id: extract
26
+ display_name: "用語抽出(LLM)"
27
+ actor: llm
28
+ note: "対象ドキュメント群から一括抽出。既存 docs/glossary/ は除外"
29
+ - id: confirm_terms
30
+ display_name: "用語一覧を確認"
31
+ mode: confirm
32
+ - id: articleize
33
+ display_name: "記事生成(LLM)"
34
+ actor: llm
35
+ - id: save
36
+ display_name: "単語帳保存"
37
+ - id: link
38
+ display_name: "dde-link 実行(CLI)"
39
+ actor: cli
40
+ note: "対象ドキュメント群と同じファイルセットに適用"
41
+
42
+ must_rules:
43
+ - id: save
44
+ text: "生成した記事は保存(無条件)"
45
+ - id: output_table
46
+ text: "抽出した用語を一覧テーブルで出力してからユーザーに確認"
47
+ - id: choices
48
+ text: "記事生成後に番号付き選択肢を提示(省略しない)"
49
+
50
+ extract:
51
+ actor: llm
52
+ scope: document_group # 1ファイルではなく選択されたドキュメント群全体
53
+ filter: "選択した読者レベルで分からない用語のみ(docs/glossary/ に既存記事があれば除外)"
54
+ output: "用語一覧テーブル(用語 / 出現ドキュメント / 重要度)"
55
+
56
+ articleize:
57
+ actor: llm
58
+ per: term
59
+ output_dir: docs/glossary/
60
+ filename_rule: "<slug>.md(英語), <slug>.ja.md(日本語)"
61
+ length:
62
+ short: "〜200字。定義1文 + 例1文"
63
+ medium: "〜500字。定義 + 説明 + 例。デフォルト"
64
+ long: "〜1000字。定義 + 詳細 + コード例 + 関連用語"
65
+
66
+ output_dir: dde/sessions/
67
+
68
+ post_actions:
69
+ - id: more_docs
70
+ display_name: "別のドキュメント群も処理する"
71
+ - id: another_level
72
+ display_name: "別の読者レベルで再実行する"
73
+ - id: run_link
74
+ display_name: "dde-link でリンクを付ける(まだの場合)"
75
+ - id: diagrams
76
+ display_name: "図が必要な箇所を検出する"
77
+ - id: later
78
+ display_name: "後で"
@@ -0,0 +1,117 @@
1
+ // dictionary.js — 用語辞書の構築
2
+ // docs/glossary/ の .md ファイル名から用語を自動推定し、
3
+ // dictionary.yaml があれば上書きする
4
+
5
+ import { readdirSync, readFileSync, existsSync } from 'fs';
6
+ import { join, basename } from 'path';
7
+ import { parse as parseYaml } from 'yaml';
8
+
9
+ /**
10
+ * ファイル名(拡張子なし)から英語用語バリエーションを生成
11
+ * jwt → ["JWT", "jwt"]
12
+ * multi-tenant → ["multi-tenant", "Multi-tenant", "multi tenant", "Multi tenant"]
13
+ * session-management → ["session management", "Session management", "Session Management", "session-management", "Session-management"]
14
+ */
15
+ export function inferTerms(slug) {
16
+ const withSpaces = slug.replace(/-/g, ' ');
17
+ const withHyphens = slug;
18
+
19
+ const variants = new Set();
20
+
21
+ // 3文字以下はすべて大文字バリエーション追加(JWT, XSS, SQL など)
22
+ if (slug.replace(/-/g, '').length <= 3) {
23
+ variants.add(slug.toUpperCase());
24
+ variants.add(slug.toLowerCase());
25
+ }
26
+
27
+ // スペース版
28
+ variants.add(withSpaces);
29
+ variants.add(capitalize(withSpaces));
30
+ variants.add(titleCase(withSpaces));
31
+
32
+ // ハイフン版(元のまま)
33
+ if (withHyphens !== withSpaces) {
34
+ variants.add(withHyphens);
35
+ variants.add(capitalize(withHyphens));
36
+ }
37
+
38
+ return [...variants].filter(Boolean);
39
+ }
40
+
41
+ function capitalize(str) {
42
+ if (!str) return str;
43
+ return str.charAt(0).toUpperCase() + str.slice(1);
44
+ }
45
+
46
+ function titleCase(str) {
47
+ return str.replace(/\b\w/g, c => c.toUpperCase());
48
+ }
49
+
50
+ /**
51
+ * 用語辞書を構築して返す
52
+ * @param {string} glossaryDir - 用語集ディレクトリ(docs/glossary/ など)
53
+ * @param {string|null} dictionaryPath - dictionary.yaml のパス
54
+ * @param {string} lang - 'en' | 'ja'
55
+ * @returns {{ term: string, file: string, lang: string }[]}
56
+ */
57
+ export function buildDictionary(glossaryDir, dictionaryPath, lang = 'en') {
58
+ const entries = [];
59
+
60
+ // 1. glossaryDir の .md ファイルを収集(.ja.md は除外)
61
+ if (existsSync(glossaryDir)) {
62
+ const files = readdirSync(glossaryDir).filter(f => {
63
+ return f.endsWith('.md') && !f.endsWith('.ja.md') && f !== 'README.md';
64
+ });
65
+
66
+ for (const file of files) {
67
+ const slug = basename(file, '.md');
68
+ const filePath = join(glossaryDir, file);
69
+ const terms = inferTerms(slug);
70
+ for (const term of terms) {
71
+ entries.push({ term, file: filePath, lang: 'en' });
72
+ }
73
+ }
74
+ }
75
+
76
+ // 2. dictionary.yaml で上書き・追加
77
+ const dictPath = dictionaryPath || join(glossaryDir, 'dictionary.yaml');
78
+ if (existsSync(dictPath)) {
79
+ const raw = readFileSync(dictPath, 'utf8');
80
+ const dict = parseYaml(raw);
81
+
82
+ for (const [filename, config] of Object.entries(dict || {})) {
83
+ if (!config || filename === 'README.md') continue;
84
+ const filePath = join(glossaryDir, filename);
85
+
86
+ // 既存エントリを削除して上書き
87
+ const idx = entries.findIndex(e => e.file === filePath);
88
+ if (idx !== -1) {
89
+ // そのファイルの全エントリを削除
90
+ const toRemove = entries.filter(e => e.file === filePath);
91
+ for (const r of toRemove) {
92
+ entries.splice(entries.indexOf(r), 1);
93
+ }
94
+ }
95
+
96
+ const enTerms = config.en || [];
97
+ for (const term of enTerms) {
98
+ entries.push({ term, file: filePath, lang: 'en' });
99
+ }
100
+
101
+ if (lang === 'ja') {
102
+ const jaTerms = config.ja || [];
103
+ for (const term of jaTerms) {
104
+ // .ja.md があればそちらにリンク、なければ .md にリンク
105
+ const jaFile = filePath.replace(/\.md$/, '.ja.md');
106
+ const targetFile = existsSync(jaFile) ? jaFile : filePath;
107
+ entries.push({ term, file: targetFile, lang: 'ja' });
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ // 3. 文字数降順ソート(最長一致のため)
114
+ entries.sort((a, b) => b.term.length - a.term.length);
115
+
116
+ return entries;
117
+ }
package/lib/linker.js ADDED
@@ -0,0 +1,104 @@
1
+ // linker.js — dde-link のオーケストレーター
2
+ // dictionary + markdown を組み合わせてリンク処理を実行する
3
+
4
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
5
+ import { join, dirname } from 'path';
6
+ import { buildDictionary } from './dictionary.js';
7
+ import { processMarkdown, findUnlinked } from './markdown.js';
8
+
9
+ /**
10
+ * ファイルの言語を検出する
11
+ * .ja.md → 'ja', それ以外 → 'en'
12
+ */
13
+ export function detectLang(filePath, forceLang = 'auto') {
14
+ if (forceLang && forceLang !== 'auto') return forceLang;
15
+ return filePath.endsWith('.ja.md') ? 'ja' : 'en';
16
+ }
17
+
18
+ /**
19
+ * テキスト diff を生成(シンプルな行単位)
20
+ */
21
+ function createDiff(original, modified) {
22
+ const origLines = original.split('\n');
23
+ const modLines = modified.split('\n');
24
+ const lines = [];
25
+ const maxLen = Math.max(origLines.length, modLines.length);
26
+
27
+ for (let i = 0; i < maxLen; i++) {
28
+ const o = origLines[i];
29
+ const m = modLines[i];
30
+ if (o === m) {
31
+ lines.push(` ${o ?? ''}`);
32
+ } else {
33
+ if (o !== undefined) lines.push(`- ${o}`);
34
+ if (m !== undefined) lines.push(`+ ${m}`);
35
+ }
36
+ }
37
+ return lines.join('\n');
38
+ }
39
+
40
+ /**
41
+ * メイン処理
42
+ * @param {string} filePath - 対象 Markdown ファイルのパス
43
+ * @param {object} options
44
+ * @param {string} [options.glossaryDir] - 用語集ディレクトリ
45
+ * @param {string} [options.dictionaryPath] - dictionary.yaml のパス
46
+ * @param {string} [options.lang] - 'auto' | 'en' | 'ja'
47
+ * @param {boolean} [options.check] - リンク漏れ検出モード
48
+ * @param {boolean} [options.dryRun] - diff プレビューモード
49
+ * @returns {{ changeCount: number, unlinked?: any[], diff?: string }}
50
+ */
51
+ export function link(filePath, options = {}) {
52
+ if (!existsSync(filePath)) {
53
+ throw new Error(`File not found: ${filePath}`);
54
+ }
55
+
56
+ const lang = detectLang(filePath, options.lang);
57
+
58
+ // glossaryDir のデフォルト: ファイルから見て docs/glossary/
59
+ // または CWD からの docs/glossary/
60
+ const glossaryDir = options.glossaryDir
61
+ || findGlossaryDir(filePath)
62
+ || join(process.cwd(), 'docs', 'glossary');
63
+
64
+ const dictionaryPath = options.dictionaryPath
65
+ || join(glossaryDir, 'dictionary.yaml');
66
+
67
+ const dict = buildDictionary(glossaryDir, dictionaryPath, lang);
68
+ const original = readFileSync(filePath, 'utf8');
69
+
70
+ if (options.check) {
71
+ const unlinked = findUnlinked(original, dict, lang);
72
+ return { changeCount: unlinked.length, unlinked };
73
+ }
74
+
75
+ const { content, changeCount } = processMarkdown(original, dict, lang, filePath);
76
+
77
+ if (options.dryRun) {
78
+ const diff = createDiff(original, content);
79
+ return { changeCount, diff };
80
+ }
81
+
82
+ // ファイル上書き
83
+ if (content !== original) {
84
+ writeFileSync(filePath, content, 'utf8');
85
+ }
86
+
87
+ return { changeCount };
88
+ }
89
+
90
+ /**
91
+ * ファイルパスから docs/glossary/ を探す
92
+ * 親ディレクトリを辿って docs/glossary/ が見つかれば返す
93
+ */
94
+ function findGlossaryDir(filePath) {
95
+ let dir = dirname(filePath);
96
+ for (let i = 0; i < 10; i++) {
97
+ const candidate = join(dir, 'docs', 'glossary');
98
+ if (existsSync(candidate)) return candidate;
99
+ const parent = dirname(dir);
100
+ if (parent === dir) break;
101
+ dir = parent;
102
+ }
103
+ return null;
104
+ }
@@ -0,0 +1,199 @@
1
+ // markdown.js — Markdown のパースと用語リンク化
2
+ // unified + remark を使って AST を操作する
3
+
4
+ import { unified } from 'unified';
5
+ import remarkParse from 'remark-parse';
6
+ import remarkStringify from 'remark-stringify';
7
+ import { visit } from 'unist-util-visit';
8
+ import { relative, dirname, resolve } from 'path';
9
+
10
+ /**
11
+ * テキストノードに対して用語マッチングを行い、リンクノードに置換する
12
+ * @param {string} content - Markdown テキスト
13
+ * @param {{ term: string, file: string, lang: string }[]} dictionary - ソート済み辞書
14
+ * @param {string} lang - 'en' | 'ja'
15
+ * @param {string} [sourceFile] - リンクを埋め込むファイルのパス(相対パス計算用)
16
+ * @returns {{ content: string, changeCount: number }}
17
+ */
18
+ export function processMarkdown(content, dictionary, lang = 'en', sourceFile = null) {
19
+ if (!dictionary || dictionary.length === 0) {
20
+ return { content, changeCount: 0 };
21
+ }
22
+
23
+ const tree = unified().use(remarkParse).parse(content);
24
+ let changeCount = 0;
25
+
26
+ // 段落ごとにマッチ済み用語を追跡
27
+ // { paragraphNode → Set<term> }
28
+ const matchedPerParagraph = new Map();
29
+
30
+ // リンク化をスキップするノード種別
31
+ const skipTypes = new Set(['code', 'inlineCode', 'heading', 'link', 'image']);
32
+
33
+ // スキップ対象の祖先を持つか判定するため、祖先スタックを管理
34
+ visit(tree, (node, index, parent) => {
35
+ if (node.type !== 'text') return;
36
+
37
+ // 祖先にスキップ対象があるか確認
38
+ // unist-util-visit は祖先を直接渡さないので、
39
+ // parent の type をチェック(link の子テキストはスキップ)
40
+ if (parent && skipTypes.has(parent.type)) return;
41
+
42
+ // 段落の親を特定(マッチカウント用)
43
+ // 段落内のテキストノードは同じ paragraph 祖先を持つ
44
+ // ここでは parent が paragraph または tableCell などを想定
45
+ const paragraphKey = findParagraphAncestor(tree, node);
46
+
47
+ if (!matchedPerParagraph.has(paragraphKey)) {
48
+ matchedPerParagraph.set(paragraphKey, new Set());
49
+ }
50
+ const matched = matchedPerParagraph.get(paragraphKey);
51
+
52
+ // 辞書の各用語でマッチング(最長一致順にソート済み)
53
+ const text = node.value;
54
+ const replacements = findReplacements(text, dictionary, matched, lang);
55
+
56
+ if (replacements.length === 0) return;
57
+
58
+ // テキストノードを複数ノード(text + link)に分割
59
+ const newNodes = buildNodes(text, replacements, dictionary, sourceFile);
60
+ if (newNodes.length === 0) return;
61
+
62
+ // parent の children 内で node を newNodes に置換
63
+ if (parent && Array.isArray(parent.children)) {
64
+ const idx = parent.children.indexOf(node);
65
+ if (idx !== -1) {
66
+ parent.children.splice(idx, 1, ...newNodes);
67
+ changeCount += replacements.length;
68
+ // マッチした用語を記録
69
+ for (const r of replacements) {
70
+ matched.add(r.term);
71
+ }
72
+ }
73
+ }
74
+ });
75
+
76
+ const result = unified().use(remarkStringify, {
77
+ bullet: '-',
78
+ fences: true,
79
+ }).stringify(tree);
80
+
81
+ return { content: result, changeCount };
82
+ }
83
+
84
+ /**
85
+ * テキスト内で用語の置換箇所を検出する
86
+ * - 段落ごとに 1 用語 1 回まで
87
+ * - 最長一致(辞書はすでに降順ソート済み)
88
+ */
89
+ function findReplacements(text, dictionary, alreadyMatched, lang) {
90
+ // 使用済み範囲を追跡(重複マッチ防止)
91
+ const usedRanges = [];
92
+ const replacements = [];
93
+
94
+ for (const entry of dictionary) {
95
+ if (entry.lang !== lang && entry.lang !== 'en') continue;
96
+ if (alreadyMatched.has(entry.term)) continue;
97
+
98
+ const idx = text.indexOf(entry.term);
99
+ if (idx === -1) continue;
100
+
101
+ // 重複範囲チェック
102
+ const start = idx;
103
+ const end = idx + entry.term.length;
104
+ const overlaps = usedRanges.some(r => start < r.end && end > r.start);
105
+ if (overlaps) continue;
106
+
107
+ usedRanges.push({ start, end });
108
+ replacements.push({ term: entry.term, file: entry.file, start, end });
109
+ }
110
+
111
+ // 位置順にソート
112
+ replacements.sort((a, b) => a.start - b.start);
113
+ return replacements;
114
+ }
115
+
116
+ /**
117
+ * テキストと置換リストから AST ノード配列を生成
118
+ * @param {string} sourceFile - リンクを埋め込むファイルのパス(相対パス計算用)
119
+ */
120
+ function buildNodes(text, replacements, dictionary, sourceFile = null) {
121
+ const nodes = [];
122
+ let cursor = 0;
123
+
124
+ for (const r of replacements) {
125
+ // 置換前のテキスト
126
+ if (r.start > cursor) {
127
+ nodes.push({ type: 'text', value: text.slice(cursor, r.start) });
128
+ }
129
+ // リンクURL: sourceFile がある場合は相対パスに変換
130
+ let url = r.file;
131
+ if (sourceFile) {
132
+ const from = dirname(resolve(sourceFile));
133
+ const to = resolve(r.file);
134
+ url = relative(from, to);
135
+ // Windows パス区切りを / に統一
136
+ url = url.replace(/\\/g, '/');
137
+ }
138
+ // リンクノード
139
+ nodes.push({
140
+ type: 'link',
141
+ url,
142
+ children: [{ type: 'text', value: r.term }],
143
+ });
144
+ cursor = r.end;
145
+ }
146
+
147
+ // 残りのテキスト
148
+ if (cursor < text.length) {
149
+ nodes.push({ type: 'text', value: text.slice(cursor) });
150
+ }
151
+
152
+ return nodes;
153
+ }
154
+
155
+ /**
156
+ * ノードが属する段落(または親ブロック)を探して識別子として返す
157
+ * 単純に node への参照をキーとして使う
158
+ */
159
+ function findParagraphAncestor(tree, targetNode) {
160
+ // 段落を特定するために tree を走査
161
+ let found = null;
162
+ visit(tree, ['paragraph', 'tableCell', 'listItem'], (node) => {
163
+ if (found) return;
164
+ // このノードの子孫に targetNode があるか
165
+ let has = false;
166
+ visit(node, 'text', (t) => {
167
+ if (t === targetNode) has = true;
168
+ });
169
+ if (has) found = node;
170
+ });
171
+ return found || targetNode;
172
+ }
173
+
174
+ /**
175
+ * Markdown 内でリンクされていない用語を検出する(--check 用)
176
+ * @returns {{ term: string, file: string, count: number }[]}
177
+ */
178
+ export function findUnlinked(content, dictionary, lang = 'en') {
179
+ const tree = unified().use(remarkParse).parse(content);
180
+ const skipTypes = new Set(['code', 'inlineCode', 'heading', 'link', 'image']);
181
+ const unlinked = new Map(); // term → { file, count }
182
+
183
+ visit(tree, 'text', (node, index, parent) => {
184
+ if (parent && skipTypes.has(parent.type)) return;
185
+
186
+ for (const entry of dictionary) {
187
+ if (entry.lang !== lang && entry.lang !== 'en') continue;
188
+ if (node.value.includes(entry.term)) {
189
+ const key = entry.term;
190
+ if (!unlinked.has(key)) {
191
+ unlinked.set(key, { term: entry.term, file: entry.file, count: 0 });
192
+ }
193
+ unlinked.get(key).count++;
194
+ }
195
+ }
196
+ });
197
+
198
+ return [...unlinked.values()];
199
+ }
package/method.md ADDED
@@ -0,0 +1,93 @@
1
+ # DDE Method — Document Deficit Extraction
2
+
3
+ ## 前提条件
4
+
5
+ - **必要なもの**: LLM(Claude, GPT-4, Gemini 等)へのアクセス
6
+ - **推奨**: Claude Code(skills/ で自動発動)
7
+ - **入力**: レビュー対象のドキュメント(README, API 仕様, 設計書など)
8
+ - **出力**: 用語集記事 + 単語帳(dictionary.yaml)+ クリッカブルリンク
9
+
10
+ ---
11
+
12
+ ## 最低限これだけ読め(3分版)
13
+
14
+ > **TL;DR** — 以下を読めば DDE を始められる。
15
+
16
+ **DDE とは**: 読者レベルを決めて、ドキュメントの用語を抜き出して、記事化する。それを単語帳にして、ドキュメントをクリッカブルにする。
17
+
18
+ **4 ステップ**:
19
+ 1. **Level** — 想定読者のレベルを決める(expert / beginner / grandma)
20
+ 2. **Extract** — そのレベルで「分からない用語」をドキュメントから抽出する
21
+ 3. **Articleize** — 用語ごとに glossary 記事を生成する
22
+ 4. **Link** — dde-link で用語をクリッカブルにする
23
+
24
+ **最初にやること**:
25
+ 1. ドキュメントを用意する(README.md など)
26
+ 2. 「DDE して」と Claude Code に言う
27
+ 3. 読者レベルを確認または指定する
28
+ 4. 生成された記事をレビューして承認する
29
+
30
+ ---
31
+
32
+ ## 原理
33
+
34
+ 普通のドキュメントレビューは「書いてあることの検証」。
35
+ DDE は「読者には分からない用語の洗い出し」。
36
+
37
+ 書いた側には自明な用語が、読者には不透明なまま残っている。
38
+ この「前提の非対称」を、読者レベルを明示して LLM に検出させる。
39
+
40
+ ---
41
+
42
+ ## DDE の 4 ステップ
43
+
44
+ ```
45
+ Step 1: Level — 想定読者を決める
46
+ 「beginner」「expert」「grandma」のどれか。
47
+ 曖昧なら beginner をデフォルトにする。
48
+
49
+ Step 2: Extract — 用語を抽出する
50
+ LLM がドキュメントを読んで、
51
+ そのレベルの読者が分からない用語を列挙する。
52
+ docs/glossary/ に既存記事があれば除外する。
53
+
54
+ Step 3: Articleize — 記事を生成する
55
+ 用語ごとに docs/glossary/<slug>.md を生成する。
56
+ レベルに応じてトーンを変える。
57
+
58
+ Step 4: Link — dde-link を実行する
59
+ npx dde-link <file>
60
+ 用語を [term](docs/glossary/xxx.md) に自動置換する。
61
+ ```
62
+
63
+ ---
64
+
65
+ ## 3 段階の読者レベル
66
+
67
+ ```
68
+ expert — 同分野の開発者(定義 + 技術詳細 + コード例)
69
+ beginner — 初学者(やさしい説明 + 具体例)
70
+ grandma — 技術ゼロ(身近な例えだけ)
71
+ ```
72
+
73
+ 同じ用語でも、レベルによって抽出される用語も記事のトーンも変わる。
74
+
75
+ ---
76
+
77
+ ## dde-link の動作
78
+
79
+ ```
80
+ 1. docs/glossary/ の .md ファイル名から用語を自動推定
81
+ 2. dictionary.yaml があれば上書き(日本語用語・別名対応)
82
+ 3. 最長一致、段落ごとに 1 回 → [用語](docs/glossary/xxx.md) に置換
83
+ スキップ: コードブロック / インラインコード / 見出し / 既存リンク
84
+ ```
85
+
86
+ ---
87
+
88
+ ## 実績(volta-auth-proxy)
89
+
90
+ - 241 の用語集記事(120 EN + 121 JA)
91
+ - README に 334 のクリッカブルリンク
92
+ - 11 の mermaid フロー図
93
+ - 3 段階の読者レベル対応
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@unlaxer/dde-toolkit",
3
+ "version": "0.1.0",
4
+ "description": "Document Deficit Extraction — find what's not understood in your docs",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "dde-install": "bin/dde-install.js",
9
+ "dde-link": "bin/dde-link.js",
10
+ "dde-tool": "bin/dde-tool.js"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "lib/",
15
+ "flows/",
16
+ "skills/",
17
+ "templates/",
18
+ "method.md",
19
+ "agents-dde-section.md",
20
+ "version.txt",
21
+ "LICENSE"
22
+ ],
23
+ "keywords": [
24
+ "documentation",
25
+ "glossary",
26
+ "auto-link",
27
+ "mermaid",
28
+ "llm",
29
+ "claude-code",
30
+ "dde",
31
+ "dge"
32
+ ],
33
+ "dependencies": {
34
+ "unified": "^11.0.0",
35
+ "remark-parse": "^11.0.0",
36
+ "remark-stringify": "^11.0.0",
37
+ "unist-util-visit": "^5.0.0",
38
+ "yaml": "^2.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "vitest": "^2.0.0"
42
+ },
43
+ "scripts": {
44
+ "test": "vitest run",
45
+ "test:watch": "vitest"
46
+ }
47
+ }
@@ -0,0 +1,223 @@
1
+ <!-- DDE-toolkit (MIT License) -->
2
+
3
+ # Skill: DDE Session
4
+
5
+ ## Trigger
6
+ 「DDE して」「用語集を作って」「単語帳を作って」「ドキュメントレビューして」「リンクが足りない」「<ファイル名> を DDE して」
7
+
8
+ ## MUST(3 個。これだけ守れ)
9
+ 1. **生成した記事は無条件で保存。** ユーザーに聞かない。
10
+ 2. **抽出した用語を一覧テーブルで出力してからユーザーに確認。**
11
+ 3. **記事生成後に番号付き選択肢を提示。省略するな。**
12
+
13
+ ---
14
+
15
+ ## 初回オンボーディング
16
+ テーマなしで「DDE」「DDE って何」と呼ばれたとき:
17
+
18
+ ```
19
+ DDE toolkit v0.1.0 — Document Deficit Extraction
20
+
21
+ 読者レベルを決めて → 用語を抽出して → 記事化 → 単語帳 → クリッカブルなドキュメント
22
+
23
+ 📖 フロー:
24
+ 1. ドキュメント群を選択(対象ファイル・除外フォルダを確認)
25
+ 2. 読者レベルを設定(LLM が推定して提案)
26
+ 3. 記事の分量を設定(short / medium / long)
27
+ 4. 用語を LLM が一括抽出 → 一覧確認
28
+ 5. 用語ごとに glossary 記事を生成 → docs/glossary/
29
+ 6. dde-link が対象ドキュメント群にリンクを埋め込む
30
+
31
+ 👥 3 段階の読者レベル:
32
+ expert — 同分野の開発者
33
+ beginner — 初学者(デフォルト)
34
+ grandma — 技術ゼロ
35
+
36
+ 🔗 dde-link(LLM 不要):
37
+ npx dde-link README.md # 自動リンク実行
38
+ npx dde-link README.md --check # リンク漏れチェック(CI 用)
39
+ npx dde-link README.md --dry-run # プレビュー
40
+
41
+ 詳しくは: dde/method.md
42
+ ```
43
+
44
+ ---
45
+
46
+ ## 手順
47
+
48
+ ### Step 0: flow 読み込み
49
+ `dde/flows/` の YAML、なければ `kit/flows/` の YAML を読む。
50
+ `defaults.exclude_dirs` を確認する(デフォルト: `dde/`, `dge/`, `node_modules/`, `.git/`, `.claude/`)。
51
+
52
+ ### Step 1: 対象ドキュメント群の選択(MUST: ユーザーに確認)
53
+
54
+ プロジェクト内の `.md` ファイルを列挙する。
55
+ - **50 ファイル未満**: ファイル一覧を表示
56
+ - **50 ファイル以上**: フォルダ一覧を表示
57
+
58
+ ```
59
+ 対象ドキュメントを選択してください。
60
+
61
+ フォルダ一覧(dde/, dge/, node_modules/ は除外済み):
62
+ ✅ docs/ (12 ファイル)
63
+ ✅ README.md
64
+ ✅ CONTRIBUTING.md
65
+ ⬜ docs/internal/ (除外候補)
66
+
67
+ 除外したいファイル・フォルダがあれば指定してください。
68
+ なければそのまま進みます。
69
+ ```
70
+
71
+ 確定したファイルセットが「抽出元 = リンク適用先」になる。
72
+
73
+ ### Step 2: 読者レベルの設定(MUST: LLM が推定してから確認)
74
+
75
+ 対象ドキュメントの語彙・トーンを見て LLM がレベルを推定し、ユーザーに提案する。
76
+
77
+ ```
78
+ ドキュメントを読みました。推定: beginner
79
+
80
+ 根拠: 専門用語が多いが説明がない。初学者には伝わらない箇所が目立つ。
81
+
82
+ 読者レベルを確認してください:
83
+ 1. expert — 同分野の開発者
84
+ 2. beginner — 初学者 ← 推定
85
+ 3. grandma — 技術ゼロ
86
+
87
+ このまま beginner で進めますか?
88
+ ```
89
+
90
+ ### Step 3: 記事の分量設定
91
+
92
+ ```
93
+ 記事の分量を選んでください:
94
+ 1. short — 〜200字(定義1文 + 例1文)
95
+ 2. medium — 〜500字(定義 + 説明 + 例)← デフォルト
96
+ 3. long — 〜1000字(定義 + 詳細 + コード例 + 関連用語)
97
+ 4. カスタム — 文字数を直接指定
98
+
99
+ このまま medium で進めますか?
100
+ ```
101
+
102
+ ### Step 4: 用語抽出(LLM)
103
+
104
+ 対象ドキュメント群を読んで、選択した読者レベルで「分からない可能性がある用語」を一括抽出する。
105
+
106
+ **ルール:**
107
+ - `docs/glossary/` に既存記事があるものは除外(スラッグが一致するもの)
108
+ - 固有名詞(プロダクト名・企業名など)は除外
109
+ - コードブロック内の識別子は除外
110
+ - 重要度: その用語なしでは内容が理解できない → 🔴 High / 補足があると助かる → 🟡 Medium
111
+
112
+ **出力(MUST: ユーザーに確認してから記事生成へ進む):**
113
+
114
+ ```
115
+ ## 抽出した用語一覧(beginner 視点)
116
+
117
+ | # | 用語 | 出現ドキュメント | 既存記事 | 重要度 |
118
+ |---|------|----------------|---------|--------|
119
+ | 1 | JWT | README.md, docs/auth.md | なし | 🔴 High |
120
+ | 2 | OAuth 2.0 | docs/auth.md | なし | 🔴 High |
121
+ | 3 | リフレッシュトークン | docs/auth.md | なし | 🟡 Medium |
122
+
123
+ N 件抽出しました。記事を生成しますか?
124
+ 除外したい用語があれば番号で指定してください(例: 3,5)。
125
+ ```
126
+
127
+ ### Step 5: 記事生成(LLM)
128
+
129
+ 確認後、用語ごとに `docs/glossary/<slug>.md` を生成して保存する。
130
+
131
+ **スラッグ規則:** 小文字、スペース → ハイフン(`json-web-token.md`)
132
+
133
+ **テンプレート(expert):**
134
+ ```markdown
135
+ # <Term>
136
+
137
+ ## 定義
138
+ <1-2 文で核心を定義>
139
+
140
+ ## 技術詳細
141
+ <実装・仕様の詳細>
142
+
143
+ ## コード例
144
+ \`\`\`<lang>
145
+ <example>
146
+ \`\`\`
147
+
148
+ ## 関連用語
149
+ - [<term>](../glossary/<slug>.md)
150
+ ```
151
+
152
+ **テンプレート(beginner):**
153
+ ```markdown
154
+ # <Term>
155
+
156
+ ## ひとことで言うと
157
+ <1 文でやさしく>
158
+
159
+ ## もう少し詳しく
160
+ <例えを使って 3-5 文>
161
+
162
+ ## 使われる場面
163
+ <具体的なシナリオ>
164
+
165
+ ## 関連用語
166
+ - [<term>](../glossary/<slug>.md)
167
+ ```
168
+
169
+ **テンプレート(grandma):**
170
+ ```markdown
171
+ # <Term>
172
+
173
+ ## 身近な例えで言うと
174
+ <日常の例え 1-3 文>
175
+ ```
176
+
177
+ `dde-tool save docs/glossary/<slug>.md` で保存。
178
+
179
+ ### Step 6: 単語帳(dictionary.yaml)更新
180
+
181
+ 日本語ドキュメントが対象の場合、または日本語用語・別名を追加したい場合、
182
+ `docs/glossary/dictionary.yaml` を生成・更新する。
183
+
184
+ ```yaml
185
+ # docs/glossary/dictionary.yaml
186
+ jwt.md:
187
+ en: ["JWT", "JSON Web Token"]
188
+ ja: ["JWT"]
189
+
190
+ oauth.md:
191
+ en: ["OAuth", "OAuth 2.0"]
192
+ ja: ["OAuth"]
193
+ ```
194
+
195
+ ### Step 7: dde-link の実行(CLI)
196
+
197
+ **適用先は Step 1 で選択したドキュメント群と同じ。** ユーザーに再確認は不要。
198
+
199
+ ```bash
200
+ # 選択したファイルに対して順番に実行
201
+ npx dde-link README.md
202
+ npx dde-link docs/auth.md
203
+ # ...
204
+ ```
205
+
206
+ 初回は `--dry-run` で差分を表示してから `--fix` を推奨する。
207
+
208
+ ### Step 8: 次アクション提示(MUST: 省略しない)
209
+
210
+ ```
211
+ 1. 別のドキュメント群も処理する
212
+ 2. 別の読者レベルで再実行する
213
+ 3. dde-link を実行する(まだの場合)
214
+ 4. 図が必要な箇所を検出する
215
+ 5. 後で
216
+ ```
217
+
218
+ ---
219
+
220
+ ## 注意
221
+ - `docs/glossary/` に既存記事がある場合は上書きしない(差分のみ提案)
222
+ - dde-link の `--fix` は上書き。事前に `--dry-run` を推奨
223
+ - 将来フェーズ: HTML からの用語抽出 + ホバー表示(v0.2.0 予定)
@@ -0,0 +1,64 @@
1
+ <!-- DDE-toolkit (MIT License) -->
2
+
3
+ # Skill: DDE toolkit アップデート
4
+
5
+ ## Trigger
6
+ ユーザーが以下のいずれかを言ったとき:
7
+ - 「DDE を更新して」
8
+ - 「DDE をアップデートして」
9
+ - 「dde update」
10
+
11
+ ## 手順
12
+
13
+ ### Step 1: 現在のバージョンを確認
14
+ `dde/version.txt` を読んでローカルバージョンを表示する。
15
+ ファイルがなければ「バージョン情報がありません(古いインストールです)」と表示。
16
+
17
+ ### Step 2: 更新元を特定
18
+
19
+ `node_modules/@unlaxer/dde-toolkit/package.json` の version と `dde/version.txt` を比較:
20
+
21
+ ```
22
+ 現在: v0.1.0
23
+ 更新元: v0.2.0
24
+ ```
25
+
26
+ npm install されていなければ `npm update @unlaxer/dde-toolkit` の手順を案内する。
27
+
28
+ ### Step 3: 更新内容を説明してユーザーに確認
29
+
30
+ ```
31
+ 以下の toolkit ファイルが上書きされます:
32
+ - dde/method.md
33
+ - dde/flows/*.yaml
34
+ - dde/templates/*.md
35
+ - dde/bin/*
36
+ - dde/version.txt
37
+ - .claude/skills/dde-session.md
38
+ - .claude/skills/dde-update.md
39
+
40
+ 以下は触りません:
41
+ - docs/glossary/(あなたの用語集記事)
42
+ - dde/sessions/(あなたの DDE session 出力)
43
+
44
+ 更新しますか?
45
+ ```
46
+
47
+ **ユーザーの確認を待つ。勝手に上書きしない。**
48
+
49
+ ### Step 4: 更新を実行
50
+
51
+ ```bash
52
+ npx dde-install
53
+ ```
54
+
55
+ ### Step 5: 結果を報告
56
+
57
+ ```
58
+ DDE toolkit を v<新バージョン> に更新しました。
59
+ docs/glossary/ と dde/sessions/ は変更されていません。
60
+ ```
61
+
62
+ ## MUST ルール
63
+ 1. **更新前に必ずユーザーの確認を得る。**
64
+ 2. **docs/glossary/ と dde/sessions/ には絶対に触らない。**
package/version.txt ADDED
@@ -0,0 +1 @@
1
+ 0.1.0