@mettlecast/domain-cli 0.2.6 → 0.2.8

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.
@@ -64,9 +64,14 @@ export async function buildRegistry(domainRoot) {
64
64
  Promise.all(paths.integrations.map(load)),
65
65
  paths.publishes ? load(paths.publishes) : Promise.resolve([]),
66
66
  ]);
67
+ const VALID_API_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
67
68
  const apis = paths.apis.flatMap((filePath, i) => (apiExports[i] ?? [])
68
69
  .filter(e => e['_kind'] === 'api')
69
70
  .map(e => {
71
+ const rawMethod = typeof e['method'] === 'string' ? e['method'].toUpperCase() : '';
72
+ if (!rawMethod || !VALID_API_METHODS.has(rawMethod)) {
73
+ warnings.push(`${relPath(filePath)}: defineApi "${e['id']}" has invalid or missing method "${rawMethod || '(none)'}". Use one of: ${[...VALID_API_METHODS].join(', ')}.`);
74
+ }
70
75
  const rawVersions = e['versions'];
71
76
  const versionSnapshots = rawVersions
72
77
  ? Object.entries(rawVersions).map(([ver, v]) => ({
@@ -81,7 +86,7 @@ export async function buildRegistry(domainRoot) {
81
86
  kind: 'api',
82
87
  handlerFile: relPath(filePath),
83
88
  path: String(e['path']),
84
- method: typeof e['method'] === 'string' ? e['method'].toUpperCase() : 'ANY',
89
+ method: VALID_API_METHODS.has(rawMethod) ? rawMethod : 'GET',
85
90
  authType: e['auth']?.type ?? 'jwt',
86
91
  description: typeof e['description'] === 'string' ? e['description'] : undefined,
87
92
  deployment: deployment(e),
@@ -26,20 +26,23 @@ function makeEvalScript(absoluteFilePath) {
26
26
  // Convert to file:// URL so ESM loader works on all platforms (especially Windows).
27
27
  const fileUrl = pathToFileURL(absoluteFilePath).href;
28
28
  return `
29
- import { zodToJsonSchema } from 'zod-to-json-schema';
29
+ import { z } from 'zod';
30
30
  import * as mod from ${JSON.stringify(fileUrl)};
31
31
  const KINDS = new Set(['api','webhook','subscriber','schedule','job','action','integration','event','domain']);
32
32
  function isZod(v) {
33
33
  return v && typeof v === 'object' && typeof v.parse === 'function' && typeof v.safeParse === 'function' && v._def !== undefined;
34
34
  }
35
+ function toJsonSchema(v) {
36
+ try { return z.toJSONSchema(v); } catch { /* skip unserializable */ }
37
+ }
35
38
  function strip(obj) {
36
39
  const out = {};
37
40
  for (const [k, v] of Object.entries(obj)) {
38
41
  if (typeof v === 'function') continue;
39
42
  if (isZod(v)) {
40
- try { out[k] = zodToJsonSchema(v, { target: 'jsonSchema7' }); } catch { /* skip unserializable */ }
43
+ out[k] = toJsonSchema(v);
41
44
  } else if (Array.isArray(v)) {
42
- out[k] = v.map(item => (item && typeof item === 'object' && !isZod(item) ? strip(item) : isZod(item) ? (() => { try { return zodToJsonSchema(item, { target: 'jsonSchema7' }); } catch { return undefined; } })() : item));
45
+ out[k] = v.map(item => (item && typeof item === 'object' && !isZod(item) ? strip(item) : isZod(item) ? toJsonSchema(item) : item));
43
46
  } else if (v && typeof v === 'object') {
44
47
  out[k] = strip(v);
45
48
  } else {
@@ -42,6 +42,29 @@ export interface CatalogSubscriber {
42
42
  semverRange: string;
43
43
  description?: string;
44
44
  }
45
+ /** A single job entry in the catalog. */
46
+ export interface CatalogJob {
47
+ id: string;
48
+ domainId: string;
49
+ maxRetries: number;
50
+ visibilityTimeoutSeconds: number;
51
+ description?: string;
52
+ }
53
+ /** A single schedule entry in the catalog. */
54
+ export interface CatalogSchedule {
55
+ id: string;
56
+ domainId: string;
57
+ cron: string;
58
+ enabled: boolean;
59
+ description?: string;
60
+ }
61
+ /** A single integration entry in the catalog. */
62
+ export interface CatalogIntegration {
63
+ id: string;
64
+ domainId: string;
65
+ baseUrl: string;
66
+ description?: string;
67
+ }
45
68
  /** A single domain entry in the catalog. */
46
69
  export interface CatalogDomain {
47
70
  id: string;
@@ -58,6 +81,9 @@ export interface DomainCatalog {
58
81
  actions: CatalogAction[];
59
82
  events: CatalogEvent[];
60
83
  subscribers: CatalogSubscriber[];
84
+ jobs: CatalogJob[];
85
+ schedules: CatalogSchedule[];
86
+ integrations: CatalogIntegration[];
61
87
  }
62
88
  /**
63
89
  * Build the combined domain catalog from all per-domain registry files.
@@ -1,4 +1,4 @@
1
- import { readFile, writeFile, readdir } from 'node:fs/promises';
1
+ import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
2
2
  import { join, resolve } from 'node:path';
3
3
  import { cliLogger } from '../utils/logger.js';
4
4
  /**
@@ -29,6 +29,9 @@ export async function runBuildCatalog(tibDir) {
29
29
  actions: [],
30
30
  events: [],
31
31
  subscribers: [],
32
+ jobs: [],
33
+ schedules: [],
34
+ integrations: [],
32
35
  };
33
36
  for (const file of registryFiles) {
34
37
  const raw = await readFile(join(dir, file), 'utf8');
@@ -97,9 +100,40 @@ export async function runBuildCatalog(tibDir) {
97
100
  description: sub['description'],
98
101
  });
99
102
  }
103
+ const jobs = registry['jobs'] ?? [];
104
+ for (const job of jobs) {
105
+ catalog.jobs.push({
106
+ id: String(job['id']),
107
+ domainId,
108
+ maxRetries: Number(job['maxRetries'] ?? 3),
109
+ visibilityTimeoutSeconds: Number(job['visibilityTimeoutSeconds'] ?? 300),
110
+ description: job['description'],
111
+ });
112
+ }
113
+ const schedules = registry['schedules'] ?? [];
114
+ for (const schedule of schedules) {
115
+ catalog.schedules.push({
116
+ id: String(schedule['id']),
117
+ domainId,
118
+ cron: String(schedule['cron'] ?? ''),
119
+ enabled: Boolean(schedule['enabled'] ?? true),
120
+ description: schedule['description'],
121
+ });
122
+ }
123
+ const integrations = registry['integrations'] ?? [];
124
+ for (const integration of integrations) {
125
+ catalog.integrations.push({
126
+ id: String(integration['id']),
127
+ domainId,
128
+ baseUrl: String(integration['baseUrl'] ?? ''),
129
+ description: integration['description'],
130
+ });
131
+ }
100
132
  }
101
- const outPath = join(dir, 'domain-registry.json');
133
+ const mcDir = join(process.cwd(), '.mc');
134
+ await mkdir(mcDir, { recursive: true });
135
+ const outPath = join(mcDir, 'domain-registry.json');
102
136
  await writeFile(outPath, JSON.stringify(catalog, null, 2), 'utf8');
103
- cliLogger.info({ outPath, domains: catalog.domains.length, apis: catalog.apis.length, actions: catalog.actions.length, events: catalog.events.length }, 'Domain catalog written');
137
+ cliLogger.info({ outPath, domains: catalog.domains.length, apis: catalog.apis.length, actions: catalog.actions.length, events: catalog.events.length, jobs: catalog.jobs.length, schedules: catalog.schedules.length, integrations: catalog.integrations.length }, 'Domain catalog written');
104
138
  return catalog;
105
139
  }
@@ -1,5 +1,5 @@
1
1
  import { writeFile, mkdir, readdir, readFile } from 'node:fs/promises';
2
- import { join, resolve } from 'node:path';
2
+ import { join, resolve, basename } from 'node:path';
3
3
  import { buildRegistry } from '../builder/build-registry.js';
4
4
  import { buildActionsTypes } from '../builder/build-types.js';
5
5
  import { cliLogger } from '../utils/logger.js';
@@ -12,9 +12,10 @@ import { cliLogger } from '../utils/logger.js';
12
12
  */
13
13
  export async function runBuild(options) {
14
14
  const domainRoot = resolve(options.domainRoot);
15
+ const domainId = basename(domainRoot);
15
16
  const outFile = options.outFile
16
17
  ? resolve(options.outFile)
17
- : join(process.cwd(), '.tib', 'domain-registry.json');
18
+ : join(process.cwd(), '.tib', `${domainId}-registry.json`);
18
19
  cliLogger.info({ domainRoot }, 'Building domain registry');
19
20
  const { registry, warnings } = await buildRegistry(domainRoot);
20
21
  for (const w of warnings) {
@@ -117,9 +117,48 @@ export async function runValidate(domainRoot, exitOnFailure = true, config = { m
117
117
  }
118
118
  }));
119
119
  errors.push(...checkSubscriberSemverRanges(registry.subscribers));
120
+ const VALID_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
120
121
  for (const api of registry.apis) {
121
122
  const entry = api;
122
123
  errors.push(...checkLifecycleConsistency(entry, `api '${api.id}'`));
124
+ if (!api.requestSchema) {
125
+ errors.push({
126
+ code: 'MISSING_API_REQUEST_SCHEMA',
127
+ message: `API '${api.id}' (${api.method} ${api.path}) has no request schema. Define input/output in defineApi versions.`,
128
+ });
129
+ }
130
+ if (!api.responseSchema) {
131
+ errors.push({
132
+ code: 'MISSING_API_RESPONSE_SCHEMA',
133
+ message: `API '${api.id}' (${api.method} ${api.path}) has no response schema. Define input/output in defineApi versions.`,
134
+ });
135
+ }
136
+ if (!VALID_METHODS.has(api.method)) {
137
+ errors.push({
138
+ code: 'INVALID_API_METHOD',
139
+ message: `API '${api.id}' (${api.method} ${api.path}) has invalid method "${api.method}". Use one of: ${[...VALID_METHODS].join(', ')}.`,
140
+ });
141
+ }
142
+ function hasExampleOrDefault(schema) {
143
+ if (!schema)
144
+ return false;
145
+ if (schema['default'] !== undefined)
146
+ return true;
147
+ const examples = schema['examples'] ?? schema['example'];
148
+ return Array.isArray(examples) && examples.length > 0;
149
+ }
150
+ if (api.requestSchema && !hasExampleOrDefault(api.requestSchema)) {
151
+ errors.push({
152
+ code: 'MISSING_API_REQUEST_EXAMPLE',
153
+ message: `API '${api.id}' (${api.method} ${api.path}) request schema has no example data. Add .default({...}) to your Zod schema.`,
154
+ });
155
+ }
156
+ if (api.responseSchema && !hasExampleOrDefault(api.responseSchema)) {
157
+ errors.push({
158
+ code: 'MISSING_API_RESPONSE_EXAMPLE',
159
+ message: `API '${api.id}' (${api.method} ${api.path}) response schema has no example data. Add .default({...}) to your Zod schema.`,
160
+ });
161
+ }
123
162
  }
124
163
  const rawPathErrors = await checkRawPathViolations(registry.domainRoot, config);
125
164
  errors.push(...rawPathErrors);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mettlecast/domain-cli",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org",
@@ -21,21 +21,21 @@
21
21
  "dependencies": {
22
22
  "@aws-sdk/client-s3": "^3.0.0",
23
23
  "@aws-sdk/client-sfn": "^3.0.0",
24
- "@mettlecast/domain-runtime": "*",
25
24
  "@mettlecast/domain-cdk-packer": "*",
25
+ "@mettlecast/domain-runtime": "*",
26
26
  "commander": "^12.0.0",
27
+ "dotenv": "^16.0.0",
27
28
  "fastify": "^5.0.0",
28
- "tar": "^7.0.0",
29
- "tsx": "^4.0.0",
30
- "zod-to-json-schema": "^3.0.0",
31
29
  "pino": "^9.0.0",
32
30
  "pino-pretty": "^11.0.0",
33
- "dotenv": "^16.0.0",
34
- "semver": "^7.0.0"
31
+ "semver": "^7.0.0",
32
+ "tar": "^7.0.0",
33
+ "tsx": "^4.0.0",
34
+ "zod-to-json-schema": "^3.0.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^20.0.0",
38
- "@types/semver": "^7.0.0",
38
+ "@types/semver": "^7.7.1",
39
39
  "@types/tar": "^6.0.0",
40
40
  "typescript": "^5.0.0",
41
41
  "vitest": "^2.0.0"
@@ -99,10 +99,16 @@ export async function buildRegistry(domainRoot: string): Promise<BuildResult> {
99
99
  paths.publishes ? load(paths.publishes) : Promise.resolve([]),
100
100
  ]);
101
101
 
102
+ const VALID_API_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
103
+
102
104
  const apis: ApiRegistryEntry[] = paths.apis.flatMap((filePath, i) =>
103
105
  (apiExports[i] ?? [])
104
106
  .filter(e => e['_kind'] === 'api')
105
107
  .map(e => {
108
+ const rawMethod = typeof e['method'] === 'string' ? e['method'].toUpperCase() : '';
109
+ if (!rawMethod || !VALID_API_METHODS.has(rawMethod)) {
110
+ warnings.push(`${relPath(filePath)}: defineApi "${e['id']}" has invalid or missing method "${rawMethod || '(none)'}". Use one of: ${[...VALID_API_METHODS].join(', ')}.`);
111
+ }
106
112
  const rawVersions = e['versions'] as Record<string, { input?: unknown; output?: unknown }> | undefined;
107
113
  const versionSnapshots: ApiVersionSnapshot[] = rawVersions
108
114
  ? Object.entries(rawVersions).map(([ver, v]) => ({
@@ -117,7 +123,7 @@ export async function buildRegistry(domainRoot: string): Promise<BuildResult> {
117
123
  kind: 'api' as const,
118
124
  handlerFile: relPath(filePath),
119
125
  path: String(e['path']),
120
- method: typeof e['method'] === 'string' ? e['method'].toUpperCase() : 'ANY',
126
+ method: VALID_API_METHODS.has(rawMethod) ? rawMethod : 'GET',
121
127
  authType: ((e['auth'] as { type?: string } | undefined)?.type as 'jwt' | 'api-key' | 'none' | undefined) ?? 'jwt',
122
128
  description: typeof e['description'] === 'string' ? e['description'] : undefined,
123
129
  deployment: deployment(e),
@@ -43,20 +43,23 @@ function makeEvalScript(absoluteFilePath: string): string {
43
43
  // Convert to file:// URL so ESM loader works on all platforms (especially Windows).
44
44
  const fileUrl = pathToFileURL(absoluteFilePath).href;
45
45
  return `
46
- import { zodToJsonSchema } from 'zod-to-json-schema';
46
+ import { z } from 'zod';
47
47
  import * as mod from ${JSON.stringify(fileUrl)};
48
48
  const KINDS = new Set(['api','webhook','subscriber','schedule','job','action','integration','event','domain']);
49
49
  function isZod(v) {
50
50
  return v && typeof v === 'object' && typeof v.parse === 'function' && typeof v.safeParse === 'function' && v._def !== undefined;
51
51
  }
52
+ function toJsonSchema(v) {
53
+ try { return z.toJSONSchema(v); } catch { /* skip unserializable */ }
54
+ }
52
55
  function strip(obj) {
53
56
  const out = {};
54
57
  for (const [k, v] of Object.entries(obj)) {
55
58
  if (typeof v === 'function') continue;
56
59
  if (isZod(v)) {
57
- try { out[k] = zodToJsonSchema(v, { target: 'jsonSchema7' }); } catch { /* skip unserializable */ }
60
+ out[k] = toJsonSchema(v);
58
61
  } else if (Array.isArray(v)) {
59
- out[k] = v.map(item => (item && typeof item === 'object' && !isZod(item) ? strip(item) : isZod(item) ? (() => { try { return zodToJsonSchema(item, { target: 'jsonSchema7' }); } catch { return undefined; } })() : item));
62
+ out[k] = v.map(item => (item && typeof item === 'object' && !isZod(item) ? strip(item) : isZod(item) ? toJsonSchema(item) : item));
60
63
  } else if (v && typeof v === 'object') {
61
64
  out[k] = strip(v);
62
65
  } else {
@@ -1,4 +1,4 @@
1
- import { readFile, writeFile, readdir } from 'node:fs/promises';
1
+ import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
2
2
  import { join, resolve } from 'node:path';
3
3
  import { cliLogger } from '../utils/logger.js';
4
4
 
@@ -43,6 +43,32 @@ export interface CatalogSubscriber {
43
43
  description?: string;
44
44
  }
45
45
 
46
+ /** A single job entry in the catalog. */
47
+ export interface CatalogJob {
48
+ id: string;
49
+ domainId: string;
50
+ maxRetries: number;
51
+ visibilityTimeoutSeconds: number;
52
+ description?: string;
53
+ }
54
+
55
+ /** A single schedule entry in the catalog. */
56
+ export interface CatalogSchedule {
57
+ id: string;
58
+ domainId: string;
59
+ cron: string;
60
+ enabled: boolean;
61
+ description?: string;
62
+ }
63
+
64
+ /** A single integration entry in the catalog. */
65
+ export interface CatalogIntegration {
66
+ id: string;
67
+ domainId: string;
68
+ baseUrl: string;
69
+ description?: string;
70
+ }
71
+
46
72
  /** A single domain entry in the catalog. */
47
73
  export interface CatalogDomain {
48
74
  id: string;
@@ -60,6 +86,9 @@ export interface DomainCatalog {
60
86
  actions: CatalogAction[];
61
87
  events: CatalogEvent[];
62
88
  subscribers: CatalogSubscriber[];
89
+ jobs: CatalogJob[];
90
+ schedules: CatalogSchedule[];
91
+ integrations: CatalogIntegration[];
63
92
  }
64
93
 
65
94
  /**
@@ -92,6 +121,9 @@ export async function runBuildCatalog(tibDir?: string): Promise<DomainCatalog> {
92
121
  actions: [],
93
122
  events: [],
94
123
  subscribers: [],
124
+ jobs: [],
125
+ schedules: [],
126
+ integrations: [],
95
127
  };
96
128
 
97
129
  for (const file of registryFiles) {
@@ -167,13 +199,47 @@ export async function runBuildCatalog(tibDir?: string): Promise<DomainCatalog> {
167
199
  description: sub['description'] as string | undefined,
168
200
  });
169
201
  }
202
+
203
+ const jobs = (registry['jobs'] as Record<string, unknown>[] | undefined) ?? [];
204
+ for (const job of jobs) {
205
+ catalog.jobs.push({
206
+ id: String(job['id']),
207
+ domainId,
208
+ maxRetries: Number(job['maxRetries'] ?? 3),
209
+ visibilityTimeoutSeconds: Number(job['visibilityTimeoutSeconds'] ?? 300),
210
+ description: job['description'] as string | undefined,
211
+ });
212
+ }
213
+
214
+ const schedules = (registry['schedules'] as Record<string, unknown>[] | undefined) ?? [];
215
+ for (const schedule of schedules) {
216
+ catalog.schedules.push({
217
+ id: String(schedule['id']),
218
+ domainId,
219
+ cron: String(schedule['cron'] ?? ''),
220
+ enabled: Boolean(schedule['enabled'] ?? true),
221
+ description: schedule['description'] as string | undefined,
222
+ });
223
+ }
224
+
225
+ const integrations = (registry['integrations'] as Record<string, unknown>[] | undefined) ?? [];
226
+ for (const integration of integrations) {
227
+ catalog.integrations.push({
228
+ id: String(integration['id']),
229
+ domainId,
230
+ baseUrl: String(integration['baseUrl'] ?? ''),
231
+ description: integration['description'] as string | undefined,
232
+ });
233
+ }
170
234
  }
171
235
 
172
- const outPath = join(dir, 'domain-registry.json');
236
+ const mcDir = join(process.cwd(), '.mc');
237
+ await mkdir(mcDir, { recursive: true });
238
+ const outPath = join(mcDir, 'domain-registry.json');
173
239
  await writeFile(outPath, JSON.stringify(catalog, null, 2), 'utf8');
174
240
 
175
241
  cliLogger.info(
176
- { outPath, domains: catalog.domains.length, apis: catalog.apis.length, actions: catalog.actions.length, events: catalog.events.length },
242
+ { outPath, domains: catalog.domains.length, apis: catalog.apis.length, actions: catalog.actions.length, events: catalog.events.length, jobs: catalog.jobs.length, schedules: catalog.schedules.length, integrations: catalog.integrations.length },
177
243
  'Domain catalog written'
178
244
  );
179
245
 
@@ -1,5 +1,5 @@
1
1
  import { writeFile, mkdir, readdir, readFile } from 'node:fs/promises';
2
- import { join, resolve } from 'node:path';
2
+ import { join, resolve, basename } from 'node:path';
3
3
  import type { DomainRegistry } from '@mettlecast/domain-cdk-packer';
4
4
  import { buildRegistry } from '../builder/build-registry.js';
5
5
  import { buildActionsTypes } from '../builder/build-types.js';
@@ -24,9 +24,10 @@ export interface BuildOptions {
24
24
  */
25
25
  export async function runBuild(options: BuildOptions): Promise<string> {
26
26
  const domainRoot = resolve(options.domainRoot);
27
+ const domainId = basename(domainRoot);
27
28
  const outFile = options.outFile
28
29
  ? resolve(options.outFile)
29
- : join(process.cwd(), '.tib', 'domain-registry.json');
30
+ : join(process.cwd(), '.tib', `${domainId}-registry.json`);
30
31
 
31
32
  cliLogger.info({ domainRoot }, 'Building domain registry');
32
33
 
@@ -173,9 +173,46 @@ export async function runValidate(
173
173
 
174
174
  errors.push(...checkSubscriberSemverRanges(registry.subscribers));
175
175
 
176
+ const VALID_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
176
177
  for (const api of registry.apis) {
177
178
  const entry = api as unknown as { deprecatedAt?: string; sunsetAt?: string };
178
179
  errors.push(...checkLifecycleConsistency(entry, `api '${api.id}'`));
180
+ if (!api.requestSchema) {
181
+ errors.push({
182
+ code: 'MISSING_API_REQUEST_SCHEMA',
183
+ message: `API '${api.id}' (${api.method} ${api.path}) has no request schema. Define input/output in defineApi versions.`,
184
+ });
185
+ }
186
+ if (!api.responseSchema) {
187
+ errors.push({
188
+ code: 'MISSING_API_RESPONSE_SCHEMA',
189
+ message: `API '${api.id}' (${api.method} ${api.path}) has no response schema. Define input/output in defineApi versions.`,
190
+ });
191
+ }
192
+ if (!VALID_METHODS.has(api.method)) {
193
+ errors.push({
194
+ code: 'INVALID_API_METHOD',
195
+ message: `API '${api.id}' (${api.method} ${api.path}) has invalid method "${api.method}". Use one of: ${[...VALID_METHODS].join(', ')}.`,
196
+ });
197
+ }
198
+ function hasExampleOrDefault(schema: Record<string, unknown> | undefined): boolean {
199
+ if (!schema) return false;
200
+ if (schema['default'] !== undefined) return true;
201
+ const examples = schema['examples'] ?? schema['example'];
202
+ return Array.isArray(examples) && examples.length > 0;
203
+ }
204
+ if (api.requestSchema && !hasExampleOrDefault(api.requestSchema)) {
205
+ errors.push({
206
+ code: 'MISSING_API_REQUEST_EXAMPLE',
207
+ message: `API '${api.id}' (${api.method} ${api.path}) request schema has no example data. Add .default({...}) to your Zod schema.`,
208
+ });
209
+ }
210
+ if (api.responseSchema && !hasExampleOrDefault(api.responseSchema)) {
211
+ errors.push({
212
+ code: 'MISSING_API_RESPONSE_EXAMPLE',
213
+ message: `API '${api.id}' (${api.method} ${api.path}) response schema has no example data. Add .default({...}) to your Zod schema.`,
214
+ });
215
+ }
179
216
  }
180
217
 
181
218
  const rawPathErrors = await checkRawPathViolations(registry.domainRoot, config);