Working Model
あなたは小さな *.model.json スペックを書きます。パラメータ(スライダー)、状態、小さな安全なルール言語によるルール、イベントのシナリオ、状態にバインドされたビジュアルパーツ、キャプション、そして主張を含みます。CLIはNodeでそれをシミュレートし、すべての主張をチェックし、テキストをリントし、同じエンジンを実行し、読者がスライダーをドラッグすると主張をライブで再チェックする1つのオフラインHTMLページをビルドします。
ポイント:テストされる説明です。モデルが裏付けられない主張は、ユーザーの目に触れる前に失敗します。
コマンド
すべてのコマンドはNode 20以降で動作し、npm installは不要です。引用符を維持してください:パスにスペースが含まれる可能性があります。
node "${CLAUDE_SKILL_DIR}/scripts/working-model.mjs" new token-bucket --dir ./explainers # すでにパスするスペック
node "${CLAUDE_SKILL_DIR}/scripts/working-model.mjs" check ./explainers/token-bucket.model.json # スキーマ、シミュレート、主張、リント
node "${CLAUDE_SKILL_DIR}/scripts/working-model.mjs" check ./explainers/token-bucket.model.json --mutate # 主張は壊れたルールをキャッチするか?
node "${CLAUDE_SKILL_DIR}/scripts/working-model.mjs" build ./explainers/token-bucket.model.json -o ./explainers/token-bucket.html
node "${CLAUDE_SKILL_DIR}/scripts/working-model.mjs" layout ./explainers/token-bucket.model.json --out ./explainers/shots # ローカルのChromeまたはEdgeが必要
node "${CLAUDE_SKILL_DIR}/scripts/working-model.mjs" snap ./explainers/token-bucket.model.json --at 2 -o shot.png # オプションの画像
node "${CLAUDE_SKILL_DIR}/scripts/working-model.mjs" fuzz ./explainers/token-bucket.model.json --cases 300 --seed 7 # シード付き反例探索
node "${CLAUDE_SKILL_DIR}/scripts/working-model.mjs" check ./explainers/token-bucket.model.json --trace t.normalized.json --mapping trace-map.json # モデルと観測トレースの比較
以下、working-model は node "${CLAUDE_SKILL_DIR}/scripts/working-model.mjs" の省略形です。${CLAUDE_SKILL_DIR} を展開しないエージェントでは、このファイルがあるフォルダを使用してください。check の終了コード:0 はすべての主張がパス、1 は主張またはリントエラーが失敗、2 はスペックが無効です。機械可読な出力を得るには --json を、他のスライダー位置を試すには --set name=value を追加してください。--mutate は主張が失敗しない限り0で終了します。--mutate-strict (および --min-score 80)を追加すると、主張が決して失敗しない場合やスコアが低い場合に1で終了します。build --mutate はスコアをページに保存します。
ワークフロー
- 驚くべき部分を示す最小のモデルを選んでください。 1つのメカニズム、1つのシナリオ、2〜4個のスライダー。自問してください:賢い読者は何を間違えるだろうか? その瞬間が画面上で起こるようにシナリオを構築してください(バケツを空にするバースト、一緒に到着するリトライ、キャッシュをフラッシュするスキャン)。
- スキャフォールドするか、例をコピーしてください。
working-model new <name>から始めるか、examples/内の最も近いファイル(トークンバケット、ジッター付きバックオフ、LRUキャッシュ、TCP AIMD、サーキットブレーカー、SIR疫病モデル、複利、負荷分散、ブルームフィルタ、Raftリーダー選出、在庫発注点)から始めてください。 - パラメータ、状態、ルールを書いてください。 離散時間:各ティックは
rules.tickを実行し、そのティックのイベントはrules.on.<event>ハンドラを起動します。単位は正直に保ってください(timeUnit、スライダーのunit)。リンクがアニメーションさせたり、主張が監視すべきものにはemit('name', payload)を使用してください。- 時間は浮動小数点数です。
tはtick × dtなので、t + leadとして格納された時間は後のtと正確に等しくなることはありません。期限とカウントダウンにはティック番号を格納してください(due = tick + round(lead / dt)、その後tick >= due)。references/spec.mdには、「動作するルールを書く」の下にこのような罠がさらにあります。 - リストは値です:
a = bはコピーします。インデックスを介してネストされたリストを変更してください(push(queues[i], job))。
- 時間は浮動小数点数です。
- 3〜8個の、重要な洞察を捉える主張を書いてください。些細なことは避けてください。 ルールをそのまま読み返す主張(「トークンは容量を超えない」、ルールが
min(capacity, ...)の場合)は失敗できず、何も教えません。興味深い主張は、読者が推測できないことを述べます:「キャッシュより1キー大きいループはまったくヒットしない」、「ジッターはピークリトライ負荷を下げる」。適切な種類(at、always、never、eventually、whenever、sweep、compare、詳細はreferences/claims.mdを参照)を使用してください。- 少なくとも1つの主張はデフォルトで成立し、スライダーが移動したときに失敗する必要があります。これにより、読者は説明がどこで成立しなくなるかを確認できます。
checkは、どのスライダー位置がどの主張を壊すかをリストします。 - 各主張は、あなたのモデルだけでなく、実際のメカニズムについて真実である必要があります。主要な事実のソースを
sourcesに、モデルの前提をsummaryに記載してください。主張がこのシードでのみ成立する場合は、「この実行では」と書いてください。 - 文が式がチェックする内容を正確に述べていることを確認してください。
checkは式を証明し、文を証明するわけではありません。ステップ7は、主張がそもそも失敗できるかどうかをテストします。 - 最も遅いスライダー設定に対して
durationを十分長くするか、eventuallyの主張は実行が終了したためにのみ失敗します。
- 少なくとも1つの主張はデフォルトで成立し、スライダーが移動したときに失敗する必要があります。これにより、読者は説明がどこで成立しなくなるかを確認できます。
- テキスト内の数値をバインドしてください。 キャプションと主張テキストは
{expression}の穴を受け入れます:{capacity}と書き、スライダーが動くと古くなるリテラルの10は決して使わないでください。checkはリテラル数値(W_LITERAL_NUMBER)について警告し、バインディングを提案します。キャプションはそれが依存する主張を引用しなければなりません(cites)。失敗している主張を引用することはエラーです。 checkを「N/N claims pass」とエラーなしで言うまで実行してください。 警告も修正してください。- 次に
check --mutateを実行してください:主張をテストします。 これは意図的にルールを壊します(1つの小さな変更を各ミュータントに適用:<を<=に、popをshiftに、行の削除、イベントの1ティック遅延)、すべてのミュータントに対してすべての主張を再チェックし、どの主張がどのバグをキャッチするかを報告します。- すべての主張は少なくとも1つのミュータントを殺さなければなりません。 「決して失敗しなかった主張」としてリストされた主張は失敗できません:それはルールを言い換えているか、空虚にパスしているか、シナリオがそれを実行していません。主張を強化するか(式が文のすべてを述べるようにする)、シナリオを変更して主張が試されるようにしてください(1回のバーストではなく段階的な到着、収まらないループ)。
references/claims.mdに実例があります。 - 生存した各ミュータントについて、主張を追加または強化するか、その変更が見えない理由を返信で説明してください(たとえば、キー名を変更するだけの場合)。本当に無害なものだけを、その理由とともにスペックの
mutate.ignoreに追加してください。スコアを上げるために主張やアサーションを弱めることは絶対にしないでください。 - 同等または視覚のみとマークされたミュータントは、主張が見えるものを何も変更しません。スコアから除外されます。視覚のみのものはページが描画するものを変更するので、スクリーンショットでそれらの数値を確認してください。
- すべての主張は少なくとも1つのミュータントを殺さなければなりません。 「決して失敗しなかった主張」としてリストされた主張は失敗できません:それはルールを言い換えているか、空虚にパスしているか、シナリオがそれを実行していません。主張を強化するか(式が文のすべてを述べるようにする)、シナリオを変更して主張が試されるようにしてください(1回のバーストではなく段階的な到着、収まらないループ)。
- 主張をパスさせるために、主張を弱めたり削除したりしないでください。 主張が失敗した場合、どちらが間違っているかを判断してください:
- モデルが間違っている(ルールのバグ、間違ったデフォルト、欠落したメカニズム):モデルを修正してください。
- 主張が間違っている(実際のメカニズムはそのように動作しない、または条件の下でのみそうなる):主張を修正し、何が間違っていたかを述べてください。 どちらであったか、そしてその理由を1~2文でユーザーに伝えてください。パスするまで許容範囲を緩めたり、主張をより簡単なものと交換したりすることは許可されていません。
- ビルドしてから
layoutを実行してください。 すべてのE_問題(パーツの重なり、テキストのはみ出し、テキストの衝突、390ピクセルでのページの横スクロール)を修正してください。通常は:パーツにより多くのグリッドセルを与える、ラベルを短くする、またはパーツを移動します。W_LINK_CROSSES(別のパーツを横切って描かれた矢印)も修正してください:パーツを移動するか、viaポイントを追加します。それが書き出すスクリーンショットを見て、表示されているすべての数値が意図したものであることを確認してください:パーツのバインディングはどの主張でもテストされていません。読者が依存するカウンターやゲージについては、expect(同じ数値を計算する独立した方法、たとえば"expect": "len(queue)")を追加し、2つが異なる場合にcheckが失敗するようにしてください。 - 引き継ぎ: HTMLパス、主張サマリー行(
6/6 claims pass)と突然変異スコア、各主張ごとに1行、そしてどのスライダーがどの主張を壊すか。読者が知っておくべきモデルの前提があれば言及してください。
反例の探索
check と check --mutate がパスした後、working-model fuzz spec.model.json --cases 300 --seed 7 は、シードされた近傍のケース(スライダー値、モデルシード、fuzz ブロックがファズ可能とマークしたイベント(シフト、繰り返し、またはドロップ))で同じ主張を実行します。次に working-model minimize spec.model.json --case .working-model/fuzz/first-failure.json は、最初の失敗を縮小し、同じ主張が失敗し続けるようにします。縮小されたケースを平易な言葉で報告してください(「散在する失敗とブレーカーのトリップの間の成功をドロップする」)。デフォルト以外の設定での失敗は、説明がどこで成立しなくなるかについての発見です:実質的な根拠に基づいて主張またはモデルを修正し、反例を保持し、それを黙らせるために主張を弱めないでください。「失敗が見つかりませんでした」は証明ではありません。詳細:docs/FUZZ.md。
観測トレースとの比較
これは、ユーザーが実際のシステムのトレース(JSONL、またはOTLP JSONサブセット)を持っている場合、または examples/reality-bridge/ の合成デモを要求した場合にのみ使用してください。トレースは決して必須ではありません。
- 最初に、トレースにはプライベートデータ(リクエストID、ユーザーID、IP、トークン)が含まれる可能性があり、ローカルで作業することを伝えてください。 インポートは、マッピングが読み取る属性のみを保持します:
working-model trace import trace.jsonl --mapping trace-map.json --out trace.normalized.json。ユーザーが、インストルメンテーションがマップされた種類のすべてのイベントを記録することを確認した場合にのみ--completeを渡してください。それ以外の場合、完全性はunknownのままです。 - 書く際には4つのものを区別してください: モデルの前提、ソースからのメカニズムの事実、モデル内の主張、観測されたアサーション。前提はスペックの
reality.assumptionsに配置してください。 - トレースが実際に含むものからマッピングを書いてください。 トレースが記録しない相関IDやイベントを決して発明しないでください。両側にサブジェクトがない出力は無相関であり、そのアサーションは決定的ではありません。すべてのモデルイベントは、再生されるか
internalEventsにリストされます。 working-model check spec.model.json --trace trace.normalized.json --mapping trace-map.jsonを実行してください。 モデルラインと観測ラインを別々に報告してください(Model: 4/4 claims pass | Observed: 1 pass, 2 fail, 0 inconclusive)、最初の不一致をそのイベントIDとともに、そしてそれぞれの決定的でない理由をそのまま報告してください。終了コード1は、主張または観測アサーションが失敗したことを意味します。2は無効なトレースまたはマッピング。3はファイルが見つからないか制限です。- トレースが矛盾する場合、実質的な根拠に基づいて前提を修正してください。 通常は2番目のモデルで修正し、反例を保持してください。パスするためだけに
minPrecision、minRecall、またはwithinMsを緩めないでください。 - ページで見るために、
working-model build spec.model.json --trace trace.normalized.json --mapping trace-map.json -o page.htmlを実行してください:通常の説明に加えて、モデル対観測のタイムライン、最初の不一致、およびすべてのアサーションが含まれます。正規化されたトレースを保持するため、ローカルに留まります。 - ユーザーが公開を要求した場合にのみ、最初に
working-model bundle spec.model.json --public ... --previewを実行し、主要なリストを表示し、その後--previewなしで実行してください。実際のトレースにはユーザーの明示的な--acknowledge-privacyが必要です。ユーザーに代わってそれを渡さないでください。 - 証拠が示さないものを述べてください: 未観測の状態、サンプリングされたトレース、カバーされる時間枠。これはトレース証拠を伴う実行可能な説明であり、形式的な検証ではありません。
フォーマット、セレクター、アサーションの種類:docs/REALITY-BRIDGE.md。脅威モデル:docs/TRACE-SECURITY.md。証拠の限界:docs/CONFORMANCE-LIMITS.md。
CIでの整合性の維持
ユーザーがプルリクエストごとに説明をチェックしたい場合、.working-model-ci.json(モデル、および主張、突然変異、ファズ、現実、パリティのうちどれを実行するか、明示的なしきい値を指定)を追加し、.github/workflows/working-model-ci.example.yml をコピーしてください。working-model ci --config .working-model-ci.json --format markdown は、レビュアーが読むサマリーを出力します。CIをパスさせるためにしきい値を下げないでください。詳細:docs/CI.md。
スペックの概要
{
"title": "トークンバケットレートリミッター",
"summary": "バケットは最大 {capacity} トークンを保持し、毎秒 {refill} の割合で補充されます。",
"sources": [{ "title": "RFC 2697", "url": "https://www.rfc-editor.org/rfc/rfc2697" }],
"timeUnit": "s", "dt": 0.1, "duration": 16, "seed": 7,
"params": [{ "id": "capacity", "label": "バケットサイズ", "min": 1, "max": 30, "step": 1, "default": 10, "unit": "トークン" }],
"events": [{ "name": "request", "at": 1, "count": 15 }],
"state": { "tokens": "capacity", "accepted": 0, "rejected": 0 },
"derived": { "fill": "tokens / capacity" },
"rules": {
"tick": "tokens = min(capacity, tokens + 2 * dt)",
"on": { "request": ["if tokens >= 1 {", " tokens -= 1; accepted += 1; emit('accept')", "} else {", " rejected += 1; emit('reject')", "}"] }
},
"layout": { "cols": 12, "rows": 8 },
"parts": [
{ "id": "bucket", "type": "tank", "at": [4, 0, 4, 4], "label": "バケット", "level": "fill", "text": "{tokens:.1} / {capacity}" },
{ "id": "chart", "type": "chart", "at": [0, 5, 12, 3], "label": "トークン", "series": [{ "expr": "tokens" }] }
],
"narration": [{ "from": 1, "to": 3, "text": "最初の {capacity} リクエストが蓄積されたトークンを消費します。", "cites": ["burst-passes"] }],
"claims": [{ "id": "burst-passes", "kind": "at", "t": 1, "expr": "accepted == capacity", "text": "満杯のバケットは、最初の {capacity} リクエストのみを通過させます。" }]
}
state内の文字列は式です("capacity")。テキスト値には内部引用符が必要です("'closed'")。- ルール言語:数値、文字列、リスト、
true/false、+ - * / % **、比較、and or not、c ? a : b、let、if / else if / else、while、for x in list、break、continue、代入演算子、およびmin max abs clamp round sqrt exp log rand randint chance pick len push pop shift unshift remove contains indexOf slice sum range fill emitなどの関数。メンバーアクセス、ユーザー関数、evalはありません。完全なリファレンス:references/spec.md。 - パーツ:
box tank queue stack grid counter gauge lamp node link chart label group。at: [col, row, width, height]でグリッド上に配置されます。バインディングとオプション:references/parts.md。 - 主張と良い主張の書き方:
references/claims.md
…
[翻訳用に切り詰められました]