fix(route-explorer): close Asia→DE lane gap for TW/KR/JP/VN/TH/PH/IN + lock down#3832
Conversation
…+ lock down PR #3828 fixed HK→Germany by adding `china-europe-suez` and `asia-europe-cape` to HK's `nearestRouteIds` in `scripts/shared/country-port-clusters.json`, and explicitly deferred TW/KR/JP/VN/TH/PH because each needed review against actual shipping data. While triaging a reporter's HK→DE / Premium Stock / WM Analyst empty-state screenshots (pr-3718 session), I confirmed those six countries plus IN still fail with `noModeledLane: true` against DE — server reports the gap honestly, UI shows "No modeled lane for this pair." Root cause for each is identical to HK's: the country has only Pacific/intra-Asia/Gulf-oil route IDs, none of which appear in DE's cluster (`china-europe-suez`, `asia-europe-cape`, `transatlantic`). Server intersection at server/worldmonitor/supply-chain/v1/get-route-explorer-lane.ts:233-234 returns zero shared routes → noModeledLane=true. Fix: add `china-europe-suez` and `asia-europe-cape` (the trunk Asia↔Europe container routes terminating at Rotterdam) to TW, KR, JP, VN, TH, PH, IN. Both routes are tagged `category: 'container', status: 'active'` in src/config/trade-routes.ts; they are factually correct for every major Asian export hub shipping to Northern Europe. Lock-down: tests/country-port-clusters-asia-europe-lane.test.mts (3 cases) 1. Data invariant — every Asian port country in {CN,HK,TW,JP,KR,SG,MY,ID, TH,VN,PH,IN} must have a non-empty `nearestRouteIds` intersection with DE in the cluster JSON. Reverting any of the seven new entries flips this test red with a clear "add china-europe-suez and/or asia-europe- cape to <ISO2>'s nearestRouteIds" error. 2. Computed lane — `computeLane('HK','DE','85','container')` must return `noModeledLane: false` and a non-empty `primaryRouteId`. This is the EXACT shape of the reporter's screenshot (HK→Germany, Electrical & Electronics HS2=85, Container, AUTO badge); the original PR #3828 had no test pinning this case, so a future contributor reverting the HK entry would not be caught. 3. Full Asian-port matrix — same `noModeledLane: false` assertion for all 12 Asian export hubs against DE, defending the algorithm boundary (computeLane) on top of the data invariant. Bite-test: reverting just the data change (git stash on the JSON) flips tests #1 and #3 red, naming the exact 7 offenders (TW/JP/KR/TH/VN/PH/IN); test #2 stays green because HK was already fixed by #3828. Confirms each assertion bites its intended regression. The 30-query smoke matrix in tests/route-explorer-lane.test.mts (38 cases) stays 38/38 green. tsc clean. This complements PR #3828's HK fix and clears the deferred-follow-up note without touching unrelated areas. The reporter's third screenshot (HK→DE "No modeled lane") will resolve on next deploy regardless of this PR (PR #3828 already fixed HK); this PR additionally prevents TW/KR/JP/VN/TH/ PH/IN from showing the same dead-end to other Asia-export-hub users, and makes both fixes regression-proof.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Greptile SummaryExtends the Asia→Germany lane gap fix from PR #3828 (which covered HK) to the seven deferred countries — TW, KR, JP, VN, TH, PH, and IN — by adding
Confidence Score: 5/5Safe to merge — data-only additions to the cluster JSON with an additive regression test; no existing lane pairs are affected. The JSON changes expand nearestRouteIds for seven countries using two confirmed-active route IDs; set-intersection logic means additions can only open new lane matches, never break existing ones. The test additions are purely additive and the PR author's bite-test confirms each assertion targets the correct offender. No files require special attention; the two observations flagged are informational and do not affect correctness. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["computeLane(fromIso2, toIso2, hs2, cargoType)"]
B["Load fromCluster.nearestRouteIds"]
C["Load toCluster.nearestRouteIds"]
D["sharedRoutes = intersection(fromRoutes, toRoutes)"]
E{sharedRoutes.length === 0?}
F["noModeledLane: true\n(was the bug for TW/KR/JP/VN/TH/PH/IN → DE)"]
G["rankSharedRoutesByCargo(sharedRoutes, cargoType)"]
H["primaryRouteId = rankedRoutes[0]"]
I["Return lane result\nnoModeledLane: false"]
A --> B & C
B --> D
C --> D
D --> E
E -- yes --> F
E -- no --> G
G --> H
H --> I
subgraph "Fix: Added to TW/KR/JP/VN/TH/PH/IN"
J["china-europe-suez (already in DE)"]
K["asia-europe-cape (already in DE)"]
end
D -. "now includes" .-> J & K
Reviews (1): Last reviewed commit: "fix(route-explorer): close Asia→DE lane ..." | Re-trigger Greptile |
| }); | ||
|
|
||
| it('computeLane(HK → DE, container, HS2=85) returns noModeledLane: false', async () => { | ||
| const res = await computeLane({ fromIso2: 'HK', toIso2: 'DE', hs2: '85', cargoType: 'container' }, new Map()); | ||
| assert.equal(res.noModeledLane, false, 'HK → DE container/electronics must resolve a primary route (regression of the user-reported pr-3718 symptom)'); | ||
| assert.ok(res.primaryRouteId.length > 0, 'primaryRouteId must be populated when noModeledLane is false'); | ||
| }); |
There was a problem hiding this comment.
Redundant HK-specific test case
Test 2 (the standalone HK → DE assertion) is fully exercised by test 3, which iterates over ASIAN_PORT_COUNTRIES — a constant that already includes 'HK' at position 1. Any regression that breaks the HK entry would be caught by both tests, producing two failure messages about the same country. Consider either removing test 2 or replacing the HK-specific assertion with a spot-check of primaryRouteId for all countries (which test 3 currently omits).
| "TW": { "nearestRouteIds": ["china-us-west", "intra-asia-container"], "coastSide": "pacific" }, | ||
| "JP": { "nearestRouteIds": ["gulf-asia-oil", "qatar-asia-lng", "intra-asia-container"], "coastSide": "pacific" }, | ||
| "KR": { "nearestRouteIds": ["gulf-asia-oil", "qatar-asia-lng", "intra-asia-container"], "coastSide": "pacific" }, | ||
| "IN": { "nearestRouteIds": ["india-europe", "india-se-asia", "gulf-asia-oil", "china-europe-suez", "asia-europe-cape"], "coastSide": "indian" }, |
There was a problem hiding this comment.
india-europe remains invisible to DE lane matching
IN already carries india-europe (an active container route from Nhava Sheva to Rotterdam), but DE's cluster does not include india-europe. The intersection logic in computeLane therefore ignores it, and IN→DE will resolve through china-europe-suez / asia-europe-cape instead. This PR's fix is correct and unblocks the lane, but if india-europe is ever meant to be the canonical India↔Europe lane shown in Route Explorer analytics, DE's entry (line 11) would also need india-europe added. Worth a note for whoever owns the route data.
…urope, not china-europe-suez Reviewer (correct): adding `china-europe-suez` + `asia-europe-cape` to IN satisfied `noModeledLane === false` for IN→DE but pushed the resolver to pick `china-europe-suez` (Shanghai → Rotterdam) as the primary route. The Route Explorer UI highlights the route's from→to ports, so an Indian shipment to Germany rendered a Shanghai origin port. The test was vacuous on this dimension — it only asserted `noModeledLane === false`, never which route resolved. Root cause of my original choice: DE's `nearestRouteIds` didn't list `india-europe`, so no intersection existed for IN→DE via that route. I papered over the missing intersection by polluting IN's profile with China-origin trunk routes instead of fixing DE's profile. Correct fix (follows existing convention): every European country that already has `china-europe-suez` (the Asia-Europe trunk to Rotterdam) also gets `india-europe` (the India-Europe trunk to Rotterdam). Both routes terminate at the same Northern-European hub; the data model treats trunk routes as broadly serving any European port country. Applied uniformly to DE, GB, FR, IT, ES, NL, BE, PL, SE, DK, FI, GR, PT (the 13 European countries with `china-europe-suez`). IN's `nearestRouteIds` reverts to `["india-europe", "india-se-asia", "gulf-asia-oil"]` — no more China-origin pollution. IN→DE now intersects on `india-europe` (only shared route), so the resolver picks it unambiguously. Test tightening (the actual lock the reviewer asked for): `tests/country-port-clusters-asia-europe-lane.test.mts` adds an `EXPECTED_PRIMARY_ROUTE_TO_DE` map and asserts `computeLane(<iso2>, 'DE', '85', 'container').primaryRouteId === expected` for all 12 Asian-port countries. IN→DE must equal `'india-europe'`; all others must equal `'china-europe-suez'`. Removed the weaker "noModeledLane: false for the matrix" test — the stricter primaryRouteId assertion subsumes it (a `noModeledLane=true` response fails the equality check with a clear message naming the offender). Bite-test of the new assertion: - Revert `india-europe` from DE only → both tests #1 and #3 flip red naming IN: data invariant says "IN → DE: shared routes = []", lane assertion says "IN → DE: noModeledLane=true (expected primaryRouteId='india-europe')". Confirms the assertion bites the exact reviewer-flagged regression. - HK→DE remains green (china-europe-suez is correct for an HK origin). - 38/38 of the existing `tests/route-explorer-lane.test.mts` smoke matrix stays green. tsc clean. The other Asian origins (TW/JP/KR/VN/TH/PH/SG/MY/ID) already correctly intersect DE on `china-europe-suez` (added in this PR's first commit) — that's the trunk route their containers actually take to Europe, so their primaryRouteId assertion was already correct.
…w P2) Greptile P2: the standalone HK → DE test was fully subsumed by the matrix test, which iterates over ASIAN_PORT_COUNTRIES (includes HK at index 1) and asserts `primaryRouteId === EXPECTED_PRIMARY_ROUTE_TO_DE[iso2]`. HK's expected value is 'china-europe-suez'; a regression on HK fails the equality check with a clear message, just like every other origin. Renamed the surviving matrix test to call out HK→DE explicitly so the provenance trail to the pr-3718 reporter symptom isn't lost.
Summary
china-europe-suez+asia-europe-capeto HK inscripts/shared/country-port-clusters.json, but explicitly deferred TW/KR/JP/VN/TH/PH ("each needed review against actual shipping data"). While triaging a reporter's pre-deploy screenshots (HK→DE "No modeled lane", empty WM Analyst, empty Premium Stock), I confirmed those six countries plus IN still resolve tonoModeledLane: trueagainst DE — same root cause as HK, same fix shape.china-europe-suez+asia-europe-cape(trunk Asia↔Europe container routes terminating at Rotterdam,status: 'active'insrc/config/trade-routes.ts) to TW, KR, JP, VN, TH, PH, IN.tests/country-port-clusters-asia-europe-lane.test.mts(3 cases) — data invariant on the cluster JSON, computed-lane assertion for the exact reporter shape (HK→DE/85/container), and full Asian-port matrix against DE.Why this needs to ship even though the reporter's HK→DE will fix itself
The reporter's screenshots predate today's deploy of PRs #3814 + #3828, so HK→DE will already work for them after this morning's deploy (no client action needed). This PR:
Bite-test of the regression test
With this PR's data fix reverted (
git stashon the JSON), the test fires:HK stays green (already fixed by #3828). Confirms each assertion bites the right regression.
Test plan
npx tsx --test tests/country-port-clusters-asia-europe-lane.test.mts— 3/3 greennpx tsx --test tests/route-explorer-lane.test.mts(existing 30-query smoke matrix) — 38/38 green, no regressionnpx tsc --noEmitclean