@reactive-contracts/compiler 0.1.0-beta

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.
@@ -0,0 +1,1094 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/cli/commands/init.ts
7
+ import fse from "fs-extra";
8
+ import { existsSync } from "fs";
9
+ import * as path from "path";
10
+ import { fileURLToPath } from "url";
11
+
12
+ // src/cli/utils/logger.ts
13
+ import pc from "picocolors";
14
+ function success(message) {
15
+ console.log(pc.green("\u2713"), message);
16
+ }
17
+ function error(message) {
18
+ console.log(pc.red("\u2717"), message);
19
+ }
20
+ function warning(message) {
21
+ console.log(pc.yellow("\u26A0"), message);
22
+ }
23
+ function info(message) {
24
+ console.log(pc.blue("\u2139"), message);
25
+ }
26
+ function header(message) {
27
+ console.log("\n" + pc.bold(pc.cyan(message)) + "\n");
28
+ }
29
+
30
+ // src/cli/utils/spinner.ts
31
+ import ora from "ora";
32
+ var currentSpinner = null;
33
+ function start(text) {
34
+ if (currentSpinner) {
35
+ currentSpinner.stop();
36
+ }
37
+ currentSpinner = ora(text).start();
38
+ return currentSpinner;
39
+ }
40
+ function succeed(text) {
41
+ if (currentSpinner) {
42
+ currentSpinner.succeed(text);
43
+ currentSpinner = null;
44
+ }
45
+ }
46
+ function fail(text) {
47
+ if (currentSpinner) {
48
+ currentSpinner.fail(text);
49
+ currentSpinner = null;
50
+ }
51
+ }
52
+ function stop() {
53
+ if (currentSpinner) {
54
+ currentSpinner.stop();
55
+ currentSpinner = null;
56
+ }
57
+ }
58
+
59
+ // src/cli/commands/init.ts
60
+ var __filename = fileURLToPath(import.meta.url);
61
+ var __dirname = path.dirname(__filename);
62
+ function getTemplatesDir() {
63
+ const templatesPath = path.join(__dirname, "templates");
64
+ if (!existsSync(templatesPath)) {
65
+ throw new Error(
66
+ `Templates directory not found at ${templatesPath}. Please rebuild the package.`
67
+ );
68
+ }
69
+ return templatesPath;
70
+ }
71
+ async function init() {
72
+ try {
73
+ header("Initializing Reactive Contracts");
74
+ const cwd = process.cwd();
75
+ start("Creating directory structure...");
76
+ await fse.ensureDir(path.join(cwd, "contracts"));
77
+ await fse.ensureDir(path.join(cwd, "generated", "frontend"));
78
+ await fse.ensureDir(path.join(cwd, "generated", "backend"));
79
+ await fse.ensureDir(path.join(cwd, "generated", "runtime"));
80
+ succeed("Created directory structure");
81
+ start("Creating config file...");
82
+ const configPath = path.join(cwd, "rcontracts.config.ts");
83
+ if (await fse.pathExists(configPath)) {
84
+ stop();
85
+ info("Config file already exists, skipping");
86
+ } else {
87
+ const templatePath = path.join(getTemplatesDir(), "rcontracts.config.template.ts");
88
+ await fse.copy(templatePath, configPath);
89
+ succeed("Created rcontracts.config.ts");
90
+ }
91
+ start("Updating .gitignore...");
92
+ const gitignorePath = path.join(cwd, ".gitignore");
93
+ let gitignoreContent = "";
94
+ if (await fse.pathExists(gitignorePath)) {
95
+ gitignoreContent = await fse.readFile(gitignorePath, "utf-8");
96
+ }
97
+ if (!gitignoreContent.includes("generated/")) {
98
+ const gitignoreTemplate = await fse.readFile(
99
+ path.join(getTemplatesDir(), "gitignore.template"),
100
+ "utf-8"
101
+ );
102
+ const updatedContent = gitignoreContent ? `${gitignoreContent}
103
+
104
+ # Reactive Contracts
105
+ ${gitignoreTemplate}` : gitignoreTemplate;
106
+ await fse.writeFile(gitignorePath, updatedContent);
107
+ succeed("Updated .gitignore");
108
+ } else {
109
+ stop();
110
+ info(".gitignore already includes generated/, skipping");
111
+ }
112
+ start("Creating sample contract...");
113
+ const sampleContractPath = path.join(cwd, "contracts", "sample.contract.ts");
114
+ if (await fse.pathExists(sampleContractPath)) {
115
+ stop();
116
+ info("Sample contract already exists, skipping");
117
+ } else {
118
+ const sampleTemplatePath = path.join(getTemplatesDir(), "sample-contract.template.ts");
119
+ await fse.copy(sampleTemplatePath, sampleContractPath);
120
+ succeed("Created sample contract");
121
+ }
122
+ success("Initialization complete!");
123
+ console.log("\nNext steps:");
124
+ console.log(" 1. Review the sample contract in contracts/sample.contract.ts");
125
+ console.log(" 2. Customize rcontracts.config.ts if needed");
126
+ console.log(" 3. Run: npx rcontracts compile");
127
+ console.log(" 4. Check the generated/ directory for output");
128
+ } catch (err) {
129
+ fail("Initialization failed");
130
+ error(`Failed to initialize: ${err instanceof Error ? err.message : String(err)}`);
131
+ process.exit(1);
132
+ }
133
+ }
134
+
135
+ // src/cli/commands/compile.ts
136
+ import { glob } from "glob";
137
+ import * as path2 from "path";
138
+ import { pathToFileURL } from "url";
139
+ import { register } from "tsx/esm/api";
140
+
141
+ // src/config/index.ts
142
+ async function loadConfig(_configPath = "./rcontracts.config.ts") {
143
+ return {
144
+ contracts: "./contracts/**/*.contract.ts",
145
+ output: {
146
+ frontend: "./generated/frontend",
147
+ backend: "./generated/backend",
148
+ runtime: "./generated/runtime"
149
+ }
150
+ };
151
+ }
152
+
153
+ // src/validator/index.ts
154
+ function validateContract(contract) {
155
+ const errors = [];
156
+ const warnings = [];
157
+ if (!contract || typeof contract !== "object") {
158
+ errors.push("Contract must be a valid object");
159
+ return { valid: false, errors, warnings };
160
+ }
161
+ if (contract._brand !== "Contract") {
162
+ errors.push("Invalid contract: missing _brand property");
163
+ return { valid: false, errors, warnings };
164
+ }
165
+ const { definition } = contract;
166
+ if (!definition.name || typeof definition.name !== "string") {
167
+ errors.push("Contract must have a valid name (non-empty string)");
168
+ } else if (!/^[A-Z][a-zA-Z0-9]*$/.test(definition.name)) {
169
+ warnings.push(
170
+ "Contract name should be in PascalCase (e.g., UserProfile, not userProfile or user_profile)"
171
+ );
172
+ }
173
+ if (!definition.intent || typeof definition.intent !== "string") {
174
+ errors.push("Contract must have a valid intent (non-empty string)");
175
+ } else if (definition.intent.length < 10) {
176
+ warnings.push("Intent should be descriptive (at least 10 characters)");
177
+ }
178
+ if (!definition.shape || typeof definition.shape !== "object") {
179
+ errors.push("Contract must have a valid shape definition");
180
+ } else {
181
+ validateShape(definition.shape, "", errors, warnings);
182
+ }
183
+ if (definition.constraints) {
184
+ validateConstraints(definition.constraints, errors, warnings);
185
+ }
186
+ if (definition.reactivity) {
187
+ validateReactivity(definition.reactivity, definition.shape, errors, warnings);
188
+ }
189
+ if (definition.versioning) {
190
+ validateVersioning(definition.versioning, errors, warnings);
191
+ }
192
+ return {
193
+ valid: errors.length === 0,
194
+ errors,
195
+ warnings
196
+ };
197
+ }
198
+ function validateShape(shape, path4, errors, warnings) {
199
+ if (!shape || typeof shape !== "object") {
200
+ errors.push(`Invalid shape at ${path4 || "root"}: must be an object`);
201
+ return;
202
+ }
203
+ const keys = Object.keys(shape);
204
+ if (keys.length === 0) {
205
+ warnings.push(`Shape at ${path4 || "root"} is empty`);
206
+ }
207
+ for (const key of keys) {
208
+ const fieldPath = path4 ? `${path4}.${key}` : key;
209
+ const value = shape[key];
210
+ if (!value) {
211
+ errors.push(`Field "${fieldPath}" has undefined value`);
212
+ continue;
213
+ }
214
+ if (!/^[a-z][a-zA-Z0-9]*$/.test(key)) {
215
+ warnings.push(
216
+ `Field name "${fieldPath}" should be in camelCase (e.g., firstName, not first_name)`
217
+ );
218
+ }
219
+ validateTypeDefinition(value, fieldPath, errors, warnings);
220
+ }
221
+ }
222
+ function validateTypeDefinition(type, path4, errors, warnings) {
223
+ if (typeof type === "string") {
224
+ const validPrimitives = ["string", "number", "boolean", "Date", "null", "undefined"];
225
+ const isURL = type === "URL" || type.startsWith("URL<");
226
+ if (!validPrimitives.includes(type) && !isURL) {
227
+ errors.push(`Invalid type at ${path4}: "${type}" is not a valid primitive or URL type`);
228
+ }
229
+ if (isURL && type !== "URL") {
230
+ const urlMatch = type.match(/^URL<(.+)>$/);
231
+ if (!urlMatch) {
232
+ errors.push(`Invalid URL type at ${path4}: must be "URL" or "URL<options>"`);
233
+ }
234
+ }
235
+ } else if (typeof type === "object" && type !== null) {
236
+ if ("_brand" in type && type._brand === "DerivedField") {
237
+ validateDerivedField(
238
+ type,
239
+ path4,
240
+ errors,
241
+ warnings
242
+ );
243
+ } else {
244
+ validateShape(type, path4, errors, warnings);
245
+ }
246
+ } else {
247
+ errors.push(`Invalid type at ${path4}: must be a string, object, or DerivedField`);
248
+ }
249
+ }
250
+ function validateDerivedField(field, path4, errors, _warnings) {
251
+ if (typeof field.derive !== "function") {
252
+ errors.push(`Derived field at ${path4} must have a derive function`);
253
+ }
254
+ if (field.dependencies && !Array.isArray(field.dependencies)) {
255
+ errors.push(`Derived field dependencies at ${path4} must be an array`);
256
+ }
257
+ if (field.preferredLayer && !["client", "edge", "origin"].includes(field.preferredLayer)) {
258
+ errors.push(`Derived field preferredLayer at ${path4} must be 'client', 'edge', or 'origin'`);
259
+ }
260
+ if (field.dependencies && Array.isArray(field.dependencies)) {
261
+ for (const dep of field.dependencies) {
262
+ if (typeof dep !== "string") {
263
+ errors.push(`Derived field dependency at ${path4} must be a string`);
264
+ }
265
+ }
266
+ }
267
+ }
268
+ function validateConstraints(constraints, errors, _warnings) {
269
+ if (constraints.latency) {
270
+ const { max, fallback } = constraints.latency;
271
+ if (!max || typeof max !== "string") {
272
+ errors.push('Latency constraint must have a max value (e.g., "100ms")');
273
+ } else {
274
+ if (!/^\d+(ms|s|m)$/.test(max)) {
275
+ errors.push(`Invalid latency max format: "${max}". Use format like "100ms", "1s", or "1m"`);
276
+ }
277
+ }
278
+ if (fallback && !["cachedVersion", "degraded", "error"].includes(fallback)) {
279
+ errors.push('Latency fallback must be "cachedVersion", "degraded", or "error"');
280
+ }
281
+ }
282
+ if (constraints.freshness) {
283
+ const { maxAge, staleWhileRevalidate } = constraints.freshness;
284
+ if (!maxAge || typeof maxAge !== "string") {
285
+ errors.push("Freshness constraint must have a maxAge value");
286
+ } else if (!/^\d+(ms|s|m|h|d)$/.test(maxAge)) {
287
+ errors.push(
288
+ `Invalid freshness maxAge format: "${maxAge}". Use format like "5m", "1h", or "1d"`
289
+ );
290
+ }
291
+ if (staleWhileRevalidate && !/^\d+(ms|s|m|h|d)$/.test(staleWhileRevalidate)) {
292
+ errors.push(
293
+ `Invalid staleWhileRevalidate format: "${staleWhileRevalidate}". Use format like "5m", "1h", or "1d"`
294
+ );
295
+ }
296
+ }
297
+ if (constraints.availability) {
298
+ const { uptime, gracefulDegradation } = constraints.availability;
299
+ if (!uptime || typeof uptime !== "string") {
300
+ errors.push("Availability constraint must have an uptime value");
301
+ } else if (!/^\d+(\.\d+)?%$/.test(uptime)) {
302
+ errors.push(`Invalid uptime format: "${uptime}". Use format like "99.9%"`);
303
+ }
304
+ if (gracefulDegradation !== void 0 && typeof gracefulDegradation !== "boolean") {
305
+ errors.push("Availability gracefulDegradation must be a boolean");
306
+ }
307
+ }
308
+ }
309
+ function validateReactivity(reactivity, shape, errors, warnings) {
310
+ const allFields = /* @__PURE__ */ new Set();
311
+ collectFieldPaths(shape, "", allFields);
312
+ if (reactivity.realtime) {
313
+ if (!Array.isArray(reactivity.realtime)) {
314
+ errors.push("Reactivity realtime must be an array of field paths");
315
+ } else {
316
+ for (const field of reactivity.realtime) {
317
+ if (typeof field !== "string") {
318
+ errors.push("Realtime field must be a string");
319
+ } else if (!allFields.has(field)) {
320
+ warnings.push(`Realtime field "${field}" does not exist in shape`);
321
+ }
322
+ }
323
+ }
324
+ }
325
+ if (reactivity.static) {
326
+ if (!Array.isArray(reactivity.static)) {
327
+ errors.push("Reactivity static must be an array of field paths");
328
+ } else {
329
+ for (const field of reactivity.static) {
330
+ if (typeof field !== "string") {
331
+ errors.push("Static field must be a string");
332
+ } else if (!allFields.has(field)) {
333
+ warnings.push(`Static field "${field}" does not exist in shape`);
334
+ }
335
+ }
336
+ }
337
+ }
338
+ if (reactivity.polling) {
339
+ if (!Array.isArray(reactivity.polling)) {
340
+ errors.push("Reactivity polling must be an array");
341
+ } else {
342
+ for (const config of reactivity.polling) {
343
+ if (!config.field || typeof config.field !== "string") {
344
+ errors.push("Polling config must have a field property");
345
+ } else if (!allFields.has(config.field)) {
346
+ warnings.push(`Polling field "${config.field}" does not exist in shape`);
347
+ }
348
+ if (!config.interval || typeof config.interval !== "string") {
349
+ errors.push("Polling config must have an interval property");
350
+ } else if (!/^\d+(ms|s|m)$/.test(config.interval)) {
351
+ errors.push(
352
+ `Invalid polling interval format: "${config.interval}". Use format like "30s" or "5m"`
353
+ );
354
+ }
355
+ }
356
+ }
357
+ }
358
+ if (reactivity.eventDriven) {
359
+ if (!Array.isArray(reactivity.eventDriven)) {
360
+ errors.push("Reactivity eventDriven must be an array");
361
+ } else {
362
+ for (const config of reactivity.eventDriven) {
363
+ if (!config.field || typeof config.field !== "string") {
364
+ errors.push("EventDriven config must have a field property");
365
+ } else if (!allFields.has(config.field)) {
366
+ warnings.push(`EventDriven field "${config.field}" does not exist in shape`);
367
+ }
368
+ if (!config.on || !Array.isArray(config.on)) {
369
+ errors.push('EventDriven config must have an "on" array of event names');
370
+ } else if (config.on.length === 0) {
371
+ warnings.push(`EventDriven config for "${config.field}" has no events`);
372
+ }
373
+ }
374
+ }
375
+ }
376
+ }
377
+ function validateVersioning(versioning, errors, warnings) {
378
+ if (!versioning.version || typeof versioning.version !== "string") {
379
+ errors.push("Versioning must have a version string");
380
+ } else if (!/^\d+\.\d+\.\d+$/.test(versioning.version)) {
381
+ warnings.push(`Version "${versioning.version}" should follow semver format (e.g., "1.0.0")`);
382
+ }
383
+ if (versioning.deprecated && !Array.isArray(versioning.deprecated)) {
384
+ errors.push("Versioning deprecated must be an array of field paths");
385
+ }
386
+ if (versioning.migration && typeof versioning.migration !== "function") {
387
+ errors.push("Versioning migration must be a function");
388
+ }
389
+ }
390
+ function collectFieldPaths(shape, prefix, result) {
391
+ for (const [key, value] of Object.entries(shape)) {
392
+ const path4 = prefix ? `${prefix}.${key}` : key;
393
+ result.add(path4);
394
+ if (typeof value === "object" && value !== null && !("_brand" in value) && typeof value !== "string") {
395
+ collectFieldPaths(value, path4, result);
396
+ }
397
+ }
398
+ }
399
+
400
+ // src/analyzer/index.ts
401
+ function analyzeLatency(contract) {
402
+ const { definition } = contract;
403
+ const suggestions = [];
404
+ if (!definition.constraints?.latency) {
405
+ suggestions.push("Consider adding a latency constraint to ensure performance SLAs");
406
+ return {
407
+ status: "ok",
408
+ suggestions
409
+ };
410
+ }
411
+ const { max: maxLatency, fallback } = definition.constraints.latency;
412
+ const latencyMs = parseLatencyToMs(maxLatency);
413
+ if (latencyMs === null) {
414
+ return {
415
+ status: "error",
416
+ message: `Invalid latency format: ${maxLatency}`,
417
+ suggestions: ['Use format like "100ms", "1s", or "1m"']
418
+ };
419
+ }
420
+ const analysis = analyzeLatencyRequirement(latencyMs);
421
+ if (!fallback) {
422
+ suggestions.push("Consider specifying a fallback strategy (cachedVersion, degraded, or error)");
423
+ }
424
+ if (latencyMs < 50) {
425
+ suggestions.push(
426
+ "Very aggressive latency target (<50ms). Consider edge caching or CDN for static data."
427
+ );
428
+ } else if (latencyMs < 100) {
429
+ suggestions.push(
430
+ "Moderate latency target (<100ms). Ensure database queries are optimized and indexed."
431
+ );
432
+ } else if (latencyMs > 1e3) {
433
+ suggestions.push(
434
+ "High latency tolerance (>1s). Consider if this provides good user experience."
435
+ );
436
+ }
437
+ if (hasComplexDerivations(definition.shape)) {
438
+ suggestions.push(
439
+ "Contract contains derived fields. Consider computing these at edge or origin for better performance."
440
+ );
441
+ }
442
+ if (definition.reactivity?.realtime && definition.reactivity.realtime.length > 0) {
443
+ suggestions.push(
444
+ "Realtime fields require WebSocket connections. Ensure your infrastructure supports this."
445
+ );
446
+ }
447
+ return {
448
+ status: analysis.status,
449
+ estimated: analysis.estimated,
450
+ message: analysis.message,
451
+ suggestions
452
+ };
453
+ }
454
+ function parseLatencyToMs(latency) {
455
+ const match = latency.match(/^(\d+)(ms|s|m)$/);
456
+ if (!match || !match[1] || !match[2]) return null;
457
+ const value = parseInt(match[1], 10);
458
+ const unit = match[2];
459
+ switch (unit) {
460
+ case "ms":
461
+ return value;
462
+ case "s":
463
+ return value * 1e3;
464
+ case "m":
465
+ return value * 60 * 1e3;
466
+ default:
467
+ return null;
468
+ }
469
+ }
470
+ function analyzeLatencyRequirement(latencyMs) {
471
+ if (latencyMs < 10) {
472
+ return {
473
+ status: "error",
474
+ estimated: "Typically 50-100ms for database queries",
475
+ message: "Latency target <10ms is extremely difficult to achieve consistently"
476
+ };
477
+ }
478
+ if (latencyMs < 50) {
479
+ return {
480
+ status: "warning",
481
+ estimated: "Requires edge caching or CDN",
482
+ message: "Latency target <50ms requires careful optimization"
483
+ };
484
+ }
485
+ if (latencyMs <= 200) {
486
+ return {
487
+ status: "ok",
488
+ estimated: "Achievable with optimized queries and caching",
489
+ message: "Latency target is reasonable for most applications"
490
+ };
491
+ }
492
+ if (latencyMs <= 1e3) {
493
+ return {
494
+ status: "ok",
495
+ estimated: "Easily achievable with standard backend",
496
+ message: "Latency target is conservative and should be easy to meet"
497
+ };
498
+ }
499
+ return {
500
+ status: "warning",
501
+ estimated: "Very high tolerance",
502
+ message: "Consider if this latency provides acceptable user experience"
503
+ };
504
+ }
505
+ function hasComplexDerivations(shape) {
506
+ if (!shape || typeof shape !== "object") {
507
+ return false;
508
+ }
509
+ for (const value of Object.values(shape)) {
510
+ if (typeof value === "object" && value !== null) {
511
+ if ("_brand" in value && value._brand === "DerivedField") {
512
+ return true;
513
+ }
514
+ if (hasComplexDerivations(value)) {
515
+ return true;
516
+ }
517
+ }
518
+ }
519
+ return false;
520
+ }
521
+
522
+ // src/generator/index.ts
523
+ import { writeFile, mkdir } from "fs/promises";
524
+ import { dirname as dirname2 } from "path";
525
+ async function generateFrontendTypes(contract, outputPath) {
526
+ const { definition } = contract;
527
+ const typeName = definition.name;
528
+ const typeDefinitions = generateTypeDefinitions(definition.shape, `${typeName}Shape`);
529
+ const content = `/**
530
+ * Auto-generated types for ${typeName} contract
531
+ * DO NOT EDIT - This file is generated by @reactive-contracts/compiler
532
+ */
533
+
534
+ import type { ContractStatus } from '@reactive-contracts/core';
535
+
536
+ ${typeDefinitions}
537
+
538
+ /**
539
+ * Parameters for ${typeName} contract
540
+ */
541
+ export interface ${typeName}Params {
542
+ // Add your params here based on contract requirements
543
+ [key: string]: unknown;
544
+ }
545
+
546
+ /**
547
+ * Full result type for ${typeName} contract
548
+ */
549
+ export interface ${typeName}Result {
550
+ data: ${typeName}Shape;
551
+ status: ContractStatus;
552
+ metadata: {
553
+ executionTime: number;
554
+ cacheHit: boolean;
555
+ derivedAt: 'client' | 'edge' | 'origin';
556
+ };
557
+ }
558
+
559
+ /**
560
+ * Contract intent: ${definition.intent}
561
+ */
562
+ export const ${typeName}Intent = '${definition.intent}' as const;
563
+ `;
564
+ await mkdir(dirname2(outputPath), { recursive: true });
565
+ await writeFile(outputPath, content, "utf-8");
566
+ }
567
+ async function generateBackendResolver(contract, outputPath) {
568
+ const { definition } = contract;
569
+ const typeName = definition.name;
570
+ const shapeType = generateTypeDefinitions(definition.shape, `${typeName}ResolverShape`);
571
+ const content = `/**
572
+ * Auto-generated resolver template for ${typeName} contract
573
+ * Generated by @reactive-contracts/compiler
574
+ *
575
+ * Intent: ${definition.intent}
576
+ */
577
+
578
+ import { implementContract } from '@reactive-contracts/server';
579
+ import type { Contract } from '@reactive-contracts/core';
580
+ import type { ResolverContext } from '@reactive-contracts/server';
581
+
582
+ ${shapeType}
583
+
584
+ /**
585
+ * Implement the ${typeName} contract resolver
586
+ *
587
+ * This function should return data matching the contract shape.
588
+ * Derived fields will be computed automatically - don't include them.
589
+ */
590
+ export const ${typeName}Resolver = implementContract(
591
+ // Import your contract definition here
592
+ {} as Contract, // Replace with your contract
593
+ {
594
+ async resolve(params: Record<string, unknown>, context: ResolverContext): Promise<${typeName}ResolverShape> {
595
+ // TODO: Implement your data fetching logic here
596
+
597
+ // Example:
598
+ // const data = await db.query(...);
599
+ // return {
600
+ // // Map your data to match the contract shape
601
+ // };
602
+
603
+ throw new Error('${typeName}Resolver not implemented yet');
604
+ },
605
+
606
+ // Optional: Configure caching
607
+ cache: {
608
+ ttl: '5m',
609
+ staleWhileRevalidate: '1h',
610
+ tags: (params) => [\`${typeName.toLowerCase()}:\${params.id}\`],
611
+ },
612
+ }
613
+ );
614
+ `;
615
+ await mkdir(dirname2(outputPath), { recursive: true });
616
+ await writeFile(outputPath, content, "utf-8");
617
+ }
618
+ async function generateRuntimeNegotiator(contract, outputPath) {
619
+ const { definition } = contract;
620
+ const typeName = definition.name;
621
+ const content = `/**
622
+ * Auto-generated runtime negotiator for ${typeName} contract
623
+ * DO NOT EDIT - This file is generated by @reactive-contracts/compiler
624
+ */
625
+
626
+ import type { Contract } from '@reactive-contracts/core';
627
+
628
+ /**
629
+ * Runtime negotiator for ${typeName}
630
+ * Handles SLA monitoring, fallback logic, and performance tracking
631
+ */
632
+ export class ${typeName}Negotiator {
633
+ private contract: Contract;
634
+ private metrics: {
635
+ executionTime: number[];
636
+ cacheHits: number;
637
+ cacheMisses: number;
638
+ } = {
639
+ executionTime: [],
640
+ cacheHits: 0,
641
+ cacheMisses: 0,
642
+ };
643
+
644
+ constructor(contract: Contract) {
645
+ this.contract = contract;
646
+ }
647
+
648
+ /**
649
+ * Execute contract with SLA monitoring
650
+ */
651
+ async execute<TData>(
652
+ resolver: () => Promise<TData>,
653
+ options?: {
654
+ useCache?: boolean;
655
+ timeout?: number;
656
+ }
657
+ ): Promise<{
658
+ data: TData;
659
+ status: {
660
+ latency: 'normal' | 'degraded' | 'violated';
661
+ freshness: 'fresh' | 'stale' | 'expired';
662
+ availability: 'available' | 'degraded' | 'unavailable';
663
+ };
664
+ metadata: {
665
+ executionTime: number;
666
+ cacheHit: boolean;
667
+ derivedAt: 'client' | 'edge' | 'origin';
668
+ };
669
+ }> {
670
+ const startTime = performance.now();
671
+
672
+ try {
673
+ // Execute resolver
674
+ const data = await resolver();
675
+ const executionTime = performance.now() - startTime;
676
+
677
+ // Track metrics
678
+ this.metrics.executionTime.push(executionTime);
679
+ if (options?.useCache) {
680
+ this.metrics.cacheHits++;
681
+ } else {
682
+ this.metrics.cacheMisses++;
683
+ }
684
+
685
+ // Determine latency status
686
+ const maxLatency = this.getMaxLatency();
687
+ const latencyStatus = this.evaluateLatency(executionTime, maxLatency);
688
+
689
+ return {
690
+ data,
691
+ status: {
692
+ latency: latencyStatus,
693
+ freshness: 'fresh',
694
+ availability: 'available',
695
+ },
696
+ metadata: {
697
+ executionTime,
698
+ cacheHit: options?.useCache ?? false,
699
+ derivedAt: 'origin',
700
+ },
701
+ };
702
+ } catch (error) {
703
+ // Handle fallback based on contract constraints
704
+ throw error;
705
+ }
706
+ }
707
+
708
+ /**
709
+ * Get metrics for monitoring
710
+ */
711
+ getMetrics() {
712
+ const avgExecutionTime =
713
+ this.metrics.executionTime.length > 0
714
+ ? this.metrics.executionTime.reduce((a, b) => a + b, 0) / this.metrics.executionTime.length
715
+ : 0;
716
+
717
+ return {
718
+ averageExecutionTime: avgExecutionTime,
719
+ p95ExecutionTime: this.calculateP95(),
720
+ cacheHitRate:
721
+ this.metrics.cacheHits / (this.metrics.cacheHits + this.metrics.cacheMisses) || 0,
722
+ totalExecutions: this.metrics.executionTime.length,
723
+ };
724
+ }
725
+
726
+ private getMaxLatency(): number {
727
+ const latency = this.contract.definition.constraints?.latency?.max;
728
+ if (!latency) return Infinity;
729
+
730
+ // Simple parsing for MVP
731
+ const match = latency.match(/^(\\d+)(ms|s|m)$/);
732
+ if (!match) return Infinity;
733
+
734
+ const value = parseInt(match[1], 10);
735
+ const unit = match[2];
736
+
737
+ switch (unit) {
738
+ case 'ms': return value;
739
+ case 's': return value * 1000;
740
+ case 'm': return value * 60 * 1000;
741
+ default: return Infinity;
742
+ }
743
+ }
744
+
745
+ private evaluateLatency(
746
+ executionTime: number,
747
+ maxLatency: number
748
+ ): 'normal' | 'degraded' | 'violated' {
749
+ if (executionTime <= maxLatency) {
750
+ return 'normal';
751
+ } else if (executionTime <= maxLatency * 1.5) {
752
+ return 'degraded';
753
+ } else {
754
+ return 'violated';
755
+ }
756
+ }
757
+
758
+ private calculateP95(): number {
759
+ if (this.metrics.executionTime.length === 0) return 0;
760
+
761
+ const sorted = [...this.metrics.executionTime].sort((a, b) => a - b);
762
+ const index = Math.floor(sorted.length * 0.95);
763
+ return sorted[index];
764
+ }
765
+ }
766
+
767
+ /**
768
+ * Create a new negotiator instance
769
+ */
770
+ export function create${typeName}Negotiator(contract: Contract): ${typeName}Negotiator {
771
+ return new ${typeName}Negotiator(contract);
772
+ }
773
+ `;
774
+ await mkdir(dirname2(outputPath), { recursive: true });
775
+ await writeFile(outputPath, content, "utf-8");
776
+ }
777
+ function generateTypeDefinitions(shape, typeName) {
778
+ const fields = generateShapeFields(shape, 0);
779
+ return `export interface ${typeName} {
780
+ ${fields}
781
+ }`;
782
+ }
783
+ function generateShapeFields(shape, indent) {
784
+ const indentStr = " ".repeat(indent + 1);
785
+ const lines = [];
786
+ for (const [key, value] of Object.entries(shape)) {
787
+ const typeStr = generateTypeString(value, indent + 1);
788
+ lines.push(`${indentStr}${key}: ${typeStr};`);
789
+ }
790
+ return lines.join("\n");
791
+ }
792
+ function generateTypeString(type, indent) {
793
+ if (typeof type === "string") {
794
+ switch (type) {
795
+ case "string":
796
+ return "string";
797
+ case "number":
798
+ return "number";
799
+ case "boolean":
800
+ return "boolean";
801
+ case "Date":
802
+ return "Date";
803
+ case "null":
804
+ return "null";
805
+ case "undefined":
806
+ return "undefined";
807
+ default:
808
+ if (type === "URL" || type.startsWith("URL<")) {
809
+ return "string";
810
+ }
811
+ return "any";
812
+ }
813
+ } else if (typeof type === "object" && type !== null) {
814
+ if ("_brand" in type && type._brand === "DerivedField") {
815
+ return "any";
816
+ } else {
817
+ const fields = generateShapeFields(type, indent);
818
+ const indentStr = " ".repeat(indent);
819
+ return `{
820
+ ${fields}
821
+ ${indentStr}}`;
822
+ }
823
+ }
824
+ return "any";
825
+ }
826
+
827
+ // src/cli/commands/compile.ts
828
+ var tsxUnregister = register();
829
+ async function compile() {
830
+ try {
831
+ header("Compiling Reactive Contracts");
832
+ start("Loading configuration...");
833
+ const config = await loadConfig();
834
+ succeed("Configuration loaded");
835
+ start("Finding contract files...");
836
+ const contractFiles = await glob(config.contracts, {
837
+ cwd: process.cwd(),
838
+ absolute: true
839
+ });
840
+ if (contractFiles.length === 0) {
841
+ fail("No contract files found");
842
+ warning(`No files matching pattern: ${config.contracts}`);
843
+ info('Run "npx rcontracts init" to create a sample contract');
844
+ return;
845
+ }
846
+ succeed(`Found ${contractFiles.length} contract file(s)`);
847
+ let successCount = 0;
848
+ let errorCount = 0;
849
+ for (const file of contractFiles) {
850
+ const fileName = path2.basename(file, ".contract.ts");
851
+ start(`Processing ${fileName}...`);
852
+ try {
853
+ const fileUrl = pathToFileURL(file).href;
854
+ const module = await import(fileUrl);
855
+ const contractExports = Object.entries(module).filter(
856
+ ([key, value]) => key.endsWith("Contract") && typeof value === "object" && value !== null && "_brand" in value && value._brand === "Contract"
857
+ );
858
+ if (contractExports.length === 0) {
859
+ fail(`No contract found in ${fileName}`);
860
+ warning(`File ${file} does not export a contract`);
861
+ errorCount++;
862
+ continue;
863
+ }
864
+ for (const [exportName, contractObj] of contractExports) {
865
+ const contract = contractObj;
866
+ const validation = validateContract(contract);
867
+ if (!validation.valid) {
868
+ fail(`Validation failed for ${exportName}`);
869
+ for (const err of validation.errors) {
870
+ error(` \u2717 ${err}`);
871
+ }
872
+ errorCount++;
873
+ continue;
874
+ }
875
+ if (validation.warnings.length > 0) {
876
+ for (const warn of validation.warnings) {
877
+ warning(` \u26A0 ${warn}`);
878
+ }
879
+ }
880
+ const latencyAnalysis = analyzeLatency(contract);
881
+ if (latencyAnalysis.status === "error") {
882
+ fail(`Latency analysis failed for ${exportName}`);
883
+ error(` \u2717 ${latencyAnalysis.message}`);
884
+ if (config.validation?.strictLatency) {
885
+ errorCount++;
886
+ continue;
887
+ }
888
+ } else if (latencyAnalysis.status === "warning") {
889
+ warning(` \u26A0 ${latencyAnalysis.message}`);
890
+ }
891
+ if (latencyAnalysis.suggestions && latencyAnalysis.suggestions.length > 0) {
892
+ for (const suggestion of latencyAnalysis.suggestions) {
893
+ info(` \u{1F4A1} ${suggestion}`);
894
+ }
895
+ }
896
+ const contractName = contract.definition.name;
897
+ const frontendPath = path2.join(
898
+ process.cwd(),
899
+ config.output.frontend,
900
+ `${contractName}.ts`
901
+ );
902
+ await generateFrontendTypes(contract, frontendPath);
903
+ const backendPath = path2.join(
904
+ process.cwd(),
905
+ config.output.backend,
906
+ `${contractName}.resolver.ts`
907
+ );
908
+ await generateBackendResolver(contract, backendPath);
909
+ const runtimePath = path2.join(
910
+ process.cwd(),
911
+ config.output.runtime,
912
+ `${contractName}.negotiator.ts`
913
+ );
914
+ await generateRuntimeNegotiator(contract, runtimePath);
915
+ succeed(`\u2713 Validated and generated code for ${exportName}`);
916
+ successCount++;
917
+ }
918
+ } catch (err) {
919
+ fail(`Failed to process ${fileName}`);
920
+ error(` ${err instanceof Error ? err.message : String(err)}`);
921
+ errorCount++;
922
+ }
923
+ }
924
+ tsxUnregister();
925
+ console.log("");
926
+ if (successCount > 0) {
927
+ success(`\u2713 Successfully compiled ${successCount} contract(s)`);
928
+ info(` Frontend types \u2192 ${config.output.frontend}`);
929
+ info(` Backend resolvers \u2192 ${config.output.backend}`);
930
+ info(` Runtime negotiators \u2192 ${config.output.runtime}`);
931
+ }
932
+ if (errorCount > 0) {
933
+ error(`\u2717 Failed to compile ${errorCount} contract(s)`);
934
+ process.exit(1);
935
+ }
936
+ } catch (err) {
937
+ error(`Failed to compile: ${err instanceof Error ? err.message : String(err)}`);
938
+ process.exit(1);
939
+ }
940
+ }
941
+
942
+ // src/cli/commands/validate.ts
943
+ import { glob as glob2 } from "glob";
944
+ import * as path3 from "path";
945
+ import { pathToFileURL as pathToFileURL2 } from "url";
946
+ import { register as register2 } from "tsx/esm/api";
947
+ var tsxUnregister2 = register2();
948
+ async function validate() {
949
+ try {
950
+ header("Validating Reactive Contracts");
951
+ start("Loading configuration...");
952
+ const config = await loadConfig();
953
+ succeed("Configuration loaded");
954
+ start("Finding contract files...");
955
+ const contractFiles = await glob2(config.contracts, {
956
+ cwd: process.cwd(),
957
+ absolute: true
958
+ });
959
+ if (contractFiles.length === 0) {
960
+ fail("No contract files found");
961
+ warning(`No files matching pattern: ${config.contracts}`);
962
+ info('Run "npx rcontracts init" to create a sample contract');
963
+ return;
964
+ }
965
+ succeed(`Found ${contractFiles.length} contract file(s)`);
966
+ let validCount = 0;
967
+ let invalidCount = 0;
968
+ let warningCount = 0;
969
+ for (const file of contractFiles) {
970
+ const fileName = path3.basename(file, ".contract.ts");
971
+ start(`Validating ${fileName}...`);
972
+ try {
973
+ const fileUrl = pathToFileURL2(file).href;
974
+ const module = await import(fileUrl);
975
+ const contractExports = Object.entries(module).filter(
976
+ ([key, value]) => key.endsWith("Contract") && typeof value === "object" && value !== null && "_brand" in value && value._brand === "Contract"
977
+ );
978
+ if (contractExports.length === 0) {
979
+ fail(`No contract found in ${fileName}`);
980
+ warning(`File ${file} does not export a contract`);
981
+ invalidCount++;
982
+ continue;
983
+ }
984
+ for (const [exportName, contractObj] of contractExports) {
985
+ const contract = contractObj;
986
+ const validation = validateContract(contract);
987
+ if (!validation.valid) {
988
+ fail(`\u2717 ${exportName} - INVALID`);
989
+ for (const err of validation.errors) {
990
+ error(` ${err}`);
991
+ }
992
+ invalidCount++;
993
+ } else {
994
+ const latencyAnalysis = analyzeLatency(contract);
995
+ if (latencyAnalysis.status === "error") {
996
+ if (config.validation?.strictLatency) {
997
+ fail(`\u2717 ${exportName} - INVALID (latency constraint violation)`);
998
+ error(` ${latencyAnalysis.message}`);
999
+ invalidCount++;
1000
+ } else {
1001
+ stop();
1002
+ warning(`\u26A0 ${exportName} - WARNING (latency issue)`);
1003
+ warning(` ${latencyAnalysis.message}`);
1004
+ warningCount++;
1005
+ validCount++;
1006
+ }
1007
+ } else {
1008
+ succeed(`\u2713 ${exportName} - VALID`);
1009
+ validCount++;
1010
+ if (validation.warnings.length > 0) {
1011
+ for (const warn of validation.warnings) {
1012
+ warning(` ${warn}`);
1013
+ }
1014
+ warningCount++;
1015
+ }
1016
+ if (latencyAnalysis.status === "warning") {
1017
+ warning(` ${latencyAnalysis.message}`);
1018
+ warningCount++;
1019
+ }
1020
+ if (latencyAnalysis.suggestions && latencyAnalysis.suggestions.length > 0) {
1021
+ for (const suggestion of latencyAnalysis.suggestions) {
1022
+ info(` \u{1F4A1} ${suggestion}`);
1023
+ }
1024
+ }
1025
+ }
1026
+ }
1027
+ }
1028
+ } catch (err) {
1029
+ fail(`Failed to validate ${fileName}`);
1030
+ error(` ${err instanceof Error ? err.message : String(err)}`);
1031
+ invalidCount++;
1032
+ }
1033
+ }
1034
+ tsxUnregister2();
1035
+ console.log("");
1036
+ console.log("Validation Summary:");
1037
+ console.log(` \u2713 Valid: ${validCount}`);
1038
+ if (warningCount > 0) {
1039
+ console.log(` \u26A0 Warnings: ${warningCount}`);
1040
+ }
1041
+ if (invalidCount > 0) {
1042
+ console.log(` \u2717 Invalid: ${invalidCount}`);
1043
+ }
1044
+ if (invalidCount > 0) {
1045
+ error("\nValidation failed. Please fix the errors above.");
1046
+ process.exit(1);
1047
+ } else if (warningCount > 0) {
1048
+ success("\nValidation passed with warnings");
1049
+ } else {
1050
+ success("\nAll contracts are valid!");
1051
+ }
1052
+ } catch (err) {
1053
+ error(`Failed to validate: ${err instanceof Error ? err.message : String(err)}`);
1054
+ process.exit(1);
1055
+ }
1056
+ }
1057
+
1058
+ // src/cli/commands/diagnose.ts
1059
+ async function diagnose(name) {
1060
+ try {
1061
+ header(`Diagnosing Contract: ${name}`);
1062
+ console.log(`
1063
+ Contract: ${name}`);
1064
+ console.log("Status: Not implemented yet");
1065
+ } catch (err) {
1066
+ error(`Failed to diagnose: ${err instanceof Error ? err.message : String(err)}`);
1067
+ process.exit(1);
1068
+ }
1069
+ }
1070
+
1071
+ // src/cli/commands/diff.ts
1072
+ async function diff() {
1073
+ warning("The diff command is coming in beta!");
1074
+ console.log("\nThis feature will show changes since the last compilation.");
1075
+ console.log("Stay tuned for the beta release.");
1076
+ }
1077
+
1078
+ // src/cli/commands/migrate.ts
1079
+ async function migrate() {
1080
+ warning("The migrate command is coming in beta!");
1081
+ console.log("\nThis feature will run contract migrations.");
1082
+ console.log("Stay tuned for the beta release.");
1083
+ }
1084
+
1085
+ // src/cli/index.ts
1086
+ var program = new Command();
1087
+ program.name("rcontracts").description("Reactive Contracts CLI - Build-time compiler and validator").version("0.1.0-alpha");
1088
+ program.command("init").description("Initialize Reactive Contracts in your project").action(init);
1089
+ program.command("compile").description("Compile all contracts and generate code").action(compile);
1090
+ program.command("validate").description("Validate contracts without generating code").action(validate);
1091
+ program.command("diagnose <name>").description("Show detailed analysis for a specific contract").action(diagnose);
1092
+ program.command("diff").description("Show changes since last compile").action(diff);
1093
+ program.command("migrate").description("Run contract migrations").action(migrate);
1094
+ program.parse();