@toolproof-core/schema 1.0.3 → 1.0.4

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 (70) hide show
  1. package/dist/generated/normalized/Genesis.json +17 -9
  2. package/dist/generated/resources/Genesis.json +19 -11
  3. package/dist/generated/schemas/Genesis.json +15 -7
  4. package/dist/generated/schemas/standalone/RawStrategy.json +15 -7
  5. package/dist/generated/schemas/standalone/RunnableStrategy.json +15 -7
  6. package/dist/generated/schemas/standalone/StrategyRun.json +15 -7
  7. package/dist/generated/{types → typesTS}/standalone/Resource_Genesis.d.ts +1 -1
  8. package/{src/generated/types → dist/generated/typesTS}/standalone/Resource_Job.d.ts +1 -1
  9. package/dist/generated/{types → typesTS}/standalone/Resource_RawStrategy.d.ts +1 -1
  10. package/{src/generated/types → dist/generated/typesTS}/standalone/Resource_ResourceType.d.ts +1 -1
  11. package/dist/generated/{types → typesTS}/standalone/Resource_RunnableStrategy.d.ts +1 -1
  12. package/dist/generated/typesTS/typesTS.d.ts +745 -0
  13. package/dist/index.d.ts +6 -9
  14. package/dist/index.js +0 -2
  15. package/dist/scripts/_lib/config.d.ts +10 -10
  16. package/dist/scripts/_lib/config.js +23 -23
  17. package/dist/scripts/extractSchemasFromResourceTypeShells.js +109 -103
  18. package/dist/scripts/generateDependencies.js +15 -14
  19. package/dist/scripts/generateSchemaShims.js +76 -84
  20. package/dist/scripts/generateStandaloneSchema.js +47 -37
  21. package/dist/scripts/generateStandaloneTypeTS.js +108 -0
  22. package/dist/scripts/generateTypesTS.js +430 -0
  23. package/dist/scripts/normalizeAnchorsToPointers.js +37 -30
  24. package/dist/scripts/wrapResourceTypesWithResourceShells.js +14 -16
  25. package/package.json +11 -12
  26. package/src/Genesis.json +2007 -1999
  27. package/{dist/generated → src/generated/dependencies}/dependencyMap.json +8 -7
  28. package/src/generated/normalized/Genesis.json +17 -9
  29. package/src/generated/resources/Genesis.json +19 -11
  30. package/src/generated/schemas/Genesis.json +15 -7
  31. package/src/generated/schemas/standalone/RawStrategy.json +15 -7
  32. package/src/generated/schemas/standalone/RunnableStrategy.json +15 -7
  33. package/src/generated/schemas/standalone/StrategyRun.json +15 -7
  34. package/src/generated/{types → typesTS}/standalone/Resource_Genesis.d.ts +1 -1
  35. package/{dist/generated/types → src/generated/typesTS}/standalone/Resource_Job.d.ts +1 -1
  36. package/src/generated/{types → typesTS}/standalone/Resource_RawStrategy.d.ts +1 -1
  37. package/{dist/generated/types → src/generated/typesTS}/standalone/Resource_ResourceType.d.ts +1 -1
  38. package/src/generated/{types → typesTS}/standalone/Resource_RunnableStrategy.d.ts +1 -1
  39. package/src/generated/typesTS/typesTS.d.ts +745 -0
  40. package/src/index.ts +67 -93
  41. package/src/scripts/_lib/config.ts +205 -205
  42. package/src/scripts/extractSchemasFromResourceTypeShells.ts +261 -218
  43. package/src/scripts/generateDependencies.ts +121 -120
  44. package/src/scripts/generateSchemaShims.ts +127 -135
  45. package/src/scripts/generateStandaloneSchema.ts +185 -175
  46. package/src/scripts/generateStandaloneTypeTS.ts +127 -0
  47. package/src/scripts/generateTypesTS.ts +532 -0
  48. package/src/scripts/normalizeAnchorsToPointers.ts +115 -123
  49. package/src/scripts/wrapResourceTypesWithResourceShells.ts +82 -84
  50. package/dist/generated/types/types.d.ts +0 -1723
  51. package/dist/scripts/generateStandaloneType.js +0 -102
  52. package/dist/scripts/generateTypes.js +0 -550
  53. package/src/generated/dependencyMap.json +0 -292
  54. package/src/generated/types/types.d.ts +0 -1723
  55. package/src/scripts/generateStandaloneType.ts +0 -119
  56. package/src/scripts/generateTypes.ts +0 -615
  57. /package/dist/generated/{types → typesTS}/standalone/Resource_Genesis.js +0 -0
  58. /package/dist/generated/{types → typesTS}/standalone/Resource_Job.js +0 -0
  59. /package/dist/generated/{types → typesTS}/standalone/Resource_RawStrategy.js +0 -0
  60. /package/dist/generated/{types → typesTS}/standalone/Resource_ResourceType.js +0 -0
  61. /package/dist/generated/{types → typesTS}/standalone/Resource_RunnableStrategy.js +0 -0
  62. /package/dist/generated/{types/types.js → typesTS/typesTS.js} +0 -0
  63. /package/dist/scripts/{generateStandaloneType.d.ts → generateStandaloneTypeTS.d.ts} +0 -0
  64. /package/dist/scripts/{generateTypes.d.ts → generateTypesTS.d.ts} +0 -0
  65. /package/src/generated/{types → typesTS}/standalone/Resource_Genesis.js +0 -0
  66. /package/src/generated/{types → typesTS}/standalone/Resource_Job.js +0 -0
  67. /package/src/generated/{types → typesTS}/standalone/Resource_RawStrategy.js +0 -0
  68. /package/src/generated/{types → typesTS}/standalone/Resource_ResourceType.js +0 -0
  69. /package/src/generated/{types → typesTS}/standalone/Resource_RunnableStrategy.js +0 -0
  70. /package/src/generated/{types/types.js → typesTS/typesTS.js} +0 -0
