From 06e34372ede6027b7332cfc7682b983852325fd8 Mon Sep 17 00:00:00 2001 From: rouggy Date: Sat, 1 Aug 2026 00:57:34 +0200 Subject: [PATCH] fix: a hand-assigned award reference no longer loses to the matcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported on the DARC DOK award, with DL2FDM: a German address matches two or three DOKs, the operator deletes the wrong ones and assigns the right one, and the next recompute puts the wrong ones straight back. The manual reference was APPENDED to what the matcher found. Storage already kept exactly one manual entry per award — setOverrideRef drops the previous one — so it was an override everywhere except at the one place it mattered. The operator was arguing with the matcher and could not win. It now replaces. The trace records what was superseded, so a corrected QSO can still be explained rather than being merely different. A QSO with no correction keeps every automatic reference, which the test also pins: an override must not quietly disable matching for everyone else. This does not address the over-matching itself — an address containing "Berlin Mitte, 10115 Berlin" genuinely matches two DOK descriptions. That is the next question, and a separate one. --- changelog.json | 6 ++-- frontend/wailsjs/go/models.ts | 2 ++ internal/award/award.go | 23 ++++++++++++-- internal/award/override_test.go | 56 +++++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 internal/award/override_test.go diff --git a/changelog.json b/changelog.json index d1e7683..00257a6 100644 --- a/changelog.json +++ b/changelog.json @@ -4,11 +4,13 @@ "date": "", "en": [ "Serial CAT no longer keys the radio just by connecting: DTR and RTS are dropped on Xiegu, Yaesu and Kenwood as they already were on Icom — a Xiegu G90 behind a DE-19 went straight into transmit and stayed there.", - "Xiegu: choose how the rig is keyed — CI-V command, RTS or DTR. A G90 does not transmit on the CI-V command, so this is also what lets WSJT-X transmit through the shared CAT link." + "Xiegu: choose how the rig is keyed — CI-V command, RTS or DTR. A G90 does not transmit on the CI-V command, so this is also what lets WSJT-X transmit through the shared CAT link.", + "A reference you assign by hand now replaces what the matcher found instead of being added to it, so a correction survives the next recompute." ], "fr": [ "Le CAT série ne met plus la radio en émission par le seul fait de se connecter : DTR et RTS sont abaissés sur Xiegu, Yaesu et Kenwood comme ils l'étaient déjà sur Icom — un Xiegu G90 derrière un DE-19 partait en émission et y restait.", - "Xiegu : choix du passage en émission — commande CI-V, RTS ou DTR. Un G90 n'émet pas sur la commande CI-V, c'est donc aussi ce qui permet à WSJT-X d'émettre via le partage CAT." + "Xiegu : choix du passage en émission — commande CI-V, RTS ou DTR. Un G90 n'émet pas sur la commande CI-V, c'est donc aussi ce qui permet à WSJT-X d'émettre via le partage CAT.", + "Une référence que vous affectez à la main remplace désormais ce que la détection avait trouvé au lieu de s'y ajouter : une correction survit au recalcul suivant." ] }, { diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 35e9dd0..4d498b5 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -461,6 +461,7 @@ export namespace award { ref_count: number; steps: Step[]; manual?: string[]; + superseded?: string[]; result: string[]; static createFrom(source: any = {}) { @@ -476,6 +477,7 @@ export namespace award { this.ref_count = source["ref_count"]; this.steps = this.convertValues(source["steps"], Step); this.manual = source["manual"]; + this.superseded = source["superseded"]; this.result = source["result"]; } diff --git a/internal/award/award.go b/internal/award/award.go index fff83c9..4d81129 100644 --- a/internal/award/award.go +++ b/internal/award/award.go @@ -859,7 +859,11 @@ type Explanation struct { RefCount int `json:"ref_count"` // size of that list Steps []Step `json:"steps"` Manual []string `json:"manual,omitempty"` // references the operator assigned by hand - Result []string `json:"result"` // what the QSO finally counts for + // Superseded is what the matcher had found before the operator's choice + // replaced it — shown in the trace so a correction is explainable rather + // than mysterious. + Superseded []string `json:"superseded,omitempty"` + Result []string `json:"result"` // what the QSO finally counts for } // Explain runs the matcher on a single QSO and reports what it did. It goes @@ -977,7 +981,22 @@ func candidatesTrace(d *Def, re *regexp.Regexp, q *qso.QSO, rl refList, hasList if ex != nil { ex.Manual = manual } - found = append(found, manual...) + // An operator's choice REPLACES what the matcher found. It does not join it. + // + // Adding to it was the behaviour, and it made a correction impossible: a + // German address matching two or three DOKs kept them all, so deleting the + // wrong ones and assigning the right one lasted exactly until the next + // recompute, which put them straight back. The operator was arguing with + // the matcher and could not win. + // + // One manual entry is stored per award (setOverrideRef drops the previous + // one), so this is an override in the storage as well as in the name. + if len(manual) > 0 { + if ex != nil { + ex.Superseded = dedupe(found) + } + found = manual + } out := dedupe(found) if ex != nil { ex.Result = out diff --git a/internal/award/override_test.go b/internal/award/override_test.go new file mode 100644 index 0000000..5ffbf02 --- /dev/null +++ b/internal/award/override_test.go @@ -0,0 +1,56 @@ +package award + +import ( + "strings" + "testing" + + "hamlog/internal/qso" +) + +// An operator's correction must survive a recompute. +// +// Reported on the DARC DOK award: a German address matches two or three DOKs, +// the operator deletes the wrong ones and assigns the right one, and the next +// recompute puts the wrong ones straight back. The manual reference was being +// ADDED to what the matcher found instead of replacing it, so the correction +// could never win. +func TestManualReferenceReplacesTheMatch(t *testing.T) { + // An award that reads the address and matches a reference's description — + // the shape the DOK award uses, and the shape that over-matches. + def := Def{ + Code: "DLD", Name: "DARC DOK", Field: "address", + MatchBy: "description", Type: "QSOFIELDS", Valid: true, + } + metas := []RefMeta{ + {Code: "A01", Name: "Berlin", Valid: true}, + {Code: "B05", Name: "Berlin Mitte", Valid: true}, + } + // An address that legitimately matches both. + q := &qso.QSO{ + Callsign: "DL2FDM", + Address: "Berlin Mitte, 10115 Berlin", + } + + auto := Explain(def, metas, q) + if len(auto.Result) < 2 { + t.Fatalf("the test needs an over-matching case; got %v", auto.Result) + } + + // The operator picks the right one. + q.Extras = map[string]string{ManualRefsKey: "DLD@B05"} + fixed := Explain(def, metas, q) + + if got := strings.Join(fixed.Result, ","); got != "B05" { + t.Errorf("after the correction the QSO counts for %q — want B05 alone", got) + } + if len(fixed.Superseded) == 0 { + t.Error("the trace does not say what the correction replaced") + } + + // A QSO with no correction still gets everything the matcher found: the + // override must not disable matching for everyone else. + q2 := &qso.QSO{Callsign: "DL3XYZ", Address: "Berlin Mitte, 10115 Berlin"} + if len(Explain(def, metas, q2).Result) < 2 { + t.Error("an untouched QSO lost its automatic references") + } +}