@tsonic/frontend 0.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 (145) hide show
  1. package/package.json +53 -0
  2. package/src/dependency-graph.ts +18 -0
  3. package/src/dotnet-metadata.ts +121 -0
  4. package/src/graph/builder.ts +81 -0
  5. package/src/graph/circular.ts +58 -0
  6. package/src/graph/extraction/exports.ts +55 -0
  7. package/src/graph/extraction/imports.ts +81 -0
  8. package/src/graph/extraction/index.ts +7 -0
  9. package/src/graph/extraction/orchestrator.ts +99 -0
  10. package/src/graph/extraction.ts +10 -0
  11. package/src/graph/helpers.ts +51 -0
  12. package/src/graph/index.ts +17 -0
  13. package/src/graph/types.ts +13 -0
  14. package/src/index.ts +80 -0
  15. package/src/ir/binding-resolution.test.ts +585 -0
  16. package/src/ir/builder/exports.ts +78 -0
  17. package/src/ir/builder/helpers.ts +27 -0
  18. package/src/ir/builder/imports.ts +153 -0
  19. package/src/ir/builder/index.ts +10 -0
  20. package/src/ir/builder/orchestrator.ts +178 -0
  21. package/src/ir/builder/statements.ts +55 -0
  22. package/src/ir/builder/types.ts +8 -0
  23. package/src/ir/builder/validation.ts +129 -0
  24. package/src/ir/builder.test.ts +581 -0
  25. package/src/ir/builder.ts +14 -0
  26. package/src/ir/converters/expressions/access.ts +99 -0
  27. package/src/ir/converters/expressions/calls.ts +137 -0
  28. package/src/ir/converters/expressions/collections.ts +84 -0
  29. package/src/ir/converters/expressions/functions.ts +62 -0
  30. package/src/ir/converters/expressions/helpers.ts +264 -0
  31. package/src/ir/converters/expressions/index.ts +43 -0
  32. package/src/ir/converters/expressions/literals.ts +22 -0
  33. package/src/ir/converters/expressions/operators.ts +147 -0
  34. package/src/ir/converters/expressions/other.ts +60 -0
  35. package/src/ir/converters/statements/control/blocks.ts +22 -0
  36. package/src/ir/converters/statements/control/conditionals.ts +67 -0
  37. package/src/ir/converters/statements/control/exceptions.ts +43 -0
  38. package/src/ir/converters/statements/control/index.ts +17 -0
  39. package/src/ir/converters/statements/control/loops.ts +99 -0
  40. package/src/ir/converters/statements/control.ts +17 -0
  41. package/src/ir/converters/statements/declarations/classes/constructors.ts +120 -0
  42. package/src/ir/converters/statements/declarations/classes/index.ts +12 -0
  43. package/src/ir/converters/statements/declarations/classes/methods.ts +61 -0
  44. package/src/ir/converters/statements/declarations/classes/orchestrator.ts +166 -0
  45. package/src/ir/converters/statements/declarations/classes/override-detection.ts +116 -0
  46. package/src/ir/converters/statements/declarations/classes/properties.ts +63 -0
  47. package/src/ir/converters/statements/declarations/classes.ts +6 -0
  48. package/src/ir/converters/statements/declarations/enums.ts +29 -0
  49. package/src/ir/converters/statements/declarations/functions.ts +39 -0
  50. package/src/ir/converters/statements/declarations/index.ts +14 -0
  51. package/src/ir/converters/statements/declarations/interfaces.ts +131 -0
  52. package/src/ir/converters/statements/declarations/registry.ts +45 -0
  53. package/src/ir/converters/statements/declarations/type-aliases.ts +25 -0
  54. package/src/ir/converters/statements/declarations/variables.ts +60 -0
  55. package/src/ir/converters/statements/declarations.ts +16 -0
  56. package/src/ir/converters/statements/helpers.ts +174 -0
  57. package/src/ir/converters/statements/index.ts +40 -0
  58. package/src/ir/expression-converter.ts +207 -0
  59. package/src/ir/generic-validator.ts +100 -0
  60. package/src/ir/hierarchical-bindings-e2e.test.ts +163 -0
  61. package/src/ir/index.ts +6 -0
  62. package/src/ir/statement-converter.ts +128 -0
  63. package/src/ir/type-converter/arrays.ts +20 -0
  64. package/src/ir/type-converter/converter.ts +10 -0
  65. package/src/ir/type-converter/functions.ts +22 -0
  66. package/src/ir/type-converter/index.ts +11 -0
  67. package/src/ir/type-converter/inference.ts +122 -0
  68. package/src/ir/type-converter/literals.ts +40 -0
  69. package/src/ir/type-converter/objects.ts +107 -0
  70. package/src/ir/type-converter/orchestrator.ts +85 -0
  71. package/src/ir/type-converter/patterns.ts +73 -0
  72. package/src/ir/type-converter/primitives.ts +57 -0
  73. package/src/ir/type-converter/references.ts +64 -0
  74. package/src/ir/type-converter/unions-intersections.ts +34 -0
  75. package/src/ir/type-converter.ts +13 -0
  76. package/src/ir/types/expressions.ts +215 -0
  77. package/src/ir/types/guards.ts +39 -0
  78. package/src/ir/types/helpers.ts +135 -0
  79. package/src/ir/types/index.ts +108 -0
  80. package/src/ir/types/ir-types.ts +96 -0
  81. package/src/ir/types/module.ts +57 -0
  82. package/src/ir/types/statements.ts +238 -0
  83. package/src/ir/types.ts +97 -0
  84. package/src/metadata/bindings-loader.test.ts +144 -0
  85. package/src/metadata/bindings-loader.ts +357 -0
  86. package/src/metadata/index.ts +15 -0
  87. package/src/metadata/library-loader.ts +153 -0
  88. package/src/metadata/loader.test.ts +156 -0
  89. package/src/metadata/loader.ts +382 -0
  90. package/src/program/bindings.test.ts +512 -0
  91. package/src/program/bindings.ts +253 -0
  92. package/src/program/config.ts +30 -0
  93. package/src/program/creation.ts +249 -0
  94. package/src/program/dependency-graph.ts +245 -0
  95. package/src/program/diagnostics.ts +103 -0
  96. package/src/program/index.ts +19 -0
  97. package/src/program/metadata.ts +68 -0
  98. package/src/program/queries.ts +18 -0
  99. package/src/program/types.ts +38 -0
  100. package/src/program.ts +13 -0
  101. package/src/resolver/dotnet-import-resolver.ts +226 -0
  102. package/src/resolver/import-resolution.ts +177 -0
  103. package/src/resolver/index.ts +18 -0
  104. package/src/resolver/namespace.test.ts +86 -0
  105. package/src/resolver/namespace.ts +42 -0
  106. package/src/resolver/naming.ts +38 -0
  107. package/src/resolver/path-resolution.ts +22 -0
  108. package/src/resolver/types.ts +15 -0
  109. package/src/resolver.test.ts +155 -0
  110. package/src/resolver.ts +14 -0
  111. package/src/symbol-table/builder.ts +114 -0
  112. package/src/symbol-table/creation.ts +42 -0
  113. package/src/symbol-table/helpers.ts +18 -0
  114. package/src/symbol-table/index.ts +13 -0
  115. package/src/symbol-table/queries.ts +42 -0
  116. package/src/symbol-table/types.ts +28 -0
  117. package/src/symbol-table.ts +14 -0
  118. package/src/types/bindings.ts +172 -0
  119. package/src/types/diagnostic.test.ts +164 -0
  120. package/src/types/diagnostic.ts +153 -0
  121. package/src/types/explicit-views.test.ts +113 -0
  122. package/src/types/explicit-views.ts +218 -0
  123. package/src/types/metadata.ts +229 -0
  124. package/src/types/module.ts +99 -0
  125. package/src/types/nested-types.test.ts +194 -0
  126. package/src/types/nested-types.ts +215 -0
  127. package/src/types/parameter-modifiers.ts +173 -0
  128. package/src/types/ref-parameters.test.ts +192 -0
  129. package/src/types/ref-parameters.ts +268 -0
  130. package/src/types/result.test.ts +157 -0
  131. package/src/types/result.ts +48 -0
  132. package/src/types/support-types.test.ts +81 -0
  133. package/src/types/support-types.ts +288 -0
  134. package/src/types/test-harness.ts +180 -0
  135. package/src/validation/exports.ts +98 -0
  136. package/src/validation/features.ts +89 -0
  137. package/src/validation/generics.ts +40 -0
  138. package/src/validation/helpers.ts +31 -0
  139. package/src/validation/imports.ts +97 -0
  140. package/src/validation/index.ts +11 -0
  141. package/src/validation/orchestrator.ts +51 -0
  142. package/src/validation/static-safety.ts +267 -0
  143. package/src/validator.test.ts +468 -0
  144. package/src/validator.ts +15 -0
  145. package/tsconfig.json +13 -0
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Library Loader - Load metadata and bindings from external library directories.
3
+ *
4
+ * External libraries (specified via --lib flag) contain both metadata and bindings:
5
+ * lib-path/
6
+ * .metadata/
7
+ * Namespace.metadata.json
8
+ * .bindings/
9
+ * Namespace.bindings.json
10
+ *
11
+ * This module provides helpers to load both from a single library root path.
12
+ */
13
+
14
+ import { join } from "path";
15
+ import { existsSync, statSync } from "fs";
16
+ import type { Result } from "../types/result.js";
17
+ import type { Diagnostic } from "../types/diagnostic.js";
18
+ import { loadMetadataDirectory } from "./loader.js";
19
+ import { loadBindingsDirectory } from "./bindings-loader.js";
20
+ import type { MetadataFile } from "../types/metadata.js";
21
+ import type { BindingsFile } from "../types/bindings.js";
22
+
23
+ /**
24
+ * Loaded library data (metadata + bindings).
25
+ */
26
+ export type LibraryData = {
27
+ readonly metadata: ReadonlyArray<MetadataFile>;
28
+ readonly bindings: ReadonlyArray<BindingsFile>;
29
+ };
30
+
31
+ /**
32
+ * Load metadata and bindings from a library directory.
33
+ *
34
+ * Looks for:
35
+ * - ${libraryPath}/.metadata/ directory
36
+ * - ${libraryPath}/.bindings/ directory
37
+ *
38
+ * @param libraryPath - Path to external library root
39
+ * @returns Result with library data or diagnostics
40
+ */
41
+ export const loadLibrary = (
42
+ libraryPath: string
43
+ ): Result<LibraryData, Diagnostic[]> => {
44
+ const diagnostics: Diagnostic[] = [];
45
+
46
+ // Validate library path exists
47
+ if (!existsSync(libraryPath)) {
48
+ return {
49
+ ok: false,
50
+ error: [
51
+ {
52
+ code: "TSN9016",
53
+ severity: "error",
54
+ message: `Library directory not found: ${libraryPath}`,
55
+ },
56
+ ],
57
+ };
58
+ }
59
+
60
+ // Validate it's a directory
61
+ if (!statSync(libraryPath).isDirectory()) {
62
+ return {
63
+ ok: false,
64
+ error: [
65
+ {
66
+ code: "TSN9017",
67
+ severity: "error",
68
+ message: `Library path is not a directory: ${libraryPath}`,
69
+ },
70
+ ],
71
+ };
72
+ }
73
+
74
+ // Load metadata from .metadata/ subdirectory
75
+ const metadataPath = join(libraryPath, ".metadata");
76
+ let metadata: ReadonlyArray<MetadataFile> = [];
77
+
78
+ if (existsSync(metadataPath)) {
79
+ const metadataResult = loadMetadataDirectory(metadataPath);
80
+ if (metadataResult.ok) {
81
+ metadata = metadataResult.value;
82
+ } else {
83
+ diagnostics.push(...metadataResult.error);
84
+ }
85
+ }
86
+
87
+ // Load bindings from .bindings/ subdirectory
88
+ const bindingsPath = join(libraryPath, ".bindings");
89
+ let bindings: ReadonlyArray<BindingsFile> = [];
90
+
91
+ if (existsSync(bindingsPath)) {
92
+ const bindingsResult = loadBindingsDirectory(bindingsPath);
93
+ if (bindingsResult.ok) {
94
+ bindings = bindingsResult.value;
95
+ } else {
96
+ diagnostics.push(...bindingsResult.error);
97
+ }
98
+ }
99
+
100
+ // Return success even if one or both are missing (library might be incomplete)
101
+ return {
102
+ ok: true,
103
+ value: {
104
+ metadata,
105
+ bindings,
106
+ },
107
+ };
108
+ };
109
+
110
+ /**
111
+ * Load metadata and bindings from multiple library directories.
112
+ *
113
+ * @param libraryPaths - Array of library root paths
114
+ * @returns Result with merged library data or diagnostics
115
+ */
116
+ export const loadLibraries = (
117
+ libraryPaths: readonly string[]
118
+ ): Result<LibraryData, Diagnostic[]> => {
119
+ const allMetadata: MetadataFile[] = [];
120
+ const allBindings: BindingsFile[] = [];
121
+ const allDiagnostics: Diagnostic[] = [];
122
+
123
+ for (const libPath of libraryPaths) {
124
+ const result = loadLibrary(libPath);
125
+ if (result.ok) {
126
+ allMetadata.push(...result.value.metadata);
127
+ allBindings.push(...result.value.bindings);
128
+ } else {
129
+ allDiagnostics.push(...result.error);
130
+ }
131
+ }
132
+
133
+ // If we have any diagnostics but also loaded some data, still succeed
134
+ // (allows partial library loading)
135
+ if (
136
+ allDiagnostics.length > 0 &&
137
+ allMetadata.length === 0 &&
138
+ allBindings.length === 0
139
+ ) {
140
+ return {
141
+ ok: false,
142
+ error: allDiagnostics,
143
+ };
144
+ }
145
+
146
+ return {
147
+ ok: true,
148
+ value: {
149
+ metadata: allMetadata,
150
+ bindings: allBindings,
151
+ },
152
+ };
153
+ };
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Tests for metadata JSON loader
3
+ */
4
+
5
+ import { describe, it } from "mocha";
6
+ import { strict as assert } from "assert";
7
+ import * as fs from "fs";
8
+ import * as path from "path";
9
+ import * as os from "os";
10
+ import { loadMetadataFile, loadMetadataDirectory } from "./loader.js";
11
+ import type { MetadataFile } from "../types/metadata.js";
12
+
13
+ describe("Metadata Loader", () => {
14
+ describe("loadMetadataFile", () => {
15
+ it("should load valid metadata file", () => {
16
+ // Create temporary metadata file
17
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tsonic-test-"));
18
+ const metadataPath = path.join(tmpDir, "test.metadata.json");
19
+
20
+ const metadata: MetadataFile = {
21
+ namespace: "System",
22
+ contributingAssemblies: ["System.Runtime"],
23
+ types: [
24
+ {
25
+ clrName: "System.String",
26
+ tsEmitName: "String",
27
+ kind: "Class",
28
+ accessibility: "Public",
29
+ isAbstract: false,
30
+ isSealed: true,
31
+ isStatic: false,
32
+ arity: 0,
33
+ methods: [],
34
+ properties: [],
35
+ fields: [],
36
+ events: [],
37
+ constructors: [],
38
+ },
39
+ ],
40
+ };
41
+
42
+ fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
43
+
44
+ const result = loadMetadataFile(metadataPath);
45
+
46
+ // Cleanup
47
+ fs.unlinkSync(metadataPath);
48
+ fs.rmdirSync(tmpDir);
49
+
50
+ assert.ok(result.ok);
51
+ if (result.ok) {
52
+ assert.equal(result.value.namespace, "System");
53
+ assert.equal(result.value.types.length, 1);
54
+ assert.equal(result.value.types[0]?.tsEmitName, "String");
55
+ }
56
+ });
57
+
58
+ it("should return error for non-existent file", () => {
59
+ const result = loadMetadataFile("/nonexistent/file.metadata.json");
60
+
61
+ assert.ok(!result.ok);
62
+ if (!result.ok) {
63
+ assert.equal(result.error[0]?.code, "TSN9001");
64
+ }
65
+ });
66
+
67
+ it("should return error for invalid JSON", () => {
68
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tsonic-test-"));
69
+ const metadataPath = path.join(tmpDir, "invalid.metadata.json");
70
+
71
+ fs.writeFileSync(metadataPath, "{ invalid json }");
72
+
73
+ const result = loadMetadataFile(metadataPath);
74
+
75
+ // Cleanup
76
+ fs.unlinkSync(metadataPath);
77
+ fs.rmdirSync(tmpDir);
78
+
79
+ assert.ok(!result.ok);
80
+ if (!result.ok) {
81
+ assert.equal(result.error[0]?.code, "TSN9003");
82
+ }
83
+ });
84
+
85
+ it("should return error for missing required fields", () => {
86
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tsonic-test-"));
87
+ const metadataPath = path.join(tmpDir, "incomplete.metadata.json");
88
+
89
+ // Missing namespace field
90
+ fs.writeFileSync(
91
+ metadataPath,
92
+ JSON.stringify({ contributingAssemblies: [], types: [] })
93
+ );
94
+
95
+ const result = loadMetadataFile(metadataPath);
96
+
97
+ // Cleanup
98
+ fs.unlinkSync(metadataPath);
99
+ fs.rmdirSync(tmpDir);
100
+
101
+ assert.ok(!result.ok);
102
+ if (!result.ok) {
103
+ assert.equal(result.error[0]?.code, "TSN9005");
104
+ }
105
+ });
106
+ });
107
+
108
+ describe("loadMetadataDirectory", () => {
109
+ it("should load all metadata files from directory", () => {
110
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tsonic-test-"));
111
+
112
+ // Create two metadata files
113
+ const metadata1: MetadataFile = {
114
+ namespace: "System",
115
+ contributingAssemblies: ["System.Runtime"],
116
+ types: [],
117
+ };
118
+
119
+ const metadata2: MetadataFile = {
120
+ namespace: "System.Collections",
121
+ contributingAssemblies: ["System.Collections"],
122
+ types: [],
123
+ };
124
+
125
+ fs.writeFileSync(
126
+ path.join(tmpDir, "System.metadata.json"),
127
+ JSON.stringify(metadata1)
128
+ );
129
+ fs.writeFileSync(
130
+ path.join(tmpDir, "System.Collections.metadata.json"),
131
+ JSON.stringify(metadata2)
132
+ );
133
+
134
+ const result = loadMetadataDirectory(tmpDir);
135
+
136
+ // Cleanup
137
+ fs.unlinkSync(path.join(tmpDir, "System.metadata.json"));
138
+ fs.unlinkSync(path.join(tmpDir, "System.Collections.metadata.json"));
139
+ fs.rmdirSync(tmpDir);
140
+
141
+ assert.ok(result.ok);
142
+ if (result.ok) {
143
+ assert.equal(result.value.length, 2);
144
+ }
145
+ });
146
+
147
+ it("should return error for non-existent directory", () => {
148
+ const result = loadMetadataDirectory("/nonexistent/directory");
149
+
150
+ assert.ok(!result.ok);
151
+ if (!result.ok) {
152
+ assert.equal(result.error[0]?.code, "TSN9016");
153
+ }
154
+ });
155
+ });
156
+ });
@@ -0,0 +1,382 @@
1
+ /**
2
+ * Metadata JSON loader - Reads and validates .metadata.json files from tsbindgen.
3
+ *
4
+ * This module provides pure functions to load CLR metadata files and validate
5
+ * their structure against the expected schema.
6
+ *
7
+ * @see spec/metadata.md for complete schema documentation
8
+ */
9
+
10
+ import * as fs from "fs";
11
+ import * as path from "path";
12
+ import type { Result } from "../types/result.ts";
13
+ import type { Diagnostic } from "../types/diagnostic.ts";
14
+ import type {
15
+ MetadataFile,
16
+ TypeMetadata,
17
+ TypeKind,
18
+ Accessibility,
19
+ } from "../types/metadata.ts";
20
+
21
+ /**
22
+ * Load and parse a metadata.json file.
23
+ *
24
+ * @param filePath - Absolute path to the .metadata.json file
25
+ * @returns Result containing parsed metadata or diagnostics
26
+ */
27
+ export const loadMetadataFile = (
28
+ filePath: string
29
+ ): Result<MetadataFile, Diagnostic[]> => {
30
+ // Check file exists
31
+ if (!fs.existsSync(filePath)) {
32
+ return {
33
+ ok: false,
34
+ error: [
35
+ {
36
+ code: "TSN9001",
37
+ message: `Metadata file not found: ${filePath}`,
38
+ severity: "error",
39
+ location: undefined,
40
+ },
41
+ ],
42
+ };
43
+ }
44
+
45
+ // Read file contents
46
+ let content: string;
47
+ try {
48
+ content = fs.readFileSync(filePath, "utf-8");
49
+ } catch (error) {
50
+ return {
51
+ ok: false,
52
+ error: [
53
+ {
54
+ code: "TSN9002",
55
+ message: `Failed to read metadata file: ${error}`,
56
+ severity: "error",
57
+ location: undefined,
58
+ },
59
+ ],
60
+ };
61
+ }
62
+
63
+ // Parse JSON
64
+ let parsed: unknown;
65
+ try {
66
+ parsed = JSON.parse(content);
67
+ } catch (error) {
68
+ return {
69
+ ok: false,
70
+ error: [
71
+ {
72
+ code: "TSN9003",
73
+ message: `Invalid JSON in metadata file: ${error}`,
74
+ severity: "error",
75
+ location: undefined,
76
+ },
77
+ ],
78
+ };
79
+ }
80
+
81
+ // Validate structure
82
+ const validation = validateMetadataFile(parsed, filePath);
83
+ if (!validation.ok) {
84
+ return validation;
85
+ }
86
+
87
+ return { ok: true, value: validation.value };
88
+ };
89
+
90
+ /**
91
+ * Validate that parsed JSON matches MetadataFile schema.
92
+ *
93
+ * @param data - Parsed JSON data
94
+ * @param filePath - File path for error messages
95
+ * @returns Result containing validated metadata or diagnostics
96
+ */
97
+ const validateMetadataFile = (
98
+ data: unknown,
99
+ filePath: string
100
+ ): Result<MetadataFile, Diagnostic[]> => {
101
+ const diagnostics: Diagnostic[] = [];
102
+
103
+ if (typeof data !== "object" || data === null) {
104
+ return {
105
+ ok: false,
106
+ error: [
107
+ {
108
+ code: "TSN9004",
109
+ message: `Metadata file must be an object, got ${typeof data}`,
110
+ severity: "error",
111
+ location: undefined,
112
+ },
113
+ ],
114
+ };
115
+ }
116
+
117
+ const obj = data as Record<string, unknown>;
118
+
119
+ // Validate namespace
120
+ if (typeof obj.namespace !== "string") {
121
+ diagnostics.push({
122
+ code: "TSN9005",
123
+ message: `Missing or invalid 'namespace' field in ${path.basename(filePath)}`,
124
+ severity: "error",
125
+ location: undefined,
126
+ });
127
+ }
128
+
129
+ // Validate contributingAssemblies
130
+ if (!Array.isArray(obj.contributingAssemblies)) {
131
+ diagnostics.push({
132
+ code: "TSN9006",
133
+ message: `Missing or invalid 'contributingAssemblies' field in ${path.basename(filePath)}`,
134
+ severity: "error",
135
+ location: undefined,
136
+ });
137
+ } else if (
138
+ !obj.contributingAssemblies.every((item) => typeof item === "string")
139
+ ) {
140
+ diagnostics.push({
141
+ code: "TSN9007",
142
+ message: `All 'contributingAssemblies' must be strings in ${path.basename(filePath)}`,
143
+ severity: "error",
144
+ location: undefined,
145
+ });
146
+ }
147
+
148
+ // Validate types array
149
+ if (!Array.isArray(obj.types)) {
150
+ diagnostics.push({
151
+ code: "TSN9008",
152
+ message: `Missing or invalid 'types' field in ${path.basename(filePath)}`,
153
+ severity: "error",
154
+ location: undefined,
155
+ });
156
+ } else {
157
+ // Validate each type
158
+ for (let i = 0; i < obj.types.length; i++) {
159
+ const typeValidation = validateTypeMetadata(obj.types[i], filePath, i);
160
+ if (!typeValidation.ok) {
161
+ diagnostics.push(...typeValidation.error);
162
+ }
163
+ }
164
+ }
165
+
166
+ if (diagnostics.length > 0) {
167
+ return { ok: false, error: diagnostics };
168
+ }
169
+
170
+ return { ok: true, value: obj as MetadataFile };
171
+ };
172
+
173
+ /**
174
+ * Validate a single TypeMetadata object.
175
+ *
176
+ * @param data - Type metadata to validate
177
+ * @param filePath - File path for error messages
178
+ * @param index - Type index in array
179
+ * @returns Result indicating validation success or errors
180
+ */
181
+ const validateTypeMetadata = (
182
+ data: unknown,
183
+ filePath: string,
184
+ index: number
185
+ ): Result<TypeMetadata, Diagnostic[]> => {
186
+ const diagnostics: Diagnostic[] = [];
187
+ const context = `type ${index} in ${path.basename(filePath)}`;
188
+
189
+ if (typeof data !== "object" || data === null) {
190
+ return {
191
+ ok: false,
192
+ error: [
193
+ {
194
+ code: "TSN9009",
195
+ message: `Invalid ${context}: must be an object`,
196
+ severity: "error",
197
+ location: undefined,
198
+ },
199
+ ],
200
+ };
201
+ }
202
+
203
+ const type = data as Record<string, unknown>;
204
+
205
+ // Validate required string fields
206
+ const requiredStringFields = ["clrName", "tsEmitName"];
207
+ for (const field of requiredStringFields) {
208
+ if (typeof type[field] !== "string") {
209
+ diagnostics.push({
210
+ code: "TSN9010",
211
+ message: `Invalid ${context}: missing or invalid '${field}'`,
212
+ severity: "error",
213
+ location: undefined,
214
+ });
215
+ }
216
+ }
217
+
218
+ // Validate kind
219
+ const validKinds: TypeKind[] = [
220
+ "Class",
221
+ "Interface",
222
+ "Struct",
223
+ "Enum",
224
+ "Delegate",
225
+ ];
226
+ if (!validKinds.includes(type.kind as TypeKind)) {
227
+ diagnostics.push({
228
+ code: "TSN9011",
229
+ message: `Invalid ${context}: 'kind' must be one of ${validKinds.join(", ")}`,
230
+ severity: "error",
231
+ location: undefined,
232
+ });
233
+ }
234
+
235
+ // Validate accessibility
236
+ const validAccessibility: Accessibility[] = [
237
+ "Public",
238
+ "Internal",
239
+ "Protected",
240
+ "ProtectedInternal",
241
+ "PrivateProtected",
242
+ "Private",
243
+ ];
244
+ if (!validAccessibility.includes(type.accessibility as Accessibility)) {
245
+ diagnostics.push({
246
+ code: "TSN9012",
247
+ message: `Invalid ${context}: 'accessibility' must be one of ${validAccessibility.join(", ")}`,
248
+ severity: "error",
249
+ location: undefined,
250
+ });
251
+ }
252
+
253
+ // Validate required boolean fields
254
+ const requiredBooleanFields = ["isAbstract", "isSealed", "isStatic"];
255
+ for (const field of requiredBooleanFields) {
256
+ if (typeof type[field] !== "boolean") {
257
+ diagnostics.push({
258
+ code: "TSN9013",
259
+ message: `Invalid ${context}: '${field}' must be a boolean`,
260
+ severity: "error",
261
+ location: undefined,
262
+ });
263
+ }
264
+ }
265
+
266
+ // Validate arity
267
+ if (typeof type.arity !== "number" || type.arity < 0) {
268
+ diagnostics.push({
269
+ code: "TSN9014",
270
+ message: `Invalid ${context}: 'arity' must be a non-negative number`,
271
+ severity: "error",
272
+ location: undefined,
273
+ });
274
+ }
275
+
276
+ // Validate required array fields
277
+ const requiredArrayFields = [
278
+ "methods",
279
+ "properties",
280
+ "fields",
281
+ "events",
282
+ "constructors",
283
+ ];
284
+ for (const field of requiredArrayFields) {
285
+ if (!Array.isArray(type[field])) {
286
+ diagnostics.push({
287
+ code: "TSN9015",
288
+ message: `Invalid ${context}: '${field}' must be an array`,
289
+ severity: "error",
290
+ location: undefined,
291
+ });
292
+ }
293
+ }
294
+
295
+ // Note: We're doing basic structural validation here
296
+ // More detailed validation of nested objects (methods, properties, etc.)
297
+ // can be added in future iterations if needed
298
+
299
+ if (diagnostics.length > 0) {
300
+ return { ok: false, error: diagnostics };
301
+ }
302
+
303
+ return { ok: true, value: type as TypeMetadata };
304
+ };
305
+
306
+ /**
307
+ * Load metadata from a directory containing .metadata.json files.
308
+ *
309
+ * @param directoryPath - Path to directory containing metadata files
310
+ * @returns Result containing array of loaded metadata files or diagnostics
311
+ */
312
+ export const loadMetadataDirectory = (
313
+ directoryPath: string
314
+ ): Result<MetadataFile[], Diagnostic[]> => {
315
+ if (!fs.existsSync(directoryPath)) {
316
+ return {
317
+ ok: false,
318
+ error: [
319
+ {
320
+ code: "TSN9016",
321
+ message: `Metadata directory not found: ${directoryPath}`,
322
+ severity: "error",
323
+ location: undefined,
324
+ },
325
+ ],
326
+ };
327
+ }
328
+
329
+ const stats = fs.statSync(directoryPath);
330
+ if (!stats.isDirectory()) {
331
+ return {
332
+ ok: false,
333
+ error: [
334
+ {
335
+ code: "TSN9017",
336
+ message: `Not a directory: ${directoryPath}`,
337
+ severity: "error",
338
+ location: undefined,
339
+ },
340
+ ],
341
+ };
342
+ }
343
+
344
+ // Find all .metadata.json files
345
+ const files = fs
346
+ .readdirSync(directoryPath)
347
+ .filter((file) => file.endsWith(".metadata.json"))
348
+ .map((file) => path.join(directoryPath, file));
349
+
350
+ if (files.length === 0) {
351
+ return {
352
+ ok: false,
353
+ error: [
354
+ {
355
+ code: "TSN9018",
356
+ message: `No .metadata.json files found in ${directoryPath}`,
357
+ severity: "warning",
358
+ location: undefined,
359
+ },
360
+ ],
361
+ };
362
+ }
363
+
364
+ // Load each file
365
+ const metadataFiles: MetadataFile[] = [];
366
+ const diagnostics: Diagnostic[] = [];
367
+
368
+ for (const file of files) {
369
+ const result = loadMetadataFile(file);
370
+ if (result.ok) {
371
+ metadataFiles.push(result.value);
372
+ } else {
373
+ diagnostics.push(...result.error);
374
+ }
375
+ }
376
+
377
+ if (diagnostics.length > 0) {
378
+ return { ok: false, error: diagnostics };
379
+ }
380
+
381
+ return { ok: true, value: metadataFiles };
382
+ };