@finbheara/archive-client 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -95,6 +95,13 @@ c.features; // [{ feature: "dancers", state: "full" | "missing", missing: [...
95
95
  | `teams` | `teams.results`, `teams.rosters`, `teams.resultRosters` | 260 |
96
96
  | `teams.fields` | the fields `teams.results` (`age`, `gender`, `printed_status`) and `teams.rosters` (`dance`, `school_printed_as_of`, a member's `gender`) carry; absent, never refused, without it | 266 |
97
97
  | `events` | `events.years` | 260 |
98
+ | `tabs`, `panels` | `tabs.query`, `tabs.stream`; `panels.query` | 184 |
99
+ | `feis-panels` | `feisPanels.query` | 260 |
100
+ | `staff.adjudicators` | `staff.adjudicators` | 230 |
101
+ | `schools.registry` | `schools.list` | 260 |
102
+ | `schools.prints`, `schools.redirects` | `schools.prints`; `schools.redirects` | 184 |
103
+ | `schools.lineage` | `schools.links`, `schools.line`, `schools.lineage` | 184 |
104
+ | `schools.associations` | `schools.associations` | 215 |
98
105
 
99
106
  A spec is `pinned` (the default: the user-local pin, else the latest), `latest`, a cut number
100
107
  (`42`, or `42.1` for a fraction) or a tag (`archive-YYYY.MM.DD-N`).
@@ -227,6 +234,29 @@ rows were published under and their rows, so it agrees with each print lookup of
227
234
 
228
235
  Every answer carries the release tag it came from.
229
236
 
237
+ ### The schools in bulk
238
+
239
+ ```ts
240
+ await rel.schools.list(); // { schools: [{ school_id, display, regions, places }] }: the registry
241
+ await rel.schools.prints({ event: "<event>", year: 2024 });
242
+ // { prints: [{ event, year, print, school_id, rows }] }
243
+ await rel.schools.redirects(); // { redirects: [{ old_id, new_id, kind, since }] }
244
+ await rel.schools.lineage(); // { edges: [{ id, direction, other, shape, year, provisional,
245
+ // decision, confidence }] }
246
+ await rel.schools.associations({ school_id: "<id>" });
247
+ // { associations: [{ at, seq, date, person, person_printed,
248
+ // person_type, credentials, caption, school_printed, school_id,
249
+ // association_type, src_type, src, candidate_school_id, candidate_rung, ... }] }
250
+ ```
251
+
252
+ `prints` is every (print, school id) pair the release's result rows attach in an event-year (the
253
+ print trimmed), with the rows that carry it: an event's (case-blind), a year's, both or every one.
254
+ A row attached to no school is in no pair, and a team result's school is `rel.teams.results`'.
255
+ `lineage` is each edge end as the release publishes it (an edge may carry both ends): `links` and
256
+ `line` are the graph over them. `associations` is the people a directory lists beside a school, as
257
+ evidence of an association, never an identity: `school_id` is the school the print attached to,
258
+ `candidate_school_id` one it reached without attaching.
259
+
230
260
  ### How two schools are linked
231
261
 
232
262
  ```ts
@@ -550,6 +580,33 @@ await rel.feises.dates(); // every feis id with its first a
550
580
  await rel.majors.tabulated({ min_true_round: 3 }); // the majors addresses marked to round 3 or later
551
581
  ```
552
582
 
