fix: a hand-assigned award reference no longer loses to the matcher

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.
This commit is contained in:
2026-08-01 00:57:34 +02:00
parent d23c4c4037
commit 06e34372ed
4 changed files with 83 additions and 4 deletions
+4 -2
View File
@@ -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."
]
},
{
+2
View File
@@ -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"];
}
+21 -2
View File
@@ -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
+56
View File
@@ -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")
}
}