@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/src/commands/doctor.ts
CHANGED
|
@@ -18,15 +18,13 @@ export const DOCTOR_FIX_FLAG = '--fix';
|
|
|
18
18
|
export interface DoctorOptions {
|
|
19
19
|
/** Root directory of the project (defaults to cwd). */
|
|
20
20
|
projectRoot?: string;
|
|
21
|
-
/**
|
|
21
|
+
/** Deprecated legacy flag retained as a no-op for older automation. */
|
|
22
22
|
relocate?: boolean;
|
|
23
23
|
/**
|
|
24
|
-
* Auto-remediate before reporting.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* (0 files moved, exit 0). Does NOT auto-generate missing fixtures
|
|
29
|
-
* (deferred).
|
|
24
|
+
* Auto-remediate before reporting. The legacy relocation routine is now a
|
|
25
|
+
* no-op because current scaffold manifests intentionally track managed files
|
|
26
|
+
* in their installed project locations (infra/modules, domains, .github, etc.).
|
|
27
|
+
* Does NOT auto-generate missing fixtures (deferred).
|
|
30
28
|
*/
|
|
31
29
|
fix?: boolean;
|
|
32
30
|
/**
|
|
@@ -123,8 +121,10 @@ async function checkHandlersUseResult(projectRoot: string): Promise<DoctorCheck>
|
|
|
123
121
|
const missing: string[] = [];
|
|
124
122
|
for (const file of apiFiles) {
|
|
125
123
|
const content = await readFile(file, 'utf8');
|
|
126
|
-
// A file contains a handler export — check the return type
|
|
127
|
-
|
|
124
|
+
// A file contains a handler export — check the return type.
|
|
125
|
+
// Issue #4689: defineApi was removed. The action-first
|
|
126
|
+
// contract uses defineAction with exposure.type='api'.
|
|
127
|
+
if (/defineAction\s*\(/.test(content)) {
|
|
128
128
|
// Look for Result<T> in the handler's return type annotation
|
|
129
129
|
if (!/: .*Result</.test(content)) {
|
|
130
130
|
missing.push(relative(projectRoot, file));
|
|
@@ -224,16 +224,15 @@ async function checkGeneratedClientsFresh(projectRoot: string): Promise<DoctorCh
|
|
|
224
224
|
export async function runDoctor(opts: DoctorOptions = {}): Promise<DoctorReport> {
|
|
225
225
|
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
226
226
|
|
|
227
|
-
// --relocate
|
|
228
|
-
// --fix
|
|
227
|
+
// --relocate is retained as a safe no-op for older automation.
|
|
228
|
+
// --fix runs that no-op preflight first, then the full report.
|
|
229
229
|
// When both are set, --fix takes precedence so the report still runs.
|
|
230
230
|
if (opts.relocate && !opts.fix) {
|
|
231
231
|
return runRelocate(projectRoot);
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
// --fix preflight: run the relocation
|
|
235
|
-
// the doctor report.
|
|
236
|
-
// runRelocate returns a no-op summary check (0 moved, 0 warned, 8 skipped).
|
|
234
|
+
// --fix preflight: run the legacy relocation no-op and merge its check into
|
|
235
|
+
// the doctor report.
|
|
237
236
|
let preflightChecks: DoctorCheck[] = [];
|
|
238
237
|
if (opts.fix) {
|
|
239
238
|
const relocationReport = await runRelocate(projectRoot);
|
|
@@ -451,49 +450,64 @@ async function checkNoCrossDomainImports(projectRoot: string): Promise<DoctorChe
|
|
|
451
450
|
async function checkApisHaveVersions(projectRoot: string): Promise<DoctorCheck> {
|
|
452
451
|
try {
|
|
453
452
|
const domainsDir = join(projectRoot, 'domains');
|
|
454
|
-
const
|
|
453
|
+
const allActionFiles: string[] = [];
|
|
455
454
|
|
|
456
|
-
// Find
|
|
455
|
+
// Find API-exposed action files in both actions/ and api/ directories.
|
|
456
|
+
// Issue #4689: defineApi was removed. API-exposed actions live in
|
|
457
|
+
// domains/*/actions/ but legacy api/ directories may still exist.
|
|
457
458
|
const entries = await readdir(domainsDir, { withFileTypes: true });
|
|
458
459
|
for (const entry of entries) {
|
|
459
460
|
if (!entry.isDirectory()) continue;
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
461
|
+
for (const dir of ['actions', 'api']) {
|
|
462
|
+
const actionDir = join(domainsDir, entry.name, dir);
|
|
463
|
+
const files = findFiles(actionDir, /\.ts$/);
|
|
464
|
+
allActionFiles.push(...files);
|
|
465
|
+
}
|
|
463
466
|
}
|
|
464
|
-
const apiFiles = allApiFiles;
|
|
465
467
|
|
|
466
|
-
|
|
468
|
+
// Only consider files that define an API-exposed action (defineAction
|
|
469
|
+
// with exposure.type === 'api' or exposure: { type: 'api' }).
|
|
470
|
+
const apiExposedFiles: string[] = [];
|
|
471
|
+
for (const file of allActionFiles) {
|
|
472
|
+
const content = await readFile(file, 'utf8');
|
|
473
|
+
if (/defineAction\s*\(/.test(content) && /exposure\s*:\s*\{\s*type\s*:\s*['"]api['"]/.test(content)) {
|
|
474
|
+
apiExposedFiles.push(file);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (apiExposedFiles.length === 0) {
|
|
467
479
|
return {
|
|
468
480
|
name: 'APIs declare versions',
|
|
469
481
|
status: 'PASS',
|
|
470
|
-
message: 'No
|
|
482
|
+
message: 'No API-exposed actions found (optional)',
|
|
471
483
|
kNodeRef: 'K:runbook:add-domain',
|
|
472
484
|
};
|
|
473
485
|
}
|
|
474
486
|
|
|
475
|
-
//
|
|
476
|
-
|
|
477
|
-
|
|
487
|
+
// Issue #4689: action-first contract requires input/output Zod schemas
|
|
488
|
+
// with `.default({...})` example data. Check that each API-exposed
|
|
489
|
+
// action file contains `.default(` (indicating example data).
|
|
490
|
+
let missingDefaults = 0;
|
|
491
|
+
for (const file of apiExposedFiles) {
|
|
478
492
|
const content = await readFile(file, 'utf8');
|
|
479
|
-
if (!content.includes('
|
|
480
|
-
|
|
493
|
+
if (!content.includes('.default(')) {
|
|
494
|
+
missingDefaults++;
|
|
481
495
|
}
|
|
482
496
|
}
|
|
483
497
|
|
|
484
|
-
if (
|
|
498
|
+
if (missingDefaults === 0) {
|
|
485
499
|
return {
|
|
486
500
|
name: 'APIs declare versions',
|
|
487
501
|
status: 'PASS',
|
|
488
|
-
message: `All ${
|
|
502
|
+
message: `All ${apiExposedFiles.length} API-exposed action(s) have .default() example data`,
|
|
489
503
|
kNodeRef: 'K:runbook:add-domain',
|
|
490
504
|
};
|
|
491
505
|
} else {
|
|
492
506
|
return {
|
|
493
507
|
name: 'APIs declare versions',
|
|
494
508
|
status: 'FAIL',
|
|
495
|
-
message: `${
|
|
496
|
-
fixHint:
|
|
509
|
+
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.`,
|
|
510
|
+
fixHint: "Add `input` and `output` Zod schemas with `.default({...})` to defineAction({ exposure: { type: 'api', ... } }) calls",
|
|
497
511
|
kNodeRef: 'K:runbook:add-domain',
|
|
498
512
|
};
|
|
499
513
|
}
|
|
@@ -501,7 +515,7 @@ async function checkApisHaveVersions(projectRoot: string): Promise<DoctorCheck>
|
|
|
501
515
|
return {
|
|
502
516
|
name: 'APIs declare versions',
|
|
503
517
|
status: 'WARN',
|
|
504
|
-
message: `Could not check API
|
|
518
|
+
message: `Could not check API schemas: ${String(err)}`,
|
|
505
519
|
kNodeRef: 'K:runbook:add-domain',
|
|
506
520
|
};
|
|
507
521
|
}
|
|
@@ -510,30 +524,42 @@ async function checkApisHaveVersions(projectRoot: string): Promise<DoctorCheck>
|
|
|
510
524
|
async function checkApisHaveTenancy(projectRoot: string): Promise<DoctorCheck> {
|
|
511
525
|
try {
|
|
512
526
|
const domainsDir = join(projectRoot, 'domains');
|
|
513
|
-
const
|
|
527
|
+
const allActionFiles: string[] = [];
|
|
514
528
|
|
|
515
|
-
// Find
|
|
529
|
+
// Find API-exposed action files in both actions/ and api/ directories.
|
|
530
|
+
// Issue #4689: defineApi was removed. Tenancy is now declared on the
|
|
531
|
+
// action's exposure block via `exposure.tenancy`.
|
|
516
532
|
const entries = await readdir(domainsDir, { withFileTypes: true });
|
|
517
533
|
for (const entry of entries) {
|
|
518
534
|
if (!entry.isDirectory()) continue;
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
535
|
+
for (const dir of ['actions', 'api']) {
|
|
536
|
+
const actionDir = join(domainsDir, entry.name, dir);
|
|
537
|
+
const files = findFiles(actionDir, /\.ts$/);
|
|
538
|
+
allActionFiles.push(...files);
|
|
539
|
+
}
|
|
522
540
|
}
|
|
523
|
-
const apiFiles = allApiFiles;
|
|
524
541
|
|
|
525
|
-
|
|
542
|
+
// Only consider files that define an API-exposed action.
|
|
543
|
+
const apiExposedFiles: string[] = [];
|
|
544
|
+
for (const file of allActionFiles) {
|
|
545
|
+
const content = await readFile(file, 'utf8');
|
|
546
|
+
if (/defineAction\s*\(/.test(content) && /exposure\s*:\s*\{\s*type\s*:\s*['"]api['"]/.test(content)) {
|
|
547
|
+
apiExposedFiles.push(file);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (apiExposedFiles.length === 0) {
|
|
526
552
|
return {
|
|
527
553
|
name: 'APIs declare tenancy',
|
|
528
554
|
status: 'PASS',
|
|
529
|
-
message: 'No
|
|
555
|
+
message: 'No API-exposed actions found (optional)',
|
|
530
556
|
kNodeRef: 'K:convention:tier-1-foundations',
|
|
531
557
|
};
|
|
532
558
|
}
|
|
533
559
|
|
|
534
|
-
//
|
|
560
|
+
// Check for `tenancy:` anywhere in each API-exposed action file.
|
|
535
561
|
let missingTenancy = 0;
|
|
536
|
-
for (const file of
|
|
562
|
+
for (const file of apiExposedFiles) {
|
|
537
563
|
const content = await readFile(file, 'utf8');
|
|
538
564
|
if (!content.includes('tenancy:')) {
|
|
539
565
|
missingTenancy++;
|
|
@@ -544,15 +570,15 @@ async function checkApisHaveTenancy(projectRoot: string): Promise<DoctorCheck> {
|
|
|
544
570
|
return {
|
|
545
571
|
name: 'APIs declare tenancy',
|
|
546
572
|
status: 'PASS',
|
|
547
|
-
message: `All ${
|
|
573
|
+
message: `All ${apiExposedFiles.length} API-exposed action(s) declare tenancy`,
|
|
548
574
|
kNodeRef: 'K:convention:tier-1-foundations',
|
|
549
575
|
};
|
|
550
576
|
} else {
|
|
551
577
|
return {
|
|
552
578
|
name: 'APIs declare tenancy',
|
|
553
579
|
status: 'FAIL',
|
|
554
|
-
message: `${missingTenancy}/${
|
|
555
|
-
fixHint: "Add tenancy: 'required' | 'none' | 'system' to
|
|
580
|
+
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'\`.`,
|
|
581
|
+
fixHint: "Add `exposure.tenancy: 'required' | 'none' | 'system'` to defineAction({ exposure: { type: 'api', ... } }) calls",
|
|
556
582
|
kNodeRef: 'K:convention:tier-1-foundations',
|
|
557
583
|
};
|
|
558
584
|
}
|
|
@@ -568,14 +594,22 @@ async function checkApisHaveTenancy(projectRoot: string): Promise<DoctorCheck> {
|
|
|
568
594
|
|
|
569
595
|
async function checkScaffoldConfigMatchesDisk(projectRoot: string): Promise<DoctorCheck> {
|
|
570
596
|
try {
|
|
571
|
-
const configPath = join(projectRoot, '.
|
|
597
|
+
const configPath = join(projectRoot, '.mc', 'scaffold-config.json');
|
|
572
598
|
const domainsDir = join(projectRoot, 'domains');
|
|
573
599
|
|
|
574
600
|
let configDomains: string[] = [];
|
|
575
601
|
try {
|
|
576
602
|
const configContent = await readFile(configPath, 'utf8');
|
|
577
603
|
const config = JSON.parse(configContent) as { domainIds?: string[] };
|
|
578
|
-
|
|
604
|
+
if (!Array.isArray(config.domainIds)) {
|
|
605
|
+
return {
|
|
606
|
+
name: 'scaffold-config.json matches on-disk',
|
|
607
|
+
status: 'PASS',
|
|
608
|
+
message: '.mc/scaffold-config.json has no domainIds array — domain list sync check skipped',
|
|
609
|
+
kNodeRef: 'K:runbook:add-domain',
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
configDomains = config.domainIds;
|
|
579
613
|
} catch {
|
|
580
614
|
return {
|
|
581
615
|
name: 'scaffold-config.json matches on-disk',
|
|
@@ -868,7 +902,7 @@ async function checkAllRoutesUseTanStackRouter(projectRoot: string): Promise<Doc
|
|
|
868
902
|
|
|
869
903
|
/**
|
|
870
904
|
* Check W5-4: Lambda handler files contain initOtel() call.
|
|
871
|
-
|
|
905
|
+
* Scans `domains/*\/actions/*.ts` and `domains/*\/api/*.ts` and FAILs if any handler is missing initOtel().
|
|
872
906
|
*/
|
|
873
907
|
async function checkOtelInitInLambdas(projectRoot: string): Promise<DoctorCheck> {
|
|
874
908
|
try {
|
|
@@ -885,9 +919,11 @@ async function checkOtelInitInLambdas(projectRoot: string): Promise<DoctorCheck>
|
|
|
885
919
|
const handlerFiles: string[] = [];
|
|
886
920
|
for (const entry of entries) {
|
|
887
921
|
if (!entry.isDirectory()) continue;
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
|
|
922
|
+
for (const dir of ['actions', 'api']) {
|
|
923
|
+
const handlerDir = join(domainsDir, entry.name, dir);
|
|
924
|
+
const files = findFiles(handlerDir, /\.ts$/);
|
|
925
|
+
handlerFiles.push(...files);
|
|
926
|
+
}
|
|
891
927
|
}
|
|
892
928
|
|
|
893
929
|
if (handlerFiles.length === 0) {
|
|
@@ -1205,7 +1241,7 @@ async function checkDomainBrainReachable(projectRoot: string): Promise<DoctorChe
|
|
|
1205
1241
|
*/
|
|
1206
1242
|
async function checkActionsHaveTypes(projectRoot: string): Promise<DoctorCheck> {
|
|
1207
1243
|
try {
|
|
1208
|
-
const typesPath = join(projectRoot, '.
|
|
1244
|
+
const typesPath = join(projectRoot, '.mc', 'actions-types.d.ts');
|
|
1209
1245
|
const domainsDir = join(projectRoot, 'domains');
|
|
1210
1246
|
const discoveredActions: string[] = [];
|
|
1211
1247
|
|
|
@@ -1435,7 +1471,7 @@ async function checkSeedPagesHaveNoHeaders(projectRoot: string): Promise<DoctorC
|
|
|
1435
1471
|
|
|
1436
1472
|
/**
|
|
1437
1473
|
* Check Part C: Layout validation checks
|
|
1438
|
-
* 1.
|
|
1474
|
+
* 1. managed files are not registered in obsolete legacy locations
|
|
1439
1475
|
* 2. tracked/seed files inside .mc/
|
|
1440
1476
|
* 3. manifest entries pointing at missing files
|
|
1441
1477
|
* 4. scaffold-tracked files (with @mc-scaffold: header) that lack manifest entries
|
|
@@ -1452,26 +1488,29 @@ async function checkLayoutPolicy(projectRoot: string): Promise<DoctorCheck[]> {
|
|
|
1452
1488
|
return [];
|
|
1453
1489
|
}
|
|
1454
1490
|
|
|
1455
|
-
// Check 1:
|
|
1456
|
-
//
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1491
|
+
// Check 1: managed files should not be registered in obsolete legacy
|
|
1492
|
+
// locations. Current scaffold manifests intentionally track managed files at
|
|
1493
|
+
// their installed paths (infra/modules, domains, .github, CLAUDE.md, etc.).
|
|
1494
|
+
const legacyManagedLocations = manifest.files.filter(f =>
|
|
1495
|
+
f.policy === 'managed' && (
|
|
1496
|
+
f.path.startsWith('.tib/') ||
|
|
1497
|
+
f.path.startsWith('.mc/infra/') ||
|
|
1498
|
+
f.path === 'mc-deploy.yml'
|
|
1499
|
+
)
|
|
1461
1500
|
);
|
|
1462
|
-
if (
|
|
1501
|
+
if (legacyManagedLocations.length === 0) {
|
|
1463
1502
|
results.push({
|
|
1464
|
-
name: 'Layout:
|
|
1503
|
+
name: 'Layout: no managed files in legacy locations',
|
|
1465
1504
|
status: 'PASS',
|
|
1466
|
-
message: '
|
|
1505
|
+
message: 'Managed scaffold files use current manifest paths',
|
|
1467
1506
|
});
|
|
1468
1507
|
} else {
|
|
1469
|
-
for (const f of
|
|
1508
|
+
for (const f of legacyManagedLocations) {
|
|
1470
1509
|
results.push({
|
|
1471
|
-
name: 'Layout:
|
|
1510
|
+
name: 'Layout: no managed files in legacy locations',
|
|
1472
1511
|
status: 'FAIL',
|
|
1473
|
-
message: `
|
|
1474
|
-
fixHint:
|
|
1512
|
+
message: `Managed scaffold file registered in legacy location: ${f.path}`,
|
|
1513
|
+
fixHint: 'Run `npx mc-domain-module upgrade` to refresh scaffold metadata',
|
|
1475
1514
|
file: f.path,
|
|
1476
1515
|
});
|
|
1477
1516
|
}
|
|
@@ -1521,7 +1560,7 @@ async function checkLayoutPolicy(projectRoot: string): Promise<DoctorCheck[]> {
|
|
|
1521
1560
|
name: 'Layout: manifest entries exist on disk',
|
|
1522
1561
|
status: 'FAIL',
|
|
1523
1562
|
message: `${missingFiles.length} manifest entry/entries point to missing files:\n ${missingFiles.join('\n ')}`,
|
|
1524
|
-
fixHint: 'Run
|
|
1563
|
+
fixHint: 'Run `npx mc-domain-module upgrade` to refresh scaffold metadata, or restore the missing files',
|
|
1525
1564
|
});
|
|
1526
1565
|
}
|
|
1527
1566
|
|
|
@@ -1566,7 +1605,7 @@ async function checkLayoutPolicy(projectRoot: string): Promise<DoctorCheck[]> {
|
|
|
1566
1605
|
name: 'Layout: scaffold-tracked files have manifest entries',
|
|
1567
1606
|
status: 'WARN',
|
|
1568
1607
|
message: `${untracked.length} file(s) with @mc-scaffold: header not in manifest:\n ${untracked.join('\n ')}`,
|
|
1569
|
-
fixHint: 'Run
|
|
1608
|
+
fixHint: 'Run `npx mc-domain-module upgrade` to refresh scaffold metadata',
|
|
1570
1609
|
});
|
|
1571
1610
|
}
|
|
1572
1611
|
|
|
@@ -1574,156 +1613,19 @@ async function checkLayoutPolicy(projectRoot: string): Promise<DoctorCheck[]> {
|
|
|
1574
1613
|
}
|
|
1575
1614
|
|
|
1576
1615
|
/**
|
|
1577
|
-
*
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
{ oldPath: 'infra/modules/PowerTuningStack.ts', newPath: '.mc/infra/modules/PowerTuningStack.ts', policy: 'managed' },
|
|
1584
|
-
{ oldPath: 'infra/cdk.json', newPath: '.mc/infra/cdk.json', policy: 'managed' },
|
|
1585
|
-
{ oldPath: 'infra/tsconfig.json', newPath: '.mc/infra/tsconfig.json', policy: 'managed' },
|
|
1586
|
-
{ oldPath: 'infra/package.json', newPath: '.mc/infra/package.json', policy: 'managed' },
|
|
1587
|
-
{ oldPath: 'mc-deploy.yml', newPath: '.github/workflows/mc-deploy.yml', policy: 'managed' },
|
|
1588
|
-
];
|
|
1589
|
-
|
|
1590
|
-
/**
|
|
1591
|
-
* Run relocation of old-layout scaffold files to new layout
|
|
1616
|
+
* Legacy relocation entrypoint retained for older automation.
|
|
1617
|
+
*
|
|
1618
|
+
* Earlier scaffold versions tried to move generated infra into .mc/infra. The
|
|
1619
|
+
* current scaffold contract keeps generated infra under infra/modules and uses
|
|
1620
|
+
* .mc/manifest.json plus .mc/modules-hashes.json for ownership/drift tracking.
|
|
1621
|
+
* Moving files here would corrupt modern projects, so relocation is now a no-op.
|
|
1592
1622
|
*/
|
|
1593
1623
|
async function runRelocate(projectRoot: string): Promise<DoctorReport> {
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
const manifest = await readManifest(projectRoot);
|
|
1602
|
-
if (!manifest) {
|
|
1603
|
-
checks.push({
|
|
1604
|
-
name: 'Relocate: manifest present',
|
|
1605
|
-
status: 'FAIL',
|
|
1606
|
-
message: '.mc/manifest.json not found — cannot relocate without a manifest',
|
|
1607
|
-
fixHint: 'This project may not be a TIB scaffold project',
|
|
1608
|
-
});
|
|
1609
|
-
return { checks, exitCode: 1, pass: false };
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
let moved = 0;
|
|
1613
|
-
let warned = 0;
|
|
1614
|
-
let skipped = 0;
|
|
1615
|
-
|
|
1616
|
-
for (const { oldPath, newPath, policy } of RELOCATION_MAP) {
|
|
1617
|
-
const oldAbsPath = join(projectRoot, oldPath);
|
|
1618
|
-
const newAbsPath = join(projectRoot, newPath);
|
|
1619
|
-
|
|
1620
|
-
// Check if old file exists
|
|
1621
|
-
const { access: fsAccess } = await import('node:fs/promises');
|
|
1622
|
-
try { await fsAccess(oldAbsPath); } catch { skipped++; continue; }
|
|
1623
|
-
|
|
1624
|
-
// Check if it has the scaffold header (confirm it's scaffold-owned)
|
|
1625
|
-
let content: string;
|
|
1626
|
-
try {
|
|
1627
|
-
content = await readFile(oldAbsPath, 'utf-8');
|
|
1628
|
-
} catch {
|
|
1629
|
-
skipped++;
|
|
1630
|
-
continue;
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
if (!content.includes('@mc-scaffold:')) {
|
|
1634
|
-
// File exists but no header — user-owned, leave it
|
|
1635
|
-
checks.push({
|
|
1636
|
-
name: 'Relocate: checking ' + oldPath,
|
|
1637
|
-
status: 'WARN',
|
|
1638
|
-
message: `${oldPath} has no @mc-scaffold: header — left in place (user-owned)`,
|
|
1639
|
-
file: oldPath,
|
|
1640
|
-
});
|
|
1641
|
-
warned++;
|
|
1642
|
-
continue;
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
// Check for user drift
|
|
1646
|
-
const manifestEntry = manifest.files.find(f => f.path === oldPath);
|
|
1647
|
-
if (manifestEntry) {
|
|
1648
|
-
const diskSha = await computeChecksumFile(oldAbsPath);
|
|
1649
|
-
if (diskSha && diskSha !== manifestEntry.sha256) {
|
|
1650
|
-
// File has been modified — leave with warning
|
|
1651
|
-
checks.push({
|
|
1652
|
-
name: 'Relocate: checking ' + oldPath,
|
|
1653
|
-
status: 'WARN',
|
|
1654
|
-
message: `${oldPath} has local modifications — left in place. Migrate manually: move to ${newPath}`,
|
|
1655
|
-
file: oldPath,
|
|
1656
|
-
});
|
|
1657
|
-
warned++;
|
|
1658
|
-
continue;
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
// Check if destination already exists
|
|
1663
|
-
try {
|
|
1664
|
-
await fsAccess(newAbsPath);
|
|
1665
|
-
checks.push({
|
|
1666
|
-
name: 'Relocate: ' + oldPath,
|
|
1667
|
-
status: 'WARN',
|
|
1668
|
-
message: `${newPath} already exists — skipping move of ${oldPath}`,
|
|
1669
|
-
file: newPath,
|
|
1670
|
-
});
|
|
1671
|
-
warned++;
|
|
1672
|
-
continue;
|
|
1673
|
-
} catch { /* destination doesn't exist, good */ }
|
|
1674
|
-
|
|
1675
|
-
// Move the file
|
|
1676
|
-
try {
|
|
1677
|
-
await mkdir(dirname(newAbsPath), { recursive: true });
|
|
1678
|
-
// Copy + delete (rename may fail across filesystems)
|
|
1679
|
-
await writeFile(newAbsPath, content, 'utf-8');
|
|
1680
|
-
|
|
1681
|
-
// Update manifest: remove old entry, add new with new path + policy
|
|
1682
|
-
manifest.files = manifest.files.filter(f => f.path !== oldPath);
|
|
1683
|
-
const sha256 = manifestEntry?.sha256 ?? computeChecksumString(content);
|
|
1684
|
-
upsertManifestFile(manifest, {
|
|
1685
|
-
path: newPath,
|
|
1686
|
-
module: manifestEntry?.module ?? 'domain',
|
|
1687
|
-
moduleVersion: manifestEntry?.moduleVersion ?? '1.0.0',
|
|
1688
|
-
sha256,
|
|
1689
|
-
wasTemplate: manifestEntry?.wasTemplate ?? false,
|
|
1690
|
-
installedAt: manifestEntry?.installedAt ?? new Date().toISOString(),
|
|
1691
|
-
policy,
|
|
1692
|
-
});
|
|
1693
|
-
|
|
1694
|
-
// Delete old file
|
|
1695
|
-
await unlink(oldAbsPath);
|
|
1696
|
-
|
|
1697
|
-
checks.push({
|
|
1698
|
-
name: 'Relocate: ' + oldPath,
|
|
1699
|
-
status: 'PASS',
|
|
1700
|
-
message: `Moved ${oldPath} → ${newPath} (policy: ${policy})`,
|
|
1701
|
-
file: newPath,
|
|
1702
|
-
});
|
|
1703
|
-
moved++;
|
|
1704
|
-
} catch (err) {
|
|
1705
|
-
checks.push({
|
|
1706
|
-
name: 'Relocate: ' + oldPath,
|
|
1707
|
-
status: 'FAIL',
|
|
1708
|
-
message: `Failed to move ${oldPath} → ${newPath}: ${String(err)}`,
|
|
1709
|
-
file: oldPath,
|
|
1710
|
-
});
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
// Write updated manifest
|
|
1715
|
-
if (moved > 0) {
|
|
1716
|
-
await writeManifest(projectRoot, manifest);
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
// Summary check
|
|
1720
|
-
checks.push({
|
|
1721
|
-
name: 'Relocate: summary',
|
|
1722
|
-
status: warned > 0 ? 'WARN' : 'PASS',
|
|
1723
|
-
message: `Relocation complete: ${moved} file(s) moved, ${warned} warning(s), ${skipped} skipped (not present)`,
|
|
1724
|
-
});
|
|
1725
|
-
|
|
1726
|
-
const hasFail = checks.some(c => c.status === 'FAIL');
|
|
1727
|
-
const exitCode = hasFail ? 1 : 0;
|
|
1728
|
-
return { checks, exitCode, pass: exitCode === 0 };
|
|
1624
|
+
void projectRoot;
|
|
1625
|
+
const checks: DoctorCheck[] = [{
|
|
1626
|
+
name: 'Relocate: deprecated no-op',
|
|
1627
|
+
status: 'PASS',
|
|
1628
|
+
message: 'No files moved. Current scaffold layout keeps generated files at their installed paths and tracks ownership via .mc/manifest.json.',
|
|
1629
|
+
}];
|
|
1630
|
+
return { checks, exitCode: 0, pass: true };
|
|
1729
1631
|
}
|
package/src/commands/explain.ts
CHANGED
|
@@ -58,25 +58,25 @@ const RULE_EXPLANATIONS: Record<string, RuleExplanation> = {
|
|
|
58
58
|
ruleId: 'no-raw-http-server',
|
|
59
59
|
title: 'No raw HTTP server in domain code',
|
|
60
60
|
description:
|
|
61
|
-
'Domain code must not create raw HTTP servers (express, fastify, etc.). All HTTP handling goes through
|
|
61
|
+
'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.',
|
|
62
62
|
severity: 'error',
|
|
63
63
|
category: 'structure',
|
|
64
64
|
kNodeRef: 'K:convention:tier-1-foundations',
|
|
65
|
-
fixHint: 'Wrap your HTTP handler with
|
|
65
|
+
fixHint: 'Wrap your HTTP handler with defineAction({ exposure: { type: \'api\', ... } }). The scaffold handles API Gateway wiring.',
|
|
66
66
|
exampleBad: "import express from 'express';\nconst app = express();",
|
|
67
|
-
exampleGood: "export const
|
|
67
|
+
exampleGood: "export const myAction = defineAction({ id: 'my-action', exposure: { type: 'api', path: '/v1/my-action', method: 'GET', auth: 'required', tenancy: 'required' }, ... });",
|
|
68
68
|
},
|
|
69
69
|
'require-define-primitive': {
|
|
70
70
|
ruleId: 'require-define-primitive',
|
|
71
71
|
title: 'Require define primitive factories',
|
|
72
72
|
description:
|
|
73
|
-
'All domain handlers must use the appropriate factory function:
|
|
73
|
+
'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.',
|
|
74
74
|
severity: 'error',
|
|
75
75
|
category: 'structure',
|
|
76
76
|
kNodeRef: 'K:runbook:add-domain',
|
|
77
77
|
fixHint: "Wrap your handler with the appropriate define* factory from @mettlecast/domain-runtime.",
|
|
78
78
|
exampleBad: "export const handler = async (event) => ({ statusCode: 200 });",
|
|
79
|
-
exampleGood: "export const
|
|
79
|
+
exampleGood: "export const myAction = defineAction({ id: 'my-action', exposure: { type: 'api', ... }, ... });",
|
|
80
80
|
},
|
|
81
81
|
'flow-domain-ownership': {
|
|
82
82
|
ruleId: 'flow-domain-ownership',
|
|
@@ -106,25 +106,25 @@ const RULE_EXPLANATIONS: Record<string, RuleExplanation> = {
|
|
|
106
106
|
ruleId: 'apis-have-versions',
|
|
107
107
|
title: 'APIs declare versions',
|
|
108
108
|
description:
|
|
109
|
-
'Every
|
|
109
|
+
'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).',
|
|
110
110
|
severity: 'error',
|
|
111
111
|
category: 'correctness',
|
|
112
112
|
kNodeRef: 'K:runbook:add-domain',
|
|
113
|
-
fixHint: 'Add
|
|
114
|
-
exampleBad: "
|
|
115
|
-
exampleGood: "
|
|
113
|
+
fixHint: 'Add Zod input/output schemas to your defineAction call (e.g. z.object({...}).default({...})).',
|
|
114
|
+
exampleBad: "defineAction({ id: 'my-action', exposure: { type: 'api', path: '/v1/my-action', ... }, handler: ... }) // missing input/output schemas",
|
|
115
|
+
exampleGood: "defineAction({ id: 'my-action', exposure: { type: 'api', ... }, input: z.object({...}).default({...}), output: z.object({...}).default({...}), handler: ... });",
|
|
116
116
|
},
|
|
117
117
|
'apis-have-tenancy': {
|
|
118
118
|
ruleId: 'apis-have-tenancy',
|
|
119
119
|
title: 'APIs declare tenancy',
|
|
120
120
|
description:
|
|
121
|
-
'Every
|
|
121
|
+
'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).',
|
|
122
122
|
severity: 'error',
|
|
123
123
|
category: 'correctness',
|
|
124
124
|
kNodeRef: 'K:convention:tier-1-foundations',
|
|
125
|
-
fixHint: "Add tenancy: 'required' | 'none' | 'system' to your
|
|
126
|
-
exampleBad: "
|
|
127
|
-
exampleGood: "
|
|
125
|
+
fixHint: "Add `exposure.tenancy: 'required' | 'none' | 'system'` to your defineAction call.",
|
|
126
|
+
exampleBad: "defineAction({ id: 'my-action', exposure: { type: 'api', path: '/v1/my-action' }, ... }) // missing exposure.tenancy",
|
|
127
|
+
exampleGood: "defineAction({ id: 'my-action', exposure: { type: 'api', path: '/v1/my-action', method: 'GET', auth: 'required', tenancy: 'required' }, ... });",
|
|
128
128
|
},
|
|
129
129
|
'no-cross-domain-imports': {
|
|
130
130
|
ruleId: 'no-cross-domain-imports',
|