@aws/ml-container-creator 0.2.5 → 0.2.6

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 (60) hide show
  1. package/bin/cli.js +7 -2
  2. package/package.json +7 -8
  3. package/servers/base-image-picker/index.js +3 -3
  4. package/servers/base-image-picker/manifest.json +4 -2
  5. package/servers/instance-sizer/index.js +561 -0
  6. package/servers/instance-sizer/lib/instance-ranker.js +245 -0
  7. package/servers/instance-sizer/lib/model-resolver.js +265 -0
  8. package/servers/instance-sizer/lib/vram-estimator.js +177 -0
  9. package/servers/instance-sizer/manifest.json +17 -0
  10. package/servers/instance-sizer/package.json +15 -0
  11. package/servers/{instance-recommender → lib}/catalogs/instances.json +136 -34
  12. package/servers/{base-image-picker → lib}/catalogs/model-servers.json +19 -249
  13. package/servers/lib/catalogs/model-sizes.json +131 -0
  14. package/servers/lib/catalogs/models.json +602 -0
  15. package/servers/{model-picker → lib}/catalogs/popular-diffusors.json +32 -10
  16. package/servers/{model-picker → lib}/catalogs/popular-transformers.json +59 -26
  17. package/servers/{base-image-picker → lib}/catalogs/python-slim.json +12 -12
  18. package/servers/lib/schemas/image-catalog.schema.json +0 -12
  19. package/servers/lib/schemas/instances.schema.json +29 -0
  20. package/servers/lib/schemas/model-catalog.schema.json +12 -10
  21. package/servers/lib/schemas/unified-model-catalog.schema.json +129 -0
  22. package/servers/model-picker/index.js +2 -3
  23. package/servers/model-picker/manifest.json +2 -3
  24. package/servers/region-picker/index.js +1 -1
  25. package/servers/region-picker/manifest.json +1 -1
  26. package/src/app.js +17 -0
  27. package/src/lib/bootstrap-command-handler.js +38 -0
  28. package/src/lib/cli-handler.js +3 -3
  29. package/src/lib/config-manager.js +4 -1
  30. package/src/lib/configuration-manager.js +2 -2
  31. package/src/lib/cross-cutting-checker.js +341 -0
  32. package/src/lib/dry-run-validator.js +78 -0
  33. package/src/lib/generation-validator.js +102 -0
  34. package/src/lib/mcp-validator-config.js +89 -0
  35. package/src/lib/payload-builder.js +153 -0
  36. package/src/lib/prompt-runner.js +445 -135
  37. package/src/lib/prompts.js +1 -1
  38. package/src/lib/registry-loader.js +5 -5
  39. package/src/lib/schema-sync.js +203 -0
  40. package/src/lib/schema-validation-engine.js +195 -0
  41. package/src/lib/service-model-parser.js +102 -0
  42. package/src/lib/validate-runner.js +167 -0
  43. package/src/lib/validation-report.js +133 -0
  44. package/src/lib/validators/base-validator.js +36 -0
  45. package/src/lib/validators/catalog-validator.js +177 -0
  46. package/src/lib/validators/enum-validator.js +120 -0
  47. package/src/lib/validators/required-field-validator.js +150 -0
  48. package/src/lib/validators/type-validator.js +313 -0
  49. package/templates/Dockerfile +1 -1
  50. package/templates/do/build +15 -5
  51. package/templates/do/run +5 -1
  52. package/templates/do/validate +61 -0
  53. package/servers/instance-recommender/LICENSE +0 -202
  54. package/servers/instance-recommender/index.js +0 -284
  55. package/servers/instance-recommender/manifest.json +0 -16
  56. package/servers/instance-recommender/package.json +0 -15
  57. /package/servers/{model-picker → lib}/catalogs/jumpstart-public.json +0 -0
  58. /package/servers/{region-picker → lib}/catalogs/regions.json +0 -0
  59. /package/servers/{base-image-picker → lib}/catalogs/triton-backends.json +0 -0
  60. /package/servers/{base-image-picker → lib}/catalogs/triton.json +0 -0
