npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

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-client

Bun 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 repo option (owner/name)
  • the ARCHIVE_CLIENT_REPO environment 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 state
  • plain: one start line when a file (or an index) starts and one done (or failed, cancelled) line when it ends, with its size and time; no control character, nothing else
  • none: 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 s

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

Every 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/domain schoolEncoder noun index, with the registry and the retired ids
  • feises: every feis by id, platform, host code, series and name, with its competition and result counts and the regions its competitions read
  • dancers (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/domain personEncoder noun index), the dancers each print was published under, each dancer's schools and years
  • staff (keyed the same way): teachers and adjudicators by name (a roleEncoder noun index) and TD number: the schools' staff lists and TD links, the adjudicator register, and the seats each sat
  • comps: every competition by its feis and the feis's own code, with its series and year
  • teams: the team results, the roster teams with their members, and how they join, by event-year
  • events: 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_why

A 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 paths

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

NO 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 by rel.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_YEARS of @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 else nameKinship (a given name's variant or diminutive). Such a candidate's nameLevel is form and evidence.form lists the tolerances. A form is set aside only by the hard school fact (why: "name-form", the fact in r.fullName.refuted or r.initials.refuted where 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).

  • region is the release's own spelling, the codes dancer_years.region prints (SRO, MAO, ENG_SOUTH, ...), case folded. A region the release reads no attended year in narrows and orders nothing, and r.oireachtas.note says so.
  • year is 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/age reads it), so a regional held in December 2026 is year: 2026, read against dancer_years.year as 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. A carried or carried-back row is not read: it repeats an attended year, which is read directly. A school or cocompetitor row 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 current year 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 T3 competition that year whose address is on the floor. The release publishes no residence flag, and on the event roster every T3 event is a residence qualifier and every residence qualifier is T3, so an attended year with a T3 row 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's place_printed, atts and withdrawn: an address with a printed absence (NS, DNS, DNA, No Show, Did Not Attend, Did Not Compete, in the place cell or as an atts token) or a DNT (withdrawn DNT, or did-not-start in older releases) on any of its rows danced nothing there. A bare NP or NR is 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 majors

A 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, no DNT), whose competition publishes a feis_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 dance is 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 later

dancers.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" }