portage-ucp 0.15.1 → 0.16.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f99f8f55e4bbfeb276c93c8f1c7e63d36b3f30661702303d1295103e2ea4993c
4
- data.tar.gz: 1c21857f2b52165c30994ba40bab0d8522520bfe8d7bb50b7459898630b71647
3
+ metadata.gz: 0a7cd8175dc8b1acd3a884205a5df5f030e897bf1090cdf6675fd582f7c4f0ee
4
+ data.tar.gz: 8acedc01a0e9dff428670248e9257647b26fb7ee54ea1e0625f67bf169c81285
5
5
  SHA512:
6
- metadata.gz: c406f740bebf4a83c4eb9184d62e6abc693478ba97b0ab51cd4549bcc8996addd658b84f437f488ffffafa03782be6b2ea464b05057545091380b86b02066f7a
7
- data.tar.gz: 6acd7187aad41ebae15d667ebc150e6896e1f0f72b2f86211d173bd77a25a26d1cc0119d7a9375329c688d13423a655244c43b04537a30aa9a3c851c12c4efca
6
+ metadata.gz: 6422591fbb901e8e901b4ca6b6bba5b3cd8c6e776d1dd3980536c8245598787fbe2b25d4dd193bf5a05ed87cf6c4e2ff0d7f0b93222ae2716631f23fa028b7f6
7
+ data.tar.gz: 3d5928f821375dc3a71b7f9cae13864430d90b0167aebb8ca35f5ed008fe3e5617e0df0a95ada2f2db2fc962d8309eea7d852453374a81ce046f1609b9a366b7
data/CHANGELOG.md CHANGED
@@ -6,6 +6,9 @@ pre-1.0, so APIs may still shift between minor versions.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.16.0] - 2026-10-21
10
+
11
+ - **Changed: MCP tool descriptions rewritten against the TDQS rubric** (tdqs.dev, the score Glama shows). Each tool's description now names its siblings for when and when not to use it, states the auth and rate-limit behavior of state-changing tools, and says what it returns, since tools have no output schema. Parameter descriptions say where IDs come from. `Mcp::Server.build` now sends server `instructions` (`ToolMetadata::INSTRUCTIONS`, overridable with `instructions:`) that walk the catalog → cart → checkout → order flow. `tools/list` text only; tool names, schemas and annotations are unchanged.
9
12
  - **Added: `dev.ucp.shopping.permalink` capability** (UCP 2026-08-25). An adapter that overrides `Adapter#permalink_endpoint` (default `nil`) to return its https browser endpoint is advertised with `config.endpoint`. `Capability` takes an optional `config:` callable for the manifest entry's `config`. No MCP tool: the permalink is a browser redirect the host serves (design-log §61).
10
13
  - **Added: policy disclosure warnings on `Cart` and `Checkout`** (UCP 2026-08-25). Each entry of `policies:` now also yields a `messages[]` warning with `code` equal to the policy `type`, `presentation: "disclosure"`, `content` from the description and `url`/`path` when the policy has them. Derived in `to_wire_h`, so every adapter gets it and nothing is stored. Orders are unchanged (design-log §62).
11
14
  - **Added: optional `messages:` on `Cart` and `Checkout`.** An array of wire-shaped message hashes (default `[]`) an adapter can set itself. `to_wire_h` emits them before the derived policy warnings and drops a derived warning when the adapter already warned with the same `code`. `messages` is omitted from the wire when the combined list is empty (design-log §63).
