@seeed-studio/solution-cli 0.1.0 → 0.2.0

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.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Unified Catalog DTO definition and validator conforming to v2 schema.
3
+ */
4
+
5
+ export const SCHEMA_VERSION = '2.0';
6
+ export const SCHEMA_URL = 'https://spec.seeedstudio.com/schemas/v2/catalog-dto.json';
7
+
8
+ /**
9
+ * Creates a standardized Catalog DTO response.
10
+ *
11
+ * @param {Object} options
12
+ * @param {'typesense' | 'bundled_manifest'} options.source
13
+ * @param {'FRESH' | 'STALE' | 'UNKNOWN'} [options.freshness='FRESH']
14
+ * @param {string} [options.datasetVersion='2026-10-13.1']
15
+ * @param {Array} options.records
16
+ * @param {Object} [options.fieldCompleteness]
17
+ * @param {Array<string>} [options.warnings=[]]
18
+ * @returns {Object} Validated Catalog DTO
19
+ */
20
+ export function createCatalogDto({
21
+ source = 'bundled_manifest',
22
+ freshness = 'FRESH',
23
+ datasetVersion = '2026-10-13.1',
24
+ records = [],
25
+ fieldCompleteness = { spec: true, compatibility: true, offer: source === 'typesense' },
26
+ warnings = [],
27
+ }) {
28
+ return {
29
+ $schema: SCHEMA_URL,
30
+ schema_version: SCHEMA_VERSION,
31
+ dataset_version: datasetVersion,
32
+ generated_at: new Date().toISOString(),
33
+ source,
34
+ freshness,
35
+ field_completeness: fieldCompleteness,
36
+ record_count: Array.isArray(records) ? records.length : 0,
37
+ records: Array.isArray(records) ? records : [],
38
+ warnings: Array.isArray(warnings) ? warnings : [],
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Validates whether an object complies with the v2 Catalog DTO structure.
44
+ *
45
+ * @param {any} dto
46
+ * @returns {{ valid: boolean, errors: Array<string> }}
47
+ */
48
+ export function validateCatalogDto(dto) {
49
+ const errors = [];
50
+ if (!dto || typeof dto !== 'object') {
51
+ return { valid: false, errors: ['DTO must be an object'] };
52
+ }
53
+ if (dto.schema_version !== SCHEMA_VERSION) {
54
+ errors.push(`Invalid schema_version: expected ${SCHEMA_VERSION}, got ${dto.schema_version}`);
55
+ }
56
+ if (!['typesense', 'bundled_manifest'].includes(dto.source)) {
57
+ errors.push(`Invalid source: ${dto.source}`);
58
+ }
59
+ if (!['FRESH', 'STALE', 'UNKNOWN'].includes(dto.freshness)) {
60
+ errors.push(`Invalid freshness: ${dto.freshness}`);
61
+ }
62
+ if (!Array.isArray(dto.records)) {
63
+ errors.push('records must be an array');
64
+ }
65
+ return {
66
+ valid: errors.length === 0,
67
+ errors,
68
+ };
69
+ }
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Deterministic Machine Compatibility Evaluator for IoT Hardware and Accessories.
3
+ *
4
+ * Implements machine-evaluable compatibility assertions across 6 physical dimensions:
5
+ * 1. power_supply: Voltage, tolerance, min current/watts, plug dimensions, polarity, USB-C PD profiles
6
+ * 2. rf_antenna: RF connector gender (N-Type, SMA, RP-SMA), impedance, frequency bands
7
+ * 3. m2_storage: Key-M vs Key-E, form factor (2242 vs 2280), PCIe/NVMe vs SATA
8
+ * 4. poe: 802.3af (15.4W) vs 802.3at (30W) vs 802.3bt
9
+ * 5. usb: Port generation, bandwidth, and host power output
10
+ * 6. os_firmware: Kernel / JetPack / firmware version constraints
11
+ */
12
+
13
+ export const COMPATIBILITY_STATUS = {
14
+ VERIFIED: 'VERIFIED',
15
+ REQUIRES_ADAPTER: 'REQUIRES_ADAPTER',
16
+ INCOMPATIBLE: 'INCOMPATIBLE',
17
+ UNKNOWN: 'UNKNOWN',
18
+ CONFLICT: 'CONFLICT',
19
+ };
20
+
21
+ /**
22
+ * Evaluates power supply compatibility between a host device and an adapter/cable.
23
+ *
24
+ * @param {Object} hostSpec - Host physical power requirements
25
+ * @param {Object} accessorySpec - Power accessory specifications
26
+ * @returns {{ status: string, dimension: string, reason: string, details: Object }}
27
+ */
28
+ export function evaluatePowerCompatibility(hostSpec, accessorySpec) {
29
+ if (!hostSpec || !accessorySpec) {
30
+ return {
31
+ status: COMPATIBILITY_STATUS.UNKNOWN,
32
+ dimension: 'power_supply',
33
+ reason: 'Missing power specifications for host or accessory',
34
+ details: {},
35
+ };
36
+ }
37
+
38
+ // 1. USB-C Power Delivery checks
39
+ if (hostSpec.connector === 'usb_c' || accessorySpec.connector === 'usb_c') {
40
+ if (hostSpec.connector !== accessorySpec.connector) {
41
+ return {
42
+ status: COMPATIBILITY_STATUS.INCOMPATIBLE,
43
+ dimension: 'power_supply',
44
+ reason: `Connector mismatch: host requires ${hostSpec.connector}, accessory provides ${accessorySpec.connector}`,
45
+ details: { hostConnector: hostSpec.connector, accessoryConnector: accessorySpec.connector },
46
+ };
47
+ }
48
+ const hostMinWatts = hostSpec.min_watts || ((hostSpec.voltage_v || 5.0) * (hostSpec.min_current_a || 2.0));
49
+ const accWatts = accessorySpec.watts || ((accessorySpec.voltage_v || 5.0) * (accessorySpec.max_current_a || 3.0));
50
+ if (accWatts < hostMinWatts) {
51
+ return {
52
+ status: COMPATIBILITY_STATUS.INCOMPATIBLE,
53
+ dimension: 'power_supply',
54
+ reason: `Insufficient wattage: host requires at least ${hostMinWatts}W, accessory provides ${accWatts}W`,
55
+ details: { requiredWatts: hostMinWatts, providedWatts: accWatts },
56
+ };
57
+ }
58
+ return {
59
+ status: COMPATIBILITY_STATUS.VERIFIED,
60
+ dimension: 'power_supply',
61
+ reason: `Verified USB-C power delivery: ${accWatts}W meets or exceeds required ${hostMinWatts}W`,
62
+ details: { requiredWatts: hostMinWatts, providedWatts: accWatts },
63
+ };
64
+ }
65
+
66
+ // 2. DC Barrel Jack checks (Voltage, Current, Plug Dimensions, Polarity)
67
+ const hostV = hostSpec.voltage_v;
68
+ const accV = accessorySpec.voltage_v;
69
+ if (hostV && accV) {
70
+ const tolerance = hostSpec.tolerance_percent || 5.0;
71
+ const diffPercent = Math.abs(hostV - accV) / hostV * 100;
72
+ if (diffPercent > tolerance) {
73
+ return {
74
+ status: COMPATIBILITY_STATUS.INCOMPATIBLE,
75
+ dimension: 'power_supply',
76
+ reason: `Voltage mismatch: host requires ${hostV}V (±${tolerance}%), accessory supplies ${accV}V`,
77
+ details: { requiredVoltage: hostV, suppliedVoltage: accV },
78
+ };
79
+ }
80
+ }
81
+
82
+ if (hostSpec.min_current_a && accessorySpec.max_current_a) {
83
+ if (accessorySpec.max_current_a < hostSpec.min_current_a) {
84
+ return {
85
+ status: COMPATIBILITY_STATUS.INCOMPATIBLE,
86
+ dimension: 'power_supply',
87
+ reason: `Current capacity too low: host requires at least ${hostSpec.min_current_a}A, adapter rated for ${accessorySpec.max_current_a}A`,
88
+ details: { requiredCurrent: hostSpec.min_current_a, providedCurrent: accessorySpec.max_current_a },
89
+ };
90
+ }
91
+ }
92
+
93
+ // Plug dimension check (e.g. 5.5x2.5mm vs 5.5x2.1mm)
94
+ if (hostSpec.connector && accessorySpec.connector) {
95
+ if (hostSpec.connector !== accessorySpec.connector) {
96
+ return {
97
+ status: COMPATIBILITY_STATUS.REQUIRES_ADAPTER,
98
+ dimension: 'power_supply',
99
+ reason: `DC Barrel plug mismatch: host requires ${hostSpec.connector}, accessory has ${accessorySpec.connector}. Requires a barrel converter adapter.`,
100
+ details: { hostConnector: hostSpec.connector, accessoryConnector: accessorySpec.connector },
101
+ };
102
+ }
103
+ }
104
+
105
+ // Polarity check (center_positive vs center_negative)
106
+ if (hostSpec.polarity && accessorySpec.polarity) {
107
+ if (hostSpec.polarity !== accessorySpec.polarity) {
108
+ return {
109
+ status: COMPATIBILITY_STATUS.INCOMPATIBLE,
110
+ dimension: 'power_supply',
111
+ reason: `Polarity inversion danger: host requires ${hostSpec.polarity}, accessory is ${accessorySpec.polarity}`,
112
+ details: { hostPolarity: hostSpec.polarity, accessoryPolarity: accessorySpec.polarity },
113
+ };
114
+ }
115
+ }
116
+
117
+ return {
118
+ status: COMPATIBILITY_STATUS.VERIFIED,
119
+ dimension: 'power_supply',
120
+ reason: `Power supply verified: ${accV || hostV}V @ ${accessorySpec.max_current_a || hostSpec.min_current_a}A matches requirements`,
121
+ details: { voltage: accV || hostV, current: accessorySpec.max_current_a || hostSpec.min_current_a },
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Evaluates RF Antenna compatibility between a host device and an antenna.
127
+ *
128
+ * @param {Object} hostSpec
129
+ * @param {Object} antennaSpec
130
+ * @returns {{ status: string, dimension: string, reason: string, details: Object }}
131
+ */
132
+ export function evaluateRfCompatibility(hostSpec, antennaSpec) {
133
+ if (!hostSpec?.rf_antenna || !antennaSpec) {
134
+ return {
135
+ status: COMPATIBILITY_STATUS.UNKNOWN,
136
+ dimension: 'rf_antenna',
137
+ reason: 'Missing RF or antenna specifications',
138
+ details: {},
139
+ };
140
+ }
141
+
142
+ const hostRf = hostSpec.rf_antenna;
143
+ const hostConn = (hostRf.connector || '').toLowerCase();
144
+ const antConn = (antennaSpec.connector || '').toLowerCase();
145
+
146
+ // 1. Connector compatibility
147
+ if (hostConn.includes('n-type female')) {
148
+ if (antConn.includes('n-type male') || antConn.includes('n-male')) {
149
+ // Direct mate
150
+ } else if (antConn.includes('sma')) {
151
+ return {
152
+ status: COMPATIBILITY_STATUS.REQUIRES_ADAPTER,
153
+ dimension: 'rf_antenna',
154
+ reason: 'Host has N-Type Female connector, antenna has SMA connector. Requires N-Male to SMA pigtail cable / adapter.',
155
+ details: { hostConnector: hostRf.connector, antennaConnector: antennaSpec.connector },
156
+ };
157
+ } else if (!antConn) {
158
+ return {
159
+ status: COMPATIBILITY_STATUS.UNKNOWN,
160
+ dimension: 'rf_antenna',
161
+ reason: 'Antenna connector type not specified',
162
+ details: { hostConnector: hostRf.connector },
163
+ };
164
+ } else {
165
+ return {
166
+ status: COMPATIBILITY_STATUS.INCOMPATIBLE,
167
+ dimension: 'rf_antenna',
168
+ reason: `Incompatible antenna connector: host has ${hostRf.connector}, antenna has ${antennaSpec.connector}`,
169
+ details: { hostConnector: hostRf.connector, antennaConnector: antennaSpec.connector },
170
+ };
171
+ }
172
+ }
173
+
174
+ // 2. Frequency band check (e.g. EU868, US915, 2.4GHz)
175
+ if (hostRf.frequency_bands && antennaSpec.frequency_band) {
176
+ const antBand = antennaSpec.frequency_band.toUpperCase();
177
+ const match = hostRf.frequency_bands.some((b) => b.toUpperCase().includes(antBand) || antBand.includes(b.toUpperCase()));
178
+ if (!match) {
179
+ return {
180
+ status: COMPATIBILITY_STATUS.INCOMPATIBLE,
181
+ dimension: 'rf_antenna',
182
+ reason: `Frequency band mismatch: host operating bands [${hostRf.frequency_bands.join(', ')}], antenna tuned for ${antennaSpec.frequency_band}`,
183
+ details: { supportedBands: hostRf.frequency_bands, antennaBand: antennaSpec.frequency_band },
184
+ };
185
+ }
186
+ }
187
+
188
+ return {
189
+ status: COMPATIBILITY_STATUS.VERIFIED,
190
+ dimension: 'rf_antenna',
191
+ reason: `Antenna verified: connector ${antennaSpec.connector || hostConn} and frequency band match host`,
192
+ details: { connector: antennaSpec.connector || hostConn },
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Evaluates M.2 expansion slot compatibility (Key-M vs Key-E, form factor 2280 vs 2242).
198
+ *
199
+ * @param {Object} hostSpec
200
+ * @param {Object} moduleSpec
201
+ * @returns {{ status: string, dimension: string, reason: string, details: Object }}
202
+ */
203
+ export function evaluateM2Compatibility(hostSpec, moduleSpec) {
204
+ if (!hostSpec?.m2_slots || !moduleSpec) {
205
+ return {
206
+ status: COMPATIBILITY_STATUS.UNKNOWN,
207
+ dimension: 'm2_storage',
208
+ reason: 'No M.2 slot or module specification available',
209
+ details: {},
210
+ };
211
+ }
212
+
213
+ const modKey = (moduleSpec.key || '').toUpperCase(); // 'M' or 'E'
214
+ const modSize = moduleSpec.size || '2280';
215
+
216
+ const matchingSlot = hostSpec.m2_slots.find((slot) => {
217
+ return slot.key.toUpperCase() === modKey;
218
+ });
219
+
220
+ if (!matchingSlot) {
221
+ return {
222
+ status: COMPATIBILITY_STATUS.INCOMPATIBLE,
223
+ dimension: 'm2_storage',
224
+ reason: `M.2 Key mismatch: module requires Key-${modKey}, but host only provides [${hostSpec.m2_slots.map((s) => 'Key-' + s.key).join(', ')}]`,
225
+ details: { requiredKey: modKey, availableSlots: hostSpec.m2_slots },
226
+ };
227
+ }
228
+
229
+ if (matchingSlot.size && matchingSlot.size !== modSize) {
230
+ return {
231
+ status: COMPATIBILITY_STATUS.REQUIRES_ADAPTER,
232
+ dimension: 'm2_storage',
233
+ reason: `Form factor mismatch: module is ${modSize}, slot designed for ${matchingSlot.size}. May require mounting bracket / standoff.`,
234
+ details: { moduleSize: modSize, slotSize: matchingSlot.size },
235
+ };
236
+ }
237
+
238
+ return {
239
+ status: COMPATIBILITY_STATUS.VERIFIED,
240
+ dimension: 'm2_storage',
241
+ reason: `M.2 slot verified: Key-${modKey} (${modSize}) matches slot ${matchingSlot.key}`,
242
+ details: { key: modKey, size: modSize, interface: matchingSlot.interface },
243
+ };
244
+ }
245
+
246
+ /**
247
+ * Master compatibility assertion runner evaluating all relevant physical dimensions.
248
+ *
249
+ * @param {Object} hostPhysicalSpec - Host physical specifications
250
+ * @param {Object} accessoryItem - Accessory object with specs and kind
251
+ * @returns {{ isCompatible: boolean, status: string, dimensions: Array<Object> }}
252
+ */
253
+ export function assertCompatibility(hostPhysicalSpec, accessoryItem) {
254
+ if (!hostPhysicalSpec || !accessoryItem) {
255
+ return {
256
+ isCompatible: false,
257
+ status: COMPATIBILITY_STATUS.UNKNOWN,
258
+ dimensions: [{ dimension: 'general', status: COMPATIBILITY_STATUS.UNKNOWN, reason: 'Insufficient specification data' }],
259
+ };
260
+ }
261
+
262
+ const evaluations = [];
263
+
264
+ // Check Power
265
+ if (accessoryItem.power_spec || accessoryItem.type?.includes('power') || accessoryItem.id?.includes('power')) {
266
+ evaluations.push(evaluatePowerCompatibility(hostPhysicalSpec.power_input, accessoryItem.power_spec || {
267
+ connector: hostPhysicalSpec.power_input?.connector,
268
+ voltage_v: hostPhysicalSpec.power_input?.voltage_v,
269
+ max_current_a: hostPhysicalSpec.power_input?.min_current_a,
270
+ }));
271
+ }
272
+
273
+ // Check RF / Antenna
274
+ if (accessoryItem.rf_spec || accessoryItem.type?.includes('antenna') || accessoryItem.id?.includes('antenna')) {
275
+ evaluations.push(evaluateRfCompatibility(hostPhysicalSpec, accessoryItem.rf_spec || {
276
+ connector: hostPhysicalSpec.rf_antenna?.required_antenna_connector || 'SMA-J (Male)',
277
+ }));
278
+ }
279
+
280
+ // Check M.2 Storage / Wireless
281
+ if (accessoryItem.m2_spec || accessoryItem.type?.includes('ssd') || accessoryItem.type?.includes('m2')) {
282
+ evaluations.push(evaluateM2Compatibility(hostPhysicalSpec, accessoryItem.m2_spec || {
283
+ key: 'M',
284
+ size: '2280',
285
+ }));
286
+ }
287
+
288
+ if (evaluations.length === 0) {
289
+ return {
290
+ isCompatible: true,
291
+ status: COMPATIBILITY_STATUS.VERIFIED,
292
+ dimensions: [{ dimension: 'generic', status: COMPATIBILITY_STATUS.VERIFIED, reason: 'General peripheral without physical slot constraint' }],
293
+ };
294
+ }
295
+
296
+ const hasIncompatible = evaluations.some((e) => e.status === COMPATIBILITY_STATUS.INCOMPATIBLE);
297
+ const hasConflict = evaluations.some((e) => e.status === COMPATIBILITY_STATUS.CONFLICT);
298
+ const hasAdapter = evaluations.some((e) => e.status === COMPATIBILITY_STATUS.REQUIRES_ADAPTER);
299
+ const hasUnknown = evaluations.some((e) => e.status === COMPATIBILITY_STATUS.UNKNOWN);
300
+
301
+ let overallStatus = COMPATIBILITY_STATUS.VERIFIED;
302
+ if (hasIncompatible) overallStatus = COMPATIBILITY_STATUS.INCOMPATIBLE;
303
+ else if (hasConflict) overallStatus = COMPATIBILITY_STATUS.CONFLICT;
304
+ else if (hasAdapter) overallStatus = COMPATIBILITY_STATUS.REQUIRES_ADAPTER;
305
+ else if (hasUnknown) overallStatus = COMPATIBILITY_STATUS.UNKNOWN;
306
+
307
+ return {
308
+ isCompatible: overallStatus === COMPATIBILITY_STATUS.VERIFIED || overallStatus === COMPATIBILITY_STATUS.REQUIRES_ADAPTER,
309
+ status: overallStatus,
310
+ dimensions: evaluations,
311
+ };
312
+ }