@finbheara/archive-client 0.9.0 → 0.11.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.
@@ -67,7 +67,7 @@ function sha256OfDigest(digest) {
67
67
  return m === null ? null : m[1].toLowerCase();
68
68
  }
69
69
 
70
- import { Age } from "@finbheara/domain/age";
70
+ 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";
@@ -339,6 +339,269 @@ function notOnTheFloor(placePrinted, atts, withdrawn) {
339
339
  return withdrawn !== null && DID_NOT_TABULATE.has(withdrawn);
340
340
  }
341
341
 
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
+
342
605
  var ENTRY_RANK = new Map([
343
606
  ["final", 0],
344
607
  ["", 1],
@@ -369,9 +632,13 @@ var COLUMNS = `r.address AS address, r.competition AS competition, c.event AS ev
369
632
  r.yob_printed AS yob_printed, r.points_printed AS points_printed,
370
633
  r.platform_dancer_id AS platform_dancer_id, r.primary_source AS primary_source,
371
634
  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
+ });
372
639
  var TRANSCRIBED = "('transcript', 'manifested')";
373
640
  var scopeDb = (s) => s === "majors" ? "majors" : "results";
374
- function streamSql(q) {
641
+ function streamSql(q, carried = { printedAge: false }) {
375
642
  const where = [];
376
643
  const args = [];
377
644
  const list = (col, v, nocase = false) => {
@@ -412,7 +679,7 @@ function streamSql(q) {
412
679
  if (q.primary === true)
413
680
  where.push("(r.primary_source IS NULL OR r.primary_source = '')");
414
681
  return {
415
- 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`,
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`,
416
683
  args
417
684
  };
418
685
  }
@@ -435,12 +702,13 @@ function rowOf(r, dates) {
435
702
  withdrawn: text(withdrawn),
436
703
  qualifier: text(rest.qualifier),
437
704
  primary_source: text(rest.primary_source),
705
+ printed_age: text(rest.printed_age),
438
706
  dancer_scope: r.dancer_id === null ? null : r.dancer_id >= FEIS_LOCAL_ID_FLOOR2 ? "per-release" : "stable"
439
707
  };
440
708
  }
441
709
  async function* streamRows(db, q) {
442
710
  const dates = feisDates(db);
443
- const { sql, args } = streamSql(q);
711
+ const { sql, args } = streamSql(q, streamColumns(db));
444
712
  const stmt = db.prepare(sql);
445
713
  try {
446
714
  for (const r of stmt.iterate(...args))
@@ -451,7 +719,7 @@ async function* streamRows(db, q) {
451
719
  }
452
720
  async function* entryRows(db, q) {
453
721
  const dates = feisDates(db);
454
- const { sql, args } = streamSql(q);
722
+ const { sql, args } = streamSql(q, streamColumns(db));
455
723
  const stmt = db.prepare(sql);
456
724
  let file = null;
457
725
  let held = new Map;
@@ -609,6 +877,21 @@ class MajorsFinder2 {
609
877
  }
610
878
 
611
879
  var LINEAGE_FILE = "school-lineage.jsonl";
880
+ function schoolLine(lineage, ids, year) {
881
+ const reached = new Set(ids);
882
+ const todo = [...ids];
883
+ for (let id = todo.pop();id !== undefined; id = todo.pop())
884
+ for (const [o, y] of lineage.get(id) ?? [])
885
+ if (!reached.has(o) && (year === null || y === null || y <= year)) {
886
+ reached.add(o);
887
+ todo.push(o);
888
+ }
889
+ return reached;
890
+ }
891
+ function initialsOnly2(name) {
892
+ const g = name.givenFields();
893
+ return g.length > 0 && g.every((f) => f.length === 1);
894
+ }
612
895
  function nameOrderOf(s) {
613
896
  return s === "surname-first" || s === "given-first" || s === "unknown" ? s : undefined;
614
897
  }
@@ -811,6 +1094,7 @@ function idChunks(ids) {
811
1094
  class DancerResolver2 {
812
1095
  rel;
813
1096
  printIds = new Map;
1097
+ rowIds = new Map;
814
1098
  lineageOf;
815
1099
  regions;
816
1100
  constructor(rel) {
@@ -822,15 +1106,17 @@ class DancerResolver2 {
822
1106
  return null;
823
1107
  await this.rel.fetch(LINEAGE_FILE);
824
1108
  const out = new Map;
825
- const join = (a, b) => {
826
- const s = out.get(a) ?? new Set;
827
- s.add(b);
828
- out.set(a, s);
1109
+ const join = (a, b, year) => {
1110
+ const m = out.get(a) ?? new Map;
1111
+ const was = m.get(b);
1112
+ m.set(b, was === undefined ? year : was === null || year === null ? null : Math.min(was, year));
1113
+ out.set(a, m);
829
1114
  };
830
1115
  for (const r of readJsonl(this.rel.path(LINEAGE_FILE)))
831
1116
  if (r.id !== r.other) {
832
- join(r.id, r.other);
833
- join(r.other, r.id);
1117
+ const year = typeof r.year === "number" ? r.year : null;
1118
+ join(r.id, r.other, year);
1119
+ join(r.other, r.id, year);
834
1120
  }
835
1121
  return out;
836
1122
  })();
@@ -952,15 +1238,7 @@ class DancerResolver2 {
952
1238
  school = { arm: s.arm, ids: schoolIds };
953
1239
  else {
954
1240
  const asked = new Set(schoolIds);
955
- const reached = new Set(schoolIds);
956
- const todo = [...schoolIds];
957
- for (let id = todo.pop();id !== undefined; id = todo.pop())
958
- for (const o of lineage.get(id) ?? [])
959
- if (!reached.has(o)) {
960
- reached.add(o);
961
- todo.push(o);
962
- }
963
- lineageIds = [...reached].filter((id) => !asked.has(id)).sort();
1241
+ lineageIds = [...schoolLine(lineage, schoolIds, null)].filter((id) => !asked.has(id)).sort();
964
1242
  school = { arm: s.arm, ids: schoolIds, lineage: lineageIds };
965
1243
  }
966
1244
  }
@@ -1034,6 +1312,55 @@ class DancerResolver2 {
1034
1312
  return true;
1035
1313
  return false;
1036
1314
  });
1315
+ 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))) {
1318
+ const lineage = await this.lineage();
1319
+ const line = lineage === null ? new Set(schoolIds) : schoolLine(lineage, schoolIds, year);
1320
+ const band = age?.age?.birthYears() ?? null;
1321
+ const slack = q.age !== undefined && !("yob" in q.age) ? DANCE_UP_YEARS : 0;
1322
+ const open = [];
1323
+ const held = [];
1324
+ const refuted = [];
1325
+ const bySchool = new Set;
1326
+ const schoolFact = async (c) => {
1327
+ const fact = await this.schoolRefutes(schoolsOf(db, c.dancer_id), line, year, q.school);
1328
+ if (fact === null)
1329
+ return false;
1330
+ refuted.push({ dancer_id: c.dancer_id, why: "school", fact });
1331
+ bySchool.add(c.dancer_id);
1332
+ return true;
1333
+ };
1334
+ for (const c of kept)
1335
+ if (!await schoolFact(c))
1336
+ open.push(c);
1337
+ for (const x of excluded) {
1338
+ const c = x.candidate;
1339
+ const own = { from: c.birth.lo, to: c.birth.hi };
1340
+ if (x.why === "age" && band !== null && slack > 0 && birthYearsMeet(band, own, slack)) {
1341
+ if (!await schoolFact(c))
1342
+ held.push(c.dancer_id);
1343
+ continue;
1344
+ }
1345
+ refuted.push({
1346
+ dancer_id: c.dancer_id,
1347
+ why: x.why,
1348
+ fact: contradictionFact(x, age, slack)
1349
+ });
1350
+ }
1351
+ initials = {
1352
+ open: [...open.map((c) => c.dancer_id), ...held],
1353
+ heldByDanceUp: held,
1354
+ refuted,
1355
+ year
1356
+ };
1357
+ if (open.length === 1 && held.length === 0) {
1358
+ for (const c of kept)
1359
+ if (bySchool.has(c.dancer_id))
1360
+ excluded.push({ candidate: c, why: "school" });
1361
+ kept = open;
1362
+ }
1363
+ }
1037
1364
  let preferred;
1038
1365
  if (kept.length > 1 && school !== undefined)
1039
1366
  for (const rank of [SCHOOL_RANK.same, SCHOOL_RANK.moved]) {
@@ -1060,6 +1387,17 @@ class DancerResolver2 {
1060
1387
  };
1061
1388
  }
1062
1389
  }
1390
+ if (preferred === undefined && kept.length > 1 && initials !== undefined) {
1391
+ const openIds = new Set(initials.open);
1392
+ const hit = kept.filter((c) => openIds.has(c.dancer_id) && c.year_hi !== null && c.year_hi >= year - 1);
1393
+ if (hit.length === 1) {
1394
+ const c = hit[0];
1395
+ preferred = {
1396
+ dancer_id: c.dancer_id,
1397
+ why: `the only candidate no hard fact refutes with a row in ${year - 1} or ${year}`
1398
+ };
1399
+ }
1400
+ }
1063
1401
  return {
1064
1402
  tag: this.rel.tag,
1065
1403
  asked: q,
@@ -1071,7 +1409,8 @@ class DancerResolver2 {
1071
1409
  ...age !== undefined ? { age: age.answer } : {},
1072
1410
  ...population !== undefined ? { population } : {},
1073
1411
  ...oireachtas !== undefined ? { oireachtas } : {},
1074
- ...school !== undefined ? { school } : {}
1412
+ ...school !== undefined ? { school } : {},
1413
+ ...initials !== undefined ? { initials } : {}
1075
1414
  };
1076
1415
  }
1077
1416
  async readOireachtas(db, o, asOf) {
@@ -1124,6 +1463,31 @@ class DancerResolver2 {
1124
1463
  }
1125
1464
  return joined ? "lineage" : earlier ? "moved" : "none";
1126
1465
  }
1466
+ async schoolRefutes(schools, line, year, asked) {
1467
+ const rows = schools.filter((s) => s.year_lo === null || s.year_lo <= year);
1468
+ if (rows.length === 0)
1469
+ return null;
1470
+ const seen = new Set;
1471
+ for (const s of rows) {
1472
+ const own = await this.schoolIdsOf(s);
1473
+ if (own.length === 0 || own.some((id) => line.has(id)))
1474
+ return null;
1475
+ for (const id of own)
1476
+ seen.add(id);
1477
+ }
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}`;
1480
+ }
1481
+ async schoolIdsOf(s) {
1482
+ const key = s.school_id ?? s.print;
1483
+ let read = this.rowIds.get(key);
1484
+ if (read === undefined) {
1485
+ const got = (await this.rel.schools.resolve(key)).candidates.map((c) => c.school_id);
1486
+ read = s.school_id === null ? got : [...new Set([s.school_id, ...got])];
1487
+ this.rowIds.set(key, read);
1488
+ }
1489
+ return read;
1490
+ }
1127
1491
  readAge(a, asOf) {
1128
1492
  const done = (age, note) => {
1129
1493
  const read = age?.isAge() ? age : null;
@@ -1232,6 +1596,26 @@ function ageEvidence(d, asked) {
1232
1596
  return "overlaps";
1233
1597
  return "contradicts";
1234
1598
  }
1599
+ function askedYear(q, asOf) {
1600
+ if (asOf !== null)
1601
+ return asOf;
1602
+ const a = q.age;
1603
+ if (a !== undefined && !("yob" in a) && typeof a.year === "number")
1604
+ return a.year;
1605
+ return new Date().getFullYear();
1606
+ }
1607
+ function contradictionFact(x, age, slack) {
1608
+ const c = x.candidate;
1609
+ if (x.why === "age") {
1610
+ const w = age?.answer.birth;
1611
+ return `born ${c.birth.lo ?? "?"}-${c.birth.hi ?? "?"}, outside ${age?.answer.read ?? "the asked age"} (born ${w?.from ?? "any"}-${w?.to ?? "any"})${slack > 0 ? ` even with ${slack} year's dance-up` : ""}`;
1612
+ }
1613
+ if (x.why === "population")
1614
+ return `published in the other population's majors solo competitions only (girls ${c.population.girls}, boys ${c.population.boys})`;
1615
+ if (x.why === "region")
1616
+ return `danced the ${c.region?.region} regional in ${c.region?.year}, another regional that year`;
1617
+ return "set aside by the school";
1618
+ }
1235
1619
 
1236
1620
  import { Age as Age2 } from "@finbheara/domain/age";
1237
1621
  import {
@@ -2248,651 +2632,394 @@ function buildIndex2(rel, b, o) {
2248
2632
  if (b.load !== undefined && o.loaded === undefined)
2249
2633
  throw new Error(`archive-client: index "${b.name}" reads its inputs first: build it with ensureIndex (or rel.index)`);
2250
2634
  mkdirSync(indexDir(rel), { recursive: true });
2251
- const tmp = `${out}.building.${process.pid}`;
2252
- rmSync(tmp, { force: true });
2253
- const t0 = performance.now();
2254
- const db = new Database(tmp, { create: true });
2255
- try {
2256
- db.run("PRAGMA journal_mode = OFF");
2257
- db.run("PRAGMA synchronous = OFF");
2258
- db.run("CREATE TABLE meta(k TEXT PRIMARY KEY, v TEXT NOT NULL)");
2259
- db.transaction(() => b.build(rel, db, o.loaded))();
2260
- const put = db.prepare("INSERT INTO meta(k, v) VALUES (?, ?)");
2261
- put.run("key", key);
2262
- put.run("builder", b.name);
2263
- put.run("version", b.version);
2264
- put.run("tag", rel.tag);
2265
- db.run("VACUUM");
2266
- } catch (e) {
2267
- db.close();
2268
- rmSync(tmp, { force: true });
2269
- throw e;
2270
- }
2271
- db.close();
2272
- renameSync(tmp, out);
2273
- o.log?.(`index ${rel.tag} ${b.name}: built in ${((performance.now() - t0) / 1000).toFixed(1)} s`);
2274
- return out;
2275
- }
2276
- async function ensureIndex2(rel, b, o) {
2277
- if (!o.force && isCurrent2(rel, b))
2278
- return indexPath2(rel, b.name);
2279
- const loaded = b.load === undefined ? undefined : await b.load(rel);
2280
- return buildIndex2(rel, b, { ...o, loaded });
2281
- }
2282
- function listIndexes2(rel) {
2283
- const dir = indexDir(rel);
2284
- if (!existsSync(dir))
2285
- return [];
2286
- return readdirSync(dir).filter((f) => f.endsWith(".sqlite")).sort().map((f) => {
2287
- const name = f.slice(0, -".sqlite".length);
2288
- const path = join(dir, f);
2289
- const b = rel.builders().find((x) => x.name === name);
2290
- return {
2291
- name,
2292
- path,
2293
- bytes: statSync(path).size,
2294
- current: b === undefined ? null : keyOf(path) === indexKey2(rel, b)
2295
- };
2296
- });
2297
- }
2298
-
2299
- import {
2300
- copyFileSync,
2301
- existsSync as existsSync5,
2302
- linkSync,
2303
- mkdirSync as mkdirSync4,
2304
- readFileSync as readFileSync4,
2305
- readdirSync as readdirSync3,
2306
- rmSync as rmSync4,
2307
- statSync as statSync3
2308
- } from "fs";
2309
- import { isAbsolute, join as join4, relative, resolve, sep } from "path";
2310
-
2311
- import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync as rmSync2 } from "fs";
2312
- import { join as join2 } from "path";
2313
- var pinPath = (root) => join2(root, "PINNED.json");
2314
- function readPin2(root) {
2315
- const p = pinPath(root);
2316
- if (!existsSync2(p))
2317
- return null;
2318
- try {
2319
- const v = JSON.parse(readFileSync2(p, "utf8"));
2320
- return typeof v.tag === "string" && parseArchiveTag2(v.tag) !== null ? { tag: v.tag, source: typeof v.source === "string" ? v.source : "unknown" } : null;
2321
- } catch {
2322
- return null;
2323
- }
2324
- }
2325
- function writePin(root, pin) {
2326
- if (parseArchiveTag2(pin.tag) === null)
2327
- throw new Error(`pin: "${pin.tag}" is not a release tag`);
2328
- writeJsonAtomic2(pinPath(root), pin);
2329
- }
2330
- function clearPin(root) {
2331
- const p = pinPath(root);
2332
- if (!existsSync2(p))
2333
- return false;
2334
- rmSync2(p);
2335
- return true;
2336
- }
2337
-
2338
- import { Database as Database2 } from "bun:sqlite";
2339
- import {
2340
- existsSync as existsSync4,
2341
- mkdirSync as mkdirSync3,
2342
- readFileSync as readFileSync3,
2343
- readdirSync as readdirSync2,
2344
- renameSync as renameSync2,
2345
- rmSync as rmSync3,
2346
- statSync as statSync2
2347
- } from "fs";
2348
- import { join as join3 } from "path";
2349
-
2350
- import { Age as Age3 } from "@finbheara/domain/age";
2351
- import { compTitle as compTitle2 } from "@finbheara/domain/title";
2352
- function youngest2(answers) {
2353
- const key = (a) => {
2354
- const iv = Age3.from(a.age, { year: null }).interval();
2355
- return iv === null ? [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY] : [iv.hi, iv.lo];
2356
- };
2357
- let best;
2358
- let bk = [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY];
2359
- for (const a of answers) {
2360
- const k = key(a);
2361
- if (best === undefined || k[0] < bk[0] || k[0] === bk[0] && k[1] > bk[1]) {
2362
- best = a;
2363
- bk = k;
2364
- }
2365
- }
2366
- return best;
2367
- }
2368
- function dancerResolverOf2(rel) {
2369
- return rel.dancers;
2370
- }
2371
-
2372
- class RegistrationResolver2 {
2373
- rel;
2374
- comps;
2375
- dancers;
2376
- constructor(rel, comps, dancers) {
2377
- this.rel = rel;
2378
- this.comps = comps;
2379
- this.dancers = dancers;
2380
- }
2381
- async dancer(q) {
2382
- const dancers = this.dancers === undefined ? dancerResolverOf2(this.rel) : this.dancers;
2383
- let put = q;
2384
- let comp;
2385
- const age = q.age;
2386
- if (age !== undefined && "comp" in age) {
2387
- const slash = age.comp.lastIndexOf("/");
2388
- if (slash > 0) {
2389
- comp = await this.comps.resolve({
2390
- feis: age.comp.slice(0, slash),
2391
- code: age.comp.slice(slash + 1),
2392
- ...age.year !== undefined ? { year: age.year } : {}
2393
- });
2394
- if (comp.arm === "unique" && comp.age !== null)
2395
- put = { ...q, age: { band: comp.age, year: comp.feis.year } };
2396
- }
2397
- }
2398
- return {
2399
- tag: this.rel.tag,
2400
- asked: q,
2401
- put,
2402
- ...comp !== undefined ? { comp } : {},
2403
- dancer: dancers === null ? { arm: "unavailable", why: "this release opens no dancer resolver" } : await dancers.resolve(put)
2404
- };
2405
- }
2406
- async resolve(rows) {
2407
- const seen = new Map;
2408
- const out = [];
2409
- for (const row of rows) {
2410
- const comps = [];
2411
- for (const code of row.codes) {
2412
- const title = row.titles?.[code];
2413
- const key = JSON.stringify([row.feis, code, row.year ?? null, title ?? null]);
2414
- let a = seen.get(key);
2415
- if (a === undefined) {
2416
- a = await this.comps.resolve({
2417
- feis: row.feis,
2418
- code,
2419
- ...row.year !== undefined ? { year: row.year } : {},
2420
- ...title !== undefined ? { title } : {}
2421
- });
2422
- seen.set(key, a);
2423
- }
2424
- comps.push(a);
2425
- }
2426
- const read = comps.filter((c) => c.arm === "unique" && c.age !== null);
2427
- const first = youngest2(read);
2428
- const printed = first !== undefined ? [] : comps.flatMap((c, i) => {
2429
- const code = row.codes[i];
2430
- const t = row.titles?.[code];
2431
- const a = t === undefined ? null : compTitle2(t).age;
2432
- return a?.isAge() === true && a.kind !== "yob" ? [{ age: a.ag1, year: c.feis.year, code }] : [];
2433
- });
2434
- const byTitle = youngest2(printed);
2435
- const age = first !== undefined ? {
2436
- band: first.age,
2437
- year: first.feis.year,
2438
- code: first.asked.code,
2439
- from: "comp"
2440
- } : byTitle !== undefined ? { band: byTitle.age, year: byTitle.year, code: byTitle.code, from: "title" } : null;
2441
- const bands = [...new Set((first !== undefined ? read : printed).map((c) => c.age))];
2442
- let population = null;
2443
- if (row.population !== undefined)
2444
- population = { asked: row.population, from: "row" };
2445
- else {
2446
- const gendered = comps.flatMap((c, i) => c.arm === "unique" && (c.population === "girls" || c.population === "boys") ? [{ pop: c.population, code: row.codes[i] }] : []);
2447
- const pops = [...new Set(gendered.map((g) => g.pop))];
2448
- if (pops.length === 1)
2449
- population = {
2450
- asked: pops[0],
2451
- from: "comp",
2452
- codes: gendered.map((g) => g.code)
2453
- };
2454
- else if (pops.length > 1)
2455
- population = {
2456
- why: `the row's comps read both populations (${gendered.map((g) => `${g.code}: ${g.pop}`).join(", ")}): none asked`
2457
- };
2458
- }
2459
- const order = nameOrderOf(row.order);
2460
- const asked = {
2461
- name: row.name,
2462
- ...order !== undefined ? { order } : {},
2463
- ...row.school !== undefined ? { school: row.school } : {},
2464
- ...age !== null ? { age: { band: age.band, year: age.year } } : {},
2465
- ...row.asOf !== undefined ? { asOf: row.asOf } : {},
2466
- ...population !== null && "asked" in population ? { population: population.asked } : {},
2467
- ...row.oireachtas !== undefined ? { oireachtas: row.oireachtas } : {}
2468
- };
2469
- const { dancer } = await this.dancer(asked);
2470
- out.push({
2471
- tag: this.rel.tag,
2472
- row,
2473
- comps,
2474
- age,
2475
- ...bands.length > 1 ? { bands } : {},
2476
- population,
2477
- dancer,
2478
- asked
2479
- });
2480
- }
2481
- return out;
2635
+ const tmp = `${out}.building.${process.pid}`;
2636
+ rmSync(tmp, { force: true });
2637
+ const t0 = performance.now();
2638
+ const db = new Database(tmp, { create: true });
2639
+ try {
2640
+ db.run("PRAGMA journal_mode = OFF");
2641
+ db.run("PRAGMA synchronous = OFF");
2642
+ db.run("CREATE TABLE meta(k TEXT PRIMARY KEY, v TEXT NOT NULL)");
2643
+ db.transaction(() => b.build(rel, db, o.loaded))();
2644
+ const put = db.prepare("INSERT INTO meta(k, v) VALUES (?, ?)");
2645
+ put.run("key", key);
2646
+ put.run("builder", b.name);
2647
+ put.run("version", b.version);
2648
+ put.run("tag", rel.tag);
2649
+ db.run("VACUUM");
2650
+ } catch (e) {
2651
+ db.close();
2652
+ rmSync(tmp, { force: true });
2653
+ throw e;
2482
2654
  }
2655
+ db.close();
2656
+ renameSync(tmp, out);
2657
+ o.log?.(`index ${rel.tag} ${b.name}: built in ${((performance.now() - t0) / 1000).toFixed(1)} s`);
2658
+ return out;
2483
2659
  }
2484
-
2485
- import { spawn } from "child_process";
2486
- import { createWriteStream as createWriteStream2, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
2487
- import { dirname } from "path";
2488
- import { Readable } from "stream";
2489
- import { pipeline as pipeline2 } from "stream/promises";
2490
- function run(cmd, args) {
2491
- return new Promise((resolve) => {
2492
- let stdout = "";
2493
- let stderr = "";
2494
- let child;
2495
- try {
2496
- child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
2497
- } catch (e) {
2498
- resolve({ status: null, stdout, stderr: e.message });
2499
- return;
2500
- }
2501
- child.stdout?.on("data", (d) => {
2502
- stdout += String(d);
2503
- });
2504
- child.stderr?.on("data", (d) => {
2505
- stderr += String(d);
2506
- });
2507
- child.on("error", (e) => resolve({ status: null, stdout, stderr: e.message }));
2508
- child.on("close", (status) => resolve({ status, stdout, stderr }));
2509
- });
2660
+ async function ensureIndex2(rel, b, o) {
2661
+ if (!o.force && isCurrent2(rel, b))
2662
+ return indexPath2(rel, b.name);
2663
+ const loaded = b.load === undefined ? undefined : await b.load(rel);
2664
+ return buildIndex2(rel, b, { ...o, loaded });
2510
2665
  }
2511
- var firstLine = (s) => s.trim().split(`
2512
- `)[0] ?? "";
2513
- var ghRemote2 = {
2514
- name: "gh",
2515
- async tags(repo) {
2516
- const r = await run("gh", [
2517
- "release",
2518
- "list",
2519
- "-R",
2520
- repo,
2521
- "--limit",
2522
- "500",
2523
- "--exclude-drafts",
2524
- "--json",
2525
- "tagName",
2526
- "--jq",
2527
- ".[].tagName"
2528
- ]);
2529
- if (r.status !== 0)
2530
- throw new Error(`gh release list -R ${repo}: ${firstLine(r.stderr)}`);
2531
- return r.stdout.split(`
2532
- `).map((t) => t.trim()).filter((t) => t !== "");
2533
- },
2534
- async assets(repo, tag) {
2535
- const r = await run("gh", ["release", "view", tag, "-R", repo, "--json", "assets"]);
2536
- if (r.status !== 0)
2537
- throw new Error(`gh release view ${tag} -R ${repo}: ${firstLine(r.stderr)}`);
2538
- const v = JSON.parse(r.stdout);
2539
- return (v.assets ?? []).map((a) => ({ name: a.name, size: a.size, digest: a.digest ?? null }));
2540
- },
2541
- async download(repo, tag, name, dest) {
2542
- const r = await run("gh", [
2543
- "release",
2544
- "download",
2545
- tag,
2546
- "-R",
2547
- repo,
2548
- "-p",
2666
+ function listIndexes2(rel) {
2667
+ const dir = indexDir(rel);
2668
+ if (!existsSync(dir))
2669
+ return [];
2670
+ return readdirSync(dir).filter((f) => f.endsWith(".sqlite")).sort().map((f) => {
2671
+ const name = f.slice(0, -".sqlite".length);
2672
+ const path = join(dir, f);
2673
+ const b = rel.builders().find((x) => x.name === name);
2674
+ return {
2549
2675
  name,
2550
- "-O",
2551
- dest,
2552
- "--clobber"
2553
- ]);
2554
- if (r.status !== 0 || !existsSync3(dest))
2555
- throw new Error(`gh release download ${tag} -p ${name}: ${firstLine(r.stderr)}`);
2676
+ path,
2677
+ bytes: statSync(path).size,
2678
+ current: b === undefined ? null : keyOf(path) === indexKey2(rel, b)
2679
+ };
2680
+ });
2681
+ }
2682
+
2683
+ import {
2684
+ copyFileSync,
2685
+ existsSync as existsSync5,
2686
+ linkSync,
2687
+ mkdirSync as mkdirSync4,
2688
+ readFileSync as readFileSync4,
2689
+ readdirSync as readdirSync3,
2690
+ rmSync as rmSync4,
2691
+ statSync as statSync3
2692
+ } from "fs";
2693
+ import { isAbsolute, join as join4, relative, resolve, sep } from "path";
2694
+
2695
+ import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync as rmSync2 } from "fs";
2696
+ import { join as join2 } from "path";
2697
+ var pinPath = (root) => join2(root, "PINNED.json");
2698
+ function readPin2(root) {
2699
+ const p = pinPath(root);
2700
+ if (!existsSync2(p))
2701
+ return null;
2702
+ try {
2703
+ const v = JSON.parse(readFileSync2(p, "utf8"));
2704
+ return typeof v.tag === "string" && parseArchiveTag2(v.tag) !== null ? { tag: v.tag, source: typeof v.source === "string" ? v.source : "unknown" } : null;
2705
+ } catch {
2706
+ return null;
2556
2707
  }
2557
- };
2558
- var API = "https://api.github.com";
2559
- function nodeWebStream(body) {
2560
- return body;
2561
2708
  }
2562
- function headers(env, accept) {
2563
- const token = env.GITHUB_TOKEN ?? env.GH_TOKEN;
2564
- return {
2565
- accept,
2566
- "user-agent": "archive-client",
2567
- "x-github-api-version": "2022-11-28",
2568
- ...token !== undefined && token !== "" ? { authorization: `Bearer ${token}` } : {}
2569
- };
2709
+ function writePin(root, pin) {
2710
+ if (parseArchiveTag2(pin.tag) === null)
2711
+ throw new Error(`pin: "${pin.tag}" is not a release tag`);
2712
+ writeJsonAtomic2(pinPath(root), pin);
2570
2713
  }
2571
- function httpsRemote2(env = process.env, fetchImpl = fetch) {
2572
- const json = async (url) => {
2573
- const res = await fetchImpl(url, { headers: headers(env, "application/vnd.github+json") });
2574
- if (!res.ok)
2575
- throw new Error(`GET ${url}: ${res.status} ${res.statusText}`);
2576
- return await res.json();
2577
- };
2578
- const release = (repo, tag) => json(`${API}/repos/${repo}/releases/tags/${encodeURIComponent(tag)}`);
2579
- return {
2580
- name: "https",
2581
- async tags(repo) {
2582
- const out = [];
2583
- for (let page = 1;page <= 10; page++) {
2584
- const rows = await json(`${API}/repos/${repo}/releases?per_page=100&page=${page}`);
2585
- for (const r of rows)
2586
- if (!r.draft)
2587
- out.push(r.tag_name);
2588
- if (rows.length < 100)
2589
- break;
2590
- }
2591
- return out;
2592
- },
2593
- async assets(repo, tag) {
2594
- return (await release(repo, tag)).assets.map((a) => ({
2595
- name: a.name,
2596
- size: a.size,
2597
- digest: a.digest ?? null
2598
- }));
2599
- },
2600
- async download(repo, tag, name, dest) {
2601
- const a = (await release(repo, tag)).assets.find((x) => x.name === name);
2602
- if (a === undefined)
2603
- throw new Error(`${tag} has no asset ${name}`);
2604
- const res = await fetchImpl(a.url, { headers: headers(env, "application/octet-stream") });
2605
- if (!res.ok || res.body === null)
2606
- throw new Error(`GET ${a.url}: ${res.status} ${res.statusText}`);
2607
- mkdirSync2(dirname(dest), { recursive: true });
2608
- await pipeline2(Readable.fromWeb(nodeWebStream(res.body)), createWriteStream2(dest));
2609
- }
2610
- };
2714
+ function clearPin(root) {
2715
+ const p = pinPath(root);
2716
+ if (!existsSync2(p))
2717
+ return false;
2718
+ rmSync2(p);
2719
+ return true;
2611
2720
  }
2612
- function fallbackRemote2(first, second) {
2613
- const both = (f) => async (...a) => {
2614
- try {
2615
- return await f(first)(...a);
2616
- } catch (e1) {
2617
- try {
2618
- return await f(second)(...a);
2619
- } catch (e2) {
2620
- throw new Error(`${first.name}: ${e1.message}; ${second.name}: ${e2.message}`);
2621
- }
2622
- }
2623
- };
2624
- return {
2625
- name: `${first.name}+${second.name}`,
2626
- tags: both((r) => r.tags.bind(r)),
2627
- assets: both((r) => r.assets.bind(r)),
2628
- download: both((r) => r.download.bind(r))
2721
+
2722
+ import { Database as Database2 } from "bun:sqlite";
2723
+ import {
2724
+ existsSync as existsSync4,
2725
+ mkdirSync as mkdirSync3,
2726
+ readFileSync as readFileSync3,
2727
+ readdirSync as readdirSync2,
2728
+ renameSync as renameSync2,
2729
+ rmSync as rmSync3,
2730
+ statSync as statSync2
2731
+ } from "fs";
2732
+ import { join as join3 } from "path";
2733
+
2734
+ import { Age as Age3 } from "@finbheara/domain/age";
2735
+ import { compTitle as compTitle2 } from "@finbheara/domain/title";
2736
+ function youngest2(answers) {
2737
+ const key = (a) => {
2738
+ const iv = Age3.from(a.age, { year: null }).interval();
2739
+ return iv === null ? [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY] : [iv.hi, iv.lo];
2629
2740
  };
2741
+ let best;
2742
+ let bk = [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY];
2743
+ for (const a of answers) {
2744
+ const k = key(a);
2745
+ if (best === undefined || k[0] < bk[0] || k[0] === bk[0] && k[1] > bk[1]) {
2746
+ best = a;
2747
+ bk = k;
2748
+ }
2749
+ }
2750
+ return best;
2630
2751
  }
2631
- var installed = null;
2632
- function setRemote2(r) {
2633
- const was = installed;
2634
- installed = r;
2635
- return was;
2636
- }
2637
- function defaultRemote() {
2638
- return installed ?? fallbackRemote2(ghRemote2, httpsRemote2());
2752
+ function dancerResolverOf2(rel) {
2753
+ return rel.dancers;
2639
2754
  }
2640
2755
 
2641
- function columnsOf2(db, table) {
2642
- return new Set(db.query("SELECT name FROM pragma_table_info(?)").all(table).map((r) => r.name));
2643
- }
2644
- var FEATURES2 = [
2645
- {
2646
- feature: "results.level",
2647
- needs: ["results:competitions.feis_level"],
2648
- since: 215,
2649
- calls: "results.query, majors.results"
2650
- },
2651
- {
2652
- feature: "majors.order",
2653
- needs: ["majors:competitions.seq"],
2654
- since: 260,
2655
- calls: "majors.competitions"
2656
- },
2657
- {
2658
- feature: "dancers",
2659
- needs: ["results:results.name_order_resolved"],
2660
- since: 260,
2661
- calls: "dancers.resolve, dancers.search, dancers.get (the dancers index)"
2662
- },
2663
- {
2664
- feature: "schools",
2665
- needs: ["school-registry.jsonl", "school-redirects.jsonl"],
2666
- since: 260,
2667
- calls: "schools.resolve, a dancer question with a school (the schools index)"
2668
- },
2669
- {
2670
- feature: "feises",
2671
- needs: [
2672
- "results:feises.date_lo",
2673
- "results:feises.date_hi",
2674
- "results:feises.feis_instance_id",
2675
- "results:feises.feis_brand_id",
2676
- "results:competitions.feis_region",
2677
- "feis-redirects.jsonl"
2678
- ],
2679
- since: 265,
2680
- calls: "feises.find, feises.series, and comps and registrations through them (the feises index)"
2681
- },
2682
- {
2683
- feature: "comps",
2684
- needs: [
2685
- "results:competitions.feis_level",
2686
- "results:competitions.round",
2687
- "results:feises.feis_brand_id"
2688
- ],
2689
- since: 265,
2690
- calls: "comps.resolve, comps.syllabus, comps.syllabusFor, registrations.resolve (the comps index)"
2691
- },
2692
- {
2693
- feature: "comps.dance",
2694
- needs: ["results:competitions.dance"],
2695
- since: 270,
2696
- calls: "the `dance` a comp of comps.resolve, comps.syllabus and comps.syllabusFor carries: null, never refused, on a release without it"
2697
- },
2698
- {
2699
- feature: "schools.lineage",
2700
- needs: ["school-lineage.jsonl"],
2701
- since: 184,
2702
- calls: "the `lineage` school evidence of dancers.resolve and dancers.search: no school is joined by lineage, never refused, on a release without it"
2703
- },
2704
- {
2705
- feature: "results.stream",
2706
- needs: [
2707
- "results:competitions.round",
2708
- "results:competitions.feis_level",
2709
- "results:competitions.level_printed",
2710
- "results:competitions.age_printed",
2711
- "results:results.yob_printed",
2712
- "results:results.primary_source",
2713
- "results:results.platform_dancer_id",
2714
- "results:results.relative_place",
2715
- "results:feises.date_lo",
2716
- "results:feises.date_hi"
2717
- ],
2718
- since: 255,
2719
- calls: "results.stream, results.entries (scopes transcribed, platform, all)"
2720
- },
2721
- {
2722
- feature: "majors.stream",
2723
- needs: [
2724
- "majors:competitions.round",
2725
- "majors:competitions.feis_level",
2726
- "majors:results.yob_printed",
2727
- "majors:results.primary_source",
2728
- "majors:results.relative_place",
2729
- "majors:feises.date_lo"
2730
- ],
2731
- since: 255,
2732
- calls: "results.stream, results.entries (scope majors)"
2733
- },
2734
- {
2735
- feature: "majors.tabs",
2736
- needs: ["majors:tabs.address", "majors:tabs.true_round"],
2737
- since: 215,
2738
- calls: "majors.tabulated"
2739
- },
2740
- {
2741
- feature: "feises.dates",
2742
- needs: ["results:feises.feis_id", "results:feises.date_lo", "results:feises.date_hi"],
2743
- since: 245,
2744
- calls: "feises.dates"
2745
- },
2746
- {
2747
- feature: "dancers.records",
2748
- needs: [
2749
- "results:dancers.display",
2750
- "results:dancers.appearances",
2751
- "results:dancers.birth_lo",
2752
- "results:dancers.birth_hi",
2753
- "results:dancers.birth_source",
2754
- "results:dancers.birth_conflict",
2755
- "results:dancers.genders"
2756
- ],
2757
- since: 215,
2758
- calls: "dancers.records"
2759
- },
2760
- {
2761
- feature: "dancers.years",
2762
- needs: [
2763
- "results:dancer_years.region",
2764
- "results:dancer_years.region_source",
2765
- "results:dancer_years.nationals",
2766
- "results:dancer_years.nationals_source",
2767
- "results:dancer_years.geohash"
2768
- ],
2769
- since: 245,
2770
- calls: "dancers.years"
2771
- },
2772
- {
2773
- feature: "teams",
2774
- needs: [
2775
- ...[
2776
- "slug",
2777
- "address",
2778
- "event",
2779
- "year",
2780
- "card",
2781
- "comp_no",
2782
- "title",
2783
- "category",
2784
- "hands",
2785
- "letter",
2786
- "label",
2787
- "name_printed",
2788
- "school_printed",
2789
- "school_loc",
2790
- "school_id",
2791
- "place",
2792
- "place_printed",
2793
- "tie",
2794
- "competitors",
2795
- "atts"
2796
- ].map((c) => `table:team_results.${c}`),
2797
- ...[
2798
- "team",
2799
- "kind",
2800
- "event_year",
2801
- "year",
2802
- "comp_no",
2803
- "title",
2804
- "team_index",
2805
- "seq",
2806
- "school_id",
2807
- "school_print_materialized"
2808
- ].map((c) => `table:roster_teams.${c}`),
2809
- ...["team", "seq", "name_printed", "name", "yob", "dancer_id"].map((c) => `table:roster_members.${c}`),
2810
- ...[
2811
- "event_year",
2812
- "comp_no",
2813
- "team",
2814
- "result_address",
2815
- "school_id",
2816
- "join_kind",
2817
- "via_split",
2818
- "roster_teams",
2819
- "result_teams",
2820
- "roster_index",
2821
- "result_letter"
2822
- ].map((c) => `table:team_result_rosters.${c}`),
2823
- "table:event_years.event_year",
2824
- "table:event_years.event",
2825
- "table:event_years.year"
2826
- ],
2827
- since: 260,
2828
- calls: "teams.results, teams.rosters, teams.resultRosters (the teams index)"
2829
- },
2830
- {
2831
- feature: "teams.fields",
2832
- needs: [
2833
- "table:team_results.age",
2834
- "table:team_results.gender",
2835
- "table:team_results.printed_status",
2836
- "table:roster_teams.dance",
2837
- "table:roster_teams.school_print_as_of",
2838
- "table:roster_members.gender"
2839
- ],
2840
- since: 266,
2841
- 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"
2756
+ class RegistrationResolver2 {
2757
+ rel;
2758
+ comps;
2759
+ dancers;
2760
+ constructor(rel, comps, dancers) {
2761
+ this.rel = rel;
2762
+ this.comps = comps;
2763
+ this.dancers = dancers;
2764
+ }
2765
+ async dancer(q) {
2766
+ const dancers = this.dancers === undefined ? dancerResolverOf2(this.rel) : this.dancers;
2767
+ let put = q;
2768
+ let comp;
2769
+ const age = q.age;
2770
+ if (age !== undefined && "comp" in age) {
2771
+ const slash = age.comp.lastIndexOf("/");
2772
+ if (slash > 0) {
2773
+ comp = await this.comps.resolve({
2774
+ feis: age.comp.slice(0, slash),
2775
+ code: age.comp.slice(slash + 1),
2776
+ ...age.year !== undefined ? { year: age.year } : {}
2777
+ });
2778
+ if (comp.arm === "unique" && comp.age !== null)
2779
+ put = { ...q, age: { band: comp.age, year: comp.feis.year } };
2780
+ }
2781
+ }
2782
+ return {
2783
+ tag: this.rel.tag,
2784
+ asked: q,
2785
+ put,
2786
+ ...comp !== undefined ? { comp } : {},
2787
+ dancer: dancers === null ? { arm: "unavailable", why: "this release opens no dancer resolver" } : await dancers.resolve(put)
2788
+ };
2789
+ }
2790
+ async resolve(rows) {
2791
+ const seen = new Map;
2792
+ const out = [];
2793
+ for (const row of rows) {
2794
+ const comps = [];
2795
+ for (const code of row.codes) {
2796
+ const title = row.titles?.[code];
2797
+ const key = JSON.stringify([row.feis, code, row.year ?? null, title ?? null]);
2798
+ let a = seen.get(key);
2799
+ if (a === undefined) {
2800
+ a = await this.comps.resolve({
2801
+ feis: row.feis,
2802
+ code,
2803
+ ...row.year !== undefined ? { year: row.year } : {},
2804
+ ...title !== undefined ? { title } : {}
2805
+ });
2806
+ seen.set(key, a);
2807
+ }
2808
+ comps.push(a);
2809
+ }
2810
+ const read = comps.filter((c) => c.arm === "unique" && c.age !== null);
2811
+ const first = youngest2(read);
2812
+ const printed = first !== undefined ? [] : comps.flatMap((c, i) => {
2813
+ const code = row.codes[i];
2814
+ const t = row.titles?.[code];
2815
+ const a = t === undefined ? null : compTitle2(t).age;
2816
+ return a?.isAge() === true && a.kind !== "yob" ? [{ age: a.ag1, year: c.feis.year, code }] : [];
2817
+ });
2818
+ const byTitle = youngest2(printed);
2819
+ const age = first !== undefined ? {
2820
+ band: first.age,
2821
+ year: first.feis.year,
2822
+ code: first.asked.code,
2823
+ from: "comp"
2824
+ } : byTitle !== undefined ? { band: byTitle.age, year: byTitle.year, code: byTitle.code, from: "title" } : null;
2825
+ const bands = [...new Set((first !== undefined ? read : printed).map((c) => c.age))];
2826
+ let population = null;
2827
+ if (row.population !== undefined)
2828
+ population = { asked: row.population, from: "row" };
2829
+ else {
2830
+ const gendered = comps.flatMap((c, i) => c.arm === "unique" && (c.population === "girls" || c.population === "boys") ? [{ pop: c.population, code: row.codes[i] }] : []);
2831
+ const pops = [...new Set(gendered.map((g) => g.pop))];
2832
+ if (pops.length === 1)
2833
+ population = {
2834
+ asked: pops[0],
2835
+ from: "comp",
2836
+ codes: gendered.map((g) => g.code)
2837
+ };
2838
+ else if (pops.length > 1)
2839
+ population = {
2840
+ why: `the row's comps read both populations (${gendered.map((g) => `${g.code}: ${g.pop}`).join(", ")}): none asked`
2841
+ };
2842
+ }
2843
+ const order = nameOrderOf(row.order);
2844
+ const asked = {
2845
+ name: row.name,
2846
+ ...order !== undefined ? { order } : {},
2847
+ ...row.school !== undefined ? { school: row.school } : {},
2848
+ ...age !== null ? { age: { band: age.band, year: age.year } } : {},
2849
+ ...row.asOf !== undefined ? { asOf: row.asOf } : {},
2850
+ ...population !== null && "asked" in population ? { population: population.asked } : {},
2851
+ ...row.oireachtas !== undefined ? { oireachtas: row.oireachtas } : {}
2852
+ };
2853
+ const { dancer } = await this.dancer(asked);
2854
+ out.push({
2855
+ tag: this.rel.tag,
2856
+ row,
2857
+ comps,
2858
+ age,
2859
+ ...bands.length > 1 ? { bands } : {},
2860
+ population,
2861
+ dancer,
2862
+ asked
2863
+ });
2864
+ }
2865
+ return out;
2866
+ }
2867
+ }
2868
+
2869
+ import { spawn } from "child_process";
2870
+ import { createWriteStream as createWriteStream2, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
2871
+ import { dirname } from "path";
2872
+ import { Readable } from "stream";
2873
+ import { pipeline as pipeline2 } from "stream/promises";
2874
+ function run(cmd, args) {
2875
+ return new Promise((resolve) => {
2876
+ let stdout = "";
2877
+ let stderr = "";
2878
+ let child;
2879
+ try {
2880
+ child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
2881
+ } catch (e) {
2882
+ resolve({ status: null, stdout, stderr: e.message });
2883
+ return;
2884
+ }
2885
+ child.stdout?.on("data", (d) => {
2886
+ stdout += String(d);
2887
+ });
2888
+ child.stderr?.on("data", (d) => {
2889
+ stderr += String(d);
2890
+ });
2891
+ child.on("error", (e) => resolve({ status: null, stdout, stderr: e.message }));
2892
+ child.on("close", (status) => resolve({ status, stdout, stderr }));
2893
+ });
2894
+ }
2895
+ var firstLine = (s) => s.trim().split(`
2896
+ `)[0] ?? "";
2897
+ var ghRemote2 = {
2898
+ name: "gh",
2899
+ async tags(repo) {
2900
+ const r = await run("gh", [
2901
+ "release",
2902
+ "list",
2903
+ "-R",
2904
+ repo,
2905
+ "--limit",
2906
+ "500",
2907
+ "--exclude-drafts",
2908
+ "--json",
2909
+ "tagName",
2910
+ "--jq",
2911
+ ".[].tagName"
2912
+ ]);
2913
+ if (r.status !== 0)
2914
+ throw new Error(`gh release list -R ${repo}: ${firstLine(r.stderr)}`);
2915
+ return r.stdout.split(`
2916
+ `).map((t) => t.trim()).filter((t) => t !== "");
2842
2917
  },
2843
- {
2844
- feature: "events",
2845
- needs: [
2846
- "event_year",
2847
- "event",
2848
- "year",
2849
- "date_lo",
2850
- "date_hi",
2851
- "resolution",
2852
- "source",
2853
- "date_printed",
2854
- "place",
2855
- "place_printed"
2856
- ].map((c) => `table:event_years.${c}`),
2857
- since: 260,
2858
- calls: "events.years (the events index)"
2918
+ async assets(repo, tag) {
2919
+ const r = await run("gh", ["release", "view", tag, "-R", repo, "--json", "assets"]);
2920
+ if (r.status !== 0)
2921
+ throw new Error(`gh release view ${tag} -R ${repo}: ${firstLine(r.stderr)}`);
2922
+ const v = JSON.parse(r.stdout);
2923
+ return (v.assets ?? []).map((a) => ({ name: a.name, size: a.size, digest: a.digest ?? null }));
2859
2924
  },
2860
- {
2861
- feature: "staff",
2862
- needs: [
2863
- "results:adjudicators",
2864
- "results:feis_panels",
2865
- "school-staff.jsonl",
2866
- "school-teachers.jsonl",
2867
- "school-registry.jsonl"
2868
- ],
2869
- since: 260,
2870
- calls: "staff.teacher, staff.adjudicator (the staff index)"
2871
- }
2872
- ];
2873
- async function hasNeed(probe, n) {
2874
- const t = /^table:([A-Za-z_]+)(?:\.([A-Za-z_]+))?$/.exec(n);
2875
- if (t !== null) {
2876
- if (probe.tableColumns === undefined)
2877
- return false;
2878
- const cols = await probe.tableColumns(t[1]);
2879
- return t[2] === undefined ? cols.size > 0 : cols.has(t[2]);
2925
+ async download(repo, tag, name, dest) {
2926
+ const r = await run("gh", [
2927
+ "release",
2928
+ "download",
2929
+ tag,
2930
+ "-R",
2931
+ repo,
2932
+ "-p",
2933
+ name,
2934
+ "-O",
2935
+ dest,
2936
+ "--clobber"
2937
+ ]);
2938
+ if (r.status !== 0 || !existsSync3(dest))
2939
+ throw new Error(`gh release download ${tag} -p ${name}: ${firstLine(r.stderr)}`);
2880
2940
  }
2881
- const m = /^(results|majors):([A-Za-z_]+)(?:\.([A-Za-z_]+))?$/.exec(n);
2882
- if (m === null)
2883
- return probe.publishes(n);
2884
- const cols = columnsOf2(await probe.db(m[1]), m[2]);
2885
- return m[3] === undefined ? cols.size > 0 : cols.has(m[3]);
2941
+ };
2942
+ var API = "https://api.github.com";
2943
+ function nodeWebStream(body) {
2944
+ return body;
2886
2945
  }
2887
- async function capabilityOf2(probe, f) {
2888
- const missing = [];
2889
- for (const n of f.needs)
2890
- if (!await hasNeed(probe, n))
2891
- missing.push(n);
2892
- return { ...f, state: missing.length === 0 ? "full" : "missing", missing };
2946
+ function headers(env, accept) {
2947
+ const token = env.GITHUB_TOKEN ?? env.GH_TOKEN;
2948
+ return {
2949
+ accept,
2950
+ "user-agent": "archive-client",
2951
+ "x-github-api-version": "2022-11-28",
2952
+ ...token !== undefined && token !== "" ? { authorization: `Bearer ${token}` } : {}
2953
+ };
2893
2954
  }
2894
- function needsRelease2(tag, c) {
2895
- 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`);
2955
+ function httpsRemote2(env = process.env, fetchImpl = fetch) {
2956
+ const json = async (url) => {
2957
+ const res = await fetchImpl(url, { headers: headers(env, "application/vnd.github+json") });
2958
+ if (!res.ok)
2959
+ throw new Error(`GET ${url}: ${res.status} ${res.statusText}`);
2960
+ return await res.json();
2961
+ };
2962
+ const release = (repo, tag) => json(`${API}/repos/${repo}/releases/tags/${encodeURIComponent(tag)}`);
2963
+ return {
2964
+ name: "https",
2965
+ async tags(repo) {
2966
+ const out = [];
2967
+ for (let page = 1;page <= 10; page++) {
2968
+ const rows = await json(`${API}/repos/${repo}/releases?per_page=100&page=${page}`);
2969
+ for (const r of rows)
2970
+ if (!r.draft)
2971
+ out.push(r.tag_name);
2972
+ if (rows.length < 100)
2973
+ break;
2974
+ }
2975
+ return out;
2976
+ },
2977
+ async assets(repo, tag) {
2978
+ return (await release(repo, tag)).assets.map((a) => ({
2979
+ name: a.name,
2980
+ size: a.size,
2981
+ digest: a.digest ?? null
2982
+ }));
2983
+ },
2984
+ async download(repo, tag, name, dest) {
2985
+ const a = (await release(repo, tag)).assets.find((x) => x.name === name);
2986
+ if (a === undefined)
2987
+ throw new Error(`${tag} has no asset ${name}`);
2988
+ const res = await fetchImpl(a.url, { headers: headers(env, "application/octet-stream") });
2989
+ if (!res.ok || res.body === null)
2990
+ throw new Error(`GET ${a.url}: ${res.status} ${res.statusText}`);
2991
+ mkdirSync2(dirname(dest), { recursive: true });
2992
+ await pipeline2(Readable.fromWeb(nodeWebStream(res.body)), createWriteStream2(dest));
2993
+ }
2994
+ };
2995
+ }
2996
+ function fallbackRemote2(first, second) {
2997
+ const both = (f) => async (...a) => {
2998
+ try {
2999
+ return await f(first)(...a);
3000
+ } catch (e1) {
3001
+ try {
3002
+ return await f(second)(...a);
3003
+ } catch (e2) {
3004
+ throw new Error(`${first.name}: ${e1.message}; ${second.name}: ${e2.message}`);
3005
+ }
3006
+ }
3007
+ };
3008
+ return {
3009
+ name: `${first.name}+${second.name}`,
3010
+ tags: both((r) => r.tags.bind(r)),
3011
+ assets: both((r) => r.assets.bind(r)),
3012
+ download: both((r) => r.download.bind(r))
3013
+ };
3014
+ }
3015
+ var installed = null;
3016
+ function setRemote2(r) {
3017
+ const was = installed;
3018
+ installed = r;
3019
+ return was;
3020
+ }
3021
+ function defaultRemote() {
3022
+ return installed ?? fallbackRemote2(ghRemote2, httpsRemote2());
2896
3023
  }
2897
3024
 
2898
3025
  function tagDir(root, tag) {
@@ -3413,4 +3540,4 @@ function releases2(opts = {}) {
3413
3540
  return new ReleaseManager2(opts);
3414
3541
  }
3415
3542
 
3416
- export { DATABASES2, planAsset2, sha256File2, unzstdTo2, NAME_LEVELS2, nameLevel2, NO_CALENDAR2, FeisFinder2, entryRank2, FEIS_LOCAL_ID_FLOOR2, ResultFinder2, MajorsFinder2, nameOrderOf, DancerResolver2, readPopulation2, canonicalOf2, CompResolver2, StaffFinder2, SchoolResolver2, TeamFinder2, EventFinder2, defaultBuilders2, indexPath2, indexKey2, isCurrent2, buildIndex2, ensureIndex2, listIndexes2, readPin2, youngest2, dancerResolverOf2, RegistrationResolver2, ghRemote2, httpsRemote2, fallbackRemote2, setRemote2, columnsOf2, FEATURES2, capabilityOf2, needsRelease2, resolveTag2, Release2, openRelease2, openCached2, ReleaseManager2, releases2 };
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 };