@ham2k/extension-sdk 0.5.4 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +14 -2
- package/dist/scoring.js +21 -12
- package/docs/hooks.md +30 -12
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @ham2k/extension-sdk 0.5.
|
|
1
|
+
// @ham2k/extension-sdk 0.5.6
|
|
2
2
|
/** Experimental v1 scene contract. All coordinates are in the scene's viewBox. */
|
|
3
3
|
export interface SvgScene {
|
|
4
4
|
version: 1;
|
|
@@ -102,7 +102,7 @@ export interface SvgSceneControl {
|
|
|
102
102
|
hover?: boolean;
|
|
103
103
|
/** Accessible descriptions indexed by (value - min) / step. */
|
|
104
104
|
valueLabels?: string[];
|
|
105
|
-
/** Knobs use relative horizontal/upward dragging.
|
|
105
|
+
/** Knobs use relative horizontal/upward dragging. Value per scene unit; default 1. */
|
|
106
106
|
sensitivity?: number;
|
|
107
107
|
}
|
|
108
108
|
export interface PanelSceneEvent {
|
|
@@ -236,6 +236,7 @@ export interface HookContext {
|
|
|
236
236
|
export interface HistoryForCallOptions {
|
|
237
237
|
refType?: string | string[];
|
|
238
238
|
refPrefix?: string | string[];
|
|
239
|
+
refsSegmentAware?: boolean;
|
|
239
240
|
operation?: string | string[];
|
|
240
241
|
excludeOperation?: string | string[];
|
|
241
242
|
fromDate?: string;
|
|
@@ -392,6 +393,7 @@ export interface RefGeojson {
|
|
|
392
393
|
export interface LoggingControlDescriptor {
|
|
393
394
|
key: string;
|
|
394
395
|
label: string;
|
|
396
|
+
shortLabel?: string;
|
|
395
397
|
icon?: string;
|
|
396
398
|
color?: string;
|
|
397
399
|
order?: number;
|
|
@@ -631,12 +633,14 @@ export interface ScoreQsosRequest {
|
|
|
631
633
|
segments?: OperationSegmentPayload[];
|
|
632
634
|
ref?: Record<string, JSONValue>;
|
|
633
635
|
resumeFrom?: JSONValue;
|
|
636
|
+
resumeDay?: number;
|
|
634
637
|
}
|
|
635
638
|
export interface ScoreQsosResult {
|
|
636
639
|
qsoScores: Record<string, QsoScoreVerdict>;
|
|
637
640
|
daySections: DayScoreSummary[];
|
|
638
641
|
operationSummary: Record<string, ScoreTally>;
|
|
639
642
|
scoresheet?: JSONValue;
|
|
643
|
+
scoresheetDay?: number;
|
|
640
644
|
}
|
|
641
645
|
export interface ScoreQsoRequest {
|
|
642
646
|
operation: Record<string, JSONValue>;
|
|
@@ -645,6 +649,8 @@ export interface ScoreQsoRequest {
|
|
|
645
649
|
ref?: Record<string, JSONValue>;
|
|
646
650
|
segments?: OperationSegmentPayload[];
|
|
647
651
|
resumeFrom?: JSONValue;
|
|
652
|
+
resumeDay?: number;
|
|
653
|
+
qsos?: Record<string, JSONValue>[];
|
|
648
654
|
}
|
|
649
655
|
export interface QsoScoreNotices {
|
|
650
656
|
notices: string[];
|
|
@@ -660,6 +666,8 @@ export interface ScoreCandidatesRequest {
|
|
|
660
666
|
ref?: Record<string, JSONValue>;
|
|
661
667
|
segments?: OperationSegmentPayload[];
|
|
662
668
|
resumeFrom?: JSONValue;
|
|
669
|
+
resumeDay?: number;
|
|
670
|
+
qsos?: Record<string, JSONValue>[];
|
|
663
671
|
}
|
|
664
672
|
export type ScoreCandidatesResult = Record<string, QsoScoreNotices>;
|
|
665
673
|
export interface ScoringHook {
|
|
@@ -707,6 +715,10 @@ export interface CommandAction {
|
|
|
707
715
|
wpm: number;
|
|
708
716
|
};
|
|
709
717
|
showKeyer?: Record<string, never>;
|
|
718
|
+
setEnterSends?: {
|
|
719
|
+
on?: boolean;
|
|
720
|
+
running?: boolean;
|
|
721
|
+
};
|
|
710
722
|
updateQso?: Record<string, JSONValue>;
|
|
711
723
|
addQso?: Record<string, JSONValue>;
|
|
712
724
|
sendSpots?: {
|
package/dist/scoring.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
const DAY_IN_MILLIS = 1e3 * 60 * 60 * 24;
|
|
2
|
-
function
|
|
2
|
+
function copySheet(sheet) {
|
|
3
3
|
return JSON.parse(JSON.stringify(sheet));
|
|
4
4
|
}
|
|
5
|
+
function openCheckpoint(resumeFrom, resumeDay) {
|
|
6
|
+
return { sheet: copySheet(resumeFrom), day: typeof resumeDay === "number" ? resumeDay : void 0 };
|
|
7
|
+
}
|
|
5
8
|
function dayOf(qso) {
|
|
6
9
|
const millis = Number(qso.startAtMillis ?? 0);
|
|
7
10
|
return millis - millis % DAY_IN_MILLIS;
|
|
@@ -27,9 +30,9 @@ function contestScorer(scorer, options = {}) {
|
|
|
27
30
|
const refs = operation.refs ?? [];
|
|
28
31
|
return refs.find((r) => typeof r?.type === "string" && scopedRefTypes.includes(r.type));
|
|
29
32
|
}
|
|
30
|
-
function fold(scoresheet, qsos, args, ctx, onQso, onDayEnd, segments) {
|
|
33
|
+
function fold(scoresheet, qsos, args, ctx, onQso, onDayEnd, segments, startDay) {
|
|
31
34
|
let sheet = scoresheet;
|
|
32
|
-
let currentDay;
|
|
35
|
+
let currentDay = startDay;
|
|
33
36
|
let dayCount = 0;
|
|
34
37
|
const timeline = segments && segments.length > 1 ? segments : void 0;
|
|
35
38
|
let segmentIndex = 0;
|
|
@@ -60,11 +63,14 @@ function contestScorer(scorer, options = {}) {
|
|
|
60
63
|
onQso?.(typeof qso.uuid === "string" ? qso.uuid : void 0, result.score);
|
|
61
64
|
}
|
|
62
65
|
if (currentDay !== void 0) onDayEnd?.(currentDay, dayCount, sheet);
|
|
63
|
-
return sheet;
|
|
66
|
+
return { sheet, day: currentDay };
|
|
64
67
|
}
|
|
65
68
|
async function liveScoresheet(args, ctx, excludeUuid) {
|
|
66
69
|
if (args.resumeFrom !== void 0) {
|
|
67
|
-
|
|
70
|
+
const { sheet, day } = openCheckpoint(args.resumeFrom, args.resumeDay);
|
|
71
|
+
const tail = chronological(args.qsos ?? []).filter((q) => !excludeUuid || q.uuid !== excludeUuid);
|
|
72
|
+
if (tail.length === 0) return { sheet, day };
|
|
73
|
+
return fold(sheet, tail, args, ctx, void 0, void 0, args.segments, day);
|
|
68
74
|
}
|
|
69
75
|
const uuid = typeof args.operation.uuid === "string" ? args.operation.uuid : void 0;
|
|
70
76
|
const log = uuid ? await ctx.getQsos?.(uuid) ?? [] : [];
|
|
@@ -85,10 +91,11 @@ function contestScorer(scorer, options = {}) {
|
|
|
85
91
|
scope,
|
|
86
92
|
async scoreQsos(args, ctx) {
|
|
87
93
|
const qsos = chronological(args.qsos);
|
|
88
|
-
const
|
|
94
|
+
const resumed = args.resumeFrom !== void 0 ? openCheckpoint(args.resumeFrom, args.resumeDay) : void 0;
|
|
95
|
+
const start = resumed?.sheet ?? scorer.startScoresheet({ operation: args.operation, ref: args.ref }, ctx);
|
|
89
96
|
const qsoScores = {};
|
|
90
97
|
const daySections = [];
|
|
91
|
-
const end = fold(
|
|
98
|
+
const { sheet: end, day: endDay } = fold(
|
|
92
99
|
start,
|
|
93
100
|
qsos,
|
|
94
101
|
args,
|
|
@@ -103,7 +110,8 @@ function contestScorer(scorer, options = {}) {
|
|
|
103
110
|
scores: scorer.summarizeScore({ scoresheet: sheet, operation: args.operation, ref: args.ref, scope: "day" }, ctx)
|
|
104
111
|
});
|
|
105
112
|
},
|
|
106
|
-
args.segments
|
|
113
|
+
args.segments,
|
|
114
|
+
resumed?.day
|
|
107
115
|
);
|
|
108
116
|
return {
|
|
109
117
|
qsoScores,
|
|
@@ -112,14 +120,15 @@ function contestScorer(scorer, options = {}) {
|
|
|
112
120
|
{ scoresheet: end, operation: args.operation, ref: args.ref, scope: "operation" },
|
|
113
121
|
ctx
|
|
114
122
|
),
|
|
115
|
-
scoresheet: end
|
|
123
|
+
scoresheet: end,
|
|
124
|
+
...endDay !== void 0 && { scoresheetDay: endDay }
|
|
116
125
|
};
|
|
117
126
|
},
|
|
118
127
|
/// Live path: score the candidate against the log so far and report only
|
|
119
128
|
/// its notices/alerts. Nothing is recorded — the resulting scoresheet is
|
|
120
129
|
/// discarded, which is exactly why one `scoreQso` can serve both paths.
|
|
121
130
|
async scoreQso(args, ctx) {
|
|
122
|
-
const sheet = await liveScoresheet(args, ctx, args.excludeUuid);
|
|
131
|
+
const { sheet } = await liveScoresheet(args, ctx, args.excludeUuid);
|
|
123
132
|
const { score } = scorer.scoreQso(
|
|
124
133
|
{ scoresheet: sheet, qso: candidateAsOfNow(args.qso), operation: args.operation, ref: args.ref, isNewDay: false },
|
|
125
134
|
ctx
|
|
@@ -138,12 +147,12 @@ function contestScorer(scorer, options = {}) {
|
|
|
138
147
|
/// contract that makes the batch fold linear. Without it the first
|
|
139
148
|
/// candidate's call would count as worked for every candidate after it.
|
|
140
149
|
async scoreCandidates(args, ctx) {
|
|
141
|
-
const base = await liveScoresheet(args, ctx);
|
|
150
|
+
const { sheet: base } = await liveScoresheet(args, ctx);
|
|
142
151
|
const result = {};
|
|
143
152
|
for (const candidate of args.candidates) {
|
|
144
153
|
const { score } = scorer.scoreQso(
|
|
145
154
|
{
|
|
146
|
-
scoresheet:
|
|
155
|
+
scoresheet: copySheet(base),
|
|
147
156
|
qso: candidateAsOfNow(candidate.qso),
|
|
148
157
|
operation: args.operation,
|
|
149
158
|
ref: args.ref,
|
package/docs/hooks.md
CHANGED
|
@@ -64,10 +64,14 @@ hook method receives `(args, ctx)` where `ctx: HookContext` is:
|
|
|
64
64
|
`options` (`HistoryForCallOptions` in `types.ts`) narrow the history
|
|
65
65
|
before it is ordered and limited, so `{ refType: 'cwt' }` finds last
|
|
66
66
|
month's CWT exchange behind a week of POTA contacts. The filters are
|
|
67
|
-
`refType` (a ref in the QSO's own `refs`
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
`refType` (a ref in the QSO's own `refs` or its operation's, since an
|
|
68
|
+
activation's ref lives on the operation), `refPrefix` (such a ref whose
|
|
69
|
+
`ref` starts with it, ignoring ASCII case; with `refType`, the same ref
|
|
70
|
+
must pass both, so `{ refType: 'potaActivation', refPrefix: 'FR-' }` finds
|
|
71
|
+
every contact made activating a French park; an operation's refs are its
|
|
72
|
+
base ones unless `refsSegmentAware: true`, which reads them as they stood
|
|
73
|
+
at each contact, so a rove's second park or a contest that ended at a
|
|
74
|
+
break is seen), `operation`/`excludeOperation`,
|
|
71
75
|
`band`/`excludeBand`, `mode`/`excludeMode` (`SSB` also matches its
|
|
72
76
|
sidebands, and `CW`/`PHONE`/`DATA` match their whole family), whole UTC
|
|
73
77
|
days through `fromDate`/`toDate` or `fromDateMillis`/`toDateMillis`, and
|
|
@@ -495,8 +499,11 @@ interface ActivityHook {
|
|
|
495
499
|
suggest?(args: SuggestArgs, ctx): Promise<ActivitySuggestion[]>
|
|
496
500
|
processQsoBeforeSave?(args: { qso, operation }, ctx): Promise<Patch | null>
|
|
497
501
|
}
|
|
498
|
-
// LoggingControlDescriptor: {key, label, icon?, color?, order?,
|
|
499
|
-
// allowsMultiple?, editable?, input}
|
|
502
|
+
// LoggingControlDescriptor: {key, label, shortLabel?, icon?, color?, order?,
|
|
503
|
+
// optionType?, allowsMultiple?, editable?, input}
|
|
504
|
+
// `shortLabel` is the same name in fewer characters, for a field the logging
|
|
505
|
+
// row has squeezed below its label's width. The core measures and picks; the
|
|
506
|
+
// extension only says what the short form is.
|
|
500
507
|
// SuggestArgs: {operation, location?: {lat, lon}, callsign?, searchTerm?, scoped?}
|
|
501
508
|
// ActivitySuggestion: Ref & {distance?, relevance?, allowsMultiple?}
|
|
502
509
|
|
|
@@ -847,7 +854,7 @@ interface CommandHook {
|
|
|
847
854
|
// CommandInterpretation: {expectsParams?, mixedCase?, error?, describe?, confirm?, commands?: CommandAction[]}
|
|
848
855
|
// CommandCatalogEntry: {command, describe, params?, category?, needsOperation?, expectsParams?, suggest?}
|
|
849
856
|
// CommandAction (closed set): {setVfo?, setPower?, updateQso?, addQso?,
|
|
850
|
-
// setKeyerSpeed?, showKeyer?, sendSpots?, syncAll?, updateSettings?, updateSetting?, openSettings?,
|
|
857
|
+
// setKeyerSpeed?, showKeyer?, setEnterSends?, sendSpots?, syncAll?, updateSettings?, updateSetting?, openSettings?,
|
|
851
858
|
// updateOperation?, markSegment?, setCallField?, reloadDataFiles?, devNotice?, devStatus?,
|
|
852
859
|
// devProgress?, devHammer?, devSplash?, devOnboard?, devTester?, devPopup?,
|
|
853
860
|
// devTimeTravel?, toggleExperiment?}
|
|
@@ -1164,14 +1171,16 @@ interface ScoringHook {
|
|
|
1164
1171
|
scoreQso?(args: ScoreQsoRequest, ctx): Promise<QsoScoreNotices>
|
|
1165
1172
|
scoreCandidates?(args: ScoreCandidatesRequest, ctx): Promise<ScoreCandidatesResult>
|
|
1166
1173
|
}
|
|
1167
|
-
// ScoreQsosRequest: {operation, qsos, ref?, resumeFrom?}
|
|
1174
|
+
// ScoreQsosRequest: {operation, qsos, ref?, segments?, resumeFrom?, resumeDay?}
|
|
1168
1175
|
// ScoreQsosResult: {
|
|
1169
1176
|
// qsoScores: Record<uuid, QsoScoreVerdict>, // one per QSO claimed; see above
|
|
1170
1177
|
// daySections: {day, count, scores: Record<tallyKey, ScoreTally>}[],
|
|
1171
1178
|
// operationSummary: Record<tallyKey, ScoreTally>,
|
|
1172
1179
|
// scoresheet, // checkpoint to resume from
|
|
1180
|
+
// scoresheetDay?, // the day it ends on, handed back as resumeDay
|
|
1173
1181
|
// }
|
|
1174
|
-
//
|
|
1182
|
+
// ScoreQsoRequest: {operation, qso, excludeUuid?, ref?, segments?, resumeFrom?, resumeDay?, qsos?}
|
|
1183
|
+
// ScoreCandidatesRequest: {operation, candidates: {key, qso}[], ref?, segments?, resumeFrom?, resumeDay?, qsos?}
|
|
1175
1184
|
// ScoreCandidatesResult: Record<candidateKey, QsoScoreNotices>
|
|
1176
1185
|
```
|
|
1177
1186
|
|
|
@@ -1183,7 +1192,15 @@ both live paths for free.
|
|
|
1183
1192
|
A candidate that carries no `startAtMillis` (the draft's time is still
|
|
1184
1193
|
automatic) reaches the scorer stamped with the current time on both live
|
|
1185
1194
|
paths, so a day-bucketed rule judges it as of now rather than at the epoch; a
|
|
1186
|
-
candidate that states a time keeps it.
|
|
1195
|
+
candidate that states a time keeps it. On both live paths a `resumeFrom`
|
|
1196
|
+
checkpoint, with the `qsos` logged since it, stands in for the whole-log read:
|
|
1197
|
+
the harness copies the checkpoint, replays that tail onto it (continuing the
|
|
1198
|
+
day `resumeDay` names, so no scorer's per-day tally restarts mid-day) and
|
|
1199
|
+
scores the candidate against the result. The host sends a checkpoint only when
|
|
1200
|
+
it can prove the log is what the checkpoint folded plus appended contacts
|
|
1201
|
+
(`scoreQso`), or when the log has not moved since the checkpoint was written
|
|
1202
|
+
(`scoreCandidates`, which sends no tail yet); otherwise the harness reads the
|
|
1203
|
+
log through `ctx.getQsos` and folds it cold.
|
|
1187
1204
|
`scoreCandidates` answers the same question for many at once, which is what the
|
|
1188
1205
|
Spots Panel asks to mark the spots already worked: the log is folded once per
|
|
1189
1206
|
scorer and every candidate scored against a COPY of the result, so no candidate
|
|
@@ -1284,13 +1301,14 @@ numeric value patches; local-only controls and animation frames make no bridge
|
|
|
1284
1301
|
calls. Controls with `continuous: true` send `change` events during dragging and a
|
|
1285
1302
|
final `commit` on release; pending movements are coalesced to the newest value.
|
|
1286
1303
|
Numeric text supports `scale`, `truncate`, `modulo`, and `minIntegerDigits`
|
|
1287
|
-
for local numeric formatting
|
|
1304
|
+
for local numeric formatting; bindings accept `scale`, `truncate` and `modulo`
|
|
1305
|
+
too, applied to the value before the ramp. KI2D’s Radio Panel uses these for its Modern
|
|
1288
1306
|
frequency groups; its settings offer Modern/LCD styles and radio selection. Local sliders may use `hover` and `discrete` hourly buckets. Text `samples`
|
|
1289
1307
|
may hold numbers or preformatted strings; control `valueLabels` supplies accessible
|
|
1290
1308
|
value descriptions. Transform/opacity bindings may also supply numeric `samples`,
|
|
1291
1309
|
equally spaced over their `input` range, instead of the `output` ramp. The host
|
|
1292
1310
|
interpolates and clamps these locally; chart markers and callouts can follow
|
|
1293
|
-
individual readings without bridge calls. See [the
|
|
1311
|
+
individual readings without bridge calls. See [the design notes and open milestones](https://github.com/ham2k/halo/blob/main/docs/design/svg-scenes.md)
|
|
1294
1312
|
and the `k2hrc-svg-scenes` sample. This API is experimental.
|
|
1295
1313
|
|
|
1296
1314
|
Scene-capable hosts also supply `args.environment` on **render and event** calls:
|