sbuilder-mcp 0.46.3 → 0.47.1
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/CHANGELOG.md +18 -0
- package/CHANGELOG.vi.md +18 -0
- package/dist/catalog/deadkeys.generated.js +4 -14
- package/dist/catalog/elements.generated.js +246 -26
- package/dist/catalog/search.js +219 -3
- package/dist/catalog/shapes.generated.js +1 -1
- package/dist/catalog/translations.generated.js +2 -1
- package/dist/domains/site/vocabulary.js +10 -5
- package/dist/tools/api.js +21 -1
- package/dist/tools/page.js +102 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@ All notable changes to this project are documented in this file.
|
|
|
6
6
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
7
7
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## [0.47.1] - 2026-09-14
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- `sb_page_create` with `is_homepage` set on a site that already has one no longer creates a duplicate that steals the star and leaves the platform's own original home page reachable at no address; it now adopts the existing home page instead, renaming it when the caller named it something else, and reports what it did instead of creating anything.
|
|
13
|
+
|
|
14
|
+
## [0.47.0] - 2026-09-14
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `sb_api_find`'s call sheet now names a `summary_covers` list whenever the platform wrote one sentence for several routes at once, so a call sheet is no longer read as if its summary belongs to that route alone; `sb_api_find` adds a one-time directive pointing at the sharpest case, `/refund` versus `/refund-via-gateway`, where reading the shared sentence would record a refund that never pays anybody.
|
|
18
|
+
- `sb_api_find` and `sb_api_call` no longer inline a request body definition for a GET or DELETE operation whenever the document's declared body is a verbatim copy of a write operation's, naming the route it was copied from instead of describing a body the route cannot take.
|
|
19
|
+
- `sb_api_find` and `sb_api_call` now list every path parameter a route actually needs, even when the document's own `@Param` list omits it, so a caller no longer has to make a failing call first to discover an argument the sheet never mentioned.
|
|
20
|
+
- The catalog's legal-value vocabulary grows to cover 18 more elements read directly off the platform's own declarations, including `popup`'s trigger frequency and close position, `tab`'s alignment, `menu`'s expand type and submenu style, `theme-switcher` and `locale-switcher`'s variants, `chat-widget`'s corner, `cart-drawer` and `hamburger-menu`'s slide-in edge, and a new "Product viewer" mode on `spline-scene` for drag-to-turn, pinch-to-zoom inspection of a 3D model.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- Duplicate `@Param` entries copied onto an operation by a shared doc comment no longer appear more than once in `sb_api_find`'s call sheet.
|
|
24
|
+
- `image-comparison`'s `splitDirection` config key is no longer reported as a dead key, because the platform itself has dropped the seed rather than leaving it unread.
|
|
25
|
+
- The catalog's fallback note for a config or specials value now credits the renderer that actually normalises an unrecognised value, instead of crediting a platform vocabulary declaration that defines the legal words but says nothing about what happens to any other one.
|
|
26
|
+
|
|
9
27
|
## [0.46.3] - 2026-09-14
|
|
10
28
|
|
|
11
29
|
### Fixed
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,24 @@ Mọi thay đổi đáng chú ý của dự án được ghi lại trong file n
|
|
|
6
6
|
Định dạng dựa trên [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
7
7
|
và dự án tuân theo [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## [0.47.1] - 2026-09-14
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- `sb_page_create` với `is_homepage` được bật trên một site đã có sẵn trang chủ giờ không còn tạo ra một trang trùng lặp cướp mất ngôi sao và khiến trang chủ gốc của nền tảng trở nên không thể truy cập ở bất kỳ địa chỉ nào; giờ nó nhận lại trang chủ hiện có, đổi tên nếu người gọi đặt tên khác, và báo cáo việc đã làm thay vì tạo trang mới.
|
|
13
|
+
|
|
14
|
+
## [0.47.0] - 2026-09-14
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- Call sheet của `sb_api_find` giờ nêu danh sách `summary_covers` mỗi khi nền tảng viết chung một câu mô tả cho nhiều route, nhờ đó một call sheet không còn bị đọc như thể câu mô tả đó chỉ dành riêng cho route ấy; `sb_api_find` thêm một chỉ dẫn phát một lần trỏ đến trường hợp gay gắt nhất, `/refund` so với `/refund-via-gateway`, nơi đọc theo câu mô tả dùng chung sẽ ghi nhận một khoản hoàn tiền mà không ai thực sự nhận được.
|
|
18
|
+
- `sb_api_find` và `sb_api_call` không còn chèn nguyên định nghĩa request body cho một operation GET hoặc DELETE mỗi khi body được khai báo trong tài liệu là bản sao y nguyên của một operation ghi dữ liệu, mà thay vào đó nêu tên route đã bị sao chép từ đó thay vì mô tả một body mà route này không thể nhận.
|
|
19
|
+
- `sb_api_find` và `sb_api_call` giờ liệt kê đầy đủ mọi path parameter mà một route thực sự cần, kể cả khi danh sách `@Param` của tài liệu bỏ sót nó, nhờ đó người gọi không còn phải thử gọi thất bại trước mới phát hiện ra một đối số mà call sheet chưa từng nhắc tới.
|
|
20
|
+
- Bảng giá trị hợp lệ của catalog mở rộng để bao phủ thêm 18 phần tử, đọc trực tiếp từ các khai báo của chính nền tảng, gồm tần suất kích hoạt và vị trí nút đóng của `popup`, căn chỉnh của `tab`, kiểu mở rộng và kiểu submenu của `menu`, biến thể của `theme-switcher` và `locale-switcher`, góc đặt của `chat-widget`, cạnh trượt ra của `cart-drawer` và `hamburger-menu`, cùng một chế độ mới "Product viewer" trên `spline-scene` cho phép kéo để xoay, chụm để phóng to khi xem một mô hình 3D.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- Các mục `@Param` bị trùng do một đoạn chú thích tài liệu dùng chung sao chép vào một operation giờ không còn xuất hiện nhiều lần trong call sheet của `sb_api_find`.
|
|
24
|
+
- Config key `splitDirection` của `image-comparison` không còn bị báo là dead key, vì chính nền tảng đã bỏ luôn giá trị seed đó thay vì để nó không ai đọc tới.
|
|
25
|
+
- Ghi chú fallback của catalog cho một giá trị config hoặc specials giờ ghi công cho đúng renderer thực sự chuẩn hóa một giá trị không nhận diện được, thay vì ghi công cho một khai báo bảng giá trị của nền tảng — vốn chỉ định nghĩa các từ hợp lệ chứ không nói gì về việc điều gì xảy ra với một từ khác.
|
|
26
|
+
|
|
9
27
|
## [0.46.3] - 2026-09-14
|
|
10
28
|
|
|
11
29
|
### Fixed
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export const DEAD_KEY_SOURCE = {
|
|
2
|
-
"files":
|
|
3
|
-
"identifiers":
|
|
2
|
+
"files": 3974,
|
|
3
|
+
"identifiers": 76582,
|
|
4
4
|
"seededKeys": 384,
|
|
5
|
-
"dead":
|
|
5
|
+
"dead": 0
|
|
6
6
|
};
|
|
7
7
|
/**
|
|
8
8
|
* Keys an element SEEDS and no renderer anywhere reads.
|
|
@@ -16,14 +16,4 @@ export const DEAD_KEY_SOURCE = {
|
|
|
16
16
|
* because the platform accepts the value and a refusal here would invent a rule
|
|
17
17
|
* it does not have.
|
|
18
18
|
*/
|
|
19
|
-
export const DEAD_KEYS = {
|
|
20
|
-
"splitDirection": {
|
|
21
|
-
"key": "splitDirection",
|
|
22
|
-
"namespaces": [
|
|
23
|
-
"config"
|
|
24
|
-
],
|
|
25
|
-
"seededBy": [
|
|
26
|
-
"image-comparison"
|
|
27
|
-
]
|
|
28
|
-
}
|
|
29
|
-
};
|
|
19
|
+
export const DEAD_KEYS = {};
|
|
@@ -3832,7 +3832,8 @@ export const ELEMENTS = {
|
|
|
3832
3832
|
"modelUrl": "",
|
|
3833
3833
|
"posterUrl": "",
|
|
3834
3834
|
"sceneControls": [],
|
|
3835
|
-
"sceneGallery": "podium"
|
|
3835
|
+
"sceneGallery": "podium",
|
|
3836
|
+
"sceneViewer": "off"
|
|
3836
3837
|
},
|
|
3837
3838
|
"style": {
|
|
3838
3839
|
"width": "100%",
|
|
@@ -3899,6 +3900,13 @@ export const ELEMENTS = {
|
|
|
3899
3900
|
"scene_model"
|
|
3900
3901
|
]
|
|
3901
3902
|
},
|
|
3903
|
+
{
|
|
3904
|
+
"key": "scene_viewer",
|
|
3905
|
+
"label": "Product viewer",
|
|
3906
|
+
"controls": [
|
|
3907
|
+
"scene_viewer"
|
|
3908
|
+
]
|
|
3909
|
+
},
|
|
3902
3910
|
{
|
|
3903
3911
|
"key": "scene_display",
|
|
3904
3912
|
"label": "Display",
|
|
@@ -3966,6 +3974,7 @@ export const ELEMENTS = {
|
|
|
3966
3974
|
"scene_effect",
|
|
3967
3975
|
"scene_gallery",
|
|
3968
3976
|
"scene_model",
|
|
3977
|
+
"scene_viewer",
|
|
3969
3978
|
"scene_display",
|
|
3970
3979
|
"scene_controls",
|
|
3971
3980
|
"border",
|
|
@@ -3993,7 +4002,8 @@ export const ELEMENTS = {
|
|
|
3993
4002
|
"Effect: four shaders (gradient mesh, floating particles, waves, aurora) with speed, intensity and a grain toggle. Its colours follow the site theme until you switch them to custom",
|
|
3994
4003
|
"Spline: paste the link from Spline — Export → Viewer → copy link (…/scene.splinecode)",
|
|
3995
4004
|
"Add a poster image so the box is not blank while a Spline scene loads; an effect draws immediately and needs none",
|
|
3996
|
-
"Scene controls (rotate on scroll, tilt with the mouse) drive named objects, so they apply to a Spline scene and not to an effect, which has no objects"
|
|
4005
|
+
"Scene controls (rotate on scroll, tilt with the mouse) drive named objects, so they apply to a Spline scene and not to an effect, which has no objects",
|
|
4006
|
+
"For letting a shopper INSPECT a product, turn on Product viewer instead of building scene controls — it is drag to turn, wheel or pinch to zoom, and a slow idle spin, and it works on a phone where the mouse triggers do not. It is offered on the built-in scenes and on your own model, and it replaces the scene controls rather than joining them: both move the same object"
|
|
3997
4007
|
],
|
|
3998
4008
|
"semantics": [
|
|
3999
4009
|
"3d",
|
|
@@ -9777,7 +9787,6 @@ export const ELEMENTS = {
|
|
|
9777
9787
|
"afterSize": "cover",
|
|
9778
9788
|
"afterPosition": "center center",
|
|
9779
9789
|
"splitPosition": 50,
|
|
9780
|
-
"splitDirection": "horizontal",
|
|
9781
9790
|
"imageRatio": "auto"
|
|
9782
9791
|
}
|
|
9783
9792
|
},
|
|
@@ -36916,6 +36925,24 @@ export const ELEMENT_VALUES = {
|
|
|
36916
36925
|
],
|
|
36917
36926
|
"fallback": "",
|
|
36918
36927
|
"readBy": "nodes/popup/html.go:triggerType"
|
|
36928
|
+
},
|
|
36929
|
+
"triggerFreq": {
|
|
36930
|
+
"target": "config",
|
|
36931
|
+
"writeKey": "triggerFreq",
|
|
36932
|
+
"values": [
|
|
36933
|
+
"always",
|
|
36934
|
+
"once"
|
|
36935
|
+
],
|
|
36936
|
+
"readBy": "POPUP_TRIGGER_FREQS (VOCAB in schema/src/elements/popup/meta.ts)"
|
|
36937
|
+
},
|
|
36938
|
+
"closePos": {
|
|
36939
|
+
"target": "config",
|
|
36940
|
+
"writeKey": "closePos",
|
|
36941
|
+
"values": [
|
|
36942
|
+
"inside",
|
|
36943
|
+
"outside"
|
|
36944
|
+
],
|
|
36945
|
+
"readBy": "POPUP_CLOSE_POSITIONS (VOCAB in schema/src/elements/popup/meta.ts)"
|
|
36919
36946
|
}
|
|
36920
36947
|
},
|
|
36921
36948
|
"pricing-dataset": {
|
|
@@ -36944,6 +36971,15 @@ export const ELEMENT_VALUES = {
|
|
|
36944
36971
|
"open": true,
|
|
36945
36972
|
"readBy": "nodes/product-image-feature/css.go:featureRatioCss"
|
|
36946
36973
|
},
|
|
36974
|
+
"featureClickAction": {
|
|
36975
|
+
"target": "config",
|
|
36976
|
+
"writeKey": "featureClickAction",
|
|
36977
|
+
"values": [
|
|
36978
|
+
"open_gallery",
|
|
36979
|
+
"open_product_page"
|
|
36980
|
+
],
|
|
36981
|
+
"readBy": "FEATURE_CLICK_ACTIONS (VOCAB in schema/src/elements/product-image-feature/meta.ts)"
|
|
36982
|
+
},
|
|
36947
36983
|
"main_image_source": {
|
|
36948
36984
|
"target": "config",
|
|
36949
36985
|
"writeKey": "mainImageSource",
|
|
@@ -36964,11 +37000,49 @@ export const ELEMENT_VALUES = {
|
|
|
36964
37000
|
"right",
|
|
36965
37001
|
"top"
|
|
36966
37002
|
],
|
|
37003
|
+
"readBy": "TAB_POSITIONS (VOCAB in schema/src/elements/tab/meta.ts)",
|
|
36967
37004
|
"fallback": "top",
|
|
36968
|
-
"
|
|
37005
|
+
"fallbackReadBy": "nodes/tab/html.go:position"
|
|
37006
|
+
},
|
|
37007
|
+
"tabAlign": {
|
|
37008
|
+
"target": "config",
|
|
37009
|
+
"writeKey": "tabAlign",
|
|
37010
|
+
"values": [
|
|
37011
|
+
"center",
|
|
37012
|
+
"left",
|
|
37013
|
+
"right"
|
|
37014
|
+
],
|
|
37015
|
+
"readBy": "TAB_ALIGNS (VOCAB in schema/src/elements/tab/meta.ts)"
|
|
36969
37016
|
}
|
|
36970
37017
|
},
|
|
36971
37018
|
"spline-scene": {
|
|
37019
|
+
"eventsTarget": {
|
|
37020
|
+
"target": "config",
|
|
37021
|
+
"writeKey": "eventsTarget",
|
|
37022
|
+
"values": [
|
|
37023
|
+
"global",
|
|
37024
|
+
"local"
|
|
37025
|
+
],
|
|
37026
|
+
"readBy": "SCENE_EVENTS_TARGETS (VOCAB in schema/src/elements/spline-scene/meta.ts)"
|
|
37027
|
+
},
|
|
37028
|
+
"mobileMode": {
|
|
37029
|
+
"target": "config",
|
|
37030
|
+
"writeKey": "mobileMode",
|
|
37031
|
+
"values": [
|
|
37032
|
+
"poster",
|
|
37033
|
+
"scene"
|
|
37034
|
+
],
|
|
37035
|
+
"readBy": "SCENE_MOBILE_MODES (VOCAB in schema/src/elements/spline-scene/meta.ts)"
|
|
37036
|
+
},
|
|
37037
|
+
"effectColors": {
|
|
37038
|
+
"target": "config",
|
|
37039
|
+
"writeKey": "effectColors",
|
|
37040
|
+
"values": [
|
|
37041
|
+
"custom",
|
|
37042
|
+
"theme"
|
|
37043
|
+
],
|
|
37044
|
+
"readBy": "SCENE_COLOR_MODES (VOCAB in schema/src/elements/spline-scene/meta.ts)"
|
|
37045
|
+
},
|
|
36972
37046
|
"source": {
|
|
36973
37047
|
"target": "specials",
|
|
36974
37048
|
"writeKey": "source",
|
|
@@ -36980,6 +37054,15 @@ export const ELEMENT_VALUES = {
|
|
|
36980
37054
|
],
|
|
36981
37055
|
"readBy": "SCENE_SOURCES (schema/src/elements/spline-scene/meta.ts)"
|
|
36982
37056
|
},
|
|
37057
|
+
"sceneViewer": {
|
|
37058
|
+
"target": "specials",
|
|
37059
|
+
"writeKey": "sceneViewer",
|
|
37060
|
+
"values": [
|
|
37061
|
+
"off",
|
|
37062
|
+
"on"
|
|
37063
|
+
],
|
|
37064
|
+
"readBy": "SCENE_VIEWER_MODES (schema/src/elements/spline-scene/meta.ts)"
|
|
37065
|
+
},
|
|
36983
37066
|
"effect": {
|
|
36984
37067
|
"target": "config",
|
|
36985
37068
|
"writeKey": "effect",
|
|
@@ -37012,7 +37095,7 @@ export const ELEMENT_VALUES = {
|
|
|
37012
37095
|
"normal",
|
|
37013
37096
|
"slow"
|
|
37014
37097
|
],
|
|
37015
|
-
"readBy": "SCENE_SPEEDS (schema/src/elements/spline-scene/meta.ts)"
|
|
37098
|
+
"readBy": "SCENE_SPEEDS (VOCAB in schema/src/elements/spline-scene/meta.ts)"
|
|
37016
37099
|
},
|
|
37017
37100
|
"intensity": {
|
|
37018
37101
|
"target": "config",
|
|
@@ -37022,16 +37105,41 @@ export const ELEMENT_VALUES = {
|
|
|
37022
37105
|
"soft",
|
|
37023
37106
|
"strong"
|
|
37024
37107
|
],
|
|
37025
|
-
"readBy": "SCENE_INTENSITIES (schema/src/elements/spline-scene/meta.ts)"
|
|
37026
|
-
}
|
|
37027
|
-
|
|
37108
|
+
"readBy": "SCENE_INTENSITIES (VOCAB in schema/src/elements/spline-scene/meta.ts)"
|
|
37109
|
+
}
|
|
37110
|
+
},
|
|
37111
|
+
"text-marquee": {
|
|
37112
|
+
"marqueeDirection": {
|
|
37028
37113
|
"target": "config",
|
|
37029
|
-
"writeKey": "
|
|
37114
|
+
"writeKey": "marqueeDirection",
|
|
37030
37115
|
"values": [
|
|
37031
|
-
"
|
|
37032
|
-
"
|
|
37116
|
+
"left",
|
|
37117
|
+
"right"
|
|
37118
|
+
],
|
|
37119
|
+
"readBy": "MARQUEE_DIRECTIONS (VOCAB in schema/src/elements/text-marquee/meta.ts)"
|
|
37120
|
+
}
|
|
37121
|
+
},
|
|
37122
|
+
"google-map": {
|
|
37123
|
+
"mapType": {
|
|
37124
|
+
"target": "specials",
|
|
37125
|
+
"writeKey": "mapType",
|
|
37126
|
+
"values": [
|
|
37127
|
+
"code",
|
|
37128
|
+
"location"
|
|
37033
37129
|
],
|
|
37034
|
-
"readBy": "
|
|
37130
|
+
"readBy": "MAP_TYPES (VOCAB in schema/src/elements/google-map/meta.ts)"
|
|
37131
|
+
}
|
|
37132
|
+
},
|
|
37133
|
+
"form-file": {
|
|
37134
|
+
"fileFormat": {
|
|
37135
|
+
"target": "specials",
|
|
37136
|
+
"writeKey": "fileFormat",
|
|
37137
|
+
"values": [
|
|
37138
|
+
"image",
|
|
37139
|
+
"pdf",
|
|
37140
|
+
"video"
|
|
37141
|
+
],
|
|
37142
|
+
"readBy": "FILE_FORMATS (VOCAB in schema/src/elements/form-file/meta.ts)"
|
|
37035
37143
|
}
|
|
37036
37144
|
},
|
|
37037
37145
|
"form-select": {
|
|
@@ -37060,6 +37168,17 @@ export const ELEMENT_VALUES = {
|
|
|
37060
37168
|
"readBy": "OPTION_SOURCES (schema/src/elements/formOptionSource.ts)"
|
|
37061
37169
|
}
|
|
37062
37170
|
},
|
|
37171
|
+
"form-payment": {
|
|
37172
|
+
"logoShape": {
|
|
37173
|
+
"target": "specials",
|
|
37174
|
+
"writeKey": "logoShape",
|
|
37175
|
+
"values": [
|
|
37176
|
+
"auto",
|
|
37177
|
+
"square"
|
|
37178
|
+
],
|
|
37179
|
+
"readBy": "PAY_LOGO_SHAPES (VOCAB in schema/src/elements/form-payment/meta.ts)"
|
|
37180
|
+
}
|
|
37181
|
+
},
|
|
37063
37182
|
"form-checkbox": {
|
|
37064
37183
|
"optionSource": {
|
|
37065
37184
|
"target": "config",
|
|
@@ -37116,6 +37235,94 @@ export const ELEMENT_VALUES = {
|
|
|
37116
37235
|
"readBy": "DATE_PICKERS (schema/src/elements/form-calendar/meta.ts)"
|
|
37117
37236
|
}
|
|
37118
37237
|
},
|
|
37238
|
+
"locale-switcher": {
|
|
37239
|
+
"variant": {
|
|
37240
|
+
"target": "specials",
|
|
37241
|
+
"writeKey": "variant",
|
|
37242
|
+
"values": [
|
|
37243
|
+
"dropdown",
|
|
37244
|
+
"inline",
|
|
37245
|
+
"panel",
|
|
37246
|
+
"select"
|
|
37247
|
+
],
|
|
37248
|
+
"readBy": "LOCALE_SWITCH_VARIANTS (VOCAB in schema/src/elements/locale-switcher/meta.ts)"
|
|
37249
|
+
},
|
|
37250
|
+
"locale_switch_label": {
|
|
37251
|
+
"target": "specials",
|
|
37252
|
+
"writeKey": "labelMode",
|
|
37253
|
+
"values": [
|
|
37254
|
+
"code",
|
|
37255
|
+
"name",
|
|
37256
|
+
"both",
|
|
37257
|
+
"currency"
|
|
37258
|
+
],
|
|
37259
|
+
"readBy": "locale_switch_label (editor picker)"
|
|
37260
|
+
}
|
|
37261
|
+
},
|
|
37262
|
+
"theme-switcher": {
|
|
37263
|
+
"variant": {
|
|
37264
|
+
"target": "specials",
|
|
37265
|
+
"writeKey": "variant",
|
|
37266
|
+
"values": [
|
|
37267
|
+
"icon",
|
|
37268
|
+
"segmented",
|
|
37269
|
+
"switch"
|
|
37270
|
+
],
|
|
37271
|
+
"readBy": "THEME_SWITCH_VARIANTS (VOCAB in schema/src/elements/theme-switcher/meta.ts)"
|
|
37272
|
+
}
|
|
37273
|
+
},
|
|
37274
|
+
"image-marquee": {
|
|
37275
|
+
"marqueeDirection": {
|
|
37276
|
+
"target": "config",
|
|
37277
|
+
"writeKey": "marqueeDirection",
|
|
37278
|
+
"values": [
|
|
37279
|
+
"left",
|
|
37280
|
+
"right"
|
|
37281
|
+
],
|
|
37282
|
+
"readBy": "MARQUEE_DIRECTIONS (VOCAB in schema/src/elements/image-marquee/meta.ts)"
|
|
37283
|
+
}
|
|
37284
|
+
},
|
|
37285
|
+
"menu": {
|
|
37286
|
+
"expandType": {
|
|
37287
|
+
"target": "config",
|
|
37288
|
+
"writeKey": "expandType",
|
|
37289
|
+
"values": [
|
|
37290
|
+
"click",
|
|
37291
|
+
"hover"
|
|
37292
|
+
],
|
|
37293
|
+
"readBy": "EXPAND_TYPES (VOCAB in schema/src/elements/menu/meta.ts)"
|
|
37294
|
+
},
|
|
37295
|
+
"submenuStyle": {
|
|
37296
|
+
"target": "config",
|
|
37297
|
+
"writeKey": "submenuStyle",
|
|
37298
|
+
"values": [
|
|
37299
|
+
"cascade",
|
|
37300
|
+
"collapse",
|
|
37301
|
+
"dropdown"
|
|
37302
|
+
],
|
|
37303
|
+
"readBy": "SUBMENU_STYLES (VOCAB in schema/src/elements/menu/meta.ts)"
|
|
37304
|
+
}
|
|
37305
|
+
},
|
|
37306
|
+
"chat-widget": {
|
|
37307
|
+
"side": {
|
|
37308
|
+
"target": "config",
|
|
37309
|
+
"writeKey": "side",
|
|
37310
|
+
"values": [
|
|
37311
|
+
"left",
|
|
37312
|
+
"right"
|
|
37313
|
+
],
|
|
37314
|
+
"readBy": "CHAT_HSIDES (VOCAB in schema/src/elements/chat-widget/meta.ts)"
|
|
37315
|
+
},
|
|
37316
|
+
"vside": {
|
|
37317
|
+
"target": "config",
|
|
37318
|
+
"writeKey": "vside",
|
|
37319
|
+
"values": [
|
|
37320
|
+
"bottom",
|
|
37321
|
+
"top"
|
|
37322
|
+
],
|
|
37323
|
+
"readBy": "CHAT_VSIDES (VOCAB in schema/src/elements/chat-widget/meta.ts)"
|
|
37324
|
+
}
|
|
37325
|
+
},
|
|
37119
37326
|
"*": {
|
|
37120
37327
|
"backgroundSceneSource": {
|
|
37121
37328
|
"target": "config",
|
|
@@ -37180,7 +37387,7 @@ export const ELEMENT_VALUES = {
|
|
|
37180
37387
|
"custom",
|
|
37181
37388
|
"theme"
|
|
37182
37389
|
],
|
|
37183
|
-
"readBy": "
|
|
37390
|
+
"readBy": "SCENE_COLOR_MODES (schema/src/elements/spline-scene/meta.ts)"
|
|
37184
37391
|
}
|
|
37185
37392
|
},
|
|
37186
37393
|
"filter-checkbox": {
|
|
@@ -37487,19 +37694,6 @@ export const ELEMENT_VALUES = {
|
|
|
37487
37694
|
"readBy": "cart_total_part (editor picker)"
|
|
37488
37695
|
}
|
|
37489
37696
|
},
|
|
37490
|
-
"locale-switcher": {
|
|
37491
|
-
"locale_switch_label": {
|
|
37492
|
-
"target": "specials",
|
|
37493
|
-
"writeKey": "labelMode",
|
|
37494
|
-
"values": [
|
|
37495
|
-
"code",
|
|
37496
|
-
"name",
|
|
37497
|
-
"both",
|
|
37498
|
-
"currency"
|
|
37499
|
-
],
|
|
37500
|
-
"readBy": "locale_switch_label (editor picker)"
|
|
37501
|
-
}
|
|
37502
|
-
},
|
|
37503
37697
|
"member-field": {
|
|
37504
37698
|
"member_field_source": {
|
|
37505
37699
|
"target": "specials",
|
|
@@ -37635,6 +37829,32 @@ export const ELEMENT_VALUES = {
|
|
|
37635
37829
|
],
|
|
37636
37830
|
"readBy": "qr_source (editor picker)"
|
|
37637
37831
|
}
|
|
37832
|
+
},
|
|
37833
|
+
"cart-drawer": {
|
|
37834
|
+
"direct": {
|
|
37835
|
+
"target": "config",
|
|
37836
|
+
"writeKey": "direct",
|
|
37837
|
+
"values": [
|
|
37838
|
+
"bottom",
|
|
37839
|
+
"left",
|
|
37840
|
+
"right",
|
|
37841
|
+
"top"
|
|
37842
|
+
],
|
|
37843
|
+
"readBy": "DRAWER_EDGES (VOCAB in schema/src/elements/cart-drawer/meta.ts)"
|
|
37844
|
+
}
|
|
37845
|
+
},
|
|
37846
|
+
"hamburger-menu": {
|
|
37847
|
+
"direct": {
|
|
37848
|
+
"target": "config",
|
|
37849
|
+
"writeKey": "direct",
|
|
37850
|
+
"values": [
|
|
37851
|
+
"bottom",
|
|
37852
|
+
"left",
|
|
37853
|
+
"right",
|
|
37854
|
+
"top"
|
|
37855
|
+
],
|
|
37856
|
+
"readBy": "DRAWER_EDGES (VOCAB in schema/src/elements/hamburger-menu/meta.ts)"
|
|
37857
|
+
}
|
|
37638
37858
|
}
|
|
37639
37859
|
};
|
|
37640
37860
|
/**
|
package/dist/catalog/search.js
CHANGED
|
@@ -76,6 +76,199 @@ export function searchOperations(query, opts = {}) {
|
|
|
76
76
|
* Saying "no body" for the second group would be the silent failure — the model
|
|
77
77
|
* would send an empty PUT and wipe a page. So the two get different words.
|
|
78
78
|
*/
|
|
79
|
+
/**
|
|
80
|
+
* ONE DOC BLOCK OVER SEVERAL `@Router` LINES, AND THE SUMMARY IS THE HALF
|
|
81
|
+
* NOTHING CORRECTED.
|
|
82
|
+
*
|
|
83
|
+
* This file already records the defect for BODIES — swag attaches one comment
|
|
84
|
+
* block's `@Param body` to every route beneath it, so a listing GET claimed a
|
|
85
|
+
* body it does not take — and the fix was to read the decode site instead. The
|
|
86
|
+
* SUMMARY is copied by the same mechanism and has no such correction: 302 of
|
|
87
|
+
* 524 operations share a summary with another operation.
|
|
88
|
+
*
|
|
89
|
+
* MOST OF THAT IS HARMLESS AND MUST NOT BE FLAGGED. "List, create, update or
|
|
90
|
+
* delete a course's lessons" on all four CRUD routes is an UMBRELLA — it names
|
|
91
|
+
* the whole group and is true of each member, and 105 of the 119 shared
|
|
92
|
+
* summaries are that shape. Flagging them would bury the ones that matter.
|
|
93
|
+
*
|
|
94
|
+
* THE DISCRIMINATOR IS THE TRAILING LITERAL SEGMENT, which is the same signal
|
|
95
|
+
* `scripts/shapes.ts` already trusts to pick a handler out of a dispatcher that
|
|
96
|
+
* routes by path ("the route's own trailing literal segment picks the callee").
|
|
97
|
+
* Operations sharing a summary while differing only by METHOD or by a trailing
|
|
98
|
+
* `{param}` are one resource; ones whose literal tails DIFFER are separate
|
|
99
|
+
* ACTIONS, and one sentence can describe at most one of them. That leaves 14
|
|
100
|
+
* groups and 41 operations.
|
|
101
|
+
*
|
|
102
|
+
* WHY THIS IS WORTH A FIELD. The costliest is money: this platform documents
|
|
103
|
+
* `/refund` as RECORDING a refund made elsewhere and `/refund-via-gateway` as
|
|
104
|
+
* ASKING the gateway to send it, and the catalog gives BOTH — plus `POST
|
|
105
|
+
* /payment-transactions`, which opens a pay link — the single summary "ASK the
|
|
106
|
+
* gateway to send the money back". An agent asked to refund a customer reaches
|
|
107
|
+
* for the obvious name, reads a sentence promising the money moves, and records
|
|
108
|
+
* a refund that never pays anybody. `POST .../versions` (snapshot) and
|
|
109
|
+
* `.../restore` share one summary while running in OPPOSITE directions, and
|
|
110
|
+
* `/invitations/{id}/accept` and `/decline` are both described as "Withdraw an
|
|
111
|
+
* invitation".
|
|
112
|
+
*
|
|
113
|
+
* IT REPORTS THE COVERAGE AND NEVER GUESSES THE MISSING SENTENCE. The per-route
|
|
114
|
+
* text does exist — in each package's route-map comment — but those maps write
|
|
115
|
+
* paths relatively, abbreviate methods (`PATCH/DEL`), carry query strings and
|
|
116
|
+
* often no description at all, so recovering a summary from them would invent
|
|
117
|
+
* exactly the kind of confident wrong sentence this exists to remove. Naming
|
|
118
|
+
* the routes the sentence covers is knowable, is enough for the agent to read
|
|
119
|
+
* the path instead, and cannot be wrong.
|
|
120
|
+
*
|
|
121
|
+
* Derived from the catalog rather than generated into it: it is a fact ABOUT
|
|
122
|
+
* the document, not one read off the platform, so generating it would ship a
|
|
123
|
+
* table restating data the same file already carries.
|
|
124
|
+
*/
|
|
125
|
+
const summaryCoverage = (() => {
|
|
126
|
+
let byId = null;
|
|
127
|
+
/** The last segment that is not a `{param}` — an action tail, or a resource. */
|
|
128
|
+
const tail = (path) => {
|
|
129
|
+
const literal = path.split('/').filter((seg) => seg && !seg.startsWith('{'));
|
|
130
|
+
return literal[literal.length - 1] ?? '';
|
|
131
|
+
};
|
|
132
|
+
const build = () => {
|
|
133
|
+
const groups = new Map();
|
|
134
|
+
for (const op of API_OPERATIONS) {
|
|
135
|
+
if (!op.summary)
|
|
136
|
+
continue;
|
|
137
|
+
const g = groups.get(op.summary);
|
|
138
|
+
if (g)
|
|
139
|
+
g.push(op);
|
|
140
|
+
else
|
|
141
|
+
groups.set(op.summary, [op]);
|
|
142
|
+
}
|
|
143
|
+
const out = new Map();
|
|
144
|
+
for (const members of groups.values()) {
|
|
145
|
+
if (members.length < 2)
|
|
146
|
+
continue;
|
|
147
|
+
if (new Set(members.map((m) => tail(m.path))).size < 2)
|
|
148
|
+
continue;
|
|
149
|
+
for (const op of members) {
|
|
150
|
+
out.set(op.id, members.filter((m) => m.id !== op.id).map((m) => `${m.method} ${m.path}`));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
};
|
|
155
|
+
return (id) => (byId ??= build()).get(id);
|
|
156
|
+
})();
|
|
157
|
+
/** Whether this operation's summary is one the platform wrote for a group. */
|
|
158
|
+
export function summaryIsShared(id) {
|
|
159
|
+
return summaryCoverage(id) !== undefined;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* THE PARAMS, ONCE EACH.
|
|
163
|
+
*
|
|
164
|
+
* The stacked block copies `@Param` lines as well as prose, so an operation in
|
|
165
|
+
* a group of four carries `siteId` four times: 29 operations list a parameter
|
|
166
|
+
* more than once, and `POST /payment-transactions` reports `siteId` three
|
|
167
|
+
* times. It is noise in the one field an agent reads to build the call, and it
|
|
168
|
+
* costs tokens on every search line.
|
|
169
|
+
*
|
|
170
|
+
* Deduplicated by NAME AND LOCATION, which is what identifies a parameter on
|
|
171
|
+
* the wire — two entries agreeing on both are one parameter however many blocks
|
|
172
|
+
* declared it. The first is kept, so a description is never invented or merged.
|
|
173
|
+
* No operation claims a path param its own path lacks (measured), so the
|
|
174
|
+
* deduplication only ever removes a repeat.
|
|
175
|
+
*
|
|
176
|
+
* AND THE PATH IS THE AUTHORITY ON WHAT THE CALL NEEDS, which is the other
|
|
177
|
+
* direction and was costing a round trip. THIRTY operations declare a `{param}`
|
|
178
|
+
* in their path that the document lists under no `@Param` at all —
|
|
179
|
+
* `POST /api/sites/{siteId}/pages` lists NONE of its own, and
|
|
180
|
+
* `PUT /api/sites/{siteId}/courses/{id}/questions/{questionId}` lists every one
|
|
181
|
+
* but `questionId`. `callOperation` already ignores this list and walks the
|
|
182
|
+
* PATH, so the call is not broken; what breaks is the agent, which reads
|
|
183
|
+
* `params`, sends what it says, and is told it is missing an argument the sheet
|
|
184
|
+
* never mentioned. Synthesised here so the sheet says what the call demands,
|
|
185
|
+
* and marked so nobody mistakes it for something the platform described.
|
|
186
|
+
*/
|
|
187
|
+
function visibleParams(op) {
|
|
188
|
+
const seen = new Set();
|
|
189
|
+
const out = op.params.filter((p) => {
|
|
190
|
+
if (p.in === 'body')
|
|
191
|
+
return false;
|
|
192
|
+
const id = `${p.in}:${p.name}`;
|
|
193
|
+
if (seen.has(id))
|
|
194
|
+
return false;
|
|
195
|
+
seen.add(id);
|
|
196
|
+
return true;
|
|
197
|
+
});
|
|
198
|
+
for (const m of op.path.matchAll(/\{([^}]+)\}/g)) {
|
|
199
|
+
if (seen.has(`path:${m[1]}`))
|
|
200
|
+
continue;
|
|
201
|
+
seen.add(`path:${m[1]}`);
|
|
202
|
+
out.push({
|
|
203
|
+
name: m[1],
|
|
204
|
+
in: 'path',
|
|
205
|
+
required: true,
|
|
206
|
+
type: 'string',
|
|
207
|
+
description: 'In the path, and undocumented — recovered from the route itself.',
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* A READ THAT CLAIMS A BODY IS CLAIMING ITS NEIGHBOUR'S.
|
|
214
|
+
*
|
|
215
|
+
* The same stacking that copies a summary copies every `@Param` in the block,
|
|
216
|
+
* so 90 GET and DELETE operations declare a request body. `describeOperation`
|
|
217
|
+
* believed them and inlined the DEFINITION: measured, ~83 KB of body schema
|
|
218
|
+
* across those 90 call sheets — about 927 bytes each — describing a body the
|
|
219
|
+
* route cannot take. `GET /api/sites/{siteId}/customers`, a listing, ships the
|
|
220
|
+
* whole customer object and reports `body: "described"`.
|
|
221
|
+
*
|
|
222
|
+
* THE DECODE SITES CANNOT ANSWER THIS ONE, and reading their silence as a no
|
|
223
|
+
* would be the mistake this repo already records in another form ("the absence
|
|
224
|
+
* of a string is evidence about the string, not about the behaviour").
|
|
225
|
+
* `scripts/shapes.ts` sets `WRITE_METHODS = POST | PUT | PATCH` and never looks
|
|
226
|
+
* at a read, so "0 of 90 confirmed" is structural, not a finding.
|
|
227
|
+
*
|
|
228
|
+
* SO THE TEST IS THE MECHANISM ITSELF, which is visible in the document. A
|
|
229
|
+
* stacked block gives every route the SAME `@Param`, byte for byte — name, type
|
|
230
|
+
* and description. MEASURED: all 90 carry a body param identical to some write
|
|
231
|
+
* operation's, and for 86 that donor is on the same path or the same resource,
|
|
232
|
+
* which is a doc block covering several methods. That is the copy caught in the
|
|
233
|
+
* act, not an assumption about what a GET may carry.
|
|
234
|
+
*
|
|
235
|
+
* It answers only for reads. A WRITE's body claim stays believed: this file
|
|
236
|
+
* already records that most writes are UNDER-annotated, and `PUT
|
|
237
|
+
* /pages/{id}/source` carries a whole page document while declaring nothing —
|
|
238
|
+
* so the risk there runs the other way and a shared `@Param` may be the only
|
|
239
|
+
* description of a real body.
|
|
240
|
+
*/
|
|
241
|
+
const copiedBodyDonor = (() => {
|
|
242
|
+
let byId = null;
|
|
243
|
+
const sig = (p) => JSON.stringify([p.name, p.type, p.description]);
|
|
244
|
+
const build = () => {
|
|
245
|
+
const donors = new Map();
|
|
246
|
+
for (const op of API_OPERATIONS) {
|
|
247
|
+
if (op.method !== 'POST' && op.method !== 'PUT' && op.method !== 'PATCH')
|
|
248
|
+
continue;
|
|
249
|
+
for (const p of op.params)
|
|
250
|
+
if (p.in === 'body' && !donors.has(sig(p)))
|
|
251
|
+
donors.set(sig(p), op);
|
|
252
|
+
}
|
|
253
|
+
const out = new Map();
|
|
254
|
+
for (const op of API_OPERATIONS) {
|
|
255
|
+
if (op.method !== 'GET' && op.method !== 'DELETE')
|
|
256
|
+
continue;
|
|
257
|
+
const bodies = op.params.filter((p) => p.in === 'body');
|
|
258
|
+
if (!bodies.length)
|
|
259
|
+
continue;
|
|
260
|
+
// EVERY one must be accounted for. A read carrying one copied param and
|
|
261
|
+
// one of its own would be a shape nothing here has seen, and the honest
|
|
262
|
+
// answer to that is to say nothing and leave the document's claim alone.
|
|
263
|
+
const found = bodies.map((p) => donors.get(sig(p)));
|
|
264
|
+
if (found.some((d) => d === undefined))
|
|
265
|
+
continue;
|
|
266
|
+
out.set(op.id, `${found[0].method} ${found[0].path}`);
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
};
|
|
270
|
+
return (id) => (byId ??= build()).get(id);
|
|
271
|
+
})();
|
|
79
272
|
export function describeOperation(op) {
|
|
80
273
|
const out = {
|
|
81
274
|
id: op.id,
|
|
@@ -84,8 +277,14 @@ export function describeOperation(op) {
|
|
|
84
277
|
summary: op.summary,
|
|
85
278
|
tags: op.tags,
|
|
86
279
|
credential: op.credential,
|
|
87
|
-
params: op
|
|
280
|
+
params: visibleParams(op),
|
|
88
281
|
};
|
|
282
|
+
// SAY WHO ELSE THE SENTENCE ABOVE IS ABOUT. Placed directly under `summary`
|
|
283
|
+
// because that is the field it qualifies: an agent that has read the sentence
|
|
284
|
+
// and moved on has already taken the wrong operation.
|
|
285
|
+
const covers = summaryCoverage(op.id);
|
|
286
|
+
if (covers)
|
|
287
|
+
out.summary_covers = covers;
|
|
89
288
|
const hasBody = op.params.some((p) => p.in === 'body');
|
|
90
289
|
const isWrite = op.method === 'POST' || op.method === 'PUT' || op.method === 'PATCH';
|
|
91
290
|
// THE HANDLER OUTRANKS THE DOCUMENT. `swagger.json` describes 46 of 212 write
|
|
@@ -172,6 +371,16 @@ export function describeOperation(op) {
|
|
|
172
371
|
out.body_shape = shape;
|
|
173
372
|
return out;
|
|
174
373
|
}
|
|
374
|
+
// Said BEFORE the body branches, because the whole point is not to inline a
|
|
375
|
+
// definition for a body this route does not take.
|
|
376
|
+
const copiedFrom = copiedBodyDonor(op.id);
|
|
377
|
+
if (copiedFrom) {
|
|
378
|
+
out.body_note =
|
|
379
|
+
`The document declares a request body here, and it is a VERBATIM copy of ${copiedFrom}'s ` +
|
|
380
|
+
'— one doc comment over several @Router lines gives every route in the block the same ' +
|
|
381
|
+
'@Param. Send no body; the schema is that other route\'s.';
|
|
382
|
+
return out;
|
|
383
|
+
}
|
|
175
384
|
if (hasBody && op.bodyDescribed && op.bodyRef) {
|
|
176
385
|
out.body_schema = API_DEFINITIONS[op.bodyRef];
|
|
177
386
|
}
|
|
@@ -209,9 +418,16 @@ export function summarizeOperation(op) {
|
|
|
209
418
|
path: op.path,
|
|
210
419
|
summary: op.summary,
|
|
211
420
|
credential: op.credential,
|
|
212
|
-
params: op
|
|
421
|
+
params: visibleParams(op).map((p) => (p.required ? p.name : `?${p.name}`)),
|
|
213
422
|
};
|
|
214
|
-
|
|
423
|
+
// ONE BOOLEAN, because this line is where the CHOICE is made and a search hit
|
|
424
|
+
// must stay a line. It says only "this sentence was written for a group of
|
|
425
|
+
// routes, so read the path"; the call sheet names the group.
|
|
426
|
+
if (summaryIsShared(op.id))
|
|
427
|
+
out.summary_shared = true;
|
|
428
|
+
// A copied body is not this route's, so the line says nothing rather than
|
|
429
|
+
// `described` — see `copiedBodyDonor`.
|
|
430
|
+
if (hasBody && !copiedBodyDonor(op.id))
|
|
215
431
|
out.body = op.bodyDescribed && op.bodyRef ? 'described' : 'undescribed';
|
|
216
432
|
else if (isWrite)
|
|
217
433
|
out.body = 'none_declared';
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
export const TRANSLATION_SOURCE = {
|
|
4
4
|
"elements": 58,
|
|
5
5
|
"pairs": 142,
|
|
6
|
-
"neverKeys":
|
|
6
|
+
"neverKeys": 164,
|
|
7
7
|
"entityTypes": 12
|
|
8
8
|
};
|
|
9
9
|
/** Every entity type a translation record can name, including "node". */
|
|
@@ -417,6 +417,7 @@ export const NEVER_TRANSLATED = [
|
|
|
417
417
|
"sceneControls",
|
|
418
418
|
"sceneGallery",
|
|
419
419
|
"sceneUrl",
|
|
420
|
+
"sceneViewer",
|
|
420
421
|
"searchBehavior",
|
|
421
422
|
"searchClearSwap",
|
|
422
423
|
"searchDebounceMs",
|
|
@@ -128,10 +128,15 @@ export function vocabularyForWrite(type, namespace, key) {
|
|
|
128
128
|
* `mediaImageRatio: "4 / 5"` is CORRECT and reporting it would send a caller
|
|
129
129
|
* to "fix" a working page — the same cost this repo already records for the
|
|
130
130
|
* `category` alias. Nothing is said at all.
|
|
131
|
-
* - A FALLBACK IS ONLY CLAIMED WHERE
|
|
132
|
-
* proves what an author may choose and is
|
|
133
|
-
* with anything else. Naming a fallback
|
|
134
|
-
* table exists to remove, so the note says
|
|
131
|
+
* - A FALLBACK IS ONLY CLAIMED WHERE A SOURCE SAYS ONE, AND IT IS CREDITED TO
|
|
132
|
+
* THAT SOURCE. The editor's picker proves what an author may choose and is
|
|
133
|
+
* silent on what the renderer does with anything else. Naming a fallback
|
|
134
|
+
* there would be the invention this table exists to remove, so the note says
|
|
135
|
+
* what it knows and stops. Where a schema declaration has SUPERSEDED a Go
|
|
136
|
+
* reading the two halves come from different places — the list says which
|
|
137
|
+
* words mean something, the renderer's `default:` arm says what an unknown
|
|
138
|
+
* one renders as — so the sentence names `fallbackReadBy`, not the
|
|
139
|
+
* declaration, which normalises nothing.
|
|
135
140
|
*/
|
|
136
141
|
export function unknownWriteNote(type, namespace, key, value) {
|
|
137
142
|
const vocab = vocabularyForWrite(type, namespace, key);
|
|
@@ -144,7 +149,7 @@ export function unknownWriteNote(type, namespace, key, value) {
|
|
|
144
149
|
const list = vocab.values.map((v) => (v === '' ? '"" (unset)' : v)).join(', ');
|
|
145
150
|
return (`${namespace}.${key} = ${JSON.stringify(value)} is not a value ${type}'s renderer knows. ` +
|
|
146
151
|
(vocab.fallback !== undefined
|
|
147
|
-
? `${vocab.readBy} normalises anything unrecognised to ${vocab.fallback === '' ? '"" (unset)' : `"${vocab.fallback}"`}, so this stores, saves and publishes with no error and renders as that. `
|
|
152
|
+
? `${vocab.fallbackReadBy ?? vocab.readBy} normalises anything unrecognised to ${vocab.fallback === '' ? '"" (unset)' : `"${vocab.fallback}"`}, so this stores, saves and publishes with no error and renders as that. `
|
|
148
153
|
: `${vocab.readBy} is the source, and it is silent on what the renderer does with a word ` +
|
|
149
154
|
'outside the list — every comparable key in this catalog normalises silently rather ' +
|
|
150
155
|
'than erroring. ') +
|
package/dist/tools/api.js
CHANGED
|
@@ -303,6 +303,20 @@ export async function callOperation(ctx, args) {
|
|
|
303
303
|
const projection = args.pick ?? LIST_PROJECTIONS[op.id];
|
|
304
304
|
return shapeResponse(raw, { pick: projection, max_items: args.max_items });
|
|
305
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* WHY A SUMMARY CAN BE ABOUT A DIFFERENT ROUTE.
|
|
308
|
+
*
|
|
309
|
+
* The platform stacks ONE doc comment over several `@Router` lines and swag
|
|
310
|
+
* copies it to each, so the summary — and the description, which it also merges
|
|
311
|
+
* across blocks — belongs to the group rather than to the route. This repo
|
|
312
|
+
* already corrects the same defect for BODIES by reading the decode site; there
|
|
313
|
+
* is no such correction for prose, and no per-route sentence exists to recover.
|
|
314
|
+
*/
|
|
315
|
+
const SHARED_SUMMARY_NOTICE = 'summary_covers means the platform wrote that one sentence for several routes at once (one ' +
|
|
316
|
+
'doc comment over several @Router lines), so it may describe one of the listed routes rather ' +
|
|
317
|
+
'than this one — trust the PATH and the body shape, not the sentence. The sharpest case is ' +
|
|
318
|
+
'money: /payment-transactions/{id}/refund only RECORDS a refund made elsewhere, while ' +
|
|
319
|
+
'/refund-via-gateway is the one that asks the gateway to actually send it.';
|
|
306
320
|
export function registerApiTools(server, ctx) {
|
|
307
321
|
server.registerTool('sb_api_find', {
|
|
308
322
|
description: 'Search platform API operations by intent (query: one line per match), or read one ' +
|
|
@@ -327,7 +341,13 @@ export function registerApiTools(server, ctx) {
|
|
|
327
341
|
const op = findOperation(id);
|
|
328
342
|
if (!op)
|
|
329
343
|
throw new Error(`sbuilder: unknown operation "${id}" — search with query first`);
|
|
330
|
-
|
|
344
|
+
const sheet = describeOperation(op);
|
|
345
|
+
// The WHY is a directive, so it is said once per process; the routes it
|
|
346
|
+
// covers are DATA and ride on every sheet that has them.
|
|
347
|
+
const why = sheet.summary_covers
|
|
348
|
+
? ctx.notices.once('shared_summary', SHARED_SUMMARY_NOTICE)
|
|
349
|
+
: undefined;
|
|
350
|
+
return text(why ? { ...sheet, directive: why } : sheet);
|
|
331
351
|
}
|
|
332
352
|
if (!query) {
|
|
333
353
|
throw new Error('sbuilder: sb_api_find needs a query (search) or an id (call sheet)');
|
package/dist/tools/page.js
CHANGED
|
@@ -311,6 +311,29 @@ async function siteChrome(ctx, siteId) {
|
|
|
311
311
|
return {};
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* The site's own home page, or null when it genuinely has none.
|
|
316
|
+
*
|
|
317
|
+
* THROWS RATHER THAN GUESSES. Answering "none" for a listing that could not be
|
|
318
|
+
* read is the one wrong answer available here: it is indistinguishable from an
|
|
319
|
+
* empty site, and the caller acts on it by creating a home page the site
|
|
320
|
+
* already had. `siteChrome` above may swallow its read because a page created
|
|
321
|
+
* without chrome is a page a person can fix; this one may not, because the page
|
|
322
|
+
* it creates cannot be un-created and takes the star off whichever page held it.
|
|
323
|
+
*/
|
|
324
|
+
async function existingHomepage(ctx, siteId) {
|
|
325
|
+
const listed = (await request({
|
|
326
|
+
base: ctx.base,
|
|
327
|
+
method: 'GET',
|
|
328
|
+
path: `/api/sites/${encodeURIComponent(siteId)}/pages`,
|
|
329
|
+
token: siteToken(ctx),
|
|
330
|
+
fetchImpl: ctx.fetchImpl,
|
|
331
|
+
}));
|
|
332
|
+
const home = (listed.pages ?? []).find((p) => p.isHomepage === true);
|
|
333
|
+
if (!home || typeof home.id !== 'string' || !home.id)
|
|
334
|
+
return null;
|
|
335
|
+
return { id: home.id, name: typeof home.name === 'string' ? home.name : '' };
|
|
336
|
+
}
|
|
314
337
|
export function registerPageTools(server, ctx) {
|
|
315
338
|
const session = new PageSession(ctx);
|
|
316
339
|
server.registerTool('sb_page_open', {
|
|
@@ -1012,7 +1035,85 @@ export function registerPageTools(server, ctx) {
|
|
|
1012
1035
|
// fine, while `siteChrome` asks whether the SITE has globals and it does.
|
|
1013
1036
|
// Measured: three pages built with these tools, every one of them bare,
|
|
1014
1037
|
// beside a store page carrying its header as ROOT's first child.
|
|
1015
|
-
|
|
1038
|
+
// A SITE HAS ONE HOME PAGE, AND BY THE TIME AN AGENT ASKS FOR ONE IT
|
|
1039
|
+
// USUALLY EXISTS ALREADY.
|
|
1040
|
+
//
|
|
1041
|
+
// `isHomepage: true` does not mean "make this the home page" to the
|
|
1042
|
+
// platform. It means MOVE THE STAR: CreatePage demotes whoever holds it
|
|
1043
|
+
// and promotes this one. An agent building a store reads the flag the
|
|
1044
|
+
// first way and asks for it on a site the editor already gave a home page
|
|
1045
|
+
// to, so the site ends up with two — the new one on "/", the old one
|
|
1046
|
+
// demoted and holding nothing. Measured on a real store: pg_439cb121
|
|
1047
|
+
// "Home" and pg_237719d4 "Trang chủ", both slug "", both path "/", the
|
|
1048
|
+
// first reachable at no address at all and still listed as a page.
|
|
1049
|
+
//
|
|
1050
|
+
// So the flag is honoured as what the caller meant — the site's home page
|
|
1051
|
+
// — which is the one it already has. Adopted, renamed when the caller
|
|
1052
|
+
// named it something else, never duplicated. The same rule sb_import_site
|
|
1053
|
+
// already follows when its entry page lands on a site with a home page.
|
|
1054
|
+
//
|
|
1055
|
+
// REPLACING the home page with a DIFFERENT page stays possible and stays
|
|
1056
|
+
// explicit: create it without the flag, build it, then PATCH isHomepage
|
|
1057
|
+
// through sb_api_call. That is a decision, and it should read like one.
|
|
1058
|
+
const adopt = is_homepage === true ? await existingHomepage(ctx, site_id) : null;
|
|
1059
|
+
const rename = adopt && name.trim() !== '' && name !== adopt.name ? name : '';
|
|
1060
|
+
// Read AFTER the adopt decision and skipped when it holds: siteChrome
|
|
1061
|
+
// reads the header and footer off the home page, so asking it which
|
|
1062
|
+
// chrome to dress the home page in is two requests to answer "its own".
|
|
1063
|
+
const wear = chrome !== false && !adopt ? await siteChrome(ctx, site_id) : {};
|
|
1064
|
+
if (adopt && dry_run !== false) {
|
|
1065
|
+
return text({
|
|
1066
|
+
dry_run: true,
|
|
1067
|
+
into: 'the existing home page',
|
|
1068
|
+
page: adopt,
|
|
1069
|
+
...(rename ? { would_rename: { from: adopt.name, to: rename } } : {}),
|
|
1070
|
+
note: 'Nothing would be created. This site already has a home page and a site has ' +
|
|
1071
|
+
'exactly one, so a create carrying isHomepage would have taken the star off ' +
|
|
1072
|
+
`${JSON.stringify(adopt.name)} and left it with no address. Open ${adopt.id} ` +
|
|
1073
|
+
'with sb_page_open and build it.',
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
if (adopt) {
|
|
1077
|
+
// THE RENAME IS THE ONLY WRITE. Not the seed — this page may already be
|
|
1078
|
+
// the site's front door, and overwriting a document nobody asked to
|
|
1079
|
+
// replace is the one thing worse than the duplicate this branch exists
|
|
1080
|
+
// to prevent. Not the chrome either: siteChrome reads the header and
|
|
1081
|
+
// footer OFF the home page, so this page is where they came from.
|
|
1082
|
+
let renamed_to;
|
|
1083
|
+
let rename_failed;
|
|
1084
|
+
if (rename) {
|
|
1085
|
+
try {
|
|
1086
|
+
await request({
|
|
1087
|
+
base: ctx.base,
|
|
1088
|
+
method: 'PATCH',
|
|
1089
|
+
path: `${path}/${encodeURIComponent(adopt.id)}`,
|
|
1090
|
+
token: siteToken(ctx),
|
|
1091
|
+
body: { name: rename },
|
|
1092
|
+
fetchImpl: ctx.fetchImpl,
|
|
1093
|
+
});
|
|
1094
|
+
renamed_to = rename;
|
|
1095
|
+
}
|
|
1096
|
+
catch (e) {
|
|
1097
|
+
// The page is still the right one to build on, so a failed rename
|
|
1098
|
+
// is reported, never raised — the caller asked for a home page and
|
|
1099
|
+
// this is it, under its old name.
|
|
1100
|
+
rename_failed = e.message.replace(/^sbuilder:\s*/, '').slice(0, 160);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
return text({
|
|
1104
|
+
into: 'the existing home page',
|
|
1105
|
+
page: { id: adopt.id, name: renamed_to ?? adopt.name },
|
|
1106
|
+
...(renamed_to ? { renamed: { from: adopt.name, to: renamed_to } } : {}),
|
|
1107
|
+
...(rename_failed ? { rename_failed } : {}),
|
|
1108
|
+
...(slug ? { slug_ignored: 'A home page is served at "/" and carries no slug.' } : {}),
|
|
1109
|
+
...(type && type !== 'page' ? { type_ignored: `Kept as it is; adopting does not retype a page to "${type}".` } : {}),
|
|
1110
|
+
note: 'Nothing was created. This site already had a home page and a site has exactly ' +
|
|
1111
|
+
'one, so creating another would have taken the star off this page and left it ' +
|
|
1112
|
+
`with no address. Open ${adopt.id} with sb_page_open and build it. To hand the ` +
|
|
1113
|
+
'home page over to a DIFFERENT page instead, create that page WITHOUT ' +
|
|
1114
|
+
'is_homepage and then PATCH isHomepage on it through sb_api_call.',
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1016
1117
|
if (dry_run !== false) {
|
|
1017
1118
|
return text({
|
|
1018
1119
|
dry_run: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.1",
|
|
4
4
|
"description": "MCP server that designs and operates a Store Builder site — pages, data, theme and publish — through the platform's own API and live-edit protocol.",
|
|
5
5
|
"mcpName": "io.github.vuluu2k/sbuilder-mcp",
|
|
6
6
|
"type": "module",
|