@finbheara/domain 0.54.0 → 0.56.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
@@ -44,6 +44,7 @@ ESM, Node ≥ 22 or Bun ≥ 1.4.2. MIT. Not affiliated with or endorsed by any d
44
44
  | `@finbheara/domain/clrg-regions` | `REGIONS`, `regionOf`, `resolvePhrase` |
45
45
  | `@finbheara/domain/event` | `EVENT_REGISTER`, `EVENT_REGISTER_BY_CODE`, `EVENT_TIER`, `eventTier`, `isMajorEvent`, `eventCodeOfSlug`, the `EventRegisterRow` type: the CLRG championships of tier 1 to 3 (below) |
46
46
  | `@finbheara/domain/school` | `createSchoolNameReader`, `DEFAULT_SCHOOL_NAME_READER`, `compareSchoolNames`, `foldSchoolName`, `schoolEncoder`, `schoolColumns` |
47
+ | `@finbheara/domain/school-graph` | `schoolGraph`, `FAMILY_EDGES`, `LINK_YEARS`, `ANY_PATH`, `FAMILY_PATH`, and the `SchoolLink`/`LinkPath`/`LinkPolicy` types: how two schools are linked, as candidate paths with flags (below) |
47
48
  | `@finbheara/domain/tune` | `tune()`, `tuneRelation`, `tempoLegal`, `settingAt`, `TUNE_NODES`: the CLRG set dance list, each tune with its time, speeds and bars |
48
49
  | `@finbheara/domain/ceili` | `ceili()`, `ceiliRelation`, `CEILI_NODES`: the céilí handbook dances |
49
50
  | `@finbheara/domain/dance` | `dance()`: a print read as a tune and a céilí dance, narrowed by its round |
@@ -269,6 +270,46 @@ invisible characters dropped and its whitespace collapsed; a `regex` is compiled
269
270
  flag and anchors itself. Your rows are asked before the shipped ones and the first match
270
271
  answers.
271
272
 
