@finbheara/archive-client 0.11.0 → 0.13.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.
@@ -71,9 +71,16 @@ import { Age, DANCE_UP_YEARS, birthYearsMeet } 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";
75
74
  import {
75
+ compTitle,
76
+ populationIn,
77
+ populationRelation
78
+ } from "@finbheara/domain/title";
79
+ import {
80
+ composeNames,
76
81
  foldNameKey,
82
+ nameBlockKeys,
83
+ nameKinship,
77
84
  surnameBlock
78
85
  } from "@finbheara/names/normalize";
79
86
  import namesPkg from "@finbheara/names/package.json";
@@ -87,6 +94,7 @@ var NAME_LEVELS2 = [
87
94
  ...NAME_LEVEL_COLUMNS,
88
95
  "initial",
89
96
  "middle",
97
+ "form",
90
98
  "surname",
91
99
  "like"
92
100
  ];
@@ -339,286 +347,23 @@ function notOnTheFloor(placePrinted, atts, withdrawn) {
339
347
  return withdrawn !== null && DID_NOT_TABULATE.has(withdrawn);
340
348
  }
341
349
 
342
- function columnsOf2(db, table) {
343
- return new Set(db.query("SELECT name FROM pragma_table_info(?)").all(table).map((r) => r.name));
344
- }
345
- var FEATURES2 = [
346
- {
347
- feature: "results.level",
348
- needs: ["results:competitions.feis_level"],
349
- since: 215,
350
- calls: "results.query, majors.results"
351
- },
352
- {
353
- feature: "majors.order",
354
- needs: ["majors:competitions.seq"],
355
- since: 260,
356
- calls: "majors.competitions"
357
- },
358
- {
359
- feature: "dancers",
360
- needs: ["results:results.name_order_resolved"],
361
- since: 260,
362
- calls: "dancers.resolve, dancers.search, dancers.get (the dancers index)"
363
- },
364
- {
365
- feature: "schools",
366
- needs: ["school-registry.jsonl", "school-redirects.jsonl"],
367
- since: 260,
368
- calls: "schools.resolve, a dancer question with a school (the schools index)"
369
- },
370
- {
371
- feature: "feises",
372
- needs: [
373
- "results:feises.date_lo",
374
- "results:feises.date_hi",
375
- "results:feises.feis_instance_id",
376
- "results:feises.feis_brand_id",
377
- "results:competitions.feis_region",
378
- "feis-redirects.jsonl"
379
- ],
380
- since: 265,
381
- calls: "feises.find, feises.series, and comps and registrations through them (the feises index)"
382
- },
383
- {
384
- feature: "comps",
385
- needs: [
386
- "results:competitions.feis_level",
387
- "results:competitions.round",
388
- "results:feises.feis_brand_id"
389
- ],
390
- since: 265,
391
- calls: "comps.resolve, comps.syllabus, comps.syllabusFor, registrations.resolve (the comps index)"
392
- },
393
- {
394
- feature: "comps.dance",
395
- needs: ["results:competitions.dance"],
396
- since: 270,
397
- calls: "the `dance` a comp of comps.resolve, comps.syllabus and comps.syllabusFor carries: null, never refused, on a release without it"
398
- },
399
- {
400
- feature: "schools.lineage",
401
- needs: ["school-lineage.jsonl"],
402
- since: 184,
403
- calls: "the `lineage` school evidence of dancers.resolve and dancers.search: no school is joined by lineage, never refused, on a release without it"
404
- },
405
- {
406
- feature: "results.stream",
407
- needs: [
408
- "results:competitions.round",
409
- "results:competitions.feis_level",
410
- "results:competitions.level_printed",
411
- "results:competitions.age_printed",
412
- "results:results.yob_printed",
413
- "results:results.primary_source",
414
- "results:results.platform_dancer_id",
415
- "results:results.relative_place",
416
- "results:feises.date_lo",
417
- "results:feises.date_hi"
418
- ],
419
- since: 255,
420
- calls: "results.stream, results.entries (scopes transcribed, platform, all)"
421
- },
422
- {
423
- feature: "majors.stream",
424
- needs: [
425
- "majors:competitions.round",
426
- "majors:competitions.feis_level",
427
- "majors:results.yob_printed",
428
- "majors:results.primary_source",
429
- "majors:results.relative_place",
430
- "majors:feises.date_lo"
431
- ],
432
- since: 255,
433
- calls: "results.stream, results.entries (scope majors)"
434
- },
435
- {
436
- feature: "results.printed_age",
437
- needs: ["results:results.printed_age", "majors:results.printed_age"],
438
- since: 271,
439
- calls: "the `printed_age` a row of results.stream and results.entries carries: null, never refused, on a release without it"
440
- },
441
- {
442
- feature: "majors.tabs",
443
- needs: ["majors:tabs.address", "majors:tabs.true_round"],
444
- since: 215,
445
- calls: "majors.tabulated"
446
- },
447
- {
448
- feature: "feises.dates",
449
- needs: ["results:feises.feis_id", "results:feises.date_lo", "results:feises.date_hi"],
450
- since: 245,
451
- calls: "feises.dates"
452
- },
453
- {
454
- feature: "dancers.records",
455
- needs: [
456
- "results:dancers.display",
457
- "results:dancers.appearances",
458
- "results:dancers.birth_lo",
459
- "results:dancers.birth_hi",
460
- "results:dancers.birth_source",
461
- "results:dancers.birth_conflict",
462
- "results:dancers.genders"
463
- ],
464
- since: 215,
465
- calls: "dancers.records"
466
- },
467
- {
468
- feature: "dancers.years",
469
- needs: [
470
- "results:dancer_years.region",
471
- "results:dancer_years.region_source",
472
- "results:dancer_years.nationals",
473
- "results:dancer_years.nationals_source",
474
- "results:dancer_years.geohash"
475
- ],
476
- since: 245,
477
- calls: "dancers.years"
478
- },
479
- {
480
- feature: "teams",
481
- needs: [
482
- ...[
483
- "slug",
484
- "address",
485
- "event",
486
- "year",
487
- "card",
488
- "comp_no",
489
- "title",
490
- "category",
491
- "hands",
492
- "letter",
493
- "label",
494
- "name_printed",
495
- "school_printed",
496
- "school_loc",
497
- "school_id",
498
- "place",
499
- "place_printed",
500
- "tie",
501
- "competitors",
502
- "atts"
503
- ].map((c) => `table:team_results.${c}`),
504
- ...[
505
- "team",
506
- "kind",
507
- "event_year",
508
- "year",
509
- "comp_no",
510
- "title",
511
- "team_index",
512
- "seq",
513
- "school_id",
514
- "school_print_materialized"
515
- ].map((c) => `table:roster_teams.${c}`),
516
- ...["team", "seq", "name_printed", "name", "yob", "dancer_id"].map((c) => `table:roster_members.${c}`),
517
- ...[
518
- "event_year",
519
- "comp_no",
520
- "team",
521
- "result_address",
522
- "school_id",
523
- "join_kind",
524
- "via_split",
525
- "roster_teams",
526
- "result_teams",
527
- "roster_index",
528
- "result_letter"
529
- ].map((c) => `table:team_result_rosters.${c}`),
530
- "table:event_years.event_year",
531
- "table:event_years.event",
532
- "table:event_years.year"
533
- ],
534
- since: 260,
535
- calls: "teams.results, teams.rosters, teams.resultRosters (the teams index)"
536
- },
537
- {
538
- feature: "teams.fields",
539
- needs: [
540
- "table:team_results.age",
541
- "table:team_results.gender",
542
- "table:team_results.printed_status",
543
- "table:roster_teams.dance",
544
- "table:roster_teams.school_print_as_of",
545
- "table:roster_members.gender"
546
- ],
547
- since: 266,
548
- calls: "the fields teams.results (age, gender, printed_status) and teams.rosters (dance, school_printed_as_of, a member's gender) carry where present: absent, never refused, on a release without them"
549
- },
550
- {
551
- feature: "events",
552
- needs: [
553
- "event_year",
554
- "event",
555
- "year",
556
- "date_lo",
557
- "date_hi",
558
- "resolution",
559
- "source",
560
- "date_printed",
561
- "place",
562
- "place_printed"
563
- ].map((c) => `table:event_years.${c}`),
564
- since: 260,
565
- calls: "events.years (the events index)"
566
- },
567
- {
568
- feature: "staff",
569
- needs: [
570
- "results:adjudicators",
571
- "results:feis_panels",
572
- "school-staff.jsonl",
573
- "school-teachers.jsonl",
574
- "school-registry.jsonl"
575
- ],
576
- since: 260,
577
- calls: "staff.teacher, staff.adjudicator (the staff index)"
578
- }
579
- ];
580
- async function hasNeed(probe, n) {
581
- const t = /^table:([A-Za-z_]+)(?:\.([A-Za-z_]+))?$/.exec(n);
582
- if (t !== null) {
583
- if (probe.tableColumns === undefined)
584
- return false;
585
- const cols = await probe.tableColumns(t[1]);
586
- return t[2] === undefined ? cols.size > 0 : cols.has(t[2]);
587
- }
588
- const m = /^(results|majors):([A-Za-z_]+)(?:\.([A-Za-z_]+))?$/.exec(n);
589
- if (m === null)
590
- return probe.publishes(n);
591
- const cols = columnsOf2(await probe.db(m[1]), m[2]);
592
- return m[3] === undefined ? cols.size > 0 : cols.has(m[3]);
593
- }
594
- async function capabilityOf2(probe, f) {
595
- const missing = [];
596
- for (const n of f.needs)
597
- if (!await hasNeed(probe, n))
598
- missing.push(n);
599
- return { ...f, state: missing.length === 0 ? "full" : "missing", missing };
600
- }
601
- function needsRelease2(tag, c) {
602
- 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`);
603
- }
604
-
605
- var ENTRY_RANK = new Map([
606
- ["final", 0],
607
- ["", 1],
608
- ["recall", 2],
609
- ["standings", 3],
610
- ["unplaced", 4],
611
- ["qual", 5],
612
- ["qual-a", 5],
613
- ["qual-b", 5]
614
- ]);
615
- function entryRank2(section) {
616
- const known = ENTRY_RANK.get(section);
617
- if (known !== undefined)
618
- return known;
619
- if (/^solo(?:-r\d+)?$/.test(section))
620
- return 6;
621
- return 9;
350
+ var ENTRY_RANK = new Map([
351
+ ["final", 0],
352
+ ["", 1],
353
+ ["recall", 2],
354
+ ["standings", 3],
355
+ ["unplaced", 4],
356
+ ["qual", 5],
357
+ ["qual-a", 5],
358
+ ["qual-b", 5]
359
+ ]);
360
+ function entryRank2(section) {
361
+ const known = ENTRY_RANK.get(section);
362
+ if (known !== undefined)
363
+ return known;
364
+ if (/^solo(?:-r\d+)?$/.test(section))
365
+ return 6;
366
+ return 9;
622
367
  }
623
368
  var COLUMNS = `r.address AS address, r.competition AS competition, c.event AS event,
624
369
  c.year AS year, c.tier AS tier, c.comp_no AS comp_no, c.title AS title, c.category AS category,
@@ -632,13 +377,9 @@ var COLUMNS = `r.address AS address, r.competition AS competition, c.event AS ev
632
377
  r.yob_printed AS yob_printed, r.points_printed AS points_printed,
633
378
  r.platform_dancer_id AS platform_dancer_id, r.primary_source AS primary_source,
634
379
  r.file AS file, r.line AS line`;
635
- var printedAgeColumn = (carried) => carried ? "r.printed_age AS printed_age" : "NULL AS printed_age";
636
- var streamColumns = (db) => ({
637
- printedAge: columnsOf2(db, "results").has("printed_age")
638
- });
639
380
  var TRANSCRIBED = "('transcript', 'manifested')";
640
381
  var scopeDb = (s) => s === "majors" ? "majors" : "results";
641
- function streamSql(q, carried = { printedAge: false }) {
382
+ function streamSql(q) {
642
383
  const where = [];
643
384
  const args = [];
644
385
  const list = (col, v, nocase = false) => {
@@ -679,7 +420,7 @@ function streamSql(q, carried = { printedAge: false }) {
679
420
  if (q.primary === true)
680
421
  where.push("(r.primary_source IS NULL OR r.primary_source = '')");
681
422
  return {
682
- sql: `SELECT ${COLUMNS}, ${printedAgeColumn(carried.printedAge)} FROM results r JOIN competitions c ON c.file = r.file AND c.line = r.competition_line${where.length === 0 ? "" : ` WHERE ${where.join(" AND ")}`} ORDER BY r.file, r.line`,
423
+ sql: `SELECT ${COLUMNS} FROM results r JOIN competitions c ON c.file = r.file AND c.line = r.competition_line${where.length === 0 ? "" : ` WHERE ${where.join(" AND ")}`} ORDER BY r.file, r.line`,
683
424
  args
684
425
  };
685
426
  }
@@ -702,13 +443,12 @@ function rowOf(r, dates) {
702
443
  withdrawn: text(withdrawn),
703
444
  qualifier: text(rest.qualifier),
704
445
  primary_source: text(rest.primary_source),
705
- printed_age: text(rest.printed_age),
706
446
  dancer_scope: r.dancer_id === null ? null : r.dancer_id >= FEIS_LOCAL_ID_FLOOR2 ? "per-release" : "stable"
707
447
  };
708
448
  }
709
449
  async function* streamRows(db, q) {
710
450
  const dates = feisDates(db);
711
- const { sql, args } = streamSql(q, streamColumns(db));
451
+ const { sql, args } = streamSql(q);
712
452
  const stmt = db.prepare(sql);
713
453
  try {
714
454
  for (const r of stmt.iterate(...args))
@@ -719,7 +459,7 @@ async function* streamRows(db, q) {
719
459
  }
720
460
  async function* entryRows(db, q) {
721
461
  const dates = feisDates(db);
722
- const { sql, args } = streamSql(q, streamColumns(db));
462
+ const { sql, args } = streamSql(q);
723
463
  const stmt = db.prepare(sql);
724
464
  let file = null;
725
465
  let held = new Map;
@@ -904,7 +644,7 @@ var PERSON = { table: "person", value: "id", valueType: "INTEGER PRIMARY KEY" };
904
644
  var ASKED = ["surname_key", "nn4", "blk_surname"];
905
645
  var dancersIndex = {
906
646
  name: "dancers",
907
- version: "4",
647
+ version: "5",
908
648
  inputs: ["results"],
909
649
  keyParts: () => [...PACKAGE_KEY_PARTS],
910
650
  says: "every published dancer print at the names package's keys, its dancers, schools and years",
@@ -916,6 +656,8 @@ var dancersIndex = {
916
656
  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)");
917
657
  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");
918
658
  out.run("CREATE TABLE dancer_regional(dancer_id INTEGER NOT NULL, year INTEGER NOT NULL, region TEXT NOT NULL, regional INTEGER NOT NULL, PRIMARY KEY(dancer_id, year)) WITHOUT ROWID");
659
+ out.run("CREATE TABLE person_block(block TEXT NOT NULL, person INTEGER NOT NULL)");
660
+ const putBlock = out.prepare("INSERT INTO person_block VALUES (?, ?)");
919
661
  const src = rel.db("results");
920
662
  const groups = src.query(`SELECT r.name_printed AS p, r.name_order_resolved AS o, r.dancer_id AS d,
921
663
  r.school_printed AS sp, r.school_id AS s,
@@ -937,6 +679,8 @@ var dancersIndex = {
937
679
  id = ++next;
938
680
  names.add(name, id);
939
681
  personOf.set(reading, id);
682
+ for (const b of nameBlockKeys(name))
683
+ putBlock.run(b, id);
940
684
  }
941
685
  personOf.set(k, id);
942
686
  }
@@ -1012,6 +756,7 @@ var dancersIndex = {
1012
756
  for (const s of schema.indexes)
1013
757
  if (ASKED.some((c) => s.endsWith(`(${c})`)))
1014
758
  out.run(s);
759
+ out.run("CREATE INDEX person_block_block ON person_block(block)");
1015
760
  out.run("CREATE INDEX print_dancer_person ON print_dancer(person)");
1016
761
  out.run("CREATE INDEX print_dancer_dancer ON print_dancer(dancer_id)");
1017
762
  out.run("CREATE INDEX dancer_school_dancer ON dancer_school(dancer_id)");
@@ -1031,6 +776,7 @@ var NAME_WEIGHT = {
1031
776
  nn4: 3,
1032
777
  initial: 2,
1033
778
  middle: 1.5,
779
+ form: 1.25,
1034
780
  surname: 1,
1035
781
  like: 1
1036
782
  };
@@ -1171,7 +917,56 @@ class DancerResolver2 {
1171
917
  if (level !== null)
1172
918
  hits.set(value, level);
1173
919
  }
1174
- return this.answer(db, q, hits);
920
+ const exact = await this.answer(db, q, hits);
921
+ if (exact.arm !== "none")
922
+ return exact;
923
+ const born = await this.answer(db, q, hits, true, { birthSlack: true });
924
+ if (born.arm !== "none")
925
+ return { ...born, widened: "birth-conflict" };
926
+ const forms = this.formsOf(db, names, asked, hits);
927
+ if (forms.size === 0)
928
+ return exact;
929
+ const all = new Map(hits);
930
+ for (const id of forms.keys())
931
+ all.set(id, "form");
932
+ const formed = await this.answer(db, q, all, true, { birthSlack: true, forms });
933
+ return formed.arm === "none" ? exact : { ...formed, widened: "name-form" };
934
+ }
935
+ formsOf(db, names, asked, hits) {
936
+ const out = new Map;
937
+ const keys = nameBlockKeys(asked);
938
+ const persons = new Set;
939
+ if (keys.length > 0)
940
+ for (const r of db.query(`SELECT DISTINCT person FROM person_block WHERE block IN (${keys.map(() => "?").join(",")})`).all(...keys))
941
+ persons.add(r.person);
942
+ const rows = new Map;
943
+ for (const { value, row } of names.rows({ surname_key: [asked.surnameKey()] }, [
944
+ "print",
945
+ "reading"
946
+ ]))
947
+ rows.set(value, row);
948
+ const ids = [...persons].filter((id) => !hits.has(id) && !rows.has(id));
949
+ for (let i = 0;i < ids.length; i += 500) {
950
+ const chunk = ids.slice(i, i + 500);
951
+ for (const r of db.query(`SELECT id, print, reading FROM person WHERE id IN (${chunk.map(() => "?").join(",")})`).all(...chunk))
952
+ rows.set(r.id, { print: r.print, reading: r.reading });
953
+ }
954
+ for (const [id, row] of rows) {
955
+ if (hits.has(id))
956
+ continue;
957
+ const c = ENCODER.decode(row);
958
+ if (!c.isPerson())
959
+ continue;
960
+ const composed = composeNames(asked, c);
961
+ if (composed.composition !== "conflict" && composed.composition !== "silent") {
962
+ out.set(id, [...composed.via]);
963
+ continue;
964
+ }
965
+ const kin = nameKinship(asked, c);
966
+ if (kin.relation !== "different")
967
+ out.set(id, [...kin.via]);
968
+ }
969
+ return out;
1175
970
  }
1176
971
  async search(q) {
1177
972
  if (q.name !== undefined) {
@@ -1222,7 +1017,7 @@ class DancerResolver2 {
1222
1017
  schools: schoolsOf(db, dancerId)
1223
1018
  };
1224
1019
  }
1225
- async answer(db, q, hits, narrow = true) {
1020
+ async answer(db, q, hits, narrow = true, widen = {}) {
1226
1021
  const asOf = yearOf(q.asOf);
1227
1022
  const age = q.age === undefined ? undefined : this.readAge(q.age, asOf);
1228
1023
  const population = q.population === undefined ? undefined : readPopulation2(q.population);
@@ -1231,7 +1026,7 @@ class DancerResolver2 {
1231
1026
  let lineageIds = [];
1232
1027
  let school;
1233
1028
  if (q.school !== undefined) {
1234
- const s = await this.rel.schools.resolve(q.school);
1029
+ const s = await this.rel.schools.resolve(q.school, { year: askedYear(q, asOf) });
1235
1030
  schoolIds = s.candidates.map((c) => c.school_id);
1236
1031
  const lineage = await this.lineage();
1237
1032
  if (lineage === null)
@@ -1251,9 +1046,11 @@ class DancerResolver2 {
1251
1046
  unlinked.set(r.print, (unlinked.get(r.print) ?? 0) + r.n);
1252
1047
  continue;
1253
1048
  }
1254
- const e = byDancer.get(r.d) ?? { level, prints: [] };
1049
+ const e = byDancer.get(r.d) ?? { level, prints: [], via: new Set };
1255
1050
  e.level = finer(e.level, level);
1256
1051
  e.prints.push({ print: r.print, rows: r.n, level });
1052
+ for (const v of widen.forms?.get(person) ?? [])
1053
+ e.via.add(v);
1257
1054
  byDancer.set(r.d, e);
1258
1055
  }
1259
1056
  const dq = db.query("SELECT * FROM dancer WHERE dancer_id = ?");
@@ -1264,7 +1061,15 @@ class DancerResolver2 {
1264
1061
  continue;
1265
1062
  const schools = schoolsOf(db, id);
1266
1063
  const latest = latestSchool(schools, asOf);
1267
- const ageEv = ageEvidence(d, age?.age ?? null);
1064
+ let ageEv = ageEvidence(d, age?.age ?? null);
1065
+ let widenedBirth = false;
1066
+ if (widen.birthSlack && ageEv === "contradicts" && d.birth_conflict !== 0) {
1067
+ const wider = ageEvidence(d, age?.age ?? null, 1);
1068
+ if (wider !== "contradicts") {
1069
+ ageEv = wider;
1070
+ widenedBirth = true;
1071
+ }
1072
+ }
1268
1073
  const pop = populationOf(db, id);
1269
1074
  const popEv = populationEvidence(pop, population?.read ?? null);
1270
1075
  const schoolEv = schoolIds === null ? "unasked" : schoolIds.length === 0 ? "unresolved" : await this.schoolEvidence(schools, schoolIds, lineageIds, asOf);
@@ -1285,8 +1090,10 @@ class DancerResolver2 {
1285
1090
  age: ageEv,
1286
1091
  school: schoolEv,
1287
1092
  population: popEv,
1288
- region: regionEv
1093
+ region: regionEv,
1094
+ ...e.level === "form" ? { form: [...e.via] } : {}
1289
1095
  },
1096
+ ...widenedBirth ? { widened: "birth-conflict" } : {},
1290
1097
  prints: e.prints.sort((a, b) => b.rows - a.rows || (a.print < b.print ? -1 : 1)),
1291
1098
  rows: d.rows,
1292
1099
  year_lo: d.year_lo,
@@ -1313,18 +1120,54 @@ class DancerResolver2 {
1313
1120
  return false;
1314
1121
  });
1315
1122
  const year = askedYear(q, asOf);
1316
- let initials;
1317
- if (narrow && kept.length > 1 && schoolIds !== null && schoolIds.length > 0 && initialsOnly2(dancerName(q.name, q.order === "unknown" ? undefined : q.order))) {
1123
+ const band = age?.age?.birthYears() ?? null;
1124
+ const adultBy = adultBornBy(year);
1125
+ const adultAsked = adultBy !== null && adultCategory(q.age, age?.age ?? null);
1126
+ const returningOf = (c) => adultAsked && c.birth.hi !== null && c.birth.hi <= adultBy ? c.birth.lo : null;
1127
+ let line = null;
1128
+ if (schoolIds !== null && schoolIds.length > 0) {
1318
1129
  const lineage = await this.lineage();
1319
- const line = lineage === null ? new Set(schoolIds) : schoolLine(lineage, schoolIds, year);
1320
- const band = age?.age?.birthYears() ?? null;
1130
+ line = lineage === null ? new Set(schoolIds) : schoolLine(lineage, schoolIds, year);
1131
+ }
1132
+ const formFacts = new Map;
1133
+ if (narrow && widen.forms !== undefined && line !== null) {
1134
+ const next = [];
1135
+ for (const c of kept) {
1136
+ if (c.evidence.nameLevel === "form") {
1137
+ const fact = await this.schoolRefutes(schoolsOf(db, c.dancer_id), line, year, q.school, returningOf(c));
1138
+ if (fact !== null) {
1139
+ formFacts.set(c.dancer_id, fact);
1140
+ excluded.push({ candidate: c, why: "name-form" });
1141
+ continue;
1142
+ }
1143
+ }
1144
+ next.push(c);
1145
+ }
1146
+ kept = next;
1147
+ }
1148
+ const askedName = dancerName(q.name, q.order === "unknown" ? undefined : q.order);
1149
+ const byInitials = initialsOnly2(askedName);
1150
+ let narrowing;
1151
+ if (narrow && kept.length > 1 && line !== null) {
1321
1152
  const slack = q.age !== undefined && !("yob" in q.age) ? DANCE_UP_YEARS : 0;
1322
1153
  const open = [];
1323
1154
  const held = [];
1324
1155
  const refuted = [];
1325
1156
  const bySchool = new Set;
1157
+ const givenFact = (c) => {
1158
+ if (byInitials || c.evidence.nameLevel !== "initial")
1159
+ return false;
1160
+ const fact = otherGiven(db, c.dancer_id, askedName);
1161
+ if (fact === null)
1162
+ return false;
1163
+ refuted.push({ dancer_id: c.dancer_id, why: "given", fact });
1164
+ bySchool.add(c.dancer_id);
1165
+ return true;
1166
+ };
1326
1167
  const schoolFact = async (c) => {
1327
- const fact = await this.schoolRefutes(schoolsOf(db, c.dancer_id), line, year, q.school);
1168
+ if (givenFact(c))
1169
+ return true;
1170
+ const fact = await this.schoolRefutes(schoolsOf(db, c.dancer_id), line, year, q.school, returningOf(c));
1328
1171
  if (fact === null)
1329
1172
  return false;
1330
1173
  refuted.push({ dancer_id: c.dancer_id, why: "school", fact });
@@ -1345,10 +1188,10 @@ class DancerResolver2 {
1345
1188
  refuted.push({
1346
1189
  dancer_id: c.dancer_id,
1347
1190
  why: x.why,
1348
- fact: contradictionFact(x, age, slack)
1191
+ fact: formFacts.get(c.dancer_id) ?? contradictionFact(x, age, slack)
1349
1192
  });
1350
1193
  }
1351
- initials = {
1194
+ narrowing = {
1352
1195
  open: [...open.map((c) => c.dancer_id), ...held],
1353
1196
  heldByDanceUp: held,
1354
1197
  refuted,
@@ -1357,7 +1200,10 @@ class DancerResolver2 {
1357
1200
  if (open.length === 1 && held.length === 0) {
1358
1201
  for (const c of kept)
1359
1202
  if (bySchool.has(c.dancer_id))
1360
- excluded.push({ candidate: c, why: "school" });
1203
+ excluded.push({
1204
+ candidate: c,
1205
+ why: refuted.find((r) => r.dancer_id === c.dancer_id)?.why === "given" ? "given" : "school"
1206
+ });
1361
1207
  kept = open;
1362
1208
  }
1363
1209
  }
@@ -1387,8 +1233,8 @@ class DancerResolver2 {
1387
1233
  };
1388
1234
  }
1389
1235
  }
1390
- if (preferred === undefined && kept.length > 1 && initials !== undefined) {
1391
- const openIds = new Set(initials.open);
1236
+ if (preferred === undefined && kept.length > 1 && narrowing !== undefined) {
1237
+ const openIds = new Set(narrowing.open);
1392
1238
  const hit = kept.filter((c) => openIds.has(c.dancer_id) && c.year_hi !== null && c.year_hi >= year - 1);
1393
1239
  if (hit.length === 1) {
1394
1240
  const c = hit[0];
@@ -1410,7 +1256,7 @@ class DancerResolver2 {
1410
1256
  ...population !== undefined ? { population } : {},
1411
1257
  ...oireachtas !== undefined ? { oireachtas } : {},
1412
1258
  ...school !== undefined ? { school } : {},
1413
- ...initials !== undefined ? { initials } : {}
1259
+ ...narrowing !== undefined ? byInitials ? { initials: narrowing } : { fullName: narrowing } : {}
1414
1260
  };
1415
1261
  }
1416
1262
  async readOireachtas(db, o, asOf) {
@@ -1463,20 +1309,30 @@ class DancerResolver2 {
1463
1309
  }
1464
1310
  return joined ? "lineage" : earlier ? "moved" : "none";
1465
1311
  }
1466
- async schoolRefutes(schools, line, year, asked) {
1312
+ async schoolRefutes(schools, line, year, asked, returning = null) {
1467
1313
  const rows = schools.filter((s) => s.year_lo === null || s.year_lo <= year);
1468
1314
  if (rows.length === 0)
1469
1315
  return null;
1470
1316
  const seen = new Set;
1317
+ const kept = [];
1318
+ let youth = 0;
1471
1319
  for (const s of rows) {
1472
1320
  const own = await this.schoolIdsOf(s);
1473
1321
  if (own.length === 0 || own.some((id) => line.has(id)))
1474
1322
  return null;
1323
+ if (returning !== null && youthRow(s, returning)) {
1324
+ youth++;
1325
+ continue;
1326
+ }
1327
+ kept.push(s);
1475
1328
  for (const id of own)
1476
1329
  seen.add(id);
1477
1330
  }
1478
- const last = latestSchool(rows, year);
1479
- return `every school row to ${year} reads ${[...seen].sort().join(", ")} (the latest ${last?.school_id ?? last?.print} in ${last?.year_hi}), none of them ${asked ?? "the asked school"} or a school its line joined by ${year}`;
1331
+ if (kept.length === 0)
1332
+ return null;
1333
+ const last = latestSchool(kept, year);
1334
+ const set = youth > 0 ? ` (${youth} youth row(s) set aside for an adult-category question)` : "";
1335
+ return `every school row to ${year} reads ${[...seen].sort().join(", ")} (the latest ${last?.school_id ?? last?.print} in ${last?.year_hi}), none of them ${asked ?? "the asked school"} or a school its line joined by ${year}${set}`;
1480
1336
  }
1481
1337
  async schoolIdsOf(s) {
1482
1338
  const key = s.school_id ?? s.print;
@@ -1515,6 +1371,40 @@ class DancerResolver2 {
1515
1371
  return done(Age.from(c.age, { year: c.year }));
1516
1372
  }
1517
1373
  }
1374
+ function adultBornBy(year) {
1375
+ const a = Age.from("adult", { year });
1376
+ return a.isAge() ? a.birthYears().to : null;
1377
+ }
1378
+ function adultCategory(asked, read) {
1379
+ if (read?.isAge() && read.isAdult())
1380
+ return true;
1381
+ if (asked === undefined || !("band" in asked))
1382
+ return false;
1383
+ const t = compTitle(asked.band).age;
1384
+ return t?.isAge() === true && t.isAdult();
1385
+ }
1386
+ function youthRow(s, bornLo) {
1387
+ if (bornLo === null || s.year_hi === null)
1388
+ return false;
1389
+ const by = adultBornBy(s.year_hi);
1390
+ return by !== null && bornLo > by;
1391
+ }
1392
+ function otherGiven(db, dancerId, asked) {
1393
+ const r = db.query("SELECT names_printed FROM dancer WHERE dancer_id = ?").get(dancerId);
1394
+ if (r === null)
1395
+ return null;
1396
+ for (const print of JSON.parse(r.names_printed)) {
1397
+ const p = dancerName(print);
1398
+ if (!p.isPerson() || (p.givenFields()[0]?.length ?? 0) < 2)
1399
+ continue;
1400
+ if (nameLevel2(asked, p) !== null)
1401
+ continue;
1402
+ if (nameKinship(asked, p).relation !== "different")
1403
+ continue;
1404
+ return `printed "${print}", another given name than the asked one, and met it only through an initial`;
1405
+ }
1406
+ return null;
1407
+ }
1518
1408
  function birthOf(d) {
1519
1409
  return {
1520
1410
  lo: d.birth_lo,
@@ -1582,12 +1472,12 @@ function regionEvidence(db, dancerId, region, year) {
1582
1472
  return { word: "contradicts", region: at(latest) };
1583
1473
  return { word: latest.region === region ? "same" : "moved", region: at(latest) };
1584
1474
  }
1585
- function ageEvidence(d, asked) {
1475
+ function ageEvidence(d, asked, slack = 0) {
1586
1476
  if (asked === null)
1587
1477
  return "unasked";
1588
1478
  if (d.birth_lo === null && d.birth_hi === null)
1589
1479
  return "unknown";
1590
- const own = Age.fromYOB(d.birth_lo, d.birth_hi);
1480
+ const own = Age.fromYOB(d.birth_lo === null ? null : d.birth_lo - slack, d.birth_hi === null ? null : d.birth_hi + slack);
1591
1481
  if (!own.isAge())
1592
1482
  return "unknown";
1593
1483
  if (own.fitsIn(asked))
@@ -1620,7 +1510,7 @@ function contradictionFact(x, age, slack) {
1620
1510
  import { Age as Age2 } from "@finbheara/domain/age";
1621
1511
  import {
1622
1512
  codePopulation,
1623
- compTitle,
1513
+ compTitle as compTitle2,
1624
1514
  matchTitle,
1625
1515
  populationIn as populationIn2,
1626
1516
  secondIn,
@@ -1689,7 +1579,7 @@ function compOf(r, codes = []) {
1689
1579
  const stated = r.gender_source === "level" ? "" : populationIn2(r.gender ?? "");
1690
1580
  const pop = stated || populationIn2(r.title ?? "") || (r.platform === "ifeis" && codes.length > 0 ? codePopulation(r.code, codes) : "") || populationIn2(r.gender ?? "");
1691
1581
  const second = [r.title, r.level_printed, r.dance_printed].some((t) => t != null && secondIn(t));
1692
- const round = r.round ?? (r.title === null ? null : compTitle(r.title).round?.toString() ?? null);
1582
+ const round = r.round ?? (r.title === null ? null : compTitle2(r.title).round?.toString() ?? null);
1693
1583
  return {
1694
1584
  competition: r.competition,
1695
1585
  feis_id: r.feis_id,
@@ -1950,7 +1840,7 @@ class CompResolver2 {
1950
1840
  if (q.title === undefined || q.title.trim() === "")
1951
1841
  return answer("unique", "prior-year", hits, { from, check: "unchecked" });
1952
1842
  const m = matchTitle(q.title, [entryOf(hit)]);
1953
- const r = titleRelation(m.title, compTitle(hit.title ?? "", {}));
1843
+ const r = titleRelation(m.title, compTitle2(hit.title ?? "", {}));
1954
1844
  const check = {
1955
1845
  title: q.title,
1956
1846
  agrees: m.isSingular(),
@@ -2222,22 +2112,28 @@ var encoderOf = (r) => schoolEncoder({ reader: r.reader });
2222
2112
  var TABLE = "school_name";
2223
2113
  var schoolsIndex = {
2224
2114
  name: "schools",
2225
- version: "2",
2115
+ version: "3",
2226
2116
  inputs: ["results", "school-registry.jsonl", "school-redirects.jsonl"],
2227
2117
  says: "every published school print and its ids, read at the school reader's slots",
2228
2118
  keyParts: (rel) => [`reader=${rel.schoolReader().id}`],
2229
2119
  build(rel, out) {
2230
2120
  const choice = rel.schoolReader();
2231
2121
  const encoder = encoderOf(choice);
2232
- out.run("CREATE TABLE print_id(print TEXT NOT NULL, school_id TEXT NOT NULL, rows INTEGER NOT NULL, PRIMARY KEY(print, school_id)) WITHOUT ROWID");
2122
+ out.run("CREATE TABLE print_id(print TEXT NOT NULL, school_id TEXT NOT NULL, rows INTEGER NOT NULL, year_lo INTEGER, year_hi INTEGER, PRIMARY KEY(print, school_id)) WITHOUT ROWID");
2233
2123
  out.run("CREATE TABLE registry(school_id TEXT PRIMARY KEY, display TEXT NOT NULL, regions TEXT NOT NULL, places TEXT NOT NULL)");
2234
2124
  out.run("CREATE TABLE redirect(old_id TEXT PRIMARY KEY, new_id TEXT NOT NULL, kind TEXT NOT NULL)");
2235
2125
  const schema = indexSchema3(encoder, { table: TABLE });
2236
2126
  out.run(schema.table);
2237
- const pairs = rel.db("results").query("SELECT trim(school_printed) AS p, school_id AS s, count(*) AS n FROM results WHERE school_id IS NOT NULL GROUP BY 1, 2 ORDER BY 1, 2").all();
2238
- const putPair = out.prepare("INSERT INTO print_id VALUES (?, ?, ?)");
2127
+ const src = rel.db("results");
2128
+ const lined = src.query("PRAGMA table_info(results)").all().some((c) => c.name === "competition_line");
2129
+ const pairs = src.query(lined ? `SELECT trim(r.school_printed) AS p, r.school_id AS s, count(*) AS n,
2130
+ min(c.year) AS lo, max(c.year) AS hi
2131
+ FROM results r LEFT JOIN competitions c ON c.file = r.file AND c.line = r.competition_line
2132
+ WHERE r.school_id IS NOT NULL GROUP BY 1, 2 ORDER BY 1, 2` : `SELECT trim(school_printed) AS p, school_id AS s, count(*) AS n, NULL AS lo, NULL AS hi
2133
+ FROM results WHERE school_id IS NOT NULL GROUP BY 1, 2 ORDER BY 1, 2`).all();
2134
+ const putPair = out.prepare("INSERT INTO print_id VALUES (?, ?, ?, ?, ?)");
2239
2135
  for (const r of pairs)
2240
- putPair.run(r.p, r.s, r.n);
2136
+ putPair.run(r.p, r.s, r.n, r.lo, r.hi);
2241
2137
  out.run("CREATE INDEX print_id_school ON print_id(school_id)");
2242
2138
  const names = createIndex3(out, encoder, {
2243
2139
  table: TABLE
@@ -2282,6 +2178,23 @@ class SchoolResolver2 {
2282
2178
  };
2283
2179
  const answer = (candidates, extra = {}) => {
2284
2180
  let cs = candidates.sort((a, b) => b.rows - a.rows || (a.school_id < b.school_id ? -1 : 1));
2181
+ let year;
2182
+ if (q.year !== undefined && cs.length > 1 && cs.every((c) => c.years !== undefined)) {
2183
+ const y = q.year;
2184
+ const span = (c) => c.years;
2185
+ let kept = cs.filter((c) => {
2186
+ const { lo, hi } = span(c);
2187
+ return lo !== null && hi !== null && lo <= y && y <= hi;
2188
+ });
2189
+ if (kept.length === 0) {
2190
+ const before = cs.filter((c) => span(c).hi !== null && span(c).hi < y);
2191
+ const last = Math.max(...before.map((c) => span(c).hi));
2192
+ kept = before.filter((c) => span(c).hi === last);
2193
+ }
2194
+ year = { asked: y, narrowed: kept.length > 0 && kept.length < cs.length };
2195
+ if (kept.length > 0)
2196
+ cs = kept;
2197
+ }
2285
2198
  let region;
2286
2199
  if (q.region !== undefined) {
2287
2200
  const kept = cs.filter((c) => c.regions.length === 0 || c.regions.includes(q.region));
@@ -2295,6 +2208,7 @@ class SchoolResolver2 {
2295
2208
  arm: cs.length === 0 ? "none" : cs.length === 1 ? "unique" : "ambiguous",
2296
2209
  candidates: cs,
2297
2210
  ...region !== undefined ? { region } : {},
2211
+ ...year !== undefined ? { year } : {},
2298
2212
  ...extra
2299
2213
  };
2300
2214
  };
@@ -2318,15 +2232,27 @@ class SchoolResolver2 {
2318
2232
  return answer([byId(live, "redirect")], { redirected: { from: a, to: live } });
2319
2233
  const byPrints = (prints, by) => {
2320
2234
  const ids = new Map;
2321
- const q2 = db.query("SELECT school_id, rows FROM print_id WHERE print = ?");
2235
+ const q2 = db.query("SELECT school_id, rows, year_lo AS lo, year_hi AS hi FROM print_id WHERE print = ?");
2322
2236
  for (const p of prints)
2323
2237
  for (const r of q2.all(p)) {
2324
- const e = ids.get(r.school_id) ?? { rows: 0, prints: new Set };
2238
+ const e = ids.get(r.school_id) ?? {
2239
+ rows: 0,
2240
+ prints: new Set,
2241
+ lo: null,
2242
+ hi: null
2243
+ };
2325
2244
  e.rows += r.rows;
2326
2245
  e.prints.add(p);
2246
+ if (r.lo !== null && (e.lo === null || r.lo < e.lo))
2247
+ e.lo = r.lo;
2248
+ if (r.hi !== null && (e.hi === null || r.hi > e.hi))
2249
+ e.hi = r.hi;
2327
2250
  ids.set(r.school_id, e);
2328
2251
  }
2329
- return [...ids].map(([id, e]) => candidate(id, by, e.rows, [...e.prints].sort()));
2252
+ return [...ids].map(([id, e]) => ({
2253
+ ...candidate(id, by, e.rows, [...e.prints].sort()),
2254
+ years: { lo: e.lo, hi: e.hi }
2255
+ }));
2330
2256
  };
2331
2257
  const exact = byPrints([a], "print");
2332
2258
  if (exact.length > 0)
@@ -2732,7 +2658,7 @@ import {
2732
2658
  import { join as join3 } from "path";
2733
2659
 
2734
2660
  import { Age as Age3 } from "@finbheara/domain/age";
2735
- import { compTitle as compTitle2 } from "@finbheara/domain/title";
2661
+ import { compTitle as compTitle3 } from "@finbheara/domain/title";
2736
2662
  function youngest2(answers) {
2737
2663
  const key = (a) => {
2738
2664
  const iv = Age3.from(a.age, { year: null }).interval();
@@ -2812,7 +2738,7 @@ class RegistrationResolver2 {
2812
2738
  const printed = first !== undefined ? [] : comps.flatMap((c, i) => {
2813
2739
  const code = row.codes[i];
2814
2740
  const t = row.titles?.[code];
2815
- const a = t === undefined ? null : compTitle2(t).age;
2741
+ const a = t === undefined ? null : compTitle3(t).age;
2816
2742
  return a?.isAge() === true && a.kind !== "yob" ? [{ age: a.ag1, year: c.feis.year, code }] : [];
2817
2743
  });
2818
2744
  const byTitle = youngest2(printed);
@@ -3022,6 +2948,263 @@ function defaultRemote() {
3022
2948
  return installed ?? fallbackRemote2(ghRemote2, httpsRemote2());
3023
2949
  }
3024
2950
 
2951
+ function columnsOf2(db, table) {
2952
+ return new Set(db.query("SELECT name FROM pragma_table_info(?)").all(table).map((r) => r.name));
2953
+ }
2954
+ var FEATURES2 = [
2955
+ {
2956
+ feature: "results.level",
2957
+ needs: ["results:competitions.feis_level"],
2958
+ since: 215,
2959
+ calls: "results.query, majors.results"
2960
+ },
2961
+ {
2962
+ feature: "majors.order",
2963
+ needs: ["majors:competitions.seq"],
2964
+ since: 260,
2965
+ calls: "majors.competitions"
2966
+ },
2967
+ {
2968
+ feature: "dancers",
2969
+ needs: ["results:results.name_order_resolved"],
2970
+ since: 260,
2971
+ calls: "dancers.resolve, dancers.search, dancers.get (the dancers index)"
2972
+ },
2973
+ {
2974
+ feature: "schools",
2975
+ needs: ["school-registry.jsonl", "school-redirects.jsonl"],
2976
+ since: 260,
2977
+ calls: "schools.resolve, a dancer question with a school (the schools index)"
2978
+ },
2979
+ {
2980
+ feature: "feises",
2981
+ needs: [
2982
+ "results:feises.date_lo",
2983
+ "results:feises.date_hi",
2984
+ "results:feises.feis_instance_id",
2985
+ "results:feises.feis_brand_id",
2986
+ "results:competitions.feis_region",
2987
+ "feis-redirects.jsonl"
2988
+ ],
2989
+ since: 265,
2990
+ calls: "feises.find, feises.series, and comps and registrations through them (the feises index)"
2991
+ },
2992
+ {
2993
+ feature: "comps",
2994
+ needs: [
2995
+ "results:competitions.feis_level",
2996
+ "results:competitions.round",
2997
+ "results:feises.feis_brand_id"
2998
+ ],
2999
+ since: 265,
3000
+ calls: "comps.resolve, comps.syllabus, comps.syllabusFor, registrations.resolve (the comps index)"
3001
+ },
3002
+ {
3003
+ feature: "comps.dance",
3004
+ needs: ["results:competitions.dance"],
3005
+ since: 270,
3006
+ calls: "the `dance` a comp of comps.resolve, comps.syllabus and comps.syllabusFor carries: null, never refused, on a release without it"
3007
+ },
3008
+ {
3009
+ feature: "schools.lineage",
3010
+ needs: ["school-lineage.jsonl"],
3011
+ since: 184,
3012
+ calls: "the `lineage` school evidence of dancers.resolve and dancers.search: no school is joined by lineage, never refused, on a release without it"
3013
+ },
3014
+ {
3015
+ feature: "results.stream",
3016
+ needs: [
3017
+ "results:competitions.round",
3018
+ "results:competitions.feis_level",
3019
+ "results:competitions.level_printed",
3020
+ "results:competitions.age_printed",
3021
+ "results:results.yob_printed",
3022
+ "results:results.primary_source",
3023
+ "results:results.platform_dancer_id",
3024
+ "results:results.relative_place",
3025
+ "results:feises.date_lo",
3026
+ "results:feises.date_hi"
3027
+ ],
3028
+ since: 255,
3029
+ calls: "results.stream, results.entries (scopes transcribed, platform, all)"
3030
+ },
3031
+ {
3032
+ feature: "majors.stream",
3033
+ needs: [
3034
+ "majors:competitions.round",
3035
+ "majors:competitions.feis_level",
3036
+ "majors:results.yob_printed",
3037
+ "majors:results.primary_source",
3038
+ "majors:results.relative_place",
3039
+ "majors:feises.date_lo"
3040
+ ],
3041
+ since: 255,
3042
+ calls: "results.stream, results.entries (scope majors)"
3043
+ },
3044
+ {
3045
+ feature: "majors.tabs",
3046
+ needs: ["majors:tabs.address", "majors:tabs.true_round"],
3047
+ since: 215,
3048
+ calls: "majors.tabulated"
3049
+ },
3050
+ {
3051
+ feature: "feises.dates",
3052
+ needs: ["results:feises.feis_id", "results:feises.date_lo", "results:feises.date_hi"],
3053
+ since: 245,
3054
+ calls: "feises.dates"
3055
+ },
3056
+ {
3057
+ feature: "dancers.records",
3058
+ needs: [
3059
+ "results:dancers.display",
3060
+ "results:dancers.appearances",
3061
+ "results:dancers.birth_lo",
3062
+ "results:dancers.birth_hi",
3063
+ "results:dancers.birth_source",
3064
+ "results:dancers.birth_conflict",
3065
+ "results:dancers.genders"
3066
+ ],
3067
+ since: 215,
3068
+ calls: "dancers.records"
3069
+ },
3070
+ {
3071
+ feature: "dancers.years",
3072
+ needs: [
3073
+ "results:dancer_years.region",
3074
+ "results:dancer_years.region_source",
3075
+ "results:dancer_years.nationals",
3076
+ "results:dancer_years.nationals_source",
3077
+ "results:dancer_years.geohash"
3078
+ ],
3079
+ since: 245,
3080
+ calls: "dancers.years"
3081
+ },
3082
+ {
3083
+ feature: "teams",
3084
+ needs: [
3085
+ ...[
3086
+ "slug",
3087
+ "address",
3088
+ "event",
3089
+ "year",
3090
+ "card",
3091
+ "comp_no",
3092
+ "title",
3093
+ "category",
3094
+ "hands",
3095
+ "letter",
3096
+ "label",
3097
+ "name_printed",
3098
+ "school_printed",
3099
+ "school_loc",
3100
+ "school_id",
3101
+ "place",
3102
+ "place_printed",
3103
+ "tie",
3104
+ "competitors",
3105
+ "atts"
3106
+ ].map((c) => `table:team_results.${c}`),
3107
+ ...[
3108
+ "team",
3109
+ "kind",
3110
+ "event_year",
3111
+ "year",
3112
+ "comp_no",
3113
+ "title",
3114
+ "team_index",
3115
+ "seq",
3116
+ "school_id",
3117
+ "school_print_materialized"
3118
+ ].map((c) => `table:roster_teams.${c}`),
3119
+ ...["team", "seq", "name_printed", "name", "yob", "dancer_id"].map((c) => `table:roster_members.${c}`),
3120
+ ...[
3121
+ "event_year",
3122
+ "comp_no",
3123
+ "team",
3124
+ "result_address",
3125
+ "school_id",
3126
+ "join_kind",
3127
+ "via_split",
3128
+ "roster_teams",
3129
+ "result_teams",
3130
+ "roster_index",
3131
+ "result_letter"
3132
+ ].map((c) => `table:team_result_rosters.${c}`),
3133
+ "table:event_years.event_year",
3134
+ "table:event_years.event",
3135
+ "table:event_years.year"
3136
+ ],
3137
+ since: 260,
3138
+ calls: "teams.results, teams.rosters, teams.resultRosters (the teams index)"
3139
+ },
3140
+ {
3141
+ feature: "teams.fields",
3142
+ needs: [
3143
+ "table:team_results.age",
3144
+ "table:team_results.gender",
3145
+ "table:team_results.printed_status",
3146
+ "table:roster_teams.dance",
3147
+ "table:roster_teams.school_print_as_of",
3148
+ "table:roster_members.gender"
3149
+ ],
3150
+ since: 266,
3151
+ calls: "the fields teams.results (age, gender, printed_status) and teams.rosters (dance, school_printed_as_of, a member's gender) carry where present: absent, never refused, on a release without them"
3152
+ },
3153
+ {
3154
+ feature: "events",
3155
+ needs: [
3156
+ "event_year",
3157
+ "event",
3158
+ "year",
3159
+ "date_lo",
3160
+ "date_hi",
3161
+ "resolution",
3162
+ "source",
3163
+ "date_printed",
3164
+ "place",
3165
+ "place_printed"
3166
+ ].map((c) => `table:event_years.${c}`),
3167
+ since: 260,
3168
+ calls: "events.years (the events index)"
3169
+ },
3170
+ {
3171
+ feature: "staff",
3172
+ needs: [
3173
+ "results:adjudicators",
3174
+ "results:feis_panels",
3175
+ "school-staff.jsonl",
3176
+ "school-teachers.jsonl",
3177
+ "school-registry.jsonl"
3178
+ ],
3179
+ since: 260,
3180
+ calls: "staff.teacher, staff.adjudicator (the staff index)"
3181
+ }
3182
+ ];
3183
+ async function hasNeed(probe, n) {
3184
+ const t = /^table:([A-Za-z_]+)(?:\.([A-Za-z_]+))?$/.exec(n);
3185
+ if (t !== null) {
3186
+ if (probe.tableColumns === undefined)
3187
+ return false;
3188
+ const cols = await probe.tableColumns(t[1]);
3189
+ return t[2] === undefined ? cols.size > 0 : cols.has(t[2]);
3190
+ }
3191
+ const m = /^(results|majors):([A-Za-z_]+)(?:\.([A-Za-z_]+))?$/.exec(n);
3192
+ if (m === null)
3193
+ return probe.publishes(n);
3194
+ const cols = columnsOf2(await probe.db(m[1]), m[2]);
3195
+ return m[3] === undefined ? cols.size > 0 : cols.has(m[3]);
3196
+ }
3197
+ async function capabilityOf2(probe, f) {
3198
+ const missing = [];
3199
+ for (const n of f.needs)
3200
+ if (!await hasNeed(probe, n))
3201
+ missing.push(n);
3202
+ return { ...f, state: missing.length === 0 ? "full" : "missing", missing };
3203
+ }
3204
+ function needsRelease2(tag, c) {
3205
+ 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`);
3206
+ }
3207
+
3025
3208
  function tagDir(root, tag) {
3026
3209
  if (parseArchiveTag2(tag) === null)
3027
3210
  throw new Error(`archive-client: "${tag}" is not a release tag (archive-YYYY.MM.DD[-N[.k]])`);
@@ -3540,4 +3723,4 @@ function releases2(opts = {}) {
3540
3723
  return new ReleaseManager2(opts);
3541
3724
  }
3542
3725
 
3543
- export { DATABASES2, planAsset2, sha256File2, unzstdTo2, NAME_LEVELS2, nameLevel2, NO_CALENDAR2, FeisFinder2, columnsOf2, FEATURES2, capabilityOf2, needsRelease2, entryRank2, FEIS_LOCAL_ID_FLOOR2, ResultFinder2, MajorsFinder2, initialsOnly2, nameOrderOf, DancerResolver2, readPopulation2, canonicalOf2, CompResolver2, StaffFinder2, SchoolResolver2, TeamFinder2, EventFinder2, defaultBuilders2, indexPath2, indexKey2, isCurrent2, buildIndex2, ensureIndex2, listIndexes2, readPin2, youngest2, dancerResolverOf2, RegistrationResolver2, ghRemote2, httpsRemote2, fallbackRemote2, setRemote2, resolveTag2, Release2, openRelease2, openCached2, ReleaseManager2, releases2 };
3726
+ export { DATABASES2, planAsset2, sha256File2, unzstdTo2, NAME_LEVELS2, nameLevel2, NO_CALENDAR2, FeisFinder2, entryRank2, FEIS_LOCAL_ID_FLOOR2, ResultFinder2, MajorsFinder2, initialsOnly2, nameOrderOf, DancerResolver2, readPopulation2, canonicalOf2, CompResolver2, StaffFinder2, SchoolResolver2, TeamFinder2, EventFinder2, defaultBuilders2, indexPath2, indexKey2, isCurrent2, buildIndex2, ensureIndex2, listIndexes2, readPin2, youngest2, dancerResolverOf2, RegistrationResolver2, ghRemote2, httpsRemote2, fallbackRemote2, setRemote2, columnsOf2, FEATURES2, capabilityOf2, needsRelease2, resolveTag2, Release2, openRelease2, openCached2, ReleaseManager2, releases2 };