sdd-forge 0.1.0-alpha.702 → 0.1.0-alpha.774

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 (98) hide show
  1. package/README.md +10 -15
  2. package/package.json +7 -3
  3. package/src/AGENTS.md +58 -11
  4. package/src/api.js +12 -0
  5. package/src/docs/commands/agents.js +1 -1
  6. package/src/docs/commands/data.js +2 -2
  7. package/src/docs/commands/forge.js +1 -1
  8. package/src/docs/commands/init.js +2 -1
  9. package/src/docs/commands/readme.js +2 -1
  10. package/src/docs/commands/review.js +2 -2
  11. package/src/docs/commands/scan.js +2 -5
  12. package/src/docs/commands/text.js +2 -2
  13. package/src/docs/commands/translate.js +1 -1
  14. package/src/docs/data/docs.js +2 -2
  15. package/src/docs/lib/command-context.js +3 -1
  16. package/src/docs/lib/data-source-loader.js +26 -14
  17. package/src/docs/lib/resolver-factory.js +12 -8
  18. package/src/docs/lib/template-merger.js +11 -7
  19. package/src/flow/commands/merge.js +6 -6
  20. package/src/flow/commands/report.js +68 -12
  21. package/src/flow/commands/review.js +26 -19
  22. package/src/flow/lib/get-check.js +6 -8
  23. package/src/flow/lib/get-context.js +3 -2
  24. package/src/flow/lib/get-guardrail.js +2 -2
  25. package/src/flow/lib/get-prompt.js +7 -25
  26. package/src/flow/lib/get-status.js +53 -26
  27. package/src/flow/lib/get-test-result.js +90 -0
  28. package/src/flow/lib/run-finalize.js +55 -15
  29. package/src/flow/lib/run-gate.js +73 -14
  30. package/src/flow/lib/run-impl-confirm.js +7 -3
  31. package/src/flow/lib/run-prepare-spec.js +29 -15
  32. package/src/flow/lib/run-resume.js +2 -0
  33. package/src/flow/lib/run-retro.js +8 -7
  34. package/src/flow/lib/run-review.js +6 -0
  35. package/src/flow/lib/run-sync.js +4 -3
  36. package/src/flow/lib/set-auto.js +3 -2
  37. package/src/flow/lib/set-init.js +34 -0
  38. package/src/flow/lib/set-issue.js +8 -3
  39. package/src/flow/lib/set-metric.js +5 -6
  40. package/src/flow/lib/set-req.js +10 -3
  41. package/src/flow/lib/set-step.js +5 -0
  42. package/src/flow/lib/set-summary.js +10 -1
  43. package/src/flow/registry.js +29 -8
  44. package/src/flow.js +89 -29
  45. package/src/help.js +2 -0
  46. package/src/lib/agent.js +10 -10
  47. package/src/lib/cli.js +4 -0
  48. package/src/lib/config.js +16 -3
  49. package/src/lib/constants.js +109 -0
  50. package/src/lib/flow-state.js +324 -57
  51. package/src/lib/git-helpers.js +27 -6
  52. package/src/lib/lint.js +3 -2
  53. package/src/lib/log.js +15 -2
  54. package/src/lib/presets.js +63 -7
  55. package/src/lib/process.js +5 -8
  56. package/src/lib/types.js +1 -1
  57. package/src/loader.js +36 -0
  58. package/src/locale/en/messages.json +1 -1
  59. package/src/locale/en/ui.json +1 -0
  60. package/src/locale/ja/messages.json +1 -1
  61. package/src/locale/ja/ui.json +1 -0
  62. package/src/metrics/commands/token.js +416 -0
  63. package/src/metrics.js +38 -0
  64. package/src/presets/cakephp2/data/config.js +11 -9
  65. package/src/presets/cakephp2/data/email.js +5 -4
  66. package/src/presets/cakephp2/data/libs.js +4 -3
  67. package/src/presets/cakephp2/data/tests.js +2 -1
  68. package/src/presets/cakephp2/data/views.js +2 -1
  69. package/src/presets/laravel/data/commands.js +2 -1
  70. package/src/presets/laravel/data/config.js +6 -5
  71. package/src/presets/laravel/data/controllers.js +2 -1
  72. package/src/presets/laravel/data/models.js +2 -1
  73. package/src/presets/laravel/data/routes.js +2 -1
  74. package/src/presets/laravel/data/tables.js +2 -1
  75. package/src/presets/lib/path-match.js +36 -0
  76. package/src/presets/mysql/NOTICE +26 -0
  77. package/src/presets/mysql/guardrail.json +108 -0
  78. package/src/presets/mysql/preset.json +6 -0
  79. package/src/presets/nextjs/data/components.js +3 -2
  80. package/src/presets/nextjs/data/routes.js +3 -2
  81. package/src/presets/symfony/data/commands.js +2 -1
  82. package/src/presets/symfony/data/config.js +5 -4
  83. package/src/presets/symfony/data/controllers.js +2 -1
  84. package/src/presets/symfony/data/entities.js +2 -1
  85. package/src/presets/symfony/data/routes.js +2 -1
  86. package/src/presets/symfony/data/tables.js +2 -1
  87. package/src/presets/webapp/NOTICE +16 -0
  88. package/src/presets/webapp/guardrail.json +32 -0
  89. package/src/sdd-forge.js +13 -3
  90. package/src/setup.js +11 -11
  91. package/src/templates/config.example.json +7 -7
  92. package/src/templates/partials/core-principle.md +6 -0
  93. package/src/templates/skills/sdd-forge.flow-finalize/SKILL.md +2 -0
  94. package/src/templates/skills/sdd-forge.flow-impl/SKILL.md +3 -1
  95. package/src/templates/skills/sdd-forge.flow-plan/SKILL.md +37 -13
  96. package/src/templates/skills/sdd-forge.flow-status/SKILL.md +2 -1
  97. package/src/upgrade.js +2 -2
  98. package/src/flow/lib/phases.js +0 -15
