yodogawa 2.1.3 → 2.2.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +134 -94
  2. package/LICENSE +1 -1
  3. package/README.md +351 -263
  4. package/bin/checks/id-trace.js +137 -0
  5. package/bin/checks/links.js +59 -0
  6. package/bin/checks/placeholder.js +132 -0
  7. package/bin/checks/structure.js +127 -0
  8. package/bin/cli.js +51 -67
  9. package/bin/commands/doctor.js +128 -0
  10. package/bin/commands/install.js +58 -0
  11. package/bin/commands/new-task.js +117 -0
  12. package/bin/lib/check-cli.js +19 -0
  13. package/bin/lib/findings.js +31 -0
  14. package/bin/lib/markdown.js +103 -0
  15. package/bin/lib/project-spec.js +138 -0
  16. package/bin/lib/walk-md.js +23 -0
  17. package/package.json +68 -55
  18. package/skills/a-001-setup-doc-structure/SKILL.md +68 -68
  19. package/skills/a-001-setup-doc-structure/reference/directory-structure.md +75 -75
  20. package/skills/a-002-initialize-project/SKILL.md +145 -118
  21. package/skills/a-002-initialize-project/reference/hearing-questions.md +91 -41
  22. package/skills/a-002-initialize-project/reference/structure-check.md +12 -22
  23. package/skills/a-002a-slice-mvp-scope/SKILL.md +105 -0
  24. package/skills/a-002b-define-user-stories/SKILL.md +80 -0
  25. package/skills/a-002b-define-user-stories/reference/user-stories-guide.md +78 -0
  26. package/skills/a-003-create-scenarios/SKILL.md +97 -96
  27. package/{templates/project/02-behavior/01-scenarios.md → skills/a-003-create-scenarios/reference/detailed-gherkin-template.md} +413 -406
  28. package/skills/a-003-create-scenarios/reference/structure-check.md +20 -17
  29. package/skills/a-004-define-domain-model/SKILL.md +107 -98
  30. package/skills/a-004-define-domain-model/reference/event-storming-guide.md +33 -7
  31. package/skills/a-004-define-domain-model/reference/ubiquitous-language-guide.md +49 -0
  32. package/skills/a-005-create-domain-diagram/SKILL.md +18 -17
  33. package/skills/a-006-review-requirements-domain/SKILL.md +59 -22
  34. package/skills/a-006-review-requirements-domain/examples/review-report-template.md +27 -7
  35. package/skills/a-006-review-requirements-domain/reference/consistency-checks.md +53 -18
  36. package/skills/a-007-define-tech-stack/SKILL.md +99 -99
  37. package/skills/a-008-define-repository-structure/SKILL.md +96 -96
  38. package/skills/a-009-define-screen-design/SKILL.md +103 -103
  39. package/skills/a-010-define-design-system/SKILL.md +130 -130
  40. package/skills/a-011-define-data-model/SKILL.md +118 -118
  41. package/skills/a-012-define-api-spec/SKILL.md +105 -105
  42. package/skills/a-013-define-architecture/SKILL.md +98 -98
  43. package/skills/a-014-define-infrastructure/SKILL.md +118 -110
  44. package/skills/{a-002-initialize-project → a-014-define-infrastructure}/examples/nfr-baseline.md +2 -1
  45. package/{templates/project/01-requirements/04-non-functional-requirements.md → skills/a-014-define-infrastructure/examples/non-functional-requirements.md} +120 -115
  46. package/skills/a-015-review-design/reference/consistency-checks.md +1 -1
  47. package/skills/b-001-create-task-directory/SKILL.md +68 -68
  48. package/skills/b-002-create-task-definition/SKILL.md +114 -114
  49. package/skills/b-003-create-task-research/SKILL.md +128 -128
  50. package/skills/b-004-create-task-implementation/SKILL.md +98 -98
  51. package/skills/b-005-review-task/reference/assessment-criteria.md +79 -79
  52. package/skills/c-001-implement-task/SKILL.md +186 -186
  53. package/skills/c-001-implement-task/reference/implementation-loop.md +65 -65
  54. package/skills/c-002-update-documentation/SKILL.md +159 -159
  55. package/skills/c-002-update-documentation/examples/project-doc-updates.md +4 -4
  56. package/skills/c-002-update-documentation/reference/doc-structure-and-checks.md +99 -97
  57. package/templates/project/01-requirements/01-product-brief.md +186 -0
  58. package/templates/project/01-requirements/02-mvp-scope.md +64 -0
  59. package/templates/project/01-requirements/03-parking-lot.md +29 -0
  60. package/templates/project/01-requirements/05-user-stories.md +28 -124
  61. package/templates/project/01-requirements/{02-features-implemented.md → 06-features-implemented.md} +77 -73
  62. package/templates/project/02-behavior/01-core-scenarios.md +80 -0
  63. package/templates/project/03-domain/01-domain-model.md +120 -339
  64. package/templates/project/03-domain/01-domain-sketch.md +90 -0
  65. package/templates/project/03-domain/02-ubiquitous-language.md +32 -153
  66. package/templates/project/04-design/01-tech-stack.md +367 -367
  67. package/templates/project/04-design/02-repository-structure.md +391 -391
  68. package/templates/project/04-design/03-screen-design.md +596 -596
  69. package/templates/project/04-design/04-design-system.md +261 -261
  70. package/templates/project/04-design/05-data-model.md +211 -211
  71. package/templates/project/04-design/06-api-spec.md +226 -226
  72. package/templates/project/04-design/07-architecture.md +183 -183
  73. package/templates/project/04-design/08-infrastructure.md +180 -180
  74. package/templates/project/AI_CONTEXT.md +55 -0
  75. package/templates/project/STAKEHOLDER-SUMMARY.md +66 -0
  76. package/templates/tasks/task-template/a-definition.md +143 -143
  77. package/templates/tasks/task-template/b-research.md +185 -185
  78. package/templates/tasks/task-template/c-implementation.md +200 -200
  79. package/templates/project/01-requirements/01-system-overview.md +0 -49
  80. package/templates/project/01-requirements/03-features-planned.md +0 -75
