0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

物語の時系列をテストデータで検証する:Node.jsでイベントの前後関係を確認する

0
Last updated at Posted at 2026-10-20

この記事はPogeeの編集企画として、生成AIを用いて作成しています。図書館の出来事、JSON、検査ルールはすべて独立した架空の教材です。Pogeeの内部形式、非公開API、実際の会話ログは使っていません。掲載コードはローカルで実行確認しています。

続き物の会話を確認していると、「本を見つける前に返した」のように、文章だけでは見落としやすい前後関係があります。イベントに時刻を付けるだけでなく、「この出来事はどの出来事より後か」をデータに残すと、単純な矛盾を機械的に確認できます。

今回は3件の合成イベントをJSONにし、Node.jsだけで検査します。前回の固定応答の文字列チェックとは別に、出来事の依存関係を扱う例です。モデルを呼び出して物語を生成した結果や、Pogeeの記憶機能を評価した結果ではありません。

前回:会話シナリオのテストケースをYAMLで管理する最小例

時刻と依存関係を、別の情報として残す

afterには、このイベントより先に起きていなければならないイベントのIDを並べます。ここでのルールは「厳密に後」です。同時刻も不合格にします。並行してよい出来事には、無理にafterを付けません。JSONの配列順そのものは時系列の判定に使いません。

架空の図書館の3イベントをUTC時刻とafterで検査する流れ

図1:架空の図書館エピソードをUTCの時刻とafterで検査する流れ。生成AIの支援で作成した説明図で、Pogeeの製品画面・実会話・内部実装ではありません。

timeline.json:図書館の3イベント

prepareは貸出準備、find_bookは本を見つけること、return_bookは本を返すことを表す架空のIDです。日付も教材用の固定値です。時刻はYYYY-MM-DDTHH:mm:ss.SSSZのUTC表記に統一します。例えば09:00Zは日本時間の18:00です。

{
  "version": 1,
  "events": [
    {
      "id": "prepare",
      "at": "2026-10-18T09:00:00.000Z",
      "after": []
    },
    {
      "id": "find_book",
      "at": "2026-10-18T09:15:00.000Z",
      "after": ["prepare"]
    },
    {
      "id": "return_book",
      "at": "2026-10-18T09:30:00.000Z",
      "after": ["find_book"]
    }
  ]
}

find_bookのafterがprepareを指すので、find_book.atはprepare.atより後でなければなりません。return_bookはfind_bookより後です。この例はイベントが瞬間的に発生する前提で、所要時間や期間の重なりは表現していません。

check-timeline.mjs:入力不備と時系列の衝突を分ける

実行確認はNode.js v25.2.1で行いました。追加のnpmパッケージは不要です。timeline.jsonと次のcheck-timeline.mjsを同じ空の作業ディレクトリに置きます。.mjsなのでES Modulesとして読み込まれます。

import { readFileSync } from "node:fs";

const idPattern = /^[a-z][a-z0-9_-]{0,63}$/;
const utcPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
const isId = (value) => typeof value === "string" && idPattern.test(value);

function isUtc(value) {
  if (typeof value !== "string" || !utcPattern.test(value)) return false;
  const milliseconds = Date.parse(value);
  return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === value;
}

try {
  const data = JSON.parse(readFileSync(process.argv[2] ?? "timeline.json", "utf8"));
  if (data?.version !== 1 || !Array.isArray(data.events) || data.events.length === 0) {
    throw new Error("expected version=1 and a non-empty events array");
  }
  const events = new Map();
  for (const [index, event] of data.events.entries()) {
    if (!event || !isId(event.id) || !isUtc(event.at) ||
        !Array.isArray(event.after) || !event.after.every(isId)) {
      throw new Error(`events[${index}]: invalid id, UTC timestamp or after array`);
    }
    if (events.has(event.id)) throw new Error(`duplicate id: ${event.id}`);
    if (new Set(event.after).size !== event.after.length) {
      throw new Error(`duplicate dependency: ${event.id}`);
    }
    events.set(event.id, event);
  }
  for (const event of events.values()) {
    for (const predecessor of event.after) {
      if (!events.has(predecessor)) {
        throw new Error(`unknown dependency: ${event.id} -> ${predecessor}`);
      }
    }
  }
  const failures = [];
  for (const event of events.values()) {
    for (const predecessor of event.after) {
      if (Date.parse(event.at) <= Date.parse(events.get(predecessor).at)) {
        failures.push(`${event.id} must be later than ${predecessor}`);
      }
    }
  }
  for (const failure of failures) console.log(`FAIL ${failure}`);
  console.log(`${events.size} events, ${failures.length} timeline conflicts`);
  process.exitCode = failures.length ? 1 : 0;
} catch (error) {
  console.error(`INPUT_ERROR ${error.message}`);
  process.exitCode = 2;
}

