gwshop: fix primary-branch tie-break and refresh the committed catalog
- src/gwshop.rs: pick the FIRST longest facet path on ties, matching the original Python max(paths, key=len). Rust's max_by_key returned the last, which silently reclassified ~2298 entries for products faceted under several branches (e.g. a generic kit listed per chapter). Adds a regression test. - AGENTS.md: document the first-longest tie-break and the facet-partition coverage shortfall (non-system merchandise left out; no model kits). - data/gwshop_catalog.json: regenerate with the fixed binary (3491 -> 3459 entries; 34 removed, 2 added, 188 structural + 785 collections changes).
This commit is contained in:
@@ -242,6 +242,14 @@ sorting pipeline — **change it first, then the code**. Summary:
|
||||
public **search-only** key live in the gitignored `algolia.json` (or
|
||||
`ALGOLIA_APP_ID`/`ALGOLIA_API_KEY`); never commit any other Algolia key.
|
||||
`data/gwshop_catalog.json` is the committed snapshot and the sorter default.
|
||||
- The dump partitions the index on the `GameSystemsRoot` hierarchical facet
|
||||
(the search-only key cannot `/browse`; plain pagination caps at 1000 hits).
|
||||
When a product is faceted under several branches of equal depth (e.g. a
|
||||
generic kit listed per chapter), the **first longest path wins** — Python's
|
||||
`max(paths, key=len)` semantics, reproduced explicitly because Rust's
|
||||
`max_by_key` returns the *last* maximum. Products carrying no
|
||||
`GameSystemsRoot` path (paints, Black Library books, White Dwarf, bases,
|
||||
gifts — no miniature kits) are left out; the coverage shortfall is logged.
|
||||
- Bookkeeping (`sorter_applied.jsonl`, `sorter_low_confidence.txt`) is written
|
||||
to `<library>/../_logs/` by default — never inside the library, which may be
|
||||
a seeded torrent whose file set is frozen.
|
||||
|
||||
+48377
-48010
File diff suppressed because it is too large
Load Diff
+36
-6
@@ -454,12 +454,18 @@ pub fn build_catalog(hits: &[Value]) -> Value {
|
||||
continue;
|
||||
}
|
||||
// primary branch = longest path (most specific), e.g.
|
||||
// [40k, Space Marines, Raven Guard, Walker]
|
||||
let primary = paths
|
||||
.iter()
|
||||
.max_by_key(|p| p.len())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
// [40k, Space Marines, Raven Guard, Walker].
|
||||
// python `max(paths, key=len)` keeps the FIRST longest path on ties;
|
||||
// Rust's `max_by_key` would keep the LAST, so select explicitly to
|
||||
// stay faithful to the original (products faceted under several
|
||||
// branches, e.g. a generic kit listed per chapter, depend on this).
|
||||
let primary = paths.iter().fold(Vec::new(), |best, p| {
|
||||
if p.len() > best.len() {
|
||||
p.clone()
|
||||
} else {
|
||||
best
|
||||
}
|
||||
});
|
||||
let at = |i: usize| -> Value {
|
||||
primary
|
||||
.get(i)
|
||||
@@ -596,6 +602,30 @@ mod tests {
|
||||
assert_eq!(e["source"], "gwshop");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_catalog_primary_branch_prefers_first_longest_tie() {
|
||||
// python max(paths, key=len) keeps the first longest path; the class
|
||||
// layer must come from that branch, not the last tied one.
|
||||
let hit = json!({
|
||||
"objectID": "obj3",
|
||||
"name": "Stomper Tribe",
|
||||
"sku": "S3",
|
||||
"GameSystemsRoot": {
|
||||
"lvl0": ["Age of Sigmar"],
|
||||
"lvl1": ["Age of Sigmar > Grand Alliance Destruction"],
|
||||
"lvl2": ["Age of Sigmar > Grand Alliance Destruction > Sons of Behemat"],
|
||||
"lvl3": [
|
||||
"Age of Sigmar > Grand Alliance Destruction > Sons of Behemat > Gargant",
|
||||
"Age of Sigmar > Grand Alliance Destruction > Sons of Behemat > Monster",
|
||||
],
|
||||
}
|
||||
});
|
||||
let cat = build_catalog(&[hit]);
|
||||
let e = &cat["entries"][0];
|
||||
assert_eq!(e["faction"], "Sons of Behemat");
|
||||
assert_eq!(e["class"], "Gargant");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_catalog_dedups_and_handles_pathless() {
|
||||
let hit = json!({"objectID": "obj2", "name": "Paint"});
|
||||
|
||||
Reference in New Issue
Block a user