@finbheara/archive-client 0.7.0 → 0.8.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
@@ -234,7 +234,17 @@ level of the names package: the print, the key, the folded key, the tokens in an
234
234
  (a middle name one print omits: `nameRelation`). `age` is how the dancer's published birth window
235
235
  meets the asked age (`Age.fitsIn`, `overlaps`): `fits`, `overlaps`, `contradicts`, `unknown`.
236
236
  `school` is how the dancer's schools meet the asked one: `same` (the latest school as of `asOf`),
237
- `moved` (an earlier one), `none`, `unresolved` (the print reached no school).
237
+ `lineage` (the latest school is one the release's lineage joins to the asked one), `moved` (an
238
+ earlier one is the asked one), `none`, `unresolved` (the print reached no school).
239
+
240
+ THE LINEAGE is the release's `school-lineage.jsonl`: every line joins its `id` and its `other`,
241
+ whatever its direction (`precedes`, `succeeds`) or shape (a rename, a split, a merger, an absorb,
242
+ a relation), so a file carrying one end of an edge reads as one carrying both. The whole line
243
+ joins: every school reached through any number of edges, in either direction (the two halves of a
244
+ split are joined through their predecessor, the schools of a merger through their successor). An
245
+ earlier row at a joined school alone is no match. `r.school.lineage` lists the ids joined to the
246
+ asked ones. A release that publishes no lineage (feature `schools.lineage`) joins
247
+ nothing and its answer carries no `lineage`, never refused.
238
248
 
239
249
  `population` is how the dancer's published majors population meets the asked one: `same`,
240
250
  `contradicts`, `both`, `unknown`, `unasked`. A dancer's population is read only from its rows at
@@ -251,12 +261,13 @@ majors solo competitions and in none of the asked one's (listed in `excluded`, `
251
261
  `"population"`, and a candidate contradicting both says `age`). A dancer published in both, or in
252
262
  neither, stays. A population word that names no one population (`mixed`) narrows nothing, and the
253
263
  answer says so (`r.population.note`). The school is evidence and order, never a reason
254
- to drop anyone: candidates whose latest school is the asked one come first, then those that ever
255
- danced for it, then the rest. A school row meets the asked school by its id, else by its print
264
+ to drop anyone: candidates whose latest school is the asked one, or one the lineage joins to it,
265
+ come first, then those that ever danced for it, then the rest. A school row meets the asked school by its id, else by its print
256
266
  (one `rel.schools` resolves to the asked ids), so a latest print the release
257
267
  filed under no id is still `same`. `unique` is one candidate left after the age, never a best
258
268
  guess promoted. Where the school singles out one candidate, `preferred` names it with the reason,
259
- for a caller to weigh, and `arm` stays as counted. The `score` orders candidates and is no
269
+ for a caller to weigh, and `arm` stays as counted. A `lineage` match ranks with `same`: one of
270
+ each singles out neither. The `score` orders candidates and is no
260
271
  probability.
261
272
 
262
273
  A FEIS-LOCAL ID (`FEIS_LOCAL_ID_FLOOR`, 2^34, and up) is valid in its release only: a candidate and a result row
@@ -106,6 +106,215 @@ function nameLevel2(q, c) {
106
106
  return null;
107
107
  }
108
108
 
