kaedeasset記事 · Notes霧走町 流氷に眠る

手帳の一行は、誰から・どこで・どの経路で聞いたかを持つ

Every notebook line records who said it, where, and by what route

 D11-3-T21-notebook-provenance・技術・日本語入力でプレイする仕組み

入力を解いて返すdata provenanceW3C PROV (PROV-DM)where-provenancewhy-provenanceclaude自然言語処理

港で佐伯に昨夜の居場所を聞くと、手帳に「佐伯「十五日の夜は10時まで番屋で飲んでいた」」と一行増えます。
その一行は、本文のほかに「港で」「佐伯と話している時に」「佐伯の居場所の話の段から」書いた、という出所を持っています。
この回は、手帳に何を控え、何を控えないか、その理由を書きます。

T19 で、段が旗を立てて手帳に一文を書くところまで書きました。
手帳は、プレイヤーが推理の材料を並べる場所です。
そこに一行でも「まだ聞いていない事実」が載ると、推理が成り立ちません。

この回の位置と、前後の回#

区切り: T03〜T24 日本語入力を解いて返すまで(22 回のうち 19 回目)
先に読むとよい回: T19(段と旗)・T10(固有名詞の辞書)
次に読む回: T22(図から答えが漏れる)

名前 — data provenance#

正式名
data provenance(データの来歴)
関連
W3C PROV(PROV-DM)、where-provenance と why-provenance
略称
PROV
日本語
データの来歴、出所、プロヴナンス
初出・代表
来歴のモデルの標準は Moreau & Missier 編, "PROV-DM: The PROV Data Model"(W3C 勧告, 2013-04-30)。データベースの問い合わせ結果の来歴を why と where に分けたのは Buneman, Khanna & Tan, "Why and Where: A Characterization of Data Provenance"(ICDT 2001, LNCS 1973)。data provenance という語の初出は確かめていません

データの来歴とは#

あるデータが、どこから来て、何によって、誰によって作られたかの記録です。
品質を判断する、結果を再現する、誤りの元をたどる、責任の所在を示す、のに使います。
科学の計算の記録、データベースの整理、ニュースの出所の表示、機械学習のデータの管理などで使われます。

W3C の PROV-DM は、来歴を三種類のものと、その間の関係で書きます。

  • Entity(実体)… データや物
  • Activity(活動)… 実体を使い、生み出す出来事
  • Agent(行為者)… 活動や実体に責任を持つ人や仕組み

関係は、wasGeneratedBy(この活動が生んだ)、used(この活動が使った)、wasAttributedTo(この人のものだ)、wasDerivedFrom(この実体から作った)、wasAssociatedWith(この活動に関わった人)などです。

Buneman ほか(2001)は、問い合わせの結果の一つの値について、二つの来歴を分けました。

  • why-provenance … その結果がある理由になった、元のデータの組
  • where-provenance … その値を、元のどの場所から写したか

よく知られた弱点は三つです。

  • 細かさの選び方。細かく取るほど記録が膨らみ、粗く取るほど役に立たない
  • 取りこぼし。記録する仕組みを通らない変更は、来歴に残らない
  • 後から再構成できない。書いた瞬間に取らなかった来歴は、ログから推測するしかなくなる

うちの手帳の一行#

AdvTurn.hpp の NotebookEntry です。

struct NotebookEntry
{
int index = 0; // 取得順の通し番号(1始まり・欠番なし)。セーブをまたいで不変
std::string text;
std::string who; // 書いた時点の対話相手 id(空可。場所イベント・地の文は空)
std::string where; // 書いた時点の現在地 id
std::string source; // "stage"(意図の段)| "enter"(入場)| "event"(場所イベント)
std::string intent; // stage のとき、意図 id
std::string speaker; // stage のとき、意図の話者 id(現場調査なら sceneSpeaker)。who と違い「誰の口から出たか」
std::vector<std::string> entities; // 意図の entities ∪ 本文が名を挙げた entity(Matcher.entitiesIn)
};

