@finbheara/archive-client 0.9.0 → 0.10.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
@@ -220,8 +220,9 @@ const r = await rel.dancers.resolve({
220
220
  });
221
221
  r.arm; // "unique" | "ambiguous" | "none"
222
222
  r.candidates; // [{ dancer_id, display, scope, score, evidence: { nameLevel, age, school, population, region }, prints, ... }]
223
- r.excluded; // the candidates a contradiction set aside (why: "age" | "population" | "region")
224
- r.preferred; // NON-BINDING: { dancer_id, why } where the school (else the region) singles one out; never moves arm
223
+ r.excluded; // the candidates a contradiction set aside (why: "age" | "population" | "region" | "school")
224
+ r.preferred; // NON-BINDING: { dancer_id, why } where the school (else the region, else recent rows) singles one out
225
+ r.initials; // an initials-only name with a school: { open, heldByDanceUp, refuted: [{ dancer_id, why, fact }], year }
225
226
  r.unlinked; // rows printing a matched name that the release linked to no dancer
226
227
 
227
228
  await rel.dancers.search({ surname: "Quellan" }); // or { like: "a* quellan" }, or { name }
@@ -271,6 +272,28 @@ for a caller to weigh, and `arm` stays as counted. A `lineage` match ranks with
271
272
  each singles out neither. The `score` orders candidates and is no
272
273
  probability.
273
274
 
275
+ AN INITIALS-ONLY NAME (`Butler, A`, every given field one letter) with a school the release
276
+ resolves is narrowed by the evidence the question itself carries, where more than one candidate
277
+ is left. A candidate is REFUTED only by a hard fact (`r.initials.refuted`, each with its `fact` in
278
+ words):
279
+
280
+ - `school`: every school row of the dancer up to the asked year reads to a school id (an id
281
+ followed through the redirects, a print read by `rel.schools`), and none of them is the asked
282
+ school or a school its line had joined by that year (lineage edges dated in or before the asked
283
+ year). A dancer with no school row, or a print no school reads, is unknown and never refuted. A
284
+ dancer that ever danced for the asked school's line is never refuted by the school.
285
+ - `age`: the birth window misses the asked band even with one band's dance-up (`DANCE_UP_YEARS`
286
+ of `@finbheara/domain/age`: a dancer may enter the band one year older than the dancer's own). A
287
+ candidate the age set aside that the dance-up holds stays open (`r.initials.heldByDanceUp`).
288
+ - `population`, `region`: the contradictions above.
289
+
290
+ `r.initials.open` is every candidate no hard fact refutes. The arm is `unique` ONLY where exactly
291
+ one candidate is open and the age did not set it aside: the candidates the school refuted then
292
+ move to `excluded` (`why: "school"`). Otherwise the arm stays `ambiguous` with every candidate,
293
+ and where the school and the region single out no one, `preferred` names the only open candidate
294
+ with a row in the asked year or the year before. A full given name is never narrowed this way: two
295
+ full given names that differ are two names, and the school only orders them.
296
+
274
297
  THE REGIONAL OIREACHTAS (`oireachtas: { region, year? }`). A dancer enters one
275
298
  residence-qualifying regional Oireachtas a year. Two regionals in one year are two dancers, the
276
299
  overseas entry excepted. The release already applies that rule to each dancer-year it publishes:
@@ -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";
@@ -609,6 +609,21 @@ class MajorsFinder2 {
609
609
  }
610
610
 
611
611
  var LINEAGE_FILE = "school-lineage.jsonl";
612
+ function schoolLine(lineage, ids, year) {
613
+ const reached = new Set(ids);
614
+ const todo = [...ids];
615
+ for (let id = todo.pop();id !== undefined; id = todo.pop())
616
+ for (const [o, y] of lineage.get(id) ?? [])
617
+ if (!reached.has(o) && (year === null || y === null || y <= year)) {
618
+ reached.add(o);
619
+ todo.push(o);
620
+ }
621
+ return reached;
622
+ }
623
+ function initialsOnly2(name) {
624
+ const g = name.givenFields();
625
+ return g.length > 0 && g.every((f) => f.length === 1);
626
+ }
612
627
  function nameOrderOf(s) {
613
628
  return s === "surname-first" || s === "given-first" || s === "unknown" ? s : undefined;
614
629
  }
