dflow-sdd-ddd 0.10.0 → 0.11.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 +43 -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/modify-existing-flow.md +38 -0
- package/templates/brownfield/references/new-feature-flow.md +28 -0
- package/templates/brownfield/references/new-phase-flow.md +8 -1
- package/templates/brownfield/references/pr-review-checklist.md +7 -1
- package/templates/brownfield/scaffolding/AI-AGENT-GUIDE.md +6 -28
- package/templates/brownfield/templates/context-map.md +12 -4
- package/templates/common/references/ddd-modeling-guide.md +643 -0
- package/templates/greenfield/references/drift-verification.md +60 -12
- package/templates/greenfield/references/new-feature-flow.md +35 -7
- package/templates/greenfield/references/new-phase-flow.md +4 -1
- package/templates/greenfield/references/pr-review-checklist.md +9 -1
- package/templates/greenfield/scaffolding/AI-AGENT-GUIDE.md +0 -28
- package/templates/greenfield/templates/aggregate-design.md +6 -0
- package/templates/greenfield/templates/context-map.md +13 -4
- package/templates/greenfield/templates/events.md +4 -1
- package/docs/migrating-to-dflow-v1.md +0 -234
- package/templates/greenfield/references/ddd-modeling-guide.md +0 -351
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.11.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
|
|
|
@@ -282,8 +282,46 @@ Decision framework:
|
|
|
282
282
|
- **Extract now** if: the logic is being significantly modified anyway
|
|
283
283
|
- **Extract now** if: the logic is duplicated elsewhere and we need the single source of truth
|
|
284
284
|
- **Defer extraction** if: the change is a one-line fix and the surrounding code is too tangled
|
|
285
|
+
- **Consider not extracting at all** if: the context is `generic` (per the
|
|
286
|
+
context-map Subdomain Type) — a commodity capability's endgame is wholesale
|
|
287
|
+
replacement with an off-the-shelf package / service, so extracting its rules
|
|
288
|
+
one by one is wasted effort; record the replacement intent as the tech-debt
|
|
289
|
+
entry instead
|
|
285
290
|
- **Always record** the extraction opportunity in tech-debt.md even if deferring
|
|
286
291
|
|
|
292
|
+
If the target context has **no Subdomain Type** (not in `context-map.md`, or
|
|
293
|
+
the column is absent), ask the developer once to classify it. If they'd rather
|
|
294
|
+
not decide now, record it as an Open Question in the spec and run the
|
|
295
|
+
extraction decision on the framework above — do **not** assume `generic`.
|
|
296
|
+
|
|
297
|
+
**Aggregate emergence check.** Before extracting yet another rule onto an
|
|
298
|
+
existing concept, look at what has already accumulated on it (in `models.md` /
|
|
299
|
+
`rules.md`). The signal that it has stopped being a loose entity and is
|
|
300
|
+
becoming an **Aggregate Root with a consistency boundary**: **2+ non-trivial
|
|
301
|
+
state-transition rules on the same lifecycle identity, or any invariant that
|
|
302
|
+
must check / update multiple fields or child records atomically.** (A
|
|
303
|
+
*consistency boundary* means state that must hold or change **together,
|
|
304
|
+
atomically** — not mere relatedness: same screen, related nouns, or local
|
|
305
|
+
single-field input validation do **not** count.) When you see it, continuing
|
|
306
|
+
to extract rules one by one as T2 will leave the boundary undrawn — surface it
|
|
307
|
+
and **suggest escalating to T1** (`/dflow:new-phase` inside an active feature,
|
|
308
|
+
else `/dflow:new-feature`) to model the Aggregate deliberately: its invariants,
|
|
309
|
+
what must change atomically, what it protects. For *how* to model it —
|
|
310
|
+
invariant classification, set-based / uniqueness rules, aggregate sizing — read
|
|
311
|
+
`references/ddd-modeling-guide.md` (its **Edition note** maps each recording
|
|
312
|
+
surface to brownfield's `models.md` / `rules.md`). In `models.md`, **mark the
|
|
313
|
+
existing Entity row as the Aggregate Root and note the protected invariants /
|
|
314
|
+
atomic-change scope in its Responsibility / Notes** — brownfield `models.md`
|
|
315
|
+
has no separate Aggregates section, do not invent one; update the Repository
|
|
316
|
+
row if one exists. If the developer defers, **record the emergence observation
|
|
317
|
+
in `tech-debt.md`** so the boundary decision is not silently lost.
|
|
318
|
+
|
|
319
|
+
If the context is **`generic`** (Subdomain Type), emergence is usually a
|
|
320
|
+
*replacement / adapter-boundary* debt signal, not a cue for deep T1 modeling —
|
|
321
|
+
record the replacement intent (consistent with the generic extraction fallback
|
|
322
|
+
above) rather than escalating, unless the developer explicitly chooses to
|
|
323
|
+
model it.
|
|
324
|
+
|
|
287
325
|
### Generate Implementation Tasks List
|
|
288
326
|
|
|
289
327
|
For a phase-spec modification, AI generates a concrete task list and writes it into the spec's `Implementation Tasks` section using `[LAYER]-[NUMBER]:description` (DOMAIN / DELIVERY / DATA / TEST).
|
|
@@ -50,6 +50,23 @@ If no matching context exists:
|
|
|
50
50
|
2. Create `dflow/specs/domain/{new-context}/context.md` using the context-definition template
|
|
51
51
|
3. Get developer confirmation before proceeding
|
|
52
52
|
|
|
53
|
+
Once the BC is confirmed, classify and record its **Subdomain Type** as part
|
|
54
|
+
of the same confirmation (not a separate gate):
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
"Is this capability core (差異化來源), supporting (必要但非差異化),
|
|
58
|
+
or generic (可買 / 可套件 / 簡單 CRUD)? I'll record it in context-map.md."
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
If the BC already has a Subdomain Type, reuse it — don't re-ask unless the
|
|
62
|
+
developer wants to reclassify. Record it in
|
|
63
|
+
`dflow/specs/domain/context-map.md`:
|
|
64
|
+
|
|
65
|
+
- If the file **does not exist** (the Brownfield track does not mandate it —
|
|
66
|
+
contexts emerge organically), create it from `templates/context-map.md`.
|
|
67
|
+
- If it exists but the **Subdomain Type column is missing**, add the column
|
|
68
|
+
**preserving every existing row** — do not rewrite their content.
|
|
69
|
+
|
|
53
70
|
**→ Transition (step-internal)**: Step 2 complete. Announce "Step 2 complete (BC identified). Entering Step 3: Domain Concept Discovery." and continue.
|
|
54
71
|
|
|
55
72
|
## Step 3: Domain Concept Discovery
|
|
@@ -62,6 +79,17 @@ Walk through these questions:
|
|
|
62
79
|
- **What are the states/statuses?** → State machines to model
|
|
63
80
|
- **What external data is needed?** → Interfaces to define
|
|
64
81
|
|
|
82
|
+
If one concept gathers rules / invariants that must hold together — **a state
|
|
83
|
+
machine over its lifecycle, or invariants spanning several of its fields /
|
|
84
|
+
child records** — treat it as a candidate **Aggregate Root** (a consistency
|
|
85
|
+
boundary = atomic, not mere relatedness), not just an Entity. Note its
|
|
86
|
+
invariants and what must change atomically against its `models.md` Entity row,
|
|
87
|
+
and confirm the boundary with the developer. (A state machine is one common
|
|
88
|
+
signal, not a prerequisite.) For the tactical patterns — invariant
|
|
89
|
+
classification, set-based / uniqueness rules, value objects, aggregate sizing —
|
|
90
|
+
read `references/ddd-modeling-guide.md` (its **Edition note** maps recording
|
|
91
|
+
surfaces to brownfield's `models.md` / `rules.md`).
|
|
92
|
+
|
|
65
93
|
For each new concept:
|
|
66
94
|
1. Check glossary — add if missing
|
|
67
95
|
2. Check if it already exists in models.md — extend if needed
|
|
@@ -92,7 +92,14 @@ Walk the developer through what the new phase covers:
|
|
|
92
92
|
BRs (ADDED), changed BRs (MODIFIED), removed BRs (REMOVED), renamed
|
|
93
93
|
(RENAMED). Items not mentioned stay UNCHANGED implicitly.
|
|
94
94
|
3. **Any Domain concepts introduced or changed?** New Entities / Value
|
|
95
|
-
Objects / Services touching `src/Domain/{context}/`?
|
|
95
|
+
Objects / Services touching `src/Domain/{context}/`? If this phase is an
|
|
96
|
+
**Aggregate-emergence escalation** (handed off from `/dflow:modify-existing`),
|
|
97
|
+
record the candidate Aggregate Root, the invariants it protects, and what
|
|
98
|
+
must change atomically — marking the Aggregate Root on its `models.md`
|
|
99
|
+
Entity row (no separate Aggregates section). For how to model it (invariant
|
|
100
|
+
classification, set-based / uniqueness rules, aggregate sizing), read
|
|
101
|
+
`references/ddd-modeling-guide.md` (its **Edition note** maps recording
|
|
102
|
+
surfaces to brownfield's `models.md` / `rules.md`).
|
|
96
103
|
4. **Data structure impact?** New tables, columns, indices?
|
|
97
104
|
5. **Why now?** Priority — informs sequencing relative to other phases.
|
|
98
105
|
|
|
@@ -139,7 +139,13 @@ for later?"
|
|
|
139
139
|
## Glossary Consistency
|
|
140
140
|
|
|
141
141
|
- [ ] **New terms documented** — Any new business concept added to glossary.md?
|
|
142
|
-
- [ ] **
|
|
142
|
+
- [ ] **Naming matches the Ubiquitous Language** — for each **domain-facing**
|
|
143
|
+
class / method / variable the diff introduces (skip DTO / test / framework
|
|
144
|
+
names), is there a matching term in `glossary.md`? The `Code Mapping` column
|
|
145
|
+
maps each term to its `{Namespace/Class/Member}` — a domain name with no
|
|
146
|
+
glossary term, or a term whose Code Mapping is now stale, is the signal.
|
|
147
|
+
- [ ] **No synonym drift** — is the code naming a concept with a different word
|
|
148
|
+
than the glossary? Align it. (Judgment call, not a string match.)
|
|
143
149
|
- [ ] **No ambiguous terms** — Are domain-specific terms used precisely?
|
|
144
150
|
|
|
145
151
|
Example check:
|
|
@@ -102,6 +102,12 @@ input like this (supporting files live in the workflow bundle at
|
|
|
102
102
|
`dflow/specs/features/backlog/` and suggest work based on migration value.
|
|
103
103
|
- **"I'm creating a branch"** → read `references/git-integration.md`; verify
|
|
104
104
|
branch naming and ensure a spec exists before coding starts.
|
|
105
|
+
- **"I'm designing a domain model" / "How should I model X?" / building or
|
|
106
|
+
reshaping an Aggregate** → read `references/ddd-modeling-guide.md` (DDD
|
|
107
|
+
tactical patterns: aggregates, invariants, value objects, domain events). It
|
|
108
|
+
is written with Greenfield artifact names; see its **Edition note** for where
|
|
109
|
+
Brownfield records the same decisions (`models.md` / `rules.md` /
|
|
110
|
+
`behavior.md` / `migration/tech-debt.md`).
|
|
105
111
|
- **"Dflow seems wrong" / "this template is confusing"** (or you notice Dflow
|
|
106
112
|
guidance drift) → suggest `/dflow:report-dflow-feedback`; never submit
|
|
107
113
|
anything upstream automatically.
|
|
@@ -344,34 +350,6 @@ during the completion checklist, not via template section markers.
|
|
|
344
350
|
- Direct SQL in delivery/entrypoint code? → Record
|
|
345
351
|
- Magic numbers or undocumented statuses? → Record and add to glossary
|
|
346
352
|
|
|
347
|
-
## Pre-V1 Artifacts Detection
|
|
348
|
-
|
|
349
|
-
When working in a project that adopted Dflow before `dflow-sdd-ddd@0.1.0`,
|
|
350
|
-
you may encounter layout or naming patterns that predate the V1 baseline.
|
|
351
|
-
If any of the following appear, surface the observation to the developer
|
|
352
|
-
and recommend manual migration; do not rewrite anything silently.
|
|
353
|
-
|
|
354
|
-
Signals:
|
|
355
|
-
|
|
356
|
-
- Top-level `specs/` directory containing Dflow-shaped content (V1 layout
|
|
357
|
-
uses `dflow/specs/`).
|
|
358
|
-
- `_共用/` directory under `specs/` or `dflow/specs/` (V1 uses `shared/`).
|
|
359
|
-
- Section headings in Traditional Chinese where V1 templates render
|
|
360
|
-
canonical English; compare against `TEMPLATE-LANGUAGE-GLOSSARY.md` if
|
|
361
|
-
available.
|
|
362
|
-
- References to a runtime `/dflow:init-project` slash command (V1
|
|
363
|
-
replaced it with the Dflow CLI init command (`dflow init`, or
|
|
364
|
-
`npx dflow-sdd-ddd init` when using the no-install path)).
|
|
365
|
-
- A root `CLAUDE.md`, `AGENTS.md`, or equivalent that holds the full
|
|
366
|
-
Dflow workflow text instead of being a thin shim pointing to this
|
|
367
|
-
file.
|
|
368
|
-
- `dflow/specs/shared/_conventions.md` is missing the `> Dflow Version:`
|
|
369
|
-
front-matter line (V1 init writes it automatically).
|
|
370
|
-
|
|
371
|
-
Recommend `docs/migrating-to-dflow-v1.md` for the manual migration
|
|
372
|
-
checklist. Migration affects every spec the team has written; manual
|
|
373
|
-
review is required.
|
|
374
|
-
|
|
375
353
|
## Workflow Steps
|
|
376
354
|
|
|
377
355
|
This guide is the **command registry, routing rules, and project context**.
|
|
@@ -6,15 +6,23 @@
|
|
|
6
6
|
|
|
7
7
|
## Context List
|
|
8
8
|
|
|
9
|
-
| Bounded Context | Responsibility | Owner / Team | Primary Code Area | Notes |
|
|
10
|
-
|
|
11
|
-
| {Context name} | {業務責任} | {owner} | `{project/path/or/namespace}` | {optional notes} |
|
|
9
|
+
| Bounded Context | Responsibility | Subdomain Type | Owner / Team | Primary Code Area | Notes |
|
|
10
|
+
|---|---|---|---|---|---|
|
|
11
|
+
| {Context name} | {業務責任} | core / supporting / generic | {owner} | `{project/path/or/namespace}` | {optional notes} |
|
|
12
|
+
|
|
13
|
+
> **Subdomain Type** — 判別問句:「這塊功能換成現成 SaaS / 套件,系統的差異化會消失嗎?」
|
|
14
|
+
> (差異化不限商業競爭優勢;內部系統指獨特的營運優勢 / 任務成果。)會 → `core`;
|
|
15
|
+
> 不會、但需要為自家流程客製 → `supporting`(必要、常需客製、非差異化);
|
|
16
|
+
> 不會、且現成方案存在 → `generic`。多數 BC 是 supporting,`core` 通常只有 1–2 個;
|
|
17
|
+
> 全標 core = 沒分類。分類是可修訂的初判,改判時更新本欄並在 Notes 留一行理由。
|
|
18
|
+
> 它影響漸進抽離的取捨(`generic` 傾向整塊替換而非逐條抽離,見 modify-existing-flow
|
|
19
|
+
> Step 4),但**不**降低 BR 紀錄、Tier ceremony、或安全 / 測試 / 可靠性要求。
|
|
12
20
|
|
|
13
21
|
## Relationships
|
|
14
22
|
|
|
15
23
|
| Source Context | Target Context | Relationship Type | Integration Mechanism | Notes |
|
|
16
24
|
|---|---|---|---|---|
|
|
17
|
-
| {Source} | {Target} | {Customer/Supplier, Conformist, ACL, Shared Kernel, etc.} | {DB table, service call, file, manual process} | {optional notes} |
|
|
25
|
+
| {Source} | {Target} | {Customer/Supplier, Conformist, ACL, Shared Kernel, Separate Ways, Big Ball of Mud (BBoM), OHS, etc.} | {DB table, service call, file, manual process} | {optional notes} |
|
|
18
26
|
|
|
19
27
|
## Integration Notes
|
|
20
28
|
|