Skip to content
contestdatabase.org
Toggle navigation

Specification

Implement ACD

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.

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.

schema/contest.schema.json not yet available

Primitive contracts

Nine, each with at least one test vector per numbered rule. A computing primitive without vectors is a claim, not a contract.

primitives/ not yet available

Negative tests

Twelve one-line breakages of the real documents. Each one has to be rejected — the half of the checking that has teeth.

tests/mutations.mjs not yet available

Purely declarative, never executive

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

  • No arithmetic — outside the overall formula
  • No regular expressions — not even in an element's shape
  • No built-in functions — a computation is a named primitive with a contract
  • No recursion — and no way to define one
1 operator in six contests

The language ended smaller than it started

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.

A document, end to end

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.

cq-ww-ssb.acd.yaml excerpt
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.

What is actually specified

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.

7 Element types
rst, serial, enum, number, string, callsign, locator. length is the only shape specification.
14 Constraint types
A catalogue, each type with fixed fields and a fixed enforceability level.
8 Category dimensions
Grounded in what the entrant declares, not in a log format.
3 Point forms
A number, a table by band or mode group, and one declarative transform.
2 Grammars
Conditions are purely boolean. formula does arithmetic and knows no paths.
9 Primitives
Each with a versioned contract and at least one test vector per rule.

Adding an element type, a catalogue entry or a language capability is a version jump, not a free-form field in the document.

Three gates before publish

Nothing reaches published without clearing all three. Two of them are incomplete, and saying so is the point.

  1. 01

    Editorial

    Is the rule source cited? Is an interpretation marked as an interpretation? A human decides this; no machine can.

  2. 02

    Static

    Is the document internally consistent, without executing it? The JSON Schema decides everything visible in a single object; the rest needs the linter below.

  3. 03

    Dynamic

    Does a reference engine reproduce the document's own hand-calculated conformance cases? The engine is a separate project and is not built yet.

Am I conformant?

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.

The document, field by field

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.

BlockFieldTypeNotes
documentacd"0.1"Format version. A constant, not a range.
versionintegerAppend-only. With the uuid this is the citable reference.
statusenumdraft | published | superseded
identityuuiduuidThe identifier. Never changes, never reused.
slugstringThe human handle, in ACD's own namespace. Not a Cabrillo or ADIF name.
cabrillo_namestringInterop anchor. Absent where the contest does not use Cabrillo.
adif_contest_idstring | nullADIF's own contest id. null where ADIF has no entry.
log_formatenumcabrillo | edi — what the entrant actually submits.
rules_urlurlThe sponsor's original document.
schedulerecurrencestringDescriptive, in words. Not a generator.
window{start, end, duration}Weekday and time of day. Concrete UTC times live on the instance.
operatingbands, modesenum[]ADIF values only.
categoriesid, labelcategory[]expand builds the cross product before every other check.
exchangesendelement[]Typed list, never free text. One of the seven element types.
whenexpressionOptional. Selects the branch by a property of the sending station.
scoringscore_typeenumMandatory, no default. claimed is the only value in scope.
reference_data{name, version}[]Every dataset named and pinned, e.g. cty at cty-3501.
derivemapExactly one primitive call per target. Writes only to worked.* or qso.*
dupepath[]Paths, not bare names: [worked.call, band, mode].
qso_validrequirement[]All must hold. Invalid is not the same as zero points. Each entry is checkable or advisory.
qso_pointsrule[]Ordered. First match wins, exactly one else, always last.
multipliers{source, scope, when}[]scope ∈ per_band | once. A when per source, mirroring qso_points.
formulaexpressionThe only place with arithmetic — and it knows no paths and no primitives.
constraintstypeenumOne of fourteen catalogue types. An unknown type is an error.
operator_profilename, typefield[]Declares what me.* may read beyond the fixed core.
primitivesname, version{name, version}[]Pins the contract version, never the algorithm.
conformanceoperator, log, expectedcase[]Hand-calculated. Two cases required as soon as scoring reads me.*

The seven element types

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.