583
+ ## Marks sheets and panels
584
+
585
+ ```ts
586
+ const s = await rel.tabs.query({ competition: "<competition>" }); // or { address }, and a { round }
587
+ s.rows; // [{ address, competition, card, line, event, year, tier, round, true_round, seat_index,
588
+ // section, panel_size, column_order, drop_rule, the judge (name, print, TD number,
589
+ // id, unmatched reason), mark, mark_printed, place, place_printed, tie, points,
590
+ // points_printed }]
591
+ for await (const t of rel.tabs.stream({ tier: ["T1", "T2", "T3"], year: [2015, 2025] })) ...
592
+ await rel.panels.query({ competition: "<competition>" }); // { seats: [{ competition, panel, round,
593
+ // seat_index, address, the judge (print, TD number, id, unmatched reason) }] }
594
+ await rel.feisPanels.query({ address: "fweb:<code>.<comp>" }); // { seats: [{ address, platform, round,
595
+ // seat_index, panel_size, panel_kind, the judge (print, state printed, TD number, id,
596
+ // unmatched reason, basis) }] }
597
+ ```
598
+
599
+ A marks sheet is a row per card, round and seat, with the seat's mark, place and points as read
600
+ beside their prints, from `results.sqlite` (every tabulated sheet, the T4 events' too). The stream
601
+ takes an event (or several, case-blind), a year or an inclusive range, tiers, competitions, or
602
+ nothing (every cell), and yields one competition's rows together. A sheet's `event`, `year` and
603
+ `tier` are its competition's: a solo competition's from the results, a team competition's from the
604
+ team results (with the tier the release gives that event-year's solo competitions). A sheet the
605
+ release places in neither carries nulls, and a filter on them never reaches it. `panels` and
606
+ `feisPanels` with nothing named answer every seat. `round` is `rx` where one panel sat every round.
607
+ A seat carries its judge's TD number where the release linked one, else a judge id, else the
608
+ reason it is linked to neither (`TabRow`, `PanelSeat` and `FeisPanelSeat` name the fields).
609
+
553
610
  ## Teams and event-years
554
611
 
555
612
  ```ts
@@ -596,6 +653,9 @@ release into the `teams` and `events` indexes.
596
653
  ```ts
597
654
  await rel.staff.teacher("<name>"); // or a TD number
598
655
  await rel.staff.adjudicator("<name>");
656
+ await rel.staff.adjudicators({ year: 2025 }); // the register's rows (or a TD number's, or every one):
657
+ // [{ the TD number, year, cat, name_printed, country, country_printed, country_code, region,
658
+ // region_printed, region_code, geohash, src }] (`RegisterRow`)
599
659
  ```
600
660
 
601
661
  One person per TD number (else per judge id, else per name): the schools taught at (role,
@@ -747,6 +807,13 @@ is passed to the dancer resolver as asked (`rel.dancers`, above).
747
807
  idq records --years [--release <spec>] [--dancer <id,...>] [--year <y>]
748
808
  idq feis-dates [--release <spec>]
749
809
  idq tabulated [--release <spec>] [--min-round <n>]
810
+ idq tabs [--release <spec>] --comp <competition> | --address <address> [--round <r>]
811
+ idq tabs --stream [--release <spec>] [--comp <c,...>] [--event <e,...>] [--year <y> | --year <from>..<to>] [--tier <t,...>]
812
+ idq panels [--release <spec>] [--comp <competition>]
813
+ idq feis-panels [--release <spec>] [--address <feis competition>] [--platform <p>]
814
+ idq register [--release <spec>] [--year <y>] [--td <TD number>]
815
+ idq schools [--release <spec>] [--prints [--event <e>] [--year <y>] | --redirects | --lineage
816
+ | --associations [--school <school_id>]]
750
817
  idq teams [--release <spec>] [--event <e>] [--year <y>] [--rosters | --joins]
751
818
  idq events [--release <spec>] [--event <e>] [--year <y>]
752
819
  idq teacher [--release <spec>] <name|TD number>
@@ -759,7 +826,7 @@ is passed to the dancer resolver as asked (`rel.dancers`, above).
759
826
  [--oireachtas <region>[:<year>]]
760
827
  idq resolve registrations
761
828
 
762
- `stream` prints one JSON line per row (or entry), as it reads them. `dancer` prints the results of the one dancer where the answer is unique (every candidate's with
829
+ `stream` (and `tabs --stream`) prints one JSON line per row (or entry, or cell), as it reads them. `dancer` prints the results of the one dancer where the answer is unique (every candidate's with
763
830
  `--rows`). `--json` prints the call's answer as it is. `--cache` and `--repo` override the defaults.
764
831
 
765
832
  `update` moves to the newest published release, as a package manager's `update` moves to the newest
@@ -774,6 +774,96 @@ import {
774
774
  DEFAULT_SCHOOL_NAME_READER,
775
775
  schoolEncoder
776
776
  } from "@finbheara/domain/school";
777
+
778
+ var str = (v) => typeof v === "string" && v !== "" ? v : null;
779
+ var num = (v) => typeof v === "number" ? v : null;
780
+ var uniq = (xs) => [...new Set(xs)];
781
+ async function registrySchools(rel) {
782
+ await rel.require("schools.registry");
783
+ await rel.fetch("school-registry.jsonl");
784
+ const schools = readJsonl(rel.path("school-registry.jsonl")).map((r) => ({
785
+ school_id: r.school_id,
786
+ display: r.display,
787
+ regions: uniq((r.regions ?? []).map((x) => x.region)),
788
+ places: uniq((r.locations ?? []).map((x) => x.place))
789
+ })).sort((a, b) => a.school_id < b.school_id ? -1 : a.school_id > b.school_id ? 1 : 0);
790
+ return { tag: rel.tag, schools };
791
+ }
792
+ async function schoolPrints(rel, q) {
793
+ await rel.fetch("results");
794
+ await rel.require("schools.prints");
795
+ const where = ["r.school_id IS NOT NULL"];
796
+ const args = [];
797
+ if (q.event !== undefined) {
798
+ where.push("c.event = ? COLLATE NOCASE");
799
+ args.push(q.event);
800
+ }
801
+ if (q.year !== undefined) {
802
+ where.push("c.year = ?");
803
+ args.push(q.year);
804
+ }
805
+ const prints = rel.db("results").query(`SELECT c.event AS event, c.year AS year, trim(r.school_printed) AS print,
806
+ r.school_id AS school_id, count(*) AS rows
807
+ FROM competitions c JOIN results r ON r.file = c.file AND r.competition_line = c.line
808
+ WHERE ${where.join(" AND ")} GROUP BY 1, 2, 3, 4 ORDER BY 2, 1, 3, 4`).all(...args);
809
+ return { tag: rel.tag, asked: q, prints };
810
+ }
811
+ async function schoolRedirects(rel) {
812
+ await rel.require("schools.redirects");
813
+ await rel.fetch("school-redirects.jsonl");
814
+ const redirects = readJsonl(rel.path("school-redirects.jsonl")).map((r) => ({
815
+ old_id: String(r.old_id),
816
+ new_id: String(r.new_id),
817
+ kind: String(r.kind),
818
+ since: num(r.since)
819
+ }));
820
+ return { tag: rel.tag, redirects };
821
+ }
822
+ async function schoolLineage(rel) {
823
+ await rel.require("schools.lineage");
824
+ await rel.fetch("school-lineage.jsonl");
825
+ const edges = readJsonl(rel.path("school-lineage.jsonl")).map((r) => ({
826
+ id: String(r.id),
827
+ direction: String(r.direction),
828
+ other: String(r.other),
829
+ shape: String(r.shape),
830
+ year: num(r.year),
831
+ provisional: r.provisional === true,
832
+ decision: str(r.decision),
833
+ confidence: num(r.confidence)
834
+ }));
835
+ return { tag: rel.tag, edges };
836
+ }
837
+ async function observedAssociations(rel, q) {
838
+ await rel.require("schools.associations");
839
+ await rel.fetch("observed-associations.jsonl");
840
+ const associations = [];
841
+ for (const r of readJsonl(rel.path("observed-associations.jsonl"))) {
842
+ const school_id = str(r.schoolId);
843
+ if (q.school_id !== undefined && school_id !== q.school_id)
844
+ continue;
845
+ associations.push({
846
+ at: String(r.at),
847
+ seq: num(r.seq) ?? 0,
848
+ date: str(r.date),
849
+ person: str(r.person),
850
+ person_printed: typeof r.personPrinted === "string" ? r.personPrinted : "",
851
+ reading: str(r.reading),
852
+ person_type: str(r.personType),
853
+ credentials: str(r.credentials),
854
+ caption: str(r.caption),
855
+ school_printed: str(r.school),
856
+ school_id,
857
+ association_type: str(r.associationType),
858
+ src_type: str(r.srcType),
859
+ src: str(r.src),
860
+ candidate_school_id: str(r.candidateSchoolId),
861
+ candidate_rung: str(r.candidateRung)
862
+ });
863
+ }
864
+ return { tag: rel.tag, asked: q, associations };
865
+ }
866
+
777
867
  function readsSpanishNames2(candidates) {
778
868
  return candidates.some((c) => c.spanishNames !== undefined && (c.spanishNames.file || c.prints.some((p) => c.spanishNames?.prints.includes(p.trim()))));
779
869
  }
@@ -846,6 +936,21 @@ class SchoolResolver2 {
846
936
  this.graph ??= releaseSchoolGraph2(this.rel);
847
937
  return { tag: this.rel.tag, ...(await this.graph).line(a.trim(), q) };
848
938
  }
939
+ list() {
940
+ return registrySchools(this.rel);
941
+ }
942
+ prints(q = {}) {
943
+ return schoolPrints(this.rel, q);
944
+ }
945
+ redirects() {
946
+ return schoolRedirects(this.rel);
947
+ }
948
+ lineage() {
949
+ return schoolLineage(this.rel);
950
+ }
951
+ associations(q = {}) {
952
+ return observedAssociations(this.rel, q);
953
+ }
849
954
  async resolve(asked, q = {}) {
850
955
  const db = await this.rel.indexDb("schools");
851
956
  const reg = db.query("SELECT display, regions, places, spanish FROM registry WHERE school_id = ?");
@@ -2246,6 +2351,218 @@ import {
2246
2351
  roleEncoder
2247
2352
  } from "@finbheara/domain/noun-index";
2248
2353
  import { roleName } from "@finbheara/domain/person";
2354
+
2355
+ var TAB_COLUMNS = `address, slug AS competition, card, line, round, true_round, seat_index,
2356
+ section, panel_size, column_order, drop_rule, judge_name, judge_name_printed, judge_clrg,
2357
+ judge_id, judge_unmatched, mark, mark_printed, place, place_printed, tie, points, points_printed`;
2358
+ var TAB_NEEDS = [
2359
+ "address",
2360
+ "slug",
2361
+ "card",
2362
+ "line",
2363
+ "round",
2364
+ "true_round",
2365
+ "seat_index",
2366
+ "section",
2367
+ "panel_size",
2368
+ "column_order",
2369
+ "drop_rule",
2370
+ "judge_name",
2371
+ "judge_name_printed",
2372
+ "judge_clrg",
2373
+ "judge_id",
2374
+ "judge_unmatched",
2375
+ "mark",
2376
+ "mark_printed",
2377
+ "place",
2378
+ "place_printed",
2379
+ "tie",
2380
+ "points",
2381
+ "points_printed"
2382
+ ].map((c) => `results:tabs.${c}`);
2383
+ var NO_SHEET = { event: null, year: null, tier: null };
2384
+ var cardRange = (competition) => [`${competition}#`, `${competition}$`];
2385
+
2386
+ class TabFinder2 {
2387
+ rel;
2388
+ constructor(rel) {
2389
+ this.rel = rel;
2390
+ }
2391
+ async db() {
2392
+ await this.rel.fetch("results");
2393
+ await this.rel.require("tabs");
2394
+ return this.rel.db("results");
2395
+ }
2396
+ async sheets(db, only) {
2397
+ const out = new Map;
2398
+ const tiers = new Map;
2399
+ const rows = [];
2400
+ if (only === null)
2401
+ rows.push(...db.query("SELECT competition, event, year, tier FROM competitions WHERE competition IN (SELECT DISTINCT slug FROM tabs)").all());
2402
+ else
2403
+ for (let i = 0;i < only.length; i += 500) {
2404
+ const part = only.slice(i, i + 500);
2405
+ rows.push(...db.query(`SELECT competition, event, year, tier FROM competitions WHERE competition IN (${part.map(() => "?").join(", ")})`).all(...part));
2406
+ }
2407
+ for (const r of rows) {
2408
+ out.set(r.competition, { event: r.event, year: r.year, tier: r.tier });
2409
+ tiers.set(`${r.event.toLowerCase()}\x00${r.year}`, r.tier);
2410
+ }
2411
+ const missing = only === null ? null : only.filter((c) => !out.has(c));
2412
+ if (missing !== null && missing.length === 0)
2413
+ return out;
2414
+ if ((await this.rel.capability("teams")).state !== "full")
2415
+ return out;
2416
+ const teams = await this.rel.indexDb("teams");
2417
+ const team = teams.query("SELECT competition, event, year FROM result WHERE competition IS NOT NULL GROUP BY competition");
2418
+ const want = missing === null ? null : new Set(missing);
2419
+ for (const t of team.all()) {
2420
+ if (out.has(t.competition) || want !== null && !want.has(t.competition))
2421
+ continue;
2422
+ out.set(t.competition, {
2423
+ event: t.event,
2424
+ year: t.year,
2425
+ tier: tiers.get(`${t.event.toLowerCase()}\x00${t.year}`) ?? null
2426
+ });
2427
+ }
2428
+ if ([...out.values()].some((s) => s.tier === null && s.event !== null)) {
2429
+ for (const r of db.query("SELECT event, year, min(tier) AS tier FROM competitions GROUP BY event, year").all())
2430
+ tiers.set(`${r.event.toLowerCase()}\x00${r.year}`, r.tier);
2431
+ for (const s of out.values())
2432
+ if (s.tier === null && s.event !== null)
2433
+ s.tier = tiers.get(`${s.event.toLowerCase()}\x00${s.year}`) ?? null;
2434
+ }
2435
+ return out;
2436
+ }
2437
+ async query(q) {
2438
+ const db = await this.db();
2439
+ const where = [];
2440
+ const args = [];
2441
+ if (q.address !== undefined) {
2442
+ where.push("address = ?");
2443
+ args.push(q.address);
2444
+ } else if (q.competition !== undefined) {
2445
+ const [lo, hi] = cardRange(q.competition);
2446
+ where.push("address >= ? AND address < ? AND slug = ?");
2447
+ args.push(lo, hi, q.competition);
2448
+ } else
2449
+ throw new Error("archive-client: a tabs query names a competition or an address");
2450
+ if (q.round !== undefined) {
2451
+ where.push("round = ?");
2452
+ args.push(q.round);
2453
+ }
2454
+ const raw = db.query(`SELECT ${TAB_COLUMNS} FROM tabs WHERE ${where.join(" AND ")} ORDER BY address, round, seat_index, line`).all(...args);
2455
+ const sheets = raw.length === 0 ? new Map : await this.sheets(db, [...new Set(raw.map((r) => r.competition))]);
2456
+ return {
2457
+ tag: this.rel.tag,
2458
+ asked: q,
2459
+ rows: raw.map((r) => ({ ...r, ...sheets.get(r.competition) ?? NO_SHEET }))
2460
+ };
2461
+ }
2462
+ async* stream(q = {}) {
2463
+ const db = await this.db();
2464
+ const sheets = await this.sheets(db, q.competition ?? null);
2465
+ const events = q.event === undefined ? null : new Set((typeof q.event === "string" ? [q.event] : q.event).map((e) => e.toLowerCase()));
2466
+ const tiers = q.tier === undefined ? null : new Set(q.tier);
2467
+ const holds = (s) => {
2468
+ if (events !== null && (s.event === null || !events.has(s.event.toLowerCase())))
2469
+ return false;
2470
+ if (tiers !== null && (s.tier === null || !tiers.has(s.tier)))
2471
+ return false;
2472
+ if (typeof q.year === "number" && s.year !== q.year)
2473
+ return false;
2474
+ if (Array.isArray(q.year) && (s.year === null || s.year < q.year[0] || s.year > q.year[1]))
2475
+ return false;
2476
+ return true;
2477
+ };
2478
+ const filtered = events !== null || tiers !== null || q.year !== undefined;
2479
+ const run = function* (stmt, args) {
2480
+ for (const r of stmt.iterate(...args))
2481
+ yield { ...r, ...sheets.get(r.competition) ?? NO_SHEET };
2482
+ };
2483
+ if (!filtered && q.competition === undefined) {
2484
+ const all = db.prepare(`SELECT ${TAB_COLUMNS} FROM tabs ORDER BY address, round, seat_index, line`);
2485
+ try {
2486
+ yield* run(all, []);
2487
+ } finally {
2488
+ all.finalize();
2489
+ }
2490
+ return;
2491
+ }
2492
+ const asked = [...new Set(q.competition ?? sheets.keys())].filter((c) => holds(sheets.get(c) ?? NO_SHEET));
2493
+ const one = db.prepare(`SELECT ${TAB_COLUMNS} FROM tabs WHERE address >= ? AND address < ? AND slug = ? ORDER BY address, round, seat_index, line`);
2494
+ try {
2495
+ for (const c of asked.sort((a, b) => a < b ? -1 : a > b ? 1 : 0)) {
2496
+ const [lo, hi] = cardRange(c);
2497
+ yield* run(one, [lo, hi, c]);
2498
+ }
2499
+ } finally {
2500
+ one.finalize();
2501
+ }
2502
+ }
2503
+ }
2504
+
2505
+ class PanelFinder2 {
2506
+ rel;
2507
+ constructor(rel) {
2508
+ this.rel = rel;
2509
+ }
2510
+ async query(q = {}) {
2511
+ await this.rel.fetch("results");
2512
+ await this.rel.require("panels");
2513
+ const where = q.competition === undefined ? "" : " WHERE slug = ?";
2514
+ const seats = this.rel.db("results").query(`SELECT slug AS competition, panel, round, seat_index, address, judge_name_printed,
2515
+ judge_clrg, judge_id, judge_unmatched
2516
+ FROM panels${where} ORDER BY slug, panel, round, seat_index`).all(...q.competition === undefined ? [] : [q.competition]);
2517
+ return { tag: this.rel.tag, asked: q, seats };
2518
+ }
2519
+ }
2520
+
2521
+ class FeisPanelFinder2 {
2522
+ rel;
2523
+ constructor(rel) {
2524
+ this.rel = rel;
2525
+ }
2526
+ async query(q = {}) {
2527
+ await this.rel.fetch("results");
2528
+ await this.rel.require("feis-panels");
2529
+ const where = [];
2530
+ const args = [];
2531
+ if (q.address !== undefined) {
2532
+ where.push("address = ?");
2533
+ args.push(q.address);
2534
+ }
2535
+ if (q.platform !== undefined) {
2536
+ where.push("platform = ?");
2537
+ args.push(q.platform);
2538
+ }
2539
+ const seats = this.rel.db("results").query(`SELECT address, platform, round, seat_index, panel_size, panel_kind, judge_name_printed,
2540
+ judge_state_printed, judge_clrg, judge_id, judge_unmatched, judge_basis
2541
+ FROM feis_panels${where.length === 0 ? "" : ` WHERE ${where.join(" AND ")}`}
2542
+ ORDER BY address, round, seat_index`).all(...args);
2543
+ return { tag: this.rel.tag, asked: q, seats };
2544
+ }
2545
+ }
2546
+ async function registerRows(rel, q) {
2547
+ await rel.fetch("results");
2548
+ await rel.require("staff.adjudicators");
2549
+ const where = [];
2550
+ const args = [];
2551
+ if (q.judge_clrg !== undefined) {
2552
+ where.push("judge_clrg = ?");
2553
+ args.push(q.judge_clrg);
2554
+ }
2555
+ if (q.year !== undefined) {
2556
+ where.push("year = ?");
2557
+ args.push(q.year);
2558
+ }
2559
+ const rows = rel.db("results").query(`SELECT judge_clrg, year, cat, name_printed, country, country_printed, country_code, region,
2560
+ region_printed, region_code, geohash, src
2561
+ FROM adjudicators${where.length === 0 ? "" : ` WHERE ${where.join(" AND ")}`}
2562
+ ORDER BY judge_clrg, year`).all(...args);
2563
+ return { tag: rel.tag, asked: q, rows };
2564
+ }
2565
+
2249
2566
  var ENCODER2 = roleEncoder();
2250
2567
  var WHO = { table: "who", value: "entry", valueType: "INTEGER PRIMARY KEY" };
2251
2568
  var ASKED2 = ["surname_key", "nn4", "id"];
@@ -2363,6 +2680,9 @@ class StaffFinder2 {
2363
2680
  adjudicator(asked) {
2364
2681
  return this.find(asked, "adjudicator");
2365
2682
  }
2683
+ adjudicators(q = {}) {
2684
+ return registerRows(this.rel, q);
2685
+ }
2366
2686
  async find(asked, role) {
2367
2687
  const db = await this.rel.indexDb("staff");
2368
2688
  const a = asked.trim();
@@ -2507,8 +2827,8 @@ async function parquetColumns(path) {
2507
2827
  return new Set(parquetSchema(meta).children.map((c) => c.element.name));
2508
2828
  }
2509
2829
 
2510
- var str = (v) => typeof v === "string" && v !== "" ? v : null;
2511
- var num = (v) => typeof v === "number" ? v : null;
2830
+ var str2 = (v) => typeof v === "string" && v !== "" ? v : null;
2831
+ var num2 = (v) => typeof v === "number" ? v : null;
2512
2832
  var strs = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
2513
2833
  var scope = (id) => id === null ? null : id >= FEIS_LOCAL_ID_FLOOR2 ? "per-release" : "stable";
2514
2834
  var TEAM_TABLES = [
@@ -2563,9 +2883,9 @@ var teamsIndex = {
2563
2883
  const ey = new Map;
2564
2884
  const putEy = out.prepare("INSERT OR IGNORE INTO event_year VALUES (?, ?, ?)");
2565
2885
  for (const r of t.event_years) {
2566
- const k = str(r.event_year);
2567
- const event = str(r.event);
2568
- const year = num(r.year);
2886
+ const k = str2(r.event_year);
2887
+ const event = str2(r.event);
2888
+ const year = num2(r.year);
2569
2889
  if (k === null || event === null || year === null)
2570
2890
  continue;
2571
2891
  ey.set(k, { event, year });
@@ -2574,19 +2894,19 @@ var teamsIndex = {
2574
2894
  const putR = out.prepare("INSERT OR IGNORE INTO result VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
2575
2895
  let ord = 0;
2576
2896
  for (const r of t.team_results)
2577
- putR.run(str(r.address), str(r.slug), str(r.event), num(r.year), str(r.card), str(r.comp_no), str(r.title), str(r.category), num(r.hands), str(r.letter), str(r.label), str(r.name_printed), str(r.school_printed), str(r.school_loc), str(r.school_id), num(r.place), str(r.place_printed), num(r.tie), num(r.competitors), JSON.stringify(strs(r.atts)), str(r.age), str(r.gender), JSON.stringify(strs(r.printed_status)), ord++);
2897
+ putR.run(str2(r.address), str2(r.slug), str2(r.event), num2(r.year), str2(r.card), str2(r.comp_no), str2(r.title), str2(r.category), num2(r.hands), str2(r.letter), str2(r.label), str2(r.name_printed), str2(r.school_printed), str2(r.school_loc), str2(r.school_id), num2(r.place), str2(r.place_printed), num2(r.tie), num2(r.competitors), JSON.stringify(strs(r.atts)), str2(r.age), str2(r.gender), JSON.stringify(strs(r.printed_status)), ord++);
2578
2898
  const putT = out.prepare("INSERT OR IGNORE INTO roster_team VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
2579
2899
  for (const r of t.roster_teams) {
2580
- const e = ey.get(str(r.event_year) ?? "");
2581
- putT.run(str(r.team), str(r.kind), e?.event ?? null, num(r.year) ?? e?.year ?? null, str(r.comp_no), str(r.title), str(r.team_index), num(r.seq), str(r.school_id), str(r.school_print_materialized), str(r.dance), str(r.school_print_as_of), ord++);
2900
+ const e = ey.get(str2(r.event_year) ?? "");
2901
+ putT.run(str2(r.team), str2(r.kind), e?.event ?? null, num2(r.year) ?? e?.year ?? null, str2(r.comp_no), str2(r.title), str2(r.team_index), num2(r.seq), str2(r.school_id), str2(r.school_print_materialized), str2(r.dance), str2(r.school_print_as_of), ord++);
2582
2902
  }
2583
2903
  const putM = out.prepare("INSERT INTO roster_member VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
2584
2904
  for (const r of t.roster_members)
2585
- putM.run(str(r.team), num(r.seq), typeof r.name_printed === "string" ? r.name_printed : "", str(r.name), num(r.yob), num(r.dancer_id), str(r.gender), ord++);
2905
+ putM.run(str2(r.team), num2(r.seq), typeof r.name_printed === "string" ? r.name_printed : "", str2(r.name), num2(r.yob), num2(r.dancer_id), str2(r.gender), ord++);
2586
2906
  const putJ = out.prepare("INSERT INTO result_roster VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
2587
2907
  for (const r of t.team_result_rosters) {
2588
- const e = ey.get(str(r.event_year) ?? "");
2589
- putJ.run(str(r.team), e?.event ?? null, e?.year ?? null, str(r.comp_no), str(r.school_id), str(r.result_address), str(r.join_kind), num(r.via_split) === 1 ? 1 : 0, num(r.roster_teams), num(r.result_teams), str(r.roster_index), str(r.result_letter), ord++);
2908
+ const e = ey.get(str2(r.event_year) ?? "");
2909
+ putJ.run(str2(r.team), e?.event ?? null, e?.year ?? null, str2(r.comp_no), str2(r.school_id), str2(r.result_address), str2(r.join_kind), num2(r.via_split) === 1 ? 1 : 0, num2(r.roster_teams), num2(r.result_teams), str2(r.roster_index), str2(r.result_letter), ord++);
2590
2910
  }
2591
2911
  for (const s of TEAMS_INDEXES)
2592
2912
  out.run(s);
@@ -2606,11 +2926,11 @@ var eventsIndex = {
2606
2926
  out.run(EVENTS_SCHEMA);
2607
2927
  const put = out.prepare("INSERT OR IGNORE INTO event_year VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
2608
2928
  for (const r of rows) {
2609
- const event = str(r.event);
2610
- const year = num(r.year);
2929
+ const event = str2(r.event);
2930
+ const year = num2(r.year);
2611
2931
  if (event === null || year === null)
2612
2932
  continue;
2613
- put.run(event, year, str(r.date_lo), str(r.date_hi), str(r.resolution), str(r.source), JSON.stringify(strs(r.date_printed)), str(r.place), JSON.stringify(strs(r.place_printed)));
2933
+ put.run(event, year, str2(r.date_lo), str2(r.date_hi), str2(r.resolution), str2(r.source), JSON.stringify(strs(r.date_printed)), str2(r.place), JSON.stringify(strs(r.place_printed)));
2614
2934
  }
2615
2935
  out.run("CREATE INDEX event_year_event ON event_year(event COLLATE NOCASE)");
2616
2936
  }
@@ -3238,7 +3558,7 @@ var FEATURES2 = [
3238
3558
  feature: "schools.lineage",
3239
3559
  needs: ["school-lineage.jsonl"],
3240
3560
  since: 184,
3241
- calls: "schools.links and schools.line (refused on a release without it), and the `lineage` school evidence of dancers.resolve and dancers.search: no school is joined by lineage, never refused, on a release without it"
3561
+ calls: "schools.links, schools.line and schools.lineage (refused on a release without it), and the `lineage` school evidence of dancers.resolve and dancers.search: no school is joined by lineage, never refused, on a release without it"
3242
3562
  },
3243
3563
  {
3244
3564
  feature: "results.stream",
@@ -3407,6 +3727,91 @@ var FEATURES2 = [
3407
3727
  ],
3408
3728
  since: 260,
3409
3729
  calls: "staff.teacher, staff.adjudicator (the staff index)"
3730
+ },
3731
+ { feature: "tabs", needs: TAB_NEEDS, since: 184, calls: "tabs.query, tabs.stream" },
3732
+ {
3733
+ feature: "panels",
3734
+ needs: [
3735
+ "slug",
3736
+ "panel",
3737
+ "round",
3738
+ "seat_index",
3739
+ "address",
3740
+ "judge_name_printed",
3741
+ "judge_clrg",
3742
+ "judge_id",
3743
+ "judge_unmatched"
3744
+ ].map((c) => `results:panels.${c}`),
3745
+ since: 184,
3746
+ calls: "panels.query"
3747
+ },
3748
+ {
3749
+ feature: "feis-panels",
3750
+ needs: [
3751
+ "address",
3752
+ "platform",
3753
+ "round",
3754
+ "seat_index",
3755
+ "panel_size",
3756
+ "panel_kind",
3757
+ "judge_name_printed",
3758
+ "judge_state_printed",
3759
+ "judge_clrg",
3760
+ "judge_id",
3761
+ "judge_unmatched",
3762
+ "judge_basis"
3763
+ ].map((c) => `results:feis_panels.${c}`),
3764
+ since: 260,
3765
+ calls: "feisPanels.query"
3766
+ },
3767
+ {
3768
+ feature: "staff.adjudicators",
3769
+ needs: [
3770
+ "judge_clrg",
3771
+ "year",
3772
+ "cat",
3773
+ "name_printed",
3774
+ "country",
3775
+ "country_printed",
3776
+ "country_code",
3777
+ "region",
3778
+ "region_printed",
3779
+ "region_code",
3780
+ "geohash",
3781
+ "src"
3782
+ ].map((c) => `results:adjudicators.${c}`),
3783
+ since: 230,
3784
+ calls: "staff.adjudicators"
3785
+ },
3786
+ {
3787
+ feature: "schools.registry",
3788
+ needs: ["school-registry.jsonl"],
3789
+ since: 260,
3790
+ calls: "schools.list"
3791
+ },
3792
+ {
3793
+ feature: "schools.prints",
3794
+ needs: [
3795
+ "results:results.school_printed",
3796
+ "results:results.school_id",
3797
+ "results:results.competition_line",
3798
+ "results:competitions.event",
3799
+ "results:competitions.year"
3800
+ ],
3801
+ since: 184,
3802
+ calls: "schools.prints"
3803
+ },
3804
+ {
3805
+ feature: "schools.redirects",
3806
+ needs: ["school-redirects.jsonl"],
3807
+ since: 184,
3808
+ calls: "schools.redirects"
3809
+ },
3810
+ {
3811
+ feature: "schools.associations",
3812
+ needs: ["observed-associations.jsonl"],
3813
+ since: 215,
3814
+ calls: "schools.associations"
3410
3815
  }
3411
3816
  ];
3412
3817
  async function hasNeed(probe, n) {
@@ -3490,6 +3895,9 @@ class Release2 {
3490
3895
  registrations;
3491
3896
  teams;
3492
3897
  events;
3898
+ tabs;
3899
+ panels;
3900
+ feisPanels;
3493
3901
  opts;
3494
3902
  handles = new Map;
3495
3903
  remoteAssets = null;
@@ -3512,6 +3920,9 @@ class Release2 {
3512
3920
  this.registrations = new RegistrationResolver2(this, this.comps, opts.dancerResolver);
3513
3921
  this.teams = new TeamFinder2(this);
3514
3922
  this.events = new EventFinder2(this);
3923
+ this.tabs = new TabFinder2(this);
3924
+ this.panels = new PanelFinder2(this);
3925
+ this.feisPanels = new FeisPanelFinder2(this);
3515
3926
  }
3516
3927
  receipt() {
3517
3928
  return readReceipt(this.dir) ?? { tag: this.tag, source: "unknown", files: {} };
@@ -4232,4 +4643,4 @@ function staleWarning2(answered, latest, floor, tags = [], command = "idq") {
4232
4643
  return `answered from ${answered}. ${floorTag ?? `cut ${floor.cut}`} is required${why}: run \`${command} use ${floor.cut}\` to get it`;
4233
4644
  }
4234
4645
 
4235
- export { DATABASES2, planAsset2, sha256File2, unzstdTo2, NAME_LEVELS2, nameLevel2, NO_CALENDAR2, FeisFinder2, ADVANCEMENT_DANCES2, entryRank2, FEIS_LOCAL_ID_FLOOR2, ResultFinder2, MajorsFinder2, LINEAGE_FILE2, linksOfLineage2, releaseSchoolGraph2, readsSpanishNames2, SchoolResolver2, initialsOnly2, nameOrderOf, DancerResolver2, readPopulation2, syllabusSourceOf2, canonicalOf2, CompResolver2, StaffFinder2, TeamFinder2, EventFinder2, defaultBuilders2, indexPath2, indexKey2, isCurrent2, buildIndex2, ensureIndex2, listIndexes2, readPin2, youngest2, dancerResolverOf2, RegistrationResolver2, ghRemote2, httpsRemote2, fallbackRemote2, setRemote2, defaultRemote, columnsOf2, FEATURES2, capabilityOf2, needsRelease2, resolveTag2, Release2, openRelease2, openCached2, DEFAULT_KEEP2, ReleaseManager2, releases2, LOCAL_DIR2, localRoot2, newerThan2, incomplete2, releaseVersion2, pickBuild2, buildTag2, lacks, openBuild2, sweepBuilds2, LATEST_CHECK_MS2, LATEST_CHECK_BUDGET_MS2, latestCheckPath2, readLatestCheck2, latestCheck2, within, staleWarning2 };
4646
+ export { DATABASES2, planAsset2, sha256File2, unzstdTo2, NAME_LEVELS2, nameLevel2, NO_CALENDAR2, FeisFinder2, ADVANCEMENT_DANCES2, entryRank2, FEIS_LOCAL_ID_FLOOR2, ResultFinder2, MajorsFinder2, LINEAGE_FILE2, linksOfLineage2, releaseSchoolGraph2, readsSpanishNames2, SchoolResolver2, initialsOnly2, nameOrderOf, DancerResolver2, readPopulation2, syllabusSourceOf2, canonicalOf2, CompResolver2, TabFinder2, PanelFinder2, FeisPanelFinder2, StaffFinder2, TeamFinder2, EventFinder2, defaultBuilders2, indexPath2, indexKey2, isCurrent2, buildIndex2, ensureIndex2, listIndexes2, readPin2, youngest2, dancerResolverOf2, RegistrationResolver2, ghRemote2, httpsRemote2, fallbackRemote2, setRemote2, defaultRemote, columnsOf2, FEATURES2, capabilityOf2, needsRelease2, resolveTag2, Release2, openRelease2, openCached2, DEFAULT_KEEP2, ReleaseManager2, releases2, LOCAL_DIR2, localRoot2, newerThan2, incomplete2, releaseVersion2, pickBuild2, buildTag2, lacks, openBuild2, sweepBuilds2, LATEST_CHECK_MS2, LATEST_CHECK_BUDGET_MS2, latestCheckPath2, readLatestCheck2, latestCheck2, within, staleWarning2 };