@finbheara/archive-client 0.1.2 → 0.3.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
@@ -20,7 +20,7 @@ type declarations (`.d.ts`), so a TypeScript project needs Bun's types (`"types"
20
20
  `bunx idq <verb>`.
21
21
 
22
22
  The version is 0.x while the API settles: a minor bump may break, so pin a minor
23
- (`"@finbheara/archive-client": "0.1.x"`).
23
+ (`"@finbheara/archive-client": "0.2.x"`).
24
24
 
25
25
  ## Configuration
26
26
 
@@ -67,6 +67,25 @@ rel.path("school-registry.jsonl"); // its cach
67
67
  rel.close();
68
68
  ```
69
69
 
70
+ An older release that lacks what a call reads (a column, a table or a side file a newer release
71
+ added) is REFUSED BY NAME, never read in part: the call throws, naming the feature, what the
72
+ release lacks and the first cut that publishes it. What a release carries is read from its own
73
+ schema and assets, never from its tag:
74
+
75
+ ```ts
76
+ const c = await rel.capabilities();
77
+ c.features; // [{ feature: "dancers", state: "full" | "missing", missing: [...], since: 260, calls }]
78
+ ```
79
+
80
+ | feature | the calls | from cut |
81
+ | --- | --- | --- |
82
+ | `results.level` | `results.query`, `majors.results` | 215 |
83
+ | `dancers` | `dancers.resolve`, `search`, `get` | 260 |
84
+ | `schools` | `schools.resolve`, a dancer question with a school | 260 |
85
+ | `staff` | `staff.teacher`, `staff.adjudicator` | 260 |
86
+ | `majors.order` | `majors.competitions` | 260 |
87
+ | `feises`, `comps` | `feises.find`, `feises.series`, `comps.*`, `registrations.resolve` | 265 |
88
+
70
89
  A spec is `pinned` (the default: the user-local pin, else the latest), `latest`, a cut number
71
90
  (`42`, or `42.1` for a fraction) or a tag (`archive-YYYY.MM.DD-N`).
72
91
 
@@ -182,10 +201,11 @@ const r = await rel.dancers.resolve({
182
201
  school: "Ardvarna Academy", // a print or an id, through rel.schools
183
202
  age: { band: "U10", year: 2026 }, // or { yob: 2016 }, or { comp: "<competition>" }
184
203
  asOf: "2026-10-17", // the school as of then
204
+ population: "girls", // or "boys": only a contradiction excludes
185
205
  });
186
206
  r.arm; // "unique" | "ambiguous" | "none"
187
207
  r.candidates; // [{ dancer_id, display, scope, score, evidence: { nameLevel, age, school }, prints, ... }]
188
- r.excluded; // the candidates an age contradiction set aside (why: "age")
208
+ r.excluded; // the candidates a contradiction set aside (why: "age" | "population")
189
209
  r.preferred; // NON-BINDING: { dancer_id, why } where the school singles one out; never moves arm
190
210
  r.unlinked; // rows printing a matched name that the release linked to no dancer
191
211
 
@@ -202,8 +222,21 @@ meets the asked age (`Age.fitsIn`, `overlaps`): `fits`, `overlaps`, `contradicts
202
222
  `school` is how the dancer's schools meet the asked one: `same` (the latest school as of `asOf`),
203
223
  `moved` (an earlier one), `none`, `unresolved` (the print reached no school).
204
224
 
225
+ `population` is how the dancer's published majors population meets the asked one: `same`,
226
+ `contradicts`, `both`, `unknown`, `unasked`. A dancer's population is read only from its rows at
227
+ the majors (tiers T1 to T3) in competitions whose published `gender` reads girls or boys
228
+ (`@finbheara/domain`'s `populationIn`), whatever the competition's category (a solo championship
229
+ often publishes none), counted on the candidate (`population: { girls, boys }`). The results hold
230
+ individual dancers' rows only (a team is published apart), so every such row is a solo row. A feis
231
+ competition is no evidence either way (below open a feis runs its competitions mixed and may merge
232
+ its few boys into the girls' group), and a mixed competition reads `mixed`.
233
+
205
234
  NO SILENT GUESS. Only a contradiction sets a candidate aside: a birth window that shares no birth
206
- year with the asked age (listed in `excluded`). The school is evidence and order, never a reason
235
+ year with the asked age, or a dancer the release publishes in the other population's gendered
236
+ majors solo competitions and in none of the asked one's (listed in `excluded`, `why: "age"` or
237
+ `"population"`, and a candidate contradicting both says `age`). A dancer published in both, or in
238
+ neither, stays. A population word that names no one population (`mixed`) narrows nothing, and the
239
+ answer says so (`r.population.note`). The school is evidence and order, never a reason
207
240
  to drop anyone: candidates whose latest school is the asked one come first, then those that ever
208
241
  danced for it, then the rest. A school row meets the asked school by its id, else by its print
209
242
  (one `rel.schools` resolves to the asked ids), so a latest print the release
@@ -216,6 +249,10 @@ A FEIS-LOCAL ID (`FEIS_LOCAL_ID_FLOOR`, 2^34, and up) is valid in its release on
216
249
  carrying one say `scope: "per-release"` (`dancer_scope` on a row), so a caller never stores one
217
250
  across releases. Every answer carries the release tag.
218
251
 
252
+ A MAJORS ID (under the floor) is kept across releases on a best-effort basis only: a merge
253
+ retires one, a split mints one, and a renumbered source page can hand one to another dancer. A
254
+ caller that joins ids across releases checks the name and the rows it expects.
255
+
219
256
  ## Results and the majors
220
257
 
221
258
  ```ts
@@ -251,6 +288,8 @@ c.source; // "syllabus" | "prior-year" | "title"
251
288
  c.kind; // "championship" | "grade" | "unrated"
252
289
  c.level; // "OC" (FF, BG, AB, NV, PW, PC, OC), c.feis_level 700
253
290
  c.age; // "U10", c.population "mixed", c.canonical "710OC", c.title, c.competition
291
+ c.age_source; // "printed" | "title" | "code" | "merged" | null, c.age_printed "U10" as printed
292
+ c.second; // true for a second chance or a supplementary comp beside the main one
254
293
  c.from; // { feis_ids: ["fweb:ABC25"], year: 2025 } where an earlier year answered
255
294
  c.check; // "unchecked", or what the caller's `title` says about the earlier comp
256
295
  c.syllabus_source; // "results": what the syllabus the answer read is read from
@@ -293,6 +332,21 @@ QuickFeis numbers its own and-over championships (`15 & over` prelim `616PC`, `1
293
332
  `719OC`). An and-over code is then the same string as an `Under N + 1` one, and `age` (`O18`)
294
333
  tells them apart. An adult band, a range and a grade render none.
295
334
 