109
+ import { readFileSync } from "fs";
110
+ var SCHEMA = `
111
+ CREATE TABLE feis(
112
+ feis_id TEXT PRIMARY KEY, source TEXT NOT NULL, token TEXT NOT NULL, host_id TEXT NOT NULL,
113
+ name TEXT NOT NULL, date_lo TEXT, date_hi TEXT, date_printed TEXT, year INTEGER,
114
+ location TEXT, instance_id TEXT, brand_id TEXT, regions TEXT NOT NULL,
115
+ competitions INTEGER NOT NULL, rows INTEGER NOT NULL, captures INTEGER NOT NULL
116
+ );
117
+ CREATE TABLE redirect(old_id TEXT PRIMARY KEY, new_id TEXT NOT NULL, kind TEXT NOT NULL);
118
+ `;
119
+ var INDEXES = [
120
+ "CREATE INDEX feis_host ON feis(host_id COLLATE NOCASE)",
121
+ "CREATE INDEX feis_token ON feis(token)",
122
+ "CREATE INDEX feis_source ON feis(source)",
123
+ "CREATE INDEX feis_instance ON feis(instance_id)",
124
+ "CREATE INDEX feis_brand ON feis(brand_id)",
125
+ "CREATE INDEX feis_year ON feis(year)"
126
+ ];
127
+ function readJsonl(path) {
128
+ return readFileSync(path, "utf8").split(`
129
+ `).filter((l) => l.trim() !== "").map((l) => JSON.parse(l));
130
+ }
131
+ var feisesIndex = {
132
+ name: "feises",
133
+ version: "2",
134
+ inputs: ["results", "feis-redirects.jsonl"],
135
+ says: "every feis by id, platform, code, series and name, with its counts and regions",
136
+ build(rel, out) {
137
+ out.run(SCHEMA);
138
+ const src = rel.db("results");
139
+ const counts = new Map;
140
+ for (const r of src.query("SELECT event, count(*) AS n, json_group_array(DISTINCT feis_region) AS regions FROM competitions GROUP BY event").all())
141
+ counts.set(r.event, {
142
+ comps: r.n,
143
+ regions: new Set(JSON.parse(r.regions).filter((x) => x !== null))
144
+ });
145
+ const rows = new Map;
146
+ for (const r of src.query("SELECT c.event AS event, sum(r.n) AS n FROM (SELECT competition, count(*) AS n FROM results GROUP BY competition) r JOIN (SELECT DISTINCT competition, event FROM competitions) c ON c.competition = r.competition GROUP BY c.event").all())
147
+ rows.set(r.event, r.n);
148
+ const put = out.prepare("INSERT OR IGNORE INTO feis VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
149
+ const captures = new Map;
150
+ const feisRows = src.query("SELECT feis_id, source, host_id, name_printed, date_lo, date_hi, date_printed, year, location_printed, feis_instance_id, feis_brand_id FROM feises ORDER BY file, line").all();
151
+ for (const f of feisRows)
152
+ captures.set(f.feis_id, (captures.get(f.feis_id) ?? 0) + 1);
153
+ for (const f of feisRows) {
154
+ const c = counts.get(f.feis_id);
155
+ const colon = f.feis_id.indexOf(":");
156
+ put.run(f.feis_id, f.source, colon === -1 ? f.source : f.feis_id.slice(0, colon), f.host_id, f.name_printed, f.date_lo, f.date_hi, f.date_printed, f.year, f.location_printed, f.feis_instance_id, f.feis_brand_id, JSON.stringify([...c?.regions ?? []].sort()), c?.comps ?? 0, rows.get(f.feis_id) ?? 0, captures.get(f.feis_id) ?? 0);
157
+ }
158
+ const red = out.prepare("INSERT OR REPLACE INTO redirect VALUES (?, ?, ?)");
159
+ for (const r of readJsonl(rel.path("feis-redirects.jsonl")))
160
+ red.run(r.old_id, r.new_id, r.kind);
161
+ for (const s of INDEXES)
162
+ out.run(s);
163
+ }
164
+ };
165
+ var NO_CALENDAR2 = "the release holds a feis-year once its results are captured and carries no calendar of scheduled feiseanna";
166
+ var globToLike = (g) => g.replace(/[\\%_]/g, (c) => `\\${c}`).replace(/\*/g, "%").replace(/\?/g, "_");
167
+
168
+ class FeisFinder2 {
169
+ rel;
170
+ constructor(rel) {
171
+ this.rel = rel;
172
+ }
173
+ async dates() {
174
+ await this.rel.fetch("results");
175
+ await this.rel.require("feises.dates");
176
+ const dates = this.rel.db("results").query("SELECT feis_id, min(date_lo) AS date_lo, max(date_hi) AS date_hi FROM feises GROUP BY feis_id ORDER BY feis_id").all();
177
+ return { tag: this.rel.tag, dates };
178
+ }
179
+ async find(q) {
180
+ const db = await this.rel.indexDb("feises");
181
+ const where = [];
182
+ const args = [];
183
+ let redirected;
184
+ if (q.platform !== undefined) {
185
+ where.push("(source = ? OR token = ?)");
186
+ args.push(q.platform, q.platform);
187
+ }
188
+ if (q.code !== undefined) {
189
+ where.push("host_id = ? COLLATE NOCASE");
190
+ args.push(q.code);
191
+ }
192
+ if (q.id !== undefined) {
193
+ let id = q.id;
194
+ const seen = new Set;
195
+ for (;; ) {
196
+ const r = db.query("SELECT new_id FROM redirect WHERE old_id = ?").get(id);
197
+ if (r === null || seen.has(r.new_id))
198
+ break;
199
+ seen.add(id);
200
+ id = r.new_id;
201
+ }
202
+ if (id !== q.id)
203
+ redirected = { from: q.id, to: id };
204
+ where.push("(feis_id = ? OR instance_id = ? OR brand_id = ?)");
205
+ args.push(id, id, id);
206
+ }
207
+ if (q.name !== undefined) {
208
+ where.push("name LIKE ? ESCAPE '\\'");
209
+ args.push(globToLike(q.name));
210
+ }
211
+ if (q.year !== undefined) {
212
+ where.push("year = ?");
213
+ args.push(q.year);
214
+ }
215
+ const sql = `SELECT * FROM feis${where.length > 0 ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY date_lo, feis_id`;
216
+ const feises = db.query(sql).all(...args).map((r) => ({
217
+ ...r,
218
+ regions: JSON.parse(r.regions)
219
+ }));
220
+ return { tag: this.rel.tag, feises, ...redirected !== undefined ? { redirected } : {} };
221
+ }
222
+ async codeStem(token, stem) {
223
+ const db = await this.rel.indexDb("feises");
224
+ const like = db.query("SELECT host_id, year, brand_id FROM feis WHERE token = ? AND host_id LIKE ? ESCAPE '\\' COLLATE NOCASE").all(token, `${stem.replace(/[\\%_]/g, (c) => `\\${c}`)}__`);
225
+ const fits = like.filter((f) => {
226
+ const d = /(\d{2})$/.exec(f.host_id);
227
+ return d !== null && f.host_id.slice(0, -2).toLowerCase() === stem.toLowerCase() && f.year !== null && f.year % 100 === Number(d[1]) && f.brand_id !== null;
228
+ });
229
+ return [...new Set(fits.map((f) => f.brand_id))].sort();
230
+ }
231
+ async seriesOf(brand) {
232
+ const db = await this.rel.indexDb("feises");
233
+ const rows = db.query("SELECT * FROM feis WHERE brand_id = ? ORDER BY year, date_lo, feis_id").all(brand);
234
+ const byYear = new Map;
235
+ for (const r of rows) {
236
+ const i = byYear.get(r.year);
237
+ if (i === undefined)
238
+ byYear.set(r.year, {
239
+ year: r.year,
240
+ feis_ids: [r.feis_id],
241
+ date_lo: r.date_lo,
242
+ name: r.name
243
+ });
244
+ else
245
+ i.feis_ids.push(r.feis_id);
246
+ }
247
+ const instances = [...byYear.values()];
248
+ const newest = rows[rows.length - 1];
249
+ return {
250
+ series_id: brand,
251
+ name: newest?.name ?? "",
252
+ host: newest?.source ?? "",
253
+ hosts: [...new Set(rows.map((r) => r.source))].sort(),
254
+ codes: [...new Set(rows.map((r) => r.host_id))].sort(),
255
+ years: instances.map((i) => i.year).filter((y) => y !== null),
256
+ instances
257
+ };
258
+ }
259
+ async series(q) {
260
+ const asked = q.trim();
261
+ const answer = async (brands, via, more = {}) => {
262
+ const candidates = [];
263
+ for (const b of brands)
264
+ candidates.push(await this.seriesOf(b));
265
+ const arm = candidates.length === 0 ? "none" : candidates.length === 1 ? "unique" : "ambiguous";
266
+ const u = arm === "unique" ? candidates[0] : null;
267
+ return {
268
+ tag: this.rel.tag,
269
+ asked,
270
+ arm,
271
+ via: candidates.length === 0 ? null : via,
272
+ series_id: u?.series_id ?? null,
273
+ name: u?.name ?? null,
274
+ host: u?.host ?? null,
275
+ years: u?.years ?? [],
276
+ instances: u?.instances ?? [],
277
+ next: null,
278
+ ...u !== null ? { next_why: NO_CALENDAR2 } : {},
279
+ candidates,
280
+ ...arm === "ambiguous" ? { why: `${candidates.length} series answer "${asked}": ${brands.join(", ")}` } : {},
281
+ ...more
282
+ };
283
+ };
284
+ if (asked === "")
285
+ return answer([], null, { why: "no series was asked" });
286
+ const byId = await this.find({ id: asked });
287
+ if (byId.feises.length > 0) {
288
+ const brands = [...new Set(byId.feises.map((f) => f.brand_id))];
289
+ const filed = brands.filter((b) => b !== null).sort();
290
+ const redirected = byId.redirected !== undefined ? { redirected: byId.redirected } : {};
291
+ if (filed.length === 0)
292
+ return answer([], null, {
293
+ ...redirected,
294
+ why: `the release files ${byId.feises.map((f) => f.feis_id).join(", ")} under no series`
295
+ });
296
+ return answer(filed, "id", redirected);
297
+ }
298
+ const code = /^([^:\s]+):(.*?)(\d{2})?$/.exec(asked);
299
+ if (code !== null) {
300
+ const [, token, stem] = code;
301
+ const brands = stem === "" ? [] : await this.codeStem(token, stem);
302
+ if (brands.length > 0)
303
+ return answer(brands, "code-stem");
304
+ }
305
+ const glob = /[*?]/.test(asked) ? asked : `*${asked}*`;
306
+ const byName = await this.find({ name: glob });
307
+ const brands = [
308
+ ...new Set(byName.feises.map((f) => f.brand_id).filter((b) => b !== null))
309
+ ].sort();
310
+ if (brands.length > 0)
311
+ return answer(brands, "name");
312
+ return answer([], null, {
313
+ why: byName.feises.length > 0 ? `the feises named "${asked}" are filed under no series` : `the release holds no feis or series "${asked}"`
314
+ });
315
+ }
316
+ }
317
+
109
318
  var ENTRY_RANK = new Map([
110
319
  ["final", 0],
111
320
  ["", 1],
@@ -375,6 +584,7 @@ class MajorsFinder2 {
375
584
  }
376
585
  }
377
586
 
587
+ var LINEAGE_FILE = "school-lineage.jsonl";
378
588
  function nameOrderOf(s) {
379
589
  return s === "surname-first" || s === "given-first" || s === "unknown" ? s : undefined;
380
590
  }
@@ -506,11 +716,17 @@ var AGE_WEIGHT = {
506
716
  };
507
717
  var SCHOOL_RANK = {
508
718
  same: 0,
719
+ lineage: 0,
509
720
  moved: 1,
510
721
  none: 2,
511
722
  unresolved: 2,
512
723
  unasked: 2
513
724
  };
725
+ var PREFERRED_WHY = {
726
+ same: "whose latest school is",
727
+ lineage: "whose latest school the release's lineage joins to",
728
+ moved: "that ever danced for"
729
+ };
514
730
  var POPULATION_WEIGHT = {
515
731
  same: 1,
516
732
  both: 0,
@@ -520,6 +736,7 @@ var POPULATION_WEIGHT = {
520
736
  };
521
737
  var SCHOOL_WEIGHT = {
522
738
  same: 2,
739
+ lineage: 2,
523
740
  moved: 1,
524
741
  none: 0,
525
742
  unresolved: 0,
@@ -535,9 +752,30 @@ function idChunks(ids) {
535
752
  class DancerResolver2 {
536
753
  rel;
537
754
  printIds = new Map;
755
+ lineageOf;
538
756
  constructor(rel) {
539
757
  this.rel = rel;
540
758
  }
759
+ lineage() {
760
+ this.lineageOf ??= (async () => {
761
+ if ((await this.rel.capability("schools.lineage")).state !== "full")
762
+ return null;
763
+ await this.rel.fetch(LINEAGE_FILE);
764
+ const out = new Map;
765
+ const join = (a, b) => {
766
+ const s = out.get(a) ?? new Set;
767
+ s.add(b);
768
+ out.set(a, s);
769
+ };
770
+ for (const r of readJsonl(this.rel.path(LINEAGE_FILE)))
771
+ if (r.id !== r.other) {
772
+ join(r.id, r.other);
773
+ join(r.other, r.id);
774
+ }
775
+ return out;
776
+ })();
777
+ return this.lineageOf;
778
+ }
541
779
  async records(q = {}) {
542
780
  await this.rel.fetch("results");
543
781
  await this.rel.require("dancers.records");
@@ -643,11 +881,27 @@ class DancerResolver2 {
643
881
  const age = q.age === undefined ? undefined : this.readAge(q.age, asOf);
644
882
  const population = q.population === undefined ? undefined : readPopulation2(q.population);
645
883
  let schoolIds = null;
884
+ let lineageIds = [];
646
885
  let school;
647
886
  if (q.school !== undefined) {
648
887
  const s = await this.rel.schools.resolve(q.school);
649
888
  schoolIds = s.candidates.map((c) => c.school_id);
650
- school = { arm: s.arm, ids: schoolIds };
889
+ const lineage = await this.lineage();
890
+ if (lineage === null)
891
+ school = { arm: s.arm, ids: schoolIds };
892
+ else {
893
+ const asked = new Set(schoolIds);
894
+ const reached = new Set(schoolIds);
895
+ const todo = [...schoolIds];
896
+ for (let id = todo.pop();id !== undefined; id = todo.pop())
897
+ for (const o of lineage.get(id) ?? [])
898
+ if (!reached.has(o)) {
899
+ reached.add(o);
900
+ todo.push(o);
901
+ }
902
+ lineageIds = [...reached].filter((id) => !asked.has(id)).sort();
903
+ school = { arm: s.arm, ids: schoolIds, lineage: lineageIds };
904
+ }
651
905
  }
652
906
  const byDancer = new Map;
653
907
  const unlinked = new Map;
@@ -674,7 +928,7 @@ class DancerResolver2 {
674
928
  const ageEv = ageEvidence(d, age?.age ?? null);
675
929
  const pop = populationOf(db, id);
676
930
  const popEv = populationEvidence(pop, population?.read ?? null);
677
- const schoolEv = schoolIds === null ? "unasked" : schoolIds.length === 0 ? "unresolved" : await this.schoolEvidence(schools, schoolIds, asOf);
931
+ const schoolEv = schoolIds === null ? "unasked" : schoolIds.length === 0 ? "unresolved" : await this.schoolEvidence(schools, schoolIds, lineageIds, asOf);
678
932
  all.push({
679
933
  dancer_id: id,
680
934
  display: d.display,
@@ -706,15 +960,17 @@ class DancerResolver2 {
706
960
  });
707
961
  let preferred;
708
962
  if (kept.length > 1 && school !== undefined)
709
- for (const ev of ["same", "moved"]) {
710
- const hit = kept.filter((c) => SCHOOL_RANK[c.evidence.school] === SCHOOL_RANK[ev]);
963
+ for (const rank of [SCHOOL_RANK.same, SCHOOL_RANK.moved]) {
964
+ const hit = kept.filter((c) => SCHOOL_RANK[c.evidence.school] === rank);
711
965
  if (hit.length === 0)
712
966
  continue;
713
- if (hit.length === 1)
967
+ if (hit.length === 1) {
968
+ const c = hit[0];
714
969
  preferred = {
715
- dancer_id: hit[0].dancer_id,
716
- why: `the only candidate ${ev === "same" ? "whose latest school is" : "that ever danced for"} ${q.school}`
970
+ dancer_id: c.dancer_id,
971
+ why: `the only candidate ${PREFERRED_WHY[c.evidence.school]} ${q.school}`
717
972
  };
973
+ }
718
974
  break;
719
975
  }
720
976
  return {
@@ -730,29 +986,33 @@ class DancerResolver2 {
730
986
  ...school !== undefined ? { school } : {}
731
987
  };
732
988
  }
733
- async schoolEvidence(schools, ids, asOf) {
989
+ async schoolEvidence(schools, ids, lineage, asOf) {
734
990
  const rows = schools.filter((s) => asOf === null || s.year_lo === null || s.year_lo <= asOf);
735
- const meets = async (s) => {
991
+ const idsOf = async (s) => {
736
992
  if (s.school_id !== null)
737
- return ids.includes(s.school_id);
993
+ return [s.school_id];
738
994
  let read = this.printIds.get(s.print);
739
995
  if (read === undefined) {
740
996
  read = (await this.rel.schools.resolve(s.print)).candidates.map((c) => c.school_id);
741
997
  this.printIds.set(s.print, read);
742
998
  }
743
- return read.some((id) => ids.includes(id));
999
+ return read;
744
1000
  };
745
1001
  const top = (s) => asOf !== null && s.year_hi !== null ? Math.min(s.year_hi, asOf) : s.year_hi ?? -1;
746
1002
  const latest = Math.max(-1, ...rows.map(top));
747
1003
  let earlier = false;
1004
+ let joined = false;
748
1005
  for (const s of rows) {
749
- if (!await meets(s))
750
- continue;
751
- if (top(s) === latest)
752
- return "same";
753
- earlier = true;
1006
+ const own = await idsOf(s);
1007
+ const isLatest = top(s) === latest;
1008
+ if (own.some((id) => ids.includes(id))) {
1009
+ if (isLatest)
1010
+ return "same";
1011
+ earlier = true;
1012
+ } else if (isLatest && own.some((id) => lineage.includes(id)))
1013
+ joined = true;
754
1014
  }
755
- return earlier ? "moved" : "none";
1015
+ return joined ? "lineage" : earlier ? "moved" : "none";
756
1016
  }
757
1017
  readAge(a, asOf) {
758
1018
  const done = (age, note) => {
@@ -865,7 +1125,7 @@ var LEVEL = Object.freeze({
865
1125
  600: "PC",
866
1126
  700: "OC"
867
1127
  });
868
- var SCHEMA = `
1128
+ var SCHEMA2 = `
869
1129
  CREATE TABLE comp(
870
1130
  competition TEXT PRIMARY KEY, feis_id TEXT NOT NULL, brand_id TEXT, year INTEGER,
871
1131
  code TEXT NOT NULL, code_fold TEXT NOT NULL, title TEXT, age TEXT, gender TEXT,
@@ -874,7 +1134,7 @@ CREATE TABLE comp(
874
1134
  gender_source TEXT, level_printed TEXT, dance_printed TEXT, dance TEXT
875
1135
  );
876
1136
  `;
877
- var INDEXES = [
1137
+ var INDEXES2 = [
878
1138
  "CREATE INDEX comp_feis_code ON comp(feis_id, code_fold)",
879
1139
  "CREATE INDEX comp_brand_year ON comp(brand_id, year)"
880
1140
  ];
@@ -893,7 +1153,7 @@ var compsIndex = {
893
1153
  inputs: ["results"],
894
1154
  says: "every competition by its feis and its own code, with its series and year",
895
1155
  build(rel, out) {
896
- out.run(SCHEMA);
1156
+ out.run(SCHEMA2);
897
1157
  const src = rel.db("results");
898
1158
  const brand = new Map;
899
1159
  for (const f of src.query("SELECT feis_id, feis_brand_id, year FROM feises ORDER BY file, line").all())
@@ -909,7 +1169,7 @@ var compsIndex = {
909
1169
  const f = brand.get(c.event);
910
1170
  put.run(c.competition, c.event, f?.brand ?? null, c.year ?? f?.year ?? null, c.comp_no.trim(), fold(c.comp_no), c.title, c.age, c.gender, c.feis_level, c.round, c.source, captures.get(c.competition) ?? 1, c.file, c.line, c.age_source, c.age_printed, c.gender_source, c.level_printed, c.dance_printed, c.dance);
911
1171
  }
912
- for (const s of INDEXES)
1172
+ for (const s of INDEXES2)
913
1173
  out.run(s);
914
1174
  }
915
1175
  };
@@ -1199,215 +1459,6 @@ class CompResolver2 {
1199
1459
  }
1200
1460
  }
1201
1461
 
1202
- import { readFileSync } from "fs";
1203
- var SCHEMA2 = `
1204
- CREATE TABLE feis(
1205
- feis_id TEXT PRIMARY KEY, source TEXT NOT NULL, token TEXT NOT NULL, host_id TEXT NOT NULL,
1206
- name TEXT NOT NULL, date_lo TEXT, date_hi TEXT, date_printed TEXT, year INTEGER,
1207
- location TEXT, instance_id TEXT, brand_id TEXT, regions TEXT NOT NULL,
1208
- competitions INTEGER NOT NULL, rows INTEGER NOT NULL, captures INTEGER NOT NULL
1209
- );
1210
- CREATE TABLE redirect(old_id TEXT PRIMARY KEY, new_id TEXT NOT NULL, kind TEXT NOT NULL);
1211
- `;
1212
- var INDEXES2 = [
1213
- "CREATE INDEX feis_host ON feis(host_id COLLATE NOCASE)",
1214
- "CREATE INDEX feis_token ON feis(token)",
1215
- "CREATE INDEX feis_source ON feis(source)",
1216
- "CREATE INDEX feis_instance ON feis(instance_id)",
1217
- "CREATE INDEX feis_brand ON feis(brand_id)",
1218
- "CREATE INDEX feis_year ON feis(year)"
1219
- ];
1220
- function readJsonl(path) {
1221
- return readFileSync(path, "utf8").split(`
1222
- `).filter((l) => l.trim() !== "").map((l) => JSON.parse(l));
1223
- }
1224
- var feisesIndex = {
1225
- name: "feises",
1226
- version: "2",
1227
- inputs: ["results", "feis-redirects.jsonl"],
1228
- says: "every feis by id, platform, code, series and name, with its counts and regions",
1229
- build(rel, out) {
1230
- out.run(SCHEMA2);
1231
- const src = rel.db("results");
1232
- const counts = new Map;
1233
- for (const r of src.query("SELECT event, count(*) AS n, json_group_array(DISTINCT feis_region) AS regions FROM competitions GROUP BY event").all())
1234
- counts.set(r.event, {
1235
- comps: r.n,
1236
- regions: new Set(JSON.parse(r.regions).filter((x) => x !== null))
1237
- });
1238
- const rows = new Map;
1239
- for (const r of src.query("SELECT c.event AS event, sum(r.n) AS n FROM (SELECT competition, count(*) AS n FROM results GROUP BY competition) r JOIN (SELECT DISTINCT competition, event FROM competitions) c ON c.competition = r.competition GROUP BY c.event").all())
1240
- rows.set(r.event, r.n);
1241
- const put = out.prepare("INSERT OR IGNORE INTO feis VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
1242
- const captures = new Map;
1243
- const feisRows = src.query("SELECT feis_id, source, host_id, name_printed, date_lo, date_hi, date_printed, year, location_printed, feis_instance_id, feis_brand_id FROM feises ORDER BY file, line").all();
1244
- for (const f of feisRows)
1245
- captures.set(f.feis_id, (captures.get(f.feis_id) ?? 0) + 1);
1246
- for (const f of feisRows) {
1247
- const c = counts.get(f.feis_id);
1248
- const colon = f.feis_id.indexOf(":");
1249
- put.run(f.feis_id, f.source, colon === -1 ? f.source : f.feis_id.slice(0, colon), f.host_id, f.name_printed, f.date_lo, f.date_hi, f.date_printed, f.year, f.location_printed, f.feis_instance_id, f.feis_brand_id, JSON.stringify([...c?.regions ?? []].sort()), c?.comps ?? 0, rows.get(f.feis_id) ?? 0, captures.get(f.feis_id) ?? 0);
1250
- }
1251
- const red = out.prepare("INSERT OR REPLACE INTO redirect VALUES (?, ?, ?)");
1252
- for (const r of readJsonl(rel.path("feis-redirects.jsonl")))
1253
- red.run(r.old_id, r.new_id, r.kind);
1254
- for (const s of INDEXES2)
1255
- out.run(s);
1256
- }
1257
- };
1258
- var NO_CALENDAR2 = "the release holds a feis-year once its results are captured and carries no calendar of scheduled feiseanna";
1259
- var globToLike = (g) => g.replace(/[\\%_]/g, (c) => `\\${c}`).replace(/\*/g, "%").replace(/\?/g, "_");
1260
-
1261
- class FeisFinder2 {
1262
- rel;
1263
- constructor(rel) {
1264
- this.rel = rel;
1265
- }
1266
- async dates() {
1267
- await this.rel.fetch("results");
1268
- await this.rel.require("feises.dates");
1269
- const dates = this.rel.db("results").query("SELECT feis_id, min(date_lo) AS date_lo, max(date_hi) AS date_hi FROM feises GROUP BY feis_id ORDER BY feis_id").all();
1270
- return { tag: this.rel.tag, dates };
1271
- }
1272
- async find(q) {
1273
- const db = await this.rel.indexDb("feises");
1274
- const where = [];
1275
- const args = [];
1276
- let redirected;
1277
- if (q.platform !== undefined) {
1278
- where.push("(source = ? OR token = ?)");
1279
- args.push(q.platform, q.platform);
1280
- }
1281
- if (q.code !== undefined) {
1282
- where.push("host_id = ? COLLATE NOCASE");
1283
- args.push(q.code);
1284
- }
1285
- if (q.id !== undefined) {
1286
- let id = q.id;
1287
- const seen = new Set;
1288
- for (;; ) {
1289
- const r = db.query("SELECT new_id FROM redirect WHERE old_id = ?").get(id);
1290
- if (r === null || seen.has(r.new_id))
1291
- break;
1292
- seen.add(id);
1293
- id = r.new_id;
1294
- }
1295
- if (id !== q.id)
1296
- redirected = { from: q.id, to: id };
1297
- where.push("(feis_id = ? OR instance_id = ? OR brand_id = ?)");
1298
- args.push(id, id, id);
1299
- }
1300
- if (q.name !== undefined) {
1301
- where.push("name LIKE ? ESCAPE '\\'");
1302
- args.push(globToLike(q.name));
1303
- }
1304
- if (q.year !== undefined) {
1305
- where.push("year = ?");
1306
- args.push(q.year);
1307
- }
1308
- const sql = `SELECT * FROM feis${where.length > 0 ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY date_lo, feis_id`;
1309
- const feises = db.query(sql).all(...args).map((r) => ({
1310
- ...r,
1311
- regions: JSON.parse(r.regions)
1312
- }));
1313
- return { tag: this.rel.tag, feises, ...redirected !== undefined ? { redirected } : {} };
1314
- }
1315
- async codeStem(token, stem) {
1316
- const db = await this.rel.indexDb("feises");
1317
- const like = db.query("SELECT host_id, year, brand_id FROM feis WHERE token = ? AND host_id LIKE ? ESCAPE '\\' COLLATE NOCASE").all(token, `${stem.replace(/[\\%_]/g, (c) => `\\${c}`)}__`);
1318
- const fits = like.filter((f) => {
1319
- const d = /(\d{2})$/.exec(f.host_id);
1320
- return d !== null && f.host_id.slice(0, -2).toLowerCase() === stem.toLowerCase() && f.year !== null && f.year % 100 === Number(d[1]) && f.brand_id !== null;
1321
- });
1322
- return [...new Set(fits.map((f) => f.brand_id))].sort();
1323
- }
1324
- async seriesOf(brand) {
1325
- const db = await this.rel.indexDb("feises");
1326
- const rows = db.query("SELECT * FROM feis WHERE brand_id = ? ORDER BY year, date_lo, feis_id").all(brand);
1327
- const byYear = new Map;
1328
- for (const r of rows) {
1329
- const i = byYear.get(r.year);
1330
- if (i === undefined)
1331
- byYear.set(r.year, {
1332
- year: r.year,
1333
- feis_ids: [r.feis_id],
1334
- date_lo: r.date_lo,
1335
- name: r.name
1336
- });
1337
- else
1338
- i.feis_ids.push(r.feis_id);
1339
- }
1340
- const instances = [...byYear.values()];
1341
- const newest = rows[rows.length - 1];
1342
- return {
1343
- series_id: brand,
1344
- name: newest?.name ?? "",
1345
- host: newest?.source ?? "",
1346
- hosts: [...new Set(rows.map((r) => r.source))].sort(),
1347
- codes: [...new Set(rows.map((r) => r.host_id))].sort(),
1348
- years: instances.map((i) => i.year).filter((y) => y !== null),
1349
- instances
1350
- };
1351
- }
1352
- async series(q) {
1353
- const asked = q.trim();
1354
- const answer = async (brands, via, more = {}) => {
1355
- const candidates = [];
1356
- for (const b of brands)
1357
- candidates.push(await this.seriesOf(b));
1358
- const arm = candidates.length === 0 ? "none" : candidates.length === 1 ? "unique" : "ambiguous";
1359
- const u = arm === "unique" ? candidates[0] : null;
1360
- return {
1361
- tag: this.rel.tag,
1362
- asked,
1363
- arm,
1364
- via: candidates.length === 0 ? null : via,
1365
- series_id: u?.series_id ?? null,
1366
- name: u?.name ?? null,
1367
- host: u?.host ?? null,
1368
- years: u?.years ?? [],
1369
- instances: u?.instances ?? [],
1370
- next: null,
1371
- ...u !== null ? { next_why: NO_CALENDAR2 } : {},
1372
- candidates,
1373
- ...arm === "ambiguous" ? { why: `${candidates.length} series answer "${asked}": ${brands.join(", ")}` } : {},
1374
- ...more
1375
- };
1376
- };
1377
- if (asked === "")
1378
- return answer([], null, { why: "no series was asked" });
1379
- const byId = await this.find({ id: asked });
1380
- if (byId.feises.length > 0) {
1381
- const brands = [...new Set(byId.feises.map((f) => f.brand_id))];
1382
- const filed = brands.filter((b) => b !== null).sort();
1383
- const redirected = byId.redirected !== undefined ? { redirected: byId.redirected } : {};
1384
- if (filed.length === 0)
1385
- return answer([], null, {
1386
- ...redirected,
1387
- why: `the release files ${byId.feises.map((f) => f.feis_id).join(", ")} under no series`
1388
- });
1389
- return answer(filed, "id", redirected);
1390
- }
1391
- const code = /^([^:\s]+):(.*?)(\d{2})?$/.exec(asked);
1392
- if (code !== null) {
1393
- const [, token, stem] = code;
1394
- const brands = stem === "" ? [] : await this.codeStem(token, stem);
1395
- if (brands.length > 0)
1396
- return answer(brands, "code-stem");
1397
- }
1398
- const glob = /[*?]/.test(asked) ? asked : `*${asked}*`;
1399
- const byName = await this.find({ name: glob });
1400
- const brands = [
1401
- ...new Set(byName.feises.map((f) => f.brand_id).filter((b) => b !== null))
1402
- ].sort();
1403
- if (brands.length > 0)
1404
- return answer(brands, "name");
1405
- return answer([], null, {
1406
- why: byName.feises.length > 0 ? `the feises named "${asked}" are filed under no series` : `the release holds no feis or series "${asked}"`
1407
- });
1408
- }
1409
- }
1410
-
1411
1462
  import { Database } from "bun:sqlite";
1412
1463
  import { createHash as createHash2 } from "crypto";
1413
1464
  import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "fs";
@@ -2517,6 +2568,12 @@ var FEATURES2 = [
2517
2568
  since: 270,
2518
2569
  calls: "the `dance` a comp of comps.resolve, comps.syllabus and comps.syllabusFor carries: null, never refused, on a release without it"
2519
2570
  },
2571
+ {
2572
+ feature: "schools.lineage",
2573
+ needs: ["school-lineage.jsonl"],
2574
+ since: 184,
2575
+ calls: "the `lineage` school evidence of dancers.resolve and dancers.search: no school is joined by lineage, never refused, on a release without it"
2576
+ },
2520
2577
  {
2521
2578
  feature: "results.stream",
2522
2579
  needs: [
@@ -3229,4 +3286,4 @@ function releases2(opts = {}) {
3229
3286
  return new ReleaseManager2(opts);
3230
3287
  }
3231
3288
 
3232
- export { DATABASES2, planAsset2, sha256File2, unzstdTo2, NAME_LEVELS2, nameLevel2, entryRank2, FEIS_LOCAL_ID_FLOOR2, ResultFinder2, MajorsFinder2, nameOrderOf, DancerResolver2, readPopulation2, canonicalOf2, CompResolver2, NO_CALENDAR2, FeisFinder2, 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 };
3289
+ 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 };
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  nameOrderOf,
5
5
  openRelease2,
6
6
  releases2
7
- } from "./chunks/index-4ph8n4ga.js";
7
+ } from "./chunks/index-mg6dw0y5.js";
8
8
  import"./chunks/index-hne1wadw.js";
9
9
  import"./chunks/index-hghdy980.js";
10
10
 
@@ -178,7 +178,7 @@ function printDancers(a, say) {
178
178
  if (a.age !== undefined)
179
179
  say(` age asked: ${a.age.read ?? "(unread)"}${a.age.birth !== null ? ` born ${a.age.birth.from ?? "?"}-${a.age.birth.to ?? "?"}` : ""}${a.age.note !== undefined ? ` (${a.age.note})` : ""}`);
180
180
  if (a.school !== undefined)
181
- say(` school asked: ${a.school.arm} ${a.school.ids.join(", ") || "-"}`);
181
+ say(` school asked: ${a.school.arm} ${a.school.ids.join(", ") || "-"}${a.school.lineage !== undefined && a.school.lineage.length > 0 ? ` (lineage: ${a.school.lineage.join(", ")})` : ""}`);
182
182
  if (a.population !== undefined)
183
183
  say(` population asked: ${a.population.read ?? "(unread)"}${a.population.note !== undefined ? ` (${a.population.note})` : ""}`);
184
184
  for (const c of a.candidates)
package/dist/index.js CHANGED
@@ -6,6 +6,8 @@ import {
6
6
  unzstdTo2,
7
7
  NAME_LEVELS2,
8
8
  nameLevel2,
9
+ NO_CALENDAR2,
10
+ FeisFinder2,
9
11
  entryRank2,
10
12
  FEIS_LOCAL_ID_FLOOR2,
11
13
  ResultFinder2,
@@ -14,8 +16,6 @@ import {
14
16
  readPopulation2,
15
17
  canonicalOf2,
16
18
  CompResolver2,
17
- NO_CALENDAR2,
18
- FeisFinder2,
19
19
  StaffFinder2,
20
20
  SchoolResolver2,
21
21
  TeamFinder2,
@@ -45,7 +45,7 @@ import {
45
45
  openCached2,
46
46
  ReleaseManager2,
47
47
  releases2
48
- } from "./chunks/index-4ph8n4ga.js";
48
+ } from "./chunks/index-mg6dw0y5.js";
49
49
  import {
50
50
  cacheRoot2,
51
51
  repoOf2
@@ -24,7 +24,7 @@ export interface DancerQuery {
24
24
  }
25
25
  export type AgeEvidence = "fits" | "overlaps" | "contradicts" | "unknown" | "unasked";
26
26
  export type PopulationEvidence = "same" | "contradicts" | "both" | "unknown" | "unasked";
27
- export type SchoolEvidence = "same" | "moved" | "none" | "unresolved" | "unasked";
27
+ export type SchoolEvidence = "same" | "lineage" | "moved" | "none" | "unresolved" | "unasked";
28
28
  export interface DancerSchool {
29
29
  school_id: string | null;
30
30
  print: string;
@@ -98,6 +98,7 @@ export interface DancerAnswer {
98
98
  school?: {
99
99
  arm: string;
100
100
  ids: string[];
101
+ lineage?: string[];
101
102
  };
102
103
  refused?: string;
103
104
  }
@@ -163,7 +164,9 @@ export interface DancerYear {
163
164
  export declare class DancerResolver {
164
165
  private readonly rel;
165
166
  private readonly printIds;
167
+ private lineageOf;
166
168
  constructor(rel: Release);
169
+ private lineage;
167
170
  records(q?: {
168
171
  ids?: readonly number[];
169
172
  }): Promise<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finbheara/archive-client",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Open a versioned release of an Irish dance results archive: download, verify, decompress and cache it by tag, keep the cache, and resolve feises and schools against it",
5
5
  "keywords": ["irish-dance", "results", "release", "cache", "sqlite"],
6
6
  "license": "MIT",