package/README.md CHANGED
@@ -88,9 +88,9 @@ If you already have source code, generate documentation to get a complete pictur
88
88
 
89
89
  | Command | Phase |
90
90
  |---|---|
91
- | `$sdd-forge flow prepare --title "..." --base main` | plan (create spec + branch) |
92
- | `$sdd-forge flow run review` | implement (AI code review) |
93
- | `$sdd-forge flow run finalize --mode all` | finalize (wrap-up) |
91
+ | `$sdd-forge.flow-plan` | plan (specification) |
92
+ | `$sdd-forge.flow-impl` | implement (coding + review) |
93
+ | `$sdd-forge.flow-finalize` | finalize (commit, merge, docs sync, cleanup) |
94
94
 
95
95
  ## Commands
96
96
 
@@ -98,12 +98,6 @@ If you already have source code, generate documentation to get a complete pictur
98
98
  |---|---|
99
99
  | `setup` | Register project and generate config |
100
100
  | `docs build` | Run the full documentation pipeline |
101
- | `docs readme` | Generate `README.md` from `docs/` |
102
- | `docs review` | Check documentation quality |
103
- | `flow prepare` | Create spec and branch |
104
- | `flow get status` | Show flow progress |
105
- | `presets` | List available presets |
106
- | `help` | Show all commands |
107
101
 
108
102
  See `sdd-forge help` or the [command reference](docs/cli_commands.md) for the full list.
109
103
 
@@ -129,12 +123,13 @@ See the [configuration reference](docs/configuration.md) for details.
129
123
  <!-- {{data("cli.docs.chapters", {header: "", labels: "Chapter|Summary", ignoreError: true})}} -->
130
124
  | Chapter | Summary |
131
125
  | --- | --- |
132
- | [Tool Overview and Architecture](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/overview.md) | |
133
- | [Technology Stack and Operations](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/stack_and_ops.md) | |
134
- | [Project Structure](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/project_structure.md) | |
135
- | [CLI Command Reference](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/cli_commands.md) | |
136
- | [Configuration and Customization](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/configuration.md) | |
137
- | [Internal Design](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/internal_design.md) | |
126
+ | [Tool Overview and Architecture](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/overview.md) | This chapter introduces sdd-forge, a CLI tool that automates documentation generation from source code analysis and e… |
127
+ | [Technology Stack and Operations](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/stack_and_ops.md) | This chapter covers the technology stack, dependency management, deployment, and operations procedures for sdd-forge,… |
128
+ | [Project Structure](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/project_structure.md) | This chapter describes the overall directory organization of the sdd-forge project, which is structured around seven … |
129
+ | [CLI Command Reference](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/cli_commands.md) | This chapter documents 8 CLI entrypoints and routed commands in the analyzed files: setup, check, check freshness, do… |
130
+ | [Configuration and Customization](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/configuration.md) | sdd-forge is configured through a single project-level JSON file (.sdd-forge/config.json) and optionally extended by … |
131
+ | [Internal Design](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/internal_design.md) | This chapter describes the internal structure of sdd-forge: a layered CLI tool built entirely on Node.js built-in mod… |
132
+ | [Preset Creation Guide](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/creating_presets.md) | |
138
133
  <!-- {{/data}} -->
139
134
 
140
135
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdd-forge",
3
- "version": "0.1.0-alpha.702",
3
+ "version": "0.1.0-alpha.774",
4
4
  "description": "Spec-Driven Development tooling for automated documentation generation",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,12 +10,17 @@
10
10
  "bin": {
11
11
  "sdd-forge": "./src/sdd-forge.js"
12
12
  },
13
+ "exports": {
14
+ ".": "./src/sdd-forge.js",
15
+ "./api": "./src/api.js",
16
+ "./presets/*": "./src/presets/*"
17
+ },
13
18
  "files": [
14
19
  "src/",
15
20
  "!src/presets/*/tests/"
16
21
  ],
17
22
  "engines": {
18
- "node": ">=18.0.0"
23
+ "node": ">=18.19.0"
19
24
  },
20
25
  "keywords": [
21
26
  "sdd",
@@ -25,7 +30,6 @@
25
30
  "technical-docs"
26
31
  ],