本文以外は、全部「その一文を書いた瞬間に決まっていたこと」です。
ゲームの見解(この行は重要だ、この行は佐伯の嘘だ)は一つも入りません。

who と speaker を分けているのは、話している相手と、その話の出どころが違うことがあるからです。
現場を調べた行は、who が空で、speaker は現場(scene)です。

書く所は一か所です(AdvSession.hpp の pushNote)。

void pushNote(const std::string& Text, AdvTurn& turn,
const std::string& NoteSource, const Intent* NoteIntent)
{
NotebookEntry E;
E.index = static_cast<int>(Notebook.size()) + 1;
E.text = Text;
E.who = Engine.state.speaker;
E.where = Engine.state.where;
E.source = NoteSource;
if (NoteIntent)
{
E.intent = NoteIntent->id;
E.speaker = NoteIntent->speaker;
E.entities = NoteIntent->entities;
}
for (const std::string& Id : Matcher.entitiesIn(Text))
{
if (std::find(E.entities.begin(), E.entities.end(), Id) == E.entities.end()) { E.entities.push_back(Id); }
}
Notebook.push_back(E);
turn.notesGained.push_back(Text);
}

ここで新しい判断はしません。
いま画面に映っている相手と場所を、そのまま控えるだけです。
entities だけは、本文の文字列を固有名詞の辞書に当てて足します。入力の側で材料を数えるのと同じ規則(T10 の回)です。

セーブの中の形#

セーブ(adv-save/2)には、欄を 1 文字の鍵で入れます。
今日、駐在所と港で入力した後のセーブから、2 行目と 3 行目を写します(2 行目の材料の一覧は省きます)。

{ "i": 2, "t": "堂本「先週、番屋で佐伯と組合長が怒鳴り合っていたらしい。金の話だとか」",
"w": "domoto", "l": "police_box", "s": "stage", "n": "domoto.quarrel", "k": "domoto" },
{ "i": 3, "t": "佐伯「十五日の夜は10時まで番屋で飲んでいた」",
"w": "saeki", "l": "port", "s": "stage", "n": "saeki.alibi_night", "k": "saeki",
"e": ["alibi", "banya", "jikoku", "saeki"] }

1 行目は、宿に着いたときの事件の概要で、s が enter、w が源三、l が宿です。
画面の手帳には、本文と、場所と相手の表示名が出ます。

何を書き、何を書かないか#

手帳の一文には、二つの出どころがあります。

  • 段の note … その台詞を実際に聞いた、という一文。段ごとに作者が書く
  • フラグの既定文(notes.json)… 旗が立ったときの既定の一文

既定文の方に危険があります。
同じ旗が、別の人・別の経路で立つことがあるからです。
「ハルから聞いたのに、手帳には堂本と出る」「その経路ではまだ聞いていない名前が手帳に載る」が、8 月に実際に起きました。

そこで、段の note を優先し、既定文は旗が新しく立ったときにだけ書きます。
applySets の要点を写します。

for (const std::string& F : sets)
{
if (Engine.state.flags.count(F) > 0) { continue; }
Engine.state.set(F);
turn.flagsGained.push_back(F);
bAnyNew = true;
if (!bOverride && !bSuppress)
{
const auto It = Notes.find(F);
if (It != Notes.end()) { pushNote(It->second, turn, NoteSource, NoteIntent); }
}
…
}
if (bOverride && !overrideNote.empty() && !hasNote(overrideNote))
{
pushNote(overrideNote, turn, NoteSource, NoteIntent);
}

境界の扱いです。

  • 段の note は、旗が別の経路で既に立っていても書く。その台詞を実際に聞いた証拠だからです。8 月に、旗が先に立っていたせいで、別の人から聞いた推理の核が、AI のプレイヤー 2 人とも手帳に 1 行も残らなかったことがあり、この形にしました
  • 同じ文が既にあれば、二度は書かない(hasNote は本文の一致で見る)
  • 状態別応答(T19 の回)は、note を「省略=元の段の note」「空文字=この応答では新しい行を書かない」「文字あり=差し替え」の三つで区別する。空文字を「既定」と読む古い約束と混ざらないよう、型(NotePolicy の Inherit・Suppress・Override)で分けています
  • 言い直しでは、新しい行を書かない。代わりに、その段で前に手帳に載った文を「前に聞いた話」として添える。手帳にまだ無い文は添えない

