ガイド 第34

リファラル & ファクション

tier/perk・descendant counting

@event/networking

referral.ts は3系統の純関数だけで構成される: ティア/特典の解決(設定注入)・RFC-4180 CSV パーサカウント順位付け。しきい値も特典ラベルも呼び出し側が注入し、guild 固有のラベル・Google Sheet URL・4S live 取得・ロゴ名寄せは移植していない(スコープ外)。「ファクション(faction)」の木構造そのものは referral.ts ではなく createRegistrySchema の org エッジが持つ — 本章はその境界も正直に示す。

ティア / 特典 — 設定注入

TierDef{ id, min, label? }PerkDef{ min, label }resolveTier は配列を先頭一致で走査するので、tiers は min 降順で渡す(この並び順が契約)。null / 非有限 / 全しきい値未満は null を返す。

import {
  resolveTier, resolvePerks,
  type TierDef, type PerkDef,
} from "@event/networking";

// しきい値・ラベルは呼び出し側の定義(値はイベント側で自由に決める)
const tiers: TierDef[] = [
  { id: "platinum", min: 20, label: "Platinum" },
  { id: "gold",     min: 10, label: "Gold" },
  { id: "silver",   min: 5,  label: "Silver" },
];
const perks: PerkDef[] = [
  { min: 5,  label: "団体ロゴ掲載" },
  { min: 20, label: "基調講演枠" },
];

resolveTier(12, tiers);   // → "gold"(先頭一致 = min 降順が前提)
resolveTier(3, tiers);    // → null(最小しきい値未満)
resolvePerks(12, perks);  // → ["団体ロゴ掲載"](min 昇順/降順どちらでも安定)

resolvePerks は解放済みラベルを配列で返すだけなので、tiers と違い並び順に非依存(内部で filter するため)。ティアと特典は独立関数で、同じ count を両方に食わせて使う。

CSV 取込 — parseCsv + parseCount

外部シートからカウントを取り込む経路。parseCsv(text) は quote 対応の最小 RFC-4180 実装:

  • " で囲んだフィールド内の , \n はデータ扱い
  • フィールド内 "" は 1 個の " にアンエスケープ
  • 行区切りは \n(先行 \r は無視 = CRLF/LF 両対応)
  • 末尾に改行が無くても最終行を取りこぼさない

parseCount(raw) は全角数字→半角に寄せて整数化し、空欄・非数値(計測不可 等)は null(0 ではない)。rankByCountnullsLast と組で「未計測は末尾」を実現する。

import { parseCsv, parseCount } from "@event/networking";

const rows = parseCsv('org,referrals\n"Acme, Inc.",12\n学生団体X,\n');
// → [["org","referrals"], ["Acme, Inc.","12"], ["学生団体X",""]]
parseCount(rows[2]?.[1]);   // → 12(全角→半角)
parseCount(rows[3]?.[1]);   // → null(空欄 = 計測不可)

カウント順位付け と descendant counting

rankByCount(rows, getCount, opts)count 降順で並べて全体順位 rank(1始まり)を各行に付す。nullsLast(既定 true)で未計測(null)を末尾に送る。この関数は count でしか並べ替えない — tier が付いた行だけを対象にしたい場合の絞り込みは呼び出し側で事前 filter する。

import { rankByCount, resolveTier } from "@event/networking";

const ranked = rankByCount(
  [{ org: "A", n: 20 }, { org: "B", n: null }, { org: "C", n: 8 }],
  (r) => r.n,
);
// → [{org:"A",n:20,rank:1}, {org:"C",n:8,rank:2}, {org:"B",n:null,rank:3}]
ranked.map((r) => ({ ...r, tier: resolveTier(r.n, tiers) }));

「descendant counting(子孫カウント)」の実体は正直に区別する。 referral.tscount という数値を受け取って tier/perk/rank に落とすだけで、木を辿って子孫数を数える処理は持たない。ファクションの木構造(親子・始祖エッジ)は createRegistrySchema の org 側が保持する:

エッジカラム意味
始祖/代表org.founderSlug木の根となる外部識別子(NON-unique)
親エンティティorg.parentOrgId支部の自己参照(faction の枝)
統合先org.mergedIntoId重複エンティティのマージ
在籍tenure(org×person×year)人数カウントの供給元

つまり descendant の集計そのもの(parentOrgId の再帰 walk や tenure の person 数え上げ)は本パッケージ未実装 / 呼び出し側の責務である。networking が提供するのは (1) 集計に使えるエッジ列(registry)、(2) 外部シートからの取込(parseCsv/parseCount)、(3) 得た count の順位付け(rankByCount)、(4) tier/perk への写像(resolveTier/resolvePerks)まで。木の走査ロジックを足すなら、count を組み立ててからこの4段に流し込む形になる。

配線(純関数・Neon 不要)

一連は「CSV or tenure 集計 → parseCountrankByCountresolveTier / resolvePerks」の一方向パイプ。すべて副作用ゼロの純関数なので Neon 未接続の dev でも即動く。descendant の木 walk と 4S live 取得だけが上位レイヤ / 別パッケージに残る。