@@ -811,6 +826,7 @@ function idChunks(ids) {
811
826
  class DancerResolver2 {
812
827
  rel;
813
828
  printIds = new Map;
829
+ rowIds = new Map;
814
830
  lineageOf;
815
831
  regions;
816
832
  constructor(rel) {
@@ -822,15 +838,17 @@ class DancerResolver2 {
822
838
  return null;
823
839
  await this.rel.fetch(LINEAGE_FILE);
824
840
  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);
841
+ const join = (a, b, year) => {
842
+ const m = out.get(a) ?? new Map;
843
+ const was = m.get(b);
844
+ m.set(b, was === undefined ? year : was === null || year === null ? null : Math.min(was, year));
845
+ out.set(a, m);
829
846
  };
830
847
  for (const r of readJsonl(this.rel.path(LINEAGE_FILE)))
831
848
  if (r.id !== r.other) {
832
- join(r.id, r.other);
833
- join(r.other, r.id);
849
+ const year = typeof r.year === "number" ? r.year : null;
850
+ join(r.id, r.other, year);
851
+ join(r.other, r.id, year);
834
852
  }
835
853
  return out;
836
854
  })();
@@ -952,15 +970,7 @@ class DancerResolver2 {
952
970
  school = { arm: s.arm, ids: schoolIds };
953
971
  else {
954
972
  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();
973
+ lineageIds = [...schoolLine(lineage, schoolIds, null)].filter((id) => !asked.has(id)).sort();
964
974
  school = { arm: s.arm, ids: schoolIds, lineage: lineageIds };
965
975
  }
966
976
  }
@@ -1034,6 +1044,55 @@ class DancerResolver2 {
1034
1044
  return true;
1035
1045
  return false;
1036
1046
  });
1047
+ const year = askedYear(q, asOf);
1048
+ let initials;
1049
+ if (narrow && kept.length > 1 && schoolIds !== null && schoolIds.length > 0 && initialsOnly2(dancerName(q.name, q.order === "unknown" ? undefined : q.order))) {
1050
+ const lineage = await this.lineage();
1051
+ const line = lineage === null ? new Set(schoolIds) : schoolLine(lineage, schoolIds, year);
1052
+ const band = age?.age?.birthYears() ?? null;
1053
+ const slack = q.age !== undefined && !("yob" in q.age) ? DANCE_UP_YEARS : 0;
1054
+ const open = [];
1055
+ const held = [];
1056
+ const refuted = [];
1057
+ const bySchool = new Set;
1058
+ const schoolFact = async (c) => {
1059
+ const fact = await this.schoolRefutes(schoolsOf(db, c.dancer_id), line, year, q.school);
1060
+ if (fact === null)
1061
+ return false;
1062
+ refuted.push({ dancer_id: c.dancer_id, why: "school", fact });
1063
+ bySchool.add(c.dancer_id);
1064
+ return true;
1065
+ };
1066
+ for (const c of kept)
1067
+ if (!await schoolFact(c))
1068
+ open.push(c);
1069
+ for (const x of excluded) {
1070
+ const c = x.candidate;
1071
+ const own = { from: c.birth.lo, to: c.birth.hi };
1072
+ if (x.why === "age" && band !== null && slack > 0 && birthYearsMeet(band, own, slack)) {
1073
+ if (!await schoolFact(c))
1074
+ held.push(c.dancer_id);
1075
+ continue;
1076
+ }
1077
+ refuted.push({
1078
+ dancer_id: c.dancer_id,
1079
+ why: x.why,
1080
+ fact: contradictionFact(x, age, slack)
1081
+ });
1082
+ }
1083
+ initials = {
1084
+ open: [...open.map((c) => c.dancer_id), ...held],
1085
+ heldByDanceUp: held,
1086
+ refuted,
1087
+ year
1088
+ };
1089
+ if (open.length === 1 && held.length === 0) {
1090
+ for (const c of kept)
1091
+ if (bySchool.has(c.dancer_id))
1092
+ excluded.push({ candidate: c, why: "school" });
1093
+ kept = open;
1094
+ }
1095
+ }
1037
1096
  let preferred;
1038
1097
  if (kept.length > 1 && school !== undefined)
1039
1098
  for (const rank of [SCHOOL_RANK.same, SCHOOL_RANK.moved]) {
@@ -1060,6 +1119,17 @@ class DancerResolver2 {
1060
1119
  };
1061
1120
  }
1062
1121
  }
