@finbheara/archive-client 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -216,11 +216,12 @@ const r = await rel.dancers.resolve({
216
216
  age: { band: "U10", year: 2026 }, // or { yob: 2016 }, or { comp: "<competition>" }
217
217
  asOf: "2026-10-17", // the school as of then
218
218
  population: "girls", // or "boys": only a contradiction excludes
219
+ oireachtas: { region: "SRO", year: 2026 }, // the regional the roster is for (below)
219
220
  });
220
221
  r.arm; // "unique" | "ambiguous" | "none"
221
- r.candidates; // [{ dancer_id, display, scope, score, evidence: { nameLevel, age, school }, prints, ... }]
222
- r.excluded; // the candidates a contradiction set aside (why: "age" | "population")
223
- r.preferred; // NON-BINDING: { dancer_id, why } where the school singles one out; never moves arm
222
+ r.candidates; // [{ dancer_id, display, scope, score, evidence: { nameLevel, age, school, population, region }, prints, ... }]
223
+ r.excluded; // the candidates a contradiction set aside (why: "age" | "population" | "region")
224
+ r.preferred; // NON-BINDING: { dancer_id, why } where the school (else the region) singles one out; never moves arm
224
225
  r.unlinked; // rows printing a matched name that the release linked to no dancer
225
226
 
226
227
  await rel.dancers.search({ surname: "Quellan" }); // or { like: "a* quellan" }, or { name }
@@ -234,7 +235,17 @@ level of the names package: the print, the key, the folded key, the tokens in an
234
235
  (a middle name one print omits: `nameRelation`). `age` is how the dancer's published birth window
235
236
  meets the asked age (`Age.fitsIn`, `overlaps`): `fits`, `overlaps`, `contradicts`, `unknown`.
236
237
  `school` is how the dancer's schools meet the asked one: `same` (the latest school as of `asOf`),
237
- `moved` (an earlier one), `none`, `unresolved` (the print reached no school).
238
+ `lineage` (the latest school is one the release's lineage joins to the asked one), `moved` (an
239
+ earlier one is the asked one), `none`, `unresolved` (the print reached no school).
240
+
241
+ THE LINEAGE is the release's `school-lineage.jsonl`: every line joins its `id` and its `other`,
242
+ whatever its direction (`precedes`, `succeeds`) or shape (a rename, a split, a merger, an absorb,
243
+ a relation), so a file carrying one end of an edge reads as one carrying both. The whole line
244
+ joins: every school reached through any number of edges, in either direction (the two halves of a
245
+ split are joined through their predecessor, the schools of a merger through their successor). An
246
+ earlier row at a joined school alone is no match. `r.school.lineage` lists the ids joined to the
247
+ asked ones. A release that publishes no lineage (feature `schools.lineage`) joins
248
+ nothing and its answer carries no `lineage`, never refused.
238
249
 
239
250
  `population` is how the dancer's published majors population meets the asked one: `same`,
240
251
  `contradicts`, `both`, `unknown`, `unasked`. A dancer's population is read only from its rows at
@@ -251,14 +262,66 @@ majors solo competitions and in none of the asked one's (listed in `excluded`, `
251
262
  `"population"`, and a candidate contradicting both says `age`). A dancer published in both, or in
252
263
  neither, stays. A population word that names no one population (`mixed`) narrows nothing, and the
253
264
  answer says so (`r.population.note`). The school is evidence and order, never a reason
254
- to drop anyone: candidates whose latest school is the asked one come first, then those that ever
255
- danced for it, then the rest. A school row meets the asked school by its id, else by its print
265
+ to drop anyone: candidates whose latest school is the asked one, or one the lineage joins to it,
266
+ 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
256
267
  (one `rel.schools` resolves to the asked ids), so a latest print the release
257
268
  filed under no id is still `same`. `unique` is one candidate left after the age, never a best
258
269
  guess promoted. Where the school singles out one candidate, `preferred` names it with the reason,
259
- for a caller to weigh, and `arm` stays as counted. The `score` orders candidates and is no
270
+ for a caller to weigh, and `arm` stays as counted. A `lineage` match ranks with `same`: one of
271
+ each singles out neither. The `score` orders candidates and is no
260
272
  probability.
261
273
 
274
+ THE REGIONAL OIREACHTAS (`oireachtas: { region, year? }`). A dancer enters one
275
+ residence-qualifying regional Oireachtas a year. Two regionals in one year are two dancers, the
276
+ overseas entry excepted. The release already applies that rule to each dancer-year it publishes:
277
+ `dancer_years.region` with `region_source = "current"` is the one regional the dancer attended that
278
+ year, the dancer's own overseas entries left out and a year with two regionals given no `current`
279
+ region. Where a year has no regional, an Australian or New Zealand national stands in. The client
280
+ reads those rows and nothing else (`rel.dancers.years` serves them all).
281
+
282
+ - `region` is the release's own spelling, the codes `dancer_years.region` prints (`SRO`, `MAO`,
283
+ `ENG_SOUTH`, ...), case folded. A region the release reads no attended year in narrows and orders
284
+ nothing, and `r.oireachtas.note` says so.
285
+ - `year` is the regional's own calendar year, `asOf`'s year where none is given. The calendar year
286
+ is the only unit (an age is an age on 1 January of the competition's year, as
287
+ `@finbheara/domain/age` reads it), so a regional held in December 2026 is `year: 2026`, read
288
+ against `dancer_years.year` as it is, never shifted by month.
289
+ - THE DENOMINATOR is the candidate's attended (`current`) dancer-years at or before the asked
290
+ year, counted on the candidate (`region.attended`). Later years are not read. A `carried` or
291
+ `carried-back` row is not read: it repeats an attended year, which is read directly. A `school`
292
+ or `cocompetitor` row is not read: it is a vote of other dancers' regions, a prior for a
293
+ dancer-year with no card of its own, not evidence about this one.
294
+ - A `current` year may rest on a registration alone: a no-show still says where the dancer lives.
295
+ So it decides the region, and never alone a contradiction.
296
+ - A DANCED REGIONAL is a row at a `T3` competition that year whose address is on the floor. The
297
+ release publishes no residence flag, and on the event roster every `T3` event is a residence
298
+ qualifier and every residence qualifier is `T3`, so an attended year with a `T3` row was read from
299
+ a regional, and one without from a national standing in for one. ON THE FLOOR is the release's
300
+ own absence rule, read from the row's `place_printed`, `atts` and `withdrawn`: an address with a
301
+ printed absence (`NS`, `DNS`, `DNA`, `No Show`, `Did Not Attend`, `Did Not Compete`, in the place
302
+ cell or as an `atts` token) or a DNT (`withdrawn` `DNT`, or `did-not-start` in older releases) on
303
+ any of its rows danced nothing there. A bare `NP` or `NR` is a dancer placed nowhere, on the
304
+ floor.
305
+
306
+ `evidence.region` is one of:
307
+
308
+ | word | counts | weight |
309
+ |---|---|---|
310
+ | `contradicts` | the asked year is attended in another region, and the dancer danced a regional that year: set aside (`why: "region"`) | excluded |
311
+ | `same` | the latest attended year's region is the asked one | 2 where that year is the asked year, 1 where earlier |
312
+ | `moved` | the latest attended year's region is another one (an earlier year, or a national in the asked year) | 0 |
313
+ | `unknown` | no attended year at or before the asked year | 0 |
314
+ | `unasked` | no `oireachtas` asked | 0 |
315
+
316
+ `region` on a candidate is the deciding dancer-year (`{ year, region, regional, attended }`): the
317
+ asked year where it contradicts, else the latest attended one. `moved` weighs nothing: dancers
318
+ move, and no rate at which dancers move is measured, so it is said and never scored. Candidates
319
+ sort by the school first, then `same` before every other region word, then `score`. Where the
320
+ school singles out no one, `preferred` names the only candidate of the school's first rank (every
321
+ candidate where no school is asked) whose region is `same`. The asked entry's own printed location
322
+ is not asked, so an overseas entrant is asked without `oireachtas`. A release that publishes no
323
+ dancer-years (feature `dancers.years`) reads every candidate `unknown`, with the note.
324
+
262
325
  A FEIS-LOCAL ID (`FEIS_LOCAL_ID_FLOOR`, 2^34, and up) is valid in its release only: a candidate and a result row
263
326
  carrying one say `scope: "per-release"` (`dancer_scope` on a row), so a caller never stores one
264
327
  across releases. Every answer carries the release tag.
