@toolproof-core/schema 1.0.3 → 1.0.5

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 (57) hide show
  1. package/dist/generated/normalized/Genesis.json +16 -265
  2. package/dist/generated/resources/Genesis.json +18 -305
  3. package/dist/generated/schemas/Genesis.json +14 -152
  4. package/dist/generated/schemas/standalone/Goal.json +0 -33
  5. package/dist/generated/schemas/standalone/Job.json +0 -42
  6. package/dist/generated/schemas/standalone/RawStrategy.json +14 -52
  7. package/dist/generated/schemas/standalone/ResourceType.json +0 -34
  8. package/dist/generated/schemas/standalone/RunnableStrategy.json +14 -57
  9. package/dist/generated/schemas/standalone/StrategyRun.json +14 -71
  10. package/dist/generated/types/standalone/Resource_Genesis.d.ts +1 -1
  11. package/dist/generated/types/standalone/Resource_Job.d.ts +1 -1
  12. package/dist/generated/types/standalone/Resource_RawStrategy.d.ts +1 -1
  13. package/dist/generated/types/standalone/Resource_ResourceType.d.ts +1 -1
  14. package/dist/generated/types/standalone/Resource_RunnableStrategy.d.ts +1 -1
  15. package/dist/generated/types/types.d.ts +119 -1126
  16. package/dist/index.d.ts +1 -4
  17. package/dist/index.js +0 -2
  18. package/dist/scripts/_lib/config.d.ts +6 -6
  19. package/dist/scripts/_lib/config.js +10 -12
  20. package/dist/scripts/extractSchemasFromResourceTypeShells.js +109 -103
  21. package/dist/scripts/generateDependencies.js +15 -14
  22. package/dist/scripts/generateSchemaShims.js +77 -85
  23. package/dist/scripts/generateStandaloneSchema.js +47 -37
  24. package/dist/scripts/generateStandaloneType.js +85 -79
  25. package/dist/scripts/generateTypes.js +350 -470
  26. package/dist/scripts/normalizeAnchorsToPointers.d.ts +1 -1
  27. package/dist/scripts/normalizeAnchorsToPointers.js +61 -33
  28. package/dist/scripts/wrapResourceTypesWithResourceShells.js +14 -16
  29. package/package.json +7 -8
  30. package/src/Genesis.json +1837 -1999
  31. package/src/generated/{dependencyMap.json → dependencies/dependencyMap.json} +9 -19
  32. package/src/generated/normalized/Genesis.json +16 -265
  33. package/src/generated/resources/Genesis.json +18 -305
  34. package/src/generated/schemas/Genesis.json +14 -152
  35. package/src/generated/schemas/standalone/Goal.json +0 -33
  36. package/src/generated/schemas/standalone/Job.json +0 -42
  37. package/src/generated/schemas/standalone/RawStrategy.json +14 -52
  38. package/src/generated/schemas/standalone/ResourceType.json +0 -34
  39. package/src/generated/schemas/standalone/RunnableStrategy.json +14 -57
  40. package/src/generated/schemas/standalone/StrategyRun.json +14 -71
  41. package/src/generated/types/standalone/Resource_Genesis.d.ts +1 -1
  42. package/src/generated/types/standalone/Resource_Job.d.ts +1 -1
  43. package/src/generated/types/standalone/Resource_RawStrategy.d.ts +1 -1
  44. package/src/generated/types/standalone/Resource_ResourceType.d.ts +1 -1
  45. package/src/generated/types/standalone/Resource_RunnableStrategy.d.ts +1 -1
  46. package/src/generated/types/types.d.ts +119 -1126
  47. package/src/index.ts +66 -93
  48. package/src/scripts/_lib/config.ts +203 -205
  49. package/src/scripts/extractSchemasFromResourceTypeShells.ts +261 -218
  50. package/src/scripts/generateDependencies.ts +121 -120
  51. package/src/scripts/generateSchemaShims.ts +127 -135
  52. package/src/scripts/generateStandaloneSchema.ts +185 -175
  53. package/src/scripts/generateStandaloneType.ts +127 -119
  54. package/src/scripts/generateTypes.ts +532 -615
  55. package/src/scripts/normalizeAnchorsToPointers.ts +141 -123
  56. package/src/scripts/wrapResourceTypesWithResourceShells.ts +82 -84
  57. package/dist/generated/dependencyMap.json +0 -292
@@ -1,503 +1,385 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { compileFromFile } from 'json-schema-to-typescript';
3
+ import { compile } from 'json-schema-to-typescript';
4
4
  import { getConfig } from './_lib/config.js';