335
+ `age` is the release's reading, and `age_source` says where it was read. A `merged` comp (a
336
+ platform's comps merged into one, `710OCM` with no `710OC` beside it) prints one member's band,
337
+ so its `age` is the union of its members' (`U12` for `710OCM - U10 Open Champion` where the next
338
+ open the feis prints is `713OCM`), and `age_printed` keeps the platform's own field (`U10`). A
339
+ joined code at the bottom of its series stays open below (`710OC/711OC` with no open under 10
340
+ beside it is `U11/11yr`, every age under 11), as a younger dancer dances up into it.
341
+
342
+ `population` is the sex the release states, else a population word in the title, else the
343
+ code's sex suffix read with every code its feis printed (`codePopulation` from
344
+ `@finbheara/domain/title`: `720OCB` beside `720OCG` is boys and girls, `512M` beside `512` is
345
+ boys, a lone `713OCM` says nothing), else the release's `mixed` for a grade or a preliminary
346
+ championship. `second` is read from the words of the title and the platform's level and dance
347
+ fields (`secondIn`: `2nd Chance`, `2nd Chc`, `Second Chance`, `Supplementary Prelim`, `2nd Open
348
+ Champ`), and never from a code's digit, which marks a round's half at some hosts (`610PC2H`).
349
+
296
350
  ## Registrations
297
351
 
298
352
  ```ts
@@ -301,10 +355,13 @@ const rows = await rel.registrations.resolve([
301
355
  ]);
302
356
  rows[0].comps; // one competition answer per code
303
357
  rows[0].age; // { band: "U10", year: 2026, code: "610", from: "comp" }: the band the dancer is asked at
358
+ rows[0].population; // { asked: "girls", from: "comp", codes: ["610"] }: who the dancer is asked as
304
359
  rows[0].dancer; // the dancer resolver's answer
305
360
  ```
306
361
 
307
- Each code is resolved as above. The dancer is asked at the YOUNGEST band the row's competitions
362
+ Each code is resolved as above. The dancer is asked at the row's own `population` where it gives
363
+ one, else at the one population its unique comps read (`from: "comp"`, with the codes). Comps
364
+ reading both populations give none, with the reason. The dancer is asked at the YOUNGEST band the row's competitions
308
365
  read (the lowest ceiling, then the highest floor): a dancer may dance up into an older band and
309
366
  never down, so the youngest bounds the dancer. Every band a row read is listed (`bands`) where
310
367
  there are several. Where no comp answers with a band (a syllabus read from results that lacks the
@@ -323,6 +380,7 @@ and the competitions still resolve.
323
380
  idq add <dir> --tag <tag>
324
381
  idq index [--release <spec>] [<name>...] [--force]
325
382
  idq verify [--release <spec>]
383
+ idq capabilities [--release <spec>]
326
384
  idq prune [--keep N] [--dry-run]
327
385
  idq rm <tag> [--dry-run]
328
386
  idq drop-indexes [<tag>] [--dry-run]
@@ -331,7 +389,8 @@ and the competitions still resolve.
331
389
  idq school [--release <spec>] <print|school_id> [--region <code>]
332
390
  idq dancer [--release <spec>] <name> | --surname <s> | --like <glob> | --id <dancer_id>
333
391
  [--order surname-first|given-first] [--school <print|id>]
334
- [--yob <y> | --band <band> [--year <y>] | --comp <competition>] [--as-of <date>] [--rows]
392
+ [--yob <y> | --band <band> [--year <y>] | --comp <competition>] [--as-of <date>]
393
+ [--population girls|boys] [--rows]
335
394
  idq results [--release <spec>] [--dancer <id>] [--feis <id>] [--comp <c>] [--school <s>] [--year <y>] [--limit <n>]
336
395
  idq majors [--release <spec>] [--event <e>] [--year <y>] [--comp <c>] [--dancer <id>] [--rows]
337
396
  idq teacher [--release <spec>] <name|TD number>
@@ -348,7 +407,7 @@ and the competitions still resolve.
348
407
 
349
408
  `resolve` speaks JSON lines: one answer per line out, and with no positional question the
350
409
  questions are JSON lines on stdin, one per line (`{feis, code, year?, title?}`,
351
- `{name, school?, age?}`, `{name, school?, codes, feis, year?}`). A line that is no JSON object,
410
+ `{name, school?, age?, population?}`, `{name, school?, codes, feis, year?, population?}`). A line that is no JSON object,
352
411
  or no question of its kind, answers `{tag, line, error}` and the other lines still answer. The
353
412
  exit is non-zero where any line erred. A dancer's age may be given as a comp
354
413
  (`{"comp": "<feis>/<code>"}`, or `--comp` on the command line): `rel.registrations.dancer` reads
@@ -71,6 +71,7 @@ import { Age } from "@finbheara/domain/age";
71
71
  import { createIndex, indexSchema, personEncoder } from "@finbheara/domain/noun-index";
72
72
  import domainPkg from "@finbheara/domain/package.json";
73
73
  import { dancerName } from "@finbheara/domain/person";
74
+ import { populationIn, populationRelation } from "@finbheara/domain/title";
74
75
  import {
75
76
  foldNameKey,
76
77
  surnameBlock
@@ -160,6 +161,7 @@ class ResultFinder2 {
160
161
  }
161
162
  async query(q) {
162
163
  await this.rel.fetch("results");
164
+ await this.rel.require("results.level");
163
165
  return queryResults(this.rel, "results", q);
164
166
  }
165
167
  }
@@ -188,6 +190,7 @@ class MajorsFinder2 {
188
190
  }
