@finbheara/archive-client 0.18.0 → 0.20.1

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
@@ -80,6 +80,7 @@ c.features; // [{ feature: "dancers", state: "full" | "missing", missing: [...
80
80
  | feature | the calls | from cut |
81
81
  | --- | --- | --- |
82
82
  | `results.level` | `results.query`, `majors.results` | 215 |
83
+ | `results.dancer-level` | `results.dancerLevel` | 270 |
83
84
  | `dancers` | `dancers.resolve`, `search`, `get` | 260 |
84
85
  | `schools` | `schools.resolve`, a dancer question with a school | 260 |
85
86
  | `staff` | `staff.teacher`, `staff.adjudicator` | 260 |
@@ -100,7 +101,10 @@ A spec is `pinned` (the default: the user-local pin, else the latest), `latest`,
100
101
 
101
102
  EVERY FILE IS CHECKED before it lands: the downloaded bytes against the host's own sha256 digest,
102
103
  and the content against the manifest (a database's decompressed bytes against
103
- `databases.<name>.sha256`, a parquet file against `parquet.files[<name>].sha256`). A mismatch
104
+ `databases.<name>.sha256`, a side file's decompressed bytes against `sideFiles.<name>.sha256`, a
105
+ parquet file against `parquet.files[<name>].sha256`). A side file the manifest names under
106
+ `sideFiles` is published zstd'd (`<name>.zst`, or the `file` it states) and lands plain under its
107
+ own name. A mismatch
104
108
  refuses and leaves nothing in place. A file nothing vouches for is kept and its receipt says so
105
109
  (`strict: true` refuses it). A database is decompressed in process, streamed, never through a
106
110
  `zstd` binary.
@@ -448,6 +452,49 @@ A row carries the prints and the ids that stand for them: the address, the compe
448
452
  and age, the place, the name and school as printed with the dancer and school ids. The majors are
449
453
  read from `majors.sqlite`, fetched on first use.
450
454
 
455
+ ### What level a dancer dances
456
+
457
+ ```ts
458
+ await rel.results.dancerLevel({ dancer_id: 1001, year: 2025 });
459
+ // { level: 200, grades: { advancement_range: { lo: 300, hi: 400 },
460
+ // level_range: { lo: 200, hi: 400 }, oireachtas_level: true } }
461
+ await rel.results.dancerLevel({ dancer_id: 1002 }); // its latest year: { level: 600 }
462
+ ```
463
+
464
+ No single number is a grades dancer's level. Grades are won PER DANCE: a dancer can be Novice in
465
+ the reel and Prizewinner in the slip jig at once. Only a championship (600 preliminary, 700 open)
466
+ is a level of the dancer. A competition's `feis_level` is the competition's rung. A set, a set
467
+ dance, a treble reel or a special prints a rung that its entrants need not hold. So the answer
468
+ reads:
469
+
470
+ - **the year**: the competition's calendar year. When it is omitted, the latest year in which the
471
+ dancer has a rated feis row.
472
+ - **a rated row**: a feis row (tier `T4`, a platform's) of the dancer, on the floor (no printed
473
+ absence, no `DNT`), whose competition publishes a `feis_level`.
474
+ - **a championship row**: a rated row at 600 or 700. A 700 whose title or level field also names
475
+ the preliminary (`PC and OC`, `Prelim and Champ`) joins the two, and it reads 600 here.
476
+ - **a graded row**: a rated row at 100 to 500 whose `dance` is one of the six graded dances (reel,
477
+ light jig, single jig, slip jig, treble jig, hornpipe) and whose title, level or dance field names
478
+ no special (`special`, `spcl`, `trophy`). Sets, set dances, treble reels and specials are never
479
+ read.
480
+
481
+ | field | meaning |
482
+ | --- | --- |
483
+ | `level` | for a championship dancer (any championship row that year), the LOWEST championship rung danced that year. Otherwise the lowest graded rung danced that year. Null where the year holds neither |
484
+ | `grades` | present only for a grades dancer: no championship row that year, and at least one graded row |
485
+ | `grades.advancement_range` | `{ lo, hi }`, the lowest and highest rung over the reel, slip jig, treble jig and hornpipe, the dances a dancer advances in. Null where none of them was danced |
486
+ | `grades.level_range` | `{ lo, hi }` over all six graded dances |
487
+ | `grades.oireachtas_level` | `true` where the dancer danced a championship solo at a regional Oireachtas that year: a row on the floor at a tier `T3` event, the same calendar year, whose competition's `category` is `Solo`. A grade championship that the release files as `Solo` counts as well (one regional, in one year, prints ten) |
488
+
489
+ Only feis rows set the level. A dancer with majors rows and no feis row in the year (tier `T4`) gets `level` null, whatever they danced at the majors.
490
+
491
+ A dance that was not danced that year takes no part: each range covers what was danced. An adult
492
+ grade row is read like any other, since the adult ladder also grades per dance. A release older
493
+ than the published `dance` column is refused by name (`results.dancer-level`).
494
+
495
+ Each call reads the dancer's rows once. The release does not index the dancer id, so one call
496
+ scans the results table, as `results.query` does (about half a second on a full release).
497
+
451
498
  ## Every row, streamed
