@specferret/core 0.1.3 → 0.2.0

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 (94) hide show
  1. package/README.md +62 -62
  2. package/dist/context/index.d.ts +5 -1
  3. package/dist/context/index.d.ts.map +1 -1
  4. package/dist/context/index.js +47 -1
  5. package/dist/context/index.js.map +1 -1
  6. package/dist/extractor/__fixtures__/typescript/generics.d.ts +6 -0
  7. package/dist/extractor/__fixtures__/typescript/generics.d.ts.map +1 -0
  8. package/dist/extractor/__fixtures__/typescript/generics.js +2 -0
  9. package/dist/extractor/__fixtures__/typescript/generics.js.map +1 -0
  10. package/dist/extractor/__fixtures__/typescript/intersections.d.ts +6 -0
  11. package/dist/extractor/__fixtures__/typescript/intersections.d.ts.map +1 -0
  12. package/dist/extractor/__fixtures__/typescript/intersections.js +2 -0
  13. package/dist/extractor/__fixtures__/typescript/intersections.js.map +1 -0
  14. package/dist/extractor/__fixtures__/typescript/optional-nested.d.ts +8 -0
  15. package/dist/extractor/__fixtures__/typescript/optional-nested.d.ts.map +1 -0
  16. package/dist/extractor/__fixtures__/typescript/optional-nested.js +2 -0
  17. package/dist/extractor/__fixtures__/typescript/optional-nested.js.map +1 -0
  18. package/dist/extractor/__fixtures__/typescript/unions.d.ts +5 -0
  19. package/dist/extractor/__fixtures__/typescript/unions.d.ts.map +1 -0
  20. package/dist/extractor/__fixtures__/typescript/unions.js +2 -0
  21. package/dist/extractor/__fixtures__/typescript/unions.js.map +1 -0
  22. package/dist/extractor/__fixtures__/typescript/unsupported-syntax.d.ts +7 -0
  23. package/dist/extractor/__fixtures__/typescript/unsupported-syntax.d.ts.map +1 -0
  24. package/dist/extractor/__fixtures__/typescript/unsupported-syntax.js +2 -0
  25. package/dist/extractor/__fixtures__/typescript/unsupported-syntax.js.map +1 -0
  26. package/dist/extractor/contract-types.d.ts +5 -0
  27. package/dist/extractor/contract-types.d.ts.map +1 -0
  28. package/dist/extractor/contract-types.js +8 -0
  29. package/dist/extractor/contract-types.js.map +1 -0
  30. package/dist/extractor/frontmatter.d.ts +6 -1
  31. package/dist/extractor/frontmatter.d.ts.map +1 -1
  32. package/dist/extractor/frontmatter.js +17 -6
  33. package/dist/extractor/frontmatter.js.map +1 -1
  34. package/dist/extractor/typescript.d.ts +6 -1
  35. package/dist/extractor/typescript.d.ts.map +1 -1
  36. package/dist/extractor/typescript.js +403 -185
  37. package/dist/extractor/typescript.js.map +1 -1
  38. package/dist/extractor/upward-classifier.d.ts +25 -0
  39. package/dist/extractor/upward-classifier.d.ts.map +1 -0
  40. package/dist/extractor/upward-classifier.js +48 -0
  41. package/dist/extractor/upward-classifier.js.map +1 -0
  42. package/dist/extractor/validator.d.ts +9 -0
  43. package/dist/extractor/validator.d.ts.map +1 -1
  44. package/dist/extractor/validator.js +28 -9
  45. package/dist/extractor/validator.js.map +1 -1
  46. package/dist/index.d.ts +14 -12
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +14 -12
  49. package/dist/index.js.map +1 -1
  50. package/dist/reconciler/index.d.ts +10 -0
  51. package/dist/reconciler/index.d.ts.map +1 -1
  52. package/dist/reconciler/index.js +76 -0
  53. package/dist/reconciler/index.js.map +1 -1
  54. package/dist/store/sqlite.d.ts +1 -1
  55. package/dist/store/sqlite.d.ts.map +1 -1
  56. package/dist/store/sqlite.js +106 -101
  57. package/dist/store/sqlite.js.map +1 -1
  58. package/dist/store/types.d.ts +6 -2
  59. package/dist/store/types.d.ts.map +1 -1
  60. package/package.json +47 -44
  61. package/src/config.ts +48 -0
  62. package/src/context/index.test.ts +274 -0
  63. package/src/context/index.ts +178 -0
  64. package/src/extractor/__fixtures__/typescript/generics.golden.json +36 -0
  65. package/src/extractor/__fixtures__/typescript/generics.ts +5 -0
  66. package/src/extractor/__fixtures__/typescript/intersections.golden.json +18 -0
  67. package/src/extractor/__fixtures__/typescript/intersections.ts +5 -0
  68. package/src/extractor/__fixtures__/typescript/optional-nested.golden.json +41 -0
  69. package/src/extractor/__fixtures__/typescript/optional-nested.ts +7 -0
  70. package/src/extractor/__fixtures__/typescript/unions.golden.json +30 -0
  71. package/src/extractor/__fixtures__/typescript/unions.ts +4 -0
  72. package/src/extractor/__fixtures__/typescript/unsupported-syntax.golden.json +20 -0
  73. package/src/extractor/__fixtures__/typescript/unsupported-syntax.ts +6 -0
  74. package/src/extractor/contract-types.ts +11 -0
  75. package/src/extractor/frontmatter.test.ts +291 -0
  76. package/src/extractor/frontmatter.ts +87 -0
  77. package/src/extractor/hash.ts +26 -0
  78. package/src/extractor/typescript.test.ts +339 -0
  79. package/src/extractor/typescript.ts +597 -0
  80. package/src/extractor/upward-classifier.test.ts +188 -0
  81. package/src/extractor/upward-classifier.ts +68 -0
  82. package/src/extractor/validator.test.ts +228 -0
  83. package/src/extractor/validator.ts +199 -0
  84. package/src/index.ts +17 -0
  85. package/src/reconciler/import-suggestions.test.ts +101 -0
  86. package/src/reconciler/import-suggestions.ts +157 -0
  87. package/src/reconciler/index.test.ts +248 -0
  88. package/src/reconciler/index.ts +445 -0
  89. package/src/store/factory.ts +54 -0
  90. package/src/store/postgres.ts +84 -0
  91. package/src/store/sqlite.test.ts +328 -0
  92. package/src/store/sqlite.ts +231 -0
  93. package/src/store/types.ts +101 -0
  94. package/src/utils/paths.ts +24 -0
