JSON Schemas
Four files in Draft 2020-12: the contest document, an instance with concrete UTC times, the operator profile, and the shared building blocks.
Specification
Everything a parser needs: the document layout, the closed vocabularies, the primitive contracts, the JSON Schema, and the test vectors that decide whether your implementation is conformant.
Four files in Draft 2020-12: the contest document, an instance with concrete UTC times, the operator profile, and the shared building blocks.
Nine, each with at least one test vector per numbered rule. A computing primitive without vectors is a claim, not a contract.
Twelve one-line breakages of the real documents. Each one has to be rejected — the half of the checking that has teeth.
ACD is purely declarative, never executive.
A document describes rules, it does not execute them. Where a contest seems to compute something, the question is whether the computation can be named instead of written out — because a submitted document is someone else's code running in another party's scorer.
That is not a stylistic preference. It is the only reason a registry can accept a definition from a stranger and hand it to a logging program without reading every line first.
What the expression language cannot do
Once points could be expressed declaratively, no when, then or derive in any of the six documents contained arithmetic any more — so arithmetic and the ordering comparisons were struck from the grammar altogether. The only operator left in the entire corpus is the * in formula.
The five content blocks of a real definition, colour-coded. Four of them are flat and declarative; scoring is the hard one, by design and not by accident.
identity:
uuid: 9f2b6d41-7c33-4a8e-b5d0-1e8a7c4f2b90
slug: cq-ww-ssb
name: "CQ World Wide DX Contest, SSB"
sponsor: "CQ Magazine"
cabrillo_name: CQ-WW-SSB
adif_contest_id: CQ-WW-SSB
rules_url: https://cqww.com/rules.htm
schedule:
recurrence: "last full weekend of October"
window:
start: { weekday: saturday, time: "00:00" }
duration: PT48H
operating:
bands: [160m, 80m, 40m, 20m, 15m, 10m]
modes: [SSB]
exchange:
- send:
- { name: rs, type: rst }
- { name: zone, type: enum, vocabulary: cq-zones }
scoring:
score_type: claimed
reference_data:
- { name: cty, version: cty-3501 }
- { name: cqww-countries, version: cty-3501 }
derive:
worked.cont: "resolve_continent(worked.call, cty)"
worked.country: "resolve_country(worked.call, cqww-countries)"
dupe: [worked.call, band, mode]
qso_points:
- when: worked.country == me.country
then: 0
- when: worked.cont != me.cont
then: 3
- when: me.cont == 'NA'
then: 2
- else: 1
multipliers:
- { source: received.zone, scope: per_band }
- { source: worked.country, scope: per_band }
formula: sum(qso_points) * count(multipliers)The citable reference
acd:9f2b6d41-7c33-4a8e-b5d0-1e8a7c4f2b90@1
The citable reference is the uuid and the version, never the slug. The slug is a human handle in ACD's own namespace and is not guaranteed against renaming; the uuid is what a consumer pins.
Every one of these lists is closed. An unknown value is a validation error, not an extension point — that is what stops ACD from drifting into one logging program's export format.
Adding an element type, a catalogue entry or a language capability is a version jump, not a free-form field in the document.
Nothing reaches published without clearing all three. Two of them are incomplete, and saying so is the point.
Is the rule source cited? Is an interpretation marked as an interpretation? A human decides this; no machine can.
Is the document internally consistent, without executing it? The JSON Schema decides everything visible in a single object; the rest needs the linter below.
Does a reference engine reproduce the document's own hand-calculated conformance cases? The engine is a separate project and is not built yet.
An implementation is conformant when it reproduces all test vectors of every primitive it uses, and every conformance case shipped with the definition. Any deviation means not conformant.
That is deliberately the only enforcement mechanism this project has. There is no certification, no badge and no licence to revoke — the vectors either pass or they don't. A document pins each primitive by contract version, never by algorithm: a different computation is a different primitive with its own name, so a contract can never change meaning underneath you.
Nine top-level keys are required, seven are optional. Four of the content blocks are flat and declarative. Scoring is the hard one, by design.
| Block | Field | Type | Notes |
|---|---|---|---|
| document | acd | "0.1" | Format version. A constant, not a range. |
| version | integer | Append-only. With the uuid this is the citable reference. | |
| status | enum | draft | published | superseded | |
| identity | uuid | uuid | The identifier. Never changes, never reused. |
| slug | string | The human handle, in ACD's own namespace. Not a Cabrillo or ADIF name. | |
| cabrillo_name | string | Interop anchor. Absent where the contest does not use Cabrillo. | |
| adif_contest_id | string | null | ADIF's own contest id. null where ADIF has no entry. | |
| log_format | enum | cabrillo | edi — what the entrant actually submits. | |
| rules_url | url | The sponsor's original document. | |
| schedule | recurrence | string | Descriptive, in words. Not a generator. |
| window | {start, end, duration} | Weekday and time of day. Concrete UTC times live on the instance. | |
| operating | bands, modes | enum[] | ADIF values only. |
| categories | id, label | category[] | expand builds the cross product before every other check. |
| exchange | send | element[] | Typed list, never free text. One of the seven element types. |
| when | expression | Optional. Selects the branch by a property of the sending station. | |
| scoring | score_type | enum | Mandatory, no default. claimed is the only value in scope. |
| reference_data | {name, version}[] | Every dataset named and pinned, e.g. cty at cty-3501. | |
| derive | map | Exactly one primitive call per target. Writes only to worked.* or qso.* | |
| dupe | path[] | Paths, not bare names: [worked.call, band, mode]. | |
| qso_valid | requirement[] | All must hold. Invalid is not the same as zero points. Each entry is checkable or advisory. | |
| qso_points | rule[] | Ordered. First match wins, exactly one else, always last. | |
| multipliers | {source, scope, when}[] | scope ∈ per_band | once. A when per source, mirroring qso_points. | |
| formula | expression | The only place with arithmetic — and it knows no paths and no primitives. | |
| constraints | type | enum | One of fourteen catalogue types. An unknown type is an error. |
| operator_profile | name, type | field[] | Declares what me.* may read beyond the fixed core. |
| primitives | name, version | {name, version}[] | Pins the contract version, never the algorithm. |
| conformance | operator, log, expected | case[] | Hand-calculated. Two cases required as soon as scoring reads me.* |
Closed list. An exchange element and a profile field draw from the same set, so a value the entrant sends and a value they declare are described identically.
| Type | What it is | Extra fields | Evidenced by |
|---|---|---|---|
| rst | Signal report, adapts to the mode: 59 on SSB, 599 on CW | — | all six |
| serial | Running number | start, min_digits, scope | cq-wpx-ssb |
| enum | One value from a named vocabulary | vocabulary (required) | helvetia |
| number | A number | unit | cq-ww-ssb (zone) |
| string | Free text | length, unit | arrl-ss-ssb (check) |
| callsign | A callsign | length | arrl-ss-ssb |
| locator | A Maidenhead locator | length (2, 4, 6 or 8) | iaru-r1-145mhz |
length is the only shape specification there is. No pattern: a regular expression in an element field is the same foreign code in someone else's scorer as one in a when. And a received value is never rewritten — whoever needs a transformed value names it with derive and reads it back from qso.*
A dimension says one thing only: who you are ranked against. It never says what you are allowed to do — that lives in constraints, always, even when the same fact is also a dimension.
| Dimension | Cabrillo header | Values |
|---|---|---|
| operator | CATEGORY-OPERATOR | single-op, multi-op, checklog |
| band | CATEGORY-BAND | all, 160m … 241g, light, vhf-3-band, vhf-fm-only |
| mode | CATEGORY-MODE | cw, digi, fm, rtty, ssb, mixed |
| power | CATEGORY-POWER | high, low, qrp |
| assisted | CATEGORY-ASSISTED | true, false |
| transmitters | CATEGORY-TRANSMITTER | one, two, limited, unlimited, swl |
| station | CATEGORY-STATION | distributed, fixed, mobile, portable, rover, expedition, hq, school, explorer … |
| time | CATEGORY-TIME | 6-hours, 8-hours, 12-hours, 24-hours |
The list is closed because six hand-written contests needed exactly these, not because a committee maintains a header list. The Cabrillo column is the concrete form of the entrant's declaration for five of the six — but Cabrillo's own vocabularies change, so an ACD value is informed by it and not bound to it. ACD writes values lower case, Cabrillo upper case; the mapping is mechanical.
Fourteen types, closed, each with its own fields and a fixed enforceability level. This is where a logger finds out what an entry may actually do, instead of guessing it from the category name.
| Type | Enforceability | What it covers |
|---|---|---|
| operating_time | checkable | Maximum operating time, minimum rest, how off periods are counted, and what happens to contacts beyond the limit |
| band_change_rate | checkable | Band changes per window, or a minimum dwell time on a band |
| required_log_fields | checkable | Fields the submitted log has to carry, e.g. the transmitter number |
| signal_limit | declarable | Simultaneous signals, per band or per station |
| multiplier_station | declarable | Whether a second transmitter may work multipliers, and whether only new ones |
| location_fixed | declarable | One location for the whole duration |
| station_extent | declarable | Maximum diameter of the station |
| distributed_sites | declarable | Several sites within one entity or zone |
| location_region | declarable | Whether the entry is tied to a region, and which |
| qso_alerting | declarable | Cluster, skimmer and spotting assistance |
| self_spotting | declarable | Spotting your own station |
| remote | declarable | Remote operation, and receivers away from the site |
| post_contest_correction | advisory | Whether calls and exchanges may be fixed afterwards |
| antenna | advisory | Antenna restrictions, as prose |
checkable means a scorer can decide it from the log alone. declarable means the entrant asserts it and only data outside the log could disprove it. advisory means prose: display it, never apply it. declarable is by far the largest group, and that is an honest finding rather than a gap — most contest rules are simply not decidable from a log. Permission fields carry three values, not two: permitted, forbidden and restricted, the last one only ever with a note.
A primitive is a named derivation a definition references by name — resolve_dxcc(worked.call, cty) and friends. We specify them; you implement them. There are no built-in functions: sum(qso_points) and count(multipliers) in formula are not calls but two fixed names for two fixed quantities.
A path is exactly two levels deep and begins with one of these namespaces. An unknown name is a hard error at gate 2, not a warning. worked., received. and qso. never appear inside exchange — at the point the exchange is described there is no contact yet.
| Namespace | Means |
|---|---|
| me.* | Your own station: the fixed core plus every declared profile field |
| worked.* | The station you contacted, including everything derive writes there |
| received.* | The exchange elements you received |
| qso.* | Values derived for this contact itself, such as a computed distance |
| sender.* | The sending station — only inside an exchange branch condition |
| band_groups.* | A named set of bands declared in the document |
| mode_groups.* | The same, for modes |
| band, mode, time | Properties of the contact itself. The three free-standing values, no namespace |
A contract, with its vectors
primitive: resolve_dxcc
contract: 1
signature: (call: callsign, dataset: dataset) -> string
kind: looking up # dataset comes in as an argument
vectors:
resolve_dxcc('HB9HIL', cty) → HB # base case
resolve_dxcc('W1AW', cty) → K # primary prefix, not call prefix
resolve_dxcc('KH6XX', cty) → KH6 # own entity within a country
resolve_dxcc('PA/N8BJQ', cty) → PA # added prefix with its own entity
resolve_dxcc('HB9HIL/P', cty) → HB # added suffix, no entity of its own
resolve_dxcc('hb9hil', cty) → HB # case is irrelevant
resolve_dxcc('QQ1ZZ', cty) → absent # no match, no abortEvery primitive is total. It never aborts: an unresolvable lookup returns an absent value, a multiplier over it does not count, a condition over it evaluates to unknown, and a failed derive does not invalidate the QSO — unlike a missing received exchange element, which does.
Normative, end-to-end: an operator profile, a miniature log, the expected points per QSO, the expected multiplier count and the expected claimed score. Every one of them was calculated by hand from the rules before any code existed.
This is what separates a definition that parses from one that computes. A file can be schema-green and still produce wrong numbers. And one case is not always enough: if an expression in scoring reads a me.* path, the document must carry at least two cases whose profiles differ in a field that is read. CQ WW is the reason — its two-point rule is unreachable for a European entrant, so a wrong number there would pass a single case unnoticed.
conformance: cq-ww-ssb, case A
operator: { call: HB9HIL, cont: EU, country: HB, zone: 14 }
datasets: cty-3501, cq-waz-40
log:
1 DL1ABC 20m SSB → 1 # same continent, other country
2 W1AW 20m SSB → 3 # other continent
3 HB9AA 20m SSB → 0 # own country, multiplier still counts
4 JA1XYZ 40m SSB → 3 # other continent
5 W1AW 40m SSB → 3 # other band, counts again
6 W1AW 20m SSB → - # dupe, dropped
expected:
qso_points: 10
multipliers: 9 # 4 zones + 5 countries, per band
claimed_score: 90The JSON Schema decides everything visible in a single object. Everything that weighs two parts of a document against each other has no expression in JSON Schema at all, and needs a linter that is not written yet. The gap is listed here rather than discovered later.
Twenty of these are code that does not exist. They belong with the reference engine, which is not built either. Until then the schema catches the shape errors, a human catches the rest, and this list says exactly where the boundary runs.
The reference is a contract. These rules are what make it one.
The only editable state. Created automatically when a published version is opened.
Never touched again, by anyone, for any reason. Corrections become a new version.
Still retrievable, and carries a pointer to its successor.
Once an API URL is published it stays. That is why the endpoints are not final yet.
The domain is unfamiliar to most developers. These are the terms the spec assumes.
| QSO | A single radio contact between two stations. One row in a log. |
| Exchange | The information both stations must trade for the QSO to count. Contest-specific: a serial number, a zone, sometimes a first name. |
| DXCC | The official counting unit for countries and territories. K = USA, DL = Germany, HB = Switzerland. |
| CQ / ITU zone | Two competing numbered divisions of the world. A frequent multiplier source. |
| Multiplier | Something that multiplies the point total — "every new DXCC entity counts once". |
| Dupe | A duplicate contact. Which combination counts as a repeat is declared per contest — usually call, band and mode, but ARRL Sweepstakes counts a station once across all bands. |
| Locator | Maidenhead grid square, e.g. JN47. On VHF it is the exchange, and the distance between two of them is the score. |
| Cabrillo | De-facto standard for log submission on HF. Gives us the category vocabulary. |
| EDI | REG1TEST, the submission format for the IARU Region 1 VHF contests. Proof that not every contest is Cabrillo. |
| ADIF | General QSO interchange format. Gives us the band and mode enums. |
| cty.dat | Maps callsign prefixes to DXCC entity, continent and zones. Continuously updated, therefore named and pinned in the document. |
| claimed score | What a single log claims for itself. Deterministically recomputable — and the only kind of score this site deals in. |
| adjudicated score | The official result after cross-checking every participant's log. Only the sponsor can compute it. Out of scope here. |
Example client code in several languages ships alongside the schema. not yet available