1122
+ if (preferred === undefined && kept.length > 1 && initials !== undefined) {
1123
+ const openIds = new Set(initials.open);
1124
+ const hit = kept.filter((c) => openIds.has(c.dancer_id) && c.year_hi !== null && c.year_hi >= year - 1);
1125
+ if (hit.length === 1) {
1126
+ const c = hit[0];
1127
+ preferred = {
1128
+ dancer_id: c.dancer_id,
1129
+ why: `the only candidate no hard fact refutes with a row in ${year - 1} or ${year}`
1130
+ };
1131
+ }
1132
+ }
1063
1133
  return {
1064
1134
  tag: this.rel.tag,
1065
1135
  asked: q,
@@ -1071,7 +1141,8 @@ class DancerResolver2 {
1071
1141
  ...age !== undefined ? { age: age.answer } : {},
1072
1142
  ...population !== undefined ? { population } : {},
1073
1143
  ...oireachtas !== undefined ? { oireachtas } : {},
1074
- ...school !== undefined ? { school } : {}
1144
+ ...school !== undefined ? { school } : {},
1145
+ ...initials !== undefined ? { initials } : {}
1075
1146
  };
1076
1147
  }
1077
1148
  async readOireachtas(db, o, asOf) {
@@ -1124,6 +1195,31 @@ class DancerResolver2 {
1124
1195
  }
1125
1196
  return joined ? "lineage" : earlier ? "moved" : "none";
1126
1197
  }
1198
+ async schoolRefutes(schools, line, year, asked) {
1199
+ const rows = schools.filter((s) => s.year_lo === null || s.year_lo <= year);
1200
+ if (rows.length === 0)
1201
+ return null;
1202
+ const seen = new Set;
1203
+ for (const s of rows) {
1204
+ const own = await this.schoolIdsOf(s);
1205
+ if (own.length === 0 || own.some((id) => line.has(id)))
1206
+ return null;
1207
+ for (const id of own)
1208
+ seen.add(id);
1209
+ }
1210
+ const last = latestSchool(rows, year);
1211
+ 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}`;
1212
+ }
1213
+ async schoolIdsOf(s) {
1214
+ const key = s.school_id ?? s.print;
1215
+ let read = this.rowIds.get(key);
1216
+ if (read === undefined) {
1217
+ const got = (await this.rel.schools.resolve(key)).candidates.map((c) => c.school_id);
1218
+ read = s.school_id === null ? got : [...new Set([s.school_id, ...got])];
1219
+ this.rowIds.set(key, read);
1220
+ }
1221
+ return read;
1222
+ }
1127
1223
  readAge(a, asOf) {
1128
1224
  const done = (age, note) => {
1129
1225
  const read = age?.isAge() ? age : null;
@@ -1232,6 +1328,26 @@ function ageEvidence(d, asked) {
1232
1328
  return "overlaps";
1233
1329
  return "contradicts";
1234
1330
  }
1331
+ function askedYear(q, asOf) {
1332
+ if (asOf !== null)
1333
+ return asOf;
1334
+ const a = q.age;
1335
+ if (a !== undefined && !("yob" in a) && typeof a.year === "number")
1336
+ return a.year;
1337
+ return new Date().getFullYear();
1338
+ }
1339
+ function contradictionFact(x, age, slack) {
1340
+ const c = x.candidate;
1341
+ if (x.why === "age") {
1342
+ const w = age?.answer.birth;
1343
+ 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` : ""}`;
1344
+ }
1345
+ if (x.why === "population")
1346
+ return `published in the other population's majors solo competitions only (girls ${c.population.girls}, boys ${c.population.boys})`;
1347
+ if (x.why === "region")
1348
+ return `danced the ${c.region?.region} regional in ${c.region?.year}, another regional that year`;
1349
+ return "set aside by the school";
1350
+ }
1235
1351
 
1236
1352
  import { Age as Age2 } from "@finbheara/domain/age";
1237
1353
  import {
@@ -3413,4 +3529,4 @@ function releases2(opts = {}) {
3413
3529
  return new ReleaseManager2(opts);
3414
3530
  }
3415
3531
 
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 };
3532
+ export { DATABASES2, planAsset2, sha256File2, unzstdTo2, NAME_LEVELS2, nameLevel2, NO_CALENDAR2, FeisFinder2, entryRank2, FEIS_LOCAL_ID_FLOOR2, ResultFinder2, MajorsFinder2, initialsOnly2, nameOrderOf, DancerResolver2, readPopulation2, canonicalOf2, CompResolver2, StaffFinder2, SchoolResolver2, TeamFinder2, EventFinder2, defaultBuilders2, indexPath2, indexKey2, isCurrent2, buildIndex2, ensureIndex2, listIndexes2, readPin2, youngest2, dancerResolverOf2, RegistrationResolver2, ghRemote2, httpsRemote2, fallbackRemote2, setRemote2, columnsOf2, FEATURES2, capabilityOf2, needsRelease2, resolveTag2, Release2, openRelease2, openCached2, ReleaseManager2, releases2 };
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  nameOrderOf,
5
5
  openRelease2,
