@workos/oagen 0.0.1 → 0.1.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.
- package/README.md +1 -3
- package/dist/{chunk-P5XUDUGY.js → chunk-VUSRNIG2.js} +5 -92
- package/dist/cli/index.js +177 -466
- package/dist/compat/index.js +40 -2
- package/dist/index.d.ts +2 -25
- package/dist/index.js +122 -290
- package/dist/scripts/smoke/baseline.js +88 -26
- package/dist/smoke.js +88 -26
- package/dist/verify/index.js +1 -1
- package/docs/agents/architecture.md +1 -1
- package/docs/agents/emitters.md +2 -2
- package/docs/architecture/dependency-layers.md +1 -5
- package/docs/architecture/emitter-contract.md +13 -10
- package/docs/architecture/extractor-contract.md +30 -22
- package/docs/architecture/ir-types.md +2 -1
- package/docs/architecture/pipeline.md +1 -1
- package/docs/architecture/workflows.md +21 -20
- package/docs/cli.md +10 -14
- package/docs/contributor/public-api.md +2 -2
- package/package.json +1 -1
- package/skills/check-emitter-parity/SKILL.md +232 -0
package/dist/cli/index.js
CHANGED
|
@@ -284,17 +284,26 @@ function buildInlineModel(name, schema) {
|
|
|
284
284
|
|
|
285
285
|
// src/parser/schemas.ts
|
|
286
286
|
function extractSchemas(schemas) {
|
|
287
|
-
const models = [];
|
|
288
287
|
const enums = [];
|
|
289
|
-
if (!schemas) return { models, enums };
|
|
288
|
+
if (!schemas) return { models: [], enums };
|
|
289
|
+
const modelsByCleanName = /* @__PURE__ */ new Map();
|
|
290
290
|
for (const [name, schema] of Object.entries(schemas)) {
|
|
291
291
|
const pascalName = cleanSchemaName(toPascalCase(name));
|
|
292
292
|
if (schema.enum) {
|
|
293
293
|
enums.push(extractEnum(pascalName, schema));
|
|
294
294
|
} else {
|
|
295
|
-
|
|
295
|
+
const model = extractModel(pascalName, schema, schemas);
|
|
296
|
+
const existing = modelsByCleanName.get(pascalName);
|
|
297
|
+
if (existing) {
|
|
298
|
+
if (model.fields.length > existing.fields.length) {
|
|
299
|
+
modelsByCleanName.set(pascalName, model);
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
modelsByCleanName.set(pascalName, model);
|
|
303
|
+
}
|
|
296
304
|
}
|
|
297
305
|
}
|
|
306
|
+
const models = [...modelsByCleanName.values()];
|
|
298
307
|
const modelNames = new Set(models.map((m) => m.name));
|
|
299
308
|
const inlineQueue = [...models];
|
|
300
309
|
while (inlineQueue.length > 0) {
|
|
@@ -385,9 +394,9 @@ function extractNestedSchema(name, schema) {
|
|
|
385
394
|
if (variant.properties && (variant.type === "object" || !variant.type)) {
|
|
386
395
|
const requiredSet = new Set(variant.required ?? []);
|
|
387
396
|
const fields = [];
|
|
388
|
-
for (const [fn,
|
|
389
|
-
if (!
|
|
390
|
-
fields.push(buildFieldFromSchema(fn,
|
|
397
|
+
for (const [fn, fs2] of Object.entries(variant.properties)) {
|
|
398
|
+
if (!fs2) continue;
|
|
399
|
+
fields.push(buildFieldFromSchema(fn, fs2, name, requiredSet));
|
|
391
400
|
}
|
|
392
401
|
const variantName = models.length === 0 ? name : `${name}${models.length + 1}`;
|
|
393
402
|
models.push({ name: variantName, description: variant.description, fields });
|
|
@@ -398,18 +407,18 @@ function extractNestedSchema(name, schema) {
|
|
|
398
407
|
if (schema.properties && (schema.type === "object" || !schema.type)) {
|
|
399
408
|
const requiredSet = new Set(schema.required ?? []);
|
|
400
409
|
const fields = [];
|
|
401
|
-
for (const [fn,
|
|
402
|
-
if (!
|
|
403
|
-
fields.push(buildFieldFromSchema(fn,
|
|
410
|
+
for (const [fn, fs2] of Object.entries(schema.properties)) {
|
|
411
|
+
if (!fs2) continue;
|
|
412
|
+
fields.push(buildFieldFromSchema(fn, fs2, name, requiredSet));
|
|
404
413
|
}
|
|
405
414
|
return [{ name, description: schema.description, fields }];
|
|
406
415
|
}
|
|
407
416
|
if (schema.type === "array" && schema.items?.properties) {
|
|
408
417
|
const requiredSet = new Set(schema.items.required ?? []);
|
|
409
418
|
const fields = [];
|
|
410
|
-
for (const [fn,
|
|
411
|
-
if (!
|
|
412
|
-
fields.push(buildFieldFromSchema(fn,
|
|
419
|
+
for (const [fn, fs2] of Object.entries(schema.items.properties)) {
|
|
420
|
+
if (!fs2) continue;
|
|
421
|
+
fields.push(buildFieldFromSchema(fn, fs2, name, requiredSet));
|
|
413
422
|
}
|
|
414
423
|
return [{ name, description: schema.items.description, fields }];
|
|
415
424
|
}
|
|
@@ -724,6 +733,18 @@ function classifyAndExtractResponse(schema, contextName) {
|
|
|
724
733
|
}
|
|
725
734
|
return extractDirectResource(schema, contextName);
|
|
726
735
|
}
|
|
736
|
+
var KNOWN_DATA_PATHS = /* @__PURE__ */ new Set(["data", "items", "results", "records", "entries", "values", "nodes", "edges"]);
|
|
737
|
+
var PAGINATION_METADATA_PATTERNS = [
|
|
738
|
+
"metadata",
|
|
739
|
+
"pagination",
|
|
740
|
+
"cursor",
|
|
741
|
+
"has_more",
|
|
742
|
+
"page_info",
|
|
743
|
+
"total",
|
|
744
|
+
"next_page",
|
|
745
|
+
"previous_page",
|
|
746
|
+
"offset"
|
|
747
|
+
];
|
|
727
748
|
function detectListEnvelope(schema) {
|
|
728
749
|
const propSources = [];
|
|
729
750
|
if (schema.allOf) {
|
|
@@ -740,17 +761,26 @@ function detectListEnvelope(schema) {
|
|
|
740
761
|
Object.assign(mergedProps, source);
|
|
741
762
|
}
|
|
742
763
|
const arrayProps = [];
|
|
743
|
-
|
|
764
|
+
const nonArrayKeys = [];
|
|
744
765
|
for (const [key, propSchema] of Object.entries(mergedProps)) {
|
|
745
766
|
if (!propSchema) continue;
|
|
746
767
|
if (propSchema.type === "array") {
|
|
747
768
|
arrayProps.push(key);
|
|
748
769
|
} else {
|
|
749
|
-
|
|
770
|
+
nonArrayKeys.push(key);
|
|
750
771
|
}
|
|
751
772
|
}
|
|
752
|
-
if (arrayProps.length === 1 &&
|
|
753
|
-
|
|
773
|
+
if (arrayProps.length === 1 && nonArrayKeys.length >= 1) {
|
|
774
|
+
const arrayPropName = arrayProps[0];
|
|
775
|
+
if (KNOWN_DATA_PATHS.has(arrayPropName)) {
|
|
776
|
+
return { isEnvelope: true, dataPath: arrayPropName };
|
|
777
|
+
}
|
|
778
|
+
const hasPaginationCompanion = nonArrayKeys.some(
|
|
779
|
+
(key) => PAGINATION_METADATA_PATTERNS.some((pattern) => key.toLowerCase().includes(pattern))
|
|
780
|
+
);
|
|
781
|
+
if (hasPaginationCompanion) {
|
|
782
|
+
return { isEnvelope: true, dataPath: arrayPropName };
|
|
783
|
+
}
|
|
754
784
|
}
|
|
755
785
|
return { isEnvelope: false, dataPath: null };
|
|
756
786
|
}
|
|
@@ -1015,15 +1045,15 @@ function extractOperations(paths, operationIdTransform) {
|
|
|
1015
1045
|
if (!paths) return { services: [], inlineModels: [] };
|
|
1016
1046
|
const serviceMap = /* @__PURE__ */ new Map();
|
|
1017
1047
|
const inlineModels = [];
|
|
1018
|
-
for (const [
|
|
1048
|
+
for (const [path6, pathItem] of Object.entries(paths)) {
|
|
1019
1049
|
const pathLevelParams = pathItem.parameters ?? [];
|
|
1020
1050
|
for (const method of HTTP_METHODS) {
|
|
1021
1051
|
const op = pathItem[method];
|
|
1022
1052
|
if (!op) continue;
|
|
1023
|
-
const serviceName = inferServiceName(
|
|
1053
|
+
const serviceName = inferServiceName(path6, op.tags?.[0]);
|
|
1024
1054
|
const { operation, inlineModels: opModels } = buildOperation(
|
|
1025
1055
|
method,
|
|
1026
|
-
|
|
1056
|
+
path6,
|
|
1027
1057
|
op,
|
|
1028
1058
|
pathLevelParams,
|
|
1029
1059
|
operationIdTransform
|
|
@@ -1043,28 +1073,28 @@ function extractOperations(paths, operationIdTransform) {
|
|
|
1043
1073
|
}));
|
|
1044
1074
|
return { services, inlineModels };
|
|
1045
1075
|
}
|
|
1046
|
-
function inferServiceName(
|
|
1076
|
+
function inferServiceName(path6, tag) {
|
|
1047
1077
|
if (tag) {
|
|
1048
1078
|
return toPascalCase(tag);
|
|
1049
1079
|
}
|
|
1050
|
-
const segments =
|
|
1080
|
+
const segments = path6.split("/").filter(Boolean);
|
|
1051
1081
|
const first = segments[0] ?? "Default";
|
|
1052
1082
|
if (first.startsWith("{")) return "Default";
|
|
1053
1083
|
return toPascalCase(first);
|
|
1054
1084
|
}
|
|
1055
|
-
function inferOperationName(method,
|
|
1085
|
+
function inferOperationName(method, path6, operationId, operationIdTransform) {
|
|
1056
1086
|
if (operationId) {
|
|
1057
1087
|
if (operationIdTransform) {
|
|
1058
1088
|
return operationIdTransform(operationId);
|
|
1059
1089
|
}
|
|
1060
1090
|
return toCamelCase(operationId);
|
|
1061
1091
|
}
|
|
1062
|
-
const verb = inferVerb(method,
|
|
1063
|
-
const resource = inferResourceFromPath(
|
|
1092
|
+
const verb = inferVerb(method, path6);
|
|
1093
|
+
const resource = inferResourceFromPath(path6);
|
|
1064
1094
|
return resource ? toCamelCase(`${verb}_${resource}`) : verb;
|
|
1065
1095
|
}
|
|
1066
|
-
function inferVerb(method,
|
|
1067
|
-
const segments =
|
|
1096
|
+
function inferVerb(method, path6) {
|
|
1097
|
+
const segments = path6.split("/").filter(Boolean);
|
|
1068
1098
|
const hasTrailingParam = segments.length > 0 && segments[segments.length - 1].startsWith("{");
|
|
1069
1099
|
switch (method) {
|
|
1070
1100
|
case "get":
|
|
@@ -1086,8 +1116,8 @@ function inferVerb(method, path8) {
|
|
|
1086
1116
|
return method;
|
|
1087
1117
|
}
|
|
1088
1118
|
}
|
|
1089
|
-
function inferResourceFromPath(
|
|
1090
|
-
const segments =
|
|
1119
|
+
function inferResourceFromPath(path6) {
|
|
1120
|
+
const segments = path6.split("/").filter(Boolean);
|
|
1091
1121
|
if (segments.length <= 1) return null;
|
|
1092
1122
|
let resourceSegment = null;
|
|
1093
1123
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
@@ -1135,15 +1165,15 @@ function disambiguateOperationNames(operations) {
|
|
|
1135
1165
|
}
|
|
1136
1166
|
}
|
|
1137
1167
|
}
|
|
1138
|
-
function inferDeeperContext(
|
|
1139
|
-
const segments =
|
|
1168
|
+
function inferDeeperContext(path6) {
|
|
1169
|
+
const segments = path6.split("/").filter(Boolean);
|
|
1140
1170
|
const nonParams = segments.filter((s) => !s.startsWith("{"));
|
|
1141
1171
|
if (nonParams.length >= 3) {
|
|
1142
1172
|
return toPascalCase(nonParams.slice(1, -1).join("_"));
|
|
1143
1173
|
}
|
|
1144
1174
|
return null;
|
|
1145
1175
|
}
|
|
1146
|
-
function buildOperation(method,
|
|
1176
|
+
function buildOperation(method, path6, op, pathLevelParams, operationIdTransform) {
|
|
1147
1177
|
const allParams = [...pathLevelParams, ...op.parameters ?? []];
|
|
1148
1178
|
const hasIdempotencyHeader = allParams.some((p) => p.in === "header" && p.name.toLowerCase() === "idempotency-key");
|
|
1149
1179
|
const pathParams = extractParams(allParams, "path");
|
|
@@ -1160,7 +1190,7 @@ function buildOperation(method, path8, op, pathLevelParams, operationIdTransform
|
|
|
1160
1190
|
isPaginated,
|
|
1161
1191
|
dataPath: responseDataPath,
|
|
1162
1192
|
itemType: responseItemType
|
|
1163
|
-
} = extractResponses(op.responses, op,
|
|
1193
|
+
} = extractResponses(op.responses, op, path6, method);
|
|
1164
1194
|
inlineModels.push(...reqBodyModels);
|
|
1165
1195
|
const paginationFromParams = detectPagination(response, queryParams, responseDataPath);
|
|
1166
1196
|
let pagination;
|
|
@@ -1178,10 +1208,10 @@ function buildOperation(method, path8, op, pathLevelParams, operationIdTransform
|
|
|
1178
1208
|
}
|
|
1179
1209
|
return {
|
|
1180
1210
|
operation: {
|
|
1181
|
-
name: inferOperationName(method,
|
|
1211
|
+
name: inferOperationName(method, path6, op.operationId, operationIdTransform),
|
|
1182
1212
|
description: buildDescription(op.summary, op.description),
|
|
1183
1213
|
httpMethod: method,
|
|
1184
|
-
path:
|
|
1214
|
+
path: path6,
|
|
1185
1215
|
pathParams,
|
|
1186
1216
|
queryParams,
|
|
1187
1217
|
headerParams,
|
|
@@ -1312,15 +1342,15 @@ function deriveOneOfVariantName(variant, baseName, existingModels) {
|
|
|
1312
1342
|
}
|
|
1313
1343
|
return name;
|
|
1314
1344
|
}
|
|
1315
|
-
function deriveResponseName(op,
|
|
1345
|
+
function deriveResponseName(op, path6, method) {
|
|
1316
1346
|
if (op?.operationId) {
|
|
1317
1347
|
return cleanSchemaName(toPascalCase(op.operationId) + "Response");
|
|
1318
1348
|
}
|
|
1319
|
-
const segments =
|
|
1349
|
+
const segments = path6.split("/").filter(Boolean);
|
|
1320
1350
|
const resource = segments[0] ?? "Unknown";
|
|
1321
1351
|
return toPascalCase(resource) + toPascalCase(method) + "Response";
|
|
1322
1352
|
}
|
|
1323
|
-
function extractResponses(responses, op,
|
|
1353
|
+
function extractResponses(responses, op, path6, method) {
|
|
1324
1354
|
const errors = [];
|
|
1325
1355
|
let response = { kind: "primitive", type: "string" };
|
|
1326
1356
|
let inlineModels = [];
|
|
@@ -1335,7 +1365,7 @@ function extractResponses(responses, op, path8, method) {
|
|
|
1335
1365
|
let extractedType = { kind: "primitive", type: "unknown" };
|
|
1336
1366
|
const jsonContent = resp.content?.["application/json"];
|
|
1337
1367
|
if (jsonContent?.schema) {
|
|
1338
|
-
const contextName = deriveResponseName(op,
|
|
1368
|
+
const contextName = deriveResponseName(op, path6 ?? "/", method ?? "get");
|
|
1339
1369
|
const result = classifyAndExtractResponse(jsonContent.schema, contextName);
|
|
1340
1370
|
extractedType = result.response;
|
|
1341
1371
|
inlineModels = result.inlineModels;
|
|
@@ -1354,7 +1384,7 @@ function extractResponses(responses, op, path8, method) {
|
|
|
1354
1384
|
if (firstContent?.schema) {
|
|
1355
1385
|
extractedType = schemaToTypeRef(
|
|
1356
1386
|
firstContent.schema,
|
|
1357
|
-
deriveResponseName(op,
|
|
1387
|
+
deriveResponseName(op, path6 ?? "/", method ?? "get")
|
|
1358
1388
|
);
|
|
1359
1389
|
}
|
|
1360
1390
|
}
|
|
@@ -1387,20 +1417,24 @@ function keepLargestByName(models) {
|
|
|
1387
1417
|
}
|
|
1388
1418
|
return [...byName.values()];
|
|
1389
1419
|
}
|
|
1390
|
-
function
|
|
1420
|
+
function rewriteModelRef(ref, oldName, newName) {
|
|
1421
|
+
walkTypeRef(ref, {
|
|
1422
|
+
model: (r) => {
|
|
1423
|
+
if (r.name === oldName) r.name = newName;
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
function rewriteModelRefs(models, services, oldName, newName) {
|
|
1428
|
+
for (const model of models) {
|
|
1429
|
+
for (const field of model.fields) {
|
|
1430
|
+
rewriteModelRef(field.type, oldName, newName);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1391
1433
|
for (const service of services) {
|
|
1392
1434
|
for (const op of service.operations) {
|
|
1393
|
-
|
|
1394
|
-
model: (r) => {
|
|
1395
|
-
if (r.name === oldName) r.name = newName;
|
|
1396
|
-
}
|
|
1397
|
-
});
|
|
1435
|
+
rewriteModelRef(op.response, oldName, newName);
|
|
1398
1436
|
if (op.requestBody) {
|
|
1399
|
-
|
|
1400
|
-
model: (r) => {
|
|
1401
|
-
if (r.name === oldName) r.name = newName;
|
|
1402
|
-
}
|
|
1403
|
-
});
|
|
1437
|
+
rewriteModelRef(op.requestBody, oldName, newName);
|
|
1404
1438
|
}
|
|
1405
1439
|
}
|
|
1406
1440
|
}
|
|
@@ -1449,9 +1483,30 @@ function mergeFieldInlineModels(existingModels, fieldInlineModels) {
|
|
|
1449
1483
|
});
|
|
1450
1484
|
return [...existingModels, ...keepLargestByName(deduplicatedFieldModels)];
|
|
1451
1485
|
}
|
|
1486
|
+
function hasModelConstDiscriminant(model) {
|
|
1487
|
+
return model.fields.some(
|
|
1488
|
+
(f) => (f.name === "object" || f.name === "type") && f.type.kind === "literal" && typeof f.type.value === "string"
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
function isModelReferencedByOthers(name, models, excludeNames) {
|
|
1492
|
+
for (const model of models) {
|
|
1493
|
+
if (excludeNames.has(model.name)) continue;
|
|
1494
|
+
for (const field of model.fields) {
|
|
1495
|
+
let found = false;
|
|
1496
|
+
walkTypeRef(field.type, {
|
|
1497
|
+
model: (r) => {
|
|
1498
|
+
if (r.name === name) found = true;
|
|
1499
|
+
}
|
|
1500
|
+
});
|
|
1501
|
+
if (found) return true;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return false;
|
|
1505
|
+
}
|
|
1452
1506
|
function collapseJsonSuffixModels(models, services) {
|
|
1453
1507
|
const normalizedModels = [...models];
|
|
1454
1508
|
const byName = new Map(normalizedModels.map((m) => [m.name, m]));
|
|
1509
|
+
const mergeCandidates = [];
|
|
1455
1510
|
for (const model of normalizedModels) {
|
|
1456
1511
|
if (!model.name.endsWith("Json")) continue;
|
|
1457
1512
|
const baseName = model.name.slice(0, -4);
|
|
@@ -1459,13 +1514,20 @@ function collapseJsonSuffixModels(models, services) {
|
|
|
1459
1514
|
if (!baseModel || model.fields.length <= baseModel.fields.length) continue;
|
|
1460
1515
|
const isSuperset = baseModel.fields.every((f) => model.fields.some((mf) => mf.name === f.name));
|
|
1461
1516
|
if (!isSuperset) continue;
|
|
1462
|
-
baseModel
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1517
|
+
if (hasModelConstDiscriminant(model) && hasModelConstDiscriminant(baseModel)) continue;
|
|
1518
|
+
if (isModelReferencedByOthers(model.name, normalizedModels, /* @__PURE__ */ new Set([model.name, baseName]))) continue;
|
|
1519
|
+
mergeCandidates.push({ jsonModel: model, baseModel });
|
|
1520
|
+
}
|
|
1521
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
1522
|
+
for (const { jsonModel, baseModel } of mergeCandidates) {
|
|
1523
|
+
baseModel.fields = jsonModel.fields;
|
|
1524
|
+
toRemove.add(jsonModel.name);
|
|
1525
|
+
rewriteModelRefs(normalizedModels, services, jsonModel.name, baseModel.name);
|
|
1526
|
+
console.warn(
|
|
1527
|
+
`[oagen] Merged "${jsonModel.name}" into "${baseModel.name}" (${jsonModel.fields.length} fields, superset)`
|
|
1528
|
+
);
|
|
1467
1529
|
}
|
|
1468
|
-
return normalizedModels;
|
|
1530
|
+
return normalizedModels.filter((m) => !toRemove.has(m.name));
|
|
1469
1531
|
}
|
|
1470
1532
|
|
|
1471
1533
|
// src/parser/collect-inline-enums.ts
|
|
@@ -1527,11 +1589,11 @@ async function parseSpec(specPath, options) {
|
|
|
1527
1589
|
options?.operationIdTransform
|
|
1528
1590
|
);
|
|
1529
1591
|
const responseNormalizedModels = mergeInlineResponseModels(models, inlineModels);
|
|
1530
|
-
const jsonCollapsedModels = collapseJsonSuffixModels(responseNormalizedModels, services);
|
|
1531
1592
|
const fieldInlineModels = extractInlineModelsFromSchemas(
|
|
1532
1593
|
spec.components?.schemas
|
|
1533
1594
|
);
|
|
1534
|
-
const
|
|
1595
|
+
const fieldMergedModels = mergeFieldInlineModels(responseNormalizedModels, fieldInlineModels);
|
|
1596
|
+
const finalModels = collapseJsonSuffixModels(fieldMergedModels, services);
|
|
1535
1597
|
collectInlineEnumsFromModels(finalModels, enums);
|
|
1536
1598
|
const auth = extractAuthSchemes(spec.components?.securitySchemes);
|
|
1537
1599
|
const serverEntries = (spec.servers ?? []).map((s) => ({ url: s.url ?? "", description: s.description })).filter((s) => s.url);
|
|
@@ -1617,11 +1679,11 @@ function declarationKey(node) {
|
|
|
1617
1679
|
function importEntries(node, source) {
|
|
1618
1680
|
const entries = [];
|
|
1619
1681
|
const collect = (spec) => {
|
|
1620
|
-
const
|
|
1682
|
+
const path6 = spec.childForFieldName("path")?.text ?? spec.text;
|
|
1621
1683
|
const name = spec.childForFieldName("name")?.text ?? "";
|
|
1622
1684
|
const text = source.slice(spec.startIndex, spec.endIndex);
|
|
1623
1685
|
entries.push({
|
|
1624
|
-
key: `${name}:${
|
|
1686
|
+
key: `${name}:${path6.replace(/^"|"$/g, "")}`,
|
|
1625
1687
|
text
|
|
1626
1688
|
});
|
|
1627
1689
|
};
|
|
@@ -2427,75 +2489,11 @@ async function extractStatements(source, language) {
|
|
|
2427
2489
|
const tree = safeParse(parser, source);
|
|
2428
2490
|
return adapter.parseStatements(tree, source);
|
|
2429
2491
|
}
|
|
2430
|
-
async function mergeIntoExisting(existingContent, generatedContent, language, header
|
|
2492
|
+
async function mergeIntoExisting(existingContent, generatedContent, language, header) {
|
|
2431
2493
|
const adapter = getMergeAdapter(language);
|
|
2432
2494
|
if (!adapter) {
|
|
2433
2495
|
throw new Error(`No merge adapter configured for language "${language}"`);
|
|
2434
2496
|
}
|
|
2435
|
-
if (options?.docstringOnly) {
|
|
2436
|
-
let result2 = existingContent;
|
|
2437
|
-
let docstringUpdates2 = 0;
|
|
2438
|
-
const parser = await getParser(language);
|
|
2439
|
-
const resultTree = safeParse(parser, result2);
|
|
2440
|
-
const generatedTree = safeParse(parser, generatedContent);
|
|
2441
|
-
const resultDocs = adapter.extractDocstrings(resultTree, result2);
|
|
2442
|
-
const generatedDocs = adapter.extractDocstrings(generatedTree, generatedContent);
|
|
2443
|
-
const ignoredRegions = findIgnoredRegions(result2);
|
|
2444
|
-
const ignoredSymbols = buildIgnoredSymbolNames(resultDocs, ignoredRegions);
|
|
2445
|
-
const headerLine2 = header.trim();
|
|
2446
|
-
const edits = [];
|
|
2447
|
-
for (const [symbolName, genInfo] of generatedDocs) {
|
|
2448
|
-
const existInfo = resultDocs.get(symbolName);
|
|
2449
|
-
if (!existInfo) continue;
|
|
2450
|
-
if (ignoredSymbols.has(symbolName)) continue;
|
|
2451
|
-
const genDoc = genInfo.docstring && genInfo.docstring.text.trim() !== headerLine2 ? genInfo.docstring : null;
|
|
2452
|
-
if (genDoc) {
|
|
2453
|
-
if (existInfo.docstring) {
|
|
2454
|
-
const isPreserved = existInfo.docstring.text.includes("@oagen-ignore");
|
|
2455
|
-
if (!isPreserved && existInfo.docstring.text !== genDoc.text) {
|
|
2456
|
-
edits.push({
|
|
2457
|
-
start: existInfo.docstring.startIndex,
|
|
2458
|
-
end: existInfo.docstring.endIndex,
|
|
2459
|
-
newText: genDoc.text
|
|
2460
|
-
});
|
|
2461
|
-
docstringUpdates2++;
|
|
2462
|
-
}
|
|
2463
|
-
} else {
|
|
2464
|
-
const lineStart = existInfo.declStartIndex - existInfo.declColumn;
|
|
2465
|
-
const indent = " ".repeat(existInfo.declColumn);
|
|
2466
|
-
edits.push({ start: lineStart, end: lineStart, newText: indent + genDoc.text + "\n" });
|
|
2467
|
-
docstringUpdates2++;
|
|
2468
|
-
}
|
|
2469
|
-
}
|
|
2470
|
-
for (const [memberName, genMember] of genInfo.members) {
|
|
2471
|
-
const existMember = existInfo.members.get(memberName);
|
|
2472
|
-
if (!existMember || !genMember.docstring) continue;
|
|
2473
|
-
if (existMember.docstring) {
|
|
2474
|
-
const isPreserved = existMember.docstring.text.includes("@oagen-ignore");
|
|
2475
|
-
if (!isPreserved && existMember.docstring.text !== genMember.docstring.text) {
|
|
2476
|
-
edits.push({
|
|
2477
|
-
start: existMember.docstring.startIndex,
|
|
2478
|
-
end: existMember.docstring.endIndex,
|
|
2479
|
-
newText: genMember.docstring.text
|
|
2480
|
-
});
|
|
2481
|
-
docstringUpdates2++;
|
|
2482
|
-
}
|
|
2483
|
-
} else {
|
|
2484
|
-
const lineStart = existMember.declStartIndex - existMember.declColumn;
|
|
2485
|
-
const indent = " ".repeat(existMember.declColumn);
|
|
2486
|
-
edits.push({ start: lineStart, end: lineStart, newText: indent + genMember.docstring.text + "\n" });
|
|
2487
|
-
docstringUpdates2++;
|
|
2488
|
-
}
|
|
2489
|
-
}
|
|
2490
|
-
}
|
|
2491
|
-
if (edits.length > 0) {
|
|
2492
|
-
edits.sort((a, b) => b.start - a.start);
|
|
2493
|
-
for (const edit of edits) {
|
|
2494
|
-
result2 = result2.slice(0, edit.start) + edit.newText + result2.slice(edit.end);
|
|
2495
|
-
}
|
|
2496
|
-
}
|
|
2497
|
-
return { content: result2, added: 0, preserved: 0, changed: docstringUpdates2 > 0 };
|
|
2498
|
-
}
|
|
2499
2497
|
const existingStatements = await extractStatements(existingContent, language);
|
|
2500
2498
|
const generatedStatements = await extractStatements(generatedContent, language);
|
|
2501
2499
|
const existingKeys = /* @__PURE__ */ new Set();
|
|
@@ -2768,22 +2766,6 @@ async function writeFiles(files, outputDir, options) {
|
|
|
2768
2766
|
}
|
|
2769
2767
|
if (language && hasGrammar(language)) {
|
|
2770
2768
|
try {
|
|
2771
|
-
if (file.mergeMode === "docstring-only") {
|
|
2772
|
-
const mergeResult2 = await mergeIntoExisting(existingContent, file.content, language, header, {
|
|
2773
|
-
docstringOnly: true
|
|
2774
|
-
});
|
|
2775
|
-
let finalContent2 = mergeResult2.content;
|
|
2776
|
-
if (header && !finalContent2.startsWith(header)) {
|
|
2777
|
-
finalContent2 = header + "\n\n" + finalContent2;
|
|
2778
|
-
}
|
|
2779
|
-
if (finalContent2 === existingContent) {
|
|
2780
|
-
result.identical.push(file.path);
|
|
2781
|
-
} else {
|
|
2782
|
-
await fs.writeFile(fullPath, finalContent2, "utf-8");
|
|
2783
|
-
result.merged.push(file.path);
|
|
2784
|
-
}
|
|
2785
|
-
continue;
|
|
2786
|
-
}
|
|
2787
2769
|
const mergeResult = await mergeIntoExisting(existingContent, file.content, language, header);
|
|
2788
2770
|
let finalContent = mergeResult.content;
|
|
2789
2771
|
if (header && !finalContent.startsWith(header)) {
|
|
@@ -2849,24 +2831,20 @@ function generateFiles(spec, emitter, options) {
|
|
|
2849
2831
|
}
|
|
2850
2832
|
|
|
2851
2833
|
// src/engine/integrate.ts
|
|
2852
|
-
|
|
2853
|
-
import * as path2 from "path";
|
|
2854
|
-
function mapFilesForTargetIntegration(files, language, targetDir) {
|
|
2834
|
+
function mapFilesForTargetIntegration(files, language) {
|
|
2855
2835
|
const langPrefix = `${language}/`;
|
|
2856
2836
|
return files.filter((f) => f.integrateTarget !== false).map((f) => {
|
|
2857
2837
|
const stripped = f.path.startsWith(langPrefix) ? f.path.replace(langPrefix, "") : f.path;
|
|
2858
|
-
const existsInTarget = targetDir ? fs2.existsSync(path2.join(targetDir, stripped)) : false;
|
|
2859
2838
|
return {
|
|
2860
2839
|
...f,
|
|
2861
2840
|
skipIfExists: false,
|
|
2862
2841
|
// Always merge in target — never hard-skip
|
|
2863
|
-
mergeMode: existsInTarget ? "docstring-only" : "full",
|
|
2864
2842
|
path: stripped
|
|
2865
2843
|
};
|
|
2866
2844
|
});
|
|
2867
2845
|
}
|
|
2868
2846
|
async function integrateGeneratedFiles(opts) {
|
|
2869
|
-
return writeFiles(mapFilesForTargetIntegration(opts.files, opts.language
|
|
2847
|
+
return writeFiles(mapFilesForTargetIntegration(opts.files, opts.language), opts.targetDir, {
|
|
2870
2848
|
language: opts.language,
|
|
2871
2849
|
header: opts.header
|
|
2872
2850
|
});
|
|
@@ -2933,8 +2911,8 @@ function getEmitter(language) {
|
|
|
2933
2911
|
}
|
|
2934
2912
|
|
|
2935
2913
|
// src/cli/overlay-loader.ts
|
|
2936
|
-
import { readFileSync, existsSync
|
|
2937
|
-
import * as
|
|
2914
|
+
import { readFileSync, existsSync } from "fs";
|
|
2915
|
+
import * as path2 from "path";
|
|
2938
2916
|
|
|
2939
2917
|
// src/compat/language-hints.ts
|
|
2940
2918
|
var UNTYPED_MAP_PATTERNS = /* @__PURE__ */ new Set([
|
|
@@ -3157,8 +3135,8 @@ function buildOverlayLookup(surface, manifest, spec, hints, options) {
|
|
|
3157
3135
|
for (const name of Object.keys(surface.typeAliases)) {
|
|
3158
3136
|
lookup.typeAliasByName.set(name, name);
|
|
3159
3137
|
}
|
|
3160
|
-
for (const [
|
|
3161
|
-
lookup.requiredExports.set(
|
|
3138
|
+
for (const [path6, symbols] of Object.entries(surface.exports)) {
|
|
3139
|
+
lookup.requiredExports.set(path6, new Set(symbols));
|
|
3162
3140
|
}
|
|
3163
3141
|
for (const record of [surface.classes, surface.interfaces, surface.typeAliases, surface.enums]) {
|
|
3164
3142
|
for (const [name, item] of Object.entries(record)) {
|
|
@@ -3325,11 +3303,11 @@ function patchOverlay(overlay, violations, baseline) {
|
|
|
3325
3303
|
if (v.category === "export-structure") {
|
|
3326
3304
|
const match = v.symbolPath.match(/^exports\[(.+?)\]\.(.+)$/);
|
|
3327
3305
|
if (match) {
|
|
3328
|
-
const [,
|
|
3329
|
-
if (!patched.requiredExports.has(
|
|
3330
|
-
patched.requiredExports.set(
|
|
3306
|
+
const [, path6, symbol] = match;
|
|
3307
|
+
if (!patched.requiredExports.has(path6)) {
|
|
3308
|
+
patched.requiredExports.set(path6, /* @__PURE__ */ new Set());
|
|
3331
3309
|
}
|
|
3332
|
-
patched.requiredExports.get(
|
|
3310
|
+
patched.requiredExports.get(path6).add(symbol);
|
|
3333
3311
|
}
|
|
3334
3312
|
}
|
|
3335
3313
|
}
|
|
@@ -3374,8 +3352,8 @@ function loadOverlayContext(opts) {
|
|
|
3374
3352
|
);
|
|
3375
3353
|
}
|
|
3376
3354
|
let manifest;
|
|
3377
|
-
const resolvedManifestPath = opts.manifestPath ??
|
|
3378
|
-
if (opts.manifestPath ||
|
|
3355
|
+
const resolvedManifestPath = opts.manifestPath ?? path2.join(opts.outputDir, "smoke-manifest.json");
|
|
3356
|
+
if (opts.manifestPath || existsSync(resolvedManifestPath)) {
|
|
3379
3357
|
try {
|
|
3380
3358
|
const parsed = JSON.parse(readFileSync(resolvedManifestPath, "utf-8"));
|
|
3381
3359
|
if (Array.isArray(parsed)) {
|
|
@@ -3862,288 +3840,21 @@ function summarize(changes) {
|
|
|
3862
3840
|
return { added, removed, modified, breaking, additive };
|
|
3863
3841
|
}
|
|
3864
3842
|
|
|
3865
|
-
// src/differ/file-map.ts
|
|
3866
|
-
function mapChangesToFiles(changes, emitter, ctx) {
|
|
3867
|
-
const regenerate = /* @__PURE__ */ new Set();
|
|
3868
|
-
const toDelete = /* @__PURE__ */ new Set();
|
|
3869
|
-
const modelFiles = /* @__PURE__ */ new Map();
|
|
3870
|
-
const enumFiles = /* @__PURE__ */ new Map();
|
|
3871
|
-
const serviceFiles = /* @__PURE__ */ new Map();
|
|
3872
|
-
for (const model of ctx.spec.models) {
|
|
3873
|
-
const files = emitter.generateModels([model], ctx);
|
|
3874
|
-
const sigs = emitter.generateTypeSignatures?.({ ...ctx.spec, models: [model], enums: [], services: [] }, ctx) ?? [];
|
|
3875
|
-
modelFiles.set(
|
|
3876
|
-
model.name,
|
|
3877
|
-
[...files, ...sigs].map((f) => f.path)
|
|
3878
|
-
);
|
|
3879
|
-
}
|
|
3880
|
-
for (const enumDef of ctx.spec.enums) {
|
|
3881
|
-
const files = emitter.generateEnums([enumDef], ctx);
|
|
3882
|
-
enumFiles.set(
|
|
3883
|
-
enumDef.name,
|
|
3884
|
-
files.map((f) => f.path)
|
|
3885
|
-
);
|
|
3886
|
-
}
|
|
3887
|
-
for (const service of ctx.spec.services) {
|
|
3888
|
-
const files = [
|
|
3889
|
-
...emitter.generateResources([service], ctx),
|
|
3890
|
-
...emitter.generateTests({ ...ctx.spec, services: [service], models: [], enums: [] }, ctx)
|
|
3891
|
-
];
|
|
3892
|
-
const sigs = emitter.generateTypeSignatures?.({ ...ctx.spec, services: [service], models: [], enums: [] }, ctx) ?? [];
|
|
3893
|
-
serviceFiles.set(
|
|
3894
|
-
service.name,
|
|
3895
|
-
[...files, ...sigs].map((f) => f.path)
|
|
3896
|
-
);
|
|
3897
|
-
}
|
|
3898
|
-
const modelToServices = buildReferenceGraph(ctx.spec);
|
|
3899
|
-
for (const change of changes) {
|
|
3900
|
-
switch (change.kind) {
|
|
3901
|
-
case "model-added":
|
|
3902
|
-
case "model-modified": {
|
|
3903
|
-
const files = modelFiles.get(change.name) ?? [];
|
|
3904
|
-
files.forEach((f) => regenerate.add(f));
|
|
3905
|
-
const refs = modelToServices.get(change.name) ?? [];
|
|
3906
|
-
for (const svc of refs) {
|
|
3907
|
-
const svcFiles = serviceFiles.get(svc) ?? [];
|
|
3908
|
-
svcFiles.forEach((f) => regenerate.add(f));
|
|
3909
|
-
}
|
|
3910
|
-
break;
|
|
3911
|
-
}
|
|
3912
|
-
case "model-removed": {
|
|
3913
|
-
const files = modelFiles.get(change.name) ?? [];
|
|
3914
|
-
files.forEach((f) => toDelete.add(f));
|
|
3915
|
-
const refs = modelToServices.get(change.name) ?? [];
|
|
3916
|
-
for (const svc of refs) {
|
|
3917
|
-
const svcFiles = serviceFiles.get(svc) ?? [];
|
|
3918
|
-
svcFiles.forEach((f) => regenerate.add(f));
|
|
3919
|
-
}
|
|
3920
|
-
break;
|
|
3921
|
-
}
|
|
3922
|
-
case "enum-added":
|
|
3923
|
-
case "enum-modified": {
|
|
3924
|
-
const files = enumFiles.get(change.name) ?? [];
|
|
3925
|
-
files.forEach((f) => regenerate.add(f));
|
|
3926
|
-
const refs = modelToServices.get(change.name) ?? [];
|
|
3927
|
-
for (const svc of refs) {
|
|
3928
|
-
const svcFiles = serviceFiles.get(svc) ?? [];
|
|
3929
|
-
svcFiles.forEach((f) => regenerate.add(f));
|
|
3930
|
-
}
|
|
3931
|
-
break;
|
|
3932
|
-
}
|
|
3933
|
-
case "enum-removed": {
|
|
3934
|
-
const files = enumFiles.get(change.name) ?? [];
|
|
3935
|
-
files.forEach((f) => toDelete.add(f));
|
|
3936
|
-
const refs = modelToServices.get(change.name) ?? [];
|
|
3937
|
-
for (const svc of refs) {
|
|
3938
|
-
const svcFiles = serviceFiles.get(svc) ?? [];
|
|
3939
|
-
svcFiles.forEach((f) => regenerate.add(f));
|
|
3940
|
-
}
|
|
3941
|
-
break;
|
|
3942
|
-
}
|
|
3943
|
-
case "service-added": {
|
|
3944
|
-
const files = serviceFiles.get(change.name) ?? [];
|
|
3945
|
-
files.forEach((f) => regenerate.add(f));
|
|
3946
|
-
break;
|
|
3947
|
-
}
|
|
3948
|
-
case "service-removed": {
|
|
3949
|
-
const files = serviceFiles.get(change.name) ?? [];
|
|
3950
|
-
files.forEach((f) => toDelete.add(f));
|
|
3951
|
-
break;
|
|
3952
|
-
}
|
|
3953
|
-
case "operation-added":
|
|
3954
|
-
case "operation-removed":
|
|
3955
|
-
case "operation-modified": {
|
|
3956
|
-
const files = serviceFiles.get(change.serviceName) ?? [];
|
|
3957
|
-
files.forEach((f) => regenerate.add(f));
|
|
3958
|
-
break;
|
|
3959
|
-
}
|
|
3960
|
-
}
|
|
3961
|
-
}
|
|
3962
|
-
for (const f of toDelete) {
|
|
3963
|
-
regenerate.delete(f);
|
|
3964
|
-
}
|
|
3965
|
-
return {
|
|
3966
|
-
regenerate: [...regenerate].sort(),
|
|
3967
|
-
delete: [...toDelete].sort()
|
|
3968
|
-
};
|
|
3969
|
-
}
|
|
3970
|
-
function buildReferenceGraph(spec) {
|
|
3971
|
-
const refs = /* @__PURE__ */ new Map();
|
|
3972
|
-
function trackRef(typeRef, serviceName) {
|
|
3973
|
-
const addName = (name) => {
|
|
3974
|
-
if (!refs.has(name)) refs.set(name, /* @__PURE__ */ new Set());
|
|
3975
|
-
refs.get(name).add(serviceName);
|
|
3976
|
-
};
|
|
3977
|
-
walkTypeRef(typeRef, {
|
|
3978
|
-
model: (r) => addName(r.name),
|
|
3979
|
-
enum: (r) => addName(r.name)
|
|
3980
|
-
});
|
|
3981
|
-
}
|
|
3982
|
-
for (const service of spec.services) {
|
|
3983
|
-
for (const op of service.operations) {
|
|
3984
|
-
for (const p of [...op.pathParams, ...op.queryParams, ...op.headerParams]) {
|
|
3985
|
-
trackRef(p.type, service.name);
|
|
3986
|
-
}
|
|
3987
|
-
if (op.requestBody) trackRef(op.requestBody, service.name);
|
|
3988
|
-
trackRef(op.response, service.name);
|
|
3989
|
-
}
|
|
3990
|
-
}
|
|
3991
|
-
const modelDeps = /* @__PURE__ */ new Map();
|
|
3992
|
-
function collectDeps(typeRef, deps) {
|
|
3993
|
-
walkTypeRef(typeRef, {
|
|
3994
|
-
model: (r) => deps.add(r.name),
|
|
3995
|
-
enum: (r) => deps.add(r.name)
|
|
3996
|
-
});
|
|
3997
|
-
}
|
|
3998
|
-
for (const model of spec.models) {
|
|
3999
|
-
const deps = /* @__PURE__ */ new Set();
|
|
4000
|
-
for (const field of model.fields) {
|
|
4001
|
-
collectDeps(field.type, deps);
|
|
4002
|
-
}
|
|
4003
|
-
deps.delete(model.name);
|
|
4004
|
-
if (deps.size > 0) {
|
|
4005
|
-
modelDeps.set(model.name, deps);
|
|
4006
|
-
}
|
|
4007
|
-
}
|
|
4008
|
-
let changed = true;
|
|
4009
|
-
while (changed) {
|
|
4010
|
-
changed = false;
|
|
4011
|
-
for (const [modelName, services] of refs) {
|
|
4012
|
-
const deps = modelDeps.get(modelName);
|
|
4013
|
-
if (!deps) continue;
|
|
4014
|
-
for (const dep of deps) {
|
|
4015
|
-
if (!refs.has(dep)) refs.set(dep, /* @__PURE__ */ new Set());
|
|
4016
|
-
const depServices = refs.get(dep);
|
|
4017
|
-
for (const svc of services) {
|
|
4018
|
-
if (!depServices.has(svc)) {
|
|
4019
|
-
depServices.add(svc);
|
|
4020
|
-
changed = true;
|
|
4021
|
-
}
|
|
4022
|
-
}
|
|
4023
|
-
}
|
|
4024
|
-
}
|
|
4025
|
-
}
|
|
4026
|
-
const result = /* @__PURE__ */ new Map();
|
|
4027
|
-
for (const [name, svcSet] of refs) {
|
|
4028
|
-
result.set(name, [...svcSet]);
|
|
4029
|
-
}
|
|
4030
|
-
return result;
|
|
4031
|
-
}
|
|
4032
|
-
|
|
4033
|
-
// src/engine/incremental.ts
|
|
4034
|
-
import * as fs3 from "fs/promises";
|
|
4035
|
-
import * as path4 from "path";
|
|
4036
|
-
async function generateIncremental(oldSpec, newSpec, emitter, options) {
|
|
4037
|
-
const diff = diffSpecs(oldSpec, newSpec);
|
|
4038
|
-
if (diff.changes.length === 0) {
|
|
4039
|
-
return { generated: [], deleted: [], diff };
|
|
4040
|
-
}
|
|
4041
|
-
const ctx = buildEmitterContext(newSpec, options);
|
|
4042
|
-
const affected = mapChangesToFiles(diff.changes, emitter, ctx);
|
|
4043
|
-
const allFiles = generateAllFiles(newSpec, emitter, ctx);
|
|
4044
|
-
const header = emitter.fileHeader();
|
|
4045
|
-
const affectedSet = new Set(affected.regenerate);
|
|
4046
|
-
const generated = applyFileHeaders(
|
|
4047
|
-
allFiles.filter((f) => affectedSet.has(f.path)),
|
|
4048
|
-
header
|
|
4049
|
-
);
|
|
4050
|
-
if (!options.dryRun) {
|
|
4051
|
-
const writeResult = await writeFiles(generated, options.outputDir, {
|
|
4052
|
-
language: emitter.language,
|
|
4053
|
-
header
|
|
4054
|
-
});
|
|
4055
|
-
if (writeResult.ignored.length > 0) {
|
|
4056
|
-
console.log(`Ignored ${writeResult.ignored.length} files (@oagen-ignore-file)`);
|
|
4057
|
-
}
|
|
4058
|
-
if (options.target) {
|
|
4059
|
-
const targetResult = await integrateGeneratedFiles({
|
|
4060
|
-
files: generated,
|
|
4061
|
-
language: emitter.language,
|
|
4062
|
-
targetDir: options.target,
|
|
4063
|
-
header
|
|
4064
|
-
});
|
|
4065
|
-
if (targetResult.written.length > 0) {
|
|
4066
|
-
console.log(`Target: created ${targetResult.written.length} new files`);
|
|
4067
|
-
}
|
|
4068
|
-
if (targetResult.merged.length > 0) {
|
|
4069
|
-
console.log(`Target: merged into ${targetResult.merged.length} existing files (additive only)`);
|
|
4070
|
-
}
|
|
4071
|
-
if (targetResult.skipped.length > 0) {
|
|
4072
|
-
console.log(`Target: skipped ${targetResult.skipped.length} files (excluded or no grammar)`);
|
|
4073
|
-
}
|
|
4074
|
-
if (targetResult.ignored.length > 0) {
|
|
4075
|
-
console.log(`Target: ignored ${targetResult.ignored.length} files (@oagen-ignore-file)`);
|
|
4076
|
-
}
|
|
4077
|
-
}
|
|
4078
|
-
if (options.force) {
|
|
4079
|
-
for (const filePath of affected.delete) {
|
|
4080
|
-
const fullPath = path4.join(options.outputDir, filePath);
|
|
4081
|
-
await fs3.rm(fullPath, { force: true });
|
|
4082
|
-
}
|
|
4083
|
-
}
|
|
4084
|
-
}
|
|
4085
|
-
return {
|
|
4086
|
-
generated,
|
|
4087
|
-
deleted: options.force ? affected.delete : [],
|
|
4088
|
-
diff
|
|
4089
|
-
};
|
|
4090
|
-
}
|
|
4091
|
-
|
|
4092
3843
|
// src/cli/diff.ts
|
|
4093
3844
|
async function diffCommand(opts) {
|
|
4094
3845
|
const parseOptions = { operationIdTransform: opts.operationIdTransform };
|
|
4095
|
-
let [oldSpec, newSpec] = await Promise.all([
|
|
4096
|
-
parseSpec(opts.old, parseOptions),
|
|
4097
|
-
parseSpec(opts.new, parseOptions)
|
|
4098
|
-
]);
|
|
3846
|
+
let [oldSpec, newSpec] = await Promise.all([parseSpec(opts.old, parseOptions), parseSpec(opts.new, parseOptions)]);
|
|
4099
3847
|
if (opts.docUrl) {
|
|
4100
3848
|
oldSpec = expandDocUrls(oldSpec, opts.docUrl);
|
|
4101
3849
|
newSpec = expandDocUrls(newSpec, opts.docUrl);
|
|
4102
3850
|
}
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
);
|
|
4111
|
-
}
|
|
4112
|
-
if (!opts.lang || !opts.output) {
|
|
4113
|
-
throw new CommandError("--lang and --output are required for incremental generation", "", 1);
|
|
4114
|
-
}
|
|
4115
|
-
let apiSurface;
|
|
4116
|
-
let overlayLookup;
|
|
4117
|
-
if (opts.apiSurface) {
|
|
4118
|
-
const ctx = loadOverlayContext({
|
|
4119
|
-
apiSurfacePath: opts.apiSurface,
|
|
4120
|
-
manifestPath: opts.manifest,
|
|
4121
|
-
outputDir: opts.output,
|
|
4122
|
-
lang: opts.lang
|
|
4123
|
-
});
|
|
4124
|
-
apiSurface = ctx.apiSurface;
|
|
4125
|
-
overlayLookup = ctx.overlayLookup;
|
|
4126
|
-
}
|
|
4127
|
-
const emitter = getEmitter(opts.lang);
|
|
4128
|
-
const result = await generateIncremental(oldSpec, newSpec, emitter, {
|
|
4129
|
-
namespace: newSpec.name,
|
|
4130
|
-
outputDir: opts.output,
|
|
4131
|
-
force: opts.force,
|
|
4132
|
-
target: opts.target,
|
|
4133
|
-
apiSurface,
|
|
4134
|
-
overlayLookup
|
|
4135
|
-
});
|
|
4136
|
-
if (result.diff.changes.length === 0) {
|
|
4137
|
-
console.log("No changes detected");
|
|
4138
|
-
} else {
|
|
4139
|
-
console.log(`Regenerated ${result.generated.length} files`);
|
|
4140
|
-
if (result.deleted.length > 0) {
|
|
4141
|
-
console.log(`Deleted ${result.deleted.length} files`);
|
|
4142
|
-
}
|
|
4143
|
-
if (!opts.force && result.diff.changes.some((c) => c.kind.endsWith("-removed"))) {
|
|
4144
|
-
console.log("Use --force to delete files for removed schemas");
|
|
4145
|
-
}
|
|
4146
|
-
}
|
|
3851
|
+
const diff = diffSpecs(oldSpec, newSpec);
|
|
3852
|
+
console.log(JSON.stringify(diff, null, 2));
|
|
3853
|
+
throw new CommandError(
|
|
3854
|
+
"",
|
|
3855
|
+
"",
|
|
3856
|
+
diff.summary.breaking > 0 ? 2 : diff.summary.modified > 0 || diff.summary.removed > 0 ? 1 : diff.summary.added > 0 ? 1 : 0
|
|
3857
|
+
);
|
|
4147
3858
|
}
|
|
4148
3859
|
|
|
4149
3860
|
// src/cli/extract.ts
|
|
@@ -4158,7 +3869,7 @@ async function extractCommand(opts) {
|
|
|
4158
3869
|
}
|
|
4159
3870
|
|
|
4160
3871
|
// src/cli/verify.ts
|
|
4161
|
-
import { existsSync as
|
|
3872
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
4162
3873
|
|
|
4163
3874
|
// src/compat/spec-filter.ts
|
|
4164
3875
|
function specDerivedNames(spec, hints) {
|
|
@@ -4257,9 +3968,9 @@ function filterSurface(surface, allowedNames, opts) {
|
|
|
4257
3968
|
let filteredClasses = filterRecord(surface.classes, allowedNames);
|
|
4258
3969
|
if (methodPaths) {
|
|
4259
3970
|
const serviceClassNames = /* @__PURE__ */ new Set();
|
|
4260
|
-
for (const
|
|
4261
|
-
const dot =
|
|
4262
|
-
if (dot !== -1) serviceClassNames.add(
|
|
3971
|
+
for (const path6 of methodPaths) {
|
|
3972
|
+
const dot = path6.indexOf(".");
|
|
3973
|
+
if (dot !== -1) serviceClassNames.add(path6.slice(0, dot));
|
|
4263
3974
|
}
|
|
4264
3975
|
const newClasses = {};
|
|
4265
3976
|
for (const [name, cls] of Object.entries(filteredClasses)) {
|
|
@@ -4686,17 +4397,17 @@ function diffSurfaces(baseline, candidate, hints) {
|
|
|
4686
4397
|
additions.push({ symbolPath: name, symbolType: "enum" });
|
|
4687
4398
|
}
|
|
4688
4399
|
}
|
|
4689
|
-
for (const [
|
|
4690
|
-
const candExports = candidate.exports[
|
|
4400
|
+
for (const [path6, baseExports] of Object.entries(baseline.exports)) {
|
|
4401
|
+
const candExports = candidate.exports[path6];
|
|
4691
4402
|
if (!candExports) {
|
|
4692
4403
|
for (const exp of baseExports) {
|
|
4693
4404
|
violations.push({
|
|
4694
4405
|
category: "export-structure",
|
|
4695
4406
|
severity: "warning",
|
|
4696
|
-
symbolPath: `exports[${
|
|
4407
|
+
symbolPath: `exports[${path6}].${exp}`,
|
|
4697
4408
|
baseline: exp,
|
|
4698
4409
|
candidate: "(missing)",
|
|
4699
|
-
message: `Export "${exp}" from "${
|
|
4410
|
+
message: `Export "${exp}" from "${path6}" not found in generated output`
|
|
4700
4411
|
});
|
|
4701
4412
|
}
|
|
4702
4413
|
continue;
|
|
@@ -4707,10 +4418,10 @@ function diffSurfaces(baseline, candidate, hints) {
|
|
|
4707
4418
|
violations.push({
|
|
4708
4419
|
category: "export-structure",
|
|
4709
4420
|
severity: "warning",
|
|
4710
|
-
symbolPath: `exports[${
|
|
4421
|
+
symbolPath: `exports[${path6}].${exp}`,
|
|
4711
4422
|
baseline: exp,
|
|
4712
4423
|
candidate: "(missing)",
|
|
4713
|
-
message: `Export "${exp}" from "${
|
|
4424
|
+
message: `Export "${exp}" from "${path6}" not found in generated output`
|
|
4714
4425
|
});
|
|
4715
4426
|
}
|
|
4716
4427
|
}
|
|
@@ -4764,8 +4475,8 @@ async function runCompatCheck(baseline, outputDir, lang, spec) {
|
|
|
4764
4475
|
}
|
|
4765
4476
|
|
|
4766
4477
|
// src/verify/run-overlay-retry-loop.ts
|
|
4767
|
-
import { existsSync as
|
|
4768
|
-
import * as
|
|
4478
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, unlinkSync, statSync, openSync, readSync, closeSync } from "fs";
|
|
4479
|
+
import * as path3 from "path";
|
|
4769
4480
|
var PATCHABLE_CATEGORIES = /* @__PURE__ */ new Set(["public-api", "export-structure"]);
|
|
4770
4481
|
var GENERATED_MARKERS = ["Code generated by oagen", "auto-generated by oagen", "This file is auto-generated"];
|
|
4771
4482
|
function cleanGeneratedFiles(dir) {
|
|
@@ -4776,7 +4487,7 @@ function cleanGeneratedFiles(dir) {
|
|
|
4776
4487
|
return;
|
|
4777
4488
|
}
|
|
4778
4489
|
for (const entry of entries) {
|
|
4779
|
-
const full =
|
|
4490
|
+
const full = path3.join(dir, entry);
|
|
4780
4491
|
let stat;
|
|
4781
4492
|
try {
|
|
4782
4493
|
stat = statSync(full);
|
|
@@ -4805,8 +4516,8 @@ function cleanGeneratedFiles(dir) {
|
|
|
4805
4516
|
}
|
|
4806
4517
|
}
|
|
4807
4518
|
function loadManifestFromDir(outputDir) {
|
|
4808
|
-
const manifestPath =
|
|
4809
|
-
if (!
|
|
4519
|
+
const manifestPath = path3.join(outputDir, "smoke-manifest.json");
|
|
4520
|
+
if (!existsSync2(manifestPath)) return void 0;
|
|
4810
4521
|
try {
|
|
4811
4522
|
const parsed = JSON.parse(readFileSync2(manifestPath, "utf-8"));
|
|
4812
4523
|
if (Array.isArray(parsed)) return parsed;
|
|
@@ -4994,17 +4705,17 @@ function runStalenessCheck(baseline, oldSpec, newSpec, lang) {
|
|
|
4994
4705
|
|
|
4995
4706
|
// src/verify/run-smoke-check.ts
|
|
4996
4707
|
import { execFileSync } from "child_process";
|
|
4997
|
-
import { existsSync as
|
|
4708
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
4998
4709
|
import { fileURLToPath } from "url";
|
|
4999
|
-
import * as
|
|
4710
|
+
import * as path4 from "path";
|
|
5000
4711
|
var __filename = fileURLToPath(import.meta.url);
|
|
5001
|
-
var __dirname =
|
|
4712
|
+
var __dirname = path4.dirname(__filename);
|
|
5002
4713
|
function resolveScript(scriptRelPath) {
|
|
5003
|
-
const distScript =
|
|
5004
|
-
if (
|
|
4714
|
+
const distScript = path4.resolve(__dirname, "..", "..", "dist", "scripts", scriptRelPath.replace(/\.ts$/, ".js"));
|
|
4715
|
+
if (existsSync3(distScript)) {
|
|
5005
4716
|
return { bin: "node", script: distScript };
|
|
5006
4717
|
}
|
|
5007
|
-
const srcScript =
|
|
4718
|
+
const srcScript = path4.resolve(__dirname, "..", "..", "scripts", scriptRelPath);
|
|
5008
4719
|
return { bin: "npx", script: srcScript };
|
|
5009
4720
|
}
|
|
5010
4721
|
function runPackagedScript(scriptRelPath, args) {
|
|
@@ -5016,7 +4727,7 @@ function runSmokeCheck(opts) {
|
|
|
5016
4727
|
const { spec, lang, output, rawResults, smokeConfig, smokeRunner } = opts;
|
|
5017
4728
|
let baselinePath = rawResults ?? "smoke-results-raw.json";
|
|
5018
4729
|
let generatedBaseline = false;
|
|
5019
|
-
if (!rawResults && !
|
|
4730
|
+
if (!rawResults && !existsSync3("smoke-results-raw.json")) {
|
|
5020
4731
|
if (!spec) {
|
|
5021
4732
|
throw new CommandError(
|
|
5022
4733
|
"error: --spec <path> or OPENAPI_SPEC_PATH env var is required when no raw baseline exists",
|
|
@@ -5045,10 +4756,10 @@ function runSmokeCheck(opts) {
|
|
|
5045
4756
|
}
|
|
5046
4757
|
return { passed: true, baselinePath, generatedBaseline };
|
|
5047
4758
|
} catch {
|
|
5048
|
-
if (
|
|
4759
|
+
if (existsSync3("smoke-compile-errors.json")) {
|
|
5049
4760
|
return { passed: false, compileErrors: true, baselinePath, generatedBaseline };
|
|
5050
4761
|
}
|
|
5051
|
-
const findingsCount =
|
|
4762
|
+
const findingsCount = existsSync3("smoke-diff-findings.json") ? JSON.parse(readFileSync3("smoke-diff-findings.json", "utf-8")).length : void 0;
|
|
5052
4763
|
return { passed: false, findingsCount, baselinePath, generatedBaseline };
|
|
5053
4764
|
}
|
|
5054
4765
|
}
|
|
@@ -5206,7 +4917,7 @@ ${separator}`);
|
|
|
5206
4917
|
}
|
|
5207
4918
|
stepNum++;
|
|
5208
4919
|
}
|
|
5209
|
-
const needsBaselineStep = !rawResults && !
|
|
4920
|
+
const needsBaselineStep = !rawResults && !existsSync4("smoke-results-raw.json");
|
|
5210
4921
|
if (needsBaselineStep) {
|
|
5211
4922
|
console.log(`
|
|
5212
4923
|
${separator}`);
|
|
@@ -5259,7 +4970,7 @@ Remediation guide (by finding type):
|
|
|
5259
4970
|
|
|
5260
4971
|
// src/cli/init.ts
|
|
5261
4972
|
import { resolve as resolve3, relative } from "path";
|
|
5262
|
-
import { existsSync as
|
|
4973
|
+
import { existsSync as existsSync5, mkdirSync, writeFileSync as writeFileSync3, readFileSync as readFileSync5 } from "fs";
|
|
5263
4974
|
|
|
5264
4975
|
// src/cli/templates/init.ts
|
|
5265
4976
|
function oagenScripts(lang) {
|
|
@@ -5425,7 +5136,7 @@ export const ${varName}: Emitter = {
|
|
|
5425
5136
|
async function initCommand(opts) {
|
|
5426
5137
|
const projectDir = resolve3(opts.project ?? ".");
|
|
5427
5138
|
const lang = opts.lang;
|
|
5428
|
-
if (
|
|
5139
|
+
if (existsSync5(resolve3(projectDir, "oagen.config.ts"))) {
|
|
5429
5140
|
throw new Error("Project already initialized \u2014 oagen.config.ts exists");
|
|
5430
5141
|
}
|
|
5431
5142
|
const dirs = [`src/${lang}`, "test", "smoke", "docs/sdk-architecture"];
|
|
@@ -5442,7 +5153,7 @@ async function initCommand(opts) {
|
|
|
5442
5153
|
];
|
|
5443
5154
|
const packageJsonPath = resolve3(projectDir, "package.json");
|
|
5444
5155
|
let packageJsonContent;
|
|
5445
|
-
if (
|
|
5156
|
+
if (existsSync5(packageJsonPath)) {
|
|
5446
5157
|
const existingContent = readFileSync5(packageJsonPath, "utf-8");
|
|
5447
5158
|
const existingPkg = JSON.parse(existingContent);
|
|
5448
5159
|
const newPkg = JSON.parse(packageJson(lang));
|
|
@@ -5467,7 +5178,7 @@ async function initCommand(opts) {
|
|
|
5467
5178
|
}
|
|
5468
5179
|
const gitignorePath = resolve3(projectDir, ".gitignore");
|
|
5469
5180
|
let gitignoreContent;
|
|
5470
|
-
if (
|
|
5181
|
+
if (existsSync5(gitignorePath)) {
|
|
5471
5182
|
const existingContent = readFileSync5(gitignorePath, "utf-8");
|
|
5472
5183
|
const newContent = gitignore();
|
|
5473
5184
|
const existingLines = new Set(
|
|
@@ -5505,14 +5216,14 @@ async function initCommand(opts) {
|
|
|
5505
5216
|
}
|
|
5506
5217
|
|
|
5507
5218
|
// src/cli/config-loader.ts
|
|
5508
|
-
import * as
|
|
5509
|
-
import { existsSync as
|
|
5219
|
+
import * as path5 from "path";
|
|
5220
|
+
import { existsSync as existsSync6 } from "fs";
|
|
5510
5221
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
5511
5222
|
var CONFIG_NAMES = ["oagen.config.ts", "oagen.config.js", "oagen.config.mjs"];
|
|
5512
5223
|
async function loadConfig(cwd = process.cwd()) {
|
|
5513
5224
|
for (const name of CONFIG_NAMES) {
|
|
5514
|
-
const configPath =
|
|
5515
|
-
if (!
|
|
5225
|
+
const configPath = path5.resolve(cwd, name);
|
|
5226
|
+
if (!existsSync6(configPath)) continue;
|
|
5516
5227
|
try {
|
|
5517
5228
|
const mod = await import(pathToFileURL2(configPath).href);
|
|
5518
5229
|
const config = mod.default ?? mod;
|
|
@@ -5578,7 +5289,7 @@ program.command("generate").description("Run a registered emitter against an Ope
|
|
|
5578
5289
|
handleError
|
|
5579
5290
|
);
|
|
5580
5291
|
});
|
|
5581
|
-
program.command("diff").description("
|
|
5292
|
+
program.command("diff").description("Compare two OpenAPI specs and output a diff report").requiredOption("--old <path>", "Path to old spec").requiredOption("--new <path>", "Path to new spec").action((opts) => {
|
|
5582
5293
|
diffCommand({ ...opts, operationIdTransform: configOperationIdTransform, docUrl: configDocUrl }).catch(handleError);
|
|
5583
5294
|
});
|
|
5584
5295
|
program.command("extract").description("Extract public API surface from a live SDK").requiredOption("--sdk-path <path>", "Path to the live SDK").requiredOption("--lang <language>", "Target language").option("--output <path>", "Output file path").action((opts) => {
|