@vibe-lang/runtime 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/package.json +46 -0
  2. package/src/ast/index.ts +375 -0
  3. package/src/ast.ts +2 -0
  4. package/src/debug/advanced-features.ts +482 -0
  5. package/src/debug/bun-inspector.ts +424 -0
  6. package/src/debug/handoff-manager.ts +283 -0
  7. package/src/debug/index.ts +150 -0
  8. package/src/debug/runner.ts +365 -0
  9. package/src/debug/server.ts +565 -0
  10. package/src/debug/stack-merger.ts +267 -0
  11. package/src/debug/state.ts +581 -0
  12. package/src/debug/test/advanced-features.test.ts +300 -0
  13. package/src/debug/test/e2e.test.ts +218 -0
  14. package/src/debug/test/handoff-manager.test.ts +256 -0
  15. package/src/debug/test/runner.test.ts +256 -0
  16. package/src/debug/test/stack-merger.test.ts +163 -0
  17. package/src/debug/test/state.test.ts +400 -0
  18. package/src/debug/test/ts-debug-integration.test.ts +374 -0
  19. package/src/debug/test/ts-import-tracker.test.ts +125 -0
  20. package/src/debug/test/ts-source-map.test.ts +169 -0
  21. package/src/debug/ts-import-tracker.ts +151 -0
  22. package/src/debug/ts-source-map.ts +171 -0
  23. package/src/errors/index.ts +124 -0
  24. package/src/index.ts +358 -0
  25. package/src/lexer/index.ts +348 -0
  26. package/src/lexer.ts +2 -0
  27. package/src/parser/index.ts +792 -0
  28. package/src/parser/parse.ts +45 -0
  29. package/src/parser/test/async.test.ts +248 -0
  30. package/src/parser/test/destructuring.test.ts +167 -0
  31. package/src/parser/test/do-expression.test.ts +486 -0
  32. package/src/parser/test/errors/do-expression.test.ts +95 -0
  33. package/src/parser/test/errors/error-locations.test.ts +230 -0
  34. package/src/parser/test/errors/invalid-expressions.test.ts +144 -0
  35. package/src/parser/test/errors/missing-tokens.test.ts +126 -0
  36. package/src/parser/test/errors/model-declaration.test.ts +185 -0
  37. package/src/parser/test/errors/nested-blocks.test.ts +226 -0
  38. package/src/parser/test/errors/unclosed-delimiters.test.ts +122 -0
  39. package/src/parser/test/errors/unexpected-tokens.test.ts +120 -0
  40. package/src/parser/test/import-export.test.ts +143 -0
  41. package/src/parser/test/literals.test.ts +404 -0
  42. package/src/parser/test/model-declaration.test.ts +161 -0
  43. package/src/parser/test/nested-blocks.test.ts +402 -0
  44. package/src/parser/test/parser.test.ts +743 -0
  45. package/src/parser/test/private.test.ts +136 -0
  46. package/src/parser/test/template-literal.test.ts +127 -0
  47. package/src/parser/test/tool-declaration.test.ts +302 -0
  48. package/src/parser/test/ts-block.test.ts +252 -0
  49. package/src/parser/test/type-annotations.test.ts +254 -0
  50. package/src/parser/visitor/helpers.ts +330 -0
  51. package/src/parser/visitor.ts +794 -0
  52. package/src/parser.ts +2 -0
  53. package/src/runtime/ai/cache-chunking.test.ts +69 -0
  54. package/src/runtime/ai/cache-chunking.ts +73 -0
  55. package/src/runtime/ai/client.ts +109 -0
  56. package/src/runtime/ai/context.ts +168 -0
  57. package/src/runtime/ai/formatters.ts +316 -0
  58. package/src/runtime/ai/index.ts +38 -0
  59. package/src/runtime/ai/language-ref.ts +38 -0
  60. package/src/runtime/ai/providers/anthropic.ts +253 -0
  61. package/src/runtime/ai/providers/google.ts +201 -0
  62. package/src/runtime/ai/providers/openai.ts +156 -0
  63. package/src/runtime/ai/retry.ts +100 -0
  64. package/src/runtime/ai/return-tools.ts +301 -0
  65. package/src/runtime/ai/test/client.test.ts +83 -0
  66. package/src/runtime/ai/test/formatters.test.ts +485 -0
  67. package/src/runtime/ai/test/retry.test.ts +137 -0
  68. package/src/runtime/ai/test/return-tools.test.ts +450 -0
  69. package/src/runtime/ai/test/tool-loop.test.ts +319 -0
  70. package/src/runtime/ai/test/tool-schema.test.ts +241 -0
  71. package/src/runtime/ai/tool-loop.ts +203 -0
  72. package/src/runtime/ai/tool-schema.ts +151 -0
  73. package/src/runtime/ai/types.ts +113 -0
  74. package/src/runtime/ai-logger.ts +255 -0
  75. package/src/runtime/ai-provider.ts +347 -0
  76. package/src/runtime/async/dependencies.ts +276 -0
  77. package/src/runtime/async/executor.ts +293 -0
  78. package/src/runtime/async/index.ts +43 -0
  79. package/src/runtime/async/scheduling.ts +163 -0
  80. package/src/runtime/async/test/dependencies.test.ts +284 -0
  81. package/src/runtime/async/test/executor.test.ts +388 -0
  82. package/src/runtime/context.ts +357 -0
  83. package/src/runtime/exec/ai.ts +139 -0
  84. package/src/runtime/exec/expressions.ts +475 -0
  85. package/src/runtime/exec/frames.ts +26 -0
  86. package/src/runtime/exec/functions.ts +305 -0
  87. package/src/runtime/exec/interpolation.ts +312 -0
  88. package/src/runtime/exec/statements.ts +604 -0
  89. package/src/runtime/exec/tools.ts +129 -0
  90. package/src/runtime/exec/typescript.ts +215 -0
  91. package/src/runtime/exec/variables.ts +279 -0
  92. package/src/runtime/index.ts +975 -0
  93. package/src/runtime/modules.ts +452 -0
  94. package/src/runtime/serialize.ts +103 -0
  95. package/src/runtime/state.ts +489 -0
  96. package/src/runtime/stdlib/core.ts +45 -0
  97. package/src/runtime/stdlib/directory.test.ts +156 -0
  98. package/src/runtime/stdlib/edit.test.ts +154 -0
  99. package/src/runtime/stdlib/fastEdit.test.ts +201 -0
  100. package/src/runtime/stdlib/glob.test.ts +106 -0
  101. package/src/runtime/stdlib/grep.test.ts +144 -0
  102. package/src/runtime/stdlib/index.ts +16 -0
  103. package/src/runtime/stdlib/readFile.test.ts +123 -0
  104. package/src/runtime/stdlib/tools/index.ts +707 -0
  105. package/src/runtime/stdlib/writeFile.test.ts +157 -0
  106. package/src/runtime/step.ts +969 -0
  107. package/src/runtime/test/ai-context.test.ts +1086 -0
  108. package/src/runtime/test/ai-result-object.test.ts +419 -0
  109. package/src/runtime/test/ai-tool-flow.test.ts +859 -0
  110. package/src/runtime/test/async-execution-order.test.ts +618 -0
  111. package/src/runtime/test/async-execution.test.ts +344 -0
  112. package/src/runtime/test/async-nested.test.ts +660 -0
  113. package/src/runtime/test/async-parallel-timing.test.ts +546 -0
  114. package/src/runtime/test/basic1.test.ts +154 -0
  115. package/src/runtime/test/binary-operators.test.ts +431 -0
  116. package/src/runtime/test/break-statement.test.ts +257 -0
  117. package/src/runtime/test/context-modes.test.ts +650 -0
  118. package/src/runtime/test/context.test.ts +466 -0
  119. package/src/runtime/test/core-functions.test.ts +228 -0
  120. package/src/runtime/test/e2e.test.ts +88 -0
  121. package/src/runtime/test/error-locations/error-locations.test.ts +80 -0
  122. package/src/runtime/test/error-locations/main-error.vibe +4 -0
  123. package/src/runtime/test/error-locations/main-import-error.vibe +3 -0
  124. package/src/runtime/test/error-locations/utils/helper.vibe +5 -0
  125. package/src/runtime/test/for-in.test.ts +312 -0
  126. package/src/runtime/test/helpers.ts +69 -0
  127. package/src/runtime/test/imports.test.ts +334 -0
  128. package/src/runtime/test/json-expressions.test.ts +232 -0
  129. package/src/runtime/test/literals.test.ts +372 -0
  130. package/src/runtime/test/logical-indexing.test.ts +478 -0
  131. package/src/runtime/test/member-methods.test.ts +324 -0
  132. package/src/runtime/test/model-config.test.ts +338 -0
  133. package/src/runtime/test/null-handling.test.ts +342 -0
  134. package/src/runtime/test/private-visibility.test.ts +332 -0
  135. package/src/runtime/test/runtime-state.test.ts +514 -0
  136. package/src/runtime/test/scoping.test.ts +370 -0
  137. package/src/runtime/test/string-interpolation.test.ts +354 -0
  138. package/src/runtime/test/template-literal.test.ts +181 -0
  139. package/src/runtime/test/tool-execution.test.ts +467 -0
  140. package/src/runtime/test/tool-schema-generation.test.ts +477 -0
  141. package/src/runtime/test/tostring.test.ts +210 -0
  142. package/src/runtime/test/ts-block.test.ts +594 -0
  143. package/src/runtime/test/ts-error-location.test.ts +231 -0
  144. package/src/runtime/test/types.test.ts +732 -0
  145. package/src/runtime/test/verbose-logger.test.ts +710 -0
  146. package/src/runtime/test/vibe-expression.test.ts +54 -0
  147. package/src/runtime/test/vibe-value-errors.test.ts +541 -0
  148. package/src/runtime/test/while.test.ts +232 -0
  149. package/src/runtime/tools/builtin.ts +30 -0
  150. package/src/runtime/tools/directory-tools.ts +70 -0
  151. package/src/runtime/tools/file-tools.ts +228 -0
  152. package/src/runtime/tools/index.ts +5 -0
  153. package/src/runtime/tools/registry.ts +48 -0
  154. package/src/runtime/tools/search-tools.ts +134 -0
  155. package/src/runtime/tools/security.ts +36 -0
  156. package/src/runtime/tools/system-tools.ts +312 -0
  157. package/src/runtime/tools/test/fixtures/base-types.ts +40 -0
  158. package/src/runtime/tools/test/fixtures/test-types.ts +132 -0
  159. package/src/runtime/tools/test/registry.test.ts +713 -0
  160. package/src/runtime/tools/test/security.test.ts +86 -0
  161. package/src/runtime/tools/test/system-tools.test.ts +679 -0
  162. package/src/runtime/tools/test/ts-schema.test.ts +357 -0
  163. package/src/runtime/tools/ts-schema.ts +341 -0
  164. package/src/runtime/tools/types.ts +89 -0
  165. package/src/runtime/tools/utility-tools.ts +198 -0
  166. package/src/runtime/ts-eval.ts +126 -0
  167. package/src/runtime/types.ts +797 -0
  168. package/src/runtime/validation.ts +160 -0
  169. package/src/runtime/verbose-logger.ts +459 -0
  170. package/src/runtime.ts +2 -0
  171. package/src/semantic/analyzer-context.ts +62 -0
  172. package/src/semantic/analyzer-validators.ts +575 -0
  173. package/src/semantic/analyzer-visitors.ts +534 -0
  174. package/src/semantic/analyzer.ts +83 -0
  175. package/src/semantic/index.ts +11 -0
  176. package/src/semantic/symbol-table.ts +58 -0
  177. package/src/semantic/test/async-validation.test.ts +301 -0
  178. package/src/semantic/test/compress-validation.test.ts +179 -0
  179. package/src/semantic/test/const-reassignment.test.ts +111 -0
  180. package/src/semantic/test/control-flow.test.ts +346 -0
  181. package/src/semantic/test/destructuring.test.ts +185 -0
  182. package/src/semantic/test/duplicate-declarations.test.ts +168 -0
  183. package/src/semantic/test/export-validation.test.ts +111 -0
  184. package/src/semantic/test/fixtures/math.ts +31 -0
  185. package/src/semantic/test/imports.test.ts +148 -0
  186. package/src/semantic/test/json-type.test.ts +68 -0
  187. package/src/semantic/test/literals.test.ts +127 -0
  188. package/src/semantic/test/model-validation.test.ts +179 -0
  189. package/src/semantic/test/prompt-validation.test.ts +343 -0
  190. package/src/semantic/test/scoping.test.ts +312 -0
  191. package/src/semantic/test/tool-validation.test.ts +306 -0
  192. package/src/semantic/test/ts-type-checking.test.ts +563 -0
  193. package/src/semantic/test/type-constraints.test.ts +111 -0
  194. package/src/semantic/test/type-inference.test.ts +87 -0
  195. package/src/semantic/test/type-validation.test.ts +552 -0
  196. package/src/semantic/test/undefined-variables.test.ts +163 -0
  197. package/src/semantic/ts-block-checker.ts +204 -0
  198. package/src/semantic/ts-signatures.ts +194 -0
  199. package/src/semantic/ts-types.ts +170 -0
  200. package/src/semantic/types.ts +58 -0
  201. package/tests/fixtures/conditional-logic.vibe +14 -0
  202. package/tests/fixtures/function-call.vibe +16 -0
  203. package/tests/fixtures/imports/cycle-detection/a.vibe +6 -0
  204. package/tests/fixtures/imports/cycle-detection/b.vibe +5 -0
  205. package/tests/fixtures/imports/cycle-detection/main.vibe +3 -0
  206. package/tests/fixtures/imports/module-isolation/main-b.vibe +8 -0
  207. package/tests/fixtures/imports/module-isolation/main.vibe +9 -0
  208. package/tests/fixtures/imports/module-isolation/moduleA.vibe +6 -0
  209. package/tests/fixtures/imports/module-isolation/moduleB.vibe +6 -0
  210. package/tests/fixtures/imports/nested-import/helper.vibe +6 -0
  211. package/tests/fixtures/imports/nested-import/main.vibe +3 -0
  212. package/tests/fixtures/imports/nested-import/utils.ts +3 -0
  213. package/tests/fixtures/imports/nested-isolation/file2.vibe +15 -0
  214. package/tests/fixtures/imports/nested-isolation/file3.vibe +10 -0
  215. package/tests/fixtures/imports/nested-isolation/main.vibe +21 -0
  216. package/tests/fixtures/imports/pure-cycle/a.vibe +5 -0
  217. package/tests/fixtures/imports/pure-cycle/b.vibe +5 -0
  218. package/tests/fixtures/imports/pure-cycle/main.vibe +3 -0
  219. package/tests/fixtures/imports/ts-boolean/checks.ts +14 -0
  220. package/tests/fixtures/imports/ts-boolean/main.vibe +10 -0
  221. package/tests/fixtures/imports/ts-boolean/type-mismatch.vibe +5 -0
  222. package/tests/fixtures/imports/ts-boolean/use-constant.vibe +18 -0
  223. package/tests/fixtures/imports/ts-error-handling/helpers.ts +42 -0
  224. package/tests/fixtures/imports/ts-error-handling/main.vibe +5 -0
  225. package/tests/fixtures/imports/ts-import/main.vibe +4 -0
  226. package/tests/fixtures/imports/ts-import/math.ts +9 -0
  227. package/tests/fixtures/imports/ts-variables/call-non-function.vibe +5 -0
  228. package/tests/fixtures/imports/ts-variables/data.ts +10 -0
  229. package/tests/fixtures/imports/ts-variables/import-json.vibe +5 -0
  230. package/tests/fixtures/imports/ts-variables/import-type-mismatch.vibe +5 -0
  231. package/tests/fixtures/imports/ts-variables/import-variable.vibe +5 -0
  232. package/tests/fixtures/imports/vibe-import/greet.vibe +5 -0
  233. package/tests/fixtures/imports/vibe-import/main.vibe +3 -0
  234. package/tests/fixtures/multiple-ai-calls.vibe +10 -0
  235. package/tests/fixtures/simple-greeting.vibe +6 -0
  236. package/tests/fixtures/template-literals.vibe +11 -0
  237. package/tests/integration/basic-ai/basic-ai.integration.test.ts +166 -0
  238. package/tests/integration/basic-ai/basic-ai.vibe +12 -0
  239. package/tests/integration/bug-fix/bug-fix.integration.test.ts +201 -0
  240. package/tests/integration/bug-fix/buggy-code.ts +22 -0
  241. package/tests/integration/bug-fix/fix-bug.vibe +21 -0
  242. package/tests/integration/compress/compress.integration.test.ts +206 -0
  243. package/tests/integration/destructuring/destructuring.integration.test.ts +92 -0
  244. package/tests/integration/hello-world-translator/hello-world-translator.integration.test.ts +61 -0
  245. package/tests/integration/line-annotator/context-modes.integration.test.ts +261 -0
  246. package/tests/integration/line-annotator/line-annotator.integration.test.ts +148 -0
  247. package/tests/integration/multi-feature/cumulative-sum.integration.test.ts +75 -0
  248. package/tests/integration/multi-feature/number-analyzer.integration.test.ts +191 -0
  249. package/tests/integration/multi-feature/number-analyzer.vibe +59 -0
  250. package/tests/integration/tool-calls/tool-calls.integration.test.ts +93 -0
