@valbuild/shared 0.113.0 → 0.116.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.
@@ -2018,31 +2018,6 @@ var Api = {
2018
2018
  })])
2019
2019
  }
2020
2020
  },
2021
- "/commit-summary": {
2022
- GET: {
2023
- req: {
2024
- query: {
2025
- patch_id: zod.z.array(PatchId)
2026
- },
2027
- cookies: {
2028
- val_session: zod.z.string().optional()
2029
- }
2030
- },
2031
- res: zod.z.union([unauthorizedResponse, zod.z.object({
2032
- status: zod.z.literal(400),
2033
- json: zod.z.object({
2034
- message: zod.z.string()
2035
- })
2036
- }), zod.z.object({
2037
- status: zod.z.literal(200),
2038
- json: zod.z.object({
2039
- patchIds: zod.z.array(PatchId),
2040
- baseSha: zod.z.string(),
2041
- commitSummary: zod.z.string().nullable()
2042
- })
2043
- })])
2044
- }
2045
- },
2046
2021
  "/save": {
2047
2022
  POST: {
2048
2023
  req: {
@@ -2214,7 +2189,21 @@ var Api = {
2214
2189
  status: zod.z.literal(200),
2215
2190
  json: zod.z.object({
2216
2191
  nonce: zod.z.string(),
2217
- wsUrl: zod.z.string()
2192
+ wsUrl: zod.z.string(),
2193
+ /**
2194
+ * The AI providers this project can actually reach.
2195
+ *
2196
+ * AI runs on a key the org or the user brought, so an org with only
2197
+ * an Anthropic key cannot use a GPT model — asking for one gets a
2198
+ * refusal. The client owns the model catalog and picks a model whose
2199
+ * provider is in here.
2200
+ *
2201
+ * `string[]` and not an enum on purpose: the content server may add
2202
+ * a provider before the Studio knows of it, and a strict enum would
2203
+ * reject the whole response over a name this version has not heard
2204
+ * of. Unknown entries are simply ignored.
2205
+ */
2206
+ providers: zod.z.array(zod.z.string()).optional()
2218
2207
  })
2219
2208
  }), zod.z.object({
2220
2209
  status: zod.z.literal(500),
@@ -3460,6 +3449,753 @@ function withValCanvasParam(url) {
3460
3449
  return "".concat(url).concat(separator).concat(VAL_CANVAS_PARAM, "=1");
3461
3450
  }
3462
3451
 
3452
+ /**
3453
+ * Local `yyyy-MM-dd`, which is the one thing this module used `date-fns` for.
3454
+ *
3455
+ * Inlined rather than carried along: `@valbuild/shared` depends on nothing but
3456
+ * `@valbuild/core` and zod, and a whole date library is not worth adding to
3457
+ * that for a single format string. Deliberately the *local* date, not
3458
+ * `toISOString().slice(0, 10)` — the latter is UTC, so anyone west of
3459
+ * Greenwich would get yesterday's date as their "today" default.
3460
+ */
3461
+ function formatLocalDate(date) {
3462
+ var year = String(date.getFullYear()).padStart(4, "0");
3463
+ var month = String(date.getMonth() + 1).padStart(2, "0");
3464
+ var day = String(date.getDate()).padStart(2, "0");
3465
+ return "".concat(year, "-").concat(month, "-").concat(day);
3466
+ }
3467
+ function clampDateString(value, options) {
3468
+ if (options !== null && options !== void 0 && options.to && value > options.to) return options.to;
3469
+ if (options !== null && options !== void 0 && options.from && value < options.from) return options.from;
3470
+ return value;
3471
+ }
3472
+ function clampDateTimeString(value, options) {
3473
+ if (!options) return value;
3474
+ var ms = Date.parse(value);
3475
+ if (Number.isNaN(ms)) return value;
3476
+ // Stored datetime values are always UTC ISO strings, so normalize the
3477
+ // clamped bound (which may carry a timezone offset) via toISOString().
3478
+ if (options.to) {
3479
+ var toMs = Date.parse(options.to);
3480
+ if (!Number.isNaN(toMs) && ms > toMs) return new Date(toMs).toISOString();
3481
+ }
3482
+ if (options.from) {
3483
+ var fromMs = Date.parse(options.from);
3484
+ if (!Number.isNaN(fromMs) && ms < fromMs) return new Date(fromMs).toISOString();
3485
+ }
3486
+ return value;
3487
+ }
3488
+ function emptyOf(schema) {
3489
+ if (schema.type === "object") {
3490
+ return Object.fromEntries(Object.keys(schema.items).map(function (key) {
3491
+ return [key, emptyOf(schema.items[key])];
3492
+ }));
3493
+ } else if (schema.type === "array") {
3494
+ return [];
3495
+ } else if (schema.type === "record") {
3496
+ return {};
3497
+ } else if (schema.opt) {
3498
+ return null;
3499
+ } else if (schema.type === "richtext") {
3500
+ return [];
3501
+ } else if (schema.type === "string") {
3502
+ return "";
3503
+ } else if (schema.type === "boolean") {
3504
+ return false;
3505
+ } else if (schema.type === "number") {
3506
+ return 0;
3507
+ } else if (schema.type === "keyOf") {
3508
+ if (schema.values === "string") {
3509
+ return ""; // TODO: figure out this: user code might very well fail in this case
3510
+ } else {
3511
+ return schema.values[0];
3512
+ }
3513
+ } else if (schema.type === "route") {
3514
+ return ""; // Empty string as default route value
3515
+ } else if (schema.type === "file" || schema.type === "image") {
3516
+ return null; // returning null is the only thing we can do, however, it means that the patches cannot be applied yet since that might fail
3517
+ } else if (schema.type === "literal") {
3518
+ return schema.value;
3519
+ } else if (schema.type === "union") {
3520
+ if (typeof schema.key === "string") {
3521
+ return emptyOf(schema.items[0]);
3522
+ }
3523
+ return schema.key.value;
3524
+ } else if (schema.type === "date") {
3525
+ return clampDateString(formatLocalDate(new Date()), schema.options);
3526
+ } else if (schema.type === "dateTime") {
3527
+ return clampDateTimeString(new Date().toISOString(), schema.options);
3528
+ } else if (schema.type === "color") {
3529
+ var _schema$options$forma, _schema$options;
3530
+ // Mid grey: visible against both a light and a dark canvas
3531
+ return core.Internal.color.formatColor({
3532
+ r: 128,
3533
+ g: 128,
3534
+ b: 128,
3535
+ a: 1
3536
+ }, (_schema$options$forma = (_schema$options = schema.options) === null || _schema$options === void 0 ? void 0 : _schema$options.format) !== null && _schema$options$forma !== void 0 ? _schema$options$forma : core.DEFAULT_COLOR_FORMAT);
3537
+ } else if (schema.type === "code") {
3538
+ return ""; // An empty editor: no language has a sensible starting snippet
3539
+ }
3540
+ var _exhaustiveCheck = schema;
3541
+ throw Error("Unexpected schema type: " + JSON.stringify(_exhaustiveCheck));
3542
+ }
3543
+
3544
+ var toolNames = ["get_all_schema", "get_source", "search_content", "validate_content", "create_patch", "add_session_image_to_gallery", "remove_image_gallery_entry", "navigate_to", "get_current_context", "get_patches", "get_source_path_from_route", "set_session_name", "show_compare_view", "ask_user_question", "duplicate_source", "empty_at_path", "count_entries", "get_record_keys"];
3545
+
3546
+ function buildFileRefValue(filePath, metadata) {
3547
+ return _objectSpread2({
3548
+ path: filePath
3549
+ }, metadata);
3550
+ }
3551
+ function getSourceAt(source, path) {
3552
+ var current = source;
3553
+ var _iterator = _createForOfIteratorHelper(path),
3554
+ _step;
3555
+ try {
3556
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
3557
+ var part = _step.value;
3558
+ if (current === null || current === undefined) return undefined;
3559
+ if (Array.isArray(current)) {
3560
+ var idx = Number(part);
3561
+ if (!Number.isInteger(idx)) return undefined;
3562
+ current = current[idx];
3563
+ } else if (_typeof(current) === "object") {
3564
+ current = current[part];
3565
+ } else {
3566
+ return undefined;
3567
+ }
3568
+ }
3569
+ } catch (err) {
3570
+ _iterator.e(err);
3571
+ } finally {
3572
+ _iterator.f();
3573
+ }
3574
+ return current;
3575
+ }
3576
+ /**
3577
+ * Walks `schema` along `path`, returning a structured result:
3578
+ * - `leaf`: walked the full path; resolved schema at the end.
3579
+ * - `richtext`: hit a richtext mid-walk; richtext is the resolved schema.
3580
+ * - `gallery-traversed`: hit an `s.images()` record and walked past it (i.e. the
3581
+ * path tries to address an entry inside the gallery — a clue the AI should
3582
+ * have used the gallery tool instead).
3583
+ * - `unresolved`: the path could not be walked.
3584
+ */
3585
+ function resolveSerializedSchemaAtPath(schema, path) {
3586
+ var current = schema;
3587
+ for (var i = 0; i < path.length; i++) {
3588
+ var part = path[i];
3589
+ if (current.type === "object") {
3590
+ var next = current.items[part];
3591
+ if (!next) return {
3592
+ kind: "unresolved"
3593
+ };
3594
+ current = next;
3595
+ } else if (current.type === "array") {
3596
+ current = current.item;
3597
+ } else if (current.type === "record") {
3598
+ if (current.mediaType === "images") {
3599
+ return {
3600
+ kind: "gallery-traversed",
3601
+ schema: current
3602
+ };
3603
+ }
3604
+ current = current.item;
3605
+ } else if (current.type === "richtext") {
3606
+ return {
3607
+ kind: "richtext",
3608
+ schema: current,
3609
+ remainingPath: path.slice(i)
3610
+ };
3611
+ } else if (current.type === "union") {
3612
+ if (typeof current.key === "string") {
3613
+ var _iterator2 = _createForOfIteratorHelper(current.items),
3614
+ _step2;
3615
+ try {
3616
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
3617
+ var variant = _step2.value;
3618
+ var resolved = resolveSerializedSchemaAtPath(variant, path.slice(i));
3619
+ if (resolved.kind !== "unresolved") return resolved;
3620
+ }
3621
+ } catch (err) {
3622
+ _iterator2.e(err);
3623
+ } finally {
3624
+ _iterator2.f();
3625
+ }
3626
+ return {
3627
+ kind: "unresolved"
3628
+ };
3629
+ }
3630
+ return {
3631
+ kind: "unresolved"
3632
+ };
3633
+ } else {
3634
+ return {
3635
+ kind: "unresolved"
3636
+ };
3637
+ }
3638
+ }
3639
+ return {
3640
+ kind: "leaf",
3641
+ schema: current
3642
+ };
3643
+ }
3644
+ function getInlineImgInfo(richtextSchema) {
3645
+ var _richtextSchema$optio;
3646
+ if (richtextSchema.type !== "richtext") return {
3647
+ kind: "not-allowed"
3648
+ };
3649
+ var img = (_richtextSchema$optio = richtextSchema.options) === null || _richtextSchema$optio === void 0 || (_richtextSchema$optio = _richtextSchema$optio.inline) === null || _richtextSchema$optio === void 0 ? void 0 : _richtextSchema$optio.img;
3650
+ if (!img) return {
3651
+ kind: "not-allowed"
3652
+ };
3653
+ if (img === true) return {
3654
+ kind: "allowed",
3655
+ remote: false
3656
+ };
3657
+ return {
3658
+ kind: "allowed",
3659
+ remote: img.remote === true
3660
+ };
3661
+ }
3662
+ function isRemoteSchema(schema) {
3663
+ if (schema.type === "image" || schema.type === "file") {
3664
+ return schema.remote === true;
3665
+ }
3666
+ if (schema.type === "record") {
3667
+ return schema.remote === true;
3668
+ }
3669
+ return false;
3670
+ }
3671
+ function safeParsePatch(patch) {
3672
+ var parsed = Patch.safeParse(patch);
3673
+ if (!parsed.success) {
3674
+ return {
3675
+ kind: "error",
3676
+ message: "Built patch failed schema validation: ".concat(JSON.stringify(parsed.error))
3677
+ };
3678
+ }
3679
+ return {
3680
+ kind: "ok",
3681
+ patch: parsed.data
3682
+ };
3683
+ }
3684
+ function checkGallerySchema(moduleSchema) {
3685
+ if (moduleSchema.type === "record" && moduleSchema.mediaType === "images") {
3686
+ return null;
3687
+ }
3688
+ if (moduleSchema.type === "image") {
3689
+ return {
3690
+ kind: "wrong-tool",
3691
+ suggestedTool: "create_patch",
3692
+ reason: "Module is a plain image field, not an images gallery. Use create_patch with a SessionKey value."
3693
+ };
3694
+ }
3695
+ if (moduleSchema.type === "richtext") {
3696
+ return {
3697
+ kind: "wrong-tool",
3698
+ suggestedTool: "create_patch",
3699
+ reason: "Module is a richtext, not an images gallery. Use create_patch with a SessionKey inside a {tag:'img', src: SessionKey} node."
3700
+ };
3701
+ }
3702
+ return {
3703
+ kind: "error",
3704
+ message: "Module schema is not an images gallery (expected record with mediaType=\"images\", got '".concat(moduleSchema.type, "').")
3705
+ };
3706
+ }
3707
+ function buildImageGalleryPatch(args, moduleSchema) {
3708
+ var wrong = checkGallerySchema(moduleSchema);
3709
+ if (wrong) return wrong;
3710
+ var remote = isRemoteSchema(moduleSchema);
3711
+ var patch = [{
3712
+ op: "add",
3713
+ path: [args.filePath],
3714
+ value: _objectSpread2({}, args.metadata)
3715
+ }, {
3716
+ op: "file",
3717
+ path: [args.filePath],
3718
+ filePath: args.filePath,
3719
+ value: args.imageKey,
3720
+ remote: remote,
3721
+ metadata: args.metadata
3722
+ }];
3723
+ return safeParsePatch(patch);
3724
+ }
3725
+ function buildRemoveImageGalleryEntryPatch(args, moduleSchema, moduleSource) {
3726
+ var wrong = checkGallerySchema(moduleSchema);
3727
+ if (wrong) return wrong;
3728
+ if (!moduleSource || _typeof(moduleSource) !== "object" || Array.isArray(moduleSource)) {
3729
+ return {
3730
+ kind: "error",
3731
+ message: "Cannot remove from gallery: module source is not an object."
3732
+ };
3733
+ }
3734
+ var entries = moduleSource;
3735
+ if (!Object.prototype.hasOwnProperty.call(entries, args.filePath)) {
3736
+ var availableKeys = Object.keys(entries);
3737
+ var hint = availableKeys.length > 0 ? " Available entries: ".concat(availableKeys.map(function (k) {
3738
+ return "\"".concat(k, "\"");
3739
+ }).join(", "), ".") : " The gallery is empty.";
3740
+ return {
3741
+ kind: "error",
3742
+ message: "No gallery entry with file_path \"".concat(args.filePath, "\".").concat(hint)
3743
+ };
3744
+ }
3745
+ var remote = core.Internal.remote.splitRemoteRef(args.filePath).status === "success";
3746
+ var patch = [{
3747
+ op: "remove",
3748
+ path: [args.filePath]
3749
+ }, {
3750
+ op: "file",
3751
+ path: [args.filePath],
3752
+ filePath: args.filePath,
3753
+ value: null,
3754
+ remote: remote
3755
+ }];
3756
+ return safeParsePatch(patch);
3757
+ }
3758
+
3759
+ /**
3760
+ * Decides whether the duplicate/empty operation should produce an "add" or
3761
+ * "replace" JSON patch op by inspecting the destination's *parent* schema.
3762
+ *
3763
+ * - array / record parent → "add" (creating a new slot)
3764
+ * - object parent → "replace" (slot exists by schema definition)
3765
+ * - empty path → "replace" (overwriting the whole module)
3766
+ * - richtext / gallery parent → not supported; redirect to the right tool
3767
+ */
3768
+ var GALLERY_REDIRECT = {
3769
+ kind: "wrong-tool",
3770
+ suggestedTool: "add_session_image_to_gallery",
3771
+ reason: "Destination is a media gallery (s.images() / s.files()). Gallery entries are keyed by file path and carry a file on disk, so they cannot be created with a plain patch. Use add_session_image_to_gallery."
3772
+ };
3773
+
3774
+ /** An `s.images()` / `s.files()` record: a gallery, not an ordinary record. */
3775
+ function isGallery(schema) {
3776
+ return schema.type === "record" && schema.mediaType !== undefined;
3777
+ }
3778
+ function decideOp(moduleSchema, destinationPath) {
3779
+ // The DESTINATION itself, checked before its parent: `["gallery"]` resolves
3780
+ // its parent to the enclosing object and would otherwise look like an
3781
+ // ordinary `replace` that overwrites a whole gallery.
3782
+ var destination = resolveSerializedSchemaAtPath(moduleSchema, destinationPath);
3783
+ if (destination.kind === "gallery-traversed") {
3784
+ return GALLERY_REDIRECT;
3785
+ }
3786
+ if (destination.kind === "richtext") {
3787
+ return {
3788
+ kind: "wrong-tool",
3789
+ suggestedTool: "create_patch",
3790
+ reason: "Destination is a richtext value. duplicate_source and empty_at_path do not handle richtext edits."
3791
+ };
3792
+ }
3793
+ if (destination.kind === "leaf") {
3794
+ if (isGallery(destination.schema)) {
3795
+ return GALLERY_REDIRECT;
3796
+ }
3797
+ // A richtext value the path lands exactly ON resolves as a leaf, not as
3798
+ // `kind: "richtext"` (which only fires when the walk continues INTO it).
3799
+ if (destination.schema.type === "richtext") {
3800
+ return {
3801
+ kind: "wrong-tool",
3802
+ suggestedTool: "create_patch",
3803
+ reason: "Destination is a richtext value. duplicate_source and empty_at_path do not handle richtext edits."
3804
+ };
3805
+ }
3806
+ }
3807
+ if (destinationPath.length === 0) {
3808
+ return {
3809
+ kind: "ok",
3810
+ op: "replace",
3811
+ parent: "module"
3812
+ };
3813
+ }
3814
+ var parent = resolveSerializedSchemaAtPath(moduleSchema, destinationPath.slice(0, -1));
3815
+ if (parent.kind === "unresolved") {
3816
+ return {
3817
+ kind: "error",
3818
+ message: "Destination parent path ".concat(JSON.stringify(destinationPath.slice(0, -1)), " does not resolve in this module's schema.")
3819
+ };
3820
+ }
3821
+ if (parent.kind === "richtext") {
3822
+ return {
3823
+ kind: "wrong-tool",
3824
+ suggestedTool: "create_patch",
3825
+ reason: "Destination is inside a richtext value. duplicate_source and empty_at_path do not handle richtext edits."
3826
+ };
3827
+ }
3828
+ // `gallery-traversed` means the walk stepped INTO a gallery, so the
3829
+ // destination is a gallery entry; `isGallery` catches the case where the
3830
+ // parent IS the gallery and the destination is a new entry key in it.
3831
+ if (parent.kind === "gallery-traversed" || isGallery(parent.schema)) {
3832
+ return GALLERY_REDIRECT;
3833
+ }
3834
+ var schema = parent.schema;
3835
+ if (schema.type === "array" || schema.type === "record") {
3836
+ return {
3837
+ kind: "ok",
3838
+ op: "add",
3839
+ parent: schema.type
3840
+ };
3841
+ }
3842
+ if (schema.type === "object") {
3843
+ return {
3844
+ kind: "ok",
3845
+ op: "replace",
3846
+ parent: "object"
3847
+ };
3848
+ }
3849
+ return {
3850
+ kind: "error",
3851
+ message: "Destination parent has schema type \"".concat(schema.type, "\" \u2014 cannot use duplicate_source/empty_at_path here. Use create_patch instead.")
3852
+ };
3853
+ }
3854
+ function buildDuplicatePatch(args, moduleSchema, moduleSource) {
3855
+ var value = getSourceAt(moduleSource, args.sourcePath);
3856
+ if (value === undefined) {
3857
+ return {
3858
+ kind: "error",
3859
+ message: "Source path ".concat(JSON.stringify(args.sourcePath), " does not exist in the module. Use get_source to inspect the current contents.")
3860
+ };
3861
+ }
3862
+ var decision = decideOp(moduleSchema, args.destinationPath);
3863
+ if (decision.kind === "wrong-tool") {
3864
+ return {
3865
+ kind: "wrong-tool",
3866
+ suggestedTool: decision.suggestedTool,
3867
+ reason: decision.reason
3868
+ };
3869
+ }
3870
+ if (decision.kind === "error") {
3871
+ return {
3872
+ kind: "error",
3873
+ message: decision.message
3874
+ };
3875
+ }
3876
+ var occupied = refuseOccupiedRecordKey(decision.parent, moduleSource, args.destinationPath);
3877
+ if (occupied) {
3878
+ return occupied;
3879
+ }
3880
+ return safeParsePatch([{
3881
+ op: decision.op,
3882
+ path: args.destinationPath,
3883
+ value: value
3884
+ }]);
3885
+ }
3886
+
3887
+ /**
3888
+ * Refuse to write over an existing record entry.
3889
+ *
3890
+ * `add` into a record replaces whatever is at that key, so duplicating or
3891
+ * scaffolding onto an occupied key silently destroys the entry that was there.
3892
+ * A caller guessing at keys — an agent especially — hits this by accident, and
3893
+ * nothing downstream reports it: the patch is valid, the content stays valid,
3894
+ * and an entry is simply gone.
3895
+ *
3896
+ * Only records. An array `add` inserts before the index rather than replacing
3897
+ * it, and an object's fields exist by definition, so `replace` on one is the
3898
+ * whole point rather than a mistake.
3899
+ */
3900
+ function refuseOccupiedRecordKey(parent, moduleSource, destinationPath) {
3901
+ if (parent !== "record") {
3902
+ return null;
3903
+ }
3904
+ if (getSourceAt(moduleSource, destinationPath) === undefined) {
3905
+ return null;
3906
+ }
3907
+ var key = destinationPath[destinationPath.length - 1];
3908
+ return {
3909
+ kind: "error",
3910
+ message: "Destination key ".concat(JSON.stringify(key), " already exists, and writing to it would replace the entry that is there. Pick a key that is free -- get_record_keys lists the ones in use -- or use create_patch if you meant to change the existing entry.")
3911
+ };
3912
+ }
3913
+ function buildEmptyAtPathPatch(args, moduleSchema,
3914
+ // Required, not optional: the occupied-key check below is the only thing
3915
+ // standing between "scaffold an entry" and "delete the entry that was there",
3916
+ // and an optional argument is one a caller forgets.
3917
+ moduleSource) {
3918
+ var destinationSchema;
3919
+ if (args.destinationPath.length === 0) {
3920
+ destinationSchema = moduleSchema;
3921
+ } else {
3922
+ var result = resolveSerializedSchemaAtPath(moduleSchema, args.destinationPath);
3923
+ if (result.kind === "unresolved") {
3924
+ return {
3925
+ kind: "error",
3926
+ message: "Destination path ".concat(JSON.stringify(args.destinationPath), " does not resolve in this module's schema.")
3927
+ };
3928
+ }
3929
+ if (result.kind === "richtext") {
3930
+ return {
3931
+ kind: "wrong-tool",
3932
+ suggestedTool: "create_patch",
3933
+ reason: "Destination is a richtext value. Use create_patch to build richtext content."
3934
+ };
3935
+ }
3936
+ if (result.kind === "gallery-traversed") {
3937
+ return {
3938
+ kind: "wrong-tool",
3939
+ suggestedTool: "add_session_image_to_gallery",
3940
+ reason: "Destination is inside an images gallery. Use add_session_image_to_gallery to add entries."
3941
+ };
3942
+ }
3943
+ destinationSchema = result.schema;
3944
+ }
3945
+ var decision = decideOp(moduleSchema, args.destinationPath);
3946
+ if (decision.kind === "wrong-tool") {
3947
+ return {
3948
+ kind: "wrong-tool",
3949
+ suggestedTool: decision.suggestedTool,
3950
+ reason: decision.reason
3951
+ };
3952
+ }
3953
+ if (decision.kind === "error") {
3954
+ return {
3955
+ kind: "error",
3956
+ message: decision.message
3957
+ };
3958
+ }
3959
+ var occupied = refuseOccupiedRecordKey(decision.parent, moduleSource, args.destinationPath);
3960
+ if (occupied) {
3961
+ return occupied;
3962
+ }
3963
+ var value = emptyOf(destinationSchema);
3964
+ return safeParsePatch([{
3965
+ op: decision.op,
3966
+ path: args.destinationPath,
3967
+ value: value
3968
+ }]);
3969
+ }
3970
+
3971
+ /**
3972
+ * What kind of container a path points at.
3973
+ *
3974
+ * `gallery` and `richtext` are called out separately from `record`/`array`
3975
+ * because the two callers disagree about them: `count_entries` can count either,
3976
+ * `get_record_keys` documents that it refuses both.
3977
+ */
3978
+
3979
+ /**
3980
+ * Classifies the value at `path` using the SCHEMA, not just its runtime shape.
3981
+ *
3982
+ * The runtime shape is not enough: an `s.image()`/`s.file()` source, a richtext
3983
+ * node and a union variant are all plain objects at runtime, so classifying by
3984
+ * `typeof` alone let `get_record_keys` list a file ref's `_ref`/`_type` internals
3985
+ * or a richtext node's `tag`/`children` as if they were record keys.
3986
+ */
3987
+ function describeContainerAtPath(moduleSchema, moduleSource, path) {
3988
+ var value = getSourceAt(moduleSource, path);
3989
+ if (value === undefined) {
3990
+ return {
3991
+ kind: "error",
3992
+ reason: "missing",
3993
+ message: "Path ".concat(JSON.stringify(path), " does not exist in the module source. Use get_source to inspect the current contents.")
3994
+ };
3995
+ }
3996
+ var resolved = resolveSerializedSchemaAtPath(moduleSchema, path);
3997
+ if (resolved.kind === "unresolved") {
3998
+ return {
3999
+ kind: "error",
4000
+ reason: "missing",
4001
+ message: "Path ".concat(JSON.stringify(path), " does not resolve in this module's schema.")
4002
+ };
4003
+ }
4004
+ if (resolved.kind === "richtext") {
4005
+ // Mid-richtext: the path addresses a node inside the richtext value, whose
4006
+ // internals (tag, children, ...) are not entries.
4007
+ return {
4008
+ kind: "error",
4009
+ reason: "not-a-container",
4010
+ message: "Path ".concat(JSON.stringify(path), " points inside a richtext value, which has no entries to count or list.")
4011
+ };
4012
+ }
4013
+ if (resolved.kind === "gallery-traversed") {
4014
+ return {
4015
+ kind: "error",
4016
+ reason: "not-a-container",
4017
+ message: "Path ".concat(JSON.stringify(path), " points inside a gallery entry, which has no entries to count or list.")
4018
+ };
4019
+ }
4020
+ var schema = resolved.schema;
4021
+ if (schema.type === "richtext") {
4022
+ if (!Array.isArray(value)) {
4023
+ return {
4024
+ kind: "error",
4025
+ reason: "not-a-container",
4026
+ message: "Path ".concat(JSON.stringify(path), " is a richtext value but its source is not a block array.")
4027
+ };
4028
+ }
4029
+ return {
4030
+ kind: "ok",
4031
+ container: "richtext",
4032
+ value: value
4033
+ };
4034
+ }
4035
+ if (schema.type === "array") {
4036
+ if (!Array.isArray(value)) {
4037
+ return {
4038
+ kind: "error",
4039
+ reason: "not-a-container",
4040
+ message: "Path ".concat(JSON.stringify(path), " is an array in the schema but its source is not an array.")
4041
+ };
4042
+ }
4043
+ return {
4044
+ kind: "ok",
4045
+ container: "array",
4046
+ value: value
4047
+ };
4048
+ }
4049
+ if (schema.type === "record" || schema.type === "object") {
4050
+ if (value === null || _typeof(value) !== "object" || Array.isArray(value)) {
4051
+ return {
4052
+ kind: "error",
4053
+ reason: "not-a-container",
4054
+ message: "Path ".concat(JSON.stringify(path), " points to a ").concat(value === null ? "null" : Array.isArray(value) ? "array" : _typeof(value), " value, which has no entries to count or list.")
4055
+ };
4056
+ }
4057
+ return {
4058
+ kind: "ok",
4059
+ container: schema.type === "record" ? isGallery(schema) ? "gallery" : "record" : "object",
4060
+ value: value
4061
+ };
4062
+ }
4063
+ // image / file / union / string / number / boolean / date / literal / keyOf /
4064
+ // route: not containers, whatever their runtime shape looks like.
4065
+ return {
4066
+ kind: "error",
4067
+ reason: "not-a-container",
4068
+ message: "Path ".concat(JSON.stringify(path), " points to a \"").concat(schema.type, "\" value, which has no entries to count or list.")
4069
+ };
4070
+ }
4071
+
4072
+ /**
4073
+ * Partition validation errors by whether the UI should surface them.
4074
+ *
4075
+ * `skipped` errors carry only fix codes the user can't (or shouldn't) act on
4076
+ * in the browser:
4077
+ * - image/file metadata checks (need to read bytes from disk)
4078
+ * - remote upload/download/check (need a personal access token + network)
4079
+ * - gallery directory uniqueness + untracked-files checks (need fs traversal)
4080
+ *
4081
+ * The server (ValOps + createFixPatch) resolves all of these automatically on
4082
+ * save, and the CLI's `runValidation` does the same. So the user never needs
4083
+ * to see them in the studio — but we keep them in the `skipped` partition,
4084
+ * not dropped, so we can later surface a "N fixes pending — saving will
4085
+ * repair them" badge or wire up an explicit "fix all" affordance without
4086
+ * having to plumb the data back in.
4087
+ *
4088
+ * `keyof:check-keys` and `router:check-route` are surfaced if they reach
4089
+ * this function — they're meant to be resolved upfront by the shared
4090
+ * `resolveSchemaSourceFixes`. A remnant here means resolution didn't run
4091
+ * for some reason; the user-facing "Invalid key/route" message is more
4092
+ * useful than silent hiding.
4093
+ */
4094
+ function partitionValidationErrors(validationErrors) {
4095
+ var surfaced = {};
4096
+ var skipped = {};
4097
+ for (var sourcePathS in validationErrors) {
4098
+ var sourcePath = sourcePathS;
4099
+ var _iterator = _createForOfIteratorHelper(validationErrors[sourcePath]),
4100
+ _step;
4101
+ try {
4102
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
4103
+ var error = _step.value;
4104
+ if (isSkippedFix(error)) {
4105
+ var _skipped$sourcePath;
4106
+ ((_skipped$sourcePath = skipped[sourcePath]) !== null && _skipped$sourcePath !== void 0 ? _skipped$sourcePath : skipped[sourcePath] = []).push(error);
4107
+ } else {
4108
+ var _surfaced$sourcePath;
4109
+ ((_surfaced$sourcePath = surfaced[sourcePath]) !== null && _surfaced$sourcePath !== void 0 ? _surfaced$sourcePath : surfaced[sourcePath] = []).push(error);
4110
+ }
4111
+ }
4112
+ } catch (err) {
4113
+ _iterator.e(err);
4114
+ } finally {
4115
+ _iterator.f();
4116
+ }
4117
+ }
4118
+ return {
4119
+ surfaced: surfaced,
4120
+ skipped: skipped
4121
+ };
4122
+ }
4123
+ function isSkippedFix(error) {
4124
+ var _error$fixes;
4125
+ var fixes = (_error$fixes = error.fixes) !== null && _error$fixes !== void 0 ? _error$fixes : [];
4126
+ if (fixes.length === 0) return false;
4127
+ // Skip iff EVERY fix on the error is in the skip set — an error tagged
4128
+ // with both a surfaceable and a skippable fix must still be shown.
4129
+ return fixes.every(isSkippableFixCode);
4130
+ }
4131
+ function isSkippableFixCode(fix) {
4132
+ switch (fix) {
4133
+ case "image:add-metadata":
4134
+ case "image:check-metadata":
4135
+ case "image:upload-remote":
4136
+ case "image:download-remote":
4137
+ case "image:check-remote":
4138
+ case "images:check-remote":
4139
+ case "images:upload-remote":
4140
+ case "file:add-metadata":
4141
+ case "file:check-metadata":
4142
+ case "file:upload-remote":
4143
+ case "file:download-remote":
4144
+ case "file:check-remote":
4145
+ case "files:check-remote":
4146
+ case "files:upload-remote":
4147
+ case "images:check-unique-folder":
4148
+ case "files:check-unique-folder":
4149
+ case "images:check-all-files":
4150
+ case "files:check-all-files":
4151
+ return true;
4152
+ case "keyof:check-keys":
4153
+ case "router:check-route":
4154
+ case "jsonValues:extract-entry":
4155
+ // `jsonValues:extract-entry` is an entry inlined in the `.val.ts`. The
4156
+ // browser cannot fix it (that means writing a new `*.val.json` and
4157
+ // rewriting the module) and saving does not repair it either, so surface
4158
+ // it: the message names the command (`val validate --fix`) that does.
4159
+ return false;
4160
+ default:
4161
+ {
4162
+ return false;
4163
+ }
4164
+ }
4165
+ }
4166
+
4167
+ /**
4168
+ * The errors a user must actually act on.
4169
+ *
4170
+ * Two passes stand between a schema's complaint and something worth blocking a
4171
+ * publish for:
4172
+ *
4173
+ * - `resolveSchemaSourceFixes` resolves the CROSS-MODULE fixes —
4174
+ * `keyof:check-keys` and `router:check-route` — against the live schema and
4175
+ * source. Unresolved, those arrive as "This error should typically be
4176
+ * processed by Val internally. Seeing this error most likely means you have a
4177
+ * Val version mismatch", which is not something a user can do anything with.
4178
+ * - `partitionValidationErrors` drops the fixes the SERVER applies on save:
4179
+ * image and file metadata, remote files, gallery directory checks. Blocking on
4180
+ * those would refuse a publish for something the publish itself repairs.
4181
+ *
4182
+ * ## Everything that gates on validation must use this
4183
+ *
4184
+ * That is the whole reason it lives in `validation/` rather than beside one of
4185
+ * its callers. There are three: the errors a field shows, the AI flow's check on
4186
+ * a proposed patch, and — the one that bites — `system.publish`. A publish gate
4187
+ * reading raw errors refuses on `router:check-route` for every route module in
4188
+ * the project, which is a Save button that does nothing and says
4189
+ * "validation-errors" with no error visible anywhere on screen.
4190
+ */
4191
+ function filterBlockingValidationErrors(validationErrors, schemas, sources) {
4192
+ var resolved = resolveSchemaSourceFixes(validationErrors, {
4193
+ schemas: schemas !== null && schemas !== void 0 ? schemas : {},
4194
+ sources: sources !== null && sources !== void 0 ? sources : {}
4195
+ });
4196
+ return partitionValidationErrors(resolved).surfaced;
4197
+ }
4198
+
3463
4199
  exports.Api = Api;
3464
4200
  exports.CodeOptions = CodeOptions;
3465
4201
  exports.ColorOptions = ColorOptions;
@@ -3515,25 +4251,40 @@ exports.VAL_STATE_COOKIE = VAL_STATE_COOKIE;
3515
4251
  exports.VAL_THEME_SESSION_STORAGE_KEY = VAL_THEME_SESSION_STORAGE_KEY;
3516
4252
  exports.ValCommit = ValCommit;
3517
4253
  exports.ValDeployment = ValDeployment;
4254
+ exports.buildDuplicatePatch = buildDuplicatePatch;
4255
+ exports.buildEmptyAtPathPatch = buildEmptyAtPathPatch;
4256
+ exports.buildFileRefValue = buildFileRefValue;
4257
+ exports.buildImageGalleryPatch = buildImageGalleryPatch;
4258
+ exports.buildRemoveImageGalleryEntryPatch = buildRemoveImageGalleryEntryPatch;
3518
4259
  exports.createRegExpFromPattern = createRegExpFromPattern;
3519
4260
  exports.createValClient = createValClient;
4261
+ exports.describeContainerAtPath = describeContainerAtPath;
4262
+ exports.emptyOf = emptyOf;
4263
+ exports.filterBlockingValidationErrors = filterBlockingValidationErrors;
3520
4264
  exports.filterRoutesByPatterns = filterRoutesByPatterns;
3521
4265
  exports.findSimilar = findSimilar;
3522
4266
  exports.getErrorMessageFromUnknownJson = getErrorMessageFromUnknownJson;
4267
+ exports.getInlineImgInfo = getInlineImgInfo;
3523
4268
  exports.getNextAppRouterSitemapTree = getNextAppRouterSitemapTree;
3524
4269
  exports.getNextAppRouterSourceFolder = getNextAppRouterSourceFolder;
3525
4270
  exports.getPatternFromModuleFilePath = getPatternFromModuleFilePath;
3526
4271
  exports.getRoutesOf = getRoutesOf;
3527
4272
  exports.getRoutesWithModulePaths = getRoutesWithModulePaths;
4273
+ exports.getSourceAt = getSourceAt;
4274
+ exports.isRemoteSchema = isRemoteSchema;
3528
4275
  exports.isValCanvasFrame = isValCanvasFrame;
3529
4276
  exports.isValCanvasPageMessage = isValCanvasPageMessage;
3530
4277
  exports.isValCanvasStudioMessage = isValCanvasStudioMessage;
3531
4278
  exports.levenshtein = levenshtein;
3532
4279
  exports.parseRoutePattern = parseRoutePattern;
4280
+ exports.partitionValidationErrors = partitionValidationErrors;
3533
4281
  exports.remirrorToRichTextSource = remirrorToRichTextSource;
3534
4282
  exports.resolveSchemaSourceFixForError = resolveSchemaSourceFixForError;
3535
4283
  exports.resolveSchemaSourceFixes = resolveSchemaSourceFixes;
4284
+ exports.resolveSerializedSchemaAtPath = resolveSerializedSchemaAtPath;
3536
4285
  exports.richTextToRemirror = richTextToRemirror;
4286
+ exports.safeParsePatch = safeParsePatch;
4287
+ exports.toolNames = toolNames;
3537
4288
  exports.urlOf = urlOf;
3538
4289
  exports.validateRoutePatterns = validateRoutePatterns;
3539
4290
  exports.withValCanvasParam = withValCanvasParam;