@toolproof-core/schema 1.0.0 → 1.0.1

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 (78) hide show
  1. package/dist/generated/types/standalone/Resource_Genesis.d.ts +3 -0
  2. package/dist/generated/types/standalone/Resource_Job.d.ts +3 -0
  3. package/dist/generated/types/standalone/Resource_RawStrategy.d.ts +3 -0
  4. package/dist/generated/types/standalone/Resource_ResourceType.d.ts +3 -0
  5. package/dist/generated/types/standalone/Resource_RunnableStrategy.d.ts +3 -0
  6. package/dist/generated/types/types.d.ts +487 -548
  7. package/dist/scripts/_lib/config.d.ts +12 -6
  8. package/dist/scripts/_lib/config.js +33 -15
  9. package/dist/scripts/{extractSchemas.js → extractSchemasFromResourceTypeShells.js} +14 -27
  10. package/dist/scripts/generateDependencies.js +2 -2
  11. package/dist/scripts/generateSchemaShims.js +22 -4
  12. package/dist/scripts/{extractSubSchemaWithDefs.js → generateStandaloneSchema.js} +12 -34
  13. package/dist/scripts/{generateResourceTypeType.js → generateStandaloneType.js} +23 -14
  14. package/dist/scripts/generateTypes.js +1 -1
  15. package/dist/scripts/{rewriteAnchors.js → normalizeAnchorsToPointers.js} +2 -2
  16. package/dist/scripts/{generateResourceShells.js → wrapResourceTypesWithResourceShells.js} +12 -34
  17. package/package.json +8 -8
  18. package/src/Genesis.json +44 -87
  19. package/src/generated/{dependencies.json → dependencyMap.json} +207 -214
  20. package/src/generated/{resourceTypes → normalized}/Genesis.json +44 -87
  21. package/src/generated/resources/Genesis.json +415 -478
  22. package/src/generated/schemas/Genesis.json +1026 -1057
  23. package/src/generated/schemas/{Goal.json → standalone/Goal.json} +34 -34
  24. package/src/generated/schemas/{Job.json → standalone/Job.json} +85 -85
  25. package/src/generated/schemas/{RawStrategy.json → standalone/RawStrategy.json} +238 -263
  26. package/src/generated/schemas/{ResourceType.json → standalone/ResourceType.json} +60 -60
  27. package/src/generated/schemas/{RunnableStrategy.json → standalone/RunnableStrategy.json} +296 -321
  28. package/src/generated/schemas/{StrategyRun.json → standalone/StrategyRun.json} +390 -421
  29. package/src/generated/types/standalone/Resource_Genesis.d.ts +3 -0
  30. package/src/generated/types/standalone/Resource_Job.d.ts +3 -0
  31. package/src/generated/types/standalone/Resource_RawStrategy.d.ts +3 -0
  32. package/src/generated/types/standalone/Resource_ResourceType.d.ts +3 -0
  33. package/src/generated/types/standalone/Resource_RunnableStrategy.d.ts +3 -0
  34. package/src/generated/types/types.d.ts +487 -548
  35. package/src/generated/types/types.js +1 -1
  36. package/src/scripts/_lib/config.ts +42 -18
  37. package/src/scripts/extractSchemasFromResourceTypeShells.ts +218 -0
  38. package/src/scripts/generateDependencies.ts +2 -2
  39. package/src/scripts/generateSchemaShims.ts +24 -4
  40. package/src/scripts/{extractSubSchemaWithDefs.ts → generateStandaloneSchema.ts} +13 -34
  41. package/src/scripts/{generateResourceTypeType.ts → generateStandaloneType.ts} +23 -14
  42. package/src/scripts/generateTypes.ts +1 -1
  43. package/src/scripts/{rewriteAnchors.ts → normalizeAnchorsToPointers.ts} +3 -3
  44. package/src/scripts/{generateResourceShells.ts → wrapResourceTypesWithResourceShells.ts} +13 -34
  45. package/dist/generated/types/Resource_Genesis.d.ts +0 -3
  46. package/dist/generated/types/Resource_Job.d.ts +0 -3
  47. package/dist/generated/types/Resource_RawStrategy.d.ts +0 -3
  48. package/dist/generated/types/Resource_ResourceType.d.ts +0 -3
  49. package/dist/generated/types/Resource_RunnableStrategy.d.ts +0 -3
  50. package/src/Roadmap.json +0 -102
  51. package/src/generated/types/Resource_Genesis.d.ts +0 -3
  52. package/src/generated/types/Resource_Job.d.ts +0 -3
  53. package/src/generated/types/Resource_RawStrategy.d.ts +0 -3
  54. package/src/generated/types/Resource_ResourceType.d.ts +0 -3
  55. package/src/generated/types/Resource_RunnableStrategy.d.ts +0 -3
  56. package/src/scripts/extractSchemas.ts +0 -229
  57. /package/dist/generated/types/{Resource_Genesis.js → standalone/Resource_Genesis.js} +0 -0
  58. /package/dist/generated/types/{Resource_Job.js → standalone/Resource_Job.js} +0 -0
  59. /package/dist/generated/types/{Resource_RawStrategy.js → standalone/Resource_RawStrategy.js} +0 -0
  60. /package/dist/generated/types/{Resource_ResourceType.js → standalone/Resource_ResourceType.js} +0 -0
  61. /package/dist/generated/types/{Resource_RunnableStrategy.js → standalone/Resource_RunnableStrategy.js} +0 -0
  62. /package/dist/scripts/{extractSchemas.d.ts → extractSchemasFromResourceTypeShells.d.ts} +0 -0
  63. /package/dist/scripts/{extractSubSchemaWithDefs.d.ts → generateStandaloneSchema.d.ts} +0 -0
  64. /package/dist/scripts/{generateResourceShells.d.ts → generateStandaloneType.d.ts} +0 -0
  65. /package/dist/scripts/{generateResourceTypeType.d.ts → normalizeAnchorsToPointers.d.ts} +0 -0
  66. /package/dist/scripts/{rewriteAnchors.d.ts → wrapResourceTypesWithResourceShells.d.ts} +0 -0
  67. /package/src/generated/{resourceTypes → normalized}/Genesis.ts +0 -0
  68. /package/src/generated/schemas/{Goal.ts → standalone/Goal.ts} +0 -0
  69. /package/src/generated/schemas/{Job.ts → standalone/Job.ts} +0 -0
  70. /package/src/generated/schemas/{RawStrategy.ts → standalone/RawStrategy.ts} +0 -0
  71. /package/src/generated/schemas/{ResourceType.ts → standalone/ResourceType.ts} +0 -0
  72. /package/src/generated/schemas/{RunnableStrategy.ts → standalone/RunnableStrategy.ts} +0 -0
  73. /package/src/generated/schemas/{StrategyRun.ts → standalone/StrategyRun.ts} +0 -0
  74. /package/src/generated/types/{Resource_Genesis.js → standalone/Resource_Genesis.js} +0 -0
  75. /package/src/generated/types/{Resource_Job.js → standalone/Resource_Job.js} +0 -0
  76. /package/src/generated/types/{Resource_RawStrategy.js → standalone/Resource_RawStrategy.js} +0 -0
  77. /package/src/generated/types/{Resource_ResourceType.js → standalone/Resource_ResourceType.js} +0 -0
  78. /package/src/generated/types/{Resource_RunnableStrategy.js → standalone/Resource_RunnableStrategy.js} +0 -0