5
- const config = getConfig();
6
- const projectRoot = config.getRoot();
7
- const inputDir = config.getSchemasDir();
8
- // We emit under src/genesis/generated/types and dist/genesis/generated/types
9
- const srcLibTypesDir = config.getTypesSrcDir();
10
- const srcLibOutputPath = config.getTypesSrcPath('types.d.ts');
11
- // Build an index of all schema files by their basename
12
- // This supports location-independent $id values where folder segments were removed
13
- function buildSchemaIndex(root) {
14
- const index = new Map();
15
- const stack = [root];
16
- while (stack.length) {
17
- const current = stack.pop();
18
- const entries = fs.readdirSync(current, { withFileTypes: true });
19
- for (const entry of entries) {
20
- const full = path.join(current, entry.name);
21
- if (entry.isDirectory()) {
22
- // Skip dist or types output folders if present inside schemas (defensive)
23
- if (entry.name === 'node_modules' || entry.name.startsWith('.'))
24
- continue;
25
- stack.push(full);
26
- }
27
- else if (entry.isFile() && entry.name.endsWith('.json')) {
28
- // Ignore temp files that can be left behind after crashes
29
- if (entry.name === '.combined-schema.json')
30
- continue;
31
- if (entry.name.startsWith('.normalized.'))
32
- continue;
33
- if (entry.name.startsWith('.'))
34
- continue;
35
- const baseName = entry.name; // keep extension for direct mapping
36
- if (index.has(baseName)) {
37
- // Hard fail on collisions so they can be fixed explicitly
38
- const existing = index.get(baseName);
39
- throw new Error(`Schema basename collision detected for "${baseName}"\n` +
40
- `First: ${existing}\n` +
41
- `Second: ${full}\n` +
42
- `Please rename one of the schemas to ensure unique basenames.`);
43
- }
44
- else {
45
- index.set(baseName, full);
46
- }
47
- }
5
+ // PURE: Format a JSON path for error messages.
6
+ function formatPath(pathSegments) {
7
+ let out = '';
8
+ for (const seg of pathSegments) {
9
+ if (typeof seg === 'number') {
10
+ out += `[${seg}]`;
11
+ continue;
48
12
  }
13
+ out = out ? `${out}.${seg}` : seg;
49
14
  }
50
- return index;
15
+ return out || '<root>';
51
16
  }
52
- // List all schema files (relative to inputDir), excluding documentation and temp files
53
- function listAllSchemaFiles(root) {
54
- const files = [];
55
- const stack = [root];
56
- while (stack.length) {
57
- const current = stack.pop();
58
- const entries = fs.readdirSync(current, { withFileTypes: true });
59
- for (const entry of entries) {
60
- const full = path.join(current, entry.name);
61
- if (entry.isDirectory()) {
62
- if (entry.name === 'documentation' || entry.name === 'node_modules' || entry.name.startsWith('.'))
63
- continue;
64
- stack.push(full);
65
- }
66
- else if (entry.isFile() && entry.name.endsWith('.json')) {
67
- // Ignore temp files that can be left behind after crashes
68
- if (entry.name === '.combined-schema.json')
69
- continue;
70
- if (entry.name.startsWith('.normalized.'))
71
- continue;
72
- if (entry.name.startsWith('.'))
73
- continue;
74
- // produce path relative to root with posix separators
75
- const rel = path.relative(root, full).split(path.sep).join('/');
76
- files.push(rel);
77
- }
17
+ // PURE: Validate schema array-keyword shapes; returns a list of issues (no mutation, no I/O).
18
+ function validateSchemaArrayKeywords(node, parentKey, pathSegments = []) {
19
+ if (Array.isArray(node)) {
20
+ const issues = [];
21
+ for (let i = 0; i < node.length; i++) {
22
+ issues.push(...validateSchemaArrayKeywords(node[i], parentKey, pathSegments.concat([i])));
78
23
  }
24
+ return issues;
79
25
  }
80
- files.sort(); // deterministic order
81
- return files;
82
- }
83
- async function main() {
84
- // Ensure types output folder exists
85
- fs.mkdirSync(srcLibTypesDir, { recursive: true });
86
- const parts = [];
87
- parts.push('// Auto-generated from JSON schemas. Do not edit.\n');
88
- // Precompute index for location-independent IDs (filename only after version segment)
89
- const schemaIndex = buildSchemaIndex(inputDir);
90
- const idToCanonical = {};
91
- // Custom resolver to map our absolute schema IDs to local files, preventing HTTP fetches.
92
- const toolproofResolver = {
93
- order: 1,
94
- canRead: (file) => {
95
- let url = (typeof file.url === 'string' ? file.url : file.url?.href) || '';
96
- // Strip accidental wrapping quotes
97
- if ((url.startsWith("'") && url.endsWith("'")) || (url.startsWith('"') && url.endsWith('"'))) {
98
- url = url.slice(1, -1);
99
- }
100
- return config.isSchemaUrl(url);
101
- },
102
- read: (file) => {
103
- let url = (typeof file.url === 'string' ? file.url : file.url?.href) || '';
104
- // Strip accidental wrapping quotes
105
- if ((url.startsWith("'") && url.endsWith("'")) || (url.startsWith('"') && url.endsWith('"'))) {
106
- url = url.slice(1, -1);
26
+ if (!node || typeof node !== 'object')
27
+ return [];
28
+ // IMPORTANT:
29
+ // In meta-schemas we often have `properties: { required: { ...schema... } }`.
30
+ // Here, the key "required" is a *property name*, not the JSON-Schema keyword.
31
+ // So when we are iterating a property-name map, do not treat keys as schema keywords.
32
+ const isPropertyNameMap = parentKey === 'properties' ||
33
+ parentKey === 'patternProperties' ||
34
+ parentKey === '$defs' ||
35
+ parentKey === 'dependentSchemas' ||
36
+ parentKey === 'dependentRequired';
37
+ const arrayKeys = ['anyOf', 'allOf', 'oneOf', 'required', 'enum'];
38
+ const issues = [];
39
+ for (const [k, v] of Object.entries(node)) {
40
+ // Keyword sanity checks for schema objects (regardless of where they appear).
41
+ // These reduce the chance of feeding obviously malformed shapes into the generator.
42
+ if (k === 'properties' || k === 'patternProperties' || k === '$defs' || k === 'dependentSchemas') {
43
+ if (v === null || typeof v !== 'object' || Array.isArray(v)) {
44
+ issues.push(`${formatPath(pathSegments.concat([k]))}: expected \`${k}\` to be an object map, got ${v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v}`);
107
45
  }
108
- // Strip hash part (anchors) for path resolution
109
- const noHash = url.split('#')[0];
110
- // Extract schema name using config
111
- const schemaName = config.extractSchemaName(noHash);
112
- const fileName = schemaName.endsWith('.json') ? schemaName : `${schemaName}.json`;
113
- // Resolve by basename only (location-independent IDs)
114
- const indexed = schemaIndex.get(fileName);
115
- if (indexed) {
116
- return fs.readFileSync(indexed, 'utf8');
117
- }
118
- throw new Error(`Toolproof resolver: could not locate schema for URL "${url}". ` +
119
- `Tried basename "${fileName}" in schema index. Ensure unique basenames and correct $id.`);
120
- }
121
- };
122
- // Files to include in the combined schema (auto-discovered, excludes documentation)
123
- const toCompile = listAllSchemaFiles(inputDir);
124
- // Collect schema refs and defs; we'll compile files individually to avoid combined-schema traversal issues.
125
- const definitions = {};
126
- const includedNames = [];
127
- for (const fileName of toCompile) {
128
- const p = path.join(inputDir, fileName);
129
- if (!fs.existsSync(p)) {
130
- console.warn(`Schema file missing, skipping: ${p}`);
131
- continue;
132
46
  }
133
- // Definition key: basename without extension, with path segments removed
134
- const base = path.basename(fileName, '.json');
135
- // Prefer the schema's declared $id so all references point to the same absolute URI
136
- // This helps json-schema-to-typescript dedupe declarations instead of emitting FooJson and FooJson1
137
- let refValue = `./${fileName}`;
138
- try {
139
- const raw = fs.readFileSync(p, 'utf8');
140
- const parsed = JSON.parse(raw);
141
- if (parsed && typeof parsed.$id === 'string' && parsed.$id.trim()) {
142
- // Sanitize IDs that may have been emitted with surrounding quotes
143
- // e.g. quotes around schema URL
144
- let cleaned = parsed.$id.trim();
145
- if ((cleaned.startsWith("'") && cleaned.endsWith("'")) || (cleaned.startsWith('"') && cleaned.endsWith('"'))) {
146
- cleaned = cleaned.slice(1, -1);
147
- }
148
- refValue = cleaned;
149
- idToCanonical[refValue] = base + 'Json';
47
+ if (k === 'dependentRequired') {
48
+ if (v === null || typeof v !== 'object' || Array.isArray(v)) {
49
+ issues.push(`${formatPath(pathSegments.concat([k]))}: expected \`dependentRequired\` to be an object map, got ${v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v}`);
150
50
  }
151
- // Promote this file's $defs to top-level combined $defs so each gets its own exported type
152
- if (parsed && parsed.$defs && typeof parsed.$defs === 'object') {
153
- const defNames = Object.keys(parsed.$defs).filter((k) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(k));
154
- for (const defName of defNames) {
155
- // Prefer bare def name; if collision, namespace with file base
156
- let entryName = defName;
157
- if (definitions[entryName]) {
158
- entryName = `${base}_${defName}`;
51
+ else {
52
+ for (const [depKey, depVal] of Object.entries(v)) {
53
+ if (!Array.isArray(depVal)) {
54
+ issues.push(`${formatPath(pathSegments.concat([k, depKey]))}: expected dependentRequired entries to be string arrays, got ${depVal === null ? 'null' : typeof depVal}`);
55
+ continue;
159
56
  }
160
- if (!definitions[entryName]) {
161
- definitions[entryName] = { $ref: `${refValue}#/$defs/${defName}` };
162
- includedNames.push(entryName);
57
+ for (let i = 0; i < depVal.length; i++) {
58
+ if (typeof depVal[i] !== 'string') {
59
+ issues.push(`${formatPath(pathSegments.concat([k, depKey, i]))}: expected dependentRequired entries to be strings, got ${depVal[i] === null ? 'null' : typeof depVal[i]}`);
60
+ }
163
61
  }
164
62
  }
165
63
  }
166
64
  }
167
- catch (e) {
168
- // If parsing fails, fall back to relative ref; proceed gracefully
169
- }
170
- definitions[base] = { $ref: refValue };
171
- includedNames.push(base);
172
- }
173
- if (includedNames.length === 0) {
174
- console.warn('No schema files found to compile. Nothing to do.');
175
- return;
176
- }
177
- // Instead of building a combined schema (which can trip json-schema-to-typescript traversals),
178
- // compile each discovered schema file individually and concatenate the outputs.
179
- try {
180
- let ts = '';
181
- for (const fileName of toCompile) {
182
- const schemaPath = path.join(inputDir, fileName);
183
- if (!fs.existsSync(schemaPath))
184
- continue;
185
- // Load and defensively normalize array-expected keywords to avoid generator crashes
186
- let toCompilePath = schemaPath;
187
- try {
188
- const rawSchema = fs.readFileSync(schemaPath, 'utf8');
189
- const parsedSchema = JSON.parse(rawSchema);
190
- function normalizeArrays(node, parentKey) {
191
- if (Array.isArray(node)) {
192
- for (let i = 0; i < node.length; i++)
193
- normalizeArrays(node[i], parentKey);
194
- return;
195
- }
196
- if (!node || typeof node !== 'object')
197
- return;
198
- // IMPORTANT:
199
- // In meta-schemas we often have `properties: { required: { ...schema... } }`.
200
- // Here, the key "required" is a *property name*, not the JSON-Schema keyword
201
- // "required". Blindly normalizing would wrap that schema object into an array,
202
- // causing json-schema-to-typescript to emit the schema-shape instead of `string[]`.
203
- //
204
- // So: only coerce array-keywords when we're not inside a map of property names.
205
- const isPropertyNameMap = parentKey === 'properties'
206
- || parentKey === 'patternProperties'
207
- || parentKey === '$defs'
208
- || parentKey === 'definitions'
209
- || parentKey === 'dependentSchemas';
210
- if (!isPropertyNameMap) {
211
- const arrayKeys = ['anyOf', 'allOf', 'oneOf', 'required', 'enum'];
212
- for (const k of arrayKeys) {
213
- if (k in node) {
214
- const v = node[k];
215
- if (!Array.isArray(v))
216
- node[k] = [v];
217
- }
65
+ if (!isPropertyNameMap && arrayKeys.includes(k) && Object.prototype.hasOwnProperty.call(node, k)) {
66
+ if (!Array.isArray(v)) {
67
+ issues.push(`${formatPath(pathSegments.concat([k]))}: expected \`${k}\` to be an array, got ${v === null ? 'null' : typeof v}`);
68
+ }
69
+ else {
70
+ if ((k === 'anyOf' || k === 'allOf' || k === 'oneOf') && v.length === 0) {
71
+ issues.push(`${formatPath(pathSegments.concat([k]))}: expected \`${k}\` to be a non-empty array`);
72
+ }
73
+ if (k === 'anyOf' || k === 'allOf' || k === 'oneOf') {
74
+ for (let i = 0; i < v.length; i++) {
75
+ const item = v[i];
76
+ const ok = typeof item === 'boolean' ||
77
+ (item !== null && typeof item === 'object' && !Array.isArray(item));
78
+ if (!ok) {
79
+ issues.push(`${formatPath(pathSegments.concat([k, i]))}: expected a schema (object or boolean), got ${item === null ? 'null' : Array.isArray(item) ? 'array' : typeof item}`);
218
80
  }
219
81
  }
220
- for (const [k, v] of Object.entries(node))
221
- normalizeArrays(v, k);
222
82
  }
223
- // json-schema-to-typescript has a long-standing quirk:
224
- // when a schema uses `allOf`, sibling object keywords like `properties`/`required`
225
- // can be ignored in the emitted TS (it treats `allOf` as the whole schema).
226
- //
227
- // Example (Genesis $defs/Job):
228
- // { type: 'object', allOf: [Documented, RolesWrapper], properties: { identity: ... }, required: [...] }
229
- // can become:
230
- // type Job = Documented & RolesWrapper
231
- //
232
- // To avoid cluttering the source JSON schemas, we normalize to a temp file:
233
- // move those sibling object keywords into an extra `allOf` item.
234
- function normalizeAllOfSiblingObjectKeywords(node) {
235
- if (Array.isArray(node)) {
236
- for (const item of node)
237
- normalizeAllOfSiblingObjectKeywords(item);
238
- return;
239
- }
240
- if (!node || typeof node !== 'object')
241
- return;
242
- const hasAllOf = Array.isArray(node.allOf) && node.allOf.length > 0;
243
- const looksLikeObjectSchema = node.type === 'object' ||
244
- node.properties !== undefined ||
245
- node.required !== undefined ||
246
- node.unevaluatedProperties !== undefined ||
247
- node.additionalProperties !== undefined;
248
- if (hasAllOf && looksLikeObjectSchema) {
249
- const siblingKeys = [
250
- 'properties',
251
- 'required',
252
- 'additionalProperties',
253
- 'unevaluatedProperties',
254
- 'propertyNames',
255
- 'patternProperties',
256
- 'dependentRequired',
257
- 'dependentSchemas',
258
- 'minProperties',
259
- 'maxProperties'
260
- ];
261
- const hasSiblingObjectKeywords = siblingKeys.some((k) => k in node);
262
- if (hasSiblingObjectKeywords) {
263
- const overlay = {};
264
- if (node.type === 'object')
265
- overlay.type = 'object';
266
- for (const k of siblingKeys) {
267
- if (k in node) {
268
- overlay[k] = node[k];
269
- delete node[k];
270
- }
271
- }
272
- // Prepend so it participates in the intersection early.
273
- node.allOf = [overlay, ...node.allOf];
83
+ if (k === 'required') {
84
+ // `required` must be string[] when used as a JSON-Schema keyword.
85
+ const seen = new Set();
86
+ for (let i = 0; i < v.length; i++) {
87
+ if (typeof v[i] !== 'string') {
88
+ issues.push(`${formatPath(pathSegments.concat([k, i]))}: expected \`required\` entries to be strings, got ${v[i] === null ? 'null' : typeof v[i]}`);
89
+ continue;
90
+ }
91
+ if (seen.has(v[i])) {
92
+ issues.push(`${formatPath(pathSegments.concat([k]))}: duplicate required entry \`${v[i]}\``);
274
93
  }
94
+ seen.add(v[i]);
275
95
  }
276
- for (const v of Object.values(node))
277
- normalizeAllOfSiblingObjectKeywords(v);
278
96
  }
279
- // Normalize expected arrays to prevent traversal crashes
280
- normalizeArrays(parsedSchema);
281
- // Normalize `allOf` + sibling object keywords so the TS generator doesn't drop them.
282
- normalizeAllOfSiblingObjectKeywords(parsedSchema);
283
- const tmpPath = path.join(inputDir, `.normalized.${path.basename(fileName)}`);
284
- fs.writeFileSync(tmpPath, JSON.stringify(parsedSchema, null, 2), 'utf8');
285
- toCompilePath = tmpPath;
286
97
  }
287
- catch (e) {
288
- // If normalization fails, fall back to original path
289
- }
290
- const part = await compileFromFile(toCompilePath, {
291
- bannerComment: '',
292
- //
293
- declareExternallyReferenced: true,
294
- unreachableDefinitions: true,
295
- // Forward ref parser options so absolute $id/$ref URLs resolve from local files
296
- $refOptions: {
297
- // Don’t go to the network; we provide a local resolver for our domain
298
- resolve: {
299
- file: { order: 2 },
300
- http: false,
301
- https: false,
302
- toolproof: toolproofResolver
303
- }
304
- }
305
- });
306
- ts += '\n' + part + '\n';
307
- // Cleanup temp normalized file
308
- if (toCompilePath !== schemaPath) {
309
- try {
310
- fs.unlinkSync(toCompilePath);
98
+ }
99
+ issues.push(...validateSchemaArrayKeywords(v, k, pathSegments.concat([k])));
100
+ }
101
+ return issues;
102
+ }
103
+ // PURE: Move sibling object keywords into `allOf` overlays (returns a new tree; does not mutate inputs).
104
+ function normalizeAllOfSiblingObjectKeywords(node) {
105
+ if (Array.isArray(node)) {
106
+ let changed = false;
107
+ const out = node.map((item) => {
108
+ const next = normalizeAllOfSiblingObjectKeywords(item);
109
+ if (next !== item)
110
+ changed = true;
111
+ return next;
112
+ });
113
+ return changed ? out : node;
114
+ }
115
+ if (!node || typeof node !== 'object')
116
+ return node;
117
+ const hasAllOf = Array.isArray(node.allOf) && node.allOf.length > 0;
118
+ const looksLikeObjectSchema = node.type === 'object' ||
119
+ node.properties !== undefined ||
120
+ node.required !== undefined ||
121
+ node.unevaluatedProperties !== undefined ||
122
+ node.additionalProperties !== undefined;
123
+ const siblingKeys = [
124
+ 'properties',
125
+ 'required',
126
+ 'additionalProperties',
127
+ 'unevaluatedProperties',
128
+ 'propertyNames',
129
+ 'patternProperties',
130
+ 'dependentRequired',
131
+ 'dependentSchemas',
132
+ 'minProperties',
133
+ 'maxProperties'
134
+ ];
135
+ let localNode = node;
136
+ if (hasAllOf && looksLikeObjectSchema) {
137
+ const hasSiblingObjectKeywords = siblingKeys.some((k) => k in localNode);
138
+ if (hasSiblingObjectKeywords) {
139
+ const overlay = {};
140
+ if (localNode.type === 'object')
141
+ overlay.type = 'object';
142
+ const nextNode = { ...localNode };
143
+ for (const k of siblingKeys) {
144
+ if (k in nextNode) {
145
+ overlay[k] = nextNode[k];
146
+ delete nextNode[k];
311
147
  }
312
- catch { }
313
148
  }
149
+ nextNode.allOf = [overlay, ...nextNode.allOf];
150
+ localNode = nextNode;
314
151
  }
315
- // Remove permissive index signatures that make interfaces open-ended.
316
- // Keep meaningful map-like signatures (e.g., `[k: string]: ResourceRoleValue`) intact.
317
- // Robust single-pass: delete the entire line (with optional trailing newline) where the signature appears.
318
- // This avoids introducing extra blank lines while handling CRLF/LF and varying indentation.
319
- ts = ts.replace(/^\s*\[k:\s*string\]:\s*unknown;\s*(?:\r?\n)?/gm, '');
320
- // Fix meta-schema types where json-schema-to-typescript can still incorrectly interpret
321
- // schema definitions as literal values (or emit overly-restrictive `{}` objects).
322
- // We do this as a broad post-pass on the emitted TS because the generator output varies
323
- // (e.g. `allOf?: { }[];` vs `allOf?: [{type:"array"; ...}]`).
324
- // `$defs?: { }` or `$defs: { }` -> map type (preserve required/optional marker)
325
- // NOTE: We emit `Record<...>` instead of an index signature because later cleanup
326
- // strips standalone `[k: string]: unknown;` lines.
327
- ts = ts.replace(/^(\s*)(\$defs\??:\s*)\{\s*\r?\n\s*\};/gm, (_m, indent, head) => `${indent}${head}Record<string, unknown>;`);
328
- // `properties?: { }` or `properties: { }` -> map type
329
- ts = ts.replace(/^(\s*)(properties\??:\s*)\{\s*\r?\n\s*\};/gm, (_m, indent, head) => `${indent}${head}Record<string, unknown>;`);
330
- // `allOf?: { }[];` (and similar for anyOf/oneOf) -> array of schema-ish objects
331
- ts = ts.replace(/^(\s*)((?:allOf|anyOf|oneOf)\??:\s*)\{\s*\r?\n\s*\}\[\];/gm, (_m, indent, head) => `${indent}${head}Array<{[k: string]: unknown}>;`);
332
- // Older broken shapes from earlier normalization (keep for backwards compatibility)
333
- ts = ts.replace(/((?:allOf|anyOf|oneOf)\??:\s*)\[\{type:\s*"array";\s*items:\s*\{type:\s*"object"\}\}\];/g, '$1Array<{[k: string]: unknown}>;');
334
- ts = ts.replace(/required\?:\s*\[\{type:\s*"array";\s*items:\s*\{type:\s*"string"\};\s*uniqueItems:\s*true\}\];/g, 'required?: string[];');
335
- // Similarly fix IdentityProp/MeritProp which have the same issue
336
- ts = ts.replace(/^(export interface IdentityProp[\s\S]*?)(required:\s*\[\{type:\s*"array";\s*contains:\s*\{const:\s*"identity"\};\s*items:\s*\{type:\s*"string"\};\s*uniqueItems:\s*true\}\];)/gm, '$1required?: string[];');
337
- ts = ts.replace(/^(export interface MeritProp[\s\S]*?)(required:\s*\[\{type:\s*"array";\s*items:\s*\{type:\s*"string"\};\s*uniqueItems:\s*true\}\];)/gm, '$1required?: string[];');
338
- // Prune verbose type/interface names produced from absolute $id URLs.
339
- // Deterministic pruning based on original $id -> baseName map
340
- // This avoids heuristic truncation that dropped prefixes.
341
- function idToGeneratedIdentifier(id) {
342
- // json-schema-to-typescript seems to create a PascalCase of the URL with protocol prefix
343
- // Simplified reconstruction: 'https://' => 'Https', then capitalize path & host segments
344
- const noProto = id.replace(/^https?:\/\//i, '');
345
- const tokens = noProto
346
- .split(/[\/#?.=&_-]+/)
347
- .filter(Boolean)
348
- .map((t) => t.replace(/[^A-Za-z0-9]/g, ''))
349
- .filter(Boolean)
350
- .map((t) => t.charAt(0).toUpperCase() + t.slice(1));
351
- return 'Https' + tokens.join('') + 'Json';
152
+ }
153
+ let changed = localNode !== node;
154
+ const out = localNode === node ? {} : { ...localNode };
155
+ for (const [k, v] of Object.entries(localNode)) {
156
+ const next = normalizeAllOfSiblingObjectKeywords(v);
157
+ if (next !== v) {
158
+ if (!changed) {
159
+ changed = true;
160
+ Object.assign(out, localNode);
161
+ }
162
+ out[k] = next;
352
163
  }
353
- // Perform replacements for known IDs
354
- for (const [id, canonical] of Object.entries(idToCanonical)) {
355
- const longName = idToGeneratedIdentifier(id);
356
- if (longName === canonical)
357
- continue; // already minimal
358
- const re = new RegExp(`\\b${longName}\\b`, 'g');
359
- ts = ts.replace(re, canonical);
164
+ else if (changed) {
165
+ out[k] = v;
360
166
  }
361
- // Remove version prefixes inside any remaining long identifiers: Https...V0... -> remove V0 if followed by capital
362
- ts = ts.replace(/(Https[A-Za-z0-9_]*?)V\d+([A-Z])/g, '$1$2');
363
- // Final cleanup: aggressively strip the domain prefix `HttpsSchemasToolproofCom` from ALL identifiers.
364
- // This is safe because those long names are only artifacts of json-schema-to-typescript; base names don't start with that sequence.
365
- ts = ts.replace(/\bHttpsSchemasToolproofCom(?=[A-Z])/g, '');
366
- // Remove accidental duplicate union entries in any exported union types after shortening.
367
- ts = ts.replace(/export type ([A-Za-z0-9_]+) =([\s\S]*?);/g, (m, typeName, body) => {
368
- const lines = body.split(/\n/);
369
- const seen2 = new Set();
370
- const kept = [];
371
- for (const line of lines) {
372
- const trimmed = line.trim();
373
- const match = /^\|\s*([A-Za-z0-9_]+)\b/.exec(trimmed);
374
- if (match) {
375
- const name = match[1];
376
- if (!seen2.has(name)) {
377
- seen2.add(name);
378
- kept.push(' | ' + name);
379
- }
380
- }
381
- else if (trimmed.length) {
382
- kept.push(line);
383
- }
384
- }
385
- return `export type ${typeName} =\n` + kept.join('\n') + ';';
386
- });
387
- // If nothing was emitted, compile Genesis.json as a final fallback.
388
- if (!ts || !ts.trim()) {
389
- const primary = path.join(inputDir, 'Genesis.json');
390
- if (fs.existsSync(primary)) {
391
- ts = await compileFromFile(primary, {
392
- bannerComment: '',
393
- declareExternallyReferenced: true,
394
- unreachableDefinitions: true,
395
- $refOptions: {
396
- resolve: {
397
- file: { order: 2 },
398
- http: false,
399
- https: false,
400
- toolproof: toolproofResolver
401
- }
402
- }
403
- });
404
- }
167
+ }
168
+ return changed ? out : node;
169
+ }
170
+ // PURE: Clone + apply all generator-normalizations.
171
+ function normalizeSchemaForGenerator(schema) {
172
+ return normalizeAllOfSiblingObjectKeywords(schema);
173
+ }
174
+ // PURE: Convert a regex-ish pattern into a TS template literal when recognizable.
175
+ function deriveTemplateFromPattern(pattern) {
176
+ // Common (and currently canonical for identities): ^PREFIX-.+$ => `PREFIX-${string}`
177
+ const m1 = /^\^([^$]+)\.\+\$/.exec(pattern);
178
+ if (m1) {
179
+ const prefix = m1[1];
180
+ if (!/[`]/.test(prefix)) {
181
+ return '`' + prefix + '${string}`';
405
182
  }
406
- // Still nothing? ensure we emit a module so downstream imports don't fail.
407
- if (!ts || !ts.trim()) {
408
- ts = '// (No concrete types emitted by generator)\nexport {}\n';
183
+ }
184
+ return undefined;
185
+ }
186
+ // PURE: Extract Identity template-literal aliases from a parsed schema.
187
+ function loadPatternTemplatesFromSchema(schema) {
188
+ const map = {};
189
+ const defs = schema?.$defs && typeof schema.$defs === 'object' ? schema.$defs : {};
190
+ for (const [defName, defVal] of Object.entries(defs)) {
191
+ const isPatternType = /Identity$/.test(defName);
192
+ if (!isPatternType)
193
+ continue;
194
+ const v = defVal;
195
+ if (v && v.type === 'string' && typeof v.pattern === 'string') {
196
+ const tmpl = deriveTemplateFromPattern(v.pattern);
197
+ if (tmpl)
198
+ map[defName] = tmpl;
409
199
  }
410
- // Overlay Identity/Ref aliases with template literal types inferred from Genesis.json patterns.
411
- // For each $defs entry ending with "Identity" or "Ref", and that provides a `pattern`,
412
- // derive a TS template literal; otherwise fall back to plain `string`.
413
- function deriveTemplateFromPattern(pattern) {
414
- // Common form: ^PREFIX-.+$ => PREFIX-${string}
415
- const m1 = /^\^([^$]+)\.\+\$/.exec(pattern);
416
- if (m1) {
417
- const prefix = m1[1];
418
- if (!/[`]/.test(prefix)) {
419
- return '`' + prefix + '${string}`';
420
- }
421
- }
422
- // Slightly stricter forms: ^PREFIX-[A-Za-z0-9]+$ => PREFIX-${string}
423
- const m2 = /^\^([A-Za-z0-9._:-]+-)\[?\^?[A-Za-z0-9]+\]?\+?\$/.exec(pattern);
424
- if (m2) {
425
- const prefix = m2[1];
426
- if (!/[`]/.test(prefix)) {
427
- return '`' + prefix + '${string}`';
200
+ }
201
+ return map;
202
+ }
203
+ // PURE: Remove duplicate union members from emitted `export type X = | ...` blocks.
204
+ function removeDuplicateUnionEntries(ts) {
205
+ return ts.replace(/export type ([A-Za-z0-9_]+) =([\s\S]*?);/g, (_m, typeName, body) => {
206
+ const lines = body.split(/\r?\n/);
207
+ const seen = new Set();
208
+ const kept = [];
209
+ for (const line of lines) {
210
+ const trimmed = line.trim();
211
+ const match = /^\|\s*([A-Za-z0-9_]+)\b/.exec(trimmed);
212
+ if (match) {
213
+ const name = match[1];
214
+ if (!seen.has(name)) {
215
+ seen.add(name);
216
+ kept.push(' | ' + name);
428
217
  }
218
+ continue;
429
219
  }
430
- return undefined;
220
+ if (trimmed.length)
221
+ kept.push(line);
431
222
  }
432
- function loadPatternTemplates() {
433
- const map = {};
434
- try {
435
- const genesisPath = path.join(inputDir, config.getSourceFile());
436
- if (fs.existsSync(genesisPath)) {
437
- const raw = fs.readFileSync(genesisPath, 'utf8');
438
- const parsed = JSON.parse(raw);
439
- const defs = parsed?.$defs && typeof parsed.$defs === 'object' ? parsed.$defs : {};
440
- for (const [defName, defVal] of Object.entries(defs)) {
441
- // Process pattern-bearing string types.
442
- // Supports "...Identity" / "...Ref" conventions.
443
- const isPatternType = /(?:Identity|Ref)$/.test(defName);
444
- if (!isPatternType)
445
- continue;
446
- const v = defVal;
447
- if (v && v.type === 'string' && typeof v.pattern === 'string') {
448
- const tmpl = deriveTemplateFromPattern(v.pattern);
449
- if (tmpl)
450
- map[defName] = tmpl;
451
- }
452
- }
223
+ return `export type ${typeName} =\n` + kept.join('\n') + ';';
224
+ });
225
+ }
226
+ // PURE: Escape a string for use inside a RegExp pattern.
227
+ function escapeRegExpLiteral(value) {
228
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
229
+ }
230
+ // PURE: Fix a known json-schema-to-typescript edge case where a JSON-like recursive value union
231
+ // accidentally includes a direct self-reference (`| JsonData`) instead of the intended array case (`| JsonData[]`).
232
+ function fixJsonDataSelfReference(ts) {
233
+ const lines = ts.split(/\r?\n/);
234
+ const startIndex = lines.findIndex((l) => /^export\s+type\s+JsonData\s*=\s*$/.test(l.trimEnd()));
235
+ if (startIndex < 0)
236
+ return ts;
237
+ // This type currently ends with a union member object whose closing line is `};`.
238
+ // Find that terminator after the type header.
239
+ let endIndex = -1;
240
+ for (let i = startIndex + 1; i < lines.length; i++) {
241
+ if (/^\s*};\s*$/.test(lines[i])) {
242
+ endIndex = i;
243
+ break;
244
+ }
245
+ }
246
+ if (endIndex < 0)
247
+ return ts;
248
+ for (let i = startIndex + 1; i < endIndex; i++) {
249
+ if (/^\s*\|\s*JsonData\s*$/.test(lines[i])) {
250
+ lines[i] = lines[i].replace(/\bJsonData\b/, 'JsonData[]');
251
+ }
252
+ }
253
+ return lines.join('\n');
254
+ }
255
+ // PURE: Apply deterministic post-processing to the generator output (no I/O).
256
+ function postProcessEmittedTypes(ts, parsedSchema) {
257
+ // Remove permissive index signatures that make interfaces open-ended.
258
+ // Keep meaningful map-like signatures (e.g., `[k: string]: ResourceRoleValue`) intact.
259
+ // Robust single-pass: delete the entire line (with optional trailing newline) where the signature appears.
260
+ // This avoids introducing extra blank lines while handling CRLF/LF and varying indentation.
261
+ ts = ts.replace(/^\s*\[k:\s*string\]:\s*unknown;\s*(?:\r?\n)?/gm, '');
262
+ // Fix meta-schema types where json-schema-to-typescript can still incorrectly interpret
263
+ // schema definitions as literal values (or emit overly-restrictive `{}` objects).
264
+ // We do this as a broad post-pass on the emitted TS because the generator output varies
265
+ // (e.g. `allOf?: { }[];` vs `allOf?: [{type:"array"; ...}]`).
266
+ // `$defs?: { }` or `$defs: { }` -> map type (preserve required/optional marker)
267
+ // NOTE: We emit `Record<...>` instead of an index signature because later cleanup
268
+ // strips standalone `[k: string]: unknown;` lines.
269
+ ts = ts.replace(/^(\s*)(\$defs\??:\s*)\{\s*\r?\n\s*\};/gm, (_m, indent, head) => `${indent}${head}Record<string, unknown>;`);
270
+ // `properties?: { }` or `properties: { }` -> map type
271
+ ts = ts.replace(/^(\s*)(properties\??:\s*)\{\s*\r?\n\s*\};/gm, (_m, indent, head) => `${indent}${head}Record<string, unknown>;`);
272
+ // `allOf?: { }[];` (and similar for anyOf/oneOf) -> array of schema-ish objects
273
+ ts = ts.replace(/^(\s*)((?:allOf|anyOf|oneOf)\??:\s*)\{\s*\r?\n\s*\}\[\];/gm, (_m, indent, head) => `${indent}${head}Array<{[k: string]: unknown}>;`);
274
+ ts = removeDuplicateUnionEntries(ts);
275
+ // Strip URL-derived root schema interface names produced by json-schema-to-typescript
276
+ // (e.g. `HttpsSchemasToolproofComV2GenesisJson`). These are naming artifacts and not
277
+ // meaningful domain types.
278
+ const schemaId = parsedSchema?.$id;
279
+ if (typeof schemaId === 'string') {
280
+ const m = /\/([^/]+)\.json$/i.exec(schemaId);
281
+ const rootBaseName = m?.[1];
282
+ if (rootBaseName) {
283
+ const rootJsonSuffix = `${rootBaseName}Json`;
284
+ const rootJsonSuffixEsc = escapeRegExpLiteral(rootJsonSuffix);
285
+ // Remove the (typically empty) URL-derived root interface, if present.
286
+ ts = ts.replace(new RegExp(`^export interface Https[A-Za-z0-9_]*${rootJsonSuffixEsc}\\s*\\{\\s*\\}\\s*(?:\\r?\\n)?`, 'gm'), '');
287
+ // Replace remaining references to that URL-derived name in doc/comments.
288
+ ts = ts.replace(new RegExp(`\\bHttps[A-Za-z0-9_]*${rootJsonSuffixEsc}\\b`, 'g'), rootBaseName);
289
+ }
290
+ }
291
+ if (!ts || !ts.trim()) {
292
+ throw new Error('Type generator emitted no output for Genesis schema.');
293
+ }
294
+ // Overlay Identity aliases with template literal types inferred from Genesis.json patterns.
295
+ // For each $defs entry ending with "Identity" that provides a `pattern`,
296
+ // derive a TS template literal; otherwise fall back to plain `string`.
297
+ const patternTemplates = loadPatternTemplatesFromSchema(parsedSchema);
298
+ // Replace any exported Identity aliases to use the inferred template literals where available.
299
+ // Handle the common `= string;` output from json-schema-to-typescript.
300
+ ts = ts.replace(/^(export\s+type\s+)([A-Za-z_][A-Za-z0-9_]*Identity)(\s*=\s*)string\s*;$/gm, (_m, p1, typeName, p3) => {
301
+ const tmpl = patternTemplates[typeName];
302
+ return `${p1}${typeName}${p3}${tmpl ?? 'string'};`;
303
+ });
304
+ // Post-process map-like interfaces to enforce key constraints via template literal Identity types.
305
+ // Replace index-signature interfaces with Record<KeyType, ValueType> so object literal keys are checked.
306
+ const resourceRoleKeyType = 'ResourceRoleIdentity';
307
+ const jobStepKeyType = 'JobStepIdentity';
308
+ const strategyThreadKeyType = 'StrategyThreadIdentity';
309
+ ts = ts.replace(/export interface RoleMap\s*{[^}]*}/g, `export type RoleMap = Record<${resourceRoleKeyType}, ResourceRoleValue>;`);
310
+ // Normalize StrategyState & related socket maps to identity-keyed Records.
311
+ // These are emitted as `[k: string]` by json-schema-to-typescript but are identity-keyed in practice.
312
+ // Per schema: JobStepSocket: Record<ResourceRoleIdentity, Resource>
313
+ // StrategyState: Record<JobStepIdentity, JobStepSocket>
314
+ const jobStepSocketValueType = 'Resource';
315
+ ts = ts.replace(/export interface JobStepSocket\s*\{\s*\[k:\s*string\]:\s*[^;]+;\s*\}/g, `export type JobStepSocket = Record<${resourceRoleKeyType}, ${jobStepSocketValueType}>;`);
316
+ ts = ts.replace(/export interface StrategyState\s*\{\s*\[k:\s*string\]:\s*JobStepSocket;\s*\}/g, `export type StrategyState = Record<${jobStepKeyType}, JobStepSocket>;`);
317
+ ts = ts.replace(/(strategyStateUpdate\??:\s*)\{\s*\[k:\s*string\]:\s*JobStepSocket;\s*\};/g, `$1Record<${jobStepKeyType}, JobStepSocket>;`);
318
+ // Ensure key constraints for strategyThreadMap are preserved as template-literal identity keys.
319
+ // json-schema-to-typescript emits `[k: string]: StepArray;`, but we want keys to be `StrategyThreadIdentity`.
320
+ ts = ts.replace(/export interface StrategyThreadMap\s*\{\s*\[k:\s*string\]:\s*StepArray;\s*\}/g, `export type StrategyThreadMap = Record<${strategyThreadKeyType}, StepArray>;`);
321
+ ts = fixJsonDataSelfReference(ts);
322
+ return ts;
323
+ }
324
+ // PURE: Add banner + formatting/guards to build the final .d.ts content.
325
+ function finalizeOutputDts(emittedType) {
326
+ const banner = '// Auto-generated from JSON schemas. Do not edit.\n';
327
+ let output = banner + '\n' + emittedType + '\n';
328
+ // Final guard: strip any lingering `[k: string]: unknown;` that might have been
329
+ // reintroduced by later transforms.
330
+ output = output.replace(/^\s*\[k:\s*string\]:\s*unknown;\s*$/gm, '');
331
+ // Cosmetic post-format: remove lone blank lines before closing braces and collapse excessive blank lines
332
+ // - Remove a single blank line before `};` and `}`
333
+ // - Collapse 3+ consecutive newlines into a maximum of 2
334
+ output = output
335
+ .replace(/\r?\n\s*\r?\n(\s*};)/g, '\n$1')
336
+ .replace(/\r?\n\s*\r?\n(\s*})/g, '\n$1')
337
+ .replace(/(\r?\n){3,}/g, '\n\n');
338
+ // As an additional safeguard, make sure the final .d.ts is treated as a module.
339
+ // If no export/interface/module is present, append an empty export.
340
+ if (!/\bexport\b|\bdeclare\s+module\b|\bdeclare\s+namespace\b/.test(output)) {
341
+ output += '\nexport {}\n';
342
+ }
343
+ return output;
344
+ }
345
+ // IMPURE: Main script entrypoint (config + filesystem I/O + console + process exit).
346
+ async function main() {
347
+ try {
348
+ const config = getConfig();
349
+ const inputDir = config.getSchemasDir();
350
+ const srcLibTypesDir = config.getTypesSrcDir();
351
+ const srcLibOutputPath = config.getTypesSrcPath('types.d.ts');
352
+ fs.mkdirSync(srcLibTypesDir, { recursive: true });
353
+ const schemaFileName = config.getSourceFile();
354
+ const schemaPath = path.join(inputDir, schemaFileName);
355
+ if (!fs.existsSync(schemaPath)) {
356
+ throw new Error(`Root schema not found: ${schemaPath}\n` +
357
+ `Run the schema extraction step first (e.g. pnpm run extractSchemasFromResourceTypeShells).`);
358
+ }
359
+ const rawSchema = fs.readFileSync(schemaPath, 'utf8');
360
+ const parsedSchema = JSON.parse(rawSchema);
361
+ const validationIssues = validateSchemaArrayKeywords(parsedSchema);
362
+ if (validationIssues.length) {
363
+ throw new Error('Genesis schema is not in canonical form for type generation:\n' +
364
+ validationIssues.map((s) => `- ${s}`).join('\n'));
365
+ }
366
+ const normalizedSchema = normalizeSchemaForGenerator(parsedSchema);
367
+ let ts;
368
+ const rootName = path.basename(schemaFileName, path.extname(schemaFileName));
369
+ ts = await compile(normalizedSchema, rootName, {
370
+ bannerComment: '',
371
+ declareExternallyReferenced: true,
372
+ unreachableDefinitions: true,
373
+ $refOptions: {
374
+ resolve: {
375
+ file: { order: 2 },
376
+ http: false,
377
+ https: false
453
378
  }
454
379
  }
455
- catch {
456
- // ignore failures; we'll just fall back to string
457
- }
458
- return map;
459
- }
460
- const patternTemplates = loadPatternTemplates();
461
- // Replace any exported Identity/Ref aliases to use the inferred template literals where available.
462
- // Handle the common `= string;` output from json-schema-to-typescript.
463
- ts = ts.replace(/^(export\s+type\s+)([A-Za-z_][A-Za-z0-9_]*(?:Identity|Ref))(\s*=\s*)string\s*;$/gm, (_m, p1, typeName, p3) => {
464
- const tmpl = patternTemplates[typeName];
465
- return `${p1}${typeName}${p3}${tmpl ?? 'string'};`;
466
380
  });
467
- // Post-process map-like interfaces to enforce key constraints via template literal Identity types.
468
- // Replace index-signature interfaces with Record<KeyType, ValueType> so object literal keys are checked.
469
- const resourceRoleKeyType = 'ResourceRoleIdentity';
470
- const resourceKeyType = 'ResourceIdentity';
471
- const executionKeyType = 'ExecutionIdentity';
472
- const strategyThreadKeyType = 'StrategyThreadIdentity';
473
- ts = ts.replace(/export interface RoleMap\s*{[^}]*}/g, `export type RoleMap = Record<${resourceRoleKeyType}, ResourceRoleValue>;`);
474
- ts = ts.replace(/export interface RoleBindingMap\s*{[^}]*}/g, `export type RoleBindingMap = Record<${resourceRoleKeyType}, ${resourceKeyType}>;`);
475
- // Normalize StrategyState & related socket maps to identity-keyed Records.
476
- // These are emitted as `[k: string]` by json-schema-to-typescript but are identity-keyed in practice.
477
- const executionSocketValueType = 'ResourceMissing | ResourcePotentialInput | ResourcePotentialOutput | Resource';
478
- ts = ts.replace(/export interface ExecutionSocket\s*\{\s*\[k:\s*string\]:\s*[^;]+;\s*\}/g, `export type ExecutionSocket = Record<${resourceRoleKeyType}, ${executionSocketValueType}>;`);
479
- ts = ts.replace(/export interface StrategyState\s*\{\s*\[k:\s*string\]:\s*ExecutionSocket;\s*\}/g, `export type StrategyState = Record<${executionKeyType}, ExecutionSocket>;`);
480
- ts = ts.replace(/(strategyStateUpdate\??:\s*)\{\s*\[k:\s*string\]:\s*ExecutionSocket;\s*\};/g, `$1Record<${executionKeyType}, ExecutionSocket>;`);
481
- // Ensure key constraints for strategyThreadMap are preserved as template-literal identity keys.
482
- // json-schema-to-typescript emits `[k: string]: Step[];`, but we want keys to be `StrategyThreadIdentity`.
483
- ts = ts.replace(/export interface StrategyThreadMap\s*\{\s*\[k:\s*string\]:\s*Step\[\];\s*\}/g, `export type StrategyThreadMap = Record<${strategyThreadKeyType}, Step[]>;`);
484
- parts.push(ts);
485
- let output = parts.join('\n');
486
- // Final guard: strip any lingering `[k: string]: unknown;` that might have been
487
- // reintroduced by later transforms.
488
- output = output.replace(/^\s*\[k:\s*string\]:\s*unknown;\s*$/gm, '');
489
- // Cosmetic post-format: remove lone blank lines before closing braces and collapse excessive blank lines
490
- // - Remove a single blank line before `};` and `}`
491
- // - Collapse 3+ consecutive newlines into a maximum of 2
492
- output = output
493
- .replace(/\r?\n\s*\r?\n(\s*};)/g, '\n$1')
494
- .replace(/\r?\n\s*\r?\n(\s*})/g, '\n$1')
495
- .replace(/(\r?\n){3,}/g, '\n\n');
496
- // As an additional safeguard, make sure the final .d.ts is treated as a module.
497
- // If no export/interface/module is present, append an empty export.
498
- if (!/\bexport\b|\bdeclare\s+module\b|\bdeclare\s+namespace\b/.test(output)) {
499
- output += '\nexport {}\n';
500
- }
381
+ ts = postProcessEmittedTypes(ts, parsedSchema);
382
+ const output = finalizeOutputDts(ts);
501
383
  // Write only under configured types output folder
502
384
  try {
503
385
  fs.writeFileSync(srcLibOutputPath, output, 'utf8');
@@ -540,11 +422,9 @@ async function main() {
540
422
  console.warn('Failed to write types.js to dist/_lib:', e);
541
423
  }
542
424
  }
543
- finally {
544
- // No temp combined schema used.
425
+ catch (err) {
426
+ console.error(err);
427
+ process.exitCode = 1;
545
428
  }
546
429
  }
547
- main().catch((err) => {
548
- console.error(err);
549
- process.exitCode = 1;
550
- });
430
+ void main();