第32章では privacy.ts の三層防御を地図として示した。本章はその内側 — 同意ゲーティングの正確な意味論・公開射影の物理ガード・k匿名の出口関数・DSR/audit の model プリミティブ を、実際に呼べるコードで掘る。すべて @event/networking の純関数で、I/O も UI も持たない。名鑑は PII の塊なので「1層でも通れば漏れる」構造にしないのが設計思想である。
同意ゲーティング — fail-closed の順序
ConsentScope は名鑑専用同意の語彙を3値に固定する(DB の scope は自由 text だが、誤記防止に定数を使う):
| 定数 | 値 | 意味 |
|---|---|---|
ConsentScope.LIST_WITH_NAME | "list_with_name" | 氏名つきで公開カードに載る |
ConsentScope.SHOW_COHORT | "show_cohort" | 期(cohort)等の追加属性を出す |
ConsentScope.CONTACT | "contact" | 連絡を受け取る |
判定は2段。まず isEligibleForListing が subject 自体の掲載床を fail-closed で見る — suppressed / minor(ADULT_AGE=18 未満)/ matchMethod==="name_norm"(氏名だけの低確度マッチ)のどれか1つでも真なら、いかなる同意があっても false。その上に scope 別の許可が乗る。
import {
ConsentScope, hasGrantedConsent, isEligibleForListing,
canListWithName, evaluateListingGrants,
type RegistrySubject, type ConsentRecord,
} from "@event/networking";
const subject: RegistrySubject = {
slug: "yamada-taro", publishState: "public",
suppressed: false, minor: false, matchMethod: "slug",
};
const consents: ConsentRecord[] = [{ granted: true, scope: "list_with_name" }];
isEligibleForListing(subject); // true(床を通過)
canListWithName(subject, consents); // true(床 ∧ not hidden ∧ list_with_name 同意)
hasGrantedConsent(consents, scope) の意味論は厳密だ: granted===true かつ scope 一致の行が1つでもあれば true(some)。ただし scope が null/空文字の行は包括同意として全 scope を許可する。CSV 由来のような1行同意にも効く。
canListWithName は床に加えて hidden でないこと(本人が明示非公開を選んでいれば同意があっても載せない)を見る。canContact は subject に持たない contactPref(PII 最小のため呼び出し側から渡す・未指定は none 扱いで fail-closed)が none 以外であることを追加要求する。evaluateListingGrants は person×consent を1度だけ評価して ListingGrants { listWithName, contact, showCohort } にまとめる — snapshot 生成側はこれを呼べば全許可が揃う。
公開射影 — 同意とは独立した第二の物理ガード
gating を通っても、publishState が public でなければ出さない。isSubjectPublishable が状態フラグのみで(同意は見ない)publishState==="public" ∧ not suppressed ∧ not minor ∧ not name_norm を確認する。これは canListWithName と独立した多層防御で、consent 層のバグが射影層をすり抜けない。
sanitizeSubjectsForPublic(subjects, project) の肝は ...spread を使わないことだ。project 関数が返す新オブジェクトに公開フィールドだけを cherry-pick するので、入力行に連絡先や審査メモが紛れていても出力に物理的に存在し得ない。射影不可(publishable=false か slug が null)は null に落として除外する。
import { sanitizeSubjectsForPublic } from "@event/networking";
const cards = sanitizeSubjectsForPublic(people, (p) => ({
slug: p.slug, // project 内では slug 非 null が保証される
cohortEnroll: p.cohortEnroll,
})); // 連絡先・matchMethod 等は出力に一切現れない
jsonb など緩い辞書は pickKeys(src, allowlist) / sanitizeMeta(meta, allowlist) でキー allowlist に絞る。どちらも allowlist 外は物理的に落とす。
k匿名 — 集計出口(K_MIN=5)
person_id/slug を一切載せない集計セル(CountCell = { count; [k]: unknown })の出口で、実数の小セルを秘匿する。isSafeCount が土台で、count が有限かつ >= K_MIN(5) のときだけ true(負値・非整数・0 件は安全側で false = 存在を漏らさない)。
| 関数 | 挙動 | 用途 |
|---|---|---|
applyKAnonymity(cells) | count>=5 の行だけ残す(他は落とす) | 「出してよい行だけ出す」ランキング/内訳 |
maskKAnonymity(cells) | 行は残し count を null に潰す(suppressed:true を付す) | 全カテゴリ列挙・小セルの実数だけ伏せる |
safeScalar(count) | 単一スカラ。安全なら数値、危険なら null | 「あるセグメント総数=N件」 |
applyComplementSafe(cells) | 補集合漏れガード(下記) | 総数と内訳を同時公開する時 |
import { applyKAnonymity, safeScalar, applyComplementSafe } from "@event/networking";
applyKAnonymity([{ region: "東京", count: 12 }, { region: "鳥取", count: 3 }]);
// → [{ region: "東京", count: 12 }] ※ 3件セルは落ちる
safeScalar(4); // → null(<5)
applyComplementSafe は「総数 − 内訳の和」で1セルが復元される事故を防ぐ。秘匿セルがちょうど1個だけ残ると差分で逆算できるので、残った中の最小 count セルをもう1つ落とす(2個以上秘匿されれば個別復元は不能)。総数側は呼び元が safeScalar で別途処理する前提。
DSR / audit — model プリミティブに限定(正直な範囲)
@event/networking は DSR のリクエスト処理フローや専用 audit_log テーブルを実装しない。 提供するのは createRegistrySchema のデータモデル層プリミティブのみである:
- 抑止 / 消去:
person.suppressed/org.suppressed(default false)。true にすれば gating も射影も fail-closed で弾く — これが opt-out / 忘れられる権利の受け皿。物理削除ではなくフラグ。 - 同意の証跡:
consentテーブルがgranted(default false — CSV/外部由来は false で投入し、昇格してから掲載)・scope・source(csv_import/self_register/notify_optin)・policyVersion(法的証跡の核)・grantedAtを持つ。同意の「いつ・どのポリシー版で・どこ由来か」はこの列で追える。
追記専用 auditLog や guardedArchive(soft-delete の deletedAt 刻印)といった運用フロー本体は本パッケージには無い — それらは @event/crm-schema 側(第15章 MCP-first 二層 DB 参照)の責務である。networking が保証するのは「消去・同意の状態を型安全に持ち、gating/射影/k匿名がそれを fail-closed で尊重する」ところまで。DSR の受付〜実行〜記録のワークフローは設計のみ / 本パッケージ未実装であり、上位(crm-schema の audit + admin action)で組む。