Merge origin/main: keep the substring rule, read its tie

main had moved on with retrieval work validated against real queries —
minTokenHits (the word match needs two thirds of the label, not all of
it), separator folding so "Parle G"/"Parle-G"/"ParleG" all reach Parle-G,
the floor at 0.50 after "Paracetamol" came back as "Paneer Makhni 500ml"
at 0.304, and ties broken on cosine distance instead of name. All of that
is kept exactly as it was.

The conflict was in textScore: this branch replaced the substring rule
with a coverage formula to stop a bare brand name resolving to one
arbitrary product. That is the wrong half to change. The substring rule
scores every product of a brand 0.95 IDENTICALLY, and that tie is not the
bug — it is the signal. isAmbiguous reads it, so the branch's coverage
rewrite is dropped and the ambiguity layer alone does the work:

  "britannia" → all 258 rows tie at 0.95 → ambiguous: true + candidates
  "Parle G"   → folding and the single-character token still land it
  a real name → runner-up far behind → match, unchanged

Dropped with it: scanSpecificEnough, the per-hit text score, and the
proportional confirmation bonus — the flat +0.10 is back. Simpler, and it
leaves main's tuning untouched.

TestTextScoreRewardsSpecificityNotJustOverlap tested the removed formula
and is replaced by TestABrandNameScoresItsProductsIdentically, which
guards the tie itself: a formula that broke it on name length or word
count would bring the bug back.

Docs carry both rationales, and now say plainly that confidence stays
high on the ambiguous path — gate on `ambiguous`, never on `confidence`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-23 11:05:03 +05:30
25 changed files with 1661 additions and 228 deletions

View File

@@ -296,10 +296,20 @@ type NewPartner struct {
Where they work — ONE district, not a set.
`Applocationid` is the home region and goes on the partner row itself,
because `GetPartners` filters on it and the rider app reads it.
`Applocationids` is every region they cover and goes to
`partnerlocations` — one partner routinely serves several cities, and
that is the whole reason the link table exists.
because the rider app reads it. The same region is also written to
`partnerlocations`, which is the table that may hold SEVERAL — a partner
routinely serves more than one city, and that is why the link table
exists, and partners with two are live — partner 44 covers regions 1 and
2. Nothing on THIS path creates one: `regionsOf` returns this single
field and the console's form offers one district, never a set. So a
multi-region partner can be read and must be handled, but cannot yet be
made here.
`GetPartners` reads the link table rather than this field, for two
reasons. It is the column allowed to grow, so a partner who covers a
second city will be found there without another change. And
`partnerinfo` is shared with another product that writes no link rows,
so having one is what marks a partner as ours.
*/
Applocationid int `json:"applocationid"`
/*

View File

@@ -155,6 +155,23 @@ type Products struct {
// `catalogueProductColumns` casts its text[] columns to text.
Productimages string `json:"productimages,omitempty" gorm:"column:productimages;type:jsonb"`
// The catalogue's own record of this product, as it stood at import.
//
// Holds the fields the snapshot does not have columns for — fssai_license,
// highlights, nutrients, providers, price_range, variant_key, title,
// sku_source, search_query — so the console can show them without asking
// the catalogue again. It asked on every drawer open, and got nothing back
// the moment a re-scrape retired the source row, taking a licence number
// and a nutrition panel off a product the shop was still selling.
//
// Empty for anything that did not come from the catalogue: a sheet-imported
// product has no such record, and the drawer falls back to the live lookup
// for those exactly as before.
//
// A string for the same reason `Productimages` is one — GORM's raw
// scan-into-struct silently drops slice- and map-kind destination fields.
Cataloguefacts string `json:"cataloguefacts,omitempty" gorm:"column:cataloguefacts;type:jsonb"`
Productdesc string `json:"productdesc,omitempty"`
Productsku string `json:"productsku,omitempty"`
Brandid int `json:"brandid,omitempty"`
@@ -225,6 +242,28 @@ type Locationproducts struct {
Productimage string `json:"productimage,omitempty"`
Productdesc string `json:"productdesc,omitempty"`
Productsku string `json:"productsku,omitempty"`
// Three columns this read used to leave in the table.
//
// All three are stored on `products` and none of them reached the store
// catalogue screen, because this struct had no field to scan them into —
// so the console could not use what the import had gone to the trouble of
// saving:
//
// Imageid the catalogue's durable key, and what HealthScorePanel
// joins on. Absent, the panel reads it as "this product
// never came from the catalogue" and renders nothing — for
// EVERY product, including ones that plainly did.
// Productimages the rest of a product's photos. `imagesOf()` parses this
// and always got undefined, so the gallery fell back to
// the single `productimage` and the extra images — 90 of
// nestle's 123 products have them — were never shown.
// Cataloguefacts the licence, nutrition, highlights, providers and price
// range kept at import so they survive a re-scrape.
Imageid string `json:"imageid,omitempty"`
Productimages string `json:"productimages,omitempty"`
Cataloguefacts string `json:"cataloguefacts,omitempty"`
Brandid int `json:"brandid,omitempty"`
Productbrand string `json:"productbrand,omitempty"`
Productunit string `json:"productunit"`

View File

@@ -71,6 +71,14 @@ type Tenantinfo struct {
Allocationid int `json:"allocationid"`
Allocationtype string `json:"allocationtype"`
Allocationmode int `json:"allocationmode"`
// How many outlets this merchant has.
//
// Only `GetAllTenants` fills this; it is 0 everywhere else, which is why it
// is last and optional rather than part of the record proper. The console's
// store list previously derived it by counting duplicate rows, and this
// endpoint has never returned duplicates — see the note on the query.
Branchcount int `json:"branchcount"`
}
type Tenantlocations struct {
@@ -190,6 +198,10 @@ type StaffInfo struct {
Tenantid int `json:"tenantid"`
Locationid int `json:"locationid"`
Locationname string `json:"locationname"`
// Whether this login still works, straight off `app_users.status`.
// Without it every row on the console's Users & access screen read
// "Unknown", because the field was never selected or sent.
Status string `json:"status"`
}
type Tenantuser struct {