portage-cli 0.9.0 → 0.11.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 +4 -4
- data/CHANGELOG.md +64 -0
- data/README.md +33 -4
- data/known-stores/categories.yml +6841 -18
- data/known-stores/category-stoplist.yml +40 -0
- data/known-stores/category-synonyms.yml +15 -0
- data/lib/portage/cli/browser_import/categorize.rb +5 -2
- data/lib/portage/cli/buy.rb +3 -24
- data/lib/portage/cli/check.rb +164 -0
- data/lib/portage/cli/check_next_step.rb +51 -0
- data/lib/portage/cli/classifier/ranking.rb +135 -0
- data/lib/portage/cli/classifier/table.rb +63 -0
- data/lib/portage/cli/classifier.rb +23 -44
- data/lib/portage/cli/doctor.rb +16 -6
- data/lib/portage/cli/find.rb +5 -3
- data/lib/portage/cli/handoff_host.rb +33 -0
- data/lib/portage/cli/index/builder.rb +49 -12
- data/lib/portage/cli/index/database.rb +150 -0
- data/lib/portage/cli/index/entry_product.rb +37 -0
- data/lib/portage/cli/index/legacy_import.rb +54 -0
- data/lib/portage/cli/index/product_store.rb +73 -35
- data/lib/portage/cli/index/schema.rb +70 -0
- data/lib/portage/cli/index/search.rb +73 -0
- data/lib/portage/cli/index/sources/storefront_products/mapper.rb +127 -0
- data/lib/portage/cli/index/sources/storefront_products/pages.rb +114 -0
- data/lib/portage/cli/index/sources/storefront_products/robots.rb +70 -0
- data/lib/portage/cli/index/sources/storefront_products.rb +147 -0
- data/lib/portage/cli/index/sources.rb +5 -2
- data/lib/portage/cli/index/store.rb +23 -47
- data/lib/portage/cli/index.rb +1 -0
- data/lib/portage/cli/offer_sources.rb +17 -2
- data/lib/portage/cli/version.rb +1 -1
- data/lib/portage/cli.rb +142 -12
- metadata +34 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f4200c871324b9f35de00e58c32177ec087c9922b3adda2ae45aa1b4673478f1
|
|
4
|
+
data.tar.gz: e719be3b212a547be84ff5fa1232b08303c96288b7acf60f0fd9c2f1982fa0ef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3bb580b0b6ac745462ecc241b4e151fa16c2a8651ac32aa6d53c84aca0b910da6095182680b6a832b3431539904a697332279de9b9340fb491581821eccf6519
|
|
7
|
+
data.tar.gz: 7a75983b229489dc8519caa7a176122b00c8f41f56922f4b373c551a9ab044dace860f7ecdd6ba93d63feddf84177391d3ae92f13a6280acd138e704590ea4f3
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,70 @@ pre-1.0, so APIs may still shift between minor versions.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.11.0] - 2026-10-01
|
|
10
|
+
|
|
11
|
+
- **Category classification uses the whole taxonomy.** `known-stores/categories.yml` is now generated
|
|
12
|
+
by `script/categories` (stdlib only) from Google's product taxonomy, edition 2021-09-21, instead of
|
|
13
|
+
coming from a script that was never committed. It keeps the same ids and the same top-two-level keys
|
|
14
|
+
(so `~/.portage/categories.yml` overrides still work), but a level-2 node's `keywords` now include
|
|
15
|
+
its descendants' names ("Chandeliers" counts for Lighting), and its parent's words sit in a separate
|
|
16
|
+
`parent_keywords` list. The file grows from 20KB to 101KB. A shipped stoplist
|
|
17
|
+
(`known-stores/category-stoplist.yml`, every word with its reason) removes merchandising and url words
|
|
18
|
+
("new", "collection", "sale", "gift", "accessories", "products", ...) from both the data and the input,
|
|
19
|
+
and a small synonyms file (`category-synonyms.yml`, each word tied to a golden case) adds the few
|
|
20
|
+
words the taxonomy lacks ("pendant", "sconce"). `Classifier.categories_for` keeps its signature (plus
|
|
21
|
+
an optional `stoplist_path:`) and returns at most three ids, best first. Scoring: a keyword counts
|
|
22
|
+
2 and a `parent_keyword` 1, times how often the input repeats the word (1 + ln count) and, for the
|
|
23
|
+
cut, how rare it is. A word counts once per node, and an id scoring under half the strongest is
|
|
24
|
+
dropped. Ties go to the node whose own name the input covers most, then to one whose name has no
|
|
25
|
+
stoplisted word, then to the smaller node. On a 100-case golden set (Light Yard, JB Hi-Fi, shopper
|
|
26
|
+
queries, browser-import titles and urls) top-1 accuracy goes from 12% to 70%. "Pendant Light" is
|
|
27
|
+
Lighting, "New Collection" is no longer Toll Collection Devices, and 160 of Light Yard's 164 products
|
|
28
|
+
classify as Lighting (1 before). Products already in the index keep the category they were crawled
|
|
29
|
+
with: crawl the store again (`portage index build --sources storefront_products`) to re-classify them.
|
|
30
|
+
- **The local index is now a SQLite file.** `~/.portage/index/index.sqlite3` (mode 0600, WAL)
|
|
31
|
+
replaces `stores.json` and `products.json`, through the new `sqlite3` gem dependency (`~> 2.9`,
|
|
32
|
+
precompiled for macOS and Linux). An existing `stores.json`/`products.json` is imported once on
|
|
33
|
+
first use and renamed to `*.json.migrated`; it is never deleted. `Index::Store` and
|
|
34
|
+
`Index::ProductStore` keep their APIs, and `ProductStore#upsert_many` writes a batch in one
|
|
35
|
+
transaction. Index write failures now raise instead of being dropped. `portage doctor` reports the
|
|
36
|
+
database path, row counts and whether FTS5 is available. The known-stores cache and `index export`
|
|
37
|
+
output stay JSON.
|
|
38
|
+
- **Storefront catalogue crawl and `portage index search`.** A new opt-in index source,
|
|
39
|
+
`storefront_products`, reads a Shopify store's public `/products.json` into the local index. Run it
|
|
40
|
+
with `portage index add URL --crawl` for one store, or `portage index build --sources
|
|
41
|
+
storefront_products` for stores already indexed (25 a run, least recently crawled first). Each
|
|
42
|
+
product is mapped through the UCP `Product` shape and stored with its handle, URL, first image,
|
|
43
|
+
options and variant ids. Price and availability are dropped before anything is written. A crawl
|
|
44
|
+
reads at most 20 pages of 250 a store, 1s apart. It waits out one 429's `Retry-After` (capped at 60s)
|
|
45
|
+
and stops on a second, and it obeys `robots.txt` for every page URL. It never contacts a hand-off-only
|
|
46
|
+
host, and it skips a 404, a redirect or a non-JSON answer (a bot wall). What happened is kept on the
|
|
47
|
+
store entry as `crawl`. `portage index search QUERY [--category ID] [--store HOST] [--limit N]
|
|
48
|
+
[--json]` searches the index locally with SQLite FTS5, ranked by bm25, and sends no request. Without
|
|
49
|
+
FTS5 it falls back to an unranked text match and reports `engine: "like"`. `index show --products` now
|
|
50
|
+
pages (`--page N`, `--per-page N`, default 50). `portage check` suggests the `index add ... --crawl`
|
|
51
|
+
command for a Shopify or native-UCP store (`index_hint`) but never runs it. `find` and `buy` never
|
|
52
|
+
crawl.
|
|
53
|
+
- **Product cards for agents.** `find` offers (and `shopify_catalog` offers) gain a `product` field:
|
|
54
|
+
the store's UCP `Product` wire hash as served, with `media` cut to the first image, so an agent or
|
|
55
|
+
UI can draw a card without another request. The flat fields (`title`, `amount`, `currency`, `url`,
|
|
56
|
+
`product_id`, `store`) are unchanged, the retailer API sources leave `product` out, and `history`
|
|
57
|
+
does not save it. `portage index search` results are marked `live: false` and each hit gains
|
|
58
|
+
`product`, built from the fields the index keeps (title, handle, URL, first image, options, variant
|
|
59
|
+
ids, category), never a price. Text output says the hits are not live.
|
|
60
|
+
|
|
61
|
+
## [0.10.0] - 2026-09-29
|
|
62
|
+
|
|
63
|
+
- **`portage check <url> [--json]`.** Reports whether Portage can buy from a store and
|
|
64
|
+
how: `verdict` (`automated`, `webmcp`, `handoff`, `unsupported`), `next_step`, and the
|
|
65
|
+
detail behind them (native UCP, platform, adapter install and missing env, hand-off-only,
|
|
66
|
+
WebMCP status). Wraps `Portage::Ucp::Check` and follows the precedence `buy` uses.
|
|
67
|
+
Plain GETs only, hand-off-only hosts are not contacted, and WebMCP is read only from an
|
|
68
|
+
already-open Portage profile tab. Exits `0` for `automated` and `webmcp`. The hand-off-only
|
|
69
|
+
test moved into a shared `HandoffHost` so `buy` and `check` can't disagree.
|
|
70
|
+
Follows a homepage `<link rel="ucp">` manifest pointer through `portage-ucp` 0.11.0,
|
|
71
|
+
which this release now requires (`~> 0.11`).
|
|
72
|
+
|
|
9
73
|
## [0.9.0] - 2026-09-29
|
|
10
74
|
|
|
11
75
|
- **Human pick and approve** (`docs/plans/human-pick-and-approve.md`, Phases 1-3).
|
data/README.md
CHANGED
|
@@ -133,6 +133,7 @@ portage buy --query "..." [--store URL] [--max-price N] [--limit N] ...
|
|
|
133
133
|
portage find --query "..." [--max-price N] [--limit N] [--json]
|
|
134
134
|
portage compare <url> --product-id ID [--id VALUE ...] [--results N]
|
|
135
135
|
[--max-price N] [--json]
|
|
136
|
+
portage check <url> [--json]
|
|
136
137
|
portage pick [--search LAST|SEARCH_ID] [--via auto|tty|agent] [--json]
|
|
137
138
|
[--choose REF | --compare REF | --view REF]
|
|
138
139
|
portage approve QUOTE_ID [--via auto|tty|agent] [--relayed-yes | --view] [--json]
|
|
@@ -154,8 +155,9 @@ portage policy set [--per-transaction-cap N --currency CUR]
|
|
|
154
155
|
portage orders reconcile [--checkout ID] [--json]
|
|
155
156
|
portage index build [--sources a,b] [--queries FILE] [--dry-run] [--export DIR] [--json]
|
|
156
157
|
portage index refresh [--sources a,b] [--queries FILE] [--dry-run] [--export DIR] [--json]
|
|
157
|
-
portage index show [--stores|--products] [--json]
|
|
158
|
-
portage index
|
|
158
|
+
portage index show [--stores|--products [--page N] [--per-page N]] [--json]
|
|
159
|
+
portage index search QUERY [--category ID] [--store HOST] [--limit N] [--json]
|
|
160
|
+
portage index add <url> [--crawl] [--json]
|
|
159
161
|
portage index remove <host> [--json]
|
|
160
162
|
portage index sources [--json]
|
|
161
163
|
portage browser import [--browser chrome|edge|brave|arc|firefox|safari] [--profile-root DIR]
|
|
@@ -218,6 +220,18 @@ used (an unreadable value, an out-of-range threshold) stops the buy before
|
|
|
218
220
|
anything runs: on stderr normally, or as a JSON report with `outcome:
|
|
219
221
|
"invalid_option"` under `--json`.
|
|
220
222
|
|
|
223
|
+
### Check
|
|
224
|
+
|
|
225
|
+
`portage check <url> [--json]` answers "can Portage buy from this store, and how?".
|
|
226
|
+
It checks for a native `/.well-known/ucp` manifest, detects the platform, notes
|
|
227
|
+
whether its adapter gem is installed and which env vars are missing, and looks for
|
|
228
|
+
WebMCP tools, using the same hand-off-only rules as `buy`. `verdict` is
|
|
229
|
+
`automated`, `webmcp`, `handoff` or `unsupported`, with a plain-English
|
|
230
|
+
`next_step`. Exits `0` for `automated` and `webmcp`. It sends plain GETs only, and
|
|
231
|
+
never contacts a hand-off-only host. WebMCP is read only from a tab your Portage
|
|
232
|
+
browser profile already has open on the store; it never launches a browser.
|
|
233
|
+
Takes the `--proxy*` flags.
|
|
234
|
+
|
|
221
235
|
### Compare
|
|
222
236
|
|
|
223
237
|
`portage compare <url> --product-id ID` finds other stores selling the same
|
|
@@ -465,13 +479,16 @@ portage index build --sources shopify_catalog,stores_file
|
|
|
465
479
|
portage index build --queries queries.txt # one query per line, instead of the built-in taxonomy sweep
|
|
466
480
|
portage index refresh # re-verify entries older than 7 days, add new ones
|
|
467
481
|
portage index show --stores --json
|
|
468
|
-
portage index show --products --json
|
|
482
|
+
portage index show --products --page 2 --json # 50 a page; --per-page N
|
|
483
|
+
portage index search "wall light" --store some-shop.example --json
|
|
469
484
|
portage index add https://some-shop.example
|
|
485
|
+
portage index add https://some-shop.example --crawl # also read its /products.json catalogue
|
|
486
|
+
portage index build --sources storefront_products # crawl the catalogues of stores already indexed
|
|
470
487
|
portage index remove some-shop.example
|
|
471
488
|
portage index sources # name, what each fetches, source file path
|
|
472
489
|
```
|
|
473
490
|
|
|
474
|
-
Stored
|
|
491
|
+
Stored in `~/.portage/index/index.sqlite3` (mode 0600), **never in git** and
|
|
475
492
|
**never containing a price or stock field** — those are always fetched live.
|
|
476
493
|
Each new origin gets exactly one `/.well-known/ucp` probe (capped at 500 new
|
|
477
494
|
probes per run), throttled, with progress output. Sources:
|
|
@@ -483,6 +500,18 @@ probes per run), throttled, with progress output. Sources:
|
|
|
483
500
|
| `browser` | Whatever `portage browser import` (below) already saved — this source itself never reads a browser | on, but yields nothing unless you've run `browser import` |
|
|
484
501
|
| `wikidata` | Retailers'/brands' official sites via a public SPARQL query | opt-in (`--sources wikidata`) |
|
|
485
502
|
| `webmcp_sweep` | Which WebMCP preset an origin matches, when a bridge is attached | opt-in, needs a bridge |
|
|
503
|
+
| `storefront_products` | Each indexed Shopify store's own `/products.json`: title, brand, handle, URL, first image, options and variant ids, mapped through the UCP `Product` shape with price and availability dropped | opt-in (`--sources storefront_products`, or `index add URL --crawl`) |
|
|
504
|
+
|
|
505
|
+
**Catalogue crawls are polite and opt-in.** `storefront_products` never runs
|
|
506
|
+
from `find` or `buy` (`portage check` only prints the `index add ... --crawl`
|
|
507
|
+
command). It crawls at most 20 pages of 250 products a store and 25 stores a
|
|
508
|
+
run, least recently crawled first, 1s apart. It waits out one 429's
|
|
509
|
+
`Retry-After` (capped at 60s) and stops that store on a second. It obeys
|
|
510
|
+
`robots.txt` for every page URL, never contacts a hand-off-only host, and
|
|
511
|
+
skips a store that answers 404, a redirect or anything but products JSON (a
|
|
512
|
+
bot wall). What happened is kept on the store entry as `crawl`.
|
|
513
|
+
`portage index search` then searches those products locally (SQLite FTS5,
|
|
514
|
+
or a plain text match if FTS5 is missing), with no request.
|
|
486
515
|
|
|
487
516
|
**The index is untrusted data, on the same footing as any other `find`
|
|
488
517
|
candidate.** It never feeds `Policy#merchant_allowlist` and never counts as
|