@mettlecast/domain-cli 0.2.59 → 0.2.60

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 (49) hide show
  1. package/dist/builder/build-registry.js +128 -13
  2. package/dist/builder/load-module.js +3 -3
  3. package/dist/cli.js +2 -2
  4. package/dist/commands/build-catalog.d.ts +2 -2
  5. package/dist/commands/build-catalog.js +5 -5
  6. package/dist/commands/build-flows.js +1 -1
  7. package/dist/commands/build.js +5 -5
  8. package/dist/commands/check-hashes.js +2 -2
  9. package/dist/commands/create-project.js +1 -1
  10. package/dist/commands/doctor.d.ts +5 -7
  11. package/dist/commands/doctor.js +44 -165
  12. package/dist/commands/power-tune.js +2 -2
  13. package/dist/commands/show-dns.d.ts +1 -1
  14. package/dist/commands/show-dns.js +5 -5
  15. package/dist/commands/upgrade.js +17 -11
  16. package/dist/commands/validate.js +183 -0
  17. package/dist/utils/header-inject.js +2 -2
  18. package/dist/utils/install-file.d.ts +1 -1
  19. package/dist/utils/install-file.js +1 -1
  20. package/dist/utils/manifest.js +1 -2
  21. package/dist/utils/scaffold-config.d.ts +2 -2
  22. package/dist/utils/scaffold-config.js +1 -1
  23. package/package.json +1 -1
  24. package/src/__tests__/commands/check-hashes.test.ts +9 -9
  25. package/src/__tests__/commands/upgrade.test.ts +7 -7
  26. package/src/__tests__/doctor.test.ts +60 -67
  27. package/src/__tests__/package-freshness.test.ts +114 -0
  28. package/src/__tests__/scaffold-src/part-a-layout.test.ts +10 -10
  29. package/src/__tests__/scripts/package-scaffold.test.ts +5 -5
  30. package/src/__tests__/utils/install-file.test.ts +2 -2
  31. package/src/__tests__/utils/manifest.test.ts +2 -2
  32. package/src/__tests__/validate.test.ts +652 -1
  33. package/src/builder/build-registry.ts +147 -15
  34. package/src/builder/load-module.ts +3 -3
  35. package/src/cli.ts +3 -3
  36. package/src/commands/build-catalog.ts +5 -5
  37. package/src/commands/build-flows.ts +1 -1
  38. package/src/commands/build.ts +5 -5
  39. package/src/commands/check-hashes.ts +2 -2
  40. package/src/commands/create-project.ts +1 -1
  41. package/src/commands/doctor.ts +52 -181
  42. package/src/commands/power-tune.ts +2 -2
  43. package/src/commands/show-dns.ts +5 -5
  44. package/src/commands/upgrade.ts +16 -10
  45. package/src/commands/validate.ts +226 -1
  46. package/src/utils/header-inject.ts +2 -2
  47. package/src/utils/install-file.ts +1 -1
  48. package/src/utils/manifest.ts +1 -2
  49. package/src/utils/scaffold-config.ts +3 -3
@@ -1,6 +1,90 @@
1
1
  import { relative, resolve } from 'node:path';
2
2
  import { walkDomainDir } from '../utils/file-helpers.js';
3
3
  import { loadModuleExports } from './load-module.js';
