@finbheara/archive-client 0.15.1 → 0.16.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
@@ -184,8 +184,8 @@ s.next; // the next scheduled instance where the release knows one, else n
184
184
  ```
185
185
 
186
186
  A series is asked by any id of it (a feis, instance or brand id, a retired one followed), by a
187
- host code with or without its year (held codes `<stem><yy>` whose two digits are their own year),
188
- or by a name (a glob, or a plain word read as `*word*`). Two series answer `ambiguous` with both.
187
+ host code with or without its year (held codes `<stem><yy>` whose two digits are their own year,
188
+ never a host id of digits alone, which is a sequence number), or by a name (a glob, or a plain word read as `*word*`). Two series answer `ambiguous` with both.
189
189
  A release holds a feis-year once its results are captured and carries no calendar, so `next` is
190
190
  `null` today, with the reason.
191
191
 
@@ -212,6 +212,26 @@ rows were published under and their rows, so it agrees with each print lookup of
212
212
 
213
213
  Every answer carries the release tag it came from.
214
214
 
215
+ ### How two schools are linked
216
+
217
+ ```ts
218
+ const l = await rel.schools.links("ostrel-academy", "wyvern-academy", { year: 2026 });
219
+ l.arm; // "self" | "linked" | "none"
220
+ l.candidates; // [{ hops, steps: [{ at, next, link: { from, to, shape, year }, flags }], flags, confidence }]
221
+
222
+ const line = await rel.schools.line("ostrel-academy");
223
+ line.candidates; // every school on its line, nearest first, each with its paths
224
+ ```
225
+
226
+ The release's school lineage (a rename, an absorption, a merger, a split, or a `related` link of
227
+ unknown type, each dated where a year is known) read by `@finbheara/domain/school-graph`: every
228
+ simple path between two schools, each with its flags (`relatedHops`, `maxYearGap`, `datedAfter`,
229
+ `undated`, `provisional`, `redirected` and the rest), never a yes or a no. Pass a `policy`, your
230
+ reading over those flags, and the answer carries its verdict (`l.policy.passes`), exact however
231
+ many paths are listed. A retired id is followed to the live one. The release publishes no decision
232
+ behind an edge, so a path carries no `confidence` here. A release that publishes no lineage
233
+ refuses both calls by name.
234
+
215
235
  ## Dancers
216
236
 
217
237
  ```ts
@@ -265,7 +285,9 @@ joins: every school reached through any number of edges, in either direction (th
265
285
  split are joined through their predecessor, the schools of a merger through their successor). An
266
286
  earlier row at a joined school alone is no match. `r.school.lineage` lists the ids joined to the
267
287
  asked ones. A release that publishes no lineage (feature `schools.lineage`) joins
268
- nothing and its answer carries no `lineage`, never refused.
288
+ nothing and its answer carries no `lineage`, never refused. The line is read on the same school
289
+ graph as `rel.schools.line` (`@finbheara/domain/school-graph`): untimed, every path, and as of a
290
+ year, every path none of whose steps is dated after it (`LINE_AS_OF`).
269
291
 
270
292
  `population` is how the dancer's published majors population meets the asked one: `same`,
271
293
  `contradicts`, `both`, `unknown`, `unasked`. A dancer's population is read only from its rows at
@@ -562,7 +584,8 @@ held feis-year is still read from that feis-year's own competitions.
562
584
  A feis the release holds names itself, its year and its series. A series instance or brand id
563
585
  names the series. A feis id the release does not hold is placed in a series by its host code
564
586
  (`<token>:<stem><yy>` where the held codes of that stem carry their own year), and a stem two
565
- series share answers `ambiguous`. A held feis-year never falls back to an earlier year, and two
587
+ series share answers `ambiguous`. A host id of digits alone (`qf:926`, `fw:1001`) is a sequence
588
+ number and carries no stem: unheld, it is placed nowhere. A held feis-year never falls back to an earlier year, and two
566
589
  competitions printing one code are never folded into one.
567
590
 
568
591
  `kind` and `level` are read off the release's published rung (`feis_level`: 100 first feis to
@@ -71,6 +71,7 @@ import { Age, DANCE_UP_YEARS, birthYearsMeet } from "@finbheara/domain/age";
71
71
  import { createIndex as createIndex2, indexSchema as indexSchema2, personEncoder } from "@finbheara/domain/noun-index";
72
72
  import domainPkg from "@finbheara/domain/package.json";
73
73
  import { dancerName } from "@finbheara/domain/person";
74
+ import { schoolGraph as schoolGraph2 } from "@finbheara/domain/school-graph";
74
75
  import {
75
76
  compTitle,
76
77
  populationIn,
@@ -244,6 +245,8 @@ class FeisFinder2 {
244
245
  return { tag: this.rel.tag, feises, ...redirected !== undefined ? { redirected } : {} };
245
246
  }
246
247
  async codeStem(token, stem) {
248
+ if (/^\d+$/.test(stem))
249
+ return [];
247
250
  const db = await this.rel.indexDb("feises");
248
251
  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}`)}__`);
