@panproto/core 0.7.0 → 0.8.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.
package/dist/index.cjs CHANGED
@@ -2060,6 +2060,10 @@ class Panproto {
2060
2060
  this.#wasm = wasm;
2061
2061
  this.#protocols = /* @__PURE__ */ new Map();
2062
2062
  }
2063
+ /** The WASM module reference. Internal use only. */
2064
+ get _wasm() {
2065
+ return this.#wasm;
2066
+ }
2063
2067
  /**
2064
2068
  * Initialize the panproto SDK by loading the WASM module.
2065
2069
  *
@@ -2365,6 +2369,535 @@ class Panproto {
2365
2369
  this.#protocols.clear();
2366
2370
  }
2367
2371
  }
2372
+ class SchemaEnrichment {
2373
+ #schema;
2374
+ #defaults;
2375
+ #coercions;
2376
+ #mergers;
2377
+ #policies;
2378
+ constructor(schema, defaults = [], coercions = [], mergers = [], policies = []) {
2379
+ this.#schema = schema;
2380
+ this.#defaults = defaults;
2381
+ this.#coercions = coercions;
2382
+ this.#mergers = mergers;
2383
+ this.#policies = policies;
2384
+ }
2385
+ /**
2386
+ * Add a default expression for a vertex.
2387
+ *
2388
+ * The expression is evaluated when forward migration encounters a
2389
+ * missing value at the given vertex.
2390
+ *
2391
+ * @param vertex - The vertex identifier to attach the default to
2392
+ * @param expr - The default expression
2393
+ * @returns A new enrichment with the default added
2394
+ * @throws {@link PanprotoError} if the vertex is not in the schema
2395
+ */
2396
+ addDefault(vertex, expr) {
2397
+ this.#assertVertex(vertex);
2398
+ if (this.#defaults.some((d) => d.vertex === vertex)) {
2399
+ throw new PanprotoError(
2400
+ `Default already exists for vertex "${vertex}". Remove it first with removeDefault().`
2401
+ );
2402
+ }
2403
+ return new SchemaEnrichment(
2404
+ this.#schema,
2405
+ [...this.#defaults, { vertex, expr }],
2406
+ this.#coercions,
2407
+ this.#mergers,
2408
+ this.#policies
2409
+ );
2410
+ }
2411
+ /**
2412
+ * Add a coercion function between two value kinds.
2413
+ *
2414
+ * The expression defines how to convert values from `fromKind` to
2415
+ * `toKind`. It receives a single argument (the source value) and
2416
+ * must produce a value of the target kind.
2417
+ *
2418
+ * @param fromKind - Source value kind (e.g., 'int')
2419
+ * @param toKind - Target value kind (e.g., 'float')
2420
+ * @param expr - The coercion expression
2421
+ * @returns A new enrichment with the coercion added
2422
+ * @throws {@link PanprotoError} if a coercion for this pair already exists
2423
+ */
2424
+ addCoercion(fromKind, toKind, expr) {
2425
+ if (this.#coercions.some((c) => c.from === fromKind && c.to === toKind)) {
2426
+ throw new PanprotoError(
2427
+ `Coercion from "${fromKind}" to "${toKind}" already exists. Remove it first with removeCoercion().`
2428
+ );
2429
+ }
2430
+ return new SchemaEnrichment(
2431
+ this.#schema,
2432
+ this.#defaults,
2433
+ [...this.#coercions, { from: fromKind, to: toKind, expr }],
2434
+ this.#mergers,
2435
+ this.#policies
2436
+ );
2437
+ }
2438
+ /**
2439
+ * Add a merger expression for a vertex.
2440
+ *
2441
+ * The expression defines how to merge two conflicting values at the
2442
+ * given vertex. It receives two arguments (left and right values)
2443
+ * and must produce a single merged value.
2444
+ *
2445
+ * @param vertex - The vertex identifier to attach the merger to
2446
+ * @param expr - The merger expression
2447
+ * @returns A new enrichment with the merger added
2448
+ * @throws {@link PanprotoError} if the vertex is not in the schema
2449
+ */
2450
+ addMerger(vertex, expr) {
2451
+ this.#assertVertex(vertex);
2452
+ if (this.#mergers.some((m) => m.vertex === vertex)) {
2453
+ throw new PanprotoError(
2454
+ `Merger already exists for vertex "${vertex}". Remove it first.`
2455
+ );
2456
+ }
2457
+ return new SchemaEnrichment(
2458
+ this.#schema,
2459
+ this.#defaults,
2460
+ this.#coercions,
2461
+ [...this.#mergers, { vertex, expr }],
2462
+ this.#policies
2463
+ );
2464
+ }
2465
+ /**
2466
+ * Add a conflict resolution policy for a vertex.
2467
+ *
2468
+ * @param vertex - The vertex identifier
2469
+ * @param strategy - The conflict resolution strategy
2470
+ * @returns A new enrichment with the policy added
2471
+ * @throws {@link PanprotoError} if the vertex is not in the schema
2472
+ */
2473
+ addPolicy(vertex, strategy) {
2474
+ this.#assertVertex(vertex);
2475
+ if (this.#policies.some((p) => p.vertex === vertex)) {
2476
+ throw new PanprotoError(
2477
+ `Policy already exists for vertex "${vertex}". Remove it first.`
2478
+ );
2479
+ }
2480
+ return new SchemaEnrichment(
2481
+ this.#schema,
2482
+ this.#defaults,
2483
+ this.#coercions,
2484
+ this.#mergers,
2485
+ [...this.#policies, { vertex, strategy }]
2486
+ );
2487
+ }
2488
+ /**
2489
+ * Remove the default expression for a vertex.
2490
+ *
2491
+ * @param vertex - The vertex identifier
2492
+ * @returns A new enrichment with the default removed
2493
+ * @throws {@link PanprotoError} if no default exists for the vertex
2494
+ */
2495
+ removeDefault(vertex) {
2496
+ const filtered = this.#defaults.filter((d) => d.vertex !== vertex);
2497
+ if (filtered.length === this.#defaults.length) {
2498
+ throw new PanprotoError(
2499
+ `No default exists for vertex "${vertex}".`
2500
+ );
2501
+ }
2502
+ return new SchemaEnrichment(
2503
+ this.#schema,
2504
+ filtered,
2505
+ this.#coercions,
2506
+ this.#mergers,
2507
+ this.#policies
2508
+ );
2509
+ }
2510
+ /**
2511
+ * Remove the coercion function for a value kind pair.
2512
+ *
2513
+ * @param fromKind - Source value kind
2514
+ * @param toKind - Target value kind
2515
+ * @returns A new enrichment with the coercion removed
2516
+ * @throws {@link PanprotoError} if no coercion exists for the pair
2517
+ */
2518
+ removeCoercion(fromKind, toKind) {
2519
+ const filtered = this.#coercions.filter(
2520
+ (c) => !(c.from === fromKind && c.to === toKind)
2521
+ );
2522
+ if (filtered.length === this.#coercions.length) {
2523
+ throw new PanprotoError(
2524
+ `No coercion exists from "${fromKind}" to "${toKind}".`
2525
+ );
2526
+ }
2527
+ return new SchemaEnrichment(
2528
+ this.#schema,
2529
+ this.#defaults,
2530
+ filtered,
2531
+ this.#mergers,
2532
+ this.#policies
2533
+ );
2534
+ }
2535
+ /**
2536
+ * List all enrichments currently attached.
2537
+ *
2538
+ * @returns An enrichment summary with defaults, coercions, mergers, and policies
2539
+ */
2540
+ listEnrichments() {
2541
+ return {
2542
+ defaults: this.#defaults.map((d) => ({ vertex: d.vertex, expr: d.expr })),
2543
+ coercions: this.#coercions.map((c) => ({ from: c.from, to: c.to, expr: c.expr })),
2544
+ mergers: this.#mergers.map((m) => ({ vertex: m.vertex, expr: m.expr })),
2545
+ policies: this.#policies.map((p) => ({ vertex: p.vertex, strategy: p.strategy }))
2546
+ };
2547
+ }
2548
+ /**
2549
+ * Build the enriched schema.
2550
+ *
2551
+ * Returns a new `BuiltSchema` with the enrichments recorded in the
2552
+ * schema data. The underlying WASM handle is shared with the original
2553
+ * schema (enrichments are metadata that the SDK tracks client-side).
2554
+ *
2555
+ * @returns A new BuiltSchema with enrichment metadata
2556
+ */
2557
+ build() {
2558
+ const originalData = this.#schema.data;
2559
+ const enrichedData = {
2560
+ ...originalData,
2561
+ constraints: {
2562
+ ...originalData.constraints
2563
+ }
2564
+ };
2565
+ const enrichedConstraints = { ...enrichedData.constraints };
2566
+ for (const def of this.#defaults) {
2567
+ const existing = enrichedConstraints[def.vertex] ?? [];
2568
+ enrichedConstraints[def.vertex] = [
2569
+ ...existing,
2570
+ { sort: "__default", value: JSON.stringify(def.expr) }
2571
+ ];
2572
+ }
2573
+ for (const coercion of this.#coercions) {
2574
+ const key = `__coercion:${coercion.from}:${coercion.to}`;
2575
+ const existing = enrichedConstraints[key] ?? [];
2576
+ enrichedConstraints[key] = [
2577
+ ...existing,
2578
+ { sort: "__coercion", value: JSON.stringify(coercion.expr) }
2579
+ ];
2580
+ }
2581
+ for (const merger of this.#mergers) {
2582
+ const existing = enrichedConstraints[merger.vertex] ?? [];
2583
+ enrichedConstraints[merger.vertex] = [
2584
+ ...existing,
2585
+ { sort: "__merger", value: JSON.stringify(merger.expr) }
2586
+ ];
2587
+ }
2588
+ for (const policy of this.#policies) {
2589
+ const existing = enrichedConstraints[policy.vertex] ?? [];
2590
+ enrichedConstraints[policy.vertex] = [
2591
+ ...existing,
2592
+ { sort: "__policy", value: JSON.stringify(policy.strategy) }
2593
+ ];
2594
+ }
2595
+ const enrichedSchemaData = {
2596
+ ...enrichedData,
2597
+ constraints: enrichedConstraints
2598
+ };
2599
+ return new BuiltSchema(
2600
+ this.#schema._handle,
2601
+ enrichedSchemaData,
2602
+ this.#schema._wasm
2603
+ );
2604
+ }
2605
+ /**
2606
+ * Assert that a vertex exists in the schema.
2607
+ *
2608
+ * @param vertex - The vertex to check
2609
+ * @throws {@link PanprotoError} if the vertex is not found
2610
+ */
2611
+ #assertVertex(vertex) {
2612
+ if (!(vertex in this.#schema.vertices)) {
2613
+ throw new PanprotoError(
2614
+ `Vertex "${vertex}" not found in schema. Available vertices: ${Object.keys(this.#schema.vertices).join(", ")}`
2615
+ );
2616
+ }
2617
+ }
2618
+ }
2619
+ class ExprBuilder {
2620
+ /** This class is not instantiable; all methods are static. */
2621
+ constructor() {
2622
+ }
2623
+ /**
2624
+ * Create a variable reference expression.
2625
+ *
2626
+ * @param name - The variable name to reference
2627
+ * @returns A variable expression node
2628
+ */
2629
+ static var_(name) {
2630
+ return { type: "var", name };
2631
+ }
2632
+ /**
2633
+ * Create a literal expression.
2634
+ *
2635
+ * @param value - The literal value
2636
+ * @returns A literal expression node
2637
+ */
2638
+ static lit(value) {
2639
+ return { type: "lit", value };
2640
+ }
2641
+ /**
2642
+ * Create a lambda (anonymous function) expression.
2643
+ *
2644
+ * @param param - The parameter name
2645
+ * @param body - The function body expression
2646
+ * @returns A lambda expression node
2647
+ */
2648
+ static lam(param, body) {
2649
+ return { type: "lam", param, body };
2650
+ }
2651
+ /**
2652
+ * Create a function application expression.
2653
+ *
2654
+ * When multiple arguments are provided, they are applied left-to-right
2655
+ * via currying: `app(f, a, b)` becomes `app(app(f, a), b)`.
2656
+ *
2657
+ * @param func - The function expression
2658
+ * @param args - One or more argument expressions
2659
+ * @returns An application expression node (possibly nested)
2660
+ */
2661
+ static app(func, ...args) {
2662
+ let result = func;
2663
+ for (const arg of args) {
2664
+ result = { type: "app", func: result, arg };
2665
+ }
2666
+ return result;
2667
+ }
2668
+ /**
2669
+ * Create a let-binding expression.
2670
+ *
2671
+ * Binds `value` to `name` in the scope of `body`.
2672
+ *
2673
+ * @param name - The variable name to bind
2674
+ * @param value - The value expression to bind
2675
+ * @param body - The body expression where the binding is in scope
2676
+ * @returns A let expression node
2677
+ */
2678
+ static let_(name, value, body) {
2679
+ return { type: "let", name, value, body };
2680
+ }
2681
+ /**
2682
+ * Create a field access expression.
2683
+ *
2684
+ * @param expr - The record expression to access
2685
+ * @param name - The field name
2686
+ * @returns A field access expression node
2687
+ */
2688
+ static field(expr, name) {
2689
+ return { type: "field", expr, name };
2690
+ }
2691
+ /**
2692
+ * Create a record literal expression.
2693
+ *
2694
+ * @param fields - A mapping of field names to expressions
2695
+ * @returns A record expression node
2696
+ */
2697
+ static record(fields) {
2698
+ const entries = Object.entries(fields);
2699
+ return { type: "record", fields: entries };
2700
+ }
2701
+ /**
2702
+ * Create a list literal expression.
2703
+ *
2704
+ * @param items - The list element expressions
2705
+ * @returns A list expression node
2706
+ */
2707
+ static list(...items) {
2708
+ return { type: "list", items };
2709
+ }
2710
+ /**
2711
+ * Create a pattern-match expression.
2712
+ *
2713
+ * @param scrutinee - The expression to match against
2714
+ * @param arms - Pattern-expression pairs tried in order
2715
+ * @returns A match expression node
2716
+ */
2717
+ static match_(scrutinee, arms) {
2718
+ return { type: "match", scrutinee, arms };
2719
+ }
2720
+ /**
2721
+ * Create a builtin operation expression.
2722
+ *
2723
+ * @param op - The builtin operation name
2724
+ * @param args - Argument expressions for the operation
2725
+ * @returns A builtin expression node
2726
+ */
2727
+ static builtin(op, ...args) {
2728
+ return { type: "builtin", op, args };
2729
+ }
2730
+ /**
2731
+ * Create an index expression for list or record access.
2732
+ *
2733
+ * @param expr - The collection expression
2734
+ * @param index - The index expression
2735
+ * @returns An index expression node
2736
+ */
2737
+ static index(expr, index) {
2738
+ return { type: "index", expr, index };
2739
+ }
2740
+ // -----------------------------------------------------------------
2741
+ // Convenience arithmetic helpers
2742
+ // -----------------------------------------------------------------
2743
+ /**
2744
+ * Add two expressions.
2745
+ *
2746
+ * @param a - Left operand
2747
+ * @param b - Right operand
2748
+ * @returns A builtin 'Add' expression
2749
+ */
2750
+ static add(a, b) {
2751
+ return ExprBuilder.builtin("Add", a, b);
2752
+ }
2753
+ /**
2754
+ * Subtract two expressions.
2755
+ *
2756
+ * @param a - Left operand
2757
+ * @param b - Right operand
2758
+ * @returns A builtin 'Sub' expression
2759
+ */
2760
+ static sub(a, b) {
2761
+ return ExprBuilder.builtin("Sub", a, b);
2762
+ }
2763
+ /**
2764
+ * Multiply two expressions.
2765
+ *
2766
+ * @param a - Left operand
2767
+ * @param b - Right operand
2768
+ * @returns A builtin 'Mul' expression
2769
+ */
2770
+ static mul(a, b) {
2771
+ return ExprBuilder.builtin("Mul", a, b);
2772
+ }
2773
+ /**
2774
+ * Concatenate two expressions (strings or lists).
2775
+ *
2776
+ * @param a - Left operand
2777
+ * @param b - Right operand
2778
+ * @returns A builtin 'Concat' expression
2779
+ */
2780
+ static concat(a, b) {
2781
+ return ExprBuilder.builtin("Concat", a, b);
2782
+ }
2783
+ }
2784
+ function classifyOpticKind(chain, schema, _wasm) {
2785
+ const spec = chain.requirements(schema);
2786
+ const hasDefaults = spec.forwardDefaults.length > 0;
2787
+ const hasCaptured = spec.capturedData.length > 0;
2788
+ if (!hasDefaults && !hasCaptured && spec.kind === "empty") {
2789
+ return "iso";
2790
+ }
2791
+ if (hasDefaults && hasCaptured) {
2792
+ return "affine";
2793
+ }
2794
+ if (hasCaptured) {
2795
+ return "lens";
2796
+ }
2797
+ if (hasDefaults) {
2798
+ return "prism";
2799
+ }
2800
+ return "traversal";
2801
+ }
2802
+ function runDryRun(compiled, instances, srcSchema, _tgtSchema, wasm) {
2803
+ const totalRecords = instances.length;
2804
+ const failed = [];
2805
+ let successful = 0;
2806
+ let recordIndex = 0;
2807
+ for (const record of instances) {
2808
+ try {
2809
+ const inputBytes = packToWasm(record);
2810
+ const instanceBytes = wasm.exports.json_to_instance(
2811
+ srcSchema._handle.id,
2812
+ inputBytes
2813
+ );
2814
+ wasm.exports.lift_record(compiled._handle.id, instanceBytes);
2815
+ successful++;
2816
+ } catch (error) {
2817
+ const reason = categorizeFailure(error);
2818
+ failed.push({ recordId: recordIndex, reason });
2819
+ }
2820
+ recordIndex++;
2821
+ }
2822
+ const coverageRatio = totalRecords > 0 ? successful / totalRecords : 1;
2823
+ return {
2824
+ totalRecords,
2825
+ successful,
2826
+ failed,
2827
+ coverageRatio
2828
+ };
2829
+ }
2830
+ function categorizeFailure(error) {
2831
+ const message = error instanceof Error ? error.message : String(error);
2832
+ if (message.includes("constraint") || message.includes("Constraint")) {
2833
+ const constraintMatch = /constraint\s+"?([^"]+)"?\s+violated.*?value\s+"?([^"]*)"?/i.exec(message);
2834
+ return {
2835
+ type: "constraint_violation",
2836
+ constraint: constraintMatch?.[1] ?? "unknown",
2837
+ value: constraintMatch?.[2] ?? "unknown"
2838
+ };
2839
+ }
2840
+ if (message.includes("required") || message.includes("missing")) {
2841
+ const fieldMatch = /(?:required|missing)\s+(?:field\s+)?"?([^"]+)"?/i.exec(message);
2842
+ return {
2843
+ type: "missing_required_field",
2844
+ field: fieldMatch?.[1] ?? "unknown"
2845
+ };
2846
+ }
2847
+ if (message.includes("type") && message.includes("mismatch")) {
2848
+ const typeMatch = /expected\s+"?([^"]+)"?\s+got\s+"?([^"]+)"?/i.exec(message);
2849
+ return {
2850
+ type: "type_mismatch",
2851
+ expected: typeMatch?.[1] ?? "unknown",
2852
+ got: typeMatch?.[2] ?? "unknown"
2853
+ };
2854
+ }
2855
+ return {
2856
+ type: "expr_eval_failed",
2857
+ exprName: "migration",
2858
+ error: message
2859
+ };
2860
+ }
2861
+ class MigrationAnalysis {
2862
+ #wasm;
2863
+ /**
2864
+ * Create a new migration analysis instance.
2865
+ *
2866
+ * @param panproto - The Panproto instance providing WASM access
2867
+ */
2868
+ constructor(panproto) {
2869
+ this.#wasm = panproto._wasm;
2870
+ }
2871
+ /**
2872
+ * Run a dry-run migration and return a coverage report.
2873
+ *
2874
+ * Tests each instance record against the compiled migration without
2875
+ * persisting results, producing detailed failure information for
2876
+ * records that cannot be migrated.
2877
+ *
2878
+ * @param compiled - The compiled migration to test
2879
+ * @param instances - Array of instance records (plain objects)
2880
+ * @param srcSchema - The source schema the instances conform to
2881
+ * @param tgtSchema - The target schema
2882
+ * @returns A coverage report with per-record success/failure data
2883
+ */
2884
+ dryRun(compiled, instances, srcSchema, tgtSchema) {
2885
+ return runDryRun(compiled, instances, srcSchema, tgtSchema, this.#wasm);
2886
+ }
2887
+ /**
2888
+ * Classify the optic kind of a protolens chain.
2889
+ *
2890
+ * Determines whether the chain represents an isomorphism, lens, prism,
2891
+ * affine transformation, or traversal based on its complement structure.
2892
+ *
2893
+ * @param chain - The protolens chain to classify
2894
+ * @param schema - The schema to check the chain against
2895
+ * @returns The optic kind classification
2896
+ */
2897
+ opticKind(chain, schema) {
2898
+ return classifyOpticKind(chain, schema, this.#wasm);
2899
+ }
2900
+ }
2368
2901
  class TheoryHandle {
2369
2902
  #handle;
2370
2903
  /** @internal Retained for future sort/op inspection methods. */
@@ -2503,12 +3036,14 @@ exports.CompatReport = CompatReport;
2503
3036
  exports.CompiledMigration = CompiledMigration;
2504
3037
  exports.DataSetHandle = DataSetHandle;
2505
3038
  exports.ExistenceCheckError = ExistenceCheckError;
3039
+ exports.ExprBuilder = ExprBuilder;
2506
3040
  exports.FullDiffReport = FullDiffReport;
2507
3041
  exports.GRAPHQL_SPEC = GRAPHQL_SPEC;
2508
3042
  exports.Instance = Instance;
2509
3043
  exports.IoRegistry = IoRegistry;
2510
3044
  exports.JSON_SCHEMA_SPEC = JSON_SCHEMA_SPEC;
2511
3045
  exports.LensHandle = LensHandle;
3046
+ exports.MigrationAnalysis = MigrationAnalysis;
2512
3047
  exports.MigrationBuilder = MigrationBuilder;
2513
3048
  exports.MigrationError = MigrationError;
2514
3049
  exports.PROTOBUF_SPEC = PROTOBUF_SPEC;
@@ -2520,6 +3055,7 @@ exports.ProtolensChainHandle = ProtolensChainHandle;
2520
3055
  exports.Repository = Repository;
2521
3056
  exports.SQL_SPEC = SQL_SPEC;
2522
3057
  exports.SchemaBuilder = SchemaBuilder;
3058
+ exports.SchemaEnrichment = SchemaEnrichment;
2523
3059
  exports.SchemaValidationError = SchemaValidationError;
2524
3060
  exports.SymmetricLensHandle = SymmetricLensHandle;
2525
3061
  exports.TheoryBuilder = TheoryBuilder;