@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.
- package/dist/builder/build-registry.d.ts +1 -1
- package/dist/builder/build-registry.js +129 -49
- package/dist/builder/build-types.d.ts +1 -1
- package/dist/builder/load-module.d.ts +1 -1
- package/dist/builder/load-module.js +3 -3
- package/dist/cli.js +3 -3
- package/dist/commands/add-api.js +2 -2
- package/dist/commands/add-domain.js +4 -4
- package/dist/commands/add-fixture-factory.js +5 -6
- package/dist/commands/build-catalog.d.ts +8 -24
- package/dist/commands/build-catalog.js +12 -23
- package/dist/commands/build-flows.js +1 -1
- package/dist/commands/build.js +7 -6
- package/dist/commands/check-hashes.js +2 -2
- package/dist/commands/create-project.js +1 -1
- package/dist/commands/dev.js +1 -1
- package/dist/commands/doctor.d.ts +5 -7
- package/dist/commands/doctor.js +110 -202
- package/dist/commands/explain.js +13 -13
- package/dist/commands/generate-openapi.d.ts +10 -1
- package/dist/commands/generate-openapi.js +19 -33
- package/dist/commands/power-tune.js +2 -2
- package/dist/commands/show-dns.d.ts +1 -1
- package/dist/commands/show-dns.js +5 -5
- package/dist/commands/show.d.ts +2 -3
- package/dist/commands/show.js +0 -2
- package/dist/commands/test.js +0 -1
- package/dist/commands/upgrade-backend.js +3 -3
- package/dist/commands/upgrade.js +17 -11
- package/dist/commands/validate.js +144 -39
- package/dist/server/api-server.d.ts +1 -1
- package/dist/server/mount-routes.d.ts +11 -2
- package/dist/server/mount-routes.js +20 -8
- package/dist/templates/api-skeleton.d.ts +5 -0
- package/dist/templates/api-skeleton.js +28 -27
- package/dist/templates/claude-md.js +1 -1
- package/dist/templates/patterns/api/create-with-event.d.ts +4 -0
- package/dist/templates/patterns/api/create-with-event.js +38 -32
- package/dist/templates/patterns/api/idempotent-mutation.d.ts +4 -0
- package/dist/templates/patterns/api/idempotent-mutation.js +47 -41
- package/dist/templates/patterns/api/paginated-list.d.ts +4 -0
- package/dist/templates/patterns/api/paginated-list.js +30 -24
- package/dist/templates/patterns/api/simple-crud.d.ts +4 -0
- package/dist/templates/patterns/api/simple-crud.js +46 -35
- package/dist/templates/patterns/api/streaming-list.d.ts +4 -0
- package/dist/templates/patterns/api/streaming-list.js +46 -41
- package/dist/templates/patterns/api/system-admin.d.ts +4 -0
- package/dist/templates/patterns/api/system-admin.js +59 -52
- package/dist/templates/patterns/api/webhook-receiver-style.d.ts +4 -0
- package/dist/templates/patterns/api/webhook-receiver-style.js +43 -35
- package/dist/types.d.ts +100 -0
- package/dist/types.js +1 -0
- package/dist/utils/file-helpers.d.ts +0 -2
- package/dist/utils/file-helpers.js +2 -3
- package/dist/utils/header-inject.js +2 -2
- package/dist/utils/install-file.d.ts +1 -1
- package/dist/utils/install-file.js +1 -1
- package/dist/utils/manifest.js +1 -2
- package/dist/utils/scaffold-config.d.ts +8 -2
- package/dist/utils/scaffold-config.js +3 -1
- package/package.json +1 -1
- package/src/__tests__/build-registry.test.ts +43 -20
- package/src/__tests__/build-types.test.ts +4 -7
- package/src/__tests__/builder/walkDomainDir.test.ts +19 -21
- package/src/__tests__/commands/add-api.test.ts +12 -10
- package/src/__tests__/commands/add-domain.test.ts +8 -5
- package/src/__tests__/commands/check-hashes.test.ts +9 -9
- package/src/__tests__/commands/create-project.test.ts +5 -5
- package/src/__tests__/commands/dev.test.ts +0 -1
- package/src/__tests__/commands/upgrade.test.ts +7 -7
- package/src/__tests__/doctor.test.ts +60 -67
- package/src/__tests__/mount-routes.test.ts +64 -23
- package/src/__tests__/package-freshness.test.ts +94 -0
- package/src/__tests__/scaffold-src/part-a-layout.test.ts +10 -10
- package/src/__tests__/scripts/package-scaffold.test.ts +5 -5
- package/src/__tests__/smoke/scaffold.test.ts +13 -15
- package/src/__tests__/utils/install-file.test.ts +2 -2
- package/src/__tests__/utils/manifest.test.ts +2 -2
- package/src/__tests__/validate.test.ts +570 -1
- package/src/builder/build-registry.ts +154 -59
- package/src/builder/build-types.ts +1 -1
- package/src/builder/load-module.ts +3 -3
- package/src/cli.ts +4 -4
- package/src/commands/add-api.ts +2 -2
- package/src/commands/add-domain.ts +4 -4
- package/src/commands/add-fixture-factory.ts +5 -6
- package/src/commands/build-catalog.ts +18 -40
- package/src/commands/build-flows.ts +1 -1
- package/src/commands/build.ts +8 -7
- package/src/commands/check-hashes.ts +2 -2
- package/src/commands/create-project.ts +1 -1
- package/src/commands/dev.ts +1 -1
- package/src/commands/doctor.ts +120 -218
- package/src/commands/explain.ts +13 -13
- package/src/commands/generate-openapi.ts +30 -52
- package/src/commands/power-tune.ts +2 -2
- package/src/commands/show-dns.ts +5 -5
- package/src/commands/show.ts +2 -5
- package/src/commands/test.ts +0 -1
- package/src/commands/upgrade-backend.ts +3 -3
- package/src/commands/upgrade.ts +16 -10
- package/src/commands/validate.ts +180 -40
- package/src/server/api-server.ts +1 -1
- package/src/server/mount-routes.ts +21 -10
- package/src/templates/api-skeleton.ts +29 -28
- package/src/templates/claude-md.ts +1 -1
- package/src/templates/patterns/api/create-with-event.ts +39 -33
- package/src/templates/patterns/api/idempotent-mutation.ts +48 -42
- package/src/templates/patterns/api/paginated-list.ts +31 -25
- package/src/templates/patterns/api/simple-crud.ts +47 -36
- package/src/templates/patterns/api/streaming-list.ts +47 -42
- package/src/templates/patterns/api/system-admin.ts +60 -53
- package/src/templates/patterns/api/webhook-receiver-style.ts +48 -40
- package/src/types.ts +128 -0
- package/src/utils/file-helpers.ts +2 -5
- package/src/utils/header-inject.ts +2 -2
- package/src/utils/install-file.ts +1 -1
- package/src/utils/manifest.ts +1 -2
- package/src/utils/scaffold-config.ts +12 -3
package/dist/commands/doctor.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
161
|
-
// --fix
|
|
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
|
|
167
|
-
// the doctor report.
|
|
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
|
|
366
|
-
// Find
|
|
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
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
376
|
-
|
|
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
|
|
393
|
+
message: 'No API-exposed actions found (optional)',
|
|
381
394
|
kNodeRef: 'K:runbook:add-domain',
|
|
382
395
|
};
|
|
383
396
|
}
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
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('
|
|
389
|
-
|
|
403
|
+
if (!content.includes('.default(')) {
|
|
404
|
+
missingDefaults++;
|
|
390
405
|
}
|
|
391
406
|
}
|
|
392
|
-
if (
|
|
407
|
+
if (missingDefaults === 0) {
|
|
393
408
|
return {
|
|
394
409
|
name: 'APIs declare versions',
|
|
395
410
|
status: 'PASS',
|
|
396
|
-
message: `All ${
|
|
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: `${
|
|
405
|
-
fixHint:
|
|
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
|
|
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
|
|
423
|
-
// Find
|
|
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
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
433
|
-
|
|
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
|
|
463
|
+
message: 'No API-exposed actions found (optional)',
|
|
438
464
|
kNodeRef: 'K:convention:tier-1-foundations',
|
|
439
465
|
};
|
|
440
466
|
}
|
|
441
|
-
//
|
|
467
|
+
// Check for `tenancy:` anywhere in each API-exposed action file.
|
|
442
468
|
let missingTenancy = 0;
|
|
443
|
-
for (const file of
|
|
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 ${
|
|
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}/${
|
|
462
|
-
fixHint: "Add tenancy: 'required' | 'none' | 'system' to
|
|
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, '.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
791
|
-
|
|
792
|
-
|
|
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, '.
|
|
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.
|
|
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:
|
|
1365
|
-
//
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
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:
|
|
1408
|
+
name: 'Layout: no managed files in legacy locations',
|
|
1372
1409
|
status: 'PASS',
|
|
1373
|
-
message: '
|
|
1410
|
+
message: 'Managed scaffold files use current manifest paths',
|
|
1374
1411
|
});
|
|
1375
1412
|
}
|
|
1376
1413
|
else {
|
|
1377
|
-
for (const f of
|
|
1414
|
+
for (const f of legacyManagedLocations) {
|
|
1378
1415
|
results.push({
|
|
1379
|
-
name: 'Layout:
|
|
1416
|
+
name: 'Layout: no managed files in legacy locations',
|
|
1380
1417
|
status: 'FAIL',
|
|
1381
|
-
message: `
|
|
1382
|
-
fixHint:
|
|
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
|
|
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
|
|
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
|
-
*
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
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
|
-
|
|
1509
|
-
const
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
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
|
}
|
package/dist/commands/explain.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
83
|
-
exampleBad: "
|
|
84
|
-
exampleGood: "
|
|
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
|
|
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
|
|
94
|
-
exampleBad: "
|
|
95
|
-
exampleGood: "
|
|
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
|
-
* `
|
|
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.
|