@@ -23,7 +23,8 @@ module Portage
23
23
  def self.build(adapter:, registry: Portage::Ucp.configuration.registry,
24
24
  authenticator: Portage::Ucp.configuration.authenticator,
25
25
  rate_limiter: Portage::Ucp.configuration.rate_limiter,
26
- logger: Portage::Ucp.configuration.logger, journal: nil, **server_opts)
26
+ logger: Portage::Ucp.configuration.logger, journal: nil,
27
+ instructions: ToolMetadata::INSTRUCTIONS, **server_opts)
27
28
  context = Context.new(
28
29
  dispatcher: Portage::Ucp::Dispatcher.new(adapter: adapter, registry: registry, logger: logger,
29
30
  journal: journal),
@@ -36,7 +37,7 @@ module Portage
36
37
  end
37
38
  end
38
39
 
39
- ::MCP::Server.new(name: "portage-ucp", tools: tools, **server_opts)
40
+ ::MCP::Server.new(name: "portage-ucp", tools: tools, instructions: instructions, **server_opts)
40
41
  end
41
42
 
42
43
  def self.build_tool(adapter:, capability:, action_name:, method_name:, context:)
@@ -6,9 +6,9 @@ module Portage
6
6
  # ::MCP::Tool. Kept separate from Capability (name/version/actions/
7
7
  # schema — the UCP wire contract) since this is MCP-presentation only:
8
8
  # what a calling agent reads before it decides whether and how to call
9
- # a tool, not part of the UCP spec itself. Content mirrors the @param/
10
- # @return docs already on Adapter's methods (adapter.rb) rather than
11
- # inventing anything new.
9
+ # a tool, not part of the UCP spec itself. Written against the TDQS
10
+ # rubric (tdqs.dev): purpose, when/when-not with named siblings,
11
+ # behavior annotations can't carry, and return shape (no output schema).
12
12
  module ToolMetadata
13
13
  ID = "A stable identifier returned by an earlier call (e.g. get_cart, create_checkout) " \
14
14
  "— not something the caller makes up.".freeze
@@ -16,6 +16,20 @@ module Portage
16
16
  "key returns the original result instead of repeating the effect.".freeze
17
17
  REPLACE_NOTE = "Replaces the existing contents rather than adding to them — resend the " \
18
18
  "full desired list with an item adjusted, added, or omitted.".freeze
19
+ AUTH_NOTE = "Requires an authenticated session and may be rate-limited; a rejected call " \
20
+ "returns an error result and changes nothing.".freeze
21
+ OPEN_NOTE = "Needs no authentication and is never rate-limited.".freeze
22
+
23
+ INSTRUCTIONS = "Shopping tools for one store, following the UCP lifecycle: catalog → cart → " \
24
+ "checkout → order. Find products with search_catalog, read them with " \
25
+ "get_product or lookup_catalog. Either build a cart (create_cart/update_cart) " \
26
+ "or go straight to create_checkout; a checkout never charges until " \
27
+ "complete_checkout, which must only follow the shopper's explicit approval of " \
28
+ "the exact total shown by get_checkout. After purchase, use get_order, " \
29
+ "cancel_order, request_return and refund_order. Catalog reads are open; every " \
30
+ "tool that changes state needs an authenticated session, and a server run " \
31
+ "without one is read-only. Payment is a single-use token, never a card number. " \
32
+ "Prices and stock come live from the store and can change between calls.".freeze
19
33
 
20
34
  # JSON Schema `type` for parameter names whose shape is unambiguous
21
35
  # from the name alone and constant across every action it appears
@@ -30,155 +44,194 @@ module Portage
30
44
 
31
45
  TOOLS = {
32
46
  "search_catalog" => {
33
- description: "Search the store's product catalog by free-text query, returning up to " \
34
- "`limit` matches with price and availability. Use this to discover products " \
35
- "when you don't yet have an ID; once you do, use get_product for one or " \
36
- "lookup_catalog for several — neither re-runs a search.",
37
- params: { "query" => "Free-text search query.", "limit" => "Maximum number of results to return." },
47
+ description: "Search the store's product catalog by free-text query and return up to " \
48
+ "`limit` matching products, each with ID, title, price range, variants and " \
49
+ "availability. Use it to discover products when you have no product ID yet; " \
50
+ "once you have IDs, use get_product (one) or lookup_catalog (several) " \
51
+ "instead of searching again. #{OPEN_NOTE} An empty product list means no " \
52
+ "match, not an error; results reflect the live store and can change between calls.",
53
+ params: {
54
+ "query" => "Free-text search, e.g. a product name, brand or category (\"burton snowboard\").",
55
+ "limit" => "Maximum number of products to return; 5–20 suits most searches."
56
+ },
38
57
  annotations: { read_only_hint: true, destructive_hint: false, idempotent_hint: true }
39
58
  },
40
59
  "get_product" => {
41
- description: "Fetch full details — price, availability, and whatever else the store's " \
42
- "catalog carries — for one product by ID. Returns not-found rather than " \
43
- "raising if the ID doesn't exist. Use search_catalog first if you don't " \
44
- "already have the ID; use lookup_catalog instead for several known IDs, to " \
45
- "avoid one call per product.",
60
+ description: "Fetch the full record for one product by ID: title, description, price " \
61
+ "range, every variant with its own price and availability, URL and store " \
62
+ "policies. Use it when you need variant-level detail for a single product " \
63
+ "before adding it to a cart; use lookup_catalog for several IDs at once, and " \
64
+ "search_catalog if you don't have an ID. #{OPEN_NOTE} An unknown ID returns " \
65
+ "an empty result rather than an error.",
46
66
  params: { "product_id" => "Product ID, as returned by search_catalog or lookup_catalog." },
47
67
  annotations: { read_only_hint: true, destructive_hint: false, idempotent_hint: true }
48
68
  },
49
69
  "lookup_catalog" => {
50
- description: "Batch-fetch several products by ID in one call, same result shape as " \
51
- "search_catalog. Use when you already have a set of product IDs — e.g. " \
52
- "hydrating a cart or order — instead of calling get_product once per ID.",
70
+ description: "Fetch several products by ID in one call, returning the same product list " \
71
+ "shape as search_catalog. Use it when you already hold a set of IDs — e.g. to " \
72
+ "refresh prices for a cart or order — instead of calling get_product once per " \
73
+ "ID; use search_catalog when you have a query rather than IDs. #{OPEN_NOTE} " \
74
+ "IDs the store doesn't recognise are omitted from the result, not reported as errors.",
53
75
  params: { "product_ids" => "Product IDs to fetch, as returned by search_catalog, a cart, or an order." },
54
76
  annotations: { read_only_hint: true, destructive_hint: false, idempotent_hint: true }
55
77
  },
56
78
  "create_cart" => {
57
- description: "Start a new cart from a list of line items — the cart's complete starting " \
58
- "contents, not an item appended to an existing one. Use update_cart to change " \
59
- "a cart that already exists. Optional discount_codes applies coupons if the " \
60
- "store supports them.",
79
+ description: "Create a new cart holding the given line items and return it with its ID, " \
80
+ "line items, currency and totals. Use it to collect items before checkout; " \
81
+ "to change a cart that already exists use update_cart, and to buy straight " \
82
+ "away without a cart use create_checkout. Nothing is reserved or charged. " \
83
+ "#{AUTH_NOTE}",
61
84
  params: {
62
- "line_items" => "Items to add, each {product_id:, quantity:}.",
85
+ "line_items" => "The cart's starting contents, each {product_id:, quantity:} with a " \
86
+ "product or variant ID from the catalog tools.",
63
87
  "idempotency_key" => IDEMPOTENCY_KEY,
64
- "discount_codes" => "Discount/coupon codes to apply, if the store supports them."
88
+ "discount_codes" => "Optional coupon codes to apply, if the store supports them."
65
89
  },
66
90
  annotations: { read_only_hint: false, destructive_hint: false, idempotent_hint: true }
67
91
  },
68
92
  "get_cart" => {
69
- description: "Fetch a cart's current contents and totals by ID.",
70
- params: { "cart_id" => ID },
93
+ description: "Fetch an existing cart by ID and return its line items, currency, applied " \
94
+ "discounts and current totals, recalculated against live store prices. Use " \
95
+ "it to show the shopper what is in the cart; it is for carts only — use " \
96
+ "get_checkout once a checkout exists and get_order after purchase. #{OPEN_NOTE}",
97
+ params: { "cart_id" => "Cart ID returned by create_cart. #{ID}" },
71
98
  annotations: { read_only_hint: true, destructive_hint: false, idempotent_hint: true }
72
99
  },
73
100
  "update_cart" => {
74
- description: "Replace a cart's contents. #{REPLACE_NOTE} Use this for every change to an " \
75
- "existing cart: adding an item, removing one, or changing a quantity.",
101
+ description: "Replace an existing cart's line items and discount codes, then return the " \
102
+ "updated cart with new totals. #{REPLACE_NOTE} Use it for every change to a " \
103
+ "cart — add, remove or change a quantity; use create_cart only for a new " \
104
+ "cart and update_checkout once the shopper has moved to checkout. " \
105
+ "Nothing is charged. #{AUTH_NOTE}",
76
106
  params: {
77
- "cart_id" => ID,
78
- "line_items" => "The cart's complete desired contents, each {product_id:, quantity:}. #{REPLACE_NOTE}",
107
+ "cart_id" => "Cart ID returned by create_cart. #{ID}",
108
+ "line_items" => "The cart's complete desired contents, each {product_id:, quantity:}.",
79
109
  "idempotency_key" => IDEMPOTENCY_KEY,
80
- "discount_codes" => "Discount/coupon codes to apply, if the store supports them."
110
+ "discount_codes" => "Optional coupon codes, replacing those applied; leave out to keep the current ones."
81
111
  },
82
112
  annotations: { read_only_hint: false, destructive_hint: false, idempotent_hint: true }
83
113
  },
84
114
  "cancel_cart" => {
85
- description: "Cancel and discard a cart. Irreversible — use this to abandon a cart the " \
86
- "shopper no longer wants.",
87
- params: { "cart_id" => ID, "idempotency_key" => IDEMPOTENCY_KEY },
115
+ description: "Abandon a cart and return it in its final state; don't reuse the cart ID " \
116
+ "afterwards (some stores delete it, others such as Shopify let it expire). " \
117
+ "Use it when the shopper gives up on the cart; to just " \
118
+ "remove items use update_cart instead, and to abandon a checkout use " \
119
+ "cancel_checkout. No money is involved. #{AUTH_NOTE}",
120
+ params: { "cart_id" => "Cart ID returned by create_cart. #{ID}", "idempotency_key" => IDEMPOTENCY_KEY },
88
121
  annotations: { read_only_hint: false, destructive_hint: true, idempotent_hint: true }
89
122
  },
90
123
  "create_checkout" => {
91
- description: "Start a checkout directly from line items, skipping a separate cart. " \
92
- "Optional discount_codes applies coupons; optional fulfillment carries the " \
93
- "shopper's desired shipping or pickup selection, if the store supports it. " \
94
- "Does not charge anything — call complete_checkout once the shopper approves " \
95
- "the total from get_checkout.",
124
+ description: "Open a checkout for the given line items and return it with its ID, status, " \
125
+ "currency, itemised totals (including tax and shipping once known) and links. " \
126
+ "Use it when the shopper is ready to buy; use create_cart instead if they " \
127
+ "are still collecting items. Does not charge — complete_checkout does that, " \
128
+ "only after the shopper approves the total. #{AUTH_NOTE}",
96
129
  params: {
97
- "line_items" => "Items to buy, each {product_id:, quantity:}.",
130
+ "line_items" => "Items to buy, each {product_id:, quantity:} with a product or variant ID " \
131
+ "from the catalog tools or a cart.",
98
132
  "idempotency_key" => IDEMPOTENCY_KEY,
99
- "discount_codes" => "Discount/coupon codes to apply, if the store supports them.",
100
- "fulfillment" => "Desired shipping/pickup method selection, if the store supports it."
133
+ "discount_codes" => "Optional coupon codes to apply, if the store supports them.",
134
+ "fulfillment" => "Optional shipping or pickup selection; leave it out to get the " \
135
+ "store's available options back on the checkout."
101
136
  },
102
137
  annotations: { read_only_hint: false, destructive_hint: false, idempotent_hint: true }
103
138
  },
104
139
  "get_checkout" => {
105
- description: "Fetch a checkout's current contents, totals, and status by ID.",
106
- params: { "checkout_id" => ID },
140
+ description: "Fetch an existing checkout by ID and return its status, line items, " \
141
+ "currency and itemised totals. Use it to show the shopper the exact final " \
142
+ "total to approve before complete_checkout, or to check whether a completed " \
143
+ "checkout produced an order; use get_cart for carts and get_order for placed " \
144
+ "orders. #{OPEN_NOTE}",
145
+ params: { "checkout_id" => "Checkout ID returned by create_checkout. #{ID}" },
107
146
  annotations: { read_only_hint: true, destructive_hint: false, idempotent_hint: true }
108
147
  },
109
148
  "update_checkout" => {
110
- description: "Replace a checkout's contents before payment. #{REPLACE_NOTE} Also accepts " \
111
- "updated discount_codes or fulfillment selection. Does not charge anything.",
149
+ description: "Replace an open checkout's line items, discount codes or fulfillment " \
150
+ "selection and return it with recalculated totals. #{REPLACE_NOTE} Use it " \
151
+ "before payment only; a completed checkout can't change — adjust the order " \
152
+ "with cancel_order or request_return instead. Does not charge, and any total " \
153
+ "the shopper approved earlier must be re-approved. #{AUTH_NOTE}",
112
154
  params: {
113
- "checkout_id" => ID,
114
- "line_items" => "The checkout's complete desired contents, each {product_id:, quantity:}. " \
115
- "#{REPLACE_NOTE}",
155
+ "checkout_id" => "Checkout ID returned by create_checkout. #{ID}",
156
+ "line_items" => "The checkout's complete desired contents, each {product_id:, quantity:}.",
116
157
  "idempotency_key" => IDEMPOTENCY_KEY,
117
- "discount_codes" => "Discount/coupon codes to apply, if the store supports them.",
118
- "fulfillment" => "Desired shipping/pickup method selection, if the store supports it."
158
+ "discount_codes" => "Optional coupon codes, replacing those applied; leave out to keep the current ones.",
159
+ "fulfillment" => "Optional shipping or pickup selection, from the options listed on the checkout."
119
160
  },
120
161
  annotations: { read_only_hint: false, destructive_hint: false, idempotent_hint: true }
121
162
  },
122
163
  "complete_checkout" => {
123
- description: "Charge the checkout and place the order — the one tool here that moves " \
124
- "money. Takes a single-use payment_token from a payment handler, never a raw " \
125
- "card number. Re-checks stock at this point, since catalog calls don't " \
126
- "guarantee live inventory, and fails rather than charging if a line item is " \
127
- "no longer available. Only call this after the shopper has explicitly " \
128
- "approved the exact total from get_checkout.",
164
+ description: "Charge the shopper and place the order for an open checkout — the only tool " \
165
+ "here that spends money. Call it once, only after the shopper has explicitly " \
166
+ "approved the exact total from get_checkout; never to preview a price. Before " \
167
+ "charging it rejects anything resembling a raw card number, enforces the " \
168
+ "operator's spending caps and allowlist, may require a human confirmation, " \
169
+ "and re-checks stock, failing without a charge if anything is off. Returns " \
170
+ "the checkout with status completed and the new order. #{AUTH_NOTE}",
129
171
  params: {
130
- "checkout_id" => ID,
172
+ "checkout_id" => "Checkout ID returned by create_checkout. #{ID}",
131
173
  "payment_token" => "Single-use payment token from a payment handler — never a raw card number.",
132
- "idempotency_key" => IDEMPOTENCY_KEY,
174
+ "idempotency_key" => "#{IDEMPOTENCY_KEY} Reuse it on retry so a timeout never charges twice.",
133
175
  "mandate" => "Optional AP2 payment mandate authorizing this charge, alongside or instead " \
134
- "of payment_token."
176
+ "of payment_token; its signature is verified before use."
135
177
  },
136
178
  annotations: { read_only_hint: false, destructive_hint: true, idempotent_hint: true }
137
179
  },
138
180
  "cancel_checkout" => {
139
- description: "Cancel a checkout before payment. Irreversible — use this to abandon a " \
140
- "checkout the shopper no longer wants. Safe with respect to money, since " \
141
- "complete_checkout hasn't run yet.",
142
- params: { "checkout_id" => ID, "idempotency_key" => IDEMPOTENCY_KEY },
181
+ description: "Abandon an open checkout before payment and return it with status canceled; " \
182
+ "it cannot be completed afterwards. Use it when the shopper declines the " \
183
+ "total; nothing has been charged yet, so no refund is needed. For a checkout " \
184
+ "that already completed, use cancel_order instead. #{AUTH_NOTE}",
185
+ params: { "checkout_id" => "Checkout ID returned by create_checkout. #{ID}",
186
+ "idempotency_key" => IDEMPOTENCY_KEY },
143
187
  annotations: { read_only_hint: false, destructive_hint: true, idempotent_hint: true }
144
188
  },
145
189
  "get_order" => {
146
- description: "Fetch a placed order's status, line items, and totals by ID. Returns " \
147
- "not-found rather than raising if the ID doesn't exist.",
148
- params: { "order_id" => ID },
190
+ description: "Fetch a placed order by ID and return its line items with per-item " \
191
+ "fulfillment status, shipping details, totals, permalink and any " \
192
+ "cancellations, returns or refunds applied so far. Use it to answer " \
193
+ "\"where is my order\" or to pick line item IDs for request_return or " \
194
+ "refund_order; use get_checkout for anything not yet paid. #{OPEN_NOTE} " \
195
+ "An unknown ID returns an empty result rather than an error.",
196
+ params: { "order_id" => "Order ID from a completed checkout. #{ID}" },
149
197
  annotations: { read_only_hint: true, destructive_hint: false, idempotent_hint: true }
150
198
  },
151
199
  "cancel_order" => {
152
- description: "Cancel an already-placed order. Irreversible. Optional reason is a " \
153
- "free-text note for the merchant, not a fixed enum.",
200
+ description: "Cancel an entire placed order and return it with the cancellation recorded. " \
201
+ "Use it before the order ships; for part of an order, or once it has shipped, " \
202
+ "use request_return. Whether a refund follows automatically depends on the " \
203
+ "store — check the returned order's adjustments. Cannot be undone. #{AUTH_NOTE}",
154
204
  params: {
155
- "order_id" => ID, "idempotency_key" => IDEMPOTENCY_KEY,
156
- "reason" => "Optional free-text reason for the cancellation."
205
+ "order_id" => "Order ID from a completed checkout. #{ID}", "idempotency_key" => IDEMPOTENCY_KEY,
206
+ "reason" => "Optional free-text note for the merchant, e.g. \"ordered by mistake\"."
157
207
  },
158
208
  annotations: { read_only_hint: false, destructive_hint: true, idempotent_hint: true }
159
209
  },
160
210
  "request_return" => {
161
- description: "Request a return for one or more order line items. This files the request; " \
162
- "it doesn't refund money itself — the order shows the items as pending until " \
163
- "the merchant processes it. Use refund_order only once a refund has actually " \
164
- "been approved.",
211
+ description: "File a return request for some or all line items of a placed order and " \
212
+ "return the order with the request recorded as pending. It only asks — no " \
213
+ "money moves until the merchant approves and a refund is issued with " \
214
+ "refund_order. Use it for delivered items; use cancel_order for an order " \
215
+ "that hasn't shipped. #{AUTH_NOTE}",
165
216
  params: {
166
- "order_id" => ID,
167
- "line_items" => "Items to return, each {id:, quantity:}.",
217
+ "order_id" => "Order ID from a completed checkout. #{ID}",
218
+ "line_items" => "Items to return, each {id:, quantity:} using the order line item IDs from get_order.",
168
219
  "idempotency_key" => IDEMPOTENCY_KEY,
169
- "reason" => "Optional free-text reason for the return."
220
+ "reason" => "Optional free-text reason for the merchant, e.g. \"wrong size\"."
170
221
  },
171
222
  annotations: { read_only_hint: false, destructive_hint: false, idempotent_hint: true }
172
223
  },
173
224
  "refund_order" => {
174
- description: "Refund one or more order line items, moving money back to the shopper. " \
175
- "Irreversible. Use request_return first if the merchant requires a return " \
176
- "before refunding.",
225
+ description: "Refund some or all line items of a placed order back to the original " \
226
+ "payment method and return the order with the refund recorded. Money moves " \
227
+ "and it cannot be reversed. Use it once a refund is agreed; if the store " \
228
+ "needs the goods back first, call request_return before this. To call off " \
229
+ "a whole unshipped order, use cancel_order. #{AUTH_NOTE}",
177
230
  params: {
178
- "order_id" => ID,
179
- "line_items" => "Items to refund, each {id:, quantity:}.",
231
+ "order_id" => "Order ID from a completed checkout. #{ID}",
232
+ "line_items" => "Items to refund, each {id:, quantity:} using the order line item IDs from get_order.",
180
233
  "idempotency_key" => IDEMPOTENCY_KEY,
181
- "reason" => "Optional free-text reason for the refund."
234
+ "reason" => "Optional free-text reason for the merchant."
182
235
  },
183
236
  annotations: { read_only_hint: false, destructive_hint: true, idempotent_hint: true }
184
237
  }
@@ -1,5 +1,5 @@
1
1
  module Portage
2
2
  module Ucp
3
- VERSION = "0.15.1".freeze
3
+ VERSION = "0.16.0".freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: portage-ucp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.1
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom Whitbread
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-18 00:00:00.000000000 Z
11
+ date: 2026-10-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64