@ham2k/extension-sdk 0.8.1 → 0.9.0

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.
@@ -1,6 +1,6 @@
1
1
  import { createCachedTranslator } from "./i18n.js";
2
+ import { baseCallOf } from "./callsigns.js";
2
3
  import { modesMatchForDupe } from "./modes.js";
3
- import { parseCallsign } from "@ham2k/lib-callsigns";
4
4
  import { fmtInteger } from "@ham2k/lib-format-tools";
5
5
  import en from "./i18n/activityScoring.en.json";
6
6
  import es from "./i18n/activityScoring.es.json";
@@ -201,10 +201,6 @@ function isGuestOperator(operator, operation) {
201
201
  const stations = String(operation.stationCall ?? "").split(",").map((call) => baseCallOf(call)).filter((call) => call.length > 0);
202
202
  return stations.length > 0 && !stations.includes(baseCallOf(operator));
203
203
  }
204
- function baseCallOf(call) {
205
- const trimmed = call.trim().toUpperCase();
206
- return parseCallsign(trimmed).baseCall || trimmed;
207
- }
208
204
  function summarizeActivation(activatedRefs, operatorRefs, qsos, duplicates, huntedQsos, scope, qsosToActivate, rules, t, ctx) {
209
205
  const refKeys = Object.keys(activatedRefs).sort();
210
206
  const lowest = Math.min(...refKeys.map((key) => activatedRefs[key]));
@@ -0,0 +1,8 @@
1
+ import { parseCallsign } from "@ham2k/lib-callsigns";
2
+ function baseCallOf(call) {
3
+ const trimmed = call.trim().toUpperCase();
4
+ return parseCallsign(trimmed).baseCall || trimmed;
5
+ }
6
+ export {
7
+ baseCallOf
8
+ };
@@ -0,0 +1,48 @@
1
+ import { createCachedTranslator } from "./i18n.js";
2
+ import { fmtInteger, fmtNumber } from "@ham2k/lib-format-tools";
3
+ import en from "./i18n/contestSummary.en.json";
4
+ import es from "./i18n/contestSummary.es.json";
5
+ const tFor = createCachedTranslator({ en: { translation: en }, es: { translation: es } });
6
+ function contestArithmetic(args, ctx) {
7
+ const t = tFor(ctx);
8
+ const clauses = [args.qsos === 1 ? t("qsoOne") : t("qsoMany", { formatted: fmtInteger(args.qsos) })];
9
+ if (args.points !== void 0) {
10
+ const bonus = args.bonus ? t("bonus", { formatted: fmtInteger(args.bonus) }) : "";
11
+ let expression = args.points === 1 ? t("pointOne") : t("points", { formatted: fmtInteger(args.points) });
12
+ if (bonus && args.bonusFirst) expression = `(${expression} ${bonus})`;
13
+ if (args.mults !== void 0) {
14
+ const mults = args.multsNoun ? `${fmtInteger(args.mults)} ${args.multsNoun}` : args.mults === 1 ? t("multOne") : t("multMany", { formatted: fmtInteger(args.mults) });
15
+ expression += ` \xD7 ${mults}`;
16
+ if (args.multParts && args.multParts.length > 0) expression += ` (${args.multParts.join(", ")})`;
17
+ }
18
+ if (args.factor) {
19
+ const count = Number.isInteger(args.factor.count) ? fmtInteger(args.factor.count) : fmtNumber(args.factor.count, "oneDecimal");
20
+ expression += ` \xD7 ${count} ${args.factor.noun}`;
21
+ }
22
+ if (bonus && !args.bonusFirst) expression += ` ${bonus}`;
23
+ clauses.push(expression);
24
+ }
25
+ clauses.push(...args.more ?? []);
26
+ return clauses.join(", ");
27
+ }
28
+ function contestSummary(args, ctx) {
29
+ if (args.scope === "day") return {};
30
+ const t = tFor(ctx);
31
+ return {
32
+ [args.key]: {
33
+ ...args.extra,
34
+ key: args.key,
35
+ for: "operation",
36
+ icon: args.icon,
37
+ total: args.total,
38
+ label: args.total === 1 ? t("titleOnePoint", { title: args.title }) : t("titleManyPoints", { title: args.title, formatted: fmtInteger(args.total) }),
39
+ summary: fmtInteger(args.total),
40
+ longSummary: [args.arithmetic, args.detail].filter((part) => part).join("\n\n"),
41
+ ...args.grid ? { grid: true } : {}
42
+ }
43
+ };
44
+ }
45
+ export {
46
+ contestArithmetic,
47
+ contestSummary
48
+ };
@@ -0,0 +1,11 @@
1
+ {
2
+ "titleOnePoint": "{{title}}: 1 point",
3
+ "titleManyPoints": "{{title}}: {{formatted}} points",
4
+ "qsoOne": "1 QSO",
5
+ "qsoMany": "{{formatted}} QSOs",
6
+ "pointOne": "1 pt",
7
+ "points": "{{formatted}} pts",
8
+ "multOne": "1 mult",
9
+ "multMany": "{{formatted}} mults",
10
+ "bonus": "+ {{formatted}} bonus"
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "titleOnePoint": "{{title}}: 1 punto",
3
+ "titleManyPoints": "{{title}}: {{formatted}} puntos",
4
+ "qsoOne": "1 QSO",
5
+ "qsoMany": "{{formatted}} QSOs",
6
+ "pointOne": "1 pt",
7
+ "points": "{{formatted}} pts",
8
+ "multOne": "1 mult",
9
+ "multMany": "{{formatted}} mults",
10
+ "bonus": "+ {{formatted}} de bonificación"
11
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- // @ham2k/extension-sdk 0.8.1
1
+ // @ham2k/extension-sdk 0.9.0
2
2
  /** Experimental v1 scene contract. All coordinates are in the scene's viewBox. */
3
3
  export interface SvgScene {
4
4
  version: 1;
@@ -1539,6 +1539,33 @@ export declare function contestScorer<S extends Scoresheet = Scoresheet>(scorer:
1539
1539
  scope: ScoringScope;
1540
1540
  };
1541
1541
  export declare function tally(key: string, forScope: "day" | "operation", total: number, extra?: Partial<ScoreTally>): ScoreTally;
1542
+ export interface ContestArithmetic {
1543
+ qsos: number;
1544
+ points?: number;
1545
+ mults?: number;
1546
+ multsNoun?: string;
1547
+ multParts?: string[];
1548
+ bonus?: number;
1549
+ bonusFirst?: boolean;
1550
+ factor?: {
1551
+ count: number;
1552
+ noun: string;
1553
+ };
1554
+ more?: string[];
1555
+ }
1556
+ export declare function contestArithmetic(args: ContestArithmetic, ctx: HookContext): string;
1557
+ export interface ContestSummaryArgs {
1558
+ key: string;
1559
+ scope: "day" | "operation";
1560
+ icon: string;
1561
+ title: string;
1562
+ total: number;
1563
+ arithmetic: string;
1564
+ detail?: string;
1565
+ grid?: boolean;
1566
+ extra?: Record<string, JSONValue>;
1567
+ }
1568
+ export declare function contestSummary(args: ContestSummaryArgs, ctx: HookContext): Record<string, ScoreTally>;
1542
1569
  export declare function operationForQso(base: Record<string, JSONValue>, qso: Record<string, JSONValue>, segments?: OperationSegmentPayload[]): Record<string, JSONValue>;
1543
1570
  export declare function segmentsWith(segments: OperationSegmentPayload[] | undefined, transform: (operation: Record<string, JSONValue>) => Record<string, JSONValue>): OperationSegmentPayload[] | undefined;
1544
1571
  export type UniqueAxis = "day" | "band" | "mode" | "ref";
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ export * from "./dxcc.js";
8
8
  export * from "./location.js";
9
9
  export * from "./modes.js";
10
10
  export * from "./scoring.js";
11
+ export * from "./contestSummary.js";
11
12
  export * from "./segments.js";
12
13
  export * from "./activityScoring.js";
13
14
  export * from "./activityExports.js";
@@ -50,7 +50,8 @@ app at all. Declare the lowest version that has what the bundle uses, so it
50
50
  reaches every app that can run it: 2 adds `host.webSocket`, and the packer
51
51
  refuses a manifest declaring `webSockets` under anything less; 3 adds timers
52
52
  and `onShow`/`onHide`, and from 3 the build rewrites a bare `setTimeout` into
53
- the SDK's (README's "Timers").
53
+ the SDK's (README's "Timers"); 4 lets `host.fetch` reach a plain-`http://` host
54
+ the app fronts with its https proxy (README's `host.fetch`).
54
55
 
55
56
  Three fields are **refused** in a distributed bundle, and it is worth
56
57
  understanding why:
@@ -259,7 +260,7 @@ It reports every problem at once rather than one per run:
259
260
  h2kext-pack: ./build is not ready to package:
260
261
  manifest.json: missing required field 'name'
261
262
  manifest.json: key 'my-clock' must start with your callsign — 'my' is not one (e.g. ki2d-my-clock)
262
- manifest.json: missing 'api' — declare the extension API this was built against (currently 3)
263
+ manifest.json: missing 'api' — declare the extension API this was built against (currently 4)
263
264
  ```
264
265
 
265
266
  `--force-name` waives the naming convention below. It exists for Ham2K's own
package/docs/hooks.md CHANGED
@@ -1224,6 +1224,12 @@ those notice keys (`newPark` is state-parks' spelling of `newRef`) rather than
1224
1224
  inventing a spelling of your own: an unrecognized key leaves core's `duplicate`
1225
1225
  standing beside your credit, on the one row that can show only one.
1226
1226
 
1227
+ A contest's `summarizeScore` returns `contestSummary(...)` rather than
1228
+ building its tallies by hand: one tally titled with the contest's name and
1229
+ score, its detail opening on a `contestArithmetic(...)` line, and nothing at
1230
+ all for the `day` scope — a contest's multipliers run the whole event
1231
+ (docs/design/contests.md §5.2).
1232
+
1227
1233
  A scorer's internal *rules* stay private to its scoresheet: contests don't
1228
1234
  share a rule vocabulary, so only notices, alerts, points and summary tallies
1229
1235
  cross the boundary. `scoreQso` **may mutate** the scoresheet it's given and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ham2k/extension-sdk",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Write extensions for the Ham2K Logger: typed hook contracts and the host API",
5
5
  "keywords": [
6
6
  "ham2k",
@@ -20,9 +20,8 @@
20
20
  // See `docs/hooks.md` §`scoring`, and `contests/cqww` in the app's repository
21
21
  // for the whole thing.
22
22
 
23
- import { annotateCallAgainstCountryFile, contestScorer, defineExtension } from "@ham2k/extension-sdk"
24
- import type { ContestScorer, HookContext, JSONValue, LoggingControlDescriptor, Ref, ScoreTally } from "@ham2k/extension-sdk"
25
- import { fmtInteger } from "@ham2k/lib-format-tools"
23
+ import { annotateCallAgainstCountryFile, contestArithmetic, contestScorer, contestSummary, defineExtension } from "@ham2k/extension-sdk"
24
+ import type { ContestScorer, HookContext, JSONValue, LoggingControlDescriptor, Ref } from "@ham2k/extension-sdk"
26
25
 
27
26
  import manifest from "../manifest.json" with { type: "json" }
28
27
 
@@ -126,24 +125,23 @@ const CQWWScorer: ContestScorer<Scoresheet> = {
126
125
  return { scoresheet, score: { value: points } }
127
126
  },
128
127
 
129
- /// The score as the operator reads it. `scope` is 'day' or the whole
130
- /// contest; this sample reports the same totals for both, where the real
131
- /// extension tracks a separate per-day tally.
132
- summarizeScore({ scoresheet, scope }): Record<string, ScoreTally> {
128
+ /// The score as the operator reads it. Every contest summarizes the same way — its name and score as the
129
+ /// title, then the arithmetic — so the SDK builds the tally. It also answers
130
+ /// the day scope with nothing: a contest's multipliers run the whole event.
131
+ summarizeScore({ scoresheet, scope }, ctx) {
133
132
  const mults = Object.keys(scoresheet.mults).length
134
- return {
135
- [TYPE]: {
133
+ return contestSummary(
134
+ {
136
135
  key: TYPE,
137
- for: scope,
136
+ scope,
138
137
  icon: manifest.icon,
138
+ title: manifest.shortName,
139
139
  total: scoresheet.points * mults,
140
- points: scoresheet.points,
141
- mults,
142
- qsos: scoresheet.qsos,
143
- label: `${fmtInteger(scoresheet.points)} × ${fmtInteger(mults)}`,
144
- summary: `${fmtInteger(scoresheet.points * mults)}`,
140
+ arithmetic: contestArithmetic({ qsos: scoresheet.qsos, points: scoresheet.points, mults }, ctx),
141
+ extra: { points: scoresheet.points, mults, qsos: scoresheet.qsos },
145
142
  },
146
- }
143
+ ctx,
144
+ )
147
145
  },
148
146
  }
149
147