273
+ ## Link two schools
274
+
275
+ A school is renamed, absorbs another, merges, splits, opens a site, or is linked to another in a
276
+ way nobody has typed (`related`). Hand those edges in, and ask how two schools are linked. The
277
+ answer is every simple path between them, each with its flags, never a yes or a no:
278
+
279
+ ```ts
280
+ import { FAMILY_PATH, schoolGraph } from "@finbheara/domain/school-graph";
281
+
282
+ const g = schoolGraph({
283
+ links: [
284
+ { from: "Alpha Academy", to: "Alpha Rince", shape: "rename", year: 2010 },
285
+ { from: "Alpha Rince", to: "Beta School", shape: "merger", year: 2016, provisional: true },
286
+ { from: "Beta School", to: "Gamma Dance", shape: "related", year: null },
287
+ ],
288
+ });
289
+
290
+ const r = g.links("Alpha Academy", "Gamma Dance", { year: 2018, policy: FAMILY_PATH });
291
+ r.arm; // "linked"
292
+ r.candidates[0].hops; // 3
293
+ r.candidates[0].flags; // { relatedHops: 1, provisional: 1, maxYearGap: 8, ... }
294
+ r.policy; // { name: "family", passes: false }
295
+
296
+ g.within("Alpha Academy", FAMILY_PATH); // ["Alpha Academy", "Alpha Rince", "Beta School"]
297
+ g.line("Beta School").candidates.map((c) => c.school); // every school on its line
298
+ ```
299
+
300
+ A path's flags: `relatedHops` (steps of unknown type), `nonFamilyHops` (steps outside
301
+ `FAMILY_EDGES`), `maxYearGap` (the widest gap between a step's year and the asked year),
302
+ `datedAfter`, `undated`, `provisional`, `unbacked` (a step handed in with `decision: null`),
303
+ `contradicted` (your own check of the data, handed in), `sites` and `redirected`. Its
304
+ `confidence` is its weakest step's.
305
+
306
+ A `LinkPolicy` is your reading over those flags: a step predicate (`keep`) and a hop limit
307
+ (`maxHops`), or `anyOf` several. `passes` and `within` answer it as a reachability walk over the
308
+ steps it keeps, so the verdict is exact, and `maxPaths` (and the `tooMany` it sets) only limits
309
+ which candidate paths are listed. The shortest path, and the shortest one that passes, are always
310
+ listed. An edge never makes two schools one: `arm` is `self` only where both ids reach one
311
+ school through your `redirect`.
312
+
272
313
  ## Read a set dance
273
314
 
274
315
  `tune()` reads a spoken or printed set dance as the tunes of the official list it may name. A
@@ -80,6 +80,7 @@ class RoleRefusedName {
80
80
  suffixes = [];
81
81
  refusal;
82
82
  verdict;
83
+ corruption;
83
84
  role;
84
85
  sentinel;
85
86
  constructor(read, role, sentinel) {
@@ -87,6 +88,7 @@ class RoleRefusedName {
87
88
  this.sentinel = sentinel;
88
89
  this.nn0 = read.nn0;
89
90
  this.nn1 = read.nn1;
91
+ this.corruption = read.corruption;
90
92
  this.refusal = { rule: "placeholder", printed: read.nn0, value: NAME_REFUSED };
91
93
  this.verdict = { kind: "refused", refusal: this.refusal, rules: read.verdict.rules };
92
94
  }
@@ -1,7 +1,7 @@
1
1
  import"./chunks/place-tcgfvemw.js";
2
2
  import {
3
3
  roleName2
4
- } from "./chunks/place-bz7zr7a5.js";
4
+ } from "./chunks/place-hrhzz95q.js";
5
5
 
6
6
  import {
7
7
  NAME_COLUMNS,
package/dist/person.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  seatName2,
11
11
  dancerName2,
12
12
  teacherName2
13
- } from "./chunks/place-bz7zr7a5.js";
13
+ } from "./chunks/place-hrhzz95q.js";
14
14
  import {
15
15
  SHARED_ROW_SENTINELS2,
16
16
  sentinelIn2
@@ -0,0 +1,283 @@
1
+ var FAMILY_EDGES = [
2
+ "rename",
3
+ "absorb",
4
+ "merger",
5
+ "split",
6
+ "branch",
7
+ "site-of"
8
+ ];
9
+ var LINK_YEARS = 5;
10
+ var ANY_PATH = { name: "any-path" };
11
+ var FAMILY_PATH = { name: "family", keep: (s) => !s.flags.nonFamily };
12
+ var stepFlags = (l, year, family, site) => ({
13
+ related: l.shape === "related",
14
+ nonFamily: !family.has(l.shape),
15
+ yearGap: year === null || l.year === null ? null : Math.abs(l.year - year),
16
+ datedAfter: year !== null && l.year !== null && l.year > year,
17
+ undated: l.year === null,
18
+ provisional: l.provisional === true,
19
+ unbacked: l.decision === null,
20
+ contradicted: l.contradicted === true,
21
+ site
22
+ });
23
+ var cmp = (x, y) => x < y ? -1 : x > y ? 1 : 0;
24
+ function schoolGraph(input) {
25
+ const family = new Set(input.family ?? FAMILY_EDGES);
26
+ const all = [];
27
+ const siteLinks = new Set;
28
+ const arcs = new Map;
29
+ const add = (l, site) => {
30
+ all.push(l);
31
+ if (site)
32
+ siteLinks.add(l);
33
+ if (l.from === l.to)
34
+ return;
35
+ for (const [x, y] of [
36
+ [l.from, l.to],
37
+ [l.to, l.from]
38
+ ]) {
39
+ const list = arcs.get(x);
40
+ const arc = { next: y, link: l, site };
41
+ if (list === undefined)
42
+ arcs.set(x, [arc]);
43
+ else
44
+ list.push(arc);
45
+ }
46
+ };
47
+ for (const l of input.links)
48
+ add(l, false);
49
+ for (const [s, p] of input.sites ?? [])
50
+ add({ from: p, to: s, shape: "site-of", year: null, decision: undefined }, true);
51
+ for (const list of arcs.values())
52
+ list.sort((p, q) => cmp(p.next, q.next) || cmp(p.link.shape, q.link.shape) || (p.link.year ?? -1) - (q.link.year ?? -1));
53
+ const schools = [...arcs.keys()].sort(cmp);
54
+ const redirect = (id) => {
55
+ const seen = new Set([id]);
56
+ let at = id;
57
+ for (let n = input.redirect?.(at) ?? null;n !== null && !seen.has(n); ) {
58
+ seen.add(n);
59
+ at = n;
60
+ n = input.redirect?.(at) ?? null;
61
+ }
62
+ return at;
63
+ };
64
+ const stepOf = (at, arc, year) => ({
65
+ at,
66
+ next: arc.next,
67
+ link: arc.link,
68
+ flags: stepFlags(arc.link, year, family, arc.site)
69
+ });
70
+ const pathOf = (steps, to, redirected) => {
71
+ let maxYearGap = null;
72
+ let confidence = steps.length === 0 ? null : 1;
73
+ const f = {
74
+ relatedHops: 0,
75
+ nonFamilyHops: 0,
76
+ datedAfter: 0,
77
+ undated: 0,
78
+ provisional: 0,
79
+ unbacked: 0,
80
+ contradicted: 0,
81
+ sites: 0
82
+ };
83
+ for (const s of steps) {
84
+ const g = s.flags;
85
+ if (g.related)
86
+ f.relatedHops++;
87
+ if (g.nonFamily)
88
+ f.nonFamilyHops++;
89
+ if (g.datedAfter)
90
+ f.datedAfter++;
91
+ if (g.undated)
92
+ f.undated++;
93
+ if (g.provisional)
94
+ f.provisional++;
95
+ if (g.unbacked)
96
+ f.unbacked++;
97
+ if (g.contradicted)
98
+ f.contradicted++;
99
+ if (g.site)
100
+ f.sites++;
101
+ if (g.yearGap !== null && (maxYearGap === null || g.yearGap > maxYearGap))
102
+ maxYearGap = g.yearGap;
103
+ const c = s.link.confidence;
104
+ confidence = confidence === null || c === undefined || c === null ? null : Math.min(confidence, c);
105
+ }
106
+ return {
107
+ to,
108
+ steps,
109
+ hops: steps.length,
110
+ flags: { ...f, maxYearGap, redirected },
111
+ confidence
112
+ };
113
+ };
114
+ const pathPasses = (p, policy) => {
115
+ if ("anyOf" in policy)
116
+ return policy.anyOf.some((q) => pathPasses(p, q));
117
+ if (policy.maxHops !== undefined && p.hops > policy.maxHops)
118
+ return false;
119
+ const keep = policy.keep;
120
+ return keep === undefined || p.steps.every(keep);
121
+ };
122
+ const walk = (a, policy, year) => {
123
+ const reached = new Map([[a, []]]);
124
+ let frontier = [a];
125
+ for (let depth = 0;frontier.length > 0; depth++) {
126
+ if (policy.maxHops !== undefined && depth >= policy.maxHops)
127
+ break;
128
+ const next = [];
129
+ for (const at of frontier)
130
+ for (const arc of arcs.get(at) ?? []) {
131
+ if (reached.has(arc.next))
132
+ continue;
133
+ const s = stepOf(at, arc, year);
134
+ if (policy.keep !== undefined && !policy.keep(s))
135
+ continue;
136
+ reached.set(arc.next, [...reached.get(at), s]);
137
+ next.push(arc.next);
138
+ }
139
+ frontier = next;
140
+ }
141
+ return reached;
142
+ };
143
+ const reach = (a, policy, year) => {
144
+ if (!("anyOf" in policy))
145
+ return walk(a, policy, year);
146
+ const out = new Map;
147
+ for (const q of policy.anyOf)
148
+ for (const [s, steps] of reach(a, q, year)) {
149
+ const was = out.get(s);
150
+ if (was === undefined || steps.length < was.length)
151
+ out.set(s, steps);
152
+ }
153
+ return out;
154
+ };
155
+ const simplePaths = (a, b, year, max) => {
156
+ const paths = [];
157
+ let budget = 20000;
158
+ let more = false;
159
+ const on = new Set([a]);
160
+ const stack = [];
161
+ const go = (at) => {
162
+ for (const arc of arcs.get(at) ?? []) {
163
+ if (more)
164
+ return;
165
+ if (--budget < 0 || paths.length >= max) {
166
+ more = true;
167
+ return;
168
+ }
169
+ if (on.has(arc.next))
170
+ continue;
171
+ stack.push(stepOf(at, arc, year));
172
+ if (arc.next === b)
173
+ paths.push([...stack]);
174
+ else {
175
+ on.add(arc.next);
176
+ go(arc.next);
177
+ on.delete(arc.next);
178
+ }
179
+ stack.pop();
180
+ }
181
+ };
182
+ if (a !== b)
183
+ go(a);
184
+ return { paths, more };
185
+ };
186
+ const sameSteps = (x, y) => x.length === y.length && x.every((s, i) => s.link === y[i]?.link && s.next === y[i]?.next);
187
+ const rank = (p, q) => p.flags.contradicted - q.flags.contradicted || p.hops - q.hops || (q.confidence ?? -1) - (p.confidence ?? -1);
188
+ const candidatesTo = (a, b, year, max, redirected, policy, shortest, passing) => {
189
+ const { paths, more } = simplePaths(a, b, year, max);
190
+ for (const must of [passing, shortest])
191
+ if (must !== undefined && !paths.some((p) => sameSteps(p, must)))
192
+ paths.push(must);
193
+ const out = paths.map((steps) => {
194
+ const p = pathOf(steps, b, redirected);
195
+ return policy === undefined ? p : { ...p, passes: pathPasses(p, policy) };
196
+ });
197
+ out.sort(rank);
198
+ return { paths: out, tooMany: more };
199
+ };
200
+ const yearOf = (q) => q?.year ?? null;
201
+ return {
202
+ schools,
203
+ edges: all,
204
+ between: (a, b) => all.filter((l) => !siteLinks.has(l) && (l.from === a && l.to === b || l.from === b && l.to === a)),
205
+ passes: (a, b, policy, year = null) => reach(a, policy, year).has(b),
206
+ within: (a, policy, year = null) => [...reach(a, policy, year).keys()].sort(cmp),
207
+ links: (a0, b0, q) => {
208
+ const [a, b] = [redirect(a0), redirect(b0)];
209
+ const redirected = (a !== a0 ? 1 : 0) + (b !== b0 ? 1 : 0);
210
+ const year = yearOf(q);
211
+ const policy = q?.policy;
212
+ const verdict = policy === undefined ? undefined : { name: policy.name, passes: false };
213
+ if (a === b)
214
+ return {
215
+ asked: { a: a0, b: b0 },
216
+ a,
217
+ b,
218
+ arm: "self",
219
+ candidates: [],
220
+ tooMany: false,
221
+ ...verdict === undefined ? {} : { policy: { ...verdict, passes: true } }
222
+ };
223
+ const shortest = walk(a, ANY_PATH, year).get(b);
224
+ const passing = policy === undefined ? undefined : reach(a, policy, year).get(b);
225
+ if (verdict !== undefined)
226
+ verdict.passes = passing !== undefined;
227
+ if (shortest === undefined)
228
+ return {
229
+ asked: { a: a0, b: b0 },
230
+ a,
231
+ b,
232
+ arm: "none",
233
+ candidates: [],
234
+ tooMany: false,
235
+ ...verdict === undefined ? {} : { policy: verdict }
236
+ };
237
+ const { paths, tooMany } = candidatesTo(a, b, year, q?.maxPaths ?? 8, redirected, policy, shortest, passing);
238
+ return {
239
+ asked: { a: a0, b: b0 },
240
+ a,
241
+ b,
242
+ arm: "linked",
243
+ candidates: paths,
244
+ tooMany,
245
+ ...verdict === undefined ? {} : { policy: verdict }
246
+ };
247
+ },
248
+ line: (a0, q) => {
249
+ const a = redirect(a0);
250
+ const redirected = a !== a0 ? 1 : 0;
251
+ const year = yearOf(q);
252
+ const policy = q?.policy;
253
+ const any = walk(a, ANY_PATH, year);
254
+ const passing = policy === undefined ? undefined : reach(a, policy, year);
255
+ const candidates = [];
256
+ for (const [s, shortest] of any) {
257
+ if (s === a)
258
+ continue;
259
+ const { paths, tooMany } = candidatesTo(a, s, year, q?.maxPaths ?? 8, redirected, policy, shortest, passing?.get(s));
260
+ candidates.push({
261
+ school: s,
262
+ paths,
263
+ tooMany,
264
+ ...passing === undefined ? {} : { passes: passing.has(s) }
265
+ });
266
+ }
267
+ candidates.sort((x, y) => Number(y.passes ?? false) - Number(x.passes ?? false) || (x.paths[0]?.hops ?? 0) - (y.paths[0]?.hops ?? 0) || cmp(x.school, y.school));
268
+ return {
269
+ asked: a0,
270
+ school: a,
271
+ candidates,
272
+ ...policy === undefined ? {} : { policy: { name: policy.name } }
273
+ };
274
+ }
275
+ };
276
+ }
277
+ export {
278
+ ANY_PATH,
279
+ FAMILY_EDGES,
280
+ FAMILY_PATH,
281
+ LINK_YEARS,
282
+ schoolGraph
283
+ };
@@ -0,0 +1,111 @@
1
+ export type LinkShape = "rename" | "absorb" | "merger" | "split" | "branch" | "related" | "site-of" | (string & {});
2
+ export declare const FAMILY_EDGES: readonly LinkShape[];
3
+ export declare const LINK_YEARS = 5;
4
+ export interface SchoolLink {
5
+ from: string;
6
+ to: string;
7
+ shape: LinkShape;
8
+ year: number | null;
9
+ provisional?: boolean;
10
+ confidence?: number | null;
11
+ decision?: string | null;
12
+ contradicted?: boolean;
13
+ }
14
+ export interface StepFlags {
15
+ related: boolean;
16
+ nonFamily: boolean;
17
+ yearGap: number | null;
18
+ datedAfter: boolean;
19
+ undated: boolean;
20
+ provisional: boolean;
21
+ unbacked: boolean;
22
+ contradicted: boolean;
23
+ site: boolean;
24
+ }
25
+ export interface LinkStep {
26
+ at: string;
27
+ next: string;
28
+ link: SchoolLink;
29
+ flags: StepFlags;
30
+ }
31
+ export interface PathFlags {
32
+ relatedHops: number;
33
+ nonFamilyHops: number;
34
+ maxYearGap: number | null;
35
+ datedAfter: number;
36
+ undated: number;
37
+ provisional: number;
38
+ unbacked: number;
39
+ contradicted: number;
40
+ sites: number;
41
+ redirected: number;
42
+ }
43
+ export interface LinkPath {
44
+ to: string;
45
+ steps: LinkStep[];
46
+ hops: number;
47
+ flags: PathFlags;
48
+ confidence: number | null;
49
+ passes?: boolean;
50
+ }
51
+ export type LinkPolicy = StepPolicy | {
52
+ name: string;
53
+ anyOf: readonly LinkPolicy[];
54
+ };
55
+ export interface StepPolicy {
56
+ name: string;
57
+ keep?: (step: LinkStep) => boolean;
58
+ maxHops?: number;
59
+ }
60
+ export interface LinkQuery {
61
+ year?: number | null;
62
+ policy?: LinkPolicy;
63
+ maxPaths?: number;
64
+ }
65
+ export interface LinkAnswer {
66
+ asked: {
67
+ a: string;
68
+ b: string;
69
+ };
70
+ a: string;
71
+ b: string;
72
+ arm: "self" | "linked" | "none";
73
+ candidates: LinkPath[];
74
+ tooMany: boolean;
75
+ policy?: {
76
+ name: string;
77
+ passes: boolean;
78
+ };
79
+ }
80
+ export interface LineCandidate {
81
+ school: string;
82
+ paths: LinkPath[];
83
+ tooMany: boolean;
84
+ passes?: boolean;
85
+ }
86
+ export interface LineAnswer {
87
+ asked: string;
88
+ school: string;
89
+ candidates: LineCandidate[];
90
+ policy?: {
91
+ name: string;
92
+ };
93
+ }
94
+ export interface SchoolGraphInput {
95
+ links: Iterable<SchoolLink>;
96
+ sites?: Iterable<readonly [site: string, school: string]>;
97
+ redirect?: (id: string) => string | null;
98
+ family?: readonly LinkShape[];
99
+ }
100
+ export interface SchoolGraph {
101
+ readonly schools: readonly string[];
102
+ readonly edges: readonly SchoolLink[];
103
+ between(a: string, b: string): SchoolLink[];
104
+ links(a: string, b: string, q?: LinkQuery): LinkAnswer;
105
+ line(a: string, q?: LinkQuery): LineAnswer;
106
+ passes(a: string, b: string, policy: LinkPolicy, year?: number | null): boolean;
107
+ within(a: string, policy: LinkPolicy, year?: number | null): string[];
108
+ }
109
+ export declare const ANY_PATH: StepPolicy;
110
+ export declare const FAMILY_PATH: StepPolicy;
111
+ export declare function schoolGraph(input: SchoolGraphInput): SchoolGraph;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@finbheara/domain",
3
- "version": "0.54.0",
4
- "description": "Places, CLRG regions and events, school names, ages, dances, competition titles, codes and schedules, and a person's print in its role, as they are printed: read, compared and told apart",
3
+ "version": "0.56.0",
4
+ "description": "Places, CLRG regions and events, school names and the links between schools, ages, dances, competition titles, codes and schedules, and a person's print in its role, as they are printed: read, compared and told apart",
5
5
  "keywords": [
6
6
  "places",
7
7
  "regions",
@@ -81,6 +81,10 @@
81
81
  "types": "./dist/types/event.d.ts",
82
82
  "default": "./dist/event.js"
83
83
  },
84
+ "./school-graph": {
85
+ "types": "./dist/types/school-graph.d.ts",
86
+ "default": "./dist/school-graph.js"
87
+ },
84
88
  "./package.json": "./package.json"
85
89
  },
86
90
  "files": ["dist/*.js", "dist/chunks/*.js", "dist/types/**/*.d.ts", "README.md", "LICENSE"],
@@ -97,7 +101,7 @@
97
101
  "prepublishOnly": "bun scripts/build.ts && bun scripts/guard-tarball.ts"
98
102
  },
99
103
  "dependencies": {
100
- "@finbheara/names": "0.26.0"
104
+ "@finbheara/names": "0.28.0"
101
105
  },
102
106
  "devDependencies": {
103
107
  "typescript": "7.0.2"