@@ -1,153 +1,374 @@
1
- class TypeParser {
2
- input;
3
- i = 0;
4
- constructor(input) {
5
- this.input = input;
6
- }
7
- parseObject() {
8
- const diagnostics = [];
9
- const object = this.parseObjectType(diagnostics);
10
- return { shape: object, diagnostics };
11
- }
12
- parseObjectType(diagnostics) {
13
- this.skipWs();
14
- if (this.peek() !== "{") {
15
- diagnostics.push("Expected object type starting with '{'.");
16
- return { type: "object", properties: {}, required: [] };
17
- }
18
- this.i++; // '{'
19
- const properties = {};
20
- const required = [];
21
- while (this.i < this.input.length) {
22
- this.skipWs();
23
- if (this.peek() === "}") {
24
- this.i++; // '}'
25
- break;
26
- }
27
- const name = this.readIdentifier();
28
- if (!name) {
29
- diagnostics.push("Unable to parse property name in object type.");
30
- this.advanceToDelimiter();
31
- continue;
32
- }
33
- this.skipWs();
34
- let optional = false;
35
- if (this.peek() === "?") {
36
- optional = true;
37
- this.i++;
38
- }
39
- this.skipWs();
40
- if (this.peek() !== ":") {
41
- diagnostics.push(`Expected ':' after property '${name}'.`);
42
- this.advanceToDelimiter();
43
- continue;
44
- }
45
- this.i++; // ':'
46
- this.skipWs();
47
- const schema = this.parseTypeExpr(diagnostics);
48
- properties[name] = schema;
49
- if (!optional) {
50
- required.push(name);
51
- }
52
- this.skipWs();
53
- if (this.peek() === ";" || this.peek() === ",") {
54
- this.i++;
55
- }
1
+ import Parser from 'tree-sitter';
2
+ import tsLanguage from 'tree-sitter-typescript';
3
+ import { CONTRACT_TYPES } from './contract-types.js';
4
+ const parser = new Parser();
5
+ parser.setLanguage(tsLanguage.typescript);
6
+ function normalizeIdSegment(value) {
7
+ return value
8
+ .toLowerCase()
9
+ .replace(/[^a-z0-9/._-]+/g, '-')
10
+ .replace(/-+/g, '-')
11
+ .replace(/(^[-./_]+)|([-./_]+$)/g, '');
12
+ }
13
+ function normalizeFilePathForId(filePath) {
14
+ const normalized = filePath.replace(/\\/g, '/').replace(/\.[cm]?tsx?$/i, '');
15
+ // Prefer repository-relative paths for deterministic ids. When absolute
16
+ // paths are passed, keep from the first src/ segment when available.
17
+ if (/^[a-z]:\//i.test(normalized) || normalized.startsWith('/')) {
18
+ const segments = normalized.split('/').filter(Boolean);
19
+ const srcIndex = segments.findIndex((segment) => segment.toLowerCase() === 'src');
20
+ if (srcIndex >= 0) {
21
+ return segments.slice(srcIndex).join('/');
22
+ }
23
+ // Fallback to the last 3 path segments to reduce machine-specific prefixes.
24
+ return segments.slice(-3).join('/');
25
+ }
26
+ return normalized;
27
+ }
28
+ function inferContractType() {
29
+ // Inferred extraction currently emits the strict six-core-type set and
30
+ // defaults to `type` unless an annotation override supplies another type.
31
+ return 'type';
32
+ }
33
+ function inferContractId(filePath, symbol) {
34
+ const normalizedPath = normalizeFilePathForId(filePath);
35
+ const pathPart = normalizeIdSegment(normalizedPath) || 'source';
36
+ const symbolPart = normalizeIdSegment(symbol) || 'contract';
37
+ return `type.${pathPart}/${symbolPart}`;
38
+ }
39
+ function allocateDeterministicInferredId(baseId, seenCounts, allocatedIds, diagnostics, filePath, symbol) {
40
+ let count = (seenCounts.get(baseId) ?? 0) + 1;
41
+ let allocatedId = count === 1 ? baseId : `${baseId}-${count}`;
42
+ while (allocatedIds.has(allocatedId)) {
43
+ count += 1;
44
+ allocatedId = `${baseId}-${count}`;
45
+ }
46
+ seenCounts.set(baseId, count);
47
+ if (allocatedId !== baseId) {
48
+ diagnostics.push(`${filePath} (${symbol}): Inferred id collision for '${baseId}'. Assigned deterministic suffix '${allocatedId}'.`);
49
+ }
50
+ allocatedIds.set(allocatedId, symbol);
51
+ return allocatedId;
52
+ }
53
+ function toSchemaType(text) {
54
+ switch (text) {
55
+ case 'string':
56
+ return { type: 'string' };
57
+ case 'number':
58
+ case 'bigint':
59
+ return { type: 'number' };
60
+ case 'boolean':
61
+ return { type: 'boolean' };
62
+ case 'null':
63
+ return { type: 'null' };
64
+ case 'Date':
65
+ return { type: 'string', format: 'date-time' };
66
+ case 'unknown':
67
+ case 'any':
68
+ return { type: 'object' };
69
+ default:
70
+ return { type: 'object' };
71
+ }
72
+ }
73
+ function getIdentifierText(node) {
74
+ if (!node)
75
+ return null;
76
+ if (node.type === 'identifier' || node.type === 'property_identifier' || node.type === 'type_identifier') {
77
+ return node.text;
78
+ }
79
+ const idNode = node.namedChildren.find((child) => child.type === 'identifier' || child.type === 'property_identifier' || child.type === 'type_identifier');
80
+ return idNode ? idNode.text : null;
81
+ }
82
+ function extractAnnotationOverrides(filePath, content) {
83
+ const annotations = parseAnnotations(content);
84
+ const overrides = new Map();
85
+ const errors = [];
86
+ for (const annotation of annotations) {
87
+ const declaration = extractDeclaration(content, annotation.index);
88
+ if (declaration.error) {
89
+ errors.push(`${filePath}: ${declaration.error}`);
90
+ continue;
91
+ }
92
+ overrides.set(declaration.symbol, annotation);
93
+ }
94
+ return { overrides, errors };
95
+ }
96
+ function collectExportedDeclarations(root) {
97
+ const declarations = [];
98
+ for (const node of root.namedChildren) {
99
+ if (node.type !== 'export_statement') {
100
+ continue;
101
+ }
102
+ const declaration = node.namedChildren.find((child) => ['interface_declaration', 'type_alias_declaration', 'enum_declaration', 'function_declaration'].includes(child.type));
103
+ if (!declaration) {
104
+ continue;
105
+ }
106
+ const symbolNode = declaration.childForFieldName('name');
107
+ const symbol = getIdentifierText(symbolNode);
108
+ if (!symbol) {
109
+ continue;
110
+ }
111
+ if (declaration.type === 'interface_declaration') {
112
+ declarations.push({ kind: 'interface', symbol, node: declaration });
113
+ continue;
114
+ }
115
+ if (declaration.type === 'type_alias_declaration') {
116
+ declarations.push({ kind: 'type', symbol, node: declaration });
117
+ continue;
118
+ }
119
+ if (declaration.type === 'enum_declaration') {
120
+ declarations.push({ kind: 'enum', symbol, node: declaration });
121
+ continue;
122
+ }
123
+ declarations.push({ kind: 'function', symbol, node: declaration });
124
+ }
125
+ return declarations;
126
+ }
127
+ function findChildByType(node, types) {
128
+ return node.namedChildren.find((child) => types.includes(child.type)) ?? null;
129
+ }
130
+ function parseTypeNode(filePath, symbol, node, diagnostics) {
131
+ if (node.type === 'type_annotation') {
132
+ const inner = node.namedChildren[0];
133
+ if (!inner) {
134
+ return { type: 'object' };
135
+ }
136
+ return parseTypeNode(filePath, symbol, inner, diagnostics);
137
+ }
138
+ if (node.type === 'predefined_type') {
139
+ return toSchemaType(node.text);
140
+ }
141
+ if (node.type === 'type_identifier') {
142
+ if (node.text === 'Date') {
143
+ return { type: 'string', format: 'date-time' };
144
+ }
145
+ diagnostics.push(`${filePath} (${symbol}): Unsupported referenced type '${node.text}'. Falling back to object.`);
146
+ return { type: 'object' };
147
+ }
148
+ if (node.type === 'array_type') {
149
+ const element = node.namedChildren[0];
150
+ if (!element) {
151
+ diagnostics.push(`${filePath} (${symbol}): Unable to parse array element type. Falling back to object items.`);
152
+ return { type: 'array', items: { type: 'object' } };
56
153
  }
57
154
  return {
58
- type: "object",
59
- properties,
60
- required,
155
+ type: 'array',
156
+ items: parseTypeNode(filePath, symbol, element, diagnostics),
61
157
  };
62
158
  }
63
- parseTypeExpr(diagnostics) {
64
- this.skipWs();
65
- if (this.peek() === "{") {
66
- return this.parseObjectType(diagnostics);
67
- }
68
- const ident = this.readIdentifier();
69
- if (!ident) {
70
- diagnostics.push("Unable to parse type expression.");
71
- return { type: "object" };
72
- }
73
- let base;
74
- switch (ident) {
75
- case "string":
76
- base = { type: "string" };
77
- break;
78
- case "number":
79
- base = { type: "number" };
80
- break;
81
- case "boolean":
82
- base = { type: "boolean" };
83
- break;
84
- case "Date":
85
- base = { type: "string", format: "date-time" };
86
- break;
87
- default:
88
- diagnostics.push(`Unsupported referenced type '${ident}'. Falling back to object.`);
89
- base = { type: "object" };
90
- break;
91
- }
92
- while (true) {
93
- this.skipWs();
94
- if (this.peek() === "[" && this.peek(1) === "]") {
95
- this.i += 2;
96
- base = { type: "array", items: base };
97
- continue;
159
+ if (node.type === 'union_type') {
160
+ diagnostics.push(`${filePath} (${symbol}): Union types are not supported in extraction. Falling back to object.`);
161
+ return { type: 'object' };
162
+ }
163
+ if (node.type === 'intersection_type') {
164
+ diagnostics.push(`${filePath} (${symbol}): Intersection types are not supported in extraction. Falling back to object.`);
165
+ return { type: 'object' };
166
+ }
167
+ if (node.type === 'literal_type') {
168
+ const valueNode = node.namedChildren[0];
169
+ if (!valueNode) {
170
+ return { type: 'object' };
171
+ }
172
+ if (valueNode.type === 'string') {
173
+ return { type: 'string', enum: [valueNode.text.slice(1, -1)] };
174
+ }
175
+ if (valueNode.type === 'number') {
176
+ return { type: 'number', enum: [Number(valueNode.text)] };
177
+ }
178
+ if (valueNode.type === 'true' || valueNode.type === 'false') {
179
+ return { type: 'boolean', enum: [valueNode.type === 'true'] };
180
+ }
181
+ return { type: 'object' };
182
+ }
183
+ if (node.type === 'parenthesized_type') {
184
+ const inner = node.namedChildren[0];
185
+ if (!inner) {
186
+ return { type: 'object' };
187
+ }
188
+ return parseTypeNode(filePath, symbol, inner, diagnostics);
189
+ }
190
+ if (node.type === 'object_type' || node.type === 'interface_body') {
191
+ return parseObjectShape(filePath, symbol, node, diagnostics);
192
+ }
193
+ if (node.type === 'function_type') {
194
+ const parameters = node.childForFieldName('parameters');
195
+ const returnType = node.childForFieldName('return_type');
196
+ return {
197
+ type: 'object',
198
+ properties: {
199
+ params: parseParametersShape(filePath, symbol, parameters, diagnostics),
200
+ returns: returnType ? parseTypeNode(filePath, symbol, returnType, diagnostics) : { type: 'object' },
201
+ },
202
+ required: ['params', 'returns'],
203
+ };
204
+ }
205
+ diagnostics.push(`${filePath} (${symbol}): Unsupported node type '${node.type}'. Falling back to object.`);
206
+ return { type: 'object' };
207
+ }
208
+ function parseObjectShape(filePath, symbol, node, diagnostics) {
209
+ const properties = {};
210
+ const required = [];
211
+ const members = node.namedChildren;
212
+ for (const member of members) {
213
+ if (!['property_signature', 'method_signature'].includes(member.type)) {
214
+ diagnostics.push(`${filePath} (${symbol}): Unsupported object member type '${member.type}'. Falling back by skipping this member.`);
215
+ continue;
216
+ }
217
+ const nameNode = member.childForFieldName('name') ?? findChildByType(member, ['property_identifier', 'identifier', 'string']);
218
+ const propertyName = getIdentifierText(nameNode) ?? nameNode?.text?.replace(/^['"]|['"]$/g, '');
219
+ if (!propertyName) {
220
+ diagnostics.push(`${filePath} (${symbol}): Unable to parse property name in object type.`);
221
+ continue;
222
+ }
223
+ // Check for a '?' token that is a direct (unnamed) child of the member node.
224
+ // This correctly distinguishes optional properties (bar?: string) from required
225
+ // methods that happen to have optional parameters (fn(x?: string): void).
226
+ const optional = member.children.some((child) => !child.isNamed && child.text === '?');
227
+ let schema;
228
+ if (member.type === 'method_signature') {
229
+ const parameters = member.childForFieldName('parameters');
230
+ const returnType = member.childForFieldName('return_type');
231
+ schema = {
232
+ type: 'object',
233
+ properties: {
234
+ params: parseParametersShape(filePath, symbol, parameters, diagnostics),
235
+ returns: returnType ? parseTypeNode(filePath, symbol, returnType, diagnostics) : { type: 'object' },
236
+ },
237
+ required: ['params', 'returns'],
238
+ };
239
+ }
240
+ else {
241
+ const typeNode = member.childForFieldName('type') ??
242
+ member.childForFieldName('value') ??
243
+ findChildByType(member, [
244
+ 'type_annotation',
245
+ 'predefined_type',
246
+ 'type_identifier',
247
+ 'array_type',
248
+ 'union_type',
249
+ 'intersection_type',
250
+ 'object_type',
251
+ 'literal_type',
252
+ 'parenthesized_type',
253
+ 'function_type',
254
+ ]);
255
+ if (!typeNode) {
256
+ diagnostics.push(`${filePath} (${symbol}): Unable to parse type expression for property '${propertyName}'.`);
257
+ schema = { type: 'object' };
98
258
  }
99
- break;
100
- }
101
- this.skipWs();
102
- if (this.peek() === "|") {
103
- diagnostics.push("Union types are not supported in Sprint 3 extraction.");
104
- while (this.i < this.input.length) {
105
- const ch = this.peek();
106
- if (ch === ";" || ch === "," || ch === "}" || ch === "\n") {
107
- break;
108
- }
109
- this.i++;
259
+ else {
260
+ schema = parseTypeNode(filePath, symbol, typeNode, diagnostics);
110
261
  }
111
262
  }
112
- return base;
263
+ properties[propertyName] = schema;
264
+ if (!optional) {
265
+ required.push(propertyName);
266
+ }
113
267
  }
114
- readIdentifier() {
115
- this.skipWs();
116
- const start = this.i;
117
- while (this.i < this.input.length) {
118
- const ch = this.input[this.i];
119
- const ok = /[A-Za-z0-9_]/.test(ch);
120
- if (!ok) {
121
- break;
122
- }
123
- this.i++;
268
+ return {
269
+ type: 'object',
270
+ properties,
271
+ required,
272
+ };
273
+ }
274
+ function parseParametersShape(filePath, symbol, parametersNode, diagnostics) {
275
+ if (!parametersNode) {
276
+ return { type: 'object', properties: {}, required: [] };
277
+ }
278
+ const properties = {};
279
+ const required = [];
280
+ for (const parameter of parametersNode.namedChildren) {
281
+ if (!['required_parameter', 'optional_parameter', 'rest_parameter'].includes(parameter.type)) {
282
+ continue;
283
+ }
284
+ const nameNode = parameter.childForFieldName('pattern') ??
285
+ parameter.childForFieldName('name') ??
286
+ // rest_parameter exposes its identifier as a direct named child with no field name
287
+ parameter.namedChildren.find((c) => c.type === 'identifier') ??
288
+ null;
289
+ const parameterName = getIdentifierText(nameNode) ?? 'arg';
290
+ const typeNode = parameter.childForFieldName('type') ??
291
+ findChildByType(parameter, [
292
+ 'type_annotation',
293
+ 'predefined_type',
294
+ 'type_identifier',
295
+ 'array_type',
296
+ 'union_type',
297
+ 'intersection_type',
298
+ 'object_type',
299
+ ]);
300
+ properties[parameterName] = typeNode ? parseTypeNode(filePath, symbol, typeNode, diagnostics) : { type: 'object' };
301
+ // A rest parameter is represented as required_parameter with a rest_pattern child.
302
+ // It can be called with zero arguments, so it must not appear in required[].
303
+ const isRestParameter = parameter.namedChildren.some((c) => c.type === 'rest_pattern');
304
+ if (parameter.type !== 'optional_parameter' && parameter.type !== 'rest_parameter' && !isRestParameter) {
305
+ required.push(parameterName);
124
306
  }
125
- return this.input.slice(start, this.i);
126
307
  }
127
- skipWs() {
128
- while (this.i < this.input.length) {
129
- const ch = this.input[this.i];
130
- if (!/\s/.test(ch)) {
131
- break;
132
- }
133
- this.i++;
308
+ return {
309
+ type: 'object',
310
+ properties,
311
+ required,
312
+ };
313
+ }
314
+ function parseEnumShape(node) {
315
+ const body = node.childForFieldName('body') ?? findChildByType(node, ['enum_body']);
316
+ if (!body) {
317
+ return { type: 'string', enum: [] };
318
+ }
319
+ const values = body.namedChildren
320
+ .map((child) => {
321
+ // Bare member: Admin
322
+ if (child.type === 'property_identifier' || child.type === 'identifier') {
323
+ return child.text;
134
324
  }
325
+ // Valued member: Admin = "admin" or Admin = 1
326
+ if (child.type === 'enum_assignment') {
327
+ const nameChild = child.namedChildren.find((c) => c.type === 'property_identifier' || c.type === 'identifier');
328
+ return nameChild?.text ?? null;
329
+ }
330
+ return null;
331
+ })
332
+ .filter((v) => v !== null);
333
+ return {
334
+ type: 'string',
335
+ enum: values,
336
+ };
337
+ }
338
+ function parseDeclarationShape(filePath, declaration, diagnostics) {
339
+ if (declaration.kind === 'interface') {
340
+ const body = declaration.node.childForFieldName('body') ?? findChildByType(declaration.node, ['interface_body']);
341
+ if (!body) {
342
+ diagnostics.push(`${filePath} (${declaration.symbol}): Interface body was not found. Falling back to object.`);
343
+ return { type: 'object', properties: {}, required: [] };
344
+ }
345
+ return parseObjectShape(filePath, declaration.symbol, body, diagnostics);
135
346
  }
136
- advanceToDelimiter() {
137
- while (this.i < this.input.length) {
138
- const ch = this.input[this.i];
139
- if (ch === ";" || ch === "\n" || ch === "}") {
140
- return;
141
- }
142
- this.i++;
347
+ if (declaration.kind === 'type') {
348
+ const valueNode = declaration.node.childForFieldName('value') ?? declaration.node.namedChildren.at(-1);
349
+ if (!valueNode) {
350
+ diagnostics.push(`${filePath} (${declaration.symbol}): Type alias value was not found. Falling back to object.`);
351
+ return { type: 'object' };
143
352
  }
353
+ return parseTypeNode(filePath, declaration.symbol, valueNode, diagnostics);
144
354
  }
145
- peek(offset = 0) {
146
- return this.input[this.i + offset] ?? "";
355
+ if (declaration.kind === 'enum') {
356
+ return parseEnumShape(declaration.node);
147
357
  }
358
+ const parameters = declaration.node.childForFieldName('parameters');
359
+ const returnType = declaration.node.childForFieldName('return_type');
360
+ return {
361
+ type: 'object',
362
+ properties: {
363
+ params: parseParametersShape(filePath, declaration.symbol, parameters, diagnostics),
364
+ returns: returnType ? parseTypeNode(filePath, declaration.symbol, returnType, diagnostics) : { type: 'object' },
365
+ },
366
+ required: ['params', 'returns'],
367
+ };
148
368
  }
149
369
  function parseAnnotations(content) {
150
- const pattern = /^\s*\/\/\s*@ferret-contract:\s*([^\s]+)\s+(api|table|type|event|flow|config)\s*$/gm;
370
+ const contractTypePattern = CONTRACT_TYPES.join('|');
371
+ const pattern = new RegExp(`^\\s*//\\s*@ferret-contract:\\s*([^\\s]+)\\s+(${contractTypePattern})\\s*$`, 'gm');
151
372
  const out = [];
152
373
  let match;
153
374
  while ((match = pattern.exec(content)) !== null) {
@@ -161,72 +382,69 @@ function parseAnnotations(content) {
161
382
  }
162
383
  function extractDeclaration(content, start) {
163
384
  const tail = content.slice(start);
164
- const decl = /(?:export\s+)?(interface|type)\s+([A-Za-z0-9_]+)\s*(?:=)?\s*/m.exec(tail);
165
- if (!decl || decl.index === undefined) {
166
- return {
167
- symbol: "unknown",
168
- objectBody: "",
169
- error: "No interface/type declaration found after annotation.",
170
- };
171
- }
172
- const symbol = decl[2];
173
- const offset = start + decl.index + decl[0].length;
174
- const bodyStart = content.indexOf("{", offset);
175
- if (bodyStart === -1) {
176
- return {
177
- symbol,
178
- objectBody: "",
179
- error: `Declaration '${symbol}' is not an object-like type.`,
180
- };
181
- }
182
- let depth = 0;
183
- let end = -1;
184
- for (let i = bodyStart; i < content.length; i++) {
185
- const ch = content[i];
186
- if (ch === "{")
187
- depth++;
188
- if (ch === "}") {
189
- depth--;
190
- if (depth === 0) {
191
- end = i;
192
- break;
193
- }
194
- }
195
- }
196
- if (end === -1) {
385
+ const decl = /(?:export\s+)?(interface|type)\s+([A-Za-z0-9_]+)/m.exec(tail);
386
+ if (!decl) {
197
387
  return {
198
- symbol,
199
- objectBody: "",
200
- error: `Unclosed object type for declaration '${symbol}'.`,
388
+ symbol: 'unknown',
389
+ error: 'No interface/type declaration found after annotation.',
201
390
  };
202
391
  }
203
- const objectBody = content.slice(bodyStart, end + 1);
204
- return { symbol, objectBody };
392
+ return { symbol: decl[2] };
205
393
  }
206
394
  export function extractContractsFromTypeScript(filePath, content) {
207
- const annotations = parseAnnotations(content);
395
+ const annotationResult = extractAnnotationOverrides(filePath, content);
396
+ const annotationOverrides = annotationResult.overrides;
208
397
  const contracts = [];
398
+ const inferredIdCounts = new Map();
399
+ const allocatedIds = new Map();
400
+ const errors = [...annotationResult.errors];
209
401
  const diagnostics = [];
210
- for (const annotation of annotations) {
211
- const declaration = extractDeclaration(content, annotation.index);
212
- if (declaration.error) {
213
- diagnostics.push(`${filePath}: ${declaration.error}`);
214
- continue;
402
+ let tree;
403
+ try {
404
+ tree = parser.parse(content);
405
+ }
406
+ catch (error) {
407
+ const message = error instanceof Error ? error.message : 'Unknown parse error.';
408
+ errors.push(`${filePath}: Tree-sitter parse failed: ${message}`);
409
+ return { contracts, errors, diagnostics };
410
+ }
411
+ if (tree.rootNode.hasError) {
412
+ diagnostics.push(`${filePath}: Tree-sitter detected syntax errors; extraction may be partial.`);
413
+ }
414
+ const declarations = collectExportedDeclarations(tree.rootNode);
415
+ for (const declaration of declarations) {
416
+ const shape = parseDeclarationShape(filePath, declaration, diagnostics);
417
+ const override = annotationOverrides.get(declaration.symbol);
418
+ let allocatedId;
419
+ if (override?.id) {
420
+ const existingOwner = allocatedIds.get(override.id);
421
+ if (existingOwner && existingOwner !== declaration.symbol) {
422
+ errors.push(`${filePath} (${declaration.symbol}): Annotation id '${override.id}' duplicates id already used by '${existingOwner}'.`);
423
+ continue;
424
+ }
425
+ allocatedId = override.id;
426
+ allocatedIds.set(allocatedId, declaration.symbol);
215
427
  }
216
- const parser = new TypeParser(declaration.objectBody);
217
- const { shape, diagnostics: parserDiagnostics } = parser.parseObject();
218
- for (const d of parserDiagnostics) {
219
- diagnostics.push(`${filePath} (${declaration.symbol}): ${d}`);
428
+ else {
429
+ const inferredId = inferContractId(filePath, declaration.symbol);
430
+ allocatedId = allocateDeterministicInferredId(inferredId, inferredIdCounts, allocatedIds, diagnostics, filePath, declaration.symbol);
220
431
  }
221
432
  contracts.push({
222
- id: annotation.id,
223
- type: annotation.type,
433
+ id: allocatedId,
434
+ type: override?.type ?? inferContractType(),
224
435
  shape,
225
436
  sourceSymbol: declaration.symbol,
226
437
  filePath,
438
+ idSource: override?.id ? 'annotated' : 'inferred',
227
439
  });
228
440
  }
229
- contracts.sort((a, b) => a.id.localeCompare(b.id));
230
- return { contracts, diagnostics };
441
+ for (const [symbol, annotation] of annotationOverrides.entries()) {
442
+ if (contracts.some((contract) => contract.sourceSymbol === symbol)) {
443
+ continue;
444
+ }
445
+ errors.push(`${filePath}: Annotation for '${annotation.id}' did not match an exported declaration.`);
446
+ }
447
+ contracts.sort((a, b) => (a.id === b.id ? a.sourceSymbol.localeCompare(b.sourceSymbol) : a.id.localeCompare(b.id)));
448
+ return { contracts, errors, diagnostics };
231
449
  }
232
450
  //# sourceMappingURL=typescript.js.map