dflow-sdd-ddd 0.12.0 → 0.14.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 (42) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/CONTRIBUTING.md +6 -9
  3. package/README.en.md +117 -40
  4. package/README.md +47 -15
  5. package/TEMPLATE-COVERAGE.md +3 -2
  6. package/bin/dflow.js +80 -3
  7. package/docs/evaluating-dflow.en.md +21 -2
  8. package/docs/evaluating-dflow.md +17 -3
  9. package/docs/npm-publish-checklist.md +3 -1
  10. package/docs/release-versioning-policy.md +3 -2
  11. package/docs/using-with-claude-code.en.md +35 -22
  12. package/docs/using-with-claude-code.md +27 -17
  13. package/docs/using-with-codex.en.md +20 -10
  14. package/docs/using-with-codex.md +13 -8
  15. package/docs/using-with-github-copilot.en.md +20 -9
  16. package/docs/using-with-github-copilot.md +14 -7
  17. package/lib/doctor-checks.js +178 -0
  18. package/lib/init.js +894 -36
  19. package/lib/render.js +1263 -0
  20. package/package.json +5 -2
  21. package/templates/brownfield/references/init-project-flow.md +46 -2
  22. package/templates/brownfield/scaffolding/AI-AGENT-GUIDE.md +13 -1
  23. package/templates/brownfield/templates/_index.md +2 -0
  24. package/templates/brownfield/templates/context-definition.md +2 -0
  25. package/templates/brownfield/templates/context-map.md +1 -0
  26. package/templates/brownfield/templates/glossary.md +1 -0
  27. package/templates/brownfield/templates/models.md +1 -0
  28. package/templates/brownfield/templates/phase-spec.md +2 -0
  29. package/templates/brownfield/templates/rules.md +1 -0
  30. package/templates/brownfield/templates/tech-debt.md +1 -0
  31. package/templates/greenfield/references/init-project-flow.md +48 -6
  32. package/templates/greenfield/scaffolding/AI-AGENT-GUIDE.md +13 -1
  33. package/templates/greenfield/templates/_index.md +2 -0
  34. package/templates/greenfield/templates/aggregate-design.md +2 -0
  35. package/templates/greenfield/templates/context-definition.md +2 -0
  36. package/templates/greenfield/templates/context-map.md +1 -0
  37. package/templates/greenfield/templates/events.md +1 -0
  38. package/templates/greenfield/templates/glossary.md +1 -0
  39. package/templates/greenfield/templates/models.md +1 -0
  40. package/templates/greenfield/templates/phase-spec.md +2 -0
  41. package/templates/greenfield/templates/rules.md +1 -0
  42. package/templates/greenfield/templates/tech-debt.md +1 -0
@@ -137,11 +137,14 @@ dflow configure-agents --command-adapters
137
137
  **連字號** `/dflow-<id>`,不是 canonical 的**冒號** `/dflow:<id>`——後者是
138
138
  Claude / Codex 的命令寫法,在 Copilot 只能當文字稱呼、不能當命令輸入。
139
139
 
140
- ### `--skills` flag 與 Copilot 的 skill 觸發
140
+ ### Copilot 的 skill 觸發(init 預設安裝)
141
141
 
142
- `dflow configure-agents --skills` 會為 **Claude Code、Codex 與 GitHub Copilot**
142
+ Dflow 會為 **Claude Code、Codex 與 GitHub Copilot**
143
143
  各自投影同一份工具中立的 thin skill 到它們的 project-level skill 路徑;Copilot 的是
144
- `.github/skills/dflow/SKILL.md`。實測(2026-06-05)確認 Copilot 會從**自己原生的
144
+ `.github/skills/dflow/SKILL.md`。這份 skill 現在**預設安裝**:`dflow init` 有選
145
+ Copilot 就裝(互動問一題預設 Y、非互動直接裝)、`dflow configure-agents` 對新選且
146
+ 尚無 skill 的 Copilot 也補問;`dflow configure-agents --skills` 用於補裝或強制
147
+ 重生成。實測(2026-06-05)確認 Copilot 會從**自己原生的
145
148
  `.github/skills/`** 探索並運作(即使移除 `.claude`/`.agents` 的跨讀路徑也成立),
