@finbheara/archive-client
v0.53.0
Published
Open a versioned release of an Irish dance results archive: download, verify, decompress and cache it by tag, keep the cache, and resolve feises and schools against it
Maintainers
Readme
@finbheara/archive-client
Open a versioned release of an Irish dance results dataset and work with it locally: download,
verify, decompress and cache it by tag, keep a user-local cache of releases, and resolve feises,
schools, competitions, registration lists, dancers, results, majors, teams, event dates, teachers
and adjudicators against one. A command line, idq, is a thin wrapper over the same calls.
Bun 1.4.2 or later (the databases open through bun:sqlite). MIT.
Install
bun add @finbheara/archive-clientBun only: the databases open through bun:sqlite. The package ships built JavaScript and its
type declarations (.d.ts), so a TypeScript project needs Bun's types ("types": ["bun"] or
["bun-types"]) and nothing else from the client: no allowImportingTsExtensions, no
preserveSymlinks. The idq command line comes with it: in a project that installed it,
bunx idq <verb>.
The version is 0.x while the API settles: a minor bump may break, so pin a minor
("@finbheara/archive-client": "0.7.x").
Configuration
The package carries no data and names no releases repository: it reads releases from a repository you name, your own.
| what | option | environment | user-local |
| --- | --- | --- | --- |
| the releases repository (owner/name) | repo | ARCHIVE_CLIENT_REPO | idq config repo <owner/name> |
| the cache root | cache | ARCHIVE_CLIENT_CACHE | |
The option wins over the environment, the environment over the user-local config. With no
repository, a call that needs the network refuses by name. With no cache set, the cache is
archive-client in your home cache directory.
A private releases repository needs credentials: gh logged in to an account that can read it,
or GITHUB_TOKEN (or GH_TOKEN) holding a token that can.
Where the releases come from
A release is a GitHub release of some repository: a MANIFEST.json, the databases
(results.sqlite.zst, majors.sqlite.zst) and the side files (jsonl, parquet). The package names
no repository. Give one, in order of precedence:
- the
repooption (owner/name) - the
ARCHIVE_CLIENT_REPOenvironment variable - the user-local config:
idq config repo <owner/name>
gh is used first (it reads your own login), and the public API over HTTPS where gh is missing
or fails (GITHUB_TOKEN or GH_TOKEN as a bearer token, needed where the repository is not public).
Open a release
import { openRelease } from "@finbheara/archive-client";
const rel = await openRelease("latest", { assets: ["results", "majors"] });
rel.tag; // the release tag
rel.cut; // the cut number the tag carries
rel.manifest; // its MANIFEST.json
rel.db("results").query("SELECT count(*) AS n FROM results").get(); // read-only handle
await rel.fetch("school-registry.jsonl"); // any other asset, on demand
rel.path("school-registry.jsonl"); // its cached file
rel.close();An older release that lacks what a call reads (a column, a table or a side file a newer release added) is REFUSED BY NAME, never read in part: the call throws, naming the feature, what the release lacks and the first cut that publishes it. What a release carries is read from its own schema and assets, never from its tag:
const c = await rel.capabilities();
c.features; // [{ feature: "dancers", state: "full" | "missing", missing: [...], since: 260, calls }]| feature | the calls | from cut |
| --- | --- | --- |
| results.level | results.query, majors.results | 215 |
| results.dancer-level | results.dancerLevel | 270 |
| dancers | dancers.resolve, search, get | 260 |
| schools | schools.resolve, a dancer question with a school | 260 |
| staff | staff.teacher, staff.adjudicator | 260 |
| majors.order | majors.competitions | 260 |
| feises, comps | feises.find, feises.series, comps.*, registrations.resolve | 265 |
| comps.dance | the dance a comp of comps.* carries: null, never refused, without it | 270 |
| results.stream, majors.stream | results.stream, results.entries | 255 |
| results.stream.competition, majors.stream.competition | the competition fields a streamed row carries (competition_line, dance_printed, dance, recalled_only, comp_flags, competitors_basis); absent, never refused, without them | 270 |
| feises.dates | feises.dates | 245 |
| feises.name | the name feises.find and feises.series answer: the release's reading, the label as printed except where it lost a letter (U+FFFD) the feis's own pages print clean; name_printed beside it is the label verbatim. Without it name is the label, never refused | 350 |
| dancers.records | dancers.records | 215 |
| dancers.records.names | the names_printed a record of dancers.records carries; absent, never refused, without it | 215 |
| dancers.years | dancers.years | 245 |
| results.stream.competitors_printed, majors.stream.competitors_printed | the competitors_printed a streamed row carries (the competed count the page printed); absent, never refused, without it | 345 |
| results.attach_basis, majors.attach_basis | the attach_basis a row of results.query, majors.results, results.stream and results.entries carries (how its dancer_id was answered, or why not); absent, never refused, without it | 345 |
| results.major, majors.major | the major a row of results.stream and results.entries carries (1 where its competition is a main event of a T1-T3 event, exactly the rows majors.sqlite holds); absent without it, and the stream filter major is refused by name without it | 345 |
| results.relative_place_err, majors.relative_place_err | the relative_place_err a row of results.stream and results.entries carries (how far below relative_place the true value may sit, 0 over an exact field); absent, never refused, without it | 350 |
| results.marks_printed, majors.marks_printed | the marks_printed a row of results.stream and results.entries carries (the marks total a row served or printed, iFeis's beside its points and a Bucharest transcript row's, one string verbatim); absent, never refused, without it | 350 |
| results.status_printed, majors.status_printed | the status_printed a row of results.stream and results.entries carries (a transcribed row's status cell as printed, verbatim, in printed order, [] where none); absent, never refused, without it | 350 |
| results.round_points_printed, majors.round_points_printed | the round_points_printed a row of results.stream and results.entries carries (the per-round points a platform served, FeisFWD's, in its slot order, a slot served nothing in null); absent, never refused, without it | 350 |
| dancers.records.birth_consistency, dancers.years.birth_consistency | the birth_consistency (0-100, floored: the percent of the dancer's, or the dancer-year's, readable rows that fit the birth window) and its denominator birth_consistency_rows a record of dancers.records and a dancer-year of dancers.years carry; absent, never refused, without it | 345 |
| dancers.records.birth_deadband, dancers.years.birth_deadband | the birth_deadband_rows a record of dancers.records and a dancer-year of dancers.years carry (the readable rows a one-year dance-up alone keeps off the birth window, outside birth_consistency and its denominator); absent, never refused, without it | 345 |
| dancers.years.printed | the printed_regions a dancer-year of dancers.years carries (every region the year printed, where region picks one); absent, never refused, without it | 341 |
| majors.tabs | majors.tabulated | 215 |
| teams | teams.results, teams.rosters, teams.resultRosters | 260 |
| teams.fields | the fields teams.results (age, gender, printed_status) and teams.rosters (dance, school_printed_as_of, a member's gender) carry; absent, never refused, without it | 266 |
| teams.recall | the competitors_printed and recalled_only a row of teams.results carries; absent, never refused, without it | 350 |
| events | events.years | 260 |
| events.published | the published (results, or attended: held, its dancers named in name-school-listings.jsonl, no place published) and listed (those lines) an event-year of events.years carries; absent, never refused, without it | 350 |
| upcoming.feises | upcoming.feises, and the next of feises.series (null with the reason without it) | 285 |
| upcoming.fields, upcoming.entrants | upcoming.fields, upcoming.entrants (285 and 290 carry the tables empty) | 291 |
| upcoming.fields.instance | the feis_instance_id upcoming.fields carries; absent, never refused, without it | 320 |
| calendar | calendar.get, calendar.list | 320 |
| calendar.expected | each calendar stage's expected; absent, never refused, without it | 320 |
| calendar.next | calendar.next | 320 |
| calendar.missing | calendar.missing | 320 |
| calendar.filed | the filed of calendar.missing; empty, never refused, without it | 320 |
| tabs, panels | tabs.query, tabs.stream; panels.query | 184 |
| feis-panels | feisPanels.query | 260 |
| staff.adjudicators | staff.adjudicators | 230 |
| schools.registry | schools.list | 260 |
| schools.prints, schools.redirects | schools.prints; schools.redirects | 184 |
| schools.lineage | schools.links, schools.line, schools.lineage | 184 |
| schools.associations | schools.associations | 215 |
A spec is pinned (the default: the user-local pin, else the latest), latest, a cut number
(42, or 42.1 for a fraction) or a tag (archive-YYYY.MM.DD-N).
EVERY FILE IS CHECKED before it lands: the downloaded bytes against the host's own sha256 digest,
and the content against the manifest (a database's decompressed bytes against
databases.<name>.sha256, a side file's decompressed bytes against sideFiles.<name>.sha256, a
parquet file against parquet.files[<name>].sha256). A side file the manifest names under
sideFiles is published zstd'd (<name>.zst, or the file it states) and lands plain under its
own name. A mismatch
refuses and leaves nothing in place. A file nothing vouches for is kept and its receipt says so
(strict: true refuses it). A database is decompressed in process, streamed, never through a
zstd binary.
The cache
One directory per tag under the cache root: the cache option, else ARCHIVE_CLIENT_CACHE,
else archive-client in your home cache directory.
<root>/<tag>/MANIFEST.json
<root>/<tag>/results.sqlite decompressed, verified
<root>/<tag>/<asset> any other file, as published
<root>/<tag>/.client.json the receipt: every file's sha256 and what verified it
<root>/<tag>/index/<name>.sqlite the per-tag indexes
<root>/PINNED.json the user-local pin
<root>/config.json the user-local config
<root>/local/<tag>/ a local build a default query answered from (below)A file already in place with no receipt (laid out by hand) is adopted where the manifest's hash vouches for it, and fetched again where it does not. A published release never changes, so a verified file is never fetched twice.
EACH DOWNLOAD WRITES A TEMP FILE OF ITS OWN beside its destination,
<root>/<tag>/.<file>.<pid>-<hex>.part, and renames it into place only once both its hashes
checked. A failure or a cancel leaves only what was there before, and two processes fetching one
release into one cache each land a whole, verified file (the last rename wins, the same bytes). A
temp file whose process is no longer running (a killed fetch's, or an index's
<name>.sqlite.building.<pid>) is removed by the next fetch or build in its directory.
Downloads: concurrent, streamed, reported
rel.fetch(...assets) (and so openRelease, fetch, index, use and update) downloads up
to concurrency files at once (the option; default DEFAULT_CONCURRENCY, 4), and each one is
hashed, decompressed and hashed again WHILE its bytes arrive, as a container pull downloads and
extracts at once: a file is verified when its last byte lands. use (and index) builds each
index as soon as its own inputs have landed, while the rest still download. The first file that
fails cancels the others, waits for them to stop, and fails the call with its own error, naming
the file; use then moves no pin. rel.fetching(assets) answers at once with each asset's own
promise (each(asset)), the whole (all) and cancel().
A Remote streams where it has bytes(repo, tag, name, signal) (gh release download -O -, or
the HTTPS body); one without it is read through download, to a temp file, then decompressed.
The progress option is told every file's and every index's state and bytes
(ProgressEvent: downloading, unpacking, verifying, indexing, then done, failed or
cancelled), and progressRenderer(mode, { line }) prints them in one of three modes:
tty: one line per file and per index, redrawn in place: name, bytes of the total, unpacked bytes, rate and stateplain: onestartline when a file (or an index) starts and onedone(orfailed,cancelled) line when it ends, with its size and time; no control character, nothing elsenone: nothing
The command line picks tty where stderr is a terminal and plain where it is not (redirected, a
CI log, an agent's capture). --progress tty|plain|none, or IDQ_PROGRESS, forces a mode. A
cache already current fetches nothing and prints nothing:
start <tag> results.sqlite.zst (179.0 MB)
start <tag> school-registry.jsonl (272.5 kB)
done <tag> school-registry.jsonl 272.5 kB in 0.9 s
done <tag> results.sqlite.zst 179.0 MB -> 1.42 GB in 9.8 s
start <tag> index schools
done <tag> index schools in 12.3 sThe release manager
import { releases } from "@finbheara/archive-client";
const m = releases();
await m.list({ remote: true }); // cached releases, sizes, files, indexes, the pin
await m.fetch("latest", { assets: ["results"] });
await m.pin("42"); // a bare openRelease() now opens it
m.unpin();
await m.add("<dir>", "archive-YYYY.MM.DD-N"); // a release built locally, under its own tag
await m.index("pinned"); // build every index
m.prune({ keep: 2, dryRun: true }); // the plan: what would go, and its bytes
await m.use("latest"); // fetch, index, pin, then prune to the newest 2
m.remove("<tag>"); // refuses the pinned release
m.dropIndexes(); // the index files only
await m.verify("<tag>"); // re-hash every cached file against its receiptEvery removal answers its plan first (dryRun), keeps the pinned release, and touches nothing
outside the cache root. A release's indexes live under its directory and go with it.
THE CACHE KEEPS ITSELF. A release is gigabytes, so a use (and so an update) that lands its
release prunes the cache once the pin has moved: it keeps the newest keep releases by tag (date,
cut, fraction), the new pin, and the pin before it, so a reader still on the previous release is
not cut off. A release fetched with --release and never pinned counts like any other. keep is
the call's own (--keep N, use(spec, { keep })), else the user-local config keep <N>, else
DEFAULT_KEEP (2), and a plain prune reads the same count. --no-prune (prune: false) skips
it, and a fetch or index that fails prunes nothing. The answer carries the plan (pruned), and
the command line prints one line per release removed and the MB freed.
The indexes
The release databases carry a primary key per table and nothing more, so lookups build side
indexes, one SQLite file per index per tag, each keyed on its builder's version and the sha256 of
every input it read. A file whose key is stale is rebuilt, never served. These ship here, and a
caller may hand its own (builders option, an IndexBuilder, with an optional async load step
whose answer its build receives):
schools: every published school print with the ids its rows carry, filed into a@finbheara/domainschoolEncodernoun index, with the registry and the retired idsfeises: every feis by id, platform, host code, series and name, with its competition and result counts and the regions its competitions readdancers(keyed on the names and domain package versions too): every published dancer print at the names package's keys (one row per print and reading, a@finbheara/domainpersonEncodernoun index), the dancers each print was published under, each dancer's schools and yearsstaff(keyed the same way): teachers and adjudicators by name (aroleEncodernoun index) and TD number: the schools' staff lists and TD links, the adjudicator register, and the seats each satcomps: every competition by its feis and the feis's own code, with its series and yearteams: the team results, the roster teams with their members, and how they join, by event-yearevents: every event-year's dates and place
Feises
await rel.feises.find({ platform: "feisweb", code: "ABC24" }); // a source or its id token
await rel.feises.find({ id: "<feis, series instance or brand id>" }); // a retired id followed
await rel.feises.find({ name: "*Spring*", year: 2024 });One answer per feis id: its name and the label as printed (name_printed), dates, location,
series ids, regions, and how many competitions, rows and captures it has. A name glob reads both.
const s = await rel.feises.series("Ardvarna Spring"); // a name, a host code ("fweb:ABC", "fweb:ABC26"), any feis id
s.arm; // "unique" | "ambiguous" | "none"
s.series_id; // the series, with s.name, s.host ("feisweb"), s.years, s.instances
s.next; // the next scheduled instance where the release knows one, else null with s.next_whyA series is asked by any id of it (a feis, instance or brand id, a retired one followed), by a
host code with or without its year (held codes <stem><yy> whose two digits are their own year,
never a host id of digits alone, which is a sequence number), or by a name (a glob, or a plain word read as *word*). Two series answer ambiguous with both.
A MAJORS EVENT IS NO FEIS SERIES. A code of the release's roster (NAIDC, either case) is never
read as a name, so it never answers a feis whose name happens to print its letters. It answers the
series the release files that event's platform captures under (the code in lowercase, its
instances the code and a four-digit year, via: "majors"), or none with why naming the event,
and s.major is its roster row ({ code, tier, name }). rel.comps.place reads it the same way: the captures of the asked event-year, held, or
no feis with why. The roster is the manifest's roster; a release written before it carries one
reads the codes its rows show are majors (a T1 to T3 event, a series filed under event-year
instances), and a code those rows never name is not known there (rosterOf(rel)).
next is the series' earliest occurrence of the feis-index dated on or after the cut's day
(rel.upcoming.feises, state upcoming), under its brand id or one retired onto it: { feis_id,
date, date_hi, occurrence_id, name_printed, hosts, captured_id, series_basis }, feis_id the
first listing's that names one. It is null with next_why where the feis-index lists none, and
on a release without upcoming_feises (feature upcoming.feises).
Schools
const a = await rel.schools.resolve("Ardvarna Academy of Irish Dance (Calgary, AB)", { region: "WCRO" });
a.arm; // "unique" | "ambiguous" | "none"
a.candidates; // [{ school_id, display, rows, by, prints, regions, places, years? }]The rungs, the first that answers wins: a school id, a retired id followed to the live one, the
print exactly as the release published it, then the print read by @finbheara/domain's school
reader at its filing key (sn3) and at its real words. ambiguous and none are answers: a
best guess is never promoted to unique. region drops only a candidate the registry places in
other regions, and keeps one it places nowhere. year reads a print at that year, on a print
rung only: where the print reaches two or more ids, the ids whose rows printed it that year are
kept (a print one school took over from another names the one that held it then), else the ids
that printed it last before that year, and where the year is before every one of them, all stay
(a.year: { asked, narrowed }, each candidate's years: { lo, hi }). A dancer question reads
its school print at its own year. A print the reader refuses (a placeholder)
answers none with the reason. An id (or a retired id followed) answers with every print its
rows were published under and their rows, so it agrees with each print lookup of that school.
Every answer carries the release tag it came from.
The schools in bulk
await rel.schools.list(); // { schools: [{ school_id, display, regions, places }] }: the registry
await rel.schools.prints({ event: "<event>", year: 2024 });
// { prints: [{ event, year, print, school_id, rows }] }
await rel.schools.redirects(); // { redirects: [{ old_id, new_id, kind, since }] }
await rel.schools.lineage(); // { edges: [{ id, direction, other, shape, year, provisional,
// decision, confidence }] }
await rel.schools.associations({ school_id: "<id>" });
// { associations: [{ at, seq, date, person, person_printed,
// person_type, credentials, caption, school_printed, school_id,
// association_type, src_type, src, candidate_school_id, candidate_rung, ... }] }prints is every (print, school id) pair the release's result rows attach in an event-year (the
print trimmed), with the rows that carry it: an event's (case-blind), a year's, both or every one.
A row attached to no school is in no pair, and a team result's school is rel.teams.results'.
lineage is each edge end as the release publishes it (an edge may carry both ends): links and
line are the graph over them. associations is the people a directory lists beside a school, as
evidence of an association, never an identity: school_id is the school the print attached to,
candidate_school_id one it reached without attaching.
How two schools are linked
const l = await rel.schools.links("ostrel-academy", "wyvern-academy", { year: 2026 });
l.arm; // "self" | "linked" | "none"
l.candidates; // [{ hops, steps: [{ at, next, link: { from, to, shape, year }, flags }], flags, confidence }]
const line = await rel.schools.line("ostrel-academy");
line.candidates; // every school on its line, nearest first, each with its pathsThe release's school lineage (a rename, an absorption, a merger, a split, or a related link of
unknown type, each dated where a year is known) read by @finbheara/domain/school-graph: every
simple path between two schools, each with its flags (relatedHops, maxYearGap, datedAfter,
undated, provisional, redirected and the rest), never a yes or a no. Pass a policy, your
reading over those flags, and the answer carries its verdict (l.policy.passes), exact however
many paths are listed. A retired id is followed to the live one.
A release that publishes the decision behind each edge (a lineage line's decision and
confidence) gives each step its link.decision and link.confidence. A path's confidence is
its weakest step's. Read it for what it is: the decision's probability that the two ids are
not one school. It is not the probability that the edge's shape or year is right, nor that the
two schools are linked at all: a decision whose alternative is two schools with no link counts
that alternative inside its confidence. An edge no recorded decision backs has a null decision: its step reads
unbacked, the path counts it in flags.unbacked, and the path's confidence is null. On a
release that publishes neither field, no step carries either, no path is unbacked, and every
path's confidence is null. A release that publishes no lineage refuses both calls by name.
Dancers
const r = await rel.dancers.resolve({
name: "Quellan, A", order: "surname-first", // initials and partial given names are first-class
school: "Ardvarna Academy", // a print or an id, through rel.schools
age: { band: "U10", year: 2026 }, // or { yob: 2016 }, or { comp: "<competition>" }
asOf: "2026-10-17", // the school as of then
population: "girls", // or "boys": only a contradiction excludes
oireachtas: { region: "SRO", year: 2026 }, // the regional the roster is for (below)
});
r.arm; // "unique" | "ambiguous" | "none"
r.candidates; // [{ dancer_id, display, scope, score, evidence: { nameLevel, age, school, population, region }, prints, ... }]
r.excluded; // the candidates a contradiction set aside (why: "age" | "population" | "region" | "school")
r.preferred; // NON-BINDING: { dancer_id, why } where the school (else the region, else recent rows) singles one out
r.initials; // an initials-only name with a school: { open, heldByDanceUp, refuted: [{ dancer_id, why, fact }], year }
r.unlinked; // rows printing a matched name that the release linked to no dancer
await rel.dancers.search({ surname: "Quellan" }); // or { like: "a* quellan" }, or { name }
// at most `limit` (default 50, 0 for all);
// `truncated` counts the rest
await rel.dancers.get(1001); // every print and school of one dancerNO NEW MATCHER. A print is read in the dancer's role by @finbheara/domain over
@finbheara/names, and a candidate's nameLevel is the finest of: nn0 to nn4 (equal at that
level of the names package: the print, the key, the folded key, the tokens in any order),
initial (one surname, one given name the other's made poorer: givenAbbreviates), middle
(a middle name one print omits: nameRelation), given-middle (one surname, and the first given
name one print uses is a later given name of the other's, Grace Zumock of Ailbhe Grace Zumock:
asked only where no finer level found anyone), spanish (see below). age is how the dancer's published birth window
meets the asked age (Age.fitsIn, overlaps): fits, overlaps, contradicts, unknown.
school is how the dancer's schools meet the asked one: same (the latest school as of asOf),
lineage (the latest school is one the release's lineage joins to the asked one), moved (an
earlier one is the asked one), none, unresolved (the print reached no school).
A SPANISH NAME, AT A SITE THAT CARRIES spanishNames. A register prints the full form, given
names and a paternal and a maternal surname (Ana Inés Varrondo Quelmar), and a results sheet
often prints one given name and the paternal surname alone (Inés Varrondo). A school file, or a
branch of one, is flagged spanishNames: true in the store, and the release's
school-registry.jsonl line carries it as spanish_names: { file, prints } (the file's own flag,
and the prints of its flagged branches). Where the name as asked finds no one and the asked school
reads that way (readsSpanishNames(candidates): a flagged file, or a print that reached it is one
of its flagged branches' prints), every print of every dancer at that school is asked under the
names package's Spanish reading (createNameKinship({ spanishNames: true })), and a print it
relates is a candidate at nameLevel: "spanish". A release with no spanish_names reads every
school the default way. A name the release already finds answers exactly as before.
THE LINEAGE is the release's school-lineage.jsonl: every line joins its id and its other,
whatever its direction (precedes, succeeds) or shape (a rename, a split, a merger, an absorb,
a relation), so a file carrying one end of an edge reads as one carrying both. The whole line
joins: every school reached through any number of edges, in either direction (the two halves of a
split are joined through their predecessor, the schools of a merger through their successor). An
earlier row at a joined school alone is no match. r.school.lineage lists the ids joined to the
asked ones. A release that publishes no lineage (feature schools.lineage) joins
nothing and its answer carries no lineage, never refused. The line is read on the same school
graph as rel.schools.line (@finbheara/domain/school-graph): untimed, every path, and as of a
year, every path none of whose steps is dated after it, a rename at any year (LINE_AS_OF).
population is how the dancer's published majors population meets the asked one: same,
contradicts, both, unknown, unasked. A dancer's population is read only from its rows at
the majors (tiers T1 to T3) in competitions whose published gender reads girls or boys
(@finbheara/domain's populationIn), whatever the competition's category (a solo championship
often publishes none), counted on the candidate (population: { girls, boys }). The results hold
individual dancers' rows only (a team is published apart), so every such row is a solo row. A feis
competition is no evidence either way (below open a feis runs its competitions mixed and may merge
its few boys into the girls' group), and a mixed competition reads mixed.
NO SILENT GUESS. Only a contradiction sets a candidate aside: a birth window that shares no birth
year with the asked age, or a dancer the release publishes in the other population's gendered
majors solo competitions and in none of the asked one's (listed in excluded, why: "age" or
"population", and a candidate contradicting both says age). A dancer published in both, or in
neither, stays. A population word that names no one population (mixed) narrows nothing, and the
answer says so (r.population.note). Outside the narrowing below, the school is evidence and
order, never a reason to drop anyone: candidates whose latest school is the asked one, or one the lineage joins to it,
come first, then those that ever danced for it, then the rest. A school row meets the asked school by its id, else by its print
(one rel.schools resolves to the asked ids), so a latest print the release
filed under no id is still same. unique is one candidate left after the age, never a best
guess promoted. Where the school singles out one candidate, preferred names it with the reason,
for a caller to weigh, and arm stays as counted. A lineage match ranks with same: one of
each singles out neither. The score orders candidates and is no
probability.
AN INITIALS-ONLY NAME (Butler, A, every given field one letter) with a school the release
resolves is narrowed by the evidence the question itself carries, where more than one candidate
is left. A candidate is REFUTED only by a hard fact (r.initials.refuted, each with its fact in
words):
school: every school row of the dancer up to the asked year reads to a school id (an id followed through the redirects, a print read byrel.schools), and none of them is the asked school or a school its line had joined by that year (lineage edges dated in or before the asked year, and a rename at any year: one school under two names). A dancer with no school row, or a print no school reads, is unknown and never refuted. A dancer that ever danced for the asked school's line is never refuted by the school. For an adult-category question (the asked band is the adult band, read by@finbheara/domain:Adult,Advanced Adult,Adult Beginner,Open Adult,Adult Masters), a row the candidate danced under 18 (at every birth year of its window, in the row's latest year) refutes nothing where the candidate is 18 or over in the asked year, however long ago the row is: many adult dancers danced as children at another school. An and-over band (O18,18 & Over,18+),Senior, a year of birth and a youth question are unchanged.age: the birth window misses the asked band even with one band's dance-up (DANCE_UP_YEARSof@finbheara/domain/age: a dancer may enter the band one year older than the dancer's own). A candidate the age set aside that the dance-up holds stays open (r.initials.heldByDanceUp).population,region: the contradictions above.
r.initials.open is every candidate no hard fact refutes. The arm is unique ONLY where exactly
one candidate is open and the age did not set it aside: the candidates the school refuted then
move to excluded (why: "school"). Otherwise the arm stays ambiguous with every candidate,
and where the school and the region single out no one, preferred names the only open candidate
with a row in the asked year or the year before.
A FULL GIVEN NAME with a school the release resolves is narrowed the same way (r.fullName, the
same shape as r.initials), with one more hard fact:
given: a candidate the name met only through an initial print (Quellan, A.) that also printed a full given name the names package relates in no way to the asked one is another dancer. An initial-only candidate, a variant or a diminutive (nameKinship) stays.
A candidate at another school is set aside only by the hard school fact, never for being elsewhere: a dancer who ever danced for the asked school's line stays open.
THE WIDENED PASSES (r.widened), only where the name as asked is left with no one, so a widened
match never answers over an exact one:
birth-conflict: a birth window the release flags (birth.conflict) is read one year wider on each side, and the candidate says so (candidate.widened).name-form: the name read in its other forms by@finbheara/names:composeNames(a print that left out a second surname, a middle name or a given name's letters, or a double-barrelled surname read as its parts) or elsenameKinship(a given name's variant or diminutive). Such a candidate'snameLevelisformandevidence.formlists the tolerances. A form is set aside only by the hard school fact (why: "name-form", the fact inr.fullName.refutedorr.initials.refutedwhere the answer was narrowed): every school row of the dancer, to the asked year, reads outside the asked school's line. A dancer with no school row, or a print no school reads, is unknown and stays.
THE REGIONAL OIREACHTAS (oireachtas: { region, year? }). A dancer enters one
residence-qualifying regional Oireachtas a year. Two regionals in one year are two dancers, the
overseas entry excepted. The release already applies that rule to each dancer-year it publishes:
dancer_years.region with region_source = "current" is the one regional the dancer attended that
year, the dancer's own overseas entries left out and a year with two regionals given no current
region. Where a year has no regional, an Australian or New Zealand national stands in. The client
reads those rows and nothing else (rel.dancers.years serves them all).
regionis the release's own spelling, the codesdancer_years.regionprints (SRO,MAO,ENG_SOUTH, ...), case folded. A region the release reads no attended year in narrows and orders nothing, andr.oireachtas.notesays so.yearis the regional's own calendar year,asOf's year where none is given. The calendar year is the only unit (an age is an age on 1 January of the competition's year, as@finbheara/domain/agereads it), so a regional held in December 2026 isyear: 2026, read againstdancer_years.yearas it is, never shifted by month.- THE DENOMINATOR is the candidate's attended (
current) dancer-years at or before the asked year, counted on the candidate (region.attended). Later years are not read. Acarriedorcarried-backrow is not read: it repeats an attended year, which is read directly. Aschoolorcocompetitorrow is not read: it is a vote of other dancers' regions, a prior for a dancer-year with no card of its own, not evidence about this one. - A
currentyear may rest on a registration alone: a no-show still says where the dancer lives. So it decides the region, and never alone a contradiction. - A DANCED REGIONAL is a row at a
T3competition that year whose address is on the floor. The release publishes no residence flag, and on the event roster everyT3event is a residence qualifier and every residence qualifier isT3, so an attended year with aT3row was read from a regional, and one without from a national standing in for one. ON THE FLOOR is the release's own absence rule, read from the row'splace_printed,attsandwithdrawn: an address with a printed absence (NS,DNS,DNA,No Show,Did Not Attend,Did Not Compete, in the place cell or as anattstoken) or a DNT (withdrawnDNT, ordid-not-startin older releases) on any of its rows danced nothing there. A bareNPorNRis a dancer placed nowhere, on the floor.
evidence.region is one of:
| word | counts | weight |
|---|---|---|
| contradicts | the asked year is attended in another region, and the dancer danced a regional that year: set aside (why: "region") | excluded |
| same | the latest attended year's region is the asked one | 2 where that year is the asked year, 1 where earlier |
| moved | the latest attended year's region is another one (an earlier year, or a national in the asked year) | 0 |
| unknown | no attended year at or before the asked year | 0 |
| unasked | no oireachtas asked | 0 |
region on a candidate is the deciding dancer-year ({ year, region, regional, attended }): the
asked year where it contradicts, else the latest attended one. moved weighs nothing: dancers
move, and no rate at which dancers move is measured, so it is said and never scored. Candidates
sort by the school first, then same before every other region word, then score. Where the
school singles out no one, preferred names the only candidate of the school's first rank (every
candidate where no school is asked) whose region is same. The asked entry's own printed location
is not asked, so an overseas entrant is asked without oireachtas. A release that publishes no
dancer-years (feature dancers.years) reads every candidate unknown, with the note.
A FEIS-LOCAL ID (FEIS_LOCAL_ID_FLOOR, 2^34, and up) is valid in its release only: a candidate and a result row
carrying one say scope: "per-release" (dancer_scope on a row), so a caller never stores one
across releases. Every answer carries the release tag.
A MAJORS ID (under the floor) is kept across releases on a best-effort basis only: a merge retires one, a split mints one, and a renumbered source page can hand one to another dancer. A caller that joins ids across releases checks the name and the rows it expects.
Results and the majors
await rel.results.query({ dancer_id: 1001 }); // a dancer's rows, oldest first
await rel.results.query({ feis: "<feis id>", year: 2025 });
await rel.results.query({ competition: "<competition>" });
await rel.results.query({ school_id: "<school id>", year: 2025, limit: 100 });
await rel.majors.events(); // every major and its years
await rel.majors.competitions({ event: "<event>", year: 2023 });
await rel.majors.results({ dancer_id: 1001 }); // a dancer's majorsA query keeps at most limit rows (default 500; 0 is no limit) and truncated says how many
more there were. Check it is 0 before reading the rows as the whole answer.
A row carries the prints and the ids that stand for them: the address, the competition's title
and age, the place, the name and school as printed with the dancer and school ids. The majors are
read from majors.sqlite, fetched on first use.
What level a dancer dances
await rel.results.dancerLevel({ dancer_id: 1001, year: 2025 });
// { level: 200, grades: { advancement_range: { lo: 300, hi: 400 },
// level_range: { lo: 200, hi: 400 }, oireachtas_level: true } }
await rel.results.dancerLevel({ dancer_id: 1002 }); // its latest year: { level: 600 }No single number is a grades dancer's level. Grades are won PER DANCE: a dancer can be Novice in
the reel and Prizewinner in the slip jig at once. Only a championship (600 preliminary, 700 open)
is a level of the dancer. A competition's feis_level is the competition's rung. A set, a set
dance, a treble reel or a special prints a rung that its entrants need not hold. So the answer
reads:
- the year: the competition's calendar year. When it is omitted, the latest year in which the dancer has a rated feis row.
- a rated row: a feis row (tier
T4, a platform's) of the dancer, on the floor (no printed absence, noDNT), whose competition publishes afeis_level. - a championship row: a rated row at 600 or 700. A 700 whose title or level field also names
the preliminary (
PC and OC,Prelim and Champ) joins the two, and it reads 600 here. - a graded row: a rated row at 100 to 500 whose
danceis one of the six graded dances (reel, light jig, single jig, slip jig, treble jig, hornpipe) and whose title, level or dance field names no special (special,spcl,trophy). Sets, set dances, treble reels and specials are never read.
| field | meaning |
| --- | --- |
| level | for a championship dancer (any championship row that year), the LOWEST championship rung danced that year. Otherwise the lowest graded rung danced that year. Null where the year holds neither |
| grades | present only for a grades dancer: no championship row that year, and at least one graded row |
| grades.advancement_range | { lo, hi }, the lowest and highest rung over the reel, slip jig, treble jig and hornpipe, the dances a dancer advances in. Null where none of them was danced |
| grades.level_range | { lo, hi } over all six graded dances |
| grades.oireachtas_level | true where the dancer danced a championship solo at a regional Oireachtas that year: a row on the floor at a tier T3 event, the same calendar year, whose competition's category is Solo. A grade championship that the release files as Solo counts as well (one regional, in one year, prints ten) |
Only feis rows set the level. A dancer with majors rows and no feis row in the year (tier T4) gets level null, whatever they danced at the majors.
A dance that was not danced that year takes no part: each range covers what was danced. An adult
grade row is read like any other, since the adult ladder also grades per dance. A release older
than the published dance column is refused by name (results.dancer-level).
Each call reads the dancer's rows once. The release does not index the dancer id, so one call
scans the results table, as results.query does (about half a second on a full release).
Every row, streamed
for await (const row of rel.results.stream({ scope: "majors" })) { ... }
for await (const row of rel.results.stream({
scope: "platform", // "majors" | "transcribed" | "platform" | "all"
primary: true, // only rows no other source stands in for
year: [2015, 2024], // a year or an inclusive range
feis_level: [600, 700], // also: event (one or several), tier, category, dancer_id, school_id
})) { ... }
for await (const entry of rel.results.entries({ scope: "all", event: "<event>" })) { ... }A stream yields one row at a time and never holds the table in memory. A row carries its
competition beside its own fields: competition, event, year, tier, comp_no, title,
category, age, age_printed, gender, competitors, feis_level, level_printed,
date_printed, source, round, and at a feis its feis_id, feis_date_lo and
feis_date_hi. Its own: address, section, card, place, place_printed, tie,
relative_place, atts, withdrawn, qualifier, name_printed, name, school_printed,
school_loc, school_id, dancer_id, dancer_scope, yob, yob_printed, points_printed,
platform_dancer_id, primary_source, and its key in the release, file and line (one
(file, line) is one row, where an address may carry one row per section). Rows come in the
release's own order, a competition's rows together.
THE PER-ROUND POINTS A PLATFORM SERVED, where the release carries them
(rel.capability("results.round_points_printed")): round_points_printed, FeisFWD's
served round points in its own slot order, each value a string as served and a slot it served
nothing in null ([null, null, "275"]). No slot is mapped onto a round and nothing is summed;
null where no slot serves a value and on every other source's row. points_printed stays the one
all-round figure a page prints.
THE MARKS TOTAL A PLATFORM SERVED, where the release carries it
(rel.capability("results.marks_printed")): marks_printed, iFeis's served marks beside its
points, one string verbatim ("462.5"). It names no round and no judge, so it is never split
or summed, and the places follow the points, not the marks. A Bucharest transcript row (2016-2019)
carries its printed mark the same way, one adjudicator's mark and no points ("80"). Null where
the row carries none and on every other source's row.
THE STATUS CELL AS PRINTED, where the release carries it
(rel.capability("results.status_printed")): status_printed, a transcribed row's status cell
verbatim, in printed order (["Not Recalled"], ["-"], ["*"], ["WQ"]). atts is the
reading of the same cell, so a cell it reads as nothing (-, Not Recalled) is told apart from
a blank cell here. [] where the row prints none, and on every platform row.
THE COMPETITION FIELDS, where the release carries them (rel.capability("results.stream.competition"),
"majors.stream.competition" for scope majors), are on every row, null meaning no value:
| field | what it is |
| --- | --- |
| competition_line | the competition's own line in file: (file, competition_line) is one competition of the release exactly |
| dance_printed | the dance field as the page printed it. A platform that prints no title prints only this, so a filter on the kind of competition reads title and dance_printed together |
| dance | the competition's one dance as the release reads it (reel), null where it reads none |
| recalled_only | true where the page printed the recalled dancers only |
| comp_flags | what the page printed about itself, int-packed, one bit a flag (bit 10: recalled only). The release's DATA-DICTIONARY.md carries the register of every bit, and a published bit never moves |
| competitors_basis | what competitors stands on: printed (the page printed a competed count), counted (the page printed the whole field, its cards counted), net-entered (FeisWeb: the entered count less the withdrawn one, who danced), awards (FeisWeb: the fewest dancers its Awards To Top N Finishers ( p% ) line is a share of, who danced), entered or registered (only the top was printed: the page's entered or registered count, a REGISTRATION figure holding the no-shows), estimated (a majors sheet with no count that does not show its whole field: the recall rule over a recall-only sheet (twice the recalled, plus or minus 1, or the recalled where fewer than 10), else the dancers it printed, a FLOOR; never a count, so leave it out where a real count is needed), placed (the page's count fell below the last place of its placed run, so the field is that place: a dancer placed N-th proves N danced; a competed count), null where there is no number |
A release without them streams every row with the six fields ABSENT, never null.
competitors_printed (feature results.stream.competitors_printed, majors.stream.competitors_printed for scope majors) is the competed count the page itself printed, null where it printed none. competitors stays the best value. A release without it streams the field ABSENT.
attach_basis (feature results.attach_basis, majors.attach_basis for scope majors and rel.majors.results) says how a row's dancer_id was answered, or why it was not, in one integer: attach_basis & 63 is the feis attach's outcome for the row's address (0 on a majors row, 1 a platform row it carded no card for, else attached how or the gate that refused it), attach_basis & ~63 the bits of where the id came from (none where dancer_id is null). unpackAttachBasis(n) decodes it, { outcome: "birth-window", via: ["feis-local"] }, off the register ATTACH_OUTCOMES and ATTACH_VIA the package exports, and the release's DATA-DICTIONARY.md says what each value means. rel.results.query and rel.majors.results rows carry it too. A release without it returns the field ABSENT.
major (feature results.major, majors.major for scope majors) is 1 where the row's competition is a MAJOR'S MAIN EVENT: a solo championship (open or adult) or an either/or traditional set at a T1-T3 event, the competition a dancer came to the major for. A grade, a preliminary competition (a prelim-only one, or a prelim re-ranking of the open's field), a special, a traditional set co-entered with the championship, a mini championship beside its age group's full one and every feis row are 0. majors.sqlite holds exactly the 1s, so a reader of the full results set filters rel.results.stream({ scope: "all", major: true }) to read what the majors stream reads. tier stays the event's. A release without the column returns the field absent and refuses the major filter by name (needsRelease).
relative_place is read over the LARGEST field the release admits for the competition, so first place is 100 over any field and a recalled dancer's value stays high on a recall list, and relative_place_err (feature results.relative_place_err, majors.relative_place_err) says how far below it the true value may sit: the true value lies in [relative_place - relative_place_err, relative_place]. It is 0 where the field is a counted or printed competed count (and on a first place, and over a registration count every entrant of which printed), larger over a registration count (its no-shows), a recall list (the field its cards admit) or a feis page printing no count (a field of up to 150). A consumer wanting only exact values filters relative_place_err = 0. A release without it returns the field absent.
THE COMPETITION'S KEY is competition (<event-year>.<comp> at a major, <platform>:<host>.<comp>
at a feis), which holds across releases. Inside one release (file, competition_line) is exact: a
competition captured twice (two files of one platform) carries one competition in both and two
(file, competition_line) keys, and primary_source names the copy another source stands in for.
The scopes: majors reads the majors (tiers T1 to T3), transcribed the transcribed rows of
every tier, platform every row a results platform published, all every row.
AN ENTRY is one row per address, the row the card stands for: a card printed in several sections
(a recall order, the final, a single round's solo placing, a qualifying heat) keeps the final's
row, then the sectionless page's, the recall order's, the standings', the unplaced row, a heat's
and last a solo round's (entryRank), and two rows of one rank keep the later. recall_place is
the place the address's recall row carries.
Bulk reads
await rel.dancers.records(); // every dancer: display, appearances, birth_lo, birth_hi,
// birth_source, birth_conflict, genders, scope,
// names_printed (dancers.records.names),
// birth_consistency, birth_consistency_rows
// (dancers.records.birth_consistency),
// birth_deadband_rows (dancers.records.birth_deadband)
await rel.dancers.records({ ids: [1001] });
await rel.dancers.years({ ids: [1001], year: 2025 }); // region, region_source, nationals, geohash
// printed_regions (dancers.years.printed),
// birth_consistency, birth_consistency_rows
// (dancers.years.birth_consistency),
// birth_deadband_rows (dancers.years.birth_deadband)
await rel.feises.dates(); // every feis id with its first and last day
await rel.majors.tabulated({ min_true_round: 3 }); // the majors addresses marked to round 3 or laterdancers.records covers EVERY dancer the release gives an id: a majors dancer, a feis row attached
to one, and a feis-local dancer (scope: "per-release", valid in its release only). It is not the
majors' dancers only. A caller who wants those keeps the dancer_ids a scope: "majors" stream
carries. names_printed is every distinct name the dancer's rows print, sorted, over every row of
the dancer, feis rows included.
Marks sheets and panels
const s = await rel.tabs.query({ competition: "<competition>" }); // or { address }, and a { round }
s.rows; // [{ address, competition, card, line, event, year, tier, round, true_round, seat_index,
// section, panel_size, column_order, drop_rule, the judge (name, print, TD number,
// id, unmatched reason), mark, mark_printed, place, place_printed, tie, points,
// points_printed }]
for await (const t of rel.tabs.stream({ tier: ["T1", "T2", "T3"], year: [2015, 2025] })) ...
await rel.panels.query({ competition: "<competition>" }); // { seats: [{ competition, panel, round,
// seat_index, address, the judge (print, TD number, id, unmatched reason) }] }
await rel.feisPanels.query({ address: "fweb:<code>.<comp>" }); // { seats: [{ address, platform, round,
// seat_index, panel_size, panel_kind, the judge (print, state printed, TD number, id,
// unmatched reason, basis) }] }A marks sheet is a row per card, round and seat, with the seat's mark, place and points as read
beside their prints, from results.sqlite (every tabulated sheet, the T4 events' too). The stream
takes an event (or several, case-blind), a year or an inclusive range, tiers, competitions, or
nothing (every cell), and yields one competition's rows together. A sheet's event, year and
tier are its competition's: a solo competition's from the results, a team competition's from the
team results (with the tier the release gives that event-year's solo competitions). A sheet the
release places in neither carries nulls, and a filter on them never reaches it. panels and
feisPanels with nothing named answer every seat. round is rx where one panel sat every round.
A seat carries its judge's TD number where the release linked one, else a judge id, else the
reason it is linked to neither (TabRow, PanelSeat and FeisPanelSeat name the fields).
Teams and event-years
const r = await rel.teams.results({ event: "<event>", year: 2024 });
r.results; // [{ address, competition, event, year, comp_no, title, category, hands, age, gender,
// letter, label, school_printed, school_id, place, place_printed, printed_status, tie,
// competitors, competitors_printed, recalled_only, atts, ... }]
const t = await rel.teams.rosters({ event: "<event>", year: 2024 });
t.teams; // [{ team, kind, event, year, comp_no, title, team_index, seq, school_id, school_printed,
// school_printed_as_of, dance, members: [{ seq, name_printed, name, yob, gender, dancer_id, dancer_scope }] }]
const j = await rel.teams.resultRosters({ event: "<event>", year: 2024 });
j.joins; // [{ team, result_address, join_kind, via_split, roster_index, result_letter, ... }]
const e = await rel.events.years({ event: "<event>" });
e.years; // [{ event, year, date_lo, date_hi, resolution, source, date_printed, place, place_printed,
// published, listed }]A query names an event (case-blind), a year, both, or neither (every row). Everything comes from
ONE release: a roster member's dancer_id is the same release's id as the results', so a caller
never joins team records of one release to dancer ids of another. A roster is a sheet of a school's
teams (kind: "rosterTeam") or a result card's own line-up (lineup). join_kind says how a
roster team met the results (exact, card, branch) or why it did not (no-result,
no-school, no-roster, multi-team-unjoined), and result_address is null where none joined.
An event-year's dates are a window (date_lo, date_hi) with how fine it is (resolution:
day, month, calendar) and what it is read from (source), and place is a place-tree node
where the release knows one. published is results where the event-year publishes result rows
and attended where it was held and only a listing page (a recall dump) names its dancers: no
competition or place is published, listed counts the event-year's lines of
name-school-listings.jsonl, and its calendar row reads terminal attended. A team result's age and gender are the competition's band and sex
as the release reads them, printed_status the row's printed status cell verbatim. Its
competitors is the field: the printed count, except on a sheet that printed the recalled teams
only (recalled_only), where it is the entries the event-year's programme lists, never fewer than
the rows, and null with no list; competitors_printed is the printed count as printed. A roster
team's school_id is its members' own school that year. Its school_printed is the school a
registration system held for it on school_printed_as_of, often years later, so a rename or a move
since shows there and it says nothing about the school that year. Its dance is the dance it was
entered for, and a member's gender is printed only as M:
the source withholds every other print, so null says nothing about a dancer's sex. A field the
source left empty is null.
THOSE NEWER FIELDS ARE ABSENT, NEVER NULL, ON A RELEASE THAT DOES NOT CARRY THEM: a team result's
age, gender, printed_status, a roster team's school_printed_as_of, dance and a member's
gender (feature teams.fields, see the table above). Absent means "this release does not carry it", null
means the release carries the field and has no value. teams.results and teams.rosters answer on
every release the teams feature names, and (await rel.capability("teams.fields")).state says which shape
a release has. A team result's competitors_printed and recalled_only are feature teams.recall,
absent the same way. The rows are read once per
release into the teams and events indexes.
Upcoming feiseanna and their competitor lists
const o = await rel.upcoming.feises({ state: "upcoming", from: "2026-11-01" });
o.feises; // [{ occurrence_id, occurrence_ids, name_printed, date_lo, date_hi, year, hosts,
// feis_brand_id, feis_instance_id, series_basis, brand_candidates, captured_id, state }]
const f = await rel.upcoming.fields({ state: "pending" }); // state, feis, field_id, from, to
f.fields; // [{ field_id, feis_id, source, host_id, competition_code, count_only, occurrence_id,
// name_printed, date_lo, date_hi, year, feis_brand_id, feis_instance_id, comp,
// state, flags,
// results_competitions, results_feis_ids, held_out, merged_into, version, versions,
// captured_date, sha256,
// limits, entrants, stated_rows, printed_total }]
const e = await rel.upcoming.entrants({ field_id: "fweb:ABC26/101" }); // version: an older capture
e.entrants; // [{ name_printed, school_printed, school_id, yob, arm, candidates, flags,
// competitions, field_ids, captured_id, row_id, ... }]
const d = await rel.upcoming.departures({ reason: ["results", "merged"] }); // feis, field_id, from, to
d.departures; // [{ field_id, feis_id, occurrence_id, feis_brand_id, competition_code, date_lo, date_hi,
// reason, competitions, feis_ids, merged_into, held_out, last_version }]upcoming.feises is every occurrence of the cut's feis-index the release holds no results of:
upcoming (dated on or after the cut's day) or no-results (before it), each with every
platform's or calendar's listing (hosts) and its feis series where one was named (series_basis
says how; none leaves feis_brand_id null, the unjoined ones). An occurrence whose series one
filed brand answers is given its feis_instance_id when it is ANNOUNCED, before any capture of it
is held (series_basis announced on every later release), and its captures land on that
instance when they arrive, so a field's feis_instance_id is the id its results will carry.
A FIELD is one competition a competitor list names, <feis_id>/<code>, the same id on every cut. A later capture of the host is a new version
(a capture id; versions lists them), never a new field. state says how it stands on the cut:
pending (on the list), results (the release holds its results, results_competitions the
join), merged (the host registered it apart and ran it as one competition with another field: its
results are that field's, and merged_into names the survivor, the first by field_id, which
reads results), held-out (the results exist and the release holds them out: the platform door's knockout
lists the competition, a team, music or arts event, and held_out names each competition with the
knockout's reason, so there is nothing to score against), passed (its first day is before the
cut's day and no result is held) or withdrawn (the host's newest capture no longer lists it). comp is the competition reading of its code (arm,
level, feis_level, age, population, kind, title, why), null on a count list. A
QuickFeis registrant list's field reads it off the list's own by-event label (source
registration-list, title the label as printed), and flags holds the cross-check of the
list's two documents (no-event-section, event-rows-differ); [] on any other field.
limits names what the capture cannot show (registered-not-danced, paid-in-full-only, ...).
THE DEPARTURE RECORD IS THE STATE. Every state but pending is how the field left the list, and
upcoming.departures answers one record per departed field: its reason, what it became
(competitions, the results join; merged_into; held_out) and last_version. It is
re-derived on every cut from what the release holds, so a field that left keeps its record on every
later cut while the corpus keeps its capture, and a passed field whose results arrive later reads
results on that cut: a consumer that skips a release still sees it.
upcoming.entrants answers a field's rows in one version, its current one by default. A
registrant carries the resolver's arm and candidates, NEVER A dancer_id: a list shows who
registered, not who danced, and a unique row's one candidate is read with its flags
(initial-only: a FeisWeb print is a surname and an initial; name-hidden: QuickFeis printed
(Name Hidden), so the row has no name and no answer). A QuickFeis registrant list
(transcripts/*.registrationlist.jsonl) is one field per printed code, each registrant a row as
printed. count_only marks a field read from an older registration-by-school page of
the corpus entrants/ part: its rows are schools' counts (level_counts), with no names, and a
host with a registrant list publishes no such field. An unknown field or version answers no rows and says
why. On a release without the tables each call refuses by name.
The feis calendar
const g = await rel.calendar.get("plf.2026"); // an instance, an event-year (`<event><year>`), a retired
// id, a capture (`fweb:PLF26`) or an occurrence id
g.via; // "instance" | "formerly" | "feis_id" | "occurrence_id" | null
g.entries; // [{ feis_instance_id, feis_brand_id, tier, name_printed, date_lo, date_hi, date_source,
// occurrence_ids, feis_ids, stages: { announced, syllabus, entrants, run, results },
// terminal, carried, formerly }]
g.entries[0].stages.results.expected;
// { date, basis, n, platform } or null: "announced" | "brand" | "platform" |
// "pooled" | "roster-window" | "unmeasured"
const l = await rel.calendar.list({ stage: "entrants", state: "observed", from: "2026-11-01" }