dflow-sdd-ddd 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.en.md +7 -9
  3. package/README.md +10 -12
  4. package/TEMPLATE-COVERAGE.md +1 -1
  5. package/bin/dflow.js +11 -5
  6. package/docs/evaluating-dflow.en.md +2 -5
  7. package/docs/evaluating-dflow.md +1 -3
  8. package/docs/examples-by-stack.md +516 -0
  9. package/docs/migrating-to-dflow-v1.md +1 -1
  10. package/docs/release-versioning-policy.md +13 -0
  11. package/docs/using-with-claude-code.en.md +38 -8
  12. package/docs/using-with-claude-code.md +33 -7
  13. package/docs/using-with-codex.en.md +31 -5
  14. package/docs/using-with-codex.md +28 -5
  15. package/docs/using-with-github-copilot.en.md +29 -5
  16. package/docs/using-with-github-copilot.md +28 -5
  17. package/lib/init.js +437 -46
  18. package/package.json +1 -1
  19. package/templates/brownfield/scaffolding/AI-AGENT-GUIDE.md +42 -3
  20. package/templates/brownfield/scaffolding/CLAUDE-md-snippet.md +25 -15
  21. package/templates/brownfield/scaffolding/Git-principles-gitflow.md +1 -1
  22. package/templates/brownfield/scaffolding/Git-principles-trunk.md +1 -1
  23. package/templates/brownfield/scaffolding/_conventions.md +1 -1
  24. package/templates/brownfield/scaffolding/_overview.md +40 -29
  25. package/templates/brownfield/templates/CLAUDE.md +25 -17
  26. package/templates/brownfield/templates/context-definition.md +4 -4
  27. package/templates/brownfield/templates/context-map.md +1 -1
  28. package/templates/brownfield/templates/lightweight-spec.md +3 -1
  29. package/templates/brownfield/templates/models.md +1 -1
  30. package/templates/brownfield/templates/phase-spec.md +10 -8
  31. package/templates/brownfield/templates/tech-debt.md +2 -2
  32. package/templates/greenfield/scaffolding/AI-AGENT-GUIDE.md +42 -3
  33. package/templates/greenfield/scaffolding/CLAUDE-md-snippet.md +7 -6
  34. package/templates/greenfield/scaffolding/Git-principles-gitflow.md +2 -2
  35. package/templates/greenfield/scaffolding/Git-principles-trunk.md +2 -2
  36. package/templates/greenfield/scaffolding/_overview.md +29 -11
  37. package/templates/greenfield/templates/CLAUDE.md +5 -5
  38. package/docs/using-with-gemini-cli.en.md +0 -200
  39. package/docs/using-with-gemini-cli.md +0 -184
package/lib/init.js CHANGED
@@ -8,6 +8,21 @@ const pkg = require('../package.json');
8
8
  const MIN_NODE_VERSION = '22.0.0';
9
9
  const PACKAGE_ROOT = path.resolve(__dirname, '..');
10
10
  const TEMPLATE_ROOT = path.join(PACKAGE_ROOT, 'templates');
11
+ const COMMAND_REGISTRY_START = '<!-- dflow-command-registry:start -->';
12
+ const COMMAND_REGISTRY_END = '<!-- dflow-command-registry:end -->';
13
+ const EXPECTED_COMMAND_IDS = [
14
+ 'new-feature',
15
+ 'modify-existing',
16
+ 'bug-fix',
17
+ 'new-phase',
18
+ 'finish-feature',
19
+ 'verify',
20
+ 'pr-review',
21
+ 'report-dflow-feedback',
22
+ 'status',
23
+ 'next',
24
+ 'cancel'
25
+ ];
11
26
 
12
27
  const PROSE_LANGUAGE_PATTERN =
