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.
- package/CHANGELOG.md +134 -94
- package/LICENSE +1 -1
- package/README.md +351 -263
- package/bin/checks/id-trace.js +137 -0
- package/bin/checks/links.js +59 -0
- package/bin/checks/placeholder.js +132 -0
- package/bin/checks/structure.js +127 -0
- package/bin/cli.js +51 -67
- package/bin/commands/doctor.js +128 -0
- package/bin/commands/install.js +58 -0
- package/bin/commands/new-task.js +117 -0
- package/bin/lib/check-cli.js +19 -0
- package/bin/lib/findings.js +31 -0
- package/bin/lib/markdown.js +103 -0
- package/bin/lib/project-spec.js +138 -0
- package/bin/lib/walk-md.js +23 -0
- package/package.json +68 -55
- package/skills/a-001-setup-doc-structure/SKILL.md +68 -68
- package/skills/a-001-setup-doc-structure/reference/directory-structure.md +75 -75
- package/skills/a-002-initialize-project/SKILL.md +145 -118
- package/skills/a-002-initialize-project/reference/hearing-questions.md +91 -41
- package/skills/a-002-initialize-project/reference/structure-check.md +12 -22
- package/skills/a-002a-slice-mvp-scope/SKILL.md +105 -0
- package/skills/a-002b-define-user-stories/SKILL.md +80 -0
- package/skills/a-002b-define-user-stories/reference/user-stories-guide.md +78 -0
- package/skills/a-003-create-scenarios/SKILL.md +97 -96
- package/{templates/project/02-behavior/01-scenarios.md → skills/a-003-create-scenarios/reference/detailed-gherkin-template.md} +413 -406
- package/skills/a-003-create-scenarios/reference/structure-check.md +20 -17
- package/skills/a-004-define-domain-model/SKILL.md +107 -98
- package/skills/a-004-define-domain-model/reference/event-storming-guide.md +33 -7
- package/skills/a-004-define-domain-model/reference/ubiquitous-language-guide.md +49 -0
- package/skills/a-005-create-domain-diagram/SKILL.md +18 -17
- package/skills/a-006-review-requirements-domain/SKILL.md +59 -22
- package/skills/a-006-review-requirements-domain/examples/review-report-template.md +27 -7
- package/skills/a-006-review-requirements-domain/reference/consistency-checks.md +53 -18
- package/skills/a-007-define-tech-stack/SKILL.md +99 -99
- package/skills/a-008-define-repository-structure/SKILL.md +96 -96
- package/skills/a-009-define-screen-design/SKILL.md +103 -103
- package/skills/a-010-define-design-system/SKILL.md +130 -130
- package/skills/a-011-define-data-model/SKILL.md +118 -118
- package/skills/a-012-define-api-spec/SKILL.md +105 -105
- package/skills/a-013-define-architecture/SKILL.md +98 -98
- package/skills/a-014-define-infrastructure/SKILL.md +118 -110
- package/skills/{a-002-initialize-project → a-014-define-infrastructure}/examples/nfr-baseline.md +2 -1
- package/{templates/project/01-requirements/04-non-functional-requirements.md → skills/a-014-define-infrastructure/examples/non-functional-requirements.md} +120 -115
- package/skills/a-015-review-design/reference/consistency-checks.md +1 -1
- package/skills/b-001-create-task-directory/SKILL.md +68 -68
- package/skills/b-002-create-task-definition/SKILL.md +114 -114
- package/skills/b-003-create-task-research/SKILL.md +128 -128
- package/skills/b-004-create-task-implementation/SKILL.md +98 -98
- package/skills/b-005-review-task/reference/assessment-criteria.md +79 -79
- package/skills/c-001-implement-task/SKILL.md +186 -186
- package/skills/c-001-implement-task/reference/implementation-loop.md +65 -65
- package/skills/c-002-update-documentation/SKILL.md +159 -159
- package/skills/c-002-update-documentation/examples/project-doc-updates.md +4 -4
- package/skills/c-002-update-documentation/reference/doc-structure-and-checks.md +99 -97
- package/templates/project/01-requirements/01-product-brief.md +186 -0
- package/templates/project/01-requirements/02-mvp-scope.md +64 -0
- package/templates/project/01-requirements/03-parking-lot.md +29 -0
- package/templates/project/01-requirements/05-user-stories.md +28 -124
- package/templates/project/01-requirements/{02-features-implemented.md → 06-features-implemented.md} +77 -73
- package/templates/project/02-behavior/01-core-scenarios.md +80 -0
- package/templates/project/03-domain/01-domain-model.md +120 -339
- package/templates/project/03-domain/01-domain-sketch.md +90 -0
- package/templates/project/03-domain/02-ubiquitous-language.md +32 -153
- package/templates/project/04-design/01-tech-stack.md +367 -367
- package/templates/project/04-design/02-repository-structure.md +391 -391
- package/templates/project/04-design/03-screen-design.md +596 -596
- package/templates/project/04-design/04-design-system.md +261 -261
- package/templates/project/04-design/05-data-model.md +211 -211
- package/templates/project/04-design/06-api-spec.md +226 -226
- package/templates/project/04-design/07-architecture.md +183 -183
- package/templates/project/04-design/08-infrastructure.md +180 -180
- package/templates/project/AI_CONTEXT.md +55 -0
- package/templates/project/STAKEHOLDER-SUMMARY.md +66 -0
- package/templates/tasks/task-template/a-definition.md +143 -143
- package/templates/tasks/task-template/b-research.md +185 -185
- package/templates/tasks/task-template/c-implementation.md +200 -200
- package/templates/project/01-requirements/01-system-overview.md +0 -49
- package/templates/project/01-requirements/03-features-planned.md +0 -75
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const { parseLines, HEADING_RE } = require('../lib/markdown');
|
|
7
|
+
const { SEVERITY, makeFinding, toPosixRelative } = require('../lib/findings');
|
|
8
|
+
const { walkMdFiles } = require('../lib/walk-md');
|
|
9
|
+
const { ID_FAMILIES } = require('../lib/project-spec');
|
|
10
|
+
|
|
11
|
+
const CHECK = 'id-trace';
|
|
12
|
+
// 族の列挙は ID_FAMILIES(SSoT)から導出する。前後が英数字・ハイフンに連続する
|
|
13
|
+
// 出現(パス断片・複合語 CS-001-detail・P-00X のような部分一致)を除外する
|
|
14
|
+
const ID_RE = new RegExp(
|
|
15
|
+
`(?<![0-9A-Za-z-])(${ID_FAMILIES.map((f) => f.family).join('|')})-(\\d+)(?![0-9A-Za-z-])`,
|
|
16
|
+
'g'
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const ORPHAN_MESSAGES = {
|
|
20
|
+
P: (id) => `「${id}」はどのユーザーストーリーからも参照されていません(孤児ペルソナの可能性)`,
|
|
21
|
+
FN: (id) => `「${id}」は core-scenarios から参照されていません(シナリオ未カバーの可能性)`,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function collectOccurrences(projectDir) {
|
|
25
|
+
const occurrences = [];
|
|
26
|
+
for (const abs of walkMdFiles(projectDir)) {
|
|
27
|
+
const relFromProject = toPosixRelative(projectDir, abs);
|
|
28
|
+
for (const line of parseLines(fs.readFileSync(abs, 'utf8'))) {
|
|
29
|
+
if (line.inFence) continue;
|
|
30
|
+
const isHeading = HEADING_RE.test(line.visible);
|
|
31
|
+
ID_RE.lastIndex = 0;
|
|
32
|
+
let m;
|
|
33
|
+
while ((m = ID_RE.exec(line.visible)) !== null) {
|
|
34
|
+
occurrences.push({
|
|
35
|
+
family: m[1],
|
|
36
|
+
id: `${m[1]}-${m[2]}`,
|
|
37
|
+
relFile: relFromProject,
|
|
38
|
+
line: line.line,
|
|
39
|
+
isHeading,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return occurrences;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function run({ rootDir }) {
|
|
48
|
+
const projectDir = path.join(rootDir, 'docs', 'project');
|
|
49
|
+
if (!fs.existsSync(projectDir)) return [];
|
|
50
|
+
|
|
51
|
+
const findings = [];
|
|
52
|
+
const occurrences = collectOccurrences(projectDir);
|
|
53
|
+
const toFile = (relFromProject) => `docs/project/${relFromProject}`;
|
|
54
|
+
|
|
55
|
+
for (const fam of ID_FAMILIES) {
|
|
56
|
+
const famOccurrences = occurrences.filter((o) => o.family === fam.family);
|
|
57
|
+
if (famOccurrences.length === 0) continue;
|
|
58
|
+
|
|
59
|
+
const isDefinition = (o) =>
|
|
60
|
+
fam.definitionFiles.includes(o.relFile) && (!fam.headingDefinition || o.isHeading);
|
|
61
|
+
|
|
62
|
+
const definitions = new Map();
|
|
63
|
+
for (const o of famOccurrences) {
|
|
64
|
+
if (isDefinition(o) && !definitions.has(o.id)) definitions.set(o.id, o);
|
|
65
|
+
}
|
|
66
|
+
const references = famOccurrences.filter((o) => !isDefinition(o));
|
|
67
|
+
|
|
68
|
+
if (definitions.size === 0) {
|
|
69
|
+
if (fam.degradeWhenNoDefinitions) {
|
|
70
|
+
if (references.length > 0) {
|
|
71
|
+
const first = references[0];
|
|
72
|
+
findings.push(
|
|
73
|
+
makeFinding(
|
|
74
|
+
CHECK,
|
|
75
|
+
SEVERITY.WARNING,
|
|
76
|
+
toFile(first.relFile),
|
|
77
|
+
first.line,
|
|
78
|
+
`${fam.family} の定義元(${fam.definitionFiles.join(' / ')})で ${fam.family} ID が定義されていないため、${fam.family} の trace 検査をスキップしました`
|
|
79
|
+
)
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const anyDefinitionFileExists = fam.definitionFiles.some((rel) =>
|
|
85
|
+
fs.existsSync(path.join(projectDir, ...rel.split('/')))
|
|
86
|
+
);
|
|
87
|
+
if (!anyDefinitionFileExists) continue; // 定義ファイル自体が無い族は skip(structure が別途検出)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const reported = new Set();
|
|
91
|
+
for (const ref of references) {
|
|
92
|
+
if (definitions.has(ref.id) || reported.has(ref.id)) continue;
|
|
93
|
+
reported.add(ref.id);
|
|
94
|
+
findings.push(
|
|
95
|
+
makeFinding(
|
|
96
|
+
CHECK,
|
|
97
|
+
SEVERITY.ERROR,
|
|
98
|
+
toFile(ref.relFile),
|
|
99
|
+
ref.line,
|
|
100
|
+
`「${ref.id}」が ${fam.definitionFiles.join(' / ')} で定義されていません(trace 切れ)`
|
|
101
|
+
)
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (fam.orphanScanFiles) {
|
|
106
|
+
const scanFileExists = fam.orphanScanFiles.some((rel) =>
|
|
107
|
+
fs.existsSync(path.join(projectDir, ...rel.split('/')))
|
|
108
|
+
);
|
|
109
|
+
if (scanFileExists) {
|
|
110
|
+
const referencedInScan = new Set(
|
|
111
|
+
references.filter((o) => fam.orphanScanFiles.includes(o.relFile)).map((o) => o.id)
|
|
112
|
+
);
|
|
113
|
+
for (const [id, def] of definitions) {
|
|
114
|
+
if (!referencedInScan.has(id)) {
|
|
115
|
+
findings.push(
|
|
116
|
+
makeFinding(
|
|
117
|
+
CHECK,
|
|
118
|
+
SEVERITY.WARNING,
|
|
119
|
+
toFile(def.relFile),
|
|
120
|
+
def.line,
|
|
121
|
+
ORPHAN_MESSAGES[fam.family](id)
|
|
122
|
+
)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return findings;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = { run };
|
|
134
|
+
|
|
135
|
+
if (require.main === module) {
|
|
136
|
+
require('../lib/check-cli').runCheckCli(run, process.argv[2]);
|
|
137
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const { extractLinks } = require('../lib/markdown');
|
|
7
|
+
const { SEVERITY, makeFinding, toPosixRelative } = require('../lib/findings');
|
|
8
|
+
const { walkMdFiles } = require('../lib/walk-md');
|
|
9
|
+
|
|
10
|
+
const CHECK = 'links';
|
|
11
|
+
// URI スキーム付き(http/mailto/tel/data 等すべて)・アンカーのみ・絶対パスは
|
|
12
|
+
// 相対リンク検査の対象外(ネットワークアクセスはしない=決定性)
|
|
13
|
+
const SKIP_TARGET_RE = /^([a-z][a-z0-9+.-]*:|#|\/)/i;
|
|
14
|
+
|
|
15
|
+
// dir 配下の Markdown の相対リンク切れを findings として返す。
|
|
16
|
+
// リポジトリ側(scripts/repo-check.mjs)から skills/・templates/ にも
|
|
17
|
+
// 再利用できるよう、走査ロジックを分離している。
|
|
18
|
+
function collectBrokenLinks(baseDir, filePrefix) {
|
|
19
|
+
const findings = [];
|
|
20
|
+
if (!fs.existsSync(baseDir)) return findings;
|
|
21
|
+
|
|
22
|
+
for (const abs of walkMdFiles(baseDir)) {
|
|
23
|
+
const file = `${filePrefix}${toPosixRelative(baseDir, abs)}`;
|
|
24
|
+
for (const link of extractLinks(fs.readFileSync(abs, 'utf8'))) {
|
|
25
|
+
if (SKIP_TARGET_RE.test(link.target)) continue;
|
|
26
|
+
const cleaned = link.target.split('#')[0].split('?')[0];
|
|
27
|
+
if (cleaned === '') continue;
|
|
28
|
+
let decoded;
|
|
29
|
+
try {
|
|
30
|
+
decoded = decodeURIComponent(cleaned);
|
|
31
|
+
} catch {
|
|
32
|
+
decoded = cleaned;
|
|
33
|
+
}
|
|
34
|
+
const resolved = path.resolve(path.dirname(abs), decoded);
|
|
35
|
+
if (!fs.existsSync(resolved)) {
|
|
36
|
+
findings.push(
|
|
37
|
+
makeFinding(
|
|
38
|
+
CHECK,
|
|
39
|
+
SEVERITY.ERROR,
|
|
40
|
+
file,
|
|
41
|
+
link.line,
|
|
42
|
+
`リンク先「${link.target}」が存在しません`
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return findings;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function run({ rootDir }) {
|
|
52
|
+
return collectBrokenLinks(path.join(rootDir, 'docs'), 'docs/');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { run, collectBrokenLinks };
|
|
56
|
+
|
|
57
|
+
if (require.main === module) {
|
|
58
|
+
require('../lib/check-cli').runCheckCli(run, process.argv[2]);
|
|
59
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const { parseLines, splitTableCells, HEADING_RE } = require('../lib/markdown');
|
|
7
|
+
const { SEVERITY, makeFinding, toPosixRelative } = require('../lib/findings');
|
|
8
|
+
const { walkMdFiles } = require('../lib/walk-md');
|
|
9
|
+
const { KNOWN_TOKENS } = require('../lib/project-spec');
|
|
10
|
+
|
|
11
|
+
const CHECK = 'placeholder';
|
|
12
|
+
const COMMENT_ONLY_CELL_RE = /^<!--[\s\S]*-->$/;
|
|
13
|
+
const HR_RE = /^(-{3,}|\*{3,}|_{3,})$/;
|
|
14
|
+
|
|
15
|
+
// 行内にトークンが「リンク [token](...) 以外の形で」1 回でも出現するか
|
|
16
|
+
function hasRawToken(text, token) {
|
|
17
|
+
let idx = text.indexOf(token);
|
|
18
|
+
while (idx !== -1) {
|
|
19
|
+
if (text[idx + token.length] !== '(') return true;
|
|
20
|
+
idx = text.indexOf(token, idx + 1);
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 空でも正常なセクション(テンプレート上、空欄がありうるもの)
|
|
26
|
+
function isExemptHeading(text) {
|
|
27
|
+
return (
|
|
28
|
+
text.includes('(任意)') ||
|
|
29
|
+
text.includes('(任意)') ||
|
|
30
|
+
text.startsWith('メモ') ||
|
|
31
|
+
text.startsWith('Not Covered in MVP')
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function checkFile(absPath, file, findings) {
|
|
36
|
+
const lines = parseLines(fs.readFileSync(absPath, 'utf8'));
|
|
37
|
+
|
|
38
|
+
for (const line of lines) {
|
|
39
|
+
if (line.inFence) continue;
|
|
40
|
+
|
|
41
|
+
// 1. コメントのみのテーブルセル(テンプレート行がそのまま残っている)
|
|
42
|
+
if (line.visible.includes('|')) {
|
|
43
|
+
const rawCells = splitTableCells(line.raw);
|
|
44
|
+
if (rawCells && rawCells.some((cell) => COMMENT_ONLY_CELL_RE.test(cell))) {
|
|
45
|
+
findings.push(
|
|
46
|
+
makeFinding(
|
|
47
|
+
CHECK,
|
|
48
|
+
SEVERITY.WARNING,
|
|
49
|
+
file,
|
|
50
|
+
line.line,
|
|
51
|
+
'テーブルに未記入のテンプレートセル(<!-- ... -->)が残っています'
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 2. 既知の角括弧プレースホルダ(リンク [text](...) は除外)
|
|
59
|
+
const tokens = KNOWN_TOKENS.filter((token) => hasRawToken(line.visible, token));
|
|
60
|
+
if (tokens.length > 0) {
|
|
61
|
+
findings.push(
|
|
62
|
+
makeFinding(
|
|
63
|
+
CHECK,
|
|
64
|
+
SEVERITY.WARNING,
|
|
65
|
+
file,
|
|
66
|
+
line.line,
|
|
67
|
+
`テンプレートのプレースホルダ ${tokens.join(' ')} が残っています`
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 3. **例:** マーカーの残置(例示ブロックが削除されていない)
|
|
73
|
+
if (line.visible.trim() === '**例:**') {
|
|
74
|
+
findings.push(
|
|
75
|
+
makeFinding(
|
|
76
|
+
CHECK,
|
|
77
|
+
SEVERITY.WARNING,
|
|
78
|
+
file,
|
|
79
|
+
line.line,
|
|
80
|
+
'テンプレートの例示ブロック(**例:**)が残っています'
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 4. 空セクション(##/### の本文にコメント・空行・罫線以外が無い)
|
|
87
|
+
const headings = [];
|
|
88
|
+
lines.forEach((line, idx) => {
|
|
89
|
+
if (line.inFence) return;
|
|
90
|
+
const m = line.visible.match(HEADING_RE);
|
|
91
|
+
if (m) headings.push({ level: m[1].length, text: m[2].trim(), line: line.line, idx });
|
|
92
|
+
});
|
|
93
|
+
headings.forEach((heading, hi) => {
|
|
94
|
+
if (heading.level < 2 || heading.level > 3) return;
|
|
95
|
+
if (isExemptHeading(heading.text)) return;
|
|
96
|
+
const next = headings.slice(hi + 1).find((h) => h.level <= heading.level);
|
|
97
|
+
const endIdx = next ? next.idx : lines.length;
|
|
98
|
+
const hasContent = lines.slice(heading.idx + 1, endIdx).some((line) => {
|
|
99
|
+
if (line.inFence) return true; // コードブロックは本文とみなす
|
|
100
|
+
const text = line.visible.trim();
|
|
101
|
+
return text !== '' && !HR_RE.test(text) && !HEADING_RE.test(line.visible);
|
|
102
|
+
});
|
|
103
|
+
if (!hasContent) {
|
|
104
|
+
findings.push(
|
|
105
|
+
makeFinding(
|
|
106
|
+
CHECK,
|
|
107
|
+
SEVERITY.WARNING,
|
|
108
|
+
file,
|
|
109
|
+
heading.line,
|
|
110
|
+
`セクション「${heading.text}」が未記入です`
|
|
111
|
+
)
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function run({ rootDir }) {
|
|
118
|
+
const projectDir = path.join(rootDir, 'docs', 'project');
|
|
119
|
+
if (!fs.existsSync(projectDir)) return [];
|
|
120
|
+
|
|
121
|
+
const findings = [];
|
|
122
|
+
for (const abs of walkMdFiles(projectDir)) {
|
|
123
|
+
checkFile(abs, `docs/project/${toPosixRelative(projectDir, abs)}`, findings);
|
|
124
|
+
}
|
|
125
|
+
return findings;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { run };
|
|
129
|
+
|
|
130
|
+
if (require.main === module) {
|
|
131
|
+
require('../lib/check-cli').runCheckCli(run, process.argv[2]);
|
|
132
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const { parseLines, splitTableCells, HEADING_RE } = require('../lib/markdown');
|
|
7
|
+
const { SEVERITY, makeFinding } = require('../lib/findings');
|
|
8
|
+
const spec = require('../lib/project-spec');
|
|
9
|
+
|
|
10
|
+
const CHECK = 'structure';
|
|
11
|
+
|
|
12
|
+
function run({ rootDir }) {
|
|
13
|
+
const projectDir = path.join(rootDir, 'docs', 'project');
|
|
14
|
+
if (!fs.existsSync(projectDir)) {
|
|
15
|
+
return [
|
|
16
|
+
makeFinding(
|
|
17
|
+
CHECK,
|
|
18
|
+
SEVERITY.ERROR,
|
|
19
|
+
'docs/project',
|
|
20
|
+
null,
|
|
21
|
+
'docs/project が存在しません。/a-001-setup-doc-structure でドキュメント骨格を作成してください'
|
|
22
|
+
),
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const findings = [];
|
|
27
|
+
const existing = spec.CANONICAL_FILES.map((rel) =>
|
|
28
|
+
fs.existsSync(path.join(projectDir, ...rel.split('/')))
|
|
29
|
+
);
|
|
30
|
+
const frontier = existing.lastIndexOf(true);
|
|
31
|
+
|
|
32
|
+
// 骨格だけあって正準ドキュメントがゼロの状態を「健全」と誤報しない
|
|
33
|
+
if (frontier === -1) {
|
|
34
|
+
return [
|
|
35
|
+
makeFinding(
|
|
36
|
+
CHECK,
|
|
37
|
+
SEVERITY.WARNING,
|
|
38
|
+
'docs/project',
|
|
39
|
+
null,
|
|
40
|
+
'docs/project に正準ドキュメントが 1 つもありません(/a-002-initialize-project から要件定義を開始してください)'
|
|
41
|
+
),
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < frontier; i++) {
|
|
46
|
+
if (!existing[i]) {
|
|
47
|
+
findings.push(
|
|
48
|
+
makeFinding(
|
|
49
|
+
CHECK,
|
|
50
|
+
SEVERITY.ERROR,
|
|
51
|
+
`docs/project/${spec.CANONICAL_FILES[i]}`,
|
|
52
|
+
null,
|
|
53
|
+
'必須ファイルがありません(後続フェーズのドキュメントは既に存在します)'
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
spec.CANONICAL_FILES.forEach((rel, i) => {
|
|
60
|
+
if (!existing[i]) return;
|
|
61
|
+
const file = `docs/project/${rel}`;
|
|
62
|
+
const lines = parseLines(
|
|
63
|
+
fs.readFileSync(path.join(projectDir, ...rel.split('/')), 'utf8')
|
|
64
|
+
);
|
|
65
|
+
const headings = [];
|
|
66
|
+
let hasRequiredTableHeader = false;
|
|
67
|
+
const requiredHeader = spec.REQUIRED_TABLE_HEADERS[rel];
|
|
68
|
+
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
if (line.inFence) continue;
|
|
71
|
+
const m = line.visible.match(HEADING_RE);
|
|
72
|
+
// H1 はドキュメントタイトルなので必須見出しの充足に数えない
|
|
73
|
+
// (例: 02-mvp-scope.md の「# MVP Scope」がキー「MVP Scope」を偽充足しないように)
|
|
74
|
+
if (m && m[1].length >= 2) headings.push(m[2].trim());
|
|
75
|
+
if (requiredHeader && !hasRequiredTableHeader) {
|
|
76
|
+
const cells = splitTableCells(line.visible);
|
|
77
|
+
if (cells && cells.includes(requiredHeader)) hasRequiredTableHeader = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const key of spec.REQUIRED_HEADINGS[rel] || []) {
|
|
82
|
+
if (!headings.some((h) => h.startsWith(key))) {
|
|
83
|
+
findings.push(
|
|
84
|
+
makeFinding(CHECK, SEVERITY.ERROR, file, null, `必須見出し「${key}」が見つかりません`)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (requiredHeader && !hasRequiredTableHeader) {
|
|
89
|
+
findings.push(
|
|
90
|
+
makeFinding(
|
|
91
|
+
CHECK,
|
|
92
|
+
SEVERITY.ERROR,
|
|
93
|
+
file,
|
|
94
|
+
null,
|
|
95
|
+
`必須テーブルヘッダ「${requiredHeader}」を含む表が見つかりません`
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const designStarted = spec.CANONICAL_FILES.some(
|
|
102
|
+
(rel, i) => existing[i] && rel.startsWith('04-design/')
|
|
103
|
+
);
|
|
104
|
+
if (designStarted) {
|
|
105
|
+
for (const artifact of spec.REVIEW_ARTIFACTS) {
|
|
106
|
+
if (!fs.existsSync(path.join(projectDir, artifact))) {
|
|
107
|
+
findings.push(
|
|
108
|
+
makeFinding(
|
|
109
|
+
CHECK,
|
|
110
|
+
SEVERITY.WARNING,
|
|
111
|
+
`docs/project/${artifact}`,
|
|
112
|
+
null,
|
|
113
|
+
`設計フェーズに着手していますが ${artifact} がありません(/a-006-review-requirements-domain 未実施の可能性)`
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return findings;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { run };
|
|
124
|
+
|
|
125
|
+
if (require.main === module) {
|
|
126
|
+
require('../lib/check-cli').runCheckCli(run, process.argv[2]);
|
|
127
|
+
}
|
package/bin/cli.js
CHANGED
|
@@ -1,67 +1,51 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
console.log(green(`\n✔ Successfully installed Yodogawa skills for ${response.type}!`));
|
|
54
|
-
console.log(`\nNext steps:`);
|
|
55
|
-
console.log(`1. Open ${bold(targetDirName + '/skills/')} to explore the skills.`);
|
|
56
|
-
console.log(`2. Start using them in your project!\n`);
|
|
57
|
-
|
|
58
|
-
} catch (err) {
|
|
59
|
-
console.error(red(`\n✖ Error installing skills: ${err.message}`));
|
|
60
|
-
process.exit(1);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
main().catch(err => {
|
|
65
|
-
console.error(red(err));
|
|
66
|
-
process.exit(1);
|
|
67
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { red } = require('kleur');
|
|
5
|
+
const install = require('./commands/install');
|
|
6
|
+
|
|
7
|
+
const HELP = `Usage: yodogawa [command] [options]
|
|
8
|
+
|
|
9
|
+
Commands:
|
|
10
|
+
(なし) 対話形式でスキル・テンプレートをインストール
|
|
11
|
+
doctor docs/project/ のトレーサビリティ・構造を検査
|
|
12
|
+
new-task <slug> docs/tasks/ に連番タスクディレクトリを作成
|
|
13
|
+
help このヘルプを表示
|
|
14
|
+
|
|
15
|
+
各コマンドの詳細は yodogawa <command> --help を参照してください。`;
|
|
16
|
+
|
|
17
|
+
function printHelp() {
|
|
18
|
+
console.log(HELP);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function run(argv = process.argv.slice(2), { cwd = process.cwd() } = {}) {
|
|
22
|
+
const [command, ...rest] = argv;
|
|
23
|
+
if (command === undefined) {
|
|
24
|
+
// 後方互換: 引数なしは従来どおり対話インストール
|
|
25
|
+
await install.main({ cwd });
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
if (command === 'doctor') return require('./commands/doctor').main(rest, { cwd });
|
|
29
|
+
if (command === 'new-task') return require('./commands/new-task').main(rest, { cwd });
|
|
30
|
+
if (command === '--help' || command === '-h' || command === 'help') {
|
|
31
|
+
printHelp();
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
console.error(red(`✖ Unknown command: ${command}`));
|
|
35
|
+
console.error(HELP);
|
|
36
|
+
return 2;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// main は install の別名(既存利用箇所の後方互換のため維持)
|
|
40
|
+
module.exports = { main: install.main, run };
|
|
41
|
+
|
|
42
|
+
if (require.main === module) {
|
|
43
|
+
run()
|
|
44
|
+
.then((code) => {
|
|
45
|
+
process.exitCode = code;
|
|
46
|
+
})
|
|
47
|
+
.catch((err) => {
|
|
48
|
+
console.error(red(`\n✖ Error: ${err.message}`));
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
});
|
|
51
|
+
}
|