452
499
 
453
500
  ```ts
@@ -27,6 +27,17 @@ function planAsset2(name, manifest) {
27
27
  contentSha256: manifest?.databases?.[name]?.sha256 ?? null
28
28
  };
29
29
  }
30
+ const side = manifest?.sideFiles?.[name];
31
+ if (side !== undefined) {
32
+ const file = side.file ?? `${name}.zst`;
33
+ return {
34
+ asked: name,
35
+ published: file === name ? [name] : [file, name],
36
+ local: name,
37
+ decompress: true,
38
+ contentSha256: side.sha256 ?? null
39
+ };
40
+ }
30
41
  return {
31
42
  asked: name,
32
43
  published: [name],
@@ -366,6 +377,69 @@ function notOnTheFloor(placePrinted, atts, withdrawn) {
366
377
  return withdrawn !== null && DID_NOT_TABULATE.has(withdrawn);
367
378
  }
368
379
 
380
+ import { GRADE_DANCES } from "@finbheara/domain/dance-types";
381
+ var ADVANCEMENT_DANCES2 = Object.freeze([
382
+ "reel",
383
+ "slip-jig",
384
+ "treble-jig",
385
+ "hornpipe"
386
+ ]);
387
+ var SPECIAL_WORDS = Object.freeze(["special", "spcl", "trophy"]);
388
+ var JOINED_WORDS = Object.freeze([
389
+ "prelim",
390
+ "pc and oc",
391
+ "pc/oc",
392
+ "pc & oc",
393
+ "pc-oc",
394
+ "pc/c",
395
+ "pr\xEDomh",
396
+ "priomh"
397
+ ]);
398
+ var MAJORS_SOURCES = new Set(["transcript", "manifested"]);
399
+ var has = (t, words) => words.some((w) => t.includes(w));
400
+ function range(levels) {
401
+ if (levels.length === 0)
402
+ return null;
403
+ return { lo: Math.min(...levels), hi: Math.max(...levels) };
404
+ }
405
+ var isRated = (r) => r.tier === "T4" && !MAJORS_SOURCES.has(r.source) && r.feis_level !== null;
406
+ function dancerLevelOf(db, tag, q) {
407
+ const all = db.query(`SELECT c.year AS year, c.tier AS tier, c.source AS source, c.feis_level AS feis_level,
408
+ c.dance AS dance, c.category AS category,
409
+ lower(coalesce(c.title, '') || ' | ' || coalesce(c.level_printed, '') || ' | ' ||
410
+ coalesce(c.dance_printed, '')) AS t,
411
+ r.place_printed AS place_printed, r.atts AS atts, r.withdrawn AS withdrawn
412
+ FROM results r JOIN competitions c ON c.file = r.file AND c.line = r.competition_line
413
+ WHERE r.dancer_id = ? AND c.tier IN ('T3', 'T4')`).all(q.dancer_id).filter((r) => !notOnTheFloor(r.place_printed, r.atts, r.withdrawn));
414
+ let year = q.year ?? null;
415
+ if (year === null) {
416
+ for (const r of all)
417
+ if (isRated(r) && r.year !== null && (year === null || r.year > year))
418
+ year = r.year;
419
+ }
420
+ const answer = { tag, asked: q, dancer_id: q.dancer_id, year, level: null };
421
+ if (year === null)
422
+ return answer;
423
+ const rows = all.filter((r) => r.year === year);
424
+ const rated = rows.filter(isRated);
425
+ const champs = rated.filter((r) => r.feis_level >= 600).map((r) => r.feis_level === 700 && has(r.t, JOINED_WORDS) ? 600 : r.feis_level);
426
+ if (champs.length > 0)
427
+ return { ...answer, level: Math.min(...champs) };
428
+ const graded = rated.filter((r) => r.feis_level >= 100 && r.feis_level <= 500 && r.dance !== null && GRADE_DANCES.includes(r.dance) && !has(r.t, SPECIAL_WORDS));
429
+ const levelRange = range(graded.map((r) => r.feis_level));
430
+ if (levelRange === null)
431
+ return answer;
432
+ return {
433
+ ...answer,
434
+ level: levelRange.lo,
435
+ grades: {
436
+ advancement_range: range(graded.filter((r) => ADVANCEMENT_DANCES2.includes(r.dance)).map((r) => r.feis_level)),
437
+ level_range: levelRange,
438
+ oireachtas_level: rows.some((r) => r.tier === "T3" && r.category === "Solo")
439
+ }
440
+ };
441
+ }
442
+
369
443
  var ENTRY_RANK = new Map([
370
444
  ["final", 0],
371
445
  ["", 1],
@@ -565,6 +639,11 @@ class ResultFinder2 {
565
639
  await this.rel.require("results.level");
566
640
  return queryResults(this.rel, "results", q);
567
641
  }
642
+ async dancerLevel(q) {
643
+ await this.rel.fetch("results");
644
+ await this.rel.require("results.dancer-level");
645
+ return dancerLevelOf(this.rel.db("results"), this.rel.tag, q);
646
+ }
568
647
  async streamDb(q) {
569
648
  const db = scopeDb(q.scope);
570
649
  await this.rel.fetch(db);
@@ -3077,6 +3156,21 @@ var FEATURES2 = [
3077
3156
  since: 215,
3078
3157
  calls: "results.query, majors.results"
3079
3158
  },
3159
+ {
3160
+ feature: "results.dancer-level",
3161
+ needs: [
3162
+ "results:competitions.feis_level",
3163
+ "results:competitions.dance",
3164
+ "results:competitions.level_printed",
3165
+ "results:competitions.dance_printed",
3166
+ "results:competitions.category",
3167
+ "results:results.place_printed",
3168
+ "results:results.atts",
3169
+ "results:results.withdrawn"
3170
+ ],
3171
+ since: 270,
3172
+ calls: "results.dancerLevel"
3173
+ },
3080
3174
  {
3081
3175
  feature: "majors.order",
3082
3176
  needs: ["majors:competitions.seq"],
@@ -3746,7 +3840,8 @@ class ReleaseManager2 {
3746
3840
  copyFileSync(src, raw);
3747
3841
  }
3748
3842
  const base = name.replace(/\.sqlite(\.zst)?$/, "");
3749
- const asked = base !== name && isDatabase(base) ? base : name;
3843
+ const side = Object.entries(manifest.sideFiles ?? {}).find(([k, f]) => (f.file ?? `${k}.zst`) === name)?.[0];
3844
+ const asked = base !== name && isDatabase(base) ? base : side ?? name;
3750
3845
  const plan = planAsset2(asked, manifest);
3751
3846
  const landed = await land(raw, { name, digest: null }, plan, join4(dest, plan.local), this.opts, log);
3752
3847
  receipt.files[plan.local] = landed;
@@ -3842,4 +3937,4 @@ function releases2(opts = {}) {
3842
3937
  return new ReleaseManager2(opts);
3843
3938
  }
3844
3939
 
3845
- export { DATABASES2, planAsset2, sha256File2, unzstdTo2, NAME_LEVELS2, nameLevel2, NO_CALENDAR2, FeisFinder2, 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, columnsOf2, FEATURES2, capabilityOf2, needsRelease2, resolveTag2, Release2, openRelease2, openCached2, ReleaseManager2, releases2 };
3940
+ 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, columnsOf2, FEATURES2, capabilityOf2, needsRelease2, resolveTag2, Release2, openRelease2, openCached2, ReleaseManager2, releases2 };
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  nameOrderOf,
5
5
  openRelease2,
6
6
  releases2
7
- } from "./chunks/index-tr14vc1c.js";
7
+ } from "./chunks/index-6028k52f.js";
8
8
  import"./chunks/index-hne1wadw.js";
9
9
  import"./chunks/index-hghdy980.js";
10
10
 
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  nameLevel2,
9
9
  NO_CALENDAR2,
10
10
  FeisFinder2,
11
+ ADVANCEMENT_DANCES2,
11
12
  entryRank2,
12
13
  FEIS_LOCAL_ID_FLOOR2,
13
14
  ResultFinder2,
@@ -51,7 +52,7 @@ import {
51
52
  openCached2,
52
53
  ReleaseManager2,
53
54
  releases2
54
- } from "./chunks/index-tr14vc1c.js";
55
+ } from "./chunks/index-6028k52f.js";
55
56
  import {
56
57
  cacheRoot2,
57
58
  repoOf2
@@ -66,6 +67,7 @@ import {
66
67
  pickTag2
67
68
  } from "./chunks/index-hghdy980.js";
68
69
  export {
70
+ ADVANCEMENT_DANCES2 as ADVANCEMENT_DANCES,
69
71
  ARCHIVE_TAG2 as ARCHIVE_TAG,
70
72
  CompResolver2 as CompResolver,
71
73
  DATABASES2 as DATABASES,
@@ -14,6 +14,11 @@ export interface ReleaseManifest {
14
14
  table?: string;
15
15
  }>;
16
16
  };
17
+ sideFiles?: Record<string, {
18
+ file?: string;
19
+ bytes?: number;
20
+ sha256?: string;
21
+ }>;
17
22
  [k: string]: unknown;
18
23
  }
19
24
  export declare const DATABASES: readonly ["results", "majors"];
@@ -0,0 +1,24 @@
1
+ import type { Database } from "bun:sqlite";
2
+ export declare const ADVANCEMENT_DANCES: readonly string[];
3
+ export interface DancerLevelQuery {
4
+ dancer_id: number;
5
+ year?: number;
6
+ }
7
+ export interface LevelRange {
8
+ lo: number;
9
+ hi: number;
10
+ }
11
+ export interface DancerGrades {
12
+ advancement_range: LevelRange | null;
13
+ level_range: LevelRange;
14
+ oireachtas_level: boolean;
15
+ }
16
+ export interface DancerLevelAnswer {
17
+ tag: string;
18
+ asked: DancerLevelQuery;
19
+ dancer_id: number;
20
+ year: number | null;
21
+ level: number | null;
22
+ grades?: DancerGrades;
23
+ }
24
+ export declare function dancerLevelOf(db: Database, tag: string, q: DancerLevelQuery): DancerLevelAnswer;
@@ -2,6 +2,7 @@ export { type AssetPlan, DATABASES, type DatabaseName, type LandedFile, type Rec
2
2
  export { sha256File, unzstdTo } from "./bytes.js";
3
3
  export { type AgeAsked, type AgeEvidence, type DancerAnswer, type DancerBulkRecord, type DancerCandidate, type DancerQuery, type DancerRecord, DancerResolver, type DancerRegion, type DancerSchool, type DancerSearchQuery, type DancerYear, type ExcludedCandidate, FEIS_LOCAL_ID_FLOOR, type InitialsRefuted, type Narrowing, initialsOnly, type OireachtasAsked, type PopulationEvidence, type RegionEvidence, type SchoolEvidence, readPopulation, } from "./dancers.js";
4
4
  export { NAME_LEVELS, type NameLevel, nameLevel } from "./dancers-names.js";
5
+ export { ADVANCEMENT_DANCES, type DancerGrades, type DancerLevelAnswer, type DancerLevelQuery, type LevelRange, } from "./dancer-level.js";
5
6
  export { type Comp, type CompAnswer, type CompDance, type CompKind, type CompQuery, CompResolver, type CompSource, type FeisPlace, type SyllabusAnswer, type SyllabusForAnswer, type SyllabusSource, type TitleCheck, canonicalOf, syllabusSourceOf, } from "./comps.js";
6
7
  export { type ClientOptions, cacheRoot, repoOf } from "./config.js";
7
8
  export { type Feis, type FeisAnswer, FeisFinder, type FeisQuery, NO_CALENDAR, type Series, type SeriesAnswer, type SeriesInstance, type SeriesNext, } from "./feises.js";
@@ -1,3 +1,4 @@
1
+ import { type DancerLevelAnswer, type DancerLevelQuery } from "./dancer-level.js";
1
2
  import type { Release } from "./release.js";
2
3
  import { type EntryRow, type StreamQuery, type StreamRow } from "./stream.js";
3
4
  export declare const FEIS_LOCAL_ID_FLOOR = 17179869184;
@@ -44,6 +45,7 @@ export declare class ResultFinder {
44
45
  private readonly rel;
45
46
  constructor(rel: Release);
46
47
  query(q: ResultQuery): Promise<ResultAnswer>;
48
+ dancerLevel(q: DancerLevelQuery): Promise<DancerLevelAnswer>;
47
49
  private streamDb;
48
50
  stream(q: StreamQuery): AsyncGenerator<StreamRow>;
49
51
  entries(q: StreamQuery): AsyncGenerator<EntryRow>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finbheara/archive-client",
3
- "version": "0.18.0",
3
+ "version": "0.20.1",
4
4
  "description": "Open a versioned release of an Irish dance results archive: download, verify, decompress and cache it by tag, keep the cache, and resolve feises and schools against it",
5
5
  "keywords": ["irish-dance", "results", "release", "cache", "sqlite"],
6
6
  "license": "MIT",
@@ -44,8 +44,8 @@
44
44
  "prepublishOnly": "bun scripts/build.ts && bun ../../tools/checks/archive-client-tarball.ts"
45
45
  },
46
46
  "dependencies": {
47
- "@finbheara/domain": "0.56.0",
48
- "@finbheara/names": "0.28.0",
47
+ "@finbheara/domain": "0.57.1",
48
+ "@finbheara/names": "0.29.0",
49
49
  "hyparquet": "1.31.2"
50
50
  },
51
51
  "devDependencies": {