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/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 dir of bundleDirs) {
1222
- const sourceDir = path.join(TEMPLATE_ROOT, edition, dir);
1223
- let entries;
1224
- try {
1225
- entries = await fs.readdir(sourceDir);
1226
- } catch (error) {
1227
- if (error.code === 'ENOENT') {
1228
- continue;
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
- throw error;
1231
- }
1232
- for (const entry of entries) {
1233
- const sourceRel = `${dir}/${entry}`;
1234
- const sourcePath = path.join(sourceDir, entry);
1235
- const stat = await fs.stat(sourcePath);
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(edition, sourceRel);
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:${edition}/${sourceRel}`,
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
- async function readPackagedBundleFile(edition, sourceRel) {
1459
- const filePath = path.join(TEMPLATE_ROOT, edition, sourceRel);
1460
- const normalizedRoot = path.resolve(TEMPLATE_ROOT, edition);
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/${edition}/${sourceRel}`);
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/${edition}/${sourceRel}`);
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/${edition}/${sourceRel}`);
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/${edition}/${sourceRel}`);
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 legacy artifacts detected.\n');
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.10.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 provides a mechanical verification safety net that developers can run at key moments: before a PR, after a refactor, or when onboarding to an unfamiliar Bounded Context.
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 (mechanical layer)
11
+ ### This command does (core + optional hygiene)
12
12
 
13
- Three string-matching checks that AI can perform deterministically:
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, mechanical scope: just the
43
- `rules.md` ↔ `behavior.md` correspondence inside one BC
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`). It does **not** read from
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
- - [ ] **Consistent naming** — Do class/method/variable names match glossary terms?
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