@toolproof-core/schema 1.0.9 → 1.0.10
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/dist/generated/artifacts/constants.d.ts +120 -0
- package/dist/generated/artifacts/constants.js +120 -0
- package/dist/generated/artifacts/mappings.d.ts +23 -0
- package/dist/generated/artifacts/mappings.js +23 -0
- package/dist/generated/normalized/Genesis.json +67 -53
- package/dist/generated/resources/Genesis.json +424 -236
- package/dist/generated/schemas/Genesis.json +56 -42
- package/dist/generated/schemas/standalone/Job.json +2 -0
- package/dist/generated/schemas/standalone/RawStrategy.json +86 -110
- package/dist/generated/schemas/standalone/RunnableStrategy.json +108 -132
- package/dist/generated/schemas/standalone/StrategyRun.json +86 -110
- package/dist/generated/types/types.d.ts +34 -34
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -2
- package/dist/scripts/_lib/config.d.ts +3 -5
- package/dist/scripts/_lib/config.js +8 -14
- package/dist/scripts/generateConstantsAndMappings.d.ts +31 -0
- package/dist/scripts/generateConstantsAndMappings.js +243 -0
- package/dist/scripts/generateDependencies.js +1 -1
- package/dist/scripts/generateTerminals.js +2 -2
- package/dist/scripts/generateTypes.js +47 -2
- package/dist/scripts/wrapResourceTypesWithResourceShells.js +7 -3
- package/package.json +9 -10
- package/src/Genesis.json +1847 -1833
- package/src/generated/artifacts/constants.ts +121 -0
- package/src/generated/{dependencies → artifacts}/dependencyMap.json +16 -18
- package/src/generated/artifacts/mappings.ts +24 -0
- package/src/generated/{dependencies → artifacts}/terminals.json +1 -0
- package/src/generated/normalized/Genesis.json +1760 -1746
- package/src/generated/resources/Genesis.json +2796 -2608
- package/src/generated/schemas/Genesis.json +1329 -1315
- package/src/generated/schemas/standalone/Job.json +2 -0
- package/src/generated/schemas/standalone/RawStrategy.json +580 -604
- package/src/generated/schemas/standalone/RunnableStrategy.json +645 -669
- package/src/generated/schemas/standalone/StrategyRun.json +913 -937
- package/src/generated/types/types.d.ts +709 -709
- package/src/index.ts +75 -70
- package/src/scripts/_lib/config.ts +207 -215
- package/src/scripts/extractSchemasFromResourceTypeShells.ts +261 -261
- package/src/scripts/generateConstantsAndMappings.ts +309 -0
- package/src/scripts/generateDependencies.ts +121 -121
- package/src/scripts/generateSchemaShims.ts +127 -127
- package/src/scripts/generateStandaloneSchema.ts +185 -185
- package/src/scripts/generateStandaloneType.ts +127 -127
- package/src/scripts/generateTerminals.ts +73 -73
- package/src/scripts/generateTypes.ts +587 -531
- package/src/scripts/normalizeAnchorsToPointers.ts +141 -141
- package/src/scripts/wrapResourceTypesWithResourceShells.ts +86 -82
- package/dist/generated/constants/constants.d.ts +0 -60
- package/dist/generated/constants/constants.js +0 -60
- package/dist/scripts/generateConstants.d.ts +0 -12
- package/dist/scripts/generateConstants.js +0 -179
- package/src/generated/constants/constants.ts +0 -61
- package/src/scripts/generateConstants.ts +0 -217
|
@@ -1,261 +1,261 @@
|
|
|
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();
|
|
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();
|