@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 CHANGED
@@ -1,4 +1,4 @@
1
- // @ham2k/extension-sdk 0.5.4
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. Default: step per scene unit. */
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 copyCheckpoint(sheet) {
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
- return copyCheckpoint(args.resumeFrom);
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 start = args.resumeFrom !== void 0 ? copyCheckpoint(args.resumeFrom) : scorer.startScoresheet({ operation: args.operation, ref: args.ref }, ctx);
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: copyCheckpoint(base),
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`), `refPrefix` (a ref whose `ref`
68
- starts with it, ignoring ASCII case; with `refType`, the same ref must pass
69
- both, so `{ refType: 'potaActivation', refPrefix: 'FR-' }` finds French
70
- parks), `operation`/`excludeOperation`,
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?, optionType?,
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
- // ScoreCandidatesRequest: {operation, candidates: {key, qso}[], ref?, segments?, resumeFrom?}
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. KI2D’s Radio Panel uses these for its Modern
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 prototype contract and migration plan](https://github.com/ham2k/halo/blob/main/docs/design/svg-scenes.md)
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ham2k/extension-sdk",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "Write extensions for the Ham2K Logger: typed hook contracts and the host API",
5
5
  "keywords": [
6
6
  "ham2k",