sbuilder-mcp 0.19.0 → 0.20.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/CHANGELOG.md CHANGED
@@ -6,6 +6,12 @@ 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.20.0] - 2026-09-10
10
+
11
+ ### Added
12
+ - sb_review now reports a `siteChrome` gap when a site has two or more pages and no global section, so each page is carrying its own header and footer with no way to change the menu in one place.
13
+ - sb_review now reports a `cartCount` gap when something opens the cart but no `cart-count` element shows what is in it, since a shopper who adds an item otherwise sees a toast fade with no lasting sign their basket is not empty.
14
+
9
15
  ## [0.19.0] - 2026-09-10
10
16
 
11
17
  ### Added
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,12 @@ 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.20.0] - 2026-09-10
10
+
11
+ ### Added
12
+ - sb_review giờ báo gap `siteChrome` khi một site có từ hai trang trở lên mà không có global section nào, nghĩa là mỗi trang đang tự mang header và footer riêng, không có cách nào đổi menu ở một chỗ duy nhất.
13
+ - sb_review giờ báo gap `cartCount` khi có thứ mở được giỏ hàng nhưng không có element `cart-count` nào cho thấy trong giỏ có gì, vì khách thêm hàng vào giỏ chỉ thấy một toast tắt đi mà không còn dấu hiệu lâu dài nào cho thấy giỏ không rỗng.
14
+
9
15
  ## [0.19.0] - 2026-09-10
10
16
 
11
17
  ### Added
@@ -61,6 +61,12 @@ export async function gatherReadiness(ctx, siteId, pageNodes) {
61
61
  const globalNodes = globals?.globalSections
62
62
  ? globals.globalSections.flatMap((g) => Object.values(g.document?.nodes ?? {}))
63
63
  : null;
64
+ // HOW MANY SHARED SECTIONS THE SITE HAS, not just what is in them. An empty
65
+ // list is the answer to a question nothing asked: a site whose pages each
66
+ // carry their own header is not one site.
67
+ const globalKinds = globals?.globalSections
68
+ ? globals.globalSections.map((g) => g.kind ?? '')
69
+ : null;
64
70
  // ACTIVE means a shopper can see it; PURCHASABLE adds a price above zero. A
65
71
  // product priced at zero renders, adds to the cart, and totals nothing — which
66
72
  // reads as a working store right up to the money.
@@ -78,6 +84,7 @@ export async function gatherReadiness(ctx, siteId, pageNodes) {
78
84
  shippingMethods,
79
85
  pageNodes,
80
86
  globalNodes,
87
+ globalKinds,
81
88
  categories,
82
89
  categoryPageLinks,
83
90
  };
@@ -61,12 +61,43 @@ function isStore(input) {
61
61
  }
62
62
  const published = (pages, type) => pages.some((p) => p.type === type && p.status === 'published');
63
63
  const drafted = (pages, type) => pages.some((p) => p.type === type && p.status !== 'published');
64
- /** What stands between this site and a paid order, most-blocking first. */
64
+ /**
65
+ * What stands between this site and a paid order, most-blocking first — and,
66
+ * asked FIRST because it is not about money at all, whether these pages are one
67
+ * SITE.
68
+ *
69
+ * The precedent is `accountPage` and `searchPage`, already here on the same
70
+ * reasoning: a shop with no account page still takes orders, so they are a
71
+ * different question asked second. Shared chrome is a different question asked
72
+ * FIRST, because it is true of every site and not only of a store.
73
+ */
65
74
  export function readinessGaps(input) {
66
- if (!isStore(input))
67
- return [];
68
75
  const gaps = [];
69
76
  const pages = input.pages;
77
+ // A SITE WITH NO SHARED SECTION IS NOT A SITE, IT IS A STACK OF PAGES.
78
+ //
79
+ // Nothing asked this. Every tool here authors ONE page, so a build that never
80
+ // reaches for a global section gives each page its own header and footer —
81
+ // and then a change to the nav is one edit per page, the copies drift, and a
82
+ // visitor meets a slightly different site on every click. It is the most
83
+ // basic thing a website has that a generated one does not, and it is
84
+ // invisible to `sb_review`, which reads one page and finds it perfect.
85
+ //
86
+ // Two pages is the threshold: a one-page site has nothing to share with.
87
+ if (pages && pages.length >= 2 && input.globalKinds !== null && input.globalKinds?.length === 0) {
88
+ gaps.push({
89
+ id: 'siteChrome',
90
+ draft: false,
91
+ problem: `This site has ${pages.length} pages and no global section, so each one carries its own ` +
92
+ 'header and footer. Changing the menu is that many edits, the copies drift apart, and a ' +
93
+ 'visitor meets a slightly different site on every page.',
94
+ fix: 'Make them shared: POST /api/sites/{siteId}/global-sections with { name, kind: "header" | ' +
95
+ '"footer", document }, then PUT .../{id}/document with { subtree }. A page then carries a ' +
96
+ 'globalRef instead of a copy, and one edit reaches every page.',
97
+ });
98
+ }
99
+ if (!isStore(input))
100
+ return gaps;
70
101
  if (pages && !published(pages, 'checkout')) {
71
102
  const draft = drafted(pages, 'checkout');
72
103
  gaps.push({
@@ -181,6 +212,28 @@ export function readinessGaps(input) {
181
212
  fix: draft ? `Publish the ${type} page that already exists.` : fix,
182
213
  });
183
214
  }
215
+ const everywhere = input.globalNodes !== null ? [...input.pageNodes, ...input.globalNodes] : null;
216
+ // A BASKET WITH NO NUMBER ON IT. The platform shipped `cart-count` to fix
217
+ // exactly this, and made it OPT-IN: `open_cart` is an ACTION any element can
218
+ // carry, not an element type, so there is no "cart icon" to give a badge to by
219
+ // default — minting one unasked would put a number on every social glyph in
220
+ // every footer. The cost of that decision is that a site authored through
221
+ // these tools never has one: a shopper adds an item, gets a toast that fades,
222
+ // and nothing anywhere on the page says their basket holds anything.
223
+ //
224
+ // Only asked when something DOES open the cart — otherwise `cartTrigger`
225
+ // below is the finding, and this would be a second sentence about the same
226
+ // missing control.
227
+ if (everywhere && opensCart(everywhere) && !everywhere.some((n) => n.data.type === 'cart-count')) {
228
+ gaps.push({
229
+ id: 'cartCount',
230
+ draft: false,
231
+ problem: 'Something opens the cart, but nothing shows what is in it. A shopper who adds an item ' +
232
+ 'sees a toast that fades and then no evidence anywhere that their basket is not empty.',
233
+ fix: 'Give the control that opens the cart a cart-count satellite — sb_set on it with config ' +
234
+ '{ cartCountId: … } mints one, or add a cart-count beside it.',
235
+ });
236
+ }
184
237
  if (input.globalNodes !== null && !opensCart([...input.pageNodes, ...input.globalNodes])) {
185
238
  gaps.push({
186
239
  id: 'cartTrigger',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
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",