@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 CHANGED
@@ -1,4 +1,4 @@
1
- // @ham2k/extension-sdk 0.5.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. 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 {
@@ -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 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
@@ -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?, optionType?,
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
- // 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?}
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. 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
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 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)
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ham2k/extension-sdk",
3
- "version": "0.5.5",
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",