189
191
  async competitions(q) {
190
192
  const db = await this.db();
193
+ await this.rel.require("majors.order");
191
194
  const args = [q.event];
192
195
  let year = "";
193
196
  if (q.year !== undefined) {
@@ -207,6 +210,7 @@ class MajorsFinder2 {
207
210
  }
208
211
  async results(q) {
209
212
  await this.db();
213
+ await this.rel.require("results.level");
210
214
  return queryResults(this.rel, "majors", q);
211
215
  }
212
216
  }
@@ -223,7 +227,7 @@ var PERSON = { table: "person", value: "id", valueType: "INTEGER PRIMARY KEY" };
223
227
  var ASKED = ["surname_key", "nn4", "blk_surname"];
224
228
  var dancersIndex = {
225
229
  name: "dancers",
226
- version: "1",
230
+ version: "2",
227
231
  inputs: ["results"],
228
232
  keyParts: () => [...PACKAGE_KEY_PARTS],
229
233
  says: "every published dancer print at the names package's keys, its dancers, schools and years",
@@ -233,6 +237,7 @@ var dancersIndex = {
233
237
  out.run("CREATE TABLE print_dancer(person INTEGER NOT NULL, dancer_id INTEGER, rows INTEGER NOT NULL, year_lo INTEGER, year_hi INTEGER)");
234
238
  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)");
235
239
  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)");
240
+ 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");
236
241
  const src = rel.db("results");
237
242
  const groups = src.query(`SELECT r.name_printed AS p, r.name_order_resolved AS o, r.dancer_id AS d,
238
243
  r.school_printed AS sp, r.school_id AS s,
@@ -298,6 +303,15 @@ var dancersIndex = {
298
303
  const t = dt.get(d.dancer_id);
299
304
  putD.run(d.dancer_id, d.display, d.names_printed, d.genders, d.appearances, d.scope, d.birth_lo, d.birth_hi, d.birth_source, d.birth_conflict, t?.rows ?? 0, t?.lo ?? null, t?.hi ?? null);
300
305
  }
306
+ const putPop = out.prepare("INSERT INTO dancer_population VALUES (?, ?, ?) ON CONFLICT(dancer_id, population) DO UPDATE SET rows = rows + excluded.rows");
307
+ for (const r of src.query(`SELECT r.dancer_id AS d, c.gender AS g, count(*) AS n
308
+ FROM results r JOIN competitions c ON c.file = r.file AND c.line = r.competition_line
309
+ WHERE r.dancer_id IS NOT NULL AND c.tier IN ('T1', 'T2', 'T3') AND c.gender IS NOT NULL
310
+ GROUP BY 1, 2`).all()) {
311
+ const pop = populationIn(r.g);
312
+ if (pop === "girls" || pop === "boys")
313
+ putPop.run(r.d, pop, r.n);
314
+ }
301
315
  for (const s of schema.indexes)
302
316
  if (ASKED.some((c) => s.endsWith(`(${c})`)))
303
317
  out.run(s);
@@ -337,6 +351,13 @@ var SCHOOL_RANK = {
337
351
  unresolved: 2,
338
352
  unasked: 2
339
353
  };
354
+ var POPULATION_WEIGHT = {
355
+ same: 1,
356
+ both: 0,
357
+ unknown: 0,
358
+ unasked: 0,
359
+ contradicts: -10
360
+ };
340
361
  var SCHOOL_WEIGHT = {
341
362
  same: 2,
342
363
  moved: 1,
@@ -418,6 +439,7 @@ class DancerResolver2 {
418
439
  year_lo: d.year_lo,
419
440
  year_hi: d.year_hi,
420
441
  birth: birthOf(d),
442
+ population: populationOf(db, dancerId),
421
443
  prints,
422
444
  schools: schoolsOf(db, dancerId)
423
445
  };
@@ -425,6 +447,7 @@ class DancerResolver2 {
425
447
  async answer(db, q, hits, narrow = true) {
426
448
  const asOf = yearOf(q.asOf);
427
449
  const age = q.age === undefined ? undefined : this.readAge(q.age, asOf);
450
+ const population = q.population === undefined ? undefined : readPopulation2(q.population);
428
451
  let schoolIds = null;
429
452
  let school;
430
453
  if (q.school !== undefined) {
@@ -455,19 +478,22 @@ class DancerResolver2 {
455
478
  const schools = schoolsOf(db, id);
456
479
  const latest = latestSchool(schools, asOf);
457
480
  const ageEv = ageEvidence(d, age?.age ?? null);
481
+ const pop = populationOf(db, id);
482
+ const popEv = populationEvidence(pop, population?.read ?? null);
458
483
  const schoolEv = schoolIds === null ? "unasked" : schoolIds.length === 0 ? "unresolved" : await this.schoolEvidence(schools, schoolIds, asOf);
459
484
  all.push({
460
485
  dancer_id: id,
461
486
  display: d.display,
462
487
  scope: id >= FEIS_LOCAL_ID_FLOOR2 ? "per-release" : "stable",
463
488
  kind: d.kind,
464
- score: NAME_WEIGHT[e.level] + AGE_WEIGHT[ageEv] + SCHOOL_WEIGHT[schoolEv],
465
- evidence: { nameLevel: e.level, age: ageEv, school: schoolEv },
489
+ score: NAME_WEIGHT[e.level] + AGE_WEIGHT[ageEv] + SCHOOL_WEIGHT[schoolEv] + POPULATION_WEIGHT[popEv],
490
+ evidence: { nameLevel: e.level, age: ageEv, school: schoolEv, population: popEv },
466
491
  prints: e.prints.sort((a, b) => b.rows - a.rows || (a.print < b.print ? -1 : 1)),
467
492
  rows: d.rows,
468
493
  year_lo: d.year_lo,
469
494
  year_hi: d.year_hi,
470
495
  birth: birthOf(d),
496
+ population: pop,
471
497
  school: latest
472
498
  });
473
499
  }
@@ -476,9 +502,12 @@ class DancerResolver2 {
476
502
  let kept = all;
477
503
  if (narrow)
478
504
  kept = all.filter((c) => {
479
- if (c.evidence.age !== "contradicts")
505
+ if (c.evidence.age === "contradicts")
506
+ excluded.push({ candidate: c, why: "age" });
507
+ else if (c.evidence.population === "contradicts")
508
+ excluded.push({ candidate: c, why: "population" });
509
+ else
480
510
  return true;
481
- excluded.push({ candidate: c, why: "age" });
482
511
  return false;
483
512
  });
484
513
  let preferred;
@@ -503,6 +532,7 @@ class DancerResolver2 {
503
532
  ...preferred !== undefined ? { preferred } : {},
504
533
  unlinked: [...unlinked].map(([print, rows]) => ({ print, rows })).sort((a, b) => b.rows - a.rows || (a.print < b.print ? -1 : 1)),
505
534
  ...age !== undefined ? { age: age.answer } : {},
535
+ ...population !== undefined ? { population } : {},
506
536
  ...school !== undefined ? { school } : {}
507
537
  };
508
538
  }
@@ -580,6 +610,34 @@ function latestSchool(schools, asOf) {
580
610
  }
581
611
  return best;
582
612
  }
613
+ function populationOf(db, dancerId) {
614
+ const out = { girls: 0, boys: 0 };
615
+ for (const r of db.query("SELECT population, rows FROM dancer_population WHERE dancer_id = ?").all(dancerId))
616
+ if (r.population === "girls" || r.population === "boys")
617
+ out[r.population] = r.rows;
618
+ return out;
619
+ }
620
+ function readPopulation2(asked) {
621
+ const w = asked.trim().toLowerCase();
622
+ const read = w === "girls" || w === "boys" ? w : populationIn(w);
623
+ if (read === "girls" || read === "boys")
624
+ return { asked, read };
625
+ return {
626
+ asked,
627
+ read: null,
628
+ note: read === "mixed" ? `"${asked}" reads as mixed: it names no one population, so it narrows nothing` : `"${asked}" reads as no population (girls or boys): it narrows nothing`
629
+ };
630
+ }
631
+ function populationEvidence(own, asked) {
632
+ if (asked === null)
633
+ return "unasked";
634
+ if (own.girls > 0 && own.boys > 0)
635
+ return "both";
636
+ const mine = own.girls > 0 ? "girls" : own.boys > 0 ? "boys" : "";
637
+ if (mine === "")
638
+ return "unknown";
639
+ return populationRelation(asked, mine) === "disjoint" ? "contradicts" : "same";
640
+ }
583
641
  function ageEvidence(d, asked) {
584
642
  if (asked === null)
585
643
  return "unasked";
@@ -597,9 +655,11 @@ function ageEvidence(d, asked) {
597
655
 
598
656
  import { Age as Age2 } from "@finbheara/domain/age";
599
657
  import {
658
+ codePopulation,
600
659
  compTitle,
601
660
  matchTitle,
602
- populationIn,
661
+ populationIn as populationIn2,
662
+ secondIn,
603
663
  titleRelation
604
664
  } from "@finbheara/domain/title";
605
665
  var LEVEL = Object.freeze({
@@ -616,17 +676,25 @@ CREATE TABLE comp(
616
676
  competition TEXT PRIMARY KEY, feis_id TEXT NOT NULL, brand_id TEXT, year INTEGER,
617
677
  code TEXT NOT NULL, code_fold TEXT NOT NULL, title TEXT, age TEXT, gender TEXT,
618
678
  feis_level INTEGER, round TEXT, platform TEXT NOT NULL, captures INTEGER NOT NULL,
619
- file TEXT NOT NULL, line INTEGER NOT NULL
679
+ file TEXT NOT NULL, line INTEGER NOT NULL, age_source TEXT, age_printed TEXT,
680
+ gender_source TEXT, level_printed TEXT, dance_printed TEXT
620
681
  );
621
682
  `;
622
683
  var INDEXES = [
623
684
  "CREATE INDEX comp_feis_code ON comp(feis_id, code_fold)",
624
685
  "CREATE INDEX comp_brand_year ON comp(brand_id, year)"
625
686
  ];
687
+ var OPTIONAL = [
688
+ "age_source",
689
+ "age_printed",
690
+ "gender_source",
691
+ "level_printed",
692
+ "dance_printed"
693
+ ];
626
694
  var fold = (code) => code.trim().toLowerCase();
627
695
  var compsIndex = {
628
696
  name: "comps",
629
- version: "1",
697
+ version: "2",
630
698
  inputs: ["results"],
631
699
  says: "every competition by its feis and its own code, with its series and year",
632
700
  build(rel, out) {
@@ -639,19 +707,23 @@ var compsIndex = {
639
707
  const captures = new Map;
640
708
  for (const r of src.query("SELECT competition, count(*) AS n FROM competitions GROUP BY competition").all())
641
709
  captures.set(r.competition, r.n);
642
- const put = out.prepare("INSERT OR IGNORE INTO comp VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
643
- for (const c of src.query("SELECT competition, event, year, comp_no, title, age, gender, feis_level, round, source, file, line FROM competitions WHERE comp_no IS NOT NULL AND trim(comp_no) != '' ORDER BY file, line").iterate()) {
710
+ const put = out.prepare("INSERT OR IGNORE INTO comp VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
711
+ const has = new Set(src.query("PRAGMA table_info(competitions)").all().map((r) => r.name));
712
+ const opt = (col) => has.has(col) ? col : `NULL AS ${col}`;
713
+ for (const c of src.query(`SELECT competition, event, year, comp_no, title, age, gender, feis_level, round, source, file, line, ${OPTIONAL.map(opt).join(", ")} FROM competitions WHERE comp_no IS NOT NULL AND trim(comp_no) != '' ORDER BY file, line`).iterate()) {
644
714
  const f = brand.get(c.event);
645
- 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);
715
+ 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);
646
716
  }
647
717
  for (const s of INDEXES)
648
718
  out.run(s);
649
719
  }
650
720
  };
651
- function compOf(r) {
721
+ function compOf(r, codes = []) {
652
722
  const lv = r.feis_level;
653
723
  const kind = lv === null ? "unrated" : lv >= 600 ? "championship" : "grade";
654
- const pop = populationIn(r.gender ?? "") || populationIn(r.title ?? "");
724
+ const stated = r.gender_source === "level" ? "" : populationIn2(r.gender ?? "");
725
+ const pop = stated || populationIn2(r.title ?? "") || (r.platform === "ifeis" && codes.length > 0 ? codePopulation(r.code, codes) : "") || populationIn2(r.gender ?? "");
726
+ const second = [r.title, r.level_printed, r.dance_printed].some((t) => t != null && secondIn(t));
655
727
  const round = r.round ?? (r.title === null ? null : compTitle(r.title).round?.toString() ?? null);
656
728
  return {
657
729
  competition: r.competition,
@@ -663,7 +735,10 @@ function compOf(r) {
663
735
  feis_level: lv,
664
736
  level: lv === null ? null : LEVEL[lv] ?? null,
665
737
  age: r.age,
738
+ age_source: r.age_source ?? null,
739
+ age_printed: r.age_printed ?? null,
666
740
  population: pop === "" ? null : pop,
741
+ second,
667
742
  round,
668
743
  canonical: canonicalOf2(lv, r.age),
669
744
  platform: r.platform,
@@ -770,7 +845,12 @@ class CompResolver2 {
770
845
  const marks = feisIds.map(() => "?").join(", ");
771
846
  const sql = `SELECT * FROM comp WHERE feis_id IN (${marks})${code !== undefined ? " AND code_fold = ?" : ""} ORDER BY file, line`;
772
847
  const args = code !== undefined ? [...feisIds, fold(code)] : [...feisIds];
773
- return db.query(sql).all(...args).map(compOf);
848
+ const rows = db.query(sql).all(...args);
849
+ const all = code === undefined ? rows : db.query(`SELECT feis_id, code FROM comp WHERE feis_id IN (${marks})`).all(...feisIds);
850
+ const codes = new Map;
851
+ for (const r of all)
852
+ codes.set(r.feis_id, [...codes.get(r.feis_id) ?? [], r.code]);
853
+ return rows.map((r) => compOf(r, codes.get(r.feis_id) ?? []));
774
854
  }
775
855
  async idsOf(brand, year) {
776
856
  return (await this.ofSeries("", brand, year, "series")).feis_ids;
@@ -1724,13 +1804,31 @@ class RegistrationResolver2 {
1724
1804
  from: "comp"
1725
1805
  } : byTitle !== undefined ? { band: byTitle.age, year: byTitle.year, code: byTitle.code, from: "title" } : null;
1726
1806
  const bands = [...new Set((first !== undefined ? read : printed).map((c) => c.age))];
1807
+ let population = null;
1808
+ if (row.population !== undefined)
1809
+ population = { asked: row.population, from: "row" };
1810
+ else {
1811
+ const gendered = comps.flatMap((c, i) => c.arm === "unique" && (c.population === "girls" || c.population === "boys") ? [{ pop: c.population, code: row.codes[i] }] : []);
1812
+ const pops = [...new Set(gendered.map((g) => g.pop))];
1813
+ if (pops.length === 1)
1814
+ population = {
1815
+ asked: pops[0],
1816
+ from: "comp",
1817
+ codes: gendered.map((g) => g.code)
1818
+ };
1819
+ else if (pops.length > 1)
1820
+ population = {
1821
+ why: `the row's comps read both populations (${gendered.map((g) => `${g.code}: ${g.pop}`).join(", ")}): none asked`
1822
+ };
1823
+ }
1727
1824
  const order = nameOrderOf(row.order);
1728
1825
  const asked = {
1729
1826
  name: row.name,
1730
1827
  ...order !== undefined ? { order } : {},
1731
1828
  ...row.school !== undefined ? { school: row.school } : {},
1732
1829
  ...age !== null ? { age: { band: age.band, year: age.year } } : {},
1733
- ...row.asOf !== undefined ? { asOf: row.asOf } : {}
1830
+ ...row.asOf !== undefined ? { asOf: row.asOf } : {},
1831
+ ...population !== null && "asked" in population ? { population: population.asked } : {}
1734
1832
  };
1735
1833
  const { dancer } = await this.dancer(asked);
1736
1834
  out.push({
@@ -1739,6 +1837,7 @@ class RegistrationResolver2 {
1739
1837
  comps,
1740
1838
  age,
1741
1839
  ...bands.length > 1 ? { bands } : {},
1840
+ population,
1742
1841
  dancer,
1743
1842
  asked
1744
1843
  });
@@ -1903,6 +2002,88 @@ function defaultRemote() {
1903
2002
  return installed ?? fallbackRemote2(ghRemote2, httpsRemote2());
1904
2003
  }
1905
2004
 
2005
+ function columnsOf2(db, table) {
2006
+ return new Set(db.query("SELECT name FROM pragma_table_info(?)").all(table).map((r) => r.name));
2007
+ }
2008
+ var FEATURES2 = [
2009
+ {
2010
+ feature: "results.level",
2011
+ needs: ["results:competitions.feis_level"],
2012
+ since: 215,
2013
+ calls: "results.query, majors.results"
2014
+ },
2015
+ {
2016
+ feature: "majors.order",
2017
+ needs: ["majors:competitions.seq"],
2018
+ since: 260,
2019
+ calls: "majors.competitions"
2020
+ },
2021
+ {
2022
+ feature: "dancers",
2023
+ needs: ["results:results.name_order_resolved"],
2024
+ since: 260,
2025
+ calls: "dancers.resolve, dancers.search, dancers.get (the dancers index)"
2026
+ },
2027
+ {
2028
+ feature: "schools",
2029
+ needs: ["school-registry.jsonl", "school-redirects.jsonl"],
2030
+ since: 260,
2031
+ calls: "schools.resolve, a dancer question with a school (the schools index)"
2032
+ },
2033
+ {
2034
+ feature: "feises",
2035
+ needs: [
2036
+ "results:feises.date_lo",
2037
+ "results:feises.date_hi",
2038
+ "results:feises.feis_instance_id",
2039
+ "results:feises.feis_brand_id",
2040
+ "results:competitions.feis_region",
2041
+ "feis-redirects.jsonl"
2042
+ ],
2043
+ since: 265,
2044
+ calls: "feises.find, feises.series, and comps and registrations through them (the feises index)"
2045
+ },
2046
+ {
2047
+ feature: "comps",
2048
+ needs: [
2049
+ "results:competitions.feis_level",
2050
+ "results:competitions.round",
2051
+ "results:feises.feis_brand_id"
2052
+ ],
2053
+ since: 265,
2054
+ calls: "comps.resolve, comps.syllabus, comps.syllabusFor, registrations.resolve (the comps index)"
2055
+ },
2056
+ {
2057
+ feature: "staff",
2058
+ needs: [
2059
+ "results:adjudicators",
2060
+ "results:feis_panels",
2061
+ "school-staff.jsonl",
2062
+ "school-teachers.jsonl",
2063
+ "school-registry.jsonl"
2064
+ ],
2065
+ since: 260,
2066
+ calls: "staff.teacher, staff.adjudicator (the staff index)"
2067
+ }
2068
+ ];
2069
+ async function hasNeed(probe, n) {
2070
+ const m = /^(results|majors):([A-Za-z_]+)(?:\.([A-Za-z_]+))?$/.exec(n);
2071
+ if (m === null)
2072
+ return probe.publishes(n);
2073
+ const cols = columnsOf2(await probe.db(m[1]), m[2]);
2074
+ return m[3] === undefined ? cols.size > 0 : cols.has(m[3]);
2075
+ }
2076
+ async function capabilityOf2(probe, f) {
2077
+ const missing = [];
2078
+ for (const n of f.needs)
2079
+ if (!await hasNeed(probe, n))
2080
+ missing.push(n);
2081
+ return { ...f, state: missing.length === 0 ? "full" : "missing", missing };
2082
+ }
2083
+ function needsRelease2(tag, c) {
2084
+ return new Error(`archive-client: ${tag} is too old for ${c.feature} (${c.calls}): it lacks ${c.missing.join(", ")}. Open a release of cut ${c.since} or later`);
2085
+ }
2086
+
1906
2087
  function tagDir(root, tag) {
1907
2088
  if (parseArchiveTag2(tag) === null)
1908
2089
  throw new Error(`archive-client: "${tag}" is not a release tag (archive-YYYY.MM.DD[-N[.k]])`);
@@ -1999,9 +2180,62 @@ class Release2 {
1999
2180
  out.push(await this.fetchOne(a));
2000
2181
  return out;
2001
2182
  }
2183
+ async publishes(asset) {
2184
+ const plan = this.plan(asset);
2185
+ const r = this.receipt();
2186
+ if (r.files[plan.local] !== undefined)
2187
+ return true;
2188
+ if (r.source.startsWith("local:"))
2189
+ return this.has(asset);
2190
+ if (r.published !== undefined)
2191
+ return r.published.some((n) => plan.published.includes(n));
2192
+ let listed;
2193
+ try {
2194
+ listed = await this.listed();
2195
+ } catch (e) {
2196
+ throw new Error(`archive-client: cannot tell whether ${this.tag} publishes ${asset}: its cache records no asset listing and the host did not answer (${String(e)}). Open it once with the network`);
2197
+ }
2198
+ return listed.some((a) => plan.published.includes(a.name));
2199
+ }
2200
+ probe = {
2201
+ db: async (name) => {
2202
+ await this.fetch(name);
2203
+ return this.db(name);
2204
+ },
2205
+ publishes: (asset) => this.publishes(asset)
2206
+ };
2207
+ checked = new Map;
2208
+ async capability(feature) {
2209
+ let c = this.checked.get(feature);
2210
+ if (c === undefined) {
2211
+ const f = FEATURES2.find((x) => x.feature === feature);
2212
+ if (f === undefined)
2213
+ throw new Error(`archive-client: no feature "${feature}"`);
2214
+ c = await capabilityOf2(this.probe, f);
2215
+ this.checked.set(feature, c);
2216
+ }
2217
+ return c;
2218
+ }
2219
+ async capabilities() {
2220
+ const features = [];
2221
+ for (const f of FEATURES2)
2222
+ features.push(await this.capability(f.feature));
2223
+ return { tag: this.tag, features };
2224
+ }
2225
+ async require(feature) {
2226
+ const c = await this.capability(feature);
2227
+ if (c.state === "missing")
2228
+ throw needsRelease2(this.tag, c);
2229
+ }
2002
2230
  async listed() {
2003
- if (this.remoteAssets === null)
2231
+ if (this.remoteAssets === null) {
2004
2232
  this.remoteAssets = await (this.opts.remote ?? defaultRemote()).assets(requireRepo2(this.opts), this.tag);
2233
+ if (existsSync4(this.dir)) {
2234
+ const r = this.receipt();
2235
+ r.published = this.remoteAssets.map((a) => a.name).sort();
2236
+ writeJsonAtomic2(receiptPath(this.dir), r);
2237
+ }
2238
+ }
2005
2239
  return this.remoteAssets;
2006
2240
  }
2007
2241
  async fetchOne(asset) {
@@ -2062,6 +2296,8 @@ class Release2 {
2062
2296
  const b = this.builders().find((x) => x.name === name);
2063
2297
  if (b === undefined)
2064
2298
  throw new Error(`archive-client: no index "${name}" (known: ${this.builders().map((x) => x.name).join(", ")})`);
2299
+ if (FEATURES2.some((f) => f.feature === b.name))
2300
+ await this.require(b.name);
2065
2301
  await this.fetch(...b.inputs);
2066
2302
  return buildIndex2(this, b, { force: o.force ?? false, log: this.opts.log });
2067
2303
  }
@@ -2142,7 +2378,8 @@ async function openRelease2(spec = "pinned", opts = {}) {
2142
2378
  writeJsonAtomic2(receiptPath(dir), {
2143
2379
  tag,
2144
2380
  source: repo,
2145
- files: { "MANIFEST.json": landed }
2381
+ files: { "MANIFEST.json": landed },
2382
+ published: listed.map((a) => a.name).sort()
2146
2383
  });
2147
2384
  dropIfEmpty(part);
2148
2385
  }
@@ -2354,4 +2591,4 @@ function releases2(opts = {}) {
2354
2591
  return new ReleaseManager2(opts);
2355
2592
  }
2356
2593
 
2357
- export { DATABASES2, planAsset2, sha256File2, unzstdTo2, NAME_LEVELS2, nameLevel2, FEIS_LOCAL_ID_FLOOR2, ResultFinder2, MajorsFinder2, nameOrderOf, DancerResolver2, canonicalOf2, CompResolver2, NO_CALENDAR2, FeisFinder2, StaffFinder2, SchoolResolver2, defaultBuilders2, indexPath2, indexKey2, isCurrent2, buildIndex2, listIndexes2, readPin2, youngest2, dancerResolverOf2, RegistrationResolver2, ghRemote2, httpsRemote2, fallbackRemote2, setRemote2, resolveTag2, Release2, openRelease2, openCached2, ReleaseManager2, releases2 };
2594
+ export { DATABASES2, planAsset2, sha256File2, unzstdTo2, NAME_LEVELS2, nameLevel2, FEIS_LOCAL_ID_FLOOR2, ResultFinder2, MajorsFinder2, nameOrderOf, DancerResolver2, readPopulation2, canonicalOf2, CompResolver2, NO_CALENDAR2, FeisFinder2, StaffFinder2, SchoolResolver2, defaultBuilders2, indexPath2, indexKey2, isCurrent2, buildIndex2, 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-zr8tfjk0.js";
7
+ } from "./chunks/index-kp811fb0.js";
8
8
  import"./chunks/index-hne1wadw.js";
9
9
  import"./chunks/index-hghdy980.js";
10
10
 
@@ -23,6 +23,9 @@ var USAGE = `usage: idq <verb> [--cache <dir>] [--repo <owner/name>] [--json]
23
23
  index [--release <spec>] [<name>...] [--force]
24
24
  build the per-tag indexes (default: every one)
25
25
  verify [--release <spec>] re-hash every cached file against its receipt
26
+ capabilities [--release <spec>] what the release carries of what the client reads, from
27
+ its own schema: each feature full or missing (its calls
28
+ refuse, naming the first cut that publishes it)
26
29
  prune [--keep N] [--dry-run] remove all but the N newest cached releases (default 1)
27
30
  and the pinned one
28
31
  rm <tag> [--dry-run] remove one cached release (not the pinned one)
@@ -37,8 +40,9 @@ var USAGE = `usage: idq <verb> [--cache <dir>] [--repo <owner/name>] [--json]
37
40
  dancer [--release <spec>] <name> | --surname <s> | --like <glob> | --id <dancer_id>
38
41
  [--order surname-first|given-first] [--school <print|id>]
39
42
  [--yob <y> | --band <U10> [--year <y>] | --comp <competition>] [--as-of <date>]
40
- [--rows] the dancers a name reaches, each with its evidence
41
- (name level, age, school) and the arm; the results of
43
+ [--population girls|boys] [--rows]
44
+ the dancers a name reaches, each with its evidence
45
+ (name level, age, school, population) and the arm; the results of
42
46
  the one dancer where one answers (or --rows for each)
43
47
  results [--release <spec>] [--dancer <id>] [--feis <id>] [--comp <competition>]
44
48
  [--school <print|id>] [--year <y>] [--limit <n>]
@@ -61,12 +65,13 @@ var USAGE = `usage: idq <verb> [--cache <dir>] [--repo <owner/name>] [--json]
61
65
  else the newest, saying which and what it is read from
62
66
  resolve comp [<feis> <code>] [--year <y>] [--title <t>]
63
67
  resolve dancer [<name>] [--school <s>] [--comp <feis>/<code> | --yob <y> | --band <b> --year <y>]
64
- [--order <o>] [--as-of <date>]
68
+ [--order <o>] [--as-of <date>] [--population girls|boys]
65
69
  resolve registrations
66
70
  JSONL out, one answer per line; with no positional the
67
71
  questions are JSONL on stdin, one per line (a comp:
68
- {feis, code, year?, title?}, a dancer: {name, school?, age?},
69
- a registration: {name, school?, codes[], feis, year?})
72
+ {feis, code, year?, title?}, a dancer: {name, school?, age?,
73
+ population?}, a registration: {name, school?, codes[], feis,
74
+ year?, population?})
70
75
 
71
76
  <spec>: pinned (the default) | latest | a cut (42, 42.1) | a tag (archive-YYYY.MM.DD-N)`;
72
77
  var VALUE = new Set([
@@ -90,6 +95,7 @@ var VALUE = new Set([
90
95
  "--band",
91
96
  "--comp",
92
97
  "--as-of",
98
+ "--population",
93
99
  "--dancer",
94
100
  "--feis",
95
101
  "--limit",
@@ -137,8 +143,8 @@ function printCached(c, say) {
137
143
  }
138
144
  var span = (lo, hi) => lo === null ? "-" : lo === hi ? `${lo}` : `${lo}-${hi ?? "?"}`;
139
145
  function printCandidate(c, say, pad = " ") {
140
- 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}`);
141
- say(`${pad} ${c.rows} rows ${span(c.year_lo, c.year_hi)} born ${span(c.birth.lo, c.birth.hi)} (${c.birth.source}) school ${c.school === null ? "-" : `${c.school.school_id ?? "(no id)"} "${c.school.print}" ${span(c.school.year_lo, c.school.year_hi)}`}`);
146
+ 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}`);
147
+ 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)}`}`);
142
148
  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` : ""}`);
143
149
  }
144
150
  function printDancers(a, say) {
@@ -147,6 +153,8 @@ function printDancers(a, say) {
147
153
  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})` : ""}`);
148
154
  if (a.school !== undefined)
149
155
  say(` school asked: ${a.school.arm} ${a.school.ids.join(", ") || "-"}`);
156
+ if (a.population !== undefined)
157
+ say(` population asked: ${a.population.read ?? "(unread)"}${a.population.note !== undefined ? ` (${a.population.note})` : ""}`);
150
158
  for (const c of a.candidates)
151
159
  printCandidate(c, say);
152
160
  if (a.preferred !== undefined)
@@ -277,7 +285,8 @@ async function resolveVerb(rel, what, args, p, say, stdin) {
277
285
  ...nameOrderOf(p.flags.get("--order")) !== undefined ? { order: nameOrderOf(p.flags.get("--order")) } : {},
278
286
  ...p.flags.has("--school") ? { school: p.flags.get("--school") } : {},
279
287
  ...age !== undefined ? { age } : {},
280
- ...p.flags.has("--as-of") ? { asOf: p.flags.get("--as-of") } : {}
288
+ ...p.flags.has("--as-of") ? { asOf: p.flags.get("--as-of") } : {},
289
+ ...p.flags.has("--population") ? { population: p.flags.get("--population") } : {}
281
290
  }
282
291
  }
283
292
  ];
@@ -404,6 +413,20 @@ async function main(argv, io = {}) {
404
413
  });
405
414
  return 0;
406
415
  }
416
+ case "capabilities": {
417
+ const rel = await openRelease2(spec, { ...opts, assets: [] });
418
+ try {
419
+ const c = await rel.capabilities();
420
+ out(c, () => {
421
+ say(`${c.tag}: what it carries of what the client reads`);
422
+ for (const f of c.features)
423
+ say(` ${f.state.padEnd(8)} ${f.feature}${f.state === "full" ? "" : ` lacks ${f.missing.join(", ")}: a release of cut ${f.since} or later answers ${f.calls}`}`);
424
+ });
425
+ return 0;
426
+ } finally {
427
+ rel.close();
428
+ }
429
+ }
407
430
  case "verify": {
408
431
  const tag = (await m.fetch(spec, { assets: [] })).tag;
409
432
  const bad = await m.verify(tag);
@@ -522,6 +545,7 @@ async function main(argv, io = {}) {
522
545
  ...p.flags.has("--school") ? { school: p.flags.get("--school") } : {},
523
546
  ...age !== undefined ? { age } : {},
524
547
  ...p.flags.has("--as-of") ? { asOf: p.flags.get("--as-of") } : {},
548
+ ...p.flags.has("--population") ? { population: p.flags.get("--population") } : {},
525
549
  ...p.flags.has("--limit") ? { limit: Number(p.flags.get("--limit")) } : {}
526
550
  };
527
551
  const surname = p.flags.get("--surname");
@@ -666,7 +690,7 @@ async function main(argv, io = {}) {
666
690
  out(a, () => {
667
691
  say(`${a.feis.asked} ${a.year ?? "?"} ${a.held ? `held: ${a.feis.feis_ids.join(", ")}` : "not held"} series ${a.feis.brand_id ?? "-"} (years ${a.years.join(", ") || "-"}) in ${a.tag}`);
668
692
  for (const c of a.entries)
669
- say(` ${c.code} ${c.kind} ${c.level ?? "-"} ${c.age ?? "-"} ${c.population ?? "-"} "${c.title ?? ""}"`);
693
+ say(` ${c.code} ${c.kind} ${c.level ?? "-"} ${c.age ?? "-"} ${c.population ?? "-"}${c.second ? " second" : ""} "${c.title ?? ""}"`);
670
694
  say(`${a.entries.length} comp(s)`);
671
695
  });
672
696
  rel.close();
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  ResultFinder2,
11
11
  MajorsFinder2,
12
12
  DancerResolver2,
13
+ readPopulation2,
13
14
  canonicalOf2,
14
15
  CompResolver2,
15
16
  NO_CALENDAR2,
@@ -30,13 +31,17 @@ import {
30
31
  httpsRemote2,
31
32
  fallbackRemote2,
32
33
  setRemote2,
34
+ columnsOf2,
35
+ FEATURES2,
36
+ capabilityOf2,
37
+ needsRelease2,
33
38
  resolveTag2,
34
39
  Release2,
35
40
  openRelease2,
36
41
  openCached2,
37
42
  ReleaseManager2,
38
43
  releases2
39
- } from "./chunks/index-zr8tfjk0.js";
44
+ } from "./chunks/index-kp811fb0.js";
40
45
  import {
41
46
  cacheRoot2,
42
47
  repoOf2
@@ -55,6 +60,7 @@ export {
55
60
  CompResolver2 as CompResolver,
56
61
  DATABASES2 as DATABASES,
57
62
  DancerResolver2 as DancerResolver,
63
+ FEATURES2 as FEATURES,
58
64
  FEIS_LOCAL_ID_FLOOR2 as FEIS_LOCAL_ID_FLOOR,
59
65
  FeisFinder2 as FeisFinder,
60
66
  MajorsFinder2 as MajorsFinder,
@@ -69,6 +75,8 @@ export {
69
75
  buildIndex2 as buildIndex,
70
76
  cacheRoot2 as cacheRoot,
71
77
  canonicalOf2 as canonicalOf,
78
+ capabilityOf2 as capabilityOf,
79
+ columnsOf2 as columnsOf,
72
80
  compareTagsNewestFirst2 as compareTagsNewestFirst,
73
81
  cutOf2 as cutOf,
74
82
  dancerResolverOf2 as dancerResolverOf,
@@ -81,6 +89,7 @@ export {
81
89
  isCurrent2 as isCurrent,
82
90
  listIndexes2 as listIndexes,
83
91
  nameLevel2 as nameLevel,
92
+ needsRelease2 as needsRelease,
84
93
  openCached2 as openCached,
85
94
  openRelease2 as openRelease,
86
95
  parseArchiveTag2 as parseArchiveTag,
@@ -88,6 +97,7 @@ export {
88
97
  pickTag2 as pickTag,
89
98
  planAsset2 as planAsset,
90
99
  readPin2 as readPin,
100
+ readPopulation2 as readPopulation,
91
101
  releases2 as releases,
92
102
  renderArchiveTag2 as renderArchiveTag,
93
103
  repoOf2 as repoOf,
@@ -37,4 +37,5 @@ export interface Receipt {
37
37
  tag: string;
38
38
  source: string;
39
39
  files: Record<string, LandedFile>;
40
+ published?: string[];
40
41
  }
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import type { OpenOptions } from "./release.js";
3
- 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 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 [--rows] the dancers a name reaches, each with its evidence\n (name level, age, school) 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 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>]\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 a registration: {name, school?, codes[], feis, year?})\n\n <spec>: pinned (the default) | latest | a cut (42, 42.1) | a tag (archive-YYYY.MM.DD-N)";
3
+ 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 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
4
  export interface Parsed {
5
5
  positional: string[];
6
6
  flags: Map<string, string>;
@@ -15,7 +15,10 @@ export interface Comp {
15
15
  feis_level: number | null;
16
16
  level: string | null;
17
17
  age: string | null;
18
+ age_source: string | null;
19
+ age_printed: string | null;
18
20
  population: Population | null;
21
+ second: boolean;
19
22
  round: string | null;
20
23
  canonical: string | null;
21
24
  platform: string;
@@ -107,8 +110,13 @@ interface CompRow {
107
110
  round: string | null;
108
111
  platform: string;
109
112
  captures: number;
113
+ age_source?: string | null;
114
+ age_printed?: string | null;
115
+ gender_source?: string | null;
116
+ level_printed?: string | null;
117
+ dance_printed?: string | null;
110
118
  }
111
- export declare function compOf(r: CompRow): Comp;
119
+ export declare function compOf(r: CompRow, codes?: readonly string[]): Comp;
112
120
  export declare function canonicalOf(level: number | null, age: string | null): string | null;
113
121
  export declare class CompResolver {
114
122
  private readonly rel;
@@ -20,8 +20,10 @@ export interface DancerQuery {
20
20
  school?: string;
21
21
  age?: AgeAsked;
22
22
  asOf?: string;
23
+ population?: string;
23
24
  }
24
25
  export type AgeEvidence = "fits" | "overlaps" | "contradicts" | "unknown" | "unasked";
26
+ export type PopulationEvidence = "same" | "contradicts" | "both" | "unknown" | "unasked";
25
27
  export type SchoolEvidence = "same" | "moved" | "none" | "unresolved" | "unasked";
26
28
  export interface DancerSchool {
27
29
  school_id: string | null;
@@ -40,6 +42,7 @@ export interface DancerCandidate {
40
42
  nameLevel: NameLevel;
41
43
  age: AgeEvidence;
42
44
  school: SchoolEvidence;
45
+ population: PopulationEvidence;
43
46
  };
44
47
  prints: {
45
48
  print: string;
@@ -55,11 +58,15 @@ export interface DancerCandidate {
55
58
  source: string;
56
59
  conflict: boolean;
57
60
  };
61
+ population: {
62
+ girls: number;
63
+ boys: number;
64
+ };
58
65
  school: DancerSchool | null;
59
66
  }
60
67
  export interface ExcludedCandidate {
61
68
  candidate: DancerCandidate;
62
- why: "age";
69
+ why: "age" | "population";
63
70
  }
64
71
  export interface DancerAnswer {
65
72
  tag: string;
@@ -83,6 +90,11 @@ export interface DancerAnswer {
83
90
  } | null;
84
91
  note?: string;
85
92
  };
93
+ population?: {
94
+ asked: string;
95
+ read: "girls" | "boys" | null;
96
+ note?: string;
97
+ };
86
98
  school?: {
87
99
  arm: string;
88
100
  ids: string[];
@@ -113,6 +125,10 @@ export interface DancerRecord {
113
125
  source: string;
114
126
  conflict: boolean;
115
127
  };
128
+ population: {
129
+ girls: number;
130
+ boys: number;
131
+ };
116
132
  prints: {
117
133
  print: string;
118
134
  rows: number;
@@ -136,3 +152,4 @@ export declare class DancerResolver {
136
152
  private schoolEvidence;
137
153
  private readAge;
138
154
  }
155
+ export declare function readPopulation(asked: string): NonNullable<DancerAnswer["population"]>;
@@ -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 DancerCandidate, type DancerQuery, type DancerRecord, DancerResolver, type DancerSchool, type DancerSearchQuery, type ExcludedCandidate, FEIS_LOCAL_ID_FLOOR, type SchoolEvidence, } from "./dancers.js";
3
+ export { type AgeAsked, type AgeEvidence, type DancerAnswer, type DancerCandidate, type DancerQuery, type DancerRecord, DancerResolver, type DancerSchool, type DancerSearchQuery, type ExcludedCandidate, FEIS_LOCAL_ID_FLOOR, type PopulationEvidence, 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 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";
@@ -14,4 +14,5 @@ export { type OpenOptions, Release, openCached, openRelease, resolveTag, } from
14
14
  export { type MajorCompetition, type MajorEvent, MajorsFinder, type ResultAnswer, ResultFinder, type ResultQuery, type ResultRow, } from "./results.js";
15
15
  export { type DancerAnswerLike, type DancerQueryLike, type DancerResolverLike, type DancerUnavailable, type RegistrationRow, type ResolvedDancer, RegistrationResolver, type ResolvedRegistration, dancerResolverOf, youngest, } from "./registrations.js";
16
16
  export { type SchoolAnswer, type SchoolCandidate, type SchoolQuery, SchoolResolver, type SchoolRung, } from "./schools.js";
17
+ export { type Capability, FEATURES, type Feature, type SchemaProbe, capabilityOf, columnsOf, needsRelease, } from "./schema.js";
17
18
  export { ARCHIVE_TAG, type ArchiveTag, type ReleaseSpec, compareTagsNewestFirst, cutOf, parseArchiveTag, parseReleaseSpec, pickTag, renderArchiveTag, } from "./tag.js";
@@ -10,6 +10,7 @@ export interface RegistrationRow {
10
10
  year?: number;
11
11
  titles?: Readonly<Record<string, string>>;
12
12
  asOf?: string;
13
+ population?: string;
13
14
  [k: string]: unknown;
14
15
  }
15
16
  export type DancerQueryLike = DancerQuery;
@@ -36,6 +37,13 @@ export interface ResolvedRegistration {
36
37
  from: "comp" | "title";
37
38
  } | null;
38
39
  bands?: string[];
40
+ population: {
41
+ asked: string;
42
+ from: "row" | "comp";
43
+ codes?: string[];
44
+ } | {
45
+ why: string;
46
+ } | null;
39
47
  dancer: DancerAnswer | DancerAnswerLike | DancerUnavailable;
40
48
  asked?: DancerQueryLike;
41
49
  }
@@ -9,6 +9,7 @@ import { StaffFinder } from "./people.js";
9
9
  import { type DancerResolverLike, RegistrationResolver } from "./registrations.js";
10
10
  import { type Remote } from "./remote.js";
11
11
  import { MajorsFinder, ResultFinder } from "./results.js";
12
+ import { type Capability } from "./schema.js";
12
13
  import { type SchoolReaderChoice, SchoolResolver } from "./schools.js";
13
14
  import { type ReleaseSpec } from "./tag.js";
14
15
  export interface OpenOptions extends ClientOptions {
@@ -49,6 +50,15 @@ export declare class Release {
49
50
  has(asset: string): boolean;
50
51
  path(asset: string): string;
51
52
  fetch(...assets: string[]): Promise<LandedFile[]>;
53
+ publishes(asset: string): Promise<boolean>;
54
+ private readonly probe;
55
+ private readonly checked;
56
+ capability(feature: string): Promise<Capability>;
57
+ capabilities(): Promise<{
58
+ tag: string;
59
+ features: Capability[];
60
+ }>;
61
+ require(feature: string): Promise<void>;
52
62
  private listed;
53
63
  private fetchOne;
54
64
  private record;
@@ -0,0 +1,20 @@
1
+ import type { Database } from "bun:sqlite";
2
+ export declare function columnsOf(db: Database, table: string): Set<string>;
3
+ export interface Feature {
4
+ feature: string;
5
+ needs: readonly string[];
6
+ since: number;
7
+ calls: string;
8
+ }
9
+ export declare const FEATURES: readonly Feature[];
10
+ export interface Capability extends Feature {
11
+ state: "full" | "missing";
12
+ missing: string[];
13
+ }
14
+ export interface SchemaProbe {
15
+ db(name: string): Promise<Database>;
16
+ publishes(asset: string): Promise<boolean>;
17
+ }
18
+ export declare function hasNeed(probe: SchemaProbe, n: string): Promise<boolean>;
19
+ export declare function capabilityOf(probe: SchemaProbe, f: Feature): Promise<Capability>;
20
+ export declare function needsRelease(tag: string, c: Capability): Error;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finbheara/archive-client",
3
- "version": "0.1.2",
3
+ "version": "0.3.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",
@@ -44,7 +44,7 @@
44
44
  "prepublishOnly": "bun scripts/build.ts && bun ../../tools/checks/archive-client-tarball.ts"
45
45
  },
46
46
  "dependencies": {
47
- "@finbheara/domain": "0.48.0",
47
+ "@finbheara/domain": "0.49.0",
48
48
  "@finbheara/names": "0.22.0"
49
49
  },
50
50
  "devDependencies": {