4
+ /**
5
+ * Coerce a raw value into a valid backendAccess scope, defaulting to
6
+ * 'private' when the value is missing or unrecognized. Used by the
7
+ * builder when reading the new-style `backendAccess` field directly.
8
+ */
9
+ function toBackendAccess(raw) {
10
+ return raw === 'domain' || raw === 'platform' ? raw : 'private';
11
+ }
12
+ /**
13
+ * Map a legacy `visibility` scope to a `backendAccess` scope for the
14
+ * action-first migration. The legacy `workspace` scope (which previously
15
+ * permitted unauthenticated Function URL exposure) collapses to `domain`
16
+ * so that all cross-domain callers must go through `ctx.actions`.
17
+ */
18
+ function visibilityToBackendAccess(visibility) {
19
+ if (visibility === 'workspace')
20
+ return 'domain';
21
+ if (visibility === 'domain')
22
+ return 'domain';
23
+ return 'private';
24
+ }
25
+ /**
26
+ * Best-effort reverse mapping from `backendAccess` to the legacy
27
+ * `visibility` field. Used to keep the deprecated field populated so
28
+ * existing CDK constructs that read it continue to behave the same way.
29
+ *
30
+ * - private -> private
31
+ * - domain -> domain
32
+ * - platform -> workspace (closest legacy equivalent for platform-level)
33
+ */
34
+ function backendAccessToVisibility(backendAccess) {
35
+ if (backendAccess === 'platform')
36
+ return 'workspace';
37
+ return backendAccess;
38
+ }
39
+ /**
40
+ * Type guard + sanitizer for an action's `exposure` field. Returns a
41
+ * well-typed `ActionExposure` (extended with `authDeclared` /
42
+ * `tenancyDeclared` tracking flags) or the provided fallback when the
43
+ * raw value does not match a supported exposure shape.
44
+ *
45
+ * The `authDeclared` / `tenancyDeclared` flags record whether the source
46
+ * code explicitly declared each field, or whether the builder fell back
47
+ * to the safe default. They are validation-only and consumed by the
48
+ * domain CLI's validate command (Wave 6 Task 6.1) to enforce the
49
+ * action-first security model (#4619).
50
+ */
51
+ function toExposure(raw, fallback) {
52
+ if (!raw || typeof raw !== 'object')
53
+ return fallback;
54
+ const candidate = raw;
55
+ if (candidate.type === 'internal')
56
+ return { type: 'internal' };
57
+ if (candidate.type !== 'api')
58
+ return fallback;
59
+ // Best-effort validation of api exposure fields; any missing required
60
+ // string field falls back to the supplied default exposure.
61
+ const api = raw;
62
+ if (typeof api.path !== 'string' || typeof api.method !== 'string')
63
+ return fallback;
64
+ const authRaw = api.auth;
65
+ const tenancyRaw = api.tenancy;
66
+ const auth = authRaw === 'required' || authRaw === 'none' || authRaw === 'service' ? authRaw : 'required';
67
+ const tenancy = tenancyRaw === 'required' || tenancyRaw === 'none' || tenancyRaw === 'system' ? tenancyRaw : 'required';
68
+ const out = {
69
+ type: 'api',
70
+ path: api.path,
71
+ method: api.method,
72
+ auth,
73
+ tenancy,
74
+ authDeclared: authRaw === 'required' || authRaw === 'none' || authRaw === 'service',
75
+ tenancyDeclared: tenancyRaw === 'required' || tenancyRaw === 'none' || tenancyRaw === 'system',
76
+ };
77
+ if (Array.isArray(api.roles)) {
78
+ out.roles = api.roles.filter((r) => typeof r === 'string');
79
+ }
80
+ if (api.securityException && typeof api.securityException === 'object') {
81
+ const reason = api.securityException.reason;
82
+ if (typeof reason === 'string') {
83
+ out.securityException = { reason };
84
+ }
85
+ }
86
+ return out;
87
+ }
4
88
  /** Returns true if a value looks like a JSON Schema object (has a 'type' or '$schema' property). */
5
89
  function isJsonSchema(v) {
6
90
  return v !== null && typeof v === 'object' && !Array.isArray(v) &&
@@ -47,7 +131,7 @@ export async function buildRegistry(domainRoot) {
47
131
  if (!domainRaw) {
48
132
  // Emit any suppressed tsx load errors to stderr before throwing so they appear in CI logs.
49
133
  for (const w of warnings)
50
- process.stderr.write(`[tib validate] ${w}\n`);
134
+ process.stderr.write(`[mc-domain-module validate] ${w}\n`);
51
135
  throw new Error(`buildRegistry: no 'domain' export found in ${paths.domain}`);
52
136
  }
53
137
  const domain = {
@@ -149,18 +233,49 @@ export async function buildRegistry(domainRoot) {
149
233
  })));