@@ -474,7 +537,8 @@ feis's championships), the band is read from the row's own titles (`titles`, by
474
537
  domain package's title reader, the youngest again, and `age.from` says `title`. Nothing else
475
538
  stands in for a band. The dancer resolver is the release's own (`rel.dancers`, above), or the
476
539
  `dancerResolver` option. With `dancerResolver: null`, each row's dancer answers `unavailable`
477
- and the competitions still resolve.
540
+ and the competitions still resolve. A row's `oireachtas`, where the list is a regional's roster,
541
+ is passed to the dancer resolver as asked (`rel.dancers`, above).
478
542
 
479
543
  ## The command line
480
544
 
@@ -495,7 +559,7 @@ and the competitions still resolve.
495
559
  idq dancer [--release <spec>] <name> | --surname <s> | --like <glob> | --id <dancer_id>
496
560
  [--order surname-first|given-first] [--school <print|id>]
497
561
  [--yob <y> | --band <band> [--year <y>] | --comp <competition>] [--as-of <date>]
498
- [--population girls|boys] [--rows]
562
+ [--population girls|boys] [--oireachtas <region>[:<year>]] [--rows]
499
563
  idq results [--release <spec>] [--dancer <id>] [--feis <id>] [--comp <c>] [--school <s>] [--year <y>] [--limit <n>]
500
564
  idq majors [--release <spec>] [--event <e>] [--year <y>] [--comp <c>] [--dancer <id>] [--rows]
501
565
  idq stream [--release <spec>] --scope majors|transcribed|platform|all [--entries] [--event <e,...>]
@@ -514,6 +578,7 @@ and the competitions still resolve.
514
578
  idq syllabus [--release <spec>] --for <series> [year]
515
579
  idq resolve comp [<feis> <code>] [--year y] [--title t]
516
580
  idq resolve dancer [<name>] [--school s] [--comp <feis>/<code> | --yob y | --band b --year y]
581
+ [--oireachtas <region>[:<year>]]
517
582
  idq resolve registrations
518
583
 
519
584
  `stream` prints one JSON line per row (or entry), as it reads them. `dancer` prints the results of the one dancer where the answer is unique (every candidate's with
@@ -521,7 +586,8 @@ and the competitions still resolve.
521
586
 
522
587
  `resolve` speaks JSON lines: one answer per line out, and with no positional question the
523
588
  questions are JSON lines on stdin, one per line (`{feis, code, year?, title?}`,
524
- `{name, school?, age?, population?}`, `{name, school?, codes, feis, year?, population?}`). A line that is no JSON object,
589
+ `{name, school?, age?, population?, oireachtas?}`,
590
+ `{name, school?, codes, feis, year?, population?, oireachtas?}`). A line that is no JSON object,
525
591
  or no question of its kind, answers `{tag, line, error}` and the other lines still answer. The
526
592
  exit is non-zero where any line erred. A dancer's age may be given as a comp
527
593
  (`{"comp": "<feis>/<code>"}`, or `--comp` on the command line): `rel.registrations.dancer` reads
@@ -106,6 +106,239 @@ function nameLevel2(q, c) {
106
106
  return null;
107
107
  }
108
108
 
109
+ import { readFileSync } from "fs";
110
+ var SCHEMA = `
111
+ CREATE TABLE feis(
112
+ feis_id TEXT PRIMARY KEY, source TEXT NOT NULL, token TEXT NOT NULL, host_id TEXT NOT NULL,
113
+ name TEXT NOT NULL, date_lo TEXT, date_hi TEXT, date_printed TEXT, year INTEGER,
114
+ location TEXT, instance_id TEXT, brand_id TEXT, regions TEXT NOT NULL,
115
+ competitions INTEGER NOT NULL, rows INTEGER NOT NULL, captures INTEGER NOT NULL
116
+ );
117
+ CREATE TABLE redirect(old_id TEXT PRIMARY KEY, new_id TEXT NOT NULL, kind TEXT NOT NULL);
118
+ `;
119
+ var INDEXES = [
120
+ "CREATE INDEX feis_host ON feis(host_id COLLATE NOCASE)",
121
+ "CREATE INDEX feis_token ON feis(token)",
122
+ "CREATE INDEX feis_source ON feis(source)",
123
+ "CREATE INDEX feis_instance ON feis(instance_id)",
124
+ "CREATE INDEX feis_brand ON feis(brand_id)",
125
+ "CREATE INDEX feis_year ON feis(year)"
126
+ ];
127
+ function readJsonl(path) {
128
+ return readFileSync(path, "utf8").split(`
129
+ `).filter((l) => l.trim() !== "").map((l) => JSON.parse(l));
130
+ }
131
+ var feisesIndex = {
132
+ name: "feises",
133
+ version: "2",
134
+ inputs: ["results", "feis-redirects.jsonl"],
135
+ says: "every feis by id, platform, code, series and name, with its counts and regions",
136
+ build(rel, out) {
137
+ out.run(SCHEMA);
138
+ const src = rel.db("results");
139
+ const counts = new Map;
140
+ for (const r of src.query("SELECT event, count(*) AS n, json_group_array(DISTINCT feis_region) AS regions FROM competitions GROUP BY event").all())
141
+ counts.set(r.event, {
142
+ comps: r.n,
143
+ regions: new Set(JSON.parse(r.regions).filter((x) => x !== null))
144
+ });
145
+ const rows = new Map;
146
+ for (const r of src.query("SELECT c.event AS event, sum(r.n) AS n FROM (SELECT competition, count(*) AS n FROM results GROUP BY competition) r JOIN (SELECT DISTINCT competition, event FROM competitions) c ON c.competition = r.competition GROUP BY c.event").all())
147
+ rows.set(r.event, r.n);
148
+ const put = out.prepare("INSERT OR IGNORE INTO feis VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
149
+ const captures = new Map;
150
+ const feisRows = src.query("SELECT feis_id, source, host_id, name_printed, date_lo, date_hi, date_printed, year, location_printed, feis_instance_id, feis_brand_id FROM feises ORDER BY file, line").all();
151
+ for (const f of feisRows)
152
+ captures.set(f.feis_id, (captures.get(f.feis_id) ?? 0) + 1);
153
+ for (const f of feisRows) {
154
+ const c = counts.get(f.feis_id);
155
+ const colon = f.feis_id.indexOf(":");
156
+ put.run(f.feis_id, f.source, colon === -1 ? f.source : f.feis_id.slice(0, colon), f.host_id, f.name_printed, f.date_lo, f.date_hi, f.date_printed, f.year, f.location_printed, f.feis_instance_id, f.feis_brand_id, JSON.stringify([...c?.regions ?? []].sort()), c?.comps ?? 0, rows.get(f.feis_id) ?? 0, captures.get(f.feis_id) ?? 0);
157
+ }
158
+ const red = out.prepare("INSERT OR REPLACE INTO redirect VALUES (?, ?, ?)");
159
+ for (const r of readJsonl(rel.path("feis-redirects.jsonl")))
160
+ red.run(r.old_id, r.new_id, r.kind);
161
+ for (const s of INDEXES)
162
+ out.run(s);
163
+ }
164
+ };
165
+ var NO_CALENDAR2 = "the release holds a feis-year once its results are captured and carries no calendar of scheduled feiseanna";
166
+ var globToLike = (g) => g.replace(/[\\%_]/g, (c) => `\\${c}`).replace(/\*/g, "%").replace(/\?/g, "_");
167
+
168
+ class FeisFinder2 {
169
+ rel;
170
+ constructor(rel) {
171
+ this.rel = rel;
172
+ }
173
+ async dates() {
174
+ await this.rel.fetch("results");
175
+ await this.rel.require("feises.dates");
176
+ const dates = this.rel.db("results").query("SELECT feis_id, min(date_lo) AS date_lo, max(date_hi) AS date_hi FROM feises GROUP BY feis_id ORDER BY feis_id").all();
177
+ return { tag: this.rel.tag, dates };
178
+ }
179
+ async find(q) {
180
+ const db = await this.rel.indexDb("feises");
181
+ const where = [];
182
+ const args = [];
183
+ let redirected;
184
+ if (q.platform !== undefined) {
185
+ where.push("(source = ? OR token = ?)");
186
+ args.push(q.platform, q.platform);
187
+ }
188
+ if (q.code !== undefined) {
189
+ where.push("host_id = ? COLLATE NOCASE");
190
+ args.push(q.code);
191
+ }
192
+ if (q.id !== undefined) {
193
+ let id = q.id;
194
+ const seen = new Set;
195
+ for (;; ) {
196
+ const r = db.query("SELECT new_id FROM redirect WHERE old_id = ?").get(id);
197
+ if (r === null || seen.has(r.new_id))
198
+ break;
199
+ seen.add(id);
200
+ id = r.new_id;
201
+ }
202
+ if (id !== q.id)
203
+ redirected = { from: q.id, to: id };
204
+ where.push("(feis_id = ? OR instance_id = ? OR brand_id = ?)");
205
+ args.push(id, id, id);
206
+ }
207
+ if (q.name !== undefined) {
208
+ where.push("name LIKE ? ESCAPE '\\'");
209
+ args.push(globToLike(q.name));
210
+ }
211
+ if (q.year !== undefined) {
212
+ where.push("year = ?");
213
+ args.push(q.year);
214
+ }
215
+ const sql = `SELECT * FROM feis${where.length > 0 ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY date_lo, feis_id`;
216
+ const feises = db.query(sql).all(...args).map((r) => ({
217
+ ...r,
218
+ regions: JSON.parse(r.regions)
219
+ }));
220
+ return { tag: this.rel.tag, feises, ...redirected !== undefined ? { redirected } : {} };
221
+ }
222
+ async codeStem(token, stem) {
223
+ const db = await this.rel.indexDb("feises");
224
+ const like = db.query("SELECT host_id, year, brand_id FROM feis WHERE token = ? AND host_id LIKE ? ESCAPE '\\' COLLATE NOCASE").all(token, `${stem.replace(/[\\%_]/g, (c) => `\\${c}`)}__`);
225
+ const fits = like.filter((f) => {
226
+ const d = /(\d{2})$/.exec(f.host_id);
227
+ return d !== null && f.host_id.slice(0, -2).toLowerCase() === stem.toLowerCase() && f.year !== null && f.year % 100 === Number(d[1]) && f.brand_id !== null;
228
+ });
229
+ return [...new Set(fits.map((f) => f.brand_id))].sort();
230
+ }
231
+ async seriesOf(brand) {
232
+ const db = await this.rel.indexDb("feises");
233
+ const rows = db.query("SELECT * FROM feis WHERE brand_id = ? ORDER BY year, date_lo, feis_id").all(brand);
234
+ const byYear = new Map;
235
+ for (const r of rows) {
236
+ const i = byYear.get(r.year);
237
+ if (i === undefined)
238
+ byYear.set(r.year, {
239
+ year: r.year,
240
+ feis_ids: [r.feis_id],
241
+ date_lo: r.date_lo,
242
+ name: r.name
243
+ });
244
+ else
245
+ i.feis_ids.push(r.feis_id);
246
+ }
247
+ const instances = [...byYear.values()];
248
+ const newest = rows[rows.length - 1];
249
+ return {
250
+ series_id: brand,
251
+ name: newest?.name ?? "",
252
+ host: newest?.source ?? "",
253
+ hosts: [...new Set(rows.map((r) => r.source))].sort(),
254
+ codes: [...new Set(rows.map((r) => r.host_id))].sort(),
255
+ years: instances.map((i) => i.year).filter((y) => y !== null),
256
+ instances
257
+ };
258
+ }
259
+ async series(q) {
260
+ const asked = q.trim();
261
+ const answer = async (brands, via, more = {}) => {
262
+ const candidates = [];
263
+ for (const b of brands)
264
+ candidates.push(await this.seriesOf(b));
265
+ const arm = candidates.length === 0 ? "none" : candidates.length === 1 ? "unique" : "ambiguous";
266
+ const u = arm === "unique" ? candidates[0] : null;
267
+ return {
268
+ tag: this.rel.tag,
269
+ asked,
270
+ arm,
271
+ via: candidates.length === 0 ? null : via,
272
+ series_id: u?.series_id ?? null,
273
+ name: u?.name ?? null,
274
+ host: u?.host ?? null,
275
+ years: u?.years ?? [],
276
+ instances: u?.instances ?? [],
277
+ next: null,
278
+ ...u !== null ? { next_why: NO_CALENDAR2 } : {},
279
+ candidates,
280
+ ...arm === "ambiguous" ? { why: `${candidates.length} series answer "${asked}": ${brands.join(", ")}` } : {},
281
+ ...more
282
+ };
283
+ };
284
+ if (asked === "")
285
+ return answer([], null, { why: "no series was asked" });
286
+ const byId = await this.find({ id: asked });
287
+ if (byId.feises.length > 0) {
288
+ const brands = [...new Set(byId.feises.map((f) => f.brand_id))];
289
+ const filed = brands.filter((b) => b !== null).sort();
290
+ const redirected = byId.redirected !== undefined ? { redirected: byId.redirected } : {};
291
+ if (filed.length === 0)
292
+ return answer([], null, {
293
+ ...redirected,
294
+ why: `the release files ${byId.feises.map((f) => f.feis_id).join(", ")} under no series`
295
+ });
296
+ return answer(filed, "id", redirected);
297
+ }
298
+ const code = /^([^:\s]+):(.*?)(\d{2})?$/.exec(asked);
299
+ if (code !== null) {
300
+ const [, token, stem] = code;
301
+ const brands = stem === "" ? [] : await this.codeStem(token, stem);
302
+ if (brands.length > 0)
303
+ return answer(brands, "code-stem");
304
+ }
305
+ const glob = /[*?]/.test(asked) ? asked : `*${asked}*`;
306
+ const byName = await this.find({ name: glob });
307
+ const brands = [
308
+ ...new Set(byName.feises.map((f) => f.brand_id).filter((b) => b !== null))
309
+ ].sort();
310
+ if (brands.length > 0)
311
+ return answer(brands, "name");
312
+ return answer([], null, {
313
+ why: byName.feises.length > 0 ? `the feises named "${asked}" are filed under no series` : `the release holds no feis or series "${asked}"`
314
+ });
315
+ }
316
+ }
317
+
318
+ var NO_PLACE = new Set(["NP", "NR", "N/A", "NA", "NO RANK", "NORANK", "NO PLACE", "-", "--"]);
319
+ var ABSENT = new Set(["DID NOT COMPETE", "DID NOT ATTEND", "NO SHOW", "DNA", "DNS", "NS"]);
320
+ var ABSENT_WORDS = /not\s*attend|no\s*show/i;
321
+ var DID_NOT_TABULATE = new Set(["DNT", "did-not-start"]);
322
+ function printsAbsence(print) {
323
+ const s = print.trim();
324
+ if (s === "")
325
+ return false;
326
+ const upper = s.toUpperCase();
327
+ if (NO_PLACE.has(upper))
328
+ return false;
329
+ return ABSENT.has(upper) || ABSENT_WORDS.test(s);
330
+ }
331
+ function notOnTheFloor(placePrinted, atts, withdrawn) {
332
+ if (printsAbsence(placePrinted ?? ""))
333
+ return true;
334
+ if (atts !== null && atts !== "") {
335
+ for (const a of atts.split(","))
336
+ if (printsAbsence(a))
337
+ return true;
338
+ }
339
+ return withdrawn !== null && DID_NOT_TABULATE.has(withdrawn);
340
+ }
341
+
109
342
  var ENTRY_RANK = new Map([
110
343
  ["final", 0],
111
344
  ["", 1],
@@ -375,6 +608,7 @@ class MajorsFinder2 {
375
608
  }
376
609
  }
377
610
 
611
+ var LINEAGE_FILE = "school-lineage.jsonl";
378
612
  function nameOrderOf(s) {
379
613
  return s === "surname-first" || s === "given-first" || s === "unknown" ? s : undefined;
380
614
  }
@@ -387,7 +621,7 @@ var PERSON = { table: "person", value: "id", valueType: "INTEGER PRIMARY KEY" };
387
621
  var ASKED = ["surname_key", "nn4", "blk_surname"];
388
622
  var dancersIndex = {
389
623
  name: "dancers",
390
- version: "2",
624
+ version: "4",
391
625
  inputs: ["results"],
392
626
  keyParts: () => [...PACKAGE_KEY_PARTS],
393
627
  says: "every published dancer print at the names package's keys, its dancers, schools and years",
@@ -398,6 +632,7 @@ var dancersIndex = {
398
632
  out.run("CREATE TABLE dancer(dancer_id INTEGER PRIMARY KEY, display TEXT NOT NULL, names_printed TEXT NOT NULL, genders TEXT NOT NULL, appearances INTEGER NOT NULL, kind TEXT, birth_lo INTEGER, birth_hi INTEGER, birth_source TEXT NOT NULL, birth_conflict INTEGER NOT NULL, rows INTEGER NOT NULL, year_lo INTEGER, year_hi INTEGER)");
399
633
  out.run("CREATE TABLE dancer_school(dancer_id INTEGER NOT NULL, school_id TEXT, print TEXT NOT NULL, rows INTEGER NOT NULL, year_lo INTEGER, year_hi INTEGER)");
400
634
  out.run("CREATE TABLE dancer_population(dancer_id INTEGER NOT NULL, population TEXT NOT NULL, rows INTEGER NOT NULL, PRIMARY KEY(dancer_id, population)) WITHOUT ROWID");
635
+ out.run("CREATE TABLE dancer_regional(dancer_id INTEGER NOT NULL, year INTEGER NOT NULL, region TEXT NOT NULL, regional INTEGER NOT NULL, PRIMARY KEY(dancer_id, year)) WITHOUT ROWID");
401
636
  const src = rel.db("results");
402
637
  const groups = src.query(`SELECT r.name_printed AS p, r.name_order_resolved AS o, r.dancer_id AS d,
403
638
  r.school_printed AS sp, r.school_id AS s,
@@ -472,6 +707,25 @@ var dancersIndex = {
472
707
  if (pop === "girls" || pop === "boys")
473
708
  putPop.run(r.d, pop, r.n);
474
709
  }
710
+ if (src.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'dancer_years'").get() !== null) {
711
+ const cols = new Set(src.query("PRAGMA table_info(results)").all().map((c) => c.name));
712
+ const col = (c) => cols.has(c) ? `r.${c}` : "NULL";
713
+ const rows = src.query(`SELECT r.dancer_id AS d, c.year AS y, r.address AS a, ${col("place_printed")} AS p,
714
+ ${col("atts")} AS t, ${col("withdrawn")} AS w
715
+ FROM results r JOIN competitions c ON c.file = r.file AND c.line = r.competition_line
716
+ WHERE r.dancer_id IS NOT NULL AND c.tier = 'T3' AND c.year IS NOT NULL`).all();
717
+ const absent = new Set;
718
+ for (const r of rows)
719
+ if (notOnTheFloor(r.p, r.t, r.w))
720
+ absent.add(r.a);
721
+ const t3 = new Set;
722
+ for (const r of rows)
723
+ if (!absent.has(r.a))
724
+ t3.add(`${r.d}\x00${r.y}`);
725
+ const putReg = out.prepare("INSERT INTO dancer_regional VALUES (?, ?, ?, ?)");
726
+ for (const r of src.query("SELECT dancer_id AS d, year AS y, region AS r FROM dancer_years WHERE region_source = 'current' AND region IS NOT NULL").all())
727
+ putReg.run(r.d, r.y, r.r, t3.has(`${r.d}\x00${r.y}`) ? 1 : 0);
728
+ }
475
729
  for (const s of schema.indexes)
476
730
  if (ASKED.some((c) => s.endsWith(`(${c})`)))
477
731
  out.run(s);
@@ -506,11 +760,17 @@ var AGE_WEIGHT = {
506
760
  };
507
761
  var SCHOOL_RANK = {
508
762
  same: 0,
763
+ lineage: 0,
509
764
  moved: 1,
510
765
  none: 2,
511
766
  unresolved: 2,
512
767
  unasked: 2
513
768
  };
769
+ var PREFERRED_WHY = {
770
+ same: "whose latest school is",
771
+ lineage: "whose latest school the release's lineage joins to",
772
+ moved: "that ever danced for"
773
+ };
514
774
  var POPULATION_WEIGHT = {
515
775
  same: 1,
516
776
  both: 0,
@@ -518,8 +778,24 @@ var POPULATION_WEIGHT = {
518
778
  unasked: 0,
519
779
  contradicts: -10
520
780
  };
781
+ var REGION_WEIGHT = {
782
+ same: 1,
783
+ unknown: 0,
784
+ unasked: 0,
785
+ moved: 0,
786
+ contradicts: -10
787
+ };
788
+ var REGION_SAME_THAT_YEAR = 2;
789
+ var REGION_RANK = {
790
+ same: 0,
791
+ unknown: 1,
792
+ unasked: 1,
793
+ moved: 1,
794
+ contradicts: 2
795
+ };
521
796
  var SCHOOL_WEIGHT = {
522
797
  same: 2,
798
+ lineage: 2,
523
799
  moved: 1,
524
800
  none: 0,
525
801
  unresolved: 0,
@@ -535,9 +811,31 @@ function idChunks(ids) {
535
811
  class DancerResolver2 {
536
812
  rel;
537
813
  printIds = new Map;
814
+ lineageOf;
815
+ regions;
538
816
  constructor(rel) {
539
817
  this.rel = rel;
540
818
  }
819
+ lineage() {
820
+ this.lineageOf ??= (async () => {
821
+ if ((await this.rel.capability("schools.lineage")).state !== "full")
822
+ return null;
823
+ await this.rel.fetch(LINEAGE_FILE);
824
+ const out = new Map;
825
+ const join = (a, b) => {
826
+ const s = out.get(a) ?? new Set;
827
+ s.add(b);
828
+ out.set(a, s);
829
+ };
830
+ for (const r of readJsonl(this.rel.path(LINEAGE_FILE)))
831
+ if (r.id !== r.other) {
832
+ join(r.id, r.other);
833
+ join(r.other, r.id);
834
+ }
835
+ return out;
836
+ })();
837
+ return this.lineageOf;
838
+ }
541
839
  async records(q = {}) {
542
840
  await this.rel.fetch("results");
543
841
  await this.rel.require("dancers.records");
@@ -642,12 +940,29 @@ class DancerResolver2 {
642
940
  const asOf = yearOf(q.asOf);
643
941
  const age = q.age === undefined ? undefined : this.readAge(q.age, asOf);
644
942
  const population = q.population === undefined ? undefined : readPopulation2(q.population);
943
+ const oireachtas = q.oireachtas === undefined ? undefined : await this.readOireachtas(db, q.oireachtas, asOf);
645
944
  let schoolIds = null;
945
+ let lineageIds = [];
646
946
  let school;
647
947
  if (q.school !== undefined) {
648
948
  const s = await this.rel.schools.resolve(q.school);
649
949
  schoolIds = s.candidates.map((c) => c.school_id);
650
- school = { arm: s.arm, ids: schoolIds };
950
+ const lineage = await this.lineage();
951
+ if (lineage === null)
952
+ school = { arm: s.arm, ids: schoolIds };
953
+ else {
954
+ const asked = new Set(schoolIds);
955
+ const reached = new Set(schoolIds);
956
+ const todo = [...schoolIds];
957
+ for (let id = todo.pop();id !== undefined; id = todo.pop())
958
+ for (const o of lineage.get(id) ?? [])
959
+ if (!reached.has(o)) {
960
+ reached.add(o);
961
+ todo.push(o);
962
+ }
963
+ lineageIds = [...reached].filter((id) => !asked.has(id)).sort();
964
+ school = { arm: s.arm, ids: schoolIds, lineage: lineageIds };
965
+ }
651
966
  }
652
967
  const byDancer = new Map;
653
968
  const unlinked = new Map;
@@ -674,24 +989,37 @@ class DancerResolver2 {
674
989
  const ageEv = ageEvidence(d, age?.age ?? null);
675
990
  const pop = populationOf(db, id);
676
991
  const popEv = populationEvidence(pop, population?.read ?? null);
677
- const schoolEv = schoolIds === null ? "unasked" : schoolIds.length === 0 ? "unresolved" : await this.schoolEvidence(schools, schoolIds, asOf);
992
+ const schoolEv = schoolIds === null ? "unasked" : schoolIds.length === 0 ? "unresolved" : await this.schoolEvidence(schools, schoolIds, lineageIds, asOf);
993
+ const reg = oireachtas === undefined ? null : oireachtas.read === null ? {
994
+ word: "unknown",
995
+ region: { year: null, region: null, regional: false, attended: 0 }
996
+ } : regionEvidence(db, id, oireachtas.read, oireachtas.year);
997
+ const regionEv = reg === null ? "unasked" : reg.word;
998
+ const regionWeight = regionEv === "same" && reg?.region.year === oireachtas?.year ? REGION_SAME_THAT_YEAR : REGION_WEIGHT[regionEv];
678
999
  all.push({
679
1000
  dancer_id: id,
680
1001
  display: d.display,
681
1002
  scope: id >= FEIS_LOCAL_ID_FLOOR2 ? "per-release" : "stable",
682
1003
  kind: d.kind,
683
- score: NAME_WEIGHT[e.level] + AGE_WEIGHT[ageEv] + SCHOOL_WEIGHT[schoolEv] + POPULATION_WEIGHT[popEv],
684
- evidence: { nameLevel: e.level, age: ageEv, school: schoolEv, population: popEv },
1004
+ score: NAME_WEIGHT[e.level] + AGE_WEIGHT[ageEv] + SCHOOL_WEIGHT[schoolEv] + POPULATION_WEIGHT[popEv] + regionWeight,
1005
+ evidence: {
1006
+ nameLevel: e.level,
1007
+ age: ageEv,
1008
+ school: schoolEv,
1009
+ population: popEv,
1010
+ region: regionEv
1011
+ },
685
1012
  prints: e.prints.sort((a, b) => b.rows - a.rows || (a.print < b.print ? -1 : 1)),
686
1013
  rows: d.rows,
687
1014
  year_lo: d.year_lo,
688
1015
  year_hi: d.year_hi,
689
1016
  birth: birthOf(d),
690
1017
  population: pop,
691
- school: latest
1018
+ school: latest,
1019
+ ...reg !== null ? { region: reg.region } : {}
692
1020
  });
693
1021
  }
694
- all.sort((a, b) => SCHOOL_RANK[a.evidence.school] - SCHOOL_RANK[b.evidence.school] || b.score - a.score || b.rows - a.rows || a.dancer_id - b.dancer_id);
1022
+ all.sort((a, b) => SCHOOL_RANK[a.evidence.school] - SCHOOL_RANK[b.evidence.school] || REGION_RANK[a.evidence.region] - REGION_RANK[b.evidence.region] || b.score - a.score || b.rows - a.rows || a.dancer_id - b.dancer_id);
695
1023
  const excluded = [];
696
1024
  let kept = all;
697
1025
  if (narrow)
@@ -700,23 +1028,38 @@ class DancerResolver2 {
700
1028
  excluded.push({ candidate: c, why: "age" });
701
1029
  else if (c.evidence.population === "contradicts")
702
1030
  excluded.push({ candidate: c, why: "population" });
1031
+ else if (c.evidence.region === "contradicts")
1032
+ excluded.push({ candidate: c, why: "region" });
703
1033
  else
704
1034
  return true;
705
1035
  return false;
706
1036
  });
707
1037
  let preferred;
708
1038
  if (kept.length > 1 && school !== undefined)
709
- for (const ev of ["same", "moved"]) {
710
- const hit = kept.filter((c) => SCHOOL_RANK[c.evidence.school] === SCHOOL_RANK[ev]);
1039
+ for (const rank of [SCHOOL_RANK.same, SCHOOL_RANK.moved]) {
1040
+ const hit = kept.filter((c) => SCHOOL_RANK[c.evidence.school] === rank);
711
1041
  if (hit.length === 0)
712
1042
  continue;
713
- if (hit.length === 1)
1043
+ if (hit.length === 1) {
1044
+ const c = hit[0];
714
1045
  preferred = {
715
- dancer_id: hit[0].dancer_id,
716
- why: `the only candidate ${ev === "same" ? "whose latest school is" : "that ever danced for"} ${q.school}`
1046
+ dancer_id: c.dancer_id,
1047
+ why: `the only candidate ${PREFERRED_WHY[c.evidence.school]} ${q.school}`
717
1048
  };
1049
+ }
718
1050
  break;
719
1051
  }
1052
+ if (preferred === undefined && kept.length > 1 && oireachtas?.read != null) {
1053
+ const best = Math.min(...kept.map((c) => SCHOOL_RANK[c.evidence.school]));
1054
+ const hit = kept.filter((c) => SCHOOL_RANK[c.evidence.school] === best && c.evidence.region === "same");
1055
+ if (hit.length === 1) {
1056
+ const c = hit[0];
1057
+ preferred = {
1058
+ dancer_id: c.dancer_id,
1059
+ why: `the only candidate${school !== undefined ? " of the school's first rank" : ""} whose latest attended region is ${oireachtas.read}: ${c.region?.year} of ${c.region?.attended} attended year(s) to ${oireachtas.year}`
1060
+ };
1061
+ }
1062
+ }
720
1063
  return {
721
1064
  tag: this.rel.tag,
722
1065
  asked: q,
@@ -727,32 +1070,59 @@ class DancerResolver2 {
727
1070
  unlinked: [...unlinked].map(([print, rows]) => ({ print, rows })).sort((a, b) => b.rows - a.rows || (a.print < b.print ? -1 : 1)),
728
1071
  ...age !== undefined ? { age: age.answer } : {},
729
1072
  ...population !== undefined ? { population } : {},
1073
+ ...oireachtas !== undefined ? { oireachtas } : {},
730
1074
  ...school !== undefined ? { school } : {}
731
1075
  };
732
1076
  }
733
- async schoolEvidence(schools, ids, asOf) {
1077
+ async readOireachtas(db, o, asOf) {
1078
+ const year = o.year ?? asOf ?? new Date().getFullYear();
1079
+ const asked = o.region;
1080
+ if ((await this.rel.capability("dancers.years")).state !== "full")
1081
+ return {
1082
+ asked,
1083
+ read: null,
1084
+ year,
1085
+ note: "this release publishes no dancer-years: the region narrows and orders nothing"
1086
+ };
1087
+ this.regions ??= db.query("SELECT DISTINCT region FROM dancer_regional ORDER BY region").all().map((r) => r.region);
1088
+ const want = asked.trim().toUpperCase();
1089
+ const hit = this.regions.find((r) => r.toUpperCase() === want);
1090
+ if (hit === undefined)
1091
+ return {
1092
+ asked,
1093
+ read: null,
1094
+ year,
1095
+ note: `the release reads no attended year in a region "${asked}" (its spelling is dancer_years.region): it narrows and orders nothing`
1096
+ };
1097
+ return { asked, read: hit, year };
1098
+ }
1099
+ async schoolEvidence(schools, ids, lineage, asOf) {
734
1100
  const rows = schools.filter((s) => asOf === null || s.year_lo === null || s.year_lo <= asOf);
735
- const meets = async (s) => {
1101
+ const idsOf = async (s) => {
736
1102
  if (s.school_id !== null)
737
- return ids.includes(s.school_id);
1103
+ return [s.school_id];
738
1104
  let read = this.printIds.get(s.print);
739
1105
  if (read === undefined) {
740
1106
  read = (await this.rel.schools.resolve(s.print)).candidates.map((c) => c.school_id);
741
1107
  this.printIds.set(s.print, read);
742
1108
  }
743
- return read.some((id) => ids.includes(id));
1109
+ return read;
744
1110
  };
745
1111
  const top = (s) => asOf !== null && s.year_hi !== null ? Math.min(s.year_hi, asOf) : s.year_hi ?? -1;
746
1112
  const latest = Math.max(-1, ...rows.map(top));
747
1113
  let earlier = false;
1114
+ let joined = false;
748
1115
  for (const s of rows) {
749
- if (!await meets(s))
750
- continue;
751
- if (top(s) === latest)
752
- return "same";
753
- earlier = true;
1116
+ const own = await idsOf(s);
1117
+ const isLatest = top(s) === latest;
1118
+ if (own.some((id) => ids.includes(id))) {
1119
+ if (isLatest)
1120
+ return "same";
1121
+ earlier = true;
1122
+ } else if (isLatest && own.some((id) => lineage.includes(id)))
1123
+ joined = true;
754
1124
  }
755
- return earlier ? "moved" : "none";
1125
+ return joined ? "lineage" : earlier ? "moved" : "none";
756
1126
  }
757
1127
  readAge(a, asOf) {
758
1128
  const done = (age, note) => {
@@ -832,6 +1202,22 @@ function populationEvidence(own, asked) {
832
1202
  return "unknown";
833
1203
  return populationRelation(asked, mine) === "disjoint" ? "contradicts" : "same";
834
1204
  }
1205
+ function regionEvidence(db, dancerId, region, year) {
1206
+ const rows = db.query("SELECT year, region, regional FROM dancer_regional WHERE dancer_id = ? AND year <= ? ORDER BY year").all(dancerId, year);
1207
+ const attended = rows.length;
1208
+ const at = (r) => ({
1209
+ year: r.year,
1210
+ region: r.region,
1211
+ regional: r.regional === 1,
1212
+ attended
1213
+ });
1214
+ const latest = rows.at(-1);
1215
+ if (latest === undefined)
1216
+ return { word: "unknown", region: { year: null, region: null, regional: false, attended } };
1217
+ if (latest.year === year && latest.regional === 1 && latest.region !== region)
1218
+ return { word: "contradicts", region: at(latest) };
1219
+ return { word: latest.region === region ? "same" : "moved", region: at(latest) };
1220
+ }
835
1221
  function ageEvidence(d, asked) {
836
1222
  if (asked === null)
837
1223
  return "unasked";
@@ -865,7 +1251,7 @@ var LEVEL = Object.freeze({
865
1251
  600: "PC",
866
1252
  700: "OC"
867
1253
  });
868
- var SCHEMA = `
1254
+ var SCHEMA2 = `
869
1255
  CREATE TABLE comp(
870
1256
  competition TEXT PRIMARY KEY, feis_id TEXT NOT NULL, brand_id TEXT, year INTEGER,
871
1257
  code TEXT NOT NULL, code_fold TEXT NOT NULL, title TEXT, age TEXT, gender TEXT,
@@ -874,7 +1260,7 @@ CREATE TABLE comp(
874
1260
  gender_source TEXT, level_printed TEXT, dance_printed TEXT, dance TEXT
875
1261
  );
876
1262
  `;
877
- var INDEXES = [
1263
+ var INDEXES2 = [
878
1264
  "CREATE INDEX comp_feis_code ON comp(feis_id, code_fold)",
879
1265
  "CREATE INDEX comp_brand_year ON comp(brand_id, year)"
880
1266
  ];
@@ -893,7 +1279,7 @@ var compsIndex = {
893
1279
  inputs: ["results"],
894
1280
  says: "every competition by its feis and its own code, with its series and year",
895
1281
  build(rel, out) {
896
- out.run(SCHEMA);
1282
+ out.run(SCHEMA2);
897
1283
  const src = rel.db("results");
898
1284
  const brand = new Map;
899
1285
  for (const f of src.query("SELECT feis_id, feis_brand_id, year FROM feises ORDER BY file, line").all())
@@ -909,7 +1295,7 @@ var compsIndex = {
909
1295
  const f = brand.get(c.event);
910
1296
  put.run(c.competition, c.event, f?.brand ?? null, c.year ?? f?.year ?? null, c.comp_no.trim(), fold(c.comp_no), c.title, c.age, c.gender, c.feis_level, c.round, c.source, captures.get(c.competition) ?? 1, c.file, c.line, c.age_source, c.age_printed, c.gender_source, c.level_printed, c.dance_printed, c.dance);
911
1297
  }
912
- for (const s of INDEXES)
1298
+ for (const s of INDEXES2)
913
1299
  out.run(s);
914
1300
  }
915
1301
  };
@@ -1199,215 +1585,6 @@ class CompResolver2 {
1199
1585
  }
1200
1586
  }
1201
1587
 
1202
- import { readFileSync } from "fs";
1203
- var SCHEMA2 = `
1204
- CREATE TABLE feis(
1205
- feis_id TEXT PRIMARY KEY, source TEXT NOT NULL, token TEXT NOT NULL, host_id TEXT NOT NULL,
1206
- name TEXT NOT NULL, date_lo TEXT, date_hi TEXT, date_printed TEXT, year INTEGER,
1207
- location TEXT, instance_id TEXT, brand_id TEXT, regions TEXT NOT NULL,
1208
- competitions INTEGER NOT NULL, rows INTEGER NOT NULL, captures INTEGER NOT NULL
1209
- );
1210
- CREATE TABLE redirect(old_id TEXT PRIMARY KEY, new_id TEXT NOT NULL, kind TEXT NOT NULL);
1211
- `;
1212
- var INDEXES2 = [
1213
- "CREATE INDEX feis_host ON feis(host_id COLLATE NOCASE)",
1214
- "CREATE INDEX feis_token ON feis(token)",
1215
- "CREATE INDEX feis_source ON feis(source)",
1216
- "CREATE INDEX feis_instance ON feis(instance_id)",
1217
- "CREATE INDEX feis_brand ON feis(brand_id)",
1218
- "CREATE INDEX feis_year ON feis(year)"
1219
- ];
1220
- function readJsonl(path) {
1221
- return readFileSync(path, "utf8").split(`
1222
- `).filter((l) => l.trim() !== "").map((l) => JSON.parse(l));
1223
- }
1224
- var feisesIndex = {
1225
- name: "feises",
1226
- version: "2",
1227
- inputs: ["results", "feis-redirects.jsonl"],
1228
- says: "every feis by id, platform, code, series and name, with its counts and regions",
1229
- build(rel, out) {
1230
- out.run(SCHEMA2);
1231
- const src = rel.db("results");
1232
- const counts = new Map;
1233
- for (const r of src.query("SELECT event, count(*) AS n, json_group_array(DISTINCT feis_region) AS regions FROM competitions GROUP BY event").all())
1234
- counts.set(r.event, {
1235
- comps: r.n,
1236
- regions: new Set(JSON.parse(r.regions).filter((x) => x !== null))
1237
- });
1238
- const rows = new Map;
1239
- for (const r of src.query("SELECT c.event AS event, sum(r.n) AS n FROM (SELECT competition, count(*) AS n FROM results GROUP BY competition) r JOIN (SELECT DISTINCT competition, event FROM competitions) c ON c.competition = r.competition GROUP BY c.event").all())
1240
- rows.set(r.event, r.n);
1241
- const put = out.prepare("INSERT OR IGNORE INTO feis VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
1242
- const captures = new Map;
1243
- const feisRows = src.query("SELECT feis_id, source, host_id, name_printed, date_lo, date_hi, date_printed, year, location_printed, feis_instance_id, feis_brand_id FROM feises ORDER BY file, line").all();
1244
- for (const f of feisRows)
1245
- captures.set(f.feis_id, (captures.get(f.feis_id) ?? 0) + 1);
1246
- for (const f of feisRows) {
1247
- const c = counts.get(f.feis_id);
1248
- const colon = f.feis_id.indexOf(":");
1249
- put.run(f.feis_id, f.source, colon === -1 ? f.source : f.feis_id.slice(0, colon), f.host_id, f.name_printed, f.date_lo, f.date_hi, f.date_printed, f.year, f.location_printed, f.feis_instance_id, f.feis_brand_id, JSON.stringify([...c?.regions ?? []].sort()), c?.comps ?? 0, rows.get(f.feis_id) ?? 0, captures.get(f.feis_id) ?? 0);
1250
- }
1251
- const red = out.prepare("INSERT OR REPLACE INTO redirect VALUES (?, ?, ?)");
1252
- for (const r of readJsonl(rel.path("feis-redirects.jsonl")))
1253
- red.run(r.old_id, r.new_id, r.kind);
1254
- for (const s of INDEXES2)
1255
- out.run(s);
1256
- }
1257
- };
1258
- var NO_CALENDAR2 = "the release holds a feis-year once its results are captured and carries no calendar of scheduled feiseanna";
1259
- var globToLike = (g) => g.replace(/[\\%_]/g, (c) => `\\${c}`).replace(/\*/g, "%").replace(/\?/g, "_");
1260
-
1261
- class FeisFinder2 {
1262
- rel;
1263
- constructor(rel) {
1264
- this.rel = rel;
1265
- }
1266
- async dates() {
1267
- await this.rel.fetch("results");
1268
- await this.rel.require("feises.dates");
1269
- const dates = this.rel.db("results").query("SELECT feis_id, min(date_lo) AS date_lo, max(date_hi) AS date_hi FROM feises GROUP BY feis_id ORDER BY feis_id").all();
1270
- return { tag: this.rel.tag, dates };
1271
- }
1272
- async find(q) {
1273
- const db = await this.rel.indexDb("feises");
1274
- const where = [];
1275
- const args = [];
1276
- let redirected;
1277
- if (q.platform !== undefined) {
1278
- where.push("(source = ? OR token = ?)");
1279
- args.push(q.platform, q.platform);
1280
- }
1281
- if (q.code !== undefined) {
1282
- where.push("host_id = ? COLLATE NOCASE");
1283
- args.push(q.code);
1284
- }
1285
- if (q.id !== undefined) {
1286
- let id = q.id;
1287
- const seen = new Set;
1288
- for (;; ) {
1289
- const r = db.query("SELECT new_id FROM redirect WHERE old_id = ?").get(id);
1290
- if (r === null || seen.has(r.new_id))
1291
- break;
1292
- seen.add(id);
1293
- id = r.new_id;
1294
- }
1295
- if (id !== q.id)
1296
- redirected = { from: q.id, to: id };
1297
- where.push("(feis_id = ? OR instance_id = ? OR brand_id = ?)");
1298
- args.push(id, id, id);
1299
- }
1300
- if (q.name !== undefined) {
1301
- where.push("name LIKE ? ESCAPE '\\'");
1302
- args.push(globToLike(q.name));
1303
- }
1304
- if (q.year !== undefined) {
1305
- where.push("year = ?");
1306
- args.push(q.year);
1307
- }
1308
- const sql = `SELECT * FROM feis${where.length > 0 ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY date_lo, feis_id`;
1309
- const feises = db.query(sql).all(...args).map((r) => ({
1310
- ...r,
1311
- regions: JSON.parse(r.regions)
1312
- }));
1313
- return { tag: this.rel.tag, feises, ...redirected !== undefined ? { redirected } : {} };
1314
- }
1315
- async codeStem(token, stem) {
1316
- const db = await this.rel.indexDb("feises");
1317
- const like = db.query("SELECT host_id, year, brand_id FROM feis WHERE token = ? AND host_id LIKE ? ESCAPE '\\' COLLATE NOCASE").all(token, `${stem.replace(/[\\%_]/g, (c) => `\\${c}`)}__`);
1318
- const fits = like.filter((f) => {
1319
- const d = /(\d{2})$/.exec(f.host_id);
1320
- return d !== null && f.host_id.slice(0, -2).toLowerCase() === stem.toLowerCase() && f.year !== null && f.year % 100 === Number(d[1]) && f.brand_id !== null;
1321
- });
1322
- return [...new Set(fits.map((f) => f.brand_id))].sort();
1323
- }
1324
- async seriesOf(brand) {
1325
- const db = await this.rel.indexDb("feises");
1326
- const rows = db.query("SELECT * FROM feis WHERE brand_id = ? ORDER BY year, date_lo, feis_id").all(brand);
1327
- const byYear = new Map;
1328
- for (const r of rows) {
1329
- const i = byYear.get(r.year);
1330
- if (i === undefined)
1331
- byYear.set(r.year, {
1332
- year: r.year,
1333
- feis_ids: [r.feis_id],
1334
- date_lo: r.date_lo,
1335
- name: r.name
1336
- });
1337
- else
1338
- i.feis_ids.push(r.feis_id);
1339
- }
1340
- const instances = [...byYear.values()];
1341
- const newest = rows[rows.length - 1];
1342
- return {
1343
- series_id: brand,
1344
- name: newest?.name ?? "",
1345
- host: newest?.source ?? "",
1346
- hosts: [...new Set(rows.map((r) => r.source))].sort(),
1347
- codes: [...new Set(rows.map((r) => r.host_id))].sort(),
1348
- years: instances.map((i) => i.year).filter((y) => y !== null),
1349
- instances
1350
- };
1351
- }
1352
- async series(q) {
1353
- const asked = q.trim();
1354
- const answer = async (brands, via, more = {}) => {
1355
- const candidates = [];
1356
- for (const b of brands)
1357
- candidates.push(await this.seriesOf(b));
1358
- const arm = candidates.length === 0 ? "none" : candidates.length === 1 ? "unique" : "ambiguous";
1359
- const u = arm === "unique" ? candidates[0] : null;
1360
- return {
1361
- tag: this.rel.tag,
1362
- asked,
1363
- arm,
1364
- via: candidates.length === 0 ? null : via,
1365
- series_id: u?.series_id ?? null,
1366
- name: u?.name ?? null,
1367
- host: u?.host ?? null,
1368
- years: u?.years ?? [],
1369
- instances: u?.instances ?? [],
1370
- next: null,
1371
- ...u !== null ? { next_why: NO_CALENDAR2 } : {},
1372
- candidates,
1373
- ...arm === "ambiguous" ? { why: `${candidates.length} series answer "${asked}": ${brands.join(", ")}` } : {},
1374
- ...more
1375
- };
1376
- };
1377
- if (asked === "")
1378
- return answer([], null, { why: "no series was asked" });
1379
- const byId = await this.find({ id: asked });
1380
- if (byId.feises.length > 0) {
1381
- const brands = [...new Set(byId.feises.map((f) => f.brand_id))];
1382
- const filed = brands.filter((b) => b !== null).sort();
1383
- const redirected = byId.redirected !== undefined ? { redirected: byId.redirected } : {};
1384
- if (filed.length === 0)
1385
- return answer([], null, {
1386
- ...redirected,
1387
- why: `the release files ${byId.feises.map((f) => f.feis_id).join(", ")} under no series`
1388
- });
1389
- return answer(filed, "id", redirected);
1390
- }
1391
- const code = /^([^:\s]+):(.*?)(\d{2})?$/.exec(asked);
1392
- if (code !== null) {
1393
- const [, token, stem] = code;
1394
- const brands = stem === "" ? [] : await this.codeStem(token, stem);
1395
- if (brands.length > 0)
1396
- return answer(brands, "code-stem");
1397
- }
1398
- const glob = /[*?]/.test(asked) ? asked : `*${asked}*`;
1399
- const byName = await this.find({ name: glob });
1400
- const brands = [
1401
- ...new Set(byName.feises.map((f) => f.brand_id).filter((b) => b !== null))
1402
- ].sort();
1403
- if (brands.length > 0)
1404
- return answer(brands, "name");
1405
- return answer([], null, {
1406
- why: byName.feises.length > 0 ? `the feises named "${asked}" are filed under no series` : `the release holds no feis or series "${asked}"`
1407
- });
1408
- }
1409
- }
1410
-
1411
1588
  import { Database } from "bun:sqlite";
1412
1589
  import { createHash as createHash2 } from "crypto";
1413
1590
  import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "fs";
@@ -2286,7 +2463,8 @@ class RegistrationResolver2 {
2286
2463
  ...row.school !== undefined ? { school: row.school } : {},
2287
2464
  ...age !== null ? { age: { band: age.band, year: age.year } } : {},
2288
2465
  ...row.asOf !== undefined ? { asOf: row.asOf } : {},
2289
- ...population !== null && "asked" in population ? { population: population.asked } : {}
2466
+ ...population !== null && "asked" in population ? { population: population.asked } : {},
2467
+ ...row.oireachtas !== undefined ? { oireachtas: row.oireachtas } : {}
2290
2468
  };
2291
2469
  const { dancer } = await this.dancer(asked);
2292
2470
  out.push({
@@ -2517,6 +2695,12 @@ var FEATURES2 = [
2517
2695
  since: 270,
2518
2696
  calls: "the `dance` a comp of comps.resolve, comps.syllabus and comps.syllabusFor carries: null, never refused, on a release without it"
2519
2697
  },
2698
+ {
2699
+ feature: "schools.lineage",
2700
+ needs: ["school-lineage.jsonl"],
2701
+ since: 184,
2702
+ calls: "the `lineage` school evidence of dancers.resolve and dancers.search: no school is joined by lineage, never refused, on a release without it"
2703
+ },
2520
2704
  {
2521
2705
  feature: "results.stream",
2522
2706
  needs: [
@@ -3229,4 +3413,4 @@ function releases2(opts = {}) {
3229
3413
  return new ReleaseManager2(opts);
3230
3414
  }
3231
3415
 
3232
- export { DATABASES2, planAsset2, sha256File2, unzstdTo2, NAME_LEVELS2, nameLevel2, entryRank2, FEIS_LOCAL_ID_FLOOR2, ResultFinder2, MajorsFinder2, nameOrderOf, DancerResolver2, readPopulation2, canonicalOf2, CompResolver2, NO_CALENDAR2, FeisFinder2, StaffFinder2, SchoolResolver2, TeamFinder2, EventFinder2, defaultBuilders2, indexPath2, indexKey2, isCurrent2, buildIndex2, ensureIndex2, listIndexes2, readPin2, youngest2, dancerResolverOf2, RegistrationResolver2, ghRemote2, httpsRemote2, fallbackRemote2, setRemote2, columnsOf2, FEATURES2, capabilityOf2, needsRelease2, resolveTag2, Release2, openRelease2, openCached2, ReleaseManager2, releases2 };
3416
+ export { DATABASES2, planAsset2, sha256File2, unzstdTo2, NAME_LEVELS2, nameLevel2, NO_CALENDAR2, FeisFinder2, entryRank2, FEIS_LOCAL_ID_FLOOR2, ResultFinder2, MajorsFinder2, nameOrderOf, DancerResolver2, readPopulation2, canonicalOf2, CompResolver2, StaffFinder2, SchoolResolver2, TeamFinder2, EventFinder2, defaultBuilders2, indexPath2, indexKey2, isCurrent2, buildIndex2, ensureIndex2, listIndexes2, readPin2, youngest2, dancerResolverOf2, RegistrationResolver2, ghRemote2, httpsRemote2, fallbackRemote2, setRemote2, columnsOf2, FEATURES2, capabilityOf2, needsRelease2, resolveTag2, Release2, openRelease2, openCached2, ReleaseManager2, releases2 };
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  nameOrderOf,
5
5
  openRelease2,
6
6
  releases2
7
- } from "./chunks/index-4ph8n4ga.js";
7
+ } from "./chunks/index-1sj5zr8k.js";
8
8
  import"./chunks/index-hne1wadw.js";
9
9
  import"./chunks/index-hghdy980.js";
10
10
 
@@ -40,9 +40,12 @@ var USAGE = `usage: idq <verb> [--cache <dir>] [--repo <owner/name>] [--json]
40
40
  dancer [--release <spec>] <name> | --surname <s> | --like <glob> | --id <dancer_id>
41
41
  [--order surname-first|given-first] [--school <print|id>]
42
42
  [--yob <y> | --band <U10> [--year <y>] | --comp <competition>] [--as-of <date>]
43
- [--population girls|boys] [--rows]
43
+ [--population girls|boys] [--oireachtas <region>[:<year>]] [--rows]
44
44
  the dancers a name reaches, each with its evidence
45
- (name level, age, school, population) and the arm; the results of
45
+ (name level, age, school, population, region) and the arm;
46
+ --oireachtas is the regional the roster is for, its region in
47
+ the release's spelling (SRO, MAO), its year the --as-of year
48
+ where none is given; the results of
46
49
  the one dancer where one answers (or --rows for each)
47
50
  results [--release <spec>] [--dancer <id>] [--feis <id>] [--comp <competition>]
48
51
  [--school <print|id>] [--year <y>] [--limit <n>]
@@ -87,12 +90,13 @@ var USAGE = `usage: idq <verb> [--cache <dir>] [--repo <owner/name>] [--json]
87
90
  resolve comp [<feis> <code>] [--year <y>] [--title <t>]
88
91
  resolve dancer [<name>] [--school <s>] [--comp <feis>/<code> | --yob <y> | --band <b> --year <y>]
89
92
  [--order <o>] [--as-of <date>] [--population girls|boys]
93
+ [--oireachtas <region>[:<year>]]
90
94
  resolve registrations
91
95
  JSONL out, one answer per line; with no positional the
92
96
  questions are JSONL on stdin, one per line (a comp:
93
97
  {feis, code, year?, title?}, a dancer: {name, school?, age?,
94
- population?}, a registration: {name, school?, codes[], feis,
95
- year?, population?})
98
+ population?, oireachtas?}, a registration: {name, school?,
99
+ codes[], feis, year?, population?, oireachtas?})
96
100
 
97
101
  <spec>: pinned (the default) | latest | a cut (42, 42.1) | a tag (archive-YYYY.MM.DD-N)`;
98
102
  var VALUE = new Set([
@@ -117,6 +121,7 @@ var VALUE = new Set([
117
121
  "--comp",
118
122
  "--as-of",
119
123
  "--population",
124
+ "--oireachtas",
120
125
  "--dancer",
121
126
  "--feis",
122
127
  "--limit",
@@ -169,7 +174,9 @@ function printCached(c, say) {
169
174
  }
170
175
  var span = (lo, hi) => lo === null ? "-" : lo === hi ? `${lo}` : `${lo}-${hi ?? "?"}`;
171
176
  function printCandidate(c, say, pad = " ") {
172
- say(`${pad}${c.dancer_id} ${c.display}${c.scope === "per-release" ? " [per-release id]" : ""} score ${c.score} name ${c.evidence.nameLevel} age ${c.evidence.age} school ${c.evidence.school} population ${c.evidence.population}`);
177
+ say(`${pad}${c.dancer_id} ${c.display}${c.scope === "per-release" ? " [per-release id]" : ""} score ${c.score} name ${c.evidence.nameLevel} age ${c.evidence.age} school ${c.evidence.school} population ${c.evidence.population} region ${c.evidence.region}`);
178
+ if (c.region !== undefined)
179
+ say(`${pad} region ${c.region.region === null ? "-" : `${c.region.region} ${c.region.year}${c.region.regional ? " (regional)" : ""}`} attended ${c.region.attended} year(s)`);
173
180
  say(`${pad} ${c.rows} rows ${span(c.year_lo, c.year_hi)} born ${span(c.birth.lo, c.birth.hi)} (${c.birth.source}) majors girls ${c.population.girls} boys ${c.population.boys} school ${c.school === null ? "-" : `${c.school.school_id ?? "(no id)"} "${c.school.print}" ${span(c.school.year_lo, c.school.year_hi)}`}`);
174
181
  say(`${pad} prints ${c.prints.slice(0, 5).map((p) => `"${p.print}" (${p.rows}, ${p.level})`).join(", ")}${c.prints.length > 5 ? `, +${c.prints.length - 5} more` : ""}`);
175
182
  }
@@ -178,9 +185,11 @@ function printDancers(a, say) {
178
185
  if (a.age !== undefined)
179
186
  say(` age asked: ${a.age.read ?? "(unread)"}${a.age.birth !== null ? ` born ${a.age.birth.from ?? "?"}-${a.age.birth.to ?? "?"}` : ""}${a.age.note !== undefined ? ` (${a.age.note})` : ""}`);
180
187
  if (a.school !== undefined)
181
- say(` school asked: ${a.school.arm} ${a.school.ids.join(", ") || "-"}`);
188
+ say(` school asked: ${a.school.arm} ${a.school.ids.join(", ") || "-"}${a.school.lineage !== undefined && a.school.lineage.length > 0 ? ` (lineage: ${a.school.lineage.join(", ")})` : ""}`);
182
189
  if (a.population !== undefined)
183
190
  say(` population asked: ${a.population.read ?? "(unread)"}${a.population.note !== undefined ? ` (${a.population.note})` : ""}`);
191
+ if (a.oireachtas !== undefined)
192
+ say(` oireachtas asked: ${a.oireachtas.read ?? "(unread)"} ${a.oireachtas.year}${a.oireachtas.note !== undefined ? ` (${a.oireachtas.note})` : ""}`);
184
193
  for (const c of a.candidates)
185
194
  printCandidate(c, say);
186
195
  if (a.preferred !== undefined)
@@ -253,6 +262,14 @@ function jsonLines(text, check) {
253
262
  return out;
254
263
  }
255
264
  var checkComp = (q) => typeof q.feis !== "string" || q.feis.trim() === "" ? "a comp question needs feis" : q.code === undefined || q.code === null || String(q.code).trim() === "" ? "a comp question needs code" : null;
265
+ function oireachtasFlag(v) {
266
+ if (v === undefined)
267
+ return {};
268
+ const m = /^(.*?):(\d{4})$/.exec(v.trim());
269
+ return {
270
+ oireachtas: m === null ? { region: v.trim() } : { region: m[1], year: Number(m[2]) }
271
+ };
272
+ }
256
273
  var checkDancer = (q) => typeof q.name !== "string" || q.name.trim() === "" ? "a dancer question needs name" : null;
257
274
  var checkRegistration = (r) => typeof r.name !== "string" || r.name.trim() === "" ? "a registration row needs name" : typeof r.feis !== "string" || r.feis.trim() === "" ? "a registration row needs feis" : !Array.isArray(r.codes) ? "a registration row needs codes[]" : null;
258
275
  async function resolveVerb(rel, what, args, p, say, stdin) {
@@ -312,7 +329,8 @@ async function resolveVerb(rel, what, args, p, say, stdin) {
312
329
  ...p.flags.has("--school") ? { school: p.flags.get("--school") } : {},
313
330
  ...age !== undefined ? { age } : {},
314
331
  ...p.flags.has("--as-of") ? { asOf: p.flags.get("--as-of") } : {},
315
- ...p.flags.has("--population") ? { population: p.flags.get("--population") } : {}
332
+ ...p.flags.has("--population") ? { population: p.flags.get("--population") } : {},
333
+ ...oireachtasFlag(p.flags.get("--oireachtas"))
316
334
  }
317
335
  }
318
336
  ];
@@ -619,6 +637,7 @@ async function main(argv, io = {}) {
619
637
  ...age !== undefined ? { age } : {},
620
638
  ...p.flags.has("--as-of") ? { asOf: p.flags.get("--as-of") } : {},
621
639
  ...p.flags.has("--population") ? { population: p.flags.get("--population") } : {},
640
+ ...oireachtasFlag(p.flags.get("--oireachtas")),
622
641
  ...p.flags.has("--limit") ? { limit: Number(p.flags.get("--limit")) } : {}
623
642
  };
624
643
  const surname = p.flags.get("--surname");
package/dist/index.js CHANGED
@@ -6,6 +6,8 @@ import {
6
6
  unzstdTo2,
7
7
  NAME_LEVELS2,
8
8
  nameLevel2,
9
+ NO_CALENDAR2,
10
+ FeisFinder2,
9
11
  entryRank2,
10
12
  FEIS_LOCAL_ID_FLOOR2,
11
13
  ResultFinder2,
@@ -14,8 +16,6 @@ import {
14
16
  readPopulation2,
15
17
  canonicalOf2,
16
18
  CompResolver2,
17
- NO_CALENDAR2,
18
- FeisFinder2,
19
19
  StaffFinder2,
20
20
  SchoolResolver2,
21
21
  TeamFinder2,
@@ -45,7 +45,7 @@ import {
45
45
  openCached2,
46
46
  ReleaseManager2,
47
47
  releases2
48
- } from "./chunks/index-4ph8n4ga.js";
48
+ } from "./chunks/index-1sj5zr8k.js";
49
49
  import {
50
50
  cacheRoot2,
51
51
  repoOf2
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import type { OpenOptions } from "./release.js";
3
3
  import type { StreamQuery } from "./stream.js";
4
- export declare const USAGE = "usage: idq <verb> [--cache <dir>] [--repo <owner/name>] [--json]\n\n config repo <owner/name> set the user-local repository the releases come from\n list [--remote] the cached releases (size, files, indexes, the pin), and\n with --remote the published tags\n fetch [--release <spec>] [--assets results,majors,<file>...]\n download, verify and cache a release (default: results)\n pin <spec> the user-local pin: what a bare open (spec \"pinned\") opens\n unpin remove the pin (a bare open is then \"latest\")\n current the pin, and whether it is cached\n add <dir> --tag <tag> add a release built locally (its MANIFEST.json and files)\n to the cache under its tag\n index [--release <spec>] [<name>...] [--force]\n build the per-tag indexes (default: every one)\n verify [--release <spec>] re-hash every cached file against its receipt\n capabilities [--release <spec>] what the release carries of what the client reads, from\n its own schema: each feature full or missing (its calls\n refuse, naming the first cut that publishes it)\n prune [--keep N] [--dry-run] remove all but the N newest cached releases (default 1)\n and the pinned one\n rm <tag> [--dry-run] remove one cached release (not the pinned one)\n drop-indexes [<tag>] [--dry-run] remove the index files (of one release, or of every one)\n feis [--release <spec>] [--platform <p>] [--code <c>] [--id <id>] [--name <glob>] [--year <y>]\n the feises a query names\n series [--release <spec>] <name|host code|feis id>\n the series a name, a host code (fweb:ABC) or any feis id\n names: its years, hosts, codes and next instance\n school [--release <spec>] <print|school_id> [--region <code>]\n a school print or id to the published schools\n dancer [--release <spec>] <name> | --surname <s> | --like <glob> | --id <dancer_id>\n [--order surname-first|given-first] [--school <print|id>]\n [--yob <y> | --band <U10> [--year <y>] | --comp <competition>] [--as-of <date>]\n [--population girls|boys] [--rows]\n the dancers a name reaches, each with its evidence\n (name level, age, school, population) and the arm; the results of\n the one dancer where one answers (or --rows for each)\n results [--release <spec>] [--dancer <id>] [--feis <id>] [--comp <competition>]\n [--school <print|id>] [--year <y>] [--limit <n>]\n the published result rows, prints and ids\n majors [--release <spec>] [--event <e>] [--year <y>] [--comp <competition>] [--dancer <id>]\n the majors: every event and its years; an event's\n competitions; a competition's, an event's or a dancer's rows\n stream [--release <spec>] --scope majors|transcribed|platform|all [--entries] [--event <e,...>]\n [--year <y> | --year <from>..<to>] [--tier <t,...>] [--category <c,...>]\n [--level <feis_level,...>] [--dancer <id,...>] [--school <school_id>] [--primary]\n every row a scope holds, one JSON line each, never the\n whole table in memory; --entries one per address (the row\n it stands for, with its recall order)\n records [--release <spec>] [--dancer <id,...>]\n every dancer's record: display, appearances, birth window,\n populations\n records --years [--release <spec>] [--dancer <id,...>] [--year <y>]\n each dancer's region, nationals and place per year\n feis-dates [--release <spec>] every feis's first and last day\n tabulated [--release <spec>] [--min-round <n>]\n the majors addresses the marks sheets tabulate to round n\n or later (default 3)\n teams [--release <spec>] [--event <e>] [--year <y>] [--rosters | --joins]\n the team result rows of an event-year; with --rosters its\n roster teams and their dancers; with --joins how each roster\n team meets the results\n events [--release <spec>] [--event <e>] [--year <y>]\n each event-year's dates and place\n teacher [--release <spec>] <name|TD number>\n the staff lists' people: where they taught, the register,\n where they sat\n adjudicator [--release <spec>] <name|TD number>\n the register's and the panels' people: the same answer\n comp [--release <spec>] <feis> <code> [--year <y>] [--title <t>]\n a feis's own comp code to the comp it names, through that\n feis's syllabus (or the prior year's)\n syllabus [--release <spec>] <feis> [year]\n one feis-year's comps, in printed order\n syllabus [--release <spec>] --for <series> [year]\n a series' syllabus for a year: that year's where held,\n else the newest, saying which and what it is read from\n resolve comp [<feis> <code>] [--year <y>] [--title <t>]\n resolve dancer [<name>] [--school <s>] [--comp <feis>/<code> | --yob <y> | --band <b> --year <y>]\n [--order <o>] [--as-of <date>] [--population girls|boys]\n resolve registrations\n JSONL out, one answer per line; with no positional the\n questions are JSONL on stdin, one per line (a comp:\n {feis, code, year?, title?}, a dancer: {name, school?, age?,\n population?}, a registration: {name, school?, codes[], feis,\n year?, population?})\n\n <spec>: pinned (the default) | latest | a cut (42, 42.1) | a tag (archive-YYYY.MM.DD-N)";
4
+ export declare const USAGE = "usage: idq <verb> [--cache <dir>] [--repo <owner/name>] [--json]\n\n config repo <owner/name> set the user-local repository the releases come from\n list [--remote] the cached releases (size, files, indexes, the pin), and\n with --remote the published tags\n fetch [--release <spec>] [--assets results,majors,<file>...]\n download, verify and cache a release (default: results)\n pin <spec> the user-local pin: what a bare open (spec \"pinned\") opens\n unpin remove the pin (a bare open is then \"latest\")\n current the pin, and whether it is cached\n add <dir> --tag <tag> add a release built locally (its MANIFEST.json and files)\n to the cache under its tag\n index [--release <spec>] [<name>...] [--force]\n build the per-tag indexes (default: every one)\n verify [--release <spec>] re-hash every cached file against its receipt\n capabilities [--release <spec>] what the release carries of what the client reads, from\n its own schema: each feature full or missing (its calls\n refuse, naming the first cut that publishes it)\n prune [--keep N] [--dry-run] remove all but the N newest cached releases (default 1)\n and the pinned one\n rm <tag> [--dry-run] remove one cached release (not the pinned one)\n drop-indexes [<tag>] [--dry-run] remove the index files (of one release, or of every one)\n feis [--release <spec>] [--platform <p>] [--code <c>] [--id <id>] [--name <glob>] [--year <y>]\n the feises a query names\n series [--release <spec>] <name|host code|feis id>\n the series a name, a host code (fweb:ABC) or any feis id\n names: its years, hosts, codes and next instance\n school [--release <spec>] <print|school_id> [--region <code>]\n a school print or id to the published schools\n dancer [--release <spec>] <name> | --surname <s> | --like <glob> | --id <dancer_id>\n [--order surname-first|given-first] [--school <print|id>]\n [--yob <y> | --band <U10> [--year <y>] | --comp <competition>] [--as-of <date>]\n [--population girls|boys] [--oireachtas <region>[:<year>]] [--rows]\n the dancers a name reaches, each with its evidence\n (name level, age, school, population, region) and the arm;\n --oireachtas is the regional the roster is for, its region in\n the release's spelling (SRO, MAO), its year the --as-of year\n where none is given; the results of\n the one dancer where one answers (or --rows for each)\n results [--release <spec>] [--dancer <id>] [--feis <id>] [--comp <competition>]\n [--school <print|id>] [--year <y>] [--limit <n>]\n the published result rows, prints and ids\n majors [--release <spec>] [--event <e>] [--year <y>] [--comp <competition>] [--dancer <id>]\n the majors: every event and its years; an event's\n competitions; a competition's, an event's or a dancer's rows\n stream [--release <spec>] --scope majors|transcribed|platform|all [--entries] [--event <e,...>]\n [--year <y> | --year <from>..<to>] [--tier <t,...>] [--category <c,...>]\n [--level <feis_level,...>] [--dancer <id,...>] [--school <school_id>] [--primary]\n every row a scope holds, one JSON line each, never the\n whole table in memory; --entries one per address (the row\n it stands for, with its recall order)\n records [--release <spec>] [--dancer <id,...>]\n every dancer's record: display, appearances, birth window,\n populations\n records --years [--release <spec>] [--dancer <id,...>] [--year <y>]\n each dancer's region, nationals and place per year\n feis-dates [--release <spec>] every feis's first and last day\n tabulated [--release <spec>] [--min-round <n>]\n the majors addresses the marks sheets tabulate to round n\n or later (default 3)\n teams [--release <spec>] [--event <e>] [--year <y>] [--rosters | --joins]\n the team result rows of an event-year; with --rosters its\n roster teams and their dancers; with --joins how each roster\n team meets the results\n events [--release <spec>] [--event <e>] [--year <y>]\n each event-year's dates and place\n teacher [--release <spec>] <name|TD number>\n the staff lists' people: where they taught, the register,\n where they sat\n adjudicator [--release <spec>] <name|TD number>\n the register's and the panels' people: the same answer\n comp [--release <spec>] <feis> <code> [--year <y>] [--title <t>]\n a feis's own comp code to the comp it names, through that\n feis's syllabus (or the prior year's)\n syllabus [--release <spec>] <feis> [year]\n one feis-year's comps, in printed order\n syllabus [--release <spec>] --for <series> [year]\n a series' syllabus for a year: that year's where held,\n else the newest, saying which and what it is read from\n resolve comp [<feis> <code>] [--year <y>] [--title <t>]\n resolve dancer [<name>] [--school <s>] [--comp <feis>/<code> | --yob <y> | --band <b> --year <y>]\n [--order <o>] [--as-of <date>] [--population girls|boys]\n [--oireachtas <region>[:<year>]]\n resolve registrations\n JSONL out, one answer per line; with no positional the\n questions are JSONL on stdin, one per line (a comp:\n {feis, code, year?, title?}, a dancer: {name, school?, age?,\n population?, oireachtas?}, a registration: {name, school?,\n codes[], feis, year?, population?, oireachtas?})\n\n <spec>: pinned (the default) | latest | a cut (42, 42.1) | a tag (archive-YYYY.MM.DD-N)";
5
5
  export interface Parsed {
6
6
  positional: string[];
7
7
  flags: Map<string, string>;
@@ -20,11 +20,17 @@ export interface DancerQuery {
20
20
  school?: string;
21
21
  age?: AgeAsked;
22
22
  asOf?: string;
23
+ oireachtas?: OireachtasAsked;
23
24
  population?: string;
24
25
  }
26
+ export interface OireachtasAsked {
27
+ region: string;
28
+ year?: number;
29
+ }
30
+ export type RegionEvidence = "same" | "moved" | "contradicts" | "unknown" | "unasked";
25
31
  export type AgeEvidence = "fits" | "overlaps" | "contradicts" | "unknown" | "unasked";
26
32
  export type PopulationEvidence = "same" | "contradicts" | "both" | "unknown" | "unasked";
27
- export type SchoolEvidence = "same" | "moved" | "none" | "unresolved" | "unasked";
33
+ export type SchoolEvidence = "same" | "lineage" | "moved" | "none" | "unresolved" | "unasked";
28
34
  export interface DancerSchool {
29
35
  school_id: string | null;
30
36
  print: string;
@@ -43,6 +49,7 @@ export interface DancerCandidate {
43
49
  age: AgeEvidence;
44
50
  school: SchoolEvidence;
45
51
  population: PopulationEvidence;
52
+ region: RegionEvidence;
46
53
  };
47
54
  prints: {
48
55
  print: string;
@@ -63,10 +70,17 @@ export interface DancerCandidate {
63
70
  boys: number;
64
71
  };
65
72
  school: DancerSchool | null;
73
+ region?: DancerRegion;
74
+ }
75
+ export interface DancerRegion {
76
+ year: number | null;
77
+ region: string | null;
78
+ regional: boolean;
79
+ attended: number;
66
80
  }
67
81
  export interface ExcludedCandidate {
68
82
  candidate: DancerCandidate;
69
- why: "age" | "population";
83
+ why: "age" | "population" | "region";
70
84
  }
71
85
  export interface DancerAnswer {
72
86
  tag: string;
@@ -95,9 +109,16 @@ export interface DancerAnswer {
95
109
  read: "girls" | "boys" | null;
96
110
  note?: string;
97
111
  };
112
+ oireachtas?: {
113
+ asked: string;
114
+ read: string | null;
115
+ year: number;
116
+ note?: string;
117
+ };
98
118
  school?: {
99
119
  arm: string;
100
120
  ids: string[];
121
+ lineage?: string[];
101
122
  };
102
123
  refused?: string;
103
124
  }
@@ -163,7 +184,10 @@ export interface DancerYear {
163
184
  export declare class DancerResolver {
164
185
  private readonly rel;
165
186
  private readonly printIds;
187
+ private lineageOf;
188
+ private regions;
166
189
  constructor(rel: Release);
190
+ private lineage;
167
191
  records(q?: {
168
192
  ids?: readonly number[];
169
193
  }): Promise<{
@@ -182,6 +206,7 @@ export declare class DancerResolver {
182
206
  private capped;
183
207
  get(dancerId: number): Promise<DancerRecord | null>;
184
208
  private answer;
209
+ private readOireachtas;
185
210
  private schoolEvidence;
186
211
  private readAge;
187
212
  }
@@ -1,6 +1,6 @@
1
1
  export { type AssetPlan, DATABASES, type DatabaseName, type LandedFile, type Receipt, type ReleaseManifest, planAsset, } from "./assets.js";
2
2
  export { sha256File, unzstdTo } from "./bytes.js";
3
- export { type AgeAsked, type AgeEvidence, type DancerAnswer, type DancerBulkRecord, type DancerCandidate, type DancerQuery, type DancerRecord, DancerResolver, type DancerSchool, type DancerSearchQuery, type DancerYear, type ExcludedCandidate, FEIS_LOCAL_ID_FLOOR, type PopulationEvidence, type SchoolEvidence, readPopulation, } from "./dancers.js";
3
+ export { type AgeAsked, type AgeEvidence, type DancerAnswer, type DancerBulkRecord, type DancerCandidate, type DancerQuery, type DancerRecord, DancerResolver, type DancerRegion, type DancerSchool, type DancerSearchQuery, type DancerYear, type ExcludedCandidate, FEIS_LOCAL_ID_FLOOR, type OireachtasAsked, type PopulationEvidence, type RegionEvidence, type SchoolEvidence, readPopulation, } from "./dancers.js";
4
4
  export { NAME_LEVELS, type NameLevel, nameLevel } from "./dancers-names.js";
5
5
  export { type Comp, type CompAnswer, type CompDance, type CompKind, type CompQuery, CompResolver, type CompSource, type FeisPlace, type SyllabusAnswer, type SyllabusForAnswer, type SyllabusSource, type TitleCheck, canonicalOf, } from "./comps.js";
6
6
  export { type ClientOptions, cacheRoot, repoOf } from "./config.js";
@@ -0,0 +1,2 @@
1
+ export declare function printsAbsence(print: string): boolean;
2
+ export declare function notOnTheFloor(placePrinted: string | null, atts: string | null, withdrawn: string | null): boolean;
@@ -1,5 +1,5 @@
1
1
  import type { CompAnswer, CompResolver } from "./comps.js";
2
- import { type DancerAnswer, type DancerQuery } from "./dancers.js";
2
+ import { type DancerAnswer, type DancerQuery, type OireachtasAsked } from "./dancers.js";
3
3
  import type { Release } from "./release.js";
4
4
  export interface RegistrationRow {
5
5
  name: string;
@@ -11,6 +11,7 @@ export interface RegistrationRow {
11
11
  titles?: Readonly<Record<string, string>>;
12
12
  asOf?: string;
13
13
  population?: string;
14
+ oireachtas?: OireachtasAsked;
14
15
  [k: string]: unknown;
15
16
  }
16
17
  export type DancerQueryLike = DancerQuery;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finbheara/archive-client",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "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",
5
5
  "keywords": ["irish-dance", "results", "release", "cache", "sqlite"],
6
6
  "license": "MIT",