@specferret/core 0.1.2 → 0.1.4

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 (81) hide show
  1. package/dist/context/index.d.ts +5 -1
  2. package/dist/context/index.d.ts.map +1 -1
  3. package/dist/context/index.js +47 -1
  4. package/dist/context/index.js.map +1 -1
  5. package/dist/extractor/__fixtures__/typescript/generics.d.ts +6 -0
  6. package/dist/extractor/__fixtures__/typescript/generics.d.ts.map +1 -0
  7. package/dist/extractor/__fixtures__/typescript/generics.js +2 -0
  8. package/dist/extractor/__fixtures__/typescript/generics.js.map +1 -0
  9. package/dist/extractor/__fixtures__/typescript/intersections.d.ts +6 -0
  10. package/dist/extractor/__fixtures__/typescript/intersections.d.ts.map +1 -0
  11. package/dist/extractor/__fixtures__/typescript/intersections.js +2 -0
  12. package/dist/extractor/__fixtures__/typescript/intersections.js.map +1 -0
  13. package/dist/extractor/__fixtures__/typescript/optional-nested.d.ts +8 -0
  14. package/dist/extractor/__fixtures__/typescript/optional-nested.d.ts.map +1 -0
  15. package/dist/extractor/__fixtures__/typescript/optional-nested.js +2 -0
  16. package/dist/extractor/__fixtures__/typescript/optional-nested.js.map +1 -0
  17. package/dist/extractor/__fixtures__/typescript/unions.d.ts +5 -0
  18. package/dist/extractor/__fixtures__/typescript/unions.d.ts.map +1 -0
  19. package/dist/extractor/__fixtures__/typescript/unions.js +2 -0
  20. package/dist/extractor/__fixtures__/typescript/unions.js.map +1 -0
  21. package/dist/extractor/__fixtures__/typescript/unsupported-syntax.d.ts +7 -0
  22. package/dist/extractor/__fixtures__/typescript/unsupported-syntax.d.ts.map +1 -0
  23. package/dist/extractor/__fixtures__/typescript/unsupported-syntax.js +2 -0
  24. package/dist/extractor/__fixtures__/typescript/unsupported-syntax.js.map +1 -0
  25. package/dist/extractor/contract-types.d.ts +5 -0
  26. package/dist/extractor/contract-types.d.ts.map +1 -0
  27. package/dist/extractor/contract-types.js +8 -0
  28. package/dist/extractor/contract-types.js.map +1 -0
  29. package/dist/extractor/frontmatter.d.ts +2 -1
  30. package/dist/extractor/frontmatter.d.ts.map +1 -1
  31. package/dist/extractor/frontmatter.js +12 -6
  32. package/dist/extractor/frontmatter.js.map +1 -1
  33. package/dist/extractor/typescript.d.ts +6 -1
  34. package/dist/extractor/typescript.d.ts.map +1 -1
  35. package/dist/extractor/typescript.js +403 -185
  36. package/dist/extractor/typescript.js.map +1 -1
  37. package/dist/extractor/validator.d.ts +9 -0
  38. package/dist/extractor/validator.d.ts.map +1 -1
  39. package/dist/extractor/validator.js +28 -9
  40. package/dist/extractor/validator.js.map +1 -1
  41. package/dist/index.d.ts +13 -12
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +13 -12
  44. package/dist/index.js.map +1 -1
  45. package/dist/reconciler/index.d.ts +10 -0
  46. package/dist/reconciler/index.d.ts.map +1 -1
  47. package/dist/reconciler/index.js +76 -0
  48. package/dist/reconciler/index.js.map +1 -1
  49. package/package.json +4 -1
  50. package/src/config.ts +48 -0
  51. package/src/context/index.test.ts +274 -0
  52. package/src/context/index.ts +178 -0
  53. package/src/extractor/__fixtures__/typescript/generics.golden.json +36 -0
  54. package/src/extractor/__fixtures__/typescript/generics.ts +5 -0
  55. package/src/extractor/__fixtures__/typescript/intersections.golden.json +18 -0
  56. package/src/extractor/__fixtures__/typescript/intersections.ts +5 -0
  57. package/src/extractor/__fixtures__/typescript/optional-nested.golden.json +41 -0
  58. package/src/extractor/__fixtures__/typescript/optional-nested.ts +7 -0
  59. package/src/extractor/__fixtures__/typescript/unions.golden.json +30 -0
  60. package/src/extractor/__fixtures__/typescript/unions.ts +4 -0
  61. package/src/extractor/__fixtures__/typescript/unsupported-syntax.golden.json +20 -0
  62. package/src/extractor/__fixtures__/typescript/unsupported-syntax.ts +6 -0
  63. package/src/extractor/contract-types.ts +11 -0
  64. package/src/extractor/frontmatter.test.ts +217 -0
  65. package/src/extractor/frontmatter.ts +77 -0
  66. package/src/extractor/hash.ts +26 -0
  67. package/src/extractor/typescript.test.ts +339 -0
  68. package/src/extractor/typescript.ts +597 -0
  69. package/src/extractor/validator.test.ts +228 -0
  70. package/src/extractor/validator.ts +199 -0
  71. package/src/index.ts +16 -0
  72. package/src/reconciler/import-suggestions.test.ts +101 -0
  73. package/src/reconciler/import-suggestions.ts +157 -0
  74. package/src/reconciler/index.test.ts +248 -0
  75. package/src/reconciler/index.ts +445 -0
  76. package/src/store/factory.ts +54 -0
  77. package/src/store/postgres.ts +84 -0
  78. package/src/store/sqlite.test.ts +272 -0
  79. package/src/store/sqlite.ts +278 -0
  80. package/src/store/types.ts +100 -0
  81. package/src/utils/paths.ts +24 -0
