@mettlecast/domain-cli 0.2.59 → 0.2.61

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 (119) hide show
  1. package/dist/builder/build-registry.d.ts +1 -1
  2. package/dist/builder/build-registry.js +129 -49
  3. package/dist/builder/build-types.d.ts +1 -1
  4. package/dist/builder/load-module.d.ts +1 -1
  5. package/dist/builder/load-module.js +3 -3
  6. package/dist/cli.js +3 -3
  7. package/dist/commands/add-api.js +2 -2
  8. package/dist/commands/add-domain.js +4 -4
  9. package/dist/commands/add-fixture-factory.js +5 -6
  10. package/dist/commands/build-catalog.d.ts +8 -24
  11. package/dist/commands/build-catalog.js +12 -23
  12. package/dist/commands/build-flows.js +1 -1
  13. package/dist/commands/build.js +7 -6
  14. package/dist/commands/check-hashes.js +2 -2
  15. package/dist/commands/create-project.js +1 -1
  16. package/dist/commands/dev.js +1 -1
  17. package/dist/commands/doctor.d.ts +5 -7
  18. package/dist/commands/doctor.js +110 -202
  19. package/dist/commands/explain.js +13 -13
  20. package/dist/commands/generate-openapi.d.ts +10 -1
  21. package/dist/commands/generate-openapi.js +19 -33
  22. package/dist/commands/power-tune.js +2 -2
  23. package/dist/commands/show-dns.d.ts +1 -1
  24. package/dist/commands/show-dns.js +5 -5
  25. package/dist/commands/show.d.ts +2 -3
  26. package/dist/commands/show.js +0 -2
  27. package/dist/commands/test.js +0 -1
  28. package/dist/commands/upgrade-backend.js +3 -3
  29. package/dist/commands/upgrade.js +17 -11
  30. package/dist/commands/validate.js +144 -39
  31. package/dist/server/api-server.d.ts +1 -1
  32. package/dist/server/mount-routes.d.ts +11 -2
  33. package/dist/server/mount-routes.js +20 -8
  34. package/dist/templates/api-skeleton.d.ts +5 -0
  35. package/dist/templates/api-skeleton.js +28 -27
  36. package/dist/templates/claude-md.js +1 -1
  37. package/dist/templates/patterns/api/create-with-event.d.ts +4 -0
  38. package/dist/templates/patterns/api/create-with-event.js +38 -32
  39. package/dist/templates/patterns/api/idempotent-mutation.d.ts +4 -0
  40. package/dist/templates/patterns/api/idempotent-mutation.js +47 -41
  41. package/dist/templates/patterns/api/paginated-list.d.ts +4 -0
  42. package/dist/templates/patterns/api/paginated-list.js +30 -24
  43. package/dist/templates/patterns/api/simple-crud.d.ts +4 -0
  44. package/dist/templates/patterns/api/simple-crud.js +46 -35
  45. package/dist/templates/patterns/api/streaming-list.d.ts +4 -0
  46. package/dist/templates/patterns/api/streaming-list.js +46 -41
  47. package/dist/templates/patterns/api/system-admin.d.ts +4 -0
  48. package/dist/templates/patterns/api/system-admin.js +59 -52
  49. package/dist/templates/patterns/api/webhook-receiver-style.d.ts +4 -0
  50. package/dist/templates/patterns/api/webhook-receiver-style.js +43 -35
  51. package/dist/types.d.ts +100 -0
  52. package/dist/types.js +1 -0
  53. package/dist/utils/file-helpers.d.ts +0 -2
  54. package/dist/utils/file-helpers.js +2 -3
  55. package/dist/utils/header-inject.js +2 -2
  56. package/dist/utils/install-file.d.ts +1 -1
  57. package/dist/utils/install-file.js +1 -1
  58. package/dist/utils/manifest.js +1 -2
  59. package/dist/utils/scaffold-config.d.ts +8 -2
  60. package/dist/utils/scaffold-config.js +3 -1
  61. package/package.json +1 -1
  62. package/src/__tests__/build-registry.test.ts +43 -20
  63. package/src/__tests__/build-types.test.ts +4 -7
  64. package/src/__tests__/builder/walkDomainDir.test.ts +19 -21
  65. package/src/__tests__/commands/add-api.test.ts +12 -10
  66. package/src/__tests__/commands/add-domain.test.ts +8 -5
  67. package/src/__tests__/commands/check-hashes.test.ts +9 -9
  68. package/src/__tests__/commands/create-project.test.ts +5 -5
  69. package/src/__tests__/commands/dev.test.ts +0 -1
  70. package/src/__tests__/commands/upgrade.test.ts +7 -7
  71. package/src/__tests__/doctor.test.ts +60 -67
  72. package/src/__tests__/mount-routes.test.ts +64 -23
  73. package/src/__tests__/package-freshness.test.ts +94 -0
  74. package/src/__tests__/scaffold-src/part-a-layout.test.ts +10 -10
  75. package/src/__tests__/scripts/package-scaffold.test.ts +5 -5
  76. package/src/__tests__/smoke/scaffold.test.ts +13 -15
  77. package/src/__tests__/utils/install-file.test.ts +2 -2
  78. package/src/__tests__/utils/manifest.test.ts +2 -2
  79. package/src/__tests__/validate.test.ts +570 -1
  80. package/src/builder/build-registry.ts +154 -59
  81. package/src/builder/build-types.ts +1 -1
  82. package/src/builder/load-module.ts +3 -3
  83. package/src/cli.ts +4 -4
  84. package/src/commands/add-api.ts +2 -2
  85. package/src/commands/add-domain.ts +4 -4
  86. package/src/commands/add-fixture-factory.ts +5 -6
  87. package/src/commands/build-catalog.ts +18 -40
  88. package/src/commands/build-flows.ts +1 -1
  89. package/src/commands/build.ts +8 -7
  90. package/src/commands/check-hashes.ts +2 -2
  91. package/src/commands/create-project.ts +1 -1
  92. package/src/commands/dev.ts +1 -1
  93. package/src/commands/doctor.ts +120 -218
  94. package/src/commands/explain.ts +13 -13
  95. package/src/commands/generate-openapi.ts +30 -52
  96. package/src/commands/power-tune.ts +2 -2
  97. package/src/commands/show-dns.ts +5 -5
  98. package/src/commands/show.ts +2 -5
  99. package/src/commands/test.ts +0 -1
  100. package/src/commands/upgrade-backend.ts +3 -3
  101. package/src/commands/upgrade.ts +16 -10
  102. package/src/commands/validate.ts +180 -40
  103. package/src/server/api-server.ts +1 -1
  104. package/src/server/mount-routes.ts +21 -10
  105. package/src/templates/api-skeleton.ts +29 -28
  106. package/src/templates/claude-md.ts +1 -1
  107. package/src/templates/patterns/api/create-with-event.ts +39 -33
  108. package/src/templates/patterns/api/idempotent-mutation.ts +48 -42
  109. package/src/templates/patterns/api/paginated-list.ts +31 -25
  110. package/src/templates/patterns/api/simple-crud.ts +47 -36
  111. package/src/templates/patterns/api/streaming-list.ts +47 -42
  112. package/src/templates/patterns/api/system-admin.ts +60 -53
  113. package/src/templates/patterns/api/webhook-receiver-style.ts +48 -40
  114. package/src/types.ts +128 -0
  115. package/src/utils/file-helpers.ts +2 -5
  116. package/src/utils/header-inject.ts +2 -2
  117. package/src/utils/install-file.ts +1 -1
  118. package/src/utils/manifest.ts +1 -2
  119. package/src/utils/scaffold-config.ts +12 -3
