@ham2k/extension-sdk 0.5.5 → 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 +9 -2
- package/dist/scoring.js +21 -12
- package/docs/hooks.md +21 -7
- 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 {
|
|
@@ -393,6 +393,7 @@ export interface RefGeojson {
|
|
|
393
393
|
export interface LoggingControlDescriptor {
|
|
394
394
|
key: string;
|
|
395
395
|
label: string;
|
|
396
|
+
shortLabel?: string;
|
|
396
397
|
icon?: string;
|
|
397
398
|
color?: string;
|
|
398
399
|
order?: number;
|
|
@@ -632,12 +633,14 @@ export interface ScoreQsosRequest {
|
|
|
632
633
|
segments?: OperationSegmentPayload[];
|
|
633
634
|
ref?: Record<string, JSONValue>;
|
|
634
635
|
resumeFrom?: JSONValue;
|
|
636
|
+
resumeDay?: number;
|
|
635
637
|
}
|
|
636
638
|
export interface ScoreQsosResult {
|
|
637
639
|
qsoScores: Record<string, QsoScoreVerdict>;
|
|
638
640
|
daySections: DayScoreSummary[];
|
|
639
641
|
operationSummary: Record<string, ScoreTally>;
|
|
640
642
|
scoresheet?: JSONValue;
|
|
643
|
+
scoresheetDay?: number;
|
|
641
644
|
}
|
|
642
645
|
export interface ScoreQsoRequest {
|
|
643
646
|
operation: Record<string, JSONValue>;
|
|
@@ -646,6 +649,8 @@ export interface ScoreQsoRequest {
|
|
|
646
649
|
ref?: Record<string, JSONValue>;
|
|
647
650
|
segments?: OperationSegmentPayload[];
|
|
648
651
|
resumeFrom?: JSONValue;
|
|
652
|
+
resumeDay?: number;
|
|
653
|
+
qsos?: Record<string, JSONValue>[];
|
|
649
654
|
}
|
|
650
655
|
export interface QsoScoreNotices {
|
|
651
656
|
notices: string[];
|
|
@@ -661,6 +666,8 @@ export interface ScoreCandidatesRequest {
|
|
|
661
666
|
ref?: Record<string, JSONValue>;
|
|
662
667
|
segments?: OperationSegmentPayload[];
|
|
663
668
|
resumeFrom?: JSONValue;
|
|
669
|
+
resumeDay?: number;
|
|
670
|
+
qsos?: Record<string, JSONValue>[];
|
|
664
671
|
}
|
|
665
672
|
export type ScoreCandidatesResult = Record<string, QsoScoreNotices>;
|
|
666
673
|
export interface ScoringHook {
|
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
|
@@ -499,8 +499,11 @@ interface ActivityHook {
|
|
|
499
499
|
suggest?(args: SuggestArgs, ctx): Promise<ActivitySuggestion[]>
|
|
500
500
|
processQsoBeforeSave?(args: { qso, operation }, ctx): Promise<Patch | null>
|
|
501
501
|
}
|
|
502
|
-
// LoggingControlDescriptor: {key, label, icon?, color?, order?,
|
|
503
|
-
// 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.
|
|
504
507
|
// SuggestArgs: {operation, location?: {lat, lon}, callsign?, searchTerm?, scoped?}
|
|
505
508
|
// ActivitySuggestion: Ref & {distance?, relevance?, allowsMultiple?}
|
|
506
509
|
|
|
@@ -1168,14 +1171,16 @@ interface ScoringHook {
|
|
|
1168
1171
|
scoreQso?(args: ScoreQsoRequest, ctx): Promise<QsoScoreNotices>
|
|
1169
1172
|
scoreCandidates?(args: ScoreCandidatesRequest, ctx): Promise<ScoreCandidatesResult>
|
|
1170
1173
|
}
|
|
1171
|
-
// ScoreQsosRequest: {operation, qsos, ref?, resumeFrom?}
|
|
1174
|
+
// ScoreQsosRequest: {operation, qsos, ref?, segments?, resumeFrom?, resumeDay?}
|
|
1172
1175
|
// ScoreQsosResult: {
|
|
1173
1176
|
// qsoScores: Record<uuid, QsoScoreVerdict>, // one per QSO claimed; see above
|
|
1174
1177
|
// daySections: {day, count, scores: Record<tallyKey, ScoreTally>}[],
|
|
1175
1178
|
// operationSummary: Record<tallyKey, ScoreTally>,
|
|
1176
1179
|
// scoresheet, // checkpoint to resume from
|
|
1180
|
+
// scoresheetDay?, // the day it ends on, handed back as resumeDay
|
|
1177
1181
|
// }
|
|
1178
|
-
//
|
|
1182
|
+
// ScoreQsoRequest: {operation, qso, excludeUuid?, ref?, segments?, resumeFrom?, resumeDay?, qsos?}
|
|
1183
|
+
// ScoreCandidatesRequest: {operation, candidates: {key, qso}[], ref?, segments?, resumeFrom?, resumeDay?, qsos?}
|
|
1179
1184
|
// ScoreCandidatesResult: Record<candidateKey, QsoScoreNotices>
|
|
1180
1185
|
```
|
|
1181
1186
|
|
|
@@ -1187,7 +1192,15 @@ both live paths for free.
|
|
|
1187
1192
|
A candidate that carries no `startAtMillis` (the draft's time is still
|
|
1188
1193
|
automatic) reaches the scorer stamped with the current time on both live
|
|
1189
1194
|
paths, so a day-bucketed rule judges it as of now rather than at the epoch; a
|
|
1190
|
-
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.
|
|
1191
1204
|
`scoreCandidates` answers the same question for many at once, which is what the
|
|
1192
1205
|
Spots Panel asks to mark the spots already worked: the log is folded once per
|
|
1193
1206
|
scorer and every candidate scored against a COPY of the result, so no candidate
|
|
@@ -1288,13 +1301,14 @@ numeric value patches; local-only controls and animation frames make no bridge
|
|
|
1288
1301
|
calls. Controls with `continuous: true` send `change` events during dragging and a
|
|
1289
1302
|
final `commit` on release; pending movements are coalesced to the newest value.
|
|
1290
1303
|
Numeric text supports `scale`, `truncate`, `modulo`, and `minIntegerDigits`
|
|
1291
|
-
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
|
|
1292
1306
|
frequency groups; its settings offer Modern/LCD styles and radio selection. Local sliders may use `hover` and `discrete` hourly buckets. Text `samples`
|
|
1293
1307
|
may hold numbers or preformatted strings; control `valueLabels` supplies accessible
|
|
1294
1308
|
value descriptions. Transform/opacity bindings may also supply numeric `samples`,
|
|
1295
1309
|
equally spaced over their `input` range, instead of the `output` ramp. The host
|
|
1296
1310
|
interpolates and clamps these locally; chart markers and callouts can follow
|
|
1297
|
-
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)
|
|
1298
1312
|
and the `k2hrc-svg-scenes` sample. This API is experimental.
|
|
1299
1313
|
|
|
1300
1314
|
Scene-capable hosts also supply `args.environment` on **render and event** calls:
|