27
32
  "scripts": {
28
- "test": "node tests/run.js",
29
33
  "test:unit": "node tests/run.js --scope unit",
30
34
  "test:e2e": "node tests/run.js --scope e2e",
31
35
  "test:acceptance": "node tests/acceptance/run.js"
package/src/AGENTS.md CHANGED
@@ -43,7 +43,7 @@ src/
43
43
  │ ├── set.js set サブディスパッチャ
44
44
  │ ├── run.js run サブディスパッチャ
45
45
  │ ├── get/ status, resolve-context, check, prompt, qa-count, guardrail, issue
46
- │ ├── set/ step, request, issue, note, summary, req, metric, redo
46
+ │ ├── set/ step, request, issue, note, summary, req, metric
47
47
  │ ├── run/ prepare-spec, gate, review, impl-confirm, finalize, sync
48
48
  │ └── commands/ 内部ヘルパー(merge, cleanup, review の実体)
49
49
  ├── spec/commands/ init, gate, guardrail(flow/run/prepare-spec, gate が内部で呼ぶ)
@@ -152,16 +152,12 @@ base → api → graphql
152
152
  ```
153
153
  presets/<key>/
154
154
  ├── preset.json プリセット定義
155
- ├── data/ DataSource クラス群
155
+ ├── data/ DataSource クラス群(scan + resolve を兼ねる)
156
156
  │ ├── config.js 設定解析
157
157
  │ ├── controllers.js コントローラ解析
158
158
  │ └── ...
159
- ├── scan/ scan パーサー群
160
- │ ├── routes.js ルート解析
161
- │ ├── config.js 設定解析
162
- │ └── ...
163
159
  ├── tests/ プリセット固有テスト
164
- │ ├── unit/ ユニットテスト(scan パーサー I/O テスト等)
160
+ │ ├── unit/ ユニットテスト(DataSource match/parse I/O テスト等)
165
161
  │ ├── e2e/ E2E テスト(統合スキャンテスト等)
166
162
  │ └── acceptance/ acceptance テスト(preset ローカル fixture + test.js)
167
163
  └── templates/ 章テンプレート
@@ -177,21 +173,36 @@ presets/<key>/
177
173
 
178
174
  ## プリセット作成ルール
179
175
 
176
+ ### MUST: プリセット作成ガイドとの同期
177
+
178
+ プリセットの仕様・作成手順・契約(`preset.json` スキーマ、DataSource のインターフェース、`match()` / `parse()` の引数契約、resolve メソッドの戻り値型、import ルール、テンプレートディレクティブ、scan/data ペアリング規則等)を変更した場合、**`.sdd-forge/templates/*/docs/creating_presets.md`(全言語)を同じコミット内で必ず更新すること。**
179
+
180
+ 対象となる変更の例:
181
+
182
+ - `src/api.js` の公開クラス追加・削除・シグネチャ変更
183
+ - `package.json` の `exports` 変更
184
+ - `DataSource` / `Scannable` / `AnalysisEntry` / `Table` / `MarkdownText` のインターフェース変更
185
+ - `preset.json` スキーマの追加・変更
186
+ - `data/` loader のロード規約変更
187
+ - テンプレートディレクティブ(`{%extends%}`, `{%block%}`, `{{data}}`, `{{text}}`)の文法・挙動変更
188
+ - プリセット作成手順・MUST ルールの追加・変更
189
+
190
+ 本ガイドは AI エージェントがプリセットを作成する際の単一の参照ドキュメントである。**ガイドが実装とズレるとプリセット作成が破綻する**ため、実装変更と文書更新を同一 PR で行うこと。別 PR に分割してはならない。
191
+
180
192
  ### MUST: プリセット作成手順(トップダウン設計)
181
193
 
182
194
  プリセットの構成要素は以下の順序で作成すること:
183
195
 
184
196
  1. **テンプレート** (`templates/`) — どんなドキュメントを出力するか定義する
185
- 2. **DataSource** (`data/`) — テンプレートが必要とするデータを定義する
186
- 3. **scan パーサー** (`scan/`) — DataSource にデータを供給するパーサーを実装する
197
+ 2. **DataSource** (`data/`) — テンプレートが必要とするデータを `Scannable` DataSource の `match` / `parse` / `scan` で収集し、resolve メソッドで提供する
187
198
 
188
- 消費者(テンプレート)→ 仲介者(DataSource)→ 生産者(scan)の順に作ることで、不要なパーサーを書かず、必要なデータの漏れがなくなる。
199
+ 消費者(テンプレート)→ 生産者(DataSource)の順に作ることで、不要な解析を書かず、必要なデータの漏れがなくなる。scan 処理は独立した `scan/` ディレクトリではなく DataSource クラス自身が `Scannable` mixin 経由で担う。
189
200
 
190
201
  ### MUST: プリセットテストの作成
191
202
 
192
203
  プリセットは `tests/` ディレクトリを含むこと。
193
204
 
194
- - `tests/unit/` — scan パーサーの I/O テスト。最小限のフィクスチャを `createTmpDir()` で作成し、パーサー関数の入出力を検証する
205
+ - `tests/unit/` — DataSource `match` / `parse` I/O テスト。最小限のフィクスチャを `createTmpDir()` で作成し、入出力を検証する
195
206
  - `tests/e2e/` — preset.json の scan 設定検証、フルスキャンパイプラインテスト
196
207
  - `tests/acceptance/test.js` — preset ローカル fixture を使う acceptance テスト。共有処理は `tests/acceptance/lib/` を使う
197
208
  - テンプレートを作成・変更した場合は acceptance テストも実装し、実行すること
@@ -295,6 +306,42 @@ export default class SchemaSource extends DataSource {
295
306
  - analysis にデータがあれば動作する(なければ null を返す)
296
307
  - **MUST**: このパターンを使う場合、読む analysis キーを書く scan DataSource がチェーン内に存在する必要がある。対応する scan がないなら data DataSource を作ってはならない
297
308
 
309
+ ### DataSource メソッドの戻り値型(設計方針)
310
+
311
+ DataSource の resolve メソッド(`list()` / `tables()` 等、テンプレートから `{{data}}` で呼ばれるメソッド)の戻り値は、**レンダリング可能な値クラスのインスタンスまたは null** とする。生の Markdown 文字列を返してはならない。
312
+
313
+ プロジェクトは TypeScript を採用しないため、値の構造は OOP(クラス)で表現する(プロジェクト `CLAUDE.md` の「OOP による型表現」を参照)。戻り値には以下の専用クラスを用いる。
314
+
315
+ - `Table` — 表形式データ(labels と rows を保持、列数の整合を invariant として強制)
316
+ - `MarkdownText` — そのまま埋め込む Markdown 断片
317
+ - (必要に応じて)`BulletList` / `Heading` 等の追加クラス
318
+
319
+ ```javascript
320
+ import { Table } from "../../../docs/lib/renderable.js";
321
+
322
+ export default class RoutesSource extends DataSource {
323
+ list(analysis, labels) {
324
+ const routes = analysis.routes?.entries || [];
325
+ if (routes.length === 0) return null;
326
+ const rows = routes.map(r => [r.pattern, r.controller, r.action]);
327
+ return new Table(labels, rows);
328
+ }
329
+ }
330
+ ```
331
+
332
+ **利点**:
333
+
334
+ - コンストラクタで invariant を強制(例: Table は列数一致、labels 非空)
335
+ - `instanceof` による確実な型判別
336
+ - `toMarkdown()` を型に所属させることで、出力形式の拡張(将来の `toHtml()` 等)が Open/Closed に従う
337
+ - テンプレート展開層は `result.toMarkdown()` をポリモルフィックに呼ぶだけで済む
338
+ - オブジェクトリテラル(`{ type: "table", ... }`)のような discriminated union もどきは採用しない
339
+
340
+ **禁止事項**:
341
+
342
+ - 基底 `DataSource` の `toMarkdownTable()` のような Markdown 文字列生成ヘルパーを新設してはならない(既存のものは `Table` クラスへの段階移行対象)
343
+ - 戻り値として生の文字列を返してはならない。構造が単なるテキストなら `MarkdownText` でラップする
344
+
298
345
  ---
299
346
 
300
347
  ## テンプレート構文
package/src/api.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * src/api.js — sdd-forge public API entry point
3
+ *
4
+ * Re-exports the base classes needed by external presets and
5
+ * .sdd-forge/data/ DataSource overrides.
6
+ *
7
+ * Usage (from external preset files):
8
+ * import { DataSource, Scannable, AnalysisEntry } from 'sdd-forge/api';
9
+ */
10
+ export { DataSource } from "./docs/lib/data-source.js";
11
+ export { Scannable } from "./docs/lib/scan-source.js";
12
+ export { AnalysisEntry } from "./docs/lib/analysis-entry.js";
@@ -178,7 +178,7 @@ async function main(ctx) {
178
178
 
179
179
  // Load generated docs as context (instead of raw analysis.json)
180
180
  const docsDir = path.join(root, "docs");
181
- const chapterFiles = getChapterFiles(docsDir, { type: ctx.type, configChapters: ctx.config?.chapters });
181
+ const chapterFiles = getChapterFiles(docsDir, { type: ctx.type, configChapters: ctx.config?.chapters, projectRoot: root });
182
182
  const docsContent = chapterFiles.map((f) => readText(path.join(docsDir, f))).join("\n\n");
183
183
  const readmeContent = readText(path.join(srcRoot, "README.md"));
184
184
  const combinedDocs = [docsContent, readmeContent].filter(Boolean).join("\n\n---\n\n");
@@ -76,7 +76,7 @@ export function populateFromAnalysis(root, analysis, resolveFn, opts) {
76
76
  const docsDir = path.join(root, "docs");
77
77
  const changedFiles = [];
78
78
 
79
- const docsFiles = getChapterFiles(docsDir, { type: opts?.type, configChapters: opts?.configChapters });
79
+ const docsFiles = getChapterFiles(docsDir, { type: opts?.type, configChapters: opts?.configChapters, projectRoot: root });
80
80
 
81
81
  for (const file of docsFiles) {
82
82
  const filePath = path.join(docsDir, file);
@@ -137,7 +137,7 @@ async function main(ctx) {
137
137
  throw new Error(t("messages:data.resolverFailed", { message: err.message }));
138
138
  }
139
139
 
140
- const docsFiles = getChapterFiles(docsDir, { type, configChapters: ctx.config?.chapters });
140
+ const docsFiles = getChapterFiles(docsDir, { type, configChapters: ctx.config?.chapters, projectRoot: root });
141
141
 
142
142
  // Determine relative path prefix for lang.links context
143
143
  const docsDirRel = path.relative(root, docsDir).replace(/\\/g, "/");
@@ -44,7 +44,7 @@ const DEFAULT_MODE = "local";
44
44
 
45
45
  function getTargetFiles(root, type, configChapters) {
46
46
  const docsDir = path.join(root, "docs");
47
- return getChapterFiles(docsDir, { type, configChapters }).map((f) => `docs/${f}`);
47
+ return getChapterFiles(docsDir, { type, configChapters, projectRoot: root }).map((f) => `docs/${f}`);
48
48
  }
49
49
 
50
50
  /**
@@ -169,12 +169,13 @@ function main(ctx) {
169
169
  ? configLangs
170
170
  : [...configLangs, "en"];
171
171
  const configChapters = config?.chapters;
172
- const chaptersOrder = resolveChaptersOrder(type, configChapters);
172
+ const chaptersOrder = resolveChaptersOrder(type, configChapters, root);
173
173
 
174
174
  const resolutions = resolveTemplates(type, lang, {
175
175
  projectLocalDir,
176
176
  fallbackLangs,
177
177
  chaptersOrder,
178
+ projectRoot: root,
178
179
  });
179
180
 
180
181
  // 解決結果からチャプターを生成(README は除外)
@@ -75,11 +75,12 @@ async function main(ctx) {
75
75
 
76
76
  // ボトムアップでテンプレート解決
77
77
  const configChapters = config?.chapters;
78
- const chaptersOrder = resolveChaptersOrder(type, configChapters);
78
+ const chaptersOrder = resolveChaptersOrder(type, configChapters, root);
79
79
  const resolutions = resolveTemplates(type, lang, {
80
80
  projectLocalDir,
81
81
  fallbackLangs,
82
82
  chaptersOrder,
83
+ projectRoot: root,
83
84
  });
84
85
 
85
86
  const readmeRes = resolutions.find((r) => r.fileName === "README.md");
@@ -114,7 +114,7 @@ function main() {
114
114
  // Discover chapter files
115
115
  let chapterFiles = [];
116
116
  if (fs.existsSync(targetDir) && fs.statSync(targetDir).isDirectory()) {
117
- chapterFiles = getChapterFiles(targetDir, { type, configChapters: config.chapters });
117
+ chapterFiles = getChapterFiles(targetDir, { type, configChapters: config.chapters, projectRoot: root });
118
118
  }
119
119
 
120
120
  if (chapterFiles.length === 0) {
@@ -232,7 +232,7 @@ function main() {
232
232
  reportFail("messages:review.langDirMissing", { lang });
233
233
  continue;
234
234
  }
235
- const langFiles = getChapterFiles(langDir, { type, configChapters: config.chapters });
235
+ const langFiles = getChapterFiles(langDir, { type, configChapters: config.chapters, projectRoot: root });
236
236
  if (langFiles.length === 0) {
237
237
  reportFail("messages:review.langNoChapters", { lang });
238
238
  continue;
@@ -22,7 +22,7 @@ import crypto from "crypto";
22
22
  import { fileURLToPath } from "url";
23
23
  import { runIfDirect } from "../../lib/entrypoint.js";
24
24
  import { repoRoot, parseArgs } from "../../lib/cli.js";
25
- import { sddDataDir, sddOutputDir } from "../../lib/config.js";
25
+ import { sddOutputDir } from "../../lib/config.js";
26
26
  import { collectFiles } from "../lib/scanner.js";
27
27
  import { loadDataSources } from "../lib/data-source-loader.js";
28
28
  import { presetByLeaf, resolveChainSafe, resolveMultiChains } from "../../lib/presets.js";
@@ -289,7 +289,7 @@ async function main(ctx) {
289
289
  const currentFilePaths = new Set(files.map((f) => f.relPath));
290
290
 
291
291
  // 3. Load Scannable DataSources from preset chain
292
- const chains = resolveMultiChains(types);
292
+ const chains = resolveMultiChains(types, root);
293
293
  const seenDirs = new Set();
294
294
 
295
295
  let dataSources = new Map();
@@ -301,9 +301,6 @@ async function main(ctx) {
301
301
  }
302
302
  }
303
303
 
304
- const projectDataDir = sddDataDir(root);
305
- dataSources = await loadScanSources(projectDataDir, dataSources);
306
-
307
304
  // 3b. DataSource hash detection: clear entry hashes for categories whose DataSource changed
308
305
  if (existing) {
309
306
  for (const [name, source] of dataSources) {
@@ -489,7 +489,7 @@ export async function textFillFromAnalysis(root, analysis, commandId, srcRoot, o
489
489
  const docsDir = path.join(root, "docs");
490
490
  const resolvedSrcRoot = srcRoot || root;
491
491
 
492
- const targetFiles = opts?.files || getChapterFiles(docsDir, { type, configChapters: cfg.chapters });
492
+ const targetFiles = opts?.files || getChapterFiles(docsDir, { type, configChapters: cfg.chapters, projectRoot: root });
493
493
 
494
494
  const changedFiles = [];
495
495
  let totalFilled = 0;
@@ -640,7 +640,7 @@ async function main(ctx) {
640
640
  if (ctx.files) {
641
641
  targetFiles = ctx.files;
642
642
  } else {
643
- targetFiles = getChapterFiles(docsDir, { type: ctx.type, configChapters: cfg.chapters });
643
+ targetFiles = getChapterFiles(docsDir, { type: ctx.type, configChapters: cfg.chapters, projectRoot: root });
644
644
 
645
645
  // Diff-based chapter filtering: skip chapters whose entries are unchanged
646
646
  if (!ctx.force) {
@@ -175,7 +175,7 @@ async function main(ctx) {
175
175
  throw new Error("docs/ directory not found. Run 'sdd-forge init' first.");
176
176
  }
177
177
 
178
- const sourceFiles = getChapterFiles(docsDir, { type: ctx.type, configChapters: ctx.config?.chapters });
178
+ const sourceFiles = getChapterFiles(docsDir, { type: ctx.type, configChapters: ctx.config?.chapters, projectRoot: root });
179
179
  const readmePath = path.join(root, "README.md");
180
180
  const hasReadme = fs.existsSync(readmePath);
181
181
 
@@ -170,7 +170,7 @@ export default class DocsSource extends DataSource {
170
170
  const docsDir = this._docsDir || path.join(this._root, "docs");
171
171
  if (!fs.existsSync(docsDir)) return null;
172
172
 
173
- const files = getChapterFiles(docsDir, { type: this._type, configChapters: this._configChapters });
173
+ const files = getChapterFiles(docsDir, { type: this._type, configChapters: this._configChapters, projectRoot: this._root });
174
174
 
175
175
  if (files.length === 0) return null;
176
176
 
@@ -240,7 +240,7 @@ export default class DocsSource extends DataSource {
240
240
  const docsDir = this._docsDir || path.join(this._root, "docs");
241
241
  if (!fs.existsSync(docsDir)) return null;
242
242
 
243
- const files = getChapterFiles(docsDir, { type: this._type, configChapters: this._configChapters });
243
+ const files = getChapterFiles(docsDir, { type: this._type, configChapters: this._configChapters, projectRoot: this._root });
244
244
  if (files.length <= 1) return null;
245
245
 
246
246
  // Find current file in the chapter list
@@ -128,6 +128,7 @@ export function loadFullAnalysis(root) {
128
128
  * @param {Object} [options]
129
129
  * @param {string} [options.type] - プロジェクトタイプ(例: "cli/node-cli")
130
130
  * @param {string[]} [options.configChapters] - config.json の chapters 配列(最優先)
131
+ * @param {string} [options.projectRoot] - プロジェクトルート(.sdd-forge/presets/ 検索用)
131
132
  * @returns {string[]} ファイル名の配列(順序付き)
132
133
  */
133
134
  export function getChapterFiles(docsDir, options) {
@@ -135,10 +136,11 @@ export function getChapterFiles(docsDir, options) {
135
136
 
136
137
  const type = options?.type;
137
138
  const configChapters = options?.configChapters;
139
+ const projectRoot = options?.projectRoot;
138
140
  const EXCLUDE = new Set(["README.md", "AGENTS.sdd.md", "layout.md"]);
139
141
 
140
142
  if (type || configChapters?.length) {
141
- const chapters = resolveChaptersOrder(type || "base", configChapters);
143
+ const chapters = resolveChaptersOrder(type || "base", configChapters, projectRoot);
142
144
  if (chapters.length > 0) {
143
145
  const existing = chapters.filter((f) => fs.existsSync(path.join(docsDir, f)));
144
146
  if (existing.length > 0) return existing;
@@ -6,9 +6,9 @@
6
6
 
7
7
  import fs from "fs";
8
8
  import path from "path";
9
- import { createLogger } from "../../lib/progress.js";
9
+ import { pathToFileURL } from "url";
10
10
 
11
- const logger = createLogger("datasource");
11
+ const MAX_DATA_SOURCE_FILES = 1000;
12
12
 
13
13
  /**
14
14
  * Load DataSource classes from a directory and instantiate them.
@@ -23,22 +23,34 @@ const logger = createLogger("datasource");
23
23
  export async function loadDataSources(dataDir, opts) {
24
24
  const { existing, onInstance } = opts || {};
25
25
  const sources = new Map(existing || []);
26
- if (!fs.existsSync(dataDir)) return sources;
27
-
28
- const files = fs.readdirSync(dataDir).filter((f) => f.endsWith(".js"));
26
+ let entries;
27
+ try {
28
+ entries = await fs.promises.readdir(dataDir);
29
+ } catch (err) {
30
+ if (err.code === "ENOENT") return sources;
31
+ throw err;
32
+ }
33
+ const files = entries.filter((f) => f.endsWith(".js"));
34
+ if (files.length > MAX_DATA_SOURCE_FILES) {
35
+ throw new Error(
36
+ `DataSource directory ${dataDir} contains ${files.length} files, exceeding limit ${MAX_DATA_SOURCE_FILES}`,
37
+ );
38
+ }
29
39
  for (const file of files) {
30
40
  const name = path.basename(file, ".js");
41
+ const filePath = path.join(dataDir, file);
42
+ let mod;
31
43
  try {
32
- const mod = await import(path.join(dataDir, file));
33
- const Source = mod.default;
34
- if (typeof Source === "function") {
35
- const instance = new Source();
36
- instance._sourceFilePath = path.join(dataDir, file);
37
- if (onInstance && onInstance(instance, name) === false) continue;
38
- sources.set(name, instance);
39
- }
44
+ mod = await import(pathToFileURL(filePath).href);
40
45
  } catch (err) {
41
- logger.verbose(`failed to load DataSource ${name}: ${err.message}`);
46
+ throw new Error(`failed to load DataSource at ${filePath}: ${err.message}`, { cause: err });
47
+ }
48
+ const Source = mod.default;
49
+ if (typeof Source === "function") {
50
+ const instance = new Source();
51
+ instance._sourceFilePath = filePath;
52
+ if (onInstance && onInstance(instance, name) === false) continue;
53
+ sources.set(name, instance);
42
54
  }
43
55
  }
44
56
  return sources;
@@ -8,7 +8,7 @@
8
8
 
9
9
  import fs from "fs";
10
10
  import path from "path";
11
- import { sddDir, sddDataDir } from "../../lib/config.js";
11
+ import { sddDir } from "../../lib/config.js";
12
12
  import { loadDataSources as loadDataSourcesBase } from "./data-source-loader.js";
13
13
  import { resolveMultiChains, resolveChainSafe } from "../../lib/presets.js";
14
14
  import { createLogger } from "../../lib/progress.js";
@@ -66,7 +66,7 @@ const COMMON_DATA_DIR = path.resolve(
66
66
  * @param {Object} ctx - 共有コンテキスト
67
67
  * @returns {Promise<Map<string, Object>>} DataSource マップ
68
68
  */
69
- async function loadChainDataSources(chain, root, ctx) {
69
+ async function loadChainDataSources(chain, ctx) {
70
70
  let dataSources = await loadDataSources(COMMON_DATA_DIR, ctx);
71
71
 
72
72
  for (const preset of chain) {
@@ -74,10 +74,6 @@ async function loadChainDataSources(chain, root, ctx) {
74
74
  dataSources = await loadDataSources(dataDir, ctx, dataSources);
75
75
  }
76
76
 
77
- // プロジェクト固有 DataSource(最高優先)
78
- const projectDataDir = sddDataDir(root);
79
- dataSources = await loadDataSources(projectDataDir, ctx, dataSources);
80
-
81
77
  return dataSources;
82
78
  }
83
79
 
@@ -92,11 +88,19 @@ async function loadChainDataSources(chain, root, ctx) {
92
88
  * @returns {Promise<{ resolve: (preset: string, source: string, method: string, analysis: Object, labels: string[]) => string|null }>}
93
89
  */
94
90
  export async function createResolver(type, root, opts) {
91
+ // Warn about deprecated .sdd-forge/data/ directory
92
+ if (root) {
93
+ const deprecatedDataDir = path.join(root, ".sdd-forge", "data");
94
+ if (fs.existsSync(deprecatedDataDir)) {
95
+ process.stderr.write(`[sdd-forge] WARN: .sdd-forge/data/ is deprecated. Move DataSources to .sdd-forge/presets/<type>/data/ instead.\n`);
96
+ }
97
+ }
98
+
95
99
  const desc = descFactory(root);
96
100
  const loadOverrides = () => loadOverridesFor(root);
97
101
  const ctx = { desc, loadOverrides, root, docsDir: opts?.docsDir, type, configChapters: opts?.configChapters };
98
102
 
99
- const chains = resolveMultiChains(type);
103
+ const chains = resolveMultiChains(type, root);
100
104
 
101
105
  // 各チェーンの leaf key → DataSource マップ
102
106
  const resolverMap = new Map();
@@ -104,7 +108,7 @@ export async function createResolver(type, root, opts) {
104
108
  const ancestorMap = new Map();
105
109
  for (const chain of chains) {
106
110
  const leafKey = chain[chain.length - 1].key;
107
- const dataSources = await loadChainDataSources(chain, root, ctx);
111
+ const dataSources = await loadChainDataSources(chain, ctx);
108
112
  resolverMap.set(leafKey, dataSources);
109
113
  // chain 内の全プリセットを ancestor マップに登録
110
114
  for (const p of chain) {
@@ -28,9 +28,10 @@ const SPECIAL_FILES = new Set(["README.md", "AGENTS.sdd.md", "layout.md"]);
28
28
  * @param {string} presetKey - preset 名(例: "cakephp2", "node-cli")
29
29
  * @param {string} lang - ロケール(例: "ja")
30
30
  * @param {string|null} [projectLocalDir] - プロジェクトローカルテンプレートディレクトリ
31
+ * @param {string} [projectRoot] - プロジェクトルート(.sdd-forge/presets/ 検索用)
31
32
  * @returns {string[]} レイヤーディレクトリ配列(優先度高い順)
32
33
  */
33
- export function buildLayers(presetKey, lang, projectLocalDir) {
34
+ export function buildLayers(presetKey, lang, projectLocalDir, projectRoot) {
34
35
  const layers = [];
35
36
 
36
37
  // 1. project-local(最高優先)
@@ -39,7 +40,7 @@ export function buildLayers(presetKey, lang, projectLocalDir) {
39
40
  }
40
41
 
41
42
  // 2. parent チェーンを leaf → root の順で追加(優先度高い順)
42
- const chain = resolveChainSafe(presetKey);
43
+ const chain = resolveChainSafe(presetKey, projectRoot);
43
44
 
44
45
  // chain は root → leaf の順なので、逆順(leaf → root)で追加
45
46
  for (let i = chain.length - 1; i >= 0; i--) {
@@ -195,20 +196,21 @@ function mergeSourcesAdditive(sources) {
195
196
  * @param {string|null} [opts.projectLocalDir] - プロジェクトローカルテンプレートディレクトリ
196
197
  * @param {string[]} [opts.fallbackLangs] - フォールバック言語リスト
197
198
  * @param {string[]} [opts.chaptersOrder] - preset.json の chapters 順序配列
199
+ * @param {string} [opts.projectRoot] - プロジェクトルート(.sdd-forge/presets/ 検索用)
198
200
  * @returns {FileResolution[]}
199
201
  */
200
202
  export function resolveTemplates(typePath, lang, opts = {}) {
201
- const { projectLocalDir, fallbackLangs, chaptersOrder } = opts;
203
+ const { projectLocalDir, fallbackLangs, chaptersOrder, projectRoot } = opts;
202
204
 
203
205
  const types = Array.isArray(typePath) ? typePath : [typePath];
204
206
 
205
207
  // 各チェーンの leaf key ごとにレイヤーセットを構築
206
- const chains = resolveMultiChains(types);
208
+ const chains = resolveMultiChains(types, projectRoot);
207
209
  const chainLayerSets = chains.map((chain) => {
208
210
  const leafKey = chain[chain.length - 1].key;
209
211
  return {
210
212
  leafKey,
211
- layers: buildLayers(leafKey, lang, projectLocalDir),
213
+ layers: buildLayers(leafKey, lang, projectLocalDir, projectRoot),
212
214
  fallbackSets: (fallbackLangs || [])
213
215
  .filter((l) => l !== lang)
214
216
  .map((fbLang) => ({
@@ -217,6 +219,7 @@ export function resolveTemplates(typePath, lang, opts = {}) {
217
219
  leafKey,
218
220
  fbLang,
219
221
  deriveFallbackProjectLocalDir(projectLocalDir, fbLang),
222
+ projectRoot,
220
223
  ),
221
224
  })),
222
225
  };
@@ -359,9 +362,10 @@ function discoverFileNames(layers, fallbackSets, chaptersOrder) {
359
362
  *
360
363
  * @param {string|string[]} presetKeys - preset 名または配列
361
364
  * @param {string[]} [configChapters] - config.json の chapters 配列(最優先)
365
+ * @param {string} [projectRoot] - プロジェクトルート(.sdd-forge/presets/ 検索用)
362
366
  * @returns {string[]} 章ファイル名の順序配列
363
367
  */
364
- export function resolveChaptersOrder(presetKeys, configChapters) {
368
+ export function resolveChaptersOrder(presetKeys, configChapters, projectRoot) {
365
369
  // config.json の chapters が定義されていればプリセットを完全上書き
366
370
  if (configChapters?.length) {
367
371
  // Support both old string[] and new object[] formats
@@ -377,7 +381,7 @@ export function resolveChaptersOrder(presetKeys, configChapters) {
377
381
  const result = [];
378
382
 
379
383
  for (const key of keys) {
380
- const chain = resolveChainSafe(key);
384
+ const chain = resolveChainSafe(key, projectRoot);
381
385
 
382
386
  // chain 内で最も具体的な(leaf 側の)chapters を使用する。
383
387
  // 子が chapters を定義していれば親の chapters は含めない(上書き)。
@@ -9,7 +9,7 @@ import { readFileSync } from "fs";
9
9
  import { runCmd, assertOk } from "../../lib/process.js";
10
10
  import path from "path";
11
11
  import { loadConfig } from "../../lib/config.js";
12
- import { isGhAvailable } from "../../lib/git-helpers.js";
12
+ import { isGhAvailable, runGit } from "../../lib/git-helpers.js";
13
13
 
14
14
  /**
15
15
  * Resolve push remote from config.
@@ -165,7 +165,7 @@ function main(ctx) {
165
165
  const title = buildPrTitle(spec, fallbackTitle);
166
166
  const body = buildPrBody(state, spec);
167
167
 
168
- const pushRes = runCmd("git", ["push", "-u", remote, featureBranch]);
168
+ const pushRes = runGit(["push", "-u", remote, featureBranch]);
169
169
  assertOk(pushRes, "git push failed");
170
170
  const prRes = runCmd("gh", [
171
171
  "pr", "create",
@@ -185,12 +185,12 @@ function main(ctx) {
185
185
  function runSquashMerge(gitPrefix, hint) {
186
186
  const mergeArgs = [...gitPrefix, "merge", "--squash", featureBranch];
187
187
  const resetArgs = [...gitPrefix, "reset", "--merge"];
188
- const mergeRes = runCmd("git", mergeArgs);
188
+ const mergeRes = runGit(mergeArgs);
189
189
  if (!mergeRes.ok) {
190
- runCmd("git", resetArgs);
190
+ runGit(resetArgs);
191
191
  throw new Error(`Merge conflict detected. ${hint}`);
192
192
  }
193
- const commitRes = runCmd("git", [...gitPrefix, "commit", "-m", commitMsg]);
193
+ const commitRes = runGit([...gitPrefix, "commit", "-m", commitMsg]);
194
194
  assertOk(commitRes, "commit after squash merge failed");
195
195
  }
196
196
 
@@ -200,7 +200,7 @@ function main(ctx) {
200
200
  }
201
201
 
202
202
  // Branch mode
203
- const checkoutRes = runCmd("git", ["checkout", baseBranch]);
203
+ const checkoutRes = runGit(["checkout", baseBranch]);
204
204
  assertOk(checkoutRes, "git checkout failed");
205
205
  runSquashMerge([], `Run 'git rebase ${baseBranch}' and retry finalize.`);
206
206
  return { strategy: "squash" };