@@ -59,8 +59,10 @@ async function checkHandlersUseResult(projectRoot) {
59
59
  const missing = [];
60
60
  for (const file of apiFiles) {
61
61
  const content = await readFile(file, 'utf8');
62
- // A file contains a handler export — check the return type
63
- if (/defineApi\s*\(/.test(content)) {
62
+ // A file contains a handler export — check the return type.
63
+ // Issue #4689: defineApi was removed. The action-first
64
+ // contract uses defineAction with exposure.type='api'.
65
+ if (/defineAction\s*\(/.test(content)) {
64
66
  // Look for Result<T> in the handler's return type annotation
65
67
  if (!/: .*Result</.test(content)) {
66
68
  missing.push(relative(projectRoot, file));
@@ -157,15 +159,14 @@ async function checkGeneratedClientsFresh(projectRoot) {
157
159
  */
158
160
  export async function runDoctor(opts = {}) {
159
161
  const projectRoot = opts.projectRoot ?? process.cwd();
160
- // --relocate runs ONLY the relocation routine (existing behaviour).
161
- // --fix is a superset: it runs relocation first, then the full report.
162
+ // --relocate is retained as a safe no-op for older automation.
163
+ // --fix runs that no-op preflight first, then the full report.
162
164
  // When both are set, --fix takes precedence so the report still runs.
163
165
  if (opts.relocate && !opts.fix) {
164
166
  return runRelocate(projectRoot);
165
167
  }
166
- // --fix preflight: run the relocation routine and merge its checks into
167
- // the doctor report. On a clean project (no files outside .mc/ to move)
168
- // runRelocate returns a no-op summary check (0 moved, 0 warned, 8 skipped).
168
+ // --fix preflight: run the legacy relocation no-op and merge its check into
169
+ // the doctor report.
169
170
  let preflightChecks = [];
170
171
  if (opts.fix) {
171
172
  const relocationReport = await runRelocate(projectRoot);
@@ -362,38 +363,52 @@ async function checkNoCrossDomainImports(projectRoot) {
362
363
  async function checkApisHaveVersions(projectRoot) {
363
364
  try {
364
365
  const domainsDir = join(projectRoot, 'domains');
365
- const allApiFiles = [];
366
- // Find all API files in all domains
366
+ const allActionFiles = [];
367
+ // Find API-exposed action files in both actions/ and api/ directories.
368
+ // Issue #4689: defineApi was removed. API-exposed actions live in
369
+ // domains/*/actions/ but legacy api/ directories may still exist.
367
370
  const entries = await readdir(domainsDir, { withFileTypes: true });
368
371
  for (const entry of entries) {
369
372
  if (!entry.isDirectory())
370
373
  continue;
371
- const apiDir = join(domainsDir, entry.name, 'api');
372
- const apiFiles = findFiles(apiDir, /\.ts$/);
373
- allApiFiles.push(...apiFiles);
374
+ for (const dir of ['actions', 'api']) {
375
+ const actionDir = join(domainsDir, entry.name, dir);
376
+ const files = findFiles(actionDir, /\.ts$/);
377
+ allActionFiles.push(...files);
378
+ }
374
379
  }
375
- const apiFiles = allApiFiles;
376
- if (apiFiles.length === 0) {
380
+ // Only consider files that define an API-exposed action (defineAction
381
+ // with exposure.type === 'api' or exposure: { type: 'api' }).
382
+ const apiExposedFiles = [];
383
+ for (const file of allActionFiles) {
384
+ const content = await readFile(file, 'utf8');
385
+ if (/defineAction\s*\(/.test(content) && /exposure\s*:\s*\{\s*type\s*:\s*['"]api['"]/.test(content)) {
386
+ apiExposedFiles.push(file);
387
+ }
388
+ }
389
+ if (apiExposedFiles.length === 0) {
377
390
  return {
378
391
  name: 'APIs declare versions',
379
392
  status: 'PASS',
380
- message: 'No APIs found (optional)',
393
+ message: 'No API-exposed actions found (optional)',
381
394
  kNodeRef: 'K:runbook:add-domain',
382
395
  };
383
396
  }
384
- // Simple heuristic: grep for 'versions:' in each API file
385
- let missingVersions = 0;
386
- for (const file of apiFiles) {
397
+ // Issue #4689: action-first contract requires input/output Zod schemas
398
+ // with `.default({...})` example data. Check that each API-exposed
399
+ // action file contains `.default(` (indicating example data).
400
+ let missingDefaults = 0;
401
+ for (const file of apiExposedFiles) {
387
402
  const content = await readFile(file, 'utf8');
388
- if (!content.includes('versions:')) {
389
- missingVersions++;
403
+ if (!content.includes('.default(')) {
404
+ missingDefaults++;
390
405
  }
391
406
  }
392
- if (missingVersions === 0) {
407
+ if (missingDefaults === 0) {
393
408
  return {
394
409
  name: 'APIs declare versions',
395
410
  status: 'PASS',
396
- message: `All ${apiFiles.length} API files declare versions`,
411
+ message: `All ${apiExposedFiles.length} API-exposed action(s) have .default() example data`,
397
412
  kNodeRef: 'K:runbook:add-domain',
398
413
  };
399
414
  }
@@ -401,8 +416,8 @@ async function checkApisHaveVersions(projectRoot) {
401
416
  return {
402
417
  name: 'APIs declare versions',
403
418
  status: 'FAIL',
404
- message: `${missingVersions}/${apiFiles.length} API files missing versions: field`,
405
- fixHint: 'Add versions: { v1: { ... } } to defineApi calls',
419
+ message: `${missingDefaults}/${apiExposedFiles.length} API-exposed action(s) missing .default() example data. Add \`.default({...})\` to the top-level input and output Zod schemas in each defineAction({ exposure: { type: 'api', ... } }) call.`,
420
+ fixHint: "Add `input` and `output` Zod schemas with `.default({...})` to defineAction({ exposure: { type: 'api', ... } }) calls",
406
421
  kNodeRef: 'K:runbook:add-domain',
407
422
  };
408
423
  }
@@ -411,7 +426,7 @@ async function checkApisHaveVersions(projectRoot) {
411
426
  return {
412
427
  name: 'APIs declare versions',
413
428
  status: 'WARN',
414
- message: `Could not check API versions: ${String(err)}`,
429
+ message: `Could not check API schemas: ${String(err)}`,
415
430
  kNodeRef: 'K:runbook:add-domain',
416
431
  };
417
432
  }
@@ -419,28 +434,39 @@ async function checkApisHaveVersions(projectRoot) {
419
434
  async function checkApisHaveTenancy(projectRoot) {
420
435
  try {
421
436
  const domainsDir = join(projectRoot, 'domains');
422
- const allApiFiles = [];
423
- // Find all API files in all domains
437
+ const allActionFiles = [];
438
+ // Find API-exposed action files in both actions/ and api/ directories.
439
+ // Issue #4689: defineApi was removed. Tenancy is now declared on the
440
+ // action's exposure block via `exposure.tenancy`.
424
441
  const entries = await readdir(domainsDir, { withFileTypes: true });
425
442
  for (const entry of entries) {
426
443
  if (!entry.isDirectory())
427
444
  continue;
428
- const apiDir = join(domainsDir, entry.name, 'api');
429
- const apiFiles = findFiles(apiDir, /\.ts$/);
430
- allApiFiles.push(...apiFiles);
445
+ for (const dir of ['actions', 'api']) {
446
+ const actionDir = join(domainsDir, entry.name, dir);
447
+ const files = findFiles(actionDir, /\.ts$/);
448
+ allActionFiles.push(...files);
449
+ }
431
450
  }
432
- const apiFiles = allApiFiles;
433
- if (apiFiles.length === 0) {
451
+ // Only consider files that define an API-exposed action.
452
+ const apiExposedFiles = [];
453
+ for (const file of allActionFiles) {
454
+ const content = await readFile(file, 'utf8');
455
+ if (/defineAction\s*\(/.test(content) && /exposure\s*:\s*\{\s*type\s*:\s*['"]api['"]/.test(content)) {
456
+ apiExposedFiles.push(file);
457
+ }
458
+ }
459
+ if (apiExposedFiles.length === 0) {
434
460
  return {
435
461
  name: 'APIs declare tenancy',
436
462
  status: 'PASS',
437
- message: 'No APIs found (optional)',
463
+ message: 'No API-exposed actions found (optional)',
438
464
  kNodeRef: 'K:convention:tier-1-foundations',
439
465
  };
440
466
  }
441
- // Simple heuristic: grep for 'tenancy:' in each API file
467
+ // Check for `tenancy:` anywhere in each API-exposed action file.
442
468
  let missingTenancy = 0;
443
- for (const file of apiFiles) {
469
+ for (const file of apiExposedFiles) {
444
470
  const content = await readFile(file, 'utf8');
445
471
  if (!content.includes('tenancy:')) {
446
472
  missingTenancy++;
@@ -450,7 +476,7 @@ async function checkApisHaveTenancy(projectRoot) {
450
476
  return {
451
477
  name: 'APIs declare tenancy',
452
478
  status: 'PASS',
453
- message: `All ${apiFiles.length} API files declare tenancy`,
479
+ message: `All ${apiExposedFiles.length} API-exposed action(s) declare tenancy`,
454
480
  kNodeRef: 'K:convention:tier-1-foundations',
455
481
  };
456
482
  }
@@ -458,8 +484,8 @@ async function checkApisHaveTenancy(projectRoot) {
458
484
  return {
459
485
  name: 'APIs declare tenancy',
460
486
  status: 'FAIL',
461
- message: `${missingTenancy}/${apiFiles.length} API files missing tenancy: field`,
462
- fixHint: "Add tenancy: 'required' | 'none' | 'system' to defineApi calls",
487
+ message: `${missingTenancy}/${apiExposedFiles.length} API-exposed action(s) missing tenancy. Tenancy must be declared on the action's exposure block: \`exposure.tenancy: 'required' | 'none' | 'system'\`.`,
488
+ fixHint: "Add `exposure.tenancy: 'required' | 'none' | 'system'` to defineAction({ exposure: { type: 'api', ... } }) calls",
463
489
  kNodeRef: 'K:convention:tier-1-foundations',
464
490
  };
465
491
  }
@@ -475,13 +501,21 @@ async function checkApisHaveTenancy(projectRoot) {
475
501
  }
476
502
  async function checkScaffoldConfigMatchesDisk(projectRoot) {
477
503
  try {
478
- const configPath = join(projectRoot, '.tib', 'scaffold-config.json');
504
+ const configPath = join(projectRoot, '.mc', 'scaffold-config.json');
479
505
  const domainsDir = join(projectRoot, 'domains');
480
506
  let configDomains = [];
481
507
  try {
482
508
  const configContent = await readFile(configPath, 'utf8');
483
509
  const config = JSON.parse(configContent);
484
- configDomains = config.domainIds ?? [];
510
+ if (!Array.isArray(config.domainIds)) {
511
+ return {
512
+ name: 'scaffold-config.json matches on-disk',
513
+ status: 'PASS',
514
+ message: '.mc/scaffold-config.json has no domainIds array — domain list sync check skipped',
515
+ kNodeRef: 'K:runbook:add-domain',
516
+ };
517
+ }
518
+ configDomains = config.domainIds;
485
519
  }
486
520
  catch {
487
521
  return {
@@ -769,7 +803,7 @@ async function checkAllRoutesUseTanStackRouter(projectRoot) {
769
803
  }
770
804
  /**
771
805
  * Check W5-4: Lambda handler files contain initOtel() call.
772
- * Scans `domains/*\/api/*.ts` and FAILs if any handler is missing initOtel().
806
+ * Scans `domains/*\/actions/*.ts` and `domains/*\/api/*.ts` and FAILs if any handler is missing initOtel().
773
807
  */
774
808
  async function checkOtelInitInLambdas(projectRoot) {
775
809
  try {
@@ -787,9 +821,11 @@ async function checkOtelInitInLambdas(projectRoot) {
787
821
  for (const entry of entries) {
788
822
  if (!entry.isDirectory())
789
823
  continue;
790
- const apiDir = join(domainsDir, entry.name, 'api');
791
- const files = findFiles(apiDir, /\.ts$/);
792
- handlerFiles.push(...files);
824
+ for (const dir of ['actions', 'api']) {
825
+ const handlerDir = join(domainsDir, entry.name, dir);
826
+ const files = findFiles(handlerDir, /\.ts$/);
827
+ handlerFiles.push(...files);
828
+ }
793
829
  }
794
830
  if (handlerFiles.length === 0) {
795
831
  return { name: 'OTel init in Lambdas', status: 'PASS',
@@ -1114,7 +1150,7 @@ async function checkDomainBrainReachable(projectRoot) {
1114
1150
  */
1115
1151
  async function checkActionsHaveTypes(projectRoot) {
1116
1152
  try {
1117
- const typesPath = join(projectRoot, '.tib', 'actions-types.d.ts');
1153
+ const typesPath = join(projectRoot, '.mc', 'actions-types.d.ts');
1118
1154
  const domainsDir = join(projectRoot, 'domains');
1119
1155
  const discoveredActions = [];
1120
1156
  try {
@@ -1347,7 +1383,7 @@ async function checkSeedPagesHaveNoHeaders(projectRoot) {
1347
1383
  }
1348
1384
  /**
1349
1385
  * Check Part C: Layout validation checks
1350
- * 1. owned files in unexpected locations
1386
+ * 1. managed files are not registered in obsolete legacy locations
1351
1387
  * 2. tracked/seed files inside .mc/
1352
1388
  * 3. manifest entries pointing at missing files
1353
1389
  * 4. scaffold-tracked files (with @mc-scaffold: header) that lack manifest entries
@@ -1361,25 +1397,26 @@ async function checkLayoutPolicy(projectRoot) {
1361
1397
  // No manifest — skip layout checks
1362
1398
  return [];
1363
1399
  }
1364
- // Check 1: owned files in unexpected locations
1365
- // Policy: owned files should be in .mc/ OR .github/workflows/tib-*
1366
- const ownedOutsideTib = manifest.files.filter(f => f.policy === 'managed' &&
1367
- !f.path.startsWith('.mc/') &&
1368
- !/^\.github\/workflows\/tib-/.test(f.path));
1369
- if (ownedOutsideTib.length === 0) {
1400
+ // Check 1: managed files should not be registered in obsolete legacy
1401
+ // locations. Current scaffold manifests intentionally track managed files at
1402
+ // their installed paths (infra/modules, domains, .github, CLAUDE.md, etc.).
1403
+ const legacyManagedLocations = manifest.files.filter(f => f.policy === 'managed' && (f.path.startsWith('.tib/') ||
1404
+ f.path.startsWith('.mc/infra/') ||
1405
+ f.path === 'mc-deploy.yml'));
1406
+ if (legacyManagedLocations.length === 0) {
1370
1407
  results.push({
1371
- name: 'Layout: owned files in .mc/',
1408
+ name: 'Layout: no managed files in legacy locations',
1372
1409
  status: 'PASS',
1373
- message: 'All owned scaffold files are under .mc/ or .github/workflows/tib-*',
1410
+ message: 'Managed scaffold files use current manifest paths',
1374
1411
  });
1375
1412
  }
1376
1413
  else {
1377
- for (const f of ownedOutsideTib) {
1414
+ for (const f of legacyManagedLocations) {
1378
1415
  results.push({
1379
- name: 'Layout: owned files in .mc/',
1416
+ name: 'Layout: no managed files in legacy locations',
1380
1417
  status: 'FAIL',
1381
- message: `Owned scaffold file outside .mc/: ${f.path}`,
1382
- fixHint: `Run 'tib doctor --relocate' to migrate ${f.path} to .mc/`,
1418
+ message: `Managed scaffold file registered in legacy location: ${f.path}`,
1419
+ fixHint: 'Run `npx mc-domain-module upgrade` to refresh scaffold metadata',
1383
1420
  file: f.path,
1384
1421
  });
1385
1422
  }
@@ -1428,7 +1465,7 @@ async function checkLayoutPolicy(projectRoot) {
1428
1465
  name: 'Layout: manifest entries exist on disk',
1429
1466
  status: 'FAIL',
1430
1467
  message: `${missingFiles.length} manifest entry/entries point to missing files:\n ${missingFiles.join('\n ')}`,
1431
- fixHint: 'Run tib doctor --relocate or tib upgrade to fix missing files',
1468
+ fixHint: 'Run `npx mc-domain-module upgrade` to refresh scaffold metadata, or restore the missing files',
1432
1469
  });
1433
1470
  }
1434
1471
  // Check 4: scaffold-tracked files (with @mc-scaffold: header) that lack manifest entries
@@ -1483,154 +1520,25 @@ async function checkLayoutPolicy(projectRoot) {
1483
1520
  name: 'Layout: scaffold-tracked files have manifest entries',
1484
1521
  status: 'WARN',
1485
1522
  message: `${untracked.length} file(s) with @mc-scaffold: header not in manifest:\n ${untracked.join('\n ')}`,
1486
- fixHint: 'Run tib upgrade or tib doctor --relocate to register these files',
1523
+ fixHint: 'Run `npx mc-domain-module upgrade` to refresh scaffold metadata',
1487
1524
  });
1488
1525
  }
1489
1526
  return results;
1490
1527
  }
1491
1528
  /**
1492
- * Old path new path mapping for known scaffold files
1493
- */
1494
- const RELOCATION_MAP = [
1495
- { oldPath: 'infra/modules/app.ts', newPath: '.mc/infra/modules/app.ts', policy: 'managed' },
1496
- { oldPath: 'infra/modules/shared/SharedStack.ts', newPath: '.mc/infra/modules/shared/SharedStack.ts', policy: 'managed' },
1497
- { oldPath: 'infra/modules/domains/dispatch-middleware.ts', newPath: '.mc/infra/modules/domains/dispatch-middleware.ts', policy: 'managed' },
1498
- { oldPath: 'infra/modules/PowerTuningStack.ts', newPath: '.mc/infra/modules/PowerTuningStack.ts', policy: 'managed' },
1499
- { oldPath: 'infra/cdk.json', newPath: '.mc/infra/cdk.json', policy: 'managed' },
1500
- { oldPath: 'infra/tsconfig.json', newPath: '.mc/infra/tsconfig.json', policy: 'managed' },
1501
- { oldPath: 'infra/package.json', newPath: '.mc/infra/package.json', policy: 'managed' },
1502
- { oldPath: 'mc-deploy.yml', newPath: '.github/workflows/mc-deploy.yml', policy: 'managed' },
1503
- ];
1504
- /**
1505
- * Run relocation of old-layout scaffold files to new layout
1529
+ * Legacy relocation entrypoint retained for older automation.
1530
+ *
1531
+ * Earlier scaffold versions tried to move generated infra into .mc/infra. The
1532
+ * current scaffold contract keeps generated infra under infra/modules and uses
1533
+ * .mc/manifest.json plus .mc/modules-hashes.json for ownership/drift tracking.
1534
+ * Moving files here would corrupt modern projects, so relocation is now a no-op.
1506
1535
  */
1507
1536
  async function runRelocate(projectRoot) {
1508
- const checks = [];
1509
- const { readManifest, writeManifest, upsertManifestFile } = await import('../utils/manifest.js');
1510
- const { readFile, writeFile, mkdir, unlink } = await import('node:fs/promises');
1511
- const { computeChecksumFile, computeChecksumString } = await import('../utils/checksum.js');
1512
- const { join, dirname } = await import('node:path');
1513
- const manifest = await readManifest(projectRoot);
1514
- if (!manifest) {
1515
- checks.push({
1516
- name: 'Relocate: manifest present',
1517
- status: 'FAIL',
1518
- message: '.mc/manifest.json not found — cannot relocate without a manifest',
1519
- fixHint: 'This project may not be a TIB scaffold project',
1520
- });
1521
- return { checks, exitCode: 1, pass: false };
1522
- }
1523
- let moved = 0;
1524
- let warned = 0;
1525
- let skipped = 0;
1526
- for (const { oldPath, newPath, policy } of RELOCATION_MAP) {
1527
- const oldAbsPath = join(projectRoot, oldPath);
1528
- const newAbsPath = join(projectRoot, newPath);
1529
- // Check if old file exists
1530
- const { access: fsAccess } = await import('node:fs/promises');
1531
- try {
1532
- await fsAccess(oldAbsPath);
1533
- }
1534
- catch {
1535
- skipped++;
1536
- continue;
1537
- }
1538
- // Check if it has the scaffold header (confirm it's scaffold-owned)
1539
- let content;
1540
- try {
1541
- content = await readFile(oldAbsPath, 'utf-8');
1542
- }
1543
- catch {
1544
- skipped++;
1545
- continue;
1546
- }
1547
- if (!content.includes('@mc-scaffold:')) {
1548
- // File exists but no header — user-owned, leave it
1549
- checks.push({
1550
- name: 'Relocate: checking ' + oldPath,
1551
- status: 'WARN',
1552
- message: `${oldPath} has no @mc-scaffold: header — left in place (user-owned)`,
1553
- file: oldPath,
1554
- });
1555
- warned++;
1556
- continue;
1557
- }
1558
- // Check for user drift
1559
- const manifestEntry = manifest.files.find(f => f.path === oldPath);
1560
- if (manifestEntry) {
1561
- const diskSha = await computeChecksumFile(oldAbsPath);
1562
- if (diskSha && diskSha !== manifestEntry.sha256) {
1563
- // File has been modified — leave with warning
1564
- checks.push({
1565
- name: 'Relocate: checking ' + oldPath,
1566
- status: 'WARN',
1567
- message: `${oldPath} has local modifications — left in place. Migrate manually: move to ${newPath}`,
1568
- file: oldPath,
1569
- });
1570
- warned++;
1571
- continue;
1572
- }
1573
- }
1574
- // Check if destination already exists
1575
- try {
1576
- await fsAccess(newAbsPath);
1577
- checks.push({
1578
- name: 'Relocate: ' + oldPath,
1579
- status: 'WARN',
1580
- message: `${newPath} already exists — skipping move of ${oldPath}`,
1581
- file: newPath,
1582
- });
1583
- warned++;
1584
- continue;
1585
- }
1586
- catch { /* destination doesn't exist, good */ }
1587
- // Move the file
1588
- try {
1589
- await mkdir(dirname(newAbsPath), { recursive: true });
1590
- // Copy + delete (rename may fail across filesystems)
1591
- await writeFile(newAbsPath, content, 'utf-8');
1592
- // Update manifest: remove old entry, add new with new path + policy
1593
- manifest.files = manifest.files.filter(f => f.path !== oldPath);
1594
- const sha256 = manifestEntry?.sha256 ?? computeChecksumString(content);
1595
- upsertManifestFile(manifest, {
1596
- path: newPath,
1597
- module: manifestEntry?.module ?? 'domain',
1598
- moduleVersion: manifestEntry?.moduleVersion ?? '1.0.0',
1599
- sha256,
1600
- wasTemplate: manifestEntry?.wasTemplate ?? false,
1601
- installedAt: manifestEntry?.installedAt ?? new Date().toISOString(),
1602
- policy,
1603
- });
1604
- // Delete old file
1605
- await unlink(oldAbsPath);
1606
- checks.push({
1607
- name: 'Relocate: ' + oldPath,
1608
- status: 'PASS',
1609
- message: `Moved ${oldPath} → ${newPath} (policy: ${policy})`,
1610
- file: newPath,
1611
- });
1612
- moved++;
1613
- }
1614
- catch (err) {
1615
- checks.push({
1616
- name: 'Relocate: ' + oldPath,
1617
- status: 'FAIL',
1618
- message: `Failed to move ${oldPath} → ${newPath}: ${String(err)}`,
1619
- file: oldPath,
1620
- });
1621
- }
1622
- }
1623
- // Write updated manifest
1624
- if (moved > 0) {
1625
- await writeManifest(projectRoot, manifest);
1626
- }
1627
- // Summary check
1628
- checks.push({
1629
- name: 'Relocate: summary',
1630
- status: warned > 0 ? 'WARN' : 'PASS',
1631
- message: `Relocation complete: ${moved} file(s) moved, ${warned} warning(s), ${skipped} skipped (not present)`,
1632
- });
1633
- const hasFail = checks.some(c => c.status === 'FAIL');
1634
- const exitCode = hasFail ? 1 : 0;
1635
- return { checks, exitCode, pass: exitCode === 0 };
1537
+ void projectRoot;
1538
+ const checks = [{
1539
+ name: 'Relocate: deprecated no-op',
1540
+ status: 'PASS',
1541
+ message: 'No files moved. Current scaffold layout keeps generated files at their installed paths and tracks ownership via .mc/manifest.json.',
1542
+ }];
1543
+ return { checks, exitCode: 0, pass: true };
1636
1544
  }
@@ -31,24 +31,24 @@ const RULE_EXPLANATIONS = {
31
31
  'no-raw-http-server': {
32
32
  ruleId: 'no-raw-http-server',
33
33
  title: 'No raw HTTP server in domain code',
34
- description: 'Domain code must not create raw HTTP servers (express, fastify, etc.). All HTTP handling goes through defineApi which is wired to API Gateway by the scaffold.',
34
+ description: 'Domain code must not create raw HTTP servers (express, fastify, etc.). All HTTP handling goes through defineAction with exposure.type=\'api\' which is wired to API Gateway by the scaffold.',
35
35
  severity: 'error',
36
36
  category: 'structure',
37
37
  kNodeRef: 'K:convention:tier-1-foundations',
38
- fixHint: 'Wrap your HTTP handler with defineApi({ ... }). The scaffold handles API Gateway wiring.',
38
+ fixHint: 'Wrap your HTTP handler with defineAction({ exposure: { type: \'api\', ... } }). The scaffold handles API Gateway wiring.',
39
39
  exampleBad: "import express from 'express';\nconst app = express();",
40
- exampleGood: "export const myApi = defineApi({ id: 'my-api', path: '/v1/my-api', ... });",
40
+ exampleGood: "export const myAction = defineAction({ id: 'my-action', exposure: { type: 'api', path: '/v1/my-action', method: 'GET', auth: 'required', tenancy: 'required' }, ... });",
41
41
  },
42
42
  'require-define-primitive': {
43
43
  ruleId: 'require-define-primitive',
44
44
  title: 'Require define primitive factories',
45
- description: 'All domain handlers must use the appropriate factory function: defineApi for HTTP APIs, defineSubscriber for event subscribers, defineAction for cross-domain actions, defineJob for background jobs.',
45
+ description: 'All domain handlers must use the appropriate factory function: defineAction (HTTP APIs via exposure.type=\'api\', or internal-only actions), defineSubscriber for event subscribers, defineJob for background jobs, defineWebhook for webhooks, defineEvent for event types.',
46
46
  severity: 'error',
47
47
  category: 'structure',
48
48
  kNodeRef: 'K:runbook:add-domain',
49
49
  fixHint: "Wrap your handler with the appropriate define* factory from @mettlecast/domain-runtime.",
50
50
  exampleBad: "export const handler = async (event) => ({ statusCode: 200 });",
51
- exampleGood: "export const myApi = defineApi({ id: 'my-api', ... });",
51
+ exampleGood: "export const myAction = defineAction({ id: 'my-action', exposure: { type: 'api', ... }, ... });",
52
52
  },
53
53
  'flow-domain-ownership': {
54
54
  ruleId: 'flow-domain-ownership',
@@ -75,24 +75,24 @@ const RULE_EXPLANATIONS = {
75
75
  'apis-have-versions': {
76
76
  ruleId: 'apis-have-versions',
77
77
  title: 'APIs declare versions',
78
- description: 'Every defineApi must have a versions field with at least one version entry. Versioning is mandatory for API evolution.',
78
+ description: 'Every API-exposed action (defineAction with exposure.type=\'api\') must declare input and output Zod schemas with .default() so the runtime has a concrete example payload. The action-first contract replaced legacy defineApi\'s versions map (#4689).',
79
79
  severity: 'error',
80
80
  category: 'correctness',
81
81
  kNodeRef: 'K:runbook:add-domain',
82
- fixHint: 'Add versions: { v1: { status, input, output, handler } } to your defineApi call.',
83
- exampleBad: "defineApi({ id: 'my-api', path: '/v1/my-api' }) // missing versions",
84
- exampleGood: "defineApi({ id: 'my-api', path: '/v1/my-api', versions: { v1: { ... } } });",
82
+ fixHint: 'Add Zod input/output schemas to your defineAction call (e.g. z.object({...}).default({...})).',
83
+ exampleBad: "defineAction({ id: 'my-action', exposure: { type: 'api', path: '/v1/my-action', ... }, handler: ... }) // missing input/output schemas",
84
+ exampleGood: "defineAction({ id: 'my-action', exposure: { type: 'api', ... }, input: z.object({...}).default({...}), output: z.object({...}).default({...}), handler: ... });",
85
85
  },
86
86
  'apis-have-tenancy': {
87
87
  ruleId: 'apis-have-tenancy',
88
88
  title: 'APIs declare tenancy',
89
- description: 'Every defineApi must declare its tenancy mode: required (tenant-scoped), none (tenant-agnostic like registration), or system (system-internal admin).',
89
+ description: 'Every defineAction with exposure.type=\'api\' must declare exposure.tenancy: required (tenant-scoped), none (tenant-agnostic like registration), or system (system-internal admin). The action-first contract enforces this at registration time (#4689).',
90
90
  severity: 'error',
91
91
  category: 'correctness',
92
92
  kNodeRef: 'K:convention:tier-1-foundations',
93
- fixHint: "Add tenancy: 'required' | 'none' | 'system' to your defineApi call.",
94
- exampleBad: "defineApi({ id: 'my-api', path: '/v1/my-api' }) // missing tenancy",
95
- exampleGood: "defineApi({ id: 'my-api', path: '/v1/my-api', tenancy: 'required' });",
93
+ fixHint: "Add `exposure.tenancy: 'required' | 'none' | 'system'` to your defineAction call.",
94
+ exampleBad: "defineAction({ id: 'my-action', exposure: { type: 'api', path: '/v1/my-action' }, ... }) // missing exposure.tenancy",
95
+ exampleGood: "defineAction({ id: 'my-action', exposure: { type: 'api', path: '/v1/my-action', method: 'GET', auth: 'required', tenancy: 'required' }, ... });",
96
96
  },
97
97
  'no-cross-domain-imports': {
98
98
  ruleId: 'no-cross-domain-imports',
@@ -1,7 +1,12 @@
1
1
  /**
2
2
  * generate-openapi — read a domain's registry and produce an OpenAPI 3.1
3
3
  * specification as JSON. The registry is consumed at build time; each
4
- * `defineApi` entry contributes one path under the domain's route prefix.
4
+ * `defineAction({ exposure: { type: 'api', ... } })` entry contributes
5
+ * one path under the domain's route prefix.
6
+ *
7
+ * Issue #4689: the legacy `defineApi` factory was removed and the
8
+ * `registry.apis` slot is now always empty in new registries. This
9
+ * command therefore reads exclusively from `actions[]`.
5
10
  *
6
11
  * Usage: npx mc-domain-module generate-openapi <domain>
7
12
  */
@@ -11,6 +16,10 @@ export interface GenerateOpenapiOptions {
11
16
  /** Output path. Defaults to domains/<domain>/api/openapi.generated.json */
12
17
  output?: string;
13
18
  }
19
+ /**
20
+ * Run the generate-openapi command: read the domain's registry and emit
21
+ * an OpenAPI 3.1 spec covering every API-exposed action.
22
+ */
14
23
  export declare function runGenerateOpenapi(options: GenerateOpenapiOptions): Promise<string>;
15
24
  /**
16
25
  * CLI entry point.