dflow-sdd-ddd 0.10.0 → 0.12.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 +79 -0
- package/README.en.md +57 -43
- package/README.md +36 -33
- package/TEMPLATE-LANGUAGE-GLOSSARY.md +1 -0
- package/bin/dflow.js +4 -8
- package/docs/why-dflow.en.md +72 -0
- package/docs/why-dflow.md +72 -0
- package/lib/init.js +114 -77
- package/package.json +2 -2
- package/templates/brownfield/references/drift-verification.md +40 -6
- package/templates/brownfield/references/finish-feature-flow.md +85 -29
- package/templates/brownfield/references/git-integration.md +29 -9
- package/templates/brownfield/references/modify-existing-flow.md +61 -0
- package/templates/brownfield/references/new-feature-flow.md +62 -1
- package/templates/brownfield/references/new-phase-flow.md +19 -1
- package/templates/brownfield/references/pr-review-checklist.md +7 -1
- package/templates/brownfield/scaffolding/AI-AGENT-GUIDE.md +46 -33
- package/templates/brownfield/scaffolding/Git-principles-gitflow.md +1 -1
- package/templates/brownfield/scaffolding/Git-principles-trunk.md +2 -2
- package/templates/brownfield/templates/_index.md +23 -4
- package/templates/brownfield/templates/context-map.md +12 -4
- package/templates/brownfield/templates/lightweight-spec.md +3 -3
- package/templates/brownfield/templates/phase-spec.md +3 -3
- package/templates/common/references/ddd-modeling-guide.md +837 -0
- package/templates/greenfield/references/drift-verification.md +60 -12
- package/templates/greenfield/references/finish-feature-flow.md +86 -29
- package/templates/greenfield/references/git-integration.md +29 -9
- package/templates/greenfield/references/modify-existing-flow.md +23 -0
- package/templates/greenfield/references/new-feature-flow.md +70 -8
- package/templates/greenfield/references/new-phase-flow.md +15 -1
- package/templates/greenfield/references/pr-review-checklist.md +9 -1
- package/templates/greenfield/scaffolding/AI-AGENT-GUIDE.md +40 -33
- package/templates/greenfield/scaffolding/Git-principles-gitflow.md +1 -1
- package/templates/greenfield/scaffolding/Git-principles-trunk.md +4 -2
- package/templates/greenfield/templates/_index.md +23 -4
- package/templates/greenfield/templates/aggregate-design.md +8 -1
- package/templates/greenfield/templates/context-map.md +13 -4
- package/templates/greenfield/templates/events.md +5 -1
- package/templates/greenfield/templates/lightweight-spec.md +3 -3
- package/templates/greenfield/templates/phase-spec.md +3 -3
- package/docs/migrating-to-dflow-v1.md +0 -234
- package/templates/greenfield/references/ddd-modeling-guide.md +0 -351
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# 為什麼用 Dflow(即使 AI 已經會 DDD)
|
|
2
|
+
|
|
3
|
+
> **繁體中文** | [English](why-dflow.en.md)
|
|
4
|
+
|
|
5
|
+
如果你的直覺是「現在的 AI 已經會 DDD,叫它『用 DDD 建一個 feature』,aggregate、value object、event 都出得來,再加一層 spec-first 工具是不是過度工程?」——這份文件是寫給你的。它不打算說服你,而是把 Dflow 的價值、證據與限制攤平,讓你自己判斷。
|
|
6
|
+
|
|
7
|
+
## Dflow 是什麼
|
|
8
|
+
|
|
9
|
+
Dflow 不是教 AI 什麼是 DDD——它是一層 scaffold(鷹架):強迫 AI 把每個設計決策的取捨完整留檔,並補上「AI 自己補細節時容易漏、而 review 又難一眼看出」的盲區。
|
|
10
|
+
|
|
11
|
+
把「AI 會 DDD」拆成兩件事,就懂為什麼還需要它:
|
|
12
|
+
|
|
13
|
+
1. **AI 能不能說出對的 DDD 答案?** 能。模型讀過教科書,aggregate 邊界、不變式、ubiquitous language 都答得出來。
|
|
14
|
+
2. **AI 在你 review 時會不會留下夠完整的紀錄(它考慮過哪些、為何這樣選、哪裡還沒確定)讓你 audit、會不會主動 catch 它自己容易漏的陷阱?** 不一定,要看你怎麼問。
|
|
15
|
+
|
|
16
|
+
所以真正該比的不是「AI 工具 vs process」,而是:
|
|
17
|
+
|
|
18
|
+
- **AI alone** 的產出 = AI 知識 × 隱含的 prompt 結構 × 你 review 它的能力
|
|
19
|
+
- **AI + Dflow** 的產出 = AI 知識 × **明確的 elicitation scaffold** × **可審查的決策紀錄(取捨、否決的方案、open questions)** × 你 review 它的能力
|
|
20
|
+
|
|
21
|
+
差異不是「更聰明的 AI」,是「**更可審查的 AI**」。
|
|
22
|
+
|
|
23
|
+
## Dflow 的 DDD 引導是「長出來的」,不是抄教科書
|
|
24
|
+
|
|
25
|
+
Dflow 的價值有一部分在於:它的引導是從真實盲區回灌的,而且補上之後,模型真的會在後續主動沿用。一個具體、可檢查的例子——
|
|
26
|
+
|
|
27
|
+
**盲區**:模型自己建模時,把「一個充電槍同時只能有一筆進行中 session」這條唯一性規則,只用 aggregate 內 `if Status == InUse throw` 的 in-memory check 保護。DDD 教科書角度這是對的,但並發下兩個請求各自讀到 `Available`、各自通過檢查、各自 save → 不變式被破壞(modeling-correct、production-broken)。
|
|
28
|
+
|
|
29
|
+
**回灌**:把這個盲區寫成一段引導,補進 Dflow 的 `ddd-modeling-guide.md`——「Set-Based / Uniqueness Invariants」:這類「同 X 只能有一筆 active」的規則,無論怎麼切 aggregate,in-memory check 在並發下永遠不夠,要加 DB unique / partial index 或 concurrency token,並把衝突 translate 成 HTTP 409。
|
|
30
|
+
|
|
31
|
+
**沿用**:換一個 domain(冷鏈感測器「一個 sensor 同時最多掛在一個 carton」,結構平行)、且讓模型不知道自己在被測,它就**主動引用那段**、補上完整三層保護(in-memory guard + concurrency token + DB partial unique index + 409)。
|
|
32
|
+
|
|
33
|
+
這證明的是一件具體的事:**Dflow 把「AI 自己會漏的盲區」固化成可重用、會被遵循的引導。** 這不是「跑幾次就證明 AI+process 全面勝出」那種大結論(樣本很小);它是 Dflow 引導迴路有效的證據——**盲區 → 補引導 → 模型沿用**。你也能自己複現(見文末)。
|
|
34
|
+
|
|
35
|
+
> 補充誠實度:兩次 run 之間 domain 與 framing 也不同,但都不利於「不是引導的功勞」這個反方——兩個 domain 在模型的 pre-training 裡都不是高頻並發設計題材;framing 在第二次更純(不知被測),若它是主因,結果該更差而非更好。把這兩條的可能性壓低後,最能解釋這個翻轉的就剩那段引導在不在。
|
|
36
|
+
|
|
37
|
+
## 其他幾個「Dflow 強制留檔、AI 自己容易漏」
|
|
38
|
+
|
|
39
|
+
同一輪觀察裡還看到(每點都是「Dflow 做了什麼 → 沒它會怎樣」):
|
|
40
|
+
|
|
41
|
+
- **強迫寫否決理由**:aggregate 設計模板一句 prompt,誘出「選這個邊界 + 理由 + 考慮過哪些替代 + 為何否決」的完整決策段;AI 自己通常只給你一個方案,review 時你無法 audit「它想過 X 嗎」。
|
|
42
|
+
- **Step gate 把決策變成 reviewable moment**:模型在命名、模型 spike 等節點自然停下等確認;AI 自己一路寫到 code、tests 都好了你才有機會 review,這時 aggregate 邊界已經沒有商量空間。
|
|
43
|
+
- **Open Question 留檔給 domain expert**:模型把不確定的點列成 OQ 等人答,而不是「不確定就猜一個合理的」把假設藏進 code。
|
|
44
|
+
- **Ubiquitous language 不漂移**:術語表 + code mapping 讓 spec / model / code 用同一組名詞;AI 自己一段話內就能混用 Sensor / Device / Tracker。
|
|
45
|
+
- **規則可被查詢**:每條 business rule 有 ID、status、所屬 aggregate、behavior 連結;AI 自己把規則散在 prose 裡,「BR-003 影響哪些測試」只能用 grep 推敲。
|
|
46
|
+
|
|
47
|
+
## 誠實的取捨
|
|
48
|
+
|
|
49
|
+
不掩蓋限制,反而是這套論點的可信來源:
|
|
50
|
+
|
|
51
|
+
- **scope**:目前觀察只跑在單一模型 × 幾個中等複雜度 domain × lightweight modeling scope(到領域建模、不含 implementation phase)。implementation 階段的引導是否同樣有效、換不同模型會不會一樣,**未驗**。
|
|
52
|
+
- **先驗**:被測模型本來就對 DDD 有 pre-training 先驗。Dflow 證明的是「能讓『會、但不一定每次仔細想』變成『仔細想』」,**不是**「能讓完全不懂 DDD 的 AI 變會」。
|
|
53
|
+
- **採用即承諾遵循**:Dflow 是 spec-first 工具,只在被遵循時有效;AI 或人故意走偏的情境不在宣稱範圍內。這是工具屬性,不是 bug。
|
|
54
|
+
|
|
55
|
+
對需要 audit 的場景——醫療、金融、合規、安全敏感、或任何「上線出包代價高 / 個人責任重」的領域——這個 reviewability 差異是 deal-breaker。成本要分兩塊看:**產出** DDD 文件已經不是徒手做 DDD 的高人力年代——AI 幫你生規格、決策紀錄與領域模型,邊際成本主要是多花一些 token 與走一遍流程;而且光是「生成時被領域模型約束」就已讓產出更穩(如上面那個並發盲區),這層就算你沒深讀紀錄也拿得到。Dflow 的 DDD 也刻意務實裁剪(不是學院派全套)、適合一般公司的中型系統,採用門檻不高(不需要團隊先是 DDD 專家)。但要再兌現「可審查」這份價值,得有人實際去 review 那份紀錄——那才是人力與紀律的關鍵成本。所以取捨仍在、值得討論:高風險、需 audit、要長期維護時,這筆投資明顯划算;你不會去 review、失敗成本低、迭代又快時,AI alone 仍可能是更實際的選擇,不是 Dflow 一定贏。
|
|
56
|
+
|
|
57
|
+
## 自己驗證
|
|
58
|
+
|
|
59
|
+
不用相信任何說法,自己跑一次(約 10–30 分鐘):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm install -g dflow-sdd-ddd
|
|
63
|
+
mkdir dflow-test && cd dflow-test
|
|
64
|
+
git init && git commit --allow-empty -m "init"
|
|
65
|
+
dflow init # 選 greenfield + 你的 AI 工具 + 你的 stack
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
接著在你的 AI coding agent 裡跑 `/dflow:new-feature`,指派一個含「跨實例唯一」型不變式的 feature,例如「同帳號同時最多一個 active session」。看模型走到領域建模時,會不會寫到「Set-Based / Uniqueness Invariants」段(Dflow 的 `ddd-modeling-guide.md`)、cite 它、並補上 DB unique / partial index + concurrency token + 409。注意:裝最新版能驗證的是「引導在場時模型確實會用它」這一半;「引導不在場時模型會漏」那一半是上面那段在加入引導之前建立的,不是你在最新版上能切換的——最新版一律含這段。
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
Dflow 不宣稱讓 AI 更聰明,它讓 AI 更可審查:規格優先、領域語義顯式化、把決策與否決理由留檔、在實作前約束、完成前驗證漂移(drift)。為什麼領域語義本身在 AI 時代更關鍵,見 [為什麼 AI 時代 DDD 更重要](why-ddd-for-ai.md)。
|
package/lib/init.js
CHANGED
|
@@ -26,6 +26,13 @@ const AGENT_SHIM_SECTION_END = '<!-- dflow-generated: agent-shim END -->';
|
|
|
26
26
|
const WORKFLOW_BUNDLE_DEST = 'dflow/specs/shared/dflow-workflows';
|
|
27
27
|
const WORKFLOW_BUNDLE_MANIFEST_PATH = `${WORKFLOW_BUNDLE_DEST}/.dflow-bundle-manifest.json`;
|
|
28
28
|
const COMMON_SKILL_SOURCE_REL = 'common/skill/SKILL.md';
|
|
29
|
+
// Files the common bundle tree (templates/common/) MUST provide. A missing
|
|
30
|
+
// common file is a broken package, NOT a retired bundle file: without this guard
|
|
31
|
+
// listBundleSourceFiles would return a smaller-but-"valid" set lacking the file,
|
|
32
|
+
// and configure-agents stale-removal would then DELETE the already-installed
|
|
33
|
+
// copy from the user's project (it diffs as "retired"). PROPOSAL-064 fresh-gate
|
|
34
|
+
// finding. Guarded before any stale cleanup / manifest write.
|
|
35
|
+
const REQUIRED_COMMON_BUNDLE_FILES = ['references/ddd-modeling-guide.md'];
|
|
29
36
|
const EXPECTED_COMMAND_IDS = [
|
|
30
37
|
'new-feature',
|
|
31
38
|
'modify-existing',
|
|
@@ -399,13 +406,6 @@ async function runPreflight(cwd) {
|
|
|
399
406
|
warnings.push('Found empty dflow/specs/. Continuing because no initialized files were found.');
|
|
400
407
|
}
|
|
401
408
|
|
|
402
|
-
const legacySpecsPath = path.join(cwd, 'specs');
|
|
403
|
-
if ((await pathExists(legacySpecsPath)) && (await containsInitializedContent(legacySpecsPath))) {
|
|
404
|
-
warnings.push(
|
|
405
|
-
'Detected legacy specs/. Dflow V1 will not migrate or modify it; new files will be created under dflow/specs/. See docs/migrating-to-dflow-v1.md for the manual migration checklist.'
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
409
|
await assertWritableProjectRoot(cwd);
|
|
410
410
|
|
|
411
411
|
if (compareVersions(process.versions.node, MIN_NODE_VERSION) < 0) {
|
|
@@ -1214,31 +1214,97 @@ async function finalizePlanItems(cwd, items) {
|
|
|
1214
1214
|
}
|
|
1215
1215
|
}
|
|
1216
1216
|
|
|
1217
|
+
// A workflow bundle file name must be unique across the common and edition
|
|
1218
|
+
// source trees, so the merged dest path / manifest entry never collide or
|
|
1219
|
+
// shadow each other. Pure (no I/O) so it is unit-testable on a synthetic list.
|
|
1220
|
+
function assertNoBundleCollision(files) {
|
|
1221
|
+
const seenBy = new Map();
|
|
1222
|
+
for (const f of files) {
|
|
1223
|
+
const prior = seenBy.get(f.sourceRel);
|
|
1224
|
+
if (prior && prior !== f.sourceRoot) {
|
|
1225
|
+
throw new InitError(
|
|
1226
|
+
`Internal error: workflow bundle file "${f.sourceRel}" exists in both templates/${prior}/ and templates/${f.sourceRoot}/. A bundle file name must be unique across the common and edition source trees.`
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
seenBy.set(f.sourceRel, f.sourceRoot);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// R3-02 (sharpened for the PROPOSAL-064 common merge): the EDITION tree itself
|
|
1234
|
+
// must contribute both the flow docs (references/) and the blank templates
|
|
1235
|
+
// (templates/) — the common tree must NOT mask a broken edition (a vanished
|
|
1236
|
+
// templates/{edition}/references/ would otherwise be hidden by common's
|
|
1237
|
+
// non-empty references/). Enforced in the scanner (not only the projector) so
|
|
1238
|
+
// BOTH callers are covered: the projector hard-fails, and doctor's existing
|
|
1239
|
+
// try/catch around listBundleSourceFiles degrades to skipping the orphan scan
|
|
1240
|
+
// rather than mis-reporting every projected flow file as orphaned. Pure (no
|
|
1241
|
+
// I/O) so it is unit-testable on a synthetic list.
|
|
1242
|
+
function assertEditionBundleComplete(files, edition) {
|
|
1243
|
+
const hasEditionRefs = files.some((f) => f.sourceRoot === edition && f.dir === 'references');
|
|
1244
|
+
const hasEditionTemplates = files.some((f) => f.sourceRoot === edition && f.dir === 'templates');
|
|
1245
|
+
if (!hasEditionRefs || !hasEditionTemplates) {
|
|
1246
|
+
throw new InitError(
|
|
1247
|
+
`Internal error: incomplete workflow bundle source for edition "${edition}" (expected files under both templates/${edition}/references/ and templates/${edition}/templates/). The installed dflow package looks incomplete.`
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// Companion to assertEditionBundleComplete for the common tree: every file the
|
|
1253
|
+
// common bundle MUST provide has to be present. A common file silently missing
|
|
1254
|
+
// (broken package / tarball) would otherwise slip through as a smaller-but-valid
|
|
1255
|
+
// set and, on re-projection, be DELETED from the user's project by stale-removal
|
|
1256
|
+
// (the manifest diff would classify the still-installed copy as "retired").
|
|
1257
|
+
// Hard-fail here, before stale cleanup / manifest write. Pure (no I/O) so it is
|
|
1258
|
+
// unit-testable on a synthetic list.
|
|
1259
|
+
function assertCommonBundleComplete(files) {
|
|
1260
|
+
const present = new Set(files.filter((f) => f.sourceRoot === 'common').map((f) => f.sourceRel));
|
|
1261
|
+
for (const required of REQUIRED_COMMON_BUNDLE_FILES) {
|
|
1262
|
+
if (!present.has(required)) {
|
|
1263
|
+
throw new InitError(
|
|
1264
|
+
`Internal error: missing required common workflow bundle file templates/common/${required}. The installed dflow package looks incomplete.`
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Bundle source files come from two trees, merged: the edition-neutral common
|
|
1271
|
+
// tree (templates/common/, PROPOSAL-064) and the per-edition tree
|
|
1272
|
+
// (templates/{edition}/). Each descriptor carries its sourceRoot so the reader
|
|
1273
|
+
// (readPackagedBundleFile) loads content from the right tree; the projected dest
|
|
1274
|
+
// path and the manifest stay keyed on sourceRel, so a common-sourced file lands
|
|
1275
|
+
// at the same dflow/.../references/<name> path in every edition. The scanner
|
|
1276
|
+
// validates the merged set (collision + complete-edition guards) before
|
|
1277
|
+
// returning, so both callers (projector, doctor) get a trustworthy list.
|
|
1217
1278
|
async function listBundleSourceFiles(edition) {
|
|
1218
1279
|
const bundleDirs = ['references', 'templates'];
|
|
1280
|
+
const sourceRoots = ['common', edition];
|
|
1219
1281
|
const files = [];
|
|
1220
1282
|
|
|
1221
|
-
for (const
|
|
1222
|
-
const
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1283
|
+
for (const sourceRoot of sourceRoots) {
|
|
1284
|
+
for (const dir of bundleDirs) {
|
|
1285
|
+
const sourceDir = path.join(TEMPLATE_ROOT, sourceRoot, dir);
|
|
1286
|
+
let entries;
|
|
1287
|
+
try {
|
|
1288
|
+
entries = await fs.readdir(sourceDir);
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
if (error.code === 'ENOENT') {
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
throw error;
|
|
1229
1294
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
if (stat.isFile()) {
|
|
1237
|
-
files.push({ sourceRel, dir, name: entry });
|
|
1295
|
+
for (const entry of entries) {
|
|
1296
|
+
const sourcePath = path.join(sourceDir, entry);
|
|
1297
|
+
const stat = await fs.stat(sourcePath);
|
|
1298
|
+
if (stat.isFile()) {
|
|
1299
|
+
files.push({ sourceRel: `${dir}/${entry}`, dir, name: entry, sourceRoot });
|
|
1300
|
+
}
|
|
1238
1301
|
}
|
|
1239
1302
|
}
|
|
1240
1303
|
}
|
|
1241
1304
|
|
|
1305
|
+
assertNoBundleCollision(files);
|
|
1306
|
+
assertEditionBundleComplete(files, edition);
|
|
1307
|
+
assertCommonBundleComplete(files);
|
|
1242
1308
|
return files;
|
|
1243
1309
|
}
|
|
1244
1310
|
|
|
@@ -1306,18 +1372,12 @@ function injectBundleMarker(content) {
|
|
|
1306
1372
|
}
|
|
1307
1373
|
|
|
1308
1374
|
async function addWorkflowBundleItems(cwd, items, warnings, edition) {
|
|
1375
|
+
// listBundleSourceFiles merges templates/common/ + templates/{edition}/ and
|
|
1376
|
+
// validates the merged set (collision + complete-edition guards) before
|
|
1377
|
+
// returning, so the projector can trust a complete set here. A broken package
|
|
1378
|
+
// throws (InitError) before any stale removal or manifest write.
|
|
1309
1379
|
const bundleFiles = await listBundleSourceFiles(edition);
|
|
1310
1380
|
|
|
1311
|
-
// R3-02: a healthy edition's bundle source is never empty. An empty scan means
|
|
1312
|
-
// a broken installed package — hard-fail BEFORE scheduling any removal or
|
|
1313
|
-
// writing the manifest, so we never overwrite the manifest with files:[]
|
|
1314
|
-
// (which would orphan every projected file and ship a workflow-less project).
|
|
1315
|
-
if (bundleFiles.length === 0) {
|
|
1316
|
-
throw new InitError(
|
|
1317
|
-
`Internal error: no workflow bundle source files found for edition "${edition}" (expected files under templates/${edition}/references/ and templates/${edition}/templates/). The installed dflow package looks incomplete.`
|
|
1318
|
-
);
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
1381
|
const newRelPaths = new Set(bundleFiles.map((f) => `${WORKFLOW_BUNDLE_DEST}/${f.sourceRel}`));
|
|
1322
1382
|
|
|
1323
1383
|
// Read the previous manifest to drive stale cleanup. Distinguish absent
|
|
@@ -1396,10 +1456,10 @@ async function addWorkflowBundleItems(cwd, items, warnings, edition) {
|
|
|
1396
1456
|
}
|
|
1397
1457
|
|
|
1398
1458
|
// Build items for current edition bundle files.
|
|
1399
|
-
for (const { sourceRel } of bundleFiles) {
|
|
1459
|
+
for (const { sourceRel, sourceRoot } of bundleFiles) {
|
|
1400
1460
|
const relativePath = `${WORKFLOW_BUNDLE_DEST}/${sourceRel}`;
|
|
1401
1461
|
const absolutePath = path.join(cwd, relativePath);
|
|
1402
|
-
const sourceContent = await readPackagedBundleFile(
|
|
1462
|
+
const sourceContent = await readPackagedBundleFile(sourceRoot, sourceRel);
|
|
1403
1463
|
const content = injectBundleMarker(sourceContent);
|
|
1404
1464
|
|
|
1405
1465
|
let action;
|
|
@@ -1423,7 +1483,7 @@ async function addWorkflowBundleItems(cwd, items, warnings, edition) {
|
|
|
1423
1483
|
|
|
1424
1484
|
items.push({
|
|
1425
1485
|
relativePath,
|
|
1426
|
-
source: `packaged-bundle:${
|
|
1486
|
+
source: `packaged-bundle:${sourceRoot}/${sourceRel}`,
|
|
1427
1487
|
notes,
|
|
1428
1488
|
content,
|
|
1429
1489
|
action,
|
|
@@ -1455,13 +1515,17 @@ async function addWorkflowBundleItems(cwd, items, warnings, edition) {
|
|
|
1455
1515
|
}
|
|
1456
1516
|
}
|
|
1457
1517
|
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1518
|
+
// Reads one bundle file's content from its source tree. sourceRoot is the
|
|
1519
|
+
// descriptor's tree ('common' or an edition) so a common-sourced file is read
|
|
1520
|
+
// from templates/common/, not templates/{edition}/ (PROPOSAL-064). The traversal
|
|
1521
|
+
// guard is re-rooted at templates/${sourceRoot}/ accordingly.
|
|
1522
|
+
async function readPackagedBundleFile(sourceRoot, sourceRel) {
|
|
1523
|
+
const filePath = path.join(TEMPLATE_ROOT, sourceRoot, sourceRel);
|
|
1524
|
+
const normalizedRoot = path.resolve(TEMPLATE_ROOT, sourceRoot);
|
|
1461
1525
|
const normalizedFilePath = path.resolve(filePath);
|
|
1462
1526
|
|
|
1463
1527
|
if (!normalizedFilePath.startsWith(`${normalizedRoot}${path.sep}`)) {
|
|
1464
|
-
throw new InitError(`Internal error: packaged bundle file not found: templates/${
|
|
1528
|
+
throw new InitError(`Internal error: packaged bundle file not found: templates/${sourceRoot}/${sourceRel}`);
|
|
1465
1529
|
}
|
|
1466
1530
|
|
|
1467
1531
|
let buffer;
|
|
@@ -1469,15 +1533,15 @@ async function readPackagedBundleFile(edition, sourceRel) {
|
|
|
1469
1533
|
buffer = await fs.readFile(normalizedFilePath);
|
|
1470
1534
|
} catch (error) {
|
|
1471
1535
|
if (error.code === 'ENOENT') {
|
|
1472
|
-
throw new InitError(`Internal error: packaged bundle file not found: templates/${
|
|
1536
|
+
throw new InitError(`Internal error: packaged bundle file not found: templates/${sourceRoot}/${sourceRel}`);
|
|
1473
1537
|
}
|
|
1474
|
-
throw new InitError(`Internal error: cannot read packaged bundle file: templates/${
|
|
1538
|
+
throw new InitError(`Internal error: cannot read packaged bundle file: templates/${sourceRoot}/${sourceRel}`);
|
|
1475
1539
|
}
|
|
1476
1540
|
|
|
1477
1541
|
try {
|
|
1478
1542
|
return new TextDecoder('utf-8', { fatal: true }).decode(buffer);
|
|
1479
1543
|
} catch {
|
|
1480
|
-
throw new InitError(`Internal error: invalid UTF-8 packaged bundle file: templates/${
|
|
1544
|
+
throw new InitError(`Internal error: invalid UTF-8 packaged bundle file: templates/${sourceRoot}/${sourceRel}`);
|
|
1481
1545
|
}
|
|
1482
1546
|
}
|
|
1483
1547
|
|
|
@@ -3056,8 +3120,6 @@ async function runDoctor(options = {}) {
|
|
|
3056
3120
|
}
|
|
3057
3121
|
|
|
3058
3122
|
const findings = [];
|
|
3059
|
-
await checkLegacyRootSpecsDir(cwd, findings);
|
|
3060
|
-
await checkLegacySharedDir(cwd, findings);
|
|
3061
3123
|
await checkConventionsDflowVersion(cwd, findings);
|
|
3062
3124
|
await checkOrphanedWorkflowBundleFiles(cwd, findings);
|
|
3063
3125
|
|
|
@@ -3073,36 +3135,6 @@ async function runDoctor(options = {}) {
|
|
|
3073
3135
|
}
|
|
3074
3136
|
}
|
|
3075
3137
|
|
|
3076
|
-
async function checkLegacyRootSpecsDir(cwd, findings) {
|
|
3077
|
-
const legacyPath = path.join(cwd, 'specs');
|
|
3078
|
-
if ((await pathExists(legacyPath)) && (await containsInitializedContent(legacyPath))) {
|
|
3079
|
-
findings.push({
|
|
3080
|
-
level: 'warn',
|
|
3081
|
-
title: 'Legacy specs/ directory at project root',
|
|
3082
|
-
detail: 'V1 layout uses dflow/specs/ instead. The CLI does not modify root specs/.',
|
|
3083
|
-
action: 'See docs/migrating-to-dflow-v1.md (Step 1) for the manual migration steps.'
|
|
3084
|
-
});
|
|
3085
|
-
}
|
|
3086
|
-
}
|
|
3087
|
-
|
|
3088
|
-
async function checkLegacySharedDir(cwd, findings) {
|
|
3089
|
-
const candidates = [
|
|
3090
|
-
path.join(cwd, 'dflow', 'specs', '_共用'),
|
|
3091
|
-
path.join(cwd, 'specs', '_共用')
|
|
3092
|
-
];
|
|
3093
|
-
for (const candidate of candidates) {
|
|
3094
|
-
if (await pathExists(candidate)) {
|
|
3095
|
-
const rel = normalizePath(path.relative(cwd, candidate));
|
|
3096
|
-
findings.push({
|
|
3097
|
-
level: 'warn',
|
|
3098
|
-
title: `Legacy ${rel}/ directory`,
|
|
3099
|
-
detail: 'V1 layout uses shared/ (canonical English directory name).',
|
|
3100
|
-
action: 'See docs/migrating-to-dflow-v1.md (Step 2) for the rename steps.'
|
|
3101
|
-
});
|
|
3102
|
-
}
|
|
3103
|
-
}
|
|
3104
|
-
}
|
|
3105
|
-
|
|
3106
3138
|
async function checkConventionsDflowVersion(cwd, findings) {
|
|
3107
3139
|
const conventionsPath = path.join(cwd, 'dflow', 'specs', 'shared', '_conventions.md');
|
|
3108
3140
|
if (!(await pathExists(conventionsPath))) return;
|
|
@@ -3185,7 +3217,7 @@ function printDoctorReport(stdout, cwd, findings) {
|
|
|
3185
3217
|
stdout.write(`Project: ${cwd}\n\n`);
|
|
3186
3218
|
|
|
3187
3219
|
if (findings.length === 0) {
|
|
3188
|
-
stdout.write('All checks passed. No
|
|
3220
|
+
stdout.write('All checks passed. No Dflow health findings detected.\n');
|
|
3189
3221
|
return;
|
|
3190
3222
|
}
|
|
3191
3223
|
|
|
@@ -3211,5 +3243,10 @@ module.exports = {
|
|
|
3211
3243
|
// Exported for tests: the write phase enforces the PROPOSAL-054 raw-equality guard
|
|
3212
3244
|
// for user-owned root agent files (changed-after-preview -> skip), which cannot be
|
|
3213
3245
|
// exercised through the CLI because preview and write happen in one process.
|
|
3214
|
-
writeFilePlan
|
|
3246
|
+
writeFilePlan,
|
|
3247
|
+
// Exported for tests (PROPOSAL-064): pure bundle-source guards, unit-tested on
|
|
3248
|
+
// synthetic descriptor lists without touching the packaged templates/ tree.
|
|
3249
|
+
assertNoBundleCollision,
|
|
3250
|
+
assertEditionBundleComplete,
|
|
3251
|
+
assertCommonBundleComplete
|
|
3215
3252
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dflow-sdd-ddd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Spec-first SDD/DDD workflow kit for AI-assisted development",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"homepage": "https://github.com/weilung/dflow-sdd-ddd#readme",
|
|
41
41
|
"scripts": {
|
|
42
|
-
"test": "node test/smoke.mjs && node test/registry-parity.mjs && node test/agent-inject.mjs"
|
|
42
|
+
"test": "node test/smoke.mjs && node test/registry-parity.mjs && node test/agent-inject.mjs && node test/bundle-guards.mjs"
|
|
43
43
|
},
|
|
44
44
|
"license": "AGPL-3.0-or-later",
|
|
45
45
|
"publishConfig": {
|
|
@@ -4,18 +4,23 @@ Triggered by `/dflow:verify` or `/dflow:verify <bounded-context>`.
|
|
|
4
4
|
|
|
5
5
|
## Purpose
|
|
6
6
|
|
|
7
|
-
The A+C structure (`rules.md` as index + `behavior.md` as scenario content) introduces a drift risk — the two files can fall out of sync. This command
|
|
7
|
+
The A+C structure (`rules.md` as index + `behavior.md` as scenario content) introduces a drift risk — the two files can fall out of sync. This command's **core** is a mechanical safety net for that `rules.md` ↔ `behavior.md` correspondence, run at key moments: before a PR, after a refactor, or when onboarding to an unfamiliar Bounded Context. On top of the core it also runs an **optional, non-blocking domain-doc hygiene check** on the model catalog (`models.md`) — see Scope.
|
|
8
8
|
|
|
9
9
|
## Scope
|
|
10
10
|
|
|
11
|
-
### This command does (
|
|
11
|
+
### This command does (core + optional hygiene)
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
A small **core** of three deterministic string-matching checks on the
|
|
14
|
+
`rules.md` ↔ `behavior.md` correspondence:
|
|
14
15
|
|
|
15
16
|
1. **BR-ID forward check**: Every `BR-*` declared in `rules.md` has a corresponding section in `behavior.md`
|
|
16
17
|
2. **Anchor validity**: If `rules.md` links to `behavior.md#section`, that anchor exists
|
|
17
18
|
3. **BR-ID reverse check**: Every `BR-*` referenced in `behavior.md` is declared in `rules.md`
|
|
18
19
|
|
|
20
|
+
Plus an **optional domain-doc hygiene** warning (non-blocking — it never fails the
|
|
21
|
+
command, only surfaces a "confirm this is intentional" signal): the `models.md`
|
|
22
|
+
Code-Mapping hygiene check (see Model Catalog Notes).
|
|
23
|
+
|
|
19
24
|
### This command does NOT do (semantic layer — explicitly excluded)
|
|
20
25
|
|
|
21
26
|
Semantic verification (LLM reads the one-line summary in `rules.md` vs the Given/When/Then in `behavior.md` and judges whether they contradict) is **out of scope**. Reasons:
|
|
@@ -39,8 +44,8 @@ Reasons:
|
|
|
39
44
|
Step 3
|
|
40
45
|
- BC-level current state is already maintained by `rules.md` /
|
|
41
46
|
`behavior.md`, written by the same `/dflow:finish-feature` Step 3
|
|
42
|
-
- `/dflow:verify` keeps a small
|
|
43
|
-
|
|
47
|
+
- `/dflow:verify` keeps a small core: the `rules.md` ↔ `behavior.md`
|
|
48
|
+
correspondence inside one BC, plus the optional models.md hygiene check below
|
|
44
49
|
- Cross-feature / cross-phase aggregation would mix `/dflow:verify`'s
|
|
45
50
|
job with `/dflow:finish-feature`'s job and produce false positives
|
|
46
51
|
during in-progress features
|
|
@@ -84,6 +89,9 @@ For each Bounded Context:
|
|
|
84
89
|
- behavior.md → templates/behavior.md
|
|
85
90
|
Or run the completion flow to populate it from existing completed specs
|
|
86
91
|
```
|
|
92
|
+
- Also locate the **optional** hygiene input for this BC — `models.md`. It feeds
|
|
93
|
+
the non-blocking Model Catalog Notes hygiene check. If it is absent, **skip the
|
|
94
|
+
check silently** — do not report or stop; it is bonus, not part of the core.
|
|
87
95
|
|
|
88
96
|
### Step 2: Extract BR-IDs from rules.md
|
|
89
97
|
|
|
@@ -153,6 +161,31 @@ Issues:
|
|
|
153
161
|
remove the stale scenario reference from behavior.md
|
|
154
162
|
```
|
|
155
163
|
|
|
164
|
+
The optional `models.md` Code-Mapping hygiene check (below) appends its
|
|
165
|
+
non-blocking `ℹ` signals to this same report and never changes the core
|
|
166
|
+
pass / fail count.
|
|
167
|
+
|
|
168
|
+
## Model Catalog Notes
|
|
169
|
+
|
|
170
|
+
A non-blocking **informational** hygiene check on `models.md`:
|
|
171
|
+
|
|
172
|
+
- For each row whose **primary name cell** holds a real value, if the
|
|
173
|
+
`Code Mapping` column is empty or still a `{Namespace.Class}` placeholder,
|
|
174
|
+
surface it:
|
|
175
|
+
```
|
|
176
|
+
ℹ models.md: Entity "ExpenseReport" has no Code Mapping yet — link it if the
|
|
177
|
+
code is extracted; if extraction is deferred, this is expected
|
|
178
|
+
```
|
|
179
|
+
- **Skip untouched seed rows by the name cell only**: a row whose name is still a
|
|
180
|
+
`{...}` placeholder is seed scaffolding. But a row with a **real name** and a
|
|
181
|
+
placeholder / empty Code Mapping is exactly the one to surface — do not skip it
|
|
182
|
+
just because that one cell still holds `{...}`.
|
|
183
|
+
- This is **informational, not drift** — Brownfield records a concept in `models.md`
|
|
184
|
+
during discovery, often *before* the code is extracted, so an empty Code Mapping
|
|
185
|
+
is a normal state, not drift. An explicit "planned / deferred" note on the row, or
|
|
186
|
+
a matching `## Code Mapping Notes` entry, counts as accounted for; do not surface
|
|
187
|
+
it.
|
|
188
|
+
|
|
156
189
|
## When to Run
|
|
157
190
|
|
|
158
191
|
Recommended trigger points (not enforced — developer's judgment):
|
|
@@ -166,7 +199,8 @@ Recommended trigger points (not enforced — developer's judgment):
|
|
|
166
199
|
## Path Assumptions
|
|
167
200
|
|
|
168
201
|
This command operates entirely within `dflow/specs/domain/{context}/` files
|
|
169
|
-
(`rules.md` and `behavior.md`
|
|
202
|
+
(`rules.md` and `behavior.md` for the core check; `models.md` for the optional
|
|
203
|
+
hygiene warning). It does **not** read from
|
|
170
204
|
`dflow/specs/features/active/{SPEC-ID}-{slug}/` directories — the feature
|
|
171
205
|
directory layout is not part of verify's input.
|
|
172
206
|
|
|
@@ -55,6 +55,10 @@ proceeding (do not flip status, do not archive, do not emit summary).
|
|
|
55
55
|
- [ ] Every phase-spec file referenced in the Phase Specs table exists at
|
|
56
56
|
the path the table claims
|
|
57
57
|
- [ ] Every phase-spec file's frontmatter has `status: completed`
|
|
58
|
+
- [ ] Every Tier = T2 row in `_index.md` Lightweight Changes references an
|
|
59
|
+
existing `lightweight-*.md` / `BUG-*.md` file in the feature directory
|
|
60
|
+
- [ ] Every such lightweight / BUG spec file's frontmatter has
|
|
61
|
+
`status: completed`
|
|
58
62
|
- [ ] `_index.md` has no obvious open items in Resume Pointer (e.g. "phase-N
|
|
59
63
|
drafting" / "implementation pending" / "TODO" markers)
|
|
60
64
|
- [ ] Current BR Snapshot table is non-empty (or feature is intentionally
|
|
@@ -65,6 +69,7 @@ If any check fails:
|
|
|
65
69
|
> found:
|
|
66
70
|
> ✗ phase-spec-2026-04-15-foo.md status is still `in-progress`
|
|
67
71
|
> ✗ Phase Specs table row 3 references missing file phase-spec-...
|
|
72
|
+
> ✗ lightweight-2026-06-20-rounding.md frontmatter status is still `in-progress`
|
|
68
73
|
>
|
|
69
74
|
> Address these (run `/dflow:new-phase` to add missing work, or fix the
|
|
70
75
|
> stale status manually), then re-run `/dflow:finish-feature`."
|
|
@@ -92,11 +97,17 @@ branch: feature/{SPEC-ID}-{slug}
|
|
|
92
97
|
---
|
|
93
98
|
```
|
|
94
99
|
|
|
95
|
-
Also update the **Resume Pointer** to reflect closeout
|
|
100
|
+
Also update the **Resume Pointer** to reflect closeout — this writes the
|
|
101
|
+
cursor's terminal state (after closeout no workflow is active on this
|
|
102
|
+
feature; do not edit the cursor again after the Step 4 closeout commit):
|
|
96
103
|
|
|
97
104
|
```
|
|
98
105
|
**Current Progress**: feature completed ({date}); all phase-specs status = completed.
|
|
99
106
|
**Next Action**: integration — push / merge / PR per the selected Git policy.
|
|
107
|
+
**Active Workflow**: none
|
|
108
|
+
**Current Step**: n/a
|
|
109
|
+
**Gates Passed**: n/a
|
|
110
|
+
**Awaiting**: none
|
|
100
111
|
```
|
|
101
112
|
|
|
102
113
|
**→ Transition (step-internal)**: Step 2 complete. Announce "Step 2 complete (status flipped). Entering Step 3: Sync BR Snapshot to BC layer." and continue.
|
|
@@ -170,7 +181,8 @@ AI runs:
|
|
|
170
181
|
```bash
|
|
171
182
|
git mv dflow/specs/features/active/{SPEC-ID}-{slug} \
|
|
172
183
|
dflow/specs/features/completed/{SPEC-ID}-{slug}
|
|
173
|
-
git status # confirm rename detection
|
|
184
|
+
git status # confirm rename detection AND check for `RM` — an `M` next to
|
|
185
|
+
# a rename means unstaged edits you must re-add before committing
|
|
174
186
|
```
|
|
175
187
|
|
|
176
188
|
`git mv` is mandatory — never use plain `mv` + `git add`. This preserves
|
|
@@ -179,37 +191,74 @@ PR diff quality stays intact across the move. See
|
|
|
179
191
|
`references/git-integration.md` § "Directory Moves Must Use git mv" for
|
|
180
192
|
the full rule set.
|
|
181
193
|
|
|
182
|
-
After the move, also `git add` any modified files from Step 3 (the
|
|
183
|
-
updated `rules.md`, `behavior.md`, `glossary.md`, `tech-debt.md`, etc.)
|
|
184
|
-
into the same stage.
|
|
185
|
-
|
|
186
194
|
**Closeout commit checkpoint** (completes the offline Local-closeout gate):
|
|
187
195
|
|
|
188
196
|
```
|
|
189
|
-
✓ Feature archived to completed/ and closeout
|
|
197
|
+
✓ Feature archived to completed/ and closeout ready to stage
|
|
190
198
|
Commit this closeout now?
|
|
191
199
|
[Y] Yes — the AI commits with your Git identity (marker per _conventions.md § AI Commit Policy)
|
|
192
200
|
[N] No — skip; you commit yourself
|
|
193
201
|
```
|
|
194
202
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
hash
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
203
|
+
Then, in this order:
|
|
204
|
+
|
|
205
|
+
1. **Record the checkpoint row first.** Write one row in the moved
|
|
206
|
+
`_index.md` Checkpoint Log — `closeout | committed` for Y, `closeout |
|
|
207
|
+
skipped` for N. The closeout row carries **no commit hash**: the closeout
|
|
208
|
+
commit cannot contain its own hash. Trace it later via
|
|
209
|
+
`git log -1 -- dflow/specs/features/completed/{SPEC-ID}-{slug}` (or the
|
|
210
|
+
optional `Dflow-Checkpoint` trailer). The "hash only after success" rule
|
|
211
|
+
still applies to spec / implementation rows — closeout is the documented
|
|
212
|
+
exception (see `references/git-integration.md` § Commit Checkpoints,
|
|
213
|
+
Branch Gate & AI Commits).
|
|
214
|
+
2. **Stage the whole archived feature directory:**
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
git add dflow/specs/features/completed/{SPEC-ID}-{slug}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
This is required, not optional: `git mv` stages the rename with the
|
|
221
|
+
**last-committed** content, so working-tree edits made earlier in this
|
|
222
|
+
flow to the moved files — the Step 2 status flip and Resume Pointer
|
|
223
|
+
update, plus the checkpoint row you just wrote — stay **unstaged** until
|
|
224
|
+
this `git add`. In `git status`, the moved `_index.md` showing `RM`
|
|
225
|
+
instead of plain `R` is exactly this signal. Then also `git add` the
|
|
226
|
+
files updated in Step 3 (the updated `rules.md`, `behavior.md`,
|
|
227
|
+
`glossary.md`, `tech-debt.md`, etc.) into the same stage.
|
|
228
|
+
3. **Commit (Y) or stop (N).** For Y the AI commits. If a pre-commit hook
|
|
229
|
+
rejects it or the commit fails, flip the checkpoint row to `failed` (the
|
|
230
|
+
row is not committed yet — edit it directly), surface the error, and
|
|
231
|
+
treat the gate as unsatisfied.
|
|
232
|
+
|
|
233
|
+
**Post-commit closeout verification** — after a successful commit, and before
|
|
234
|
+
declaring the Local-closeout gate satisfied, AI runs and reports `✓` / `✗` for
|
|
235
|
+
every item:
|
|
236
|
+
|
|
237
|
+
- [ ] `git show HEAD:dflow/specs/features/completed/{SPEC-ID}-{slug}/_index.md`
|
|
238
|
+
— one blob read verifying **two** things: frontmatter `status: completed`
|
|
239
|
+
**and** the Checkpoint Log contains the closeout row. This reads the
|
|
240
|
+
**committed** content, not the working tree — the former catches "rename
|
|
241
|
+
carried stale content", the latter catches "row never made it into the
|
|
242
|
+
commit".
|
|
243
|
+
- [ ] `dflow/specs/features/active/{SPEC-ID}-{slug}/` no longer exists (the
|
|
244
|
+
directory was moved, not copied)
|
|
245
|
+
- [ ] `git status --short` shows no leftovers related to this feature
|
|
246
|
+
(working tree clean; identify any unrelated dirty files explicitly)
|
|
247
|
+
|
|
248
|
+
If any item fails, do **not** declare closeout complete — fix it (re-add and
|
|
249
|
+
amend, or a follow-up commit; the developer chooses) and re-verify.
|
|
250
|
+
|
|
251
|
+
The Local-closeout gate is satisfied **only when the closeout is committed and
|
|
252
|
+
the verification above passes**. If you declined the commit (chose N) or it
|
|
253
|
+
failed, Local-closeout is **not** satisfied yet — commit the staged closeout
|
|
254
|
+
yourself before continuing; do not enter the Integration / PR gate with
|
|
255
|
+
uncommitted changes. Once committed and verified, the gate stands on its own
|
|
256
|
+
offline; integration happens in Step 5 when you have network.
|
|
257
|
+
|
|
258
|
+
**→ Transition (step-internal)**: Step 4 complete. Branch on the verification result:
|
|
259
|
+
|
|
260
|
+
- **Closeout commit landed and post-commit verification passed** → announce "Step 4 complete (feature archived; Local-closeout gate satisfied). Entering Step 5: Integration / PR gate." and continue.
|
|
261
|
+
- **Closeout commit was declined (N), failed, or verification reported `✗`** → **stop here.** Announce "Step 4 complete (feature archived), but the Local-closeout gate is not satisfied yet — the closeout is staged but uncommitted, or the committed content failed verification. Commit the staged changes (or fix the failure), then resume to Step 5." Do **not** enter Step 5 with uncommitted or unverified closeout changes.
|
|
213
262
|
|
|
214
263
|
## Step 5: Emit Integration Summary (Git-strategy-neutral)
|
|
215
264
|
|
|
@@ -266,10 +315,17 @@ the developer:
|
|
|
266
315
|
|
|
267
316
|
If no `follow-up-of` field, skip Step 6 and announce closeout complete:
|
|
268
317
|
> "`/dflow:finish-feature` complete for `{SPEC-ID}-{slug}`. Feature
|
|
269
|
-
> directory is now at `dflow/specs/features/completed/{SPEC-ID}-{slug}
|
|
270
|
-
>
|
|
271
|
-
>
|
|
272
|
-
>
|
|
318
|
+
> directory is now at `dflow/specs/features/completed/{SPEC-ID}-{slug}/`,
|
|
319
|
+
> with the Local-closeout gate satisfied (closeout committed and verified).
|
|
320
|
+
> Integration — merge / push / PR — follows the selected Git policy, at
|
|
321
|
+
> your discretion."
|
|
322
|
+
|
|
323
|
+
**In-flight reminder** — after the closeout announcement (with or without
|
|
324
|
+
Step 6), run the in-flight overview scan (see `AI-AGENT-GUIDE.md` § Status /
|
|
325
|
+
Control Commands) and list any other unfinished features in `active/` and any
|
|
326
|
+
in-flight feature / bugfix branches. Surfacing them at closeout is deliberate:
|
|
327
|
+
attention is about to move elsewhere, and this is exactly where half-done work
|
|
328
|
+
sinks.
|
|
273
329
|
|
|
274
330
|
## Step 6: Reverse-Update Follow-up Tracking (only if follow-up)
|
|
275
331
|
|