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
|
public **search-only** key live in the gitignored `algolia.json` (or
|
||||||
`ALGOLIA_APP_ID`/`ALGOLIA_API_KEY`); never commit any other Algolia key.
|
`ALGOLIA_APP_ID`/`ALGOLIA_API_KEY`); never commit any other Algolia key.
|
||||||
`data/gwshop_catalog.json` is the committed snapshot and the sorter default.
|
`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
|
- Bookkeeping (`sorter_applied.jsonl`, `sorter_low_confidence.txt`) is written
|
||||||
to `<library>/../_logs/` by default — never inside the library, which may be
|
to `<library>/../_logs/` by default — never inside the library, which may be
|
||||||
a seeded torrent whose file set is frozen.
|
a seeded torrent whose file set is frozen.
|
||||||
|
|||||||
+51457
-51090
File diff suppressed because it is too large
Load Diff
+36
-6
@@ -454,12 +454,18 @@ pub fn build_catalog(hits: &[Value]) -> Value {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// primary branch = longest path (most specific), e.g.
|
// primary branch = longest path (most specific), e.g.
|
||||||
// [40k, Space Marines, Raven Guard, Walker]
|
// [40k, Space Marines, Raven Guard, Walker].
|
||||||
let primary = paths
|
// python `max(paths, key=len)` keeps the FIRST longest path on ties;
|
||||||
.iter()
|
// Rust's `max_by_key` would keep the LAST, so select explicitly to
|
||||||
.max_by_key(|p| p.len())
|
// stay faithful to the original (products faceted under several
|
||||||
.cloned()
|
// branches, e.g. a generic kit listed per chapter, depend on this).
|
||||||
.unwrap_or_default();
|
let primary = paths.iter().fold(Vec::new(), |best, p| {
|
||||||
|
if p.len() > best.len() {
|
||||||
|
p.clone()
|
||||||
|
} else {
|
||||||
|
best
|
||||||
|
}
|
||||||
|
});
|
||||||
let at = |i: usize| -> Value {
|
let at = |i: usize| -> Value {
|
||||||
primary
|
primary
|
||||||
.get(i)
|
.get(i)
|
||||||
@@ -596,6 +602,30 @@ mod tests {
|
|||||||
assert_eq!(e["source"], "gwshop");
|
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]
|
#[test]
|
||||||
fn build_catalog_dedups_and_handles_pathless() {
|
fn build_catalog_dedups_and_handles_pathless() {
|
||||||
let hit = json!({"objectID": "obj2", "name": "Paint"});
|
let hit = json!({"objectID": "obj2", "name": "Paint"});
|
||||||
|
|||||||
Reference in New Issue
Block a user