6
6
  releases2
7
- } from "./chunks/index-1sj5zr8k.js";
7
+ } from "./chunks/index-hs72zekf.js";
8
8
  import"./chunks/index-hne1wadw.js";
9
9
  import"./chunks/index-hghdy980.js";
10
10
 
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  FEIS_LOCAL_ID_FLOOR2,
13
13
  ResultFinder2,
14
14
  MajorsFinder2,
15
+ initialsOnly2,
15
16
  DancerResolver2,
16
17
  readPopulation2,
17
18
  canonicalOf2,
@@ -45,7 +46,7 @@ import {
45
46
  openCached2,
46
47
  ReleaseManager2,
47
48
  releases2
48
- } from "./chunks/index-1sj5zr8k.js";
49
+ } from "./chunks/index-hs72zekf.js";
49
50
  import {
50
51
  cacheRoot2,
51
52
  repoOf2
@@ -94,6 +95,7 @@ export {
94
95
  httpsRemote2 as httpsRemote,
95
96
  indexKey2 as indexKey,
96
97
  indexPath2 as indexPath,
98
+ initialsOnly2 as initialsOnly,
97
99
  isCurrent2 as isCurrent,
98
100
  listIndexes2 as listIndexes,
99
101
  nameLevel2 as nameLevel,
@@ -1,7 +1,9 @@
1
+ import { type PersonName } from "@finbheara/names/normalize";
1
2
  import { type NameLevel } from "./dancers-names.js";
2
3
  import type { IndexBuilder } from "./indexes.js";
3
4
  import type { Release } from "./release.js";
4
5
  import { FEIS_LOCAL_ID_FLOOR } from "./results.js";
6
+ export declare function initialsOnly(name: PersonName): boolean;
5
7
  export { FEIS_LOCAL_ID_FLOOR };
6
8
  export type NameOrderAsked = "surname-first" | "given-first" | "unknown";
7
9
  export type AgeAsked = {
@@ -80,7 +82,12 @@ export interface DancerRegion {
80
82
  }
81
83
  export interface ExcludedCandidate {
82
84
  candidate: DancerCandidate;
83
- why: "age" | "population" | "region";
85
+ why: "age" | "population" | "region" | "school";
86
+ }
87
+ export interface InitialsRefuted {
88
+ dancer_id: number;
89
+ why: "school" | "age" | "population" | "region";
90
+ fact: string;
84
91
  }
85
92
  export interface DancerAnswer {
86
93
  tag: string;
@@ -121,6 +128,12 @@ export interface DancerAnswer {
121
128
  lineage?: string[];
122
129
  };
123
130
  refused?: string;
131
+ initials?: {
132
+ open: number[];
133
+ heldByDanceUp: number[];
134
+ refuted: InitialsRefuted[];
135
+ year: number;
136
+ };
124
137
  }
125
138
  export interface DancerSearchQuery extends Omit<DancerQuery, "name"> {
126
139
  name?: string;
@@ -184,6 +197,7 @@ export interface DancerYear {
184
197
  export declare class DancerResolver {
185
198
  private readonly rel;
186
199
  private readonly printIds;
200
+ private readonly rowIds;
187
201
  private lineageOf;
188
202
  private regions;
189
203
  constructor(rel: Release);
@@ -208,6 +222,8 @@ export declare class DancerResolver {
208
222
  private answer;
209
223
  private readOireachtas;
210
224
  private schoolEvidence;
225
+ private schoolRefutes;
226
+ private schoolIdsOf;
211
227
  private readAge;
212
228
  }
213
229
  export declare function readPopulation(asked: string): NonNullable<DancerAnswer["population"]>;
@@ -1,6 +1,6 @@
1
1
  export { type AssetPlan, DATABASES, type DatabaseName, type LandedFile, type Receipt, type ReleaseManifest, planAsset, } from "./assets.js";
2
2
  export { sha256File, unzstdTo } from "./bytes.js";
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 OireachtasAsked, type PopulationEvidence, type RegionEvidence, type SchoolEvidence, readPopulation, } from "./dancers.js";
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, 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
5
  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, } from "./comps.js";
6
6
  export { type ClientOptions, cacheRoot, repoOf } from "./config.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finbheara/archive-client",
3
- "version": "0.9.0",
3
+ "version": "0.10.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",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@finbheara/domain": "0.50.0",
48
- "@finbheara/names": "0.23.0",
48
+ "@finbheara/names": "0.24.0",
49
49
  "hyparquet": "1.31.2"
50
50
  },
51
51
  "devDependencies": {