手帳の一行は、誰から・どこで・どの経路で聞いたかを持つ
Every notebook line records who said it, where, and by what route
D11-3-T21-notebook-provenance・技術・日本語入力でプレイする仕組み
港で佐伯に昨夜の居場所を聞くと、手帳に「佐伯「十五日の夜は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
手帳を開くと、どの話を、どこで、誰から聞いたかが並んでいます。『霧走町 流氷に眠る』の第一章はブラウザで遊べるので、聞き込みのあとで開いてみてください。プロフィールのリンクからどうぞ。