146
149
  觸發方式依介面而異——**VS Code Chat 自然語言自動觸發**、**Copilot CLI 需打 `/dflow`
147
150
  手動喚起**(細節見上方介面 A / B)。
@@ -175,8 +178,10 @@ git rm --cached .github/prompts/dflow-*.prompt.md
175
178
  (`--cached` 只移出版控、保留工作目錄檔案。)
176
179
 
177
180
  升級 dflow 後重跑 `dflow configure-agents --command-adapters`,prompt adapter 會用**新版 registry**
178
- 重投影;但既有 `dflow/specs/shared/AI-AGENT-GUIDE.md` **不會**被覆寫——「重投影 adapter」不等於
179
- 「升級 canonical guide」。請以**相同的 dflow CLI 版本**重投影。
181
+ 重投影;`dflow/specs/shared/AI-AGENT-GUIDE.md` 內**帶 marker 的 canonical 區**也會原地刷新
182
+ (marker 以外——含 `## Project Context`——保留不動)。較舊專案的 guide 還沒有 marker 時,
183
+ 互動執行會詢問是否採用(預設 N)、非互動則跳過並警告。請以**相同的 dflow CLI 版本**重投影,
184
+ 升級後先跑 `dflow doctor` 檢視殘餘漂移(read-only)。
180
185
 
181
186
  ### 對話範例
182
187
 
@@ -206,7 +211,9 @@ Copilot: Got it. I'll create the feature spec at dflow/specs/features/active/
206
211
 
207
212
  如果你的專案中已有 `.github/copilot-instructions.md`,`init` 不會覆蓋自訂內容。
208
213
  已是 Dflow-generated shim 的檔案會原地刷新;其他已指向
209
- `dflow/specs/shared/AI-AGENT-GUIDE.md` 的檔案會略過。若既有檔案尚未指向 guide,
214
+ `dflow/specs/shared/AI-AGENT-GUIDE.md` 的檔案會略過並警告——之後在互動終端跑
215
+ `dflow configure-agents` 會詢問是否附加帶 marker 的管理區塊(預設 N),非互動
216
+ 執行維持略過並警告。若既有檔案尚未指向 guide,
210
217
  Dflow 會在確認 preview 顯示並於檔案末尾附加帶有
211
218
  `<!-- dflow-generated: agent-shim START/END -->` markers 的 Dflow block;重跑會
212
219
  原地更新同一段,不會重複。這樣可以避免破壞你已有的自訂 Copilot 指示。若你刪除
@@ -287,7 +294,7 @@ canonical 指南(`dflow/specs/shared/AI-AGENT-GUIDE.md`)在各工具之間
287
294
 
288
295
  如果你已執行 `init` 且想查看端到端的 workflow 範例:
289
296
 
290
- - 閱讀 [`dflow/specs/shared/AI-AGENT-GUIDE.md`](../sdd-ddd-greenfield-skill/scaffolding/AI-AGENT-GUIDE.md)
297
+ - 閱讀 [`dflow/specs/shared/AI-AGENT-GUIDE.md`](../templates/greenfield/scaffolding/AI-AGENT-GUIDE.md)
291
298
  (或 brownfield 等效版本)後再開始 workflow。
292
299
  - 閱讀 [`tutorial/01-greenfield/`](../tutorial/01-greenfield/walkthrough-00-setup.md),
293
300
  其中有展示 Copilot chat flow 的對話範例。