249
252
  const fits = like.filter((f) => {
@@ -632,6 +635,40 @@ class MajorsFinder2 {
632
635
  }
633
636
  }
634
637
 
638
+ import {
639
+ schoolGraph
640
+ } from "@finbheara/domain/school-graph";
641
+ var LINEAGE_FILE2 = "school-lineage.jsonl";
642
+ var REDIRECTS_FILE = "school-redirects.jsonl";
643
+ function linksOfLineage2(lines) {
644
+ const seen = new Map;
645
+ for (const r of lines) {
646
+ if (r.id === r.other)
647
+ continue;
648
+ const [from, to] = r.direction === "succeeds" ? [r.other, r.id] : [r.id, r.other];
649
+ const year = typeof r.year === "number" ? r.year : null;
650
+ const k = `${from}\x00${to}\x00${r.shape}\x00${year}`;
651
+ const was = seen.get(k);
652
+ if (was === undefined)
653
+ seen.set(k, { from, to, shape: r.shape, year, provisional: r.provisional === true });
654
+ else if (r.provisional === true)
655
+ was.provisional = true;
656
+ }
657
+ return [...seen.values()];
658
+ }
659
+ async function releaseSchoolGraph2(rel) {
660
+ await rel.require("schools.lineage");
661
+ await rel.fetch(LINEAGE_FILE2);
662
+ const links = linksOfLineage2(readJsonl(rel.path(LINEAGE_FILE2)));
663
+ const redirects = new Map;
664
+ if (await rel.publishes(REDIRECTS_FILE)) {
665
+ await rel.fetch(REDIRECTS_FILE);
666
+ for (const r of readJsonl(rel.path(REDIRECTS_FILE)))
667
+ redirects.set(r.old_id, r.new_id);
668
+ }
669
+ return schoolGraph({ links, redirect: (id) => redirects.get(id) ?? null });
670
+ }
671
+
635
672
  import { createIndex, indexSchema } from "@finbheara/domain/noun-index";
636
673
  import {
637
674
  DEFAULT_SCHOOL_NAME_READER,
@@ -697,9 +734,18 @@ var schoolsIndex = {
697
734
 
698
735
  class SchoolResolver2 {
699
736
  rel;
737
+ graph;
700
738
  constructor(rel) {
701
739
  this.rel = rel;
702
740
  }
741
+ async links(a, b, q = {}) {
742
+ this.graph ??= releaseSchoolGraph2(this.rel);
743
+ return { tag: this.rel.tag, ...(await this.graph).links(a.trim(), b.trim(), q) };
744
+ }
745
+ async line(a, q = {}) {
746
+ this.graph ??= releaseSchoolGraph2(this.rel);
747
+ return { tag: this.rel.tag, ...(await this.graph).line(a.trim(), q) };
748
+ }
703
749
  async resolve(asked, q = {}) {
704
750
  const db = await this.rel.indexDb("schools");
705
751
  const reg = db.query("SELECT display, regions, places, spanish FROM registry WHERE school_id = ?");
@@ -815,16 +861,12 @@ class SchoolResolver2 {
815
861
  }
816
862
  }
817
863
 
818
- var LINEAGE_FILE = "school-lineage.jsonl";
864
+ var LINE_AS_OF = { name: "line-as-of", keep: (s) => !s.flags.datedAfter };
819
865
  function schoolLine(lineage, ids, year) {
820
866
  const reached = new Set(ids);
821
- const todo = [...ids];
822
- for (let id = todo.pop();id !== undefined; id = todo.pop())
823
- for (const [o, y] of lineage.get(id) ?? [])
824
- if (!reached.has(o) && (year === null || y === null || y <= year)) {
825
- reached.add(o);
826
- todo.push(o);
827
- }
867
+ for (const id of ids)
868
+ for (const x of lineage.within(id, LINE_AS_OF, year))
869
+ reached.add(x);
828
870
  return reached;
829
871
  }
830
872
  function initialsOnly2(name) {
@@ -1051,21 +1093,10 @@ class DancerResolver2 {
1051
1093
  this.lineageOf ??= (async () => {
1052
1094
  if ((await this.rel.capability("schools.lineage")).state !== "full")
1053
1095
  return null;
1054
- await this.rel.fetch(LINEAGE_FILE);
1055
- const out = new Map;
1056
- const join = (a, b, year) => {
1057
- const m = out.get(a) ?? new Map;
1058
- const was = m.get(b);
1059
- m.set(b, was === undefined ? year : was === null || year === null ? null : Math.min(was, year));
1060
- out.set(a, m);
1061
- };
1062
- for (const r of readJsonl(this.rel.path(LINEAGE_FILE)))
1063
- if (r.id !== r.other) {
1064
- const year = typeof r.year === "number" ? r.year : null;
1065
- join(r.id, r.other, year);
1066
- join(r.other, r.id, year);
1067
- }
1068
- return out;
1096
+ await this.rel.fetch(LINEAGE_FILE2);
1097
+ return schoolGraph2({
1098
+ links: linksOfLineage2(readJsonl(this.rel.path(LINEAGE_FILE2)))
1099
+ });
1069
1100
  })();
1070
1101
  return this.lineageOf;
1071
1102
  }
@@ -1888,7 +1919,7 @@ class CompResolver2 {
1888
1919
  return this.ofSeries(asked, only, y, "series");
1889
1920
  }
1890
1921
  const m = /^([^:]+):(.*?)(\d{2})$/.exec(asked);
1891
- if (m !== null) {
1922
+ if (m !== null && !/^\d+$/.test(`${m[2]}${m[3]}`)) {
1892
1923
  const [, token, stem, yy] = m;
1893
1924
  const brands = await this.rel.feises.codeStem(token, stem);
1894
1925
  const y = year ?? 2000 + Number(yy);
@@ -3069,7 +3100,7 @@ var FEATURES2 = [
3069
3100
  feature: "schools.lineage",
3070
3101
  needs: ["school-lineage.jsonl"],
3071
3102
  since: 184,
3072
- calls: "the `lineage` school evidence of dancers.resolve and dancers.search: no school is joined by lineage, never refused, on a release without it"
3103
+ 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"
3073
3104
  },
3074
3105
  {
3075
3106
  feature: "results.stream",
@@ -3783,4 +3814,4 @@ function releases2(opts = {}) {
3783
3814
  return new ReleaseManager2(opts);
3784
3815
  }
3785
3816
 
3786
- export { DATABASES2, planAsset2, sha256File2, unzstdTo2, NAME_LEVELS2, nameLevel2, NO_CALENDAR2, FeisFinder2, entryRank2, FEIS_LOCAL_ID_FLOOR2, ResultFinder2, MajorsFinder2, readsSpanishNames2, SchoolResolver2, initialsOnly2, nameOrderOf, DancerResolver2, readPopulation2, 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 };
3817
+ 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, 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-9r4rw6xq.js";
7
+ } from "./chunks/index-hbb1dnnh.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,9 @@ import {
12
12
  FEIS_LOCAL_ID_FLOOR2,
13
13
  ResultFinder2,
14
14
  MajorsFinder2,
15
+ LINEAGE_FILE2,
16
+ linksOfLineage2,
17
+ releaseSchoolGraph2,
15
18
  readsSpanishNames2,
16
19
  SchoolResolver2,
17
20
  initialsOnly2,
@@ -47,7 +50,7 @@ import {
47
50
  openCached2,
48
51
  ReleaseManager2,
49
52
  releases2
50
- } from "./chunks/index-9r4rw6xq.js";
53
+ } from "./chunks/index-hbb1dnnh.js";
51
54
  import {
52
55
  cacheRoot2,
53
56
  repoOf2
@@ -70,6 +73,7 @@ export {
70
73
  FEATURES2 as FEATURES,
71
74
  FEIS_LOCAL_ID_FLOOR2 as FEIS_LOCAL_ID_FLOOR,
72
75
  FeisFinder2 as FeisFinder,
76
+ LINEAGE_FILE2 as LINEAGE_FILE,
73
77
  MajorsFinder2 as MajorsFinder,
74
78
  NAME_LEVELS2 as NAME_LEVELS,
75
79
  NO_CALENDAR2 as NO_CALENDAR,
@@ -98,6 +102,7 @@ export {
98
102
  indexPath2 as indexPath,
99
103
  initialsOnly2 as initialsOnly,
100
104
  isCurrent2 as isCurrent,
105
+ linksOfLineage2 as linksOfLineage,
101
106
  listIndexes2 as listIndexes,
102
107
  nameLevel2 as nameLevel,
103
108
  needsRelease2 as needsRelease,
@@ -110,6 +115,7 @@ export {
110
115
  readPin2 as readPin,
111
116
  readPopulation2 as readPopulation,
112
117
  readsSpanishNames2 as readsSpanishNames,
118
+ releaseSchoolGraph2 as releaseSchoolGraph,
113
119
  releases2 as releases,
114
120
  renderArchiveTag2 as renderArchiveTag,
115
121
  repoOf2 as repoOf,
@@ -1,8 +1,12 @@
1
+ import { type SchoolGraph, type StepPolicy } from "@finbheara/domain/school-graph";
1
2
  import { type PersonName } from "@finbheara/names/normalize";
2
3
  import { type NameLevel } from "./dancers-names.js";
3
4
  import type { IndexBuilder } from "./indexes.js";
4
5
  import type { Release } from "./release.js";
5
6
  import { FEIS_LOCAL_ID_FLOOR } from "./results.js";
7
+ export type Lineage = SchoolGraph;
8
+ export declare const LINE_AS_OF: StepPolicy;
9
+ export declare function schoolLine(lineage: Lineage, ids: readonly string[], year: number | null): Set<string>;
6
10
  export declare function initialsOnly(name: PersonName): boolean;
7
11
  export { FEIS_LOCAL_ID_FLOOR };
8
12
  export type NameOrderAsked = "surname-first" | "given-first" | "unknown";
@@ -14,6 +14,8 @@ export { type OpenOptions, Release, openCached, openRelease, resolveTag, } from
14
14
  export { type MajorCompetition, type MajorEvent, MajorsFinder, type ResultAnswer, ResultFinder, type ResultQuery, type ResultRow, } from "./results.js";
15
15
  export { type DancerAnswerLike, type DancerQueryLike, type DancerResolverLike, type DancerUnavailable, type RegistrationRow, type ResolvedDancer, RegistrationResolver, type ResolvedRegistration, dancerResolverOf, youngest, } from "./registrations.js";
16
16
  export { type SchoolAnswer, type SchoolCandidate, type SchoolQuery, SchoolResolver, type SchoolRung, readsSpanishNames, type SpanishNames, } from "./schools.js";
17
+ export { LINEAGE_FILE, type SchoolLineAnswer, type SchoolLinksAnswer, linksOfLineage, releaseSchoolGraph, } from "./school-links.js";
18
+ export type { LineCandidate, LinkPath, LinkPolicy, LinkQuery, LinkStep, PathFlags, SchoolLink, StepFlags, StepPolicy, } from "@finbheara/domain/school-graph";
17
19
  export { type Capability, FEATURES, type Feature, type SchemaProbe, capabilityOf, columnsOf, needsRelease, } from "./schema.js";
18
20
  export { type EntryRow, type StreamQuery, type StreamRow, type StreamScope, entryRank, } from "./stream.js";
19
21
  export { EventFinder, type EventYear, type EventYearQuery, type ResultRoster, type RosterMember, type RosterTeam, TeamFinder, type TeamResult, } from "./teams.js";
@@ -0,0 +1,20 @@
1
+ import { type LineAnswer, type LinkAnswer, type LinkQuery, type SchoolGraph, type SchoolLink } from "@finbheara/domain/school-graph";
2
+ import type { Release } from "./release.js";
3
+ export declare const LINEAGE_FILE = "school-lineage.jsonl";
4
+ export interface LineageLine {
5
+ id: string;
6
+ direction: "precedes" | "succeeds";
7
+ other: string;
8
+ shape: string;
9
+ year?: number | null;
10
+ provisional?: boolean;
11
+ }
12
+ export declare function linksOfLineage(lines: Iterable<LineageLine>): SchoolLink[];
13
+ export declare function releaseSchoolGraph(rel: Release): Promise<SchoolGraph>;
14
+ export type SchoolLinksAnswer = LinkAnswer & {
15
+ tag: string;
16
+ };
17
+ export type SchoolLineAnswer = LineAnswer & {
18
+ tag: string;
19
+ };
20
+ export type { LinkQuery };
@@ -1,6 +1,7 @@
1
1
  import { type SchoolNameParser } from "@finbheara/domain/school";
2
2
  import type { IndexBuilder } from "./indexes.js";
3
3
  import type { Release } from "./release.js";
4
+ import { type LinkQuery, type SchoolLineAnswer, type SchoolLinksAnswer } from "./school-links.js";
4
5
  export type SchoolRung = "id" | "redirect" | "print" | "sn3" | "realWords";
5
6
  export interface SchoolCandidate {
6
7
  school_id: string;
@@ -52,6 +53,9 @@ export declare const GENERIC_SCHOOL_READER: SchoolReaderChoice;
52
53
  export declare const schoolsIndex: IndexBuilder;
53
54
  export declare class SchoolResolver {
54
55
  private readonly rel;
56
+ private graph;
55
57
  constructor(rel: Release);
58
+ links(a: string, b: string, q?: LinkQuery): Promise<SchoolLinksAnswer>;
59
+ line(a: string, q?: LinkQuery): Promise<SchoolLineAnswer>;
56
60
  resolve(asked: string, q?: SchoolQuery): Promise<SchoolAnswer>;
57
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finbheara/archive-client",
3
- "version": "0.15.1",
3
+ "version": "0.16.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.54.1",
48
- "@finbheara/names": "0.27.1",
47
+ "@finbheara/domain": "0.56.0",
48
+ "@finbheara/names": "0.28.0",
49
49
  "hyparquet": "1.31.2"
50
50
  },
51
51
  "devDependencies": {