@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.
Files changed (61) hide show
  1. package/README.md +62 -298
  2. package/bin/cli.js +7 -2
  3. package/package.json +7 -8
  4. package/servers/base-image-picker/index.js +3 -3
  5. package/servers/base-image-picker/manifest.json +4 -2
  6. package/servers/instance-sizer/index.js +561 -0
  7. package/servers/instance-sizer/lib/instance-ranker.js +245 -0
  8. package/servers/instance-sizer/lib/model-resolver.js +265 -0
  9. package/servers/instance-sizer/lib/vram-estimator.js +177 -0
  10. package/servers/instance-sizer/manifest.json +17 -0
  11. package/servers/instance-sizer/package.json +15 -0
  12. package/servers/{instance-recommender → lib}/catalogs/instances.json +136 -34
  13. package/servers/{base-image-picker → lib}/catalogs/model-servers.json +19 -249
  14. package/servers/lib/catalogs/model-sizes.json +131 -0
  15. package/servers/lib/catalogs/models.json +602 -0
  16. package/servers/{model-picker → lib}/catalogs/popular-diffusors.json +32 -10
  17. package/servers/{model-picker → lib}/catalogs/popular-transformers.json +59 -26
  18. package/servers/{base-image-picker → lib}/catalogs/python-slim.json +12 -12
  19. package/servers/lib/schemas/image-catalog.schema.json +0 -12
  20. package/servers/lib/schemas/instances.schema.json +29 -0
  21. package/servers/lib/schemas/model-catalog.schema.json +12 -10
  22. package/servers/lib/schemas/unified-model-catalog.schema.json +129 -0
  23. package/servers/model-picker/index.js +2 -3
  24. package/servers/model-picker/manifest.json +2 -3
  25. package/servers/region-picker/index.js +1 -1
  26. package/servers/region-picker/manifest.json +1 -1
  27. package/src/app.js +17 -0
  28. package/src/lib/bootstrap-command-handler.js +38 -0
  29. package/src/lib/cli-handler.js +3 -3
  30. package/src/lib/config-manager.js +4 -1
  31. package/src/lib/configuration-manager.js +2 -2
  32. package/src/lib/cross-cutting-checker.js +341 -0
  33. package/src/lib/dry-run-validator.js +78 -0
  34. package/src/lib/generation-validator.js +102 -0
  35. package/src/lib/mcp-validator-config.js +89 -0
  36. package/src/lib/payload-builder.js +153 -0
  37. package/src/lib/prompt-runner.js +445 -135
  38. package/src/lib/prompts.js +1 -1
  39. package/src/lib/registry-loader.js +5 -5
  40. package/src/lib/schema-sync.js +203 -0
  41. package/src/lib/schema-validation-engine.js +195 -0
  42. package/src/lib/service-model-parser.js +102 -0
  43. package/src/lib/validate-runner.js +167 -0
  44. package/src/lib/validation-report.js +133 -0
  45. package/src/lib/validators/base-validator.js +36 -0
  46. package/src/lib/validators/catalog-validator.js +177 -0
  47. package/src/lib/validators/enum-validator.js +120 -0
  48. package/src/lib/validators/required-field-validator.js +150 -0
  49. package/src/lib/validators/type-validator.js +313 -0
  50. package/templates/Dockerfile +1 -1
  51. package/templates/do/build +15 -5
  52. package/templates/do/run +5 -1
  53. package/templates/do/validate +61 -0
  54. package/servers/instance-recommender/LICENSE +0 -202
  55. package/servers/instance-recommender/index.js +0 -284
  56. package/servers/instance-recommender/manifest.json +0 -16
  57. package/servers/instance-recommender/package.json +0 -15
  58. /package/servers/{model-picker → lib}/catalogs/jumpstart-public.json +0 -0
  59. /package/servers/{region-picker → lib}/catalogs/regions.json +0 -0
  60. /package/servers/{base-image-picker → lib}/catalogs/triton-backends.json +0 -0
  61. /package/servers/{base-image-picker → lib}/catalogs/triton.json +0 -0
@@ -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/instance-recommender/catalogs/instances.json');
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/base-image-picker/catalogs/model-servers.json'),
22
- tritonBackends: resolve(__dirname, '../../servers/base-image-picker/catalogs/triton-backends.json'),
23
- instances: resolve(__dirname, '../../servers/instance-recommender/catalogs/instances.json'),
24
- popularTransformers: resolve(__dirname, '../../servers/model-picker/catalogs/popular-transformers.json'),
25
- popularDiffusors: resolve(__dirname, '../../servers/model-picker/catalogs/popular-diffusors.json')
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 };