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") + } +}