@@ -0,0 +1,357 @@
1
+ import { describe, expect, test, beforeAll } from 'bun:test';
2
+ import { vibeTypeToJsonSchema, extractTypeSchema, createTypeExtractor, clearSchemaCache } from '../ts-schema';
3
+ import type { TypeExtractor } from '../ts-schema';
4
+ import { join } from 'path';
5
+
6
+ describe('vibeTypeToJsonSchema', () => {
7
+ // ============================================================================
8
+ // Primitive types
9
+ // ============================================================================
10
+
11
+ test('converts text type to string schema', () => {
12
+ const schema = vibeTypeToJsonSchema('text');
13
+ expect(schema).toEqual({ type: 'string' });
14
+ });
15
+
16
+ test('converts prompt type to string schema', () => {
17
+ const schema = vibeTypeToJsonSchema('prompt');
18
+ expect(schema).toEqual({ type: 'string' });
19
+ });
20
+
21
+ test('converts number type to number schema', () => {
22
+ const schema = vibeTypeToJsonSchema('number');
23
+ expect(schema).toEqual({ type: 'number' });
24
+ });
25
+
26
+ test('converts boolean type to boolean schema', () => {
27
+ const schema = vibeTypeToJsonSchema('boolean');
28
+ expect(schema).toEqual({ type: 'boolean' });
29
+ });
30
+
31
+ test('converts json type to object schema', () => {
32
+ const schema = vibeTypeToJsonSchema('json');
33
+ expect(schema).toEqual({ type: 'object', additionalProperties: true });
34
+ });
35
+
36
+ // ============================================================================
37
+ // Array types
38
+ // ============================================================================
39
+
40
+ test('converts text[] to array of strings', () => {
41
+ const schema = vibeTypeToJsonSchema('text[]');
42
+ expect(schema).toEqual({
43
+ type: 'array',
44
+ items: { type: 'string' },
45
+ });
46
+ });
47
+
48
+ test('converts number[] to array of numbers', () => {
49
+ const schema = vibeTypeToJsonSchema('number[]');
50
+ expect(schema).toEqual({
51
+ type: 'array',
52
+ items: { type: 'number' },
53
+ });
54
+ });
55
+
56
+ test('converts boolean[] to array of booleans', () => {
57
+ const schema = vibeTypeToJsonSchema('boolean[]');
58
+ expect(schema).toEqual({
59
+ type: 'array',
60
+ items: { type: 'boolean' },
61
+ });
62
+ });
63
+
64
+ test('converts json[] to array of objects', () => {
65
+ const schema = vibeTypeToJsonSchema('json[]');
66
+ expect(schema).toEqual({
67
+ type: 'array',
68
+ items: { type: 'object', additionalProperties: true },
69
+ });
70
+ });
71
+
72
+ // ============================================================================
73
+ // Unknown types
74
+ // ============================================================================
75
+
76
+ test('returns generic object for unknown type', () => {
77
+ const schema = vibeTypeToJsonSchema('UnknownType');
78
+ expect(schema).toEqual({ type: 'object', additionalProperties: true });
79
+ });
80
+
81
+ test('falls back to generic object for unmapped type', () => {
82
+ const importedTypes = new Map<string, string>();
83
+ const schema = vibeTypeToJsonSchema('UnknownType', importedTypes);
84
+ expect(schema).toEqual({ type: 'object', additionalProperties: true });
85
+ });
86
+ });
87
+
88
+ // ============================================================================
89
+ // TypeScript type extraction tests using fixtures
90
+ // Compiles once in beforeAll for efficiency (~1s instead of ~10s)
91
+ // ============================================================================
92
+
93
+ describe('extractTypeSchema with fixtures', () => {
94
+ const fixturesDir = join(__dirname, 'fixtures');
95
+ const testTypesFile = join(fixturesDir, 'test-types.ts');
96
+ const baseTypesFile = join(fixturesDir, 'base-types.ts');
97
+
98
+ let extractor: TypeExtractor;
99
+
100
+ beforeAll(() => {
101
+ clearSchemaCache();
102
+ // Compile both files once - this is where the time is spent
103
+ extractor = createTypeExtractor([testTypesFile, baseTypesFile]);
104
+ });
105
+
106
+ // ============================================================================
107
+ // Simple primitives from test-types.ts
108
+ // ============================================================================
109
+
110
+ test('extracts Person interface with primitives', () => {
111
+ const schema = extractor.extract('Person');
112
+ expect(schema).toEqual({
113
+ type: 'object',
114
+ properties: {
115
+ name: { type: 'string' },
116
+ age: { type: 'number' },
117
+ active: { type: 'boolean' },
118
+ },
119
+ required: ['name', 'age', 'active'],
120
+ });
121
+ });
122
+
123
+ test('extracts Config interface with optional properties', () => {
124
+ const schema = extractor.extract('Config');
125
+ expect(schema.properties).toHaveProperty('required');
126
+ expect(schema.properties).toHaveProperty('optional');
127
+ expect(schema.required).toContain('required');
128
+ expect(schema.required).not.toContain('optional');
129
+ });
130
+
131
+ test('extracts Container interface with array property', () => {
132
+ const schema = extractor.extract('Container');
133
+ expect(schema.properties?.items).toEqual({
134
+ type: 'array',
135
+ items: { type: 'string' },
136
+ });
137
+ });
138
+
139
+ test('extracts Status type alias', () => {
140
+ const schema = extractor.extract('Status');
141
+ expect(schema).toEqual({ type: 'string' });
142
+ });
143
+
144
+ // ============================================================================
145
+ // JSDoc extraction
146
+ // ============================================================================
147
+
148
+ test('extracts JSDoc descriptions from Documented interface', () => {
149
+ const schema = extractor.extract('Documented');
150
+ expect(schema.properties?.id.description).toBe("The user's unique identifier");
151
+ expect(schema.properties?.name.description).toBe("The user's display name");
152
+ });
153
+
154
+ // ============================================================================
155
+ // Types from base-types.ts (imported by test-types.ts)
156
+ // ============================================================================
157
+
158
+ test('extracts Address interface from base-types', () => {
159
+ const schema = extractor.extract('Address');
160
+ expect(schema).toMatchObject({
161
+ type: 'object',
162
+ properties: {
163
+ street: { type: 'string' },
164
+ city: { type: 'string' },
165
+ zipCode: { type: 'string' },
166
+ country: { type: 'string' },
167
+ },
168
+ });
169
+ expect(schema.required).toContain('street');
170
+ expect(schema.required).toContain('city');
171
+ expect(schema.required).not.toContain('country'); // optional
172
+ });
173
+
174
+ test('extracts ContactInfo with nested Address', () => {
175
+ const schema = extractor.extract('ContactInfo');
176
+ expect(schema.properties?.email).toEqual({ type: 'string' });
177
+ expect(schema.properties?.address).toMatchObject({
178
+ type: 'object',
179
+ properties: {
180
+ street: { type: 'string' },
181
+ city: { type: 'string' },
182
+ },
183
+ });
184
+ });
185
+
186
+ test('extracts Metadata interface with JSDoc and optional array', () => {
187
+ const schema = extractor.extract('Metadata');
188
+ expect(schema.properties?.createdAt.description).toBe('When the entity was created');
189
+ expect(schema.properties?.updatedAt.description).toBe('When the entity was last updated');
190
+ expect(schema.properties?.tags).toMatchObject({
191
+ type: 'array',
192
+ items: { type: 'string' },
193
+ });
194
+ expect(schema.required).not.toContain('tags'); // optional
195
+ });
196
+
197
+ test('extracts OrderItem interface', () => {
198
+ const schema = extractor.extract('OrderItem');
199
+ expect(schema).toMatchObject({
200
+ type: 'object',
201
+ properties: {
202
+ productId: { type: 'string' },
203
+ productName: { type: 'string' },
204
+ quantity: { type: 'number' },
205
+ unitPrice: { type: 'number' },
206
+ },
207
+ });
208
+ });
209
+
210
+ // ============================================================================
211
+ // Complex types using imports (test-types.ts using base-types.ts)
212
+ // ============================================================================
213
+
214
+ test('extracts User with imported ContactInfo and Metadata', () => {
215
+ const schema = extractor.extract('User');
216
+ expect(schema.properties?.id).toEqual({ type: 'string' });
217
+ expect(schema.properties?.username).toEqual({ type: 'string' });
218
+ expect(schema.properties?.contact).toMatchObject({
219
+ type: 'object',
220
+ properties: {
221
+ email: { type: 'string' },
222
+ },
223
+ });
224
+ expect(schema.properties?.metadata).toMatchObject({
225
+ type: 'object',
226
+ properties: {
227
+ createdAt: { type: 'string' },
228
+ },
229
+ });
230
+ });
231
+
232
+ test('extracts Customer with optional shippingAddress', () => {
233
+ const schema = extractor.extract('Customer');
234
+ expect(schema.required).toContain('billingAddress');
235
+ expect(schema.required).not.toContain('shippingAddress'); // optional
236
+ });
237
+
238
+ test('extracts Order with array of OrderItems', () => {
239
+ const schema = extractor.extract('Order');
240
+ expect(schema.properties?.items).toMatchObject({
241
+ type: 'array',
242
+ items: {
243
+ type: 'object',
244
+ properties: {
245
+ productId: { type: 'string' },
246
+ quantity: { type: 'number' },
247
+ },
248
+ },
249
+ });
250
+ });
251
+
252
+ // ============================================================================
253
+ // Array types interface
254
+ // ============================================================================
255
+
256
+ test('extracts ArrayTypes with various array properties', () => {
257
+ const schema = extractor.extract('ArrayTypes');
258
+ expect(schema.properties?.strings).toEqual({ type: 'array', items: { type: 'string' } });
259
+ expect(schema.properties?.numbers).toEqual({ type: 'array', items: { type: 'number' } });
260
+ expect(schema.properties?.booleans).toEqual({ type: 'array', items: { type: 'boolean' } });
261
+ expect(schema.properties?.nested).toMatchObject({
262
+ type: 'array',
263
+ items: { type: 'object' },
264
+ });
265
+ });
266
+
267
+ // ============================================================================
268
+ // Optional-only types
269
+ // ============================================================================
270
+
271
+ test('extracts AllOptional with no required fields', () => {
272
+ const schema = extractor.extract('AllOptional');
273
+ expect(schema.required).toBeUndefined();
274
+ });
275
+
276
+ // ============================================================================
277
+ // Error handling
278
+ // ============================================================================
279
+
280
+ test('throws error for non-existent type', () => {
281
+ expect(() => extractor.extract('NonExistentType')).toThrow(
282
+ "Type 'NonExistentType' not found"
283
+ );
284
+ });
285
+ });
286
+
287
+ // ============================================================================
288
+ // Tests for extractTypeSchema (the single-file API with caching)
289
+ // ============================================================================
290
+
291
+ describe('extractTypeSchema caching', () => {
292
+ const fixturesDir = join(__dirname, 'fixtures');
293
+ const testTypesFile = join(fixturesDir, 'test-types.ts');
294
+
295
+ test('caches schema for same file and type', () => {
296
+ clearSchemaCache();
297
+
298
+ // First call - compiles the file
299
+ const schema1 = extractTypeSchema(testTypesFile, 'Person');
300
+
301
+ // Second call - should use cache
302
+ const schema2 = extractTypeSchema(testTypesFile, 'Person');
303
+
304
+ expect(schema1).toEqual(schema2);
305
+ });
306
+
307
+ test('throws error for non-existent file', () => {
308
+ expect(() => extractTypeSchema('/nonexistent/path/file.ts', 'SomeType')).toThrow();
309
+ });
310
+
311
+ test('throws error for non-existent type in file', () => {
312
+ expect(() => extractTypeSchema(testTypesFile, 'NonExistentType')).toThrow(
313
+ "Type 'NonExistentType' not found"
314
+ );
315
+ });
316
+ });
317
+
318
+ // ============================================================================
319
+ // vibeTypeToJsonSchema with imported types (uses fixture file)
320
+ // ============================================================================
321
+
322
+ describe('vibeTypeToJsonSchema with imported types', () => {
323
+ const fixturesDir = join(__dirname, 'fixtures');
324
+ const testTypesFile = join(fixturesDir, 'test-types.ts');
325
+
326
+ test('resolves imported type from map', () => {
327
+ clearSchemaCache();
328
+ const importedTypes = new Map<string, string>();
329
+ importedTypes.set('Person', testTypesFile);
330
+
331
+ const schema = vibeTypeToJsonSchema('Person', importedTypes);
332
+ expect(schema).toMatchObject({
333
+ type: 'object',
334
+ properties: {
335
+ name: { type: 'string' },
336
+ age: { type: 'number' },
337
+ active: { type: 'boolean' },
338
+ },
339
+ });
340
+ });
341
+
342
+ test('handles array of imported type', () => {
343
+ const importedTypes = new Map<string, string>();
344
+ importedTypes.set('Person', testTypesFile);
345
+
346
+ const schema = vibeTypeToJsonSchema('Person[]', importedTypes);
347
+ expect(schema).toMatchObject({
348
+ type: 'array',
349
+ items: {
350
+ type: 'object',
351
+ properties: {
352
+ name: { type: 'string' },
353
+ },
354
+ },
355
+ });
356
+ });
357
+ });
@@ -0,0 +1,341 @@
1
+ import ts from 'typescript';
2
+ import type { JsonSchema } from './types';
3
+
4
+ // Cache for extracted schemas to avoid re-parsing the same file
5
+ const schemaCache = new Map<string, Map<string, JsonSchema>>();
6
+
7
+ /**
8
+ * Extract JSON Schema from a TypeScript type definition.
9
+ *
10
+ * @param sourceFile - Path to the TypeScript source file
11
+ * @param typeName - Name of the type/interface to extract
12
+ * @returns JSON Schema representation of the type
13
+ */
14
+ export function extractTypeSchema(sourceFile: string, typeName: string): JsonSchema {
15
+ // Check cache first
16
+ const fileCache = schemaCache.get(sourceFile);
17
+ if (fileCache?.has(typeName)) {
18
+ return fileCache.get(typeName)!;
19
+ }
20
+
21
+ // Create TypeScript program to analyze the file
22
+ const program = ts.createProgram([sourceFile], {
23
+ target: ts.ScriptTarget.ESNext,
24
+ module: ts.ModuleKind.ESNext,
25
+ strict: true,
26
+ });
27
+
28
+ const checker = program.getTypeChecker();
29
+ const source = program.getSourceFile(sourceFile);
30
+
31
+ if (!source) {
32
+ throw new Error(`Could not load source file: ${sourceFile}`);
33
+ }
34
+
35
+ // Find the type declaration
36
+ const typeSymbol = findTypeSymbol(source, typeName, checker);
37
+ if (!typeSymbol) {
38
+ throw new Error(`Type '${typeName}' not found in ${sourceFile}`);
39
+ }
40
+
41
+ const type = checker.getDeclaredTypeOfSymbol(typeSymbol);
42
+ const schema = typeToJsonSchema(type, checker, new Set());
43
+
44
+ // Cache the result
45
+ if (!schemaCache.has(sourceFile)) {
46
+ schemaCache.set(sourceFile, new Map());
47
+ }
48
+ schemaCache.get(sourceFile)!.set(typeName, schema);
49
+
50
+ return schema;
51
+ }
52
+
53
+ /**
54
+ * Find a type symbol by name in a source file.
55
+ */
56
+ function findTypeSymbol(
57
+ source: ts.SourceFile,
58
+ typeName: string,
59
+ checker: ts.TypeChecker
60
+ ): ts.Symbol | undefined {
61
+ let result: ts.Symbol | undefined;
62
+
63
+ function visit(node: ts.Node) {
64
+ if (result) return;
65
+
66
+ if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
67
+ if (node.name.text === typeName) {
68
+ result = checker.getSymbolAtLocation(node.name);
69
+ }
70
+ }
71
+
72
+ ts.forEachChild(node, visit);
73
+ }
74
+
75
+ visit(source);
76
+ return result;
77
+ }
78
+
79
+ /**
80
+ * Convert a TypeScript type to JSON Schema.
81
+ *
82
+ * @param type - The TypeScript type to convert
83
+ * @param checker - TypeScript type checker
84
+ * @param visited - Set of visited type IDs to prevent infinite recursion
85
+ */
86
+ function typeToJsonSchema(
87
+ type: ts.Type,
88
+ checker: ts.TypeChecker,
89
+ visited: Set<number>
90
+ ): JsonSchema {
91
+ // Prevent infinite recursion for recursive types
92
+ const typeId = (type as { id?: number }).id;
93
+ if (typeId !== undefined && visited.has(typeId)) {
94
+ return { type: 'object', additionalProperties: true };
95
+ }
96
+ if (typeId !== undefined) {
97
+ visited.add(typeId);
98
+ }
99
+
100
+ // Handle primitive types
101
+ if (type.flags & ts.TypeFlags.String) {
102
+ return { type: 'string' };
103
+ }
104
+ if (type.flags & ts.TypeFlags.Number) {
105
+ return { type: 'number' };
106
+ }
107
+ if (type.flags & ts.TypeFlags.Boolean) {
108
+ return { type: 'boolean' };
109
+ }
110
+ if (type.flags & ts.TypeFlags.Null || type.flags & ts.TypeFlags.Undefined) {
111
+ return { type: 'object' }; // JSON doesn't have null type, use object
112
+ }
113
+
114
+ // Handle string literal types
115
+ if (type.flags & ts.TypeFlags.StringLiteral) {
116
+ return { type: 'string' };
117
+ }
118
+
119
+ // Handle number literal types
120
+ if (type.flags & ts.TypeFlags.NumberLiteral) {
121
+ return { type: 'number' };
122
+ }
123
+
124
+ // Handle boolean literal types
125
+ if (type.flags & ts.TypeFlags.BooleanLiteral) {
126
+ return { type: 'boolean' };
127
+ }
128
+
129
+ // Handle union types (e.g., string | number, or T | undefined for optional)
130
+ if (type.flags & ts.TypeFlags.Union) {
131
+ const unionType = type as ts.UnionType;
132
+
133
+ // Filter out undefined and null from the union (common for optional types)
134
+ const nonNullTypes = unionType.types.filter(
135
+ (t) => !(t.flags & ts.TypeFlags.Undefined) && !(t.flags & ts.TypeFlags.Null)
136
+ );
137
+
138
+ // If only one type remains after filtering, use that
139
+ if (nonNullTypes.length === 1) {
140
+ return typeToJsonSchema(nonNullTypes[0], checker, new Set(visited));
141
+ }
142
+
143
+ // For remaining types, try to find a common type
144
+ const types = nonNullTypes.map((t) => typeToJsonSchema(t, checker, new Set(visited)));
145
+
146
+ // If all types are the same, return that type
147
+ const typeSet = new Set(types.map((t) => t.type));
148
+ if (typeSet.size === 1) {
149
+ return types[0];
150
+ }
151
+
152
+ // Otherwise, return a generic object
153
+ return { type: 'object', additionalProperties: true };
154
+ }
155
+
156
+ // Handle arrays
157
+ if (checker.isArrayType(type)) {
158
+ const typeRef = type as ts.TypeReference;
159
+ const elementType = typeRef.typeArguments?.[0];
160
+ if (elementType) {
161
+ return {
162
+ type: 'array',
163
+ items: typeToJsonSchema(elementType, checker, new Set(visited)),
164
+ };
165
+ }
166
+ return { type: 'array' };
167
+ }
168
+
169
+ // Handle tuples as arrays
170
+ if (checker.isTupleType(type)) {
171
+ const tupleType = type as ts.TupleType;
172
+ // Use the first element type or object for mixed tuples
173
+ if (tupleType.typeArguments && tupleType.typeArguments.length > 0) {
174
+ return {
175
+ type: 'array',
176
+ items: typeToJsonSchema(tupleType.typeArguments[0], checker, new Set(visited)),
177
+ };
178
+ }
179
+ return { type: 'array' };
180
+ }
181
+
182
+ // Handle objects/interfaces
183
+ if (type.flags & ts.TypeFlags.Object) {
184
+ const objectType = type as ts.ObjectType;
185
+
186
+ // Check if it's a function type
187
+ const callSignatures = type.getCallSignatures();
188
+ if (callSignatures.length > 0) {
189
+ return { type: 'object', additionalProperties: true };
190
+ }
191
+
192
+ const properties: Record<string, JsonSchema> = {};
193
+ const required: string[] = [];
194
+
195
+ const typeProperties = objectType.getProperties();
196
+ for (const prop of typeProperties) {
197
+ const propType = checker.getTypeOfSymbol(prop);
198
+ const propSchema = typeToJsonSchema(propType, checker, new Set(visited));
199
+
200
+ // Get JSDoc comment if available
201
+ const jsDocComment = prop.getDocumentationComment(checker);
202
+ if (jsDocComment.length > 0) {
203
+ propSchema.description = jsDocComment.map((c) => c.text).join('\n');
204
+ }
205
+
206
+ properties[prop.name] = propSchema;
207
+
208
+ // Check if property is required (not optional)
209
+ if (!(prop.flags & ts.SymbolFlags.Optional)) {
210
+ required.push(prop.name);
211
+ }
212
+ }
213
+
214
+ return {
215
+ type: 'object',
216
+ properties,
217
+ required: required.length > 0 ? required : undefined,
218
+ };
219
+ }
220
+
221
+ // Default fallback
222
+ return { type: 'object', additionalProperties: true };
223
+ }
224
+
225
+ /**
226
+ * Convert a Vibe type annotation to JSON Schema.
227
+ * Handles both built-in types and imported TS type names.
228
+ *
229
+ * @param vibeType - The Vibe type annotation (e.g., "text", "CustomerInfo", "Order[]")
230
+ * @param importedTypes - Map of type name to source file for resolving imported types
231
+ */
232
+ export function vibeTypeToJsonSchema(
233
+ vibeType: string,
234
+ importedTypes?: Map<string, string>
235
+ ): JsonSchema {
236
+ // Handle array types
237
+ if (vibeType.endsWith('[]')) {
238
+ const elementType = vibeType.slice(0, -2);
239
+ return {
240
+ type: 'array',
241
+ items: vibeTypeToJsonSchema(elementType, importedTypes),
242
+ };
243
+ }
244
+
245
+ // Handle built-in Vibe types
246
+ switch (vibeType) {
247
+ case 'text':
248
+ case 'prompt':
249
+ return { type: 'string' };
250
+ case 'number':
251
+ return { type: 'number' };
252
+ case 'boolean':
253
+ return { type: 'boolean' };
254
+ case 'json':
255
+ return { type: 'object', additionalProperties: true };
256
+ }
257
+
258
+ // Handle imported TS type
259
+ if (importedTypes?.has(vibeType)) {
260
+ const sourceFile = importedTypes.get(vibeType)!;
261
+ return extractTypeSchema(sourceFile, vibeType);
262
+ }
263
+
264
+ // Unknown type - return generic object
265
+ return { type: 'object', additionalProperties: true };
266
+ }
267
+
268
+ /**
269
+ * Clear the schema cache (useful for testing or when files change).
270
+ */
271
+ export function clearSchemaCache(): void {
272
+ schemaCache.clear();
273
+ }
274
+
275
+ /**
276
+ * A type extractor that has already compiled a TypeScript program.
277
+ * Use this when you need to extract multiple types from the same file(s)
278
+ * to avoid recompiling for each extraction.
279
+ */
280
+ export interface TypeExtractor {
281
+ /**
282
+ * Extract a type by name from any of the compiled source files.
283
+ */
284
+ extract(typeName: string): JsonSchema;
285
+
286
+ /**
287
+ * Get the type checker for advanced use cases.
288
+ */
289
+ getChecker(): ts.TypeChecker;
290
+ }
291
+
292
+ /**
293
+ * Create a type extractor by compiling TypeScript files once.
294
+ * Much more efficient than calling extractTypeSchema multiple times.
295
+ *
296
+ * @param sourceFiles - Array of TypeScript file paths to compile
297
+ * @returns A TypeExtractor that can extract types without recompiling
298
+ */
299
+ export function createTypeExtractor(sourceFiles: string[]): TypeExtractor {
300
+ const program = ts.createProgram(sourceFiles, {
301
+ target: ts.ScriptTarget.ESNext,
302
+ module: ts.ModuleKind.ESNext,
303
+ moduleResolution: ts.ModuleResolutionKind.NodeJs,
304
+ strict: true,
305
+ });
306
+
307
+ const checker = program.getTypeChecker();
308
+ const sources = sourceFiles.map((f) => program.getSourceFile(f)).filter(Boolean) as ts.SourceFile[];
309
+
310
+ // Build a map of all exported types across all source files
311
+ const typeSymbols = new Map<string, ts.Symbol>();
312
+
313
+ for (const source of sources) {
314
+ function visit(node: ts.Node) {
315
+ if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
316
+ const symbol = checker.getSymbolAtLocation(node.name);
317
+ if (symbol) {
318
+ typeSymbols.set(node.name.text, symbol);
319
+ }
320
+ }
321
+ ts.forEachChild(node, visit);
322
+ }
323
+ visit(source);
324
+ }
325
+
326
+ return {
327
+ extract(typeName: string): JsonSchema {
328
+ const symbol = typeSymbols.get(typeName);
329
+ if (!symbol) {
330
+ throw new Error(`Type '${typeName}' not found in compiled sources`);
331
+ }
332
+
333
+ const type = checker.getDeclaredTypeOfSymbol(symbol);
334
+ return typeToJsonSchema(type, checker, new Set());
335
+ },
336
+
337
+ getChecker(): ts.TypeChecker {
338
+ return checker;
339
+ },
340
+ };
341
+ }