最初にversion、events、ID、UTC時刻、after配列を確認し、MapでIDを引けるようにします。IDと依存先の重複、存在しない依存先は入力不備として終了コード2にします。入力を確認してから時刻を比較し、前後関係の衝突があれば終了コード1、なければ0にします。

Date.parseだけに任せず、UTC形式の正規表現とtoISOStringの往復一致も確認します。例えば存在しない日付が別の日付に補正されても、元の文字列と一致しなければ入力不備です。この教材では+09:00などのオフセット表記も受け付けません。実データで使うときは、採用する形式を決め、入力段階で別途正規化します。

この入力チェックは教材に必要な項目だけを扱います。未知の追加フィールドは拒否していません。項目数やバージョンが増える場合は、前回までに扱ったJSON Schemaなどでデータ契約を別に管理すると、検査コードとの役割を分けやすくなります。

実際の出力と、意図的に失敗させる例

node --version
node check-timeline.mjs timeline.json
v25.2.1
3 events, 0 timeline conflicts

この実行の終了コードは0です。次にtimeline.jsonをコピーしてconflict.jsonとし、find_bookのatだけを2026-10-18T08:55:00.000Zへ変更します。他の値は変えず、次を実行します。

node check-timeline.mjs conflict.json
FAIL find_book must be later than prepare
3 events, 1 timeline conflicts

終了コードは1です。find_bookがprepareより5分前なのに、afterはprepareを指しているため衝突します。配列を時刻順に並べ替えても依存関係の矛盾は直りません。まず意図した順序と時刻のどちらを訂正すべきかを確認します。

境界条件も、同じスクリプトで確かめる

ローカルで確認した入力 終了コード 判定の意味
元の3イベント/配列の逆順 0 配列順にかかわらず、宣言した前後関係が成立
逆転した時刻/同時刻 1 afterが要求する「厳密に後」を満たさない
自分自身への参照/循環する参照 1 少なくとも1辺で時刻の不等式が成立しない
重複ID/重複依存先/存在しない依存先 2 比較の前提になるデータが不正
存在しない日付/+09:00表記/afterの欠落 2 この教材の入力形式を満たさない
空のevents/壊れたJSON/ファイルなし 2 入力を読み取り、検査できない

上の分類に含まれる15入力をローカルで実行し、終了コードを確認しました。循環を専用の探索アルゴリズムで検出しているわけではありません。全てのafterに「時刻が厳密に増える」という条件を課すため、循環があれば少なくとも1か所で条件を満たせなくなります。循環経路そのものを示したい場合は、別のグラフ検査が必要です。

時系列チェックで分からないこと

このスクリプトが確認するのは、記録されたIDと時刻の関係だけです。本を返したときに本を持っていたか、登場人物が知り得ない情報を知っていないか、文章が自然かは分かりません。JSONに書かなかった出来事や、入力した時刻そのものの正しさも保証しません。

長期のストーリーを扱うなら、まず観察できた出来事と、次に満たしてほしい条件を分けて記録します。出来事の取消、訂正、異なる時間軸、同時進行を扱う場合は、この最小例のルールでは足りません。仕様に合わせた追加の状態モデルや、人が文脈を読む工程を用意します。

単純な順序の矛盾を先に見つけられると、文章を読み直すときに確認すべき箇所を絞れます。ただしローカルの教材が通ることを、AIサービス全体の一貫性や記憶の正しさの証拠にはしません。

関連資料

AIキャラクターと継続する物語を考える背景には、Pogeeの公開ガイドもあります。この記事のイベント形式と検査ルールは独立した教材で、Pogeeの内部実装や提供機能を示すものではありません。

Pogee公開ガイド:チャットの外にも日常がある、AIキャラクターとは?

Node.js:File system(readFileSync)

MDN:Date.parse()

MDN:Date.prototype.toISOString()

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?