@@ -45,6 +45,12 @@ export default class BootstrapCommandHandler {
45
45
  * @param {object} options - Parsed CLI options
46
46
  */
47
47
  async handle(args, options) {
48
+ // Handle legacy --sync-schemas flag for backward compatibility
49
+ if (options['sync-schemas']) {
50
+ await this._handleSyncSchemas();
51
+ if (args.length === 0) return;
52
+ }
53
+
48
54
  if (args.length === 0) {
49
55
  await this._handleInteractiveSetup(options);
50
56
  return;
@@ -74,6 +80,9 @@ export default class BootstrapCommandHandler {
74
80
  case 'update':
75
81
  await this._handleUpdate(options);
76
82
  break;
83
+ case 'sync-schemas':
84
+ await this._handleSyncSchemas();
85
+ break;
77
86
  default:
78
87
  console.log(`Unknown bootstrap subcommand: ${subcommand}`);
79
88
  this._showHelp();
@@ -927,6 +936,35 @@ export default class BootstrapCommandHandler {
927
936
  console.log(`\n Done. ${toRemove.length} removed, ${after.length} remaining.`);
928
937
  }
929
938
 
939
+ /**
940
+ * Handle sync-schemas subcommand: download service models and verify AWS CLI.
941
+ */
942
+ async _handleSyncSchemas() {
943
+ console.log('\n📦 Schema Sync — Downloading AWS service models...\n');
944
+
945
+ // Verify AWS CLI is installed
946
+ try {
947
+ const version = execSync('aws --version', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
948
+ console.log(` AWS CLI: ${version}`);
949
+ } catch {
950
+ console.log(' ⚠️ AWS CLI not found.');
951
+ console.log(' Install: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html');
952
+ console.log(' Continuing without AWS CLI verification...\n');
953
+ }
954
+
955
+ // Dynamic import to avoid circular dependencies
956
+ const { syncSchemas } = await import('./schema-sync.js');
957
+ const result = await syncSchemas();
958
+
959
+ if (result.success) {
960
+ console.log('\n ✅ Schema sync complete.');
961
+ } else {
962
+ console.log('\n ⚠️ Schema sync completed with errors (some services may be unavailable).');
963
+ }
964
+
965
+ console.log(` Manifest written: lastSynced = ${result.manifest.lastSynced}\n`);
966
+ }
967
+
930
968
  /**
931
969
  * Re-deploy bootstrap infrastructure using the active profile.
932
970
  * No prompts — reads all config from the existing profile and re-applies
@@ -280,9 +280,9 @@ TRANSFORMER MODEL EXAMPLES:
280
280
 
281
281
  REGISTRY CONTRIBUTION:
282
282
  To contribute to the catalogs:
283
- - Framework Catalog: servers/base-image-picker/catalogs/model-servers.json
284
- - Model Catalog: servers/model-picker/catalogs/popular-transformers.json
285
- - Instance Catalog: servers/instance-recommender/catalogs/instances.json
283
+ - Framework Catalog: servers/lib/catalogs/model-servers.json
284
+ - Model Catalog: servers/lib/catalogs/popular-transformers.json
285
+ - Instance Catalog: servers/lib/catalogs/instances.json
286
286
 
287
287
  See docs/REGISTRY_CONTRIBUTION_GUIDE.md for detailed contribution guidelines.
288
288
 
@@ -29,7 +29,7 @@ import ParameterSchemaValidator from './parameter-schema-validator.js';
29
29
 
30
30
  const __configMgrFilename = fileURLToPath(import.meta.url);
31
31
  const __configMgrDir = dirname(__configMgrFilename);
32
- const tritonBackendsCatalogPath = resolve(__configMgrDir, '../../servers/base-image-picker/catalogs/triton-backends.json');
32
+ const tritonBackendsCatalogPath = resolve(__configMgrDir, '../../servers/lib/catalogs/triton-backends.json');
33
33
 
34
34
  function loadTritonBackendsFromCatalog() {
35
35
  try {
@@ -1927,6 +1927,9 @@ export default class ConfigManager {
1927
1927
  // For required parameters: fill auto-generatable values
1928
1928
  if (this.config[param] === undefined || this.config[param] === null) {
1929
1929
  if (param === 'instanceType') {
1930
+ // If instance-sizer is configured and model is known, defer to sizer
1931
+ // The sizer query happens in PromptRunner after model is selected
1932
+ // For now, set a heuristic default that may be overridden by the sizer
1930
1933
  const arch = architecture || 'http';
1931
1934
  this.config[param] = arch === 'http' ? 'ml.m5.large' : 'ml.g5.xlarge';
1932
1935
  } else if (param === 'modelFormat') {
@@ -619,8 +619,8 @@ export default class ConfigurationManager {
619
619
  */
620
620
  _generateSubmissionInstructions(registryType, config, configEntry) {
621
621
  const registryFile = registryType === 'framework'
622
- ? 'servers/base-image-picker/catalogs/model-servers.json'
623
- : 'servers/model-picker/catalogs/popular-transformers.json';
622
+ ? 'servers/lib/catalogs/model-servers.json'
623
+ : 'servers/lib/catalogs/popular-transformers.json';
624
624
 
625
625
  const registryName = registryType === 'framework'
626
626
  ? 'Framework_Catalog'
@@ -0,0 +1,341 @@
1
+ /* eslint-disable eqeqeq */
2
+ /**
3
+ * Validates consistency rules across multiple payloads and configuration sources.
4
+ * Checks GPU counts, tensor parallelism, model source requirements, role ARN format,
5
+ * CUDA compatibility, and model type / instance alignment.
6
+ *
7
+ * Requirements: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 7.7
8
+ */
9
+ export default class CrossCuttingChecker {
10
+ /**
11
+ * Run all cross-cutting consistency checks.
12
+ * @param {Object} context - ValidationContext from PayloadBuilder
13
+ * @param {Object} instanceCatalog - Instance catalog (from servers/lib/catalogs/instances.json)
14
+ * @returns {Array} Array of Finding objects
15
+ */
16
+ check(context, instanceCatalog) {
17
+ const findings = [];
18
+
19
+ findings.push(...this.checkGpuConsistency(context, instanceCatalog));
20
+ findings.push(...this.checkTensorParallelism(context, instanceCatalog));
21
+ findings.push(...this.checkModelSourceRequirements(context));
22
+ findings.push(...this.checkRoleArnFormat(context));
23
+ findings.push(...this.checkCudaCompatibility(context, instanceCatalog));
24
+ findings.push(...this.checkModelTypeInstanceAlignment(context, instanceCatalog));
25
+
26
+ return findings;
27
+ }
28
+
29
+ /**
30
+ * Verify GPU count consistency: instance type ↔ IC spec.
31
+ * @param {Object} context - ValidationContext
32
+ * @param {Object} instanceCatalog - Instance catalog
33
+ * @returns {Array} Findings
34
+ */
35
+ checkGpuConsistency(context, instanceCatalog) {
36
+ const findings = [];
37
+ const config = context.config || {};
38
+ const catalog = instanceCatalog?.catalog || instanceCatalog || {};
39
+
40
+ const instanceType = config.INSTANCE_TYPE;
41
+ if (!instanceType) return findings;
42
+
43
+ const instanceInfo = catalog[instanceType];
44
+ if (!instanceInfo) return findings;
45
+
46
+ const instanceGpuCount = instanceInfo.gpus;
47
+ if (instanceGpuCount == null || instanceGpuCount === 0) return findings;
48
+
49
+ const icGpuCount = config.IC_GPU_COUNT;
50
+ if (icGpuCount == null) return findings;
51
+
52
+ if (Number(icGpuCount) !== Number(instanceGpuCount)) {
53
+ findings.push({
54
+ service: 'cross-cutting',
55
+ operation: 'configuration',
56
+ fieldPath: 'NumberOfAcceleratorDevicesRequired',
57
+ invalidValue: icGpuCount,
58
+ constraint: {
59
+ type: 'gpu-consistency',
60
+ expected: instanceGpuCount,
61
+ instanceType
62
+ },
63
+ severity: 'error',
64
+ confidence: 'high',
65
+ source: 'cross-cutting',
66
+ remediationHint: `NumberOfAcceleratorDevicesRequired (${icGpuCount}) does not match GPU count (${instanceGpuCount}) for instance type ${instanceType}. Set IC_GPU_COUNT to ${instanceGpuCount}.`
67
+ });
68
+ }
69
+
70
+ return findings;
71
+ }
72
+
73
+ /**
74
+ * Verify tensor parallelism three-way check:
75
+ * VLLM_TENSOR_PARALLEL_SIZE == NumberOfAcceleratorDevicesRequired == instance GPU count.
76
+ * Only applies when model server is vLLM or SGLang.
77
+ * @param {Object} context - ValidationContext
78
+ * @param {Object} instanceCatalog - Instance catalog
79
+ * @returns {Array} Findings
80
+ */
81
+ checkTensorParallelism(context, instanceCatalog) {
82
+ const findings = [];
83
+ const config = context.config || {};
84
+ const catalog = instanceCatalog?.catalog || instanceCatalog || {};
85
+
86
+ const modelServer = config.MODEL_SERVER || config.modelServer || '';
87
+ const normalizedServer = modelServer.toLowerCase();
88
+
89
+ if (normalizedServer !== 'vllm' && normalizedServer !== 'sglang') {
90
+ return findings;
91
+ }
92
+
93
+ const tpSize = config.VLLM_TENSOR_PARALLEL_SIZE;
94
+ if (tpSize == null) return findings;
95
+
96
+ const instanceType = config.INSTANCE_TYPE;
97
+ const instanceInfo = instanceType ? catalog[instanceType] : null;
98
+ const instanceGpuCount = instanceInfo?.gpus;
99
+
100
+ const icGpuCount = config.IC_GPU_COUNT;
101
+
102
+ // Check TP size vs IC GPU count
103
+ if (icGpuCount != null && Number(tpSize) !== Number(icGpuCount)) {
104
+ findings.push({
105
+ service: 'cross-cutting',
106
+ operation: 'configuration',
107
+ fieldPath: 'VLLM_TENSOR_PARALLEL_SIZE',
108
+ invalidValue: tpSize,
109
+ constraint: {
110
+ type: 'tensor-parallelism',
111
+ expected: icGpuCount,
112
+ field: 'NumberOfAcceleratorDevicesRequired'
113
+ },
114
+ severity: 'error',
115
+ confidence: 'high',
116
+ source: 'cross-cutting',
117
+ remediationHint: `VLLM_TENSOR_PARALLEL_SIZE (${tpSize}) must equal NumberOfAcceleratorDevicesRequired (${icGpuCount}) for ${modelServer}.`
118
+ });
119
+ }
120
+
121
+ // Check TP size vs instance GPU count
122
+ if (instanceGpuCount != null && Number(tpSize) !== Number(instanceGpuCount)) {
123
+ findings.push({
124
+ service: 'cross-cutting',
125
+ operation: 'configuration',
126
+ fieldPath: 'VLLM_TENSOR_PARALLEL_SIZE',
127
+ invalidValue: tpSize,
128
+ constraint: {
129
+ type: 'tensor-parallelism',
130
+ expected: instanceGpuCount,
131
+ field: 'instanceGpuCount'
132
+ },
133
+ severity: 'error',
134
+ confidence: 'high',
135
+ source: 'cross-cutting',
136
+ remediationHint: `VLLM_TENSOR_PARALLEL_SIZE (${tpSize}) must equal instance GPU count (${instanceGpuCount}) for ${instanceType}.`
137
+ });
138
+ }
139
+
140
+ return findings;
141
+ }
142
+
143
+ /**
144
+ * Verify model source requirements (artifact URI, hub access config).
145
+ * @param {Object} context - ValidationContext
146
+ * @returns {Array} Findings
147
+ */
148
+ checkModelSourceRequirements(context) {
149
+ const findings = [];
150
+ const config = context.config || {};
151
+
152
+ const modelSource = config.modelSource || config.MODEL_SOURCE || '';
153
+
154
+ // When modelSource is 'jumpstart-hub', verify HubAccessConfig.HubContentArn is present
155
+ if (modelSource === 'jumpstart-hub') {
156
+ const payloads = context.payloads || {};
157
+ let hubContentArnFound = false;
158
+
159
+ for (const payload of Object.values(payloads)) {
160
+ if (payload?.HubAccessConfig?.HubContentArn) {
161
+ hubContentArnFound = true;
162
+ break;
163
+ }
164
+ }
165
+
166
+ if (!hubContentArnFound && !config.HUB_CONTENT_ARN) {
167
+ findings.push({
168
+ service: 'cross-cutting',
169
+ operation: 'configuration',
170
+ fieldPath: 'HubAccessConfig.HubContentArn',
171
+ invalidValue: null,
172
+ constraint: {
173
+ type: 'conditional-required',
174
+ condition: 'modelSource === jumpstart-hub'
175
+ },
176
+ severity: 'error',
177
+ confidence: 'high',
178
+ source: 'cross-cutting',
179
+ remediationHint: 'When modelSource is "jumpstart-hub", HubAccessConfig.HubContentArn must be present in the payload.'
180
+ });
181
+ }
182
+ }
183
+
184
+ // When modelSource in {s3, jumpstart, jumpstart-hub, registry}, verify MODEL_ARTIFACT_URI is non-empty
185
+ const sourcesRequiringArtifact = ['s3', 'jumpstart', 'jumpstart-hub', 'registry'];
186
+ if (sourcesRequiringArtifact.includes(modelSource)) {
187
+ const artifactUri = config.MODEL_ARTIFACT_URI || '';
188
+ if (!artifactUri || artifactUri.trim() === '') {
189
+ findings.push({
190
+ service: 'cross-cutting',
191
+ operation: 'configuration',
192
+ fieldPath: 'MODEL_ARTIFACT_URI',
193
+ invalidValue: artifactUri || null,
194
+ constraint: {
195
+ type: 'conditional-required',
196
+ condition: `modelSource === ${modelSource}`
197
+ },
198
+ severity: 'error',
199
+ confidence: 'high',
200
+ source: 'cross-cutting',
201
+ remediationHint: `When modelSource is "${modelSource}", MODEL_ARTIFACT_URI must be set and non-empty.`
202
+ });
203
+ }
204
+ }
205
+
206
+ return findings;
207
+ }
208
+
209
+ /**
210
+ * Verify role ARN format for realtime-inference.
211
+ * @param {Object} context - ValidationContext
212
+ * @returns {Array} Findings
213
+ */
214
+ checkRoleArnFormat(context) {
215
+ const findings = [];
216
+ const config = context.config || {};
217
+ const deploymentTarget = context.deploymentTarget || '';
218
+
219
+ if (deploymentTarget !== 'realtime-inference') return findings;
220
+
221
+ const roleArn = config.ROLE_ARN;
222
+ if (roleArn == null || roleArn === '') return findings;
223
+
224
+ const arnPattern = /^arn:aws:iam::\d{12}:role\/.+$/;
225
+ if (!arnPattern.test(roleArn)) {
226
+ findings.push({
227
+ service: 'cross-cutting',
228
+ operation: 'configuration',
229
+ fieldPath: 'ROLE_ARN',
230
+ invalidValue: roleArn,
231
+ constraint: {
232
+ type: 'pattern',
233
+ pattern: 'arn:aws:iam::\\d{12}:role/.+'
234
+ },
235
+ severity: 'error',
236
+ confidence: 'high',
237
+ source: 'cross-cutting',
238
+ remediationHint: `ROLE_ARN "${roleArn}" does not match IAM role ARN pattern. Expected format: arn:aws:iam::<12-digit-account-id>:role/<role-name>.`
239
+ });
240
+ }
241
+
242
+ return findings;
243
+ }
244
+
245
+ /**
246
+ * Verify CUDA version compatibility: base image CUDA ∩ instance CUDA versions is non-empty.
247
+ * @param {Object} context - ValidationContext
248
+ * @param {Object} instanceCatalog - Instance catalog
249
+ * @returns {Array} Findings
250
+ */
251
+ checkCudaCompatibility(context, instanceCatalog) {
252
+ const findings = [];
253
+ const config = context.config || {};
254
+ const catalog = instanceCatalog?.catalog || instanceCatalog || {};
255
+
256
+ const instanceType = config.INSTANCE_TYPE;
257
+ if (!instanceType) return findings;
258
+
259
+ const instanceInfo = catalog[instanceType];
260
+ if (!instanceInfo) return findings;
261
+
262
+ const instanceCudaVersions = instanceInfo.cudaVersions;
263
+ if (!instanceCudaVersions || !Array.isArray(instanceCudaVersions) || instanceCudaVersions.length === 0) {
264
+ return findings;
265
+ }
266
+
267
+ // Extract base image CUDA requirement from config
268
+ const cudaRequirement = config.acceleratorVersion || config.CUDA_VERSION || '';
269
+ if (!cudaRequirement) return findings;
270
+
271
+ // Check if any instance CUDA version matches the base image requirement
272
+ // Compare major version (e.g., "12" matches "12.1", "12.2")
273
+ const requiredMajor = String(cudaRequirement).split('.')[0];
274
+
275
+ const hasCompatible = instanceCudaVersions.some(v => {
276
+ const vMajor = String(v).split('.')[0];
277
+ return vMajor === requiredMajor;
278
+ });
279
+
280
+ if (!hasCompatible) {
281
+ findings.push({
282
+ service: 'cross-cutting',
283
+ operation: 'configuration',
284
+ fieldPath: 'acceleratorVersion',
285
+ invalidValue: cudaRequirement,
286
+ constraint: {
287
+ type: 'cuda-compatibility',
288
+ instanceCudaVersions,
289
+ instanceType
290
+ },
291
+ severity: 'error',
292
+ confidence: 'high',
293
+ source: 'cross-cutting',
294
+ remediationHint: `Base image requires CUDA ${cudaRequirement} but instance ${instanceType} supports CUDA versions [${instanceCudaVersions.join(', ')}]. No compatible CUDA version found.`
295
+ });
296
+ }
297
+
298
+ return findings;
299
+ }
300
+
301
+ /**
302
+ * Verify predictor models are not assigned GPU instances.
303
+ * @param {Object} context - ValidationContext
304
+ * @param {Object} instanceCatalog - Instance catalog
305
+ * @returns {Array} Findings
306
+ */
307
+ checkModelTypeInstanceAlignment(context, instanceCatalog) {
308
+ const findings = [];
309
+ const config = context.config || {};
310
+ const catalog = instanceCatalog?.catalog || instanceCatalog || {};
311
+
312
+ const modelType = config.modelType || config.MODEL_TYPE || '';
313
+ if (modelType !== 'predictor') return findings;
314
+
315
+ const instanceType = config.INSTANCE_TYPE;
316
+ if (!instanceType) return findings;
317
+
318
+ const instanceInfo = catalog[instanceType];
319
+ if (!instanceInfo) return findings;
320
+
321
+ if (instanceInfo.gpus > 0 || instanceInfo.category === 'gpu') {
322
+ findings.push({
323
+ service: 'cross-cutting',
324
+ operation: 'configuration',
325
+ fieldPath: 'INSTANCE_TYPE',
326
+ invalidValue: instanceType,
327
+ constraint: {
328
+ type: 'model-type-alignment',
329
+ modelType: 'predictor',
330
+ instanceCategory: instanceInfo.category
331
+ },
332
+ severity: 'warning',
333
+ confidence: 'high',
334
+ source: 'cross-cutting',
335
+ remediationHint: `Model type "predictor" typically does not require GPU acceleration. Consider using a CPU instance (e.g., ml.m5.xlarge) instead of ${instanceType}.`
336
+ });
337
+ }
338
+
339
+ return findings;
340
+ }
341
+ }
@@ -0,0 +1,78 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Dry-run validator — a module that can be called during do/deploy --dry-run.
6
+ * Runs schema validation and blocks deployment if schema errors are found.
7
+ *
8
+ * Returns { passed: boolean, report: ValidationReport }
9
+ *
10
+ * Requirements: 9.1
11
+ */
12
+
13
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
14
+ import path from 'node:path';
15
+ import PayloadBuilder from './payload-builder.js';
16
+ import SchemaValidationEngine from './schema-validation-engine.js';
17
+ import ServiceModelParser from './service-model-parser.js';
18
+ import { getRegistryPath } from './schema-sync.js';
19
+
20
+ /**
21
+ * Run schema validation for dry-run mode.
22
+ * Blocks deployment if schema errors are found.
23
+ *
24
+ * @param {Object} config - Configuration values from do/config
25
+ * @param {string} deploymentTarget - 'realtime-inference' | 'async-inference' | 'batch-transform'
26
+ * @param {Object} [options]
27
+ * @param {boolean} [options.smart] - Enable smart-mode validators
28
+ * @param {string} [options.registryPath] - Override schema registry path
29
+ * @returns {Promise<{ passed: boolean, report: Object|null, skipped: boolean }>}
30
+ */
31
+ export async function validateDryRun(config, deploymentTarget, options = {}) {
32
+ const smart = options.smart || false;
33
+ const registryPath = options.registryPath || getRegistryPath();
34
+
35
+ // Skip if schema registry is not present
36
+ if (!existsSync(registryPath) || !existsSync(path.join(registryPath, 'manifest.json'))) {
37
+ return { passed: true, report: null, skipped: true };
38
+ }
39
+
40
+ // Construct payloads
41
+ const builder = new PayloadBuilder();
42
+ const context = builder.build(config, deploymentTarget);
43
+
44
+ // Load and parse service models from registry
45
+ const parser = new ServiceModelParser();
46
+ const serviceModels = [];
47
+ try {
48
+ const entries = readdirSync(registryPath, { withFileTypes: true });
49
+ for (const entry of entries) {
50
+ if (entry.isDirectory()) {
51
+ const modelPath = path.join(registryPath, entry.name, 'service-2.json');
52
+ if (existsSync(modelPath)) {
53
+ const rawModel = JSON.parse(readFileSync(modelPath, 'utf8'));
54
+ serviceModels.push(parser.parse(rawModel));
55
+ }
56
+ }
57
+ }
58
+ } catch {
59
+ return { passed: true, report: null, skipped: true };
60
+ }
61
+
62
+ // Run validation engine
63
+ const engine = new SchemaValidationEngine({
64
+ registryPath,
65
+ smartMode: smart,
66
+ serviceModels
67
+ });
68
+
69
+ const report = await engine.validate(context);
70
+ const summary = report.getSummary();
71
+
72
+ // Block deployment if errors found
73
+ const passed = summary.errors === 0;
74
+
75
+ return { passed, report, skipped: false };
76
+ }
77
+
78
+ export default { validateDryRun };
@@ -0,0 +1,102 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Generation-time validation helper.
6
+ * Called after deploy scripts are generated to validate payloads against service models.
7
+ * Prints errors as warnings (non-blocking) with a summary line.
8
+ *
9
+ * - Skips silently if schema registry is not present
10
+ * - Skips entirely if --no-validate flag is passed (check via options parameter)
11
+ *
12
+ * This is a standalone module — does NOT modify the main generator file.
13
+ *
14
+ * Requirements: 8.1, 8.2, 8.3, 8.4, 8.5
15
+ */
16
+
17
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
18
+ import path from 'node:path';
19
+ import PayloadBuilder from './payload-builder.js';
20
+ import SchemaValidationEngine from './schema-validation-engine.js';
21
+ import ServiceModelParser from './service-model-parser.js';
22
+ import { getRegistryPath } from './schema-sync.js';
23
+
24
+ /**
25
+ * Run schema validation at generation time (non-blocking).
26
+ *
27
+ * @param {Object} config - Configuration values (from generator answers or do/config)
28
+ * @param {string} deploymentTarget - 'realtime-inference' | 'async-inference' | 'batch-transform'
29
+ * @param {Object} [options]
30
+ * @param {boolean} [options.noValidate] - If true, skip validation entirely
31
+ * @param {string} [options.registryPath] - Override schema registry path
32
+ * @returns {Promise<{ skipped: boolean, report: Object|null }>}
33
+ */
34
+ export async function runGenerationValidation(config, deploymentTarget, options = {}) {
35
+ // Skip entirely if --no-validate flag is passed
36
+ if (options.noValidate) {
37
+ return { skipped: true, report: null };
38
+ }
39
+
40
+ const registryPath = options.registryPath || getRegistryPath();
41
+
42
+ // Skip silently if schema registry is not present
43
+ if (!existsSync(registryPath) || !existsSync(path.join(registryPath, 'manifest.json'))) {
44
+ return { skipped: true, report: null };
45
+ }
46
+
47
+ // Construct payloads
48
+ const builder = new PayloadBuilder();
49
+ const context = builder.build(config, deploymentTarget);
50
+
51
+ // Load and parse service models from registry
52
+ const parser = new ServiceModelParser();
53
+ const serviceModels = [];
54
+ try {
55
+ const entries = readdirSync(registryPath, { withFileTypes: true });
56
+ for (const entry of entries) {
57
+ if (entry.isDirectory()) {
58
+ const modelPath = path.join(registryPath, entry.name, 'service-2.json');
59
+ if (existsSync(modelPath)) {
60
+ const rawModel = JSON.parse(readFileSync(modelPath, 'utf8'));
61
+ serviceModels.push(parser.parse(rawModel));
62
+ }
63
+ }
64
+ }
65
+ } catch {
66
+ // If we can't load models, skip validation silently
67
+ return { skipped: true, report: null };
68
+ }
69
+
70
+ // Run validation
71
+ const engine = new SchemaValidationEngine({
72
+ registryPath,
73
+ ignoreStaleness: true,
74
+ serviceModels
75
+ });
76
+
77
+ const report = await engine.validate(context);
78
+ const summary = report.getSummary();
79
+
80
+ // Print errors as warnings (non-blocking)
81
+ if (summary.errors > 0) {
82
+ console.log('');
83
+ console.log('\x1b[33m⚠️ Schema validation found issues:\x1b[0m');
84
+
85
+ for (const error of report.schemaErrors) {
86
+ const location = [error.operation, error.fieldPath].filter(Boolean).join(' → ');
87
+ console.log(` \x1b[33m⚠\x1b[0m ${location}: ${error.invalidValue || ''} ${error.remediationHint || ''}`);
88
+ }
89
+
90
+ for (const error of report.crossCuttingErrors) {
91
+ const location = [error.operation, error.fieldPath].filter(Boolean).join(' → ');
92
+ console.log(` \x1b[33m⚠\x1b[0m ${location}: ${error.remediationHint || ''}`);
93
+ }
94
+
95
+ console.log('');
96
+ console.log(` ${summary.errors} issue(s) found. Run \x1b[36mdo/validate\x1b[0m before deployment.`);
97
+ }
98
+
99
+ return { skipped: false, report };
100
+ }
101
+
102
+ export default { runGenerationValidation };