@@ -0,0 +1,274 @@
1
+ import assert from "node:assert/strict";
2
+ import { afterEach, describe, it } from "bun:test";
3
+ import {
4
+ CONTEXT_SCHEMA_VERSION,
5
+ readContextFile,
6
+ writeContext,
7
+ type FerretContext,
8
+ } from "./index.js";
9
+ import { SqliteStore } from "../store/sqlite.js";
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+ import * as os from "node:os";
13
+ import { randomUUID } from "node:crypto";
14
+
15
+ function makeTmpDir(): string {
16
+ const tmp = path.join(os.tmpdir(), `ferret-ctx-test-${randomUUID()}`);
17
+ fs.mkdirSync(tmp, { recursive: true });
18
+ return tmp;
19
+ }
20
+
21
+ async function makeStoreWithData(tmpDir: string) {
22
+ // Use in-memory store but fake the projectRoot for file writing
23
+ const store = new SqliteStore(":memory:");
24
+ await store.init();
25
+
26
+ const nodeId = randomUUID();
27
+ await store.upsertNode({
28
+ id: nodeId,
29
+ file_path: "contracts/auth.contract.md",
30
+ hash: "abc",
31
+ status: "stable",
32
+ });
33
+
34
+ const contractId = "auth.jwt";
35
+ await store.upsertContract({
36
+ id: contractId,
37
+ node_id: nodeId,
38
+ shape_hash: "sha256hash",
39
+ shape_schema: JSON.stringify({
40
+ type: "object",
41
+ properties: { token: { type: "string" } },
42
+ required: ["token"],
43
+ }),
44
+ type: "api",
45
+ status: "stable",
46
+ });
47
+
48
+ // Second node that imports auth.jwt
49
+ const nodeId2 = randomUUID();
50
+ await store.upsertNode({
51
+ id: nodeId2,
52
+ file_path: "contracts/search.contract.md",
53
+ hash: "def",
54
+ status: "needs-review",
55
+ });
56
+
57
+ const contractId2 = "api.GET/search";
58
+ await store.upsertContract({
59
+ id: contractId2,
60
+ node_id: nodeId2,
61
+ shape_hash: "sha256hash2",
62
+ shape_schema: JSON.stringify({ type: "array" }),
63
+ type: "api",
64
+ status: "stable",
65
+ });
66
+
67
+ await store.upsertDependency({
68
+ id: randomUUID(),
69
+ source_node_id: nodeId2,
70
+ target_contract_id: contractId,
71
+ });
72
+
73
+ return { store, contractId, contractId2 };
74
+ }
75
+
76
+ describe("writeContext — Task 5", () => {
77
+ const tmps: string[] = [];
78
+
79
+ afterEach(() => {
80
+ // Clean up temp directories
81
+ for (const tmp of tmps) {
82
+ try {
83
+ fs.rmSync(tmp, { recursive: true, force: true });
84
+ } catch {}
85
+ }
86
+ tmps.length = 0;
87
+ });
88
+
89
+ it("creates context.json at the correct path", async () => {
90
+ const tmpDir = makeTmpDir();
91
+ tmps.push(tmpDir);
92
+ const { store } = await makeStoreWithData(tmpDir);
93
+
94
+ await writeContext(store, tmpDir);
95
+
96
+ const contextPath = path.join(tmpDir, ".ferret", "context.json");
97
+ assert.equal(fs.existsSync(contextPath), true);
98
+
99
+ await store.close();
100
+ });
101
+
102
+ it("produces valid JSON", async () => {
103
+ const tmpDir = makeTmpDir();
104
+ tmps.push(tmpDir);
105
+ const { store } = await makeStoreWithData(tmpDir);
106
+
107
+ await writeContext(store, tmpDir);
108
+
109
+ const contextPath = path.join(tmpDir, ".ferret", "context.json");
110
+ const raw = fs.readFileSync(contextPath, "utf-8");
111
+ assert.doesNotThrow(() => JSON.parse(raw));
112
+
113
+ await store.close();
114
+ });
115
+
116
+ it('contains version "2.0"', async () => {
117
+ const tmpDir = makeTmpDir();
118
+ tmps.push(tmpDir);
119
+ const { store } = await makeStoreWithData(tmpDir);
120
+
121
+ await writeContext(store, tmpDir);
122
+
123
+ const ctx = JSON.parse(
124
+ fs.readFileSync(path.join(tmpDir, ".ferret", "context.json"), "utf-8"),
125
+ ) as FerretContext;
126
+ assert.equal(ctx.version, "2.0");
127
+
128
+ await store.close();
129
+ });
130
+
131
+ it("contains context schemaVersion", async () => {
132
+ const tmpDir = makeTmpDir();
133
+ tmps.push(tmpDir);
134
+ const { store } = await makeStoreWithData(tmpDir);
135
+
136
+ await writeContext(store, tmpDir);
137
+
138
+ const ctx = JSON.parse(
139
+ fs.readFileSync(path.join(tmpDir, ".ferret", "context.json"), "utf-8"),
140
+ ) as FerretContext;
141
+ assert.equal(ctx.schemaVersion, CONTEXT_SCHEMA_VERSION);
142
+
143
+ await store.close();
144
+ });
145
+
146
+ it("readContextFile migrates known legacy V2 payload without schemaVersion", () => {
147
+ const tmpDir = makeTmpDir();
148
+ tmps.push(tmpDir);
149
+ const ferretDir = path.join(tmpDir, ".ferret");
150
+ fs.mkdirSync(ferretDir, { recursive: true });
151
+ const contextPath = path.join(ferretDir, "context.json");
152
+ fs.writeFileSync(
153
+ contextPath,
154
+ JSON.stringify(
155
+ {
156
+ version: "2.0",
157
+ generated: "2026-01-01T00:00:00.000Z",
158
+ contracts: [],
159
+ edges: [],
160
+ needsReview: [],
161
+ },
162
+ null,
163
+ 2,
164
+ ),
165
+ "utf-8",
166
+ );
167
+
168
+ const context = readContextFile(contextPath);
169
+ assert.equal(context.version, "2.0");
170
+ assert.equal(context.schemaVersion, CONTEXT_SCHEMA_VERSION);
171
+ assert.deepEqual(context.contracts, []);
172
+ assert.deepEqual(context.edges, []);
173
+ assert.deepEqual(context.needsReview, []);
174
+ });
175
+
176
+ it("readContextFile fails with upgrade guidance on unknown context version", () => {
177
+ const tmpDir = makeTmpDir();
178
+ tmps.push(tmpDir);
179
+ const ferretDir = path.join(tmpDir, ".ferret");
180
+ fs.mkdirSync(ferretDir, { recursive: true });
181
+ const contextPath = path.join(ferretDir, "context.json");
182
+ fs.writeFileSync(
183
+ contextPath,
184
+ JSON.stringify({
185
+ version: "9.9",
186
+ schemaVersion: CONTEXT_SCHEMA_VERSION,
187
+ generated: new Date().toISOString(),
188
+ contracts: [],
189
+ edges: [],
190
+ needsReview: [],
191
+ }),
192
+ "utf-8",
193
+ );
194
+
195
+ assert.throws(
196
+ () => readContextFile(contextPath),
197
+ /unsupported context\.json version|Run 'ferret scan'/,
198
+ );
199
+ });
200
+
201
+ it("readContextFile fails with upgrade guidance on unknown schemaVersion", () => {
202
+ const tmpDir = makeTmpDir();
203
+ tmps.push(tmpDir);
204
+ const ferretDir = path.join(tmpDir, ".ferret");
205
+ fs.mkdirSync(ferretDir, { recursive: true });
206
+ const contextPath = path.join(ferretDir, "context.json");
207
+ fs.writeFileSync(
208
+ contextPath,
209
+ JSON.stringify({
210
+ version: "2.0",
211
+ schemaVersion: "9.0.0",
212
+ generated: new Date().toISOString(),
213
+ contracts: [],
214
+ edges: [],
215
+ needsReview: [],
216
+ }),
217
+ "utf-8",
218
+ );
219
+
220
+ assert.throws(
221
+ () => readContextFile(contextPath),
222
+ /unsupported context schemaVersion|Run 'ferret scan'/,
223
+ );
224
+ });
225
+
226
+ it("stable contracts appear in contracts array", async () => {
227
+ const tmpDir = makeTmpDir();
228
+ tmps.push(tmpDir);
229
+ const { store, contractId } = await makeStoreWithData(tmpDir);
230
+
231
+ await writeContext(store, tmpDir);
232
+
233
+ const ctx = JSON.parse(
234
+ fs.readFileSync(path.join(tmpDir, ".ferret", "context.json"), "utf-8"),
235
+ ) as FerretContext;
236
+ const ids = ctx.contracts.map((c) => c.id);
237
+ assert.ok(ids.includes(contractId));
238
+
239
+ await store.close();
240
+ });
241
+
242
+ it("needs-review contracts appear in needsReview array", async () => {
243
+ const tmpDir = makeTmpDir();
244
+ tmps.push(tmpDir);
245
+ const { store, contractId2 } = await makeStoreWithData(tmpDir);
246
+
247
+ await writeContext(store, tmpDir);
248
+
249
+ const ctx = JSON.parse(
250
+ fs.readFileSync(path.join(tmpDir, ".ferret", "context.json"), "utf-8"),
251
+ ) as FerretContext;
252
+ // contractId2 belongs to the needs-review node
253
+ assert.ok(ctx.needsReview.includes(contractId2));
254
+
255
+ await store.close();
256
+ });
257
+
258
+ it("edges correctly represent dependency relationships", async () => {
259
+ const tmpDir = makeTmpDir();
260
+ tmps.push(tmpDir);
261
+ const { store, contractId } = await makeStoreWithData(tmpDir);
262
+
263
+ await writeContext(store, tmpDir);
264
+
265
+ const ctx = JSON.parse(
266
+ fs.readFileSync(path.join(tmpDir, ".ferret", "context.json"), "utf-8"),
267
+ ) as FerretContext;
268
+ const edge = ctx.edges.find((e) => e.to === contractId);
269
+ assert.notEqual(edge, undefined);
270
+ assert.equal(edge!.from, "contracts/search.contract.md");
271
+
272
+ await store.close();
273
+ });
274
+ });
@@ -0,0 +1,178 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { DBStore } from "../store/types.js";
4
+
5
+ export const CONTEXT_VERSION = "2.0" as const;
6
+ export const CONTEXT_SCHEMA_VERSION = "1.0.0" as const;
7
+
8
+ type LegacyFerretContextV2 = {
9
+ version: "2.0";
10
+ generated: string;
11
+ contracts: ContextContract[];
12
+ edges: ContextEdge[];
13
+ needsReview: string[];
14
+ };
15
+
16
+ export interface ContextContract {
17
+ id: string;
18
+ type: string;
19
+ shape: unknown;
20
+ status: string;
21
+ specFile: string | null;
22
+ codeFile: string | null;
23
+ }
24
+
25
+ export interface ContextEdge {
26
+ from: string; // source node file_path
27
+ to: string; // target contract id
28
+ }
29
+
30
+ export interface FerretContext {
31
+ version: typeof CONTEXT_VERSION;
32
+ schemaVersion: typeof CONTEXT_SCHEMA_VERSION;
33
+ generated: string; // ISO timestamp
34
+ contracts: ContextContract[];
35
+ edges: ContextEdge[];
36
+ needsReview: string[]; // contract IDs currently flagged
37
+ }
38
+
39
+ function normalizeContext(raw: unknown): FerretContext {
40
+ if (!raw || typeof raw !== "object") {
41
+ throw new Error("ferret: invalid context.json format. Run 'ferret scan' to regenerate .ferret/context.json.");
42
+ }
43
+
44
+ const candidate = raw as Record<string, unknown>;
45
+ const contextVersion = candidate.version;
46
+ if (contextVersion !== CONTEXT_VERSION) {
47
+ throw new Error(
48
+ `ferret: unsupported context.json version '${String(contextVersion)}'. Run 'ferret scan' with the current CLI to regenerate .ferret/context.json.`,
49
+ );
50
+ }
51
+
52
+ const schemaVersion = candidate.schemaVersion;
53
+ if (schemaVersion !== undefined && schemaVersion !== CONTEXT_SCHEMA_VERSION) {
54
+ throw new Error(
55
+ `ferret: unsupported context schemaVersion '${String(schemaVersion)}'. Run 'ferret scan' with the current CLI to migrate .ferret/context.json.`,
56
+ );
57
+ }
58
+
59
+ // Known migration path: V2 payloads created before schemaVersion was introduced.
60
+ const legacy = candidate as Partial<LegacyFerretContextV2>;
61
+ return {
62
+ version: CONTEXT_VERSION,
63
+ schemaVersion: CONTEXT_SCHEMA_VERSION,
64
+ generated:
65
+ typeof legacy.generated === "string"
66
+ ? legacy.generated
67
+ : new Date(0).toISOString(),
68
+ contracts: Array.isArray(legacy.contracts)
69
+ ? legacy.contracts
70
+ : [],
71
+ edges: Array.isArray(legacy.edges) ? legacy.edges : [],
72
+ needsReview: Array.isArray(legacy.needsReview)
73
+ ? legacy.needsReview
74
+ : [],
75
+ };
76
+ }
77
+
78
+ export function readContextFile(contextPath: string): FerretContext {
79
+ try {
80
+ const raw = fs.readFileSync(contextPath, "utf-8");
81
+ return normalizeContext(JSON.parse(raw));
82
+ } catch (error) {
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ if (message.startsWith("ferret:")) {
85
+ throw error;
86
+ }
87
+ throw new Error(
88
+ `ferret: unable to read .ferret/context.json (${message}). Run 'ferret scan' to regenerate it.`,
89
+ );
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Reads the full graph from the store and writes .ferret/context.json.
95
+ * Called automatically at the end of every ferret scan.
96
+ * Silent — no output. If write fails, it logs to stderr and continues.
97
+ */
98
+ export async function writeContext(
99
+ store: DBStore,
100
+ projectRoot: string,
101
+ ): Promise<void> {
102
+ const [nodes, contracts, dependencies] = await Promise.all([
103
+ store.getNodes(),
104
+ store.getContracts(),
105
+ store.getDependencies(),
106
+ ]);
107
+
108
+ // Build a map of node_id → node for quick lookup
109
+ const nodeById = new Map(nodes.map((n) => [n.id, n]));
110
+
111
+ // Map contracts to context shape
112
+ const contextContracts: ContextContract[] = contracts.map((c) => {
113
+ const parentNode = nodeById.get(c.node_id);
114
+ const nodeType = (parentNode as any)?.type ?? "spec"; // nodes don't store type in V2 yet
115
+
116
+ let shape: unknown = {};
117
+ try {
118
+ shape = JSON.parse(c.shape_schema);
119
+ } catch {
120
+ shape = {};
121
+ }
122
+
123
+ return {
124
+ id: c.id,
125
+ type: c.type,
126
+ shape,
127
+ status: c.status,
128
+ specFile:
129
+ nodeType !== "code"
130
+ ? (parentNode?.file_path?.replace(/\\/g, "/") ?? null)
131
+ : null,
132
+ codeFile:
133
+ nodeType === "code"
134
+ ? (parentNode?.file_path?.replace(/\\/g, "/") ?? null)
135
+ : null,
136
+ };
137
+ });
138
+
139
+ // Build edges: source_node file_path → target_contract_id
140
+ const edges: ContextEdge[] = dependencies.map((d) => {
141
+ const sourceNode = nodeById.get(d.source_node_id);
142
+ return {
143
+ from: (sourceNode?.file_path ?? d.source_node_id).replace(/\\/g, "/"),
144
+ to: d.target_contract_id,
145
+ };
146
+ });
147
+
148
+ // needsReview: contract IDs whose parent node is flagged needs-review
149
+ const needsReviewNodeIds = new Set(
150
+ nodes.filter((n) => n.status === "needs-review").map((n) => n.id),
151
+ );
152
+ const needsReview = contracts
153
+ .filter(
154
+ (c) => needsReviewNodeIds.has(c.node_id) || c.status === "needs-review",
155
+ )
156
+ .map((c) => c.id);
157
+
158
+ const context: FerretContext = {
159
+ version: CONTEXT_VERSION,
160
+ schemaVersion: CONTEXT_SCHEMA_VERSION,
161
+ generated: new Date().toISOString(),
162
+ contracts: contextContracts,
163
+ edges,
164
+ needsReview,
165
+ };
166
+
167
+ const ferretDir = path.join(projectRoot, ".ferret");
168
+ const contextPath = path.join(ferretDir, "context.json");
169
+
170
+ try {
171
+ if (!fs.existsSync(ferretDir)) {
172
+ fs.mkdirSync(ferretDir, { recursive: true });
173
+ }
174
+ fs.writeFileSync(contextPath, JSON.stringify(context, null, 2), "utf-8");
175
+ } catch (err) {
176
+ process.stderr.write(`⚠ Could not write context.json: ${err}\n`);
177
+ }
178
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "contracts": [
3
+ {
4
+ "id": "type.src/fixtures/generics/paginatedresponse",
5
+ "type": "type",
6
+ "shape": {
7
+ "type": "object",
8
+ "properties": {
9
+ "items": {
10
+ "type": "array",
11
+ "items": {
12
+ "type": "object"
13
+ }
14
+ },
15
+ "total": {
16
+ "type": "number"
17
+ },
18
+ "nextCursor": {
19
+ "type": "string"
20
+ }
21
+ },
22
+ "required": [
23
+ "items",
24
+ "total"
25
+ ]
26
+ },
27
+ "sourceSymbol": "PaginatedResponse",
28
+ "filePath": "src/fixtures/generics.ts",
29
+ "idSource": "inferred"
30
+ }
31
+ ],
32
+ "errors": [],
33
+ "diagnostics": [
34
+ "src/fixtures/generics.ts (PaginatedResponse): Unsupported referenced type 'T'. Falling back to object."
35
+ ]
36
+ }
@@ -0,0 +1,5 @@
1
+ export interface PaginatedResponse<T> {
2
+ items: T[];
3
+ total: number;
4
+ nextCursor?: string;
5
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "contracts": [
3
+ {
4
+ "id": "type.src/fixtures/intersections/useridentity",
5
+ "type": "type",
6
+ "shape": {
7
+ "type": "object"
8
+ },
9
+ "sourceSymbol": "UserIdentity",
10
+ "filePath": "src/fixtures/intersections.ts",
11
+ "idSource": "inferred"
12
+ }
13
+ ],
14
+ "errors": [],
15
+ "diagnostics": [
16
+ "src/fixtures/intersections.ts (UserIdentity): Intersection types are not supported in extraction. Falling back to object."
17
+ ]
18
+ }
@@ -0,0 +1,5 @@
1
+ export type UserIdentity = {
2
+ id: string;
3
+ } & {
4
+ tenantId: string;
5
+ };
@@ -0,0 +1,41 @@
1
+ {
2
+ "contracts": [
3
+ {
4
+ "id": "type.src/fixtures/optional-nested/searchresult",
5
+ "type": "type",
6
+ "shape": {
7
+ "type": "object",
8
+ "properties": {
9
+ "id": {
10
+ "type": "string"
11
+ },
12
+ "metadata": {
13
+ "type": "object",
14
+ "properties": {
15
+ "tags": {
16
+ "type": "array",
17
+ "items": {
18
+ "type": "string"
19
+ }
20
+ },
21
+ "score": {
22
+ "type": "number"
23
+ }
24
+ },
25
+ "required": [
26
+ "score"
27
+ ]
28
+ }
29
+ },
30
+ "required": [
31
+ "id"
32
+ ]
33
+ },
34
+ "sourceSymbol": "SearchResult",
35
+ "filePath": "src/fixtures/optional-nested.ts",
36
+ "idSource": "inferred"
37
+ }
38
+ ],
39
+ "errors": [],
40
+ "diagnostics": []
41
+ }
@@ -0,0 +1,7 @@
1
+ export interface SearchResult {
2
+ id: string;
3
+ metadata?: {
4
+ tags?: string[];
5
+ score: number;
6
+ };
7
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "contracts": [
3
+ {
4
+ "id": "type.src/fixtures/unions/maybeuser",
5
+ "type": "type",
6
+ "shape": {
7
+ "type": "object",
8
+ "properties": {
9
+ "id": {
10
+ "type": "object"
11
+ },
12
+ "displayName": {
13
+ "type": "object"
14
+ }
15
+ },
16
+ "required": [
17
+ "id"
18
+ ]
19
+ },
20
+ "sourceSymbol": "MaybeUser",
21
+ "filePath": "src/fixtures/unions.ts",
22
+ "idSource": "inferred"
23
+ }
24
+ ],
25
+ "errors": [],
26
+ "diagnostics": [
27
+ "src/fixtures/unions.ts (MaybeUser): Union types are not supported in extraction. Falling back to object.",
28
+ "src/fixtures/unions.ts (MaybeUser): Union types are not supported in extraction. Falling back to object."
29
+ ]
30
+ }
@@ -0,0 +1,4 @@
1
+ export interface MaybeUser {
2
+ id: string | null;
3
+ displayName?: string | null;
4
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "contracts": [
3
+ {
4
+ "id": "type.src/fixtures/unsupported-syntax/routemap",
5
+ "type": "type",
6
+ "shape": {
7
+ "type": "object",
8
+ "properties": {},
9
+ "required": []
10
+ },
11
+ "sourceSymbol": "RouteMap",
12
+ "filePath": "src/fixtures/unsupported-syntax.ts",
13
+ "idSource": "inferred"
14
+ }
15
+ ],
16
+ "errors": [],
17
+ "diagnostics": [
18
+ "src/fixtures/unsupported-syntax.ts (RouteMap): Unsupported object member type 'index_signature'. Falling back by skipping this member."
19
+ ]
20
+ }
@@ -0,0 +1,6 @@
1
+ export type RouteMap<T extends string> = {
2
+ [K in T]: {
3
+ method: 'GET' | 'POST';
4
+ secured?: boolean;
5
+ };
6
+ };
@@ -0,0 +1,11 @@
1
+ export const CONTRACT_TYPES = ['api', 'table', 'type', 'event', 'flow', 'config'] as const;
2
+
3
+ export type ContractType = (typeof CONTRACT_TYPES)[number];
4
+
5
+ export function isContractType(value: unknown): value is ContractType {
6
+ return typeof value === 'string' && (CONTRACT_TYPES as readonly string[]).includes(value);
7
+ }
8
+
9
+ export function formatAllowedContractTypes(): string {
10
+ return CONTRACT_TYPES.join(', ');
11
+ }