今日の作品データでは、フラグの既定文が 20、note を持つ段が 114、旗を立てる段が 115 でした(2026-10-29 時点)。
ほとんどの段が、自分の聞いた一文を自分で持っています。

分類は画面の側#

UE の手帳は、札を「人物・場所・物・出来事」に分けて見せます。
分ける規則は、コアではなく画面の側が持っています。

分類 条件
出来事 source が event
人物 source が stage で、speaker が人物(人の口から出たもの)
物・記録 それ以外で、entities に人物でないものがある
場所 それ以外(現場の観察・入場の描写)

人物かどうかは、固有名詞の辞書の person の印で見ます。
コアは事実だけを控え、どう見せるかは画面が決める、という分け方です。
古いセーブで欄が無い行は、場所に寄ります。

検査#

手帳には、機械で見る不変条件があります(adv-sim notebook)。

  • N1 通し番号が取得順の連番
  • N2 全部の行に出所の場所がある
  • N3 セーブして読み直しても変わらない
  • N4 同じ台本を 2 回流して一致する
  • N5 まだ立っていない旗の既定文が載っていない
  • N6 相手で絞った表示が、元の並びの部分列(並べ替えていない)
  • N7 読めないはずのセーブで、いまの状態が変わらない

今日、駐在所・港・防波堤で 4 手を流して、0 件でした。

名前どおりでない所 — data provenance#

data provenance の、ごく小さな形です。

  • PROV のモデル(実体・活動・行為者と関係)は使っていません。行ごとに、場所・相手・経路・意図・話者・材料の欄を持つだけです
  • 派生の連鎖を持ちません。「この行は、あの行から推理した」は記録しません。推理は推理記録(T20 の回)の側にあり、そちらは入力の引用を持ちます
  • ここでの where は、ゲームの中の場所です。Buneman ほかの where-provenance(値を写した元の場所)とは別の意味です。名前がぶつかるので、混ぜないでください
  • who は PROV の Agent に、speaker は wasAttributedTo に、source と intent は Activity に近い役ですが、対応を形式として持ってはいません

弱点と、採らなかった案 — data provenance#

  • 材料の一覧は文字列の一致なので、多めに当たります。辞書の別名に一般の語が混ざっている人や物は、関係の薄い行にも付きます
  • プレイヤーの入力は、行に残しません。どう聞いたからこの一文が出たか、は手帳からは分かりません。それはプレイのログ(1 行 1 手の JSONL)の役目です
  • 段の note は作者が書くので、台詞と note の食い違いは、書き手が気をつけるしかありません。lint が見るのは、名前の先出しまでです

採らなかった案です。

  • 既定文に誰から聞いたかを静的に書く(notes.json に who を持たせる)。同じ旗が複数の経路で立つので、静的に書くと必ずどこかで嘘になります
  • 分類をコアで決めて行に持たせる。分類の規則を変えるたびにセーブの中身が古くなります。事実だけを持てば、画面の規則を変えても古いセーブがそのまま新しい分類で見えます
  • 後でログから出所を組み立てる。書いた瞬間にしか分からないこと(その時の相手)を、後から推測する仕組みが増えるだけです

来歴は、書いた瞬間に取る。
手帳の一行は、その原則をいちばん小さく守った形です。

検索語#

data provenance
W3C PROV-DM
why-provenance where-provenance
provenance capture

手帳を開くと、どの話を、どこで、誰から聞いたかが並んでいます。『霧走町 流氷に眠る』の第一章はブラウザで遊べるので、聞き込みのあとで開いてみてください。プロフィールのリンクからどうぞ。

#claude #自然言語処理