@aws/ml-container-creator 0.2.4 → 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.
- package/README.md +62 -298
- package/bin/cli.js +7 -2
- package/package.json +7 -8
- package/servers/base-image-picker/index.js +3 -3
- package/servers/base-image-picker/manifest.json +4 -2
- package/servers/instance-sizer/index.js +561 -0
- package/servers/instance-sizer/lib/instance-ranker.js +245 -0
- package/servers/instance-sizer/lib/model-resolver.js +265 -0
- package/servers/instance-sizer/lib/vram-estimator.js +177 -0
- package/servers/instance-sizer/manifest.json +17 -0
- package/servers/instance-sizer/package.json +15 -0
- package/servers/{instance-recommender → lib}/catalogs/instances.json +136 -34
- package/servers/{base-image-picker → lib}/catalogs/model-servers.json +19 -249
- package/servers/lib/catalogs/model-sizes.json +131 -0
- package/servers/lib/catalogs/models.json +602 -0
- package/servers/{model-picker → lib}/catalogs/popular-diffusors.json +32 -10
- package/servers/{model-picker → lib}/catalogs/popular-transformers.json +59 -26
- package/servers/{base-image-picker → lib}/catalogs/python-slim.json +12 -12
- package/servers/lib/schemas/image-catalog.schema.json +0 -12
- package/servers/lib/schemas/instances.schema.json +29 -0
- package/servers/lib/schemas/model-catalog.schema.json +12 -10
- package/servers/lib/schemas/unified-model-catalog.schema.json +129 -0
- package/servers/model-picker/index.js +2 -3
- package/servers/model-picker/manifest.json +2 -3
- package/servers/region-picker/index.js +1 -1
- package/servers/region-picker/manifest.json +1 -1
- package/src/app.js +17 -0
- package/src/lib/bootstrap-command-handler.js +38 -0
- package/src/lib/cli-handler.js +3 -3
- package/src/lib/config-manager.js +4 -1
- package/src/lib/configuration-manager.js +2 -2
- package/src/lib/cross-cutting-checker.js +341 -0
- package/src/lib/dry-run-validator.js +78 -0
- package/src/lib/generation-validator.js +102 -0
- package/src/lib/mcp-validator-config.js +89 -0
- package/src/lib/payload-builder.js +153 -0
- package/src/lib/prompt-runner.js +445 -135
- package/src/lib/prompts.js +1 -1
- package/src/lib/registry-loader.js +5 -5
- package/src/lib/schema-sync.js +203 -0
- package/src/lib/schema-validation-engine.js +195 -0
- package/src/lib/service-model-parser.js +102 -0
- package/src/lib/validate-runner.js +167 -0
- package/src/lib/validation-report.js +133 -0
- package/src/lib/validators/base-validator.js +36 -0
- package/src/lib/validators/catalog-validator.js +177 -0
- package/src/lib/validators/enum-validator.js +120 -0
- package/src/lib/validators/required-field-validator.js +150 -0
- package/src/lib/validators/type-validator.js +313 -0
- package/templates/Dockerfile +1 -1
- package/templates/do/build +15 -5
- package/templates/do/run +5 -1
- package/templates/do/validate +61 -0
- package/servers/instance-recommender/LICENSE +0 -202
- package/servers/instance-recommender/index.js +0 -284
- package/servers/instance-recommender/manifest.json +0 -16
- package/servers/instance-recommender/package.json +0 -15
- /package/servers/{model-picker → lib}/catalogs/jumpstart-public.json +0 -0
- /package/servers/{region-picker → lib}/catalogs/regions.json +0 -0
- /package/servers/{base-image-picker → lib}/catalogs/triton-backends.json +0 -0
- /package/servers/{base-image-picker → lib}/catalogs/triton.json +0 -0
package/src/lib/prompts.js
CHANGED
|
@@ -14,7 +14,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
14
14
|
|
|
15
15
|
const __promptsFilename = fileURLToPath(import.meta.url);
|
|
16
16
|
const __promptsDir = dirname(__promptsFilename);
|
|
17
|
-
const instancesCatalogPath = resolve(__promptsDir, '../../servers/
|
|
17
|
+
const instancesCatalogPath = resolve(__promptsDir, '../../servers/lib/catalogs/instances.json');
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Load instance types from the instances.json catalog and transform
|
|
@@ -18,11 +18,11 @@ const __dirname = dirname(__filename);
|
|
|
18
18
|
|
|
19
19
|
// Catalog file paths relative to this module
|
|
20
20
|
const CATALOG_PATHS = {
|
|
21
|
-
modelServers: resolve(__dirname, '../../servers/
|
|
22
|
-
tritonBackends: resolve(__dirname, '../../servers/
|
|
23
|
-
instances: resolve(__dirname, '../../servers/
|
|
24
|
-
popularTransformers: resolve(__dirname, '../../servers/
|
|
25
|
-
popularDiffusors: resolve(__dirname, '../../servers/
|
|
21
|
+
modelServers: resolve(__dirname, '../../servers/lib/catalogs/model-servers.json'),
|
|
22
|
+
tritonBackends: resolve(__dirname, '../../servers/lib/catalogs/triton-backends.json'),
|
|
23
|
+
instances: resolve(__dirname, '../../servers/lib/catalogs/instances.json'),
|
|
24
|
+
popularTransformers: resolve(__dirname, '../../servers/lib/catalogs/popular-transformers.json'),
|
|
25
|
+
popularDiffusors: resolve(__dirname, '../../servers/lib/catalogs/popular-diffusors.json')
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
class RegistryLoader {
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Schema Sync — Downloads AWS service model files from the AWS SDK GitHub source
|
|
6
|
+
* and stores them in the local schema registry.
|
|
7
|
+
*
|
|
8
|
+
* Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 10.1
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import https from 'node:https';
|
|
12
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
|
|
16
|
+
const SERVICES = ['sagemaker', 'iam', 'ecr', 's3'];
|
|
17
|
+
|
|
18
|
+
const SOURCE_BASE_URL = 'https://raw.githubusercontent.com/aws/aws-sdk-js-v3/main/codegen/sdk-codegen/aws-models';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get the default schema registry path.
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
export function getRegistryPath() {
|
|
25
|
+
return path.join(os.homedir(), '.ml-container-creator', 'schemas');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Download a file from a URL using the built-in https module.
|
|
30
|
+
* @param {string} url - URL to download
|
|
31
|
+
* @returns {Promise<string>} - Response body as string
|
|
32
|
+
*/
|
|
33
|
+
function downloadFile(url) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
https.get(url, (res) => {
|
|
36
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
37
|
+
downloadFile(res.headers.location).then(resolve).catch(reject);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (res.statusCode !== 200) {
|
|
42
|
+
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const chunks = [];
|
|
47
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
48
|
+
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
49
|
+
res.on('error', reject);
|
|
50
|
+
}).on('error', reject);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Count shapes and enums in a service model.
|
|
56
|
+
* @param {object} model - Parsed service model JSON
|
|
57
|
+
* @returns {{ shapeCount: number, enumCount: number, version: string }}
|
|
58
|
+
*/
|
|
59
|
+
function getModelStats(model) {
|
|
60
|
+
const shapes = model.shapes || {};
|
|
61
|
+
let shapeCount = 0;
|
|
62
|
+
let enumCount = 0;
|
|
63
|
+
|
|
64
|
+
for (const shape of Object.values(shapes)) {
|
|
65
|
+
shapeCount++;
|
|
66
|
+
if (shape.type === 'string' && shape.enum) {
|
|
67
|
+
enumCount++;
|
|
68
|
+
}
|
|
69
|
+
// Smithy models use traits for enums
|
|
70
|
+
if (shape.traits && shape.traits['smithy.api#enum']) {
|
|
71
|
+
enumCount++;
|
|
72
|
+
}
|
|
73
|
+
// Smithy v2 enum shapes
|
|
74
|
+
if (shape.type === 'enum') {
|
|
75
|
+
enumCount++;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const version = model.metadata?.apiVersion || '';
|
|
80
|
+
|
|
81
|
+
return { shapeCount, enumCount, version };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Sync all service models from the AWS SDK GitHub source.
|
|
86
|
+
* @param {object} [options]
|
|
87
|
+
* @param {string} [options.registryPath] - Override registry path
|
|
88
|
+
* @param {function} [options.downloadFn] - Override download function (for testing)
|
|
89
|
+
* @returns {Promise<{ success: boolean, services: object }>}
|
|
90
|
+
*/
|
|
91
|
+
export async function syncSchemas(options = {}) {
|
|
92
|
+
const registryPath = options.registryPath || getRegistryPath();
|
|
93
|
+
const download = options.downloadFn || downloadFile;
|
|
94
|
+
|
|
95
|
+
// Ensure registry directory exists
|
|
96
|
+
mkdirSync(registryPath, { recursive: true });
|
|
97
|
+
|
|
98
|
+
const serviceStats = {};
|
|
99
|
+
let hasErrors = false;
|
|
100
|
+
|
|
101
|
+
for (const service of SERVICES) {
|
|
102
|
+
const url = `${SOURCE_BASE_URL}/${service}.json`;
|
|
103
|
+
const serviceDir = path.join(registryPath, service);
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
console.log(` Syncing ${service}...`);
|
|
107
|
+
const content = await download(url);
|
|
108
|
+
|
|
109
|
+
// Parse to extract stats
|
|
110
|
+
let model;
|
|
111
|
+
try {
|
|
112
|
+
model = JSON.parse(content);
|
|
113
|
+
} catch (parseErr) {
|
|
114
|
+
console.log(` ⚠️ ${service}: Failed to parse model — ${parseErr.message}`);
|
|
115
|
+
hasErrors = true;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const stats = getModelStats(model);
|
|
120
|
+
|
|
121
|
+
// Store the file
|
|
122
|
+
mkdirSync(serviceDir, { recursive: true });
|
|
123
|
+
writeFileSync(path.join(serviceDir, 'service-2.json'), content, 'utf8');
|
|
124
|
+
|
|
125
|
+
serviceStats[service] = {
|
|
126
|
+
shapeCount: stats.shapeCount,
|
|
127
|
+
enumCount: stats.enumCount,
|
|
128
|
+
version: stats.version
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
console.log(` ✅ ${service}: ${stats.shapeCount} shapes, ${stats.enumCount} enums`);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.log(` ⚠️ ${service}: ${err.message}`);
|
|
134
|
+
hasErrors = true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Write manifest
|
|
139
|
+
const manifest = {
|
|
140
|
+
lastSynced: new Date().toISOString(),
|
|
141
|
+
services: serviceStats,
|
|
142
|
+
source: 'https://github.com/aws/aws-sdk-js-v3/tree/main/codegen/sdk-codegen/aws-models'
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
writeFileSync(
|
|
146
|
+
path.join(registryPath, 'manifest.json'),
|
|
147
|
+
JSON.stringify(manifest, null, 4),
|
|
148
|
+
'utf8'
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return { success: !hasErrors, services: serviceStats, manifest };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Load the manifest from the schema registry.
|
|
156
|
+
* @param {string} [registryPath] - Override registry path
|
|
157
|
+
* @returns {object|null} Parsed manifest or null if not found
|
|
158
|
+
*/
|
|
159
|
+
export function loadManifest(registryPath) {
|
|
160
|
+
const regPath = registryPath || getRegistryPath();
|
|
161
|
+
const manifestPath = path.join(regPath, 'manifest.json');
|
|
162
|
+
|
|
163
|
+
if (!existsSync(manifestPath)) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
return JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Load a service model from the registry.
|
|
176
|
+
* @param {string} serviceName - Service name (e.g., 'sagemaker')
|
|
177
|
+
* @param {string} [registryPath] - Override registry path
|
|
178
|
+
* @returns {string|null} Raw file content or null if not found
|
|
179
|
+
*/
|
|
180
|
+
export function loadServiceModel(serviceName, registryPath) {
|
|
181
|
+
const regPath = registryPath || getRegistryPath();
|
|
182
|
+
const modelPath = path.join(regPath, serviceName, 'service-2.json');
|
|
183
|
+
|
|
184
|
+
if (!existsSync(modelPath)) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return readFileSync(modelPath, 'utf8');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Store a service model in the registry.
|
|
193
|
+
* @param {string} serviceName - Service name (e.g., 'sagemaker')
|
|
194
|
+
* @param {string} content - Raw file content to store
|
|
195
|
+
* @param {string} [registryPath] - Override registry path
|
|
196
|
+
*/
|
|
197
|
+
export function storeServiceModel(serviceName, content, registryPath) {
|
|
198
|
+
const regPath = registryPath || getRegistryPath();
|
|
199
|
+
const serviceDir = path.join(regPath, serviceName);
|
|
200
|
+
|
|
201
|
+
mkdirSync(serviceDir, { recursive: true });
|
|
202
|
+
writeFileSync(path.join(serviceDir, 'service-2.json'), content, 'utf8');
|
|
203
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import ValidationReport from './validation-report.js';
|
|
2
|
+
import EnumValidator from './validators/enum-validator.js';
|
|
3
|
+
import TypeValidator from './validators/type-validator.js';
|
|
4
|
+
import RequiredFieldValidator from './validators/required-field-validator.js';
|
|
5
|
+
import CrossCuttingChecker from './cross-cutting-checker.js';
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Core validation orchestrator.
|
|
11
|
+
* Loads service models, runs static and smart validators, and produces a report.
|
|
12
|
+
*
|
|
13
|
+
* Requirements: 12.1, 12.2, 12.5, 15.4, 15.5
|
|
14
|
+
*/
|
|
15
|
+
export default class SchemaValidationEngine {
|
|
16
|
+
/**
|
|
17
|
+
* @param {Object} options
|
|
18
|
+
* @param {string} options.registryPath - Path to schema registry
|
|
19
|
+
* @param {boolean} options.ignoreStaleness - Suppress staleness warnings
|
|
20
|
+
* @param {boolean} options.smartMode - Enable smart-mode validators
|
|
21
|
+
*/
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.registryPath = options.registryPath || null;
|
|
24
|
+
this.ignoreStaleness = options.ignoreStaleness || false;
|
|
25
|
+
this.smartMode = options.smartMode || false;
|
|
26
|
+
this.validators = [];
|
|
27
|
+
this.serviceModels = options.serviceModels || [];
|
|
28
|
+
this.instanceCatalog = options.instanceCatalog || null;
|
|
29
|
+
this.crossCuttingChecker = new CrossCuttingChecker();
|
|
30
|
+
|
|
31
|
+
// Auto-register built-in validators
|
|
32
|
+
this.registerValidator(new EnumValidator());
|
|
33
|
+
this.registerValidator(new TypeValidator());
|
|
34
|
+
this.registerValidator(new RequiredFieldValidator());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Run full validation pipeline.
|
|
39
|
+
* Orchestrate: load models → run static validators → run smart validators (if enabled) → return report.
|
|
40
|
+
* @param {Object} context - ValidationContext from PayloadBuilder
|
|
41
|
+
* @returns {Promise<ValidationReport>}
|
|
42
|
+
*/
|
|
43
|
+
async validate(context) {
|
|
44
|
+
const report = new ValidationReport();
|
|
45
|
+
|
|
46
|
+
// Run static validators (mode === 'static' or 'both')
|
|
47
|
+
const staticValidators = this.validators.filter(
|
|
48
|
+
v => v.mode === 'static' || v.mode === 'both'
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const priorFindings = [];
|
|
52
|
+
|
|
53
|
+
for (const validator of staticValidators) {
|
|
54
|
+
try {
|
|
55
|
+
const findings = await validator.validate(context, {
|
|
56
|
+
priorFindings: [...priorFindings],
|
|
57
|
+
serviceModels: this.serviceModels
|
|
58
|
+
});
|
|
59
|
+
for (const finding of findings) {
|
|
60
|
+
report.addFinding(finding);
|
|
61
|
+
priorFindings.push(finding);
|
|
62
|
+
}
|
|
63
|
+
} catch (err) {
|
|
64
|
+
report.warnings.push({
|
|
65
|
+
source: 'engine',
|
|
66
|
+
severity: 'warning',
|
|
67
|
+
operation: '',
|
|
68
|
+
fieldPath: '',
|
|
69
|
+
remediationHint: `Plugin "${validator.name}" threw an error: ${err.message}`
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Run cross-cutting checks after schema validators
|
|
75
|
+
if (this.instanceCatalog) {
|
|
76
|
+
try {
|
|
77
|
+
const crossCuttingFindings = this.crossCuttingChecker.check(context, this.instanceCatalog);
|
|
78
|
+
for (const finding of crossCuttingFindings) {
|
|
79
|
+
report.addFinding(finding);
|
|
80
|
+
priorFindings.push(finding);
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
report.warnings.push({
|
|
84
|
+
source: 'engine',
|
|
85
|
+
severity: 'warning',
|
|
86
|
+
operation: '',
|
|
87
|
+
fieldPath: '',
|
|
88
|
+
remediationHint: `Cross-cutting checker threw an error: ${err.message}`
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Run smart validators if enabled (mode === 'smart' or 'both')
|
|
94
|
+
if (this.smartMode) {
|
|
95
|
+
const smartValidators = this.validators.filter(
|
|
96
|
+
v => v.mode === 'smart' || v.mode === 'both'
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
for (const validator of smartValidators) {
|
|
100
|
+
// Skip validators already run in static pass (mode === 'both')
|
|
101
|
+
if (validator.mode === 'both' && staticValidators.includes(validator)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const findings = await validator.validate(context, {
|
|
107
|
+
priorFindings: [...priorFindings],
|
|
108
|
+
serviceModels: this.serviceModels
|
|
109
|
+
});
|
|
110
|
+
for (const finding of findings) {
|
|
111
|
+
report.addFinding(finding);
|
|
112
|
+
priorFindings.push(finding);
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
report.warnings.push({
|
|
116
|
+
source: 'engine',
|
|
117
|
+
severity: 'warning',
|
|
118
|
+
operation: '',
|
|
119
|
+
fieldPath: '',
|
|
120
|
+
remediationHint: `Smart plugin "${validator.name}" threw an error: ${err.message}`
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return report;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Register a custom validator plugin.
|
|
131
|
+
* @param {Object} validator - A BaseValidator instance
|
|
132
|
+
*/
|
|
133
|
+
registerValidator(validator) {
|
|
134
|
+
this.validators.push(validator);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check schema registry staleness.
|
|
139
|
+
* @returns {{ stale: boolean, lastSynced: string|null, daysSinceSync: number, registryMissing?: boolean }}
|
|
140
|
+
*/
|
|
141
|
+
checkStaleness() {
|
|
142
|
+
if (!this.registryPath) {
|
|
143
|
+
return {
|
|
144
|
+
stale: false,
|
|
145
|
+
lastSynced: null,
|
|
146
|
+
daysSinceSync: 0,
|
|
147
|
+
registryMissing: true
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let manifest;
|
|
152
|
+
try {
|
|
153
|
+
const manifestPath = path.join(this.registryPath, 'manifest.json');
|
|
154
|
+
|
|
155
|
+
if (!existsSync(manifestPath)) {
|
|
156
|
+
return {
|
|
157
|
+
stale: false,
|
|
158
|
+
lastSynced: null,
|
|
159
|
+
daysSinceSync: 0,
|
|
160
|
+
registryMissing: true
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
165
|
+
} catch {
|
|
166
|
+
return {
|
|
167
|
+
stale: false,
|
|
168
|
+
lastSynced: null,
|
|
169
|
+
daysSinceSync: 0,
|
|
170
|
+
registryMissing: true
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!manifest || !manifest.lastSynced) {
|
|
175
|
+
return {
|
|
176
|
+
stale: false,
|
|
177
|
+
lastSynced: null,
|
|
178
|
+
daysSinceSync: 0,
|
|
179
|
+
registryMissing: true
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const lastSynced = manifest.lastSynced;
|
|
184
|
+
const syncDate = new Date(lastSynced);
|
|
185
|
+
const now = new Date();
|
|
186
|
+
const daysSinceSync = Math.floor((now - syncDate) / (1000 * 60 * 60 * 24));
|
|
187
|
+
const stale = daysSinceSync > 30;
|
|
188
|
+
|
|
189
|
+
if (stale && !this.ignoreStaleness) {
|
|
190
|
+
console.log(`⚠️ Schema registry is ${daysSinceSync} days old. Run \`ml-container-creator bootstrap sync-schemas\` to update.`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { stale, lastSynced, daysSinceSync };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/* eslint-disable eqeqeq */
|
|
2
|
+
/**
|
|
3
|
+
* Parses AWS service-2.json files into a queryable in-memory index.
|
|
4
|
+
* Extracts operations, shapes, enums, constraints, and metadata.
|
|
5
|
+
*
|
|
6
|
+
* Requirements: 13.1, 13.2, 13.3, 13.4, 13.5
|
|
7
|
+
*/
|
|
8
|
+
export default class ServiceModelParser {
|
|
9
|
+
/**
|
|
10
|
+
* Parse a service-2.json file into an indexed representation.
|
|
11
|
+
* @param {Object} rawModel - Parsed JSON content of service-2.json
|
|
12
|
+
* @returns {Object} ServiceModelIndex with metadata, operations (Map), and shapes (Map)
|
|
13
|
+
*/
|
|
14
|
+
parse(rawModel) {
|
|
15
|
+
const metadata = rawModel.metadata || {};
|
|
16
|
+
const operations = new Map();
|
|
17
|
+
const shapes = new Map();
|
|
18
|
+
|
|
19
|
+
if (rawModel.operations) {
|
|
20
|
+
for (const [name, op] of Object.entries(rawModel.operations)) {
|
|
21
|
+
operations.set(name, {
|
|
22
|
+
input: op.input ? op.input.shape : null,
|
|
23
|
+
output: op.output ? op.output.shape : null,
|
|
24
|
+
errors: (op.errors || []).map(e => e.shape)
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (rawModel.shapes) {
|
|
30
|
+
for (const [name, shape] of Object.entries(rawModel.shapes)) {
|
|
31
|
+
shapes.set(name, {
|
|
32
|
+
type: shape.type,
|
|
33
|
+
required: shape.required || [],
|
|
34
|
+
members: shape.members ? new Map(Object.entries(shape.members)) : new Map(),
|
|
35
|
+
enum: shape.enum || null,
|
|
36
|
+
min: shape.min != null ? shape.min : null,
|
|
37
|
+
max: shape.max != null ? shape.max : null,
|
|
38
|
+
pattern: shape.pattern || null,
|
|
39
|
+
member: shape.member || null,
|
|
40
|
+
key: shape.key || null,
|
|
41
|
+
value: shape.value || null
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { metadata, operations, shapes };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get the input shape for an API operation.
|
|
51
|
+
* @param {Object} index - Parsed model index (ServiceModelIndex)
|
|
52
|
+
* @param {string} operationName - e.g., 'CreateEndpointConfig'
|
|
53
|
+
* @returns {Object|null} The resolved input shape definition, or null if not found
|
|
54
|
+
*/
|
|
55
|
+
getOperationInputShape(index, operationName) {
|
|
56
|
+
const op = index.operations.get(operationName);
|
|
57
|
+
if (!op || !op.input) return null;
|
|
58
|
+
return this.resolveShape(index, op.input);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve a shape reference to its full definition.
|
|
63
|
+
* @param {Object} index - Parsed model index
|
|
64
|
+
* @param {string} shapeName - Shape name from the model
|
|
65
|
+
* @returns {Object|null} ShapeDefinition or null if not found
|
|
66
|
+
*/
|
|
67
|
+
resolveShape(index, shapeName) {
|
|
68
|
+
return index.shapes.get(shapeName) || null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Extract all enum values for a given shape.
|
|
73
|
+
* @param {Object} index - Parsed model index
|
|
74
|
+
* @param {string} shapeName - Shape name
|
|
75
|
+
* @returns {Array<string>|null} Enum values or null if not an enum shape
|
|
76
|
+
*/
|
|
77
|
+
getEnumValues(index, shapeName) {
|
|
78
|
+
const shape = index.shapes.get(shapeName);
|
|
79
|
+
if (!shape) return null;
|
|
80
|
+
return shape.enum || null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get statistics about the parsed model.
|
|
85
|
+
* @param {Object} index - Parsed model index
|
|
86
|
+
* @returns {{ shapeCount: number, enumCount: number, operationCount: number }}
|
|
87
|
+
*/
|
|
88
|
+
getStats(index) {
|
|
89
|
+
let enumCount = 0;
|
|
90
|
+
for (const [, shape] of index.shapes) {
|
|
91
|
+
if (shape.enum && shape.enum.length > 0) {
|
|
92
|
+
enumCount++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
shapeCount: index.shapes.size,
|
|
98
|
+
enumCount,
|
|
99
|
+
operationCount: index.operations.size
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validation runner — the Node.js module invoked by do/validate.
|
|
6
|
+
* Loads config, constructs payloads, runs SchemaValidationEngine, and prints a report.
|
|
7
|
+
*
|
|
8
|
+
* - Prints structured report (text format by default)
|
|
9
|
+
* - Exits with code 1 if errors found, 0 if clean
|
|
10
|
+
* - Includes service model version date and fields validated count on success
|
|
11
|
+
* - Supports --smart flag for future MCP validator integration
|
|
12
|
+
*
|
|
13
|
+
* Requirements: 9.2, 9.3, 9.4
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import PayloadBuilder from './payload-builder.js';
|
|
19
|
+
import SchemaValidationEngine from './schema-validation-engine.js';
|
|
20
|
+
import ServiceModelParser from './service-model-parser.js';
|
|
21
|
+
import { getRegistryPath, loadManifest } from './schema-sync.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse a do/config shell file into a key-value object.
|
|
25
|
+
* Extracts lines matching: export KEY="value" or export KEY=value
|
|
26
|
+
*
|
|
27
|
+
* @param {string} configPath - Path to the do/config file
|
|
28
|
+
* @returns {Object} Parsed configuration values
|
|
29
|
+
*/
|
|
30
|
+
export function parseDoConfig(configPath) {
|
|
31
|
+
if (!existsSync(configPath)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const content = readFileSync(configPath, 'utf8');
|
|
36
|
+
const config = {};
|
|
37
|
+
|
|
38
|
+
for (const line of content.split('\n')) {
|
|
39
|
+
const match = line.match(/^export\s+([A-Z_][A-Z0-9_]*)=["']?([^"'\n]*)["']?/);
|
|
40
|
+
if (match) {
|
|
41
|
+
const [, key, value] = match;
|
|
42
|
+
config[key] = value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return config;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Run the full validation pipeline.
|
|
51
|
+
*
|
|
52
|
+
* @param {Object} options
|
|
53
|
+
* @param {string} [options.configDir] - Path to the do/ directory containing config
|
|
54
|
+
* @param {string} [options.format] - Output format: 'text' (default) or 'json'
|
|
55
|
+
* @param {boolean} [options.smart] - Enable smart-mode validators
|
|
56
|
+
* @param {string} [options.registryPath] - Override schema registry path
|
|
57
|
+
* @param {Object} [options.config] - Pre-parsed config (overrides configDir loading)
|
|
58
|
+
* @returns {Promise<number>} Exit code (0 = pass, 1 = fail, 2 = cannot run)
|
|
59
|
+
*/
|
|
60
|
+
export async function run(options = {}) {
|
|
61
|
+
const format = options.format || 'text';
|
|
62
|
+
const smart = options.smart || false;
|
|
63
|
+
const registryPath = options.registryPath || getRegistryPath();
|
|
64
|
+
|
|
65
|
+
// Check schema registry exists
|
|
66
|
+
if (!existsSync(registryPath) || !existsSync(path.join(registryPath, 'manifest.json'))) {
|
|
67
|
+
console.log('⚠️ Schema registry not found.');
|
|
68
|
+
console.log(' Run: ml-container-creator bootstrap sync-schemas');
|
|
69
|
+
process.exit(2);
|
|
70
|
+
return 2;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Load config
|
|
74
|
+
let config = options.config;
|
|
75
|
+
if (!config && options.configDir) {
|
|
76
|
+
const configPath = path.join(options.configDir, 'config');
|
|
77
|
+
config = parseDoConfig(configPath);
|
|
78
|
+
if (!config) {
|
|
79
|
+
console.log('❌ Could not load do/config');
|
|
80
|
+
process.exit(2);
|
|
81
|
+
return 2;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!config) {
|
|
86
|
+
console.log('❌ No configuration provided');
|
|
87
|
+
process.exit(2);
|
|
88
|
+
return 2;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const deploymentTarget = config.DEPLOYMENT_TARGET || 'realtime-inference';
|
|
92
|
+
|
|
93
|
+
// Construct payloads
|
|
94
|
+
const builder = new PayloadBuilder();
|
|
95
|
+
const context = builder.build(config, deploymentTarget);
|
|
96
|
+
|
|
97
|
+
// Load and parse service models from registry
|
|
98
|
+
const parser = new ServiceModelParser();
|
|
99
|
+
const serviceModels = [];
|
|
100
|
+
try {
|
|
101
|
+
const entries = readdirSync(registryPath, { withFileTypes: true });
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (entry.isDirectory()) {
|
|
104
|
+
const modelPath = path.join(registryPath, entry.name, 'service-2.json');
|
|
105
|
+
if (existsSync(modelPath)) {
|
|
106
|
+
const rawModel = JSON.parse(readFileSync(modelPath, 'utf8'));
|
|
107
|
+
serviceModels.push(parser.parse(rawModel));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
console.log('⚠️ Could not load service models from registry');
|
|
113
|
+
process.exit(2);
|
|
114
|
+
return 2;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Run validation engine
|
|
118
|
+
const engine = new SchemaValidationEngine({
|
|
119
|
+
registryPath,
|
|
120
|
+
smartMode: smart,
|
|
121
|
+
serviceModels
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const report = await engine.validate(context);
|
|
125
|
+
const summary = report.getSummary();
|
|
126
|
+
|
|
127
|
+
// Load manifest for version info
|
|
128
|
+
const manifest = loadManifest(registryPath);
|
|
129
|
+
|
|
130
|
+
// Output report
|
|
131
|
+
if (format === 'json') {
|
|
132
|
+
report.metadata.serviceModelVersionDate = manifest?.lastSynced || null;
|
|
133
|
+
const output = report.toJSON();
|
|
134
|
+
console.log(JSON.stringify(output, null, 2));
|
|
135
|
+
} else {
|
|
136
|
+
// Print static results immediately
|
|
137
|
+
const text = report.toText();
|
|
138
|
+
console.log(text);
|
|
139
|
+
|
|
140
|
+
// On success, print version info
|
|
141
|
+
if (summary.errors === 0) {
|
|
142
|
+
const versionDate = manifest?.lastSynced
|
|
143
|
+
? new Date(manifest.lastSynced).toISOString().split('T')[0]
|
|
144
|
+
: 'unknown';
|
|
145
|
+
console.log('');
|
|
146
|
+
console.log('✅ Validation passed');
|
|
147
|
+
console.log(` Service model version: ${versionDate}`);
|
|
148
|
+
console.log(` Fields validated: ${summary.fieldsValidated}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// If smart mode and results are streaming, display them after static
|
|
152
|
+
if (smart && summary.advisory > 0) {
|
|
153
|
+
console.log('');
|
|
154
|
+
console.log('── Smart-mode findings ──');
|
|
155
|
+
for (const finding of report.advisoryFindings) {
|
|
156
|
+
console.log(` ℹ ${finding.fieldPath || finding.operation}: ${finding.remediationHint || ''}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Exit code
|
|
162
|
+
const exitCode = summary.errors > 0 ? 1 : 0;
|
|
163
|
+
process.exit(exitCode);
|
|
164
|
+
return exitCode;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export default { run, parseDoConfig };
|