@@ -0,0 +1,128 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { bold, green, red, yellow, gray } = require('kleur');
6
+
7
+ const { SEVERITY, makeFinding, summarize, formatPosition } = require('../lib/findings');
8
+
9
+ // requiresProject: docs/project が無いとき skip するチェック。
10
+ // structure は不在自体を報告し、links は docs/ 全体(docs/tasks 含む)が対象なので走らせる。
11
+ const CHECKS = [
12
+ { name: 'structure', run: require('../checks/structure').run, requiresProject: false },
13
+ { name: 'id-trace', run: require('../checks/id-trace').run, requiresProject: true },
14
+ { name: 'placeholder', run: require('../checks/placeholder').run, requiresProject: true },
15
+ { name: 'links', run: require('../checks/links').run, requiresProject: false },
16
+ ];
17
+
18
+ const USAGE = `Usage: yodogawa doctor [--dir <path>] [--json]
19
+
20
+ docs/project/ のトレーサビリティ・構造を検査します。
21
+
22
+ Options:
23
+ --dir <path> 検査するプロジェクトのルート(既定: カレントディレクトリ)
24
+ --json 機械可読な JSON を stdout に出力
25
+ --help このヘルプを表示
26
+
27
+ Exit codes: 0 = Error なし(Warning のみ含む) / 1 = Error あり / 2 = 使い方の誤り`;
28
+
29
+ function parseArgs(argv) {
30
+ const opts = { json: false, dir: null };
31
+ for (let i = 0; i < argv.length; i++) {
32
+ const arg = argv[i];
33
+ if (arg === '--json') opts.json = true;
34
+ else if (arg === '--dir') {
35
+ const value = argv[i + 1];
36
+ if (value === undefined || value.startsWith('-')) {
37
+ return { error: '--dir にはパスを指定してください' };
38
+ }
39
+ opts.dir = argv[++i];
40
+ } else if (arg === '--help' || arg === '-h') return { help: true };
41
+ else return { error: `不明なオプション: ${arg}` };
42
+ }
43
+ return { opts };
44
+ }
45
+
46
+ function statusOf(findings) {
47
+ const { errors, warnings } = summarize(findings);
48
+ if (errors > 0) return 'fail';
49
+ if (warnings > 0) return 'warn';
50
+ return 'pass';
51
+ }
52
+
53
+ function printHuman(results, summary) {
54
+ const HEADER = {
55
+ pass: (name) => `${green('✔')} ${bold(name)}`,
56
+ warn: (name) => `${yellow('⚠')} ${bold(name)}`,
57
+ fail: (name) => `${red('✖')} ${bold(name)}`,
58
+ skip: (name) => `${gray('-')} ${bold(name)} ${gray('(skip)')}`,
59
+ };
60
+ for (const result of results) {
61
+ console.log(HEADER[result.status](result.name));
62
+ for (const f of result.findings) {
63
+ const label = f.severity === SEVERITY.ERROR ? red('error') : yellow('warning');
64
+ console.log(` ${label} ${formatPosition(f)} ${f.message}`);
65
+ }
66
+ }
67
+ const skipped = results.filter((r) => r.status === 'skip').length;
68
+ const suffix = skipped > 0 ? `, ${skipped} skipped` : '';
69
+ const line = `${summary.errors} errors, ${summary.warnings} warnings (${results.length} checks${suffix})`;
70
+ console.log(summary.errors > 0 ? red(`\n✖ ${line}`) : green(`\n✔ ${line}`));
71
+ }
72
+
73
+ async function main(argv, { cwd = process.cwd() } = {}) {
74
+ const parsed = parseArgs(argv);
75
+ if (parsed.help) {
76
+ console.log(USAGE);
77
+ return 0;
78
+ }
79
+ if (parsed.error) {
80
+ console.error(red(`✖ ${parsed.error}`));
81
+ console.error(USAGE);
82
+ return 2;
83
+ }
84
+
85
+ const rootDir = path.resolve(cwd, parsed.opts.dir || '.');
86
+ if (!fs.existsSync(rootDir)) {
87
+ console.error(red(`✖ 指定されたディレクトリが存在しません: ${rootDir}`));
88
+ return 2;
89
+ }
90
+
91
+ const projectMissing = !fs.existsSync(path.join(rootDir, 'docs', 'project'));
92
+ const results = CHECKS.map(({ name, run, requiresProject }) => {
93
+ if (projectMissing && requiresProject) {
94
+ return { name, status: 'skip', findings: [] };
95
+ }
96
+ // チェック自体の実行失敗(読み取り不能ファイル等)でも --json の stdout 純度と
97
+ // exit code 契約を守るため、throw は Error finding に変換する
98
+ let findings;
99
+ try {
100
+ findings = run({ rootDir });
101
+ } catch (err) {
102
+ findings = [
103
+ makeFinding(name, SEVERITY.ERROR, '(internal)', null, `チェックの実行に失敗しました: ${err.message}`),
104
+ ];
105
+ }
106
+ return { name, status: statusOf(findings), findings };
107
+ });
108
+
109
+ const findings = results.flatMap((r) => r.findings);
110
+ const summary = summarize(findings);
111
+ const ok = summary.errors === 0;
112
+
113
+ if (parsed.opts.json) {
114
+ const report = {
115
+ version: 1,
116
+ ok,
117
+ summary,
118
+ checks: results.map(({ name, status }) => ({ name, status })),
119
+ findings,
120
+ };
121
+ console.log(JSON.stringify(report, null, 2));
122
+ } else {
123
+ printHuman(results, summary);
124
+ }
125
+ return ok ? 0 : 1;
126
+ }
127
+
128
+ module.exports = { main };
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const prompts = require('prompts');
6
+ const { bold, green, cyan, red } = require('kleur');
7
+
8
+ async function main({ cwd = process.cwd(), pkgRoot = path.join(__dirname, '..', '..') } = {}) {
9
+ console.log(bold().cyan('\n🚀 Welcome to Yodogawa Skills Installer!\n'));
10
+
11
+ const response = await prompts({
12
+ type: 'select',
13
+ name: 'type',
14
+ message: 'Which configuration would you like to install?',
15
+ choices: [
16
+ { title: 'Claude Code (.claude/skills/)', value: 'claude' },
17
+ { title: 'Other IDEs — Cursor / Codex / Antigravity (.agents/skills/)', value: 'agents' }
18
+ ]
19
+ });
20
+
21
+ if (!response.type) {
22
+ console.log(red('✖ Operation cancelled.'));
23
+ return;
24
+ }
25
+
26
+ const dirMap = {
27
+ claude: '.claude',
28
+ agents: '.agents',
29
+ };
30
+ const targetDirName = dirMap[response.type];
31
+ const targetDir = path.join(cwd, targetDirName);
32
+
33
+ if (fs.existsSync(targetDir)) {
34
+ const confirm = await prompts({
35
+ type: 'confirm',
36
+ name: 'overwrite',
37
+ message: `Directory ${targetDirName} already exists. Merge and update its contents?`,
38
+ initial: false
39
+ });
40
+ if (!confirm.overwrite) {
41
+ console.log(red('✖ Operation cancelled.'));
42
+ return;
43
+ }
44
+ }
45
+
46
+ console.log(`\nInstalling Yodogawa skills to ${bold(targetDirName)}...`);
47
+
48
+ await fs.copy(path.join(pkgRoot, 'skills'), path.join(targetDir, 'skills'));
49
+ await fs.copy(path.join(pkgRoot, 'templates'), path.join(targetDir, 'templates'));
50
+
51
+ console.log(green(`\n✔ Successfully installed Yodogawa skills for ${response.type}!`));
52
+ console.log(`\nNext steps:`);
53
+ console.log(`1. Open ${bold(targetDirName + '/skills/')} to explore the skills.`);
54
+ console.log(`2. Start using them in your project!\n`);
55
+ console.log(`Note: reinstalling merges into existing files. Renamed or removed skills are not auto-deleted — delete ${bold(targetDirName + '/skills')} and ${bold(targetDirName + '/templates')} first to reflect them.\n`);
56
+ }
57
+
58
+ module.exports = { main };
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const { bold, green, red, yellow } = require('kleur');
6
+
7
+ // b-001-create-task-directory の採番規則に準拠(SKILL.md 手順2)
8
+ const SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
9
+ const TASK_DIR_RE = /^task(\d{6})-/;
10
+ const MAX_ID = 999999;
11
+
12
+ const USAGE = `Usage: yodogawa new-task <slug> [--dir <path>] [--json]
13
+
14
+ docs/tasks/ に連番タスク ID 付きディレクトリを作成します(作成はディレクトリのみ。
15
+ タスク定義書などのドキュメントは /b-002-create-task-definition 以降で作成します)。
16
+
17
+ Arguments:
18
+ slug タスク内容を表す英小文字・数字・ハイフンのスラッグ(3〜5 語推奨)
19
+
20
+ Options:
21
+ --dir <path> プロジェクトのルート(既定: カレントディレクトリ)
22
+ --json 機械可読な JSON を stdout に出力
23
+ --help このヘルプを表示
24
+
25
+ Exit codes: 0 = 成功 / 1 = 操作失敗 / 2 = 使い方の誤り`;
26
+
27
+ function parseArgs(argv) {
28
+ const opts = { json: false, dir: null, slug: null };
29
+ for (let i = 0; i < argv.length; i++) {
30
+ const arg = argv[i];
31
+ if (arg === '--json') opts.json = true;
32
+ else if (arg === '--dir') {
33
+ const value = argv[i + 1];
34
+ if (value === undefined || value.startsWith('-')) {
35
+ return { error: '--dir にはパスを指定してください' };
36
+ }
37
+ opts.dir = argv[++i];
38
+ } else if (arg === '--help' || arg === '-h') return { help: true };
39
+ else if (arg.startsWith('-')) return { error: `不明なオプション: ${arg}` };
40
+ else if (opts.slug === null) opts.slug = arg;
41
+ else return { error: `引数が多すぎます: ${arg}` };
42
+ }
43
+ return { opts };
44
+ }
45
+
46
+ async function main(argv, { cwd = process.cwd() } = {}) {
47
+ const parsed = parseArgs(argv);
48
+ if (parsed.help) {
49
+ console.log(USAGE);
50
+ return 0;
51
+ }
52
+ if (parsed.error) {
53
+ console.error(red(`✖ ${parsed.error}`));
54
+ console.error(USAGE);
55
+ return 2;
56
+ }
57
+
58
+ const { slug, dir, json } = parsed.opts;
59
+ if (!slug) {
60
+ console.error(red('✖ スラッグを指定してください(例: yodogawa new-task user-profile-edit)'));
61
+ console.error(USAGE);
62
+ return 2;
63
+ }
64
+ if (!SLUG_RE.test(slug)) {
65
+ console.error(
66
+ red(`✖ スラッグ「${slug}」が不正です。英小文字・数字・ハイフンのみ(連続ハイフン禁止)で指定してください`)
67
+ );
68
+ return 2;
69
+ }
70
+
71
+ const rootDir = path.resolve(cwd, dir || '.');
72
+ if (!fs.existsSync(rootDir)) {
73
+ console.error(red(`✖ 指定されたディレクトリが存在しません: ${rootDir}`));
74
+ return 2;
75
+ }
76
+ const tasksDir = path.join(rootDir, 'docs', 'tasks');
77
+ if (!fs.existsSync(tasksDir)) {
78
+ console.error(
79
+ red('✖ docs/tasks がありません。/a-001-setup-doc-structure でドキュメント骨格を先に作成してください')
80
+ );
81
+ return 1;
82
+ }
83
+
84
+ // 3〜5 語推奨(範囲外は警告のみで続行 = b-001 SKILL.md に忠実)。
85
+ // --json の stdout を汚さないよう警告は stderr へ出す。
86
+ const wordCount = slug.split('-').length;
87
+ if (wordCount < 3 || wordCount > 5) {
88
+ console.error(yellow(`⚠ スラッグは 3〜5 語を推奨します(現在 ${wordCount} 語)。このまま続行します`));
89
+ }
90
+
91
+ const ids = fs
92
+ .readdirSync(tasksDir, { withFileTypes: true })
93
+ .filter((entry) => entry.isDirectory())
94
+ .map((entry) => TASK_DIR_RE.exec(entry.name))
95
+ .filter(Boolean)
96
+ .map((m) => parseInt(m[1], 10));
97
+ const nextId = ids.length > 0 ? Math.max(...ids) + 1 : 1;
98
+ if (nextId > MAX_ID) {
99
+ console.error(red(`✖ タスク ID が上限(${MAX_ID})を超えました`));
100
+ return 1;
101
+ }
102
+
103
+ const id = `task${String(nextId).padStart(6, '0')}`;
104
+ const dirName = `${id}-${slug}`;
105
+ await fs.ensureDir(path.join(tasksDir, dirName));
106
+ const relPath = `docs/tasks/${dirName}`;
107
+
108
+ if (json) {
109
+ console.log(JSON.stringify({ id, slug, path: relPath }, null, 2));
110
+ } else {
111
+ console.log(green(`✔ タスクディレクトリ ${bold(relPath)} を作成しました`));
112
+ console.log('続いてタスク定義書を作成しますか?(/b-002-create-task-definition)');
113
+ }
114
+ return 0;
115
+ }
116
+
117
+ module.exports = { main };
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ const { summarize, formatPosition } = require('./findings');
6
+
7
+ // 各チェックを単体スクリプトとして実行するための共通シム。
8
+ // findings を 1 行 1 件で表示し、Error があれば exit 1、なければ exit 0。
9
+ function runCheckCli(run, dirArg) {
10
+ const rootDir = path.resolve(dirArg || '.');
11
+ const findings = run({ rootDir });
12
+ for (const f of findings) {
13
+ console.log(`${f.severity} ${formatPosition(f)} ${f.message}`);
14
+ }
15
+ const { errors } = summarize(findings);
16
+ process.exitCode = errors > 0 ? 1 : 0;
17
+ }
18
+
19
+ module.exports = { runCheckCli };
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ const SEVERITY = { ERROR: 'error', WARNING: 'warning' };
6
+
7
+ function makeFinding(check, severity, file, line, message) {
8
+ return { check, severity, file, line: line == null ? null : line, message };
9
+ }
10
+
11
+ function summarize(findings) {
12
+ let errors = 0;
13
+ let warnings = 0;
14
+ for (const f of findings) {
15
+ if (f.severity === SEVERITY.ERROR) errors++;
16
+ else if (f.severity === SEVERITY.WARNING) warnings++;
17
+ }
18
+ return { errors, warnings };
19
+ }
20
+
21
+ // findings の file を OS 非依存で安定させるための正規化(Windows でも posix 区切り)
22
+ function toPosixRelative(baseDir, absPath) {
23
+ return path.relative(baseDir, absPath).split(path.sep).join('/');
24
+ }
25
+
26
+ // finding の位置表示(file または file:line)
27
+ function formatPosition(finding) {
28
+ return finding.line == null ? finding.file : `${finding.file}:${finding.line}`;
29
+ }
30
+
31
+ module.exports = { SEVERITY, makeFinding, summarize, toPosixRelative, formatPosition };
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+
3
+ // Markdown を「論理行」のモデルに変換する共通前処理。
4
+ // 全チェックはこのモデル経由で走査することで、フェンスコードブロック内・
5
+ // HTML コメント内のテキストを誤検知しないことを一箇所で保証する。
6
+
7
+ const FENCE_RE = /^ {0,3}(`{3,}|~{3,})/;
8
+ const LINK_RE = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
9
+ // ATX 見出し。m[1] = レベル(# の数)、m[2] = 見出しテキスト
10
+ const HEADING_RE = /^ {0,3}(#{1,6})\s+(.+)$/;
11
+
12
+ // content → [{ raw, visible, line, inFence }]
13
+ // - line: 1 始まり
14
+ // - inFence: フェンス区切り行自身を含む
15
+ // - visible: HTML コメント区間を除去したテキスト(フェンス内は raw のまま)
16
+ function parseLines(content) {
17
+ const rawLines = String(content).split(/\r?\n/);
18
+ const lines = [];
19
+ let inFence = false;
20
+ let fenceChar = null;
21
+ let inComment = false;
22
+
23
+ for (let i = 0; i < rawLines.length; i++) {
24
+ const raw = rawLines[i];
25
+ let lineInFence = inFence;
26
+
27
+ if (!inComment) {
28
+ const fenceMatch = raw.match(FENCE_RE);
29
+ if (fenceMatch) {
30
+ if (!inFence) {
31
+ inFence = true;
32
+ fenceChar = fenceMatch[1][0];
33
+ lineInFence = true;
34
+ } else if (fenceMatch[1][0] === fenceChar) {
35
+ inFence = false;
36
+ lineInFence = true;
37
+ }
38
+ }
39
+ }
40
+
41
+ let visible;
42
+ if (lineInFence) {
43
+ visible = raw;
44
+ } else {
45
+ const stripped = stripComments(raw, inComment);
46
+ visible = stripped.visible;
47
+ inComment = stripped.inComment;
48
+ }
49
+ lines.push({ raw, visible, line: i + 1, inFence: lineInFence });
50
+ }
51
+ return lines;
52
+ }
53
+
54
+ function stripComments(text, inComment) {
55
+ let out = '';
56
+ let rest = text;
57
+ while (rest.length > 0) {
58
+ if (inComment) {
59
+ const end = rest.indexOf('-->');
60
+ if (end === -1) {
61
+ rest = '';
62
+ } else {
63
+ rest = rest.slice(end + 3);
64
+ inComment = false;
65
+ }
66
+ } else {
67
+ const start = rest.indexOf('<!--');
68
+ if (start === -1) {
69
+ out += rest;
70
+ rest = '';
71
+ } else {
72
+ out += rest.slice(0, start);
73
+ rest = rest.slice(start + 4);
74
+ inComment = true;
75
+ }
76
+ }
77
+ }
78
+ return { visible: out, inComment };
79
+ }
80
+
81
+ // content → [{ target, line }](フェンス内・コメント内のリンクは除外)
82
+ function extractLinks(content) {
83
+ const links = [];
84
+ for (const line of parseLines(content)) {
85
+ if (line.inFence) continue;
86
+ LINK_RE.lastIndex = 0;
87
+ let m;
88
+ while ((m = LINK_RE.exec(line.visible)) !== null) {
89
+ links.push({ target: m[1], line: line.line });
90
+ }
91
+ }
92
+ return links;
93
+ }
94
+
95
+ // テーブル行なら trim 済みセル配列、そうでなければ null
96
+ function splitTableCells(raw) {
97
+ const trimmed = raw.trim();
98
+ if (!trimmed.startsWith('|')) return null;
99
+ const inner = trimmed.replace(/^\|/, '').replace(/\|$/, '');
100
+ return inner.split('|').map((cell) => cell.trim());
101
+ }
102
+
103
+ module.exports = { parseLines, extractLinks, splitTableCells, HEADING_RE };
@@ -0,0 +1,138 @@
1
+ 'use strict';
2
+
3
+ // docs/project の正準構造の定義データ(Single Source of Truth)。
4
+ // 根拠は templates/project/ の実テンプレート。テンプレートの見出しを変更したら
5
+ // ここも合わせて更新すること。
6
+
7
+ // A-Series の作成順(フェーズ順)。structure チェックはこの順序で
8
+ // 「自分より後のファイルが存在するのに欠けているファイル」を Error にする
9
+ // (フェーズ・フロンティア方式。未着手フェーズのファイルは報告しない)。
10
+ const CANONICAL_FILES = [
11
+ '01-requirements/01-product-brief.md',
12
+ '01-requirements/02-mvp-scope.md',
13
+ '01-requirements/03-parking-lot.md',
14
+ '01-requirements/05-user-stories.md',
15
+ '02-behavior/01-core-scenarios.md',
16
+ '03-domain/01-domain-sketch.md',
17
+ '03-domain/02-ubiquitous-language.md',
18
+ '04-design/01-tech-stack.md',
19
+ '04-design/02-repository-structure.md',
20
+ '04-design/03-screen-design.md',
21
+ '04-design/04-design-system.md',
22
+ '04-design/05-data-model.md',
23
+ '04-design/06-api-spec.md',
24
+ '04-design/07-architecture.md',
25
+ '04-design/08-infrastructure.md',
26
+ ];
27
+
28
+ // 常に任意のファイル(06 は existing モードのみ、01-domain-model は Full DDD のみ)
29
+ const OPTIONAL_FILES = [
30
+ '01-requirements/06-features-implemented.md',
31
+ '03-domain/01-domain-model.md',
32
+ ];
33
+
34
+ // 「このキーで始まる見出し」が存在すること(前方一致。ユーザーが括弧の補足を
35
+ // 削っても壊れないよう、安定した骨格キーのみを要求する)。
36
+ const REQUIRED_HEADINGS = {
37
+ '01-requirements/01-product-brief.md': [
38
+ '背景',
39
+ 'ターゲットユーザー',
40
+ '価値提案',
41
+ '成功指標',
42
+ 'クリティカル制約',
43
+ '非ゴール',
44
+ ],
45
+ '01-requirements/02-mvp-scope.md': ['検証する仮説', 'MVP Scope', 'Out of Scope'],
46
+ '02-behavior/01-core-scenarios.md': [
47
+ 'Core Flow 一覧',
48
+ 'Day 1 Happy Path',
49
+ 'Critical Failure',
50
+ 'Not Covered in MVP',
51
+ ],
52
+ '03-domain/01-domain-sketch.md': [
53
+ '主要用語',
54
+ 'アクター',
55
+ '中核エンティティ',
56
+ '重要なビジネスルール',
57
+ 'MVP では作らない',
58
+ ],
59
+ '03-domain/02-ubiquitous-language.md': ['禁止用語'],
60
+ '04-design/01-tech-stack.md': ['テックスタック一覧', '技術選定の基準'],
61
+ '04-design/02-repository-structure.md': ['ディレクトリ構造', 'アーキテクチャパターン'],
62
+ '04-design/03-screen-design.md': ['画面一覧', '画面遷移図'],
63
+ '04-design/04-design-system.md': ['カラーパレット', 'タイポグラフィ'],
64
+ '04-design/05-data-model.md': ['エンティティ一覧', 'リレーションシップ'],
65
+ '04-design/06-api-spec.md': ['認証・認可', 'エンドポイント一覧'],
66
+ '04-design/07-architecture.md': ['システムアーキテクチャ図', 'ADR'],
67
+ '04-design/08-infrastructure.md': ['インフラ構成図', '環境構成'],
68
+ };
69
+
70
+ // 見出しが乏しいファイルは代表テーブルのヘッダセルで骨格を検査する。
71
+ const REQUIRED_TABLE_HEADERS = {
72
+ '01-requirements/03-parking-lot.md': 'Category 1',
73
+ '01-requirements/05-user-stories.md': 'ストーリーID',
74
+ };
75
+
76
+ // a-006(PM Gate)が docs/project/ 直下に生成するレビュー成果物。
77
+ // 04-design に着手しているのに無い場合は a-006 未実施の示唆として Warning。
78
+ const REVIEW_ARTIFACTS = ['AI_CONTEXT.md', 'STAKEHOLDER-SUMMARY.md'];
79
+
80
+ // templates/project/ 由来の意味プレースホルダ(placeholder チェックが検出)。
81
+ // コメント外に残っていたらテンプレート未記入とみなす。汎用の \[.+\] は
82
+ // チェックボックスや Markdown リンクと衝突するため、既知トークンのみ検出する。
83
+ // テンプレートとの同期は test/project-spec-sync.test.js が検証する。
84
+ const KNOWN_TOKENS = [
85
+ '[役割]',
86
+ '[目的]',
87
+ '[理由]',
88
+ '[課題]',
89
+ '[画面名]',
90
+ '[コンテキスト名]',
91
+ '[Bounded Context名]',
92
+ ];
93
+
94
+ // ID 体系(trace の SSoT)。definitionFiles 内の出現を定義とみなす
95
+ // (headingDefinition の族は見出し行の出現のみが定義)。
96
+ // orphanScanFiles: 定義済み ID がここから 1 度も参照されなければ孤児 Warning。
97
+ // degradeWhenNoDefinitions: 定義集合が空のとき参照ごとの Error ではなく
98
+ // 族全体で縮退 Warning 1 件にする(FN は定義元が任意ファイルのため)。
99
+ const ID_FAMILIES = [
100
+ {
101
+ family: 'P',
102
+ definitionFiles: ['01-requirements/01-product-brief.md'],
103
+ orphanScanFiles: ['01-requirements/05-user-stories.md'],
104
+ },
105
+ {
106
+ family: 'US',
107
+ definitionFiles: ['01-requirements/05-user-stories.md'],
108
+ },
109
+ {
110
+ family: 'FN',
111
+ definitionFiles: [
112
+ '01-requirements/02-mvp-scope.md',
113
+ '01-requirements/06-features-implemented.md',
114
+ ],
115
+ orphanScanFiles: ['02-behavior/01-core-scenarios.md'],
116
+ degradeWhenNoDefinitions: true,
117
+ },
118
+ {
119
+ family: 'CS',
120
+ definitionFiles: ['02-behavior/01-core-scenarios.md'],
121
+ headingDefinition: true,
122
+ },
123
+ {
124
+ family: 'CF',
125
+ definitionFiles: ['02-behavior/01-core-scenarios.md'],
126
+ headingDefinition: true,
127
+ },
128
+ ];
129
+
130
+ module.exports = {
131
+ CANONICAL_FILES,
132
+ OPTIONAL_FILES,
133
+ REQUIRED_HEADINGS,
134
+ REQUIRED_TABLE_HEADERS,
135
+ REVIEW_ARTIFACTS,
136
+ KNOWN_TOKENS,
137
+ ID_FAMILIES,
138
+ };
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ // dir 配下の .md ファイルを再帰列挙する(絶対パス、名前順で決定的)。
7
+ // Node 18 互換のため readdirSync の recursive オプションは使わない。
8
+ function walkMdFiles(dir) {
9
+ const out = [];
10
+ (function walk(current) {
11
+ const entries = fs
12
+ .readdirSync(current, { withFileTypes: true })
13
+ .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
14
+ for (const entry of entries) {
15
+ const full = path.join(current, entry.name);
16
+ if (entry.isDirectory()) walk(full);
17
+ else if (entry.isFile() && entry.name.endsWith('.md')) out.push(full);
18
+ }
19
+ })(dir);
20
+ return out;
21
+ }
22
+
23
+ module.exports = { walkMdFiles };