TypeWhat it isExtra fieldsEvidenced by
rstSignal report, adapts to the mode: 59 on SSB, 599 on CWall six
serialRunning numberstart, min_digits, scopecq-wpx-ssb
enumOne value from a named vocabularyvocabulary (required)helvetia
numberA numberunitcq-ww-ssb (zone)
stringFree textlength, unitarrl-ss-ssb (check)
callsignA callsignlengtharrl-ss-ssb
locatorA Maidenhead locatorlength (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.*

Category dimensions

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.

DimensionCabrillo headerValues
operatorCATEGORY-OPERATORsingle-op, multi-op, checklog
bandCATEGORY-BANDall, 160m … 241g, light, vhf-3-band, vhf-fm-only
modeCATEGORY-MODEcw, digi, fm, rtty, ssb, mixed
powerCATEGORY-POWERhigh, low, qrp
assistedCATEGORY-ASSISTEDtrue, false
transmittersCATEGORY-TRANSMITTERone, two, limited, unlimited, swl
stationCATEGORY-STATIONdistributed, fixed, mobile, portable, rover, expedition, hq, school, explorer …
timeCATEGORY-TIME6-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.

The constraint catalogue

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.

TypeEnforceabilityWhat it covers
operating_timecheckableMaximum operating time, minimum rest, how off periods are counted, and what happens to contacts beyond the limit
band_change_ratecheckableBand changes per window, or a minimum dwell time on a band
required_log_fieldscheckableFields the submitted log has to carry, e.g. the transmitter number
signal_limitdeclarableSimultaneous signals, per band or per station
multiplier_stationdeclarableWhether a second transmitter may work multipliers, and whether only new ones
location_fixeddeclarableOne location for the whole duration
station_extentdeclarableMaximum diameter of the station
distributed_sitesdeclarableSeveral sites within one entity or zone
location_regiondeclarableWhether the entry is tied to a region, and which
qso_alertingdeclarableCluster, skimmer and spotting assistance
self_spottingdeclarableSpotting your own station
remotedeclarableRemote operation, and receivers away from the site
post_contest_correctionadvisoryWhether calls and exchanges may be fixed afterwards
antennaadvisoryAntenna 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.

Paths and primitives

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.

NamespaceMeans
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, timeProperties 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 abort

Every 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.

Conformance cases

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:  90

What the schema cannot check

The 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.

References

  • Every id in a selector or a by_category map is a real category — after expansion
  • Every vocabulary and every dataset argument is listed in reference_data
  • Every primitive called is pinned, at a contract version that exists
  • Every me.X read is declared in operator_profile

The expression language

  • Every condition parses against the condition grammar, every formula against the arithmetic one
  • Every path is two levels deep and uses a permitted namespace in a permitted place
  • Comparisons are type-strict: 'HB' == 14 is an error, not false
  • A definitions rule does not call another one

Completeness

  • Every {placeholder} names a dimension the same expand enumerates
  • A by_category map covers every category, or carries a default
  • Two conformance cases wherever scoring reads me.*
  • Warnings where a dimension is set that no constraint ever distinguishes on

Editorial

  • Every reason names a specific place in the rules
  • Where a document resolves an ambiguity in its source, a note says it is an interpretation
  • An advisory entry has evidence in the rulebook, not the author's opinion
  • This group is gate 1, and no machine will ever check it

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.

Versioning and status

The reference is a contract. These rules are what make it one.

draft

The only editable state. Created automatically when a published version is opened.

published

Never touched again, by anyone, for any reason. Corrections become a new version.

superseded

Still retrievable, and carries a pointer to its successor.

URLs never break

Once an API URL is published it stays. That is why the endpoints are not final yet.

Glossary

The domain is unfamiliar to most developers. These are the terms the spec assumes.

QSOA single radio contact between two stations. One row in a log.
ExchangeThe information both stations must trade for the QSO to count. Contest-specific: a serial number, a zone, sometimes a first name.
DXCCThe official counting unit for countries and territories. K = USA, DL = Germany, HB = Switzerland.
CQ / ITU zoneTwo competing numbered divisions of the world. A frequent multiplier source.
MultiplierSomething that multiplies the point total — "every new DXCC entity counts once".
DupeA 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.
LocatorMaidenhead grid square, e.g. JN47. On VHF it is the exchange, and the distance between two of them is the score.
CabrilloDe-facto standard for log submission on HF. Gives us the category vocabulary.
EDIREG1TEST, the submission format for the IARU Region 1 VHF contests. Proof that not every contest is Cabrillo.
ADIFGeneral QSO interchange format. Gives us the band and mode enums.
cty.datMaps callsign prefixes to DXCC entity, continent and zones. Continuously updated, therefore named and pinned in the document.
claimed scoreWhat a single log claims for itself. Deterministically recomputable — and the only kind of score this site deals in.
adjudicated scoreThe official result after cross-checking every participant's log. Only the sponsor can compute it. Out of scope here.

Integration examples

Example client code in several languages ships alongside the schema. not yet available