@finbheara/archive-client 0.1.1 → 0.2.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
@@ -301,10 +334,13 @@ const rows = await rel.registrations.resolve([
301
334
  ]);
302
335
  rows[0].comps; // one competition answer per code
303
336
  rows[0].age; // { band: "U10", year: 2026, code: "610", from: "comp" }: the band the dancer is asked at
337
+ rows[0].population; // { asked: "girls", from: "comp", codes: ["610"] }: who the dancer is asked as
304
338
  rows[0].dancer; // the dancer resolver's answer
305
339
  ```
306
340
 
307
- Each code is resolved as above. The dancer is asked at the YOUNGEST band the row's competitions
341
+ Each code is resolved as above. The dancer is asked at the row's own `population` where it gives
342
+ one, else at the one population its unique comps read (`from: "comp"`, with the codes). Comps
343
+ reading both populations give none, with the reason. The dancer is asked at the YOUNGEST band the row's competitions
308
344
  read (the lowest ceiling, then the highest floor): a dancer may dance up into an older band and
309
345
  never down, so the youngest bounds the dancer. Every band a row read is listed (`bands`) where
310
346
  there are several. Where no comp answers with a band (a syllabus read from results that lacks the
@@ -323,6 +359,7 @@ and the competitions still resolve.
323
359
  idq add <dir> --tag <tag>
324
360
  idq index [--release <spec>] [<name>...] [--force]
325
361
  idq verify [--release <spec>]
362
+ idq capabilities [--release <spec>]
326
363
  idq prune [--keep N] [--dry-run]
327
364
  idq rm <tag> [--dry-run]
328
365
  idq drop-indexes [<tag>] [--dry-run]
@@ -331,7 +368,8 @@ and the competitions still resolve.
331
368
  idq school [--release <spec>] <print|school_id> [--region <code>]
332
369
  idq dancer [--release <spec>] <name> | --surname <s> | --like <glob> | --id <dancer_id>
333
370
  [--order surname-first|given-first] [--school <print|id>]
334
- [--yob <y> | --band <band> [--year <y>] | --comp <competition>] [--as-of <date>] [--rows]
371
+ [--yob <y> | --band <band> [--year <y>] | --comp <competition>] [--as-of <date>]
372
+ [--population girls|boys] [--rows]
335
373
  idq results [--release <spec>] [--dancer <id>] [--feis <id>] [--comp <c>] [--school <s>] [--year <y>] [--limit <n>]
336
374
  idq majors [--release <spec>] [--event <e>] [--year <y>] [--comp <c>] [--dancer <id>] [--rows]
337
375
  idq teacher [--release <spec>] <name|TD number>
@@ -348,7 +386,7 @@ and the competitions still resolve.
348
386
 
349
387
  `resolve` speaks JSON lines: one answer per line out, and with no positional question the
350
388
  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,
389
+ `{name, school?, age?, population?}`, `{name, school?, codes, feis, year?, population?}`). A line that is no JSON object,
352
390
  or no question of its kind, answers `{tag, line, error}` and the other lines still answer. The
353
391
  exit is non-zero where any line erred. A dancer's age may be given as a comp
354
392
  (`{"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";
@@ -599,7 +657,7 @@ import { Age as Age2 } from "@finbheara/domain/age";
599
657
  import {
600
658
  compTitle,
601
659
  matchTitle,
602
- populationIn,
660
+ populationIn as populationIn2,
603
661
  titleRelation
604
662
  } from "@finbheara/domain/title";
605
663
  var LEVEL = Object.freeze({
@@ -651,7 +709,7 @@ var compsIndex = {
651
709
  function compOf(r) {
652
710
  const lv = r.feis_level;
653
711
  const kind = lv === null ? "unrated" : lv >= 600 ? "championship" : "grade";
654
- const pop = populationIn(r.gender ?? "") || populationIn(r.title ?? "");
712
+ const pop = populationIn2(r.gender ?? "") || populationIn2(r.title ?? "");
655
713
  const round = r.round ?? (r.title === null ? null : compTitle(r.title).round?.toString() ?? null);
656
714
  return {
657
715
  competition: r.competition,
@@ -1724,13 +1782,31 @@ class RegistrationResolver2 {
1724
1782
  from: "comp"
1725
1783
  } : byTitle !== undefined ? { band: byTitle.age, year: byTitle.year, code: byTitle.code, from: "title" } : null;
1726
1784
  const bands = [...new Set((first !== undefined ? read : printed).map((c) => c.age))];
1785
+ let population = null;
1786
+ if (row.population !== undefined)
1787
+ population = { asked: row.population, from: "row" };
1788
+ else {
1789
+ const gendered = comps.flatMap((c, i) => c.arm === "unique" && (c.population === "girls" || c.population === "boys") ? [{ pop: c.population, code: row.codes[i] }] : []);
1790
+ const pops = [...new Set(gendered.map((g) => g.pop))];
1791
+ if (pops.length === 1)
1792
+ population = {
1793
+ asked: pops[0],
1794
+ from: "comp",
1795
+ codes: gendered.map((g) => g.code)
1796
+ };
1797
+ else if (pops.length > 1)
1798
+ population = {
1799
+ why: `the row's comps read both populations (${gendered.map((g) => `${g.code}: ${g.pop}`).join(", ")}): none asked`
1800
+ };
1801
+ }
1727
1802
  const order = nameOrderOf(row.order);
1728
1803
  const asked = {
1729
1804
  name: row.name,
1730
1805
  ...order !== undefined ? { order } : {},
1731
1806
  ...row.school !== undefined ? { school: row.school } : {},
1732
1807
  ...age !== null ? { age: { band: age.band, year: age.year } } : {},
1733
- ...row.asOf !== undefined ? { asOf: row.asOf } : {}
1808
+ ...row.asOf !== undefined ? { asOf: row.asOf } : {},
1809
+ ...population !== null && "asked" in population ? { population: population.asked } : {}
1734
1810
  };
1735
1811
  const { dancer } = await this.dancer(asked);
1736
1812
  out.push({
@@ -1739,6 +1815,7 @@ class RegistrationResolver2 {
1739
1815
  comps,
1740
1816
  age,
1741
1817
  ...bands.length > 1 ? { bands } : {},
1818
+ population,
1742
1819
  dancer,
1743
1820
  asked
1744
1821
  });
@@ -1903,6 +1980,88 @@ function defaultRemote() {
1903
1980
  return installed ?? fallbackRemote2(ghRemote2, httpsRemote2());
1904
1981
  }
1905
1982
 
1983
+ function columnsOf2(db, table) {
1984
+ return new Set(db.query("SELECT name FROM pragma_table_info(?)").all(table).map((r) => r.name));
1985
+ }
1986
+ var FEATURES2 = [
1987
+ {
1988
+ feature: "results.level",
1989
+ needs: ["results:competitions.feis_level"],
1990
+ since: 215,
1991
+ calls: "results.query, majors.results"
1992
+ },
1993
+ {
1994
+ feature: "majors.order",
1995
+ needs: ["majors:competitions.seq"],
1996
+ since: 260,
1997
+ calls: "majors.competitions"
1998
+ },
1999
+ {
2000
+ feature: "dancers",
2001
+ needs: ["results:results.name_order_resolved"],
2002
+ since: 260,
2003
+ calls: "dancers.resolve, dancers.search, dancers.get (the dancers index)"
2004
+ },
2005
+ {
2006
+ feature: "schools",
2007
+ needs: ["school-registry.jsonl", "school-redirects.jsonl"],
2008
+ since: 260,
2009
+ calls: "schools.resolve, a dancer question with a school (the schools index)"
2010
+ },
2011
+ {
2012
+ feature: "feises",
2013
+ needs: [
2014
+ "results:feises.date_lo",
2015
+ "results:feises.date_hi",
2016
+ "results:feises.feis_instance_id",
2017
+ "results:feises.feis_brand_id",
2018
+ "results:competitions.feis_region",
2019
+ "feis-redirects.jsonl"
2020
+ ],
2021
+ since: 265,
2022
+ calls: "feises.find, feises.series, and comps and registrations through them (the feises index)"
2023
+ },
2024
+ {
2025
+ feature: "comps",
2026
+ needs: [
2027
+ "results:competitions.feis_level",
2028
+ "results:competitions.round",
2029
+ "results:feises.feis_brand_id"
2030
+ ],
2031
+ since: 265,
2032
+ calls: "comps.resolve, comps.syllabus, comps.syllabusFor, registrations.resolve (the comps index)"
2033
+ },
2034
+ {
2035
+ feature: "staff",
2036
+ needs: [
2037
+ "results:adjudicators",
2038
+ "results:feis_panels",
2039
+ "school-staff.jsonl",
2040
+ "school-teachers.jsonl",
2041
+ "school-registry.jsonl"
2042
+ ],
2043
+ since: 260,
2044
+ calls: "staff.teacher, staff.adjudicator (the staff index)"
2045
+ }
2046
+ ];
2047
+ async function hasNeed(probe, n) {
2048
+ const m = /^(results|majors):([A-Za-z_]+)(?:\.([A-Za-z_]+))?$/.exec(n);
2049
+ if (m === null)
2050
+ return probe.publishes(n);
2051
+ const cols = columnsOf2(await probe.db(m[1]), m[2]);
2052
+ return m[3] === undefined ? cols.size > 0 : cols.has(m[3]);
2053
+ }
2054
+ async function capabilityOf2(probe, f) {
2055
+ const missing = [];
2056
+ for (const n of f.needs)
2057
+ if (!await hasNeed(probe, n))
2058
+ missing.push(n);
2059
+ return { ...f, state: missing.length === 0 ? "full" : "missing", missing };
2060
+ }
2061
+ function needsRelease2(tag, c) {
2062
+ 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`);
2063
+ }
2064
+
1906
2065
  function tagDir(root, tag) {
1907
2066
  if (parseArchiveTag2(tag) === null)
1908
2067
  throw new Error(`archive-client: "${tag}" is not a release tag (archive-YYYY.MM.DD[-N[.k]])`);
@@ -1999,9 +2158,62 @@ class Release2 {
1999
2158
  out.push(await this.fetchOne(a));
2000
2159
  return out;
2001
2160
  }
2161
+ async publishes(asset) {
2162
+ const plan = this.plan(asset);
2163
+ const r = this.receipt();
2164
+ if (r.files[plan.local] !== undefined)
2165
+ return true;
2166
+ if (r.source.startsWith("local:"))
2167
+ return this.has(asset);
2168
+ if (r.published !== undefined)
2169
+ return r.published.some((n) => plan.published.includes(n));
2170
+ let listed;
2171
+ try {
2172
+ listed = await this.listed();
2173
+ } catch (e) {
2174
+ 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`);
2175
+ }
2176
+ return listed.some((a) => plan.published.includes(a.name));
2177
+ }
2178
+ probe = {
2179
+ db: async (name) => {
2180
+ await this.fetch(name);
2181
+ return this.db(name);
2182
+ },
2183
+ publishes: (asset) => this.publishes(asset)
2184
+ };
2185
+ checked = new Map;
2186
+ async capability(feature) {
2187
+ let c = this.checked.get(feature);
2188
+ if (c === undefined) {
2189
+ const f = FEATURES2.find((x) => x.feature === feature);
2190
+ if (f === undefined)
2191
+ throw new Error(`archive-client: no feature "${feature}"`);
2192
+ c = await capabilityOf2(this.probe, f);
2193
+ this.checked.set(feature, c);
2194
+ }
2195
+ return c;
2196
+ }
2197
+ async capabilities() {
2198
+ const features = [];
2199
+ for (const f of FEATURES2)
2200
+ features.push(await this.capability(f.feature));
2201
+ return { tag: this.tag, features };
2202
+ }
2203
+ async require(feature) {
2204
+ const c = await this.capability(feature);
2205
+ if (c.state === "missing")
2206
+ throw needsRelease2(this.tag, c);
2207
+ }
2002
2208
  async listed() {
2003
- if (this.remoteAssets === null)
2209
+ if (this.remoteAssets === null) {
2004
2210
  this.remoteAssets = await (this.opts.remote ?? defaultRemote()).assets(requireRepo2(this.opts), this.tag);
2211
+ if (existsSync4(this.dir)) {
2212
+ const r = this.receipt();
2213
+ r.published = this.remoteAssets.map((a) => a.name).sort();
2214
+ writeJsonAtomic2(receiptPath(this.dir), r);
2215
+ }
2216
+ }
2005
2217
  return this.remoteAssets;
2006
2218
  }
2007
2219
  async fetchOne(asset) {
@@ -2062,6 +2274,8 @@ class Release2 {
2062
2274
  const b = this.builders().find((x) => x.name === name);
2063
2275
  if (b === undefined)
2064
2276
  throw new Error(`archive-client: no index "${name}" (known: ${this.builders().map((x) => x.name).join(", ")})`);
2277
+ if (FEATURES2.some((f) => f.feature === b.name))
2278
+ await this.require(b.name);
2065
2279
  await this.fetch(...b.inputs);
2066
2280
  return buildIndex2(this, b, { force: o.force ?? false, log: this.opts.log });
2067
2281
  }
@@ -2142,7 +2356,8 @@ async function openRelease2(spec = "pinned", opts = {}) {
2142
2356
  writeJsonAtomic2(receiptPath(dir), {
2143
2357
  tag,
2144
2358
  source: repo,
2145
- files: { "MANIFEST.json": landed }
2359
+ files: { "MANIFEST.json": landed },
2360
+ published: listed.map((a) => a.name).sort()
2146
2361
  });
2147
2362
  dropIfEmpty(part);
2148
2363
  }
@@ -2354,4 +2569,4 @@ function releases2(opts = {}) {
2354
2569
  return new ReleaseManager2(opts);
2355
2570
  }
2356
2571
 
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 };
2572
+ 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-6e2gb6sk.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");
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-6e2gb6sk.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>;
@@ -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.1",
3
+ "version": "0.2.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.47.0",
47
+ "@finbheara/domain": "0.48.0",
48
48
  "@finbheara/names": "0.22.0"
49
49
  },
50
50
  "devDependencies": {