@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 +21 -0
- package/agents-dde-section.md +25 -0
- package/bin/dde-install.js +162 -0
- package/bin/dde-link.js +83 -0
- package/bin/dde-tool.js +105 -0
- package/flows/quick.yaml +78 -0
- package/lib/dictionary.js +117 -0
- package/lib/linker.js +104 -0
- package/lib/markdown.js +199 -0
- package/method.md +93 -0
- package/package.json +47 -0
- package/skills/dde-session.md +223 -0
- package/skills/dde-update.md +64 -0
- package/version.txt +1 -0
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.`);
|
package/bin/dde-link.js
ADDED
|
@@ -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
|
+
}
|
package/bin/dde-tool.js
ADDED
|
@@ -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
|
+
}
|
package/flows/quick.yaml
ADDED
|
@@ -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
|
+
}
|
package/lib/markdown.js
ADDED
|
@@ -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
|