@@ -3,26 +3,11 @@ import path from 'path';
3
3
  import { getConfig } from './_lib/config.js';
4
4
 
5
5
  /**
6
- * Generate Resource envelopes for all ResourceTypes defined in Genesis.json
7
- *
8
- * This script wraps each ResourceType definition (from $defs) with a Resource envelope
6
+ * Wrap each ResourceType definition (from Genesis.nucleusSchema.$defs) with a Resource shell
9
7
  * that conforms to the Resource.nucleusSchema pattern defined in Genesis.json.
10
- *
11
- * For the top-level Genesis ResourceType, extractedData is set to {} to avoid
12
- * duplicating the entire $defs object.
13
- *
14
- * Resource identities follow the pattern RESOURCE-{Name} where Name is the key
15
- * from the $defs object. Genesis itself uses RESOURCE-Genesis.
16
- *
17
- * Usage: node ./dist/scripts/generateResourceEnvelopes.js
18
8
  */
19
- /**
20
- * Pure function to generate resource envelopes from a Genesis schema.
21
- *
22
- * @param genesis The Genesis schema object
23
- * @returns A record mapping resource names to their envelopes
24
- */
25
- function generateResourceEnvelopesLogic(genesis: any): Record<string, any> {
9
+
10
+ function generateResourceShellLogic(genesis: any): Record<string, any> {
26
11
  if (!genesis.nucleusSchema || !genesis.nucleusSchema.$defs) {
27
12
  throw new Error('Genesis.json must have nucleusSchema.$defs');
28
13
  }
@@ -30,39 +15,36 @@ function generateResourceEnvelopesLogic(genesis: any): Record<string, any> {
30
15
  const defs = genesis.nucleusSchema.$defs;
31
16
  const defKeys = Object.keys(defs);
32
17
 
33
- // Generate Resource envelopes
34
18
  const resources: Record<string, any> = {};
35
19
 
36
20
  // Genesis timestamp: 2025-11-30T00:00:00.000Z marks the genesis of ToolProof
37
21
  const genesisTimestamp = '2025-11-30T00:00:00.000Z';
38
22
 
39
- // First entry is Genesis itself with empty extractedData
40
23
  resources['Genesis'] = {
41
24
  identity: 'RESOURCE-Genesis',
42
- resourceTypeRef: 'TYPE-ResourceType',
25
+ resourceTypeHandle: 'TYPE-ResourceType',
43
26
  creationContext: {
44
- resourceRoleRef: 'ROLE-Genesis',
45
- executionRef: 'EXECUTION-Genesis'
27
+ resourceRoleHandle: 'ROLE-Genesis',
28
+ jobStepHandle: 'JOB_STEP-Genesis'
46
29
  },
47
30
  kind: 'materialized',
48
31
  timestamp: genesisTimestamp,
49
- extractedData: {}
32
+ nucleus: {}
50
33
  };
51
34
 
52
- // Generate resources for all other $defs
53
35
  defKeys.forEach((defName) => {
54
36
  const defValue = defs[defName];
55
37
 
56
38
  resources[defName] = {
57
39
  identity: `RESOURCE-${defName}`,
58
- resourceTypeRef: 'TYPE-ResourceType',
40
+ resourceTypeHandle: 'TYPE-ResourceType',
59
41
  creationContext: {
60
- resourceRoleRef: 'ROLE-Genesis',
61
- executionRef: `EXECUTION-${defName}`
42
+ resourceRoleHandle: 'ROLE-Genesis',
43
+ jobStepHandle: `JOB_STEP-${defName}`
62
44
  },
63
45
  kind: 'materialized',
64
46
  timestamp: genesisTimestamp,
65
- extractedData: defValue
47
+ nucleus: defValue
66
48
  };
67
49
  });
68
50
 
@@ -71,9 +53,8 @@ function generateResourceEnvelopesLogic(genesis: any): Record<string, any> {
71
53
 
72
54
  async function main() {
73
55
  const config = getConfig();
74
- // Use normalized version with anchor refs rewritten to pointers
75
56
  const genesisSourcePath = config.getNormalizedSourcePath();
76
- const outputPath = path.join(config.getGeneratedResourcesDir(), 'Genesis.json');
57
+ const outputPath = path.join(config.getResourcesDir(), 'Genesis.json');
77
58
 
78
59
  if (!fs.existsSync(genesisSourcePath)) {
79
60
  console.error(`Genesis source file not found at ${genesisSourcePath}`);
@@ -84,13 +65,11 @@ async function main() {
84
65
  const genesis = JSON.parse(raw);
85
66
 
86
67
  try {
87
- const resources = generateResourceEnvelopesLogic(genesis);
68
+ const resources = generateResourceShellLogic(genesis);
88
69
 
89
- // Ensure output directory exists
90
70
  const outputDir = path.dirname(outputPath);
91
71
  fs.mkdirSync(outputDir, { recursive: true });
92
72
 
93
- // Write the generated resources file
94
73
  fs.writeFileSync(outputPath, JSON.stringify(resources, null, 4) + '\n', 'utf-8');
95
74
  console.log(`Generated ${Object.keys(resources).length} Resource envelopes -> ${outputPath}`);
96
75
  } catch (error: any) {
@@ -1,3 +0,0 @@
1
- // Auto-generated strict composite type. Do not edit.
2
- import type { ResourceMetaBase, Genesis as ExtractedData } from './types.js';
3
- export type Resource_Genesis = ResourceMetaBase & { extractedData: ExtractedData };
@@ -1,3 +0,0 @@
1
- // Auto-generated strict composite type. Do not edit.
2
- import type { ResourceMetaBase, Job as ExtractedData } from './types.js';
3
- export type Resource_Job = ResourceMetaBase & { extractedData: ExtractedData };
@@ -1,3 +0,0 @@
1
- // Auto-generated strict composite type. Do not edit.
2
- import type { ResourceMetaBase, RawStrategy as ExtractedData } from './types.js';
3
- export type Resource_RawStrategy = ResourceMetaBase & { extractedData: ExtractedData };
@@ -1,3 +0,0 @@
1
- // Auto-generated strict composite type. Do not edit.
2
- import type { ResourceMetaBase, ResourceType as ExtractedData } from './types.js';
3
- export type Resource_ResourceType = ResourceMetaBase & { extractedData: ExtractedData };
@@ -1,3 +0,0 @@
1
- // Auto-generated strict composite type. Do not edit.
2
- import type { ResourceMetaBase, RunnableStrategy as ExtractedData } from './types.js';
3
- export type Resource_RunnableStrategy = ResourceMetaBase & { extractedData: ExtractedData };
package/src/Roadmap.json DELETED
@@ -1,102 +0,0 @@
1
- {
2
- "Tasks": [
3
- {
4
- "name": "Familiarization",
5
- "comment": "Familiarize yourself with the new schema structure and the changes made. Consult the 'Notes' section for specific changes in naming and structure. This will help you understand how to adapt the existing codebase to work with the new schema. Prepare a list of any questions or clarifications needed regarding the new schema structure and we'll discuss them."
6
- },
7
- {
8
- "name": "Scripts",
9
- "comment": "Implement the scripts specified in the 'Scripts' section so that they work with the new schema structure. Try to do it with business logic contained in reusable pure functions as much as possible, and with clear separation between this and file I/O. In the future, we might let Ajv take over some of this work since it already has a lot of this functionality built-in (dereferencing, $defs bundling, etc.)."
10
- },
11
- {
12
- "name": "NPM Publishing",
13
- "comment": "Publish the schema package to the new npm organization 'toolproof-core' (I have sent you an invite). Take a pull of the latest master and merge it into your branch. For schema/package.json, keep my 'name' change. The package is now called '@toolproof-core/schema' instead of '@toolproof-npm/schema'. The reason for this change is that we can now name all core packages with the '@toolproof-core' prefix, regardless of whether they are published to npm or not. I've also done one change in Genesis.json. ShellMaterializedBase now has a required 'version' property with const value 1. This should not cause any merge conflicts since you've only done changes inside scripts/ and generated/ (in addition to package.json). The only thing that needs to be changed in scripts/ is the generateResourceShells script to add the version property (with value 1)when wrapping ResourceTypes in a ShellMaterialized. Note that while we call the new ToolProof version 'v2' during development, it will officially be called 'v1'. The old version (current 'v1') will be degraded to 'v0' and deprecated. This aligns with our npm package naming, where we've till now given the packages a '0.x.x' version. In the new npm organization 'toolproof-core', we will start with version '1.0.0' for all packages. Note that I've also updated other packages (console, engine, validation, visualization), but that should not cause any merge conflicts."
14
- },
15
- {
16
- "name": "GitHub Actions",
17
- "comment": "Create a GitHub Actions workflow for the schema package (like we have in the old ToolProof repository). We'll create actions for other packages later."
18
- }
19
- ],
20
- "Notes": [
21
- {
22
- "oldName": "-Wrapper",
23
- "newName": "-Facet",
24
- "comment": "The 'Wrapper' suffix is changed to 'Facet' everywhere."
25
- },
26
- {
27
- "oldName": "Name&DescriptionWrapper",
28
- "newName": "DocumentationFacet"
29
- },
30
- {
31
- "oldName": "extractionSchema",
32
- "newName": "nucleusSchema",
33
- "comment": "This is now a maximum-permissive 'true' schema. We'll validate user-written schemas in a much better way separately."
34
- },
35
- {
36
- "oldName": "extractedData",
37
- "newName": "nucleus"
38
- },
39
- {
40
- "oldName": "Execution",
41
- "newName": "JobRun"
42
- },
43
- {
44
- "oldName": "WorkStep",
45
- "newName": "JobStep"
46
- },
47
- {
48
- "oldName": "ResourceMeta",
49
- "newName": "ShellMaterialized"
50
- },
51
- {
52
- "oldName": "StatelessStrategy",
53
- "newName": "StepsFacet",
54
- "comment": "This is a different concept. We no longer have the concept of Stateless vs Stateful strategies. We'll rather allow RunnableStrategies to be reused with new bindings."
55
- },
56
- {
57
- "oldName": "StatefulStrategy",
58
- "newName": "RawStrategy"
59
- },
60
- {
61
- "oldName": "RunRecording",
62
- "newName": "StrategyRun"
63
- }
64
- ],
65
- "Scripts": {
66
- "rewriteAnchors": {
67
- "newName": "rewriteAnchors",
68
- "comment": "Rewrites anchor-style refs (e.g. #SomeAnchor) inside Genesis.json’s nucleusSchema to JSON Pointer refs (e.g. #/$defs/SomeAnchor) for stricter tooling compatibility. Outputs normalized Genesis.json written to the configured location.",
69
- "status": "Done, In PR"
70
- },
71
- "extractSchemas": {
72
- "newName": "extractSchemas",
73
- "comment": "Takes the (normalized) Genesis.json document (expects a top-level nucleusSchema), unwraps nested ResourceTypes into raw JSON Schemas. Outputs a flattened Genesis.json at the configured location.",
74
- "status": "Done, In PR"
75
- },
76
- "extractSubSchemaWithDefs": {
77
- "newName": "extractSubSchemaWithDefs",
78
- "comment": "Extracts a single $defs.<Name> from the flattened Genesis.json and bundles it into a standalone schema including all direct + transitive local $defs dependencies referenced via #/$defs/...",
79
- "status": "Done, In PR"
80
- },
81
- "generateSchemaShims": {
82
- "newName": "generateSchemaShims",
83
- "comment": "Generates .ts shim files next to generated .json files so TypeScript/NodeNext can import JSON schemas/resources via import assertions consistently."
84
- },
85
- "generateResourceEnvelopes": {
86
- "newName": "generateResourceShells",
87
- "comment": "Wraps every Genesis.nucleusSchema.$defs[DefName] schema in a Resource shell (ToolProof resource object). Special-cases Genesis with nucleus: {} to avoid duplicating all $defs."
88
- },
89
- "generateTypes": {
90
- "newName": "generateTypes",
91
- "comment": "Runs json-schema-to-typescript across the extracted schemas directory, with a custom resolver so absolute $id URLs map to local files (no network). Applies a few normalizations (e.g., allOf sibling keyword handling), prunes noisy generated names, and overlays template-literal types for ...Identity/...Ref patterns when possible. Ensures runtime .js shims exist alongside .d.ts. Note that some of this additional processing might be unnecessary or might not fit the new schema structure."
92
- },
93
- "generateResourceTypeType": {
94
- "newName": "generateResourceTypeType",
95
- "comment": "Generates a typed Resource variant Resource_<Name> where nucleus is typed to the extracted nucleusSchema <Name>.json."
96
- },
97
- "generateDependencies": {
98
- "newName": "generateDependencies",
99
- "comment": "Generates a list of dependencies for each schema."
100
- }
101
- }
102
- }
@@ -1,3 +0,0 @@
1
- // Auto-generated strict composite type. Do not edit.
2
- import type { ResourceMetaBase, Genesis as ExtractedData } from './types.js';
3
- export type Resource_Genesis = ResourceMetaBase & { extractedData: ExtractedData };
@@ -1,3 +0,0 @@
1
- // Auto-generated strict composite type. Do not edit.
2
- import type { ResourceMetaBase, Job as ExtractedData } from './types.js';
3
- export type Resource_Job = ResourceMetaBase & { extractedData: ExtractedData };
@@ -1,3 +0,0 @@
1
- // Auto-generated strict composite type. Do not edit.
2
- import type { ResourceMetaBase, RawStrategy as ExtractedData } from './types.js';
3
- export type Resource_RawStrategy = ResourceMetaBase & { extractedData: ExtractedData };
@@ -1,3 +0,0 @@
1
- // Auto-generated strict composite type. Do not edit.
2
- import type { ResourceMetaBase, ResourceType as ExtractedData } from './types.js';
3
- export type Resource_ResourceType = ResourceMetaBase & { extractedData: ExtractedData };
@@ -1,3 +0,0 @@
1
- // Auto-generated strict composite type. Do not edit.
2
- import type { ResourceMetaBase, RunnableStrategy as ExtractedData } from './types.js';
3
- export type Resource_RunnableStrategy = ResourceMetaBase & { extractedData: ExtractedData };
@@ -1,229 +0,0 @@
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.getOutputPath(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
- // Heuristic: determine if a node is a Type envelope
70
- function isTypeEnvelope(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 envelope
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
- // Merge $defs into target, without overwriting existing keys unless identical
81
- function mergeDefs(target: Record<string, JSONValue>, source?: any, label?: string) {
82
- if (!source || typeof source !== "object") return;
83
- const src = (source as any)["$defs"];
84
- if (!src || typeof src !== "object") return;
85
- for (const [k, v] of Object.entries(src)) {
86
- if (!(k in target)) {
87
- target[k] = v as JSONValue;
88
- } else {
89
- // Best-effort: if duplicate key, require structural equality; otherwise, namespace
90
- const existing = JSON.stringify(target[k]);
91
- const incoming = JSON.stringify(v);
92
- if (existing !== incoming) {
93
- const altKey = `${k}__from_${(label || "defs").replace(/[^A-Za-z0-9_]+/g, "_")}`;
94
- if (!(altKey in target)) target[altKey] = v as JSONValue;
95
- }
96
- }
97
- }
98
- }
99
-
100
- // Deeply traverse an object replacing any Type envelope with its nucleusSchema,
101
- // and hoist its inner $defs to topDefs. Prevent infinite recursion with a visited set.
102
- function unwrapTypes(node: JSONValue, topDefs: Record<string, JSONValue>, labelPath: string[] = [], visited = new Set<any>()): JSONValue {
103
- if (node && typeof node === "object") {
104
- if (visited.has(node)) return node; // avoid cycles
105
- visited.add(node);
106
- }
107
-
108
- if (isTypeEnvelope(node)) {
109
- const env = node as any;
110
- const inner = env.nucleusSchema;
111
- // Hoist inner $defs before stripping
112
- mergeDefs(topDefs, inner, labelPath.join("_"));
113
- // Return the inner schema itself, after also unwrapping any nested envelopes it may contain
114
- const unwrappedInner = unwrapTypes(inner as JSONValue, topDefs, labelPath.concat([String(env.identity || "env")]), visited);
115
- return unwrappedInner;
116
- }
117
-
118
- if (Array.isArray(node)) {
119
- return node.map((v, i) => unwrapTypes(v, topDefs, labelPath.concat([String(i)]), visited)) as JSONValue;
120
- }
121
-
122
- if (node && typeof node === "object") {
123
- const out: Record<string, JSONValue> = {};
124
- for (const [k, v] of Object.entries(node)) {
125
- if (k === "$defs" && v && typeof v === "object" && !Array.isArray(v)) {
126
- // Process nested $defs: unwrap each entry value if it's a Type envelope
127
- const defsOut: Record<string, JSONValue> = {};
128
- for (const [dk, dv] of Object.entries(v as any)) {
129
- const unwrapped = unwrapTypes(dv as JSONValue, topDefs, labelPath.concat(["$defs", dk]), visited);
130
- defsOut[dk] = unwrapped;
131
- }
132
- out[k] = defsOut;
133
- } else {
134
- out[k] = unwrapTypes(v as JSONValue, topDefs, labelPath.concat([k]), visited);
135
- }
136
- }
137
- return out;
138
- }
139
-
140
- return node;
141
- }
142
-
143
- /**
144
- * Pure function that takes a schema document and options, and returns the flattened schema.
145
- * Performs no I/O operations.
146
- */
147
- function extractSchemaLogic(doc: any, topLevelId?: string): any {
148
- if (!doc || typeof doc !== "object" || !doc.nucleusSchema) {
149
- throw new Error("Input must be a Type JSON with an nucleusSchema at the top level");
150
- }
151
-
152
- const topSchema = (doc as any).nucleusSchema;
153
- const outDefs: Record<string, JSONValue> = {};
154
-
155
- // Seed with top-level $defs (if any) before unwrapping
156
- mergeDefs(outDefs, topSchema, "top");
157
-
158
- // Unwrap the entire top schema tree so that any nested Type envelopes become raw schemas
159
- const flattened = unwrapTypes(topSchema as JSONValue, outDefs, ["nucleusSchema"]);
160
-
161
- // Assemble output: force $schema, optionally set $id, hoist collected $defs
162
- let base: any;
163
- if (flattened && typeof flattened === "object" && !Array.isArray(flattened)) {
164
- base = { ...(flattened as any) };
165
- } else {
166
- // If flattened is not an object (should be rare for a top-level schema), wrap it
167
- base = { const: flattened };
168
- }
169
- // Assemble, but avoid duplicating $id: if the flattened base already has $id, prefer it.
170
- const output: Record<string, JSONValue> = {
171
- $schema: "https://json-schema.org/draft/2020-12/schema",
172
- ...base,
173
- };
174
- if (topLevelId && !(output as any).$id) {
175
- (output as any).$id = topLevelId;
176
- }
177
-
178
- // Enforce presence of $id: schema must declare an absolute identity.
179
- if (!(output as any).$id) {
180
- throw new Error(
181
- "Flattened schema must define $id. Provide it via CLI --id or include $id in the source nucleusSchema."
182
- );
183
- }
184
-
185
- // Merge collected defs into output.$defs, taking care not to clobber any existing
186
- if (!("$defs" in output)) output.$defs = {} as any;
187
- const finalDefs: Record<string, JSONValue> = (output.$defs as any) || {};
188
- for (const [k, v] of Object.entries(outDefs)) {
189
- if (!(k in finalDefs)) finalDefs[k] = v;
190
- }
191
- output.$defs = finalDefs as any;
192
-
193
- // Ensure a stable order for readability
194
- return orderKeys(output, ["$id", "$schema", "$vocabulary", "$defs", "title", "description", "type", "allOf", "anyOf", "oneOf", "not", "if", "then", "else", "properties", "required", "additionalProperties", "unevaluatedProperties"]);
195
- }
196
-
197
- function main() {
198
- const { inPath, outPath, topLevelId } = parseArgs();
199
-
200
- if (!fs.existsSync(inPath)) {
201
- console.error(`Input file not found at ${inPath}`);
202
- process.exit(1);
203
- }
204
-
205
- const raw = fs.readFileSync(inPath, "utf8");
206
- const doc = JSON.parse(raw);
207
-
208
- // Core logic is now in a pure function
209
- const ordered = extractSchemaLogic(doc, topLevelId);
210
-
211
- fs.mkdirSync(path.dirname(outPath), { recursive: true });
212
- fs.writeFileSync(outPath, JSON.stringify(ordered, null, 4), "utf8");
213
- console.log(`Wrote flattened schema to ${outPath}`);
214
- }
215
-
216
- function orderKeys(obj: any, preferred: string[]): any {
217
- if (Array.isArray(obj)) return obj.map((v) => orderKeys(v, preferred));
218
- if (!obj || typeof obj !== "object") return obj;
219
- const keys = Object.keys(obj);
220
- const sorted = [
221
- ...preferred.filter((k) => keys.includes(k)),
222
- ...keys.filter((k) => !preferred.includes(k)).sort()
223
- ];
224
- const out: any = {};
225
- for (const k of sorted) out[k] = orderKeys(obj[k], preferred);
226
- return out;
227
- }
228
-
229
- main();