@@ -1,218 +1,261 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import { fileURLToPath } from "url";
4
- import { getConfig } from "./_lib/config.js";
5
-
6
- type JSONValue = null | boolean | number | string | JSONValue[] | { [k: string]: JSONValue };
7
-
8
- interface ExtractOptions {
9
- inPath: string;
10
- outPath: string;
11
- topLevelId?: string;
12
- }
13
-
14
- function parseArgs(): ExtractOptions {
15
- const config = getConfig();
16
- const argv = process.argv.slice(2);
17
- let inPath = "";
18
- let outPath = "";
19
- let topLevelId: string | undefined;
20
- for (let i = 0; i < argv.length; i++) {
21
- const a = argv[i];
22
- if (a === "--in" && i + 1 < argv.length) inPath = argv[++i];
23
- else if (a === "--out" && i + 1 < argv.length) outPath = argv[++i];
24
- else if (a === "--id" && i + 1 < argv.length) {
25
- let v = argv[++i];
26
- // Strip accidental surrounding quotes from PowerShell/cmd
27
- if ((v.startsWith("'") && v.endsWith("'")) || (v.startsWith('"') && v.endsWith('"'))) {
28
- v = v.slice(1, -1);
29
- }
30
- topLevelId = v;
31
- }
32
- }
33
-
34
- // Use config defaults if not provided via CLI
35
- if (!inPath) {
36
- // Use generated/normalized version with anchor refs rewritten to pointers
37
- inPath = config.getNormalizedSourcePath();
38
- }
39
- if (!outPath) {
40
- outPath = config.getSchemaPath(config.getSourceFile());
41
- }
42
- if (!topLevelId) {
43
- topLevelId = config.getSchemaId('Genesis');
44
- }
45
-
46
- // Resolve to absolute paths from project root
47
- const cwd = config.getRoot();
48
- const wasInRelative = !path.isAbsolute(inPath);
49
- const wasOutRelative = !path.isAbsolute(outPath);
50
- if (wasInRelative) inPath = path.join(cwd, inPath);
51
- if (wasOutRelative) outPath = path.join(cwd, outPath);
52
- // Fallback: resolve relative to script directory if not found
53
- const scriptDir = path.dirname(fileURLToPath(import.meta.url));
54
- if (!fs.existsSync(inPath) && wasInRelative) inPath = path.resolve(scriptDir, inPath);
55
- const outDir = path.dirname(outPath);
56
- if (!fs.existsSync(outDir) && wasOutRelative) {
57
- // Try making directory relative to script dir
58
- const altOut = path.resolve(scriptDir, outPath);
59
- const altOutDir = path.dirname(altOut);
60
- if (!fs.existsSync(path.dirname(outPath))) {
61
- // Prefer creating outDir at cwd location if possible; otherwise fallback below when writing
62
- } else {
63
- outPath = altOut;
64
- }
65
- }
66
- return { inPath, outPath, topLevelId };
67
- }
68
-
69
- // ATTENTION: we don't want heuristic behavior.
70
- // Heuristic: determine if a node is a Type shell
71
- function isTypeShell(node: any): boolean {
72
- return (
73
- node && typeof node === "object" && !Array.isArray(node) &&
74
- // Treat any object that has an 'nucleusSchema' AND 'identity' as a Type shell
75
- // This prevents false positives where 'nucleusSchema' is just a regular schema property
76
- node.nucleusSchema && typeof node.nucleusSchema === "object" &&
77
- node.identity && typeof node.identity === "string"
78
- );
79
- }
80
-
81
- // Merge $defs into target, without overwriting existing keys unless identical
82
- function mergeDefs(target: Record<string, JSONValue>, source?: any, label?: string) {
83
- if (!source || typeof source !== "object") return;
84
- const src = (source as any)["$defs"];
85
- if (!src || typeof src !== "object") return;
86
- for (const [k, v] of Object.entries(src)) {
87
- if (!(k in target)) {
88
- target[k] = v as JSONValue;
89
- } else {
90
- // Best-effort: if duplicate key, require structural equality; otherwise, namespace
91
- const existing = JSON.stringify(target[k]);
92
- const incoming = JSON.stringify(v);
93
- if (existing !== incoming) {
94
- const altKey = `${k}__from_${(label || "defs").replace(/[^A-Za-z0-9_]+/g, "_")}`;
95
- if (!(altKey in target)) target[altKey] = v as JSONValue;
96
- }
97
- }
98
- }
99
- }
100
-
101
- // Deeply traverse an object replacing any Type shell with its nucleusSchema,
102
- // and hoist its inner $defs to topDefs. Prevent infinite recursion with a visited set.
103
- function unwrapTypes(node: JSONValue, topDefs: Record<string, JSONValue>, labelPath: string[] = [], visited = new Set<any>()): JSONValue {
104
- if (node && typeof node === "object") {
105
- if (visited.has(node)) return node; // avoid cycles
106
- visited.add(node);
107
- }
108
-
109
- if (isTypeShell(node)) {
110
- const env = node as any;
111
- const inner = env.nucleusSchema;
112
- // Hoist inner $defs before stripping
113
- mergeDefs(topDefs, inner, labelPath.join("_"));
114
- // Return the inner schema itself, after also unwrapping any nested shells it may contain
115
- const unwrappedInner = unwrapTypes(inner as JSONValue, topDefs, labelPath.concat([String(env.identity || "env")]), visited);
116
- return unwrappedInner;
117
- }
118
-
119
- if (Array.isArray(node)) {
120
- return node.map((v, i) => unwrapTypes(v, topDefs, labelPath.concat([String(i)]), visited)) as JSONValue;
121
- }
122
-
123
- if (node && typeof node === "object") {
124
- const out: Record<string, JSONValue> = {};
125
- for (const [k, v] of Object.entries(node)) {
126
- if (k === "$defs" && v && typeof v === "object" && !Array.isArray(v)) {
127
- // Process nested $defs: unwrap each entry value if it's a Type shell
128
- const defsOut: Record<string, JSONValue> = {};
129
- for (const [dk, dv] of Object.entries(v as any)) {
130
- const unwrapped = unwrapTypes(dv as JSONValue, topDefs, labelPath.concat(["$defs", dk]), visited);
131
- defsOut[dk] = unwrapped;
132
- }
133
- out[k] = defsOut;
134
- } else {
135
- out[k] = unwrapTypes(v as JSONValue, topDefs, labelPath.concat([k]), visited);
136
- }
137
- }
138
- return out;
139
- }
140
-
141
- return node;
142
- }
143
-
144
- /**
145
- * Pure function that takes a schema document and options, and returns the flattened schema.
146
- * Performs no I/O operations.
147
- */
148
- function extractSchemaLogic(doc: any, topLevelId?: string): any {
149
- if (!doc || typeof doc !== "object" || !doc.nucleusSchema) {
150
- throw new Error("Input must be a Type JSON with an nucleusSchema at the top level");
151
- }
152
-
153
- const topSchema = (doc as any).nucleusSchema;
154
-
155
- // Collect $defs so that any '#/$defs/...' pointers can be resolved from the root.
156
- const outDefs: Record<string, JSONValue> = {};
157
- // Seed with top-level $defs (if any) before unwrapping
158
- mergeDefs(outDefs, topSchema, "top");
159
-
160
- // Unwrap the entire top schema tree so that any nested Type shells become raw schemas
161
- const flattened = unwrapTypes(topSchema as JSONValue, outDefs, ["nucleusSchema"]);
162
-
163
- // Assemble output: force $schema, optionally set $id, hoist collected $defs
164
- let base: any;
165
- if (flattened && typeof flattened === "object" && !Array.isArray(flattened)) {
166
- base = { ...(flattened as any) };
167
- } else {
168
- // If flattened is not an object (should be rare for a top-level schema), wrap it
169
- base = { const: flattened };
170
- }
171
- // Assemble, but avoid duplicating $id: if the flattened base already has $id, prefer it.
172
- const output: Record<string, JSONValue> = {
173
- $schema: "https://json-schema.org/draft/2020-12/schema",
174
- ...base,
175
- };
176
- if (topLevelId && !(output as any).$id) {
177
- (output as any).$id = topLevelId;
178
- }
179
-
180
- // Enforce presence of $id: schema must declare an absolute identity.
181
- if (!(output as any).$id) {
182
- throw new Error(
183
- "Flattened schema must define $id. Provide it via CLI --id or include $id in the source nucleusSchema."
184
- );
185
- }
186
-
187
- // Hoist collected defs into output.$defs, taking care not to clobber any existing
188
- if (!("$defs" in output)) output.$defs = {} as any;
189
- const finalDefs: Record<string, JSONValue> = (output.$defs as any) || {};
190
- for (const [k, v] of Object.entries(outDefs)) {
191
- if (!(k in finalDefs)) finalDefs[k] = v;
192
- }
193
- output.$defs = finalDefs as any;
194
-
195
- // Preserve natural key ordering (do not reorder for readability)
196
- return output;
197
- }
198
-
199
- function main() {
200
- const { inPath, outPath, topLevelId } = parseArgs();
201
-
202
- if (!fs.existsSync(inPath)) {
203
- console.error(`Input file not found at ${inPath}`);
204
- process.exit(1);
205
- }
206
-
207
- const raw = fs.readFileSync(inPath, "utf8");
208
- const doc = JSON.parse(raw);
209
-
210
- // Core logic is now in a pure function
211
- const ordered = extractSchemaLogic(doc, topLevelId);
212
-
213
- fs.mkdirSync(path.dirname(outPath), { recursive: true });
214
- fs.writeFileSync(outPath, JSON.stringify(ordered, null, 4), "utf8");
215
- console.log(`Wrote flattened schema to ${outPath}`);
216
- }
217
-
218
- main();
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { getConfig } from "./_lib/config.js";
4
+
5
+ type JSONValue = null | boolean | number | string | JSONValue[] | { [k: string]: JSONValue };
6
+
7
+ interface ExtractOptions {
8
+ inPath: string;
9
+ outPath: string;
10
+ topLevelId?: string;
11
+ }
12
+
13
+ interface ExtractCliArgs {
14
+ inPath?: string;
15
+ outPath?: string;
16
+ topLevelId?: string;
17
+ }
18
+
19
+ interface TraverseResult {
20
+ value: JSONValue;
21
+ hoistEvents: Array<{ defs: Record<string, JSONValue>; label: string }>;
22
+ }
23
+
24
+ interface ExtractDefaults {
25
+ inPath: string;
26
+ outPath: string;
27
+ topLevelId: string;
28
+ }
29
+
30
+ // PURE: Parse CLI args (no defaults, no filesystem probing).
31
+ function parseArgs(argv: string[]): ExtractCliArgs {
32
+ let inPath: string | undefined;
33
+ let outPath: string | undefined;
34
+ let topLevelId: string | undefined;
35
+
36
+ for (let i = 0; i < argv.length; i++) {
37
+ const a = argv[i];
38
+ if (a === "--in" && i + 1 < argv.length) inPath = argv[++i];
39
+ else if (a === "--out" && i + 1 < argv.length) outPath = argv[++i];
40
+ else if (a === "--id" && i + 1 < argv.length) {
41
+ let v = argv[++i];
42
+ // Strip accidental surrounding quotes from PowerShell/cmd
43
+ if ((v.startsWith("'") && v.endsWith("'")) || (v.startsWith('"') && v.endsWith('"'))) {
44
+ v = v.slice(1, -1);
45
+ }
46
+ topLevelId = v;
47
+ }
48
+ }
49
+
50
+ return { inPath, outPath, topLevelId };
51
+ }
52
+
53
+ // PURE: Apply defaults for any missing values.
54
+ function applyDefaults(args: ExtractCliArgs, defaults: ExtractDefaults): ExtractOptions {
55
+ return {
56
+ inPath: args.inPath?.trim() ? args.inPath : defaults.inPath,
57
+ outPath: args.outPath?.trim() ? args.outPath : defaults.outPath,
58
+ topLevelId: args.topLevelId?.trim() ? args.topLevelId : defaults.topLevelId
59
+ };
60
+ }
61
+
62
+ // PURE: Resolve relative paths against a root directory (no heuristics).
63
+ function resolvePaths(options: ExtractOptions, rootDir: string): ExtractOptions {
64
+ const inPath = path.isAbsolute(options.inPath) ? options.inPath : path.join(rootDir, options.inPath);
65
+ const outPath = path.isAbsolute(options.outPath) ? options.outPath : path.join(rootDir, options.outPath);
66
+ return { ...options, inPath, outPath };
67
+ }
68
+
69
+ // PURE: Determine whether a node is a Type shell.
70
+ function isTypeShell(node: any): boolean {
71
+ return (
72
+ node && typeof node === "object" && !Array.isArray(node) &&
73
+ // Treat any object that has an 'nucleusSchema' AND 'identity' as a Type shell
74
+ // This prevents false positives where 'nucleusSchema' is just a regular schema property
75
+ node.nucleusSchema && typeof node.nucleusSchema === "object" &&
76
+ node.identity && typeof node.identity === "string"
77
+ );
78
+ }
79
+
80
+ // PURE: Sanitize labels for use in deterministic collision keys.
81
+ function sanitizeLabel(label: string | undefined): string {
82
+ return (label || "defs").replace(/[^A-Za-z0-9_]+/g, "_");
83
+ }
84
+
85
+ // PURE: Extract a `$defs` object from a schema node, if present.
86
+ function getDefsObject(source: any): Record<string, JSONValue> | undefined {
87
+ if (!source || typeof source !== "object") return undefined;
88
+ const src = (source as any)["$defs"];
89
+ if (!src || typeof src !== "object" || Array.isArray(src)) return undefined;
90
+ return src as Record<string, JSONValue>;
91
+ }
92
+
93
+ // PURE: Merge defs without mutating inputs; collisions are namespaced deterministically.
94
+ function mergeDefs(
95
+ existing: Record<string, JSONValue>,
96
+ incoming: Record<string, JSONValue> | undefined,
97
+ label?: string
98
+ ): Record<string, JSONValue> {
99
+ if (!incoming) return { ...existing };
100
+
101
+ const merged: Record<string, JSONValue> = { ...existing };
102
+ for (const [k, v] of Object.entries(incoming)) {
103
+ if (!(k in merged)) {
104
+ merged[k] = v;
105
+ continue;
106
+ }
107
+
108
+ const current = merged[k];
109
+ const same = JSON.stringify(current) === JSON.stringify(v);
110
+ if (same) continue;
111
+
112
+ const altKey = `${k}__from_${sanitizeLabel(label)}`;
113
+ if (!(altKey in merged)) merged[altKey] = v;
114
+ }
115
+ return merged;
116
+ }
117
+
118
+ // PURE: Unwrap Type shells and collect any encountered inner `$defs` hoist events.
119
+ function traverseTypes(node: JSONValue, labelPath: string[] = []): TraverseResult {
120
+ if (isTypeShell(node)) {
121
+ const env = node as any;
122
+ const inner = env.nucleusSchema as JSONValue;
123
+ const label = labelPath.join("_");
124
+
125
+ const innerDefs = getDefsObject(inner);
126
+ const innerResult = traverseTypes(inner, labelPath.concat([String(env.identity || "env")]));
127
+
128
+ const events: TraverseResult["hoistEvents"] = [];
129
+ if (innerDefs) events.push({ defs: innerDefs, label });
130
+ events.push(...innerResult.hoistEvents);
131
+
132
+ return { value: innerResult.value, hoistEvents: events };
133
+ }
134
+
135
+ if (Array.isArray(node)) {
136
+ const events: TraverseResult["hoistEvents"] = [];
137
+ const mapped = node.map((v, i) => {
138
+ const r = traverseTypes(v, labelPath.concat([String(i)]));
139
+ events.push(...r.hoistEvents);
140
+ return r.value;
141
+ }) as JSONValue;
142
+ return { value: mapped, hoistEvents: events };
143
+ }
144
+
145
+ if (node && typeof node === "object") {
146
+ const events: TraverseResult["hoistEvents"] = [];
147
+ const out: Record<string, JSONValue> = {};
148
+ for (const [k, v] of Object.entries(node)) {
149
+ if (k === "$defs" && v && typeof v === "object" && !Array.isArray(v)) {
150
+ const defsOut: Record<string, JSONValue> = {};
151
+ for (const [dk, dv] of Object.entries(v as any)) {
152
+ const r = traverseTypes(dv as JSONValue, labelPath.concat(["$defs", dk]));
153
+ events.push(...r.hoistEvents);
154
+ defsOut[dk] = r.value;
155
+ }
156
+ out[k] = defsOut;
157
+ continue;
158
+ }
159
+
160
+ const r = traverseTypes(v as JSONValue, labelPath.concat([k]));
161
+ events.push(...r.hoistEvents);
162
+ out[k] = r.value;
163
+ }
164
+ return { value: out, hoistEvents: events };
165
+ }
166
+
167
+ return { value: node, hoistEvents: [] };
168
+ }
169
+
170
+
171
+ // PURE: Flatten a Type shell JSON into a single JSON Schema document.
172
+ function extractSchemaLogic(doc: any, topLevelId?: string): any {
173
+ if (!doc || typeof doc !== "object" || !doc.nucleusSchema) {
174
+ throw new Error("Input must be a Type JSON with an nucleusSchema at the top level");
175
+ }
176
+
177
+ const topSchema = (doc as any).nucleusSchema;
178
+
179
+ // Collect $defs so that any '#/$defs/...' pointers can be resolved from the root.
180
+ const seededDefs = mergeDefs({}, getDefsObject(topSchema), "top");
181
+
182
+ // Unwrap the entire top schema tree so that any nested Type shells become raw schemas
183
+ const traversal = traverseTypes(topSchema as JSONValue, ["nucleusSchema"]);
184
+ const flattened = traversal.value;
185
+
186
+ let outDefs = seededDefs;
187
+ for (const ev of traversal.hoistEvents) {
188
+ outDefs = mergeDefs(outDefs, ev.defs, ev.label);
189
+ }
190
+
191
+ // Assemble output: force $schema, optionally set $id, hoist collected $defs
192
+ let base: any;
193
+ if (flattened && typeof flattened === "object" && !Array.isArray(flattened)) {
194
+ base = { ...(flattened as any) };
195
+ } else {
196
+ // If flattened is not an object (should be rare for a top-level schema), wrap it
197
+ base = { const: flattened };
198
+ }
199
+ // Assemble, but avoid duplicating $id: if the flattened base already has $id, prefer it.
200
+ const output: Record<string, JSONValue> = {
201
+ $schema: "https://json-schema.org/draft/2020-12/schema",
202
+ ...base,
203
+ };
204
+ if (topLevelId && !(output as any).$id) {
205
+ (output as any).$id = topLevelId;
206
+ }
207
+
208
+ // Enforce presence of $id: schema must declare an absolute identity.
209
+ if (!(output as any).$id) {
210
+ throw new Error(
211
+ "Flattened schema must define $id. Provide it via CLI --id or include $id in the source nucleusSchema."
212
+ );
213
+ }
214
+
215
+ // Hoist collected defs into output.$defs, taking care not to clobber any existing
216
+ if (!("$defs" in output)) output.$defs = {} as any;
217
+ const finalDefs: Record<string, JSONValue> = (output.$defs as any) || {};
218
+ for (const [k, v] of Object.entries(outDefs)) {
219
+ if (!(k in finalDefs)) finalDefs[k] = v;
220
+ }
221
+ output.$defs = finalDefs as any;
222
+
223
+ // Preserve natural key ordering (do not reorder for readability)
224
+ return output;
225
+ }
226
+
227
+ // IMPURE: Script entrypoint (config + filesystem I/O + console + process exit code).
228
+ function main() {
229
+ try {
230
+ const config = getConfig();
231
+ const defaults: ExtractDefaults = {
232
+ // Use generated/normalized version with anchor refs rewritten to pointers
233
+ inPath: config.getNormalizedSourcePath(),
234
+ outPath: config.getSchemaPath(config.getSourceFile()),
235
+ topLevelId: config.getSchemaId("Genesis")
236
+ };
237
+
238
+ const cli = parseArgs(process.argv.slice(2));
239
+ const opts = resolvePaths(applyDefaults(cli, defaults), config.getRoot());
240
+ const { inPath, outPath, topLevelId } = opts;
241
+
242
+ if (!fs.existsSync(inPath)) {
243
+ throw new Error(`Input file not found at ${inPath}`);
244
+ }
245
+
246
+ const raw = fs.readFileSync(inPath, "utf8");
247
+ const doc = JSON.parse(raw);
248
+
249
+ // Core logic is now in a pure function
250
+ const extracted = extractSchemaLogic(doc, topLevelId);
251
+
252
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
253
+ fs.writeFileSync(outPath, JSON.stringify(extracted, null, 4), "utf8");
254
+ console.log(`Wrote flattened schema to ${outPath}`);
255
+ } catch (e) {
256
+ console.error(e);
257
+ process.exitCode = 1;
258
+ }
259
+ }
260
+
261
+ main();