150
234
  const actions = paths.actions.flatMap((filePath, i) => (actionExports[i] ?? [])
151
235
  .filter(e => e['_kind'] === 'action')
152
- .map(e => ({
153
- id: String(e['id']),
154
- kind: 'action',
155
- handlerFile: relPath(filePath),
156
- visibility: String(e['visibility']),
157
- idempotent: Boolean(e['idempotent'] ?? false),
158
- description: typeof e['description'] === 'string' ? e['description'] : undefined,
159
- deployment: deployment(e),
160
- outboundAccess: outboundAccess(e),
161
- inputSchema: isJsonSchema(e['input']) ? e['input'] : undefined,
162
- outputSchema: isJsonSchema(e['output']) ? e['output'] : undefined,
163
- })));
236
+ .map(e => {
237
+ // Resolve backendAccess. New-style actions carry `backendAccess`
238
+ // directly. Legacy actions only carry `visibility`; in that case
239
+ // we collapse workspace -> domain per the migration spec, and we
240
+ // also remember the legacy flag so we can default exposure to
241
+ // `{ type: 'internal' }` for actions that have not opted in yet.
242
+ const hasBackendAccess = 'backendAccess' in e;
243
+ const rawBackendAccess = e['backendAccess'];
244
+ const rawVisibility = e['visibility'];
245
+ const backendAccess = hasBackendAccess
246
+ ? toBackendAccess(rawBackendAccess)
247
+ : visibilityToBackendAccess(rawVisibility);
248
+ const legacyVisibility = backendAccessToVisibility(backendAccess);
249
+ // Resolve exposure. New-style actions must declare their exposure;
250
+ // legacy actions that did not opt in default to `{ type: 'internal' }`
251
+ // so existing internal-only behavior is preserved during migration.
252
+ const rawExposure = e['exposure'];
253
+ const exposure = toExposure(rawExposure, { type: 'internal' });
254
+ // Record whether the source explicitly declared the `exposure` field.
255
+ // Validation-only; consumed by the domain CLI's validate command
256
+ // (Wave 6 Task 6.1) to enforce `ACTION_EXPOSURE_REQUIRED`.
257
+ const exposureDeclared = rawExposure !== undefined && rawExposure !== null
258
+ && typeof rawExposure === 'object';
259
+ return {
260
+ id: String(e['id']),
261
+ kind: 'action',
262
+ handlerFile: relPath(filePath),
263
+ backendAccess,
264
+ exposure,
265
+ exposureDeclared,
266
+ // Keep the legacy field populated so CDK constructs that still
267
+ // read `visibility` (e.g. action-construct.ts) keep working
268
+ // through the migration window. New constructs should read
269
+ // `backendAccess` and `exposure` instead.
270
+ visibility: legacyVisibility,
271
+ idempotent: Boolean(e['idempotent'] ?? false),
272
+ description: typeof e['description'] === 'string' ? e['description'] : undefined,
273
+ deployment: deployment(e),
274
+ outboundAccess: outboundAccess(e),
275
+ inputSchema: isJsonSchema(e['input']) ? e['input'] : undefined,
276
+ outputSchema: isJsonSchema(e['output']) ? e['output'] : undefined,
277
+ };
278
+ }));
164
279
  const integrations = paths.integrations.flatMap((_filePath, i) => (integrationExports[i] ?? [])
165
280
  .filter(e => e['_kind'] === 'integration')
166
281
  .map(e => ({
@@ -95,9 +95,9 @@ process.stdout.write(JSON.stringify(results));
95
95
  export async function loadModuleExports(absoluteFilePath) {
96
96
  // Use a subdir of the project root rather than OS tmpdir so that ESM import
97
97
  // resolution can walk up and find node_modules packages like zod-to-json-schema.
98
- const tibTmpDir = join(process.cwd(), '.tib', 'tmp');
99
- await mkdir(tibTmpDir, { recursive: true }).catch(() => undefined);
100
- const tempPath = join(tibTmpDir, `tib-load-${randomBytes(8).toString('hex')}.mts`);
98
+ const mcTmpDir = join(process.cwd(), '.mc', 'tmp');
99
+ await mkdir(mcTmpDir, { recursive: true }).catch(() => undefined);
100
+ const tempPath = join(mcTmpDir, `mc-load-${randomBytes(8).toString('hex')}.mts`);
101
101
  await writeFile(tempPath, makeEvalScript(absoluteFilePath), 'utf8');
102
102
  try {
103
103
  return await new Promise((resolve, reject) => {
package/dist/cli.js CHANGED
@@ -75,9 +75,9 @@ program
75
75
  program
76
76
  .command('build-catalog')
77
77
  .description('Merge all per-domain registry files into .mc/domain-registry.json for TIB sync')
78
- .option('--tib-dir <path>', 'Path to the .tib directory (defaults to .tib in cwd)')
78
+ .option('--mc-dir <path>', 'Path to the .mc registry directory (defaults to .mc in cwd)')
79
79
  .action(async (opts) => {
80
- await runBuildCatalog(opts.tibDir);
80
+ await runBuildCatalog(opts.mcDir);
81
81
  });
82
82
  program
83
83
  .command('add-domain <id>')
@@ -92,7 +92,7 @@ export interface DomainCatalog {
92
92
  /**
93
93
  * Build the combined domain catalog from all per-domain registry files.
94
94
  * Reads .mc/{domain}-registry.json files and merges them into .mc/domain-registry.json.
95
- * @param tibDir - Path to the .tib directory. Defaults to .tib in cwd.
95
+ * @param registryDir - Path to the .mc registry directory. Defaults to .mc in cwd.
96
96
  * @returns The written catalog.
97
97
  */
98
- export declare function runBuildCatalog(tibDir?: string): Promise<DomainCatalog>;
98
+ export declare function runBuildCatalog(registryDir?: string): Promise<DomainCatalog>;
@@ -4,22 +4,22 @@ import { cliLogger } from '../utils/logger.js';
4
4
  /**
5
5
  * Build the combined domain catalog from all per-domain registry files.
6
6
  * Reads .mc/{domain}-registry.json files and merges them into .mc/domain-registry.json.
7
- * @param tibDir - Path to the .tib directory. Defaults to .tib in cwd.
7
+ * @param registryDir - Path to the .mc registry directory. Defaults to .mc in cwd.
8
8
  * @returns The written catalog.
9
9
  */
10
- export async function runBuildCatalog(tibDir) {
11
- const dir = tibDir ? resolve(tibDir) : join(process.cwd(), '.tib');
10
+ export async function runBuildCatalog(registryDir) {
11
+ const dir = registryDir ? resolve(registryDir) : join(process.cwd(), '.mc');
12
12
  // Find all per-domain registry files
13
13
  let files;
14
14
  try {
15
15
  files = await readdir(dir);
16
16
  }
17
17
  catch {
18
- throw new Error(`build-catalog: .tib directory not found at ${dir}. Run tib build first.`);
18
+ throw new Error(`build-catalog: .mc registry directory not found at ${dir}. Run mc-domain-module build first.`);
19
19
  }
20
20
  const registryFiles = files.filter(f => f.endsWith('-registry.json') && f !== 'domain-registry.json');
21
21
  if (registryFiles.length === 0) {
22
- throw new Error(`build-catalog: no domain registry files found in ${dir}. Run tib build <domain> first.`);
22
+ throw new Error(`build-catalog: no domain registry files found in ${dir}. Run mc-domain-module build <domain> first.`);
23
23
  }
24
24
  const catalog = {
25
25
  version: 2,
@@ -253,7 +253,7 @@ async function loadFlowsFromDir(dir, owningDomainOverride) {
253
253
  */
254
254
  export async function runBuildFlows(options) {
255
255
  const projectRoot = options.projectRoot ?? process.cwd();
256
- const outFile = options.outFile ?? join(projectRoot, '.tib', 'flows-registry.json');
256
+ const outFile = options.outFile ?? join(projectRoot, '.mc', 'flows-registry.json');
257
257
  cliLogger.info({ projectRoot }, 'Building flows registry');
258
258
  const allEntries = [];
259
259
  const seenIds = new Set();
@@ -15,7 +15,7 @@ export async function runBuild(options) {
15
15
  const domainId = basename(domainRoot);
16
16
  const outFile = options.outFile
17
17
  ? resolve(options.outFile)
18
- : join(process.cwd(), '.tib', `${domainId}-registry.json`);
18
+ : join(process.cwd(), '.mc', `${domainId}-registry.json`);
19
19
  cliLogger.info({ domainRoot }, 'Building domain registry');
20
20
  const { registry, warnings } = await buildRegistry(domainRoot);
21
21
  for (const w of warnings) {
@@ -25,15 +25,15 @@ export async function runBuild(options) {
25
25
  await writeFile(outFile, JSON.stringify(registry), 'utf8');
26
26
  cliLogger.info({ outFile, apis: registry.apis.length, events: registry.events.length }, 'Registry written');
27
27
  // Discover all sibling registries and emit aggregated types
28
- const tibDir = join(process.cwd(), '.tib');
29
- const registryFiles = (await readdir(tibDir)).filter(f => f.endsWith('-registry.json'));
28
+ const registryDir = join(outFile, '..');
29
+ const registryFiles = (await readdir(registryDir)).filter(f => f.endsWith('-registry.json'));
30
30
  const allRegistries = [];
31
31
  for (const f of registryFiles) {
32
- const raw = await readFile(join(tibDir, f), 'utf8');
32
+ const raw = await readFile(join(registryDir, f), 'utf8');
33
33
  allRegistries.push(JSON.parse(raw));
34
34
  }
35
35
  const typesContent = buildActionsTypes(allRegistries);
36
- const typesFile = join(tibDir, 'actions-types.d.ts');
36
+ const typesFile = join(registryDir, 'actions-types.d.ts');
37
37
  await writeFile(typesFile, typesContent);
38
38
  cliLogger.info({ outFile: typesFile }, 'Types file written');
39
39
  return outFile;
@@ -10,14 +10,14 @@ import { cliLogger } from '../utils/logger.js';
10
10
  */
11
11
  export async function runCheckHashes(opts = {}) {
12
12
  const root = opts.projectRoot ?? process.cwd();
13
- const manifestPath = join(root, '.tib', 'modules-hashes.json');
13
+ const manifestPath = join(root, '.mc', 'modules-hashes.json');
14
14
  const modulesDir = join(root, 'infra', 'modules');
15
15
  let manifest;
16
16
  try {
17
17
  manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
18
18
  }
19
19
  catch (err) {
20
- throw new Error(`tib check-hashes: cannot read ${manifestPath} (${err.message}). Re-scaffold to regenerate the manifest.`);
20
+ throw new Error(`mc-domain-module check-hashes: cannot read ${manifestPath} (${err.message}). Re-scaffold to regenerate the manifest.`);
21
21
  }
22
22
  const drifted = [];
23
23
  const missing = [];
@@ -160,7 +160,7 @@ export async function runCreateProject(opts) {
160
160
  const orderedModules = topoSort(modulesJson.modules, selectedIds);
161
161
  // 5. Prepare output directory
162
162
  await mkdir(outputDir, { recursive: true });
163
- await mkdir(join(outputDir, '.tib'), { recursive: true });
163
+ await mkdir(join(outputDir, '.mc'), { recursive: true });
164
164
  // 6. Get CLI version from package.json
165
165
  let cliVersion = '0.0.0';
166
166
  try {
@@ -11,15 +11,13 @@ export declare const DOCTOR_FIX_FLAG = "--fix";
11
11
  export interface DoctorOptions {
12
12
  /** Root directory of the project (defaults to cwd). */
13
13
  projectRoot?: string;
14
- /** Flag to run relocation of old-layout scaffold files to new layout. */
14
+ /** Deprecated legacy flag retained as a no-op for older automation. */
15
15
  relocate?: boolean;
16
16
  /**
17
- * Auto-remediate before reporting. --fix is a superset of --relocate: it
18
- * first runs the relocation routine (auto-moves owned scaffold files outside
19
- * .mc/ into .mc/ and updates .mc/manifest.json), then continues with the
20
- * full doctor report. On a project that is already clean, --fix is a no-op
21
- * (0 files moved, exit 0). Does NOT auto-generate missing fixtures
22
- * (deferred).
17
+ * Auto-remediate before reporting. The legacy relocation routine is now a
18
+ * no-op because current scaffold manifests intentionally track managed files
19
+ * in their installed project locations (infra/modules, domains, .github, etc.).
20
+ * Does NOT auto-generate missing fixtures (deferred).
23
21
  */
24
22
  fix?: boolean;
25
23
  /**
@@ -157,15 +157,14 @@ async function checkGeneratedClientsFresh(projectRoot) {
157
157
  */
158
158
  export async function runDoctor(opts = {}) {
159
159
  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.
160
+ // --relocate is retained as a safe no-op for older automation.
161
+ // --fix runs that no-op preflight first, then the full report.
162
162
  // When both are set, --fix takes precedence so the report still runs.
163
163
  if (opts.relocate && !opts.fix) {
164
164
  return runRelocate(projectRoot);
165
165
  }
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).
166
+ // --fix preflight: run the legacy relocation no-op and merge its check into
167
+ // the doctor report.
169
168
  let preflightChecks = [];
170
169
  if (opts.fix) {
171
170
  const relocationReport = await runRelocate(projectRoot);
@@ -475,13 +474,21 @@ async function checkApisHaveTenancy(projectRoot) {
475
474
  }
476
475
  async function checkScaffoldConfigMatchesDisk(projectRoot) {
477
476
  try {
478
- const configPath = join(projectRoot, '.tib', 'scaffold-config.json');
477
+ const configPath = join(projectRoot, '.mc', 'scaffold-config.json');
479
478
  const domainsDir = join(projectRoot, 'domains');
480
479
  let configDomains = [];
481
480
  try {
482
481
  const configContent = await readFile(configPath, 'utf8');
483
482
  const config = JSON.parse(configContent);
484
- configDomains = config.domainIds ?? [];
483
+ if (!Array.isArray(config.domainIds)) {
484
+ return {
485
+ name: 'scaffold-config.json matches on-disk',
486
+ status: 'PASS',
487
+ message: '.mc/scaffold-config.json has no domainIds array — domain list sync check skipped',
488
+ kNodeRef: 'K:runbook:add-domain',
489
+ };
490
+ }
491
+ configDomains = config.domainIds;
485
492
  }
486
493
  catch {
487
494
  return {
@@ -1114,7 +1121,7 @@ async function checkDomainBrainReachable(projectRoot) {
1114
1121
  */
1115
1122
  async function checkActionsHaveTypes(projectRoot) {
1116
1123
  try {
1117
- const typesPath = join(projectRoot, '.tib', 'actions-types.d.ts');
1124
+ const typesPath = join(projectRoot, '.mc', 'actions-types.d.ts');
1118
1125
  const domainsDir = join(projectRoot, 'domains');
1119
1126
  const discoveredActions = [];
1120
1127
  try {
@@ -1347,7 +1354,7 @@ async function checkSeedPagesHaveNoHeaders(projectRoot) {
1347
1354
  }
1348
1355
  /**
1349
1356
  * Check Part C: Layout validation checks
1350
- * 1. owned files in unexpected locations
1357
+ * 1. managed files are not registered in obsolete legacy locations
1351
1358
  * 2. tracked/seed files inside .mc/
1352
1359
  * 3. manifest entries pointing at missing files
1353
1360
  * 4. scaffold-tracked files (with @mc-scaffold: header) that lack manifest entries
@@ -1361,25 +1368,26 @@ async function checkLayoutPolicy(projectRoot) {
1361
1368
  // No manifest — skip layout checks
1362
1369
  return [];
1363
1370
  }
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) {
1371
+ // Check 1: managed files should not be registered in obsolete legacy
1372
+ // locations. Current scaffold manifests intentionally track managed files at
1373
+ // their installed paths (infra/modules, domains, .github, CLAUDE.md, etc.).
1374
+ const legacyManagedLocations = manifest.files.filter(f => f.policy === 'managed' && (f.path.startsWith('.tib/') ||
1375
+ f.path.startsWith('.mc/infra/') ||
1376
+ f.path === 'mc-deploy.yml'));
1377
+ if (legacyManagedLocations.length === 0) {
1370
1378
  results.push({
1371
- name: 'Layout: owned files in .mc/',
1379
+ name: 'Layout: no managed files in legacy locations',
1372
1380
  status: 'PASS',
1373
- message: 'All owned scaffold files are under .mc/ or .github/workflows/tib-*',
1381
+ message: 'Managed scaffold files use current manifest paths',
1374
1382
  });
1375
1383
  }
1376
1384
  else {
1377
- for (const f of ownedOutsideTib) {
1385
+ for (const f of legacyManagedLocations) {
1378
1386
  results.push({
1379
- name: 'Layout: owned files in .mc/',
1387
+ name: 'Layout: no managed files in legacy locations',
1380
1388
  status: 'FAIL',
1381
- message: `Owned scaffold file outside .mc/: ${f.path}`,
1382
- fixHint: `Run 'tib doctor --relocate' to migrate ${f.path} to .mc/`,
1389
+ message: `Managed scaffold file registered in legacy location: ${f.path}`,
1390
+ fixHint: 'Run `npx mc-domain-module upgrade` to refresh scaffold metadata',
1383
1391
  file: f.path,
1384
1392
  });
1385
1393
  }
@@ -1428,7 +1436,7 @@ async function checkLayoutPolicy(projectRoot) {
1428
1436
  name: 'Layout: manifest entries exist on disk',
1429
1437
  status: 'FAIL',
1430
1438
  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',
1439
+ fixHint: 'Run `npx mc-domain-module upgrade` to refresh scaffold metadata, or restore the missing files',
1432
1440
  });
1433
1441
  }
1434
1442
  // Check 4: scaffold-tracked files (with @mc-scaffold: header) that lack manifest entries
@@ -1483,154 +1491,25 @@ async function checkLayoutPolicy(projectRoot) {
1483
1491
  name: 'Layout: scaffold-tracked files have manifest entries',
1484
1492
  status: 'WARN',
1485
1493
  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',
1494
+ fixHint: 'Run `npx mc-domain-module upgrade` to refresh scaffold metadata',
1487
1495
  });
1488
1496
  }
1489
1497
  return results;
1490
1498
  }
1491
1499
  /**
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
1500
+ * Legacy relocation entrypoint retained for older automation.
1501
+ *
1502
+ * Earlier scaffold versions tried to move generated infra into .mc/infra. The
1503
+ * current scaffold contract keeps generated infra under infra/modules and uses
1504
+ * .mc/manifest.json plus .mc/modules-hashes.json for ownership/drift tracking.
1505
+ * Moving files here would corrupt modern projects, so relocation is now a no-op.
1506
1506
  */
1507
1507
  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 };
1508
+ void projectRoot;
1509
+ const checks = [{
1510
+ name: 'Relocate: deprecated no-op',
1511
+ status: 'PASS',
1512
+ message: 'No files moved. Current scaffold layout keeps generated files at their installed paths and tracks ownership via .mc/manifest.json.',
1513
+ }];
1514
+ return { checks, exitCode: 0, pass: true };
1636
1515
  }
@@ -4,13 +4,13 @@ import path from 'path';
4
4
  import { cliLogger } from '../utils/logger.js';
5
5
  export async function runPowerTune(domain, primitive) {
6
6
  // Load registry to find Lambda ARN
7
- const registryPath = path.join(process.cwd(), '.tib', `${domain}-registry.json`);
7
+ const registryPath = path.join(process.cwd(), '.mc', `${domain}-registry.json`);
8
8
  let registry;
9
9
  try {
10
10
  registry = JSON.parse(readFileSync(registryPath, 'utf8'));
11
11
  }
12
12
  catch {
13
- cliLogger.error({}, `Registry not found at ${registryPath}. Run: tib build domains/${domain}`);
13
+ cliLogger.error({}, `Registry not found at ${registryPath}. Run: npx mc-domain-module build domains/${domain}`);
14
14
  process.exit(1);
15
15
  }
16
16
  // Find Lambda ARN for the primitive
@@ -3,7 +3,7 @@ export interface ShowDnsOptions {
3
3
  projectDir?: string;
4
4
  }
5
5
  /**
6
- * tib show-dns [--env dev|staging|prod] [--project-dir <path>]
6
+ * mc-domain-module show-dns [--env dev|staging|prod] [--project-dir <path>]
7
7
  *
8
8
  * Reads the scaffold-config.json and prints DNS records that must be added for
9
9
  * custom domain wiring: certificate validation CNAMEs (step 1) and traffic