13
28
  /^[A-Za-z]{2,3}(?:-[A-Za-z]{4})?(?:-(?:[A-Za-z]{2}|[0-9]{3}))?(?:-(?:[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*$/;
@@ -91,11 +106,6 @@ const AI_AGENT_OPTIONS = [
91
106
  label: 'CLAUDE.md - Claude Code',
92
107
  aliases: ['claude', 'claude.md']
93
108
  },
94
- {
95
- key: 'gemini',
96
- label: 'GEMINI.md - Gemini CLI',
97
- aliases: ['gemini', 'gemini.md']
98
- },
99
109
  {
100
110
  key: 'copilot',
101
111
  label: '.github/copilot-instructions.md - GitHub Copilot',
@@ -251,7 +261,8 @@ async function runConfigureAgents(options = {}) {
251
261
 
252
262
  const plan = await buildConfigureAgentsPlan(cwd, {
253
263
  ...projectContext,
254
- aiAgents
264
+ aiAgents,
265
+ commandAdapters: Boolean(options.commandAdapters)
255
266
  });
256
267
 
257
268
  renderPreview(stdout, plan, []);
@@ -268,7 +279,7 @@ async function runConfigureAgents(options = {}) {
268
279
  result.warnings.push(...collectUnresolvedPlaceholderWarnings(plan, result.created));
269
280
 
270
281
  printResultReport(stdout, result, plan.deferred);
271
- printConfigureAgentsNextSteps(stdout);
282
+ printConfigureAgentsNextSteps(stdout, Boolean(options.commandAdapters));
272
283
  return 0;
273
284
  } catch (error) {
274
285
  if (rl) {
@@ -383,7 +394,16 @@ async function detectProjectSignals(cwd) {
383
394
  name.endsWith('.csproj') ||
384
395
  name === 'program.cs' ||
385
396
  name === 'startup.cs' ||
386
- name === 'package.json'
397
+ name === 'package.json' ||
398
+ name === 'pom.xml' ||
399
+ name === 'build.gradle' ||
400
+ name === 'build.gradle.kts' ||
401
+ name === 'pyproject.toml' ||
402
+ name === 'requirements.txt' ||
403
+ name === 'go.mod' ||
404
+ name === 'cargo.toml' ||
405
+ name === 'composer.json' ||
406
+ name === 'gemfile'
387
407
  );
388
408
 
389
409
  const hasWebFormsFiles = Array.from(baseNames).some((name) =>
@@ -412,9 +432,36 @@ async function detectProjectSignals(cwd) {
412
432
  trackHint = 'brownfield';
413
433
  }
414
434
 
435
+ const stackHints = [];
436
+ if (coreSignal || webFormsSignal || csprojFiles.length > 0) {
437
+ stackHints.push('dotnet');
438
+ }
439
+ if (baseNames.has('pom.xml') || baseNames.has('build.gradle') || baseNames.has('build.gradle.kts')) {
440
+ stackHints.push('java');
441
+ }
442
+ if (baseNames.has('package.json')) {
443
+ stackHints.push('nodejs');
444
+ }
445
+ if (baseNames.has('pyproject.toml') || baseNames.has('requirements.txt')) {
446
+ stackHints.push('python');
447
+ }
448
+ if (baseNames.has('go.mod')) {
449
+ stackHints.push('go');
450
+ }
451
+ if (baseNames.has('cargo.toml')) {
452
+ stackHints.push('rust');
453
+ }
454
+ if (baseNames.has('composer.json')) {
455
+ stackHints.push('php');
456
+ }
457
+ if (baseNames.has('gemfile')) {
458
+ stackHints.push('ruby');
459
+ }
460
+
415
461
  return {
416
462
  hasSourceTree: hasSourceTree || relNames.has('src'),
417
- trackHint
463
+ trackHint,
464
+ stackHints
418
465
  };
419
466
  }
420
467
 
@@ -963,8 +1010,14 @@ async function buildConfigureAgentsPlan(cwd, answers) {
963
1010
  content
964
1011
  });
965
1012
 
1013
+ const commandRegistry = answers.commandAdapters ? parseDflowCommandRegistry(content) : [];
1014
+
966
1015
  for (const agent of answers.aiAgents) {
967
- await addAiAgentShim(cwd, items, agent, substitution);
1016
+ await addAiAgentShim(cwd, items, agent, substitution, { commandRegistry });
1017
+ }
1018
+
1019
+ if (answers.commandAdapters) {
1020
+ addCommandAdapterItems(items, answers.aiAgents, commandRegistry);
968
1021
  }
969
1022
 
970
1023
  await finalizePlanItems(cwd, items);
@@ -981,7 +1034,8 @@ async function buildConfigureAgentsPlan(cwd, answers) {
981
1034
  async function finalizePlanItems(cwd, items) {
982
1035
  for (const item of items) {
983
1036
  const absolute = path.join(cwd, item.relativePath);
984
- item.action = (await pathExists(absolute)) ? 'skip' : 'create';
1037
+ const targetExists = await pathExists(absolute);
1038
+ item.action = targetExists ? (item.overwrite ? 'update' : 'skip') : 'create';
985
1039
  if (item.action === 'skip') {
986
1040
  item.notes = item.notes ? `${item.notes}, already exists` : 'already exists';
987
1041
  }
@@ -989,15 +1043,23 @@ async function finalizePlanItems(cwd, items) {
989
1043
  }
990
1044
  }
991
1045
 
992
- async function addAiAgentShim(cwd, items, agent, substitution) {
1046
+ async function addAiAgentShim(cwd, items, agent, substitution, options = {}) {
993
1047
  const target = getAiAgentTarget(agent);
994
1048
  const targetPath = path.join(cwd, target.relativePath);
995
1049
  const targetExists = await pathExists(targetPath);
996
1050
  const targetConfigured = targetExists && await fileReferencesAiAgentGuide(targetPath);
997
- const relativePath = targetExists && !targetConfigured ? target.snippetPath : target.relativePath;
998
- const content = substitutePlaceholders(buildAiAgentShim(target.relativePath), substitution);
1051
+ const commandRegistry = options.commandRegistry || [];
1052
+ const codexCommandAdapterSnippet = target.relativePath === 'AGENTS.md' && commandRegistry.length > 0 && targetExists;
1053
+ const relativePath = codexCommandAdapterSnippet
1054
+ ? (targetConfigured ? 'dflow/specs/shared/AGENTS-md-command-adapters-snippet.md' : target.snippetPath)
1055
+ : (targetExists && !targetConfigured ? target.snippetPath : target.relativePath);
1056
+ const content = substitutePlaceholders(buildAiAgentShim(target.relativePath, options.commandRegistry), substitution);
999
1057
  let notes = 'selected, tool-specific shim';
1000
- if (targetConfigured) {
1058
+ if (codexCommandAdapterSnippet && targetConfigured) {
1059
+ notes = `selected, ${target.relativePath} already points to AI-AGENT-GUIDE.md; merge this command trigger snippet manually`;
1060
+ } else if (codexCommandAdapterSnippet) {
1061
+ notes = `selected, ${target.relativePath} already exists; merge this command trigger snippet manually`;
1062
+ } else if (targetConfigured) {
1001
1063
  notes = `selected, ${target.relativePath} already points to AI-AGENT-GUIDE.md`;
1002
1064
  } else if (targetExists) {
1003
1065
  notes = `selected, ${target.relativePath} already exists; merge this snippet manually`;
@@ -1007,7 +1069,8 @@ async function addAiAgentShim(cwd, items, agent, substitution) {
1007
1069
  relativePath,
1008
1070
  source: `generated:${agent}-shim`,
1009
1071
  notes,
1010
- content
1072
+ content,
1073
+ overwrite: codexCommandAdapterSnippet
1011
1074
  });
1012
1075
  }
1013
1076
 
@@ -1031,10 +1094,6 @@ function getAiAgentTarget(agent) {
1031
1094
  relativePath: 'CLAUDE.md',
1032
1095
  snippetPath: 'dflow/specs/shared/CLAUDE-md-snippet.md'
1033
1096
  },
1034
- gemini: {
1035
- relativePath: 'GEMINI.md',
1036
- snippetPath: 'dflow/specs/shared/GEMINI-md-snippet.md'
1037
- },
1038
1097
  copilot: {
1039
1098
  relativePath: '.github/copilot-instructions.md',
1040
1099
  snippetPath: 'dflow/specs/shared/copilot-instructions-snippet.md'
@@ -1044,12 +1103,16 @@ function getAiAgentTarget(agent) {
1044
1103
  return targets[agent];
1045
1104
  }
1046
1105
 
1047
- function buildAiAgentShim(targetPath) {
1106
+ function buildAiAgentShim(targetPath, commandRegistry = []) {
1048
1107
  const title = targetPath === '.github/copilot-instructions.md'
1049
1108
  ? 'GitHub Copilot Repository Instructions'
1050
1109
  : `${targetPath} - Dflow Project Instructions`;
1051
1110
 
1052
- const importHint = targetPath === 'CLAUDE.md' || targetPath === 'GEMINI.md'
1111
+ const commandTriggerHint = targetPath === 'AGENTS.md' && commandRegistry.length > 0
1112
+ ? buildCodexCommandTriggerSection(commandRegistry)
1113
+ : '';
1114
+
1115
+ const importHint = targetPath === 'CLAUDE.md'
1053
1116
  ? '\nIf your tool supports Markdown imports, the canonical guide is imported below:\n\n@dflow/specs/shared/AI-AGENT-GUIDE.md\n'
1054
1117
  : '';
1055
1118
 
@@ -1063,10 +1126,195 @@ Before planning or editing code, read and follow:
1063
1126
 
1064
1127
  Keep tool-specific instruction files small. The Dflow guide above is the
1065
1128
  single source of truth for project workflow rules, slash-command behavior,
1066
- spec locations, and SDD/DDD constraints.${importHint}
1129
+ spec locations, and SDD/DDD constraints.${commandTriggerHint}${importHint}
1067
1130
  `;
1068
1131
  }
1069
1132
 
1133
+ function addCommandAdapterItems(items, aiAgents, commandRegistry) {
1134
+ if (aiAgents.includes('claude')) {
1135
+ for (const command of commandRegistry) {
1136
+ items.push({
1137
+ relativePath: `.claude/commands/dflow/dflow-${command.id}.md`,
1138
+ source: 'generated:claude-command-adapter',
1139
+ notes: 'command adapter, derived from dflow command registry',
1140
+ content: buildThinCommandWrapper(command),
1141
+ overwrite: true
1142
+ });
1143
+ }
1144
+ }
1145
+
1146
+ if (aiAgents.includes('copilot')) {
1147
+ for (const command of commandRegistry) {
1148
+ items.push({
1149
+ relativePath: `.github/prompts/dflow-${command.id}.prompt.md`,
1150
+ source: 'generated:copilot-command-adapter',
1151
+ notes: 'command adapter, derived from dflow command registry',
1152
+ content: buildThinCommandWrapper(command),
1153
+ overwrite: true
1154
+ });
1155
+ }
1156
+ }
1157
+ }
1158
+
1159
+ function buildCodexCommandTriggerSection(commandRegistry) {
1160
+ const triggers = commandRegistry
1161
+ .map((command) => {
1162
+ const commandKind = command.scope === 'control' ? 'command' : 'workflow';
1163
+ return `- \`${command.label}\` as text, or "Run the Dflow ${command.label} ${commandKind}."`;
1164
+ })
1165
+ .join('\n');
1166
+
1167
+ return `
1168
+
1169
+ ## Dflow Text Triggers
1170
+
1171
+ Codex does not install Dflow command files. When the developer asks for a
1172
+ canonical Dflow command, treat it as a text trigger, read the canonical guide,
1173
+ and execute the matching workflow or control command.
1174
+
1175
+ Recognized canonical triggers:
1176
+
1177
+ ${triggers}
1178
+ `;
1179
+ }
1180
+
1181
+ function buildThinCommandWrapper(command) {
1182
+ const argHint = command.argHint === '-'
1183
+ ? 'Argument hint: none.'
1184
+ : `Argument hint: ${command.argHint}.`;
1185
+
1186
+ return `# /dflow-${command.id}
1187
+
1188
+ Execute the canonical \`${command.label}\` Dflow workflow or control command.
1189
+
1190
+ Definition: \`dflow/specs/shared/AI-AGENT-GUIDE.md\`
1191
+
1192
+ ${argHint}
1193
+ `;
1194
+ }
1195
+
1196
+ function parseDflowCommandRegistry(content) {
1197
+ const start = content.indexOf(COMMAND_REGISTRY_START);
1198
+ const end = content.indexOf(COMMAND_REGISTRY_END);
1199
+
1200
+ if (start === -1 || end === -1 || end <= start) {
1201
+ throw new InitError('Internal error: dflow command registry markers not found in packaged AI-AGENT-GUIDE.md.');
1202
+ }
1203
+
1204
+ const registryContent = content.slice(start + COMMAND_REGISTRY_START.length, end);
1205
+ const tableLines = registryContent
1206
+ .split(/\r?\n/)
1207
+ .map((line) => line.trim())
1208
+ .filter((line) => line.startsWith('|') && line.endsWith('|'));
1209
+
1210
+ if (tableLines.length < 3) {
1211
+ throw new InitError('Internal error: dflow command registry table is empty.');
1212
+ }
1213
+
1214
+ const header = parseMarkdownTableRow(tableLines[0]).map((cell) => cell.toLowerCase());
1215
+ const expectedHeader = ['id', 'label', 'description', 'arg-hint', 'scope'];
1216
+ if (header.length !== expectedHeader.length || !expectedHeader.every((cell, index) => header[index] === cell)) {
1217
+ throw new InitError('Internal error: dflow command registry header must be: id, label, description, arg-hint, scope.');
1218
+ }
1219
+
1220
+ const delimiter = parseMarkdownTableRow(tableLines[1]);
1221
+ if (delimiter.length !== expectedHeader.length || !delimiter.every((cell) => /^:?-{3,}:?$/.test(cell))) {
1222
+ throw new InitError('Internal error: dflow command registry delimiter row is invalid.');
1223
+ }
1224
+
1225
+ const commands = [];
1226
+ const seen = new Set();
1227
+
1228
+ for (const line of tableLines.slice(2)) {
1229
+ const cells = parseMarkdownTableRow(line);
1230
+ if (cells.length !== expectedHeader.length) {
1231
+ throw new InitError(`Internal error: invalid dflow command registry row: ${line}`);
1232
+ }
1233
+
1234
+ const command = {
1235
+ id: stripInlineCode(cells[0]),
1236
+ label: stripInlineCode(cells[1]),
1237
+ description: stripInlineCode(cells[2]),
1238
+ argHint: stripInlineCode(cells[3]),
1239
+ scope: stripInlineCode(cells[4])
1240
+ };
1241
+
1242
+ validateDflowCommandRegistryRow(command, seen);
1243
+ commands.push(command);
1244
+ }
1245
+
1246
+ const actualIds = commands.map((command) => command.id);
1247
+ const missingIds = EXPECTED_COMMAND_IDS.filter((id) => !actualIds.includes(id));
1248
+ const extraIds = actualIds.filter((id) => !EXPECTED_COMMAND_IDS.includes(id));
1249
+ if (missingIds.length > 0 || extraIds.length > 0 || commands.length !== EXPECTED_COMMAND_IDS.length) {
1250
+ throw new InitError(
1251
+ `Internal error: dflow command registry must contain exactly these command ids: ${EXPECTED_COMMAND_IDS.join(', ')}.`
1252
+ );
1253
+ }
1254
+
1255
+ return commands;
1256
+ }
1257
+
1258
+ function parseMarkdownTableRow(line) {
1259
+ const trimmed = line.trim();
1260
+ const inner = trimmed.slice(1, -1);
1261
+ const cells = [];
1262
+ let current = '';
1263
+
1264
+ for (let index = 0; index < inner.length; index += 1) {
1265
+ const char = inner[index];
1266
+ const next = inner[index + 1];
1267
+
1268
+ if (char === '\\' && next === '|') {
1269
+ current += '|';
1270
+ index += 1;
1271
+ continue;
1272
+ }
1273
+
1274
+ if (char === '|') {
1275
+ cells.push(current.trim());
1276
+ current = '';
1277
+ continue;
1278
+ }
1279
+
1280
+ current += char;
1281
+ }
1282
+
1283
+ cells.push(current.trim());
1284
+ return cells;
1285
+ }
1286
+
1287
+ function stripInlineCode(value) {
1288
+ const trimmed = String(value || '').trim();
1289
+ if (trimmed.startsWith('`') && trimmed.endsWith('`') && trimmed.length >= 2) {
1290
+ return trimmed.slice(1, -1);
1291
+ }
1292
+ return trimmed;
1293
+ }
1294
+
1295
+ function validateDflowCommandRegistryRow(command, seen) {
1296
+ if (!/^[a-z][a-z0-9-]*$/.test(command.id)) {
1297
+ throw new InitError(`Internal error: invalid dflow command id: ${command.id}`);
1298
+ }
1299
+ if (seen.has(command.id)) {
1300
+ throw new InitError(`Internal error: duplicate dflow command id: ${command.id}`);
1301
+ }
1302
+ seen.add(command.id);
1303
+
1304
+ if (command.label !== `/dflow:${command.id}`) {
1305
+ throw new InitError(`Internal error: dflow command label must be /dflow:${command.id}.`);
1306
+ }
1307
+ if (!command.description) {
1308
+ throw new InitError(`Internal error: dflow command ${command.id} is missing a description.`);
1309
+ }
1310
+ if (!command.argHint) {
1311
+ throw new InitError(`Internal error: dflow command ${command.id} is missing an arg-hint.`);
1312
+ }
1313
+ if (!command.scope) {
1314
+ throw new InitError(`Internal error: dflow command ${command.id} is missing a scope.`);
1315
+ }
1316
+ }
1317
+
1070
1318
  function buildDeferredItems(edition) {
1071
1319
  const deferred = [...DEFERRED_COMMON];
1072
1320
  if (edition === 'greenfield') {
@@ -1104,13 +1352,20 @@ async function readPackagedTemplate(edition, sourceRel) {
1104
1352
  }
1105
1353
  }
1106
1354
 
1355
+ const PLACEHOLDER_ALIASES = {
1356
+ '{Framework version}': ['{ASP.NET Core version}', '{ASP.NET WebForms version}', '{.NET Framework version}'],
1357
+ '{ORM / persistence}': ['{ORM / Data Access}'],
1358
+ '{ORM version}': ['{EF Core version}'],
1359
+ '{Mediator}': ['{MediatR version}']
1360
+ };
1361
+
1107
1362
  function buildSubstitutionMap(cwd, answers) {
1108
1363
  const extracted = extractTechStackPlaceholders(answers.techStackSummary);
1109
1364
  const gitSelection = answers.optionalFiles.filter((key) => key === 'git-trunk' || key === 'git-flow');
1110
1365
  const gitStyle = gitSelection.length === 1 ? (gitSelection[0] === 'git-trunk' ? 'trunk' : 'gitflow') : null;
1111
1366
  const systemName = path.basename(cwd);
1112
1367
 
1113
- return new Map([
1368
+ const map = new Map([
1114
1369
  ['{YYYY-MM-DD}', currentLocalDate()],
1115
1370
  ['{System Name}', systemName],
1116
1371
  ['{系統名稱}', systemName],
@@ -1120,15 +1375,26 @@ function buildSubstitutionMap(cwd, answers) {
1120
1375
  ['{migration-context}', answers.migrationContext],
1121
1376
  ['{prose-language}', answers.proseLanguage],
1122
1377
  ['{dflow-version}', pkg.version],
1123
- ['{ASP.NET Core version}', extracted.aspNetCoreVersion || '{ASP.NET Core version}'],
1124
- ['{EF Core version}', extracted.efCoreVersion || '{EF Core version}'],
1125
- ['{MediatR version}', extracted.mediatRVersion || '{MediatR version}'],
1378
+ ['{Language}', extracted.language || '{Language}'],
1379
+ ['{Framework}', extracted.framework || '{Framework}'],
1380
+ ['{Framework version}', extracted.frameworkVersion || '{Framework version}'],
1381
+ ['{ORM / persistence}', extracted.ormPersistence || '{ORM / persistence}'],
1382
+ ['{ORM version}', extracted.ormVersion || '{ORM version}'],
1383
+ ['{Mediator}', extracted.mediator || '{Mediator}'],
1126
1384
  ['{Test framework}', extracted.testFramework || '{Test framework}'],
1127
- ['{ASP.NET WebForms version}', extracted.webFormsVersion || '{ASP.NET WebForms version}'],
1128
- ['{.NET Framework version}', extracted.dotNetFrameworkVersion || '{.NET Framework version}'],
1129
- ['{ORM / Data Access}', extracted.ormDataAccess || '{ORM / Data Access}'],
1130
1385
  ['{gitflow|trunk}', gitStyle || '{gitflow|trunk}']
1131
1386
  ]);
1387
+
1388
+ for (const [canonical, aliases] of Object.entries(PLACEHOLDER_ALIASES)) {
1389
+ const value = map.get(canonical);
1390
+ if (value === undefined) continue;
1391
+ const canonicalResolved = value !== canonical;
1392
+ for (const alias of aliases) {
1393
+ map.set(alias, canonicalResolved ? value : alias);
1394
+ }
1395
+ }
1396
+
1397
+ return map;
1132
1398
  }
1133
1399
 
1134
1400
  function substitutePlaceholders(content, substitution) {
@@ -1139,19 +1405,104 @@ function substitutePlaceholders(content, substitution) {
1139
1405
  return result;
1140
1406
  }
1141
1407
 
1408
+ const LANGUAGE_PATTERNS = [
1409
+ /\bC#\s*[0-9]+(?:\.[0-9]+)?\b/i,
1410
+ /\bC#\b/,
1411
+ /\bTypeScript\s*[0-9]+(?:\.[0-9]+)?\b/i,
1412
+ /\bTypeScript\b/i,
1413
+ /\bJavaScript\b/i,
1414
+ /\bKotlin\s*[0-9]+(?:\.[0-9]+)?\b/i,
1415
+ /\bKotlin\b/i,
1416
+ /\bJava\s*[0-9]+\b/i,
1417
+ /\bJava\b/i,
1418
+ /\bPython\s*[0-9]+(?:\.[0-9]+)?\b/i,
1419
+ /\bPython\b/i,
1420
+ /\bGolang\b/i,
1421
+ /\bGo\s*[0-9]+(?:\.[0-9]+)?\b/i,
1422
+ /\bPHP\s*[0-9]+(?:\.[0-9]+)?\b/i,
1423
+ /\bPHP\b/i,
1424
+ /\bRuby\b/i
1425
+ ];
1426
+
1427
+ const FRAMEWORK_VERSION_PATTERNS = [
1428
+ /\bASP\.?NET\s+Core\s*[0-9]+(?:\.[0-9]+)?\b/i,
1429
+ /\b(?:ASP\.?NET\s+WebForms|WebForms)(?:\s+[0-9]+(?:\.[0-9]+)?)?\b/i,
1430
+ /\b\.NET\s+Framework\s*[0-9]+(?:\.[0-9]+)?\b/i,
1431
+ /\bSpring\s+Boot\s*[0-9]+(?:\.[0-9]+)?\b/i,
1432
+ /\bSpring\s+MVC\b/i,
1433
+ /\bNestJS\s*[0-9]+(?:\.[0-9]+)?\b/i,
1434
+ /\bFastify\s*[0-9]+(?:\.[0-9]+)?\b/i,
1435
+ /\bExpress(?:\.js)?\s*[0-9]+(?:\.[0-9]+)?\b/i,
1436
+ /\bDjango\s*[0-9]+(?:\.[0-9]+)?\b/i,
1437
+ /\bFastAPI\s*[0-9]+(?:\.[0-9]+)?\b/i,
1438
+ /\bFlask\s*[0-9]+(?:\.[0-9]+)?\b/i,
1439
+ /\bGin\s+v?[0-9]+(?:\.[0-9]+)?\b/i,
1440
+ /\bEcho\s+v?[0-9]+(?:\.[0-9]+)?\b/i,
1441
+ /\bLaravel\s*[0-9]+(?:\.[0-9]+)?\b/i
1442
+ ];
1443
+
1444
+ const FRAMEWORK_PATTERNS = [
1445
+ /\bASP\.?NET\s+Core\b/i,
1446
+ /\b(?:ASP\.?NET\s+WebForms|WebForms)\b/i,
1447
+ /\b\.NET\s+Framework\b/i,
1448
+ /\bSpring\s+Boot\b/i,
1449
+ /\bSpring\s+MVC\b/i,
1450
+ /\bNestJS\b/i,
1451
+ /\bFastify\b/i,
1452
+ /\bExpress(?:\.js)?\b/i,
1453
+ /\bDjango\b/i,
1454
+ /\bFastAPI\b/i,
1455
+ /\bFlask\b/i,
1456
+ /\bGin\b/i,
1457
+ /\bEcho\b/i,
1458
+ /\bLaravel\b/i
1459
+ ];
1460
+
1461
+ const ORM_VERSION_PATTERNS = [
1462
+ /\b(?:EF\s+Core|Entity\s+Framework\s+Core)\s*[0-9]+(?:\.[0-9]+)?\b/i,
1463
+ /\b(?:EF6|Entity\s+Framework\s+6)\b/i,
1464
+ /\bHibernate\s*[0-9]+(?:\.[0-9]+)?\b/i,
1465
+ /\bSpring\s+Data\s+JPA\b/i,
1466
+ /\bJPA\s*[0-9]+(?:\.[0-9]+)?\b/i,
1467
+ /\bSQLAlchemy\s*[0-9]+(?:\.[0-9]+)?\b/i,
1468
+ /\bPrisma\s*[0-9]+(?:\.[0-9]+)?\b/i,
1469
+ /\bTypeORM\s*[0-9]+(?:\.[0-9]+)?\b/i,
1470
+ /\bMikro-?ORM\s*[0-9]+(?:\.[0-9]+)?\b/i,
1471
+ /\bGORM\s+v?[0-9]+(?:\.[0-9]+)?\b/i,
1472
+ /\bEloquent\s*[0-9]+(?:\.[0-9]+)?\b/i,
1473
+ /\bDoctrine\s*[0-9]+(?:\.[0-9]+)?\b/i
1474
+ ];
1475
+
1476
+ const ORM_PERSISTENCE_PATTERNS = [
1477
+ /\b(?:EF\s+Core|Entity\s+Framework\s+Core)\b/i,
1478
+ /\b(?:EF6|Entity\s+Framework\s+6|Dapper|ADO\.NET)\b/i,
1479
+ /\bHibernate\b/i,
1480
+ /\bSpring\s+Data\s+JPA\b/i,
1481
+ /\bJPA\b/i,
1482
+ /\bSQLAlchemy\b/i,
1483
+ /\bSQLModel\b/i,
1484
+ /\bPrisma\b/i,
1485
+ /\bTypeORM\b/i,
1486
+ /\bMikro-?ORM\b/i,
1487
+ /\bGORM\b/i,
1488
+ /\bsqlx\b/i,
1489
+ /\bEloquent\b/i,
1490
+ /\bDoctrine\b/i
1491
+ ];
1492
+
1142
1493
  function extractTechStackPlaceholders(text) {
1143
1494
  if (!text || text.toLowerCase() === 'unknown') {
1144
1495
  return {};
1145
1496
  }
1146
1497
 
1147
1498
  return {
1148
- aspNetCoreVersion: firstMatch(text, /\bASP\.?NET\s+Core\s*[0-9]+(?:\.[0-9]+)?\b/i),
1149
- efCoreVersion: firstMatch(text, /\b(?:EF\s+Core|Entity\s+Framework\s+Core)\s*[0-9]+(?:\.[0-9]+)?\b/i),
1150
- mediatRVersion: firstMatch(text, /\bMediatR\s*[0-9]+(?:\.[0-9]+)?\b/i),
1151
- testFramework: extractTestFramework(text),
1152
- webFormsVersion: firstMatch(text, /\b(?:ASP\.?NET\s+WebForms|WebForms)(?:\s*[0-9]+(?:\.[0-9]+)?)?\b/i),
1153
- dotNetFrameworkVersion: firstMatch(text, /\b\.NET\s+Framework\s*[0-9]+(?:\.[0-9]+)?\b/i),
1154
- ormDataAccess: firstMatch(text, /\b(?:EF6|Entity\s+Framework\s+6|Dapper|ADO\.NET)\b/i)
1499
+ language: firstPatternMatch(text, LANGUAGE_PATTERNS),
1500
+ framework: firstPatternMatch(text, FRAMEWORK_PATTERNS),
1501
+ frameworkVersion: firstPatternMatch(text, FRAMEWORK_VERSION_PATTERNS),
1502
+ ormPersistence: firstPatternMatch(text, ORM_PERSISTENCE_PATTERNS),
1503
+ ormVersion: firstPatternMatch(text, ORM_VERSION_PATTERNS),
1504
+ mediator: firstMatch(text, /\bMediatR\s*[0-9]+(?:\.[0-9]+)?\b/i),
1505
+ testFramework: extractTestFramework(text)
1155
1506
  };
1156
1507
  }
1157
1508
 
@@ -1160,15 +1511,38 @@ function firstMatch(text, regex) {
1160
1511
  return match ? match[0] : null;
1161
1512
  }
1162
1513
 
1163
- function extractTestFramework(text) {
1164
- if (/\bxUnit\b/i.test(text)) {
1165
- return 'xUnit';
1166
- }
1167
- if (/\bNUnit\b/i.test(text)) {
1168
- return 'NUnit';
1514
+ function firstPatternMatch(text, patterns) {
1515
+ for (const pattern of patterns) {
1516
+ const match = text.match(pattern);
1517
+ if (match) {
1518
+ return match[0];
1519
+ }
1169
1520
  }
1170
- if (/\bMSTest\b/i.test(text)) {
1171
- return 'MSTest';
1521
+ return null;
1522
+ }
1523
+
1524
+ function extractTestFramework(text) {
1525
+ const patterns = [
1526
+ [/\bxUnit\b/i, 'xUnit'],
1527
+ [/\bNUnit\b/i, 'NUnit'],
1528
+ [/\bMSTest\b/i, 'MSTest'],
1529
+ [/\bJUnit\s*[0-9]+\b/i, null],
1530
+ [/\bJUnit\b/i, 'JUnit'],
1531
+ [/\bVitest\b/i, 'Vitest'],
1532
+ [/\bJest\b/i, 'Jest'],
1533
+ [/\bMocha\b/i, 'Mocha'],
1534
+ [/\bpytest\b/i, 'pytest'],
1535
+ [/\bunittest\b/i, 'unittest'],
1536
+ [/\bgo\s+test\b/i, 'go test'],
1537
+ [/\bPHPUnit\b/i, 'PHPUnit'],
1538
+ [/\bPest\b/i, 'Pest']
1539
+ ];
1540
+
1541
+ for (const [pattern, name] of patterns) {
1542
+ const match = text.match(pattern);
1543
+ if (match) {
1544
+ return name || match[0];
1545
+ }
1172
1546
  }
1173
1547
  return null;
1174
1548
  }
@@ -1271,6 +1645,7 @@ function renderPreview(stdout, plan, warnings) {
1271
1645
  async function writeFilePlan(cwd, plan) {
1272
1646
  const result = {
1273
1647
  created: [],
1648
+ updated: [],
1274
1649
  skipped: [],
1275
1650
  warnings: []
1276
1651
  };
@@ -1280,6 +1655,13 @@ async function writeFilePlan(cwd, plan) {
1280
1655
 
1281
1656
  try {
1282
1657
  if (await pathExists(targetPath)) {
1658
+ if (item.overwrite) {
1659
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
1660
+ await fs.writeFile(targetPath, item.content);
1661
+ result.updated.push(item.relativePath);
1662
+ continue;
1663
+ }
1664
+
1283
1665
  result.skipped.push(item.relativePath);
1284
1666
  result.warnings.push(`Skipped existing target: ${item.relativePath}`);
1285
1667
  if (item.relativePath === 'dflow/specs/shared/_conventions.md') {
@@ -1366,6 +1748,9 @@ function printResultReport(stdout, result, deferred) {
1366
1748
  stdout.write('\nCreated:\n');
1367
1749
  printList(stdout, result.created);
1368
1750
 
1751
+ stdout.write('\nUpdated:\n');
1752
+ printList(stdout, result.updated);
1753
+
1369
1754
  stdout.write('\nSkipped:\n');
1370
1755
  printList(stdout, result.skipped);
1371
1756
 
@@ -1386,10 +1771,15 @@ Recommended next steps:
1386
1771
  - For a new feature, use the Dflow new-feature workflow when it becomes available as a CLI command.
1387
1772
  - For brownfield changes, use the Dflow modify-existing workflow when it becomes available as a CLI command.
1388
1773
  - Before generating more specs, make sure dflow/specs/shared/_conventions.md has the correct Prose Language section.
1774
+ - For stack-specific examples (.NET, Java/Spring, Node/TypeScript, Python, Go, PHP/Laravel), see docs/examples-by-stack.md in the Dflow repo.
1389
1775
  `);
1390
1776
  }
1391
1777
 
1392
- function printConfigureAgentsNextSteps(stdout) {
1778
+ function printConfigureAgentsNextSteps(stdout, commandAdapters = false) {
1779
+ const commandAdapterStep = commandAdapters
1780
+ ? '- Command adapters use adapter-native names such as `/dflow-new-feature`; canonical `/dflow:*` names remain defined in dflow/specs/shared/AI-AGENT-GUIDE.md.\n'
1781
+ : '';
1782
+
1393
1783
  stdout.write(`
1394
1784
  Dflow AI agent configuration complete.
1395
1785
 
@@ -1397,6 +1787,7 @@ Recommended next steps:
1397
1787
  - Keep AI-agent-specific root files small.
1398
1788
  - Put durable workflow changes in dflow/specs/shared/AI-AGENT-GUIDE.md.
1399
1789
  - If a merge snippet was created, review it and merge the pointer into the existing tool instruction file.
1790
+ ${commandAdapterStep}
1400
1791
  `);
1401
1792
  }
1402
1793
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dflow-sdd-ddd",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Spec-first SDD/DDD workflow kit for AI-assisted development",
5
5
  "type": "commonjs",
6
6
  "bin": {