Let the walk and the completeness check read one rule

A box the walk between the entry boxes steps over cannot be typed in, so it
must not hold the contact back either. `EntryFields.IsComplete` counted every
box marked required, while `MoveFocus` stepped over the ones the contest does
not ask this station for, so a contest with a box for some stations only
could lock the operator out of his own log.

The seven contests the scoring audit named do not do that any more —
`bae40bb` gave each of them the boxes N1MM shows, and fifteen reproductions
across all seven and both station types log cleanly — but the rule they broke
was still there to break again. Completeness now takes the same
applicability rule the cursor uses, and `OperatingPosition` answers it in one
place for the entry window, the ESM window and logging alike.

The audit is marked up for the three findings that are already fixed and for
item 14, which turns out to be what N1MM does rather than a fault.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
This commit is contained in:
2026-09-10 20:33:30 +00:00
co-authored by Claude Opus 5
parent e56d6096f3
commit 211298e04c
6 changed files with 161 additions and 7 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ public sealed partial class EntryWindow
IsRunning = Logging.IsRunning,
HasCall = Logging.Entry.Call.Trim().Length > 0,
IsDupe = Logging.Verdict()?.IsDupe == true,
HasExchange = Logging.Entry.IsComplete,
HasExchange = Logging.IsEntryComplete,
CallSent = callSent,
ExchangeSent = exchangeSent,
SendsCallOnce = session.Settings.EsmSendsCallOnce,
+2 -2
View File
@@ -484,7 +484,7 @@ public sealed partial class EntryWindow : Window
_ = RunEsmAsync();
return;
}
if (!Logging.Entry.IsComplete)
if (!Logging.IsEntryComplete)
{
MoveFocus(forward: true);
return;
@@ -1136,7 +1136,7 @@ public sealed partial class EntryWindow : Window
// the next message of the contact instead of logging it, and Ctrl+Z
// means log it and open the window. N1MM calls `LogContact` at the
// same point
if (Logging.Entry.Call.Trim().Length > 0 && Logging.Entry.IsComplete)
if (Logging.Entry.Call.Trim().Length > 0 && Logging.IsEntryComplete)
{
LogContact();
}
+8 -3
View File
@@ -112,10 +112,15 @@ public sealed class EntryFields
Call.Trim().Length == 0 &&
Exchange.Where((f, at) => !f.IsSent && values[at].Trim().Length > 0).Any() == false;
/// True when everything the contest asks for has been typed.
public bool IsComplete =>
/// True when everything the contest asks for has been typed. `applies`
/// says whether a box is one this station is asked for: the walk between
/// the boxes steps over the ones it is not, so an empty box the cursor
/// never lands in cannot be what is missing. `OperatingPosition` asks the
/// contest, and both use the same answer.
public bool IsComplete(Func<ExchangeField, bool> applies) =>
Call.Trim().Length > 0 &&
Exchange.Where((f, at) => f.IsRequired && values[at].Trim().Length == 0).Any() == false;
Exchange.Where((f, at) =>
f.IsRequired && applies(f) && values[at].Trim().Length == 0).Any() == false;
public void Clear()
{
+8 -1
View File
@@ -253,7 +253,7 @@ public sealed class OperatingPosition
/// has not checked cannot lose an exchange or log nonsense.
public Qso LogContact()
{
if (!Entry.IsComplete)
if (!IsEntryComplete)
{
throw new InvalidOperationException($"the exchange for {Entry.Call} is not complete");
}
@@ -353,6 +353,13 @@ public sealed class OperatingPosition
/// Moves to the next box the operator types in. On top of the report boxes
/// the entry itself steps over, a contest can say a box is not for this
/// station: CQ WW RTTY asks only the US and Canada for a state.
/// True when everything this contest asks for this station has been typed.
/// A box the walk steps over is not asked for, so it does not hold the
/// contact up: `MoveFocus` and this read the same rule.
public bool IsEntryComplete => Entry.IsComplete(Applies);
private bool Applies(ExchangeField field) => !Contest.SkipsField(field, Country());
public void MoveFocus(bool forward)
{
CountryLookup? their = Country();