@@ -0,0 +1,178 @@
1
+ // PROPOSAL-058: pure, shippable drift-detection helpers for `dflow doctor`.
2
+ //
3
+ // The dev-only cross-ref resolver (scripts/check-cross-refs.mjs, PROPOSAL-055)
4
+ // is not part of the npm package, so the runtime checks reimplement the narrow
5
+ // subset doctor needs: fence-aware heading extraction, "<file> § Heading"
6
+ // reference extraction with soft-wrap joining, tolerant heading matching, and
7
+ // template-shape comparison. Everything here is I/O-free — callers read the
8
+ // files and pass contents — which keeps the checks unit-testable.
9
+
10
+ 'use strict';
11
+
12
+ // Machine-readable context lines. Shared with lib/init.js inference so the
13
+ // doctor "machine format" checks can never drift from what inference actually
14
+ // parses: inferGitPolicy / inferAiCommitMarker / inferProseLanguage read
15
+ // _conventions.md; inferTechStackSummary / inferMigrationContext read the
16
+ // `| Tech stack |` / `| Migration / legacy context |` rows of the guide's
17
+ // "## Project Context" table (PROPOSAL-076 — no packaged _overview.md template
18
+ // ever carried those rows).
19
+ const GIT_POLICY_LINE_RE = /Selected Git policy:\s*`([^`]+)`/;
20
+ const AI_COMMIT_MARKER_LINE_RE = /AI commit marker:\s*`([^`]+)`/;
21
+ const PROSE_LANGUAGE_LINE_RE = /Project prose language:\s*`([^`]+)`/;
22
+ // The row values accept Markdown-escaped pipes (`\|`) so a cell like
23
+ // `Node \| Express` is captured whole; parseContextLine unescapes them
24
+ // (PROPOSAL-076 gate G1 — a bare `[^|\n]` capture silently truncated at the
25
+ // escaped pipe while doctor still called the row machine-readable).
26
+ const TECH_STACK_ROW_RE = /\|\s*Tech stack\s*\|\s*((?:\\\||[^|\n])+?)\s*\|/i;
27
+ const MIGRATION_CONTEXT_ROW_RE = /\|\s*Migration \/ legacy context\s*\|\s*((?:\\\||[^|\n])+?)\s*\|/i;
28
+
29
+ const GIT_POLICY_VALUES = new Set(['gitflow', 'trunk']);
30
+ const AI_COMMIT_MARKER_VALUES = new Set(['none', 'co-authored-by', 'prefix']);
31
+
32
+ // Trimmed capture of a machine-readable context line, or null when the line is
33
+ // absent or its value is whitespace-only. Inference and the doctor checks must
34
+ // both parse through this helper: a value doctor accepts has to be the exact
35
+ // value configure-agents consumes (e.g. a stray space inside the backticks
36
+ // would otherwise pass doctor's trimmed validation yet miss strict comparisons
37
+ // like buildSubstitutionMap's `gitflow` check downstream).
38
+ function parseContextLine(content, re) {
39
+ const match = content.match(re);
40
+ if (!match) return null;
41
+ // Unescape Markdown-escaped pipes captured by the table-row patterns; the
42
+ // backtick context lines never contain `\|`, so this is a no-op for them.
43
+ const value = match[1].replace(/\\\|/g, '|').trim();
44
+ return value === '' ? null : value;
45
+ }
46
+
47
+ // Blank out fenced code blocks (``` / ~~~) line-by-line so headings and § refs
48
+ // inside examples are never extracted. Line positions are preserved. CommonMark
49
+ // close rules (PROPOSAL-076 gates G4/G6): a block closes only on the same fence
50
+ // character repeated at least the opening length, indented at most three
51
+ // spaces, with nothing but whitespace after — so a three-backtick line inside a
52
+ // four-backtick example, or an info-string line like ```js inside an open
53
+ // fence, is content and does not end the block early.
54
+ function blankFencedBlocks(content) {
55
+ const lines = content.split(/\r?\n/);
56
+ const out = [];
57
+ let fenceChar = null;
58
+ let fenceLen = 0;
59
+ for (const line of lines) {
60
+ if (fenceChar) {
61
+ out.push('');
62
+ const close = line.match(/^[ \t]{0,3}(```+|~~~+)[ \t]*$/);
63
+ if (close && close[1][0] === fenceChar && close[1].length >= fenceLen) {
64
+ fenceChar = null;
65
+ fenceLen = 0;
66
+ }
67
+ continue;
68
+ }
69
+ const open = line.match(/^[ \t]{0,3}(```+|~~~+)/);
70
+ if (open) {
71
+ fenceChar = open[1][0];
72
+ fenceLen = open[1].length;
73
+ out.push('');
74
+ continue;
75
+ }
76
+ out.push(line);
77
+ }
78
+ return out;
79
+ }
80
+
81
+ // All Markdown heading texts (any level), fence-aware.
82
+ function extractHeadings(content) {
83
+ const headings = [];
84
+ for (const line of blankFencedBlocks(content)) {
85
+ const m = line.match(/^#{1,6}\s+(.+?)\s*#*\s*$/);
86
+ if (m) headings.push(m[1].trim());
87
+ }
88
+ return headings;
89
+ }
90
+
91
+ // H2 ("## ") heading texts only, fence-aware — the section shape of a template.
92
+ function extractH2Headings(content) {
93
+ const headings = [];
94
+ for (const line of blankFencedBlocks(content)) {
95
+ const m = line.match(/^##\s+(.+?)\s*#*\s*$/);
96
+ if (m) headings.push(m[1].trim());
97
+ }
98
+ return headings;
99
+ }
100
+
101
+ // `<file>.md § Heading` references whose file basename is `targetBasename`.
102
+ // A soft-wrapped heading name is captured by joining the next line; a match
103
+ // anchored beyond the current line is left to that line's own iteration.
104
+ function extractSectionRefs(content, targetBasename) {
105
+ const lines = blankFencedBlocks(content);
106
+ const refs = [];
107
+ lines.forEach((line, i) => {
108
+ const firstLen = line.replace(/\s+$/, '').length;
109
+ const joined = line.replace(/\s+$/, '') + ' ' + (lines[i + 1] || '').replace(/^\s+/, '');
110
+ for (const m of joined.matchAll(/`?([A-Za-z0-9._/-]+\.md)`?\s*§\s*"?([^."`)\n]+)/g)) {
111
+ if (m.index > firstLen) continue;
112
+ const base = m[1].split('/').pop();
113
+ if (base !== targetBasename) continue;
114
+ refs.push({ line: i + 1, headingText: m[2].trim() });
115
+ }
116
+ });
117
+ return refs;
118
+ }
119
+
120
+ function normalizeHeading(text) {
121
+ return text.replace(/[`*_]/g, '').trim();
122
+ }
123
+
124
+ // Tolerant heading resolution: a reference resolves when it prefix-matches a
125
+ // real heading in either direction (covers soft wraps like "§ Workflow" for
126
+ // "Workflow Transparency" and shorthand references).
127
+ function headingResolves(referenceText, headings) {
128
+ const want = normalizeHeading(referenceText);
129
+ if (!want) return true;
130
+ return headings.some((heading) => {
131
+ const have = normalizeHeading(heading);
132
+ return have.startsWith(want) || want.startsWith(have);
133
+ });
134
+ }
135
+
136
+ // Which of the template's H2 sections are missing from a filled document — the
137
+ // "created from an older template shape" signal.
138
+ function missingTemplateSections(templateContent, documentContent) {
139
+ const have = new Set(extractH2Headings(documentContent).map(normalizeHeading));
140
+ return extractH2Headings(templateContent).filter((heading) => !have.has(normalizeHeading(heading)));
141
+ }
142
+
143
+ // True when `content` matches `templateContent` up to placeholder substitution:
144
+ // every single-line `{...}` placeholder in the template may match any text.
145
+ // Distinguishes "pristine current starter" from "edited or older starter"
146
+ // without knowing the values init substituted.
147
+ function matchesTemplateWithPlaceholders(content, templateContent) {
148
+ const normalize = (s) => s.replace(/\r\n/g, '\n').replace(/\s+$/, '');
149
+ const parts = normalize(templateContent).split(/\{[^}\n]*\}/);
150
+ const pattern = `^${parts.map(escapeRegExp).join('[\\s\\S]*?')}$`;
151
+ try {
152
+ return new RegExp(pattern).test(normalize(content));
153
+ } catch {
154
+ return false;
155
+ }
156
+ }
157
+
158
+ function escapeRegExp(s) {
159
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
160
+ }
161
+
162
+ module.exports = {
163
+ GIT_POLICY_LINE_RE,
164
+ AI_COMMIT_MARKER_LINE_RE,
165
+ PROSE_LANGUAGE_LINE_RE,
166
+ TECH_STACK_ROW_RE,
167
+ MIGRATION_CONTEXT_ROW_RE,
168
+ GIT_POLICY_VALUES,
169
+ AI_COMMIT_MARKER_VALUES,
170
+ parseContextLine,
171
+ blankFencedBlocks,
172
+ extractHeadings,
173
+ extractH2Headings,
174
+ extractSectionRefs,
175
+ headingResolves,
176
+ missingTemplateSections,
177
+ matchesTemplateWithPlaceholders
178
+ };